@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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +115 -5
- package/docs/spec-alignment.md +16 -3
- package/package.json +3 -3
- package/site/_/base.skin +372 -0
- package/site/_/base.wd +4 -0
- package/site/_/footer.wd +14 -0
- package/site/_/metrics.json +13 -0
- package/site/_/nav.skin +35 -12
- package/site/_/nav.wd +8 -10
- package/site/_/products.json +10 -0
- package/site/pages/app.wd +1 -1
- package/site/pages/carousel/index.js +82 -0
- package/site/pages/carousel/index.skin +185 -0
- package/site/pages/carousel/index.wd +109 -0
- package/site/pages/carousel/slide-1.jpg +0 -0
- package/site/pages/carousel/slide-2.jpg +0 -0
- package/site/pages/carousel/slide-3.jpg +0 -0
- package/site/pages/carousel/slide-4.jpg +0 -0
- package/site/pages/carousel/slide-5.jpg +0 -0
- package/site/pages/compass/index.skin +247 -0
- package/site/pages/compass/index.wd +194 -0
- package/site/pages/docs/index.skin +48 -28
- package/site/pages/docs/index.wd +71 -5
- package/site/pages/folio/-style.skin +375 -0
- package/site/pages/folio/-style.wd +6 -0
- package/site/pages/folio/cart.skin +309 -0
- package/site/pages/folio/cart.wd +201 -0
- package/site/pages/folio/index.skin +24 -0
- package/site/pages/folio/index.wd +167 -0
- package/site/pages/forge/index.skin +676 -0
- package/site/pages/forge/index.wd +330 -0
- package/site/pages/index.skin +135 -144
- package/site/pages/index.wd +17 -5
- package/site/pages/ledger/index.skin +257 -0
- package/site/pages/ledger/index.wd +174 -0
- package/site/pages/media/clip-poster.jpg +0 -0
- package/site/pages/media/clip.mp4 +0 -0
- package/site/pages/media/index.skin +82 -0
- package/site/pages/media/index.wd +84 -0
- package/site/pages/media/tone.mp3 +0 -0
- package/site/pages/pulse/index.skin +434 -0
- package/site/pages/pulse/index.wd +178 -0
- package/site/pages/showcase/index.skin +130 -0
- package/site/pages/showcase/index.wd +168 -0
- package/src/compiler/body.js +26 -1
- package/src/compiler/context.js +1 -1
- package/src/compiler/directives.js +199 -3
- package/src/compiler/format.js +244 -0
- package/src/compiler/interpolation.js +35 -1
- package/src/compiler/loops.js +95 -24
- package/src/compiler/markdown.js +16 -7
- package/src/compiler/predicates.js +35 -2
- package/src/headers.js +5 -0
- package/src/runtime.js +168 -14
- 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. -->
|
package/site/_/footer.wd
ADDED
|
@@ -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 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 .
|
|
23
|
+
gap .35rem 1.1rem
|
|
16
24
|
align-items center
|
|
17
|
-
padding
|
|
25
|
+
padding .85rem 1.5rem
|
|
18
26
|
max-width 100%
|
|
19
27
|
box-sizing border-box
|
|
20
28
|
border-bottom 1px solid $line
|
|
21
|
-
|
|
22
|
-
backdrop-filter blur(
|
|
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
|
|
33
|
+
z-index 20
|
|
34
|
+
font-size .92rem
|
|
26
35
|
|
|
27
36
|
@media (max-width: 600px)
|
|
28
37
|
.topnav
|
|
29
|
-
padding .
|
|
30
|
-
gap .
|
|
38
|
+
padding .75rem 1rem
|
|
39
|
+
gap .3rem .8rem
|
|
31
40
|
|
|
32
|
-
.topnav
|
|
41
|
+
.topnav .brand
|
|
42
|
+
font-weight 700
|
|
43
|
+
font-size 1.08rem
|
|
44
|
+
letter-spacing -.02em
|
|
33
45
|
color $warm
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<a href="/
|
|
10
|
-
<a href="/data/">Data & 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 ~
|
|
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 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
|