@eventuras/ratio-ui-core 0.1.0 → 0.2.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/README.md CHANGED
@@ -25,23 +25,23 @@ A theme is authored once, with references where the CSS has them:
25
25
  import { bureau } from '@eventuras/ratio-ui-core/themes/bureau';
26
26
 
27
27
  bureau.light.primary; // '#006a7d'
28
- bureau.light.shadowHard; // { x: 2, y: 2, blur: 0, color: { ref: 'text', alpha: 0.62, … } }
28
+ bureau.components.card.shadow.xs; // [{ x: 2, y: 2, blur: 0, color: { ref: 'text', alpha: 0.62, … } }]
29
29
  bureau.radius.md; // 3
30
30
  bureau.font.display; // 'Fira Sans'
31
31
  ```
32
32
 
33
33
  A renderer without a CSS engine takes the resolved view — sRGB strings,
34
- references followed inside their own mode, shadows in parts:
34
+ references followed inside their own mode, and the component tokens per
35
+ mode, every shadow layer in parts:
35
36
 
36
37
  ```ts
37
38
  import { resolveTheme } from '@eventuras/ratio-ui-core';
38
39
 
39
40
  const theme = resolveTheme(bureau);
40
- theme.light.shadowHard.color; // 'rgba(32, 36, 44, 0.62)' — the dark ink
41
- theme.dark.shadowHard.color; // 'rgba(240, 231, 210, 0.62)' — the cream ink
41
+ theme.components.light.card.shadow.xs[0].color; // 'rgba(32, 36, 44, 0.62)' — the dark ink
42
+ theme.components.dark.card.shadow.xs[0].color; // 'rgba(240, 231, 210, 0.62)' — the cream ink
42
43
  theme.scale.primary[700]; // '#006a7d' — step 700 is the light arm's teal
43
44
  theme.space.m; // 21 — points
