@womp/kookie-ui 0.22.1 → 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 (126) 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/styles.css +57 -23
  110. package/tokens/base.css +0 -21
  111. package/tokens.css +0 -21
  112. package/dist/esm/chunk-3YWVE5GL.js +0 -2
  113. package/dist/esm/chunk-3YWVE5GL.js.map +0 -7
  114. package/dist/esm/chunk-A3YQIJDA.js +0 -2
  115. package/dist/esm/chunk-A3YQIJDA.js.map +0 -7
  116. package/dist/esm/chunk-D3U4MWDZ.js +0 -1
  117. package/dist/esm/chunk-IJRBX6EV.js +0 -2
  118. package/dist/esm/chunk-IJRBX6EV.js.map +0 -7
  119. package/dist/esm/chunk-KMDXIZAG.js +0 -2
  120. /package/dist/esm/{chunk-LC5AK2ZD.js.map → chunk-BOL3VVSN.js.map} +0 -0
  121. /package/dist/esm/{chunk-WKSXMFFF.js.map → chunk-FYT7TYGS.js.map} +0 -0
  122. /package/dist/esm/{chunk-NLXJZJNN.js.map → chunk-L2R62BOB.js.map} +0 -0
  123. /package/dist/esm/{chunk-D3U4MWDZ.js.map → chunk-MYYXOFU5.js.map} +0 -0
  124. /package/dist/esm/{chunk-YK3ZUMRQ.js.map → chunk-Q6MFPATL.js.map} +0 -0
  125. /package/dist/esm/{chunk-T62PRLPN.js.map → chunk-QGCXTULQ.js.map} +0 -0
  126. /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 {
package/styles.css CHANGED
@@ -3480,30 +3480,9 @@
3480
3480
  }
3481
3481
  .kui-theme,
3482
3482
  [data-accent-color]:where(:not([data-accent-color=''], [data-accent-color='gray'])) {
3483
- --focus-1: var(--accent-1);
3484
- --focus-2: var(--accent-2);
3485
- --focus-3: var(--accent-3);
3486
- --focus-4: var(--accent-4);
3487
- --focus-5: var(--accent-5);
3488
- --focus-6: var(--accent-6);
3489
- --focus-7: var(--accent-7);
3490
3483
  --focus-8: var(--accent-8);
3491
- --focus-9: var(--accent-9);
3492
- --focus-10: var(--accent-10);
3493
- --focus-11: var(--accent-11);
3494
- --focus-12: var(--accent-12);
3495
- --focus-a1: var(--accent-a1);
3496
3484
  --focus-a2: var(--accent-a2);
3497
- --focus-a3: var(--accent-a3);
3498
- --focus-a4: var(--accent-a4);
3499
3485
  --focus-a5: var(--accent-a5);
3500
- --focus-a6: var(--accent-a6);
3501
- --focus-a7: var(--accent-a7);
3502
- --focus-a8: var(--accent-a8);
3503
- --focus-a9: var(--accent-a9);
3504
- --focus-a10: var(--accent-a10);
3505
- --focus-a11: var(--accent-a11);
3506
- --focus-a12: var(--accent-a12);
3507
3486
  --accent-ink-mix: 45%;
3508
3487
  --accent-ink: color-mix(in oklab, var(--accent-11), var(--accent-12) var(--accent-ink-mix));
3509
3488
  --accent-ink-muted: color-mix(in oklab, var(--accent-ink) 74%, transparent);
@@ -5915,7 +5894,6 @@
5915
5894
  line-height: var(--line-height-4);
5916
5895
  -webkit-transform-origin: top;
5917
5896
  transform-origin: top;
5918
- will-change: height, opacity, transform;
5919
5897
  }
5920
5898
  .kui-AccordionContentInner {
5921
5899
  padding: 0 var(--space-5) var(--space-4);
@@ -7489,6 +7467,10 @@
7489
7467
  position: relative;
7490
7468
  cursor: progress;
7491
7469
  }
7470
+ .kui-BaseButton :where(.kui-BaseButtonContent) {
7471
+ display: contents;
7472
+ visibility: hidden;
7473
+ }
7492
7474
  .kui-BaseButton :where(svg) {
7493
7475
  width: var(--base-button-icon-size);
7494
7476
  height: var(--base-button-icon-size);
@@ -11099,6 +11081,8 @@
11099
11081
  }
11100
11082
  .kui-ComboboxContent {
11101
11083
  --combobox-list-max-height: calc(300px * var(--scale));
11084
+ -ms-scroll-chaining: none;
11085
+ overscroll-behavior: contain;
11102
11086
  min-width: var(--radix-popover-trigger-width);
11103
11087
  max-height: var(--radix-popover-content-available-height);
11104
11088
  -webkit-transform-origin: var(--radix-popover-content-transform-origin);
@@ -11143,6 +11127,12 @@
11143
11127
  scrollbar-width: thin;
11144
11128
  scrollbar-color: var(--neutral-a8) transparent;
11145
11129
  }
11130
+ @supports (-webkit-app-region: drag) {
11131
+ .kui-ComboboxScrollArea {
11132
+ -ms-scroll-chaining: chained;
11133
+ overscroll-behavior: auto;
11134
+ }
11135
+ }
11146
11136
  .kui-ComboboxList {
11147
11137
  outline: 0;
11148
11138
  }
