@spunto/design-system 0.5.0 → 0.7.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
@@ -51,6 +51,10 @@ const nextConfig = { transpilePackages: ["@spunto/design-system"] }
51
51
  (a confirmation variant, non-dismissible), `Tooltip`. Their portals render into
52
52
  the provider's overlay container; `Tooltip`'s shared delay group is mounted by the
53
53
  provider too.
54
+ - _Terminal_ — `Terminal` (+`TerminalHandle`, `TerminalOptions`), a transport-agnostic
55
+ xterm.js surface: mount, shared dark ANSI theme, fit-on-resize. No opinion on where
56
+ bytes come from (WebSocket, SSE, a static string) — feed it via the `write`/`writeln`
57
+ ref handle. Opt-in addons through `options` (currently `webLinks`, clickable URLs).
54
58
  - **`SpuntoProvider` + `toast()`** — the imperative toast system (see below). The
55
59
  provider is also the umbrella that mounts the tooltip delay group and the overlay
56
60
  portal container the dialogs/selects render into.
@@ -104,6 +108,26 @@ toast.dismiss(id)
104
108
  `toastPosition` (default `"top-right"`), `toastDuration` (default `5000`, `0` =
105
109
  persistent) and `toastLimit` (default `3`).
106
110
 
111
+ ## LLM / agent docs (`llms.txt`)
112
+
113
+ The [showcase](showcase/) also exposes its documentation as **plain markdown**,
114
+ so coding agents can consume the component APIs directly. Everything is generated
115
+ from the same content that drives the visual pages
116
+ (`showcase/src/content/components.ts`) — there is no second source to keep in sync.
117
+
118
+ Served both in dev (Vite middleware) and in the built site (static files) at
119
+ `https://design.spunto.net`:
120
+
121
+ - **`/llms.txt`** — index (llmstxt.org convention) linking every page.
122
+ - **`/<component>.md`** — one page per component with import, example and the full
123
+ props API (e.g. `/button.md`, `/select.md`).
124
+ - **`/colors.md`**, `/typography.md`, `/radius.md` — the foundations/tokens.
125
+ - **`/llms-full.txt`** — every page concatenated into a single file.
126
+
127
+ Adding a component = add its entry to `content/components.ts` + a demo file in
128
+ `showcase/src/demos/`; the visual page and its `.md` are then produced
129
+ automatically (see `showcase/vite-plugin-llms.ts`).
130
+
107
131
  ## Peer requirements
108
132
 
109
133
  - React 19+, and a **Tailwind CSS v4** pipeline in the consuming app.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spunto/design-system",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",
@@ -33,6 +33,10 @@
33
33
  "types": "./src/colors.ts",
34
34
  "import": "./src/colors.ts"
35
35
  },
36
+ "./fonts": {
37
+ "types": "./src/components/fonts.tsx",
38
+ "import": "./src/components/fonts.tsx"
39
+ },
36
40
  "./styles.css": "./styles.css"
37
41
  },
38
42
  "files": [
@@ -45,6 +49,7 @@
45
49
  },
