@zvndev/darkmown 0.19.0 → 1.0.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.
Files changed (69) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +115 -5
  4. package/docs/spec-alignment.md +16 -3
  5. package/package.json +22 -4
  6. package/site/_/base.skin +372 -0
  7. package/site/_/base.wd +4 -0
  8. package/site/_/footer.wd +14 -0
  9. package/site/_/metrics.json +13 -0
  10. package/site/_/nav.skin +35 -12
  11. package/site/_/nav.wd +8 -10
  12. package/site/_/products.json +10 -0
  13. package/site/pages/app.wd +1 -1
  14. package/site/pages/carousel/index.js +82 -0
  15. package/site/pages/carousel/index.skin +185 -0
  16. package/site/pages/carousel/index.wd +109 -0
  17. package/site/pages/carousel/slide-1.jpg +0 -0
  18. package/site/pages/carousel/slide-2.jpg +0 -0
  19. package/site/pages/carousel/slide-3.jpg +0 -0
  20. package/site/pages/carousel/slide-4.jpg +0 -0
  21. package/site/pages/carousel/slide-5.jpg +0 -0
  22. package/site/pages/compass/index.skin +247 -0
  23. package/site/pages/compass/index.wd +194 -0
  24. package/site/pages/docs/index.skin +48 -28
  25. package/site/pages/docs/index.wd +71 -5
  26. package/site/pages/folio/-style.skin +375 -0
  27. package/site/pages/folio/-style.wd +6 -0
  28. package/site/pages/folio/cart.skin +309 -0
  29. package/site/pages/folio/cart.wd +201 -0
  30. package/site/pages/folio/index.skin +24 -0
  31. package/site/pages/folio/index.wd +167 -0
  32. package/site/pages/forge/index.skin +676 -0
  33. package/site/pages/forge/index.wd +330 -0
  34. package/site/pages/index.skin +135 -144
  35. package/site/pages/index.wd +17 -5
  36. package/site/pages/ledger/index.skin +257 -0
  37. package/site/pages/ledger/index.wd +174 -0
  38. package/site/pages/media/clip-poster.jpg +0 -0
  39. package/site/pages/media/clip.mp4 +0 -0
  40. package/site/pages/media/index.skin +82 -0
  41. package/site/pages/media/index.wd +84 -0
  42. package/site/pages/media/tone.mp3 +0 -0
  43. package/site/pages/pulse/index.skin +434 -0
  44. package/site/pages/pulse/index.wd +178 -0
  45. package/site/pages/showcase/index.skin +130 -0
  46. package/site/pages/showcase/index.wd +168 -0
  47. package/src/builder.js +20 -14
  48. package/src/cli.js +216 -77
  49. package/src/compiler/body.js +457 -0
  50. package/src/compiler/context.js +160 -0
  51. package/src/compiler/directives.js +1345 -0
  52. package/src/compiler/format.js +244 -0
  53. package/src/compiler/frontmatter.js +117 -0
  54. package/src/compiler/includes.js +116 -0
  55. package/src/compiler/index.js +22 -0
  56. package/src/compiler/interpolation.js +223 -0
  57. package/src/compiler/loops.js +715 -0
  58. package/src/compiler/markdown.js +242 -0
  59. package/src/compiler/page.js +267 -0
  60. package/src/compiler/predicates.js +314 -0
  61. package/src/compiler.js +26 -2932
  62. package/src/dev.js +3 -1
  63. package/src/headers.js +8 -4
  64. package/src/index.js +22 -0
  65. package/src/router.js +3 -1
  66. package/src/runtime.js +168 -14
  67. package/src/scaffold.js +158 -131
  68. package/src/skin.js +11 -2
  69. package/site/_/og.png +0 -0
