@womp/kookie-ui 0.22.0 → 0.23.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 (127) hide show
  1. package/components.css +57 -2
  2. package/dist/cjs/components/_internal/base-button.d.ts.map +1 -1
  3. package/dist/cjs/components/_internal/base-button.js +1 -1
  4. package/dist/cjs/components/_internal/base-button.js.map +4 -4
  5. package/dist/cjs/components/button.js +1 -1
  6. package/dist/cjs/components/button.js.map +4 -4
  7. package/dist/cjs/components/chatbar.js +1 -1
  8. package/dist/cjs/components/chatbar.js.map +4 -4
  9. package/dist/cjs/components/image.d.ts +23 -0
  10. package/dist/cjs/components/image.d.ts.map +1 -0
  11. package/dist/cjs/components/image.js +2 -0
  12. package/dist/cjs/components/image.js.map +7 -0
  13. package/dist/cjs/components/image.props.d.ts +19 -0
  14. package/dist/cjs/components/image.props.d.ts.map +1 -0
  15. package/dist/cjs/components/image.props.js +2 -0
  16. package/dist/cjs/components/image.props.js.map +7 -0
  17. package/dist/cjs/components/index.d.ts +1 -0
  18. package/dist/cjs/components/index.d.ts.map +1 -1
  19. package/dist/cjs/components/index.js +3 -3
  20. package/dist/cjs/components/index.js.map +4 -4
  21. package/dist/cjs/components/message-scroller.js +1 -1
  22. package/dist/cjs/components/message-scroller.js.map +4 -4
  23. package/dist/cjs/components/notice.js +1 -1
  24. package/dist/cjs/components/notice.js.map +4 -4
  25. package/dist/cjs/components/select.d.ts +1 -2
  26. package/dist/cjs/components/select.d.ts.map +1 -1
  27. package/dist/cjs/components/select.js +1 -1
  28. package/dist/cjs/components/select.js.map +3 -3
  29. package/dist/cjs/components/slider.d.ts.map +1 -1
  30. package/dist/cjs/components/slider.js +1 -1
  31. package/dist/cjs/components/slider.js.map +2 -2
  32. package/dist/cjs/components/spinner.d.ts.map +1 -1
  33. package/dist/cjs/components/spinner.js +1 -1
  34. package/dist/cjs/components/spinner.js.map +2 -2
  35. package/dist/cjs/components/toggle.js +2 -2
  36. package/dist/cjs/components/toggle.js.map +4 -4
  37. package/dist/cjs/components/toolbar.js +1 -1
  38. package/dist/cjs/components/toolbar.js.map +4 -4
  39. package/dist/cjs/index.js +4 -4
  40. package/dist/cjs/index.js.map +4 -4
  41. package/dist/esm/{chunk-PNAB6KCN.js → chunk-6DDOR73D.js} +2 -2
  42. package/dist/esm/{chunk-PNAB6KCN.js.map → chunk-6DDOR73D.js.map} +1 -1
  43. package/dist/esm/{chunk-LC5AK2ZD.js → chunk-BOL3VVSN.js} +2 -2
  44. package/dist/esm/chunk-EZYXYLR2.js +2 -0
  45. package/dist/esm/chunk-EZYXYLR2.js.map +7 -0
  46. package/dist/esm/{chunk-WKSXMFFF.js → chunk-FYT7TYGS.js} +2 -2
  47. package/dist/esm/chunk-GLV7VGBH.js +2 -0
  48. package/dist/esm/{chunk-KMDXIZAG.js.map → chunk-GLV7VGBH.js.map} +2 -2
  49. package/dist/esm/{chunk-NLXJZJNN.js → chunk-L2R62BOB.js} +2 -2
  50. package/dist/esm/chunk-M7I6ISPL.js +2 -0
  51. package/dist/esm/chunk-M7I6ISPL.js.map +7 -0
  52. package/dist/esm/chunk-MYYXOFU5.js +1 -0
  53. package/dist/esm/chunk-NQDMTOGB.js +2 -0
  54. package/dist/esm/chunk-NQDMTOGB.js.map +7 -0
  55. package/dist/esm/{chunk-YK3ZUMRQ.js → chunk-Q6MFPATL.js} +2 -2
  56. package/dist/esm/{chunk-T62PRLPN.js → chunk-QGCXTULQ.js} +2 -2
  57. package/dist/esm/chunk-UE3HATOA.js +2 -0
  58. package/dist/esm/chunk-UE3HATOA.js.map +7 -0
  59. package/dist/esm/chunk-XRCMPBAY.js +2 -0
  60. package/dist/esm/chunk-XRCMPBAY.js.map +7 -0
  61. package/dist/esm/{chunk-TJL47GIO.js → chunk-ZEX2DECM.js} +2 -2
  62. package/dist/esm/components/_internal/base-button.d.ts.map +1 -1
  63. package/dist/esm/components/_internal/base-button.js +1 -1
  64. package/dist/esm/components/button.js +1 -1
  65. package/dist/esm/components/chatbar.js +1 -1
  66. package/dist/esm/components/combobox.js +1 -1
  67. package/dist/esm/components/image.d.ts +23 -0
  68. package/dist/esm/components/image.d.ts.map +1 -0
  69. package/dist/esm/components/image.js +2 -0
  70. package/dist/esm/components/image.js.map +7 -0
  71. package/dist/esm/components/image.props.d.ts +19 -0
  72. package/dist/esm/components/image.props.d.ts.map +1 -0
  73. package/dist/esm/components/image.props.js +2 -0
  74. package/dist/esm/components/image.props.js.map +7 -0
  75. package/dist/esm/components/index.d.ts +1 -0
  76. package/dist/esm/components/index.d.ts.map +1 -1
  77. package/dist/esm/components/index.js +1 -1
  78. package/dist/esm/components/message-scroller.js +1 -1
  79. package/dist/esm/components/notice.js +1 -1
  80. package/dist/esm/components/select.d.ts +1 -2
  81. package/dist/esm/components/select.d.ts.map +1 -1
  82. package/dist/esm/components/select.js +1 -1
  83. package/dist/esm/components/slider.d.ts.map +1 -1
  84. package/dist/esm/components/slider.js +1 -1
  85. package/dist/esm/components/spinner.d.ts.map +1 -1
  86. package/dist/esm/components/spinner.js +1 -1
  87. package/dist/esm/components/toggle.js +1 -1
  88. package/dist/esm/components/toolbar.js +1 -1
  89. package/dist/esm/helpers/index.js +1 -1
  90. package/dist/esm/index.js +1 -1
  91. package/dist/esm/props/index.js +1 -1
  92. package/package.json +1 -1
  93. package/src/components/_internal/base-button.css +6 -0
  94. package/src/components/_internal/base-button.tsx +5 -5
  95. package/src/components/accordion.css +0 -1
  96. package/src/components/combobox.css +13 -0
  97. package/src/components/image.css +33 -0
  98. package/src/components/image.props.ts +18 -0
  99. package/src/components/image.tsx +71 -0
  100. package/src/components/index.css +1 -0
  101. package/src/components/index.tsx +1 -0
  102. package/src/components/select.tsx +77 -29
  103. package/src/components/shell.css +12 -0
  104. package/src/components/slider.css +1 -0
  105. package/src/components/slider.tsx +3 -12
  106. package/src/components/spinner.css +19 -1
  107. package/src/components/spinner.tsx +11 -9
  108. package/src/styles/tokens/color.css +1 -22
  109. package/src/styles/tokens/radius.css +8 -8
  110. package/styles.css +65 -31
  111. package/tokens/base.css +8 -29
  112. package/tokens.css +8 -29
  113. package/dist/esm/chunk-3YWVE5GL.js +0 -2
  114. package/dist/esm/chunk-3YWVE5GL.js.map +0 -7
  115. package/dist/esm/chunk-A3YQIJDA.js +0 -2
  116. package/dist/esm/chunk-A3YQIJDA.js.map +0 -7
  117. package/dist/esm/chunk-D3U4MWDZ.js +0 -1
  118. package/dist/esm/chunk-IJRBX6EV.js +0 -2
  119. package/dist/esm/chunk-IJRBX6EV.js.map +0 -7
  120. package/dist/esm/chunk-KMDXIZAG.js +0 -2
  121. /package/dist/esm/{chunk-LC5AK2ZD.js.map → chunk-BOL3VVSN.js.map} +0 -0
  122. /package/dist/esm/{chunk-WKSXMFFF.js.map → chunk-FYT7TYGS.js.map} +0 -0
  123. /package/dist/esm/{chunk-NLXJZJNN.js.map → chunk-L2R62BOB.js.map} +0 -0
  124. /package/dist/esm/{chunk-D3U4MWDZ.js.map → chunk-MYYXOFU5.js.map} +0 -0
  125. /package/dist/esm/{chunk-YK3ZUMRQ.js.map → chunk-Q6MFPATL.js.map} +0 -0
  126. /package/dist/esm/{chunk-T62PRLPN.js.map → chunk-QGCXTULQ.js.map} +0 -0
  127. /package/dist/esm/{chunk-TJL47GIO.js.map → chunk-ZEX2DECM.js.map} +0 -0
