@minimalstuff/ui 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,28 @@ import { THEME_INIT_SCRIPT } from '@minimalstuff/ui';
59
59
  </head>;
60
60
  ```
61
61
 
62
+ #### Status bar and `theme-color`
63
+
64
+ To make the browser or installed PWA status bar follow the theme, add a `theme-color` meta with a `data-light` and a `data-dark` color. `THEME_INIT_SCRIPT` and `useThemeStore` copy the matching one into `content` every time the theme is applied (init, `setTheme`, OS change while on `system`, another tab). A meta without both attributes is left alone, and without such a meta nothing changes.
65
+
66
+ ```html
67
+ <head>
68
+ <meta
69
+ name="theme-color"
70
+ content="#ffffff"
71
+ data-light="#ffffff"
72
+ data-dark="#0b0b0b"
73
+ />
74
+ <script>
75
+ /* THEME_INIT_SCRIPT */
76
+ </script>
77
+ </head>
78
+ ```
79
+
80
+ - Put the meta before the script in `<head>`.
81
+ - Remove any other `theme-color` meta, notably ones with a `media` attribute: the first matching one wins.
82
+ - Keep the meta in the static HTML. A meta recreated by a `<head>` manager (Inertia `<Head>`, react-helmet) is not resynced.
83
+
62
84
  With a strict Content-Security-Policy, either add the script's SHA-256 hash to `script-src` (`'sha256-<hash>'`), or write `THEME_INIT_SCRIPT` to a file at build time and load it with `<script src>`. Compute the hash with:
63
85
 
64
86
  ```bash
@@ -88,6 +110,8 @@ pnpm verify
88
110
 
89
111
  `pnpm test:stories` runs the stories in a real browser and needs Playwright's Chromium once: `pnpm exec playwright install chromium`.
90
112
 
113
+ To change the library from a project that uses it, see [docs/developing_from_a_consumer.md](docs/developing_from_a_consumer.md).
114
+
91
115
  Releases follow [docs/releasing.md](docs/releasing.md). Changes are listed in the [CHANGELOG](CHANGELOG.md).
92
116
 
93
117
  ## Report a bug
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ import { ReactElement } from 'react';
9
9
  import { ReactNode } from 'react';
10
10
  import { ReactPortal } from 'react';
11
11
  import { Ref } from 'react';
12
+ import { RefCallback } from 'react';
12
13
  import { RefObject } from 'react';
13
14
  import { StoreApi } from 'zustand';
14
15
  import { UseBoundStore } from 'zustand';
@@ -250,7 +251,7 @@ export declare interface ClientOnlyProps extends React.PropsWithChildren {
250
251
  fallback?: React.ReactNode;
251
252
  }
252
253
 
253
- export declare function Combobox({ options, filterOption, label, description, 'aria-label': ariaLabel, error, placeholder, noResultsText, clearLabel, radius, unstyled, disabled, required, className, wrapperClassName, value, defaultValue, onChange, id, }: Readonly<ComboboxProps>): JSX.Element;
254
+ export declare function Combobox({ options, filterOption, label, description, 'aria-label': ariaLabel, error, placeholder, noResultsText, clearLabel, radius, unstyled, disabled, required, className, wrapperClassName, dropdownClassName, optionLabelOverflow, value, defaultValue, onChange, id, }: Readonly<ComboboxProps>): JSX.Element;
254
255
 
