@getvitops/astro 0.7.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,200 @@
1
1
  # @getvitops/astro
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Fixed: `Subgrid` and `Cards` rendered as unstyled lists, in every format.**
8
+
9
+ `Subgrid.astro` drew its geometry with Tailwind utilities — `grid-rows-subgrid` and
10
+ `row-span-(--row-span)`, the latter Tailwind v4's arbitrary-CSS-variable syntax. No framework CSS
11
+ layer defines those, so under `css.format: 'css'` or `'bricks'` the component had no layout at all.
12
+
13
+ It failed under `'tailwind'` too, which is the part worth internalising: **Tailwind v4 is JIT and
14
+ does not scan `node_modules`**, so a class that only a shipped component references is never
15
+ generated. A consumer had to add the package to Tailwind's `@source` by hand to get a subgrid;
16
+ without that, cards silently laid out at `grid-row: auto` — visually plausible, quietly wrong. The
17
+ same trap applied to `grid` in `Subgrid` and `sr-only` / `not-sr-only` in `Popover` / `Drawer`:
18
+ those are in the generator's `TW_CLASH` list, so the framework strips its own rules for them from
19
+ the tailwind bundle and defers to Tailwind — meaning they resolved only when the consumer's own
20
+ templates happened to use the same class.
21
+
22
+ The components now emit framework classes only, and the `subgrid` pattern owns the layout:
23
+ - `Subgrid` renders `<ul class="subgrid"><li>…` and ships no `<style>` block of its own.
24
+ - `Popover` / `Drawer` use a component-scoped `visually-hidden` instead of `sr-only`. The
25
+ `not-sr-only` on their icons was a no-op and is gone.
26
+ - The `subgrid` pattern absorbed the wrapped-row margin the component used to carry, so
27
+ hand-written `.subgrid` markup gets it too, and resets list markers on `ul`/`ol`.
28
+
29
+ **Also fixed: `Cards` discarded the `card` class and every class you put on a child.** It wrote
30
+ `card` onto the slotted element and then serialised that element's _inner_ HTML, dropping the
31
+ attribute it had just set. Child `class` and `style` now reach the rendered `<li>`.
32
+
33
+ **Breaking — the subgrid custom properties are renamed.** One `--subgrid-*` vocabulary now covers
34
+ both the pattern and the component; the old names are removed, not aliased:
35
+
36
+ | removed | use |
37
+ | ------------------------------- | -------------------- |
38
+ | `--items-per-row`, `--cols` | `--subgrid-cols` |
39
+ | `--rows-per-item`, `--row-span` | `--subgrid-row-span` |
40
+ | `--row-margin` | `--subgrid-row-gap` |
41
+
42
+ `--subgrid-gap` (the grid gap) is unchanged, as are the `.subgrid-cols-*` / `.subgrid-rows-*` /
43
+ `.subgrid-responsive` modifiers. Anything still setting an old name falls back to the pattern
44
+ defaults — 3 columns, span 2 — so grep for them when you upgrade.
45
+
46
+ - **`tailwindcss` and `@tailwindcss/vite` are now optional peer dependencies, not dependencies.**
47
+
48
+ Only `css.format: 'tailwind'` ever used them, but every consumer installed them — including
49
+ `'css'` and `'bricks'` projects that never touch Tailwind. The integration now loads
50
+ `@tailwindcss/vite` lazily inside that branch and throws a directive error if it is missing.
51
+
52
+ **Migration:** if you use `css.format: 'tailwind'` (the default), add both to your
53
+ `devDependencies` — `pnpm add -D tailwindcss @tailwindcss/vite`. Most Tailwind projects already
54
+ have them. Everyone else can drop them.
55
+
56
+ ### Patch Changes
57
+
58
+ - Updated dependencies
59
+ - Updated dependencies
60
+ - Updated dependencies
61
+ - Updated dependencies [c949cae]
62
+ - Updated dependencies
63
+ - Updated dependencies
64
+ - Updated dependencies
65
+ - Updated dependencies
66
+ - @getvitops/generator@0.9.0
67
+ - @getvitops/core@0.9.0
68
+ - @getvitops/vite@0.9.0
69
+ - @getvitops/utils@0.9.0
70
+
71
+ ## 0.8.0
72
+
73
+ ### Minor Changes
74
+
75
+ - a334049: Make the semantic icon mapping reachable, and fail the build on unresolvable names.
76
+
77
+ `generateIconInclude()` — declare the semantic icon names a site needs plus which sets to draw
78
+ them from, get back the `include` map that keeps the bundle to just those glyphs — already existed
79
+ but was unreachable: it lived in `@getvitops/core/src/utils/`, which the package doesn't export.
80
+ It has moved to `@getvitops/utils` (a build-time concern, in the build-time utilities package),
81
+ which `@getvitops/astro` re-exports wholesale. So from `astro.config.mjs`:
82
+
83
+ ```js
84
+ import { generateIconInclude } from '@getvitops/astro';
85
+ import icon from 'astro-icon';
86
+
87
+ integrations: [
88
+ icon({
89
+ include: generateIconInclude({
90
+ ui: 'fa7-solid',
91
+ brand: 'simple-icons',
92
+ semantic: ['menu', 'close', 'search', 'github'],
93
+ }),
94
+ }),
95
+ ];
96
+ // → { "fa7-solid": ["bars","xmark","magnifying-glass"], "simple-icons": ["github"] }
97
+ ```
98
+
99
+ Swapping `ui` to `'lucide'` yields `{ lucide: ["menu","x","search"] }` from the same declaration —
100
+ which is the point: the semantic names are what your markup commits to, and the set is a config
101
+ choice. The output shape is the `include` both `astro-icon` and `astro-iconset` accept, so the
102
+ mapping doesn't tie you to either.
103
+
104
+ **Unresolvable names are now a build error.** Previously they were skipped silently, so swapping
105
+ sets appeared to succeed and the gaps surfaced as missing glyphs in production. The error names
106
+ every offender; an unknown set name throws too, listing the known sets.
107
+
108
+ Also fixes `@getvitops/create`'s emdash template, which pinned `@getvitops/astro: ^0.4.0` — a range
109
+ that stopped resolving when astro joined the fixed group at 0.7.0, so scaffolded projects were
110
+ stuck on the old line.
111
+
112
+ - **New: `<wc-theme-editor>`, a live theme editor you can ship with your site.**
113
+
114
+ Tune the whole design system in the browser — palette, semantic roles, type roles, spacing, layout,
115
+ pattern geometry, radii, shadows — with no rebuild. Edits are layered as `:root` custom-property
116
+ overrides, persist to localStorage, and export as CSS or as a `design-system.json` patch. It follows
117
+ your site's colour scheme and drives the same `data-theme` + storage key as `<color-scheme-toggle>`,
118
+ so the two no longer fight.
119
+
120
+ It ships as a **separate, opt-in bundle** (`@getvitops/core/editor`, ~13 kB, no Lit) and is never
121
+ registered in `elements.js`: a page that doesn't ask for it pays nothing.
122
+
123
+ ```js
124
+ // astro.config.mjs
125
+ getvitops({
126
+ css: { input: 'design-system.json', format: 'css', out: 'src/styles' },
127
+ editor: true,
128
+ });
129
+ ```
130
+
131
+ ```html
132
+ <!-- anywhere in your layout -->
133
+ <wc-theme-editor></wc-theme-editor>
134
+ ```
135
+
136
+ `editor: true` copies `editor.js` into `public/vitops/`, loads it from `<Head />`, and mirrors
137
+ `design-manifest.json` to `/vitops/design-manifest.json` (override with the `manifest` attribute).
138
+ Outside Astro, load the bundle yourself and point `manifest` at the file.
139
+
140
+ **Save straight back to your config, in dev.** `@getvitops/vite` now serves a dev-only
141
+ `/__vitops/design-system` endpoint: the editor POSTs its patch, the plugin deep-merges it into your
142
+ `design-system.json`, validates the _merged_ result, writes it back preserving the file's formatting,
143
+ then regenerates and reloads. The endpoint exists only under `vite dev` — on a static deploy the
144
+ editor detects its absence and hides the button, while everything else still works.
145
+
146
+ **Added:** `colors.roleTokens` in `design-manifest.json` — the functional token set (bg / border /
147
+ solid / on-solid / text / emphasis stops) precomputed per hue, in `default` and `surface` variants for
148
+ both appearances. Re-pointing a role at another hue needs these: `solid` is chosen by scanning the hue
149
+ and `on-solid` is a computed contrast value, so neither is derivable by a browser client.
150
+
151
+ **Removed:** the dead `interaction` block from `design-manifest.json` — a hardcoded
152
+ `{duration, easing}` literal with no schema key, no reverse-index entry, and no corresponding CSS
153
+ variable. Nothing could have consumed it meaningfully. The real animation knobs are
154
+ `--animation-duration-*` / `--custom-ease-*`.
155
+
156
+ **Fixed:** a `[popover]` reset in `popover.css` was stripping the background, padding and border from
157
+ any pattern used as a popover. It is imported after `drawer.css`, so at equal specificity it beat
158
+ `.drawer` — and `dialog` (0-0-1) too — leaving a `.drawer[popover]` transparent and unpadded despite
159
+ drawer.css documenting popover as supported. The reset now sits in `:where()` at zero specificity, so
160
+ it still overrides the UA stylesheet but any pattern class takes its surface back. Open/closed
161
+ behaviour is unchanged.
162
+
163
+ **Added:** `input[type="range"]` and `input[type="color"]` now have baseline styling in `forms.css`
164
+ (accent colour from `--ui-primary-solid`; the colour swatch picks up the control border and radius).
165
+
166
+ ### Patch Changes
167
+
168
+ - aa2863d: Persist the colour scheme across navigations, and drop the UA body margin.
169
+
170
+ **The colour scheme now sticks.** `<color-scheme-toggle>` records the explicit choice in
171
+ `localStorage` (`vitops-color-scheme`) and restores it on load. Previously every navigation reset
172
+ to System — the component defaulted to `system` on each page and its `disconnectedCallback` even
173
+ deleted the attribute on unmount, so the scheme was per-page state rather than a user preference.
174
+ Choosing System clears the key, so a visitor can go back to following their OS.
175
+
176
+ `<Head />` from `@getvitops/astro` now also emits a tiny synchronous script that applies the stored
177
+ value **before first paint**. Without it the persisted choice would still work, but every page
178
+ would render light and flip once the deferred element bundle upgraded. A test asserts the storage
179
+ key stays in step between the two (they can't share an import: core exports only prebuilt bundles,
180
+ and the Head script must be a literal in the emitted HTML).
181
+
182
+ **`body { margin: 0 }`** is now part of the framework. The UA's 8px margin offset every full-bleed
183
+ surface — sticky headers and `bg-*` bands rendered inset with a sliver of canvas around them, since
184
+ the framework owns page gutters through `.centered`'s `--gutter`. This is the only UA reset the
185
+ framework makes; it deliberately still ships no general reset (no global `box-sizing` change),
186
+ which would silently reflow existing layouts.
187
+
188
+ **Migration:** if you were compensating for the body margin with a negative offset or your own
189
+ `body { margin: 0 }`, you can drop it. Sites relying on the 8px inset will need to add their own
190
+ padding.
191
+
192
+ - Updated dependencies
193
+ - @getvitops/core@0.8.0
194
+ - @getvitops/generator@0.8.0
195
+ - @getvitops/vite@0.8.0
196
+ - @getvitops/utils@0.8.0
197
+
3
198
  ## 0.7.0
