@zvndev/darkmown 0.20.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 (57) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +115 -5
  4. package/docs/spec-alignment.md +16 -3
  5. package/package.json +3 -3
  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/compiler/body.js +26 -1
  48. package/src/compiler/context.js +1 -1
  49. package/src/compiler/directives.js +199 -3
  50. package/src/compiler/format.js +244 -0
  51. package/src/compiler/interpolation.js +35 -1
  52. package/src/compiler/loops.js +95 -24
  53. package/src/compiler/markdown.js +16 -7
  54. package/src/compiler/predicates.js +35 -2
  55. package/src/headers.js +5 -0
  56. package/src/runtime.js +168 -14
  57. package/src/skin.js +9 -1
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
+ }
@@ -0,0 +1,185 @@
1
+ /* ── Swiper demo — layers on /base.wd. The viewport clips; the track is a flex
2
+ row the colocated index.js slides with a transform. Reuses base tokens. ──── */
3
+
4
+ .app-swiper
5
+ max-width 56rem
6
+ margin 0 auto
7
+ padding 2.5rem 1.25rem 4rem
8
+
9
+ .app-swiper .swiper-head
10
+ margin-bottom 1.75rem
11
+
12
+ .app-swiper .swiper-eyebrow
13
+ font-size .72rem
14
+ font-weight 700
15
+ letter-spacing .14em
16
+ text-transform uppercase
17
+ color $accent
18
+
19
+ .app-swiper h1
20
+ margin .4rem 0 0
21
+ font-size 2.4rem
22
+ letter-spacing -.02em
23
+ color $ink
24
+
25
+ .app-swiper .swiper-tagline
26
+ margin .7rem 0 0
27
+ max-width 42rem
28
+ color $ink-soft
29
+ font-size 1rem
30
+ line-height 1.6
31
+
32
+ .app-swiper .swiper-head code, .app-swiper .swiper-tagline code, .app-swiper .swiper-readout code
33
+ bg $code-bg
34
+ color $code-ink
35
+ padding .06rem .32rem
36
+ radius 6px
37
+ font-size .85em
38
+
39
+ /* Stage */
40
+ .app-swiper .swiper-stage
41
+ margin 0
42
+
43
+ .app-swiper .swiper-viewport
44
+ overflow hidden
45
+ border 1px solid $line-soft
46
+ radius 18px
47
+ background $sunk
48
+ cursor grab
49
+ touch-action pan-y
50
+ outline none
51
+
52
+ .app-swiper .swiper-viewport:focus-visible
53
+ box-shadow 0 0 0 3px $ring
54
+
55
+ .app-swiper .swiper-viewport:active
56
+ cursor grabbing
57
+
58
+ .app-swiper .swiper-track
59
+ display flex
60
+ /* transition is set by index.js so the very first paint doesn't animate */
61
+ will-change transform
62
+
63
+ .app-swiper .swipe-slide
64
+ position relative
65
+ flex 0 0 100%
66
+ margin 0
67
+
68
+ .app-swiper .swipe-slide img
69
+ display block
70
+ width 100%
71
+ height auto
72
+ aspect-ratio 16/9
73
+ object-fit cover
74
+ user-select none
75
+ -webkit-user-drag none
76
+ pointer-events none
77
+
78
+ .app-swiper .swipe-slide figcaption
79
+ position absolute
80
+ left 0
81
+ right 0
82
+ bottom 0
83
+ display flex
84
+ flex-direction column
85
+ gap .15rem
86
+ padding 1.5rem 1.25rem 1rem
87
+ color #fff
88
+ background linear-gradient(to top, rgba(10,8,6,.72), rgba(10,8,6,0))
89
+
90
+ .app-swiper .swipe-slide figcaption strong
91
+ color #fff
92
+ font-size 1.15rem
93
+ letter-spacing -.01em
94
+
95
+ .app-swiper .swipe-slide figcaption span
96
+ font-size .82rem
97
+ opacity .85
98
+ font-variant-numeric tabular-nums
99
+
100
+ /* Controls row: arrows flank the dots */
101
+ .app-swiper .swiper-controls
102
+ display flex
103
+ align-items center
104
+ justify-content center
105
+ gap 1rem
106
+ margin 1.1rem 0 .25rem
107
+
108
+ .app-swiper .swiper-arrow
109
+ flex 0 0 auto
110
+ width 2.6rem
111
+ height 2.6rem
112
+ display inline-flex
113
+ align-items center
114
+ justify-content center
115
+ border 1px solid $line
116
+ background $panel
117
+ color $ink-soft
118
+ font-size 1.4rem
119
+ line-height 1
120
+ radius 999px
121
+ cursor pointer
122
+ transition color .14s ease, border-color .14s ease, transform .14s ease
123
+
124
+ .app-swiper .swiper-arrow:hover
125
+ color $ink
126
+ border-color $accent
127
+
128
+ .app-swiper .swiper-arrow:active
129
+ transform scale(.94)
130
+
131
+ /* Dots — each is a declarative container whose .active is reconciled by the
132
+ framework from `.active when slide == N`. */
133
+ .app-swiper .swiper-dots
134
+ display inline-flex
135
+ align-items center
136
+ gap .4rem
137
+
138
+ .app-swiper .dot button[data-wd-action]
139
+ width 1.6rem
140
+ height 1.6rem
141
+ display inline-flex
142
+ align-items center
143
+ justify-content center
144
+ border 0
145
+ background transparent
146
+ /* collapse the visible digit — it stays the button's accessible name */
147
+ font-size 0
148
+ border-radius 999px
149
+ cursor pointer
150
+ position relative
151
+
152
+ .app-swiper .dot button[data-wd-action]::after
153
+ content ""
154
+ position absolute
155
+ width .5rem
156
+ height .5rem
157
+ border-radius 999px
158
+ background $line
159
+ transition background .18s ease, transform .18s ease
160
+
161
+ .app-swiper .dot.active button[data-wd-action]::after
162
+ background $accent
163
+ transform scale(1.5)
164
+
165
+ .app-swiper .dot button[data-wd-action]:hover::after
166
+ background $accent-deep
167
+
168
+ .app-swiper .swiper-readout
169
+ margin 1rem 0 0
170
+ text-align center
171
+ font-size .9rem
172
+ color $muted
173
+
174
+ .app-swiper .swiper-readout strong
175
+ color $ink
176
+
177
+ .app-swiper .swiper-readout kbd
178
+ font-size .8em
179
+
180
+ .app-swiper .source-reveal
181
+ margin-top 2rem
182
+
183
+ @media (prefers-reduced-motion: reduce)
184
+ .app-swiper .swiper-track
185
+ transition none !important
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: Swiper — a draggable carousel wired to wd.subscribe
3
+ description: An image carousel with arrow-key nav, pointer drag / swipe, and a sliding transition — the things a deliberately tiny framework leaves to you. Darkmown owns the reactive slide state, the dots, and the readout declaratively; a colocated index.js adds the gestures through window.wd.subscribe and window.wd.set.
4
+ image: https://darkmown.com/__wd/media/og-showcase.png
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+ @include /base.wd
10
+
11
+ <main class="wide app-swiper">
12
+
13
+ <!-- No ::: section wrapper: `slide` stays a top-level state key, so the
14
+ declarative dots/readout and the colocated index.js share one `slide`. -->
15
+ :state slide = 0
16
+ :computed human = slide + 1
17
+
18
+ ::: swiper-head
19
+ ::: eyebrow .swiper-eyebrow
20
+ The escape hatch
21
+ :::
22
+
23
+ # A *swiper*, wired to `wd.subscribe`
24
+
25
+ ::: swiper-tagline
26
+ Darkmown owns the reactive `slide` state, the indicator dots, and the live readout —
27
+ declaratively, with no hand-written JavaScript. The things a framework this small
28
+ deliberately *doesn't* ship — arrow-key navigation, pointer **drag / swipe**, and the
29
+ sliding transition — live in a colocated `index.js` that talks to the very same state
30
+ through `window.wd.subscribe` and `window.wd.set`.
31
+ :::
32
+ :::
33
+
34
+ ::: swiper-stage
35
+
36
+ <div class="swiper-viewport" data-swiper tabindex="0" role="group" aria-roledescription="carousel" aria-label="Project gallery — drag, or use the arrow keys">
37
+ <div class="swiper-track" data-swiper-track>
38
+ <figure class="swipe-slide"><img src="/carousel/slide-1.jpg" alt="Warm ember-to-charcoal gradient" width="1200" height="675"><figcaption><strong>Ember</strong><span>Warm dusk · #b8462e</span></figcaption></figure>
39
+ <figure class="swipe-slide"><img src="/carousel/slide-2.jpg" alt="Teal-to-ink gradient" width="1200" height="675"><figcaption><strong>Pulse</strong><span>Signal teal · #18b89a</span></figcaption></figure>
40
+ <figure class="swipe-slide"><img src="/carousel/slide-3.jpg" alt="Violet-to-midnight gradient" width="1200" height="675"><figcaption><strong>Forge</strong><span>Electric violet · #6d5ae6</span></figcaption></figure>
41
+ <figure class="swipe-slide"><img src="/carousel/slide-4.jpg" alt="Amber-to-umber gradient" width="1200" height="675"><figcaption><strong>Compass</strong><span>Trail amber · #e0894a</span></figcaption></figure>
42
+ <figure class="swipe-slide"><img src="/carousel/slide-5.jpg" alt="Gold-to-olive gradient" width="1200" height="675"><figcaption><strong>Ledger</strong><span>Ledger gold · #c79a3a</span></figcaption></figure>
43
+ </div>
44
+ </div>
45
+
46
+ ::: swiper-controls
47
+ <button class="swiper-arrow" data-swiper-prev type="button" aria-label="Previous slide">‹</button>
48
+
49
+ ::: swiper-dots
50
+ ::: dot .active when slide == 0
51
+ :button "1" -> slide = 0
52
+ :::
53
+ ::: dot .active when slide == 1
54
+ :button "2" -> slide = 1
55
+ :::
56
+ ::: dot .active when slide == 2
57
+ :button "3" -> slide = 2
58
+ :::
59
+ ::: dot .active when slide == 3
60
+ :button "4" -> slide = 3
61
+ :::
62
+ ::: dot .active when slide == 4
63
+ :button "5" -> slide = 4
64
+ :::
65
+ :::
66
+
67
+ <button class="swiper-arrow" data-swiper-next type="button" aria-label="Next slide">›</button>
68
+ :::
69
+
70
+ ::: swiper-readout
71
+ Slide **{ human }** of 5 — drag it, press <kbd>←</kbd> / <kbd>→</kbd>, or tap a dot.
72
+ :::
73
+
74
+ :::
75
+
76
+ <details class="source-reveal">
77
+ <summary>View source — declarative core + a colocated behavior</summary>
78
+
79
+ ```wd
80
+ :state slide = 0
81
+ :computed human = slide + 1
82
+
83
+ # the dots are declarative — Darkmown reconciles `.active` for you
84
+ ::: dot .active when slide == 0
85
+ :button "1" -> slide = 0
86
+ :::
87
+
88
+ # … the readout, too
89
+ Slide { human } of 5
90
+ ```
91
+
92
+ ```js
93
+ // index.js, colocated next to the page. Loads after the runtime, so window.wd
94
+ // is ready. Drag / keyboard / transition — the gestural layer the framework
95
+ // deliberately leaves to you — all driven through the same `slide` state.
96
+ window.wd.subscribe("slide", (i) => {
97
+ track.style.transform = `translateX(${-i * 100}%)`;
98
+ });
99
+ prevButton.onclick = () => window.wd.set("slide", current - 1);
100
+ ```
101
+
102
+ The framework reconciles state, classes, and text; the escape hatch owns the
103
+ gestures. They meet at one shared `slide` value.
104
+
105
+ </details>
106
+
107
+ </main>
108
+
109
+ @include /footer.wd
Binary file
Binary file
Binary file
Binary file
Binary file