255
256
  export declare interface ComboboxOption {
256
257
  value: string;
@@ -258,6 +259,7 @@ export declare interface ComboboxOption {
258
259
  }
259
260
 
260
261
  export declare interface ComboboxProps {
262
+ /** Long lists render faster when the array keeps the same identity between renders (memoize it), and when `optionLabelOverflow` is not `'wrap'`. */
261
263
  options: readonly ComboboxOption[];
262
264
  /**
263
265
  * Decides whether an option matches the text typed in the field. Replaces
@@ -278,6 +280,19 @@ export declare interface ComboboxProps {
278
280
  required?: boolean;
279
281
  className?: string;
280
282
  wrapperClassName?: string;
283
+ /**
284
+ * Classes for the dropdown panel, applied last so they can override its
285
+ * defaults, for example a width wider than the field when option labels are
286
+ * long (`w-96`, `min-w-80`). The panel is kept inside the viewport.
287
+ */
288
+ dropdownClassName?: string;
289
+ /**
290
+ * How option labels that do not fit on one line are shown.
291
+ * - `'wrap'` (default): labels wrap; every option is rendered, so lists of thousands of options open slower.
292
+ * - `'wrap-active'`: labels are truncated with a native tooltip, except the active option, which wraps in full; long lists only render the visible options. Recommended for long lists.
293
+ * - `'truncate'`: every label is truncated with a native tooltip, the active one included; keyboard and zoom users can no longer read long labels (WCAG 1.4.10), use only for labels known to be short.
294
+ */
295
+ optionLabelOverflow?: OptionLabelOverflow;
281
296
  value?: string;
282
297
  defaultValue?: string;
283
298
  onChange?: (value: string) => void;
@@ -527,6 +542,15 @@ export declare interface KbdProps {
527
542
 
528
543
  export declare type KbdSize = ControlSize;
529
544
 
545
+ export declare type KeyboardScrollableProps = {
546
+ ref: RefCallback<HTMLElement>;
547
+ tabIndex?: 0;
548
+ role?: 'region';
549
+ 'aria-labelledby'?: string;
550
+ 'aria-label'?: string;
551
+ 'data-keyboard-scroll-region'?: true;
552
+ };
553
+
530
554
  /**
531
555
  * A dropdown anchored to `trigger` (e.g. a kebab `IconButton`): click to
532
556
  * open, click again/click outside/Escape to close, or ArrowDown/ArrowUp on
@@ -725,11 +749,15 @@ export declare interface ModalFooterProps {
725
749
 
726
750
  export declare type ModalHeight = 'auto' | 'fixed';
727
751
 
752
+ export declare type ModalPhoneLayout = 'center' | 'sheet';
753
+
728
754
  export declare type ModalProps = {
729
755
  children: ModalRenderProp;
730
756
  size?: ModalSize;
731
757
  /** `fixed` gives the dialog a stable height so it does not jump when content changes; the content area stops scrolling and a child with `flex-1 min-h-0 overflow-y-auto` scrolls on its own. */
732
758
  height?: ModalHeight;
759
+ /** `sheet` turns the dialog into a bottom sheet below the `sm` breakpoint (full width, flush with the bottom edge, only the top corners rounded, at most 85svh tall, bottom padding clearing the safe area, sliding up on open and down on close; plain fade under reduced motion). From `sm` up it renders as `center`. With `height="fixed"` the sheet is 85svh tall on phones. `unstyled` keeps the sheet position and size but drops radius and the slide. */
760
+ phoneLayout?: ModalPhoneLayout;
733
761
  radius?: Radius;
734
762
  className?: string;
735
763
  /** Applied to the `role=dialog` element. A `transform`, `filter` or `backdrop-filter` on the open panel makes it the containing block of `position: fixed` descendants, so Menus opened inside it get clipped. Keep these off the open state. Carries `data-state="open"` or `"closed"`, so `data-[state=closed]:` variants can animate it. Put your own transitions behind `motion-reduce:` variants, since `unstyled` drops the library's reduced-motion handling along with its animations. */
@@ -759,7 +787,7 @@ export declare type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
759
787
  * pills opens a searchable dropdown of checkbox-style options. Selecting an
760
788
  * option never closes the dropdown, unlike the single-select `Combobox`.
761
789
  */
762
- export declare function MultiCombobox({ options, filterOption, label, description, 'aria-label': ariaLabel, error, placeholder, searchPlaceholder, noResultsText, clearLabel, clearText, maxSelectedValues, isOptionDisabled, maxDisplayedValues, formatOverflowCount, formatSelectedCount, formatMaxSelectedHint, radius, unstyled, disabled, required, className, wrapperClassName, values, defaultValues, onChange, onDropdownClose, id, }: Readonly<MultiComboboxProps>): ReactElement;
790
+ export declare function MultiCombobox({ options, filterOption, label, description, 'aria-label': ariaLabel, error, placeholder, searchPlaceholder, noResultsText, clearLabel, clearText, maxSelectedValues, isOptionDisabled, maxDisplayedValues, formatOverflowCount, formatSelectedCount, formatMaxSelectedHint, radius, unstyled, disabled, required, className, wrapperClassName, dropdownClassName, optionLabelOverflow, values, defaultValues, onChange, onDropdownClose, id, }: Readonly<MultiComboboxProps>): ReactElement;
763
791
 
764
792
  export declare interface MultiComboboxOption {
765
793
  value: string;
@@ -767,6 +795,7 @@ export declare interface MultiComboboxOption {
767
795
  }
768
796
 
769
797
  export declare interface MultiComboboxProps {
798
+ /** Long lists render faster when the array keeps the same identity between renders (memoize it), and when `optionLabelOverflow` is not `'wrap'`. */
770
799
  options: readonly MultiComboboxOption[];
771
800
  /**
772
801
  * Decides whether an option matches the text typed in the search field.
@@ -795,6 +824,19 @@ export declare interface MultiComboboxProps {
795
824
  required?: boolean;
796
825
  className?: string;
797
826
  wrapperClassName?: string;
827
+ /**
828
+ * Classes for the dropdown panel, applied last so they can override its
829
+ * defaults, for example a width wider than the field when option labels are
830
+ * long (`w-96`, `min-w-80`). The panel is kept inside the viewport.
831
+ */
832
+ dropdownClassName?: string;
833
+ /**
834
+ * How option labels that do not fit on one line are shown.
835
+ * - `'wrap'` (default): labels wrap; every option is rendered, so lists of thousands of options open slower.
836
+ * - `'wrap-active'`: labels are truncated with a native tooltip, except the active option, which wraps in full; long lists only render the visible options. Recommended for long lists.
837
+ * - `'truncate'`: every label is truncated with a native tooltip, the active one included; keyboard and zoom users can no longer read long labels (WCAG 1.4.10), use only for labels known to be short.
838
+ */
839
+ optionLabelOverflow?: OptionLabelOverflow;
798
840
  values?: readonly string[];
799
841
  defaultValues?: readonly string[];
800
842
  onChange?: (values: readonly string[]) => void;
@@ -808,6 +850,8 @@ export declare interface MultiComboboxProps {
808
850
  id?: string;
809
851
  }
810
852
 
853
+ export declare type OptionLabelOverflow = 'wrap' | 'wrap-active' | 'truncate';
854
+
811
855
  export declare type RadioOption = ChoiceOption;
812
856
 
813
857
  export declare function RadioOptions({ options, orientation, value, defaultValue, onChange, name: nameProp, label, error, required, disabled, radius, unstyled, className, wrapperClassName, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<RadioOptionsProps>): JSX.Element;
@@ -955,7 +999,7 @@ export declare function splitIntoHighlightSegments(
955
999
  return segments;
956
1000
  }
957
1001
 
958
- export declare function Switch({ label, description, error, unstyled, fullWidth, className, wrapperClassName, checked, defaultChecked, onChange, id, 'aria-describedby': callerDescribedBy, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: Readonly<SwitchProps>): JSX.Element;
1002
+ export declare function Switch({ label, description, error, unstyled, fullWidth, className, wrapperClassName, checked, defaultChecked, onChange, id, 'aria-describedby': callerDescribedBy, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ref, ...props }: Readonly<SwitchProps>): JSX.Element;
959
1003
 
960
1004
  export declare interface SwitchProps extends Omit<ComponentPropsWithRef<'input'>, 'type' | 'className'> {
961
1005
  label?: string | ReactNode;
@@ -1052,7 +1096,10 @@ export declare type Theme = 'light' | 'dark' | 'system';
1052
1096
  * first paint, so a dark-mode visitor never sees a light flash. It mirrors
1053
1097
  * `useThemeStore`: the `dark` class is set for a stored `dark`, or for a
1054
1098
  * stored `system` (or a missing, invalid or unreadable value) while the OS
1055
- * prefers dark. Inline it in `<head>` before the stylesheet.
1099
+ * prefers dark. Every `<meta name="theme-color" data-light="..." data-dark="...">`
1100
+ * also gets its `content` set from `data-dark` or `data-light`; it must come
1101
+ * before this script in `<head>`. Inline the script in `<head>` before the
1102
+ * stylesheet.
1056
1103
  */
1057
1104
  export declare const THEME_INIT_SCRIPT: string;
1058
1105
 
@@ -1255,6 +1302,43 @@ export declare const useGlobalHotkeysStore: UseBoundStore<StoreApi<GlobalHotkeys
1255
1302
 
1256
1303
  export declare const useIsClient: () => boolean;
1257
1304
 
1305
+ /**
1306
+ * Makes a scroll container reachable by keyboard. When the container overflows
1307
+ * and holds nothing tabbable, `scrollableProps` gives it a tab stop, a
1308
+ * `region` role and an accessible name, and `focusClassName` a visible focus
1309
+ * outline. Otherwise only the `ref` is returned. Re-evaluated on resize and on
1310
+ * content changes; a container mounted after the first render is observed too.
1311
+ *
1312
+ * To combine `scrollableProps.ref` with your own ref, use a ref callback that
1313
+ * calls both.
1314
+ *
1315
+ * @example
1316
+ * const { scrollableProps, focusClassName } = useKeyboardScrollable({
1317
+ * label: 'Release notes',
1318
+ * })
1319
+ * return (
1320
+ * <div {...scrollableProps} className={cn('h-64 overflow-y-auto', focusClassName)}>
1321
+ * {content}
1322
+ * </div>
1323
+ * )
1324
+ */
1325
+ export declare function useKeyboardScrollable(options: Readonly<UseKeyboardScrollableOptions>): UseKeyboardScrollableReturn;
1326
+
1327
+ /** A scroll region must be named, exactly once: by `label` or by `labelledBy`. */
1328
+ export declare type UseKeyboardScrollableOptions = {
1329
+ label: string;
1330
+ labelledBy?: never;
1331
+ } | {
1332
+ labelledBy: string;
1333
+ label?: never;
1334
+ };
1335
+
1336
+ export declare type UseKeyboardScrollableReturn = {
1337
+ scrollableProps: KeyboardScrollableProps;
1338
+ /** Focus outline classes to add to the container's `className`; `undefined` when the container is not a tab stop. */
1339
+ focusClassName: string | undefined;
1340
+ };
1341
+
1258
1342
  export declare const useThemeStore: UseBoundStore<StoreApi<ThemeStore>>;
1259
1343
 
1260
1344
  /** Wraps `Component` so it only renders once the client has mounted. */
@@ -1268,3 +1352,10 @@ declare module 'vitest' {
1268
1352
  interface Assertion<T = unknown> extends AxeMatchers {}
1269
1353
  interface AsymmetricMatchersContaining extends AxeMatchers {}
1270
1354
  }
1355
+
1356
+ declare module 'vitest/browser' {
1357
+ interface BrowserCommands {
1358
+ /** Moves the real mouse cursor to a point outside the story document. */
1359
+ movePointerOutOfStory: () => Promise<void>;
1360
+ }
1361
+ }