44
- theme.components.button.shadow; // 'shadowHard' — read it from the mode you draw in
45
45
  ```
46
46
 
47
47
  `toCssValue` writes any value the way the web's CSS does
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { defineTheme, mix, isMix, isShadow, type ColorKey, type ColorSpec, type ColorString, type Components, type Font, type Mix, type Radius, type Scale, type ScaleStep, type Shadow, type ShadowKey, type Theme, type ThemeInput, type ThemeMode, } from './theme';
2
- export { resolveTheme, type ResolvedMode, type ResolvedShadow, type ResolvedTheme, } from './resolveTheme';
1
+ export { defineTheme, mix, isMix, isShadow, type ColorKey, type ColorSpec, type ColorString, type Components, type Font, type Mix, type Radius, type Scale, type ScaleStep, type Shadow, type ShadowLayers, type Theme, type ThemeInput, type ThemeMode, } from './theme';
2
+ export { resolveTheme, type ResolvedComponents, type ResolvedMode, type ResolvedShadow, type ResolvedTheme, } from './resolveTheme';
3
3
  export { fontSize, space, type FluidSize, type FontSizeStep, type SpaceStep, } from './scale';
4
4
  export { parseColor, formatColor, oklchToRgba, type Rgba } from './color';
5
5
  export { toCssValue } from './toCssValue';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,WAAW,EACX,GAAG,EACH,KAAK,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,GAAG,EACR,KAAK,MAAM,EACX,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,QAAQ,EACR,KAAK,EACL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,WAAW,EACX,GAAG,EACH,KAAK,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,GAAG,EACR,KAAK,MAAM,EACX,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,QAAQ,EACR,KAAK,EACL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -133,23 +133,51 @@ function resolveRgba(spec, mode, seen = []) {
133
133
  }
134
134
  function resolveMode(mode) {
135
135
  const out = {};
136
- for (const key of Object.keys(mode)) {
137
- const value = mode[key];
138
- if (isShadow(value)) out[key] = {
139
- ...value,
140
- color: formatColor(resolveRgba(value.color, mode))
141
- };
142
- else if (typeof value === "string" || isMix(value)) out[key] = formatColor(resolveRgba(value, mode));
143
- }
136
+ for (const key of Object.keys(mode)) out[key] = formatColor(resolveRgba(mode[key], mode));
144
137
  return out;
145
138
  }
139
+ function resolveComponents(components, mode) {
140
+ const layers = (shadow) => shadow?.map(({ x, y, blur, spread = 0, color }) => ({
141
+ x,
142
+ y,
143
+ blur,
144
+ spread,
145
+ color: formatColor(resolveRgba(color, mode))
146
+ })) ?? null;
147
+ const { button, card, chat } = components;
148
+ return {
149
+ button: {
150
+ radius: button.radius,
151
+ shadow: layers(button.shadow),
152
+ pressedShadow: layers(button.pressedShadow),
153
+ pressedOffset: { ...button.pressedOffset },
154
+ pressedScale: button.pressedScale
155
+ },
156
+ card: {
157
+ shadow: {
158
+ xs: layers(card.shadow.xs),
159
+ sm: layers(card.shadow.sm),
160
+ md: layers(card.shadow.md)
161
+ },
162
+ hoverShadow: layers(card.hoverShadow),
163
+ featured: {
164
+ ...card.featured,
165
+ shadow: layers(card.featured.shadow)
166
+ }
167
+ },
168
+ chat: {
169
+ barShadow: layers(chat.barShadow),
170
+ popoverShadow: layers(chat.popoverShadow)
171
+ }
172
+ };
173
+ }
146
174
  var resolveScale = (scale) => Object.fromEntries(Object.entries(scale).map(([step, value]) => [step, formatColor(parseColor(value))]));
147
175
  /**
148
176
  * The theme as a renderer without a CSS engine reads it: colours as sRGB
149
177
  * hex or rgba, references followed inside their own mode, radius, font
150
- * sizes and spacing in points, fonts by family. Component tokens keep
151
- * naming shadows, so a renderer reads them from the mode it draws in.
152
- * The authored theme is untouched.
178
+ * sizes and spacing in points, fonts by family. Component tokens come per
179
+ * mode, their shadows' colours resolved in it, so a renderer reads the mode
180
+ * it draws in. The authored theme is untouched.
153
181
  */
154
182
  function resolveTheme(theme) {
155
183
  return {
@@ -164,7 +192,10 @@ function resolveTheme(theme) {
164
192
  },
165
193
  fontSize: atSmallEnd(fontSize),
166
194
  space: atSmallEnd(space),
167
- components: structuredClone(theme.components)
195
+ components: {
196
+ light: resolveComponents(theme.components, theme.light),
197
+ dark: resolveComponents(theme.components, theme.dark)
198
+ }
168
199
  };
169
200
  }
170
201
  //#endregion
@@ -179,13 +210,18 @@ var px = (n) => n === 0 ? "0" : `${n}px`;
179
210
  * what the drift tests hold the hand-written CSS against until it lands.
180
211
  */
181
212
  function toCssValue(value) {
213
+ if (value === null) return "none";
214
+ if (Array.isArray(value)) return value.map((layer) => toCssValue(layer)).join(", ");
182
215
  if (typeof value === "number") return px(value);
183
216
  if (typeof value === "string") return value;
184
217
  if (isMix(value)) {
185
218
  const percent = Math.round(value.alpha * 100);
186
219
  return `color-mix(in ${value.space}, var(--${cssName(value.ref)}) ${percent}%, transparent)`;
187
220
  }
188
- if (isShadow(value)) return `${px(value.x)} ${px(value.y)} ${px(value.blur)} ${toCssValue(value.color)}`;
221
+ if (isShadow(value)) {
222
+ const spread = value.spread === void 0 ? "" : ` ${px(value.spread)}`;
223
+ return `${px(value.x)} ${px(value.y)} ${px(value.blur)}${spread} ${toCssValue(value.color)}`;
224
+ }
189
225
  throw new Error(`Cannot write ${JSON.stringify(value)} as CSS`);
190
226
  }
191
227
  //#endregion
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/color.ts","../src/scale.ts","../src/resolveTheme.ts","../src/cssName.ts","../src/toCssValue.ts"],"sourcesContent":["// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/** A colour in sRGB: channels 0–255, alpha 0–1. */\nexport interface Rgba {\n r: number;\n g: number;\n b: number;\n a: number;\n}\n\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst to255 = (v: number) => Math.round(clamp01(v) * 255);\n\n// Linear sRGB → sRGB transfer curve.\nconst gamma = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055);\n\n/**\n * OKLCH → sRGB, by Björn Ottosson's matrices. Out-of-gamut colours are\n * clipped per channel; CSS gamut-maps instead, so a step at the edge of\n * sRGB can land a notch off what the browser shows.\n */\nexport function oklchToRgba(L: number, C: number, h: number, a = 1): Rgba {\n const rad = (h * Math.PI) / 180;\n const A = C * Math.cos(rad);\n const B = C * Math.sin(rad);\n\n const l_ = L + 0.3963377774 * A + 0.2158037573 * B;\n const m_ = L - 0.1055613458 * A - 0.0638541728 * B;\n const s_ = L - 0.0894841775 * A - 1.291485548 * B;\n const l = l_ ** 3;\n const m = m_ ** 3;\n const s = s_ ** 3;\n\n const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;\n const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;\n const b = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;\n return { r: to255(gamma(r)), g: to255(gamma(g)), b: to255(gamma(b)), a };\n}\n\nconst HEX = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;\nconst FN = /^(rgba?|oklch)\\(\\s*([^)]*?)\\s*\\)$/i;\n\nconst num = (token: string, what: string): number => {\n const v = Number(token);\n if (Number.isNaN(v)) throw new Error(`Unexpected ${what} \"${token}\"`);\n return v;\n};\n\n/**\n * Reads the forms the themes are written in: `#rgb[a]`, `#rrggbb[aa]`,\n * `rgb()`/`rgba()` with commas or spaces, and `oklch()`. Anything else\n * throws — a theme value nobody can parse is a bug, not a fallback.\n */\nexport function parseColor(input: string): Rgba {\n const value = input.trim();\n\n const hex = HEX.exec(value)?.[1];\n if (hex) {\n const wide = hex.length <= 4 ? [...hex].map(ch => ch + ch).join('') : hex;\n const n = (i: number) => parseInt(wide.slice(i, i + 2), 16);\n return { r: n(0), g: n(2), b: n(4), a: wide.length === 8 ? n(6) / 255 : 1 };\n }\n\n const fn = FN.exec(value);\n if (!fn) throw new Error(`Unsupported colour \"${input}\"`);\n const [, name, body] = fn as unknown as [string, string, string];\n // `rgb(0 0 0 / 0.5)` and `rgba(0, 0, 0, 0.5)` both split to four tokens.\n const parts = body.split(/[\\s,/]+/).filter(Boolean);\n const alpha = parts.length === 4 ? num(parts[3]!, 'alpha') : 1;\n\n if (name.toLowerCase().startsWith('rgb')) {\n const [r, g, b] = parts.slice(0, 3).map(p => num(p, 'channel'));\n if (r === undefined || g === undefined || b === undefined) throw new Error(`Unsupported colour \"${input}\"`);\n return { r, g, b, a: alpha };\n }\n\n const [L, C, h] = parts.slice(0, 3);\n if (L === undefined || C === undefined || h === undefined) throw new Error(`Unsupported colour \"${input}\"`);\n const lightness = L.endsWith('%') ? num(L.slice(0, -1), 'lightness') / 100 : num(L, 'lightness');\n return oklchToRgba(lightness, num(C, 'chroma'), num(h, 'hue'), alpha);\n}\n\n/** `#rrggbb` when opaque, otherwise `rgba(r, g, b, a)` — both read by every renderer. */\nexport function formatColor({ r, g, b, a }: Rgba): string {\n if (a >= 1) return `#${[r, g, b].map(c => c.toString(16).padStart(2, '0')).join('')}`;\n return `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1000) / 1000})`;\n}\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n * A fluid size as the web writes it: the two ends of its `clamp()`, in rem.\n * The web grows from `min` on a phone to `max` on a wide screen.\n */\nexport type FluidSize = readonly [min: number, max: number];\n\n/**\n * The type scale, shared by every theme — `--font-size-*` in\n * `ratio-ui/src/tokens/typography.css`.\n * @beta May change before release.\n */\nexport const fontSize = {\n xs: [0.75, 0.81],\n sm: [0.88, 0.96],\n base: [1, 1.13],\n lg: [1.13, 1.27],\n xl: [1.27, 1.42],\n '2xl': [1.42, 1.6],\n '3xl': [1.6, 1.8],\n '4xl': [1.8, 2.03],\n '5xl': [2.03, 2.28],\n '6xl': [2.28, 2.57],\n} as const satisfies Record<string, FluidSize>;\n\n/**\n * The space scale, shared by every theme — `--space-*` in\n * `ratio-ui/src/tokens/spacing.css`. Single steps only; the web's\n * `--space-s-m` pairs are fluid ranges a fixed layout has no use for.\n * @beta May change before release.\n */\nexport const space = {\n '3xs': [0.25, 0.25],\n '2xs': [0.4375, 0.5],\n xs: [0.6875, 0.75],\n s: [0.875, 1],\n m: [1.3125, 1.5],\n l: [1.75, 2],\n xl: [2.625, 3],\n '2xl': [3.5, 4],\n '3xl': [5.25, 6],\n} as const satisfies Record<string, FluidSize>;\n\nexport type FontSizeStep = keyof typeof fontSize;\nexport type SpaceStep = keyof typeof space;\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\nimport { formatColor, parseColor, type Rgba } from './color';\nimport { fontSize, space, type FluidSize, type FontSizeStep, type SpaceStep } from './scale';\nimport {\n isMix,\n isShadow,\n type ColorKey,\n type ColorSpec,\n type Components,\n type Font,\n type Radius,\n type Scale,\n type ScaleStep,\n type Shadow,\n type Theme,\n type ThemeMode,\n} from './theme';\n\nexport interface ResolvedShadow {\n x: number;\n y: number;\n blur: number;\n color: string;\n}\n\n/** A mode with every colour an sRGB string and every shadow in parts. */\nexport type ResolvedMode = {\n [K in keyof ThemeMode]: ThemeMode[K] extends Shadow ? ResolvedShadow : string;\n};\n\nexport interface ResolvedTheme {\n name: string;\n light: ResolvedMode;\n dark: ResolvedMode;\n radius: Radius;\n font: Font;\n scale: { primary: Record<ScaleStep, string>; accent: Record<ScaleStep, string> };\n /** Font sizes in points, at the small end of the web's fluid range — a phone's. */\n fontSize: Record<FontSizeStep, number>;\n /** Spacing in points, at the small end of the web's fluid range. */\n space: Record<SpaceStep, number>;\n components: Components;\n}\n\n/** Points per rem, as the web's root size. */\nconst REM = 16;\n\nconst atSmallEnd = <K extends string>(sizes: Record<K, FluidSize>) =>\n Object.fromEntries(\n (Object.entries(sizes) as [K, FluidSize][]).map(([step, [min]]) => [step, Math.round(min * REM * 100) / 100]),\n ) as Record<K, number>;\n\nfunction resolveRgba(spec: ColorSpec, mode: ThemeMode, seen: ColorKey[] = []): Rgba {\n if (typeof spec === 'string') return parseColor(spec);\n if (seen.includes(spec.ref)) throw new Error(`Colour reference loops: ${[...seen, spec.ref].join(' → ')}`);\n const base = resolveRgba(mode[spec.ref], mode, [...seen, spec.ref]);\n return { ...base, a: base.a * spec.alpha };\n}\n\nfunction resolveMode(mode: ThemeMode): ResolvedMode {\n const out: Partial<Record<keyof ThemeMode, string | ResolvedShadow>> = {};\n for (const key of Object.keys(mode) as (keyof ThemeMode)[]) {\n const value = mode[key];\n if (isShadow(value)) {\n out[key] = { ...value, color: formatColor(resolveRgba(value.color, mode)) };\n } else if (typeof value === 'string' || isMix(value)) {\n out[key] = formatColor(resolveRgba(value, mode));\n }\n }\n return out as ResolvedMode;\n}\n\nconst resolveScale = (scale: Scale) =>\n Object.fromEntries(\n Object.entries(scale).map(([step, value]) => [step, formatColor(parseColor(value))]),\n ) as Record<ScaleStep, string>;\n\n/**\n * The theme as a renderer without a CSS engine reads it: colours as sRGB\n * hex or rgba, references followed inside their own mode, radius, font\n * sizes and spacing in points, fonts by family. Component tokens keep\n * naming shadows, so a renderer reads them from the mode it draws in.\n * The authored theme is untouched.\n */\nexport function resolveTheme(theme: Theme): ResolvedTheme {\n return {\n name: theme.name,\n light: resolveMode(theme.light),\n dark: resolveMode(theme.dark),\n radius: { ...theme.radius },\n font: { ...theme.font },\n scale: { primary: resolveScale(theme.scale.primary), accent: resolveScale(theme.scale.accent) },\n fontSize: atSmallEnd(fontSize),\n space: atSmallEnd(space),\n components: structuredClone(theme.components),\n };\n}\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/** The CSS token name of a key: `textOnPrimary` → `text-on-primary`, `border1` → `border-1`. */\nexport const cssName = (key: string): string =>\n key\n .replace(/([a-z])([A-Z0-9])/g, '$1-$2')\n .replace(/([0-9])([a-zA-Z])/g, '$1-$2')\n .toLowerCase();\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\nimport { cssName } from './cssName';\nimport { isMix, isShadow, type ColorSpec, type Shadow } from './theme';\n\nconst px = (n: number) => (n === 0 ? '0' : `${n}px`);\n\n/**\n * The CSS the web writes for a value: the seed of the theme generator, and\n * what the drift tests hold the hand-written CSS against until it lands.\n */\nexport function toCssValue(value: ColorSpec | Shadow | number): string {\n if (typeof value === 'number') return px(value);\n if (typeof value === 'string') return value;\n if (isMix(value)) {\n const percent = Math.round(value.alpha * 100);\n return `color-mix(in ${value.space}, var(--${cssName(value.ref)}) ${percent}%, transparent)`;\n }\n if (isShadow(value)) return `${px(value.x)} ${px(value.y)} ${px(value.blur)} ${toCssValue(value.color)}`;\n throw new Error(`Cannot write ${JSON.stringify(value)} as CSS`);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/color.ts","../src/scale.ts","../src/resolveTheme.ts","../src/cssName.ts","../src/toCssValue.ts"],"sourcesContent":["// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/** A colour in sRGB: channels 0–255, alpha 0–1. */\nexport interface Rgba {\n r: number;\n g: number;\n b: number;\n a: number;\n}\n\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst to255 = (v: number) => Math.round(clamp01(v) * 255);\n\n// Linear sRGB → sRGB transfer curve.\nconst gamma = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055);\n\n/**\n * OKLCH → sRGB, by Björn Ottosson's matrices. Out-of-gamut colours are\n * clipped per channel; CSS gamut-maps instead, so a step at the edge of\n * sRGB can land a notch off what the browser shows.\n */\nexport function oklchToRgba(L: number, C: number, h: number, a = 1): Rgba {\n const rad = (h * Math.PI) / 180;\n const A = C * Math.cos(rad);\n const B = C * Math.sin(rad);\n\n const l_ = L + 0.3963377774 * A + 0.2158037573 * B;\n const m_ = L - 0.1055613458 * A - 0.0638541728 * B;\n const s_ = L - 0.0894841775 * A - 1.291485548 * B;\n const l = l_ ** 3;\n const m = m_ ** 3;\n const s = s_ ** 3;\n\n const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;\n const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;\n const b = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;\n return { r: to255(gamma(r)), g: to255(gamma(g)), b: to255(gamma(b)), a };\n}\n\nconst HEX = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;\nconst FN = /^(rgba?|oklch)\\(\\s*([^)]*?)\\s*\\)$/i;\n\nconst num = (token: string, what: string): number => {\n const v = Number(token);\n if (Number.isNaN(v)) throw new Error(`Unexpected ${what} \"${token}\"`);\n return v;\n};\n\n/**\n * Reads the forms the themes are written in: `#rgb[a]`, `#rrggbb[aa]`,\n * `rgb()`/`rgba()` with commas or spaces, and `oklch()`. Anything else\n * throws — a theme value nobody can parse is a bug, not a fallback.\n */\nexport function parseColor(input: string): Rgba {\n const value = input.trim();\n\n const hex = HEX.exec(value)?.[1];\n if (hex) {\n const wide = hex.length <= 4 ? [...hex].map(ch => ch + ch).join('') : hex;\n const n = (i: number) => parseInt(wide.slice(i, i + 2), 16);\n return { r: n(0), g: n(2), b: n(4), a: wide.length === 8 ? n(6) / 255 : 1 };\n }\n\n const fn = FN.exec(value);\n if (!fn) throw new Error(`Unsupported colour \"${input}\"`);\n const [, name, body] = fn as unknown as [string, string, string];\n // `rgb(0 0 0 / 0.5)` and `rgba(0, 0, 0, 0.5)` both split to four tokens.\n const parts = body.split(/[\\s,/]+/).filter(Boolean);\n const alpha = parts.length === 4 ? num(parts[3]!, 'alpha') : 1;\n\n if (name.toLowerCase().startsWith('rgb')) {\n const [r, g, b] = parts.slice(0, 3).map(p => num(p, 'channel'));\n if (r === undefined || g === undefined || b === undefined) throw new Error(`Unsupported colour \"${input}\"`);\n return { r, g, b, a: alpha };\n }\n\n const [L, C, h] = parts.slice(0, 3);\n if (L === undefined || C === undefined || h === undefined) throw new Error(`Unsupported colour \"${input}\"`);\n const lightness = L.endsWith('%') ? num(L.slice(0, -1), 'lightness') / 100 : num(L, 'lightness');\n return oklchToRgba(lightness, num(C, 'chroma'), num(h, 'hue'), alpha);\n}\n\n/** `#rrggbb` when opaque, otherwise `rgba(r, g, b, a)` — both read by every renderer. */\nexport function formatColor({ r, g, b, a }: Rgba): string {\n if (a >= 1) return `#${[r, g, b].map(c => c.toString(16).padStart(2, '0')).join('')}`;\n return `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1000) / 1000})`;\n}\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n * A fluid size as the web writes it: the two ends of its `clamp()`, in rem.\n * The web grows from `min` on a phone to `max` on a wide screen.\n */\nexport type FluidSize = readonly [min: number, max: number];\n\n/**\n * The type scale, shared by every theme — `--font-size-*` in\n * `ratio-ui/src/tokens/typography.css`.\n * @beta May change before release.\n */\nexport const fontSize = {\n xs: [0.75, 0.81],\n sm: [0.88, 0.96],\n base: [1, 1.13],\n lg: [1.13, 1.27],\n xl: [1.27, 1.42],\n '2xl': [1.42, 1.6],\n '3xl': [1.6, 1.8],\n '4xl': [1.8, 2.03],\n '5xl': [2.03, 2.28],\n '6xl': [2.28, 2.57],\n} as const satisfies Record<string, FluidSize>;\n\n/**\n * The space scale, shared by every theme — `--space-*` in\n * `ratio-ui/src/tokens/spacing.css`. Single steps only; the web's\n * `--space-s-m` pairs are fluid ranges a fixed layout has no use for.\n * @beta May change before release.\n */\nexport const space = {\n '3xs': [0.25, 0.25],\n '2xs': [0.4375, 0.5],\n xs: [0.6875, 0.75],\n s: [0.875, 1],\n m: [1.3125, 1.5],\n l: [1.75, 2],\n xl: [2.625, 3],\n '2xl': [3.5, 4],\n '3xl': [5.25, 6],\n} as const satisfies Record<string, FluidSize>;\n\nexport type FontSizeStep = keyof typeof fontSize;\nexport type SpaceStep = keyof typeof space;\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\nimport { formatColor, parseColor, type Rgba } from './color';\nimport { fontSize, space, type FluidSize, type FontSizeStep, type SpaceStep } from './scale';\nimport {\n type ColorKey,\n type ColorSpec,\n type Components,\n type Font,\n type Radius,\n type Scale,\n type ScaleStep,\n type ShadowLayers,\n type Theme,\n type ThemeMode,\n} from './theme';\n\n/** One layer of a shadow, its colour an sRGB string and its spread filled in. */\nexport interface ResolvedShadow {\n x: number;\n y: number;\n blur: number;\n spread: number;\n color: string;\n}\n\n/** A mode with every colour an sRGB string. */\nexport type ResolvedMode = Record<keyof ThemeMode, string>;\n\ntype ResolvedLayers = readonly ResolvedShadow[] | null;\n\n/** The component tokens of one mode, every shadow resolved in that mode. */\nexport interface ResolvedComponents {\n button: Omit<Components['button'], 'shadow' | 'pressedShadow'> & {\n shadow: ResolvedLayers;\n pressedShadow: ResolvedLayers;\n };\n card: {\n shadow: Record<'xs' | 'sm' | 'md', ResolvedLayers>;\n hoverShadow: ResolvedLayers;\n featured: Omit<Components['card']['featured'], 'shadow'> & { shadow: ResolvedLayers };\n };\n chat: { barShadow: ResolvedLayers; popoverShadow: ResolvedLayers };\n}\n\nexport interface ResolvedTheme {\n name: string;\n light: ResolvedMode;\n dark: ResolvedMode;\n radius: Radius;\n font: Font;\n scale: { primary: Record<ScaleStep, string>; accent: Record<ScaleStep, string> };\n /** Font sizes in points, at the small end of the web's fluid range — a phone's. */\n fontSize: Record<FontSizeStep, number>;\n /** Spacing in points, at the small end of the web's fluid range. */\n space: Record<SpaceStep, number>;\n /** Per mode: a shadow's colour follows the mode it is drawn in. */\n components: { light: ResolvedComponents; dark: ResolvedComponents };\n}\n\n/** Points per rem, as the web's root size. */\nconst REM = 16;\n\nconst atSmallEnd = <K extends string>(sizes: Record<K, FluidSize>) =>\n Object.fromEntries(\n (Object.entries(sizes) as [K, FluidSize][]).map(([step, [min]]) => [step, Math.round(min * REM * 100) / 100]),\n ) as Record<K, number>;\n\nfunction resolveRgba(spec: ColorSpec, mode: ThemeMode, seen: ColorKey[] = []): Rgba {\n if (typeof spec === 'string') return parseColor(spec);\n if (seen.includes(spec.ref)) throw new Error(`Colour reference loops: ${[...seen, spec.ref].join(' → ')}`);\n const base = resolveRgba(mode[spec.ref], mode, [...seen, spec.ref]);\n return { ...base, a: base.a * spec.alpha };\n}\n\nfunction resolveMode(mode: ThemeMode): ResolvedMode {\n const out: Partial<ResolvedMode> = {};\n for (const key of Object.keys(mode) as (keyof ThemeMode)[]) {\n out[key] = formatColor(resolveRgba(mode[key], mode));\n }\n return out as ResolvedMode;\n}\n\nfunction resolveComponents(components: Components, mode: ThemeMode): ResolvedComponents {\n const layers = (shadow: ShadowLayers | null): ResolvedLayers =>\n shadow?.map(({ x, y, blur, spread = 0, color }) => ({\n x,\n y,\n blur,\n spread,\n color: formatColor(resolveRgba(color, mode)),\n })) ?? null;\n const { button, card, chat } = components;\n return {\n button: {\n radius: button.radius,\n shadow: layers(button.shadow),\n pressedShadow: layers(button.pressedShadow),\n pressedOffset: { ...button.pressedOffset },\n pressedScale: button.pressedScale,\n },\n card: {\n shadow: { xs: layers(card.shadow.xs), sm: layers(card.shadow.sm), md: layers(card.shadow.md) },\n hoverShadow: layers(card.hoverShadow),\n featured: { ...card.featured, shadow: layers(card.featured.shadow) },\n },\n chat: { barShadow: layers(chat.barShadow), popoverShadow: layers(chat.popoverShadow) },\n };\n}\n\nconst resolveScale = (scale: Scale) =>\n Object.fromEntries(\n Object.entries(scale).map(([step, value]) => [step, formatColor(parseColor(value))]),\n ) as Record<ScaleStep, string>;\n\n/**\n * The theme as a renderer without a CSS engine reads it: colours as sRGB\n * hex or rgba, references followed inside their own mode, radius, font\n * sizes and spacing in points, fonts by family. Component tokens come per\n * mode, their shadows' colours resolved in it, so a renderer reads the mode\n * it draws in. The authored theme is untouched.\n */\nexport function resolveTheme(theme: Theme): ResolvedTheme {\n return {\n name: theme.name,\n light: resolveMode(theme.light),\n dark: resolveMode(theme.dark),\n radius: { ...theme.radius },\n font: { ...theme.font },\n scale: { primary: resolveScale(theme.scale.primary), accent: resolveScale(theme.scale.accent) },\n fontSize: atSmallEnd(fontSize),\n space: atSmallEnd(space),\n components: {\n light: resolveComponents(theme.components, theme.light),\n dark: resolveComponents(theme.components, theme.dark),\n },\n };\n}\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/** The CSS token name of a key: `textOnPrimary` → `text-on-primary`, `border1` → `border-1`. */\nexport const cssName = (key: string): string =>\n key\n .replace(/([a-z])([A-Z0-9])/g, '$1-$2')\n .replace(/([0-9])([a-zA-Z])/g, '$1-$2')\n .toLowerCase();\n","// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\nimport { cssName } from './cssName';\nimport { isMix, isShadow, type ColorSpec, type Shadow, type ShadowLayers } from './theme';\n\nconst px = (n: number) => (n === 0 ? '0' : `${n}px`);\n\n/**\n * The CSS the web writes for a value: the seed of the theme generator, and\n * what the drift tests hold the hand-written CSS against until it lands.\n */\nexport function toCssValue(value: ColorSpec | Shadow | ShadowLayers | null | number): string {\n if (value === null) return 'none';\n if (Array.isArray(value)) return value.map(layer => toCssValue(layer)).join(', ');\n if (typeof value === 'number') return px(value);\n if (typeof value === 'string') return value;\n if (isMix(value)) {\n const percent = Math.round(value.alpha * 100);\n return `color-mix(in ${value.space}, var(--${cssName(value.ref)}) ${percent}%, transparent)`;\n }\n if (isShadow(value)) {\n const spread = value.spread === undefined ? '' : ` ${px(value.spread)}`;\n return `${px(value.x)} ${px(value.y)} ${px(value.blur)}${spread} ${toCssValue(value.color)}`;\n }\n throw new Error(`Cannot write ${JSON.stringify(value)} as CSS`);\n}\n"],"mappings":"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"}
@@ -1,15 +1,34 @@
1
1
  import { FontSizeStep, SpaceStep } from './scale';
