@minimalstuff/ui 5.0.1 → 5.1.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
@@ -42,10 +42,31 @@ export function Root() {
42
42
  }
43
43
  ```
44
44
 
45
+ For French, pass the bundled preset: `<UIProvider labels={FRENCH_UI_LABELS}>` (import `FRENCH_UI_LABELS` from `@minimalstuff/ui`).
46
+
45
47
  ### Dark mode
46
48
 
47
49
  Dark styles apply when `<html>` has the `dark` class. `useThemeStore` (and the `ThemeToggle` built on it) sets that class from the `light`, `dark` or `system` preference and keeps it in `localStorage`.
48
50
 
51
+ To avoid a light flash before your bundle runs, inline `THEME_INIT_SCRIPT` in `<head>`, before the stylesheet. It reads the same `localStorage` key as the store and sets the `dark` class the same way.
52
+
53
+ ```tsx
54
+ import { THEME_INIT_SCRIPT } from '@minimalstuff/ui';
55
+
56
+ <head>
57
+ <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
58
+ <link rel="stylesheet" href="/style.css" />
59
+ </head>;
60
+ ```
61
+
62
+ 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
+
64
+ ```bash
65
+ node --input-type=module -e "import { THEME_INIT_SCRIPT } from '@minimalstuff/ui'; import { createHash } from 'node:crypto'; console.log(createHash('sha256').update(THEME_INIT_SCRIPT).digest('base64'))"
66
+ ```
67
+
68
+ The hash changes if a release changes the script: recompute it when you upgrade.
69
+
49
70
  ### Modals and drawers lock page scroll
50
71
 
51
72
  While a `Modal` or `Drawer` is open, page scroll is locked on `<html>`. The scrollbar width this frees is compensated with right padding on `<html>`, so the page does not shift. That padding does not move `position: fixed` elements: anchor right-aligned ones with `right: var(--scrollbar-compensation, 0px)`. Paint the page background on `<html>` or `<body>` rather than only on a wrapper element, or the padding shows the root's background as a strip on the right.
package/dist/index.d.ts CHANGED
@@ -13,6 +13,27 @@ import { RefObject } from 'react';
13
13
  import { StoreApi } from 'zustand';
14
14
  import { UseBoundStore } from 'zustand';
15
15
 
16
+ /**
17
+ * A thin application bar with a `start` slot on the left, a centered `title`
18
+ * and an `end` slot on the right. It is a plain `<header>` (the `banner`
19
+ * landmark when it is not nested in another sectioning element), not a
20
+ * WAI-ARIA toolbar: put your own links and buttons in the slots, or use
21
+ * `Toolbar` for a group of controls with arrow-key navigation. Its minimum
22
+ * height grows with enlarged text and its horizontal padding honours the
23
+ * device safe area.
24
+ */
25
+ export declare function AppBar(appBarProps: Readonly<AppBarProps>): JSX.Element;
26
+
27
+ export declare type AppBarProps = Omit<ComponentPropsWithRef<'header'>, 'title'> & {
28
+ start?: ReactNode;
29
+ title?: ReactNode;
30
+ end?: ReactNode;
31
+ startClassName?: string;
32
+ titleClassName?: string;
33
+ endClassName?: string;
34
+ unstyled?: boolean;
35
+ };
36
+
16
37
  /**
17
38
  * The picture of a person, or the initial of `name` when there is none.
18
39
  * Labelled with the full `name` so screen readers announce the person rather
@@ -64,6 +85,31 @@ export declare const BASE_INPUT_STYLES = "w-full border border-gray-300 dark:bor
64
85
 
65
86
  export declare function Button(buttonProps: Readonly<ButtonProps>): JSX.Element;
66
87
 
88
+ /**
89
+ * The exact class string `Button` renders, for styling another element as a
90
+ * button, typically a router `<Link>` or an `<a>`. Keyboard focus ring included.
91
+ * For a selected link, add `pressed` and `aria-current`, not `aria-pressed`.
92
+ */
93
+ export declare function buttonClassName(options?: Readonly<ButtonClassNameOptions>): string;
94
+
95
+ export declare type ButtonClassNameOptions = {
96
+ variant?: ButtonVariant;
97
+ color?: ButtonColor;
98
+ size?: ControlSize;
99
+ radius?: Radius;
100
+ fullWidth?: boolean;
101
+ /**
102
+ * Applies the pressed look, for a real `<button>` toggle paired with
103
+ * `aria-pressed`. Never put `aria-pressed` on a link (not allowed on role
104
+ * link): on an `<a>`, pair the selected look with `aria-current`, for
105
+ * example `aria-current="page"`.
106
+ */
107
+ pressed?: boolean;
108
+ /** Keeps only the layout classes, like `Button`'s `unstyled` prop. */
109
+ unstyled?: boolean;
110
+ className?: string;
111
+ };
112
+
67
113
  declare type ButtonColor =
68
114
  | 'primary'
69
115
  | 'neutral'
@@ -85,6 +131,11 @@ export declare type ButtonProps = Omit<ComponentPropsWithRef<'button'>, 'childre
85
131
  * the tab order, exposed as `aria-disabled`.
86
132
  */
87
133
  loading?: boolean;
134
+ /**
135
+ * Turns the button into a toggle: sets `aria-pressed` and a pressed look (dark fill and inset ring).
136
+ * Leave it `undefined` for a regular button (no attribute).
137
+ */
138
+ pressed?: boolean;
88
139
  startIcon?: string;
89
140
  endIcon?: string;
90
141
  } & ButtonStyle;
@@ -153,7 +204,7 @@ export declare type CheckboxOption = ChoiceOption;
153
204
  export declare function CheckboxOptions({ options, orientation, values, defaultValues, onChange, name: nameProp, label, error, required, requiredLabel, disabled, radius, unstyled, className, wrapperClassName, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<CheckboxOptionsProps>): JSX.Element;
154
205
 
155
206
  export declare interface CheckboxOptionsProps extends Omit<ComponentPropsWithRef<'fieldset'>, 'onChange'> {
156
- options: string[] | CheckboxOption[];
207
+ options: readonly string[] | readonly CheckboxOption[];
157
208
  orientation?: 'vertical' | 'horizontal';
158
209
  values?: readonly string[];
159
210
  defaultValues?: readonly string[];
@@ -193,13 +244,13 @@ export declare interface ChoiceOption {
193
244
  disabled?: boolean;
194
245
  }
195
246
 
196
- export declare function ClientOnly({ children, fallback }: Readonly<ClientOnlyProps>): string | number | bigint | boolean | JSX.Element | Iterable< ReactNode> | Promise<string | number | bigint | boolean | ReactPortal | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable< ReactNode> | null | undefined> | null;
247
+ export declare function ClientOnly({ children, fallback }: Readonly<ClientOnlyProps>): string | number | bigint | boolean | Iterable< ReactNode> | Promise<string | number | bigint | boolean | ReactPortal | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable< ReactNode> | null | undefined> | JSX.Element | null;
197
248
 
198
249
  export declare interface ClientOnlyProps extends React.PropsWithChildren {
199
250
  fallback?: React.ReactNode;
200
251
  }
201
252
 
202
- export declare function Combobox({ options, label, description, 'aria-label': ariaLabel, error, placeholder, noResultsText, clearLabel, radius, unstyled, disabled, required, className, wrapperClassName, value, defaultValue, onChange, id, }: Readonly<ComboboxProps>): JSX.Element;
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;
203
254
 
204
255
  export declare interface ComboboxOption {
205
256
  value: string;
@@ -207,7 +258,13 @@ export declare interface ComboboxOption {
207
258
  }
208
259
 
209
260
  export declare interface ComboboxProps {
210
- options: ComboboxOption[];
261
+ options: readonly ComboboxOption[];
262
+ /**
263
+ * Decides whether an option matches the text typed in the field. Replaces
264
+ * the default case-insensitive "label contains the query" match, for
265
+ * example to ignore accents.
266
+ */
267
+ filterOption?: (option: ComboboxOption, query: string) => boolean;
211
268
  label?: string | ReactNode;
212
269
  description?: string | ReactNode;
213
270
  'aria-label'?: string;
@@ -289,14 +346,20 @@ export declare interface ContextMenuProps {
289
346
  /** The size scale every inline control shares (Button, IconButton, Badge, Kbd). */
290
347
  declare type ControlSize = 'xs' | 'sm' | 'md' | 'lg';
291
348
 
292
- export declare function CopyButton({ value, children, onError, }: Readonly<CopyButtonProps>): JSX.Element;
349
+ export declare function CopyButton({ value, children, label, onError, }: Readonly<CopyButtonProps>): JSX.Element;
293
350
 
294
351
  export declare interface CopyButtonProps {
295
352
  value: string;
296
- children: (props: {
353
+ /**
354
+ * Render prop for a custom control. Omit it to get the default icon button,
355
+ * which swaps its copy icon for a check while `copied`.
356
+ */
357
+ children?: (props: {
297
358
  copied: boolean;
298
359
  copy: () => void | Promise<void>;
299
360
  }) => ReactNode;
361
+ /** Accessible name of the default icon button. Defaults to `labels.copy`. */
362
+ label?: string;
300
363
  onError?: (error: unknown) => void;
301
364
  }
302
365
 
@@ -342,6 +405,9 @@ export declare type DrawerSide = 'right' | 'left';
342
405
 
343
406
  export declare type DrawerSize = 'sm' | 'md' | 'lg';
344
407
 
408
+ /** French translation of every `UILabels` entry, ready for `<UIProvider labels={FRENCH_UI_LABELS}>`. */
409
+ export declare const FRENCH_UI_LABELS: UILabels;
410
+
345
411
  declare interface GlobalHotkeysStore {
346
412
  globalHotkeysEnabled: boolean;
347
413
  setGlobalHotkeysEnabled: (value: boolean) => void;
@@ -383,6 +449,11 @@ export declare type IconButtonProps = ComponentPropsWithRef<'button'> & {
383
449
  size?: ControlSize;
384
450
  children?: ReactNode;
385
451
  tooltip?: IconButtonTooltip;
452
+ /**
453
+ * Turns the button into a toggle: sets `aria-pressed` and a pressed look (dark fill and inset ring).
454
+ * Leave it `undefined` for a regular button (no attribute).
455
+ */
456
+ pressed?: boolean;
386
457
  } & IconButtonStyle;
387
458
 
388
459
  declare type IconButtonStyle = {
@@ -670,7 +741,7 @@ export declare type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
670
741
  * pills opens a searchable dropdown of checkbox-style options. Selecting an
671
742
  * option never closes the dropdown, unlike the single-select `Combobox`.
672
743
  */
673
- export declare function MultiCombobox({ options, 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;
744
+ 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;
674
745
 
675
746
  export declare interface MultiComboboxOption {
676
747
  value: string;
@@ -679,6 +750,12 @@ export declare interface MultiComboboxOption {
679
750
 
680
751
  export declare interface MultiComboboxProps {
681
752
  options: readonly MultiComboboxOption[];
753
+ /**
754
+ * Decides whether an option matches the text typed in the search field.
755
+ * Replaces the default case-insensitive "label contains the query" match,
756
+ * for example to ignore accents.
757
+ */
758
+ filterOption?: (option: MultiComboboxOption, query: string) => boolean;
682
759
  label?: string | ReactNode;
683
760
  description?: string | ReactNode;
684
761
  'aria-label'?: string;
@@ -718,7 +795,7 @@ export declare type RadioOption = ChoiceOption;
718
795
  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;
719
796
 
720
797
  export declare interface RadioOptionsProps extends Omit<ComponentPropsWithRef<'fieldset'>, 'onChange'> {
721
- options: string[] | RadioOption[];
798
+ options: readonly string[] | readonly RadioOption[];
722
799
  orientation?: 'vertical' | 'horizontal';
723
800
  value?: string;
724
801
  defaultValue?: string;
@@ -744,7 +821,7 @@ export declare interface SelectOption {
744
821
  }
745
822
 
746
823
  export declare interface SelectProps extends Omit<ComponentPropsWithRef<'select'>, 'children'> {
747
- options: SelectOption[];
824
+ options: readonly SelectOption[];
748
825
  label?: string | ReactNode;
749
826
  description?: string | ReactNode;
750
827
  error?: string;
@@ -755,6 +832,28 @@ export declare interface SelectProps extends Omit<ComponentPropsWithRef<'select'
755
832
  wrapperClassName?: string;
756
833
  }
757
834
 
835
+ /**
836
+ * A loading indicator exposed as a `role="status"` live region. It uses the
837
+ * icon `Button` shows while loading; under `prefers-reduced-motion` it swaps
838
+ * to a static icon.
839
+ *
840
+ * The region is empty on first render and filled right after mount, because
841
+ * many screen readers ignore a live region inserted already filled. The most
842
+ * robust setup is still a persistent `role="status"` wrapper in your own
843
+ * markup that stays mounted while a `Spinner` appears and disappears inside it.
844
+ */
845
+ export declare function Spinner(spinnerProps: Readonly<SpinnerProps>): JSX.Element;
846
+
847
+ export declare type SpinnerProps = Omit<ComponentPropsWithRef<'span'>, 'children'> & {
848
+ size?: ControlSize;
849
+ /** Visible text next to the icon. Without it, `labels.loading` is announced instead. */
850
+ label?: string;
851
+ /** Strips the built-in layout, text size and icon size; size the icon with `iconClassName`. */
852
+ unstyled?: boolean;
853
+ iconClassName?: string;
854
+ labelClassName?: string;
855
+ };
856
+
758
857
  /**
759
858
  * A primary action joined to a chevron `Menu` trigger that offers secondary
760
859
  * variants of that action. Both halves share variant, color, size and radius:
@@ -869,7 +968,7 @@ export declare function Tabs({ items, value, defaultIndex, variant, orientation,
869
968
  export declare type TabsOrientation = 'horizontal' | 'vertical';
870
969
 
871
970
  export declare interface TabsProps {
872
- items: TabItem[];
971
+ items: readonly TabItem[];
873
972
  value?: number;
874
973
  defaultIndex?: number;
875
974
  variant?: TabsVariant;
@@ -930,6 +1029,15 @@ export declare interface TextareaProps extends ComponentPropsWithRef<'textarea'>
930
1029
 
931
1030
  export declare type Theme = 'light' | 'dark' | 'system';
932
1031
 
1032
+ /**
1033
+ * A self-contained script that applies the saved theme to `<html>` before the
1034
+ * first paint, so a dark-mode visitor never sees a light flash. It mirrors
1035
+ * `useThemeStore`: the `dark` class is set for a stored `dark`, or for a
1036
+ * stored `system` (or a missing, invalid or unreadable value) while the OS
1037
+ * prefers dark. Inline it in `<head>` before the stylesheet.
1038
+ */
1039
+ export declare const THEME_INIT_SCRIPT: string;
1040
+
933
1041
  declare interface ThemeStore {
934
1042
  theme: Theme;
935
1043
  setTheme: (theme: Theme) => void;
@@ -1060,8 +1168,14 @@ export declare interface UILabels {
1060
1168
  required: string;
1061
1169
  /** Screen-reader announcement of `CopyButton` after a successful copy. */
1062
1170
  copied: string;
1063
- /** Screen-reader text added to a `Button` while it is loading. */
1171
+ /** Screen-reader text added to a `Button` while it is loading, and announced by `Spinner` without a visible label. */
1064
1172
  loading: string;
1173
+ /**
1174
+ * Accessible name of the default `CopyButton` icon button. Optional so that
1175
+ * existing complete label objects keep compiling: a translated label set
1176
+ * must add it, otherwise the English "Copy" is used.
1177
+ */
1178
+ copy?: string;
1065
1179
  /** Screen-reader announcement of `CharacterCount` near and at the limit. */
1066
1180
  charactersRemaining: (remainingCount: number) => string;
1067
1181
  /** Accessible name of `ThemeToggle` for the current theme. */