@latty-ds/tokens 0.8.0 → 0.10.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
@@ -116,17 +116,17 @@ console.log(tokens.elevation['2']); // "0 4px 6px -1px rgb(…)"
116
116
 
117
117
  ### Primitive tokens (`tokens.css`)
118
118
 
119
- | Prefix | Example | Description |
120
- | --------------------------------------- | ------------------------------ | --------------------------------- |
121
- | `--lt-color-[name]-[step]` | `--lt-color-primary-500` | Color palette steps 50–900 |
122
- | `--lt-color-[name]-muted-[step]` | `--lt-color-primary-muted-300` | Desaturated variant of each color |
123
- | `--lt-color-neutral-[step]` | `--lt-color-neutral-100` | Greyscale scale |
124
- | `--lt-color-white` / `--lt-color-black` | | System colors |
125
- | `--lt-spacing-[n]` | `--lt-spacing-4` → `1rem` | Spacing scale (rem), n = 0–24 |
126
- | `--lt-spacing-px-[n]` | `--lt-spacing-px-4` → `16px` | Spacing scale (px) |
127
- | `--lt-border-radius` | `0.5rem` | Global border radius |
128
- | `--lt-elevation-[n]` | `--lt-elevation-2` | Box shadows, n = 0–5 |
129
- | `--lt-typography-fontFamily` | `"Hanken Grotesk", sans-serif` | Base font family |
119
+ | Prefix | Example | Description |
120
+ | --------------------------------------- | ------------------------------ | -------------------------------------------------------------------------- |
121
+ | `--lt-color-[name]-[step]` | `--lt-color-primary-500` | Color palette steps 50–900 |
122
+ | `--lt-color-[name]-muted-[step]` | `--lt-color-primary-muted-300` | Desaturated variant of each color |
123
+ | `--lt-color-neutral-[step]` | `--lt-color-neutral-100` | Greyscale scale |
124
+ | `--lt-color-white` / `--lt-color-black` | | System colors |
125
+ | `--lt-spacing-[n]` | `--lt-spacing-4` → `1rem` | Spacing scale (rem), n = 0–24 |
126
+ | `--lt-spacing-px-[n]` | `--lt-spacing-px-4` → `16px` | Spacing scale (px) |
127
+ | `--lt-border-radius` | `0.5rem` | Global border radius |
128
+ | `--lt-elevation-[n]` | `--lt-elevation-2` | Box shadows, n = 0–5 |
129
+ | `--lt-typography-fontFamilyPrimary` | `"Hanken Grotesk", sans-serif` | Base font family (additional `...Secondary`/`...Tertiary` when configured) |
130
130
 
131
131
  Color names: `primary`, `secondary`, `success`, `warning`, `error`, `info` (each with a `-muted` variant).
132
132
 
@@ -4,27 +4,74 @@
4
4
  * Any other CSS length (e.g. `'1.5px'`) passes through unchanged.
5
5
  */
6
6
  type BorderWidth = 'thin' | 'medium' | 'thick' | (string & {});
