@spunto/design-system 0.15.1 → 0.17.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/README.md CHANGED
@@ -46,6 +46,9 @@ const nextConfig = { transpilePackages: ["@spunto/design-system"] }
46
46
  → Tailwind color/radius mapping, a minimal base layer, and the `dot-grid` /
47
47
  `grid-lines` background utilities. Brand color is **flame** (`--primary` ≈ `#ea5400`);
48
48
  radius base is `0.25rem`; `Build`/`Ship`/`Run` are semantic (`bg-build`, `text-run`…).
49
+ Plus the two **unthemed** marketing surfaces — `--night` (`bg-night`) and
50
+ `--code-surface` (`bg-code-surface`) — and the `night-grid` / `night-glow-a`
51
+ / `night-glow-b` / `night-scroll` utilities they come with.
49
52
  - **`cn`** — `clsx` + `tailwind-merge`.
50
53
  - **`./colors`** — `cssVar`, `chartColors`, `chartRamp`, `segColors` for JS/chart contexts.
51
54
  - **Primitives** — form + layout building blocks styled on `@base-ui/react`:
@@ -60,9 +63,20 @@ const nextConfig = { transpilePackages: ["@spunto/design-system"] }
60
63
  `Alert` (+`AlertTitle`/`AlertDescription`, +`alertVariants`) — an **inline, persistent,
61
64
  declarative** status banner, the counterpart to the ephemeral imperative `toast()`.
62
65
  - _Overlays_ (plug into the `SpuntoProvider` umbrella) — `Dialog`, `AlertDialog`
63
- (a confirmation variant, non-dismissible), `Tooltip`. Their portals render into
64
- the provider's overlay container; `Tooltip`'s shared delay group is mounted by the
65
- provider too.
66
+ (a confirmation variant, non-dismissible), `Sheet`, `Tooltip`. Their portals render
67
+ into the provider's overlay container; `Tooltip`'s shared delay group is mounted by
68
+ the provider too.
69
+ - _Sheet_ — `Sheet` (+`SheetTrigger`/`SheetClose`/`SheetContent`/`SheetHeader`/
70
+ `SheetFooter`/`SheetTitle`/`SheetDescription`), the same Base UI dialog entering from
71
+ an edge. `side` (4) decides the edge, `size` (`sm`…`full`) how far it comes in — two
72
+ props rather than one bundle of `data-[side=right]:`-prefixed classes, precisely so
73
+ `className` stays the layout escape hatch (an app widens a panel or lays it out in two
74
+ columns without `!important`).
75
+ - _Table_ — `Table` (+`TableHeader`/`TableBody`/`TableFooter`/`TableRow`/`TableHead`/
76
+ `TableCell`/`TableCaption`). **Presentational only**: no sorting, no pagination, no
77
+ column model — apps already own that logic and disagree on it; what they were
78
+ duplicating is the markup and the token choices. `containerClassName` reaches the
79
+ scroll container (max-height, border, sticky-header context).
66
80
  - _CommandPalette_ — `CommandPalette` (+`CommandPaletteInput`/`List`/`Group`/`Item`/
67
81
  `Empty`/`Loading`/`Separator`/`Footer`/`Trigger`), the ⌘K palette: an input, groups,
68
82
  items, and a jump. **Domain-free**, hence its place in the root entry — it knows
@@ -77,6 +91,17 @@ const nextConfig = { transpilePackages: ["@spunto/design-system"] }
77
91
  `value`/`onValueChange` + `filter={null}`. Navigation is `href` + `render.link`, never
78
92
  `next/link` — same rule as `WorkerCard`. Not virtualized: a few hundred items stay
79
93
  fluid (a non-matching item renders nothing), beyond that filter server-side.
94
+ - _CommandMenu_ — `CommandMenu` (+`CommandMenuInput`/`List`/`Group`/`Item`/`Empty`/
95
+ `Loading`/`Separator`), the palette's vocabulary **inline**: a Base UI `Popover`
96
+ anchored to an element or a **virtual element** (a caret rect), which never moves
97
+ focus (`initialFocus`/`finalFocus` off, no backdrop, non-modal) and claims ↑ ↓ ↵ Esc
98
+ on `document` in the **capture** phase, before the field underneath. That's what a
99
+ "/" menu inside a block editor needs and what the dialog-based palette can't do
100
+ without losing the caret — hence a separate component rather than a `modal={false}`
101
+ flag: the contract is the opposite one. Enter is only claimed when an item is
102
+ highlighted, so a query with no result still inserts a line break. The query either
103
+ comes from the caller (`query`, the "/" case) or from a `CommandMenuInput` rendered
104
+ inside the popup (a filter dropdown on a button; `autoFocus` is opt-in).
80
105
  - _Terminal_ — `Terminal` (+`TerminalHandle`, `TerminalOptions`), a transport-agnostic
81
106
  xterm.js surface: mount, shared dark ANSI theme, fit-on-resize. No opinion on where
82
107
  bytes come from (WebSocket, SSE, a static string) — feed it via the `write`/`writeln`
@@ -112,11 +137,16 @@ import { Button, Card } from "@spunto/design-system"
112
137
  import { WorkerCard } from "@spunto/design-system/workers" // domain
113
138
  import { ImageCard, FeatureCard, ExtensionCard } from "@spunto/design-system/devcontainer" // domain
114
139
  import { ProjectForm, ProjectPanel } from "@spunto/design-system/projects" // domain