@@ -0,0 +1,372 @@
1
+ /* ===================================================================
2
+ Darkmown design system — the refreshed brand foundation.
3
+ Warm, literary, premium. Serif for prose, system sans for chrome,
4
+ a refined mono for code. Ships zero JS; dark mode is OS-driven via
5
+ `tokens dark` (a pure CSS media query — no flash, no toggle script).
6
+ Every refreshed page and demo app includes this via /base.wd.
7
+ =================================================================== */
8
+
9
+ tokens
10
+ ink #1c1814
11
+ ink-soft #4b4239
12
+ muted #756a5d
13
+ faint #9b9082
14
+ paper #faf8f3
15
+ paper-2 #f4f0e7
16
+ sunk #efe9dc
17
+ panel #ffffff
18
+ line #e7e1d4
19
+ line-soft #f0ebe0
20
+ accent #0f6b5e
21
+ accent-deep #0a4f45
22
+ accent-soft #e3f0ec
23
+ warm #b8462e
24
+ warm-soft #f8eae3
25
+ gold #b6822b
26
+ code-bg #1b2420
27
+ code-ink #e9efe7
28
+ code-line #2c3b34
29
+ ring #0f6b5e
30
+ radius 12px
31
+ radius-sm 8px
32
+ radius-lg 20px
33
+
34
+ tokens dark
35
+ ink #ece6db
36
+ ink-soft #c3bbab
37
+ muted #9a9081
38
+ faint #6f6657
39
+ paper #14110d
40
+ paper-2 #1a1612
41
+ sunk #221d17
42
+ panel #1e1913
43
+ line #2f281f
44
+ line-soft #261f18
45
+ accent #5ccab4
46
+ accent-deep #8fe0cf
47
+ accent-soft #16302a
48
+ warm #e88a6f
49
+ warm-soft #2c1f18
50
+ gold #d6a24e
51
+ code-bg #100d0a
52
+ code-ink #e4ebe2
53
+ code-line #2a231b
54
+ ring #5ccab4
55
+
56
+ /* ── Reset & base ───────────────────────────────────────────────── */
57
+
58
+ *
59
+ box-sizing border-box
60
+
61
+ page
62
+ margin 0
63
+ font-family ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, Cambria, serif
64
+ font-size 18px
65
+ line-height 1.65
66
+ color $ink
67
+ bg $paper
68
+ -webkit-font-smoothing antialiased
69
+ text-rendering optimizeLegibility
70
+
71
+ ::selection
72
+ bg $accent-soft
73
+ color $accent-deep
74
+
75
+ /* ── Typography ─────────────────────────────────────────────────── */
76
+
77
+ h1, h2, h3, h4
78
+ line-height 1.1
79
+ letter-spacing -.012em
80
+ color $ink
81
+ font-weight 600
82
+
83
+ h1
84
+ font-size clamp(2.5rem, 6vw, 4.5rem)
85
+ letter-spacing -.025em
86
+ margin .2em 0 .35em
87
+
88
+ h2
89
+ font-size clamp(1.6rem, 3vw, 2.25rem)
90
+ margin 2.4em 0 .6em
91
+
92
+ h3
93
+ font-size 1.3rem
94
+ margin 1.8em 0 .4em
95
+
96
+ p
97
+ margin 1em 0
98
+ max-width 68ch
99
+
100
+ a
101
+ color $accent
102
+ text-underline-offset 2px
103
+ text-decoration-thickness 1px
104
+
105
+ a:hover
106
+ color $accent-deep
107
+
108
+ strong
109
+ font-weight 650
110
+ color $ink
111
+
112
+ hr
113
+ border 0
114
+ border-top 1px solid $line
115
+ margin 3rem 0
116
+
117
+ small, .small
118
+ font-size .82rem
119
+ color $muted
120
+
121
+ /* Chrome (nav, buttons, labels, badges) leans on a clean sans for a
122
+ modern editorial contrast against the serif prose. */
123
+ .topnav, .btn, .eyebrow, .pill, .tag, .badge, .kbd, .sitefooter, .source-reveal summary
124
+ font-family system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif
125
+
126
+ /* ── Layout ─────────────────────────────────────────────────────── */
127
+
128
+ main
129
+ max-width 940px
130
+ margin 0 auto
131
+ padding 0 1.5rem 5rem
132
+
133
+ .wide
134
+ max-width 1140px
135
+
136
+ .eyebrow
137
+ display inline-block
138
+ text-transform uppercase
139
+ letter-spacing .14em
140
+ font-size .74rem
141
+ font-weight 600
142
+ color $accent
143
+ margin 0 0 .35rem
144
+
145
+ /* ── Buttons ────────────────────────────────────────────────────── */
146
+
147
+ .btn
148
+ display inline-flex
149
+ align-items center
150
+ gap .5rem
151
+ margin .4rem .5rem .4rem 0
152
+ padding .7rem 1.15rem
153
+ font-size .95rem
154
+ font-weight 550
155
+ line-height 1
156
+ color $ink
157
+ bg $panel
158
+ border 1px solid $line
159
+ radius 999px
160
+ text-decoration none
161
+ cursor pointer
162
+ transition transform .12s ease, box-shadow .12s ease, background .12s ease
163
+
164
+ .btn:hover
165
+ transform translateY(-1px)
166
+ box-shadow 0 6px 18px -8px rgba(28,24,20,.35)
167
+
168
+ .btn.primary
169
+ bg $accent
170
+ color #fff
171
+ border-color $accent
172
+
173
+ .btn.primary:hover
174
+ bg $accent-deep
175
+ color #fff
176
+
177
+ .btn.warm
178
+ bg $warm
179
+ color #fff
180
+ border-color $warm
181
+
182
+ .btn.ghost
183
+ bg transparent
184
+
185
+ /* ── Cards & surfaces ───────────────────────────────────────────── */
186
+
187
+ .card
188
+ bg $panel
189
+ border 1px solid $line
190
+ radius $radius
191
+ padding 1.4rem
192
+ box-shadow 0 1px 2px rgba(28,24,20,.04)
193
+
194
+ .grid
195
+ display grid
196
+ gap 1.1rem
197
+
198
+ .grid.cols-2
199
+ grid-template-columns repeat(2, 1fr)
200
+
201
+ .grid.cols-3
202
+ grid-template-columns repeat(3, 1fr)
203
+
204
+ .grid.auto
205
+ grid-template-columns repeat(auto-fit, minmax(240px, 1fr))
206
+
207
+ @media (max-width: 760px)
208
+ .grid.cols-2, .grid.cols-3
209
+ grid-template-columns 1fr
210
+
211
+ /* ── Pills, tags, badges ────────────────────────────────────────── */
212
+
213
+ .pill, .tag
214
+ display inline-flex
215
+ align-items center
216
+ gap .35rem
217
+ font-size .72rem
218
+ font-weight 600
219
+ text-transform uppercase
220
+ letter-spacing .06em
221
+ padding .28rem .6rem
222
+ radius 999px
223
+
224
+ .pill
225
+ bg $accent-soft
226
+ color $accent-deep
227
+
228
+ .tag
229
+ bg $sunk
230
+ color $muted
231
+
232
+ .kbd
233
+ font-family ui-monospace, "SF Mono", Menlo, monospace
234
+ font-size .8rem
235
+ padding .15rem .4rem
236
+ radius 6px
237
+ bg $panel
238
+ border 1px solid $line
239
+ color $ink-soft
240
+
241
+ /* ── Callout (the demo :note directive renders <aside class="note">) ─ */
242
+
243
+ .note, .callout
244
+ margin 1.6rem 0
245
+ padding 1rem 1.25rem
246
+ border 1px solid $line
247
+ border-left 3px solid $accent
248
+ bg $panel
249
+ radius $radius-sm
250
+ color $ink-soft
251
+ font-size .96rem
252
+
253
+ /* ── Code ───────────────────────────────────────────────────────── */
254
+
255
+ code
256
+ font-family ui-monospace, "SF Mono", Menlo, Consolas, monospace
257
+ font-size .88em
258
+ bg $sunk
259
+ color $accent-deep
260
+ padding .12rem .36rem
261
+ radius 6px
262
+
263
+ h1 code, h2 code, h3 code
264
+ font-size .82em
265
+
266
+ pre
267
+ bg $code-bg
268
+ color $code-ink
269
+ padding 1.2rem 1.35rem
270
+ margin 1.5rem 0
271
+ radius $radius
272
+ overflow-x auto
273
+ line-height 1.62
274
+ font-size .87rem
275
+ box-shadow inset 0 0 0 1px $code-line
276
+
277
+ pre code
278
+ bg transparent
279
+ color inherit
280
+ padding 0
281
+ font-size 1em
282
+
283
+ /* ── Tables ─────────────────────────────────────────────────────── */
284
+
285
+ table
286
+ border-collapse collapse
287
+ width 100%
288
+ margin 1.5rem 0
289
+ font-size .95rem
290
+
291
+ th
292
+ text-align left
293
+ padding .6rem .9rem
294
+ border-bottom 2px solid $line
295
+ color $muted
296
+ font-weight 600
297
+ font-size .82rem
298
+ text-transform uppercase
299
+ letter-spacing .04em
300
+
301
+ td
302
+ padding .6rem .9rem
303
+ border-bottom 1px solid $line-soft
304
+
305
+ blockquote
306
+ margin 1.6rem 0
307
+ padding .6rem 1.3rem
308
+ border-left 3px solid $accent
309
+ color $ink-soft
310
+ font-style italic
311
+
312
+ /* ── "View source" reveal — the two-beat trick: a slick app, then the
313
+ Markdown it's made of. Wrap a fenced ```wd block in <details>. ── */
314
+
315
+ .source-reveal
316
+ margin 1.5rem 0
317
+ border 1px solid $line
318
+ radius $radius
319
+ bg $panel
320
+ overflow hidden
321
+
322
+ .source-reveal summary
323
+ cursor pointer
324
+ padding .85rem 1.1rem
325
+ font-size .9rem
326
+ font-weight 600
327
+ color $accent-deep
328
+ list-style none
329
+ display flex
330
+ align-items center
331
+ gap .5rem
332
+
333
+ .source-reveal summary::-webkit-details-marker
334
+ display none
335
+
336
+ .source-reveal summary::before
337
+ content "›"
338
+ font-size 1.1rem
339
+ transition transform .15s ease
340
+
341
+ .source-reveal[open] summary::before
342
+ transform rotate(90deg)
343
+
344
+ .source-reveal pre
345
+ margin 0
346
+ radius 0
347
+
348
+ /* ── Footer ─────────────────────────────────────────────────────── */
349
+
350
+ .sitefooter
351
+ border-top 1px solid $line
352
+ margin-top 4rem
353
+ padding 2.5rem 1.5rem
354
+ color $muted
355
+ font-size .9rem
356
+
357
+ .sitefooter .foot-inner
358
+ max-width 940px
359
+ margin 0 auto
360
+ display flex
361
+ flex-wrap wrap
362
+ gap 1rem 2rem
363
+ align-items center
364
+ justify-content space-between
365
+
366
+ .sitefooter a
367
+ color $muted
368
+ text-decoration none
369
+ margin-right 1.1rem
370
+
371
+ .sitefooter a:hover
372
+ color $accent
package/site/_/base.wd ADDED
@@ -0,0 +1,4 @@
1
+ <!-- Shared design-system carrier. Including this file injects base.skin (the
2
+ refreshed Darkmown brand foundation: tokens, typography, components, dark
3
+ mode) into the page. It renders no markup of its own. Pair with /nav.wd at
4
+ the top and /footer.wd at the bottom. -->
@@ -0,0 +1,14 @@
1
+ <footer class="sitefooter">
2
+ <div class="foot-inner">
3
+ <div>
4
+ <strong>Darkmown</strong> — Markdown that runs.
5
+ <div class="small">Zero JS on static pages · one ~7.4&nbsp;KB runtime when reactive.</div>
6
+ </div>
7
+ <nav class="foot-links">
8
+ <a href="/docs/">Docs</a>
9
+ <a href="/showcase/">Showcase</a>
10
+ <a href="https://github.com/ZVN-DEV/darkmown">GitHub</a>
11
+ <a href="https://www.npmjs.com/package/@zvndev/darkmown">npm</a>
12
+ </nav>
13
+ </div>
14
+ </footer>
@@ -0,0 +1,13 @@
1
+ {
2
+ "updated": "2026-06-26T05:00:00Z",
3
+ "services": [
4
+ { "id": "api", "name": "API Gateway", "status": "ok", "latency": 42, "uptime": 99.98, "region": "iad", "spark": [38,41,40,44,42,39,42] },
5
+ { "id": "web", "name": "Web Frontend", "status": "ok", "latency": 18, "uptime": 99.99, "region": "global", "spark": [16,18,17,19,18,18,18] },
6
+ { "id": "db", "name": "Primary DB", "status": "warn", "latency": 137, "uptime": 99.82, "region": "iad", "spark": [90,110,120,150,140,137,137] },
7
+ { "id": "cache", "name": "Edge Cache", "status": "ok", "latency": 4, "uptime": 100.0, "region": "global", "spark": [4,3,4,5,4,4,4] },
8
+ { "id": "queue", "name": "Job Queue", "status": "ok", "latency": 61, "uptime": 99.95, "region": "sfo", "spark": [55,60,58,64,61,59,61] },
9
+ { "id": "search", "name": "Search Index", "status": "down", "latency": 0, "uptime": 97.40, "region": "sfo", "spark": [120,140,160,0,0,0,0] },
10
+ { "id": "auth", "name": "Auth Service", "status": "ok", "latency": 33, "uptime": 99.97, "region": "iad", "spark": [30,34,32,35,33,33,33] },
11
+ { "id": "mail", "name": "Mail Relay", "status": "warn", "latency": 210, "uptime": 99.60, "region": "fra", "spark": [180,200,190,230,210,205,210] }
12
+ ]
13
+ }
package/site/_/nav.skin CHANGED
@@ -9,40 +9,63 @@ tokens
9
9
  codeink #ecf3ea