@@ -11506,6 +11496,32 @@
11506
11496
  letter-spacing: calc(var(--letter-spacing-9) + var(--heading-letter-spacing));
11507
11497
  }
11508
11498
  }
11499
+ .kui-Image {
11500
+ display: block;
11501
+ max-width: 100%;
11502
+ -o-object-fit: var(--image-object-fit, cover);
11503
+ object-fit: var(--image-object-fit, cover);
11504
+ border-radius: var(--radius-2);
11505
+ background-color: var(--neutral-a3);
11506
+ }
11507
+ .kui-Image:where([data-status='loaded']) {
11508
+ background-color: transparent;
11509
+ }
11510
+ .kui-Image:where(.kui-r-fit-cover) {
11511
+ --image-object-fit: cover;
11512
+ }
11513
+ .kui-Image:where(.kui-r-fit-contain) {
11514
+ --image-object-fit: contain;
11515
+ }
11516
+ .kui-Image:where(.kui-r-fit-fill) {
11517
+ --image-object-fit: fill;
11518
+ }
11519
+ .kui-Image:where(.kui-r-fit-scale-down) {
11520
+ --image-object-fit: scale-down;
11521
+ }
11522
+ .kui-Image:where(.kui-r-fit-none) {
11523
+ --image-object-fit: none;
11524
+ }
11509
11525
  .kui-Kbd {
11510
11526
  display: -webkit-inline-box;
11511
11527
  display: -ms-inline-flexbox;
@@ -16370,6 +16386,12 @@
16370
16386
  overscroll-behavior: contain;
16371
16387
  isolation: isolate;
16372
16388
  }
16389
+ @supports (-webkit-app-region: drag) {
16390
+ .kui-ShellContent {
16391
+ -ms-scroll-chaining: chained;
16392
+ overscroll-behavior: auto;
16393
+ }
16394
+ }
16373
16395
  .kui-ShellPane {
16374
16396
  --kui-pane-inset-start: initial;
16375
16397
  --kui-pane-inset-end: initial;
@@ -17418,6 +17440,8 @@
17418
17440
  .kui-SliderTick {
17419
17441
  position: absolute;
17420
17442
  top: 0;
17443
+ -webkit-transform: translateX(-50%);
17444
+ transform: translateX(-50%);
17421
17445
  display: -webkit-box;
17422
17446
  display: -ms-flexbox;
17423
17447
  display: flex;
@@ -17481,6 +17505,12 @@
17481
17505
  position: relative;
17482
17506
  width: var(--space-5);
17483
17507
  height: var(--space-5);
17508
+ contain: paint;
17509
+ overflow-clip-margin: 4px;
17510
+ }
17511
+ .kui-SpinnerWheel {
17512
+ position: absolute;
17513
+ inset: 0;
17484
17514
  opacity: var(--spinner-opacity);
17485
17515
  -webkit-animation: kui-spinner-rotate var(--spinner-animation-duration) steps(8, end) infinite;
17486
17516
  animation: kui-spinner-rotate var(--spinner-animation-duration) steps(8, end) infinite;
@@ -17553,11 +17583,15 @@
17553
17583
  }
17554
17584
  }
17555
17585
  @media (prefers-reduced-motion: reduce) {
17556
- .kui-Spinner {
17586
+ .kui-SpinnerWheel {
17557
17587
  -webkit-animation-duration: calc(var(--spinner-animation-duration) * 3);
17558
17588
  animation-duration: calc(var(--spinner-animation-duration) * 3);
17559
17589
  }
17560
17590
  }
17591
+ .kui-SpinnerHiddenChildren {
17592
+ display: contents;
17593
+ visibility: hidden;
17594
+ }
17561
17595
  .kui-SwitchRoot {
17562
17596
  position: relative;
17563
17597
  display: -webkit-inline-box;
package/tokens/base.css CHANGED
@@ -129,30 +129,9 @@
129
129
  }
130
130
  .kui-theme,
131
131
  [data-accent-color]:where(:not([data-accent-color=''], [data-accent-color='gray'])) {
132
- --focus-1: var(--accent-1);
133
- --focus-2: var(--accent-2);
134
- --focus-3: var(--accent-3);
135
- --focus-4: var(--accent-4);
136
- --focus-5: var(--accent-5);
137
- --focus-6: var(--accent-6);
138
- --focus-7: var(--accent-7);
139
132
  --focus-8: var(--accent-8);
140
- --focus-9: var(--accent-9);
141
- --focus-10: var(--accent-10);
142
- --focus-11: var(--accent-11);
143
- --focus-12: var(--accent-12);
144
- --focus-a1: var(--accent-a1);
145
133
  --focus-a2: var(--accent-a2);
146
- --focus-a3: var(--accent-a3);
147
- --focus-a4: var(--accent-a4);
148
134
  --focus-a5: var(--accent-a5);
149
- --focus-a6: var(--accent-a6);
150
- --focus-a7: var(--accent-a7);
151
- --focus-a8: var(--accent-a8);
152
- --focus-a9: var(--accent-a9);
153
- --focus-a10: var(--accent-a10);
154
- --focus-a11: var(--accent-a11);
155
- --focus-a12: var(--accent-a12);
156
135
  --accent-ink-mix: 45%;
157
136
  --accent-ink: color-mix(in oklab, var(--accent-11), var(--accent-12) var(--accent-ink-mix));
158
137
  --accent-ink-muted: color-mix(in oklab, var(--accent-ink) 74%, transparent);