@@ -12,6 +12,10 @@
12
12
  .kui-ComboboxContent {
13
13
  --combobox-list-max-height: calc(300px * var(--scale));
14
14
 
15
+ /* Stops the list's scroll chaining to the page. Not user-scrollable (overflow: hidden), so
16
+ Chromium does not composite it the way it does a scroller with this property. */
17
+ overscroll-behavior: contain;
18
+
15
19
  min-width: var(--radix-popover-trigger-width);
16
20
  max-height: var(--radix-popover-content-available-height);
17
21
  transform-origin: var(--radix-popover-content-transform-origin);
@@ -55,6 +59,15 @@
55
59
  scrollbar-color: var(--neutral-a8) transparent;
56
60
  }
57
61
 
62
+ /* Chromium (the only engine with -webkit-app-region) composites a scroller whose
63
+ overscroll-behavior is not auto, even when the list fits: 2 layers. There the content above
64
+ stops the chain. WebKit ignores an ancestor that is not user-scrollable, so it keeps `contain`. */
65
+ @supports (-webkit-app-region: drag) {
66
+ .kui-ComboboxScrollArea {
67
+ overscroll-behavior: auto;
68
+ }
69
+ }
70
+
58
71
  .kui-ComboboxList {
59
72
  outline: 0;
60
73
  }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Image: one <img>. It fills its box by `fit`, takes the theme's corner, rests on a quiet