46
50
  "dependencies": {
47
51
  "@xterm/addon-fit": "^0.11.0",
52
+ "@xterm/addon-web-links": "^0.12.0",
48
53
  "@xterm/xterm": "^6.0.0",
49
54
  "class-variance-authority": "^0.7.1",
50
55
  "clsx": "^2.1.1",
@@ -0,0 +1,39 @@
1
+ // Font preloads for the design-system typefaces (Geist / Syne / JetBrains Mono).
2
+ //
3
+ // Renders the resource hints that let the browser fetch the fonts early instead
4
+ // of discovering them mid-render through the `@font-face` rules in `styles.css`.
5
+ // The preload URLs MUST match the `src` in those `@font-face` blocks exactly
6
+ // (same gstatic .woff2), otherwise the browser downloads each file twice.
7
+ //
8
+ // We only preload the *latin* subset of each family — the one every Spunto
9
+ // surface actually renders. The other subsets stay lazy (fetched on demand via
10
+ // `unicode-range`), so we don't waste bandwidth preloading Cyrillic/Greek/etc.
11
+ //
12
+ // Under Next 16 / React 19 these <link> elements are hoisted into <head>
13
+ // automatically, so `<FontPreloads/>` can live anywhere in the tree (e.g. the
14
+ // root layout body). In a plain React app, place it high in the tree.
15
+
16
+ // Latin-subset .woff2 for each family — identical to the latin `src` in styles.css.
17
+ const LATIN_WOFF2 = [
18
+ "https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwcGFU.woff2",
19
+ "https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbV2o-flEEny0FZhsfKu5WU4xD7OwE.woff2",
20
+ "https://fonts.gstatic.com/s/syne/v24/8vIH7w4qzmVxm2BL9A.woff2",
21
+ ] as const
22
+
23
+ export function FontPreloads() {
24
+ return (
25
+ <>
26
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
27
+ {LATIN_WOFF2.map((href) => (
28
+ <link
29
+ key={href}
30
+ rel="preload"
31
+ as="font"
32
+ type="font/woff2"
33
+ href={href}
34
+ crossOrigin="anonymous"
35
+ />
36
+ ))}
37
+ </>
38
+ )
39
+ }
@@ -42,6 +42,11 @@ export interface TerminalHandle {
42
42
  focus: () => void
43
43
  }
44
44
 
45
+ export interface TerminalOptions {
46
+ /** Turns URLs in terminal output into clickable links (`@xterm/addon-web-links`). */
47
+ webLinks?: boolean
48
+ }
49
+
45
50
  export interface TerminalProps {
46
51
  className?: string
47
52
  fontSize?: number
@@ -52,6 +57,12 @@ export interface TerminalProps {
52
57
  * common case this primitive was built for.
53
58
  */
54
59
  onData?: (data: string) => void
60
+ /**
61
+ * Opt-in xterm addons — kept out of the base bundle since most consumers
62
+ * (plain log output) don't need them. Read once at mount, like the other
63
+ * props here.
64
+ */
65
+ options?: TerminalOptions
55
66
  }
56
67
 
57
68
  /**
@@ -61,18 +72,29 @@ export interface TerminalProps {
61
72
  * plug in on top.
62
73
  */
63
74
  const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Terminal(
64
- { className, fontSize = 13, scrollback = 5000, onData },
75
+ { className, fontSize = 13, scrollback = 5000, onData, options },
65
76
  ref
66
77
  ) {
67
78
  const outerRef = useRef<HTMLDivElement>(null)
68
79
  const mountRef = useRef<HTMLDivElement>(null)
69
80
  const termRef = useRef<XTerm | null>(null)
81
+ // xterm mounts asynchronously (dynamic import); a consumer wiring up a
82
+ // transport (WebSocket, SSE...) in its own effect can easily win that race
83
+ // and call write() before termRef exists. Queue those calls instead of
84
+ // dropping them, and flush in order once xterm is ready.
85
+ const pendingRef = useRef<Array<{ method: "write" | "writeln"; data: string | Uint8Array }>>([])
70
86
 
71
87
  useImperativeHandle(
72
88
  ref,
73
89
  () => ({
74
- write: (data) => termRef.current?.write(data),
75
- writeln: (data) => termRef.current?.writeln(data),
90
+ write: (data) => {
91
+ if (termRef.current) termRef.current.write(data)
92
+ else pendingRef.current.push({ method: "write", data })
93
+ },
94
+ writeln: (data) => {
95
+ if (termRef.current) termRef.current.writeln(data)
96
+ else pendingRef.current.push({ method: "writeln", data })
97
+ },
76
98
  clear: () => termRef.current?.clear(),
77
99
  focus: () => termRef.current?.focus(),
78
100
  }),
@@ -103,6 +125,10 @@ const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Terminal(
103
125
 
104
126
  const fitAddon = new FitAddon()
105
127
  term.loadAddon(fitAddon)
128
+ if (options?.webLinks) {
129
+ const { WebLinksAddon } = await import("@xterm/addon-web-links")
130
+ term.loadAddon(new WebLinksAddon())
131
+ }
106
132
  // FitAddon computes available space from *this* element's parent minus
107
133
  // this element's own padding — it has no idea about an outer wrapper's
108
134
  // padding/border. Mounting on a padding-less, border-less inner div (see
@@ -113,6 +139,12 @@ const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Terminal(
113
139
  fitAddon.fit()
114
140
  termRef.current = term
115
141
 
142
+ for (const { method, data } of pendingRef.current) {
143
+ if (method === "writeln") term.writeln(data as string)
144
+ else term.write(data)
145
+ }
146
+ pendingRef.current = []
147
+
116
148
  if (onData) term.onData(onData)
117
149
 
118
150
  resizeObserver = new ResizeObserver(() => fitAddon.fit())
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ export { Avatar, AvatarImage, AvatarFallback } from "./components/avatar"
24
24
  // xterm.js surface — theme + fit-on-resize, transport-agnostic (feed it via
25
25
  // the write/writeln ref handle).
26
26
  export { Terminal } from "./components/terminal"
27
- export type { TerminalHandle, TerminalProps } from "./components/terminal"
27
+ export type { TerminalHandle, TerminalProps, TerminalOptions } from "./components/terminal"
28
28
 
29
29
  // Inline, declarative status banner (persistent) — the counterpart to `toast()`.
30
30
  // `alertVariants` ships from this directive-free module so it stays RSC-callable.
package/styles.css CHANGED
@@ -10,12 +10,151 @@
10
10
  * `@custom-variant dark` stays in the app (or add it there once).
11
11
  */
12
12
 
13
+ /* ─── Fonts — carried by the design system ──────────────────────────────────
14
+ *
15
+ * Canonical typography (single source of truth, decided 2026-07-23):
16
+ * text = Geist · display = Syne · mono = JetBrains Mono
17
+ *
18
+ * `src` points straight at the versioned .woff2 files on fonts.gstatic.com
19
+ * (absolute → identical everywhere, never hashed). URLs were harvested once from
20
+ * `fonts.googleapis.com/css2?family=…` with a modern User-Agent; we deliberately
21
+ * do NOT link the css2 stylesheet (render-blocking + UA-sniffed). Every subset
22
+ * gstatic serves is declared here — the browser only fetches the ones a page
23
+ * actually uses (via `unicode-range`). `<FontPreloads/>` (`@spunto/design-system/fonts`)
24
+ * preloads the latin subset of each family with matching URLs. `font-display: swap`.
25
+ *
26
+ * Follow-up cards: self-host on design.spunto.net (RGPD), anti-CLS fallback
27
+ * metrics (size-adjust / ascent-override via Fontaine). */
28
+
29
+ /* Geist — variable weight (text). */
30
+ @font-face {
31
+ font-family: "Geist";
32
+ font-style: normal;
33
+ font-weight: 100 900;
34
+ font-display: swap;
35
+ src: url(https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwRGFWfOw.woff2) format("woff2");
36
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
37
+ }
38
+ @font-face {
39
+ font-family: "Geist";
40
+ font-style: normal;
41
+ font-weight: 100 900;
42
+ font-display: swap;
43
+ src: url(https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwYGFWfOw.woff2) format("woff2");
44
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
45
+ }
46
+ @font-face {
47
+ font-family: "Geist";
48
+ font-style: normal;
49
+ font-weight: 100 900;
50
+ font-display: swap;
51
+ src: url(https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwTGFWfOw.woff2) format("woff2");
52
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
53
+ }
54
+ @font-face {
55
+ font-family: "Geist";
56
+ font-style: normal;
57
+ font-weight: 100 900;
58
+ font-display: swap;
59
+ src: url(https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwSGFWfOw.woff2) format("woff2");
60
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
61
+ }
62
+ @font-face {
63
+ font-family: "Geist";
64
+ font-style: normal;
65
+ font-weight: 100 900;
66
+ font-display: swap;
67
+ src: url(https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwcGFU.woff2) format("woff2");
68
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
69
+ }
70
+
71
+ /* JetBrains Mono — variable weight (mono). */
72
+ @font-face {
73
+ font-family: "JetBrains Mono";
74
+ font-style: normal;
75
+ font-weight: 100 800;
76
+ font-display: swap;
77
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbV2o-flEEny0FZhsfKu5WU4xD2OwG_TA.woff2) format("woff2");
78
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
79
+ }
80
+ @font-face {
81
+ font-family: "JetBrains Mono";
82
+ font-style: normal;
83
+ font-weight: 100 800;
84
+ font-display: swap;
85
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbV2o-flEEny0FZhsfKu5WU4xD_OwG_TA.woff2) format("woff2");
86
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
87
+ }
88
+ @font-face {
89
+ font-family: "JetBrains Mono";
90
+ font-style: normal;
91
+ font-weight: 100 800;
92
+ font-display: swap;
93
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbV2o-flEEny0FZhsfKu5WU4xD4OwG_TA.woff2) format("woff2");
94
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
95
+ }
96
+ @font-face {
97
+ font-family: "JetBrains Mono";
98
+ font-style: normal;
99
+ font-weight: 100 800;
100
+ font-display: swap;
101
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbV2o-flEEny0FZhsfKu5WU4xD0OwG_TA.woff2) format("woff2");
102
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
103
+ }
104
+ @font-face {
105
+ font-family: "JetBrains Mono";
106
+ font-style: normal;
107
+ font-weight: 100 800;
108
+ font-display: swap;
109
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbV2o-flEEny0FZhsfKu5WU4xD1OwG_TA.woff2) format("woff2");
110
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
111
+ }
112
+ @font-face {
113
+ font-family: "JetBrains Mono";
114
+ font-style: normal;
115
+ font-weight: 100 800;
116
+ font-display: swap;
117
+ src: url(https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbV2o-flEEny0FZhsfKu5WU4xD7OwE.woff2) format("woff2");
118
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
119
+ }
120
+
121
+ /* Syne — variable weight (display, 400–800). */
122
+ @font-face {
123
+ font-family: "Syne";
124
+ font-style: normal;
125
+ font-weight: 400 800;
126
+ font-display: swap;
127
+ src: url(https://fonts.gstatic.com/s/syne/v24/8vIH7w4qzmVxm2NL9Hz_.woff2) format("woff2");
128
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
129
+ }
130
+ @font-face {
131
+ font-family: "Syne";
132
+ font-style: normal;
133
+ font-weight: 400 800;
134
+ font-display: swap;
135
+ src: url(https://fonts.gstatic.com/s/syne/v24/8vIH7w4qzmVxm25L9Hz_.woff2) format("woff2");
136
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
137
+ }
138
+ @font-face {
139
+ font-family: "Syne";
140
+ font-style: normal;
141
+ font-weight: 400 800;
142
+ font-display: swap;
143
+ src: url(https://fonts.gstatic.com/s/syne/v24/8vIH7w4qzmVxm2BL9A.woff2) format("woff2");
144
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
145
+ }
146
+
147
+ /* Font families — real Tailwind tokens (utilities `font-sans` / `font-mono` /
148
+ * `font-display`), defined directly by the DS. No brand-specific indirection. */
149
+ @theme {
150
+ --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
151
+ --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
152
+ --font-display: "Syne", var(--font-sans);
153
+ }
154
+
13
155
  @theme inline {
14
156
  --color-background: var(--background);
15
157
  --color-foreground: var(--foreground);
16
- --font-sans: var(--font-geist-sans);
17
- --font-mono: var(--font-geist-mono);
18
- --font-syne: var(--font-syne);
19
158
  --color-sidebar-ring: var(--sidebar-ring);
20
159
  --color-sidebar-border: var(--sidebar-border);
21
160
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);