2
- import { Components, Font, Radius, ScaleStep, Shadow, Theme, ThemeMode } from './theme';
2
+ import { Components, Font, Radius, ScaleStep, Theme, ThemeMode } from './theme';
3
+ /** One layer of a shadow, its colour an sRGB string and its spread filled in. */
3
4
  export interface ResolvedShadow {
4
5
  x: number;
5
6
  y: number;
6
7
  blur: number;
8
+ spread: number;
7
9
  color: string;
8
10
  }
9
- /** A mode with every colour an sRGB string and every shadow in parts. */
10
- export type ResolvedMode = {
11
- [K in keyof ThemeMode]: ThemeMode[K] extends Shadow ? ResolvedShadow : string;
12
- };
11
+ /** A mode with every colour an sRGB string. */
12
+ export type ResolvedMode = Record<keyof ThemeMode, string>;
13
+ type ResolvedLayers = readonly ResolvedShadow[] | null;
14
+ /** The component tokens of one mode, every shadow resolved in that mode. */
15
+ export interface ResolvedComponents {
16
+ button: Omit<Components['button'], 'shadow' | 'pressedShadow'> & {
17
+ shadow: ResolvedLayers;
18
+ pressedShadow: ResolvedLayers;
19
+ };
20
+ card: {
21
+ shadow: Record<'xs' | 'sm' | 'md', ResolvedLayers>;
22
+ hoverShadow: ResolvedLayers;
23
+ featured: Omit<Components['card']['featured'], 'shadow'> & {
24
+ shadow: ResolvedLayers;
25
+ };
26
+ };
27
+ chat: {
28
+ barShadow: ResolvedLayers;
29
+ popoverShadow: ResolvedLayers;
30
+ };
31
+ }
13
32
  export interface ResolvedTheme {
14
33
  name: string;
15
34
  light: ResolvedMode;
@@ -24,14 +43,19 @@ export interface ResolvedTheme {
24
43
  fontSize: Record<FontSizeStep, number>;
25
44
  /** Spacing in points, at the small end of the web's fluid range. */
26
45
  space: Record<SpaceStep, number>;
27
- components: Components;
46
+ /** Per mode: a shadow's colour follows the mode it is drawn in. */
47
+ components: {
48
+ light: ResolvedComponents;
49
+ dark: ResolvedComponents;
50
+ };
28
51
  }
29
52
  /**
30
53
  * The theme as a renderer without a CSS engine reads it: colours as sRGB
31
54
  * hex or rgba, references followed inside their own mode, radius, font
32
- * sizes and spacing in points, fonts by family. Component tokens keep
33
- * naming shadows, so a renderer reads them from the mode it draws in.
34
- * The authored theme is untouched.
55
+ * sizes and spacing in points, fonts by family. Component tokens come per
56
+ * mode, their shadows' colours resolved in it, so a renderer reads the mode
57
+ * it draws in. The authored theme is untouched.
35
58
  */
36
59
  export declare function resolveTheme(theme: Theme): ResolvedTheme;
60
+ export {};
37
61
  //# sourceMappingURL=resolveTheme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolveTheme.d.ts","sourceRoot":"","sources":["../src/resolveTheme.ts"],"names":[],"mappings":"AAKA,OAAO,EAAmC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,MAAM,EAEX,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,KAAK,EACV,KAAK,SAAS,EACf,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG;KACxB,CAAC,IAAI,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,cAAc,GAAG,MAAM;CAC9E,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IACjF,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACvC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,EAAE,UAAU,CAAC;CACxB;AAmCD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,aAAa,CAYxD"}
1
+ {"version":3,"file":"resolveTheme.d.ts","sourceRoot":"","sources":["../src/resolveTheme.ts"],"names":[],"mappings":"AAKA,OAAO,EAAmC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,MAAM,EAEX,KAAK,SAAS,EAEd,KAAK,KAAK,EACV,KAAK,SAAS,EACf,MAAM,SAAS,CAAC;AAEjB,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,+CAA+C;AAC/C,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,SAAS,EAAE,MAAM,CAAC,CAAC;AAE3D,KAAK,cAAc,GAAG,SAAS,cAAc,EAAE,GAAG,IAAI,CAAC;AAEvD,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,eAAe,CAAC,GAAG;QAC/D,MAAM,EAAE,cAAc,CAAC;QACvB,aAAa,EAAE,cAAc,CAAC;KAC/B,CAAC;IACF,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,cAAc,CAAC,CAAC;QACnD,WAAW,EAAE,cAAc,CAAC;QAC5B,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,GAAG;YAAE,MAAM,EAAE,cAAc,CAAA;SAAE,CAAC;KACvF,CAAC;IACF,IAAI,EAAE;QAAE,SAAS,EAAE,cAAc,CAAC;QAAC,aAAa,EAAE,cAAc,CAAA;KAAE,CAAC;CACpE;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IACjF,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACvC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACjC,mEAAmE;IACnE,UAAU,EAAE;QAAE,KAAK,EAAE,kBAAkB,CAAC;QAAC,IAAI,EAAE,kBAAkB,CAAA;KAAE,CAAC;CACrE;AAyDD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,aAAa,CAexD"}
@@ -1 +1 @@
1
- {"version":3,"file":"theme-Bqyh6GYU.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/** A colour as authored: hex, `rgb()`/`rgba()`, or `oklch()`. */\nexport type ColorString = string;\n\n/**\n * A colour taken from another token of the same mode, at an alpha — what\n * the CSS writes as `color-mix(in srgb, var(--text) 62%, transparent)`.\n * A reference, not a value, so it follows the token it points at: the hard\n * shadow is \"the ink at 62%\" in light and in dark alike.\n */\nexport interface Mix {\n ref: ColorKey;\n alpha: number;\n /** The space the CSS mixes in. Mixing with transparent only scales alpha, so the resolved colour is the same in any. */\n space: 'srgb' | 'oklch';\n}\n\nexport type ColorSpec = ColorString | Mix;\n\n/** An offset shadow in parts, so each renderer draws it its own way. */\nexport interface Shadow {\n x: number;\n y: number;\n blur: number;\n color: ColorSpec;\n}\n\n/** The colours of one mode. Keys are the CSS token names in camelCase: `--text-muted` is `textMuted`. */\nexport interface ThemeMode {\n primary: ColorSpec;\n secondary: ColorSpec;\n accent: ColorSpec;\n text: ColorSpec;\n textMuted: ColorSpec;\n textSubtle: ColorSpec;\n textOnPrimary: ColorSpec;\n textOnSecondary: ColorSpec;\n textOnAccent: ColorSpec;\n surface: ColorSpec;\n surfaceGlass: ColorSpec;\n card: ColorSpec;\n cardHover: ColorSpec;\n border1: ColorSpec;\n border2: ColorSpec;\n focusRing: ColorSpec;\n errorSolid: ColorSpec;\n errorOnSolid: ColorSpec;\n errorBg: ColorSpec;\n errorBorder: ColorSpec;\n errorText: ColorSpec;\n successSolid: ColorSpec;\n successOnSolid: ColorSpec;\n successBg: ColorSpec;\n successBorder: ColorSpec;\n successText: ColorSpec;\n infoSolid: ColorSpec;\n infoOnSolid: ColorSpec;\n infoBg: ColorSpec;\n infoBorder: ColorSpec;\n infoText: ColorSpec;\n warningSolid: ColorSpec;\n warningOnSolid: ColorSpec;\n warningBg: ColorSpec;\n warningBorder: ColorSpec;\n warningText: ColorSpec;\n /**\n * The chat's colours, the web's `--chat-*`.\n * @beta Named and valued as the chat components are; may change before release.\n */\n chatSidebarBg: ColorSpec;\n /** The pointed-at message, and the active room in a list. */\n chatActiveBg: ColorSpec;\n chatActiveFg: ColorSpec;\n /** The band behind a message that mentions you; its edge is `accent`. */\n chatMentionBg: ColorSpec;\n chatNick: ColorSpec;\n chatNickOp: ColorSpec;\n chatNickVoice: ColorSpec;\n chatAvatarBg: ColorSpec;\n chatAvatarFg: ColorSpec;\n chatAvatarAltBg: ColorSpec;\n chatAvatarAltFg: ColorSpec;\n chatBubbleBg: ColorSpec;\n chatBubbleMeBg: ColorSpec;\n chatBubbleMeFg: ColorSpec;\n /** A reaction of your own. */\n chatReactionMeBg: ColorSpec;\n chatReactionMeBorder: ColorSpec;\n chatRowHoverBg: ColorSpec;\n chatPopoverBg: ColorSpec;\n shadowHard: Shadow;\n shadowHardLg: Shadow;\n shadowHardXl: Shadow;\n}\n\nexport type ShadowKey = 'shadowHard' | 'shadowHardLg' | 'shadowHardXl';\n/** The keys of a mode that hold a colour — what a `Mix` may point at. */\nexport type ColorKey = Exclude<keyof ThemeMode, ShadowKey>;\n\n/** Rounding in px, one scale for every corner. */\nexport interface Radius {\n xs: number;\n sm: number;\n md: number;\n lg: number;\n xl: number;\n pill: number;\n overlay: number;\n}\n\n/** Font families by role. Names only — each renderer loads the files. */\nexport interface Font {\n display: string;\n body: string;\n mono: string;\n}\n\n/**\n * How components wear the theme — the CSS's `--button-*`, `--card-*` and\n * the chat's shadows.\n * Shadows name a shadow of the mode, so each mode inks its own; `null` is\n * no shadow.\n * @beta Grows as renderers need more components; may change before release.\n */\nexport interface Components {\n button: {\n /** A step of `radius`. */\n radius: keyof Radius;\n shadow: ShadowKey | null;\n /** The shadow while pressed. */\n pressedShadow: ShadowKey | null;\n /** How far a pressed button moves, in px — Bureau presses into its shadow. */\n pressedOffset: { x: number; y: number };\n };\n card: {\n /** The resting shadow of each elevation tier. */\n shadow: Record<'xs' | 'sm' | 'md', ShadowKey | null>;\n /** The lift of an interactive card on hover, or on press where there is no hover. */\n hoverShadow: ShadowKey | null;\n };\n chat: {\n /** Under the composer and the room's bar. */\n barShadow: ShadowKey | null;\n /** Under the reaction picker and other popovers. */\n popoverShadow: ShadowKey | null;\n };\n}\n\nexport type ScaleStep = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;\n/** An eleven-step colour scale, as authored. */\nexport type Scale = Record<ScaleStep, ColorString>;\n\nexport interface Theme {\n name: string;\n light: ThemeMode;\n dark: ThemeMode;\n radius: Radius;\n font: Font;\n /** `accent` is @beta: added for the chat's tokens; may change before release. */\n scale: { primary: Scale; accent: Scale };\n components: Components;\n}\n\n/** What a theme file writes: `dark` overlays `light`, as the CSS cascades. */\nexport interface ThemeInput extends Omit<Theme, 'dark'> {\n dark: Partial<ThemeMode>;\n}\n\n/** A colour taken from `ref` in the same mode, at `alpha`. */\nexport const mix = (ref: ColorKey, alpha: number, space: Mix['space'] = 'srgb'): Mix => ({\n ref,\n alpha,\n space,\n});\n\n/** Fills `dark` from `light`, so a theme writes only what changes. */\nexport function defineTheme(input: ThemeInput): Theme {\n return { ...input, dark: { ...input.light, ...input.dark } };\n}\n\nexport const isMix = (value: unknown): value is Mix =>\n typeof value === 'object' && value !== null && 'ref' in value;\n\nexport const isShadow = (value: unknown): value is Shadow =>\n typeof value === 'object' && value !== null && 'blur' in value;\n"],"mappings":";;AA4KA,IAAa,OAAO,KAAe,OAAe,QAAsB,YAAiB;CACvF;CACA;CACA;AACF;;AAGA,SAAgB,YAAY,OAA0B;CACpD,OAAO;EAAE,GAAG;EAAO,MAAM;GAAE,GAAG,MAAM;GAAO,GAAG,MAAM;EAAK;CAAE;AAC7D;AAEA,IAAa,SAAS,UACpB,OAAO,UAAU,YAAY,UAAU,QAAQ,SAAS;AAE1D,IAAa,YAAY,UACvB,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU"}
1
+ {"version":3,"file":"theme-Bqyh6GYU.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\n/** A colour as authored: hex, `rgb()`/`rgba()`, or `oklch()`. */\nexport type ColorString = string;\n\n/**\n * A colour taken from another token of the same mode, at an alpha — what\n * the CSS writes as `color-mix(in srgb, var(--text) 62%, transparent)`.\n * A reference, not a value, so it follows the token it points at: the hard\n * shadow is \"the ink at 62%\" in light and in dark alike.\n */\nexport interface Mix {\n ref: ColorKey;\n alpha: number;\n /** The space the CSS mixes in. Mixing with transparent only scales alpha, so the resolved colour is the same in any. */\n space: 'srgb' | 'oklch';\n}\n\nexport type ColorSpec = ColorString | Mix;\n\n/** One layer of a shadow in parts, so each renderer draws it its own way. */\nexport interface Shadow {\n x: number;\n y: number;\n blur: number;\n /** Grows (or, negative, shrinks) the shadow before the blur. Left out, the CSS writes none. */\n spread?: number;\n color: ColorSpec;\n}\n\n/**\n * A shadow as the CSS writes it: one or more layers, the first on top. A\n * component's shadow is `null` for none.\n */\nexport type ShadowLayers = readonly Shadow[];\n\n/** The colours of one mode. Keys are the CSS token names in camelCase: `--text-muted` is `textMuted`. */\nexport interface ThemeMode {\n primary: ColorSpec;\n secondary: ColorSpec;\n accent: ColorSpec;\n text: ColorSpec;\n textMuted: ColorSpec;\n textSubtle: ColorSpec;\n textOnPrimary: ColorSpec;\n textOnSecondary: ColorSpec;\n textOnAccent: ColorSpec;\n surface: ColorSpec;\n surfaceGlass: ColorSpec;\n card: ColorSpec;\n cardHover: ColorSpec;\n border1: ColorSpec;\n border2: ColorSpec;\n focusRing: ColorSpec;\n errorSolid: ColorSpec;\n errorOnSolid: ColorSpec;\n errorBg: ColorSpec;\n errorBorder: ColorSpec;\n errorText: ColorSpec;\n successSolid: ColorSpec;\n successOnSolid: ColorSpec;\n successBg: ColorSpec;\n successBorder: ColorSpec;\n successText: ColorSpec;\n infoSolid: ColorSpec;\n infoOnSolid: ColorSpec;\n infoBg: ColorSpec;\n infoBorder: ColorSpec;\n infoText: ColorSpec;\n warningSolid: ColorSpec;\n warningOnSolid: ColorSpec;\n warningBg: ColorSpec;\n warningBorder: ColorSpec;\n warningText: ColorSpec;\n /**\n * The chat's colours, the web's `--chat-*`.\n * @beta Named and valued as the chat components are; may change before release.\n */\n chatSidebarBg: ColorSpec;\n /** The pointed-at message, and the active room in a list. */\n chatActiveBg: ColorSpec;\n chatActiveFg: ColorSpec;\n /** The band behind a message that mentions you; its edge is `accent`. */\n chatMentionBg: ColorSpec;\n chatNick: ColorSpec;\n chatNickOp: ColorSpec;\n chatNickVoice: ColorSpec;\n chatAvatarBg: ColorSpec;\n chatAvatarFg: ColorSpec;\n chatAvatarAltBg: ColorSpec;\n chatAvatarAltFg: ColorSpec;\n chatBubbleBg: ColorSpec;\n chatBubbleMeBg: ColorSpec;\n chatBubbleMeFg: ColorSpec;\n /** A reaction of your own. */\n chatReactionMeBg: ColorSpec;\n chatReactionMeBorder: ColorSpec;\n chatRowHoverBg: ColorSpec;\n chatPopoverBg: ColorSpec;\n}\n\n/** The colours of a mode — what a `Mix` may point at. */\nexport type ColorKey = keyof ThemeMode;\n\n/** Rounding in px, one scale for every corner. */\nexport interface Radius {\n xs: number;\n sm: number;\n md: number;\n lg: number;\n xl: number;\n pill: number;\n overlay: number;\n}\n\n/** Font families by role. Names only — each renderer loads the files. */\nexport interface Font {\n display: string;\n body: string;\n mono: string;\n}\n\n/**\n * How components wear the theme — the CSS's `--button-*`, `--card-*` and\n * the chat's shadows. A shadow is its layers, its colours references where\n * the CSS has them, so each mode inks its own; `null` is no shadow.\n * @beta Grows as renderers need more components; may change before release.\n */\nexport interface Components {\n button: {\n /** A step of `radius`. */\n radius: keyof Radius;\n shadow: ShadowLayers | null;\n /** The shadow while pressed. */\n pressedShadow: ShadowLayers | null;\n /** How far a pressed button moves, in px — Bureau presses into its shadow. */\n pressedOffset: { x: number; y: number };\n /** How a pressed button scales — the standard button grows a little; 1 is none. */\n pressedScale: number;\n };\n card: {\n /** The resting shadow of each elevation tier. */\n shadow: Record<'xs' | 'sm' | 'md', ShadowLayers | null>;\n /** The lift of an interactive card on hover, or on press where there is no hover. */\n hoverShadow: ShadowLayers | null;\n /** The card that should stand out (`featured`): its frame, and the shadow it keeps. */\n featured: {\n /** In px. */\n borderWidth: number;\n borderColor: ColorKey;\n shadow: ShadowLayers | null;\n };\n };\n chat: {\n /** Under the composer and the room's bar. */\n barShadow: ShadowLayers | null;\n /** Under the reaction picker and other popovers. */\n popoverShadow: ShadowLayers | null;\n };\n}\n\nexport type ScaleStep = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;\n/** An eleven-step colour scale, as authored. */\nexport type Scale = Record<ScaleStep, ColorString>;\n\nexport interface Theme {\n name: string;\n light: ThemeMode;\n dark: ThemeMode;\n radius: Radius;\n font: Font;\n /** `accent` is @beta: added for the chat's tokens; may change before release. */\n scale: { primary: Scale; accent: Scale };\n components: Components;\n}\n\n/** What a theme file writes: `dark` overlays `light`, as the CSS cascades. */\nexport interface ThemeInput extends Omit<Theme, 'dark'> {\n dark: Partial<ThemeMode>;\n}\n\n/** A colour taken from `ref` in the same mode, at `alpha`. */\nexport const mix = (ref: ColorKey, alpha: number, space: Mix['space'] = 'srgb'): Mix => ({\n ref,\n alpha,\n space,\n});\n\n/** Fills `dark` from `light`, so a theme writes only what changes. */\nexport function defineTheme(input: ThemeInput): Theme {\n return { ...input, dark: { ...input.light, ...input.dark } };\n}\n\nexport const isMix = (value: unknown): value is Mix =>\n typeof value === 'object' && value !== null && 'ref' in value;\n\nexport const isShadow = (value: unknown): value is Shadow =>\n typeof value === 'object' && value !== null && 'blur' in value;\n"],"mappings":";;AAwLA,IAAa,OAAO,KAAe,OAAe,QAAsB,YAAiB;CACvF;CACA;CACA;AACF;;AAGA,SAAgB,YAAY,OAA0B;CACpD,OAAO;EAAE,GAAG;EAAO,MAAM;GAAE,GAAG,MAAM;GAAO,GAAG,MAAM;EAAK;CAAE;AAC7D;AAEA,IAAa,SAAS,UACpB,OAAO,UAAU,YAAY,UAAU,QAAQ,SAAS;AAE1D,IAAa,YAAY,UACvB,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU"}
package/dist/theme.d.ts CHANGED
@@ -13,13 +13,20 @@ export interface Mix {
13
13
  space: 'srgb' | 'oklch';
14
14
  }