3
+ * neutral fill until it has loaded, so the box holds its place and a failed image keeps it.
4
+ * No fade-in: v1 has had no motion since 0.15. The fill clears once loaded so a transparent
5
+ * picture shows what is behind it.
6
+ */
7
+ .kui-Image {
8
+ display: block;
9
+ max-width: 100%;
10
+ object-fit: var(--image-object-fit, cover);
11
+ border-radius: var(--radius-2);
12
+ background-color: var(--neutral-a3);
13
+ }
14
+
15
+ .kui-Image:where([data-status='loaded']) {
16
+ background-color: transparent;
17
+ }
18
+
19
+ .kui-Image:where(.kui-r-fit-cover) {
20
+ --image-object-fit: cover;
21
+ }
22
+ .kui-Image:where(.kui-r-fit-contain) {
23
+ --image-object-fit: contain;
24
+ }
25
+ .kui-Image:where(.kui-r-fit-fill) {
26
+ --image-object-fit: fill;
27
+ }
28
+ .kui-Image:where(.kui-r-fit-scale-down) {
29
+ --image-object-fit: scale-down;
30
+ }
31
+ .kui-Image:where(.kui-r-fit-none) {
32
+ --image-object-fit: none;
33
+ }
@@ -0,0 +1,18 @@
1
+ import { radiusPropDef } from '../props/radius.prop.js';
2
+
3
+ import type { PropDef } from '../props/prop-def.js';
4
+
5
+ const objectFitValues = ['cover', 'contain', 'fill', 'scale-down', 'none'] as const;
6
+
7
+ const imagePropDefs = {
8
+ ...radiusPropDef,
9
+ /**
10
+ * How the picture fills its box, as CSS `object-fit`.
11
+ * @link https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
12
+ */
13
+ fit: { type: 'enum', className: 'kui-r-fit', values: objectFitValues, default: 'cover' },
14
+ } satisfies {
15
+ fit: PropDef<(typeof objectFitValues)[number]>;
16
+ };
17
+
18
+ export { imagePropDefs };
@@ -0,0 +1,71 @@
1
+ import * as React from 'react';
2
+ import classNames from 'classnames';
3
+ import { useComposedRefs, useLayoutEffect } from 'radix-ui/internal';
4
+
5
+ import { imagePropDefs } from './image.props.js';
6
+ import { extractProps } from '../helpers/extract-props.js';
7
+ import { widthPropDefs } from '../props/width.props.js';
8
+ import { heightPropDefs } from '../props/height.props.js';
9
+
10
+ import type { ComponentPropsWithout, RemovedProps } from '../helpers/component-props.js';
11
+ import type { WidthProps } from '../props/width.props.js';
12
+ import type { HeightProps } from '../props/height.props.js';
13
+ import type { GetPropDefTypes } from '../props/prop-def.js';
14
+
15
+ type ImageElement = React.ElementRef<'img'>;
16
+ type ImageOwnProps = GetPropDefTypes<typeof imagePropDefs>;
17
+ type ImageStatus = 'loading' | 'loaded' | 'error';
18
+
19
+ interface ImageProps extends ComponentPropsWithout<'img', RemovedProps | 'width' | 'height' | 'src' | 'alt'>, WidthProps, HeightProps, ImageOwnProps {
20
+ src: string;
21
+ /** Required. Pass `""` for a decorative picture. */
22
+ alt: string;
23
+ }
24
+
25
+ /**
26
+ * A plain `<img>` that loads lazily, fills its box by `fit` and takes the theme's corner.
27
+ * One element and no wrapper: `width`, `height`, `className` and `style` all land on the
28
+ * `<img>` itself.
29
+ *
30
+ * Until it loads it rests on a quiet neutral fill, which is also what stays if it fails.
31
+ */
32
+ const Image = React.forwardRef<ImageElement, ImageProps>((props, forwardedRef) => {
33
+ const { className, alt, radius, loading = 'lazy', decoding = 'async', onLoad, onError, ...imageProps } = extractProps(props, imagePropDefs, widthPropDefs, heightPropDefs);
34
+
35
+ const [status, setStatus] = React.useState<ImageStatus>('loading');
36
+ const ref = React.useRef<HTMLImageElement>(null);
37
+
38
+ // A cached image, or one that finished before hydration, never fires onLoad for us.
39
+ useLayoutEffect(() => {
40
+ const img = ref.current;
41
+ if (img?.complete) setStatus(img.naturalWidth > 0 ? 'loaded' : 'error');
42
+ else setStatus('loading');
43
+ }, [props.src]);
44
+
45
+ const setRefs = useComposedRefs(ref, forwardedRef);
46
+
47
+ return (
48
+ <img
49
+ {...imageProps}
50
+ ref={setRefs}
51
+ alt={alt}
52
+ loading={loading}
53
+ decoding={decoding}
54
+ data-radius={radius}
55
+ data-status={status}
56
+ className={classNames('kui-Image', className)}
57
+ onLoad={(event) => {
58
+ setStatus('loaded');
59
+ onLoad?.(event);
60
+ }}
61
+ onError={(event) => {
62
+ setStatus('error');
63
+ onError?.(event);
64
+ }}
65
+ />
66
+ );
67
+ });
68
+ Image.displayName = 'Image';
69
+
70
+ export { Image };
71
+ export type { ImageProps };
@@ -20,6 +20,7 @@
20
20
  @import './dropdown-menu.css';
