@wtfalch/design 0.12.0 → 0.14.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
@@ -70,20 +70,20 @@ Three layers, each falling back to the one under it:
70
70
  | | |
71
71
  |---|---|
72
72
  | **the system** | The components, the base values in `tokens.css`, the shared icons and illustrations. |
73
- | **the product** | Its mark, and its identity: the tokens that make it itself under every theme — font, shape, density. On `:root` in the product's stylesheet, so a theme that is silent on them gets the product, not tf. `src/products/<name>.ts`. |
73
+ | **the product** | Its mark, and its identity: the tokens that make it itself under every theme — font, shape, density. On `:root` in the product's stylesheet, so a theme that is silent on them gets the product, not otf. `src/products/<name>.ts`. |
74
74
  | **the theme** | A palette and a colour scheme, plus anything it deliberately changes. One `:root[data-theme='<id>']` rule each, generated from the object. |
75
75
 
76
76
  The middle layer is what lets a theme be shared between products: it names its
77
77
  colours and inherits the identity of whichever product wears it. Before it
78
78
  existed, valet's two palettes each restated valet's font and corners, and a
79
- palette written for two products would have shown tf's font on valet wherever
79
+ palette written for two products would have shown otf's font on valet wherever
80
80
  it kept quiet.
81
81
 
82
82
  The product's default theme is also written on `:root` when no `data-theme` is
83
83
  set, so the first paint is right with no attribute at all; set the attribute
84
84
  before the bundle loads only to restore a theme somebody picked (see First
85
- paint). tf's default is `system`, a `prefers-color-scheme` rule rather than a
86
- palette, so tf still sets the attribute.
85
+ paint). otf's default is `system`, a `prefers-color-scheme` rule rather than a
86
+ palette, so otf still sets the attribute.
87
87
 
88
88
  The main entry is the neutral view of all of it: `PRODUCTS` by name, `THEMES`
89
89
  as the union every product's picker and the contrast test read,
@@ -178,7 +178,7 @@ Your server stays the source of truth. `localStorage` only beats the paint.
178
178
 
179
179
  ## Marks
180
180
 
181
- Every product's mark, by name, in `brandMarks.ts`. tf's is one stroke. valet's
181
+ Every product's mark, by name, in `brandMarks.ts`. otf's is one stroke. valet's
182
182
  is a filled badge: the jacket with the shirt cut out of it and a bow tie in the
183
183
  cut, one path under `evenodd` so the surface shows through the shirt. Both are
184
184
  `currentColor`, so the stylesheet decides the colour and a theme can move it.
@@ -230,7 +230,7 @@ is the difference from `Markdown`, which parses and must sanitise.
230
230
  photographed in four themes, the open windows photographed too, and the
231
231
  contrast, reduced-motion and keyboard rules are tests rather than sentences.
232
232
  It came out of [tf](https://github.com/wtfalch/tf), which is its first consumer;
233
- valet is the second. A product is a layer: tf and valet each ship as one
233
+ valet is the second. A product is a layer: otf and valet each ship as one
234
234
  stylesheet and one entry, with their identity under their themes and their
235
235
  mark in the table.
236
236
 
@@ -4,21 +4,21 @@ import { type BrandName } from './brandMarks.js';
4
4
  *
5
5
  * One component for every wtfalch product: `name` picks the mark out of
6
6
  * `brandMarks.ts`, and adding a product is adding a row there. The default is
7
- * `tf` because it is the first, not because it is special.
7
+ * `otf` because it is the first, not because it is special.
8
8
  *
9
9
  * `currentColor`, so the stylesheet decides the colour and a theme can move
10
10
  * it; the mark itself knows nothing about green. No `width` or `height`
11
11
  * attributes either: the viewBox is the ink box, measured, and `.brand` sets
12
12
  * a height off the type scale so the width follows the aspect.
13
13
  *
14
- * Still, on purpose. Until 0.2.0 this component also morphed tf's mark into a
14
+ * Still, on purpose. Until 0.2.0 this component also morphed otf's mark into a
15
15
  * cog under the pointer, which made a shared package carry one product's
16
- * animation. That morph is tf's own `Brand` now, layered over this path; a
16
+ * animation. That morph is otf's own `Brand` now, layered over this path; a
17
17
  * product that wants its mark to move does the same in its own code, and the
18
18
  * package stays the source of the still shape every product's icon is drawn
19
19
  * from.
20
20
  *
21
- * Stroked or filled, whichever the mark is. tf's is one line at a weight;
21
+ * Stroked or filled, whichever the mark is. otf's is one line at a weight;
22
22
  * valet's is a filled badge with the shirt cut out of it and the bow inside
23
23
  * the cut, one path under `evenodd` so the surface shows through the shirt.
24
24
  * Both are `currentColor`, so `.brand` still decides the colour and a theme
@@ -5,21 +5,21 @@ import { BRAND_MARKS, BRAND_NAMES } from './brandMarks.js';
5
5
  *
6
6
  * One component for every wtfalch product: `name` picks the mark out of
7
7
  * `brandMarks.ts`, and adding a product is adding a row there. The default is
8
- * `tf` because it is the first, not because it is special.
8
+ * `otf` because it is the first, not because it is special.
9
9
  *
10
10
  * `currentColor`, so the stylesheet decides the colour and a theme can move
11
11
  * it; the mark itself knows nothing about green. No `width` or `height`
12
12
  * attributes either: the viewBox is the ink box, measured, and `.brand` sets
13
13
  * a height off the type scale so the width follows the aspect.
14
14
  *
15
- * Still, on purpose. Until 0.2.0 this component also morphed tf's mark into a
15
+ * Still, on purpose. Until 0.2.0 this component also morphed otf's mark into a
16
16
  * cog under the pointer, which made a shared package carry one product's
17
- * animation. That morph is tf's own `Brand` now, layered over this path; a
17
+ * animation. That morph is otf's own `Brand` now, layered over this path; a
18
18
  * product that wants its mark to move does the same in its own code, and the
19
19
  * package stays the source of the still shape every product's icon is drawn
20
20
  * from.
21
21
  *
22
- * Stroked or filled, whichever the mark is. tf's is one line at a weight;
22
+ * Stroked or filled, whichever the mark is. otf's is one line at a weight;
23
23
  * valet's is a filled badge with the shirt cut out of it and the bow inside
24
24
  * the cut, one path under `evenodd` so the surface shows through the shirt.
25
25
  * Both are `currentColor`, so `.brand` still decides the colour and a theme
@@ -27,7 +27,7 @@ export interface Props extends Omit<React.InputHTMLAttributes<HTMLInputElement>,
27
27
  * `Field`'s `<label for>` already names it. Taking it out of the props is
28
28
  * what keeps it off the DOM node: React passes through anything it does
29
29
  * not recognise, and the browser got `labelid="…"` with a console warning
30
- * on every field in valet and tf. */
30
+ * on every field in valet and otf. */
31
31
  labelId?: string;
32
32
  }
33
33
  declare const Input: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLInputElement>>;
@@ -60,7 +60,7 @@ export declare function Rows({ children, empty, label, look, className, }: {
60
60
  look?: 'list' | 'pick';
61
61
  className?: string;
62
62
  }): import("react").JSX.Element | null;