15
15
  export type ColorSpec = ColorString | Mix;
16
- /** An offset shadow in parts, so each renderer draws it its own way. */
16
+ /** One layer of a shadow in parts, so each renderer draws it its own way. */
17
17
  export interface Shadow {
18
18
  x: number;
19
19
  y: number;
20
20
  blur: number;
21
+ /** Grows (or, negative, shrinks) the shadow before the blur. Left out, the CSS writes none. */
22
+ spread?: number;
21
23
  color: ColorSpec;
22
24
  }
25
+ /**
26
+ * A shadow as the CSS writes it: one or more layers, the first on top. A
27
+ * component's shadow is `null` for none.
28
+ */
29
+ export type ShadowLayers = readonly Shadow[];
23
30
  /** The colours of one mode. Keys are the CSS token names in camelCase: `--text-muted` is `textMuted`. */
24
31
  export interface ThemeMode {
25
32
  primary: ColorSpec;
@@ -83,13 +90,9 @@ export interface ThemeMode {
83
90
  chatReactionMeBorder: ColorSpec;
84
91
  chatRowHoverBg: ColorSpec;
85
92
  chatPopoverBg: ColorSpec;
86
- shadowHard: Shadow;
87
- shadowHardLg: Shadow;
88
- shadowHardXl: Shadow;
89
93
  }
90
- export type ShadowKey = 'shadowHard' | 'shadowHardLg' | 'shadowHardXl';
91
- /** The keys of a mode that hold a colour — what a `Mix` may point at. */
92
- export type ColorKey = Exclude<keyof ThemeMode, ShadowKey>;
94
+ /** The colours of a mode — what a `Mix` may point at. */
95
+ export type ColorKey = keyof ThemeMode;
93
96
  /** Rounding in px, one scale for every corner. */
94
97
  export interface Radius {
95
98
  xs: number;
@@ -108,35 +111,43 @@ export interface Font {
108
111
  }
109
112
  /**
110
113
  * How components wear the theme — the CSS's `--button-*`, `--card-*` and
111
- * the chat's shadows.
112
- * Shadows name a shadow of the mode, so each mode inks its own; `null` is
113
- * no shadow.
114
+ * the chat's shadows. A shadow is its layers, its colours references where
115
+ * the CSS has them, so each mode inks its own; `null` is no shadow.
114
116
  * @beta Grows as renderers need more components; may change before release.
115
117
  */
116
118
  export interface Components {
117
119
  button: {
118
120
  /** A step of `radius`. */
119
121
  radius: keyof Radius;
120
- shadow: ShadowKey | null;
122
+ shadow: ShadowLayers | null;
121
123
  /** The shadow while pressed. */
122
- pressedShadow: ShadowKey | null;
124
+ pressedShadow: ShadowLayers | null;
123
125
  /** How far a pressed button moves, in px — Bureau presses into its shadow. */
124
126
  pressedOffset: {
125
127
  x: number;
126
128
  y: number;
127
129
  };
130
+ /** How a pressed button scales — the standard button grows a little; 1 is none. */
131
+ pressedScale: number;
128
132
  };
129
133
  card: {
130
134
  /** The resting shadow of each elevation tier. */
131
- shadow: Record<'xs' | 'sm' | 'md', ShadowKey | null>;
135
+ shadow: Record<'xs' | 'sm' | 'md', ShadowLayers | null>;
132
136
  /** The lift of an interactive card on hover, or on press where there is no hover. */
133
- hoverShadow: ShadowKey | null;
137
+ hoverShadow: ShadowLayers | null;
138
+ /** The card that should stand out (`featured`): its frame, and the shadow it keeps. */
139
+ featured: {
140
+ /** In px. */
141
+ borderWidth: number;
142
+ borderColor: ColorKey;
143
+ shadow: ShadowLayers | null;
144
+ };
134
145
  };
135
146
  chat: {
136
147
  /** Under the composer and the room's bar. */
137
- barShadow: ShadowKey | null;
148
+ barShadow: ShadowLayers | null;
138
149
  /** Under the reaction picker and other popovers. */
139
- popoverShadow: ShadowKey | null;
150
+ popoverShadow: ShadowLayers | null;
140
151
  };
141
152
  }
142
153
  export type ScaleStep = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAIA,iEAAiE;AACjE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,WAAW,GAAG;IAClB,GAAG,EAAE,QAAQ,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,wHAAwH;IACxH,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,GAAG,CAAC;AAE1C,wEAAwE;AACxE,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,yGAAyG;AACzG,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,SAAS,CAAC;IACtB,aAAa,EAAE,SAAS,CAAC;IACzB,eAAe,EAAE,SAAS,CAAC;IAC3B,YAAY,EAAE,SAAS,CAAC;IACxB,OAAO,EAAE,SAAS,CAAC;IACnB,YAAY,EAAE,SAAS,CAAC;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,SAAS,CAAC;IACtB,YAAY,EAAE,SAAS,CAAC;IACxB,OAAO,EAAE,SAAS,CAAC;IACnB,WAAW,EAAE,SAAS,CAAC;IACvB,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,SAAS,CAAC;IACxB,cAAc,EAAE,SAAS,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,WAAW,EAAE,SAAS,CAAC;IACvB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,SAAS,CAAC;IACvB,MAAM,EAAE,SAAS,CAAC;IAClB,UAAU,EAAE,SAAS,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC;IACpB,YAAY,EAAE,SAAS,CAAC;IACxB,cAAc,EAAE,SAAS,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,WAAW,EAAE,SAAS,CAAC;IACvB;;;OAGG;IACH,aAAa,EAAE,SAAS,CAAC;IACzB,6DAA6D;IAC7D,YAAY,EAAE,SAAS,CAAC;IACxB,YAAY,EAAE,SAAS,CAAC;IACxB,yEAAyE;IACzE,aAAa,EAAE,SAAS,CAAC;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,EAAE,SAAS,CAAC;IACtB,aAAa,EAAE,SAAS,CAAC;IACzB,YAAY,EAAE,SAAS,CAAC;IACxB,YAAY,EAAE,SAAS,CAAC;IACxB,eAAe,EAAE,SAAS,CAAC;IAC3B,eAAe,EAAE,SAAS,CAAC;IAC3B,YAAY,EAAE,SAAS,CAAC;IACxB,cAAc,EAAE,SAAS,CAAC;IAC1B,cAAc,EAAE,SAAS,CAAC;IAC1B,8BAA8B;IAC9B,gBAAgB,EAAE,SAAS,CAAC;IAC5B,oBAAoB,EAAE,SAAS,CAAC;IAChC,cAAc,EAAE,SAAS,CAAC;IAC1B,aAAa,EAAE,SAAS,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,cAAc,CAAC;AACvE,yEAAyE;AACzE,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,SAAS,EAAE,SAAS,CAAC,CAAC;AAE3D,kDAAkD;AAClD,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,MAAM,WAAW,IAAI;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE;QACN,0BAA0B;QAC1B,MAAM,EAAE,MAAM,MAAM,CAAC;QACrB,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;QACzB,gCAAgC;QAChC,aAAa,EAAE,SAAS,GAAG,IAAI,CAAC;QAChC,8EAA8E;QAC9E,aAAa,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACzC,CAAC;IACF,IAAI,EAAE;QACJ,iDAAiD;QACjD,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC,CAAC;QACrD,qFAAqF;QACrF,WAAW,EAAE,SAAS,GAAG,IAAI,CAAC;KAC/B,CAAC;IACF,IAAI,EAAE;QACJ,6CAA6C;QAC7C,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;QAC5B,oDAAoD;QACpD,aAAa,EAAE,SAAS,GAAG,IAAI,CAAC;KACjC,CAAC;CACH;AAED,MAAM,MAAM,SAAS,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AACvF,gDAAgD;AAChD,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAEnD,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,IAAI,CAAC;IACX,iFAAiF;IACjF,KAAK,EAAE;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,KAAK,CAAA;KAAE,CAAC;IACzC,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,8EAA8E;AAC9E,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC;IACrD,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CAC1B;AAED,8DAA8D;AAC9D,eAAO,MAAM,GAAG,GAAI,KAAK,QAAQ,EAAE,OAAO,MAAM,EAAE,QAAO,GAAG,CAAC,OAAO,CAAU,KAAG,GAI/E,CAAC;AAEH,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,CAEpD;AAED,eAAO,MAAM,KAAK,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,GACe,CAAC;AAEhE,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACa,CAAC"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bureau.d.ts","sourceRoot":"","sources":["../../src/themes/bureau.ts"],"names":[],"mappings":"AAiDA;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,oBAmKjB,CAAC"}
1
+ {"version":3,"file":"bureau.d.ts","sourceRoot":"","sources":["../../src/themes/bureau.ts"],"names":[],"mappings":"AAsDA;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,oBAiKjB,CAAC"}
@@ -1,6 +1,12 @@
1
1
  import { i as mix, t as defineTheme } from "../theme-Bqyh6GYU.js";
2
2
  //#region src/themes/bureau.ts
3
3
  var ink = mix("text", .62);
4
+ var hard = (offset) => [{
5
+ x: offset,
6
+ y: offset,
7
+ blur: 0,
8
+ color: ink
9
+ }];
4
10
  var primary = {
5
11
  50: "oklch(0.975 0.01 210)",
6
12
  100: "oklch(0.945 0.024 210)",
@@ -55,24 +61,6 @@ var bureau = defineTheme({
55
61
  border1: "#ddd6c5",
56
62
  border2: "#20242c",
57
63
  focusRing: "#006a7d",
58
- shadowHard: {
59
- x: 2,
60
- y: 2,
61
- blur: 0,
62
- color: ink
63
- },
64
- shadowHardLg: {
65
- x: 3,
66
- y: 3,
67
- blur: 0,
68
- color: ink
69
- },
70
- shadowHardXl: {
71
- x: 5,
72
- y: 5,
73
- blur: 0,
74
- color: ink
75
- },
76
64
  errorSolid: "#b53026",
77
65
  errorOnSolid: "#f4efe2",
78
66
  errorBg: "#f7e6e3",
@@ -181,24 +169,30 @@ var bureau = defineTheme({
181
169
  components: {
182
170
  button: {
183
171
  radius: "md",
184
- shadow: "shadowHard",
172
+ shadow: hard(2),
185
173
  pressedShadow: null,
186
174
  pressedOffset: {
187
175
  x: 2,
188
176
  y: 2
189
- }
177
+ },
178
+ pressedScale: 1
190
179
  },
191
180
  card: {
192
181
  shadow: {
193
- xs: "shadowHard",
194
- sm: "shadowHard",
195
- md: "shadowHard"
182
+ xs: hard(2),
183
+ sm: hard(2),
184
+ md: hard(2)
196
185
  },
197
- hoverShadow: "shadowHardLg"
186
+ hoverShadow: hard(3),
187
+ featured: {
188
+ borderWidth: 8,
189
+ borderColor: "primary",
190
+ shadow: null
191
+ }
198
192
  },
199
193
  chat: {
200
- barShadow: "shadowHard",
201
- popoverShadow: "shadowHardLg"
194
+ barShadow: hard(2),
195
+ popoverShadow: hard(3)
202
196
  }
203
197
  }
204
198
  });
@@ -1 +1 @@
1
- {"version":3,"file":"bureau.js","names":[],"sources":["../../src/themes/bureau.ts"],"sourcesContent":["// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\nimport { defineTheme, mix, type Scale } from '../theme';\n\n// The hard offset shadow, Bureau's signature: the ink at reduced alpha, so\n// it contrasts both the paper and the element without reading as a solid\n// block. A reference, so in dark mode it re-resolves to the cream ink.\nconst ink = mix('text', 0.62);\n\n// Teal, light to dark. Absolute steps serving both modes: 700 is the\n// light arm's primary, 400 sits next to the dark arm's muted teal.\nconst primary: Scale = {\n 50: 'oklch(0.975 0.01 210)',\n 100: 'oklch(0.945 0.024 210)',\n 200: 'oklch(0.89 0.045 211)',\n 300: 'oklch(0.81 0.068 210)',\n 400: 'oklch(0.7 0.085 208)',\n 500: 'oklch(0.62 0.095 212)',\n 600: 'oklch(0.55 0.097 215)',\n 700: 'oklch(0.483 0.086 216)',\n 800: 'oklch(0.4 0.07 217)',\n 900: 'oklch(0.32 0.055 218)',\n 950: 'oklch(0.24 0.04 218)',\n};\n\n// The standard theme's amber, which the CSS cascades into Bureau.\nconst accent: Scale = {\n 50: 'oklch(0.988 0.012 92)',\n 100: 'oklch(0.965 0.04 90)',\n 200: 'oklch(0.928 0.075 88)',\n 300: 'oklch(0.88 0.105 88)',\n 400: 'oklch(0.815 0.125 85)',\n 500: 'oklch(0.745 0.13 82)',\n 600: 'oklch(0.64 0.125 80)',\n 700: 'oklch(0.52 0.105 78)',\n 800: 'oklch(0.405 0.085 76)',\n 900: 'oklch(0.295 0.06 75)',\n 950: 'oklch(0.18 0.038 74)',\n};\n\n// Steps of the standard theme's warm-grey and neutral scales the chat reads.\nconst secondary100 = 'oklch(0.985 0.004 88)';\nconst secondary400 = 'oklch(0.88 0.025 82)';\nconst neutral50 = 'oklch(0.9851 0.0001 263.3)';\nconst neutral700 = 'oklch(0.3715 0 263.3)';\nconst neutral900 = 'oklch(0.2046 0 263.3)';\n\n/**\n * Bureau — \"moderne retro\", paper, teal and ink. Values as in\n * `ratio-ui/src/themes/bureau.css`, which stays the web's source until the\n * generator lands; `bureau.test.ts` holds the two to each other.\n * Semantic tokens, the chat's colours, the primary and accent scales, and\n * the button, card and chat-shadow tokens a native renderer needs; the rest of the component tokens stay in the CSS\n * until a renderer needs them.\n */\nexport const bureau = defineTheme({\n name: 'bureau',\n\n light: {\n // Dark enough to carry link text at AA on the paper and on cards.\n primary: '#006a7d', // teal\n secondary: '#d9d2bf', // muted paper, a step down from surface\n accent: '#f0b429', // ochre\n\n text: '#20242c',\n // Darkened for WCAG AA on the mid-tone paper.\n textMuted: '#5c5644',\n textSubtle: '#6b6450',\n textOnPrimary: '#f4efe2',\n textOnSecondary: '#20242c',\n textOnAccent: '#20242c',\n\n surface: '#e6e0d1', // paper\n surfaceGlass: mix('surface', 0.88, 'oklch'),\n card: '#fffdf7',\n cardHover: '#fbf6ea',\n\n border1: '#ddd6c5',\n border2: '#20242c', // the ink: the strong edge round panels\n\n focusRing: '#006a7d', // solid teal: a crisp ring, not a haze\n\n // 2px matches the press-down distance, so a pressed button covers it.\n shadowHard: { x: 2, y: 2, blur: 0, color: ink },\n shadowHardLg: { x: 3, y: 3, blur: 0, color: ink },\n shadowHardXl: { x: 5, y: 5, blur: 0, color: ink },\n\n // Brick — the only status family Bureau sets; the rest inherit.\n errorSolid: '#b53026',\n errorOnSolid: '#f4efe2',\n errorBg: '#f7e6e3',\n errorBorder: '#e3b3ac',\n errorText: '#8a241c',\n\n // Success, info and warning are the standard theme's, which the CSS\n // cascades into Bureau; written out here until the standard theme has\n // its own file to inherit from.\n successSolid: '#2e9a62',\n successOnSolid: 'oklch(0.9851 0.0001 263.3)',\n successBg: '#eaf7f0',\n successBorder: '#bfe8d2',\n successText: '#1f6b46',\n infoSolid: '#2f7fa0',\n infoOnSolid: 'oklch(0.9851 0.0001 263.3)',\n infoBg: '#eaf4fa',\n infoBorder: '#c6e3f1',\n infoText: '#225e77',\n warningSolid: '#d97706',\n warningOnSolid: '#1a1405',\n warningBg: '#fff6de',\n warningBorder: '#ffe0a3',\n warningText: '#8a4b05',\n\n // The chat's colours are the standard theme's (tokens/chat.css), read\n // through Bureau's scales.\n chatSidebarBg: secondary100,\n chatActiveBg: primary[100],\n chatActiveFg: primary[900],\n chatMentionBg: accent[100],\n chatNick: neutral700,\n chatNickOp: primary[700],\n chatNickVoice: accent[700],\n chatAvatarBg: primary[700],\n chatAvatarFg: secondary100,\n chatAvatarAltBg: accent[200],\n chatAvatarAltFg: accent[900],\n chatBubbleBg: '#fffdf7', // the card\n chatBubbleMeBg: primary[700],\n chatBubbleMeFg: secondary100,\n chatReactionMeBg: primary[100],\n chatReactionMeBorder: primary[300],\n chatRowHoverBg: 'rgb(0 0 0 / 0.035)',\n chatPopoverBg: neutral50,\n },\n\n dark: {\n // Muted: a dark page carries less colour.\n primary: '#4fa3ae', // teal\n secondary: '#20231b',\n accent: '#ffc23d', // gold\n\n text: '#f0e7d2', // ink\n textMuted: '#a39c86',\n textSubtle: '#938b75',\n textOnPrimary: '#14140f',\n textOnSecondary: '#f0e7d2',\n textOnAccent: '#14140f',\n\n surface: '#14140f',\n surfaceGlass: mix('surface', 0.88, 'oklch'),\n card: '#20231b',\n cardHover: '#272b20',\n\n border1: '#2c2c22',\n border2: '#565341',\n\n focusRing: '#4fa3ae',\n\n // Coral.\n errorSolid: '#ff6e52',\n errorOnSolid: '#14140f',\n errorBg: 'rgba(255, 110, 82, 0.18)',\n errorBorder: 'rgba(255, 110, 82, 0.32)',\n errorText: '#ffb3a4',\n\n // The standard dark tints; the solids carry over from light.\n successBg: 'rgba(46, 154, 98, 0.16)',\n successBorder: 'rgba(46, 154, 98, 0.28)',\n successText: '#a7e7c6',\n infoBg: 'rgba(47, 127, 160, 0.16)',\n infoBorder: 'rgba(47, 127, 160, 0.28)',\n infoText: '#a7d9ee',\n warningBg: 'rgba(217, 119, 6, 0.18)',\n warningBorder: 'rgba(217, 119, 6, 0.3)',\n warningText: '#ffd18a',\n\n chatSidebarBg: '#20231b', // the card\n chatActiveBg: primary[900],\n chatActiveFg: secondary100,\n chatMentionBg: 'oklch(0.26 0.05 75)',\n chatNick: secondary400,\n chatNickOp: primary[300],\n chatNickVoice: accent[300],\n chatAvatarBg: primary[400],\n chatAvatarFg: primary[950],\n chatBubbleBg: '#20231b', // the card\n chatBubbleMeBg: primary[400],\n chatBubbleMeFg: primary[950],\n chatReactionMeBg: primary[900],\n chatReactionMeBorder: primary[700],\n chatRowHoverBg: 'rgb(255 255 255 / 0.045)',\n chatPopoverBg: neutral900,\n // Shadows inherit: the same `ink` reference, now the cream text.\n },\n\n // Tight corners — every step the same hard edge.\n radius: { xs: 2, sm: 2, md: 3, lg: 4, xl: 4, pill: 3, overlay: 4 },\n\n // Families only; the web embeds woff2, native loads TTF.\n font: { display: 'Fira Sans', body: 'Fira Sans', mono: 'Space Mono' },\n\n scale: { primary, accent },\n\n components: {\n // The hard shadow at rest; a press moves the button into it, and the\n // shadow goes, so the button seems to sink into the page.\n button: {\n radius: 'md',\n shadow: 'shadowHard',\n pressedShadow: null,\n pressedOffset: { x: 2, y: 2 },\n },\n card: {\n shadow: { xs: 'shadowHard', sm: 'shadowHard', md: 'shadowHard' },\n hoverShadow: 'shadowHardLg',\n },\n chat: { barShadow: 'shadowHard', popoverShadow: 'shadowHardLg' },\n },\n});\n"],"mappings":";;AASA,IAAM,MAAM,IAAI,QAAQ,GAAI;AAI5B,IAAM,UAAiB;CACrB,IAAI;CACJ,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP;AAGA,IAAM,SAAgB;CACpB,IAAI;CACJ,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP;AAGA,IAAM,eAAe;;;;;;;;;AAcrB,IAAa,SAAS,YAAY;CAChC,MAAM;CAEN,OAAO;EAEL,SAAS;EACT,WAAW;EACX,QAAQ;EAER,MAAM;EAEN,WAAW;EACX,YAAY;EACZ,eAAe;EACf,iBAAiB;EACjB,cAAc;EAEd,SAAS;EACT,cAAc,IAAI,WAAW,KAAM,OAAO;EAC1C,MAAM;EACN,WAAW;EAEX,SAAS;EACT,SAAS;EAET,WAAW;EAGX,YAAY;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,OAAO;EAAI;EAC9C,cAAc;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,OAAO;EAAI;EAChD,cAAc;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,OAAO;EAAI;EAGhD,YAAY;EACZ,cAAc;EACd,SAAS;EACT,aAAa;EACb,WAAW;EAKX,cAAc;EACd,gBAAgB;EAChB,WAAW;EACX,eAAe;EACf,aAAa;EACb,WAAW;EACX,aAAa;EACb,QAAQ;EACR,YAAY;EACZ,UAAU;EACV,cAAc;EACd,gBAAgB;EAChB,WAAW;EACX,eAAe;EACf,aAAa;EAIb,eAAe;EACf,cAAc,QAAQ;EACtB,cAAc,QAAQ;EACtB,eAAe,OAAO;EACtB,UAAU;EACV,YAAY,QAAQ;EACpB,eAAe,OAAO;EACtB,cAAc,QAAQ;EACtB,cAAc;EACd,iBAAiB,OAAO;EACxB,iBAAiB,OAAO;EACxB,cAAc;EACd,gBAAgB,QAAQ;EACxB,gBAAgB;EAChB,kBAAkB,QAAQ;EAC1B,sBAAsB,QAAQ;EAC9B,gBAAgB;EAChB,eAAe;CACjB;CAEA,MAAM;EAEJ,SAAS;EACT,WAAW;EACX,QAAQ;EAER,MAAM;EACN,WAAW;EACX,YAAY;EACZ,eAAe;EACf,iBAAiB;EACjB,cAAc;EAEd,SAAS;EACT,cAAc,IAAI,WAAW,KAAM,OAAO;EAC1C,MAAM;EACN,WAAW;EAEX,SAAS;EACT,SAAS;EAET,WAAW;EAGX,YAAY;EACZ,cAAc;EACd,SAAS;EACT,aAAa;EACb,WAAW;EAGX,WAAW;EACX,eAAe;EACf,aAAa;EACb,QAAQ;EACR,YAAY;EACZ,UAAU;EACV,WAAW;EACX,eAAe;EACf,aAAa;EAEb,eAAe;EACf,cAAc,QAAQ;EACtB,cAAc;EACd,eAAe;EACf,UAAU;EACV,YAAY,QAAQ;EACpB,eAAe,OAAO;EACtB,cAAc,QAAQ;EACtB,cAAc,QAAQ;EACtB,cAAc;EACd,gBAAgB,QAAQ;EACxB,gBAAgB,QAAQ;EACxB,kBAAkB,QAAQ;EAC1B,sBAAsB,QAAQ;EAC9B,gBAAgB;EAChB,eAAe;CAEjB;CAGA,QAAQ;EAAE,IAAI;EAAG,IAAI;EAAG,IAAI;EAAG,IAAI;EAAG,IAAI;EAAG,MAAM;EAAG,SAAS;CAAE;CAGjE,MAAM;EAAE,SAAS;EAAa,MAAM;EAAa,MAAM;CAAa;CAEpE,OAAO;EAAE;EAAS;CAAO;CAEzB,YAAY;EAGV,QAAQ;GACN,QAAQ;GACR,QAAQ;GACR,eAAe;GACf,eAAe;IAAE,GAAG;IAAG,GAAG;GAAE;EAC9B;EACA,MAAM;GACJ,QAAQ;IAAE,IAAI;IAAc,IAAI;IAAc,IAAI;GAAa;GAC/D,aAAa;EACf;EACA,MAAM;GAAE,WAAW;GAAc,eAAe;EAAe;CACjE;AACF,CAAC"}
1
+ {"version":3,"file":"bureau.js","names":[],"sources":["../../src/themes/bureau.ts"],"sourcesContent":["// ratio-ui · design system for knowledge sharing\n// SPDX-FileCopyrightText: 2026 Losol AS\n// SPDX-License-Identifier: MPL-2.0\n\nimport { defineTheme, mix, type Scale, type ShadowLayers } from '../theme';\n\n// The hard offset shadow, Bureau's signature: the ink at reduced alpha, so\n// it contrasts both the paper and the element without reading as a solid\n// block. A reference, so in dark mode it re-resolves to the cream ink.\nconst ink = mix('text', 0.62);\n\n// The CSS's --shadow-hard (2px), --shadow-hard-lg (3px) and --shadow-hard-xl\n// (5px): an offset with no blur. 2px matches the press-down distance, so a\n// pressed button covers it.\nconst hard = (offset: number): ShadowLayers => [{ x: offset, y: offset, blur: 0, color: ink }];\n\n// Teal, light to dark. Absolute steps serving both modes: 700 is the\n// light arm's primary, 400 sits next to the dark arm's muted teal.\nconst primary: Scale = {\n 50: 'oklch(0.975 0.01 210)',\n 100: 'oklch(0.945 0.024 210)',\n 200: 'oklch(0.89 0.045 211)',\n 300: 'oklch(0.81 0.068 210)',\n 400: 'oklch(0.7 0.085 208)',\n 500: 'oklch(0.62 0.095 212)',\n 600: 'oklch(0.55 0.097 215)',\n 700: 'oklch(0.483 0.086 216)',\n 800: 'oklch(0.4 0.07 217)',\n 900: 'oklch(0.32 0.055 218)',\n 950: 'oklch(0.24 0.04 218)',\n};\n\n// The standard theme's amber, which the CSS cascades into Bureau.\nconst accent: Scale = {\n 50: 'oklch(0.988 0.012 92)',\n 100: 'oklch(0.965 0.04 90)',\n 200: 'oklch(0.928 0.075 88)',\n 300: 'oklch(0.88 0.105 88)',\n 400: 'oklch(0.815 0.125 85)',\n 500: 'oklch(0.745 0.13 82)',\n 600: 'oklch(0.64 0.125 80)',\n 700: 'oklch(0.52 0.105 78)',\n 800: 'oklch(0.405 0.085 76)',\n 900: 'oklch(0.295 0.06 75)',\n 950: 'oklch(0.18 0.038 74)',\n};\n\n// Steps of the standard theme's warm-grey and neutral scales the chat reads.\nconst secondary100 = 'oklch(0.985 0.004 88)';\nconst secondary400 = 'oklch(0.88 0.025 82)';\nconst neutral50 = 'oklch(0.9851 0.0001 263.3)';\nconst neutral700 = 'oklch(0.3715 0 263.3)';\nconst neutral900 = 'oklch(0.2046 0 263.3)';\n\n/**\n * Bureau — \"moderne retro\", paper, teal and ink. Values as in\n * `ratio-ui/src/themes/bureau.css`, which stays the web's source until the\n * generator lands; `bureau.test.ts` holds the two to each other.\n * Semantic tokens, the chat's colours, the primary and accent scales, and\n * the button, card and chat-shadow tokens a native renderer needs; the rest of the component tokens stay in the CSS\n * until a renderer needs them.\n */\nexport const bureau = defineTheme({\n name: 'bureau',\n\n light: {\n // Dark enough to carry link text at AA on the paper and on cards.\n primary: '#006a7d', // teal\n secondary: '#d9d2bf', // muted paper, a step down from surface\n accent: '#f0b429', // ochre\n\n text: '#20242c',\n // Darkened for WCAG AA on the mid-tone paper.\n textMuted: '#5c5644',\n textSubtle: '#6b6450',\n textOnPrimary: '#f4efe2',\n textOnSecondary: '#20242c',\n textOnAccent: '#20242c',\n\n surface: '#e6e0d1', // paper\n surfaceGlass: mix('surface', 0.88, 'oklch'),\n card: '#fffdf7',\n cardHover: '#fbf6ea',\n\n border1: '#ddd6c5',\n border2: '#20242c', // the ink: the strong edge round panels\n\n focusRing: '#006a7d', // solid teal: a crisp ring, not a haze\n\n // Brick — the only status family Bureau sets; the rest inherit.\n errorSolid: '#b53026',\n errorOnSolid: '#f4efe2',\n errorBg: '#f7e6e3',\n errorBorder: '#e3b3ac',\n errorText: '#8a241c',\n\n // Success, info and warning are the standard theme's, which the CSS\n // cascades into Bureau; written out here until the standard theme has\n // its own file to inherit from.\n successSolid: '#2e9a62',\n successOnSolid: 'oklch(0.9851 0.0001 263.3)',\n successBg: '#eaf7f0',\n successBorder: '#bfe8d2',\n successText: '#1f6b46',\n infoSolid: '#2f7fa0',\n infoOnSolid: 'oklch(0.9851 0.0001 263.3)',\n infoBg: '#eaf4fa',\n infoBorder: '#c6e3f1',\n infoText: '#225e77',\n warningSolid: '#d97706',\n warningOnSolid: '#1a1405',\n warningBg: '#fff6de',\n warningBorder: '#ffe0a3',\n warningText: '#8a4b05',\n\n // The chat's colours are the standard theme's (tokens/chat.css), read\n // through Bureau's scales.\n chatSidebarBg: secondary100,\n chatActiveBg: primary[100],\n chatActiveFg: primary[900],\n chatMentionBg: accent[100],\n chatNick: neutral700,\n chatNickOp: primary[700],\n chatNickVoice: accent[700],\n chatAvatarBg: primary[700],\n chatAvatarFg: secondary100,\n chatAvatarAltBg: accent[200],\n chatAvatarAltFg: accent[900],\n chatBubbleBg: '#fffdf7', // the card\n chatBubbleMeBg: primary[700],\n chatBubbleMeFg: secondary100,\n chatReactionMeBg: primary[100],\n chatReactionMeBorder: primary[300],\n chatRowHoverBg: 'rgb(0 0 0 / 0.035)',\n chatPopoverBg: neutral50,\n },\n\n dark: {\n // Muted: a dark page carries less colour.\n primary: '#4fa3ae', // teal\n secondary: '#20231b',\n accent: '#ffc23d', // gold\n\n text: '#f0e7d2', // ink\n textMuted: '#a39c86',\n textSubtle: '#938b75',\n textOnPrimary: '#14140f',\n textOnSecondary: '#f0e7d2',\n textOnAccent: '#14140f',\n\n surface: '#14140f',\n surfaceGlass: mix('surface', 0.88, 'oklch'),\n card: '#20231b',\n cardHover: '#272b20',\n\n border1: '#2c2c22',\n border2: '#565341',\n\n focusRing: '#4fa3ae',\n\n // Coral.\n errorSolid: '#ff6e52',\n errorOnSolid: '#14140f',\n errorBg: 'rgba(255, 110, 82, 0.18)',\n errorBorder: 'rgba(255, 110, 82, 0.32)',\n errorText: '#ffb3a4',\n\n // The standard dark tints; the solids carry over from light.\n successBg: 'rgba(46, 154, 98, 0.16)',\n successBorder: 'rgba(46, 154, 98, 0.28)',\n successText: '#a7e7c6',\n infoBg: 'rgba(47, 127, 160, 0.16)',\n infoBorder: 'rgba(47, 127, 160, 0.28)',\n infoText: '#a7d9ee',\n warningBg: 'rgba(217, 119, 6, 0.18)',\n warningBorder: 'rgba(217, 119, 6, 0.3)',\n warningText: '#ffd18a',\n\n chatSidebarBg: '#20231b', // the card\n chatActiveBg: primary[900],\n chatActiveFg: secondary100,\n chatMentionBg: 'oklch(0.26 0.05 75)',\n chatNick: secondary400,\n chatNickOp: primary[300],\n chatNickVoice: accent[300],\n chatAvatarBg: primary[400],\n chatAvatarFg: primary[950],\n chatBubbleBg: '#20231b', // the card\n chatBubbleMeBg: primary[400],\n chatBubbleMeFg: primary[950],\n chatReactionMeBg: primary[900],\n chatReactionMeBorder: primary[700],\n chatRowHoverBg: 'rgb(255 255 255 / 0.045)',\n chatPopoverBg: neutral900,\n // The shadows' ink is a reference, so here it is the cream text.\n },\n\n // Tight corners — every step the same hard edge.\n radius: { xs: 2, sm: 2, md: 3, lg: 4, xl: 4, pill: 3, overlay: 4 },\n\n // Families only; the web embeds woff2, native loads TTF.\n font: { display: 'Fira Sans', body: 'Fira Sans', mono: 'Space Mono' },\n\n scale: { primary, accent },\n\n components: {\n // The hard shadow at rest; a press moves the button into it, and the\n // shadow goes, so the button seems to sink into the page.\n button: {\n radius: 'md',\n shadow: hard(2),\n pressedShadow: null,\n pressedOffset: { x: 2, y: 2 },\n pressedScale: 1,\n },\n card: {\n shadow: { xs: hard(2), sm: hard(2), md: hard(2) },\n hoverShadow: hard(3),\n // Framed like a poster: a thick primary border, no shadow.\n featured: { borderWidth: 8, borderColor: 'primary', shadow: null },\n },\n chat: { barShadow: hard(2), popoverShadow: hard(3) },\n },\n});\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { ColorSpec, Shadow } from './theme';
1
+ import { ColorSpec, Shadow, ShadowLayers } from './theme';
2
2
  /**
3
3
  * The CSS the web writes for a value: the seed of the theme generator, and
4
4
  * what the drift tests hold the hand-written CSS against until it lands.
5
5
  */
6
- export declare function toCssValue(value: ColorSpec | Shadow | number): string;
6
+ export declare function toCssValue(value: ColorSpec | Shadow | ShadowLayers | null | number): string;
7
7
  //# sourceMappingURL=toCssValue.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"toCssValue.d.ts","sourceRoot":"","sources":["../src/toCssValue.ts"],"names":[],"mappings":"AAKA,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAIvE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CASrE"}
1
+ {"version":3,"file":"toCssValue.d.ts","sourceRoot":"","sources":["../src/toCssValue.ts"],"names":[],"mappings":"AAKA,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAI1F;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAc3F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventuras/ratio-ui-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The design system as data — theme values, the token vocabulary, and the pure rules every renderer of Ratio UI must agree on. No React, no DOM, no CSS.",
5
5
  "keywords": [
6
6
  "design-tokens",