21
21
  @import './combobox.css';
22
22
  @import './heading.css';
23
+ @import './image.css';
23
24
  @import './kbd.css';
24
25
  @import './link.css';
25
26
  @import './navbar.css';
@@ -24,6 +24,7 @@ export {
24
24
  ThickChevronRightIcon,
25
25
  ThickDividerHorizontalIcon,
26
26
  } from './icons.js';
27
+ export { Image, type ImageProps } from './image.js';
27
28
  export { Kbd, type KbdProps } from './kbd.js';
28
29
  export { Link, type LinkProps } from './link.js';
29
30
  export * as Navbar from './navbar.js';
@@ -21,16 +21,45 @@ import type { ComponentPropsWithout, RemovedProps } from '../helpers/component-p
21
21
 
22
22
  type SelectRootOwnProps = GetPropDefTypes<typeof selectRootPropDefs>;
23
23
 
24
- type SelectContextValue = SelectRootOwnProps;
24
+ // Trigger and Content pass the size prop def as its own record. It is hoisted because
25
+ // extractProps caches its compiled prop defs on the identity of each record it is given:
26
+ // a literal written at the call site would be a new object, and a recompile, every render.
27
+ const selectSizePropDefs = { size: selectRootPropDefs.size };
28
+
29
+ interface SelectContextValue extends SelectRootOwnProps {
30
+ /** Whether the list is open. Undefined when Content is rendered outside a Kookie Select.Root. */
31
+ open?: boolean;
32
+ }
25
33
  const SelectContext = React.createContext<SelectContextValue>({});
