@alacris/ui 0.0.0 → 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 +21 -0
- package/README.md +148 -1
- package/package.json +75 -3
- package/src/components/base.js +46 -0
- package/src/components/ui-accordion-item.js +157 -0
- package/src/components/ui-accordion.js +58 -0
- package/src/components/ui-alert.js +168 -0
- package/src/components/ui-app-bar.js +119 -0
- package/src/components/ui-autocomplete.js +371 -0
- package/src/components/ui-avatar.js +104 -0
- package/src/components/ui-backdrop.js +66 -0
- package/src/components/ui-badge.js +95 -0
- package/src/components/ui-bottom-app-bar.js +78 -0
- package/src/components/ui-bottom-nav.js +83 -0
- package/src/components/ui-breadcrumbs.js +100 -0
- package/src/components/ui-button-group.js +49 -0
- package/src/components/ui-button.js +143 -0
- package/src/components/ui-card.js +86 -0
- package/src/components/ui-carousel-item.js +53 -0
- package/src/components/ui-carousel.js +301 -0
- package/src/components/ui-checkbox.js +165 -0
- package/src/components/ui-chip-set.js +90 -0
- package/src/components/ui-chip.js +226 -0
- package/src/components/ui-container.js +59 -0
- package/src/components/ui-date-picker.js +774 -0
- package/src/components/ui-dialog.js +179 -0
- package/src/components/ui-divider.js +58 -0
- package/src/components/ui-drawer.js +169 -0
- package/src/components/ui-fab-menu.js +122 -0
- package/src/components/ui-fab.js +117 -0
- package/src/components/ui-icon-button.js +112 -0
- package/src/components/ui-icon.js +64 -0
- package/src/components/ui-list-item.js +166 -0
- package/src/components/ui-list.js +38 -0
- package/src/components/ui-loading-indicator.js +74 -0
- package/src/components/ui-menu-item.js +117 -0
- package/src/components/ui-menu.js +192 -0
- package/src/components/ui-nav-item.js +138 -0
- package/src/components/ui-nav-rail.js +111 -0
- package/src/components/ui-option.js +85 -0
- package/src/components/ui-pagination.js +173 -0
- package/src/components/ui-progress.js +100 -0
- package/src/components/ui-radio-group.js +104 -0
- package/src/components/ui-radio.js +143 -0
- package/src/components/ui-rating.js +126 -0
- package/src/components/ui-search.js +316 -0
- package/src/components/ui-select.js +418 -0
- package/src/components/ui-sheet.js +205 -0
- package/src/components/ui-side-sheet.js +221 -0
- package/src/components/ui-skeleton.js +89 -0
- package/src/components/ui-slider.js +266 -0
- package/src/components/ui-snackbar.js +233 -0
- package/src/components/ui-spinner.js +101 -0
- package/src/components/ui-split-button.js +158 -0
- package/src/components/ui-stack.js +37 -0
- package/src/components/ui-step.js +91 -0
- package/src/components/ui-stepper.js +86 -0
- package/src/components/ui-surface.js +51 -0
- package/src/components/ui-switch.js +153 -0
- package/src/components/ui-tab-panel.js +54 -0
- package/src/components/ui-tab.js +116 -0
- package/src/components/ui-table-footer.js +141 -0
- package/src/components/ui-table-toolbar.js +219 -0
- package/src/components/ui-table.js +735 -0
- package/src/components/ui-tabs.js +178 -0
- package/src/components/ui-text-field.js +319 -0
- package/src/components/ui-text.js +36 -0
- package/src/components/ui-time-picker.js +753 -0
- package/src/components/ui-toggle-button.js +170 -0
- package/src/components/ui-toggle-group.js +111 -0
- package/src/components/ui-toolbar.js +64 -0
- package/src/components/ui-tooltip.js +148 -0
- package/src/index.js +121 -0
- package/src/motion/animate.js +145 -0
- package/src/motion/flip.js +46 -0
- package/src/motion/index.js +4 -0
- package/src/motion/presence.js +104 -0
- package/src/motion/ripple.js +108 -0
- package/src/theme/apply-theme.js +142 -0
- package/src/theme/create-theme.js +70 -0
- package/src/theme/index.js +5 -0
- package/src/tokens/color.js +237 -0
- package/src/tokens/index.js +14 -0
- package/src/tokens/sys.js +76 -0
- package/src/tokens/system.js +114 -0
- package/src/tokens/typography.js +189 -0
- package/src/util/focus.js +89 -0
- package/src/util/form.js +69 -0
- package/src/util/icons.js +161 -0
- package/src/util/keys.js +110 -0
- package/src/util/position.js +102 -0
- package/src/util/table.js +230 -0
- package/types/elements.d.ts +78 -0
- package/types/index.d.ts +168 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Color engine — dependency-free tonal palettes in OKLCH.
|
|
2
|
+
//
|
|
3
|
+
// Material's color system is built on tonal palettes: thirteen-plus tones of a
|
|
4
|
+
// single hue/chroma, indexed by perceptual lightness 0–100, with semantic roles
|
|
5
|
+
// (primary, on-primary, surface-container, …) mapped onto specific tones per
|
|
6
|
+
// scheme. Google derives palettes in HCT; we get an equivalent result with
|
|
7
|
+
// OKLab/OKLCH: hold hue and chroma from the seed, target each tone's CIE
|
|
8
|
+
// luminance, and gamut-map by walking chroma down until the color fits sRGB.
|
|
9
|
+
//
|
|
10
|
+
// Everything here is pure math on hex strings — no DOM, usable in a worker or
|
|
11
|
+
// at build time to pregenerate a static theme.
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------- sRGB <-> hex
|
|
14
|
+
|
|
15
|
+
const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
|
16
|
+
|
|
17
|
+
export function hexToRgb(hex) {
|
|
18
|
+
let h = hex.replace('#', '').trim();
|
|
19
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
20
|
+
const n = parseInt(h, 16);
|
|
21
|
+
if (h.length !== 6 || Number.isNaN(n)) throw new Error(`invalid hex color: ${hex}`);
|
|
22
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rgbToHex([r, g, b]) {
|
|
26
|
+
const to = (v) => Math.round(clamp01(v / 255) * 255).toString(16).padStart(2, '0');
|
|
27
|
+
return '#' + to(r) + to(g) + to(b);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const srgbToLinear = (c) => {
|
|
31
|
+
const v = c / 255;
|
|
32
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const linearToSrgb = (v) => {
|
|
36
|
+
const c = v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
|
|
37
|
+
return clamp01(c) * 255;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------- OKLab/OKLCH
|
|
41
|
+
|
|
42
|
+
function linearToOklab(r, g, b) {
|
|
43
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
44
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
45
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
46
|
+
return [
|
|
47
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
48
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
49
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function oklabToLinear(L, a, b) {
|
|
54
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
55
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
56
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
57
|
+
return [
|
|
58
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
59
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
60
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** hex → { l, c, h } (OKLCH; h in degrees, 0 for achromatic). */
|
|
65
|
+
export function oklchFromHex(hex) {
|
|
66
|
+
const [r, g, b] = hexToRgb(hex);
|
|
67
|
+
const [L, a, bb] = linearToOklab(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b));
|
|
68
|
+
const c = Math.hypot(a, bb);
|
|
69
|
+
const h = c < 1e-6 ? 0 : ((Math.atan2(bb, a) * 180) / Math.PI + 360) % 360;
|
|
70
|
+
return { l: L, c, h };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const inGamut = ([r, g, b]) =>
|
|
74
|
+
r >= -1e-4 && r <= 1 + 1e-4 && g >= -1e-4 && g <= 1 + 1e-4 && b >= -1e-4 && b <= 1 + 1e-4;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* { l, c, h } → hex. If the color is outside sRGB, chroma is reduced (binary
|
|
78
|
+
* search) until it fits — hue and lightness are preserved, which is what keeps
|
|
79
|
+
* a tonal ramp looking like one hue.
|
|
80
|
+
*/
|
|
81
|
+
export function hexFromOklch({ l, c, h }) {
|
|
82
|
+
const rad = (h * Math.PI) / 180;
|
|
83
|
+
const attempt = (cc) => oklabToLinear(l, cc * Math.cos(rad), cc * Math.sin(rad));
|
|
84
|
+
let rgb = attempt(c);
|
|
85
|
+
if (!inGamut(rgb)) {
|
|
86
|
+
let lo = 0, hi = c;
|
|
87
|
+
for (let i = 0; i < 20; i++) {
|
|
88
|
+
const mid = (lo + hi) / 2;
|
|
89
|
+
if (inGamut(attempt(mid))) lo = mid;
|
|
90
|
+
else hi = mid;
|
|
91
|
+
}
|
|
92
|
+
rgb = attempt(lo);
|
|
93
|
+
}
|
|
94
|
+
return rgbToHex(rgb.map(linearToSrgb));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------------- tones
|
|
98
|
+
|
|
99
|
+
// Material tone T is CIE L* — convert to luminance Y, and for near-neutral
|
|
100
|
+
// colors OKLab lightness is exactly cbrt(Y), which anchors the ramp so that
|
|
101
|
+
// tone 100 is white, tone 0 is black, and tone 50 is mid-gray to the eye.
|
|
102
|
+
const toneToOklabL = (tone) => {
|
|
103
|
+
const L = tone;
|
|
104
|
+
const Y = L > 8 ? Math.pow((L + 16) / 116, 3) : L / 903.2962962;
|
|
105
|
+
return Math.cbrt(Y);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** The tone steps Material's schemes draw from. */
|
|
109
|
+
export const TONES = [0, 4, 6, 10, 12, 17, 20, 22, 24, 30, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A full tonal palette from one hue/chroma.
|
|
113
|
+
* Returns { 0: '#000000', …, 100: '#ffffff' } over TONES.
|
|
114
|
+
*/
|
|
115
|
+
export function tonalPalette(hue, chroma) {
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const tone of TONES) {
|
|
118
|
+
out[tone] =
|
|
119
|
+
tone === 0 ? '#000000'
|
|
120
|
+
: tone === 100 ? '#ffffff'
|
|
121
|
+
: hexFromOklch({ l: toneToOklabL(tone), c: chroma, h: hue });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// -------------------------------------------------------------- key palettes
|
|
127
|
+
|
|
128
|
+
// Fixed seeds for the status hues (Material error, plus the MUI-style
|
|
129
|
+
// success/warning/info set). Each can be overridden via createTheme colors.
|
|
130
|
+
export const DEFAULT_SEED = '#e8ad18';
|
|
131
|
+
const STATUS_SEEDS = { error: '#b3261e', success: '#1e8e3e', warning: '#e37400', info: '#0b57d0' };
|
|
132
|
+
|
|
133
|
+
// Chroma floors/targets in OKLCH units (roughly HCT chroma / 350).
|
|
134
|
+
const MIN_VIVID = 0.09;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build the key tonal palettes from a seed (or explicit per-palette seeds).
|
|
138
|
+
*
|
|
139
|
+
* makePalettes({ seed: '#e8ad18' })
|
|
140
|
+
* makePalettes({ colors: { primary: '#0b57d0', tertiary: '#00695c' } })
|
|
141
|
+
*
|
|
142
|
+
* Returns { primary, secondary, tertiary, neutral, neutralVariant,
|
|
143
|
+
* error, success, warning, info } — each a tonal palette.
|
|
144
|
+
*/
|
|
145
|
+
export function makePalettes({ seed = DEFAULT_SEED, colors = {} } = {}) {
|
|
146
|
+
const base = oklchFromHex(colors.primary || seed);
|
|
147
|
+
const c = Math.max(base.c, MIN_VIVID);
|
|
148
|
+
const from = (name, fallbackHue, fallbackChroma) => {
|
|
149
|
+
if (colors[name]) {
|
|
150
|
+
const k = oklchFromHex(colors[name]);
|
|
151
|
+
return tonalPalette(k.h, Math.max(k.c, name.startsWith('neutral') ? k.c : MIN_VIVID * 0.6));
|
|
152
|
+
}
|
|
153
|
+
return tonalPalette(fallbackHue, fallbackChroma);
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
primary: from('primary', base.h, c),
|
|
157
|
+
secondary: from('secondary', base.h, c / 3),
|
|
158
|
+
tertiary: from('tertiary', (base.h + 60) % 360, c / 2),
|
|
159
|
+
neutral: from('neutral', base.h, 0.012),
|
|
160
|
+
neutralVariant: from('neutralVariant', base.h, 0.024),
|
|
161
|
+
error: from('error', ...seedHC(colors.error || STATUS_SEEDS.error)),
|
|
162
|
+
success: from('success', ...seedHC(colors.success || STATUS_SEEDS.success)),
|
|
163
|
+
warning: from('warning', ...seedHC(colors.warning || STATUS_SEEDS.warning)),
|
|
164
|
+
info: from('info', ...seedHC(colors.info || STATUS_SEEDS.info)),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const seedHC = (hex) => {
|
|
169
|
+
const k = oklchFromHex(hex);
|
|
170
|
+
return [k.h, Math.max(k.c, MIN_VIVID)];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// ------------------------------------------------------------------ schemes
|
|
174
|
+
|
|
175
|
+
// Role → [palette, lightTone, darkTone]. This is the Material scheme mapping,
|
|
176
|
+
// extended with success/warning/info role sets.
|
|
177
|
+
const ROLES = {
|
|
178
|
+
primary: ['primary', 40, 80],
|
|
179
|
+
onPrimary: ['primary', 100, 20],
|
|
180
|
+
primaryContainer: ['primary', 90, 30],
|
|
181
|
+
onPrimaryContainer: ['primary', 10, 90],
|
|
182
|
+
secondary: ['secondary', 40, 80],
|
|
183
|
+
onSecondary: ['secondary', 100, 20],
|
|
184
|
+
secondaryContainer: ['secondary', 90, 30],
|
|
185
|
+
onSecondaryContainer: ['secondary', 10, 90],
|
|
186
|
+
tertiary: ['tertiary', 40, 80],
|
|
187
|
+
onTertiary: ['tertiary', 100, 20],
|
|
188
|
+
tertiaryContainer: ['tertiary', 90, 30],
|
|
189
|
+
onTertiaryContainer: ['tertiary', 10, 90],
|
|
190
|
+
error: ['error', 40, 80],
|
|
191
|
+
onError: ['error', 100, 20],
|
|
192
|
+
errorContainer: ['error', 90, 30],
|
|
193
|
+
onErrorContainer: ['error', 10, 90],
|
|
194
|
+
success: ['success', 40, 80],
|
|
195
|
+
onSuccess: ['success', 100, 20],
|
|
196
|
+
successContainer: ['success', 90, 30],
|
|
197
|
+
onSuccessContainer: ['success', 10, 90],
|
|
198
|
+
warning: ['warning', 40, 80],
|
|
199
|
+
onWarning: ['warning', 100, 20],
|
|
200
|
+
warningContainer: ['warning', 90, 30],
|
|
201
|
+
onWarningContainer: ['warning', 10, 90],
|
|
202
|
+
info: ['info', 40, 80],
|
|
203
|
+
onInfo: ['info', 100, 20],
|
|
204
|
+
infoContainer: ['info', 90, 30],
|
|
205
|
+
onInfoContainer: ['info', 10, 90],
|
|
206
|
+
surface: ['neutral', 98, 6],
|
|
207
|
+
surfaceDim: ['neutral', 87, 6],
|
|
208
|
+
surfaceBright: ['neutral', 98, 24],
|
|
209
|
+
surfaceContainerLowest: ['neutral', 100, 4],
|
|
210
|
+
surfaceContainerLow: ['neutral', 96, 10],
|
|
211
|
+
surfaceContainer: ['neutral', 94, 12],
|
|
212
|
+
surfaceContainerHigh: ['neutral', 92, 17],
|
|
213
|
+
surfaceContainerHighest: ['neutral', 90, 22],
|
|
214
|
+
onSurface: ['neutral', 10, 90],
|
|
215
|
+
surfaceVariant: ['neutralVariant', 90, 30],
|
|
216
|
+
onSurfaceVariant: ['neutralVariant', 30, 80],
|
|
217
|
+
outline: ['neutralVariant', 50, 60],
|
|
218
|
+
outlineVariant: ['neutralVariant', 80, 30],
|
|
219
|
+
inverseSurface: ['neutral', 20, 90],
|
|
220
|
+
inverseOnSurface: ['neutral', 95, 20],
|
|
221
|
+
inversePrimary: ['primary', 80, 40],
|
|
222
|
+
scrim: ['neutral', 0, 0],
|
|
223
|
+
shadow: ['neutral', 0, 0],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const COLOR_ROLES = Object.keys(ROLES);
|
|
227
|
+
|
|
228
|
+
/** Map palettes to the semantic color roles for one scheme. */
|
|
229
|
+
export function makeScheme(palettes, scheme /* 'light' | 'dark' */) {
|
|
230
|
+
const dark = scheme === 'dark';
|
|
231
|
+
const out = {};
|
|
232
|
+
for (const role of COLOR_ROLES) {
|
|
233
|
+
const [palette, light, darkTone] = ROLES[role];
|
|
234
|
+
out[role] = palettes[palette][dark ? darkTone : light];
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { sys, typeRule } from './sys.js';
|
|
2
|
+
export {
|
|
3
|
+
makePalettes, makeScheme, tonalPalette, oklchFromHex, hexFromOklch,
|
|
4
|
+
COLOR_ROLES, TONES, DEFAULT_SEED,
|
|
5
|
+
} from './color.js';
|
|
6
|
+
export {
|
|
7
|
+
typographyTokens, resolveTypography, googleFontsHref, firstFamily,
|
|
8
|
+
TYPE_ROLES, FONT_STACKS, FONT_PRESETS, DEFAULT_FONT_PRESET,
|
|
9
|
+
} from './typography.js';
|
|
10
|
+
export {
|
|
11
|
+
shapeTokens, elevationTokens, motionTokens, spacingTokens, stateTokens,
|
|
12
|
+
focusTokens, zTokens, densityTokens, RADIUS_KEYS, ELEVATION_LEVELS,
|
|
13
|
+
DURATIONS, EASINGS, SPACE_STEPS,
|
|
14
|
+
} from './system.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// `sys` — how components reference system tokens.
|
|
2
|
+
//
|
|
3
|
+
// Every value is a `var(--ui-…)` string for interpolation into `css`
|
|
4
|
+
// templates. Components never write raw `var(--ui-…)` literals; going through
|
|
5
|
+
// `sys` keeps the token names in one place and makes a typo a visible
|
|
6
|
+
// `undefined` in the stylesheet instead of a silently-inert custom property.
|
|
7
|
+
//
|
|
8
|
+
// css`:host { color: ${sys.color.onSurface}; font: ${sys.type.bodyMd}; }`
|
|
9
|
+
|
|
10
|
+
import { COLOR_ROLES } from './color.js';
|
|
11
|
+
import { TYPE_ROLES } from './typography.js';
|
|
12
|
+
import { RADIUS_KEYS, ELEVATION_LEVELS, DURATIONS, EASINGS } from './system.js';
|
|
13
|
+
|
|
14
|
+
const kebab = (s) => s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
15
|
+
const camel = (s) => s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
16
|
+
const v = (name) => `var(--ui-${name})`;
|
|
17
|
+
|
|
18
|
+
const color = {};
|
|
19
|
+
for (const role of COLOR_ROLES) color[role] = v(`color-${kebab(role)}`);
|
|
20
|
+
|
|
21
|
+
const radius = {};
|
|
22
|
+
for (const k of RADIUS_KEYS) radius[k] = v(`radius-${k}`);
|
|
23
|
+
|
|
24
|
+
const elevation = {};
|
|
25
|
+
for (const lvl of ELEVATION_LEVELS) elevation[lvl] = v(`elevation-${lvl}`);
|
|
26
|
+
|
|
27
|
+
const duration = {};
|
|
28
|
+
for (const k in DURATIONS) duration[camel(k)] = v(`duration-${k}`);
|
|
29
|
+
|
|
30
|
+
const easing = {};
|
|
31
|
+
for (const k in EASINGS) easing[camel(k)] = v(`easing-${k}`);
|
|
32
|
+
|
|
33
|
+
const type = {};
|
|
34
|
+
const tracking = {};
|
|
35
|
+
for (const role of TYPE_ROLES) {
|
|
36
|
+
type[camel(role)] = v(`type-${role}`);
|
|
37
|
+
tracking[camel(role)] = v(`type-${role}-tracking`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const sys = {
|
|
41
|
+
color,
|
|
42
|
+
radius,
|
|
43
|
+
elevation,
|
|
44
|
+
duration,
|
|
45
|
+
easing,
|
|
46
|
+
type,
|
|
47
|
+
tracking,
|
|
48
|
+
font: { brand: v('font-brand'), plain: v('font-plain'), code: v('font-code') },
|
|
49
|
+
space: (n) => v(`space-${n}`),
|
|
50
|
+
state: {
|
|
51
|
+
hover: v('state-hover'),
|
|
52
|
+
focus: v('state-focus'),
|
|
53
|
+
pressed: v('state-pressed'),
|
|
54
|
+
dragged: v('state-dragged'),
|
|
55
|
+
disabledContent: v('state-disabled-content'),
|
|
56
|
+
disabledContainer: v('state-disabled-container'),
|
|
57
|
+
},
|
|
58
|
+
focus: {
|
|
59
|
+
ring: v('focus-ring'),
|
|
60
|
+
ringWidth: v('focus-ring-width'),
|
|
61
|
+
ringOffset: v('focus-ring-offset'),
|
|
62
|
+
ringColor: v('focus-ring-color'),
|
|
63
|
+
},
|
|
64
|
+
z: {
|
|
65
|
+
appBar: v('z-app-bar'),
|
|
66
|
+
drawer: v('z-drawer'),
|
|
67
|
+
modal: v('z-modal'),
|
|
68
|
+
snackbar: v('z-snackbar'),
|
|
69
|
+
tooltip: v('z-tooltip'),
|
|
70
|
+
},
|
|
71
|
+
density: 'var(--ui-density, 0)',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** `font` + `letter-spacing` for a type role, ready to drop into a rule. */
|
|
75
|
+
export const typeRule = (role) =>
|
|
76
|
+
`font: ${type[camel(role)] || type[role]}; letter-spacing: ${tracking[camel(role)] || tracking[role]};`;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Non-color, non-type system tokens: shape, elevation, motion, spacing,
|
|
2
|
+
// state layers, focus ring, z-index. Each builder returns a flat map of
|
|
3
|
+
// token name (without the leading `--ui-`) → value.
|
|
4
|
+
|
|
5
|
+
// ------------------------------------------------------------------- shape
|
|
6
|
+
|
|
7
|
+
const RADII = { none: '0', xs: 4, sm: 8, md: 12, lg: 16, xl: 28, full: 9999 };
|
|
8
|
+
export const RADIUS_KEYS = Object.keys(RADII);
|
|
9
|
+
|
|
10
|
+
/** config: { radius: multiplier } — `radius: 0` squares every corner. */
|
|
11
|
+
export function shapeTokens({ radius = 1 } = {}) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const k of RADIUS_KEYS) {
|
|
14
|
+
const v = RADII[k];
|
|
15
|
+
out[`radius-${k}`] = v === '0' ? '0' : k === 'full' ? '9999px' : `${Math.round(v * radius)}px`;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --------------------------------------------------------------- elevation
|
|
21
|
+
|
|
22
|
+
// Material's five elevation levels as key+ambient shadow pairs, built on a
|
|
23
|
+
// themable shadow color channel so dark schemes can deepen them.
|
|
24
|
+
export const ELEVATION_LEVELS = [0, 1, 2, 3, 4, 5];
|
|
25
|
+
|
|
26
|
+
export function elevationTokens() {
|
|
27
|
+
const c1 = 'rgb(var(--ui-shadow-rgb) / 0.3)';
|
|
28
|
+
const c2 = 'rgb(var(--ui-shadow-rgb) / 0.15)';
|
|
29
|
+
return {
|
|
30
|
+
'shadow-rgb': '0 0 0',
|
|
31
|
+
'elevation-0': 'none',
|
|
32
|
+
'elevation-1': `0 1px 2px ${c1}, 0 1px 3px 1px ${c2}`,
|
|
33
|
+
'elevation-2': `0 1px 2px ${c1}, 0 2px 6px 2px ${c2}`,
|
|
34
|
+
'elevation-3': `0 1px 3px ${c1}, 0 4px 8px 3px ${c2}`,
|
|
35
|
+
'elevation-4': `0 2px 3px ${c1}, 0 6px 10px 4px ${c2}`,
|
|
36
|
+
'elevation-5': `0 4px 4px ${c1}, 0 8px 12px 6px ${c2}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------------ motion
|
|
41
|
+
|
|
42
|
+
export const DURATIONS = {
|
|
43
|
+
'short-1': 50, 'short-2': 100, 'short-3': 150, 'short-4': 200,
|
|
44
|
+
'medium-1': 250, 'medium-2': 300, 'medium-3': 350, 'medium-4': 400,
|
|
45
|
+
'long-1': 450, 'long-2': 500, 'long-3': 550, 'long-4': 600,
|
|
46
|
+
'extra-long-1': 700, 'extra-long-2': 800, 'extra-long-3': 900, 'extra-long-4': 1000,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const EASINGS = {
|
|
50
|
+
standard: 'cubic-bezier(0.2, 0, 0, 1)',
|
|
51
|
+
'standard-accelerate': 'cubic-bezier(0.3, 0, 1, 1)',
|
|
52
|
+
'standard-decelerate': 'cubic-bezier(0, 0, 0, 1)',
|
|
53
|
+
emphasized: 'cubic-bezier(0.2, 0, 0, 1)',
|
|
54
|
+
'emphasized-accelerate': 'cubic-bezier(0.3, 0, 0.8, 0.15)',
|
|
55
|
+
'emphasized-decelerate': 'cubic-bezier(0.05, 0.7, 0.1, 1)',
|
|
56
|
+
// Switch-handle overshoot — the MD3 track/handle settle (not a generic curve).
|
|
57
|
+
'emphasized-overshoot': 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
|
58
|
+
linear: 'linear',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** config: { scale } — 0 disables durations, 2 doubles them. */
|
|
62
|
+
export function motionTokens({ scale = 1 } = {}) {
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const k in DURATIONS) out[`duration-${k}`] = `${Math.round(DURATIONS[k] * scale)}ms`;
|
|
65
|
+
for (const k in EASINGS) out[`easing-${k}`] = EASINGS[k];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ----------------------------------------------------------------- spacing
|
|
70
|
+
|
|
71
|
+
export const SPACE_STEPS = [1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 24];
|
|
72
|
+
|
|
73
|
+
export function spacingTokens() {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const s of SPACE_STEPS) out[`space-${s}`] = `${s * 4}px`;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------- state / focus / z
|
|
80
|
+
|
|
81
|
+
export function stateTokens() {
|
|
82
|
+
return {
|
|
83
|
+
'state-hover': '0.08',
|
|
84
|
+
'state-focus': '0.1',
|
|
85
|
+
'state-pressed': '0.1',
|
|
86
|
+
'state-dragged': '0.16',
|
|
87
|
+
'state-disabled-content': '0.38',
|
|
88
|
+
'state-disabled-container': '0.12',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function focusTokens() {
|
|
93
|
+
return {
|
|
94
|
+
'focus-ring-width': '2px',
|
|
95
|
+
'focus-ring-offset': '2px',
|
|
96
|
+
'focus-ring-color': 'var(--ui-color-primary)',
|
|
97
|
+
'focus-ring': 'var(--ui-focus-ring-width) solid var(--ui-focus-ring-color)',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function zTokens() {
|
|
102
|
+
return {
|
|
103
|
+
'z-app-bar': '1100',
|
|
104
|
+
'z-drawer': '1200',
|
|
105
|
+
'z-modal': '1300',
|
|
106
|
+
'z-snackbar': '1400',
|
|
107
|
+
'z-tooltip': '1500',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** config: { density } — 0 (default) … -2; each step removes 4px of control height. */
|
|
112
|
+
export function densityTokens({ density = 0 } = {}) {
|
|
113
|
+
return { density: String(Math.max(-2, Math.min(0, density))) };
|
|
114
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Typography tokens — the Material type scale.
|
|
2
|
+
//
|
|
3
|
+
// Fifteen roles (display/headline/title/body/label × lg/md/sm), each emitted
|
|
4
|
+
// as granular tokens plus a `font` shorthand so a component can write
|
|
5
|
+
// `font: var(--ui-type-body-md)` and get weight, size, line-height and family
|
|
6
|
+
// in one declaration. Letter-spacing cannot ride the shorthand, so tracking is
|
|
7
|
+
// its own token.
|
|
8
|
+
//
|
|
9
|
+
// Faces are system tokens (`--ui-font-brand/plain/code`). The default is
|
|
10
|
+
// Google Sans Flex — the typeface Google's own Material 3 surfaces use —
|
|
11
|
+
// with Google Sans as the installed-on-device fallback. Swap the whole
|
|
12
|
+
// product with a preset name, a single `family`, or explicit stacks; every
|
|
13
|
+
// component follows because they consume `sys.type.*` / `sys.font.*` only.
|
|
14
|
+
|
|
15
|
+
const SYSTEM_SANS = "system-ui, -apple-system, 'Segoe UI', sans-serif";
|
|
16
|
+
const SYSTEM_MONO = "ui-monospace, 'SF Mono', Menlo, Consolas, monospace";
|
|
17
|
+
const GF = 'https://fonts.googleapis.com/css2?';
|
|
18
|
+
|
|
19
|
+
const sansStack = (...names) =>
|
|
20
|
+
names.map((n) => `'${n}'`).join(', ') + ', ' + SYSTEM_SANS;
|
|
21
|
+
|
|
22
|
+
/** Google Fonts CSS2 queries keyed by the CSS family name they load. */
|
|
23
|
+
const GF_QUERY = {
|
|
24
|
+
'Google Sans Flex': 'Google+Sans+Flex:opsz,wght@6..144,400..700',
|
|
25
|
+
'Google Sans': 'Google+Sans:wght@400;500;700',
|
|
26
|
+
Roboto: 'Roboto:wght@400;500;700',
|
|
27
|
+
'Roboto Flex': 'Roboto+Flex:opsz,wght@8..144,400..700',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Named typeface packages. A preset is the one-liner for `createTheme`:
|
|
32
|
+
* `typography: 'google-sans'` (or `{ preset: 'google-sans' }`).
|
|
33
|
+
*/
|
|
34
|
+
export const FONT_PRESETS = {
|
|
35
|
+
'google-sans-flex': {
|
|
36
|
+
brand: sansStack('Google Sans Flex', 'Google Sans'),
|
|
37
|
+
plain: sansStack('Google Sans Flex', 'Google Sans'),
|
|
38
|
+
href: GF + 'family=Google+Sans+Flex:opsz,wght@6..144,400..700&display=swap',
|
|
39
|
+
},
|
|
40
|
+
'google-sans': {
|
|
41
|
+
brand: sansStack('Google Sans', 'Google Sans Flex'),
|
|
42
|
+
plain: sansStack('Google Sans', 'Google Sans Flex'),
|
|
43
|
+
href: GF + 'family=Google+Sans:wght@400;500;700&display=swap',
|
|
44
|
+
},
|
|
45
|
+
roboto: {
|
|
46
|
+
brand: sansStack('Roboto'),
|
|
47
|
+
plain: sansStack('Roboto'),
|
|
48
|
+
href: GF + 'family=Roboto:wght@400;500;700&display=swap',
|
|
49
|
+
},
|
|
50
|
+
system: {
|
|
51
|
+
brand: SYSTEM_SANS,
|
|
52
|
+
plain: SYSTEM_SANS,
|
|
53
|
+
href: null,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const DEFAULT_FONT_PRESET = 'google-sans-flex';
|
|
58
|
+
|
|
59
|
+
/** Default stacks — what `typographyTokens()` emits with no config. */
|
|
60
|
+
export const FONT_STACKS = {
|
|
61
|
+
brand: FONT_PRESETS[DEFAULT_FONT_PRESET].brand,
|
|
62
|
+
plain: FONT_PRESETS[DEFAULT_FONT_PRESET].plain,
|
|
63
|
+
code: SYSTEM_MONO,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// role: [sizePx, lineHeightPx, weight, trackingPx, family]
|
|
67
|
+
const SCALE = {
|
|
68
|
+
'display-lg': [57, 64, 400, -0.25, 'brand'],
|
|
69
|
+
'display-md': [45, 52, 400, 0, 'brand'],
|
|
70
|
+
'display-sm': [36, 44, 400, 0, 'brand'],
|
|
71
|
+
'headline-lg': [32, 40, 400, 0, 'brand'],
|
|
72
|
+
'headline-md': [28, 36, 400, 0, 'brand'],
|
|
73
|
+
'headline-sm': [24, 32, 400, 0, 'brand'],
|
|
74
|
+
'title-lg': [22, 28, 400, 0, 'brand'],
|
|
75
|
+
'title-md': [16, 24, 500, 0.15, 'plain'],
|
|
76
|
+
'title-sm': [14, 20, 500, 0.1, 'plain'],
|
|
77
|
+
'body-lg': [16, 24, 400, 0.5, 'plain'],
|
|
78
|
+
'body-md': [14, 20, 400, 0.25, 'plain'],
|
|
79
|
+
'body-sm': [12, 16, 400, 0.4, 'plain'],
|
|
80
|
+
'label-lg': [14, 20, 500, 0.1, 'plain'],
|
|
81
|
+
'label-md': [12, 16, 500, 0.5, 'plain'],
|
|
82
|
+
'label-sm': [11, 16, 500, 0.5, 'plain'],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const TYPE_ROLES = Object.keys(SCALE);
|
|
86
|
+
|
|
87
|
+
const rem = (px) => `${+(px / 16).toFixed(4)}rem`;
|
|
88
|
+
|
|
89
|
+
const GENERIC = new Set([
|
|
90
|
+
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
|
|
91
|
+
'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math',
|
|
92
|
+
'fangsong', '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'arial',
|
|
93
|
+
'helvetica neue', 'helvetica',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const stripQuote = (s) => s.replace(/^['"]|['"]$/g, '');
|
|
97
|
+
|
|
98
|
+
/** First family name in a CSS font-family stack. */
|
|
99
|
+
export function firstFamily(stack) {
|
|
100
|
+
return stripQuote((stack || '').split(',')[0].trim());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A CSS stack from either a family name (`'Inter'`) or an already-complete
|
|
105
|
+
* stack (`Inter, system-ui, sans-serif`). Names are quoted; stacks pass through.
|
|
106
|
+
*/
|
|
107
|
+
function asStack(value, fallback) {
|
|
108
|
+
if (!value) return null;
|
|
109
|
+
const v = String(value).trim();
|
|
110
|
+
if (v.includes(',')) return v;
|
|
111
|
+
return `'${stripQuote(v)}', ${fallback}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Google Fonts stylesheet URL for the given CSS family names / stacks. */
|
|
115
|
+
export function googleFontsHref(names) {
|
|
116
|
+
const families = [];
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
for (const n of names) {
|
|
119
|
+
const fam = firstFamily(n);
|
|
120
|
+
if (!fam || GENERIC.has(fam.toLowerCase()) || seen.has(fam)) continue;
|
|
121
|
+
seen.add(fam);
|
|
122
|
+
families.push(fam);
|
|
123
|
+
}
|
|
124
|
+
if (!families.length) return null;
|
|
125
|
+
const params = families.map((fam) => 'family=' + (GF_QUERY[fam] || `${fam.replace(/ /g, '+')}:wght@400;500;700`));
|
|
126
|
+
return GF + params.join('&') + '&display=swap';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalize `createTheme({ typography })` into stacks + a stylesheet href.
|
|
131
|
+
*
|
|
132
|
+
* Accepts:
|
|
133
|
+
* 'google-sans-flex' | 'google-sans' | 'roboto' | 'system'
|
|
134
|
+
* { preset, family, brand, plain, code, scale, load }
|
|
135
|
+
*
|
|
136
|
+
* `family` sets brand and plain together. `load` is a Google Fonts (or any)
|
|
137
|
+
* stylesheet URL, `true` (default — derive from the faces), or `false`
|
|
138
|
+
* (tokens only; you host the files).
|
|
139
|
+
*/
|
|
140
|
+
export function resolveTypography(config = {}) {
|
|
141
|
+
if (typeof config === 'string') config = { preset: config };
|
|
142
|
+
|
|
143
|
+
const presetName = config.preset
|
|
144
|
+
|| (config.family || config.brand || config.plain ? null : DEFAULT_FONT_PRESET);
|
|
145
|
+
if (presetName && !FONT_PRESETS[presetName]) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Unknown font preset "${presetName}". Use one of: ${Object.keys(FONT_PRESETS).join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
const preset = presetName ? FONT_PRESETS[presetName] : null;
|
|
150
|
+
const familyStack = asStack(config.family, SYSTEM_SANS);
|
|
151
|
+
|
|
152
|
+
const brand = asStack(config.brand, SYSTEM_SANS) || familyStack || preset?.brand || FONT_STACKS.brand;
|
|
153
|
+
const plain = asStack(config.plain, SYSTEM_SANS) || familyStack || preset?.plain || FONT_STACKS.plain;
|
|
154
|
+
const code = asStack(config.code, SYSTEM_MONO) || FONT_STACKS.code;
|
|
155
|
+
const scale = config.scale ?? 1;
|
|
156
|
+
|
|
157
|
+
let href = null;
|
|
158
|
+
if (config.load === false) href = null;
|
|
159
|
+
else if (typeof config.load === 'string') href = config.load;
|
|
160
|
+
else href = preset?.href ?? googleFontsHref([brand, plain]);
|
|
161
|
+
|
|
162
|
+
return { brand, plain, code, scale, href, preset: presetName };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build the typography token map.
|
|
167
|
+
*
|
|
168
|
+
* config: a preset name, or { preset, family, brand, plain, code, scale, load }
|
|
169
|
+
*/
|
|
170
|
+
export function typographyTokens(config = {}) {
|
|
171
|
+
const { brand, plain, code, scale } = resolveTypography(config);
|
|
172
|
+
const out = {
|
|
173
|
+
'font-brand': brand,
|
|
174
|
+
'font-plain': plain,
|
|
175
|
+
'font-code': code,
|
|
176
|
+
};
|
|
177
|
+
for (const role of TYPE_ROLES) {
|
|
178
|
+
const [size, line, weight, tracking, family] = SCALE[role];
|
|
179
|
+
const fam = `var(--ui-font-${family})`;
|
|
180
|
+
out[`type-${role}-size`] = rem(size * scale);
|
|
181
|
+
out[`type-${role}-line`] = rem(line * scale);
|
|
182
|
+
out[`type-${role}-weight`] = String(weight);
|
|
183
|
+
out[`type-${role}-tracking`] = tracking ? `${tracking * scale}px` : '0';
|
|
184
|
+
out[`type-${role}-font`] = fam;
|
|
185
|
+
out[`type-${role}`] =
|
|
186
|
+
`var(--ui-type-${role}-weight) var(--ui-type-${role}-size) / var(--ui-type-${role}-line) var(--ui-type-${role}-font)`;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|