@ahrowe/ui 0.29.0 → 0.30.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.
Files changed (113) hide show
  1. package/dist/esm/common/accordion/accordion.mjs +1 -1
  2. package/dist/esm/common/accordion/accordion.mjs.map +1 -1
  3. package/dist/esm/common/accordion/accordion.module.mjs.map +1 -1
  4. package/dist/esm/common/accordion/accordion.plain.module.mjs.map +1 -1
  5. package/dist/esm/common/accordion/accordion.primary.module.mjs.map +1 -1
  6. package/dist/esm/common/alert/alert.module.mjs.map +1 -1
  7. package/dist/esm/common/badge/badge.module.mjs.map +1 -1
  8. package/dist/esm/common/bodyEnd/bodyEnd.mjs +1 -1
  9. package/dist/esm/common/bodyEnd/bodyEnd.mjs.map +1 -1
  10. package/dist/esm/common/button/button.module.mjs +1 -1
  11. package/dist/esm/common/button/button.module.mjs.map +1 -1
  12. package/dist/esm/common/card/Card.mjs +1 -1
  13. package/dist/esm/common/card/Card.mjs.map +1 -1
  14. package/dist/esm/common/card/card.module.mjs.map +1 -1
  15. package/dist/esm/common/checkbox/checkbox.mjs +1 -1
  16. package/dist/esm/common/checkbox/checkbox.mjs.map +1 -1
  17. package/dist/esm/common/checkbox/checkbox.module.mjs +1 -1
  18. package/dist/esm/common/checkbox/checkbox.module.mjs.map +1 -1
  19. package/dist/esm/common/chip/chip.mjs +1 -1
  20. package/dist/esm/common/chip/chip.mjs.map +1 -1
  21. package/dist/esm/common/chip/chip.module.mjs.map +1 -1
  22. package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
  23. package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
  24. package/dist/esm/common/colorPicker/colorPicker.module.mjs.map +1 -1
  25. package/dist/esm/common/colorPicker/toHex.mjs +2 -0
  26. package/dist/esm/common/colorPicker/toHex.mjs.map +1 -0
  27. package/dist/esm/common/datePicker/datePicker.mjs +1 -1
  28. package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
  29. package/dist/esm/common/dropZone/dropZone.mjs +1 -1
  30. package/dist/esm/common/dropZone/dropZone.mjs.map +1 -1
  31. package/dist/esm/common/fab/components/fabBase/fabBase.mjs +1 -1
  32. package/dist/esm/common/fab/components/fabBase/fabBase.mjs.map +1 -1
  33. package/dist/esm/common/fab/components/fabBase/fabBase.module.mjs.map +1 -1
  34. package/dist/esm/common/fab/fab.module.mjs.map +1 -1
  35. package/dist/esm/common/flip/flip.mjs +1 -1
  36. package/dist/esm/common/flip/flip.mjs.map +1 -1
  37. package/dist/esm/common/iconPicker/iconPicker.module.mjs.map +1 -1
  38. package/dist/esm/common/input/input.mjs +1 -1
  39. package/dist/esm/common/input/input.mjs.map +1 -1
  40. package/dist/esm/common/input/input.module.mjs +1 -1
  41. package/dist/esm/common/input/input.module.mjs.map +1 -1
  42. package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
  43. package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
  44. package/dist/esm/common/interactableDiv/interactableDiv.mjs +1 -1
  45. package/dist/esm/common/interactableDiv/interactableDiv.mjs.map +1 -1
  46. package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs.map +1 -1
  47. package/dist/esm/common/optionPicker/optionPicker.module.mjs.map +1 -1
  48. package/dist/esm/common/otpInput/otpInput.module.mjs.map +1 -1
  49. package/dist/esm/common/pagination/pagination.module.mjs.map +1 -1
  50. package/dist/esm/common/planCanvas/planCanvas.module.mjs +1 -1
  51. package/dist/esm/common/planCanvas/planCanvas.module.mjs.map +1 -1
  52. package/dist/esm/common/progressBar/progressBar.module.mjs +1 -1
  53. package/dist/esm/common/progressBar/progressBar.module.mjs.map +1 -1
  54. package/dist/esm/common/radioGroup/radioGroup.mjs +1 -1
  55. package/dist/esm/common/radioGroup/radioGroup.mjs.map +1 -1
  56. package/dist/esm/common/radioGroup/radioGroup.module.mjs.map +1 -1
  57. package/dist/esm/common/rating/rating.module.mjs.map +1 -1
  58. package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
  59. package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
  60. package/dist/esm/common/searchInput/searchInput.mjs +1 -1
  61. package/dist/esm/common/searchInput/searchInput.mjs.map +1 -1
  62. package/dist/esm/common/slider/slider.mjs +1 -1
  63. package/dist/esm/common/slider/slider.mjs.map +1 -1
  64. package/dist/esm/common/slider/slider.module.mjs.map +1 -1
  65. package/dist/esm/common/switch/switch.module.mjs.map +1 -1
  66. package/dist/esm/common/tabHeader/tabHeader.module.mjs.map +1 -1
  67. package/dist/esm/common/themeProvider/theme.module.mjs.map +1 -1
  68. package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
  69. package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
  70. package/dist/esm/common/themeProvider/themeVariablesContext.mjs +2 -0
  71. package/dist/esm/common/themeProvider/themeVariablesContext.mjs.map +1 -0
  72. package/dist/esm/common/timeInput/timeInput.mjs +1 -1
  73. package/dist/esm/common/timeInput/timeInput.mjs.map +1 -1
  74. package/dist/esm/common/timeline/timeline.mjs +1 -1
  75. package/dist/esm/common/timeline/timeline.mjs.map +1 -1
  76. package/dist/esm/common/timeline/timeline.module.mjs +1 -1
  77. package/dist/esm/common/timeline/timeline.module.mjs.map +1 -1
  78. package/dist/esm/common/timer/timer.mjs +1 -1
  79. package/dist/esm/common/timer/timer.mjs.map +1 -1
  80. package/dist/esm/common/timer/timer.module.mjs.map +1 -1
  81. package/dist/esm/common/toast/toast.module.mjs.map +1 -1
  82. package/dist/esm/common/tooltip/tooltip.module.mjs.map +1 -1
  83. package/dist/esm/common/utils/activationKey.mjs +2 -0
  84. package/dist/esm/common/utils/activationKey.mjs.map +1 -0
  85. package/dist/index.cjs +3 -3
  86. package/dist/index.cjs.map +1 -1
  87. package/dist/style.css +1 -1
  88. package/dist/types/common/colorPicker/toHex.d.ts +11 -0
  89. package/dist/types/common/themeProvider/theme.types.d.ts +105 -3
  90. package/dist/types/common/themeProvider/themeVariablesContext.d.ts +11 -0
  91. package/dist/types/common/utils/activationKey.d.ts +9 -0
  92. package/docs/Accordion.md +4 -0
  93. package/docs/Alert.md +4 -0
  94. package/docs/BodyEnd.md +3 -0
  95. package/docs/Button.md +6 -0
  96. package/docs/Card.md +6 -0
  97. package/docs/Checkbox.md +8 -0
  98. package/docs/ColorPicker.md +11 -1
  99. package/docs/Fab.md +16 -0
  100. package/docs/Flip.md +1 -1
  101. package/docs/IconPicker.md +8 -0
  102. package/docs/Input.md +12 -0
  103. package/docs/OtpInput.md +4 -0
  104. package/docs/RadioGroup.md +6 -0
  105. package/docs/Rating.md +5 -0
  106. package/docs/RoomDrawer.md +13 -5
  107. package/docs/RoomViewer.md +4 -0
  108. package/docs/Slider.md +6 -2
  109. package/docs/ThemeProvider.md +89 -1
  110. package/docs/Timer.md +7 -1
  111. package/docs/Toast.md +6 -0
  112. package/docs/Tooltip.md +15 -0
  113. package/package.json +2 -1
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Normalises any CSS colour the browser understands into hex, which is the only form
3
+ * ColorPicker's own arithmetic can take. Without it every non-hex value — `transparent`,
4
+ * `rgba(...)`, and above all `oklch(...)`, which a whole theme may be written in — silently
5
+ * became the picker's default blue: a swatch confidently showing a colour nothing asked for.
6
+ *
7
+ * The canvas 2D context is the browser's own colour parser. Under jsdom there is none, so
8
+ * this returns `undefined` and callers keep their previous fallback — meaning the widened
9
+ * behaviour is only observable in a real browser, and only `*.browser.test.ts` can cover it.
10
+ */
11
+ export declare function toHex(value: string | undefined): string | undefined;
@@ -16,6 +16,8 @@ export interface ThemeVariables {
16
16
  '--lighter-blue'?: string;
17
17
  '--black'?: string;
18
18
  '--primary-color'?: string;
19
+ /** Optional gradient layered over --primary-color on large filled surfaces (Button, Chip, ProgressBar, Switch, Slider, Pagination, OptionPicker, Badge). --primary-color stays the solid and stays required: borders, outlines, strokes, shadows and text read it, and a gradient is invalid in all of them. Unset means a flat --primary-color everywhere. */
20
+ '--primary-gradient'?: string;
19
21
  '--primary-accent'?: string;
20
22
  '--primary-lighter'?: string;
21
23
  '--accent-color'?: string;
@@ -33,6 +35,8 @@ export interface ThemeVariables {
33
35
  '--background-accent-dark'?: string;
34
36
  '--background-accent-light'?: string;
35
37
  '--background-modal'?: string;
38
+ /** Pale surface behind a checkbox and similar low-emphasis fills. */
39
+ '--light-background-color'?: string;
36
40
  '--success-color'?: string;
37
41
  '--success-lighter'?: string;
38
42
  '--error-color'?: string;
@@ -51,6 +55,8 @@ export interface ThemeVariables {
51
55
  '--header-font-weight'?: string;
52
56
  '--default-font'?: string;
53
57
  '--themed-font'?: string;
58
+ /** Font for code and stack traces. Falls back to `monospace`. */
59
+ '--monospace-font'?: string;
54
60
  '--disabled-input-color'?: string;
55
61
  '--disabled-font-color'?: string;
56
62
  '--input-background'?: string;
@@ -59,12 +65,16 @@ export interface ThemeVariables {
59
65
  '--input-label-resting-color'?: string;
60
66
  '--input-label-floating-color'?: string;
61
67
  '--search-input-color'?: string;
68
+ /** Colour of an input's error message. */
69
+ '--error-font-color'?: string;
62
70
  '--default-border-radius'?: string;
63
71
  '--padding-sides'?: string;
64
72
  '--margin-top'?: string;
65
73
  '--spacing-s'?: string;
66
74
  '--spacing-m'?: string;
67
75
  '--spacing-l'?: string;
76
+ /** Falls back to 4px. */
77
+ '--spacing-xs'?: string;
68
78
  /** Avatar corner radius. Falls back to --default-border-radius when unset. */
69
79
  '--avatar-radius'?: string;
70
80
  /** Default-style button background. Falls back to transparent when unset. */
@@ -83,9 +93,9 @@ export interface ThemeVariables {
83
93
  '--slider-range-color'?: string;
84
94
  /** Slider thumb colour. Falls back to --primary-color. */
85
95
  '--slider-thumb-color'?: string;
86
- /** Slider thumb diameter. Falls back to 18px (also settable per instance via the `size` prop). */
96
+ /** Slider thumb diameter. Falls back to 1.125x --default-font-size, i.e. 18px at a 16px base (also settable per instance via the `size` prop). */
87
97
  '--slider-thumb-size'?: string;
88
- /** Slider track thickness. Falls back to 6px. */
98
+ /** Slider track thickness. Falls back to 0.375x --default-font-size, i.e. 6px at a 16px base. */
89
99
  '--slider-track-height'?: string;
90
100
  /** Step marker diameter. Falls back to 40px. */
91
101
  '--stepper-marker-size'?: string;
@@ -115,7 +125,7 @@ export interface ThemeVariables {
115
125
  '--timer-progress-color'?: string;
116
126
  /** Timer ring colour once inside the urgent threshold. Falls back to --error-color. */
117
127
  '--timer-urgent-color'?: string;
118
- /** Timer ring diameter. Falls back to 160px (also settable per instance via the `size` prop). */
128
+ /** Timer ring diameter. Falls back to 10em, i.e. 160px at a 16px base (also settable per instance via the `size` prop). */
119
129
  '--timer-size'?: string;
120
130
  /** Timer ring stroke thickness. Falls back to 10px (also settable per instance via `strokeWidth`). */
121
131
  '--timer-stroke-width'?: string;
@@ -123,11 +133,103 @@ export interface ThemeVariables {
123
133
  '--timer-text-color'?: string;
124
134
  /** Timer label (below the readout) text colour. Falls back to --text-dark. */
125
135
  '--timer-label-color'?: string;
136
+ /** Ripple shown as the timer runs out. Falls back to --timer-progress-color. */
137
+ '--timer-ripple-color'?: string;
138
+ /** Tooltip bubble and details badge. Falls back to an inverse of the page: --text-color mixed 82% into --background. */
139
+ '--tooltip-background'?: string;
140
+ /** Text on that bubble. Falls back to --background. */
141
+ '--tooltip-color'?: string;
142
+ /** Size of the checkmark box. Falls back to 1.375em, i.e. 22px at a 16px base. */
143
+ '--checkbox-size'?: string;
144
+ /** Gap between the box and its label. Falls back to 0.5em, i.e. 8px at a 16px base. */
145
+ '--checkbox-gap'?: string;
146
+ /** Size of the inline colour swatch. Falls back to 2em, i.e. 32px at a 16px base. */
147
+ '--swatch-size'?: string;
148
+ /** Diameter of a radio. Falls back to 1.125em, i.e. 18px at a 16px base. */
149
+ '--radio-size'?: string;
150
+ /** Gap between a radio and its label. Falls back to 0.625em, i.e. 10px at a 16px base. */
151
+ '--radio-gap'?: string;
152
+ /** Diameter of the main button. Falls back to 56px. */
153
+ '--fab-size'?: string;
154
+ /** Diameter of a speed-dial entry's button. Falls back to 40px. */
155
+ '--fab-mini-size'?: string;
156
+ /** Icon box inside either. Falls back to 18px, and does not follow --fab-size. */
157
+ '--fab-icon-size'?: string;
158
+ /** Dot colour. Falls back to --primary-color. */
159
+ '--timeline-dot-color'?: string;
160
+ /** Ring drawn around a dot, separating it from the line. Falls back to --background. */
161
+ '--timeline-dot-border'?: string;
162
+ /** Dot diameter. Falls back to 14px. */
163
+ '--timeline-dot-size'?: string;
164
+ /** Diameter of a dot carrying an icon. Falls back to 28px. */
165
+ '--timeline-icon-dot-size'?: string;
166
+ /** Connector colour. Falls back to --border-color. */
167
+ '--timeline-line-color'?: string;
168
+ /** Connector thickness. Falls back to 2px. */
169
+ '--timeline-line-thickness'?: string;
170
+ /** Gap between the dot column and the content. Falls back to 16px. */
171
+ '--timeline-gap'?: string;
172
+ /** Vertical gap between items. Falls back to 20px. */
173
+ '--timeline-item-gap'?: string;
174
+ /** Row height. Falls back to 32px. */
175
+ '--tree-node-height'?: string;
176
+ /** Selected row background. Falls back to --primary-color. */
177
+ '--tree-selected-background'?: string;
178
+ /** Selected row text. Falls back to --text-on-primary. */
179
+ '--tree-selected-color'?: string;
180
+ /** Page button size. Falls back to 36px. */
181
+ '--pagination-size'?: string;
182
+ /** Gap between page buttons. Falls back to 4px. */
183
+ '--pagination-gap'?: string;
184
+ /** Current page background. Falls back to --primary-color. */
185
+ '--pagination-active-background'?: string;
186
+ /** Current page text. Falls back to --text-on-primary. */
187
+ '--pagination-active-color'?: string;
188
+ /** Filled symbol. Falls back to --primary-color. */
189
+ '--rating-filled-color'?: string;
190
+ /** Empty symbol. Falls back to --background-accent-light. */
191
+ '--rating-empty-color'?: string;
192
+ /** Symbol size. Falls back to 1.25x --default-font-size, i.e. 20px at a 16px base. */
193
+ '--rating-size'?: string;
194
+ /** Minimum height of the control. Falls back to `calc(1.25em + 14px)`. */
195
+ '--multi-select-min-height'?: string;
196
+ /** Maximum height of the open option panel. Falls back to 260px. */
197
+ '--multi-select-panel-height'?: string;
126
198
  /** Width a column collapses to once it has no cards (see `collapseEmptyColumns`). Falls back to 140px. */
127
199
  '--kanban-empty-column-width'?: string;
128
200
  '--option-picker-padding'?: string;
129
201
  /** Track (unfilled) background colour. Falls back to --background-accent. */
130
202
  '--progress-bar-track-color'?: string;
203
+ /** Canvas behind the plan. Falls back to --background. */
204
+ '--plan-canvas-background'?: string;
205
+ /** Grid lines, drawn at low opacity. Falls back to --text-color. */
206
+ '--plan-grid-color'?: string;
207
+ /** The storey below, traced under the current one. Falls back to --text-dark. */
208
+ '--plan-underlay-color'?: string;
209
+ /** Room floor. Falls back to --background-accent. */
210
+ '--plan-room-fill'?: string;
211
+ /** Room floor under the pointer. Falls back to --background-accent-light. */
212
+ '--plan-room-fill-hover'?: string;
213
+ /** Edge of the walls bounding a selected room. Falls back to --primary-color. */
214
+ '--plan-room-edge-selected'?: string;
215
+ /** Body of those same walls. Falls back to --primary-lighter. */
216
+ '--plan-room-edge-selected-fill'?: string;
217
+ /** Wall body. Falls back to --text-color mixed 58% into the canvas. */
218
+ '--plan-wall-fill'?: string;
219
+ /** Wall edge, its caps and the jamb faces. Falls back to --text-color mixed 38% in. */
220
+ '--plan-wall-outline'?: string;
221
+ /** Wall corner treatment, any `stroke-linejoin` value. Falls back to `round`. */
222
+ '--plan-wall-join'?: string;
223
+ /** How far a mitred corner may run, when --plan-wall-join is `miter`. Falls back to `4`. */
224
+ '--plan-wall-miter-limit'?: string;
225
+ /** Door leaf and swing arc. Falls back to --text-dark. */
226
+ '--plan-door-color'?: string;
227
+ /** Window pane line. Falls back to --info-color. */
228
+ '--plan-window-color'?: string;
229
+ /** RoomDrawer's snap construction lines. Falls back to --info-color. */
230
+ '--plan-guide-color'?: string;
231
+ /** RoomDrawer's ruler. Falls back to --primary-color. */
232
+ '--plan-measure-color'?: string;
131
233
  [key: string]: string | undefined;
132
234
  }
133
235
  export interface Theme {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The nearest ThemeProvider's resolved variables, for content that renders outside its DOM
3
+ * subtree. Theme variables reach a component by CSS inheritance, which a portal breaks: a
4
+ * `BodyEnd` child lands wherever `#bodyEnd` happens to be, so it picks up whatever provider
5
+ * wraps *that* node rather than the one it was written under.
6
+ *
7
+ * Deliberately its own module rather than a member of `themeProvider.tsx`: BodyEnd needs the
8
+ * context and nothing else, and importing the component for it would pull the whole
9
+ * ThemeProvider into every bundle that uses a portal.
10
+ */
11
+ export declare const ThemeVariablesContext: import('react').Context<Record<string, string> | null>;
@@ -0,0 +1,9 @@
1
+ import { default as React } from 'react';
2
+ /**
3
+ * Whether a keydown is a click-equivalent activation of the element the handler sits on.
4
+ *
5
+ * The target check is the important half: keydown bubbles, so without it a Space typed into a
6
+ * nested input activates the container and, once the container calls `preventDefault()`, never
7
+ * reaches the input at all.
8
+ */
9
+ export declare function isSelfActivation(event: React.KeyboardEvent): boolean;
package/docs/Accordion.md CHANGED
@@ -41,3 +41,7 @@ import { Accordion, AccordionStyleType } from '@ahrowe/ui';
41
41
  | `id` | `string` | Passed back in `onHeaderClicked` |
42
42
 
43
43
  **Slots:** `root` `header` `title` `icon` `body` `bodyInner`
44
+
45
+ ## Sizing
46
+
47
+ The header's font, padding and chevron follow `--default-font-size`, as does the body's padding. There is no fixed height: the header is that padding plus one line of text, so a larger base font simply makes it taller.
package/docs/Alert.md CHANGED
@@ -64,3 +64,7 @@ import { faRocket } from '@fortawesome/free-solid-svg-icons';
64
64
  **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Alert: { hideIcon: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
65
65
 
66
66
  **Slots:** `root` `icon` `content` `title` `description` `closeButton`
67
+
68
+ ## Sizing
69
+
70
+ Every length in an alert is `em`, so the padding, the icon, the close button and the accent bar keep their ratios to the body copy at any base font.
package/docs/BodyEnd.md CHANGED
@@ -30,3 +30,6 @@ import { BodyEnd } from '@ahrowe/ui';
30
30
  | `children` | `ReactNode` | Content rendered into `#bodyEnd` |
31
31
 
32
32
  **Note:** Returns `null` silently if `#bodyEnd` doesn't exist in the DOM.
33
+
34
+ **Theming crosses the portal.** Children keep the variables of the nearest `ThemeProvider` they were *written* under, not the one that happens to wrap `#bodyEnd`. BodyEnd applies them through a `display: contents` wrapper, which generates no box — positioning, stacking and clipping are unchanged — while custom properties still inherit. Variables that theme leaves unset fall through to whatever wraps `#bodyEnd`, the same cascade the children would have seen rendered in place. With no `ThemeProvider` above, no wrapper is added at all.
35
+
package/docs/Button.md CHANGED
@@ -94,3 +94,9 @@ const theme: Theme = {
94
94
 
95
95
  **Sizing in a flex row:** a Button never shrinks below its own content (`min-width: min-content`),
96
96
  so an icon is never cut off by a flexible sibling. A multi-word label still wraps above that floor.
97
+
98
+ ## Sizing
99
+
100
+ A Button is sized by its font. `--default-font-size` is the base, and `Small` and `Large` are multiples of it (0.75x and 1.25x), so a theme with a smaller base font gets proportionally smaller controls everywhere. Padding and the icon gap are in `em`, so the whole box grows with the label rather than a large button wearing a default button's padding.
101
+
102
+ `ButtonSize.Icon` is the exception: it inherits the surrounding text size and has no padding at all, because the caller sets the box.
package/docs/Card.md CHANGED
@@ -76,3 +76,9 @@ All sub-components are optional — use only what you need. Stacked sub-componen
76
76
  |------|------|-------------|
77
77
  | `actions` | `CardAction[]` | Array of `{ id, label, styleType, onClick }` renders Buttons |
78
78
  | `children` | `ReactNode` | Custom content instead of `actions` |
79
+
80
+ ## Sizing
81
+
82
+ Card is spaced by its font: the padding, the header's gap and thumbnail, the gap between title and subtitle, and the action row all scale with the theme's text size. The title is `1.125em` — 18px at a 16px base — rather than a fixed size, so it stays proportional to the body text beneath it.
83
+
84
+ `CardMedia` bleeds to the card's edges with a negative margin that has to stay equal to the padding, so change the two together.
package/docs/Checkbox.md CHANGED
@@ -38,3 +38,11 @@ const agreedValidator = new FormValidator(false, [Validators.required()]);
38
38
  | `cursorDefault` | `boolean` | Use default cursor instead of pointer |
39
39
 
40
40
  **Slots:** `icon` `checkmark` `label`
41
+
42
+ ## Sizing
43
+
44
+ The control is sized by its font, so it stays in proportion with the label it sits next to. `--checkbox-size` overrides the box and `--checkbox-gap` the space to the label; they fall back to `1.375em` and `0.5em`, i.e. 22px and 8px at a 16px base. The border and the tick's padding are `em` too, so nothing has to be retuned when the box changes.
45
+
46
+ **Checked and unchecked are the same size.** The fill is the box's own background rather than an oversized tick laid over it, so the two states occupy identical space. `background` sets that fill, and applies to the border as well.
47
+
48
+ The `width` and `height` props still win where they are given, for a checkbox that must match a fixed layout rather than the text around it.
@@ -34,9 +34,19 @@ import { ColorPicker } from '@ahrowe/ui';
34
34
 
35
35
  | Prop | Type | Description |
36
36
  |------|------|-------------|
37
- | `value` | `string` | Current hex value (e.g. `'#ff0000'`) |
37
+ | `value` | `string` | Current colour. Any CSS colour the browser understands — `'#ff0000'`, `'#f00'`, `'red'`, `'rgb(255 0 0)'`, `'oklch(0.8 0.11 85)'`, `'transparent'` — normalised to hex on the way in |
38
38
  | `onChange` | `(hex: string) => void` | Called with new hex on change |
39
39
  | `showInput` | `boolean` | Show hex text input |
40
40
  | `inline` | `boolean` | Render picker inline instead of as a popover |
41
41
 
42
42
  An eyedropper button is shown automatically next to the hex input on browsers that support the native `EyeDropper` API (Chromium-based desktop/Android browsers). It lets the user sample a colour from anywhere on screen, not just within the page. It's hidden entirely on unsupported browsers (Firefox, Safari, iOS) — no prop needed.
43
+
44
+ **Anything in, hex out.** `value` takes any CSS colour and `onChange` always emits hex (`#rrggbb`, or `#rrggbbaa` when the colour is not fully opaque), so a round trip through the picker normalises the notation. A value the browser cannot parse at all falls back to the component's default colour rather than throwing.
45
+
46
+ Parsing uses the browser's own colour engine, which means it needs a DOM with a canvas. Under jsdom every non-hex value falls back to the default instead — worth knowing when a test asserts on a colour that went in as `rgb()`.
47
+
48
+ ## Sizing
49
+
50
+ The inline swatch tracks the theme's text size, since it sits beside a label: `--swatch-size` falls back to `2em`, i.e. 32px at a 16px base, and the gap, the hex overlay and the transparency checkerboard scale with it.
51
+
52
+ The picker panel deliberately does not. It is a floating control surface with its own fixed layout — a saturation area, sliders and a hex field — and it holds together at a large base font rather than needing to grow with it.
package/docs/Fab.md CHANGED
@@ -41,3 +41,19 @@ import { faPlus, faPen, faCamera } from '@fortawesome/free-solid-svg-icons';
41
41
  | `closeMenuOnEntryClicked` | `boolean` | Auto-close menu after selection |
42
42
  | `onClick` | `() => void` | Simple click handler (no speed dial) |
43
43
  | `children` | `ReactNode` | FAB icon content |
44
+
45
+ ## Sizing
46
+
47
+ | Variable | Falls back to |
48
+ |----------|---------------|
49
+ | `--fab-size` | `56px` |
50
+ | `--fab-mini-size` | `40px` |
51
+ | `--fab-icon-size` | `18px` |
52
+
53
+ **Deliberately not tied to `--default-font-size`**, unlike Button. A FAB is a touch target holding an icon: its size is set by fingers rather than by reading comfort, it contains no text to stay in proportion with, and it floats over the page — so growing it with the text scale would only occlude more content. Couple them yourself if that is what you want:
54
+
55
+ ```css
56
+ --fab-size: calc(var(--default-font-size) * 3.5);
57
+ ```
58
+
59
+ `--fab-icon-size` is independent of `--fab-size`, so enlarging the button does not enlarge the glyph; set both. It applies to the speed-dial entries as well as the main button.
package/docs/Flip.md CHANGED
@@ -67,7 +67,7 @@ import { Tilt, Flip } from '@ahrowe/ui';
67
67
 
68
68
  **Motion:** the flip always animates over `flipDuration`, whether triggered by a click, a controlled prop change, or the ref API. Under `prefers-reduced-motion: reduce`, it flips instantly instead. `onFlipEnd` fires once the flip settles on its new face — after the transition completes normally, or synchronously under reduced motion, since no transition runs to wait for.
69
69
 
70
- **Keyboard accessibility:** when `flipOnClick` is set, the root gets `role="button"`, `tabIndex={0}`, and `aria-pressed` reflecting the current flip state, and Enter/Space both toggle the flip the same way a click does. None of this is applied when `flipOnClick` is `false`, since the element isn't interactive.
70
+ **Keyboard accessibility:** when `flipOnClick` is set, the root gets `role="button"`, `tabIndex={0}`, and `aria-pressed` reflecting the current flip state, and Enter/Space both toggle the flip the same way a click does, as long as the key was pressed on the root itself so a nested input or button keeps its own keys. None of this is applied when `flipOnClick` is `false`, since the element isn't interactive.
71
71
 
72
72
  **Key props:**
73
73
 
@@ -57,3 +57,11 @@ interface IconPickerItem {
57
57
  | `isRequired` | `boolean` | |
58
58
 
59
59
  **Slots:** `root` `label` `itemContainer` `item`
60
+
61
+ ## Selected state
62
+
63
+ A selected icon sits on `--primary-color` and draws itself in `--text-on-primary`. It used to inherit the page's `--text-color`, which meant the same selected state came out white on a dark theme and near-black on a light one.
64
+
65
+ ## Sizing
66
+
67
+ Each icon's circular background is `1.5em`, so it stays proportional to the `1em` glyph inside it, as do the padding and the grid gaps.
package/docs/Input.md CHANGED
@@ -124,3 +124,15 @@ The stored value is the formatted one, spaces included, so strip them before a l
124
124
  | `customInput` | `ReactElement` | Element rendered in place of the internal `<input>`, cloned with all of Input's props. The escape hatch for a mask library; note `autoFocus` and `autoResize` stop working, since Input cannot attach its ref |
125
125
 
126
126
  **Slots:** `label` `container` `input` `suffix` `upperRightLabel` `fieldset`
127
+
128
+ ## Browser autofill
129
+
130
+ `autoComplete` is the plain HTML attribute and is passed straight through — use `'one-time-code'`, `'new-password'`, `'street-address'` and the rest as you would on a bare `<input>`. Input does **not** set it: autofill and password managers are usability and accessibility features, and a plain field has no suggestion list of its own for them to collide with.
131
+
132
+ The combobox components — `InputDropdown`, `SearchInput`, `MultiSelect`, `DatePicker`, `TimeInput` — always set `autocomplete="off"` and do not take the prop, because the browser's dropdown would otherwise open on top of the list they render themselves.
133
+
134
+ **The floating label knows about autofill.** Chrome fills a field on page load by writing the DOM value directly, without firing anything React can observe — so the component's value stayed empty and the label sat on top of the autofilled text. Input detects it through a no-op animation bound to `:-webkit-autofill`, whose `animationstart` is the only notification the browser gives, and floats the label from that as well as from its value.
135
+
136
+ **Autofilled fields stay on the theme.** Chrome paints `:-webkit-autofill` with its own background and text colour, overriding `background` and `color` outright, which turned a filled field pale blue with near-black text on a dark theme. Input clips that background to the text instead of covering it: the page cannot win on colour, but it can say where a background paints, and `-webkit-text-fill-color` then beats the text colour. The field itself is transparent, so what shows through is whatever is genuinely behind it — inside a Modal, a Card, or a surface of your own — and nothing has to be told what that is.
137
+
138
+ A custom `--input-background` is covered by the same mechanism, because the container paints it and the transparent field reveals it. Setting a background on the field itself with the `background` shorthand — `styles={{ input: { background: '…' } }}` — resets `background-clip` along with it, and an autofilled field there falls back to Chrome's own colours; `background-color` does not.
package/docs/OtpInput.md CHANGED
@@ -90,3 +90,7 @@ const codeValidator = new FormValidator('', [Validators.required(), Validators.m
90
90
  | `autoFocus` | `boolean` | Focuses the first empty box (or the first box) on mount |
91
91
 
92
92
  **Slots:** `root` `label` `inputs` `input` `separator`
93
+
94
+ ## Sizing
95
+
96
+ The code boxes, the digit inside them, the gaps and the separator all scale with the theme's text, so the field stays in proportion with its label.
@@ -76,3 +76,9 @@ interface RadioOption {
76
76
  **Global defaults:** adopts `ConfigProvider` — e.g. `defaultProps={{ RadioGroup: { orientation: RadioGroupOrientation.Horizontal } }}`. See [ConfigProvider.md](ConfigProvider.md).
77
77
 
78
78
  **Slots:** `root` `option` `radio` `dot` `label` `description`
79
+
80
+ ## Sizing
81
+
82
+ The group is sized by its font, so the radios, the spacing between options and the gap to each label all scale with the text. `--radio-size` and `--radio-gap` override the circle and the label spacing; they fall back to `1.125em` and `0.625em`, i.e. 18px and 10px at a 16px base. The dot is always half the circle.
83
+
84
+ The `size` prop still wins where it is given, for a radio that must match a fixed layout rather than the text around it.
package/docs/Rating.md CHANGED
@@ -66,7 +66,12 @@ import { faHeart } from '@fortawesome/free-solid-svg-icons';
66
66
  |----------|---------------|
67
67
  | `--rating-filled-color` | `var(--primary-color)` |
68
68
  | `--rating-empty-color` | `var(--background-accent-light)` |
69
+ | `--rating-size` | `1.25x --default-font-size`, i.e. 20px at a 16px base |
69
70
 
70
71
  **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Rating: { allowHalf: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
71
72
 
72
73
  **Slots:** `root` `item` `iconEmpty` `iconFilled`
74
+
75
+ ## Sizing
76
+
77
+ The symbols, the gap between them and the partial-fill clip are all `em` against `--rating-size`, so setting it alone resizes the whole control.
@@ -323,26 +323,34 @@ ref.current?.loadJson(await file.text());
323
323
 
324
324
  ## Theming
325
325
 
326
- All colours come from theme variables. These control the plan itself, and each falls back to a general theme colour when unset:
326
+ All colours come from theme variables. These control the plan itself, and each falls back to a general theme colour when unset. They are declared on `ThemeVariables`, so a `Theme` gets them with autocomplete:
327
327
 
328
328
  | Variable | Falls back to |
329
329
  |----------|---------------|
330
330
  | `--plan-canvas-background` | `var(--background)` |
331
331
  | `--plan-grid-color` | `var(--text-color)` at low opacity |
332
+ | `--plan-underlay-color` | `var(--text-dark)` at low opacity |
332
333
  | `--plan-room-fill` | `var(--background-accent)` |
333
334
  | `--plan-room-fill-hover` | `var(--background-accent-light)` |
334
335
  | `--plan-room-edge-selected` / `--plan-room-edge-selected-fill` | `var(--primary-color)` / `var(--primary-lighter)` |
335
- | `--plan-wall-fill` | `var(--text-dark)` |
336
- | `--plan-wall-outline` | `var(--text-color)` |
336
+ | `--plan-wall-fill` | `var(--text-color)` mixed 58% into the canvas |
337
+ | `--plan-wall-outline` | `var(--text-color)` mixed 38% into the canvas |
338
+ | `--plan-wall-join` | `round` (see below) |
337
339
  | `--plan-wall-miter-limit` | `4` (see below) |
338
340
  | `--plan-door-color` | `var(--text-dark)` |
339
341
  | `--plan-window-color` | `var(--info-color)` |
342
+ | `--plan-guide-color` | `var(--info-color)` |
343
+ | `--plan-measure-color` | `var(--primary-color)` |
340
344
 
341
- **`--plan-wall-miter-limit`** is how sharp a corner may get before it is cut flat rather than run to a point. A mitred join extends to `1 / sin(angle / 2)` times the wall thickness, so it grows without bound as the angle closes: at 10 degrees that is eleven times the wall's own thickness, which reads as a spike fired out of the corner. The default of `4` keeps a true mitred point down to about 29 degrees and caps it at twice the wall's own thickness; sharper than that the corner is cut flat instead. Acute rooms bottom out around 45 degrees in practice and anything under 30 is not a room, so the corners that keep their point are the ones a building actually has. Raise it if you are drawing something genuinely needle-shaped and want the point kept. It governs the corners at a junction too, on the rare occasion one needs filling: where three or more walls meet, the corners between them are normally closed already, except when every wall at the node points into the same half-plane.
345
+ **Walls are drawn as mass, not as ink.** Both wall tones are mixed towards the canvas rather than set to the text colour, so a wall reads as a solid body sitting on the floor and its edge as a soft rim rather than a drafted hairline. The pair must stay two distinct values: the outline pass is only visible where the fill pass does not cover it. The jamb faces at an opening take the *body* tone, since at a doorway they are the only thing standing in for the wall.
346
+
347
+ **`--plan-wall-join`** is the corner treatment, `round` by default: the outer corners of the building come out softened, and an acute corner cannot throw a mitre spike out of the junction at all. Set it to `miter` for the drafting corner, and `--plan-wall-miter-limit` then governs how far that spike may run. The selection highlight always uses whichever join the walls use, because it is drawn over the same geometry and a different join would leave crescents of wall showing round the corners.
348
+
349
+ **`--plan-wall-miter-limit`** applies when `--plan-wall-join` is `miter`. It is how sharp a corner may get before it is cut flat rather than run to a point. A mitred join extends to `1 / sin(angle / 2)` times the wall thickness, so it grows without bound as the angle closes: at 10 degrees that is eleven times the wall's own thickness, which reads as a spike fired out of the corner. The default of `4` keeps a true mitred point down to about 29 degrees and caps it at twice the wall's own thickness; sharper than that the corner is cut flat instead. Acute rooms bottom out around 45 degrees in practice and anything under 30 is not a room, so the corners that keep their point are the ones a building actually has. Raise it if you are drawing something genuinely needle-shaped and want the point kept. It governs the corners at a junction too, on the rare occasion one needs filling: where three or more walls meet, the corners between them are normally closed already, except when every wall at the node points into the same half-plane.
342
350
 
343
351
  **Hover tints the room's floor; selection colours the walls that bound it.** They answer different questions, so they get different channels and can be read at the same time without being confused. Hover is fleeting and asks *which room is under the pointer*, where a faint fill is instant and unambiguous — a wall is not, being shared between the rooms on either side of it. Selection is state you then work inside, where a wash over the room would bury its doors and its label.
344
352
 
345
- The selection highlight is built from **the same geometry as the walls themselves** and drawn in the same two passes, so it inherits their mitred corners and their gaps at openings: a door in the boundary stays a doorway rather than being painted over, and the wall keeps a crisp edge instead of turning into a flat slab. Where two selected rooms share a wall it is drawn once, so it never comes out twice as strong as its neighbours. The room fill carries `data-state="selected" | "hovered"` if you want to style either state further.
353
+ The selection highlight is built from **the same geometry as the walls themselves** and drawn in the same two passes, so it inherits their corners and their gaps at openings: a door in the boundary stays a doorway rather than being painted over, and the wall keeps a crisp edge instead of turning into a flat slab. Where two selected rooms share a wall it is drawn once, so it never comes out twice as strong as its neighbours. The room fill carries `data-state="selected" | "hovered"` if you want to style either state further.
346
354
 
347
355
  ## Accessibility
348
356
 
@@ -54,6 +54,10 @@ A plan holds a stack of storeys and the viewer shows one at a time, with a tab p
54
54
 
55
55
  Drag to pan, wheel or pinch to zoom, and the controls in the corner zoom and fit. The plan fits itself to the viewport on mount, and refits when the floor or the canvas size changes — **until the viewer pans or zooms**, after which the view is theirs and the component stops moving it. `pannable={false}` freezes it.
56
56
 
57
+ ## Theming the plan
58
+
59
+ Walls are drawn as mass rather than as ink: the body is `var(--text-color)` mixed 58% into the canvas, its edge the same colour at 38%, and the corners are rounded. Override with `--plan-wall-fill`, `--plan-wall-outline` and `--plan-wall-join: miter`. The full list of plan variables is in [RoomDrawer.md](RoomDrawer.md#theming), and both components read the same ones.
60
+
57
61
  ## Key props
58
62
 
59
63
  | Prop | Type | Description |
package/docs/Slider.md CHANGED
@@ -92,8 +92,8 @@ const [value, setValue] = useState(40);
92
92
  | `--slider-track-color` | `var(--background-accent-light)` |
93
93
  | `--slider-range-color` | `var(--primary-color)` |
94
94
  | `--slider-thumb-color` | `var(--primary-color)` |
95
- | `--slider-thumb-size` | `18px` (also settable per instance via `size`) |
96
- | `--slider-track-height` | `6px` |
95
+ | `--slider-thumb-size` | `1.125x --default-font-size`, i.e. 18px at a 16px base (also settable per instance via `size`) |
96
+ | `--slider-track-height` | `0.375x --default-font-size`, i.e. 6px at a 16px base |
97
97
 
98
98
  **Range mode:** set `range` for two thumbs and `[from, to]` pairs.
99
99
 
@@ -123,3 +123,7 @@ different things that happen to share a word. The slot means the same in both mo
123
123
  announces the limit the thumb actually has. Both thumbs are tabbable and take the same keys.
124
124
 
125
125
  **Slots:** `root` `track` `range` `thumb` `mark` `markLabel` `value`
126
+
127
+ ## Sizing
128
+
129
+ The thumb and track scale with the theme's text, so the control stays in proportion with the value and mark labels beside it — those are sized from `--text-small-font-size` and already grew. `--slider-thumb-size` and `--slider-track-height` override them, and the `size` prop still wins where it is given.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **When to use:** Required wrapper for the entire component library. Injects all CSS custom properties as inline styles so every component inside can use theme variables. Place it at or near the root of your app.
4
4
 
5
- **Keywords:** theming, dark mode
5
+ **Keywords:** theming, dark mode, gradient
6
6
 
7
7
  **Import:** `import { ThemeProvider } from '@ahrowe/ui'`
8
8
  **Types:** `import type { Theme, ThemeVariables } from '@ahrowe/ui'`
@@ -65,6 +65,7 @@ interface Theme {
65
65
  | Variable | Purpose |
66
66
  |----------|---------|
67
67
  | `--primary-color` / `--primary-lighter` / `--primary-accent` | Brand colours |
68
+ | `--primary-gradient` | Optional gradient layered over `--primary-color` on large filled surfaces (see [Gradient primary](#gradient-primary)) |
68
69
  | `--background` | Page/surface background |
69
70
  | `--background-accent` / `--background-accent-light` | Raised surfaces, cards |
70
71
  | `--text-color` / `--text-dark` | Primary and secondary text |
@@ -89,3 +90,90 @@ interface Theme {
89
90
  **Note:** Without `ThemeProvider`, all CSS variables are undefined and components will be unstyled.
90
91
 
91
92
  **Base typography:** The wrapper also sets `font-family`, `font-size`, `font-weight`, and `color` from the theme's `--default-font`, `--default-font-size`, `--default-font-weight`, and `--text-color`, so everything inside — including content portaled into a `#bodyEnd` placed within the provider — inherits the theme's base text style without each component having to set it.
93
+
94
+ ## Gradient primary
95
+
96
+ Set `--primary-gradient` next to `--primary-color` to give the brand a gradient:
97
+
98
+ ```ts
99
+ const brandTheme: Theme = {
100
+ id: 'brand',
101
+ variables: {
102
+ '--primary-color': '#8a4bd6',
103
+ '--primary-gradient': 'linear-gradient(135deg, #6d4bd6 0%, #a24bd6 55%, #4b7ed6 100%)',
104
+ '--text-on-primary': '#ffffff',
105
+ },
106
+ };
107
+ ```
108
+
109
+ `--primary-color` stays required and stays a solid colour. A CSS gradient is an image, not a colour,
110
+ so it is invalid in every property that expects one: borders, outlines, SVG strokes, shadows,
111
+ `color`, `color-mix()`. Putting a gradient into `--primary-color` would break those silently, with no
112
+ error and no way to feature-detect it. `--primary-gradient` is therefore additive: it paints over the
113
+ solid on filled surfaces large enough to show a ramp, and everything else keeps the solid.
114
+
115
+ Surfaces that honour it:
116
+
117
+ | Component | Surface |
118
+ |-----------|---------|
119
+ | `Button` | primary style |
120
+ | `Chip` | primary style |
121
+ | `ProgressBar` | fill |
122
+ | `Switch` | checked track |
123
+ | `Slider` | filled range |
124
+ | `Pagination` | active page |
125
+ | `OptionPicker` | selected option |
126
+ | `Badge` | primary style, except the `dot` variant |
127
+ | `Alert` | default-style accent border |
128
+ | `Toast` | default-style accent border |
129
+ | `TabHeader` | active tab indicator |
130
+
131
+ Everything else stays flat: focus rings, outlines, icons, primary-coloured text, and small marks
132
+ such as the `Timeline` dot, the `Checkbox` tick and the `RadioGroup` dot. Focus rings in particular
133
+ stay a solid `--focus-border-color`, because an indicator needs one predictable contrast ratio
134
+ against whatever it sits on, not a ramp that is only high-contrast at one end.
135
+
136
+ Pick a `--primary-color` that reads as the same brand next to the gradient, since the two sit side
137
+ by side on a component such as an active `Pagination` item, which has a gradient fill inside a solid
138
+ border.
139
+
140
+ Two things to check when choosing the stops:
141
+
142
+ - **Contrast.** `--text-on-primary` is one colour sitting on the whole ramp, so keep the stops within
143
+ a similar lightness. A gradient running light to dark fails contrast at one end.
144
+ - **Element-sized ramps.** Each surface paints its own full gradient, so a wide `Button` and a narrow
145
+ one show different slices, and a `ProgressBar` ramp shifts as the value grows.
146
+ - **Seams in grouped controls.** Where two primary buttons sit against each other, in a `ButtonGroup`
147
+ or a `SplitButton`, each segment paints a full ramp, so the end of one meets the start of the next
148
+ at the seam. A gradient with a vertical axis has no seam to show, since both segments are the same
149
+ height and stretch together:
150
+
151
+ ```
152
+ linear-gradient(180deg, #8d5ce8 0%, #5a37b8 100%) /* one control, no seam */
153
+ linear-gradient(90deg, #6d4bd6 0%, #4b7ed6 100%) /* visible seam at each segment boundary */
154
+ ```
155
+
156
+ If you want a horizontal or diagonal ramp anyway, flatten the grouped case on its own:
157
+
158
+ ```css
159
+ .myToolbar [role='group'] {
160
+ --primary-gradient: none;
161
+ }
162
+ ```
163
+
164
+ To turn the gradient off for part of the app, or for a component whose fill you have overridden
165
+ through its own variable (`--slider-range-color`, `--pagination-active-background`), set it back to
166
+ `none` on that subtree:
167
+
168
+ ```css
169
+ .myPanel {
170
+ --primary-gradient: none;
171
+ }
172
+ ```
173
+
174
+ ## Nested providers and portals
175
+
176
+ Providers nest: an inner one overrides the variables it declares and inherits the rest, so a differently-themed panel inside a themed app works without restating the whole theme.
177
+
178
+ A portal breaks that by default, because theme variables travel by CSS inheritance and a portal escapes the subtree. Anything rendered through `BodyEnd` — `Tooltip`, `Dropdown`, `InputDropdown`, `ColorPicker`, `FloatingMenu`, `Popover`, `Modal`, `Toast` — therefore carries the nearest provider's variables with it, so an open panel matches the panel that opened it rather than whatever wraps `#bodyEnd`.
179
+
package/docs/Timer.md CHANGED
@@ -131,7 +131,7 @@ function Example() {
131
131
  | `--timer-progress-color` | `var(--primary-color)` |
132
132
  | `--timer-urgent-color` | `var(--error-color)` |
133
133
  | `--timer-ripple-color` | `var(--timer-progress-color)` (which itself falls back to `var(--primary-color)`) |
134
- | `--timer-size` | `160px` (also settable per instance via `size`) |
134
+ | `--timer-size` | `10em`, i.e. 160px at a 16px base (also settable per instance via `size`) |
135
135
  | `--timer-stroke-width` | `10px` (also settable per instance via `strokeWidth`) |
136
136
  | `--timer-text-color` | `var(--text-color)` |
137
137
  | `--timer-label-color` | `var(--text-dark)` |
@@ -139,3 +139,9 @@ function Example() {
139
139
  **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Timer: { adjustSeconds: 60, playSoundOnComplete: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
140
140
 
141
141
  **Slots:** `root` `ringWrapper` `ringSvg` `ringTrack` `ringProgress` `ripples` `ripple` `display` `time` `label` `controls` `addButton` `removeButton` `pauseButton` `cancelButton`
142
+
143
+ ## Sizing
144
+
145
+ The ring follows the theme's text size, because the readout inside it does: `--timer-size` falls back to `10em`, i.e. 160px at a 16px base, and the gaps and padding around it are `em` too. The `size` prop still wins where it is given.
146
+
147
+ `--timer-stroke-width` is the exception and stays in viewBox units. It already scales with the ring, and it has to match the `strokeWidth` prop, which the component subtracts from the circle's radius so the stroke fits inside the viewBox.
package/docs/Toast.md CHANGED
@@ -74,3 +74,9 @@ showToast('Done', {
74
74
  **ToastType:** `'default'` `'info'` `'success'` `'error'` `'warn'`
75
75
 
76
76
  **Slots:** `root` `icon` `content` `closeButton` `progressBar`
77
+
78
+ ## Sizing
79
+
80
+ A toast is sized and spaced by its font: the padding, the gap, the body copy, the icon, the close button, the variant accent bar and the progress bar all scale with the theme's text, as does the gap between stacked toasts.
81
+
82
+ Its `min-width` and `max-width` are `em` rather than pixels, so they bound the *measure* — roughly how many characters fit on a line — instead of a fixed width that would grow cramped as the text got larger.
package/docs/Tooltip.md CHANGED
@@ -41,3 +41,18 @@ import { Tooltip } from '@ahrowe/ui';
41
41
  | `onClick` | `() => void` | Click handler on the icon |
42
42
 
43
43
  The bubble is rendered through a portal and tracks its trigger's position directly, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists) — no `position: relative` wrapper is required. It flips to the opposite side automatically when there's no room in its preferred direction, and hides (without losing state) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view.
44
+
45
+ **Theming:** these are theme variables (typed on `ThemeVariables`). Set them theme-wide via `ThemeProvider`'s `variables`, or per instance via `style`, without fighting specificity. Each falls back to a built-in default when unset:
46
+
47
+ | Variable | Falls back to |
48
+ |----------|---------------|
49
+ | `--tooltip-background` | `var(--text-color)` mixed 82% into `var(--background)` |
50
+ | `--tooltip-color` | `var(--background)` |
51
+
52
+ The default bubble is an **inverse of the page** rather than a fixed colour: it comes out dark on a light theme and light on a dark one, so it always separates from whatever it floats over. Both the arrow and the `details` badge follow `--tooltip-background`, so overriding it recolours the whole tooltip. The `error` variant is styled from the semantic error colours (`--error-lighter`, `--error-color`, `--error-border-color`) and ignores these two.
53
+
54
+ ## Sizing
55
+
56
+ The bubble, its arrow, the `details` badge and the error variant all scale with the theme's text. The bubble's `max-width` is `em`, so it bounds a line measure rather than a pixel count as the text grows.
57
+
58
+ The badge derives its size from `--default-font-size` directly rather than from `em`. It renders inline wherever you put it, so an `em` would measure the surrounding text instead of the theme; the bubble can use `em` because it is portaled to `#bodyEnd` and always lands on the theme's own size.