26
34
 
27
- interface SelectRootProps extends SelectPrimitive.SelectProps, SelectContextValue {}
35
+ interface SelectRootProps extends SelectPrimitive.SelectProps, SelectRootOwnProps {}
28
36
  const SelectRoot: React.FC<SelectRootProps> = (props) => {
29
- const { children, size: sizeProp, ...rootProps } = props;
37
+ const {
38
+ children,
39
+ size: sizeProp,
40
+ open: openProp,
41
+ defaultOpen,
42
+ onOpenChange,
43
+ ...rootProps
44
+ } = props;
30
45
  const size = useThemeSize(sizeProp, selectRootPropDefs.size);
46
+
47
+ // Root owns the open state and hands Radix a controlled `open`, so Content learns of a
48
+ // change in the same render Radix does. Radix's uncontrolled state reports changes from an
49
+ // effect, a render late, which would mount the open list once without its wrappers.
50
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false);
51
+ const open = openProp ?? uncontrolledOpen;
52
+ const handleOpenChange = React.useCallback(
53
+ (next: boolean) => {
54
+ setUncontrolledOpen(next);
55
+ onOpenChange?.(next);
56
+ },
57
+ [onOpenChange],
58
+ );
59
+
31
60
  return (
32
- <SelectPrimitive.Root {...rootProps}>
33
- <SelectContext.Provider value={React.useMemo(() => ({ size }), [size])}>
61
+ <SelectPrimitive.Root {...rootProps} open={open} onOpenChange={handleOpenChange}>
62
+ <SelectContext.Provider value={React.useMemo(() => ({ size, open }), [size, open])}>
34
63
  {children}
35
64
  </SelectContext.Provider>
36
65
  </SelectPrimitive.Root>
@@ -61,7 +90,7 @@ const SelectTrigger = React.forwardRef<SelectTriggerElement, SelectTriggerProps>
61
90
  // Pass size value from the context to generate styles
62
91
  { size: context?.size, ...props },
63
92
  // Pass size prop def to allow it to be extracted
64
- { size: selectRootPropDefs.size },
93
+ selectSizePropDefs,
65
94
  selectTriggerPropDefs,
66
95
  );
67
96
 
@@ -143,12 +172,26 @@ const SelectContent = React.forwardRef<SelectContentElement, SelectContentProps>
143
172
  // Pass size value from the context to generate styles
144
173
  { size: context?.size, ...props },
145
174
  // Pass size prop def to allow it to be extracted
146
- { size: selectRootPropDefs.size },
175
+ selectSizePropDefs,
147
176
  selectContentPropDefs,
148
177
  );
149
178
  const themeContext = useThemeContext();
179
+
180
+ // While closed, Radix still renders Content's children, into a detached fragment: the
181
+ // items must exist for the trigger's value label, typeahead and the native form select.
182
+ // Nothing in that fragment is ever shown, so the theme wrapper and the scroll area, with
183
+ // its two resize observers, are left out until the list opens.
184
+ if (context.open === false && !contentProps.forceMount) {
185
+ return (
186
+ <SelectPrimitive.Portal container={container}>
187
+ <SelectPrimitive.Content {...contentProps} asChild={false} ref={forwardedRef}>
188
+ {children}
189
+ </SelectPrimitive.Content>
190
+ </SelectPrimitive.Portal>
191
+ );
192
+ }
193
+
150
194
  const resolvedColor = color || themeContext.accentColor;
