@archetypeai/ds-cli 0.15.0 → 0.16.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 +1 -1
- package/commands/create.js +13 -1
- package/commands/init.js +13 -1
- package/files/AGENTS.md +25 -2
- package/files/CLAUDE.md +25 -2
- package/files/ds-manifest.json +62 -7
- package/lib/add-ds-lib-tokens.js +9 -1
- package/lib/add-ds-ui-svelte.js +9 -1
- package/lib/install-fonts-local.js +54 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ The configuration is two files: the instruction file (`CLAUDE.md`/`AGENTS.md`
|
|
|
138
138
|
|
|
139
139
|
## Fonts
|
|
140
140
|
|
|
141
|
-
The `--fonts <path>` flag expects a local folder containing all
|
|
141
|
+
The `--fonts <path>` flag expects a local folder containing all 13 PP Neue Montreal `.woff2` files (8 Sans + 5 Mono). The CLI validates the folder and copies the files to `static/fonts/`, where the `@font-face` declarations in `@archetypeai/ds-lib-tokens/fonts.css` expect them. With `--no-fonts`, the theme falls back to system fonts.
|
|
142
142
|
|
|
143
143
|
## Registries
|
|
144
144
|
|
package/commands/create.js
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
createDemoPage,
|
|
13
13
|
installAgentConfig
|
|
14
14
|
} from '../lib/scaffold-ds-svelte-project.js';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
validateFontsPath,
|
|
17
|
+
installLocalFonts,
|
|
18
|
+
preloadLinkTags
|
|
19
|
+
} from '../lib/install-fonts-local.js';
|
|
16
20
|
import { isInteractive, DEFAULTS } from '../lib/is-interactive.js';
|
|
17
21
|
|
|
18
22
|
// parse flags
|
|
@@ -250,6 +254,14 @@ export async function create(args) {
|
|
|
250
254
|
const fontResult = installLocalFonts(fontsPath, projectPath);
|
|
251
255
|
if (fontResult.success) {
|
|
252
256
|
s.stop(`${fontResult.fileCount} font files installed`);
|
|
257
|
+
if (fontResult.preload.patched) {
|
|
258
|
+
p.log.info('Preloading Book, Medium and Mono Book in src/app.html');
|
|
259
|
+
} else {
|
|
260
|
+
p.log.warn(
|
|
261
|
+
`Could not add font preloads (${fontResult.preload.reason}). Add these to your <head>:\n` +
|
|
262
|
+
preloadLinkTags().join('\n')
|
|
263
|
+
);
|
|
264
|
+
}
|
|
253
265
|
} else {
|
|
254
266
|
s.stop('Failed to install fonts');
|
|
255
267
|
p.log.warn(fontResult.error);
|
package/commands/init.js
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
installLinting,
|
|
12
12
|
installAgentConfig
|
|
13
13
|
} from '../lib/scaffold-ds-svelte-project.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
validateFontsPath,
|
|
16
|
+
installLocalFonts,
|
|
17
|
+
preloadLinkTags
|
|
18
|
+
} from '../lib/install-fonts-local.js';
|
|
15
19
|
import { isInteractive, DEFAULTS } from '../lib/is-interactive.js';
|
|
16
20
|
|
|
17
21
|
// parse flags
|
|
@@ -210,6 +214,14 @@ export async function init(args) {
|
|
|
210
214
|
const fontResult = installLocalFonts(fontsPath, projectPath);
|
|
211
215
|
if (fontResult.success) {
|
|
212
216
|
s.stop(`${fontResult.fileCount} font files installed`);
|
|
217
|
+
if (fontResult.preload.patched) {
|
|
218
|
+
p.log.info('Preloading Book, Medium and Mono Book in src/app.html');
|
|
219
|
+
} else {
|
|
220
|
+
p.log.warn(
|
|
221
|
+
`Could not add font preloads (${fontResult.preload.reason}). Add these to your <head>:\n` +
|
|
222
|
+
preloadLinkTags().join('\n')
|
|
223
|
+
);
|
|
224
|
+
}
|
|
213
225
|
} else {
|
|
214
226
|
s.stop('Failed to install fonts');
|
|
215
227
|
p.log.warn(fontResult.error);
|
package/files/AGENTS.md
CHANGED
|
@@ -81,6 +81,25 @@ use them decoratively.
|
|
|
81
81
|
|
|
82
82
|
## Typography
|
|
83
83
|
|
|
84
|
+
The theme pairs every `text-*` step with its own line-height and letter-spacing
|
|
85
|
+
(`--text-sm--line-height`, `--text-sm--letter-spacing`, …), so `text-sm` alone
|
|
86
|
+
is on-brand. Never add `leading-*` or `tracking-*` next to a `text-*` utility —
|
|
87
|
+
the ramp already supplies both. `text-2xs` (10px) is the floor for dense
|
|
88
|
+
metadata; never use arbitrary sizes like `text-[11px]`.
|
|
89
|
+
|
|
90
|
+
The theme's base layer styles only authored-content elements: `body` (14px
|
|
91
|
+
sans), `h1`–`h6` (30px → 14px, `h4`–`h6` muted, `h1` title-cased), `p`,
|
|
92
|
+
`small`, `strong`, `blockquote`, plus `cursor-pointer` on `a`. It deliberately
|
|
93
|
+
does **not** style `span`, `label`, `em`, `code` or `pre` — and gives `a` no
|
|
94
|
+
typography either — they inherit from their parent, so set the size once on the
|
|
95
|
+
parent instead of on every inline child (`code`/`pre` get the mono family from
|
|
96
|
+
Tailwind preflight and stay the same size as their text). Bare links are
|
|
97
|
+
visually flat until you style them, but every `a` shows the pointer cursor even
|
|
98
|
+
without an `href`; use the `Label` primitive for labels.
|
|
99
|
+
`h1` is `capitalize` by default (use `normal-case` for ids or sentence-case
|
|
100
|
+
copy); uppercase belongs to `Label` variants and `typography="mono"`, never to
|
|
101
|
+
a heading.
|
|
102
|
+
|
|
84
103
|
Default to sans for all text. `font-mono` is reserved for exactly two cases:
|
|
85
104
|
|
|
86
105
|
- Primitives that already use it in the design system source of truth (their
|
|
@@ -110,8 +129,9 @@ darkMode.toggle(); // switch
|
|
|
110
129
|
darkMode.current; // reactive read
|
|
111
130
|
```
|
|
112
131
|
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
`CodeBlock` needs nothing here: it highlights with both themes at once (shiki
|
|
133
|
+
emits the dark colours as `--shiki-dark` custom props) and flips them in CSS, so
|
|
134
|
+
theme switches need no `{#key}` wrapper and no re-highlight.
|
|
115
135
|
|
|
116
136
|
## Page shell
|
|
117
137
|
|
|
@@ -173,6 +193,9 @@ Compose project-specific components from package primitives in
|
|
|
173
193
|
`sideBarSelection`, `lensTray*`, `backToDashboard`) exist in the package
|
|
174
194
|
types but are internal to the Archetype AI console product — never use them
|
|
175
195
|
- Keep the `@source` directives and the CSS import order exactly as above
|
|
196
|
+
- Never add global element rules (`span`, `a`, `label`, `p`, `h1`…) in app CSS,
|
|
197
|
+
and never pair `leading-*`/`tracking-*` with a `text-*` utility — the theme's
|
|
198
|
+
type scale and base layer own both; size a parent, not its inline children
|
|
176
199
|
- Never style scrollbars in app code — no `::-webkit-scrollbar` rules, no
|
|
177
200
|
`scrollbar-color`/`scrollbar-width`. The theme sets both, and setting
|
|
178
201
|
`scrollbar-*` locally also silently disables the webkit pseudo-elements on
|
package/files/CLAUDE.md
CHANGED
|
@@ -81,6 +81,25 @@ use them decoratively.
|
|
|
81
81
|
|
|
82
82
|
## Typography
|
|
83
83
|
|
|
84
|
+
The theme pairs every `text-*` step with its own line-height and letter-spacing
|
|
85
|
+
(`--text-sm--line-height`, `--text-sm--letter-spacing`, …), so `text-sm` alone
|
|
86
|
+
is on-brand. Never add `leading-*` or `tracking-*` next to a `text-*` utility —
|
|
87
|
+
the ramp already supplies both. `text-2xs` (10px) is the floor for dense
|
|
88
|
+
metadata; never use arbitrary sizes like `text-[11px]`.
|
|
89
|
+
|
|
90
|
+
The theme's base layer styles only authored-content elements: `body` (14px
|
|
91
|
+
sans), `h1`–`h6` (30px → 14px, `h4`–`h6` muted, `h1` title-cased), `p`,
|
|
92
|
+
`small`, `strong`, `blockquote`, plus `cursor-pointer` on `a`. It deliberately
|
|
93
|
+
does **not** style `span`, `label`, `em`, `code` or `pre` — and gives `a` no
|
|
94
|
+
typography either — they inherit from their parent, so set the size once on the
|
|
95
|
+
parent instead of on every inline child (`code`/`pre` get the mono family from
|
|
96
|
+
Tailwind preflight and stay the same size as their text). Bare links are
|
|
97
|
+
visually flat until you style them, but every `a` shows the pointer cursor even
|
|
98
|
+
without an `href`; use the `Label` primitive for labels.
|
|
99
|
+
`h1` is `capitalize` by default (use `normal-case` for ids or sentence-case
|
|
100
|
+
copy); uppercase belongs to `Label` variants and `typography="mono"`, never to
|
|
101
|
+
a heading.
|
|
102
|
+
|
|
84
103
|
Default to sans for all text. `font-mono` is reserved for exactly two cases:
|
|
85
104
|
|
|
86
105
|
- Primitives that already use it in the design system source of truth (their
|
|
@@ -110,8 +129,9 @@ darkMode.toggle(); // switch
|
|
|
110
129
|
darkMode.current; // reactive read
|
|
111
130
|
```
|
|
112
131
|
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
`CodeBlock` needs nothing here: it highlights with both themes at once (shiki
|
|
133
|
+
emits the dark colours as `--shiki-dark` custom props) and flips them in CSS, so
|
|
134
|
+
theme switches need no `{#key}` wrapper and no re-highlight.
|
|
115
135
|
|
|
116
136
|
## Page shell
|
|
117
137
|
|
|
@@ -173,6 +193,9 @@ Compose project-specific components from package primitives in
|
|
|
173
193
|
`sideBarSelection`, `lensTray*`, `backToDashboard`) exist in the package
|
|
174
194
|
types but are internal to the Archetype AI console product — never use them
|
|
175
195
|
- Keep the `@source` directives and the CSS import order exactly as above
|
|
196
|
+
- Never add global element rules (`span`, `a`, `label`, `p`, `h1`…) in app CSS,
|
|
197
|
+
and never pair `leading-*`/`tracking-*` with a `text-*` utility — the theme's
|
|
198
|
+
type scale and base layer own both; size a parent, not its inline children
|
|
176
199
|
- Never style scrollbars in app code — no `::-webkit-scrollbar` rules, no
|
|
177
200
|
`scrollbar-color`/`scrollbar-width`. The theme sets both, and setting
|
|
178
201
|
`scrollbar-*` locally also silently disables the webkit pseudo-elements on
|
package/files/ds-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archetypeai-design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"components": [
|
|
5
5
|
{
|
|
6
6
|
"name": "alert",
|
|
@@ -245,8 +245,8 @@
|
|
|
245
245
|
"source": "https://design-system-console.archetypeai.workers.dev/r/codeblock.json",
|
|
246
246
|
"description": "Syntax-highlighted code display (CodeBlock, shiki; yaml/json/python/shell) and editable code field with JSON-schema validation for YAML (CodeBlockEditor).",
|
|
247
247
|
"usage": [
|
|
248
|
-
"`<CodeBlock code language=\"yaml\" />` for read-only code (border built in). `language` accepts `\"yaml\" | \"json\" | \"python\" | \"shell\"` — use `\"shell\"` for bash/sh/curl snippets. Theme switches are CSS-only — no {#key} wrapper needed. Grows to content by default
|
|
249
|
-
"Config editors: `<CodeBlockEditor bind:value bind:valid bind:validating bind:schemaUnavailable schema={jsonSchema} placeholder showDownload downloadBaseName disabled />` + a status badge row driven by the bindables (validating → 'Checking…' spinner; !valid → 'Invalid'; valid+schemaUnavailable → 'YAML parses'; valid → 'Valid YAML'). Grows to content (floor via `minHeight`, default 10rem); cap
|
|
248
|
+
"`<CodeBlock code language=\"yaml\" />` for read-only code (border built in). `language` accepts `\"yaml\" | \"json\" | \"python\" | \"shell\"` — use `\"shell\"` for bash/sh/curl snippets. Theme switches are CSS-only — no {#key} wrapper needed. Grows to content by default and never clips: whenever its height is constrained — by a `max-h-*` class, by a flex/grid parent like `Dialog.Body`, or by the `maxHeight`/`scroll` props — the internal scroller shows a scrollbar instead. To cap it yourself pass `maxHeight=\"20rem\"` (any CSS length) or the `scroll` shorthand (=600px); neither is needed just to avoid clipping. `showLineNumbers={false}` hides the gutter; `wrap` wraps long lines instead of scrolling horizontally.",
|
|
249
|
+
"Config editors: `<CodeBlockEditor bind:value bind:valid bind:validating bind:schemaUnavailable schema={jsonSchema} placeholder showDownload downloadBaseName disabled />` + a status badge row driven by the bindables (validating → 'Checking…' spinner; !valid → 'Invalid'; valid+schemaUnavailable → 'YAML parses'; valid → 'Valid YAML'). Grows to content (floor via `minHeight`, default 10rem) and scrolls rather than clips whenever its height is constrained; cap it yourself via `maxHeight` (any length) or `scroll` (=600px). Visually identical to CodeBlock. `language` (same union, default `\"yaml\"`) switches highlighting; live validation runs only for `\"yaml\"` — for other languages `schema` is ignored and `valid` stays true. The download button's extension/MIME follows the language (.yaml/.json/.py/.sh)."
|
|
250
250
|
]
|
|
251
251
|
},
|
|
252
252
|
{
|
|
@@ -357,7 +357,8 @@
|
|
|
357
357
|
"usage": [
|
|
358
358
|
"Trigger via child snippet spread onto a Button (profile menu: `variant=\"accent\"` avatar + chevron, rounded-full).",
|
|
359
359
|
"Styled triggers: `DropdownMenu.Trigger variant=\"filter\"` for toolbar/list filter dropdowns; `variant=\"input\"` for form-field-styled dropdowns in pickers.",
|
|
360
|
-
"`DropdownMenu.Content align=\"end\" class=\"min-w-64\"`; items `justify=\"between\"` with a trailing icon; `closeOnSelect={false}` for in-menu toggles (dark mode); Separator before Sign Out."
|
|
360
|
+
"`DropdownMenu.Content align=\"end\" class=\"min-w-64\"`; items `justify=\"between\"` with a trailing icon; `closeOnSelect={false}` for in-menu toggles (dark mode); Separator before Sign Out.",
|
|
361
|
+
"Section headings: `DropdownMenu.Label` for a standalone heading, `DropdownMenu.GroupHeading` inside a `Group`. Both render in the console's section-header treatment by default (10px mono, uppercase, muted, more space above than below so the heading binds to the rows it introduces), so a heading recedes rather than competing with its items. Set no type or colour classes on them; if a Label is carrying a row of content rather than a heading, override with `class`."
|
|
361
362
|
],
|
|
362
363
|
"variants": {
|
|
363
364
|
"dropdownMenuTriggerVariants": {
|
|
@@ -907,6 +908,54 @@
|
|
|
907
908
|
"Horizontal between stacked sections; `orientation=\"vertical\"` in multi-column layouts."
|
|
908
909
|
]
|
|
909
910
|
},
|
|
911
|
+
{
|
|
912
|
+
"name": "side-panel",
|
|
913
|
+
"import": "@archetypeai/ds-ui-svelte-console/primitives/side-panel",
|
|
914
|
+
"source": "https://design-system-console.archetypeai.workers.dev/r/side-panel.json",
|
|
915
|
+
"description": "Non-modal side panel for peeking at or inspecting one object without leaving the list it came from.",
|
|
916
|
+
"usage": [
|
|
917
|
+
"Bind the panel to an object id, never to a row: `<SidePanel.Root open={selectedId !== null} onOpenChange={(o) => !o && (selectedId = null)}>`. Keep the id in the URL so the state survives refresh, back and sharing; the primitive itself never reads or writes the URL. Do NOT derive the panel's content straight from that id: the panel stays mounted while it closes, so the content would blank out before it leaves. Latch the last opened object instead, `let shown = $state(null); $effect(() => { const next = find(selectedId); if (next) shown = next; })`, and render from `shown`.",
|
|
918
|
+
"Peek vs inspect: content with no page of its own just fills `SidePanel.Body`. Content that has a full page renders the page's body component inside Body and puts the handoff ONCE, in the Header `actions` snippet, as an outline icon button beside the built-in Close: `<SidePanel.Header>{#snippet actions()}<Button variant=\"outline\" size=\"icon\" class=\"size-8\" aria-label=\"Open full page\" href={fullPageUrl}><ArrowUpRightIcon /></Button>{/snippet}<SidePanel.Title>…</SidePanel.Title></SidePanel.Header>`. Header actions are outline icon buttons at the sm height (`class=\"size-8\"`, Button has no small icon size) so they read as one set with Close, pinned to the header's top-right, and match `size=\"sm\"` Footer buttons; never repeat the handoff in the Footer.",
|
|
919
|
+
"It never renders an overlay or scrim and never traps focus or scroll: the list behind stays fully interactive, clicking outside does nothing, Escape closes. If the task needs the page blocked, use Dialog instead.",
|
|
920
|
+
"One panel at a time. Drilling in replaces Body's content and adds an in-panel back; it never opens a second SidePanel.",
|
|
921
|
+
"Read-mostly: actions inside are fine, but destructive confirmations spawn a Dialog over the panel. The panel does not own confirmation.",
|
|
922
|
+
"`size=\"sm\"` (384px) for a record and its fields; `size=\"md\"` (576px) when Body carries a preview, a chart or a code block. `side=\"left\"` only when the list itself lives on the right.",
|
|
923
|
+
"Header layout follows Dialog: a `space-y-2` block, so `SidePanel.Title` and then either `SidePanel.Description` (plain text) or `SidePanel.Meta` (badges, a status line, key/value metadata) stack full width. The `actions` cluster and the close sit absolutely at the top right, so the header reserves right padding and a long Title truncates rather than running under them. Give header actions the same bare icon treatment as the close via `sidePanelCloseButtonVariants`; keep `Description` when the subtitle is text, because it is what wires `aria-describedby` and `Meta` cannot.",
|
|
924
|
+
"Content renders in place (no portal) so Tab reaches it after the list; do not mount it inside a transformed ancestor, where `fixed` misplaces. Focus does not move on open. Header renders an outline icon Close by default (`showCloseButton={false}` to place `SidePanel.Close` yourself). `SidePanel.Footer` is optional: it holds the actions on the data being viewed, never Close or the full-page handoff. One primary action sits right; with `justify=\"between\"` secondary actions (copy an id, compare) go first and land left."
|
|
925
|
+
],
|
|
926
|
+
"variants": {
|
|
927
|
+
"sidePanelContentVariants": {
|
|
928
|
+
"axes": {
|
|
929
|
+
"side": [
|
|
930
|
+
"right",
|
|
931
|
+
"left"
|
|
932
|
+
],
|
|
933
|
+
"size": [
|
|
934
|
+
"sm",
|
|
935
|
+
"md"
|
|
936
|
+
],
|
|
937
|
+
"// one value today; reserves `data-mode` for a later push mode\n mode": [
|
|
938
|
+
"overlay"
|
|
939
|
+
]
|
|
940
|
+
},
|
|
941
|
+
"defaults": {
|
|
942
|
+
"side": "right",
|
|
943
|
+
"size": "sm"
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
"sidePanelFooterVariants": {
|
|
947
|
+
"axes": {
|
|
948
|
+
"justify": [
|
|
949
|
+
"default",
|
|
950
|
+
"between"
|
|
951
|
+
]
|
|
952
|
+
},
|
|
953
|
+
"defaults": {
|
|
954
|
+
"justify": "default"
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
},
|
|
910
959
|
{
|
|
911
960
|
"name": "slider",
|
|
912
961
|
"import": "@archetypeai/ds-ui-svelte-labs/primitives/slider",
|
|
@@ -1127,9 +1176,10 @@
|
|
|
1127
1176
|
"description": "Hover hint.",
|
|
1128
1177
|
"usage": [
|
|
1129
1178
|
"Wrap the app/section in `Tooltip.Provider delayDuration={200}`.",
|
|
1130
|
-
"Field help: `Tooltip.Trigger variant=\"info\"` is a filled
|
|
1179
|
+
"Field help: `Tooltip.Trigger variant=\"info\"` is a filled 16px disc holding a mono `i` — pass it no children, it renders its own glyph. Explanation goes in `Tooltip.Content`.",
|
|
1131
1180
|
"`aria-label` on the info trigger is required and is the caller's: name what is being explained (`aria-label=\"About Detection Log\"`). The glyph is `aria-hidden`, so without a label the button has no accessible name.",
|
|
1132
|
-
"The info disc squares against `Counter`'s height on purpose — a
|
|
1181
|
+
"The info disc squares against `Counter size=\"sm\"`'s height on purpose — a section title, its `Counter` and its info disc in one `flex items-center gap-2` row is the intended cluster, the solid pill reading as the count and the muted one as the meaning. 16px is also exactly the height of a `Label variant=\"sectionHeader\"` box, so the disc sits level with the title rather than towering over it.",
|
|
1182
|
+
"`size=\"lg\"` is the 20px disc, for a cluster built around `Counter size=\"default\"` (`h-5`) rather than a section label. `size` is inert on the other trigger treatments, which have no box.",
|
|
1133
1183
|
"`variant=\"inlineLink\"` — underlined trigger flowing inside prose (supported file requirements).",
|
|
1134
1184
|
"The arrow centres itself on the trigger on all four sides and protrudes half its box — `arrowClasses` is for its ink or size, not for centring corrections. If a tip looks off, it is a bug in `tooltipArrowVariants`, not something to counter-shift at the call site."
|
|
1135
1185
|
],
|
|
@@ -1140,10 +1190,15 @@
|
|
|
1140
1190
|
"default",
|
|
1141
1191
|
"info",
|
|
1142
1192
|
"inlineLink"
|
|
1193
|
+
],
|
|
1194
|
+
"size": [
|
|
1195
|
+
"default",
|
|
1196
|
+
"lg"
|
|
1143
1197
|
]
|
|
1144
1198
|
},
|
|
1145
1199
|
"defaults": {
|
|
1146
|
-
"variant": "default"
|
|
1200
|
+
"variant": "default",
|
|
1201
|
+
"size": "default"
|
|
1147
1202
|
}
|
|
1148
1203
|
}
|
|
1149
1204
|
}
|
package/lib/add-ds-lib-tokens.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
4
|
import { detectPm, getPm } from './use-package-manager.js';
|
|
5
|
-
import { validateFontsPath, installLocalFonts } from './install-fonts-local.js';
|
|
5
|
+
import { validateFontsPath, installLocalFonts, preloadLinkTags } from './install-fonts-local.js';
|
|
6
6
|
import { isInteractive } from './is-interactive.js';
|
|
7
7
|
import { prependCss } from './scaffold-ds-svelte-project.js';
|
|
8
8
|
|
|
@@ -124,6 +124,14 @@ export async function addDsLibTokens(args) {
|
|
|
124
124
|
const fontResult = installLocalFonts(fontsPath, projectDir);
|
|
125
125
|
if (fontResult.success) {
|
|
126
126
|
fontSpinner.stop(`${fontResult.fileCount} font files installed`);
|
|
127
|
+
if (fontResult.preload.patched) {
|
|
128
|
+
p.log.info('Preloading Book, Medium and Mono Book in src/app.html');
|
|
129
|
+
} else {
|
|
130
|
+
p.log.warn(
|
|
131
|
+
`Could not add font preloads (${fontResult.preload.reason}). Add these to your <head>:\n` +
|
|
132
|
+
preloadLinkTags().join('\n')
|
|
133
|
+
);
|
|
134
|
+
}
|
|
127
135
|
} else {
|
|
128
136
|
fontSpinner.stop('Failed to install fonts');
|
|
129
137
|
p.log.warn(fontResult.error);
|
package/lib/add-ds-ui-svelte.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
7
|
import { validateRegistryUrl } from './validate-url.js';
|
|
8
8
|
import { detectPm, getPm } from './use-package-manager.js';
|
|
9
|
-
import { validateFontsPath, installLocalFonts } from './install-fonts-local.js';
|
|
9
|
+
import { validateFontsPath, installLocalFonts, preloadLinkTags } from './install-fonts-local.js';
|
|
10
10
|
import { isInteractive } from './is-interactive.js';
|
|
11
11
|
import {
|
|
12
12
|
prependCss,
|
|
@@ -177,6 +177,14 @@ export async function addDsUiSvelte(args) {
|
|
|
177
177
|
const fontResult = installLocalFonts(fontsPath, projectDir);
|
|
178
178
|
if (fontResult.success) {
|
|
179
179
|
fontSpinner.stop(`${fontResult.fileCount} font files installed`);
|
|
180
|
+
if (fontResult.preload.patched) {
|
|
181
|
+
p.log.info('Preloading Book, Medium and Mono Book in src/app.html');
|
|
182
|
+
} else {
|
|
183
|
+
p.log.warn(
|
|
184
|
+
`Could not add font preloads (${fontResult.preload.reason}). Add these to your <head>:\n` +
|
|
185
|
+
preloadLinkTags().join('\n')
|
|
186
|
+
);
|
|
187
|
+
}
|
|
180
188
|
} else {
|
|
181
189
|
fontSpinner.stop('Failed to install fonts');
|
|
182
190
|
p.log.warn(fontResult.error);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, copyFileSync, mkdirSync } from 'fs';
|
|
1
|
+
import { existsSync, readdirSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join, resolve } from 'path';
|
|
3
3
|
|
|
4
4
|
// strip surrounding quotes that some terminals (VS Code, Cursor) add on drag-and-drop
|
|
@@ -13,25 +13,59 @@ function sanitizePath(p) {
|
|
|
13
13
|
return trimmed;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// all
|
|
16
|
+
// all 13 required font files
|
|
17
17
|
export const REQUIRED_FONTS = [
|
|
18
|
-
// PP Neue Montreal Sans
|
|
19
|
-
'PPNeueMontreal-Thin.
|
|
20
|
-
'PPNeueMontreal-ThinItalic.
|
|
21
|
-
'PPNeueMontreal-Light.
|
|
22
|
-
'PPNeueMontreal-Book.
|
|
23
|
-
'PPNeueMontreal-Italic.
|
|
24
|
-
'PPNeueMontreal-Medium.
|
|
25
|
-
'PPNeueMontreal-Bold.
|
|
26
|
-
'PPNeueMontreal-BoldItalic.
|
|
27
|
-
// PP Neue Montreal Mono
|
|
28
|
-
'PPNeueMontrealMono-Thin.
|
|
29
|
-
'PPNeueMontrealMono-Book.
|
|
30
|
-
'PPNeueMontrealMono-RegularItalic.
|
|
31
|
-
'PPNeueMontrealMono-Medium.
|
|
32
|
-
'PPNeueMontrealMono-Bold.
|
|
18
|
+
// PP Neue Montreal Sans
|
|
19
|
+
'PPNeueMontreal-Thin.woff2',
|
|
20
|
+
'PPNeueMontreal-ThinItalic.woff2',
|
|
21
|
+
'PPNeueMontreal-Light.woff2',
|
|
22
|
+
'PPNeueMontreal-Book.woff2',
|
|
23
|
+
'PPNeueMontreal-Italic.woff2',
|
|
24
|
+
'PPNeueMontreal-Medium.woff2',
|
|
25
|
+
'PPNeueMontreal-Bold.woff2',
|
|
26
|
+
'PPNeueMontreal-BoldItalic.woff2',
|
|
27
|
+
// PP Neue Montreal Mono
|
|
28
|
+
'PPNeueMontrealMono-Thin.woff2',
|
|
29
|
+
'PPNeueMontrealMono-Book.woff2',
|
|
30
|
+
'PPNeueMontrealMono-RegularItalic.woff2',
|
|
31
|
+
'PPNeueMontrealMono-Medium.woff2',
|
|
32
|
+
'PPNeueMontrealMono-Bold.woff2'
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
+
// the faces the first paint uses: 400 sans (body), 500 sans (emphasis), 400 mono (labels)
|
|
36
|
+
export const FONT_PRELOADS = [
|
|
37
|
+
'PPNeueMontreal-Book.woff2',
|
|
38
|
+
'PPNeueMontreal-Medium.woff2',
|
|
39
|
+
'PPNeueMontrealMono-Book.woff2'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// `crossorigin` is required on font preloads even same-origin; without it the
|
|
43
|
+
// browser fetches the file twice (once anonymous for the preload, once CORS for @font-face)
|
|
44
|
+
export function preloadLinkTags() {
|
|
45
|
+
return FONT_PRELOADS.map(
|
|
46
|
+
(file) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/${file}" />`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// insert the preload tags above %sveltekit.head% in src/app.html (idempotent)
|
|
51
|
+
export function patchAppHtmlPreload(projectPath) {
|
|
52
|
+
const htmlPath = join(projectPath, 'src', 'app.html');
|
|
53
|
+
if (!existsSync(htmlPath)) return { patched: false, reason: 'src/app.html not found' };
|
|
54
|
+
const content = readFileSync(htmlPath, 'utf-8');
|
|
55
|
+
if (content.includes(`/fonts/${FONT_PRELOADS[0]}`)) {
|
|
56
|
+
return { patched: false, reason: 'preload tags already present' };
|
|
57
|
+
}
|
|
58
|
+
const marker = /^([ \t]*)%sveltekit\.head%/m;
|
|
59
|
+
const match = content.match(marker);
|
|
60
|
+
if (!match) return { patched: false, reason: '%sveltekit.head% placeholder not found' };
|
|
61
|
+
const indent = match[1];
|
|
62
|
+
const tags = preloadLinkTags()
|
|
63
|
+
.map((tag) => `${indent}${tag}\n`)
|
|
64
|
+
.join('');
|
|
65
|
+
writeFileSync(htmlPath, content.replace(marker, `${tags}${indent}%sveltekit.head%`));
|
|
66
|
+
return { patched: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
// validate that a directory contains all required font files
|
|
36
70
|
export function validateFontsPath(fontsPath) {
|
|
37
71
|
const resolved = resolve(sanitizePath(fontsPath));
|
|
@@ -72,7 +106,7 @@ export function copyFontsToStatic(fontsPath, projectPath) {
|
|
|
72
106
|
return REQUIRED_FONTS.length;
|
|
73
107
|
}
|
|
74
108
|
|
|
75
|
-
// orchestrate: validate
|
|
109
|
+
// orchestrate: validate, copy font files to static/fonts/, and preload the first-paint faces
|
|
76
110
|
export function installLocalFonts(fontsPath, projectPath) {
|
|
77
111
|
const validation = validateFontsPath(fontsPath);
|
|
78
112
|
if (!validation.valid) {
|
|
@@ -80,6 +114,7 @@ export function installLocalFonts(fontsPath, projectPath) {
|
|
|
80
114
|
}
|
|
81
115
|
|
|
82
116
|
const fileCount = copyFontsToStatic(fontsPath, projectPath);
|
|
117
|
+
const preload = patchAppHtmlPreload(projectPath);
|
|
83
118
|
|
|
84
|
-
return { success: true, fileCount };
|
|
119
|
+
return { success: true, fileCount, preload };
|
|
85
120
|
}
|