10
10
  radius 8px
11
11
 
12
+ /* No `tokens dark` here on purpose. The shared nav loads on EVERY page,
13
+ including the older feature-demo pages (/reactive, /loops, …) whose bespoke
14
+ component styles were never designed for a dark palette — flipping their
15
+ shared tokens made buttons unreadable. Dark mode is therefore opt-in: it
16
+ lives in base.skin, which only the refreshed pages and showcase apps include.
17
+ On those pages base.skin's dark tokens drive the nav too, so it still goes
18
+ dark there. */
19
+
12
20
  .topnav
13
21
  display flex
14
22
  flex-wrap wrap
15
- gap .5rem 1rem
23
+ gap .35rem 1.1rem
16
24
  align-items center
17
- padding 1rem 2rem
25
+ padding .85rem 1.5rem
18
26
  max-width 100%
19
27
  box-sizing border-box
20
28
  border-bottom 1px solid $line
21
- bg rgba(251,250,247,.85)
22
- backdrop-filter blur(8px)
29
+ background color-mix(in srgb, $paper 82%, transparent)
30
+ backdrop-filter saturate(1.4) blur(10px)
23
31
  position sticky
24
32
  top 0
25
- z-index 10
33
+ z-index 20
34
+ font-size .92rem
26
35
 