151
- const setThumbViewport = useThumbSync<HTMLDivElement>();
152
195
  return (
153
196
  <SelectPrimitive.Portal container={container}>
154
197
  <Theme asChild>
@@ -161,31 +204,12 @@ const SelectContent = React.forwardRef<SelectContentElement, SelectContentProps>
161
204
  className={classNames(
162
205
  'kui-PopperContent',
163
206
  'kui-BaseMenuContent',
164
- 'kui-variant-soft',
207
+ 'kui-variant-soft',
165
208
  'kui-SelectContent',
166
209
  className,
167
210
  )}
168
211
  >
169
- <ScrollAreaPrimitive.Root type="auto" className="kui-ScrollAreaRoot">
170
- <SelectPrimitive.Viewport
171
- asChild
172
- className={classNames('kui-BaseMenuViewport', 'kui-SelectViewport')}
173
- >
174
- <ScrollAreaPrimitive.Viewport
175
- ref={setThumbViewport}
176
- className="kui-ScrollAreaViewport"
177
- style={{ overflowY: undefined }}
178
- >
179
- {children}
180
- </ScrollAreaPrimitive.Viewport>
181
- </SelectPrimitive.Viewport>
182
- <ScrollAreaPrimitive.Scrollbar
183
- className="kui-ScrollAreaScrollbar"
184
- orientation="vertical"
185
- >
186
- <ScrollAreaPrimitive.Thumb className="kui-ScrollAreaThumb" />
187
- </ScrollAreaPrimitive.Scrollbar>
188
- </ScrollAreaPrimitive.Root>
212
+ <SelectScrollArea>{children}</SelectScrollArea>
189
213
  </SelectPrimitive.Content>
190
214
  </Theme>
191
215
  </SelectPrimitive.Portal>
@@ -194,6 +218,30 @@ const SelectContent = React.forwardRef<SelectContentElement, SelectContentProps>
194
218
  );
195
219
  SelectContent.displayName = 'Select.Content';
196
220
 
221
+ /** The open list's scroll area. Its own component so the thumb sync hook runs only while open. */
222
+ function SelectScrollArea({ children }: { children: React.ReactNode }) {
223
+ const setThumbViewport = useThumbSync<HTMLDivElement>();
224
+ return (
225
+ <ScrollAreaPrimitive.Root type="auto" className="kui-ScrollAreaRoot">
226
+ <SelectPrimitive.Viewport
227
+ asChild
228
+ className={classNames('kui-BaseMenuViewport', 'kui-SelectViewport')}
229
+ >
230
+ <ScrollAreaPrimitive.Viewport
231
+ ref={setThumbViewport}
232
+ className="kui-ScrollAreaViewport"
233
+ style={{ overflowY: undefined }}
234
+ >
235
+ {children}
236
+ </ScrollAreaPrimitive.Viewport>
237
+ </SelectPrimitive.Viewport>
238
+ <ScrollAreaPrimitive.Scrollbar className="kui-ScrollAreaScrollbar" orientation="vertical">
239
+ <ScrollAreaPrimitive.Thumb className="kui-ScrollAreaThumb" />
240
+ </ScrollAreaPrimitive.Scrollbar>
241
+ </ScrollAreaPrimitive.Root>
242
+ );
243
+ }
244
+
197
245
  type SelectItemElement = React.ElementRef<typeof SelectPrimitive.Item>;
198
246
  interface SelectItemProps
199
247
  extends ComponentPropsWithout<typeof SelectPrimitive.Item, RemovedProps> {}
@@ -88,6 +88,18 @@
88
88
  isolation: isolate;
89
89
  }
90
90
 
91
+ /*
92
+ * Chromium (the only engine with -webkit-app-region) gives a scroller whose overscroll-behavior is
93
+ * not auto two composited layers even when its content fits: about 3 MB for a 1280x800 pane. There
94
+ * the Root's own overscroll-behavior already stops the chain, so the content does not need one.
95
+ * WebKit ignores an ancestor that is not user-scrollable, so it keeps `contain` above.
96
+ */
97
+ @supports (-webkit-app-region: drag) {
98
+ .kui-ShellContent {
99
+ overscroll-behavior: auto;
100
+ }
101
+ }
102
+
91
103
  /* ── Panes ─────────────────────────────────────────────────────────────────── */
