From efb4ca88b1d8431bb8dbddb20af4042f6cf2e118 Mon Sep 17 00:00:00 2001
From: Toufic Batache <taffou2a@gmail.com>
Date: Wed, 7 Jun 2023 15:48:33 +0000
Subject: [PATCH] =?UTF-8?q?Cr=C3=A9er=20une=20ancre=20pour=20chaque=20para?=
 =?UTF-8?q?m=C3=A8tre=20qui=20s'ajoute=20dans=20l'URL?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/lib/auditors/hashes.ts                    |  54 ++++++
 src/lib/components/NavBar.svelte              |   1 +
 .../components/test_cases/TestCaseView.svelte |  30 ++--
 .../NonVariableReferredParameter.svelte       |   9 +
 .../variables/VariableHeader.svelte           |  28 ++-
 .../VariableReferredNodeParameter.svelte      |  16 +-
 .../VariableReferredParameterHeader.svelte    |  63 ++++++-
 .../VariableReferredParameters.svelte         |  12 ++
 .../VariableReferredScaleParameter.svelte     |  12 +-
 .../VariableReferredValueParameter.svelte     |  12 +-
 src/lib/displays.ts                           |   1 +
 src/lib/urls.ts                               |  33 ++--
 src/lib/viewport.ts                           |  24 +++
 src/routes/+page.svelte                       | 164 +++++++++++-------
 src/routes/simulations/+server.ts             |   8 +-
 tailwind.config.cjs                           |  12 +-
 16 files changed, 367 insertions(+), 112 deletions(-)
 create mode 100644 src/lib/auditors/hashes.ts
 create mode 100644 src/lib/viewport.ts