27
36
  @media (max-width: 600px)
28
37
  .topnav
29
- padding .85rem 1rem
30
- gap .4rem .85rem
38
+ padding .75rem 1rem
39
+ gap .3rem .8rem
31
40
 
32
- .topnav strong
41
+ .topnav .brand
42
+ font-weight 700
43
+ font-size 1.08rem
44
+ letter-spacing -.02em
33
45
  color $warm
34
- font-size 1.05rem
46
+ text-decoration none
35
47
  margin-right .5rem
36
48
 
49
+ .topnav .nav-links
50
+ display inline-flex
51
+ flex-wrap wrap
52
+ gap .35rem 1.1rem
53
+ align-items center
54
+
37
55
  .topnav a
38
56
  color $accent
39
57
  text-decoration none
40
58
 
41
59
  .topnav a:hover
42
- text-decoration underline
60
+ color $ink
61
+
62
+ .topnav .nav-gh
63
+ margin-left auto
64
+ color $ink
65
+ font-weight 600
43
66
 
44
67
  code
45
- bg $panel
68
+ background color-mix(in srgb, $paper 50%, $panel)
46
69
  border 1px solid $line
47
70
  padding .1rem .32rem
48
71
  radius 5px
@@ -78,7 +101,7 @@ th
78
101
  text-align left