140
+ import { Section, ProductHeader } from "@spunto/design-system/marketing" // not a domain — see below
115
141
  ```
116
142
 
117
143
  One entry **per domain**, not per component: a project's form and a project's
118
144
  panel are the same concept seen twice, so they share `/projects`.
119
145
 
146
+ `/marketing` is the exception that proves the rule: it knows no Spunto concept
147
+ (that's exactly why the product registry stayed in the site), but it carries
148
+ opinions a dashboard doesn't want, so it gets an entry too.
149
+
120
150
  ### `@spunto/design-system/workers`
121
151
 
122
152
  - **`WorkerCard`** — a Spunto worker as a card: state, author, node, setup
@@ -264,6 +294,72 @@ And the read-only twin of that form:
264
294
  - **Container queries** here too: the panel is 288 px in a sidebar on a 27" screen
265
295
  and full width on a phone — and the desktop case is the narrow one.
266
296
 
297
+ ## Marketing — `/marketing`
298
+
299
+ ```tsx
300
+ import { Section, H2, Kicker, SpecList, Disclosure, CodeBlock, Figure,
301
+ CopyCommand, NightBand, NightChip, ProductHeader, Reveal,
302
+ usePrefersReducedMotion, useLoopClock } from "@spunto/design-system/marketing"
303
+ ```
304
+
305
+ The vocabulary of a **marketing page** — a numbered chapter, a display headline,
306
+ a mono kicker, key/value rows, a disclosure row, a copyable command, a night
307
+ band. Its own entry rather than the root, for two reasons: an app that draws a
308
+ dashboard has no use for it, and these primitives carry opinions the root
309
+ deliberately doesn't (a display face, a fixed dark surface).
310
+
311
+ - **`Section`** — the chapter: hairline, number in the accent, mono title,
312
+ margin `note`, then the content. Repeating one opening down a page is what
313
+ makes it read as a document rather than a stack of blocks.
314
+ - **`H2`** / **`Kicker`** — the display headline (Syne, capped at 24ch) and the
315
+ mono line above it. **One `Kicker`, not two**: the site had `Kicker` and
316
+ `Eyebrow` doing the same job under two names and two letter-spacings; the wider
317
+ one is now a `className`.
318
+ - **`SpecList`** — key/value rows, the default way to list facts without bullets.
319
+ - **`Disclosure`** — the row you open. `<details name>` gives the exclusive
320
+ accordion natively: no state, no hydration surface, and it works before the JS
321
+ lands.
322
+ - **`CodeBlock`** / **`Figure`** — a snippet on the `--code-surface` token
323
+ (deliberately unhighlighted: a highlighter is a dependency and ~30 kB for a
324
+ six-line snippet), and a framed, captioned screenshot whose **image element is
325
+ a slot** (`render.image`) — same rule as links, the package never imports
326
+ `next/image`.
327
+ - **`CopyCommand`** — the call to action when it's a command. **One component,
328
+ two registers** (`tone="day" | "night"`), where the site had `CopyCommand` and
329
+ `NightCommand`: same behaviour, two files, two places to fix a bug. What lands
330
+ in the clipboard is the joined-up one-liner, not the wrapped version.
331
+ - **`NightBand`** / **`NightChip`** — the dark band a page opens on, lit by its
332
+ `glow` accent. `--night` is *not themed*: it's the dark moment of the page, so
333
+ it stays deeper than the dark theme's own background and identical in both.
334
+ The drifting light sources are CSS animations (stopped by
335
+ `prefers-reduced-motion`), never a rAF loop.
336
+ - **`ProductHeader`** — mark, name, kind, headline, promise, the one command, the
337
+ chips. **Generalised on the way in**: the site's version took a `Product` out
338
+ of its own registry, this one knows about no product at all — everything
339
+ product-specific goes through `actions`.
340
+ - **`Reveal`** + **`usePrefersReducedMotion`** / **`useLoopClock`** — scroll
341
+ reveal as an `IntersectionObserver` and a CSS transition, and the "how far into
342
+ the loop are we" clock that pauses off screen and commits at ~14 Hz.
343
+
344
+ Two rules the package enforces here:
345
+
346
+ - **No framework imports.** No `next/link`, no `next/image`. Navigation is the
347
+ caller's (`actions` slots), and `Figure` takes an image `render` slot — the
348
+ same motif as `CommandPaletteLinkRender` and `WorkerCard`'s `render.link`.
349
+ - **No motion library.** `framer-motion` as a hard dependency would weigh on
350
+ every consumer, including the ones that never draw a landing page. `Reveal` is
351
+ ~20 lines of observer + transition instead.
352
+
353
+ `Section`, `H2`, `Kicker`, `SpecList`, `Disclosure`, `CodeBlock`, `Figure`,
354
+ `NightBand`, `NightChip` and `ProductHeader` carry **no `"use client"`** — they
355
+ render from a React Server Component. Only `CopyCommand`, `Reveal` and the hooks
356
+ are client files. (Same split, same care, as `buttonVariants`/`alertVariants`.)
357
+
358
+ **What stays in the site, on purpose:** the product registry and anything reading
359
+ it, the navs and footers (they hard-code URLs and a session), the product logos
360
+ and OG cards. The rule: *if it needs to know a product, a URL or a session, it's
361
+ the site; if it only lays out or types, it's the system.*
362
+
267
363
  ## Toasts — `SpuntoProvider` + `toast()`
268
364
 
269
365
  `SpuntoProvider` is the design system's client umbrella provider. Mount it once
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spunto/design-system",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "Spunto's shared design system — warm/flame tokens, color constants, and UI primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,10 @@
45
45
  "types": "./src/components/projects/index.ts",
46
46
  "import": "./src/components/projects/index.ts"
47
47
  },
48
+ "./marketing": {
49
+ "types": "./src/components/marketing/index.ts",
50
+ "import": "./src/components/marketing/index.ts"
51
+ },
48
52
  "./fonts": {
49
53
  "types": "./src/components/fonts.tsx",
50
54
  "import": "./src/components/fonts.tsx"