@minimalstuff/ui 4.0.0 → 4.2.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
@@ -23,6 +23,10 @@ export function App() {
23
23
 
24
24
  The `@minimalstuff/ui/style.css` import is required for styles and icons to render.
25
25
 
26
+ ### Modals and drawers lock page scroll
27
+
28
+ 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.
29
+
26
30
  ## Development
27
31
 
28
32
  ```bash
@@ -39,11 +43,7 @@ CI (lint, test, build) and npm publish run together when a GitHub Release is pub
39
43
 
40
44
  ### Prerequisites
41
45
 
42
- 1. **NPM token**
43
- Create an automation token at [npmjs.com/access-tokens](https://www.npmjs.com/access-tokens) (scope: package publish for `@minimalstuff/ui`).
44
-
45
- 2. **Repository secret**
46
- In the repo: **Settings → Secrets and variables → Actions**. Add a secret named `NPM_TOKEN` with the token value.
46
+ Publishing uses npm [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC), no token needed. On npmjs.com, `@minimalstuff/ui` > **Settings > Trusted publishing** must list GitHub Actions with organization `minimalstuff`, repository `ui` and workflow `publish.yml`.
47
47
 
48
48
  ### Procedure
49
49
 
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ import { ReactElement } from 'react';
8
8
  import { ReactNode } from 'react';
9
9
  import { ReactPortal } from 'react';
10
10
  import { Ref } from 'react';
11
+ import { RefObject } from 'react';
11
12
  import { StoreApi } from 'zustand';
12
13
  import { UseBoundStore } from 'zustand';
13
14
 
@@ -134,7 +135,7 @@ export declare interface CharacterCountProps {
134
135
  className?: string;
135
136
  }
136
137
 
137
- export declare function Checkbox({ label, description, error, radius, unstyled, fullWidth, variant, className, wrapperClassName, checked, defaultChecked, onChange, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<CheckboxProps>): JSX.Element;
138
+ export declare function Checkbox({ label, indeterminate, description, error, radius, unstyled, fullWidth, variant, className, wrapperClassName, checked, defaultChecked, onChange, id, 'aria-describedby': callerDescribedBy, ref, ...props }: Readonly<CheckboxProps>): JSX.Element;
138
139
 
139
140
  export declare type CheckboxOption = ChoiceOption;
140
141
 
@@ -160,6 +161,8 @@ export declare interface CheckboxOptionsProps extends Omit<ComponentPropsWithRef
160
161
 
161
162
  export declare interface CheckboxProps extends Omit<ComponentPropsWithRef<'input'>, 'type' | 'className'> {
162
163
  label?: string | React.ReactNode;
164
+ /** Shows a dash instead of the checkmark and exposes the mixed state to assistive technology. */
165
+ indeterminate?: boolean;
163
166
  description?: string | React.ReactNode;
164
167
  error?: string;
165
168
  radius?: Radius;
@@ -273,6 +276,35 @@ export declare interface CopyButtonProps {
273
276
  onError?: (error: unknown) => void;
274
277
  }
275
278
 
279
+ /** A modal side panel with the same focus handling, stacking and footer slot as `Modal`. */
280
+ export declare function Drawer({ isOpen, onClose, size, side, radius, dismissible, ...dialogProps }: Readonly<DrawerProps>): JSX.Element | null;
281
+
282
+ export declare interface DrawerProps {
283
+ isOpen: boolean;
284
+ onClose: () => void;
285
+ /** Names the dialog. */
286
+ title: ReactNode;
287
+ children: ReactNode;
288
+ /** Pinned below the body; a `ModalFooter` inside `children` renders in the same place. */
289
+ footer?: ReactNode;
290
+ size?: DrawerSize;
291
+ /** The edge of the viewport the panel is attached to. */
292
+ side?: DrawerSide;
293
+ /** Applied to the corners on the edge facing the page only. */
294
+ radius?: Radius;
295
+ /** Applied to the scrollable body. */
296
+ className?: string;
297
+ /** `false` hides the close button and blocks Escape and backdrop dismissal. */
298
+ dismissible?: boolean;
299
+ closeLabel?: string;
300
+ /** Wins over the first-tabbable rule for where focus lands on open; may point at an element in the body or in the footer. */
301
+ initialFocusRef?: RefObject<HTMLElement | null>;
302
+ }
303
+
304
+ export declare type DrawerSide = 'right' | 'left';
305
+
306
+ export declare type DrawerSize = 'sm' | 'md' | 'lg';
307
+
276
308
  declare interface GlobalHotkeysStore {
277
309
  globalHotkeysEnabled: boolean;
278
310
  setGlobalHotkeysEnabled: (value: boolean) => void;
@@ -298,6 +330,7 @@ export declare type IconButtonProps = ComponentPropsWithRef<'button'> & {
298
330
  'aria-label': string;
299
331
  size?: ControlSize;
300
332
  children?: ReactNode;
333
+ tooltip?: IconButtonTooltip;
301
334
  } & IconButtonStyle;
302
335
 
303
336
  declare type IconButtonStyle = {
@@ -309,10 +342,28 @@ declare type IconButtonStyle = {
309
342
  radius?: Radius;
310
343
  };
311
344
 
312
- export declare function Input({ label, error, showCharCount, minLength, maxLength, radius, unstyled, className, wrapperClassName, value, defaultValue, onChange, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<InputProps>): JSX.Element;
345
+ /**
346
+ * Wraps the button in a `Tooltip`. `true` shows the `aria-label` at the
347
+ * default position, a position string moves it, and an object also allows
348
+ * custom `content`. When the tooltip repeats the `aria-label` it does not
349
+ * describe the button, so screen readers do not announce the name twice.
350
+ */
351
+ export declare type IconButtonTooltip =
352
+ | boolean
353
+ | TooltipPosition
354
+ | IconButtonTooltipOptions;
355
+
356
+ declare interface IconButtonTooltipOptions {
357
+ position?: TooltipPosition;
358
+ /** Defaults to the button's `aria-label`. */
359
+ content?: ReactNode;
360
+ }
361
+
362
+ export declare function Input({ label, description, error, showCharCount, minLength, maxLength, radius, unstyled, className, wrapperClassName, value, defaultValue, onChange, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<InputProps>): JSX.Element;
313
363
 
314
364
  export declare interface InputProps extends ComponentPropsWithRef<'input'> {
315
365
  label?: string | ReactNode;
366
+ description?: string | ReactNode;
316
367
  error?: string;
317
368
  showCharCount?: boolean;
318
369
  minLength?: number;
@@ -481,22 +532,31 @@ export declare const Modal: Callable<ModalProps, void, {}>;
481
532
  * Declares `children` as the modal's pinned footer, wherever this is
482
533
  * rendered in the content tree — including inside a separate content
483
534
  * component, any number of levels below `Modal`'s own `children`. Renders
484
- * nothing itself; `Modal` places the actual footer in its own layout.
535
+ * nothing in place; the children are portaled into the modal's own footer
536
+ * slot, so they stay React children of this component.
485
537
  */
486
- export declare function ModalFooter({ children }: Readonly<ModalFooterProps>): null;
538
+ export declare function ModalFooter({ children }: Readonly<ModalFooterProps>): ReactPortal | null;
487
539
 
488
540
  export declare interface ModalFooterProps {
489
541
  children: ReactNode;
490
542
  }
491
543
 
544
+ export declare type ModalHeight = 'auto' | 'fixed';
545
+
492
546
  export declare interface ModalProps {
493
547
  title?: ReactNode;
494
548
  children: ModalRenderProp;
495
549
  size?: ModalSize;
550
+ /** `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. */
551
+ height?: ModalHeight;
496
552
  radius?: Radius;
497
553
  className?: string;
498
554
  dismissible?: boolean;
499
555
  closeLabel?: string;
556
+ /** `alertdialog` for urgent messages that need a response (describes the dialog by its content); pair with `dismissible={false}` when it must not be dismissed. */
557
+ role?: ModalRole;
558
+ /** Wins over the first-tabbable rule for where focus lands on open; may point at an element in the content or in a `ModalFooter`. */
559
+ initialFocusRef?: RefObject<HTMLElement | null>;
500
560
  /** Names the dialog when there is no `title`; ignored when `title` is set since the title labels it. */
501
561
  'aria-label'?: string;
502
562
  }