79
102
  border 1px solid $line
80
103
  padding .5rem .9rem
81
- bg #f1ede4
104
+ background color-mix(in srgb, $paper 60%, $line)
82
105
 
83
106
  td
84
107
  border 1px solid $line
package/site/_/nav.wd CHANGED
@@ -1,12 +1,10 @@
1
1
  <nav class="topnav">
2
- <strong>Darkmown</strong>
3
- <a href="/">Home</a>
4
- <a href="/docs/">Docs</a>
5
- <a href="/markdown/">Plain Markdown</a>
6
- <a href="/reactive/">Reactive</a>
7
- <a href="/loops/">Loops</a>
8
- <a href="/store/">Store</a>
9
- <a href="/theme/">Theme</a>
10
- <a href="/data/">Data &amp; Forms</a>
11
- <a href="/app/">Live app</a>
2
+ <a class="brand" href="/">Darkmown</a>
3
+ <span class="nav-links">
4
+ <a href="/docs/">Docs</a>
5
+ <a href="/showcase/">Showcase</a>
6
+ <a href="/reactive/">Reactive</a>
7
+ <a href="/data/">Data</a>
8
+ </span>
9
+ <a class="nav-gh" href="https://github.com/ZVN-DEV/darkmown">GitHub ↗</a>
12
10
  </nav>