63
- export declare function Row({ name, pills, hint, trail, actions, below, onClick, tone, picked, loading, waiting, align, className, }: {
63
+ export declare function Row({ name, pills, hint, trail, actions, below, onClick, tone, picked, loading, waiting, disabled, align, className, }: {
64
64
  /** The subject. Truncates -- it is the only part that may. */
65
65
  name: React.ReactNode;
66
66
  /** Qualifiers, on the name's line. Never truncated: a badge that is
@@ -93,6 +93,13 @@ export declare function Row({ name, pills, hint, trail, actions, below, onClick,
93
93
  loading?: boolean;
94
94
  /** Something else is working, so this one cannot be chosen yet. */
95
95
  waiting?: boolean;
96
+ /** This one cannot be chosen, and nothing on the page will change that: the
97
+ * build lacks something, or a setting elsewhere rules it out. Say why in
98
+ * `hint`. Drawn as `waiting` is -- dimmed, its hit area a disabled button,
99
+ * `picked` still showing through -- but a separate word, because otf's
100
+ * About pane borrowed `waiting` for "this build cannot apply updates", and
101
+ * a row that says it is waiting is a row somebody waits on. */
102
+ disabled?: boolean;
96
103
  /** `start` when the row has enough text that vertically centred buttons drift
97
104
  * away from the name they belong to. */
98
105
  align?: 'center' | 'start';
@@ -51,12 +51,13 @@ export function Rows({ children, empty, label, look = 'list', className, }) {
51
51
  // biome-ignore lint/a11y/useSemanticElements: valid ARIA on a div; a <ul> brings the browser's list reset and the stylesheet and 204 baselines key on `div.rows`.
52
52
  role: "list", "aria-label": label, children: items }));
53
53
  }
54
- export function Row({ name, pills, hint, trail, actions, below, onClick, tone, picked, loading, waiting, align = 'center', className, }) {
54
+ export function Row({ name, pills, hint, trail, actions, below, onClick, tone, picked, loading, waiting, disabled, align = 'center', className, }) {
55
55
  const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "row-subject", children: [_jsxs("span", { className: "named", children: [_jsx("span", { className: "truncate", children: name }), pills] }), hint && _jsx("div", { className: "set-hint", children: hint })] }), trail && _jsx("span", { className: "flex-none ml-auto whitespace-nowrap set-hint mono", children: trail })] }));
56
56
  const rowClass = `set-row rows-row${tone ? ` rows-${tone}` : ''}` +
57
57
  `${align === 'start' ? ' rows-top' : ''}${picked ? ' is-picked' : ''}` +
58
58
  `${loading ? ' is-loading' : ''}${waiting ? ' is-waiting' : ''}` +
59
+ `${disabled ? ' is-disabled' : ''}` +
59
60
  `${className ? ` ${className}` : ''}`;
60
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: rowClass, role: "listitem", children: [onClick ? (_jsx("button", { type: "button", className: "row-hit", disabled: waiting, onClick: onClick, children: body })) : (body), actions] }), below] }));
61
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: rowClass, role: "listitem", children: [onClick ? (_jsx("button", { type: "button", className: "row-hit", disabled: waiting || disabled, onClick: onClick, children: body })) : (body), actions] }), below] }));
61
62
  }
62
63
  export default Rows;
@@ -2,7 +2,7 @@
2
2
  * A number chosen from a range, by dragging.
3
3
  *
4
4
  * For a setting whose answer is "about this much" rather than one of a list:
5
- * how much memory tf may take, say. A `Select` with eight sizes on it makes
5
+ * how much memory otf may take, say. A `Select` with eight sizes on it makes
6
6
  * the person pick the nearest wrong one; a box you type a number into asks
7
7
  * them to know the ceiling. A slider shows the ceiling, the floor, and where
8
8
  * between them they are.
@@ -11,7 +11,7 @@ import { useEffect, useId, useRef, useState } from 'react';
11
11
  * A number chosen from a range, by dragging.
12
12
  *
13
13
  * For a setting whose answer is "about this much" rather than one of a list:
14
- * how much memory tf may take, say. A `Select` with eight sizes on it makes
14
+ * how much memory otf may take, say. A `Select` with eight sizes on it makes
15
15
  * the person pick the nearest wrong one; a box you type a number into asks
16
16
  * them to know the ceiling. A slider shows the ceiling, the floor, and where
17
17
  * between them they are.
@@ -12,7 +12,7 @@ export interface Props extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaEle
12
12
  * `Field`'s `<label for>` already names it. Taking it out of the props is
13
13
  * what keeps it off the DOM node: React passes through anything it does
14
14
  * not recognise, and the browser got `labelid="…"` with a console warning
15
- * on every field in valet and tf. */
15
+ * on every field in valet and otf. */
16
16
  labelId?: string;
17
17
  }
18
18
  declare const Textarea: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLTextAreaElement>>;
@@ -9,11 +9,11 @@
9
9
  * mark that is a figure rather than a line carries a fill rule instead of a
10
10
  * weight, and `Brand` draws whichever it is handed.
11
11
  *
12
- * A product that animates its mark does that in its own code: tf's lowercase
13
- * tf becomes a cog under the pointer, and that morph is
12
+ * A product that animates its mark does that in its own code: otf's
13
+ * lowercase otf becomes a cog under the pointer, and that morph is
14
14
  * `tf/dashboard/src/components/Brand.tsx`, not this package's business. What
15
15
  * ships here is the still mark, which is also what the product's app icon is
16
- * rendered from -- tf's `brandMark.test.ts` holds the icon, this row and its
16
+ * rendered from -- otf's `brandMark.test.ts` holds the icon, this row and its
17
17
  * own animated copy to one path.
18
18
  *
19
19
  * A sibling module rather than part of `Brand.tsx`, so that file keeps its
