@scattered-light/loomiere 0.7.1 → 0.8.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.
@@ -465,11 +465,14 @@ StyleDictionary.registerFormat({
465
465
  * Text styles are written as one DTCG typography token each (tokens/semantic/typography.json).
466
466
  * CSS can't use a composite, so this expands each into its parts plus a `font` shorthand:
467
467
  * font.body-base → font.body-base.font, .font-family, .font-size, .font-weight, .line-height,
468
- * .letter-spacing (and .text-transform when $extensions["sl.text-transform"] is set)
468
+ * .letter-spacing (and .text-transform when $extensions["sl.text-transform"] is set,
469
+ * .font-variant-numeric when $extensions["sl.font-variant-numeric"] is)
469
470
  * so --sl-font-body-base-font-size and friends exist for components. A text style whose value is
470
471
  * another text style ("{font.body-base}") takes that style's parts.
471
472
  */
472
473
  const TEXT_PARTS = { fontFamily: 'font-family', fontSize: 'font-size', fontWeight: 'font-weight', lineHeight: 'line-height', letterSpacing: 'letter-spacing' };
474
+ /** Optional parts a text style sets through $extensions["sl.<part>"]: uppercase labels, tabular figures. */
475
+ const EXTRA_PARTS = ['text-transform', 'font-variant-numeric'];
473
476
  export const TEXT_STYLE_TYPE = 'typography';
474
477
 
475
478
  export function expandTextStyles(tokens) {
@@ -499,11 +502,13 @@ export function expandTextStyles(tokens) {
499
502
  if (style.$value[prop] === undefined) throw new Error(`Text style ${name} has no ${prop}`);
500
503
  group[part] = { $value: style.$value[prop], $description: `${part} of ${name}.` };
501
504
  }
502
- const transform = child.$extensions?.['sl.text-transform'] ?? style.$extensions?.['sl.text-transform'];
503
- if (transform) group['text-transform'] = { $value: transform, $description: `text-transform of ${name}.` };
505
+ for (const part of EXTRA_PARTS) {
506
+ const value = child.$extensions?.[`sl.${part}`] ?? style.$extensions?.[`sl.${part}`];
507
+ if (value) group[part] = { $value: value, $description: `${part} of ${name}.` };
508
+ }
504
509
  group.font = {
505
510
  $value: `{${name}.font-weight} {${name}.font-size} / {${name}.line-height} {${name}.font-family}`,
506
- $description: `${what} As a font shorthand; letter-spacing and text-transform are separate.`,
511
+ $description: `${what} As a font shorthand; letter-spacing, text-transform and font-variant-numeric are separate.`,
507
512
  };
508
513
  // Parts keep the file the style came from, which says which layer they belong to.
509
514
  for (const [part, token] of Object.entries(group)) if (!part.startsWith('$')) Object.assign(token, { filePath: child.filePath, isSource: child.isSource });
@@ -549,10 +554,10 @@ function textStyleBlocks() {
549
554
  const styles = Object.entries(font).filter(([name, t]) => !name.startsWith('$') && t.$type === TEXT_STYLE_TYPE);
550
555
  return styles.map(([name, t]) => {
551
556
  const v = (part) => `var(${varName(['font', name, part])})`;
552
- const transform = t.$extensions?.['sl.text-transform'] ?? (typeof t.$value === 'string' && font[t.$value.slice(1, -1).split('.')[1]]?.$extensions?.['sl.text-transform']);
557
+ const extra = (part) => t.$extensions?.[`sl.${part}`] ?? (typeof t.$value === 'string' && font[t.$value.slice(1, -1).split('.')[1]]?.$extensions?.[`sl.${part}`]);
553
558
  // Longhands, not the font shorthand: the shorthand would also reset font-feature-settings
554
559
  // and the other font properties the page sets once on body.
555
- const parts = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing', ...(transform ? ['text-transform'] : [])];
560
+ const parts = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing', ...EXTRA_PARTS.filter(extra)];
556
561
  const lines = parts.map((part) => `${part}: ${v(part)};`);
557
562
  return `/* ${t.$description ?? name} */\n.text-style-${name} {\n${indent(lines.join('\n'), 2)}\n}`;
558
563
  });
@@ -345,8 +345,8 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
345
345
  '',
346
346
  '`window.Loomiere` holds two kinds of component:',
347
347
  '',
348
- `- **Wrappers** put their own classes on, so use them bare: ${['Button', 'Input', 'Field', 'Fieldset', 'Form', 'NavigationMenu', 'Select', 'NumberField', 'OTPField', 'Autocomplete', 'Combobox', 'Checkbox', 'Radio', 'CheckboxGroup', 'Toggle', 'ToggleGroup', 'Dialog', 'AlertDialog', 'Meter', 'Progress', 'Slider', 'Menu', 'ContextMenu', 'Menubar', 'Popover', 'PreviewCard', 'Tooltip', 'Drawer', 'Accordion', 'Collapsible', 'Tabs', 'Switch', 'Toast', 'Avatar', 'Toolbar', 'ScrollArea', 'Separator', 'Badge', 'DatePicker', 'MoneyField', 'Splitter', 'ListDetail', 'EmptyState', 'DataTable', 'DataList', 'CollectionToolbar', 'CollectionPagination', 'EditableList'].filter(has).map((n) => `\`Loomiere.${n}\``).join(', ')}. \`Loomiere.Button\` also takes \`emphasis\` (\`bold|standard|subtle|quiet\`) and \`palette\` props, and a Button holding only an icon draws itself square (it sets \`data-icon-only\`). Wrapped parts that are buttons (\`Dialog.Trigger\`, \`Menu.Trigger\`, \`Toolbar.Button\`, \`Drawer.Close\`...) take the same \`emphasis\` and \`palette\` props and draw square the same way. \`Popover.Trigger\`, \`Tooltip.Trigger\`, \`Select.Trigger\` and \`Dialog.Close\` become Buttons only when they set \`emphasis\`, \`palette\` or \`className="Button"\`. A plain \`.Button\` on a bare part is drawn square when it has an \`aria-label\` and a single icon inside, or when you set \`data-icon-only\` on it. \`Loomiere.Badge\` takes the same \`emphasis\` and \`palette\` props, plus \`status\`, \`size\` and \`dot\`.`,
349
- '- **Table, Badge, DatePicker, MoneyField, Splitter, ListDetail, EmptyState, DataTable, DataList, CollectionToolbar, CollectionPagination and EditableList are Loomiere components**, not Base UI parts. Table is plain HTML for a static table or a report: use the classes in its stylesheet. Badge is `Loomiere.Badge`. DatePicker is `Loomiere.DatePicker`; `Loomiere.Calendar` is the month grid on its own and `Loomiere.DatePresets` is the quick-selection list. MoneyField is `Loomiere.MoneyField`: an amount of money, stored as minor units. Splitter is `Loomiere.Splitter`. ListDetail is `Loomiere.ListDetail`. EmptyState is `Loomiere.EmptyState`. DataTable, DataList, CollectionToolbar and CollectionPagination come from `@scattered-light/loomiere-base-ui/custom/collection` and need `@tanstack/react-table` installed. EditableList comes from the package root or `./custom`.',
348
+ `- **Wrappers** put their own classes on, so use them bare: ${['Button', 'Input', 'Field', 'Fieldset', 'Form', 'NavigationMenu', 'Select', 'NumberField', 'OTPField', 'Autocomplete', 'Combobox', 'Checkbox', 'Radio', 'CheckboxGroup', 'Toggle', 'ToggleGroup', 'Dialog', 'AlertDialog', 'Meter', 'Progress', 'Slider', 'Menu', 'ContextMenu', 'Menubar', 'Popover', 'PreviewCard', 'Tooltip', 'Drawer', 'Accordion', 'Collapsible', 'Tabs', 'Switch', 'Toast', 'Avatar', 'Toolbar', 'ScrollArea', 'Separator', 'Badge', 'DatePicker', 'MoneyField', 'Splitter', 'ListDetail', 'EmptyState', 'DataTable', 'DataList', 'CollectionToolbar', 'CollectionPagination', 'EditableList', 'Sheet', 'StatFigure', 'DescriptionList'].filter(has).map((n) => `\`Loomiere.${n}\``).join(', ')}. \`Loomiere.Button\` also takes \`emphasis\` (\`bold|standard|subtle|quiet\`) and \`palette\` props, and a Button holding only an icon draws itself square (it sets \`data-icon-only\`). Wrapped parts that are buttons (\`Dialog.Trigger\`, \`Menu.Trigger\`, \`Toolbar.Button\`, \`Drawer.Close\`...) take the same \`emphasis\` and \`palette\` props and draw square the same way. \`Popover.Trigger\`, \`Tooltip.Trigger\`, \`Select.Trigger\` and \`Dialog.Close\` become Buttons only when they set \`emphasis\`, \`palette\` or \`className="Button"\`. A plain \`.Button\` on a bare part is drawn square when it has an \`aria-label\` and a single icon inside, or when you set \`data-icon-only\` on it. \`Loomiere.Badge\` takes the same \`emphasis\` and \`palette\` props, plus \`status\`, \`size\` and \`dot\`.`,
349
+ '- **Table, Badge, DatePicker, MoneyField, Splitter, ListDetail, EmptyState, DataTable, DataList, CollectionToolbar, CollectionPagination, EditableList, Sheet, StatFigure and DescriptionList are Loomiere components**, not Base UI parts. Table is plain HTML for a static table or a report: use the classes in its stylesheet. Badge is `Loomiere.Badge`. DatePicker is `Loomiere.DatePicker`; `Loomiere.Calendar` is the month grid on its own and `Loomiere.DatePresets` is the quick-selection list. MoneyField is `Loomiere.MoneyField`: an amount of money, stored as minor units. Splitter is `Loomiere.Splitter`. ListDetail is `Loomiere.ListDetail`. EmptyState is `Loomiere.EmptyState`. DataTable, DataList, CollectionToolbar and CollectionPagination come from `@scattered-light/loomiere-base-ui/custom/collection` and need `@tanstack/react-table` installed. EditableList, Sheet, StatFigure and DescriptionList (with `Loomiere.DescriptionItem`) come from the package root or `./custom`.',
350
350
  '',
