@d0nj/dsh-ui-custom 0.0.0-stage → 0.1.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/LICENSE +24 -0
- package/README.md +82 -2
- package/cordis.patch.yml +22 -0
- package/dist/client/client.js +1313 -0
- package/dist/client/index.d.ts +69 -0
- package/dist/client/index.js +324 -0
- package/dist/client/panel.d.ts +85 -0
- package/dist/client/panel.js +294 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +143 -0
- package/dist/resolve.d.ts +116 -0
- package/dist/resolve.js +149 -0
- package/dist/spec.d.ts +101 -0
- package/dist/spec.js +509 -0
- package/icon.svg +15 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -3
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a customization document into theme tokens.
|
|
3
|
+
*
|
|
4
|
+
* Three inputs combine, in this order:
|
|
5
|
+
*
|
|
6
|
+
* 1. A **preset**, which is a complete token set.
|
|
7
|
+
* 2. **Per-token overrides**, applied on top of the preset.
|
|
8
|
+
* 3. **Geometry**, expanded into whichever tokens it can express.
|
|
9
|
+
*
|
|
10
|
+
* The result is one `ThemeTokenOverrides` object — token name to `{light, dark}` —
|
|
11
|
+
* which is exactly what `ctx.theme.overrideTokens` accepts. Keeping the merge
|
|
12
|
+
* here, as a pure function, is what lets the panel preview a change and the Host
|
|
13
|
+
* validate one from the same code.
|
|
14
|
+
*
|
|
15
|
+
* @module @d0nj/dsh-ui-custom/resolve
|
|
16
|
+
*/
|
|
17
|
+
import { DENSITIES, FONT_SIZE_RANGES, RADIUS_MAX, RADIUS_MIN, TOKEN_NAMES, findPreset, tokenSpec, } from './spec.js';
|
|
18
|
+
/** A colour in one of the forms the theme service and browsers accept. */
|
|
19
|
+
const COLOR_PATTERN = /^(#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})|(?:rgb|hsl|oklch|lab|lch|color)\([^)]*\)|[a-z]+)$/iu;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a value is a plausible CSS colour.
|
|
22
|
+
*
|
|
23
|
+
* Colour tokens are rendered straight into CSS custom properties, so an
|
|
24
|
+
* arbitrary string would not be a security hole but would silently produce an
|
|
25
|
+
* unstyled surface. Rejecting it here turns that into a visible warning.
|
|
26
|
+
* @param value - candidate colour value.
|
|
27
|
+
* @returns true when the value looks like a CSS colour.
|
|
28
|
+
*/
|
|
29
|
+
export function isColor(value) {
|
|
30
|
+
return COLOR_PATTERN.test(value.trim());
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Clamp a number into a closed range.
|
|
34
|
+
* @param value - candidate value.
|
|
35
|
+
* @param min - lower bound.
|
|
36
|
+
* @param max - upper bound.
|
|
37
|
+
* @returns the clamped integer.
|
|
38
|
+
*/
|
|
39
|
+
export function clamp(value, min, max) {
|
|
40
|
+
if (!Number.isFinite(value))
|
|
41
|
+
return min;
|
|
42
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Expand geometry into token values.
|
|
46
|
+
*
|
|
47
|
+
* The documented token set has no radius or spacing tokens, so geometry is
|
|
48
|
+
* expressed as a *shadow layer* of `--d0nj-*` variables the panel's own styles
|
|
49
|
+
* consume, plus any `--dsw-*` tokens that exist. Emitting our own names keeps
|
|
50
|
+
* this additive: a token the harness does not define cannot break a surface,
|
|
51
|
+
* and the panel styles read ours.
|
|
52
|
+
*
|
|
53
|
+
* @param geometry - the requested geometry.
|
|
54
|
+
* @returns derived custom properties with identical light and dark values.
|
|
55
|
+
*/
|
|
56
|
+
export function geometryTokens(geometry) {
|
|
57
|
+
const radius = clamp(geometry.radius, RADIUS_MIN, RADIUS_MAX);
|
|
58
|
+
const scale = geometry.density === 'compact' ? 0.8 : geometry.density === 'spacious' ? 1.25 : 1;
|
|
59
|
+
const value = (px) => ({ light: px, dark: px });
|
|
60
|
+
return {
|
|
61
|
+
'--d0nj-radius-sm': value(`${String(Math.round(radius * 0.5))}px`),
|
|
62
|
+
'--d0nj-radius-md': value(`${String(radius)}px`),
|
|
63
|
+
'--d0nj-radius-lg': value(`${String(Math.round(radius * 1.5))}px`),
|
|
64
|
+
'--d0nj-space-1': value(`${String(Math.round(4 * scale))}px`),
|
|
65
|
+
'--d0nj-space-2': value(`${String(Math.round(8 * scale))}px`),
|
|
66
|
+
'--d0nj-space-3': value(`${String(Math.round(12 * scale))}px`),
|
|
67
|
+
'--d0nj-space-4': value(`${String(Math.round(16 * scale))}px`),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Correct one role's font size into its accepted range.
|
|
72
|
+
* @param role - font role.
|
|
73
|
+
* @param px - requested size.
|
|
74
|
+
* @returns the in-range size.
|
|
75
|
+
*/
|
|
76
|
+
export function correctFontSize(role, px) {
|
|
77
|
+
const range = FONT_SIZE_RANGES[role];
|
|
78
|
+
return clamp(px, range.min, range.max);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether a string names a known density.
|
|
82
|
+
* @param value - candidate density.
|
|
83
|
+
* @returns true for a known density.
|
|
84
|
+
*/
|
|
85
|
+
export function isDensity(value) {
|
|
86
|
+
return DENSITIES.includes(value);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a customization document into theme tokens and typography.
|
|
90
|
+
*
|
|
91
|
+
* Merge order is preset, then explicit overrides, then geometry, so a user who
|
|
92
|
+
* picks a preset and then tweaks one token keeps the tweak, and geometry always
|
|
93
|
+
* wins over a token of the same name (it is the more specific control).
|
|
94
|
+
*
|
|
95
|
+
* @param document - the customization document.
|
|
96
|
+
* @returns the token overrides, corrected typography, and any warnings.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveCustomization(document) {
|
|
99
|
+
const warnings = [];
|
|
100
|
+
const tokens = {};
|
|
101
|
+
if (document.basePreset !== 'none') {
|
|
102
|
+
const preset = findPreset(document.basePreset);
|
|
103
|
+
if (preset === undefined) {
|
|
104
|
+
warnings.push(`unknown preset "${document.basePreset}"; no base tokens applied`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
for (const [name, modes] of Object.entries(preset.tokens))
|
|
108
|
+
tokens[name] = modes;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const [name, modes] of Object.entries(document.tokens)) {
|
|
112
|
+
const spec = tokenSpec(name);
|
|
113
|
+
if (spec === undefined) {
|
|
114
|
+
// A hand-edited document can name a token this build does not document.
|
|
115
|
+
// Skipping it with a warning beats applying a property nothing reads.
|
|
116
|
+
warnings.push(`ignoring undocumented token "${name}"`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const light = modes.light.trim();
|
|
120
|
+
const dark = modes.dark.trim();
|
|
121
|
+
if (light.length === 0 || dark.length === 0) {
|
|
122
|
+
warnings.push(`token "${name}" needs both a light and a dark value`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (spec.kind === 'color' && (!isColor(light) || !isColor(dark))) {
|
|
126
|
+
warnings.push(`token "${name}" is not a valid CSS colour`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
tokens[name] = { light, dark };
|
|
130
|
+
}
|
|
131
|
+
for (const [name, modes] of Object.entries(geometryTokens(document.geometry))) {
|
|
132
|
+
tokens[name] = modes;
|
|
133
|
+
}
|
|
134
|
+
const typography = {
|
|
135
|
+
textFontFamily: document.typography.textFontFamily,
|
|
136
|
+
codeFontFamily: document.typography.codeFontFamily,
|
|
137
|
+
terminalFontFamily: document.typography.terminalFontFamily,
|
|
138
|
+
fontSize: correctFontSize('text', document.typography.fontSize),
|
|
139
|
+
codeFontSize: correctFontSize('code', document.typography.codeFontSize),
|
|
140
|
+
terminalFontSize: correctFontSize('terminal', document.typography.terminalFontSize),
|
|
141
|
+
};
|
|
142
|
+
if (typography.fontSize !== document.typography.fontSize) {
|
|
143
|
+
warnings.push(`text font size corrected to ${String(typography.fontSize)}px (allowed ${String(FONT_SIZE_RANGES.text.min)}–${String(FONT_SIZE_RANGES.text.max)})`);
|
|
144
|
+
}
|
|
145
|
+
return { tokens, typography, warnings };
|
|
146
|
+
}
|
|
147
|
+
/** Every documented token name, re-exported for the panel's reset action. */
|
|
148
|
+
export { TOKEN_NAMES };
|
|
149
|
+
//# sourceMappingURL=resolve.js.map
|
package/dist/spec.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token catalog and the shipped presets.
|
|
3
|
+
*
|
|
4
|
+
* Every override must supply **both** palette modes. The theme runtime requires
|
|
5
|
+
* it (`ThemeTokenModes`), because a token defined for only one scheme goes
|
|
6
|
+
* illegible when the user flips to the other — so this module represents that
|
|
7
|
+
* requirement in the type rather than trusting each caller to remember it.
|
|
8
|
+
*
|
|
9
|
+
* @module @d0nj/dsh-ui-custom/spec
|
|
10
|
+
*/
|
|
11
|
+
/** One token's value in each palette mode. */
|
|
12
|
+
export interface TokenModes {
|
|
13
|
+
readonly light: string;
|
|
14
|
+
readonly dark: string;
|
|
15
|
+
}
|
|
16
|
+
/** Whether a token is a colour (validated as a colour) or an arbitrary CSS value. */
|
|
17
|
+
export type TokenKind = 'color' | 'length' | 'shadow' | 'font';
|
|
18
|
+
/** One documented token the editor can change. */
|
|
19
|
+
export interface TokenSpec {
|
|
20
|
+
/** CSS custom property name, such as `--dsw-alias-bg-base`. */
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Short human label for the editor row. */
|
|
23
|
+
readonly label: string;
|
|
24
|
+
/** Grouping in the editor. */
|
|
25
|
+
readonly group: TokenGroup;
|
|
26
|
+
/** How the value is validated and rendered. */
|
|
27
|
+
readonly kind: TokenKind;
|
|
28
|
+
/** What the token affects, shown as the row hint. */
|
|
29
|
+
readonly description: string;
|
|
30
|
+
}
|
|
31
|
+
/** Editor grouping, ordered as the panel renders them. */
|
|
32
|
+
export type TokenGroup = 'surface' | 'border' | 'text' | 'accent' | 'state';
|
|
33
|
+
/** Groups in display order, with their headings. */
|
|
34
|
+
export declare const TOKEN_GROUPS: readonly {
|
|
35
|
+
readonly id: TokenGroup;
|
|
36
|
+
readonly label: string;
|
|
37
|
+
}[];
|
|
38
|
+
/**
|
|
39
|
+
* The documented alias tokens.
|
|
40
|
+
*
|
|
41
|
+
* These are the tokens the theme Inspect provider reports for this build, so the
|
|
42
|
+
* editor never offers a row that would silently do nothing.
|
|
43
|
+
*/
|
|
44
|
+
export declare const TOKEN_SPECS: readonly TokenSpec[];
|
|
45
|
+
/** Every documented token name, for validation. */
|
|
46
|
+
export declare const TOKEN_NAMES: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Look up one token's specification.
|
|
49
|
+
* @param name - CSS custom property name.
|
|
50
|
+
* @returns the specification, or `undefined` when the token is not documented.
|
|
51
|
+
*/
|
|
52
|
+
export declare function tokenSpec(name: string): TokenSpec | undefined;
|
|
53
|
+
/** One shipped preset: a complete, legible theme in both modes. */
|
|
54
|
+
export interface ThemePreset {
|
|
55
|
+
/** Stable id, used as the `basePreset` config value. */
|
|
56
|
+
readonly id: string;
|
|
57
|
+
/** Display name. */
|
|
58
|
+
readonly label: string;
|
|
59
|
+
/** One-line description shown on the preset card. */
|
|
60
|
+
readonly description: string;
|
|
61
|
+
/** The base palette this preset reads as. */
|
|
62
|
+
readonly colorScheme: 'light' | 'dark';
|
|
63
|
+
/** Complete token set — every documented token, in both modes. */
|
|
64
|
+
readonly tokens: Readonly<Record<string, TokenModes>>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Shipped presets.
|
|
68
|
+
*
|
|
69
|
+
* Each is a complete token set rather than a partial overlay, so selecting one
|
|
70
|
+
* gives a predictable result no matter what the generated base palette does.
|
|
71
|
+
* The colours are chosen to keep body text at or above WCAG AA contrast against
|
|
72
|
+
* their own surfaces, which the tests check.
|
|
73
|
+
*/
|
|
74
|
+
export declare const PRESETS: readonly ThemePreset[];
|
|
75
|
+
/** Every preset id, for validation. */
|
|
76
|
+
export declare const PRESET_IDS: readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* Look up one preset by id.
|
|
79
|
+
* @param id - preset id.
|
|
80
|
+
* @returns the preset, or `undefined` for an unknown or `none` id.
|
|
81
|
+
*/
|
|
82
|
+
export declare function findPreset(id: string): ThemePreset | undefined;
|
|
83
|
+
/** The role names the theme runtime accepts for typography. */
|
|
84
|
+
export declare const FONT_ROLES: readonly ["text", "code", "terminal"];
|
|
85
|
+
/** One font role. */
|
|
86
|
+
export type FontRole = (typeof FONT_ROLES)[number];
|
|
87
|
+
/** Accepted font-size range per role, mirroring the shipped Appearance rows. */
|
|
88
|
+
export declare const FONT_SIZE_RANGES: Readonly<Record<FontRole, {
|
|
89
|
+
readonly min: number;
|
|
90
|
+
readonly max: number;
|
|
91
|
+
readonly fallback: number;
|
|
92
|
+
}>>;
|
|
93
|
+
/** Density presets, expressed as a spacing multiplier applied to derived tokens. */
|
|
94
|
+
export declare const DENSITIES: readonly ["compact", "comfortable", "spacious"];
|
|
95
|
+
/** One density option. */
|
|
96
|
+
export type Density = (typeof DENSITIES)[number];
|
|
97
|
+
/** Radius bounds in pixels. */
|
|
98
|
+
export declare const RADIUS_MIN = 0;
|
|
99
|
+
/** Upper radius bound in pixels. */
|
|
100
|
+
export declare const RADIUS_MAX = 24;
|
|
101
|
+
//# sourceMappingURL=spec.d.ts.map
|