@signal9/era-ui 2.26.0 → 2.27.1
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/dist/dev/audit/audits/collapsed-text-trim.js +44 -16
- package/dist/docs/button.md +1 -0
- package/dist/docs/css-utilities.d.ts +21 -0
- package/dist/docs/css-utilities.js +124 -0
- package/dist/docs/generated-docs.d.ts +2 -0
- package/dist/docs/generated-docs.js +7 -0
- package/dist/docs/index.js +4 -0
- package/dist/docs/llms-txt.js +17 -1
- package/dist/docs/scroll-area.md +7 -0
- package/dist/docs/utilities.d.ts +4 -0
- package/dist/docs/utilities.js +15 -0
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/button.md +1 -0
- package/dist/generated-docs/llms-full.txt +224 -1
- package/dist/generated-docs/llms.txt +15 -1
- package/dist/generated-docs/manifest.json +72 -3
- package/dist/generated-docs/scroll-area.md +8 -0
- package/dist/generated-docs/utilities.json +53 -0
- package/dist/generated-docs/utilities.md +195 -0
- package/dist/styles/index.css +44 -15
- package/dist/styles/surfaces/glass.css +5 -1
- package/dist/ui/input/input.svelte +5 -1
- package/package.json +3 -2
- package/skill/SKILL.md +17 -0
|
@@ -2,37 +2,65 @@
|
|
|
2
2
|
* The chopped-text bug.
|
|
3
3
|
*
|
|
4
4
|
* `era-text-trim` centres the ink by trimming the line box down to the x-height
|
|
5
|
-
* band (text-box: trim-both ex alphabetic).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* band (text-box: trim-both ex alphabetic). It has two failure modes, and they
|
|
6
|
+
* are NOT the same check — the first one is why the Input shipped with every
|
|
7
|
+
* ascender sliced off the text you typed while this audit sat here reporting
|
|
8
|
+
* nothing.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 1. NATIVE EDITABLES (<input>, <textarea>, <select>).
|
|
11
|
+
* A fixed height does NOT make these safe. The UA lays out an inner editor
|
|
12
|
+
* box and clips to IT, and that inner box is the trimmed line box — so the
|
|
13
|
+
* trim collapses it to the x-height band no matter how tall the control is,
|
|
14
|
+
* and the glyphs lose their tops and tails ("d" renders as "o"). The
|
|
15
|
+
* element's own height therefore says nothing about whether it is broken:
|
|
16
|
+
* the trim being ACTIVE is the whole defect. These elements also hold their
|
|
17
|
+
* text in `value`, not in textContent, which is the other reason the old
|
|
18
|
+
* height-and-textContent check could never fire on them.
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* 2. AUTO-HEIGHT CLIPPING ELEMENTS (a `truncate` child, overflow-hidden with no
|
|
21
|
+
* height). Nothing holds the box open, so it collapses to the ~7.5px band and
|
|
22
|
+
* the clip chops the glyphs into a middle stripe. Here the tell IS geometric:
|
|
23
|
+
* a box shorter than its own font-size that is still clipping. A FIXED-height
|
|
24
|
+
* non-editable box is fine — the ink overflows the trimmed LINE box but stays
|
|
25
|
+
* inside the padding box, which is what clips.
|
|
17
26
|
*/
|
|
27
|
+
const EDITABLE = 'input, textarea, select';
|
|
28
|
+
/** The trim, as the browser actually resolved it. Chromium exposes the longhand. */
|
|
29
|
+
function activeTrim(s) {
|
|
30
|
+
const trim = s.getPropertyValue('text-box-trim').trim() || s.getPropertyValue('text-box').trim();
|
|
31
|
+
if (!trim || trim === 'none' || trim === 'normal')
|
|
32
|
+
return null;
|
|
33
|
+
return trim;
|
|
34
|
+
}
|
|
18
35
|
export const collapsedTextTrim = {
|
|
19
36
|
id: 'typography/collapsed-text-trim',
|
|
20
37
|
name: 'Trimmed text box has not collapsed',
|
|
21
|
-
description: 'era-text-trim
|
|
38
|
+
description: 'era-text-trim clips the glyphs when it lands on a native editable (any height) or on an auto-height element that clips its overflow.',
|
|
22
39
|
category: 'layout',
|
|
23
40
|
severity: 'error',
|
|
24
41
|
selector: '*',
|
|
25
42
|
check(el) {
|
|
26
43
|
const s = getComputedStyle(el);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!trim || trim === 'none' || trim === 'normal')
|
|
44
|
+
const trim = activeTrim(s);
|
|
45
|
+
if (!trim)
|
|
30
46
|
return null;
|
|
31
|
-
//
|
|
47
|
+
// (1) A native editable is broken by the trim at ANY height — it clips to the
|
|
48
|
+
// trimmed inner editor box. No geometry test, and no textContent test: an
|
|
49
|
+
// input's text lives in `value`, and an EMPTY input is just as broken, it
|
|
50
|
+
// simply has nothing to chop yet.
|
|
51
|
+
if (el.matches(EDITABLE)) {
|
|
52
|
+
const issue = {
|
|
53
|
+
auditId: 'typography/collapsed-text-trim',
|
|
54
|
+
element: el,
|
|
55
|
+
message: `text-box trim (${trim}) on a <${el.tagName.toLowerCase()}> — a native editable clips to the trimmed inner editor box, so the text you type loses its ascenders and descenders ("d" renders as "o"). Drop the trim; the tier height centres it.`,
|
|
56
|
+
details: { trim, tag: el.tagName.toLowerCase() }
|
|
57
|
+
};
|
|
58
|
+
return issue;
|
|
59
|
+
}
|
|
60
|
+
// (2) Everything else: only a collapsed, clipping box is a defect.
|
|
32
61
|
const clips = /hidden|clip|auto|scroll/.test(s.overflowX + s.overflowY);
|
|
33
62
|
if (!clips)
|
|
34
63
|
return null;
|
|
35
|
-
// Does it hold any text of its own?
|
|
36
64
|
if (!el.textContent?.trim())
|
|
37
65
|
return null;
|
|
38
66
|
const h = el.getBoundingClientRect().height;
|
package/dist/docs/button.md
CHANGED
|
@@ -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'] ?? '';
|
package/dist/docs/index.js
CHANGED
|
@@ -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('-')
|
package/dist/docs/llms-txt.js
CHANGED
|
@@ -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
|
-
'
|
|
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,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);
|