@signal9/era-ui 2.26.0 → 2.27.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.
@@ -15,4 +15,5 @@
15
15
 
16
16
  - Heights, radii, paddings, and gaps come from density tokens — never hard-code px.
17
17
  - `link` variant intentionally does not change color on hover (underline only) so it reads as static prose until confirmed interactive.
18
+ - `variant="link"` is for a _control_ that should look like a link (a button that reveals a panel, a destructive action in a row). For a link in body copy or prose, use the `era-link` CSS utility on an `<a>` instead — see [Utilities](/utilities.md).
18
19
  - For a segmented row of buttons, use `ButtonGroup`.
@@ -0,0 +1,21 @@
1
+ export interface CssUtility {
2
+ /** Class name, e.g. `era-link`. */
3
+ name: string;
4
+ /** Stylesheet the utility is declared in, relative to src/lib/styles. */
5
+ file: string;
6
+ /** One-line use case from the comment's `@use` line, if present. */
7
+ useCase?: string;
8
+ /** The rest of the doc comment — rationale, caveats, axis behaviour. */
9
+ description: string;
10
+ /** The `@utility` block verbatim, as authored. */
11
+ css: string;
12
+ }
13
+ /**
14
+ * @param sources stylesheet text keyed by path relative to src/lib/styles
15
+ * (e.g. `index.css`, `surfaces/glass.css`).
16
+ */
17
+ export declare function parseUtilities(sources: Record<string, string>): CssUtility[];
18
+ /** Renders the utilities reference — the generated `utilities.md` body. */
19
+ export declare function buildUtilitiesDoc(utilities: CssUtility[]): string;
20
+ /** One line per utility for llms.txt — name plus its searchable use case. */
21
+ export declare function buildUtilitiesIndex(utilities: CssUtility[]): string[];
@@ -0,0 +1,124 @@
1
+ /*
2
+ * Extracts era's Tailwind v4 `@utility` classes — era-link, era-text-trim,
3
+ * era-shimmer, scrollbar-none, glass-blur, … — from the stylesheet source.
4
+ *
5
+ * These are first-class API but they have no JS export and no .svelte file, so
6
+ * nothing else in the doc pipeline can see them: the component barrel misses
7
+ * them, the registry misses them, and a consumer (or an agent) reasoning from
8
+ * "what does era expose for links?" never finds `era-link`. The doc comment
9
+ * above each `@utility` is already thorough, so it — not a hand-maintained list
10
+ * — is the source of truth. This parses it.
11
+ *
12
+ * The convention the parser leans on: an `@use ` line in the comment is the
13
+ * one-line use case (what an agent searches), and the remaining prose is the
14
+ * long description. A utility with no `@use` line still documents fine; it just
15
+ * lands in the docs without a use-case summary.
16
+ *
17
+ * Pure (takes sources, returns data) so both the runtime glob loader and the
18
+ * node build script can drive it — same split as component-source-scanners.ts.
19
+ */
20
+ /** Files whose utilities are library API. Order fixes the doc's order. */
21
+ const FILE_ORDER = ['index.css', 'surfaces/glass.css'];
22
+ function cleanComment(raw) {
23
+ return raw
24
+ .split('\n')
25
+ .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd())
26
+ .join('\n')
27
+ .trim();
28
+ }
29
+ /** Split the comment into its `@use` one-liner and the surrounding prose. */
30
+ function splitUseCase(comment) {
31
+ const lines = comment.split('\n');
32
+ const start = lines.findIndex((line) => line.startsWith('@use '));
33
+ if (start === -1)
34
+ return { description: comment.trim() };
35
+ let end = start + 1;
36
+ while (end < lines.length && lines[end].trim() !== '')
37
+ end++;
38
+ const useCase = lines
39
+ .slice(start, end)
40
+ .join(' ')
41
+ .replace(/^@use\s+/, '')
42
+ .replace(/\s+/g, ' ')
43
+ .trim();
44
+ const description = [...lines.slice(0, start), ...lines.slice(end)].join('\n').trim();
45
+ return { useCase, description };
46
+ }
47
+ /** Walk from the opening brace to its match so nested rules survive intact. */
48
+ function blockEnd(source, openBrace) {
49
+ let depth = 0;
50
+ for (let i = openBrace; i < source.length; i++) {
51
+ if (source[i] === '{')
52
+ depth++;
53
+ else if (source[i] === '}' && --depth === 0)
54
+ return i;
55
+ }
56
+ return source.length - 1;
57
+ }
58
+ function parseFile(file, source) {
59
+ const out = [];
60
+ // The comment body may not contain a terminator — a lazy `[\s\S]*?` would
61
+ // happily start at the first comment in the file and run through every `*/`
62
+ // between there and the @utility.
63
+ const pattern = /\/\*((?:(?!\*\/)[\s\S])*)\*\/\s*@utility\s+([\w-]+)\s*\{/g;
64
+ for (const match of source.matchAll(pattern)) {
65
+ const openBrace = match.index + match[0].length - 1;
66
+ const end = blockEnd(source, openBrace);
67
+ const { useCase, description } = splitUseCase(cleanComment(match[1]));
68
+ out.push({
69
+ name: match[2],
70
+ file,
71
+ useCase,
72
+ description,
73
+ css: source.slice(source.indexOf('@utility', match.index), end + 1)
74
+ });
75
+ }
76
+ return out;
77
+ }
78
+ /**
79
+ * @param sources stylesheet text keyed by path relative to src/lib/styles
80
+ * (e.g. `index.css`, `surfaces/glass.css`).
81
+ */
82
+ export function parseUtilities(sources) {
83
+ const rank = (file) => {
84
+ const i = FILE_ORDER.indexOf(file);
85
+ return i === -1 ? FILE_ORDER.length : i;
86
+ };
87
+ return Object.entries(sources)
88
+ .sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b))
89
+ .flatMap(([file, source]) => parseFile(file, source));
90
+ }
91
+ const INTRO = [
92
+ 'era ships a handful of Tailwind v4 `@utility` classes for the patterns that are',
93
+ 'styling, not components — an inline link, ink-centred control text, a hidden',
94
+ 'scrollbar. They need no import: they ride along with the stylesheet.',
95
+ '',
96
+ '```ts',
97
+ 'import "@sig-nine/era-ui/css";',
98
+ '```',
99
+ '',
100
+ '```svelte',
101
+ '<a class="era-link" href="/spacing">the spacing ladder</a>',
102
+ '```'
103
+ ].join('\n');
104
+ /** Renders the utilities reference — the generated `utilities.md` body. */
105
+ export function buildUtilitiesDoc(utilities) {
106
+ const lines = ['## Overview', '', INTRO, '', '| Utility | Use it for |', '|---|---|'];
107
+ for (const u of utilities) {
108
+ lines.push(`| \`${u.name}\` | ${u.useCase ?? '—'} |`);
109
+ }
110
+ lines.push('');
111
+ for (const u of utilities) {
112
+ lines.push(`## ${u.name}`, '');
113
+ if (u.useCase)
114
+ lines.push(`**Use it for:** ${u.useCase}`, '');
115
+ if (u.description)
116
+ lines.push(u.description, '');
117
+ lines.push(`Declared in \`${u.file}\`.`, '', '```css', u.css, '```', '');
118
+ }
119
+ return lines.join('\n');
120
+ }
121
+ /** One line per utility for llms.txt — name plus its searchable use case. */
122
+ export function buildUtilitiesIndex(utilities) {
123
+ return utilities.map((u) => `- \`${u.name}\` — ${u.useCase ?? u.description.split('\n')[0]}`);
124
+ }
@@ -1,4 +1,6 @@
1
1
  export declare const docsBySlug: Record<string, string>;