@@ -0,0 +1,10 @@
1
+ [
2
+ { "id": "aurora", "name": "Aurora Lamp", "price": 149, "category": "Light", "emoji": "🪔", "tint": "#f4d58d", "blurb": "A warm dimmable glow with a hand-turned oak base." },
3
+ { "id": "briza", "name": "Briza Fan", "price": 89, "category": "Air", "emoji": "🌀", "tint": "#bcd6e8", "blurb": "Whisper-quiet bladeless airflow, USB-C powered." },
4
+ { "id": "cove", "name": "Cove Speaker", "price": 199, "category": "Sound", "emoji": "🔊", "tint": "#cbbfe6", "blurb": "Room-filling 360° sound in a palm-sized shell." },
5
+ { "id": "dune", "name": "Dune Mug", "price": 24, "category": "Kitchen", "emoji": "☕", "tint": "#e8c9a8", "blurb": "Double-walled stoneware that keeps coffee hot for hours." },
6
+ { "id": "fjord", "name": "Fjord Bottle", "price": 39, "category": "Kitchen", "emoji": "🧊", "tint": "#a8d8d0", "blurb": "Vacuum steel, 24h cold, sweat-proof matte finish." },
7
+ { "id": "glint", "name": "Glint Desk Clock", "price": 69, "category": "Light", "emoji": "🕰️", "tint": "#f0c8c0", "blurb": "E-ink face, ambient backlight, three-month battery." },
8
+ { "id": "haven", "name": "Haven Diffuser", "price": 59, "category": "Air", "emoji": "🌿", "tint": "#c4e0b4", "blurb": "Ultrasonic mist with a refillable cedar reservoir." },
9
+ { "id": "isle", "name": "Isle Earbuds", "price": 129, "category": "Sound", "emoji": "🎧", "tint": "#d8c4ec", "blurb": "Active noise cancelling, 30-hour case, tap controls." }
10
+ ]
package/site/pages/app.wd CHANGED
@@ -19,7 +19,7 @@ This whole section is plain Markdown. The list below is unrolled **at build time
19
19
  - **{ feature.title }** — { feature.body }
20
20
  @endloop
21
21
 
22
- <aside class="boundary">If you deleted everything below this line, this page would ship <strong>zero</strong> bytes of JavaScript. Everything above is static HTML. Below is where the one ~5.7&nbsp;KB gzipped runtime loads (CI-enforced under 6 KB) — and only because the document asks for it.</aside>
22
+ <aside class="boundary">If you deleted everything below this line, this page would ship <strong>zero</strong> bytes of JavaScript. Everything above is static HTML. Below is where the one ~7.4&nbsp;KB gzipped runtime loads (CI-enforced under 8 KB) — and only because the document asks for it.</aside>
23
23
 
24
24
  ## Fetched data, looped, with nested conditions
25
25
 