351
351
  '`Loomiere.Base` holds the bare Base UI parts for the wrappers too (`Loomiere.Base.Button`), for when you need the part without its classes.',
352
352
  '',
@@ -396,6 +396,9 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
396
396
  ['EditableList', 'Repeating form rows people add, edit and remove: line items, allocations.'],
397
397
  ['CollectionToolbar', 'Search, tabs, filters and the selection bar for a DataTable or a DataList.'],
398
398
  ['CollectionPagination', 'Which page of a long list you are on. It also works without a collection.'],
399
+ ['Sheet', 'Frame a section of a working page (open items, a profile, periods) with a ruled top and an optional tab label. A table or list inside runs edge to edge.'],
400
+ ['StatFigure', 'Lead a page with one figure that matters (a balance, a count to act on), with a caps label and a caption. Give a final total the double underline.'],
401
+ ['DescriptionList', 'Show label and value pairs, such as a profile. variant="leader" runs dotted leaders to values at the end of the row, as on a working paper.'],
399
402
  ['EmptyState', 'Don\'t use it for a message about something that just happened (use Toast) or a warning above content that still shows (use a notice or an inline alert).'],
400
403
  ['Progress', 'Show how far a task has got.'],
401
404
  ['Meter', 'Show a measured amount within known limits, such as storage used.'],