4
199
 
5
200
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -35,6 +35,18 @@ interface GetvitopsOptions {
35
35
  webComponents?: boolean;
36
36
  /** Generate + auto-inject the design-system CSS. Off unless provided. */
37
37
  css?: GetvitopsCssOptions;
38
+ /**
39
+ * Ship the live theme editor (default false).
40
+ *
41
+ * Copies `editor.js` into `public/vitops/`, loads it from `<Head />`, and mirrors
42
+ * the generated `design-manifest.json` next to it so `<wc-theme-editor>` resolves
43
+ * its default `/vitops/design-manifest.json`. Requires `css` (the manifest is a
44
+ * `css`-format output) — place the `<wc-theme-editor>` tag in your layout yourself.
45
+ *
46
+ * Editing is live on any deploy; **saving back** to `design-system.json` needs the
47
+ * dev server, so on a static build that button simply isn't rendered.
48
+ */
49
+ editor?: boolean;
38
50
  }
39
51
  declare function getvitops(opts?: GetvitopsOptions): AstroIntegration;
40
52
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/integration.ts"],"mappings":";;;;;UAuBiB,uBAAA;;EAEf,MAAA;EACA,YAAA;EAHe;EAKf,IAAA;;EAEA,UAAA;EACA,eAAA;AAAA;AAAA,UAGe,mBAAA;EAJf;EAMA,KAAA;EALe;EAOf,MAAA,GAAS,MAAM;EAJA;EAMf,GAAA;;;;;;;;EAQA,MAAA;AAAA;AAAA,UAGe,gBAAA;EACf,OAAA,GAAU,uBAAA;;EAEV,aAAA;EAFA;EAIA,GAAA,GAAM,mBAAmB;AAAA;AAAA,iBA0BH,SAAA,CAAU,IAAA,GAAM,gBAAA,GAAwB,gBAAgB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/integration.ts"],"mappings":";;;;;UAsBiB,uBAAA;;EAEf,MAAA;EACA,YAAA;EAHe;EAKf,IAAA;;EAEA,UAAA;EACA,eAAA;AAAA;AAAA,UAGe,mBAAA;EAJf;EAMA,KAAA;EALe;EAOf,MAAA,GAAS,MAAM;EAJA;EAMf,GAAA;;;;;;;;EAQA,MAAA;AAAA;AAAA,UAGe,gBAAA;EACf,OAAA,GAAU,uBAAA;;EAEV,aAAA;EAFA;EAIA,GAAA,GAAM,mBAAmB;EAFzB;;;;;AAcM;AACP;;;;;EADC,MAAA;AAAA;AAAA,iBA2BsB,SAAA,CAAU,IAAA,GAAM,gBAAA,GAAwB,gBAAgB"}
package/dist/index.mjs CHANGED
@@ -3,7 +3,6 @@ import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { faviconLinks, generateFavicons, writeFaviconManifest } from "@getvitops/utils";
5
5
  import vitops from "@getvitops/vite";