92
104
 
93
105
  /*
@@ -241,6 +241,7 @@
241
241
  .kui-SliderTick {
242
242
  position: absolute;
243
243
  top: 0;
244
+ transform: translateX(-50%);
244
245
  display: flex;
245
246
  flex-direction: column;
246
247
  align-items: center;
@@ -152,14 +152,7 @@ const Slider = React.forwardRef<SliderElement, SliderProps>((ownProps, forwarded
152
152
  ref={announceRef}
153
153
  aria-live="polite"
154
154
  aria-atomic="true"
155
- className="sr-only"
156
- style={{
157
- position: 'absolute',
158
- left: '-10000px',
159
- width: '1px',
160
- height: '1px',
161
- overflow: 'hidden',
162
- }}
155
+ className="kui-sr-only"
163
156
  />
164
157
 
165
158
  <SliderPrimitive.Root
@@ -205,10 +198,8 @@ const Slider = React.forwardRef<SliderElement, SliderProps>((ownProps, forwarded
205
198
  <button
206
199
  key={tick.index}
207
200
  className="kui-SliderTick"
208
- style={{
209
- left: `${tick.percentage}%`,
210
- transform: 'translateX(-50%)',
211
- }}
201
+ // Only `left` varies per tick; the centring transform lives in `.kui-SliderTick`.
202
+ style={{ left: `${tick.percentage}%` }}
212
203
  onClick={() => {
213
204
  const currentValues = sliderProps.value ?? sliderProps.defaultValue ?? [min];
214
205
  const newValue = Array.isArray(currentValues)
@@ -8,6 +8,18 @@
8
8
  position: relative;
9
9
  width: var(--space-5);
10
10
  height: var(--space-5);
11
+ /* The rotation runs on the inner wheel, clipped here. Unclipped, a running transform animation
12
+ makes Chrome give its own layer to everything painted after it that it might overlap. The
13
+ leaves stay inside this box at every step; the margin keeps their anti-aliased edges on the
14
+ diagonal steps, which a clip at the box itself shaves. */
15
+ contain: paint;
16
+ overflow-clip-margin: 4px;
17
+ }
18
+
19
+ .kui-SpinnerWheel {
20
+ position: absolute;
21
+ inset: 0;
22
+ /* On the wheel, not the root: opacity on the clipping root costs Chrome a second layer per spinner. */
11
23
  opacity: var(--spinner-opacity);
12
24
  /* Single GPU-composited rotation instead of 8 opacity animations */
13
25
  animation: kui-spinner-rotate var(--spinner-animation-duration) steps(8, end) infinite;
@@ -73,7 +85,13 @@
73
85
 
74
86
  /* A busy indicator keeps moving when motion is reduced, more slowly. */
75
87
  @media (prefers-reduced-motion: reduce) {
76
- .kui-Spinner {
88
+ .kui-SpinnerWheel {
77
89
  animation-duration: calc(var(--spinner-animation-duration) * 3);
78
90
  }
79
91
  }
92
+
93
+ /* Children of a Spinner keep their space but are not shown while it spins. A class, not an inline style. */
94
+ .kui-SpinnerHiddenChildren {
95
+ display: contents;
96
+ visibility: hidden;
97
+ }
@@ -23,14 +23,16 @@ const Spinner = React.forwardRef<SpinnerElement, SpinnerProps>((props, forwarded
23
23
 
24
24
  const spinner = (
25
25
  <span {...spinnerProps} ref={forwardedRef} className={classNames('kui-Spinner', className)}>
26
- <span className="kui-SpinnerLeaf" />
27
- <span className="kui-SpinnerLeaf" />
28
- <span className="kui-SpinnerLeaf" />
29
- <span className="kui-SpinnerLeaf" />
30
- <span className="kui-SpinnerLeaf" />
31
- <span className="kui-SpinnerLeaf" />
32
- <span className="kui-SpinnerLeaf" />
33
- <span className="kui-SpinnerLeaf" />
26
+ <span className="kui-SpinnerWheel">
27
+ <span className="kui-SpinnerLeaf" />
28
+ <span className="kui-SpinnerLeaf" />
29
+ <span className="kui-SpinnerLeaf" />
30
+ <span className="kui-SpinnerLeaf" />
31
+ <span className="kui-SpinnerLeaf" />
32
+ <span className="kui-SpinnerLeaf" />
33
+ <span className="kui-SpinnerLeaf" />
34
+ <span className="kui-SpinnerLeaf" />
35
+ </span>
34
36
  </span>
35
37
  );
36
38
 
@@ -43,7 +45,7 @@ const Spinner = React.forwardRef<SpinnerElement, SpinnerProps>((props, forwarded
43
45
  * `display: contents` removes the content from the accessibility tree in some browsers,
44
46
  * so we force remove it with `aria-hidden`
45
47
  */}
46
- <span aria-hidden style={{ display: 'contents', visibility: 'hidden' }} inert={undefined}>
48
+ <span aria-hidden className="kui-SpinnerHiddenChildren" inert={undefined}>
47
49
  {children}
48
50
  </span>
49
51
 
@@ -89,31 +89,10 @@
89
89
 
90
90
  .kui-theme,
91
91
  [data-accent-color]:where(:not([data-accent-color=''], [data-accent-color='gray'])) {
92
- --focus-1: var(--accent-1);
93
- --focus-2: var(--accent-2);
94
- --focus-3: var(--accent-3);
95
- --focus-4: var(--accent-4);
96
- --focus-5: var(--accent-5);
97
- --focus-6: var(--accent-6);
98
- --focus-7: var(--accent-7);
92
+ /* Only the aliases components read; this block runs on every coloured element. */
99
93
  --focus-8: var(--accent-8);
100
- --focus-9: var(--accent-9);
101
- --focus-10: var(--accent-10);
102
- --focus-11: var(--accent-11);
103
- --focus-12: var(--accent-12);
104
-
105
- --focus-a1: var(--accent-a1);
106
94
  --focus-a2: var(--accent-a2);
107
- --focus-a3: var(--accent-a3);
108
- --focus-a4: var(--accent-a4);
109
95
  --focus-a5: var(--accent-a5);
110
- --focus-a6: var(--accent-a6);
111
- --focus-a7: var(--accent-a7);
112
- --focus-a8: var(--accent-a8);
113
- --focus-a9: var(--accent-a9);
114
- --focus-a10: var(--accent-a10);
115
- --focus-a11: var(--accent-a11);
116
- --focus-a12: var(--accent-a12);
117
96
  }
118
97
 
119
98
  .kui-theme ::selection {
@@ -43,15 +43,15 @@
43
43
  --radius-row-3: var(--radius-control-3);
44
44
  --radius-row-4: var(--radius-control-4);
45
45
 
46
- --radius-surface-1: calc(10px * var(--scale) * var(--radius-surface-factor));
47
- --radius-surface-2: calc(16px * var(--scale) * var(--radius-surface-factor));
48
- --radius-surface-3: calc(22px * var(--scale) * var(--radius-surface-factor));
49
- --radius-surface-4: calc(28px * var(--scale) * var(--radius-surface-factor));
46
+ --radius-surface-1: calc(8px * var(--scale) * var(--radius-surface-factor));
47
+ --radius-surface-2: calc(12px * var(--scale) * var(--radius-surface-factor));
48
+ --radius-surface-3: calc(16px * var(--scale) * var(--radius-surface-factor));
49
+ --radius-surface-4: calc(20px * var(--scale) * var(--radius-surface-factor));
50
50
 
51
- --radius-overlay-1: calc(14px * var(--scale) * var(--radius-surface-factor));
52
- --radius-overlay-2: calc(20px * var(--scale) * var(--radius-surface-factor));
53
- --radius-overlay-3: calc(24px * var(--scale) * var(--radius-surface-factor));
54
- --radius-overlay-4: calc(28px * var(--scale) * var(--radius-surface-factor));
51
+ --radius-overlay-1: calc(12px * var(--scale) * var(--radius-surface-factor));
52
+ --radius-overlay-2: calc(16px * var(--scale) * var(--radius-surface-factor));
53
+ --radius-overlay-3: calc(20px * var(--scale) * var(--radius-surface-factor));
54
+ --radius-overlay-4: calc(24px * var(--scale) * var(--radius-surface-factor));
55
55
  }
56
56
 
57
57
  [data-radius='none'] {