@@ -106,7 +106,7 @@ export default {
106
106
  // class sets family, size, weight, line height and letter spacing together.
107
107
  const styles = new Map();
108
108
  for (const t of theme.tokens) {
109
- const m = t.path.match(/^font\.([\w-]+)\.(font-family|font-size|font-weight|line-height|letter-spacing|text-transform)$/);
109
+ const m = t.path.match(/^font\.([\w-]+)\.(font-family|font-size|font-weight|line-height|letter-spacing|text-transform|font-variant-numeric)$/);
110
110
  if (m) (styles.get(m[1]) ?? styles.set(m[1], []).get(m[1])).push(` ${m[2]}: var(${t.name});`);
111
111
  }
112
112
  const utilities = [...styles].map(([style, decls]) => `@utility text-${style} {\n${decls.join('\n')}\n}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scattered-light/loomiere",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Turns a small brand file into a full design token theme: OKLCH color ramps, a type scale, light, dark and high-contrast modes, plus Tailwind, MUI, Chakra, AG Grid and Figma exports.",
5
5
  "keywords": ["design-tokens", "theme", "style-dictionary", "dtcg", "oklch", "base-ui", "ag-grid", "tailwind"],
6
6
  "repository": {
package/tokens/README.md CHANGED
@@ -72,6 +72,27 @@ In CSS a look is `data-look="<name>"` on the root element, next to `data-theme`.
72
72
 
73
73
  Mojave's brand file has one: `classic`, the look the brand had before.
74
74
 
75
+ ## Working papers and square corners
76
+
77
+ Accounting and reporting apps often want the look of a working paper: ruled tops, totals with single and double rules, caps labels and figures that line up. Every piece is opt-in, so a brand that sets none of these looks exactly as before.
78
+
79
+ - **The rule.** `rule.width` (0 by default) and `rule.color` (`border.strong` by default) in `tokens/semantic/rule.json`. With a width above 0, tables, data lists (each card, in the cards variant) and drawers swap their top border for a heavier line in the rule color: `"tokens": { "rule.width": "{stroke.medium}", "rule.color": "{text.primary}" }` gives an ink rule. The Sheet component always has its own rule (`sheet.rule-width`), in the same color.
80
+ - **Totals.** `<tr data-total="subtotal">` in a `.Table` draws a single rule above the row; `data-total="grand"` adds a double rule under it. `data-rule="none"` on a label cell keeps the rules under the figures only. DataTable's `totalStyle` prop does the same for its footer. Colors and widths are `table.total-rule-color`, `table.subtotal-rule-width` and `table.grand-total-rule-width` (a CSS double border, so keep it at 3px or more).
81
+ - **Text styles.** `font.caps-condense` is a caps label (uppercase, semibold, `letter-spacing.caps` 0.08em). `font.figures-base` and `font.figures-condense` are figures: the mono family with tabular numbers. A text style can set `$extensions["sl.font-variant-numeric"]` the way it sets `sl.text-transform`; the build writes it as its own part (`--sl-font-figures-base-font-variant-numeric`) and into the `.text-style-*` class and the `@sl-pattern text-*` pattern.
82
+
83
+ **Square corners.** Every component reads its corners from a radius token, so four picks square buttons, tables, popups, surfaces and dialogs without touching a component (badges, switches, radios and avatars stay round on `radius.pill`; checkboxes keep `radius.box`):
84
+
85
+ ```json
86
+ "tokens": {
87
+ "button.radius": "{radius.sm}",
88
+ "radius.surface": "{radius.sm}",
89
+ "radius.chrome": "{radius.sm}",
90
+ "radius.modal": "{radius.sm}"
91
+ }
92
+ ```
93
+
94
+ `button.radius` points at `radius.pill` by default because buttons are pills; `radius.surface` covers tables, data lists, popups, accordions, sheets, toggle groups and code blocks; `radius.chrome` covers toolbars, menubars, tooltips and toggles; `radius.modal` covers dialogs and drawers. Point them at `{radius.none}` for fully square corners. A look can make the same picks.
95
+
75
96
  ## Roles: chrome, accent and the emphasis ladder
76
97
 
77
98
  Semantic and component tokens never name a brand color. They say `color.chrome.*` (surfaces, text, borders) or `color.accent.*` (links, focus, the loudest actions), and the brand file's `roles` decides which palettes those are:
@@ -120,7 +141,7 @@ The universal outputs are the CSS variables (`tokens.css`, works with any framew
120
141
 
121
142
  ## Text styles and utilities
122
143
 
123
- Each text style is one `typography` token in `semantic/typography.json`, named role first, then tier: `body-base`, `label-base`, `trigger-condense`, `eyebrow-condense`, `title-magnify`. The role is the job (body, label, trigger, selected, link, subtitle, eyebrow, title, code); the tier sets the size (minimize, condense, base, magnify, amplify, maximize). Titles use the display family: `title-amplify` for dialogs and sections, `title-maximize` for the page head. A style can be another style (`"link-base": "{font.body-base}"`) when only color sets it apart. The build expands every style into the CSS variables components use, `--sl-font-<style>-font` (shorthand), `-font-family`, `-font-size`, `-font-weight`, `-line-height`, `-letter-spacing` and, for uppercase styles, `-text-transform`.
144
+ Each text style is one `typography` token in `semantic/typography.json`, named role first, then tier: `body-base`, `label-base`, `trigger-condense`, `eyebrow-condense`, `title-magnify`. The role is the job (body, label, trigger, selected, link, subtitle, eyebrow, caps, title, code, figures); the tier sets the size (minimize, condense, base, magnify, amplify, maximize). Titles use the display family: `title-amplify` for dialogs and sections, `title-maximize` for the page head. A style can be another style (`"link-base": "{font.body-base}"`) when only color sets it apart. The build expands every style into the CSS variables components use, `--sl-font-<style>-font` (shorthand), `-font-family`, `-font-size`, `-font-weight`, `-line-height`, `-letter-spacing` and, for uppercase styles, `-text-transform`, and for figures, `-font-variant-numeric`.
124
145
 
125
146
  `loomiere build` also writes the utility classes into `<theme>/utilities.css`, so they always match the tokens: `.text-style-<style>` for every text style, `.icon-<tier>` for every icon size, `.focus-ring` and `.focus-ring-inset`, and `.hide-on-mobile|tablet|desktop` from the breakpoints.
126
147
 
@@ -0,0 +1,49 @@
1
+ {
2
+ "description-list": {
3
+ "$description": "DescriptionList: label and value pairs, such as an entity's profile. The default variant sets labels in a column beside their values; variant=\"leader\" runs a dotted leader from each label to its value, as on a working paper or a table of contents. Labels are body text in the secondary color, values primary text.",
4
+ "row-padding-block": {
5
+ "$value": "{space.2}",
6
+ "$description": "Space above and below each pair."
7
+ },
8
+ "row-padding-inline": {
9
+ "$value": "{space.4}",
10
+ "$description": "Space either side of each pair, so pairs inside a sheet line up with its tab label."
11
+ },
12
+ "row-border-color": {
13
+ "$value": "{border.subtle}",
14
+ "$type": "color",
15
+ "$description": "Hairline between pairs, as quiet as a table's row lines."
16
+ },
17
+ "column-gap": {
18
+ "$value": "{space.4}",
19
+ "$description": "Space between the label column and the value column in the default variant."
20
+ },
21
+ "label-width": {
22
+ "$value": "minmax(8rem, 1fr)",
23
+ "$description": "The label column in the default variant. The value column takes twice the room."
24
+ },
25
+ "label-color": {
26
+ "$value": "{text.secondary}",
27
+ "$type": "color",
28
+ "$description": "Labels."
29
+ },
30
+ "value-color": {
31
+ "$value": "{text.primary}",
32
+ "$type": "color",
33
+ "$description": "Values."
34
+ },
35
+ "leader-color": {
36
+ "$value": "{border.control}",
37
+ "$type": "color",
38
+ "$description": "The dotted leader at variant=\"leader\". The control edge color: dark enough to lead the eye, still lighter than the text."
39
+ },
40
+ "leader-width": {
41
+ "$value": "{stroke.default}",
42
+ "$description": "Thickness of the dotted leader."
43
+ },
44
+ "leader-gap": {
45
+ "$value": "{space.1-5}",
46
+ "$description": "Space between the leader and the label or value either side of it."
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "sheet": {
3
+ "$description": "Sheet: a working-paper surface. A raised panel with a heavier rule across its top, and an optional tab label that sits on the rule like the tab of a ledger page. Built from the surface, border and rule concepts; only its own measures are here.",
4
+ "background": {
5
+ "$value": "{surface.raised}",
6
+ "$type": "color",
7
+ "$description": "The sheet's fill: the raised surface, one level above the page like any card or panel."
8
+ },
9
+ "border-color": {
10
+ "$value": "{border.default}",
11
+ "$type": "color",
12
+ "$description": "The sheet's sides and bottom, the everyday divider."
13
+ },
14
+ "border-width": {
15
+ "$value": "{stroke.default}",
16
+ "$description": "Thickness of the sides and bottom."
17
+ },
18
+ "rule-width": {
19
+ "$value": "{stroke.medium}",
20
+ "$description": "The top rule. A sheet always has one, so it is its own token rather than rule.width (which is 0 until a brand turns it on for tables and drawers)."
21
+ },
22
+ "rule-color": {
23
+ "$value": "{rule.color}",
24
+ "$type": "color",
25
+ "$description": "The top rule's color: the working-paper rule color, so sheets and ruled tables match."
26
+ },
27
+ "radius": {
28
+ "$value": "{radius.surface}",
29
+ "$description": "Corners, like any card or panel. The top corners stay square under a tab label, so the tab sits flush on the rule."
30
+ },
31
+ "padding": {
32
+ "$value": "{space.4}",
33
+ "$description": "Space inside a sheet with inset set. Without it, rows and tables inside run edge to edge."
34
+ },
35
+ "tab-background": {
36
+ "$value": "{surface.inverse}",
37
+ "$type": "color",
38
+ "$description": "The tab label's fill: the inverse surface, so the label reads as the sheet's name, and its text passes AA in every theme."
39
+ },
40
+ "tab-color": {
41
+ "$value": "{text.on-inverse-surface}",
42
+ "$type": "color",
43
+ "$description": "Text on the tab label."
44
+ },
45
+ "tab-height": {
46
+ "$value": "{space.7}",
47
+ "$description": "Height of the tab label."
48
+ },
49
+ "tab-padding-inline": {
50
+ "$value": "{space.3-5}",
51
+ "$description": "Space either side of the tab label's text."
52
+ },
53
+ "tab-radius": {
54
+ "$value": "{radius.sm}",
55
+ "$description": "The tab label's top corners. Its bottom corners stay square where it meets the rule."
56
+ },
57
+ "header-gap": {
58
+ "$value": "{space.3}",
59
+ "$description": "Space between the tab label and an action at the end of the header row, such as an Edit link."
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "stat-figure": {
3
+ "$description": "StatFigure: one large figure with a caps label above and a caption under it, for the few numbers a page leads with (a balance, a count of open items). The label is caps-condense, the figure the figures text family, the caption body-condense; only its own measures are here.",
4
+ "figure-font-size": {
5
+ "$value": "{font-size.4xl}",
6
+ "$description": "Size of the figure. Large enough to lead the page, a step under the page title."
7
+ },
8
+ "figure-font-size-sm": {
9
+ "$value": "{font-size.2xl}",
10
+ "$description": "Size of the figure at size=\"sm\", for a row of figures in a card or a sheet header."
11
+ },
12
+ "figure-line-height": {
13
+ "$value": "{line-height.4xl}",
14
+ "$description": "Line height paired with figure-font-size."
15
+ },
16
+ "figure-line-height-sm": {
17
+ "$value": "{line-height.2xl}",
18
+ "$description": "Line height paired with figure-font-size-sm."
19
+ },
20
+ "figure-font-weight": {
21
+ "$value": "{font-weight.semibold}",
22
+ "$description": "Weight of the figure."
23
+ },
24
+ "figure-color": {
25
+ "$value": "{text.primary}",
26
+ "$type": "color",
27
+ "$description": "The figure's color."
28
+ },
29
+ "figure-color-accent": {
30
+ "$value": "{text.accent}",
31
+ "$type": "color",
32
+ "$description": "The figure's color at tone=\"accent\", for a figure that asks for action (open questions, items to review)."
33
+ },
34
+ "underline-width": {
35
+ "$value": "{stroke.thick}",
36
+ "$description": "The double underline at underline=\"double\", the accountant's mark for a final total. A CSS double border, so at least 3px."
37
+ },
38
+ "underline-color": {
39
+ "$value": "{rule.color}",
40
+ "$type": "color",
41
+ "$description": "The double underline's color: the working-paper rule color, like a grand total's double rule."
42
+ },
43
+ "label-color": {
44
+ "$value": "{text.secondary}",
45
+ "$type": "color",
46
+ "$description": "The caps label above the figure."
47
+ },
48
+ "caption-color": {
49
+ "$value": "{text.secondary}",
50
+ "$type": "color",
51
+ "$description": "The caption under the figure."
52
+ },
53
+ "gap": {
54
+ "$value": "{space.1}",
55
+ "$description": "Space between the label, the figure and the caption."
56
+ }
57
+ }
58
+ }
@@ -53,6 +53,10 @@
53
53
  "$value": "0.05em",
54
54
  "$description": "Small caps and labels."
55
55
  },
56
+ "caps": {
57
+ "$value": "0.08em",
58
+ "$description": "Uppercase labels that sit beside figures, such as a column head or a working-paper caption. Between wider and widest."
59
+ },
56
60
  "widest": {
57
61
  "$value": "0.1em",
58
62
  "$description": "Uppercase group labels."
@@ -91,6 +91,10 @@
91
91
  "$value": "{content.prose-color}",
92
92
  "$description": "Code block text, the same as prose."
93
93
  },
94
+ "pre-radius": {
95
+ "$value": "{radius.surface}",
96
+ "$description": "Code block corners, like any panel, so a brand that squares its surfaces squares code blocks too."
97
+ },
94
98
  "prose-color": {
95
99
  "$value": "{text.primary}",
96
100
  "$type": "color",
@@ -0,0 +1,15 @@
1
+ {
2
+ "rule": {
3
+ "$description": "The working-paper rule: a heavier top edge on sheets, tables, data lists and drawers, like the ruled line across the top of a ledger page. Off by default, so a brand turns it on with tokens: { \"rule.width\": \"{stroke.medium}\" }. While it is on, the rule takes the place of that edge's normal border.",
4
+ "width": {
5
+ "$type": "dimension",
6
+ "$value": "0px",
7
+ "$description": "Thickness of the top rule. 0 turns it off and leaves every top edge as it was. Point it at a stroke step (stroke.medium or stroke.thick) to turn it on."
8
+ },
9
+ "color": {
10
+ "$type": "color",
11
+ "$value": "{border.strong}",
12
+ "$description": "Color of the top rule. The chrome's strong border, so the rule reads as structure, not as an accent or a selection. A brand can point it at text.primary for an ink rule."
13
+ }
14
+ }
15
+ }
@@ -76,6 +76,22 @@
76
76
  "radius": {
77
77
  "$value": "{radius.surface}",
78
78
  "$description": "Grid corners, like any card or panel."
79
+ },
80
+ "total-rule-color": {
81
+ "$value": "{rule.color}",
82
+ "$description": "The line above a subtotal or total row and the double line under a grand total. The working-paper rule color, so totals and sheet tops are ruled in the same ink."
83
+ },
84
+ "subtotal-rule-width": {
85
+ "$value": "{stroke.default}",
86
+ "$description": "The single line above a subtotal row (tr data-total=\"subtotal\"), the line an accountant draws before adding a column."
87
+ },
88
+ "grand-total-rule-width": {
89
+ "$value": "{stroke.thick}",
90
+ "$description": "The double line under a grand total row (tr data-total=\"grand\"). It is drawn as a CSS double border, which needs at least 3px to show two strokes."
91
+ },
92
+ "total-background": {
93
+ "$value": "{table.background}",
94
+ "$description": "Fill of a subtotal or grand total row: the cell color, so the rules mark the total instead of a band. The footer's default totals row keeps the header tray."
79
95
  }
80
96
  }
81
97
  }
@@ -72,6 +72,20 @@
72
72
  },
73
73
  "$description": "Titles in the base tier: a toast, an accordion row, a list item heading. Titles use the display family; the tier sets the size."
74
74
  },
75
+ "figures-base": {
76
+ "$type": "typography",
77
+ "$value": {
78
+ "fontFamily": "{font-family.mono}",
79
+ "fontSize": "{font-size.md}",
80
+ "fontWeight": "{font-weight.normal}",
81
+ "letterSpacing": "{letter-spacing.normal}",
82
+ "lineHeight": "{line-height.md}"
83
+ },
84
+ "$description": "Figures in the base tier: an amount beside base text, a balance in a card. The mono family with tabular numbers, so digits line up.",
85
+ "$extensions": {
86
+ "sl.font-variant-numeric": "tabular-nums"
87
+ }
88
+ },
75
89
  "code-condense": {
76
90
  "$type": "typography",
77
91
  "$value": {
@@ -83,6 +97,20 @@
83
97
  },
84
98
  "$description": "Code uses the mono family so characters line up."
85
99
  },
100
+ "figures-condense": {
101
+ "$type": "typography",
102
+ "$value": {
103
+ "fontFamily": "{font-family.mono}",
104
+ "fontSize": "{font-size.sm}",
105
+ "fontWeight": "{font-weight.normal}",
106
+ "letterSpacing": "{letter-spacing.normal}",
107
+ "lineHeight": "{line-height.sm}"
108
+ },
109
+ "$description": "Figures in the condense tier: amounts and counts in tables and dense rows. The mono family with tabular numbers, so digits line up down a column.",
110
+ "$extensions": {
111
+ "sl.font-variant-numeric": "tabular-nums"
112
+ }
113
+ },
86
114
  "body-condense": {
87
115
  "$type": "typography",
88
116
  "$value": {
@@ -146,6 +174,20 @@
146
174
  "sl.text-transform": "uppercase"
147
175
  }
148
176
  },
177
+ "caps-condense": {
178
+ "$type": "typography",
179
+ "$value": {
180
+ "fontFamily": "{font-family.body}",
181
+ "fontSize": "{font-size.sm}",
182
+ "fontWeight": "{font-weight.semibold}",
183
+ "letterSpacing": "{letter-spacing.caps}",
184
+ "lineHeight": "{line-height.sm}"
185
+ },
186
+ "$description": "Caps labels in the condense tier: a working-paper caption, a column head over figures, a sheet's tab label. Uppercase and tracked a little wider than an eyebrow, so it reads as a label and not a heading.",
187
+ "$extensions": {
188
+ "sl.text-transform": "uppercase"
189
+ }
190
+ },
149
191
  "title-condense": {
150
192
  "$type": "typography",
151
193
  "$value": {