2
2
  export declare const knownSlugs: Set<string>;
3
+ /** Served verbatim at /utilities.json so tooling can enumerate the CSS API. */
4
+ export declare const utilitiesJson: string;
3
5
  export declare const llmsTxtTemplate: string;
4
6
  export declare const llmsFullTxt: string;
@@ -12,6 +12,11 @@ const manifestModules = import.meta.glob('../generated-docs/manifest.json', {
12
12
  import: 'default',
13
13
  eager: true
14
14
  });
15
+ const utilitiesModules = import.meta.glob('../generated-docs/utilities.json', {
16
+ query: '?raw',
17
+ import: 'default',
18
+ eager: true
19
+ });
15
20
  function stem(path) {
16
21
  return path.replace(/^\.\.\/generated-docs\//, '').replace(/\.(md|txt)$/, '');
17
22
  }
@@ -19,5 +24,7 @@ export const docsBySlug = Object.fromEntries(Object.entries(markdownFiles).map((
19
24
  const manifest = Object.values(manifestModules)[0];
20
25
  const manifestSlugs = manifest?.entries?.map((entry) => entry.slug) ?? [];
21
26
  export const knownSlugs = new Set(manifestSlugs.length ? manifestSlugs : Object.keys(docsBySlug));
27
+ /** Served verbatim at /utilities.json so tooling can enumerate the CSS API. */
28
+ export const utilitiesJson = utilitiesModules['../generated-docs/utilities.json'] ?? '';
22
29
  export const llmsTxtTemplate = textFiles['../generated-docs/llms.txt'] ?? '';
23
30
  export const llmsFullTxt = textFiles['../generated-docs/llms-full.txt'] ?? '';
@@ -1,5 +1,6 @@
1
1
  import { components, guides } from '../../routes/registry.js';
2
2
  import { buildComponentDoc } from './component-docs.js';
3
+ import { utilitiesDoc } from './utilities.js';
3
4
  const rootUiIndexSources = import.meta.glob('../ui/index.ts', {
4
5
  query: '?raw',
5
6
  import: 'default',
@@ -14,6 +15,9 @@ function slugFromPath(path) {
14
15
  return path.replace(/^\.\//, '').replace(/\.md$/, '');
15
16
  }
16
17
  const appendices = Object.fromEntries(Object.entries(markdownModules).map(([path, body]) => [slugFromPath(path), body]));
18
+ // The utilities guide has no component source to synthesize from — its body is
19
+ // parsed out of the `@utility` doc comments in src/lib/styles instead.
20
+ appendices.utilities = [appendices.utilities, utilitiesDoc].filter(Boolean).join('\n\n');
17
21
  function titleFromSlug(slug) {
18
22
  return slug
19
23
  .split('-')
@@ -1,4 +1,6 @@
1
1
  import { docs, hasDoc, listEntries } from './index.js';
2
+ import { buildUtilitiesIndex } from './css-utilities.js';
3
+ import { utilities } from './utilities.js';
2
4
  import { modes, defaultMode, surfaces, defaultSurface, corners, defaultCorners, fonts, defaultFont, motions, defaultMotion } from '../ui/provider/index.js';
3
5
  const H1 = '# era-ui';
4
6
  const MISSION = '> A Svelte 5 + Bits UI component library. Every surface — heights, paddings, radii, gaps — derives from a single spacing atom, so density is a one-attribute override at any scope.';
@@ -49,6 +51,17 @@ function orientation(origin) {
49
51
  'Each page is generated from the component source, so props/variants/defaults stay in sync with the code automatically.'
50
52
  ].join('\n');
51
53
  }
54
+ function utilitiesSection(origin) {
55
+ return [
56
+ '## CSS utilities',
57
+ '',
58
+ 'Classes, not components — they need no import beyond the stylesheet. Reach for one of these before hand-rolling the same styling or bending a component into the role.',
59
+ '',
60
+ ...buildUtilitiesIndex(utilities),
61
+ '',
62
+ `Full reference (what each emits, when to use it): \`${origin}/utilities.md\`. Machine-readable list: \`${origin}/utilities.json\`.`
63
+ ].join('\n');
64
+ }
52
65
  export function buildLlmsTxt(origin = '') {
53
66
  const entries = listEntries();
54
67
  const lines = [H1, '', MISSION, '', orientation(origin), ''];
@@ -56,6 +69,7 @@ export function buildLlmsTxt(origin = '') {
56
69
  lines.push('## Components', '');
57
70
  lines.push(entries.map((e) => e.slug).join(', '), '');
58
71
  }
72
+ lines.push(utilitiesSection(origin), '');
59
73
  return lines.join('\n');
60
74
  }
61
75
  // Module-init-cached: the concatenated reference has no request-dependent
@@ -73,7 +87,9 @@ export function buildLlmsFullTxt() {
73
87
  '',
74
88
  orientation(''),
75
89
  '',
76
- '<!-- Full component documentation, concatenated. -->',
90
+ utilitiesSection(''),
91
+ '',
92
+ '<!-- Full component documentation, concatenated. The utilities page below carries the CSS each utility emits. -->',
77
93
  ''
78
94
  ];
79
95
  for (const e of entries) {
@@ -0,0 +1,7 @@
1
+ ## Notes
2
+
3
+ - For a region that should scroll but show no scrollbar at all — an overflowing
4
+ tab strip, a chip rail, a small pane handle — you don't need ScrollArea: put
5
+ the `scrollbar-none` CSS utility on the overflowing element. See
6
+ [Utilities](/utilities.md). ScrollArea is for the case where the bar itself is
7
+ part of the design.
@@ -0,0 +1,4 @@
1
+ import { type CssUtility } from './css-utilities.js';
2
+ export declare const utilities: CssUtility[];
3
+ export declare const utilitiesDoc: string;
4
+ export type { CssUtility };
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Runtime side of the CSS-utility docs: globs the shipped stylesheets and parses
3
+ * their `@utility` blocks. The node build script (scripts/build-llm-docs.ts)
4
+ * feeds the same parser from the filesystem, so the site and the generated
5
+ * markdown can never disagree about what era exposes.
6
+ */
7
+ import { buildUtilitiesDoc, parseUtilities } from './css-utilities.js';
8
+ const cssSources = import.meta.glob('../styles/**/*.css', {
9
+ query: '?raw',
10
+ import: 'default',
11
+ eager: true
12
+ });
13
+ const sources = Object.fromEntries(Object.entries(cssSources).map(([path, body]) => [path.replace(/^\.\.\/styles\//, ''), body]));
14
+ export const utilities = parseUtilities(sources);
15
+ export const utilitiesDoc = buildUtilitiesDoc(utilities);