@@ -505,6 +565,8 @@ export declare function ModalProvider(): JSX.Element;
505
565
 
506
566
  declare type ModalRenderProp = ReactNode | ((close: () => void) => ReactNode);
507
567
 
568
+ export declare type ModalRole = 'dialog' | 'alertdialog';
569
+
508
570
  export declare type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
509
571
 
510
572
  /**
@@ -577,7 +639,7 @@ export declare interface RadioOptionsProps extends Omit<ComponentPropsWithRef<'f
577
639
 
578
640
  declare type Radius = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
579
641
 
580
- export declare function Select({ options, label, error, placeholder, radius, unstyled, className, wrapperClassName, value, defaultValue, onChange, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<SelectProps>): JSX.Element;
642
+ export declare function Select({ options, label, description, error, placeholder, radius, unstyled, className, wrapperClassName, value, defaultValue, onChange, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<SelectProps>): JSX.Element;
581
643
 
582
644
  export declare interface SelectOption {
583
645
  value: string;
@@ -587,6 +649,7 @@ export declare interface SelectOption {
587
649
  export declare interface SelectProps extends Omit<ComponentPropsWithRef<'select'>, 'children'> {
588
650
  options: SelectOption[];
589
651
  label?: string | ReactNode;
652
+ description?: string | ReactNode;
590
653
  error?: string;
591
654
  placeholder?: string;
592
655
  radius?: Radius;
@@ -652,24 +715,52 @@ export declare interface TabItem {
652
715
  content: ReactNode;
653
716
  disabled?: boolean;
654
717
  icon?: string;
718
+ /**
719
+ * Rendered after the title inside the tab button and part of the tab's
720
+ * accessible name. Meant for short text such as an error count ("1 error"),
721
+ * never a coloured dot alone. Separate from `title`, which is the React key
722
+ * and must stay stable.
723
+ */
724
+ badge?: ReactNode;
655
725
  }