@@ -0,0 +1,82 @@
1
+ // Carousel behavior — the escape hatch in action.
2
+ //
3
+ // Darkmown owns the reactive `slide` state, the indicator dots (`.active when`),
4
+ // and the "Slide N of 5" readout. This colocated module adds exactly what a
5
+ // deliberately tiny framework leaves out: arrow + keyboard + pointer-drag navigation
6
+ // and the sliding transition. It never reaches into the framework's internals —
7
+ // it reads and writes the same `slide` value through the public `window.wd` API.
8
+ //
9
+ // Module scripts run after /__wd/runtime.js (which defines window.wd) and in
10
+ // document order, so `wd` is guaranteed ready here.
11
+
12
+ const viewport = document.querySelector("[data-swiper]");
13
+
14
+ if (viewport && window.wd) {
15
+ const track = viewport.querySelector("[data-swiper-track]");
16
+ const slides = [...track.children];
17
+ const N = slides.length;
18
+ const KEY = "slide";
19
+
20
+ const wrap = (i) => ((i % N) + N) % N;
21
+ const current = () => wrap(Number(window.wd.get(KEY)) || 0);
22
+ const go = (i) => window.wd.set(KEY, wrap(i));
23
+ const SETTLE = "transform .42s cubic-bezier(.22,.61,.36,1)";
24
+
25
+ // State → view. Primed immediately, then on every settled change. Because the
26
+ // arrows/keyboard/drag below always write a wrapped value, `slide` never leaves
27
+ // [0, N-1], so the declarative dots and readout stay valid every frame.
28
+ window.wd.subscribe(KEY, (raw) => {
29
+ const i = wrap(raw);
30
+ track.style.transition = SETTLE;
31
+ track.style.transform = `translateX(${-i * 100}%)`;
32
+ slides.forEach((s, k) => s.setAttribute("aria-hidden", k === i ? "false" : "true"));
33
+ });
34
+
35
+ // Arrow buttons (plain <button>s, wired here — not :button directives).
36
+ document.querySelector("[data-swiper-prev]")?.addEventListener("click", () => go(current() - 1));
37
+ document.querySelector("[data-swiper-next]")?.addEventListener("click", () => go(current() + 1));
38
+
39
+ // Keyboard: the viewport is focusable; ← / → step through.
40
+ viewport.addEventListener("keydown", (event) => {
41
+ if (event.key === "ArrowLeft") { event.preventDefault(); go(current() - 1); }
42
+ else if (event.key === "ArrowRight") { event.preventDefault(); go(current() + 1); }
43
+ });
44
+
45
+ // Pointer drag / touch swipe. Follow the finger while dragging (transition off),
46
+ // then either advance or snap back on release.
47
+ let startX = 0;
48
+ let dragging = false;
49
+ let width = 1;
50
+
51
+ viewport.addEventListener("pointerdown", (event) => {
52
+ if (event.button != null && event.button !== 0) return;
53
+ dragging = true;
54
+ startX = event.clientX;
55
+ width = viewport.clientWidth || 1;
56
+ track.style.transition = "none";
57
+ try { viewport.setPointerCapture(event.pointerId); } catch { /* capture is best-effort */ }
58
+ });
59
+
60
+ viewport.addEventListener("pointermove", (event) => {
61
+ if (!dragging) return;
62
+ const dx = event.clientX - startX;
63
+ track.style.transform = `translateX(calc(${-current() * 100}% + ${dx}px))`;
64
+ });
65
+
66
+ const release = (event) => {
67
+ if (!dragging) return;
68
+ dragging = false;
69
+ const dx = event.clientX - startX;
70
+ if (Math.abs(dx) > width * 0.18) {
71
+ go(current() + (dx < 0 ? 1 : -1));
72
+ } else {
73
+ // Below threshold: ease back to the current slide. The value didn't change,
74
+ // so subscribe won't re-fire — reset the transform directly.
75
+ track.style.transition = "transform .3s ease";
76
+ track.style.transform = `translateX(${-current() * 100}%)`;
77
+ }
78
+ };
79
+
80
+ viewport.addEventListener("pointerup", release);
81
+ viewport.addEventListener("pointercancel", release);
82
+ }