6
- import tailwindcss from "@tailwindcss/vite";
7
6
 
8
7
  export * from "@getvitops/utils"
9
8
 
@@ -37,6 +36,8 @@ function getvitops(opts = {}) {
37
36
  const root = fileURLToPath(config.root);
38
37
  const publicDir = fileURLToPath(config.publicDir);
39
38
  const webComponents = opts.webComponents !== false;
39
+ const editor = opts.editor === true;
40
+ if (editor && !opts.css) logger.warn("editor: true needs a `css` config — it reads design-manifest.json.");
40
41
  const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);
41
42
  let hasSvg = false;
42
43
  if (opts.favicon) {
@@ -58,12 +59,14 @@ function getvitops(opts = {}) {
58
59
  const coreDist = join(dirname(fileURLToPath(import.meta.resolve("@getvitops/core/package.json"))), "dist");
59
60
  const dest = join(publicDir, "vitops");
60
61
  if (existsSync(coreDist)) {
61
- for (const f of [
62
+ const bundles = [
62
63
  "polyfills.js",
63
64
  "deferred.js",
64
65
  "elements.js",
65
66
  "polyfills"
66
- ]) {
67
+ ];
68
+ if (editor) bundles.push("editor.js");
69
+ for (const f of bundles) {
67
70
  const src = join(coreDist, f);
68
71
  if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });
69
72
  }
@@ -78,7 +81,8 @@ function getvitops(opts = {}) {
78
81
  }) : [],
79
82
  themeColor: opts.favicon?.themeColor ?? null,
80
83
  webComponents,
81
- wcBase: "/vitops"
84
+ wcBase: "/vitops",
85
+ editor
82
86
  })] } });
