@erclx/canon 4.40.2 → 4.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ ---
2
+ /**
3
+ * Serves candidate treatments of one decision from the running page, selected
4
+ * by a query parameter, with a switcher for moving between them.
5
+ *
6
+ * It exists because a decision about how something feels to cause cannot be
7
+ * settled from a capture or a recording, both of which are passive, and the
8
+ * operator has to drive the candidates themselves. Five decisions were served
9
+ * that way in one run, each with a hand-written parameter, switcher, and
10
+ * removal, none of which resembled the last.
11
+ *
12
+ * Renders nothing in a production build, and nothing in development either
13
+ * until the page is asked for an arm by name, so a page loaded without the
14
+ * parameter is what ships.
15
+ *
16
+ * An arm carries CSS when the decision is a treatment. When it is a value the
17
+ * page reads at runtime, a pace or a shader uniform, the arm carries no CSS
18
+ * and the module holding that value reads the active id off
19
+ * `document.documentElement.dataset[param]`.
20
+ */
21
+ interface Arm {
22
+ /** Appears in the URL. Use `0` for what ships, so the baseline is an arm. */
23
+ readonly id: string
24
+ /** Names the arm and what it costs, since an arm with no cost is not one. */
25
+ readonly label: string
26
+ readonly css?: string
27
+ }
28
+
29
+ interface Props {
30
+ /** The query parameter, which is also the dataset key an arm is read from. */
31
+ readonly param: string
32
+ readonly arms: readonly Arm[]
33
+ /**
34
+ * Where the switcher mounts, as a selector. A decision inside a modal needs
35
+ * its switcher inside that modal, since anything outside it sits under the
36
+ * backdrop. Defaults to the body.
37
+ */
38
+ readonly mountInto?: string
39
+ }
40
+
41
+ const { param, arms, mountInto } = Astro.props
42
+
43
+ // The component leaves the production tree rather than gating itself at
44
+ // runtime, so neither an arm nor the parameter can reach a reader.
45
+ const enabled = import.meta.env.DEV
46
+
47
+ // Held as a string and written with `set:html`. Inside an Astro expression a
48
+ // script's children are parsed the way JSX children are, so every brace in the
49
+ // source reads as an interpolation and the body ships as an unevaluated
50
+ // literal. It did, once, silently.
51
+ const SOURCE = `
52
+ const wanted = new URLSearchParams(location.search).get(param)
53
+ const active = arms.find((arm) => arm.id === wanted)
54
+
55
+ if (active) {
56
+ document.documentElement.dataset[param] = active.id
57
+
58
+ if (active.css) {
59
+ const style = document.createElement('style')
60
+ style.textContent = active.css
61
+ document.head.appendChild(style)
62
+ }
63
+
64
+ const render = () => {
65
+ const host = mountInto ? document.querySelector(mountInto) : document.body
66
+ if (!host) return
67
+ const bar = document.createElement('div')
68
+ bar.setAttribute('data-scenario-switcher', '')
69
+ for (const arm of arms) {
70
+ const link = document.createElement('a')
71
+ link.href = '?' + param + '=' + arm.id
72
+ link.textContent = arm.label
73
+ if (arm.id === active.id) link.setAttribute('data-current', 'true')
74
+ bar.appendChild(link)
75
+ }
76
+ host.appendChild(bar)
77
+ }
78
+
79
+ if (document.readyState === 'loading') {
80
+ document.addEventListener('DOMContentLoaded', render)
81
+ } else {
82
+ render()
83
+ }
84
+ }
85
+ `
86
+
87
+ const SWITCHER_CSS = `
88
+ /* Bottom left rather than bottom centre, which Astro's dev toolbar owns.
89
+ Centred, the toolbar sits over the switcher and swallows every click on it. */
90
+ [data-scenario-switcher] {
91
+ position: fixed;
92
+ left: 1.5rem;
93
+ bottom: 1.5rem;
94
+ z-index: 60;
95
+ display: flex;
96
+ flex-wrap: wrap;
97
+ gap: 0.5rem;
98
+ max-width: calc(100vw - 3rem);
99
+ font: 500 13px system-ui, sans-serif;
100
+ }
101
+ [data-scenario-switcher] a {
102
+ padding: 0.4rem 0.8rem;
103
+ border: 1px solid rgb(128 128 128 / 0.4);
104
+ border-radius: 999px;
105
+ background: rgb(255 255 255 / 0.9);
106
+ color: #1a1815;
107
+ text-decoration: none;
108
+ white-space: nowrap;
109
+ }
110
+ [data-scenario-switcher] a[data-current='true'] {
111
+ background: #1a1815;
112
+ color: #f8f4ed;
113
+ }
114
+ `
115
+ ---
116
+
117
+ {
118
+ enabled && (
119
+ <script
120
+ is:inline
121
+ define:vars={{ param, arms, mountInto }}
122
+ set:html={SOURCE}
123
+ />
124
+ )
125
+ }
126
+
127
+ {enabled && <style is:global set:html={SWITCHER_CSS} />}
@@ -58,3 +58,10 @@ Append to the `## Scripts` table:
58
58
  ## CI docs (extend)
59
59
 
60
60
  In `.claude/context/ci.md`, the Typecheck row's assertion reads: `` `astro check` passes ``. The Build row's assertion reads: `` `astro build` succeeds ``.
61
+
62
+ ## Scenario switcher
63
+
64
+ - `src/components/dev/scenarios.astro` ships as a golden config, always overwritten on sync, since it is toolkit-authored infrastructure rather than a file a project hand-edits.
65
+ - Import it into a page under test to drive candidate treatments of one decision by hand, selected by a query parameter, with a switcher for moving between them.
66
+ - Guarded by `import.meta.env.DEV`, so it renders nothing and ships nothing in a production build.
67
+ - Astro-only. It uses `is:inline`, `define:vars`, and `set:html`, which only the `.astro` file format parses. `vite-react` gets no equivalent until a real decision drives one.