diff --git a/src/lib/auditors/hashes.ts b/src/lib/auditors/hashes.ts
new file mode 100644
index 000000000..4aaebf79d
--- /dev/null
+++ b/src/lib/auditors/hashes.ts
@@ -0,0 +1,54 @@
+import {
+  auditChain,
+  auditTrimString,
+  type Audit,
+  type Auditor,
+  auditArray,
+  auditTest,
+  auditFunction,
+  auditSetNullish,
+} from "@auditors/core"
+
+export function auditHashSingleton(...auditors: Auditor[]) {
+  return auditChain(
+    auditArray(),
+    auditTest(
+      (values) => values.length <= 1,
+      "Parameter must be present only once in hash",
+    ),
+    auditFunction((value) => value[0]),
+    ...auditors,
+  )
+}
+
+export function auditSimulationHash(
+  audit: Audit,
+  hash: unknown,
+): [unknown, unknown] {
+  if (typeof hash !== "string") {
+    return audit.unexpectedType(hash, "string")
+  }
+  const query = new URLSearchParams((hash as string).replace(/^#/, ""))
+
+  const data: { [key: string]: unknown } = {}
+  for (const [key, value] of query.entries()) {
+    let values = data[key] as string[] | undefined
+    if (values === undefined) {
+      values = data[key] = []
+    }
+    values.push(value)
+  }
+  const errors: { [key: string]: unknown } = {}
+  const remainingKeys = new Set(Object.keys(data))
+
+  audit.attribute(
+    data,
+    "parameterHash",
+    true,
+    errors,
+    remainingKeys,
+    auditHashSingleton(auditTrimString),
+  )
+
+  return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
+}
diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte
index e9663554c..22dc3c496 100644
--- a/src/lib/components/NavBar.svelte
+++ b/src/lib/components/NavBar.svelte
@@ -97,6 +97,7 @@
   const testCases = getContext("testCases") as Writable<Situation[]>
 
   async function shareLink(): Promise<void> {
+    delete $displayModeWritable?.parameterHash
     const urlString = "/simulations"
     const res = await fetch(urlString, {
       body: JSON.stringify({
diff --git a/src/lib/components/test_cases/TestCaseView.svelte b/src/lib/components/test_cases/TestCaseView.svelte
index 3b089cb66..025d1c672 100644
--- a/src/lib/components/test_cases/TestCaseView.svelte
+++ b/src/lib/components/test_cases/TestCaseView.svelte
@@ -294,22 +294,20 @@
 
             <!--TICKET DE CARBURANT-->
             {#if displayMode.parametersVariableName === "taxes_tous_carburants" || (displayMode.parametersVariableName !== undefined && oilTypes.some( (oilType) => displayMode.parametersVariableName?.includes(oilType), ))}
-              <div class="w-1/2">
-                {#each oilSpendings as { depenseTtcVariableName, nombreLitresVariableName, prixTtcLitreVariableName, ticpeVariableName, tvaVariableName }}
-                  <OilSpendingBill
-                    {depenseTtcVariableName}
-                    {nombreLitresVariableName}
-                    {prixTtcLitreVariableName}
-                    on:changeTestCaseToEditIndex
-                    {situation}
-                    {situationIndex}
-                    {ticpeVariableName}
-                    {tvaVariableName}
-                    {valuesByCalculationNameByVariableName}
-                    {year}
-                  />
-                {/each}
-              </div>
+              {#each oilSpendings as { depenseTtcVariableName, nombreLitresVariableName, prixTtcLitreVariableName, ticpeVariableName, tvaVariableName }}
+                <OilSpendingBill
+                  {depenseTtcVariableName}
+                  {nombreLitresVariableName}
+                  {prixTtcLitreVariableName}
+                  on:changeTestCaseToEditIndex
+                  {situation}
+                  {situationIndex}
+                  {ticpeVariableName}
+                  {tvaVariableName}
+                  {valuesByCalculationNameByVariableName}
+                  {year}
+                />
+              {/each}
             {/if}
           </div>
 
diff --git a/src/lib/components/variables/NonVariableReferredParameter.svelte b/src/lib/components/variables/NonVariableReferredParameter.svelte
index bed19c297..e54b49e24 100644
--- a/src/lib/components/variables/NonVariableReferredParameter.svelte
+++ b/src/lib/components/variables/NonVariableReferredParameter.svelte
@@ -11,6 +11,7 @@
     rootParameter,
     rootParameterByReformName,
   } from "$lib/parameters"
+  import type { DisplayMode } from "$lib/displays"
 
   import VariableReferredNodeParameter from "./VariableReferredNodeParameter.svelte"
   import VariableReferredScaleParameter from "./VariableReferredScaleParameter.svelte"
@@ -18,8 +19,10 @@
 
   export let budget: boolean | undefined
   export let date: string
+  export let displayMode: DisplayMode
   /// Parameter name
   export let name: string
+  export let onCopyLink: (parameterName: string) => void
 
   const billName = getContext("billName") as Writable<string | undefined>
 
@@ -41,7 +44,9 @@
       {budget}
       {date}
       depth={0}
+      {displayMode}
       lawParameter={asNodeParameter(lawParameter)}
+      {onCopyLink}
     />
   {:else if billParameter.class === ParameterClass.Value}
     <VariableReferredValueParameter
@@ -49,7 +54,9 @@
       {budget}
       {date}
       depth={0}
+      {displayMode}
       lawParameter={asValueParameter(lawParameter)}
+      {onCopyLink}
     />
   {:else if billParameter.class === ParameterClass.Scale}
     <VariableReferredScaleParameter
@@ -57,7 +64,9 @@
       {budget}
       {date}
       depth={0}
+      {displayMode}
       lawParameter={asScaleParameter(lawParameter)}
+      {onCopyLink}
     />
   {/if}
 </section>
diff --git a/src/lib/components/variables/VariableHeader.svelte b/src/lib/components/variables/VariableHeader.svelte
index d473c8f18..db92d6f91 100644
--- a/src/lib/components/variables/VariableHeader.svelte
+++ b/src/lib/components/variables/VariableHeader.svelte
@@ -77,21 +77,19 @@
         </span>
       </Tooltip>
     {:else}
-      <div class="flex">
-        <a
-          id="dispositif-{variableName}-a-jour-voir-formule"
-          class="flex items-center font-bold italic text-black hover:text-le-bleu"
-          href="/variables/{variableName}"
-        >
-          <iconify-icon
-            class="mr-1 text-[#13CC03]"
-            icon="material-symbols:new-releases"
-            width="28"
-            height="28"
-          />
-          f
-        </a>
-      </div>
+      <a
+        id="dispositif-{variableName}-a-jour-voir-formule"
+        class="flex items-center font-bold italic text-black hover:text-le-bleu"
+        href="/variables/{variableName}"
+      >
+        <iconify-icon
+          class="mx-1 text-[#13CC03]"
+          icon="material-symbols:new-releases"
+          width="28"
+          height="28"
+        />
+        f
+      </a>
       <Tooltip
         class="z-40 w-60 bg-gray-100 px-3 py-1 text-left text-xs text-black"
         style="custom"
diff --git a/src/lib/components/variables/VariableReferredNodeParameter.svelte b/src/lib/components/variables/VariableReferredNodeParameter.svelte
index 19634ca7d..94139424b 100644
--- a/src/lib/components/variables/VariableReferredNodeParameter.svelte
+++ b/src/lib/components/variables/VariableReferredNodeParameter.svelte
@@ -6,6 +6,7 @@
   } from "@openfisca/json-model"
 
   import { asScaleParameter, asValueParameter } from "$lib/parameters"
+  import type { DisplayMode } from "$lib/displays"
 
   import VariableReferredParameterHeader from "./VariableReferredParameterHeader.svelte"
   import VariableReferredScaleParameter from "./VariableReferredScaleParameter.svelte"
@@ -16,9 +17,11 @@
   export let budget: boolean | undefined
   export let date: string
   export let depth: number
+  export let displayMode: DisplayMode
   export let hideNull = false
   export let lawParameter: NodeParameter | undefined
   export let name: string | undefined = undefined
+  export let onCopyLink: (parameterName: string) => void
 
   function compareUsingOrder(
     order: string[],
@@ -64,7 +67,12 @@
 </script>
 
 <section>
-  <VariableReferredParameterHeader {depth} parameter={billParameter} />
+  <VariableReferredParameterHeader
+    {depth}
+    {displayMode}
+    {onCopyLink}
+    parameter={billParameter}
+  />
 
   {#if billParameter.children !== undefined}
     <ul>
@@ -77,9 +85,11 @@
               {budget}
               {date}
               depth={depth + 1}
+              {displayMode}
               {hideNull}
               lawParameter={lawChild}
               {name}
+              {onCopyLink}
             />
           {:else if billChild.class === ParameterClass.Value}
             <VariableReferredValueParameter
@@ -87,9 +97,11 @@
               {budget}
               {date}
               depth={depth + 1}
+              {displayMode}
               {hideNull}
               lawParameter={asValueParameter(lawChild)}
               {name}
+              {onCopyLink}
             />
           {:else if billChild.class === ParameterClass.Scale}
             <VariableReferredScaleParameter
@@ -97,8 +109,10 @@
               {budget}
               {date}
               depth={depth + 1}
+              {displayMode}
               lawParameter={asScaleParameter(lawChild)}
               {name}
+              {onCopyLink}
             />
           {/if}
         </li>
diff --git a/src/lib/components/variables/VariableReferredParameterHeader.svelte b/src/lib/components/variables/VariableReferredParameterHeader.svelte
index 8973c737b..3da2f2396 100644
--- a/src/lib/components/variables/VariableReferredParameterHeader.svelte
+++ b/src/lib/components/variables/VariableReferredParameterHeader.svelte
@@ -4,20 +4,59 @@
   import { getContext } from "svelte"
 
   import type { SelfTargetAProps } from "$lib/urls"
+  import type { DisplayMode } from "$lib/displays"
+  import viewport from "$lib/viewport"
+  import { goto } from "$app/navigation"
+  import { newSimulationUrl } from "$lib/urls"
 
   export let depth: number
+  export let displayMode: DisplayMode
+  export let onCopyLink: (parameterName: string) => void
   export let parameter: Parameter
 
   const newSelfTargetAProps = getContext("newSelfTargetAProps") as (
     url: string,
   ) => SelfTargetAProps
+
+  let isCopiedSuccessfully = false
+  function copyLink() {
+    if (parameter.name !== undefined) {
+      onCopyLink(parameter.name)
+      isCopiedSuccessfully = true
+      setTimeout(() => (isCopiedSuccessfully = false), 2500)
+    }
+  }
+
+  $: isParameterSelected = displayMode.parameterHash === parameter.name
+  let htmlElement
+  let isElementInViewport = false
+  $: if (htmlElement !== undefined && isParameterSelected)
+    requestAnimationFrame(() =>
+      htmlElement.scrollIntoView({ behavior: "smooth" }),
+    )
+
+  let hasAnimationEnded = false
+  $: if (isElementInViewport)
+    setTimeout(() => {
+      hasAnimationEnded = true
+      goto(
+        newSimulationUrl({
+          ...displayMode,
+          parameterHash: undefined,
+        }),
+      )
+    }, 5000)
 </script>
 
+<!--scroll-mt-60-->
 {#if depth === 0 || parameter.title !== parameter.parent?.title}
   <h1
-    class="mr-4 font-serif text-le-gris-dispositif-dark {parameter.class !==
+    bind:this={htmlElement}
+    use:viewport
+    on:enterViewport={() => (isElementInViewport = true)}
+    class="mr-4 inline-flex scroll-mt-48 font-serif text-le-gris-dispositif-dark {parameter.class !==
     ParameterClass.Node
-      ? 'text-base italic text-le-gris-dispositif-dark'
+      ? '-ml-5 text-base italic text-le-gris-dispositif-dark'
       : depth === 0
       ? 'my-1 inline-flex text-base font-bold'
       : depth === 1
@@ -39,7 +78,25 @@
       {/each}
     {/if}
     {#if parameter.title !== parameter.parent?.title}
-      {parameter.title}&nbsp;:
+      <button
+        title="Copier le lien"
+        class="flex shrink-0 items-center justify-center self-start px-0.5 py-1 text-le-gris-dispositif-light"
+        class:hover:text-le-gris-dispositif={!isCopiedSuccessfully}
+        class:active:text-le-gris-dispositif-dark={!isCopiedSuccessfully}
+        class:!text-le-gris-dispositif-dark={isCopiedSuccessfully}
+        disabled={isCopiedSuccessfully}
+        on:click={copyLink}
+        ><iconify-icon
+          icon={!isCopiedSuccessfully ? "ri-link" : "ri-check-line"}
+        /></button
+      >
+      <span
+        class="transition-all duration-1000 ease-out"
+        class:px-2={isParameterSelected}
+        class:bg-le-bleu-light={isParameterSelected}
+        class:animate-blink={isParameterSelected && isElementInViewport}
+        style="animation-delay: 500ms">{parameter.title}&nbsp;:</span
+      >
     {/if}
   </h1>
 {/if}
diff --git a/src/lib/components/variables/VariableReferredParameters.svelte b/src/lib/components/variables/VariableReferredParameters.svelte
index b30b62de0..e9047e944 100644
--- a/src/lib/components/variables/VariableReferredParameters.svelte
+++ b/src/lib/components/variables/VariableReferredParameters.svelte
@@ -37,6 +37,7 @@
   export let date: string
   export let displayMode: DisplayMode
   export let name: string
+  export let onCopyLink: (parameterName: string) => void
 
   const billName = getContext("billName") as Writable<string | undefined>
   let openDirectParameters = true
@@ -171,9 +172,11 @@
                     budget={displayMode.budget}
                     {date}
                     depth={0}
+                    {displayMode}
                     hideNull
                     lawParameter={asNodeParameter(lawParameter)}
                     {name}
+                    {onCopyLink}
                   />
                 {:else if billParameter.class === ParameterClass.Value}
                   <VariableReferredValueParameter
@@ -181,9 +184,11 @@
                     budget={displayMode.budget}
                     {date}
                     depth={0}
+                    {displayMode}
                     hideNull
                     lawParameter={asValueParameter(lawParameter)}
                     {name}
+                    {onCopyLink}
                   />
                 {:else if billParameter.class === ParameterClass.Scale}
                   <VariableReferredScaleParameter
@@ -191,6 +196,7 @@
                     budget={displayMode.budget}
                     {date}
                     depth={0}
+                    {displayMode}
                     lawParameter={asScaleParameter(lawParameter)}
                     {name}
                   />
@@ -234,7 +240,9 @@
                     budget={displayMode.budget}
                     {date}
                     depth={0}
+                    {displayMode}
                     lawParameter={asNodeParameter(lawParameter)}
+                    {onCopyLink}
                   />
                 {:else if billParameter.class === ParameterClass.Value}
                   <VariableReferredValueParameter
@@ -242,7 +250,9 @@
                     budget={displayMode.budget}
                     {date}
                     depth={0}
+                    {displayMode}
                     lawParameter={asValueParameter(lawParameter)}
+                    {onCopyLink}
                   />
                 {:else if billParameter.class === ParameterClass.Scale}
                   <VariableReferredScaleParameter
@@ -250,7 +260,9 @@
                     budget={displayMode.budget}
                     {date}
                     depth={0}
+                    {displayMode}
                     lawParameter={asScaleParameter(lawParameter)}
+                    {onCopyLink}
                   />
                 {/if}
               </li>
diff --git a/src/lib/components/variables/VariableReferredScaleParameter.svelte b/src/lib/components/variables/VariableReferredScaleParameter.svelte
index 96ace8a8d..90e3e57c5 100644
--- a/src/lib/components/variables/VariableReferredScaleParameter.svelte
+++ b/src/lib/components/variables/VariableReferredScaleParameter.svelte
@@ -15,6 +15,7 @@
     requestTestCasesCalculation,
     type RequestedCalculations,
   } from "$lib/calculations"
+  import type { DisplayMode } from "$lib/displays"
   import ArticleModal from "$lib/components/parameters/ArticleModal.svelte"
   import VariableReferredParameterHeader from "$lib/components/variables/VariableReferredParameterHeader.svelte"
   import VariableReferredScaleAtInstant from "$lib/components/variables/VariableReferredScaleAtInstant.svelte"
@@ -32,8 +33,10 @@
   export let budget: boolean | undefined
   export let date: string
   export let depth: number
+  export let displayMode: DisplayMode
   export let lawParameter: ScaleParameter | undefined
   export let name: string | undefined = undefined
+  export let onCopyLink: (parameterName: string) => void
 
   const dateFormatter = new Intl.DateTimeFormat("fr-FR", { dateStyle: "full" })
     .format
@@ -141,8 +144,13 @@
   }
 </script>
 
-<section class="ml-5 border-l-2 border-le-gris-dispositif-light pb-4 pl-4">
-  <VariableReferredParameterHeader {depth} parameter={billParameter} />
+<section class="ml-10 border-l-2 border-le-gris-dispositif-light pb-4 pl-6">
+  <VariableReferredParameterHeader
+    {depth}
+    {displayMode}
+    parameter={billParameter}
+    {onCopyLink}
+  />
 
   <div class="mt-1 flex rounded-t bg-gray-100 pl-1 pt-2">
     <VariableReferredScaleAtInstant
diff --git a/src/lib/components/variables/VariableReferredValueParameter.svelte b/src/lib/components/variables/VariableReferredValueParameter.svelte
index 791dd7fc4..07ed794f1 100644
--- a/src/lib/components/variables/VariableReferredValueParameter.svelte
+++ b/src/lib/components/variables/VariableReferredValueParameter.svelte
@@ -14,6 +14,7 @@
     requestTestCasesCalculation,
     type RequestedCalculations,
   } from "$lib/calculations"
+  import type { DisplayMode } from "$lib/displays"
   import ArticleModal from "$lib/components/parameters/ArticleModal.svelte"
   import VariableReferredParameterHeader from "$lib/components/variables/VariableReferredParameterHeader.svelte"
   import VariableReferredValueEdit from "$lib/components/variables/VariableReferredValueEdit.svelte"
@@ -33,9 +34,11 @@
   export let budget: boolean | undefined
   export let date: string
   export let depth: number
+  export let displayMode: DisplayMode
   export let hideNull = false
   export let lawParameter: ValueParameter | undefined
   export let name: string | undefined = undefined
+  export let onCopyLink: (parameterName: string) => void
 
   let billLatestInstantValueCouplesArray: [string, ValueAtInstant][]
   const dateFormatter = new Intl.DateTimeFormat("fr-FR", { dateStyle: "full" })
@@ -149,8 +152,13 @@
 </script>
 
 {#if !hideNull || value !== null}
-  <section class="ml-5 border-l-2 border-le-gris-dispositif-light pb-6 pl-4">
-    <VariableReferredParameterHeader {depth} parameter={billParameter} />
+  <section class="ml-10 border-l-2 border-le-gris-dispositif-light pb-6 pl-6">
+    <VariableReferredParameterHeader
+      {depth}
+      {displayMode}
+      parameter={billParameter}
+      {onCopyLink}
+    />
 
     <div
       class="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 rounded bg-gray-100 p-2 pb-2"
diff --git a/src/lib/displays.ts b/src/lib/displays.ts
index 027a49046..a2da3bd84 100644
--- a/src/lib/displays.ts
+++ b/src/lib/displays.ts
@@ -9,4 +9,5 @@ export interface DisplayMode {
   variableName?: string
   waterfallName: string
   tab: string
+  parameterHash?: string
 }
diff --git a/src/lib/urls.ts b/src/lib/urls.ts
index 695c78b0d..ff7937b9d 100644
--- a/src/lib/urls.ts
+++ b/src/lib/urls.ts
@@ -13,41 +13,50 @@ export function newSelfTargetAProps(url: string): SelfTargetAProps {
 }
 
 export function newSimulationUrl(displayMode: DisplayMode): string {
-  const query: URLSearchParams = new URLSearchParams()
+  const parametersQuery: URLSearchParams = new URLSearchParams()
   if (displayMode.budget) {
-    query.append("budget", "true")
+    parametersQuery.append("budget", "true")
   }
   if (displayMode.edit !== undefined) {
-    query.append("edit", displayMode.edit.toString())
+    parametersQuery.append("edit", displayMode.edit.toString())
   }
   if (displayMode.mobileLaw) {
-    query.append("law", "true")
+    parametersQuery.append("law", "true")
   }
   if (
     displayMode.testCasesIndex.length !== 1 ||
     displayMode.testCasesIndex[0] !== 0
   ) {
     for (const testCaseIndex of displayMode.testCasesIndex) {
-      query.append("test_case", testCaseIndex.toString())
+      parametersQuery.append("test_case", testCaseIndex.toString())
     }
   }
   if (displayMode.variableName !== undefined) {
-    query.append("variable", displayMode.variableName)
+    parametersQuery.append("variable", displayMode.variableName)
   }
   if (displayMode.parameterName !== undefined) {
-    query.append("parameter", displayMode.parameterName)
+    parametersQuery.append("parameter", displayMode.parameterName)
   }
   if (displayMode.parametersVariableName !== undefined) {
-    query.append("parameters", displayMode.parametersVariableName)
+    parametersQuery.append("parameters", displayMode.parametersVariableName)
   }
   if (displayMode.waterfallName !== waterfalls[0].name) {
-    query.append("waterfall", displayMode.waterfallName)
+    parametersQuery.append("waterfall", displayMode.waterfallName)
   }
   if (displayMode.tab !== undefined) {
-    query.append("tab", displayMode.tab)
+    parametersQuery.append("tab", displayMode.tab)
   }
-  const queryString = query.toString()
-  return `/${queryString ? "?" + queryString : ""}`
+  const parametersQueryString = parametersQuery.toString()
+
+  const hashesQuery: URLSearchParams = new URLSearchParams()
+  if (displayMode.parameterHash !== undefined) {
+    hashesQuery.append("parameterHash", displayMode.parameterHash)
+  }
+  const hashesQueryString = hashesQuery.toString()
+
+  return `/${parametersQueryString ? "?" + parametersQueryString : ""}${
+    hashesQueryString ? "#" + hashesQueryString : ""
+  }`
 }
 
 export function stringifyQuery(queryParameters?: {
diff --git a/src/lib/viewport.ts b/src/lib/viewport.ts
new file mode 100644
index 000000000..c1e9ea442
--- /dev/null
+++ b/src/lib/viewport.ts
@@ -0,0 +1,24 @@
+let intersectionObserver: IntersectionObserver
+
+function ensureIntersectionObserver() {
+  if (intersectionObserver) return
+
+  intersectionObserver = new IntersectionObserver((entries) => {
+    entries.forEach((entry) => {
+      const eventName = entry.isIntersecting ? "enterViewport" : "exitViewport"
+      entry.target.dispatchEvent(new CustomEvent(eventName))
+    })
+  })
+}
+
+export default function viewport(element: HTMLElement) {
+  ensureIntersectionObserver()
+
+  intersectionObserver.observe(element)
+
+  return {
+    destroy() {
+      intersectionObserver.unobserve(element)
+    },
+  }
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 43bad027d..798cd3918 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -21,6 +21,7 @@
   import { goto } from "$app/navigation"
   import { page } from "$app/stores"
   import { auditQueryArray, auditQuerySingleton } from "$lib/auditors/queries"
+  import { auditSimulationHash } from "$lib/auditors/hashes"
   import type { BudgetSimulation } from "$lib/budgets"
   import {
     requestAllBudgetCalculations,
@@ -69,6 +70,7 @@
   } from "$lib/variables"
 
   import type { PageData } from "./$types"
+  import CopyClipboard from "$lib/components/CopyClipboard.svelte"
 
   export let data: PageData
 
@@ -123,7 +125,7 @@
 
   $: ({ user } = data)
 
-  $: ensureValidDisplayMode($page.url.searchParams)
+  $: ensureValidDisplayMode($page.url.searchParams, $page.url.hash)
 
   $: if (
     browser &&
@@ -517,25 +519,46 @@
     )
   }
 
-  function ensureValidDisplayMode(query: URLSearchParams): void {
-    let [validDisplayMode, queryError] = auditSimulationQuery(
+  function ensureValidDisplayMode(query: URLSearchParams, hash: string): void {
+    let [validQueryDisplayMode, queryError] = auditSimulationQuery(
       cleanAudit,
       query,
     ) as [DisplayMode, unknown]
+
     if (queryError !== null) {
       console.warn(
         `Query error at ${$page.url.pathname}: ${JSON.stringify(
           queryError,
           null,
           2,
-        )}\n\n${JSON.stringify(validDisplayMode, null, 2)}`,
+        )}\n\n${JSON.stringify(validQueryDisplayMode, null, 2)}`,
       )
-      validDisplayMode = {
+      validQueryDisplayMode = {
         testCasesIndex: [0],
         waterfallName: waterfalls[0].name,
       }
     }
-    displayMode = validDisplayMode
+
+    let [validHashDisplayMode, hashError] = auditSimulationHash(
+      cleanAudit,
+      hash,
+    ) as [DisplayMode, unknown]
+
+    if (hashError !== null) {
+      console.warn(
+        `Hash error at ${$page.url.pathname}: ${JSON.stringify(
+          hashError,
+          null,
+          2,
+        )}\n\n${JSON.stringify(validHashDisplayMode, null, 2)}`,
+      )
+      validHashDisplayMode = {}
+    }
+
+    displayMode = {
+      ...validQueryDisplayMode,
+      ...validHashDisplayMode,
+    }
     $testCasesIndex = displayMode.testCasesIndex
     $waterfall = waterfalls.find(
       ({ name }) => name === displayMode.waterfallName,
@@ -741,6 +764,53 @@
       { noScroll: true },
     )
   }
+
+  let clipboardElement: HTMLElement
+  async function onCopyParameterLink(parameterName: string) {
+    const urlString = "/simulations"
+    const res = await fetch(urlString, {
+      body: JSON.stringify({
+        displayMode: {
+          ...displayMode,
+          parameterHash: parameterName,
+          mobileLaw: true,
+        },
+        inputInstantsByVariableNameArray: $inputInstantsByVariableNameArray.map(
+          (inputInstantsByVariableName) =>
+            Object.fromEntries(
+              Object.entries(inputInstantsByVariableName).map(
+                ([variableName, inputInstants]) => [
+                  variableName,
+                  [...inputInstants],
+                ],
+              ),
+            ),
+        ),
+        parametricReform: $parametricReform,
+        testCases: $testCases,
+      }),
+      headers: {
+        Accept: "application/json",
+        "Content-Type": "application/json; charset=utf-8",
+      },
+      method: "POST",
+    })
+    if (!res.ok) {
+      console.error(
+        `Error ${
+          res.status
+        } while creating a share link at ${urlString}\n\n${await res.text()}`,
+      )
+      return
+    }
+    const { token } = await res.json()
+    const url = new URL(`/simulations/${token}`, $page.data.baseUrl).toString()
+    const copyClipboard = new CopyClipboard({
+      target: clipboardElement,
+      props: { value: url },
+    })
+    copyClipboard.$destroy()
+  }
 </script>
 
 <svelte:window bind:innerWidth={windowInnerWidth} on:keydown={onKeyDown} />
@@ -748,6 +818,8 @@
   <title>Calculs | {data.title}</title>
 </svelte:head>
 
+<div bind:this={clipboardElement} />
+
 <main
   class="fond flex h-full flex-1 overflow-x-clip after:absolute after:inset-0 after:z-10 after:bg-[rgba(0,0,0,.3)] after:transition-all md:overflow-hidden"
   class:after:content-none={!$isSearchActive}
@@ -994,20 +1066,20 @@
                 {/each}
               </div>
               <!-- Vue modification de la loi -->
-            {:else if displayMode.parametersVariableName !== undefined && displayMode.parameterName === undefined}
-              <div class="mb-5 flex flex-col gap-2">
-                <div class="flex justify-end">
-                  <button
-                    class="text-sm uppercase text-gray-600 hover:text-black"
-                    on:click={closeParametersEditionPane}
-                    >Autres dispositifs<iconify-icon
-                      class="ml-1 align-[-0.23rem] text-lg"
-                      icon="ri-arrow-up-line"
-                    />
-                  </button>
-                </div>
+            {:else}
+              <div class="flex justify-end">
+                <button
+                  class="text-sm uppercase text-gray-600 hover:text-black"
+                  on:click={closeParametersEditionPane}
+                  >Autres dispositifs<iconify-icon
+                    class="ml-1 align-[-0.23rem] text-lg"
+                    icon="ri-arrow-up-line"
+                  />
+                </button>
+              </div>
+              {#if displayMode.parametersVariableName !== undefined && displayMode.parameterName === undefined}
                 {#if displayMode.parametersVariableName === "csg_deductible_salaire" || displayMode.parametersVariableName === "csg_imposable_salaire"}
-                  <div class="flex justify-end">
+                  <div class="mt-2 flex justify-end">
                     <a
                       class="flex items-center text-sm uppercase text-gray-600 hover:text-black"
                       href={newSimulationUrl({
@@ -1033,53 +1105,29 @@
                     </a>
                   </div>
                 {/if}
-              </div>
 
-              <div class="flex-1 overflow-y-auto bg-white">
-                <VariableReferredParameters
-                  date={$date}
-                  {displayMode}
-                  name={displayMode.parametersVariableName}
-                />
-              </div>
-              <!-- Vue modification d'un paramètre -->
-            {:else}
-              <!-- Bouton de fermeture du dispositif en cours - DESKTOP -->
-              <div class="mx-5 mb-2 hidden justify-end md:flex">
-                <button
-                  class="text-sm uppercase text-gray-600 hover:text-black"
-                  on:click={closeParametersEditionPane}
-                  >Autres dispositifs<iconify-icon
-                    class="ml-1 align-[-0.2rem] text-lg"
-                    icon="ri-arrow-up-line"
-                  /></button
-                >
-              </div>
-              <div class="md:[calc(100vh-13rem)] overflow-y-auto">
-                <!-- Bouton de fermeture du dispositif en cours - MOBILE -->
-                <div class="mx-5 mb-2 flex justify-end md:hidden">
-                  <button
-                    class="text-sm uppercase text-gray-600 hover:text-black"
-                    on:click={closeParametersEditionPane}
-                    >Autres dispositifs<iconify-icon
-                      class="ml-1 align-[-0.2rem] text-lg"
-                      icon="ri-arrow-up-line"
-                    /></button
-                  >
+                <!--                <div class="mb-5 flex flex-col gap-2">-->
+
+                <div class="mt-5 flex-1 overflow-y-auto bg-white">
+                  <VariableReferredParameters
+                    date={$date}
+                    {displayMode}
+                    name={displayMode.parametersVariableName}
+                    onCopyLink={onCopyParameterLink}
+                  />
                 </div>
-                <h1
-                  class="mb-5 ml-5 flex border-b border-black pb-3 pt-1 text-xl font-bold text-black md:hidden"
-                >
-                  Modifier le droit en vigueur
-                </h1>
-                <div class="ml-5 bg-white">
+              {:else}
+                <div class="ml-5 mt-5 bg-white">
                   <NonVariableReferredParameter
                     budget={displayMode.budget}
                     date={$date}
+                    {displayMode}
                     name={displayMode.parameterName}
+                    onCopyLink={onCopyParameterLink}
                   />
                 </div>
-              </div>
+              {/if}
+              <!-- Vue modification d'un paramètre -->
             {/if}
           </div>
         </div>
diff --git a/src/routes/simulations/+server.ts b/src/routes/simulations/+server.ts
index 9a9cd15be..7faefcd57 100644
--- a/src/routes/simulations/+server.ts
+++ b/src/routes/simulations/+server.ts
@@ -82,7 +82,13 @@ export const POST: RequestHandler = async ({ request, url }) => {
     throw error(400, `Invalid body: ${JSON.stringify(bodyError, null, 2)}`)
   }
   const bodyJson = JSON.stringify(body, null, 2)
-  const digest = XXH64(Buffer.from(bodyJson)).toString(16)
+
+  // Sometimes, the magnitude of the BigInt value generated by XXH64()
+  // is not large enough to require 16 characters when represented in base 16.
+  // As a result, the base 16 representation has leading zeros that are not necessary,
+  // and JavaScript omits them when converting the BigInt to a string. To fix this,
+  // we add zeros in the beginning until we reach a length of 16.
+  const digest = XXH64(Buffer.from(bodyJson)).toString(16).padStart(16, "0")
 
   const simulationDir = path.join(simulationsDir, digest.substring(0, 2))
   const simulationFilePath = path.join(simulationDir, `${digest}.json`)
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 71a02805e..56fa69a1c 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -28,6 +28,9 @@ const config = {
   ],
   theme: {
     extend: {
+      animation: {
+        blink: "blinker 300ms ease-in 2",
+      },
       blur: {
         xs: "1.2px",
         xxs: "0.8px",
@@ -48,8 +51,10 @@ const config = {
         "le-vert-validation": "#13CC03",
         "le-vert-validation-dark": "#377330",
       },
-      zIndex: {
-        25: "25",
+      keyframes: {
+        blinker: {
+          "50%": { opacity: "60%" },
+        },
       },
       transitionTimingFunction: {
         "in-quad": "cubic-bezier(.55, .085, .68, .53)",
@@ -76,6 +81,9 @@ const config = {
         "in-out-circ": "cubic-bezier(.785, .135, .15, .86)",
         "in-out-back": "cubic-bezier(.68, -.6, .32, 1.6)",
       },
+      zIndex: {
+        25: "25",
+      },
     },
     fontFamily: {
       sans: ["Lato", "sans-serif"],
-- 
GitLab