83
87
  if (opts.css) {
84
88
  const format = opts.css.format ?? "tailwind";
@@ -86,9 +90,18 @@ function getvitops(opts = {}) {
86
90
  const plugins = [vitops({
87
91
  input: opts.css.input ?? "design-system.json",
88
92
  format,
89
- out
93
+ out,
94
+ ...editor ? { editorManifestDir: publicDir } : {}
90
95
  })];
91
- if (format === "tailwind") plugins.push(...tailwindcss());
96
+ if (format === "tailwind") {
97
+ let tailwindcss;
98
+ try {
99
+ ({default: tailwindcss} = await import("@tailwindcss/vite"));
100
+ } catch {
101
+ throw new Error("[getvitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` in your devDependencies — install them, or switch to css.format: 'css' for the standalone bundle.");
102
+ }
103
+ plugins.push(...tailwindcss());
104
+ }
92
105
  updateConfig({ vite: { plugins } });
93
106
  const cssFile = format === "tailwind" ? "tailwind.css" : "styles.css";
94
107
  if (opts.css.inject !== false) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/integration.ts"],"sourcesContent":["/**\n * @getvitops/astro — the Astro integration. One entry wires the design system's\n * `<head>` contributions into a consumer site:\n * - favicons + PWA manifest (generated into public/),\n * - the web-component runtime bundles (copied into public/vitops/),\n * - (opt-in) the design-system CSS, generated + auto-injected (no manual import).\n * The `<Head />` component (shipped alongside) renders the tags, reading resolved\n * config from the `virtual:getvitops/head` module this integration provides.\n */\nimport { cpSync, existsSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Format } from '@getvitops/generator';\nimport {\n type FaviconLink,\n faviconLinks,\n generateFavicons,\n writeFaviconManifest,\n} from '@getvitops/utils';\nimport vitops from '@getvitops/vite';\nimport tailwindcss from '@tailwindcss/vite';\nimport type { AstroIntegration } from 'astro';\n\nexport interface GetvitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n lowResSource?: string;\n /** App name → enables `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also `<meta name=\"theme-color\">`). */\n themeColor?: string;\n backgroundColor?: string;\n}\n\nexport interface GetvitopsCssOptions {\n /** Path to design-system.json (default 'design-system.json'). */\n input?: string;\n /** Output format (default 'tailwind'). */\n format?: Format;\n /** Directory the generated CSS is written to (default 'src/styles'). */\n out?: string;\n /**\n * Auto-inject the generated stylesheet into every SSR page (default true).\n * Set false when other integrations add routes that must not inherit the\n * design system (e.g. EmDash's `/_emdash/admin`) — then import the generated\n * file (`<out>/tailwind.css` or `<out>/styles.css`) from your site layout so\n * only your own pages (and previews rendered through them) are styled.\n */\n inject?: boolean;\n}\n\nexport interface GetvitopsOptions {\n favicon?: GetvitopsFaviconOptions;\n /** Copy + link the web-component bundles (default true). */\n webComponents?: boolean;\n /** Generate + auto-inject the design-system CSS. Off unless provided. */\n css?: GetvitopsCssOptions;\n}\n\ninterface HeadData {\n favicons: boolean;\n faviconLinks: FaviconLink[];\n themeColor: string | null;\n webComponents: boolean;\n wcBase: string;\n}\n\nconst VIRTUAL_ID = 'virtual:getvitops/head';\nconst RESOLVED_ID = `\\0${VIRTUAL_ID}`;\n\nfunction virtualHeadPlugin(data: HeadData) {\n return {\n name: '@getvitops/astro:virtual-head',\n resolveId(id: string) {\n return id === VIRTUAL_ID ? RESOLVED_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\nexport default function getvitops(opts: GetvitopsOptions = {}): AstroIntegration {\n return {\n name: '@getvitops/astro',\n hooks: {\n 'astro:config:setup': async ({ config, updateConfig, injectScript, logger }) => {\n const root = fileURLToPath(config.root);\n const publicDir = fileURLToPath(config.publicDir);\n const webComponents = opts.webComponents !== false;\n const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);\n let hasSvg = false;\n\n // 1. Favicons + manifest → public/\n if (opts.favicon) {\n const source = resolve(root, opts.favicon.source);\n hasSvg = source.endsWith('.svg');\n await generateFavicons({\n source,\n outputDir: publicDir,\n ...(opts.favicon.lowResSource\n ? { lowResSource: resolve(root, opts.favicon.lowResSource) }\n : {}),\n });\n if (opts.favicon.name && opts.favicon.themeColor) {\n await writeFaviconManifest(publicDir, {\n name: opts.favicon.name,\n themeColor: opts.favicon.themeColor,\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n }\n logger.info('generated favicons + manifest → public/');\n }\n\n // 2. Web-component bundles → public/vitops/\n if (webComponents) {\n const corePkg = fileURLToPath(import.meta.resolve('@getvitops/core/package.json'));\n const coreDist = join(dirname(corePkg), 'dist');\n const dest = join(publicDir, 'vitops');\n if (existsSync(coreDist)) {\n for (const f of ['polyfills.js', 'deferred.js', 'elements.js', 'polyfills']) {\n const src = join(coreDist, f);\n if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });\n }\n logger.info('copied web-component bundles → public/vitops/');\n } else {\n logger.warn(\n `@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`,\n );\n }\n }\n\n // 3. Virtual head module for <Head/>\n updateConfig({\n vite: {\n plugins: [\n virtualHeadPlugin({\n favicons: !!opts.favicon,\n faviconLinks: opts.favicon ? faviconLinks({ hasSvg, manifest: hasManifest }) : [],\n themeColor: opts.favicon?.themeColor ?? null,\n webComponents,\n wcBase: '/vitops',\n }),\n ],\n },\n });\n\n // 4. CSS — generate + compile + auto-inject (consumer imports nothing).\n if (opts.css) {\n const format: Format = opts.css.format ?? 'tailwind';\n const out = opts.css.out ?? 'src/styles';\n const plugins = [vitops({ input: opts.css.input ?? 'design-system.json', format, out })];\n // tailwindcss() is typed against a newer vite than @getvitops/vite's\n // peer; the Plugin shapes are structurally compatible at runtime\n // (Astro forwards them verbatim), so bridge the two type identities.\n if (format === 'tailwind') plugins.push(...(tailwindcss() as unknown as typeof plugins));\n updateConfig({ vite: { plugins } });\n const cssFile = format === 'tailwind' ? 'tailwind.css' : 'styles.css';\n if (opts.css.inject !== false) {\n const cssPath = resolve(root, out, cssFile);\n injectScript('page-ssr', `import ${JSON.stringify(cssPath)};`);\n logger.info(`design-system CSS (${format}) auto-injected from ${out}/${cssFile}`);\n } else {\n logger.info(\n `design-system CSS (${format}) generated at ${out}/${cssFile} (inject: false — import it from your layout)`,\n );\n }\n }\n },\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/integration.ts"],"sourcesContent":["/**\n * @getvitops/astro — the Astro integration. One entry wires the design system's\n * `<head>` contributions into a consumer site:\n * - favicons + PWA manifest (generated into public/),\n * - the web-component runtime bundles (copied into public/vitops/),\n * - (opt-in) the design-system CSS, generated + auto-injected (no manual import).\n * The `<Head />` component (shipped alongside) renders the tags, reading resolved\n * config from the `virtual:getvitops/head` module this integration provides.\n */\nimport { cpSync, existsSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Format } from '@getvitops/generator';\nimport {\n type FaviconLink,\n faviconLinks,\n generateFavicons,\n writeFaviconManifest,\n} from '@getvitops/utils';\nimport vitops from '@getvitops/vite';\nimport type { AstroIntegration } from 'astro';\n\nexport interface GetvitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n lowResSource?: string;\n /** App name → enables `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also `<meta name=\"theme-color\">`). */\n themeColor?: string;\n backgroundColor?: string;\n}\n\nexport interface GetvitopsCssOptions {\n /** Path to design-system.json (default 'design-system.json'). */\n input?: string;\n /** Output format (default 'tailwind'). */\n format?: Format;\n /** Directory the generated CSS is written to (default 'src/styles'). */\n out?: string;\n /**\n * Auto-inject the generated stylesheet into every SSR page (default true).\n * Set false when other integrations add routes that must not inherit the\n * design system (e.g. EmDash's `/_emdash/admin`) — then import the generated\n * file (`<out>/tailwind.css` or `<out>/styles.css`) from your site layout so\n * only your own pages (and previews rendered through them) are styled.\n */\n inject?: boolean;\n}\n\nexport interface GetvitopsOptions {\n favicon?: GetvitopsFaviconOptions;\n /** Copy + link the web-component bundles (default true). */\n webComponents?: boolean;\n /** Generate + auto-inject the design-system CSS. Off unless provided. */\n css?: GetvitopsCssOptions;\n /**\n * Ship the live theme editor (default false).\n *\n * Copies `editor.js` into `public/vitops/`, loads it from `<Head />`, and mirrors\n * the generated `design-manifest.json` next to it so `<wc-theme-editor>` resolves\n * its default `/vitops/design-manifest.json`. Requires `css` (the manifest is a\n * `css`-format output) — place the `<wc-theme-editor>` tag in your layout yourself.\n *\n * Editing is live on any deploy; **saving back** to `design-system.json` needs the\n * dev server, so on a static build that button simply isn't rendered.\n */\n editor?: boolean;\n}\n\ninterface HeadData {\n favicons: boolean;\n faviconLinks: FaviconLink[];\n themeColor: string | null;\n webComponents: boolean;\n wcBase: string;\n editor: boolean;\n}\n\nconst VIRTUAL_ID = 'virtual:getvitops/head';\nconst RESOLVED_ID = `\\0${VIRTUAL_ID}`;\n\nfunction virtualHeadPlugin(data: HeadData) {\n return {\n name: '@getvitops/astro:virtual-head',\n resolveId(id: string) {\n return id === VIRTUAL_ID ? RESOLVED_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\nexport default function getvitops(opts: GetvitopsOptions = {}): AstroIntegration {\n return {\n name: '@getvitops/astro',\n hooks: {\n 'astro:config:setup': async ({ config, updateConfig, injectScript, logger }) => {\n const root = fileURLToPath(config.root);\n const publicDir = fileURLToPath(config.publicDir);\n const webComponents = opts.webComponents !== false;\n const editor = opts.editor === true;\n if (editor && !opts.css)\n logger.warn('editor: true needs a `css` config — it reads design-manifest.json.');\n const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);\n let hasSvg = false;\n\n // 1. Favicons + manifest → public/\n if (opts.favicon) {\n const source = resolve(root, opts.favicon.source);\n hasSvg = source.endsWith('.svg');\n await generateFavicons({\n source,\n outputDir: publicDir,\n ...(opts.favicon.lowResSource\n ? { lowResSource: resolve(root, opts.favicon.lowResSource) }\n : {}),\n });\n if (opts.favicon.name && opts.favicon.themeColor) {\n await writeFaviconManifest(publicDir, {\n name: opts.favicon.name,\n themeColor: opts.favicon.themeColor,\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n }\n logger.info('generated favicons + manifest → public/');\n }\n\n // 2. Web-component bundles → public/vitops/\n if (webComponents) {\n const corePkg = fileURLToPath(import.meta.resolve('@getvitops/core/package.json'));\n const coreDist = join(dirname(corePkg), 'dist');\n const dest = join(publicDir, 'vitops');\n if (existsSync(coreDist)) {\n const bundles = ['polyfills.js', 'deferred.js', 'elements.js', 'polyfills'];\n if (editor) bundles.push('editor.js');\n for (const f of bundles) {\n const src = join(coreDist, f);\n if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });\n }\n logger.info('copied web-component bundles → public/vitops/');\n } else {\n logger.warn(\n `@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`,\n );\n }\n }\n\n // 3. Virtual head module for <Head/>\n updateConfig({\n vite: {\n plugins: [\n virtualHeadPlugin({\n favicons: !!opts.favicon,\n faviconLinks: opts.favicon ? faviconLinks({ hasSvg, manifest: hasManifest }) : [],\n themeColor: opts.favicon?.themeColor ?? null,\n webComponents,\n wcBase: '/vitops',\n editor,\n }),\n ],\n },\n });\n\n // 4. CSS — generate + compile + auto-inject (consumer imports nothing).\n if (opts.css) {\n const format: Format = opts.css.format ?? 'tailwind';\n const out = opts.css.out ?? 'src/styles';\n const plugins = [\n vitops({\n input: opts.css.input ?? 'design-system.json',\n format,\n out,\n // Mirrored inside the generate pass so the served copy can never be\n // missing on a cold start or stale after a config edit.\n ...(editor ? { editorManifestDir: publicDir } : {}),\n }),\n ];\n // Tailwind is an optional peer — only the `tailwind` format needs it,\n // so it's loaded here rather than imported at module scope. (The\n // plugin is typed against a newer vite than @getvitops/vite's peer;\n // the Plugin shapes are structurally compatible at runtime — Astro\n // forwards them verbatim — so bridge the two type identities.)\n if (format === 'tailwind') {\n let tailwindcss: () => unknown[];\n try {\n ({ default: tailwindcss } = await import('@tailwindcss/vite'));\n } catch {\n throw new Error(\n \"[getvitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` \" +\n 'in your devDependencies — install them, or switch to css.format: ' +\n \"'css' for the standalone bundle.\",\n );\n }\n plugins.push(...(tailwindcss() as unknown as typeof plugins));\n }\n updateConfig({ vite: { plugins } });\n const cssFile = format === 'tailwind' ? 'tailwind.css' : 'styles.css';\n if (opts.css.inject !== false) {\n const cssPath = resolve(root, out, cssFile);\n injectScript('page-ssr', `import ${JSON.stringify(cssPath)};`);\n logger.info(`design-system CSS (${format}) auto-injected from ${out}/${cssFile}`);\n } else {\n logger.info(\n `design-system CSS (${format}) generated at ${out}/${cssFile} (inject: false — import it from your layout)`,\n );\n }\n }\n },\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/astro",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Astro integration for the Vitops design system — favicons/PWA, web-component scripts, and CSS, via a getvitops() integration + <Head /> component.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -39,23 +39,32 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@tailwindcss/vite": "^4.3.3",
43
- "tailwindcss": "^4.3.3",
44
- "@getvitops/core": "0.7.0",
45
- "@getvitops/generator": "0.7.0",
46
- "@getvitops/utils": "0.7.0",
47
- "@getvitops/vite": "0.7.0"
42
+ "@getvitops/core": "0.9.0",
43
+ "@getvitops/vite": "0.9.0",
44
+ "@getvitops/generator": "0.9.0",
45
+ "@getvitops/utils": "0.9.0"
48
46
  },
49
47
  "devDependencies": {
50
- "astro": "^7.1.2"
48
+ "@tailwindcss/vite": "^4.3.3",
49
+ "astro": "^7.1.2",
50
+ "tailwindcss": "^4.3.3",
51
+ "vitest": "^4.1.9"
51
52
  },
52
53
  "peerDependencies": {
54
+ "@tailwindcss/vite": ">=4",
53
55
  "astro": ">=7",
54
- "astro-icon": ">=1"
56
+ "astro-icon": ">=1",
57
+ "tailwindcss": ">=4"
55
58
  },
56
59
  "peerDependenciesMeta": {
60
+ "@tailwindcss/vite": {
61
+ "optional": true
62
+ },
57
63
  "astro-icon": {
58
64
  "optional": true
65
+ },
66
+ "tailwindcss": {
67
+ "optional": true
59
68
  }
60
69
  }
61
70
  }
@@ -1,32 +1,34 @@
1
1
  ---
2
+ /**
3
+ * Cards — a `Subgrid` whose items are `card`s. The child's own class and style
4
+ * ride along, so `<article class="bg-surface">` becomes `<li class="card bg-surface">`.
5
+ *
6
+ * The class has to be put on the `<li>` we hand to Subgrid rather than written
7
+ * into the slotted element: `serialize()` returns a node's *inner* HTML, so the
8
+ * element carrying the attribute is discarded on the way through.
9
+ */
2
10
  import type { HTMLAttributes } from 'astro/types';
3
11
  import Subgrid from './Subgrid.astro';
4
- import { parseRenderedSlots, serialize } from '@getvitops/utils';
12
+ import { parseRenderedSlots, getAttribute, serialize } from '@getvitops/utils';
5
13
 
6
14
  export interface Props extends HTMLAttributes<'ul'> {}
7
15
 
8
16
  const { class: className, ...rest } = Astro.props;
9
17
 
10
18
  const nodes = parseRenderedSlots(await Astro.slots.render('default'));
11
- const slots = nodes.filter(node => 'tagName' in node).map(node => {
12
- const cls = node.attrs?.find(attr => attr.name === 'class');
13
- const value = `card${cls ? ' ' + cls.value : ''}`;
14
- if (cls) {
15
- cls.value = value;
16
- } else {
17
- node.attrs.push({ name: 'class', value });
18
- }
19
-
20
- return { html: serialize(node), attrs: node.attrs };
21
- });
19
+ const slots = nodes.filter(node => 'tagName' in node).map(node => ({
20
+ html: serialize(node),
21
+ class: getAttribute(node, 'class'),
22
+ style: getAttribute(node, 'style'),
23
+ }));
22
24
  ---
23
25
 
24
26
  <Subgrid
25
27
  role="list"
26
- class:list={["no-list-markers", className]}
28
+ class={className}
27
29
  {...rest}
28
30
  >
29
31
  {slots.map((slot) => (
30
- <li class="card-wrapper" style={slot.attrs?.find(attr => attr.name === 'style')?.value} set:html={slot.html}></li>
32
+ <li class:list={["card", slot.class]} style={slot.style} set:html={slot.html}></li>
31
33
  ))}
32
34
  </Subgrid>
@@ -49,8 +49,8 @@ const { class: dialogAttrClass, style: dialogAttrStyle, ...dialogRestAttrs } = a
49
49
  aria-label={trigger?.label || 'Open drawer'}
50
50
  {...triggerRestAttrs}
51
51
  >
52
- <span class={trigger?.icon ? 'sr-only' : ''}>{trigger?.label || 'Open'}</span>
53
- {trigger?.icon && <Icon name={iconResolver(trigger.icon)} class="not-sr-only" />}
52
+ <span class:list={[trigger?.icon && 'visually-hidden']}>{trigger?.label || 'Open'}</span>
53
+ {trigger?.icon && <Icon name={iconResolver(trigger.icon)} />}
54
54
  </button>
55
55
  </slot>
56
56
 
@@ -63,3 +63,20 @@ const { class: dialogAttrClass, style: dialogAttrStyle, ...dialogRestAttrs } = a
63
63
  >
64
64
  <slot />
65
65
  </dialog>
66
+
67
+ <style>
68
+ /* Icon-only triggers keep their label for screen readers. Component-scoped
69
+ rather than the framework's `sr-only`: that one is in the generator's
70
+ TW_CLASH list, so the tailwind format ships no rule for it. */
71
+ .visually-hidden {
72
+ position: absolute;
73
+ inline-size: 1px;
74
+ block-size: 1px;
75
+ padding: 0;
76
+ margin: -1px;
77
+ overflow: hidden;
78
+ clip-path: inset(50%);
79
+ white-space: nowrap;
80
+ border: 0;
81
+ }
82
+ </style>
@@ -14,9 +14,26 @@ import head from 'virtual:getvitops/head';
14
14
  interface Props {
15
15
  favicons?: boolean;
16
16
  webComponents?: boolean;
17
+ editor?: boolean;
17
18
  }
18
- const { favicons = head.favicons, webComponents = head.webComponents } = Astro.props;
19
+ const {
20
+ favicons = head.favicons,
21
+ webComponents = head.webComponents,
22
+ editor = head.editor,
23
+ } = Astro.props;
19
24
  const wc = head.wcBase;
25
+
26
+ /**
27
+ * Applies the remembered colour scheme before first paint.
28
+ *
29
+ * The key MUST match `WCColorSchemeToggle.STORAGE_KEY` in @getvitops/core;
30
+ * `head.test.ts` asserts they stay in step. try/catch because localStorage
31
+ * throws in private/partitioned contexts.
32
+ */
33
+ const THEME_SCRIPT =
34
+ `try{var s=localStorage.getItem('vitops-color-scheme');` +
35
+ `if(s==='light'||s==='dark'){document.documentElement.dataset.theme=s;` +
36
+ `document.documentElement.style.colorScheme=s+' only'}}catch(e){}`;
20
37
  ---
21
38
 
22
39
  {favicons && head.faviconLinks.map((l: { rel: string; href: string; type?: string; sizes?: string }) => (
@@ -26,9 +43,30 @@ const wc = head.wcBase;
26
43
 
27
44
  {webComponents && (
28
45
  <>
46
+ {/*
47
+ Apply the remembered colour scheme before first paint.
48
+
49
+ `<color-scheme-toggle>` persists the choice to localStorage, but it loads
50
+ as a deferred module — so without this every navigation would paint the
51
+ light token set and flip to dark once the element upgraded. Synchronous and
52
+ inline on purpose: it has to run before the body renders.
53
+
54
+ The key MUST match `WCColorSchemeToggle.STORAGE_KEY` in @getvitops/core;
55
+ `head.test.ts` asserts they stay in step. Wrapped in try/catch because
56
+ localStorage throws in private/partitioned contexts.
57
+ */}
58
+ <script is:inline set:html={THEME_SCRIPT} />
29
59
  <link rel="modulepreload" href={`${wc}/polyfills.js`} />
30
60
  <script is:inline type="module" src={`${wc}/polyfills.js`}></script>
31
61
  <script is:inline defer src={`${wc}/deferred.js`}></script>
32
62
  <script is:inline type="module" src={`${wc}/elements.js`}></script>
33
63
  </>
34
64
  )}
65
+
66
+ {/*
67
+ The live theme editor — a separate, opt-in bundle (getvitops({ editor: true })).
68
+ It is tooling, not part of the runtime, so it stays out of elements.js and off
69
+ every page that doesn't ask for it. Loads independently of `webComponents`:
70
+ <wc-theme-editor> registers itself and pulls in no framework element.
71
+ */}
72
+ {editor && <script is:inline type="module" src={`${wc}/editor.js`}></script>}
@@ -48,8 +48,8 @@ const { class: triggerAttrClass, ...triggerRestAttrs } = trigger?.attrs ?? {};
48
48
  aria-label={trigger?.label || 'Open'}
49
49
  {...triggerRestAttrs}
50
50
  >
51
- <span class={trigger?.icon ? 'sr-only' : ''}>{trigger?.label || 'Open'}</span>
52
- {trigger?.icon && <Icon name={iconResolver(trigger.icon)} class="not-sr-only" />}
51
+ <span class:list={[trigger?.icon && 'visually-hidden']}>{trigger?.label || 'Open'}</span>
52
+ {trigger?.icon && <Icon name={iconResolver(trigger.icon)} />}
53
53
  </button>
54
54
  </slot>
55
55
 
@@ -87,4 +87,19 @@ const { class: triggerAttrClass, ...triggerRestAttrs } = trigger?.attrs ?? {};
87
87
  font-family: inherit;
88
88
  color: inherit;
89
89
  }
90
+
91
+ /* Icon-only triggers keep their label for screen readers. Component-scoped
92
+ rather than the framework's `sr-only`: that one is in the generator's
93
+ TW_CLASH list, so the tailwind format ships no rule for it. */
94
+ .visually-hidden {
95
+ position: absolute;
96
+ inline-size: 1px;
97
+ block-size: 1px;
98
+ padding: 0;
99
+ margin: -1px;
100
+ overflow: hidden;
101
+ clip-path: inset(50%);
102
+ white-space: nowrap;
103
+ border: 0;
104
+ }
90
105
  </style>
@@ -1,4 +1,19 @@
1
1
  ---
2
+ /**
3
+ * Subgrid — re-emits slotted children as `<li>` items of a `.subgrid` grid, so
4
+ * the tranches inside each one align on shared row lines. Every declaration
5
+ * lives in the framework's `subgrid` pattern (patterns/subgrid.css), which is
6
+ * emitted in all three output formats; this file is markup only.
7
+ *
8
+ * Nothing here may reach for a Tailwind utility: Tailwind doesn't scan
9
+ * node_modules, so a class only it can emit is silently never generated — and
10
+ * classes in the generator's TW_CLASH list are stripped from the framework's
11
+ * own tailwind bundle by design.
12
+ *
13
+ * Size it from the consumer's own CSS, which keeps media-query overrides
14
+ * working: `--subgrid-cols`, `--subgrid-row-span`, `--subgrid-gap`,
15
+ * `--subgrid-row-gap`. `--num-items` is set here for nth-child-style maths.
16
+ */
2
17
  import type { HTMLAttributes } from 'astro/types';
3
18
  import { parseRenderedSlots, getAttribute, serialize, styleList } from '@getvitops/utils';
4
19
 
@@ -10,23 +25,8 @@ const nodes = parseRenderedSlots(await Astro.slots.render('default'));
10
25
  const slots = nodes.filter(node => 'tagName' in node).map(node => ({ html: serialize(node), attrs: node.attrs }));
11
26
  ---
12
27
 
13
- <style>
14
- ul {
15
- grid-template-columns: repeat(var(--cols, var(--_cols, 1)), minmax(0, 1fr));
16
- }
17
-
18
- li {
19
- --current-row: round(down, calc((sibling-index() - 1) / var(--cols)), 1);
20
-
21
- /* Apply the extra gap ONLY if the row index is > 0.
22
- We use clamp to ensure the multiplier is either 0 or 1.
23
- */
24
- --row-gap-multiplier: clamp(0, var(--current-row), 1);
25
- margin-block-start: calc(var(--row-gap-multiplier) * var(--row-margin));
26
- }
27
- </style>
28
- <ul class:list={["grid", className]} style={styleList({'--num-items': slots.length}, style)}>
28
+ <ul class:list={["subgrid", className]} style={styleList({'--num-items': slots.length}, style)}>
29
29
  {slots.map((slot) => (
30
- <li class:list={["grid grid-rows-subgrid row-span-(--row-span)", getAttribute(slot, 'class')]} style={styleList(getAttribute(slot, 'style'))} set:html={slot.html}></li>
30
+ <li class:list={[getAttribute(slot, 'class')]} style={styleList(getAttribute(slot, 'style'))} set:html={slot.html}></li>
31
31
  ))}
32
- </ul>
32
+ </ul>