@@ -22,7 +22,7 @@
22
22
  /**
23
23
  * A mark: one path in its own ink box, stroked or filled.
24
24
  *
25
- * tf's is a stroke, one line at the weight it was drawn at. valet's is a
25
+ * otf's is a stroke, one line at the weight it was drawn at. valet's is a
26
26
  * figure: a filled badge with the shirt cut out of it and the bow drawn back
27
27
  * inside the cut, which is one path under `evenodd` -- the jacket, the shirt
28
28
  * and the bow are subpaths, and the rule makes the shirt a hole the surface
@@ -43,13 +43,16 @@ export interface FillMark {
43
43
  fill: 'evenodd' | 'nonzero';
44
44
  }
45
45
  export declare const BRAND_MARKS: {
46
- /** A lowercase tf in one unbroken stroke -- down the t, round the foot, up
47
- * the f, over its head, and back along the crossbar through both stems.
48
- * The view is the stroke's outer bounds, not the canvas: the stroke is 96
49
- * wide, so 48 of cap and arc on every side is already in these. */
50
- readonly tf: {
51
- readonly view: "248 190 556 578";
52
- readonly d: "M372 262V620A100 100 0 0 0 572 620V330A92 92 0 0 1 756 330A100 100 0 0 1 656 430H296";
46
+ /** A lowercase tf merged into an O, in one unbroken stroke. Both ends of
47
+ * the tf start on the O -- the crossbar runs left and folds down onto it,
48
+ * the stem runs up and turns right onto it -- and the 95 degrees of ring
49
+ * between those two points is left out, so the O reads as open across the
50
+ * top left rather than closed. One stroke, no loose ends. The view is the
51
+ * stroke's outer bounds, not the canvas: the stroke is 96 wide, so 48 of
52
+ * cap and arc on every side is already in these. */
53
+ readonly otf: {
54
+ readonly view: "69.3 -12.2 958.4 958.4";
55
+ readonly d: "M470.2 43.0A120 120 0 0 0 372 161.0V620A100 100 0 0 0 572 620V330A92 92 0 0 1 756 330A100 100 0 0 1 656 430H248.6A120 120 0 0 0 132.9 582.0A431.2 431.2 0 1 0 470.2 43.0";
53
56
  readonly stroke: 96;
54
57
  };
55
58
  /** valet's badge. The rounded square is the jacket, the shirt is cut out of
@@ -1,11 +1,14 @@
1
1
  export const BRAND_MARKS = {
2
- /** A lowercase tf in one unbroken stroke -- down the t, round the foot, up
3
- * the f, over its head, and back along the crossbar through both stems.
4
- * The view is the stroke's outer bounds, not the canvas: the stroke is 96
5
- * wide, so 48 of cap and arc on every side is already in these. */
6
- tf: {
7
- view: '248 190 556 578',
8
- d: 'M372 262V620A100 100 0 0 0 572 620V330A92 92 0 0 1 756 330A100 100 0 0 1 656 430H296',
2
+ /** A lowercase tf merged into an O, in one unbroken stroke. Both ends of
3
+ * the tf start on the O -- the crossbar runs left and folds down onto it,
4
+ * the stem runs up and turns right onto it -- and the 95 degrees of ring
5
+ * between those two points is left out, so the O reads as open across the
6
+ * top left rather than closed. One stroke, no loose ends. The view is the
7
+ * stroke's outer bounds, not the canvas: the stroke is 96 wide, so 48 of
8
+ * cap and arc on every side is already in these. */
9
+ otf: {
10
+ view: '69.3 -12.2 958.4 958.4',
11
+ d: 'M470.2 43.0A120 120 0 0 0 372 161.0V620A100 100 0 0 0 572 620V330A92 92 0 0 1 756 330A100 100 0 0 1 656 430H248.6A120 120 0 0 0 132.9 582.0A431.2 431.2 0 1 0 470.2 43.0',
9
12
  stroke: 96,
10
13
  },
11
14
  /** valet's badge. The rounded square is the jacket, the shirt is cut out of
@@ -19,8 +19,8 @@
19
19
  * Settings and is said here rather than left implied. */
20
20
  /** Where a product records that the tour was seen. Every product that installs
21
21
  * the package gets its own key by passing one; the default is the package's,
22
- * so two products on one origin do not share a memory. tf passes
23
- * `tf-tour-seen`, the key its users already hold. */
22
+ * so two products on one origin do not share a memory. otf passes
23
+ * `otf-tour-seen`, the key its users already hold. */
24
24
  export declare const DEFAULT_TOUR_KEY = "design-tour-seen";
25
25
  export declare function tourSeen(key?: string): boolean;
26
26
  /**
@@ -19,8 +19,8 @@
19
19
  * Settings and is said here rather than left implied. */
20
20
  /** Where a product records that the tour was seen. Every product that installs
21
21
  * the package gets its own key by passing one; the default is the package's,
22
- * so two products on one origin do not share a memory. tf passes
23
- * `tf-tour-seen`, the key its users already hold. */
22
+ * so two products on one origin do not share a memory. otf passes
23
+ * `otf-tour-seen`, the key its users already hold. */
24
24
  export const DEFAULT_TOUR_KEY = 'design-tour-seen';
25
25
  export function tourSeen(key = DEFAULT_TOUR_KEY) {
26
26
  try {
@@ -1279,7 +1279,7 @@ select:focus { outline: none; border-color: var(--accent); }
1279
1279
 
1280
1280
  /* The checkbox, drawn rather than inherited.
1281
1281
  The native one is the platform's, which means it is a different shape, a
1282
- different blue and a different size on every machine tf runs on -- the one
1282
+ different blue and a different size on every machine otf runs on -- the one
1283
1283
  control in the app that refused to join the design system. It is also the
1284
1284
  only form control that can be replaced in pure CSS: unlike a select, whose
1285
1285
  dropdown belongs to the OS, a checkbox is a box and a tick.
@@ -1503,7 +1503,7 @@ select:disabled { opacity: 0.45; cursor: not-allowed; }
1503
1503
  whatever next needs to know. */
1504
1504
  /* The wordmark, and the same mark the app icon carries: `Brand.tsx` draws the
1505
1505
  path `desktop/build/icon.html` draws, and `test/brandMark.test.ts` holds
1506
- them to one string. It was "TF" set in the system font at -0.14em, matching
1506
+ them to one string. It was "otf" set in the system font at -0.14em, matching
1507
1507
  an icon that did the same; both are now one drawn line, and the tracking
1508
1508
  arithmetic that used to live here went with the letters.
1509
1509
  The height comes off the type scale rather than being a pixel: 1.5 times
@@ -1521,7 +1521,7 @@ select:disabled { opacity: 0.45; cursor: not-allowed; }
1521
1521
  width: auto;
1522
1522
  color: var(--good);
1523
1523
  /* A product that animates its mark turns it about its own centre, which is
1524
- the ink box's; tf's cog does. */
1524
+ the ink box's; otf's cog does. */
1525
1525
  transform-origin: center;
1526
1526
  }
1527
1527
 
@@ -1820,7 +1820,7 @@ input:active, textarea:active, select:active { transform: none; }
1820
1820
  .spin { animation: none; opacity: 0.6; }
1821
1821
  }
1822
1822
 
1823
- @keyframes tf-bounce {
1823
+ @keyframes otf-bounce {
1824
1824
  0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
1825
1825
  40% { opacity: 1; transform: translateY(-3px); }
1826
1826
  }
@@ -2827,8 +2827,10 @@ input[type='radio']:active:not(:disabled) {
2827
2827
  }
2828
2828
 
2829
2829
  /* Only where there is something to pick. A row without `row-hit` is not a
2830
- choice, and lighting it up on hover promises something that does not happen. */
2831
- .rows-pick > .rows-row:has(.row-hit):hover {
2830
+ choice, and lighting it up on hover promises something that does not happen.
2831
+ Nor is a row whose hit is disabled: a waiting row lit up under its own
2832
+ `not-allowed` cursor, and `disabled` would have made that permanent. */
2833
+ .rows-pick > .rows-row:has(.row-hit:enabled):hover {
2832
2834
  border-color: var(--accent);
2833
2835
  background: var(--panel-2);
2834
2836
  }
@@ -2880,9 +2882,16 @@ input[type='radio']:active:not(:disabled) {
2880
2882
  /* And nothing else can be chosen while it happens: picking a second model
2881
2883
  mid-load queues a second load, which is how a machine ends up swapping two
2882
2884
  sets of weights it cannot hold at once. */
2883
- .rows-pick > .rows-row.is-waiting { opacity: 0.5; }
2885
+ .rows-pick > .rows-row.is-waiting,
2886
+ .rows-pick > .rows-row.is-disabled { opacity: 0.5; }
2884
2887
 
2885
- .rows-pick > .rows-row.is-waiting .row-hit { cursor: not-allowed; }
2888
+ .rows-pick > .rows-row.is-waiting .row-hit,
2889
+ .rows-pick > .rows-row.is-disabled .row-hit { cursor: not-allowed; }
2890
+
2891
+ /* Dimmed once. `.row-hit:disabled` dims the button as well, and under the
2892
+ row's own opacity the hint of a disabled row, the line saying why it cannot
2893
+ be chosen, came out at a quarter strength. */
2894
+ .rows-pick > .rows-row.is-disabled > .row-hit:disabled { opacity: 1; }
2886
2895
 
2887
2896
  @media (prefers-reduced-motion: reduce) {
2888
2897
  /* The border still says which one, without the breathing. */
@@ -2934,7 +2943,7 @@ input[type='radio']:active:not(:disabled) {
2934
2943
  margin-bottom: calc(var(--space-2) * -1);
2935
2944
  }
2936
2945
 
2937
- .rows-pick > .rows-row.is-picked:has(.row-hit):hover {
2946
+ .rows-pick > .rows-row.is-picked:has(.row-hit:enabled):hover {
2938
2947
  background: color-mix(in srgb, var(--accent) 13%, var(--panel));
2939
2948
  }
2940
2949
 
@@ -2993,7 +3002,7 @@ input[type='radio']:active:not(:disabled) {
2993
3002
  border-radius: var(--radius-sm);
2994
3003
  }
2995
3004
 
2996
- .row-hit:hover .truncate { color: var(--accent); }
3005
+ .row-hit:enabled:hover .truncate { color: var(--accent); }
2997
3006
 
2998
3007
  .row-hit:focus-visible { outline: none; box-shadow: var(--focus-ring); }
2999
3008
  }
@@ -3328,7 +3337,7 @@ input[type='radio']:active:not(:disabled) {
3328
3337
  as part of the heading rather than as the first line of content. */
3329
3338
  .modal-body { padding: var(--space-2) var(--space-4) var(--space-3); overflow-y: auto; }
3330
3339
 
3331
- /* The close cross. Its rule stayed behind in tf's stylesheet when the split
3340
+ /* The close cross. Its rule stayed behind in otf's stylesheet when the split
3332
3341
  ran -- a one-letter class name, and the scan that finds classes in source
3333
3342
  dropped it -- so in the package the cross had only `.icon-btn`, which is the
3334
3343
  picker's 34px square tile with a panel behind it. The head is laid out for
@@ -3405,7 +3414,7 @@ input[type='radio']:active:not(:disabled) {
3405
3414
  /* The tone is on the box, not only on the button. A destructive question that
3406
3415
  looks like every other question is one the reader skims.
3407
3416
  `Dialog` writes this as `dialog-${tone}`, which a literal scan of the source
3408
- never sees -- so the rule was left behind in tf's stylesheet by the split and
3417
+ never sees -- so the rule was left behind in otf's stylesheet by the split and
3409
3418
  the package shipped a `tone="bad"` prop that changed nothing. */
3410
3419
  .dialog-bad { border-color: color-mix(in oklab, var(--bad) 45%, var(--border)); }
3411
3420
 
@@ -5072,6 +5081,6 @@ body.splitting * { cursor: inherit !important; }
5072
5081
  pointer-events: none;
5073
5082
  }
5074
5083
  }
5075
- /* ---- tf: the themes ---- */
5084
+ /* ---- otf: the themes ---- */
5076
5085
  :root[data-theme='night']{--bg:#0f1115;--panel:#161a21;--panel-2:#1c222b;--border:#262d38;--text:#e6e9ef;--muted:#8b94a4;--accent:#5b9dff;--accent-dim:#2a4877;--app-bg:#0f1115;color-scheme:dark}
5077
5086
  :root[data-theme='paper']{--bg:#f6f7f9;--panel:#ffffff;--panel-2:#f0f2f5;--border:#e4e8ec;--border-strong:#8792a1;--text:#191d23;--muted:#5d6773;--accent:#0e7872;--accent-dim:#7fbdb8;--on-accent:#ffffff;--good:#1c7a4a;--warn:#8a6216;--bad:#b3312c;--info:#216bc9;--shadow-1:0 4px 14px rgba(16, 24, 40, 0.08);--shadow-2:0 8px 24px rgba(16, 24, 40, 0.10);--shadow-3:0 12px 32px rgba(16, 24, 40, 0.12);--scrim:rgba(16, 24, 40, 0.32);color-scheme:light}
@@ -1,10 +1,10 @@
1
1
  /**
2
- * tf's entry: the package, as tf.
2
+ * otf's entry: the package, as otf.
3
3
  *
4
4
  * Everything the main entry exports, and on top of it `Brand`, `THEMES`,
5
- * `DEFAULT_THEME` and `applyTheme` bound to tf -- a local export shadows
5
+ * `DEFAULT_THEME` and `applyTheme` bound to otf -- a local export shadows
6
6
  * the same name from `export *`. A site is one product, so it imports this
7
- * and `@wtfalch/design/tf.css` and nothing of anyone else's.
7
+ * and `@wtfalch/design/otf.css` and nothing of anyone else's.
8
8
  */
9
9
  export * from './index.js';
10
10
  export declare const product: import("./products/index.js").Product;
@@ -1,15 +1,15 @@
1
1
  /**
2
- * tf's entry: the package, as tf.
2
+ * otf's entry: the package, as otf.
3
3
  *
4
4
  * Everything the main entry exports, and on top of it `Brand`, `THEMES`,
5
- * `DEFAULT_THEME` and `applyTheme` bound to tf -- a local export shadows
5
+ * `DEFAULT_THEME` and `applyTheme` bound to otf -- a local export shadows
6
6
  * the same name from `export *`. A site is one product, so it imports this
7
- * and `@wtfalch/design/tf.css` and nothing of anyone else's.
7
+ * and `@wtfalch/design/otf.css` and nothing of anyone else's.
8
8
  */
9
9
  export * from './index.js';
10
10
  import { bindProduct } from './products/index.js';
11
- import { tf } from './products/tf.js';
12
- const bound = bindProduct(tf);
11
+ import { otf } from './products/otf.js';
12
+ const bound = bindProduct(otf);
13
13
  export const product = bound.product;
14
14
  export const Brand = bound.Brand;
15
15
  export const THEMES = bound.THEMES;
@@ -14,7 +14,7 @@ import { type Theme, type ThemeTokens } from '../themes/index.js';
14
14
  *
15
15
  * The middle layer is what lets a theme be shared between products. A theme
16
16
  * is a sparse map, and what it is sparse *over* decides what a silent token
17
- * shows: over the base it shows tf, over the product it shows the product.
17
+ * shows: over the base it shows otf, over the product it shows the product.
18
18
  * Before this layer valet's two palettes each restated valet's font and
19
19
  * corners, because there was nowhere else to put them.
20
20
  *
@@ -36,7 +36,7 @@ export interface Product {
36
36
  }
37
37
  /** Every product, by the name of its mark. */
38
38
  export declare const PRODUCTS: {
39
- readonly tf: Product;
39
+ readonly otf: Product;
40
40
  readonly valet: Product;
41
41
  };
42
42
  /** Declare a product, so a default that is not a theme or a key `applyTheme`
@@ -47,7 +47,7 @@ export declare function defineProduct(product: Product): Product;
47
47
  *
48
48
  * An id the product does not offer resolves to the product's default rather
49
49
  * than the package's, so a valet setting that remembers a theme valet has
50
- * since dropped comes up as valet, not as tf.
50
+ * since dropped comes up as valet, not as otf.
51
51
  */
52
52
  export declare function productTheme(product: Product, theme?: string | Theme): Theme;
53
53
  type BrandProps = ComponentProps<typeof BrandDefault>;
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import BrandDefault from '../components/Brand.js';
3
3
  import { applyTheme } from '../themes/index.js';
4
4
  import { themeId } from '../themes/css.js';
5
- import { tf } from './tf.js';
5
+ import { otf } from './otf.js';
6
6
  import { valet } from './valet.js';
7
7
  /** Every product, by the name of its mark. */
8
- export const PRODUCTS = { tf, valet };
8
+ export const PRODUCTS = { otf, valet };
9
9
  /** Declare a product, so a default that is not a theme or a key `applyTheme`
10
10
  * would not use is an error where it is written. */
11
11
  export function defineProduct(product) {
@@ -24,7 +24,7 @@ export function defineProduct(product) {
24
24
  *
25
25
  * An id the product does not offer resolves to the product's default rather
26
26
  * than the package's, so a valet setting that remembers a theme valet has
27
- * since dropped comes up as valet, not as tf.
27
+ * since dropped comes up as valet, not as otf.
28
28
  */
29
29
  export function productTheme(product, theme = product.defaultTheme) {
30
30
  const resolved = typeof theme === 'string'
@@ -1,7 +1,7 @@
1
1
  import type { Theme } from '../themes/index.js';
2
2
  import type { Product } from './index.js';
3
3
  /**
4
- * tf's themes: the three the package shipped with, and the ones the gallery
4
+ * otf's themes: the three the package shipped with, and the ones the gallery
5
5
  * was photographed in first.
6
6
  *
7
7
  * `system` is a theme, not a mode. A `prefers-color-scheme` block that
@@ -14,19 +14,19 @@ export declare const system: Theme;
14
14
  export declare const night: Theme;
15
15
  export declare const paper: Theme;
16
16
  /** Keyed by the name `applyTheme` takes, which is `name` lowercased. */
17
- export declare const TF_THEMES: {
17
+ export declare const OTF_THEMES: {
18
18
  readonly system: Theme;
19
19
  readonly night: Theme;
20
20
  readonly paper: Theme;
21
21
  };
22
22
  /**
23
- * tf, the product.
23
+ * otf, the product.
24
24
  *
25
- * Its identity is empty on purpose: `tokens.css` carries tf's font, shape and
25
+ * Its identity is empty on purpose: `tokens.css` carries otf's font, shape and
26
26
  * density as the base values, so there is nothing to lay over them. `system`
27
27
  * is the default because following the OS is the choice a tool open all day
28
28
  * should make for you -- and because it is a `prefers-color-scheme` rule
29
- * rather than a palette, tf's stylesheet writes no default on `:root`; tf sets
30
- * `data-theme` before the bundle loads, as it always has.
29
+ * rather than a palette, otf's stylesheet writes no default on `:root`; otf
30
+ * sets `data-theme` before the bundle loads, as it always has.
31
31
  */
32
- export declare const tf: Product;
32
+ export declare const otf: Product;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tf's themes: the three the package shipped with, and the ones the gallery
2
+ * otf's themes: the three the package shipped with, and the ones the gallery
3
3
  * was photographed in first.
4
4
  *
5
5
  * `system` is a theme, not a mode. A `prefers-color-scheme` block that
@@ -85,20 +85,20 @@ export const paper = {
85
85
  },
86
86
  };
87
87
  /** Keyed by the name `applyTheme` takes, which is `name` lowercased. */
88
- export const TF_THEMES = { system, night, paper };
88
+ export const OTF_THEMES = { system, night, paper };
89
89
  /**
90
- * tf, the product.
90
+ * otf, the product.
91
91
  *
92
- * Its identity is empty on purpose: `tokens.css` carries tf's font, shape and
92
+ * Its identity is empty on purpose: `tokens.css` carries otf's font, shape and
93
93
  * density as the base values, so there is nothing to lay over them. `system`
94
94
  * is the default because following the OS is the choice a tool open all day
95
95
  * should make for you -- and because it is a `prefers-color-scheme` rule
96
- * rather than a palette, tf's stylesheet writes no default on `:root`; tf sets
97
- * `data-theme` before the bundle loads, as it always has.
96
+ * rather than a palette, otf's stylesheet writes no default on `:root`; otf
97
+ * sets `data-theme` before the bundle loads, as it always has.
98
98
  */
99
- export const tf = {
100
- name: 'tf',
99
+ export const otf = {
100
+ name: 'otf',
101
101
  identity: {},
102
- themes: TF_THEMES,
102
+ themes: OTF_THEMES,
103
103
  defaultTheme: 'system',
104
104
  };
@@ -8,14 +8,14 @@
8
8
  * tones are what the system says. They never share a colour.
9
9
  *
10
10
  * Every colour is measured, not judged: `contrast.test.ts` holds both palettes
11
- * to the same pairs as tf's.
11
+ * to the same pairs as otf's.
12
12
  *
13
13
  * The palettes name colours and nothing else. What makes valet valet under
14
14
  * either of them -- the type and the corners -- is the identity below, on
15
15
  * `:root` in valet's stylesheet and under every palette `applyTheme` lays on.
16
16
  * Both palettes used to restate it, which is the shape that breaks the moment
17
17
  * a theme is shared between products: silent on the font, it would have fallen
18
- * back to tf's.
18
+ * back to otf's.
19
19
  */
20
20
  const FONT = 'var(--font-sans, "IBM Plex Sans"), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
21
21
  const FONT_MONO = 'var(--font-mono, "JetBrains Mono"), ui-monospace, SFMono-Regular, Menlo, monospace';
@@ -1009,7 +1009,7 @@ select:focus { outline: none; border-color: var(--accent); }
1009
1009
 
1010
1010
  /* The checkbox, drawn rather than inherited.
1011
1011
  The native one is the platform's, which means it is a different shape, a
1012
- different blue and a different size on every machine tf runs on -- the one
1012
+ different blue and a different size on every machine otf runs on -- the one
1013
1013
  control in the app that refused to join the design system. It is also the
1014
1014
  only form control that can be replaced in pure CSS: unlike a select, whose
1015
1015
  dropdown belongs to the OS, a checkbox is a box and a tick.
@@ -1233,7 +1233,7 @@ select:disabled { opacity: 0.45; cursor: not-allowed; }
1233
1233
  whatever next needs to know. */
1234
1234
  /* The wordmark, and the same mark the app icon carries: `Brand.tsx` draws the
1235
1235
  path `desktop/build/icon.html` draws, and `test/brandMark.test.ts` holds
1236
- them to one string. It was "TF" set in the system font at -0.14em, matching
1236
+ them to one string. It was "otf" set in the system font at -0.14em, matching
1237
1237
  an icon that did the same; both are now one drawn line, and the tracking
1238
1238
  arithmetic that used to live here went with the letters.
1239
1239
  The height comes off the type scale rather than being a pixel: 1.5 times
@@ -1251,7 +1251,7 @@ select:disabled { opacity: 0.45; cursor: not-allowed; }
1251
1251
  width: auto;
1252
1252
  color: var(--good);
1253
1253
  /* A product that animates its mark turns it about its own centre, which is
1254
- the ink box's; tf's cog does. */
1254
+ the ink box's; otf's cog does. */
1255
1255
  transform-origin: center;
1256
1256
  }
1257
1257
 
@@ -1550,7 +1550,7 @@ input:active, textarea:active, select:active { transform: none; }
1550
1550
  .spin { animation: none; opacity: 0.6; }
1551
1551
  }
1552
1552
 
1553
- @keyframes tf-bounce {
1553
+ @keyframes otf-bounce {
1554
1554
  0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
1555
1555
  40% { opacity: 1; transform: translateY(-3px); }
1556
1556
  }
@@ -2557,8 +2557,10 @@ input[type='radio']:active:not(:disabled) {
2557
2557
  }
2558
2558
 
2559
2559
  /* Only where there is something to pick. A row without `row-hit` is not a
2560
- choice, and lighting it up on hover promises something that does not happen. */
2561
- .rows-pick > .rows-row:has(.row-hit):hover {
2560
+ choice, and lighting it up on hover promises something that does not happen.
2561
+ Nor is a row whose hit is disabled: a waiting row lit up under its own
2562
+ `not-allowed` cursor, and `disabled` would have made that permanent. */
2563
+ .rows-pick > .rows-row:has(.row-hit:enabled):hover {
2562
2564
  border-color: var(--accent);
2563
2565
  background: var(--panel-2);
2564
2566
  }
@@ -2610,9 +2612,16 @@ input[type='radio']:active:not(:disabled) {
2610
2612
  /* And nothing else can be chosen while it happens: picking a second model
2611
2613
  mid-load queues a second load, which is how a machine ends up swapping two
2612
2614
  sets of weights it cannot hold at once. */
2613
- .rows-pick > .rows-row.is-waiting { opacity: 0.5; }
2615
+ .rows-pick > .rows-row.is-waiting,
2616
+ .rows-pick > .rows-row.is-disabled { opacity: 0.5; }
2614
2617
 
2615
- .rows-pick > .rows-row.is-waiting .row-hit { cursor: not-allowed; }
2618
+ .rows-pick > .rows-row.is-waiting .row-hit,
2619
+ .rows-pick > .rows-row.is-disabled .row-hit { cursor: not-allowed; }
2620
+
2621
+ /* Dimmed once. `.row-hit:disabled` dims the button as well, and under the
2622
+ row's own opacity the hint of a disabled row, the line saying why it cannot
2623
+ be chosen, came out at a quarter strength. */
2624
+ .rows-pick > .rows-row.is-disabled > .row-hit:disabled { opacity: 1; }
2616
2625
 
2617
2626
  @media (prefers-reduced-motion: reduce) {
2618
2627
  /* The border still says which one, without the breathing. */
@@ -2664,7 +2673,7 @@ input[type='radio']:active:not(:disabled) {
2664
2673
  margin-bottom: calc(var(--space-2) * -1);
2665
2674
  }
2666
2675
 
2667
- .rows-pick > .rows-row.is-picked:has(.row-hit):hover {
2676
+ .rows-pick > .rows-row.is-picked:has(.row-hit:enabled):hover {
2668
2677
  background: color-mix(in srgb, var(--accent) 13%, var(--panel));
2669
2678
  }
2670
2679
 
@@ -2723,7 +2732,7 @@ input[type='radio']:active:not(:disabled) {
2723
2732
  border-radius: var(--radius-sm);
2724
2733
  }
2725
2734
 
2726
- .row-hit:hover .truncate { color: var(--accent); }
2735
+ .row-hit:enabled:hover .truncate { color: var(--accent); }
2727
2736
 
2728
2737
  .row-hit:focus-visible { outline: none; box-shadow: var(--focus-ring); }
2729
2738
  }
@@ -3058,7 +3067,7 @@ input[type='radio']:active:not(:disabled) {
3058
3067
  as part of the heading rather than as the first line of content. */
3059
3068
  .modal-body { padding: var(--space-2) var(--space-4) var(--space-3); overflow-y: auto; }
3060
3069
 
3061
- /* The close cross. Its rule stayed behind in tf's stylesheet when the split
3070
+ /* The close cross. Its rule stayed behind in otf's stylesheet when the split
3062
3071
  ran -- a one-letter class name, and the scan that finds classes in source
3063
3072
  dropped it -- so in the package the cross had only `.icon-btn`, which is the
3064
3073
  picker's 34px square tile with a panel behind it. The head is laid out for
@@ -3135,7 +3144,7 @@ input[type='radio']:active:not(:disabled) {
3135
3144
  /* The tone is on the box, not only on the button. A destructive question that
3136
3145
  looks like every other question is one the reader skims.
3137
3146
  `Dialog` writes this as `dialog-${tone}`, which a literal scan of the source
3138
- never sees -- so the rule was left behind in tf's stylesheet by the split and
3147
+ never sees -- so the rule was left behind in otf's stylesheet by the split and
3139
3148
  the package shipped a `tone="bad"` prop that changed nothing. */
3140
3149
  .dialog-bad { border-color: color-mix(in oklab, var(--bad) 45%, var(--border)); }
3141
3150
 
@@ -50,7 +50,7 @@ export declare function themeChoices(product: BrandName | Product, only?: readon
50
50
  /**
51
51
  * The blocking script, as a string to put in a `<script>` in `<head>`.
52
52
  *
53
- * `dangerouslySetInnerHTML={{ __html: themeChoiceScript('tf') }}` in a Next
53
+ * `dangerouslySetInnerHTML={{ __html: themeChoiceScript('otf') }}` in a Next
54
54
  * root layout, above everything. It reads the stored id, checks it against
55
55
  * what this app offers, and writes `data-theme` on `<html>`.
56
56
  *
@@ -49,7 +49,7 @@ export function themeChoices(product, only) {
49
49
  /**
50
50
  * The blocking script, as a string to put in a `<script>` in `<head>`.
51
51
  *
52
- * `dangerouslySetInnerHTML={{ __html: themeChoiceScript('tf') }}` in a Next
52
+ * `dangerouslySetInnerHTML={{ __html: themeChoiceScript('otf') }}` in a Next
53
53
  * root layout, above everything. It reads the stored id, checks it against
54
54
  * what this app offers, and writes `data-theme` on `<html>`.
55
55
  *
@@ -31,7 +31,7 @@ export declare function productCss(themes: Record<string, Theme>): string;
31
31
  * wins over the base values by coming later at the same specificity; the
32
32
  * default theme on `:root:not([data-theme])`, so the first paint is right with
33
33
  * no attribute at all; the components; and one rule per theme. A default with
34
- * no tokens -- tf's `system`, which is a media query rather than a palette --
34
+ * no tokens -- otf's `system`, which is a media query rather than a palette --
35
35
  * writes no default rule, and neither does an empty identity.
36
36
  *
37
37
  * Structural, not typed to `Product`: that type lives beside the React
@@ -53,7 +53,7 @@ export function productCss(themes) {
53
53
  * wins over the base values by coming later at the same specificity; the
54
54
  * default theme on `:root:not([data-theme])`, so the first paint is right with
55
55
  * no attribute at all; the components; and one rule per theme. A default with
56
- * no tokens -- tf's `system`, which is a media query rather than a palette --
56
+ * no tokens -- otf's `system`, which is a media query rather than a palette --
57
57
  * writes no default rule, and neither does an empty identity.
58
58
  *
59
59
  * Structural, not typed to `Product`: that type lives beside the React
@@ -9,7 +9,7 @@
9
9
  * than a property that silently does nothing — which is exactly the failure a
10
10
  * string-keyed map would produce, at run time, invisibly. That property is the
11
11
  * reason this file is exported from the package at all: a consumer writing a
12
- * theme gets the same compile error tf gets, and a theme that ships as JSON or
12
+ * theme gets the same compile error otf gets, and a theme that ships as JSON or
13
13
  * as a hand-written CSS file does not.
14
14
  *
15
15
  * ---
@@ -173,7 +173,7 @@ export declare function defineTheme(theme: Theme): Theme;
173
173
  * mark is, and 0.2.0 already made `Brand` the home of every product's mark by
174
174
  * name; keeping each product's palette in its own repo meant each repo
175
175
  * re-deriving first paint, the contrast measurement and a page to look at it
176
- * on. So `products/tf.ts` holds tf's three and `products/valet.ts` valet's two,
176
+ * on. So `products/otf.ts` holds otf's three and `products/valet.ts` valet's two,
177
177
  * this is the union, and the contrast test and the gallery read the union.
178
178
  *
179
179
  * A consumer that wants only its own imports the product module, or the CSS
@@ -186,7 +186,7 @@ export declare function isTheme(name: unknown): name is string;
186
186
  * Put a theme on an element.
187
187
  *
188
188
  * **The element is an argument, and that is the whole difference between this
189
- * and the version that lived in the app.** tf owns its document, so writing to
189
+ * and the version that lived in the app.** otf owns its document, so writing to
190
190
  * `documentElement` was free. A package does not: mounted inside a page it did
191
191
  * not build, it has to be able to theme a subtree and leave the rest alone.
192
192
  * Defaulting to `documentElement` keeps the app case a no-op.
@@ -9,7 +9,7 @@
9
9
  * than a property that silently does nothing — which is exactly the failure a
10
10
  * string-keyed map would produce, at run time, invisibly. That property is the
11
11
  * reason this file is exported from the package at all: a consumer writing a
12
- * theme gets the same compile error tf gets, and a theme that ships as JSON or
12
+ * theme gets the same compile error otf gets, and a theme that ships as JSON or
13
13
  * as a hand-written CSS file does not.
14
14
  *
15
15
  * ---
@@ -37,7 +37,7 @@
37
37
  * token added to the stylesheet and to no list fails the suite rather than
38
38
  * becoming a fourth, undocumented category.
39
39
  */
40
- import { TF_THEMES } from '../products/tf.js';
40
+ import { OTF_THEMES } from '../products/otf.js';
41
41
  import { VALET_THEMES } from '../products/valet.js';
42
42
  /**
43
43
  * Computed from a themeable token, and not settable.
@@ -164,13 +164,13 @@ export function defineTheme(theme) {
164
164
  * mark is, and 0.2.0 already made `Brand` the home of every product's mark by
165
165
  * name; keeping each product's palette in its own repo meant each repo
166
166
  * re-deriving first paint, the contrast measurement and a page to look at it
167
- * on. So `products/tf.ts` holds tf's three and `products/valet.ts` valet's two,
167
+ * on. So `products/otf.ts` holds otf's three and `products/valet.ts` valet's two,
168
168
  * this is the union, and the contrast test and the gallery read the union.
169
169
  *
170
170
  * A consumer that wants only its own imports the product module, or the CSS
171
171
  * `build-products.mjs` generates from it, and bundles nobody else's.
172
172
  */
173
- export const THEMES = { ...TF_THEMES, ...VALET_THEMES };
173
+ export const THEMES = { ...OTF_THEMES, ...VALET_THEMES };
174
174
  export const DEFAULT_THEME = 'system';
175
175
  export function isTheme(name) {
176
176
  return typeof name === 'string' && name in THEMES;
@@ -179,7 +179,7 @@ export function isTheme(name) {
179
179
  * Put a theme on an element.
180
180
  *
181
181
  * **The element is an argument, and that is the whole difference between this
182
- * and the version that lived in the app.** tf owns its document, so writing to
182
+ * and the version that lived in the app.** otf owns its document, so writing to
183
183
  * `documentElement` was free. A package does not: mounted inside a page it did
184
184
  * not build, it has to be able to theme a subtree and leave the rest alone.
185
185
  * Defaulting to `documentElement` keeps the app case a no-op.
@@ -191,7 +191,7 @@ export function isTheme(name) {
191
191
  */
192
192
  export function applyTheme(theme, el = document.documentElement) {
193
193
  /* A registered name, or a `Theme` object straight from `defineTheme`.
194
- The second is how an app that is not tf applies its own palette without
194
+ The second is how an app that is not otf applies its own palette without
195
195
  first pushing it into a registry it does not own -- `THEMES` is the
196
196
  built-ins, and a consumer's theme is theirs. */
197
197
  const resolved = typeof theme === 'string' ? (THEMES[theme] ?? THEMES[DEFAULT_THEME]) : theme;
package/dist/valet.css CHANGED
@@ -1283,7 +1283,7 @@ select:focus { outline: none; border-color: var(--accent); }
1283
1283
 
1284
1284
  /* The checkbox, drawn rather than inherited.
1285
1285
  The native one is the platform's, which means it is a different shape, a
1286
- different blue and a different size on every machine tf runs on -- the one
1286
+ different blue and a different size on every machine otf runs on -- the one
1287
1287
  control in the app that refused to join the design system. It is also the
1288
1288
  only form control that can be replaced in pure CSS: unlike a select, whose
1289
1289
  dropdown belongs to the OS, a checkbox is a box and a tick.
@@ -1507,7 +1507,7 @@ select:disabled { opacity: 0.45; cursor: not-allowed; }
1507
1507
  whatever next needs to know. */
1508
1508
  /* The wordmark, and the same mark the app icon carries: `Brand.tsx` draws the
1509
1509
  path `desktop/build/icon.html` draws, and `test/brandMark.test.ts` holds
1510
- them to one string. It was "TF" set in the system font at -0.14em, matching
1510
+ them to one string. It was "otf" set in the system font at -0.14em, matching
1511
1511
  an icon that did the same; both are now one drawn line, and the tracking
1512
1512
  arithmetic that used to live here went with the letters.
1513
1513
  The height comes off the type scale rather than being a pixel: 1.5 times
@@ -1525,7 +1525,7 @@ select:disabled { opacity: 0.45; cursor: not-allowed; }
1525
1525
  width: auto;
1526
1526
  color: var(--good);
1527
1527
  /* A product that animates its mark turns it about its own centre, which is
1528
- the ink box's; tf's cog does. */
1528
+ the ink box's; otf's cog does. */
1529
1529
  transform-origin: center;
1530
1530
  }
1531
1531
 
@@ -1824,7 +1824,7 @@ input:active, textarea:active, select:active { transform: none; }
1824
1824
  .spin { animation: none; opacity: 0.6; }
1825
1825
  }
1826
1826
 
1827
- @keyframes tf-bounce {
1827
+ @keyframes otf-bounce {
1828
1828
  0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
1829
1829
  40% { opacity: 1; transform: translateY(-3px); }
1830
1830
  }
@@ -2831,8 +2831,10 @@ input[type='radio']:active:not(:disabled) {
2831
2831
  }
2832
2832
 
2833
2833
  /* Only where there is something to pick. A row without `row-hit` is not a
2834
- choice, and lighting it up on hover promises something that does not happen. */
2835
- .rows-pick > .rows-row:has(.row-hit):hover {
2834
+ choice, and lighting it up on hover promises something that does not happen.
2835
+ Nor is a row whose hit is disabled: a waiting row lit up under its own
2836
+ `not-allowed` cursor, and `disabled` would have made that permanent. */
2837
+ .rows-pick > .rows-row:has(.row-hit:enabled):hover {
2836
2838
  border-color: var(--accent);
2837
2839
  background: var(--panel-2);
2838
2840
  }
@@ -2884,9 +2886,16 @@ input[type='radio']:active:not(:disabled) {
2884
2886
  /* And nothing else can be chosen while it happens: picking a second model
2885
2887
  mid-load queues a second load, which is how a machine ends up swapping two
2886
2888
  sets of weights it cannot hold at once. */
2887
- .rows-pick > .rows-row.is-waiting { opacity: 0.5; }
2889
+ .rows-pick > .rows-row.is-waiting,
2890
+ .rows-pick > .rows-row.is-disabled { opacity: 0.5; }
2888
2891
 
2889
- .rows-pick > .rows-row.is-waiting .row-hit { cursor: not-allowed; }
2892
+ .rows-pick > .rows-row.is-waiting .row-hit,
2893
+ .rows-pick > .rows-row.is-disabled .row-hit { cursor: not-allowed; }
2894
+
2895
+ /* Dimmed once. `.row-hit:disabled` dims the button as well, and under the
2896
+ row's own opacity the hint of a disabled row, the line saying why it cannot
2897
+ be chosen, came out at a quarter strength. */
2898
+ .rows-pick > .rows-row.is-disabled > .row-hit:disabled { opacity: 1; }
2890
2899
 
2891
2900
  @media (prefers-reduced-motion: reduce) {
2892
2901
  /* The border still says which one, without the breathing. */
@@ -2938,7 +2947,7 @@ input[type='radio']:active:not(:disabled) {
2938
2947
  margin-bottom: calc(var(--space-2) * -1);
2939
2948
  }
2940
2949
 
2941
- .rows-pick > .rows-row.is-picked:has(.row-hit):hover {
2950
+ .rows-pick > .rows-row.is-picked:has(.row-hit:enabled):hover {
2942
2951
  background: color-mix(in srgb, var(--accent) 13%, var(--panel));
2943
2952
  }
2944
2953
 
@@ -2997,7 +3006,7 @@ input[type='radio']:active:not(:disabled) {
2997
3006
  border-radius: var(--radius-sm);
2998
3007
  }
2999
3008
 
3000
- .row-hit:hover .truncate { color: var(--accent); }
3009
+ .row-hit:enabled:hover .truncate { color: var(--accent); }
3001
3010
 
3002
3011
  .row-hit:focus-visible { outline: none; box-shadow: var(--focus-ring); }
3003
3012
  }
@@ -3332,7 +3341,7 @@ input[type='radio']:active:not(:disabled) {
3332
3341
  as part of the heading rather than as the first line of content. */
3333
3342
  .modal-body { padding: var(--space-2) var(--space-4) var(--space-3); overflow-y: auto; }
3334
3343
 
3335
- /* The close cross. Its rule stayed behind in tf's stylesheet when the split
3344
+ /* The close cross. Its rule stayed behind in otf's stylesheet when the split
3336
3345
  ran -- a one-letter class name, and the scan that finds classes in source
3337
3346
  dropped it -- so in the package the cross had only `.icon-btn`, which is the
3338
3347
  picker's 34px square tile with a panel behind it. The head is laid out for
@@ -3409,7 +3418,7 @@ input[type='radio']:active:not(:disabled) {
3409
3418
  /* The tone is on the box, not only on the button. A destructive question that
3410
3419
  looks like every other question is one the reader skims.
3411
3420
  `Dialog` writes this as `dialog-${tone}`, which a literal scan of the source
3412
- never sees -- so the rule was left behind in tf's stylesheet by the split and
3421
+ never sees -- so the rule was left behind in otf's stylesheet by the split and
3413
3422
  the package shipped a `tone="bad"` prop that changed nothing. */
3414
3423
  .dialog-bad { border-color: color-mix(in oklab, var(--bad) 45%, var(--border)); }
3415
3424
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/design",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "wtfalch's design system: themeable components on a fixed token vocabulary.",
5
5
  "keywords": [
6
6
  "design-system",
@@ -24,7 +24,9 @@
24
24
  },
25
25
  "license": "MIT",
26
26
  "type": "module",
27
- "files": ["dist"],
27
+ "files": [
28
+ "dist"
29
+ ],
28
30
  "publishConfig": {
29
31
  "access": "public"
30
32
  },
@@ -33,15 +35,15 @@
33
35
  "types": "./dist/index.d.ts",
34
36
  "default": "./dist/index.js"
35
37
  },
36
- "./tf": {
37
- "types": "./dist/tf.d.ts",
38
- "default": "./dist/tf.js"
38
+ "./otf": {
39
+ "types": "./dist/otf.d.ts",
40
+ "default": "./dist/otf.js"
39
41
  },
40
42
  "./valet": {
41
43
  "types": "./dist/valet.d.ts",
42
44
  "default": "./dist/valet.js"
43
45
  },
44
- "./tf.css": "./dist/tf.css",
46
+ "./otf.css": "./dist/otf.css",
45
47
  "./valet.css": "./dist/valet.css",
46
48
  "./themes": {
47
49
  "types": "./dist/themes/index.d.ts",
@@ -58,16 +60,12 @@
58
60
  "default": "./dist/rich-text.js"
59
61
  }
60
62
  },
61
- "sideEffects": ["*.css"],
63
+ "sideEffects": [
64
+ "*.css"
65
+ ],
62
66
  "engines": {
63
67
  "node": ">=22.0.0"
64
68
  },
65
- "scripts": {
66
- "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/fix-extensions.mjs && node scripts/copy-css.mjs && node scripts/build-products.mjs",
67
- "prepack": "pnpm build",
68
- "typecheck": "tsc --noEmit",
69
- "test": "vitest run"
70
- },
71
69
  "peerDependencies": {
72
70
  "@tiptap/extension-link": "^3.31.0",
73
71
  "@tiptap/extensions": "^3.31.0",
@@ -120,5 +118,10 @@
120
118
  "zod": {
121
119
  "optional": true
122
120
  }
121
+ },
122
+ "scripts": {
123
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/fix-extensions.mjs && node scripts/copy-css.mjs && node scripts/build-products.mjs",
124
+ "typecheck": "tsc --noEmit",
125
+ "test": "vitest run"
123
126
  }
124
- }
127
+ }