7
+ /** Generic CSS font-family keyword used as the safety-net fallback if a CDN font fails to load. */
8
+ type FontFallback = 'sans-serif' | 'serif' | 'monospace' | 'cursive' | 'fantasy' | 'system-ui';
9
+ /**
10
+ * A single `font.family` entry: a plain CSS value, a Google Fonts CSS2 URL, or
11
+ * a URL paired with a custom generic fallback (default `'sans-serif'`) for
12
+ * when the derived family isn't sans-serif — e.g. a serif display font.
13
+ */
14
+ type FontFamilyEntry = string | {
15
+ url: string;
16
+ fallback?: FontFallback;
17
+ };
7
18
  interface LattyConfig {
8
19
  /** Built-in names: primary, secondary, success, warning, error, info. Any additional name generates a new palette. */
9
20
  colors?: Record<string, string>;
10
21
  font?: {
11
22
  /**
12
- * A CSS `font-family` value (e.g. `'Inter, sans-serif'`), or a Google
13
- * Fonts CSS2 stylesheet URL (e.g.
14
- * `'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900&display=swap'`).
15
- * URLs get `@import`ed automatically and the family name is derived from
16
- * the `family=` query param — no separate `<link>` tag needed. Stylesheet
17
- * URLs from providers other than `fonts.googleapis.com` are still loaded,
18
- * but since there's no reliable way to guess the family name from an
19
- * arbitrary CDN, pass the resulting name as a plain value instead (or
20
- * open an issue if there's a provider worth special-casing).
23
+ * A CSS `font-family` value (e.g. `'Inter, sans-serif'`), a Google Fonts
24
+ * CSS2 stylesheet URL (e.g.
25
+ * `'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900&display=swap'`),
26
+ * a `{ url, fallback }` object, or an array of any of these. URLs get
27
+ * `@import`ed automatically and the family name is derived from the
28
+ * `family=` query param — no separate `<link>` tag needed. Stylesheet
29
+ * URLs from providers other than `fonts.googleapis.com` are still
30
+ * loaded, but since there's no reliable way to guess the family name
31
+ * from an arbitrary CDN, pass the resulting name as a plain value
32
+ * instead (or open an issue if there's a provider worth special-casing).
33
+ *
34
+ * A bare URL string always falls back to the generic `sans-serif` family
35
+ * if the font itself fails to load. Use the `{ url, fallback }` object
36
+ * form to pick a different generic fallback (e.g. `'serif'` for a serif
37
+ * display font):
38
+ * ```ts
39
+ * family: { url: 'https://fonts.googleapis.com/css2?family=Playfair+Display', fallback: 'serif' }
40
+ * ```
41
+ *
42
+ * When an array is passed, each entry maps to its own token in order —
43
+ * `--lt-typography-fontFamilyPrimary`, `...Secondary`, `...Tertiary`,
44
+ * etc. — rather than being combined into a single CSS fallback stack, so
45
+ * multiple fonts (e.g. a heading font and a body font) can each be
46
+ * referenced independently in your own CSS.
21
47
  */
22
- family?: string;
48
+ family?: FontFamilyEntry | FontFamilyEntry[];
49
+ /**
50
+ * A single dedicated heading font, resolved to `--lt-typography-fontFamilyHeading`.
51
+ * `lt-text`'s heading/display variants (`h1`–`h6`, `display-*`) read this
52
+ * token automatically, falling back to `fontFamilyPrimary` when unset —
53
+ * the built-in way to do a two-typeface identity without manually
54
+ * rerouting `--lt-typography-fontFamilySecondary` via hand-written CSS.
55
+ */
56
+ heading?: FontFamilyEntry;
23
57
  };
24
58
  border?: {
25
59
  radius?: string;
26
60
  width?: BorderWidth;
61
+ /**
62
+ * Shifts which neutral palette step `--lt-border-default`/`--lt-border-strong`/
63
+ * `--lt-border-subtle` resolve to. `'high'` moves them toward a bolder,
64
+ * higher-contrast step (darker in light mode, lighter in dark mode) without
65
+ * touching theme-switching machinery.
66
+ * @default 'default'
67
+ */
68
+ contrast?: 'default' | 'high';
27
69
  };
70
+ /**
71
+ * Set to `'none'` to flatten every `--lt-elevation-N` token (box-shadow) to
72
+ * `none` in one call, instead of overriding each level individually.
73
+ */
74
+ elevation?: 'none';
28
75
  /**
29
76
  * Controls which semantic token layer is emitted.
30
77
  * - `'light'` (default) — light tokens only; ignores OS preference
@@ -45,30 +92,26 @@ declare const createStyleSheet: (userConfig?: LattyConfig) => string;
45
92
  * Injects design tokens into the document as a `<style>` element.
46
93
  * Call once at your app's entry point — before any components render.
47
94
  *
48
- * Add `data-lt` to `<html>` to enable automatic FOUC prevention: configure()
49
- * hides the document during token injection and reveals it on the next frame.
50
- * For zero FOUC on initial load, also add this one-liner before your module script:
51
- * `<style>html[data-lt]:not([data-lt-ready]){visibility:hidden}</style>`
52
- *
53
- * `font.family` also accepts a Google Fonts CSS2 stylesheet URL — configure()
54
- * both `@import`s it and derives the font-family name for you, so no separate
55
- * `<link>` tag is needed.
95
+ * `font.family` accepts a single value or an array. Each entry is either a
96
+ * plain CSS `font-family` value or a Google Fonts CSS2 stylesheet URL (which
97
+ * configure() `@import`s and derives the family name from automatically — no
98
+ * separate `<link>` tag needed). Array entries map to their own token in
99
+ * order — `--lt-typography-fontFamilyPrimary`, `...Secondary`, `...Tertiary`,
100
+ * etc. — so each font can be referenced independently in your own CSS.
56
101
  *
57
102
  * @example
58
103
  * ```html
59
- * <html lang="en" data-lt>
60
- * <head>
61
- * <style>html[data-lt]:not([data-lt-ready]){visibility:hidden}</style>
62
- * <script type="module">
63
- * import { configure } from '@latty-ds/tokens/configure';
64
- * configure({
65
- * colors: { primary: '#6366f1', secondary: '#f59e0b' },
66
- * font: { family: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap' },
67
- * border: { radius: '0.375rem', width: 'medium' },
68
- * });
69
- * </script>
70
- * </head>
71
- * </html>
104
+ * <script type="module">
105
+ * import { configure } from '@latty-ds/tokens/configure';
106
+ * configure({
107
+ * colors: { primary: '#6366f1', secondary: '#f59e0b' },
108
+ * font: { family: [
109
+ * 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap',
110
+ * 'Georgia, serif',
111
+ * ] },
112
+ * border: { radius: '0.375rem', width: 'medium' },
113
+ * });
114
+ * </script>
72
115
  * ```
73
116
  */
74
117
  declare const configure: (userConfig?: LattyConfig) => void;
package/dist/configure.js CHANGED
@@ -172,7 +172,7 @@ var buildTokens = (cfg) => {
172
172
  color = addSystemColors(color);
173
173
  const spacing = buildSpacing();
174
174
  const border = { radius: DEFAULT_BORDER_RADIUS, width: DEFAULT_BORDER_WIDTH };
175
- const typography = { fontFamily: DEFAULT_FONT_FAMILY };
175
+ const typography = { fontFamilyPrimary: DEFAULT_FONT_FAMILY };
176
176
  const elevation = buildElevation(color.neutral);
177
177
  return { color, spacing, border, typography, elevation };
178
178
  };
@@ -239,6 +239,16 @@ var accessibleTextOn = (bgHex) => {
239
239
  const darkContrast = (L + 0.05) / 0.059;
240
240
  return whiteContrast >= darkContrast ? "color-white" : "color-neutral-900";
241
241
  };
242
+ var BORDER_STEPS = {
243
+ light: {
244
+ default: { default: "200", strong: "400", subtle: "100" },
245
+ high: { default: "400", strong: "600", subtle: "300" }
246
+ },
247
+ dark: {
248
+ default: { default: "700", strong: "500", subtle: "800" },
249
+ high: { default: "500", strong: "300", subtle: "600" }
250
+ }
251
+ };
242
252
  var buildLightSemanticTokens = (opts = {}) => {
243
253
  const map = {};
244
254
  map["text-default"] = "color-neutral-900";
@@ -263,9 +273,10 @@ var buildLightSemanticTokens = (opts = {}) => {
263
273
  map[`bg-${v}`] = `color-${v}-500`;
264
274
  map[`bg-${v}-subtle`] = `color-${v}-100`;
265
275
  }
266
- map["border-default"] = "color-neutral-200";
267
- map["border-strong"] = "color-neutral-400";
268
- map["border-subtle"] = "color-neutral-100";
276
+ const lightBorderSteps = BORDER_STEPS.light[opts.borderContrast ?? "default"];
277
+ map["border-default"] = `color-neutral-${lightBorderSteps.default}`;
278
+ map["border-strong"] = `color-neutral-${lightBorderSteps.strong}`;
279
+ map["border-subtle"] = `color-neutral-${lightBorderSteps.subtle}`;
269
280
  map["border-focus"] = "color-primary-200";
270
281
  for (const v of VARIANTS) {
271
282
  map[`border-${v}`] = `color-${v}-200`;
@@ -302,9 +313,10 @@ var buildDarkSemanticTokens = (opts = {}) => {
302
313
  map[`bg-${v}`] = `color-${v}-500`;
303
314
  map[`bg-${v}-subtle`] = `color-${v}-900`;
304
315
  }
305
- map["border-default"] = "color-neutral-700";
306
- map["border-strong"] = "color-neutral-500";
307
- map["border-subtle"] = "color-neutral-800";
316
+ const darkBorderSteps = BORDER_STEPS.dark[opts.borderContrast ?? "default"];
317
+ map["border-default"] = `color-neutral-${darkBorderSteps.default}`;
318
+ map["border-strong"] = `color-neutral-${darkBorderSteps.strong}`;
319
+ map["border-subtle"] = `color-neutral-${darkBorderSteps.subtle}`;
308
320
  map["border-focus"] = "color-primary-400";
309
321
  for (const v of VARIANTS) {
310
322
  map[`border-${v}`] = `color-${v}-700`;
@@ -320,6 +332,8 @@ var buildDarkSemanticTokens = (opts = {}) => {
320
332
  var buildSemanticTokens = (mode = "light", opts = {}) => mode === "dark" ? buildDarkSemanticTokens(opts) : buildLightSemanticTokens(opts);
321
333
 
322
334
  // src/configure/font.ts
335
+ var FONT_FAMILY_SLOT_NAMES = ["Primary", "Secondary", "Tertiary", "Quaternary"];
336
+ var fontFamilySlot = (index) => `fontFamily${FONT_FAMILY_SLOT_NAMES[index] ?? String(index + 1)}`;
323
337
  var unwrapQuotes = (value) => {
324
338
  const trimmed = value.trim();
325
339
  const first = trimmed[0];
@@ -331,8 +345,10 @@ var unwrapQuotes = (value) => {
331
345
  var isUrl = (value) => /^https?:\/\//i.test(value);
332
346
  var namesFromFamilyParam = (raw) => raw.split("|").map((part) => part.split(":")[0].trim()).filter(Boolean);
333
347
  var quoteIfNeeded = (name) => /\s/.test(name) ? `"${name}"` : name;
334
- var resolveFontFamily = (value) => {
335
- const unwrapped = unwrapQuotes(value);
348
+ var resolveSingleFont = (entry) => {
349
+ const rawValue = typeof entry === "string" ? entry : entry.url;
350
+ const fallback = typeof entry === "string" ? "sans-serif" : entry.fallback ?? "sans-serif";
351
+ const unwrapped = unwrapQuotes(rawValue);
336
352
  if (!isUrl(unwrapped)) return { family: unwrapped, importUrl: null };
337
353
  let parsed;
338
354
  try {
@@ -343,11 +359,12 @@ var resolveFontFamily = (value) => {
343
359
  if (parsed.hostname.toLowerCase() === "fonts.googleapis.com") {
344
360
  const names = [...new Set(parsed.searchParams.getAll("family").flatMap(namesFromFamilyParam))];
345
361
  if (names.length > 0) {
346
- return { family: `${names.map(quoteIfNeeded).join(", ")}, sans-serif`, importUrl: unwrapped };
362
+ return { family: `${names.map(quoteIfNeeded).join(", ")}, ${fallback}`, importUrl: unwrapped };
347
363
  }
348
364
  }
349
365
  return { family: "", importUrl: unwrapped };
350
366
  };
367
+ var resolveFontFamilies = (value) => (Array.isArray(value) ? value : [value]).map(resolveSingleFont);
351
368
 
352
369
  // tokens.config.json
353
370
  var tokens_config_default = {
@@ -377,12 +394,23 @@ var resolveBorderWidth = (width) => BORDER_WIDTH_PRESETS[width] ?? width;
377
394
  var createStyleSheet = (userConfig = {}) => {
378
395
  const cfg = toInternalConfig(userConfig);
379
396
  let tokens = buildTokens(cfg);
380
- let fontImportUrl = null;
397
+ const importUrls = [];
381
398
  if (userConfig.font?.family) {
382
- const resolvedFont = resolveFontFamily(userConfig.font.family);
383
- fontImportUrl = resolvedFont.importUrl;
384
- if (resolvedFont.family) {
385
- tokens = { ...tokens, typography: { fontFamily: resolvedFont.family } };
399
+ const resolvedFonts = resolveFontFamilies(userConfig.font.family);
400
+ const typographyOverrides = {};
401
+ resolvedFonts.forEach((font, i) => {
402
+ if (font.importUrl) importUrls.push(font.importUrl);
403
+ if (font.family) typographyOverrides[fontFamilySlot(i)] = font.family;
404
+ });
405
+ if (Object.keys(typographyOverrides).length > 0) {
406
+ tokens = { ...tokens, typography: { ...tokens.typography, ...typographyOverrides } };
407
+ }
408
+ }
409
+ if (userConfig.font?.heading) {
410
+ const [resolvedHeading] = resolveFontFamilies(userConfig.font.heading);
411
+ if (resolvedHeading.importUrl) importUrls.push(resolvedHeading.importUrl);
412
+ if (resolvedHeading.family) {
413
+ tokens = { ...tokens, typography: { ...tokens.typography, fontFamilyHeading: resolvedHeading.family } };
386
414
  }
387
415
  }
388
416
  if (userConfig.border?.radius) {
@@ -391,10 +419,20 @@ var createStyleSheet = (userConfig = {}) => {
391
419
  if (userConfig.border?.width) {
392
420
  tokens = { ...tokens, border: { ...tokens.border, width: resolveBorderWidth(userConfig.border.width) } };
393
421
  }
422
+ if (userConfig.elevation === "none") {
423
+ const flatElevation = Object.fromEntries(
424
+ Object.keys(tokens.elevation).map((level) => [level, "none"])
425
+ );
426
+ tokens = { ...tokens, elevation: flatElevation };
427
+ }
394
428
  const primitives = tokensToCss(tokens);
395
429
  const theme = userConfig.theme ?? "light";
396
430
  const primary = tokens.color.primary;
397
- const semanticOpts = { primary500: primary["500"], primary400: primary["400"] };
431
+ const semanticOpts = {
432
+ primary500: primary["500"],
433
+ primary400: primary["400"],
434
+ borderContrast: userConfig.border?.contrast
435
+ };
398
436
  let body;
399
437
  if (theme === "dark") {
400
438
  body = primitives + "\n" + semanticTokensToCss(buildSemanticTokens("dark", semanticOpts));
@@ -403,22 +441,19 @@ var createStyleSheet = (userConfig = {}) => {
403
441
  } else {
404
442
  body = primitives + "\n" + semanticTokensToCss(buildSemanticTokens("light", semanticOpts)) + "\n" + semanticTokensToCss(buildSemanticTokens("dark", semanticOpts), "@media (prefers-color-scheme: dark)") + "\n" + semanticTokensToCss(buildSemanticTokens("dark", semanticOpts), '[data-theme="dark"]') + "\n" + semanticTokensToCss(buildSemanticTokens("light", semanticOpts), '[data-theme="light"]');
405
443
  }
406
- return fontImportUrl ? `@import url("${fontImportUrl}");
444
+ body += "\nbody { font-family: var(--lt-typography-fontFamilyPrimary); }\n";
445
+ const importLines = importUrls.map((url) => `@import url("${url}");`).join("\n");
446
+ return importLines ? `${importLines}
407
447
  ${body}` : body;
408
448
  };
409
449
  var configure = (userConfig = {}) => {
410
- const root = document.documentElement;
411
- root.removeAttribute("data-lt-ready");
412
450
  let style = document.getElementById("lt-tokens");
413
451
  if (!style) {
414
452
  style = document.createElement("style");
415
453
  style.id = "lt-tokens";
416
454
  document.head.prepend(style);
417
455
  }
418
- const guard = "html[data-lt]:not([data-lt-ready]){visibility:hidden}\n";
419
- const css = createStyleSheet(userConfig);
420
- style.textContent = css.startsWith("@import") ? css.replace(/^(@import[^\n]*\n)/, `$1${guard}`) : guard + css;
421
- requestAnimationFrame(() => root.setAttribute("data-lt-ready", ""));
456
+ style.textContent = createStyleSheet(userConfig);
422
457
  };
423
458
  export {
424
459
  configure,
package/dist/tokens.css CHANGED
@@ -189,5 +189,5 @@
189
189
  --lt-spacing-7: 1.75rem;
190
190
  --lt-spacing-8: 2rem;
191
191
  --lt-spacing-9: 2.25rem;
192
- --lt-typography-fontFamily: "Hanken Grotesk", sans-serif;
192
+ --lt-typography-fontFamilyPrimary: "Hanken Grotesk", sans-serif;
193
193
  }
package/dist/tokens.js CHANGED
@@ -1 +1 @@
1
- export const tokens = {"color":{"primary":{"50":"#f0e5df","100":"#ffd4b5","200":"#ffbb87","300":"#ffa860","400":"#ff9539","500":"#ff8200","600":"#d56400","700":"#963b00","800":"#5e3c25","900":"#3c1d05"},"primary-muted":{"50":"#e9e7e6","100":"#e8dfda","200":"#dfd1c8","300":"#dac8bd","400":"#d2beb2","500":"#c6b1a5","600":"#a08e82","700":"#6b5c53","800":"#50453e","900":"#2b2521"},"secondary":{"50":"#e5e7f1","100":"#cbd2ff","200":"#a3acf7","300":"#8a93f4","400":"#7076e5","500":"#5252c5","600":"#4544ad","700":"#333187","800":"#2e3254","900":"#202343"},"secondary-muted":{"50":"#e7e8e9","100":"#d5d7df","200":"#b4b7c4","300":"#a0a3b3","400":"#898c9e","500":"#6c6f81","600":"#5a5d6d","700":"#414351","800":"#363742","900":"#25262c"},"success":{"50":"#e1ebe2","100":"#beedc6","200":"#94e4a4","300":"#70df8b","400":"#4bd573","500":"#22c55e","600":"#00a244","700":"#006f22","800":"#2b4d32","900":"#0c2e16"},"success-muted":{"50":"#e7e8e7","100":"#dbe3dc","200":"#cad7cc","300":"#bfcfc1","400":"#b3c5b6","500":"#a5b8a8","600":"#839586","700":"#546256","800":"#404a41","900":"#222823"},"warning":{"50":"#ece7de","100":"#f5e0b5","200":"#f6d289","300":"#f9c962","400":"#f7bf3a","500":"#eeb308","600":"#c38e00","700":"#845a00","800":"#584720","900":"#332400"},"warning-muted":{"50":"#e8e8e6","100":"#e7e4dc","200":"#e2dcd0","300":"#ded7c8","400":"#dad1c0","500":"#d2c9b8","600":"#a9a090","700":"#6d6659","800":"#504c42","900":"#292621"},"error":{"50":"#f3e4e2","100":"#ffc8c1","200":"#ffa198","300":"#ff847b","400":"#ff6560","500":"#ef4444","600":"#cb2d31","700":"#950e18","800":"#5a312e","900":"#3f1917"},"error-muted":{"50":"#e9e7e7","100":"#e6dad8","200":"#d6c3c1","300":"#cdb4b2","400":"#bfa4a1","500":"#ab908d","600":"#8d7572","700":"#624e4c","800":"#4c3d3b","900":"#2d2423"},"info":{"50":"#e1e9ef","100":"#bce4fe","200":"#8fd2fd","300":"#6ac7ff","400":"#43b9fa","500":"#0ea5e9","600":"#0087c4","700":"#005c8d","800":"#1e465d","900":"#082a3c"},"info-muted":{"50":"#e7e8e9","100":"#d9e0e5","200":"#c5d0d7","300":"#b8c6cf","400":"#aab9c4","500":"#99a9b3","600":"#7a8993","700":"#4f5b63","800":"#3d464c","900":"#22272a"},"neutral":{"50":"#fafafa","100":"#f3f3f3","200":"#e4e4e4","300":"#cacaca","400":"#ababab","500":"#868686","600":"#696969","700":"#4d4d4d","800":"#333333","900":"#1b1b1b"},"white":"#FFFFFF","black":"#000000"},"spacing":{"px":{"0":"0px","1":"4px","2":"8px","3":"12px","4":"16px","5":"20px","6":"24px","7":"28px","8":"32px","9":"36px","10":"40px","11":"44px","12":"48px","13":"52px","14":"56px","15":"60px","16":"64px","17":"68px","18":"72px","19":"76px","20":"80px","21":"84px","22":"88px","23":"92px","24":"96px"},"rem":{"0":"0rem","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","13":"3.25rem","14":"3.5rem","15":"3.75rem","16":"4rem","17":"4.25rem","18":"4.5rem","19":"4.75rem","20":"5rem","21":"5.25rem","22":"5.5rem","23":"5.75rem","24":"6rem"}},"border":{"radius":"0.5rem","width":"1px"},"typography":{"fontFamily":"\"Hanken Grotesk\", sans-serif"},"elevation":{"0":"none","1":"0 1px 3px 0 rgb(27 27 27 / 0.1), 0 1px 2px 0 rgb(27 27 27 / 0.06)","2":"0 4px 6px -1px rgb(27 27 27 / 0.1), 0 2px 4px -1px rgb(27 27 27 / 0.06)","3":"0 10px 15px -3px rgb(27 27 27 / 0.1), 0 4px 6px -2px rgb(27 27 27 / 0.05)","4":"0 20px 25px -5px rgb(27 27 27 / 0.1), 0 10px 10px -5px rgb(27 27 27 / 0.04)","5":"0 25px 50px -12px rgb(27 27 27 / 0.25)"},"semantic":{"text-default":"color-neutral-900","text-subtle":"color-neutral-600","text-muted":"color-neutral-400","text-disabled":"color-neutral-400","text-inverse":"color-white","text-neutral":"color-neutral-700","text-primary":"color-primary-700","text-on-primary":"color-neutral-900","text-secondary":"color-secondary-700","text-on-secondary":"color-white","text-success":"color-success-700","text-on-success":"color-white","text-warning":"color-warning-700","text-on-warning":"color-warning-800","text-error":"color-error-700","text-on-error":"color-white","text-info":"color-info-700","text-on-info":"color-white","bg-default":"color-white","bg-subtle":"color-neutral-50","bg-surface":"color-neutral-100","bg-overlay":"color-neutral-200","bg-inverse":"color-neutral-900","bg-neutral-subtle":"color-neutral-100","bg-primary":"color-primary-500","bg-primary-subtle":"color-primary-100","bg-secondary":"color-secondary-500","bg-secondary-subtle":"color-secondary-100","bg-success":"color-success-500","bg-success-subtle":"color-success-100","bg-warning":"color-warning-500","bg-warning-subtle":"color-warning-100","bg-error":"color-error-500","bg-error-subtle":"color-error-100","bg-info":"color-info-500","bg-info-subtle":"color-info-100","border-default":"color-neutral-200","border-strong":"color-neutral-400","border-subtle":"color-neutral-100","border-focus":"color-primary-200","border-primary":"color-primary-200","border-primary-strong":"color-primary-500","border-secondary":"color-secondary-200","border-secondary-strong":"color-secondary-500","border-success":"color-success-200","border-success-strong":"color-success-500","border-warning":"color-warning-200","border-warning-strong":"color-warning-500","border-error":"color-error-200","border-error-strong":"color-error-500","border-info":"color-info-200","border-info-strong":"color-info-500","interactive-primary-bg":"color-primary-500","interactive-primary-bg-hover":"color-primary-600","interactive-primary-bg-active":"color-primary-700","interactive-secondary-bg":"color-secondary-500","interactive-secondary-bg-hover":"color-secondary-600","interactive-secondary-bg-active":"color-secondary-700","interactive-success-bg":"color-success-500","interactive-success-bg-hover":"color-success-600","interactive-success-bg-active":"color-success-700","interactive-warning-bg":"color-warning-500","interactive-warning-bg-hover":"color-warning-600","interactive-warning-bg-active":"color-warning-700","interactive-error-bg":"color-error-500","interactive-error-bg-hover":"color-error-600","interactive-error-bg-active":"color-error-700","interactive-info-bg":"color-info-500","interactive-info-bg-hover":"color-info-600","interactive-info-bg-active":"color-info-700"},"semanticDark":{"text-default":"color-neutral-100","text-subtle":"color-neutral-300","text-muted":"color-neutral-500","text-disabled":"color-neutral-600","text-inverse":"color-neutral-900","text-neutral":"color-neutral-200","text-primary":"color-primary-300","text-on-primary":"color-neutral-900","text-secondary":"color-secondary-300","text-on-secondary":"color-white","text-success":"color-success-300","text-on-success":"color-white","text-warning":"color-warning-300","text-on-warning":"color-warning-800","text-error":"color-error-300","text-on-error":"color-white","text-info":"color-info-300","text-on-info":"color-white","bg-default":"color-neutral-900","bg-subtle":"color-neutral-800","bg-surface":"color-neutral-800","bg-overlay":"color-neutral-700","bg-inverse":"color-neutral-50","bg-neutral-subtle":"color-neutral-800","bg-primary":"color-primary-500","bg-primary-subtle":"color-primary-900","bg-secondary":"color-secondary-500","bg-secondary-subtle":"color-secondary-900","bg-success":"color-success-500","bg-success-subtle":"color-success-900","bg-warning":"color-warning-500","bg-warning-subtle":"color-warning-900","bg-error":"color-error-500","bg-error-subtle":"color-error-900","bg-info":"color-info-500","bg-info-subtle":"color-info-900","border-default":"color-neutral-700","border-strong":"color-neutral-500","border-subtle":"color-neutral-800","border-focus":"color-primary-400","border-primary":"color-primary-700","border-primary-strong":"color-primary-500","border-secondary":"color-secondary-700","border-secondary-strong":"color-secondary-500","border-success":"color-success-700","border-success-strong":"color-success-500","border-warning":"color-warning-700","border-warning-strong":"color-warning-500","border-error":"color-error-700","border-error-strong":"color-error-500","border-info":"color-info-700","border-info-strong":"color-info-500","interactive-primary-bg":"color-primary-400","interactive-primary-bg-hover":"color-primary-300","interactive-primary-bg-active":"color-primary-500","interactive-secondary-bg":"color-secondary-400","interactive-secondary-bg-hover":"color-secondary-300","interactive-secondary-bg-active":"color-secondary-500","interactive-success-bg":"color-success-400","interactive-success-bg-hover":"color-success-300","interactive-success-bg-active":"color-success-500","interactive-warning-bg":"color-warning-400","interactive-warning-bg-hover":"color-warning-300","interactive-warning-bg-active":"color-warning-500","interactive-error-bg":"color-error-400","interactive-error-bg-hover":"color-error-300","interactive-error-bg-active":"color-error-500","interactive-info-bg":"color-info-400","interactive-info-bg-hover":"color-info-300","interactive-info-bg-active":"color-info-500"}};
1
+ export const tokens = {"color":{"primary":{"50":"#f0e5df","100":"#ffd4b5","200":"#ffbb87","300":"#ffa860","400":"#ff9539","500":"#ff8200","600":"#d56400","700":"#963b00","800":"#5e3c25","900":"#3c1d05"},"primary-muted":{"50":"#e9e7e6","100":"#e8dfda","200":"#dfd1c8","300":"#dac8bd","400":"#d2beb2","500":"#c6b1a5","600":"#a08e82","700":"#6b5c53","800":"#50453e","900":"#2b2521"},"secondary":{"50":"#e5e7f1","100":"#cbd2ff","200":"#a3acf7","300":"#8a93f4","400":"#7076e5","500":"#5252c5","600":"#4544ad","700":"#333187","800":"#2e3254","900":"#202343"},"secondary-muted":{"50":"#e7e8e9","100":"#d5d7df","200":"#b4b7c4","300":"#a0a3b3","400":"#898c9e","500":"#6c6f81","600":"#5a5d6d","700":"#414351","800":"#363742","900":"#25262c"},"success":{"50":"#e1ebe2","100":"#beedc6","200":"#94e4a4","300":"#70df8b","400":"#4bd573","500":"#22c55e","600":"#00a244","700":"#006f22","800":"#2b4d32","900":"#0c2e16"},"success-muted":{"50":"#e7e8e7","100":"#dbe3dc","200":"#cad7cc","300":"#bfcfc1","400":"#b3c5b6","500":"#a5b8a8","600":"#839586","700":"#546256","800":"#404a41","900":"#222823"},"warning":{"50":"#ece7de","100":"#f5e0b5","200":"#f6d289","300":"#f9c962","400":"#f7bf3a","500":"#eeb308","600":"#c38e00","700":"#845a00","800":"#584720","900":"#332400"},"warning-muted":{"50":"#e8e8e6","100":"#e7e4dc","200":"#e2dcd0","300":"#ded7c8","400":"#dad1c0","500":"#d2c9b8","600":"#a9a090","700":"#6d6659","800":"#504c42","900":"#292621"},"error":{"50":"#f3e4e2","100":"#ffc8c1","200":"#ffa198","300":"#ff847b","400":"#ff6560","500":"#ef4444","600":"#cb2d31","700":"#950e18","800":"#5a312e","900":"#3f1917"},"error-muted":{"50":"#e9e7e7","100":"#e6dad8","200":"#d6c3c1","300":"#cdb4b2","400":"#bfa4a1","500":"#ab908d","600":"#8d7572","700":"#624e4c","800":"#4c3d3b","900":"#2d2423"},"info":{"50":"#e1e9ef","100":"#bce4fe","200":"#8fd2fd","300":"#6ac7ff","400":"#43b9fa","500":"#0ea5e9","600":"#0087c4","700":"#005c8d","800":"#1e465d","900":"#082a3c"},"info-muted":{"50":"#e7e8e9","100":"#d9e0e5","200":"#c5d0d7","300":"#b8c6cf","400":"#aab9c4","500":"#99a9b3","600":"#7a8993","700":"#4f5b63","800":"#3d464c","900":"#22272a"},"neutral":{"50":"#fafafa","100":"#f3f3f3","200":"#e4e4e4","300":"#cacaca","400":"#ababab","500":"#868686","600":"#696969","700":"#4d4d4d","800":"#333333","900":"#1b1b1b"},"white":"#FFFFFF","black":"#000000"},"spacing":{"px":{"0":"0px","1":"4px","2":"8px","3":"12px","4":"16px","5":"20px","6":"24px","7":"28px","8":"32px","9":"36px","10":"40px","11":"44px","12":"48px","13":"52px","14":"56px","15":"60px","16":"64px","17":"68px","18":"72px","19":"76px","20":"80px","21":"84px","22":"88px","23":"92px","24":"96px"},"rem":{"0":"0rem","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","13":"3.25rem","14":"3.5rem","15":"3.75rem","16":"4rem","17":"4.25rem","18":"4.5rem","19":"4.75rem","20":"5rem","21":"5.25rem","22":"5.5rem","23":"5.75rem","24":"6rem"}},"border":{"radius":"0.5rem","width":"1px"},"typography":{"fontFamilyPrimary":"\"Hanken Grotesk\", sans-serif"},"elevation":{"0":"none","1":"0 1px 3px 0 rgb(27 27 27 / 0.1), 0 1px 2px 0 rgb(27 27 27 / 0.06)","2":"0 4px 6px -1px rgb(27 27 27 / 0.1), 0 2px 4px -1px rgb(27 27 27 / 0.06)","3":"0 10px 15px -3px rgb(27 27 27 / 0.1), 0 4px 6px -2px rgb(27 27 27 / 0.05)","4":"0 20px 25px -5px rgb(27 27 27 / 0.1), 0 10px 10px -5px rgb(27 27 27 / 0.04)","5":"0 25px 50px -12px rgb(27 27 27 / 0.25)"},"semantic":{"text-default":"color-neutral-900","text-subtle":"color-neutral-600","text-muted":"color-neutral-400","text-disabled":"color-neutral-400","text-inverse":"color-white","text-neutral":"color-neutral-700","text-primary":"color-primary-700","text-on-primary":"color-neutral-900","text-secondary":"color-secondary-700","text-on-secondary":"color-white","text-success":"color-success-700","text-on-success":"color-white","text-warning":"color-warning-700","text-on-warning":"color-warning-800","text-error":"color-error-700","text-on-error":"color-white","text-info":"color-info-700","text-on-info":"color-white","bg-default":"color-white","bg-subtle":"color-neutral-50","bg-surface":"color-neutral-100","bg-overlay":"color-neutral-200","bg-inverse":"color-neutral-900","bg-neutral-subtle":"color-neutral-100","bg-primary":"color-primary-500","bg-primary-subtle":"color-primary-100","bg-secondary":"color-secondary-500","bg-secondary-subtle":"color-secondary-100","bg-success":"color-success-500","bg-success-subtle":"color-success-100","bg-warning":"color-warning-500","bg-warning-subtle":"color-warning-100","bg-error":"color-error-500","bg-error-subtle":"color-error-100","bg-info":"color-info-500","bg-info-subtle":"color-info-100","border-default":"color-neutral-200","border-strong":"color-neutral-400","border-subtle":"color-neutral-100","border-focus":"color-primary-200","border-primary":"color-primary-200","border-primary-strong":"color-primary-500","border-secondary":"color-secondary-200","border-secondary-strong":"color-secondary-500","border-success":"color-success-200","border-success-strong":"color-success-500","border-warning":"color-warning-200","border-warning-strong":"color-warning-500","border-error":"color-error-200","border-error-strong":"color-error-500","border-info":"color-info-200","border-info-strong":"color-info-500","interactive-primary-bg":"color-primary-500","interactive-primary-bg-hover":"color-primary-600","interactive-primary-bg-active":"color-primary-700","interactive-secondary-bg":"color-secondary-500","interactive-secondary-bg-hover":"color-secondary-600","interactive-secondary-bg-active":"color-secondary-700","interactive-success-bg":"color-success-500","interactive-success-bg-hover":"color-success-600","interactive-success-bg-active":"color-success-700","interactive-warning-bg":"color-warning-500","interactive-warning-bg-hover":"color-warning-600","interactive-warning-bg-active":"color-warning-700","interactive-error-bg":"color-error-500","interactive-error-bg-hover":"color-error-600","interactive-error-bg-active":"color-error-700","interactive-info-bg":"color-info-500","interactive-info-bg-hover":"color-info-600","interactive-info-bg-active":"color-info-700"},"semanticDark":{"text-default":"color-neutral-100","text-subtle":"color-neutral-300","text-muted":"color-neutral-500","text-disabled":"color-neutral-600","text-inverse":"color-neutral-900","text-neutral":"color-neutral-200","text-primary":"color-primary-300","text-on-primary":"color-neutral-900","text-secondary":"color-secondary-300","text-on-secondary":"color-white","text-success":"color-success-300","text-on-success":"color-white","text-warning":"color-warning-300","text-on-warning":"color-warning-800","text-error":"color-error-300","text-on-error":"color-white","text-info":"color-info-300","text-on-info":"color-white","bg-default":"color-neutral-900","bg-subtle":"color-neutral-800","bg-surface":"color-neutral-800","bg-overlay":"color-neutral-700","bg-inverse":"color-neutral-50","bg-neutral-subtle":"color-neutral-800","bg-primary":"color-primary-500","bg-primary-subtle":"color-primary-900","bg-secondary":"color-secondary-500","bg-secondary-subtle":"color-secondary-900","bg-success":"color-success-500","bg-success-subtle":"color-success-900","bg-warning":"color-warning-500","bg-warning-subtle":"color-warning-900","bg-error":"color-error-500","bg-error-subtle":"color-error-900","bg-info":"color-info-500","bg-info-subtle":"color-info-900","border-default":"color-neutral-700","border-strong":"color-neutral-500","border-subtle":"color-neutral-800","border-focus":"color-primary-400","border-primary":"color-primary-700","border-primary-strong":"color-primary-500","border-secondary":"color-secondary-700","border-secondary-strong":"color-secondary-500","border-success":"color-success-700","border-success-strong":"color-success-500","border-warning":"color-warning-700","border-warning-strong":"color-warning-500","border-error":"color-error-700","border-error-strong":"color-error-500","border-info":"color-info-700","border-info-strong":"color-info-500","interactive-primary-bg":"color-primary-400","interactive-primary-bg-hover":"color-primary-300","interactive-primary-bg-active":"color-primary-500","interactive-secondary-bg":"color-secondary-400","interactive-secondary-bg-hover":"color-secondary-300","interactive-secondary-bg-active":"color-secondary-500","interactive-success-bg":"color-success-400","interactive-success-bg-hover":"color-success-300","interactive-success-bg-active":"color-success-500","interactive-warning-bg":"color-warning-400","interactive-warning-bg-hover":"color-warning-300","interactive-warning-bg-active":"color-warning-500","interactive-error-bg":"color-error-400","interactive-error-bg-hover":"color-error-300","interactive-error-bg-active":"color-error-500","interactive-info-bg":"color-info-400","interactive-info-bg-hover":"color-info-300","interactive-info-bg-active":"color-info-500"}};
package/dist/tokens.json CHANGED
@@ -220,7 +220,7 @@
220
220
  "width": "1px"
221
221
  },
222
222
  "typography": {
223
- "fontFamily": "\"Hanken Grotesk\", sans-serif"
223
+ "fontFamilyPrimary": "\"Hanken Grotesk\", sans-serif"
224
224
  },
225
225
  "elevation": {
226
226
  "0": "none",
@@ -24,7 +24,7 @@ import type { ElevationTokens } from '../elevation/';
24
24
  * radius: "0.375rem"
25
25
  * },
26
26
  * typography: {
27
- * fontFamily: "Hanken Grotesk, sans-serif"
27
+ * fontFamilyPrimary: "Hanken Grotesk, sans-serif"
28
28
  * },
29
29
  * elevation: {
30
30
  * "0": "none",
@@ -42,8 +42,8 @@ export type Tokens = {
42
42
  width: string;
43
43
  };
44
44
  typography: {
45
- fontFamily: string;
46
- };
45
+ fontFamilyPrimary: string;
46
+ } & Record<string, string>;
47
47
  elevation: ElevationTokens;
48
48
  };
49
49
  export type { Config, TokenColorName } from './';
@@ -53,27 +53,74 @@ export type { Config, TokenColorName } from './';
53
53
  * Any other CSS length (e.g. `'1.5px'`) passes through unchanged.
54
54
  */
55
55
  export type BorderWidth = 'thin' | 'medium' | 'thick' | (string & {});
56
+ /** Generic CSS font-family keyword used as the safety-net fallback if a CDN font fails to load. */
57
+ export type FontFallback = 'sans-serif' | 'serif' | 'monospace' | 'cursive' | 'fantasy' | 'system-ui';
58
+ /**
59
+ * A single `font.family` entry: a plain CSS value, a Google Fonts CSS2 URL, or
60
+ * a URL paired with a custom generic fallback (default `'sans-serif'`) for
61
+ * when the derived family isn't sans-serif — e.g. a serif display font.
62
+ */
63
+ export type FontFamilyEntry = string | {
64
+ url: string;
65
+ fallback?: FontFallback;
66
+ };
56
67
  export interface LattyConfig {
57
68
  /** Built-in names: primary, secondary, success, warning, error, info. Any additional name generates a new palette. */
58
69
  colors?: Record<string, string>;
59
70
  font?: {
60
71
  /**
61
- * A CSS `font-family` value (e.g. `'Inter, sans-serif'`), or a Google
62
- * Fonts CSS2 stylesheet URL (e.g.
63
- * `'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900&display=swap'`).
64
- * URLs get `@import`ed automatically and the family name is derived from
65
- * the `family=` query param — no separate `<link>` tag needed. Stylesheet
66
- * URLs from providers other than `fonts.googleapis.com` are still loaded,
67
- * but since there's no reliable way to guess the family name from an
68
- * arbitrary CDN, pass the resulting name as a plain value instead (or
69
- * open an issue if there's a provider worth special-casing).
72
+ * A CSS `font-family` value (e.g. `'Inter, sans-serif'`), a Google Fonts
73
+ * CSS2 stylesheet URL (e.g.
74
+ * `'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900&display=swap'`),
75
+ * a `{ url, fallback }` object, or an array of any of these. URLs get
76
+ * `@import`ed automatically and the family name is derived from the
77
+ * `family=` query param — no separate `<link>` tag needed. Stylesheet
78
+ * URLs from providers other than `fonts.googleapis.com` are still
79
+ * loaded, but since there's no reliable way to guess the family name
80
+ * from an arbitrary CDN, pass the resulting name as a plain value
81
+ * instead (or open an issue if there's a provider worth special-casing).
82
+ *
83
+ * A bare URL string always falls back to the generic `sans-serif` family
84
+ * if the font itself fails to load. Use the `{ url, fallback }` object
85
+ * form to pick a different generic fallback (e.g. `'serif'` for a serif
86
+ * display font):
87
+ * ```ts
88
+ * family: { url: 'https://fonts.googleapis.com/css2?family=Playfair+Display', fallback: 'serif' }
89
+ * ```
90
+ *
91
+ * When an array is passed, each entry maps to its own token in order —
92
+ * `--lt-typography-fontFamilyPrimary`, `...Secondary`, `...Tertiary`,
93
+ * etc. — rather than being combined into a single CSS fallback stack, so
94
+ * multiple fonts (e.g. a heading font and a body font) can each be
95
+ * referenced independently in your own CSS.
96
+ */
97
+ family?: FontFamilyEntry | FontFamilyEntry[];
98
+ /**
99
+ * A single dedicated heading font, resolved to `--lt-typography-fontFamilyHeading`.
100
+ * `lt-text`'s heading/display variants (`h1`–`h6`, `display-*`) read this
101
+ * token automatically, falling back to `fontFamilyPrimary` when unset —
102
+ * the built-in way to do a two-typeface identity without manually
103
+ * rerouting `--lt-typography-fontFamilySecondary` via hand-written CSS.
70
104
  */
71
- family?: string;
105
+ heading?: FontFamilyEntry;
72
106
  };
73
107
  border?: {
74
108
  radius?: string;
75
109
  width?: BorderWidth;
110
+ /**
111
+ * Shifts which neutral palette step `--lt-border-default`/`--lt-border-strong`/
112
+ * `--lt-border-subtle` resolve to. `'high'` moves them toward a bolder,
113
+ * higher-contrast step (darker in light mode, lighter in dark mode) without
114
+ * touching theme-switching machinery.
115
+ * @default 'default'
116
+ */
117
+ contrast?: 'default' | 'high';
76
118
  };
119
+ /**
120
+ * Set to `'none'` to flatten every `--lt-elevation-N` token (box-shadow) to
121
+ * `none` in one call, instead of overriding each level individually.
122
+ */
123
+ elevation?: 'none';
77
124
  /**
78
125
  * Controls which semantic token layer is emitted.
79
126
  * - `'light'` (default) — light tokens only; ignores OS preference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latty-ds/tokens",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Design tokens for the Latty design system — generates CSS custom properties, JSON, and JavaScript exports from a single config file",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,5 +60,5 @@
60
60
  "dependencies": {
61
61
  "culori": "^4.0.2"
62
62
  },
63
- "gitHead": "144dd8d2f7440fd5fd8ecb4f396407c4ce3be9f2"
63
+ "gitHead": "7b0b9856be6755a573abf02c0b7cbdb1a6db4fef"
64
64
  }