656
726
 
657
- export declare function Tabs({ items, value, defaultIndex, variant, size, radius, fullWidth, unstyled, animated, className, tabListClassName, panelClassName, onChange, }: Readonly<TabsProps>): JSX.Element;
727
+ export declare function Tabs({ items, value, defaultIndex, variant, orientation, size, radius, fullWidth, unstyled, animated, keepMounted, className, tabListClassName, panelClassName, onChange, }: Readonly<TabsProps>): JSX.Element;
728
+
729
+ export declare type TabsOrientation = 'horizontal' | 'vertical';
658
730
 
659
731
  export declare interface TabsProps {
660
732
  items: TabItem[];
661
733
  value?: number;
662
734
  defaultIndex?: number;
663
735
  variant?: TabsVariant;
736
+ /**
737
+ * Direction the tabs are laid out in. `vertical` stacks the tablist beside
738
+ * the panel and navigates with ArrowDown/ArrowUp instead of
739
+ * ArrowRight/ArrowLeft. Give the root a height (e.g. `className="min-h-0
740
+ * flex-1"` in a fixed-height parent) and the tablist and panel scroll
741
+ * independently. Defaults to `horizontal`.
742
+ */
743
+ orientation?: TabsOrientation;
664
744
  size?: ControlSize;
665
745
  /**
666
746
  * Only visibly rounds the `segmented` variant's track and tabs. The `line`
667
747
  * variant has no filled surface to round, so radius is a no-op there.
668
748
  */
669
749
  radius?: Radius;
750
+ /** Stretches the tabs across the tablist. A no-op when `orientation` is `vertical`. */
670
751
  fullWidth?: boolean;
671
752
  unstyled?: boolean;
672
753
  animated?: boolean;
754
+ /**
755
+ * Keeps every panel in the DOM and only hides the inactive ones, so React
756
+ * state (typed input, scroll position) inside a panel survives tab
757
+ * switches. Every tab then carries `aria-controls` pointing at its own
758
+ * panel. When `false`, only the active panel is rendered, so only the
759
+ * active tab carries `aria-controls` (it must reference an existing
760
+ * element; a dangling id fails `aria-valid-attr-value`). Defaults to
761
+ * `false`.
762
+ */
763
+ keepMounted?: boolean;
673
764
  className?: string;
674
765
  tabListClassName?: string;
675
766
  panelClassName?: string;
@@ -678,10 +769,11 @@ export declare interface TabsProps {
678
769
 
679
770
  export declare type TabsVariant = 'line' | 'segmented';
680
771
 
681
- export declare function Textarea({ label, error, showCharCount, minLength, maxLength, radius, unstyled, className, wrapperClassName, value, defaultValue, onChange, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<TextareaProps>): JSX.Element;
772
+ export declare function Textarea({ label, description, error, showCharCount, minLength, maxLength, radius, unstyled, className, wrapperClassName, value, defaultValue, onChange, id, 'aria-describedby': callerDescribedBy, ...props }: Readonly<TextareaProps>): JSX.Element;
682
773
 
683
774
  export declare interface TextareaProps extends ComponentPropsWithRef<'textarea'> {
684
775
  label?: string | ReactNode;
776
+ description?: string | ReactNode;
685
777
  error?: string;
686
778
  showCharCount?: boolean;
687
779
  minLength?: number;
@@ -699,9 +791,15 @@ declare interface ThemeStore {
699
791
  setTheme: (theme: Theme) => void;
700
792
  }
701
793
 
702
- export declare function ThemeToggle({ isTransitionEnabled, transitionDuration, transitionEasing, size, radius, variant, getAriaLabel, }: Readonly<ThemeToggleProps>): JSX.Element;
794
+ export declare function ThemeToggle({ isTransitionEnabled, transitionDuration, transitionEasing, size, radius, variant, getAriaLabel, tooltip, }: Readonly<ThemeToggleProps>): JSX.Element;
703
795
 
704
796
  export declare interface ThemeToggleProps {
797
+ /**
798
+ * Animate the theme change with the View Transition API.
799
+ * Defaults to automatic: enabled, except on WebKitGTK where
800
+ * `startViewTransition` crashes the web process (2.54).
801
+ * `prefers-reduced-motion` is always respected.
802
+ */
705
803
  isTransitionEnabled?: boolean;
706
804
  transitionDuration?: number;
707
805
  transitionEasing?: string;
@@ -709,15 +807,42 @@ export declare interface ThemeToggleProps {
709
807
  radius?: Radius;
710
808
  variant?: ButtonVariant;
711
809
  getAriaLabel?: (theme: Theme) => string;
810
+ tooltip?: IconButtonTooltip;
712
811
  }
713
812
 
813
+ /**
814
+ * A group of related controls with a single tab stop (WAI-ARIA toolbar).
815
+ * Tab enters the toolbar on the last focused item (the first enabled one
816
+ * initially); the arrow keys of the orientation (Left/Right horizontally,
817
+ * Up/Down vertically) move focus between items and wrap at the ends, Home and
818
+ * End jump to the first and last item, and disabled items are skipped. Arrow
819
+ * keys are left alone inside text fields. Requires `aria-label` or
820
+ * `aria-labelledby`.
821
+ */
822
+ export declare function Toolbar(toolbarProps: Readonly<ToolbarProps>): JSX.Element;
823
+
824
+ declare type ToolbarAccessibleName = {
825
+ 'aria-label': string;
826
+ 'aria-labelledby'?: undefined;
827
+ } | {
828
+ 'aria-labelledby': string;
829
+ 'aria-label'?: undefined;
830
+ };
831
+
832
+ declare type ToolbarOrientation = 'horizontal' | 'vertical';
833
+
834
+ export declare type ToolbarProps = Omit<ComponentPropsWithRef<'div'>, 'role' | 'aria-label' | 'aria-labelledby' | 'aria-orientation'> & {
835
+ orientation?: ToolbarOrientation;
836
+ unstyled?: boolean;
837
+ } & ToolbarAccessibleName;
838
+
714
839
  /**
715
840
  * The trigger (`children`) must be focusable, otherwise the tooltip is
716
841
  * unreachable by keyboard. Shows on hover and focus; Escape dismisses it.
717
842
  * `temporaryContent` is announced to screen readers through a polite status
718
843
  * region.
719
844
  */
720
- export declare function Tooltip({ content, children, position, showOnClick, temporaryContent, temporaryDuration, disabled, radius, onTemporaryShow, className, ref, }: Readonly<TooltipProps>): JSX.Element;
845
+ export declare function Tooltip({ content, children, position, showOnClick, temporaryContent, temporaryDuration, disabled, radius, onTemporaryShow, className, describeChild, portalContainer, ref, }: Readonly<TooltipProps>): JSX.Element;
721
846
 
722
847
  export declare type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
723
848
 
@@ -732,10 +857,76 @@ export declare interface TooltipProps {
732
857
  radius?: Radius;
733
858
  onTemporaryShow?: () => void;
734
859
  className?: string;
860
+ /**
861
+ * Whether the trigger is described by the tooltip (`aria-describedby`).
862
+ * `true` always describes, `false` never does. By default the trigger is
863
+ * described unless the tooltip text repeats its accessible name, which
864
+ * screen readers would otherwise announce twice.
865
+ */
866
+ describeChild?: boolean;
867
+ /**
868
+ * Where the tooltip is rendered. Defaults to the trigger's closest
869
+ * landmark or dialog (so it stays inside a landmark and inside an open
870
+ * modal), falling back to `document.body`. Transformed ancestors (such as the modal
871
+ * panel) can clip it; pass `portalContainer` to escape.
872
+ */
873
+ portalContainer?: HTMLElement | null;
735
874
  /** Attaches to the wrapper `<span>` around `children`, alongside `Tooltip`'s own internal ref. */
736
875
  ref?: Ref<HTMLSpanElement>;
737
876
  }
738
877
 
878
+ export declare interface UILabels {
879
+ /** Accessible name of the modal close button. */
880
+ close: string;
881
+ /** Confirm button of `ConfirmModal`. */
882
+ confirm: string;
883
+ /** Cancel button of `ConfirmModal`. */
884
+ cancel: string;
885
+ /** Empty state of `Combobox` and `MultiCombobox`. */
886
+ noResults: string;
887
+ /** Accessible name of the button clearing a selection. */
888
+ clearSelection: string;
889
+ /** Placeholder and accessible name of the `MultiCombobox` search field. */
890
+ search: string;
891
+ /** Visible text of the `MultiCombobox` clear button. */
892
+ clear: string;
893
+ /** Screen-reader suffix of a required `CheckboxOptions` group. */
894
+ required: string;
895
+ /** Accessible name of `ThemeToggle` for the current theme. */
896
+ currentTheme: (theme: Theme) => string;
897
+ /** `MultiCombobox` badge summarising the values that do not fit. */
898
+ overflowCount: (hiddenCount: number) => string;
899
+ /** `MultiCombobox` summary of the number of selected values. */
900
+ selectedCount: (selectedCount: number) => string;
901
+ /** `MultiCombobox` hint shown when the selection limit is reached. */
902
+ maxSelectedHint: (maxSelectedValues: number) => string;
903
+ }
904
+
905
+ /**
906
+ * Provides default user-facing labels (close, confirm, no results...) to every
907
+ * component below it. Precedence: explicit prop > provider > English default.
908
+ *
909
+ * `Modal` and `ConfirmModal` render where `<ModalProvider />` is mounted, so
910
+ * mount `ModalProvider` inside `UIProvider` for them to receive the labels.
911
+ */
912
+ export declare function UIProvider({ labels, announceFieldErrors, children, }: Readonly<UIProviderProps>): JSX.Element;
913
+
914
+ export declare interface UIProviderProps {
915
+ /** Overrides the default labels. Nested providers merge over their parent. */
916
+ labels?: Partial<UILabels>;
917
+ /**
918
+ * Whether field errors are announced as they appear (`role="alert"`).
919
+ * Defaults to `true`; nested providers inherit the parent value unless set.
920
+ * Set it to `false` when the app announces errors itself (an error summary
921
+ * or focus on the first invalid field): it avoids several alerts firing at
922
+ * once on submit, and lets WebKitGTK re-read the error on focus, since it
923
+ * leaves `role="alert"` content out of the computed description. The error
924
+ * stays linked by `aria-describedby` either way.
925
+ */
926
+ announceFieldErrors?: boolean;
927
+ children: ReactNode;
928
+ }
929
+
739
930
  export declare function useDisableHotkeysWhileMounted(): void;
740
931
 
741
932
  export declare const useGlobalHotkeysStore: UseBoundStore<StoreApi<GlobalHotkeysStore>>;
@@ -748,3 +939,10 @@ export declare const useThemeStore: UseBoundStore<StoreApi<ThemeStore>>;
748
939
  export declare const withClientOnly: <TProps extends object>(Component: React.ComponentType<TProps>) => React.ComponentType<TProps>;
749
940
 
750
941
  export { }
942
+
943
+
944
+
945
+ declare module 'vitest' {
946
+ interface Assertion<T = unknown> extends AxeMatchers {}
947
+ interface AsymmetricMatchersContaining extends AxeMatchers {}
948
+ }