@ahrowe/ui 0.9.0 → 0.11.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 (60) hide show
  1. package/dist/esm/common/animatedText/animatedText.mjs +2 -0
  2. package/dist/esm/common/animatedText/animatedText.mjs.map +1 -0
  3. package/dist/esm/common/animatedText/animatedText.module.mjs +2 -0
  4. package/dist/esm/common/animatedText/animatedText.module.mjs.map +1 -0
  5. package/dist/esm/common/animatedText/animatedText.types.mjs +2 -0
  6. package/dist/esm/common/animatedText/animatedText.types.mjs.map +1 -0
  7. package/dist/esm/common/floatingMenu/floatingMenu.mjs +1 -1
  8. package/dist/esm/common/floatingMenu/floatingMenu.mjs.map +1 -1
  9. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +1 -1
  10. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +1 -1
  11. package/dist/esm/common/hooks/useSwipeDismiss.mjs +2 -0
  12. package/dist/esm/common/hooks/useSwipeDismiss.mjs.map +1 -0
  13. package/dist/esm/common/popover/popover.mjs +2 -0
  14. package/dist/esm/common/popover/popover.mjs.map +1 -0
  15. package/dist/esm/common/popover/popover.module.mjs +2 -0
  16. package/dist/esm/common/popover/popover.module.mjs.map +1 -0
  17. package/dist/esm/common/popover/popover.types.mjs +2 -0
  18. package/dist/esm/common/popover/popover.types.mjs.map +1 -0
  19. package/dist/esm/common/popover/usePopoverPosition.mjs +2 -0
  20. package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -0
  21. package/dist/esm/common/stepper/stepper.mjs +1 -1
  22. package/dist/esm/common/stepper/stepper.mjs.map +1 -1
  23. package/dist/esm/common/stepper/stepper.module.mjs +1 -1
  24. package/dist/esm/common/stepper/stepper.module.mjs.map +1 -1
  25. package/dist/esm/common/stepper/stepper.types.mjs +2 -0
  26. package/dist/esm/common/stepper/stepper.types.mjs.map +1 -0
  27. package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
  28. package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
  29. package/dist/esm/common/toast/toast.mjs +1 -1
  30. package/dist/esm/common/toast/toast.mjs.map +1 -1
  31. package/dist/esm/common/toast/toast.module.mjs.map +1 -1
  32. package/dist/esm/common/utils/scrollAncestors.mjs +2 -0
  33. package/dist/esm/common/utils/scrollAncestors.mjs.map +1 -0
  34. package/dist/esm/index.mjs +1 -1
  35. package/dist/esm/logo.svg +4 -0
  36. package/dist/index.cjs +4 -4
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/logo.svg +4 -0
  39. package/dist/style.css +1 -1
  40. package/dist/types/package/common/animatedText/animatedText.d.ts +4 -0
  41. package/dist/types/package/common/animatedText/animatedText.types.d.ts +59 -0
  42. package/dist/types/package/common/animatedText/index.d.ts +2 -0
  43. package/dist/types/package/common/hooks/useSwipeDismiss.d.ts +33 -0
  44. package/dist/types/package/common/popover/index.d.ts +2 -0
  45. package/dist/types/package/common/popover/popover.d.ts +3 -0
  46. package/dist/types/package/common/popover/popover.types.d.ts +58 -0
  47. package/dist/types/package/common/popover/usePopoverPosition.d.ts +23 -0
  48. package/dist/types/package/common/stepper/index.d.ts +1 -0
  49. package/dist/types/package/common/stepper/stepper.d.ts +1 -1
  50. package/dist/types/package/common/stepper/stepper.types.d.ts +61 -5
  51. package/dist/types/package/common/themeProvider/theme.types.d.ts +22 -0
  52. package/dist/types/package/common/utils/scrollAncestors.d.ts +2 -0
  53. package/dist/types/package/index.d.ts +4 -0
  54. package/docs/AnimatedText.md +94 -0
  55. package/docs/CLAUDE.md +2 -0
  56. package/docs/FloatingMenu.md +2 -0
  57. package/docs/Popover.md +61 -0
  58. package/docs/Stepper.md +101 -8
  59. package/docs/Toast.md +2 -0
  60. package/package.json +1 -1
@@ -1,19 +1,75 @@
1
1
  import { default as React } from 'react';
2
+ import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
2
3
  import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
3
- export type StepperAlignment = 'horizontal' | 'vertical';
4
- export type StepperSlots = 'root' | 'entry' | 'entryNumber' | 'entryLabel' | 'connector';
4
+ /** Layout direction of the stepper. */
5
+ export declare enum StepperAlignment {
6
+ Horizontal = "horizontal",
7
+ Vertical = "vertical"
8
+ }
9
+ /** Visual preset for the step markers and connectors. */
10
+ export declare enum StepperStyleType {
11
+ /** Numbered markers, filled solid on completion (the default look). */
12
+ Default = "default",
13
+ /** Numbered markers that stay ringed when completed — a coloured check, no solid fill. */
14
+ Outlined = "outlined",
15
+ /** Minimal dots without numbers — compact onboarding / carousel style. */
16
+ Dots = "dots"
17
+ }
18
+ /**
19
+ * Which step bodies (`step.content`) are expanded. Only affects vertical
20
+ * steppers with per-step `content`; ignored otherwise.
21
+ */
22
+ export declare enum StepperCollapse {
23
+ /** Every step body stays expanded. */
24
+ None = "none",
25
+ /** Only the current step's body is expanded (classic vertical wizard). */
26
+ ActiveOnly = "activeOnly",
27
+ /** Completed (past) step bodies collapse; current and upcoming stay expanded. */
28
+ Completed = "completed"
29
+ }
30
+ /**
31
+ * Where the label block sits relative to the marker. Horizontal steppers use
32
+ * `Top`/`Bottom`; vertical steppers use `Left`/`Right`. An out-of-orientation
33
+ * value falls back to that orientation's default.
34
+ */
35
+ export declare enum StepperLabelPlacement {
36
+ Top = "top",
37
+ Bottom = "bottom",
38
+ Left = "left",
39
+ Right = "right"
40
+ }
41
+ export type StepperSlots = 'root' | 'entry' | 'entryNumber' | 'entryLabel' | 'connector' | 'description' | 'content' | 'optional' | 'icon';
5
42
  export interface StepperStep {
6
- label: string;
43
+ /** Primary label for the step. */
44
+ label: React.ReactNode;
45
+ /** Secondary line shown under the label. */
46
+ description?: React.ReactNode;
47
+ /** Collapsible body content, rendered under the label (vertical only). */
48
+ content?: React.ReactNode;
49
+ /** FontAwesome icon shown in the marker instead of the step number. */
50
+ icon?: IconDefinition;
51
+ /** Marks the step as errored — recolours the marker with `--stepper-error-color`. */
52
+ error?: boolean;
53
+ /** `true` renders an "Optional" hint; a node renders custom hint text. */
54
+ optional?: boolean | React.ReactNode;
55
+ /** Disables this step — never clickable, dimmed. */
56
+ disabled?: boolean;
7
57
  }
8
58
  export interface StepperProps extends HtmlProps {
9
59
  steps?: StepperStep[];
10
60
  currentStep?: number;
11
- alignment?: StepperAlignment;
61
+ alignment?: StepperAlignment | 'horizontal' | 'vertical';
12
62
  onStepClicked?: ((index: number) => void) | null;
13
63
  /** Allow clicking any step, not just completed ones. */
14
64
  nonLinear?: boolean;
15
- /** Hide the label text beneath each step entry. */
65
+ /** Hide the label text (and description) beneath each step entry. */
16
66
  hideLabels?: boolean;
67
+ /** Visual preset for markers and connectors (default `Default`). */
68
+ styleType?: StepperStyleType;
69
+ /** Which step bodies are expanded — vertical `content` only (default `None`). */
70
+ collapse?: StepperCollapse;
71
+ /** Label position relative to the marker (default: horizontal `Bottom`, vertical `Right`). */
72
+ labelPlacement?: StepperLabelPlacement;
17
73
  className?: string;
18
74
  style?: React.CSSProperties;
19
75
  classNames?: SlotClassNames<StepperSlots>;
@@ -74,6 +74,28 @@ export interface ThemeVariables {
74
74
  '--background-card-elevated'?: string;
75
75
  '--background-card-outlined'?: string;
76
76
  '--background-card-filled'?: string;
77
+ /** Step marker diameter. Falls back to 40px. */
78
+ '--stepper-marker-size'?: string;
79
+ /** Step marker corner radius. Falls back to --default-border-radius. */
80
+ '--stepper-marker-radius'?: string;
81
+ /** Idle (upcoming) marker/connector colour. Falls back to --primary-lighter. */
82
+ '--stepper-idle-color'?: string;
83
+ /** Current-step marker colour. Falls back to --primary-color. */
84
+ '--stepper-active-color'?: string;
85
+ /** Completed-step marker fill/colour. Falls back to --primary-color. */
86
+ '--stepper-done-color'?: string;
87
+ /** Errored-step marker colour. Falls back to --error-color. */
88
+ '--stepper-error-color'?: string;
89
+ /** Completed connector colour. Falls back to --primary-color. */
90
+ '--stepper-connector-color'?: string;
91
+ /** Completed connector thickness. Falls back to 3px. */
92
+ '--stepper-connector-thickness'?: string;
93
+ /** Not-yet-reached connector thickness. Falls back to 1px. */
94
+ '--stepper-connector-idle-thickness'?: string;
95
+ /** Gap between marker and label, and grid column gap. Falls back to 12px. */
96
+ '--stepper-gap'?: string;
97
+ /** Text/check colour on a filled marker. Falls back to --text-on-primary. */
98
+ '--stepper-text-on-marker'?: string;
77
99
  [key: string]: string | undefined;
78
100
  }
79
101
  export interface Theme {
@@ -0,0 +1,2 @@
1
+ export declare function getScrollableAncestors(el: HTMLElement): HTMLElement[];
2
+ export declare function isHiddenByAnyAncestor(rect: DOMRect, ancestors: HTMLElement[]): boolean;
@@ -5,6 +5,8 @@ export { default as ActionIcon } from './common/actionIcon';
5
5
  export * from './common/actionIcon';
6
6
  export { default as AnimatedLogo } from './common/animatedLogo';
7
7
  export * from './common/animatedLogo';
8
+ export { default as AnimatedText } from './common/animatedText';
9
+ export * from './common/animatedText';
8
10
  export { default as Avatar } from './common/avatar';
9
11
  export * from './common/avatar';
10
12
  export { default as Badge } from './common/badge';
@@ -66,6 +68,8 @@ export { default as OptionPicker } from './common/optionPicker';
66
68
  export * from './common/optionPicker';
67
69
  export { default as Overscroll } from './common/overscroll';
68
70
  export * from './common/overscroll';
71
+ export { default as Popover } from './common/popover';
72
+ export * from './common/popover';
69
73
  export { default as ProgressBar } from './common/progressBar';
70
74
  export * from './common/progressBar';
71
75
  export { default as RadioGroup } from './common/radioGroup';
@@ -0,0 +1,94 @@
1
+ # AnimatedText
2
+
3
+ **When to use:** Decorative text entrance or emphasis effect, for hero headlines, empty-state messages, "premium"/"new" callouts, loading/decoding states. Not for body copy that needs to stay readable while animating in bulk; keep it to short strings.
4
+
5
+ **Import:** `import { AnimatedText, AnimatedTextVariant, AnimatedTextUnit } from '@ahrowe/ui'`
6
+
7
+ **Style variants:** `AnimatedTextVariant.RiseUp` (default) | `AnimatedTextVariant.BlurIn` | `AnimatedTextVariant.Bounce` | `AnimatedTextVariant.Typewriter` | `AnimatedTextVariant.Scramble` | `AnimatedTextVariant.Glitch` | `AnimatedTextVariant.Wave` | `AnimatedTextVariant.Shine` | `AnimatedTextVariant.Highlight`
8
+
9
+ **Units (every variant but `Shine`/`Highlight`):** `AnimatedTextUnit.Letter` (default) | `AnimatedTextUnit.Word`
10
+
11
+ ```tsx
12
+ import { AnimatedText, AnimatedTextVariant, AnimatedTextUnit } from '@ahrowe/ui';
13
+
14
+ // Default: letters rise up, staggered left to right, once
15
+ <AnimatedText text="Welcome back" />
16
+
17
+ // Faster/slower stagger between letters
18
+ <AnimatedText text="Fast" staggerDelay={15} />
19
+ <AnimatedText text="Slow" staggerDelay={120} />
20
+
21
+ // Faster/slower per-letter entrance
22
+ <AnimatedText text="Snappy" duration={200} />
23
+
24
+ // Stagger by word instead of by letter
25
+ <AnimatedText text="One word after another" unit={AnimatedTextUnit.Word} staggerDelay={150} />
26
+
27
+ // Fades in from a blur
28
+ <AnimatedText text="Fading into focus" variant={AnimatedTextVariant.BlurIn} />
29
+
30
+ // Drops in from above with a spring overshoot
31
+ <AnimatedText text="Bouncing into place" variant={AnimatedTextVariant.Bounce} />
32
+
33
+ // Typed one letter at a time, with a blinking cursor at the end
34
+ <AnimatedText text="Typed one letter at a time" variant={AnimatedTextVariant.Typewriter} staggerDelay={60} />
35
+
36
+ // Typewriter, word by word instead of letter by letter
37
+ <AnimatedText
38
+ text="Typed word by word"
39
+ variant={AnimatedTextVariant.Typewriter}
40
+ unit={AnimatedTextUnit.Word}
41
+ staggerDelay={250}
42
+ />
43
+
44
+ // Resolves from random characters to the real text, left to right
45
+ <AnimatedText text="Decoding the message" variant={AnimatedTextVariant.Scramble} staggerDelay={60} />
46
+
47
+ // Briefly RGB-splits and jitters before settling
48
+ <AnimatedText text="System malfunction" variant={AnimatedTextVariant.Glitch} />
49
+
50
+ // Continuously bobs in a travelling wave, on a loop
51
+ <AnimatedText text="Riding the wave" variant={AnimatedTextVariant.Wave} />
52
+
53
+ // A highlight sweeps across the text, looping
54
+ <AnimatedText text="Premium feature" variant={AnimatedTextVariant.Shine} />
55
+
56
+ // Slower sweep
57
+ <AnimatedText text="Slow shine" variant={AnimatedTextVariant.Shine} duration={3000} />
58
+
59
+ // A marker-style highlight block wipes in behind the text, once
60
+ <AnimatedText text="Marked as important" variant={AnimatedTextVariant.Highlight} />
61
+
62
+ // Changing `text` (or `variant`/`unit`/`staggerDelay`/`duration`) automatically replays,
63
+ // so no key is needed for that. A key is only needed to replay with the exact same text again:
64
+ <AnimatedText key={replayKey} text="Hello there" />
65
+ <button onClick={() => setReplayKey((k) => k + 1)}>Replay</button>
66
+ ```
67
+
68
+ **Key props:**
69
+
70
+ | Prop | Type | Description |
71
+ |------|------|-------------|
72
+ | `text` | `string` | The text to animate (required) |
73
+ | `variant` | `AnimatedTextVariant` | Which animation plays (default `RiseUp`) |
74
+ | `unit` | `AnimatedTextUnit` | Whether entrance/loop variants stagger by letter or by whole word (default `Letter`). Ignored by `Shine`/`Highlight` |
75
+ | `staggerDelay` | `number` | Delay in ms between each unit's animation start. Applies to every variant except `Shine`/`Highlight`, which animate as a single run (default `40`) |
76
+ | `duration` | `number` | Duration in ms of one animation cycle: a single unit's entrance for `RiseUp`/`BlurIn`/`Bounce`/`Glitch` (default `500`), one bob cycle for `Wave` (default `1200`), one sweep of the highlight for `Shine` (default `2000`), or the one-shot wipe-in for `Highlight` (default `600`). Ignored by `Typewriter` and `Scramble`, since each unit resolves instantly; use `staggerDelay` to control their pacing |
77
+
78
+ **Word mode:** with `unit={AnimatedTextUnit.Word}`, each whole word animates as one block instead of each letter animating individually; `staggerDelay` then applies between words. Words are split on whitespace, and consecutive spaces collapse to one.
79
+
80
+ **Typewriter:** each letter (or word) pops in instantly at its staggered delay, with no entrance easing, since it's meant to read as being typed rather than animated in. Untyped units aren't rendered at all yet (rather than pre-rendered invisible), so the blinking cursor always tracks right after the last thing actually "typed"; it stays solid while typing and starts blinking once the sequence finishes.
81
+
82
+ **Scramble:** every unit starts as a run of random characters (matching the unit's own length, so words keep their shape) and locks in to the real value left to right, one `staggerDelay` apart; still-scrambling units keep flickering between random characters until their turn. Spaces are never scrambled.
83
+
84
+ **Glitch:** a brief RGB-channel split (`--error-color` / `--info-color`) and positional jitter, settling to the normal render partway through the entrance.
85
+
86
+ **Wave:** the only other continuously looping variant besides `Shine`. Every unit bobs up and down forever, and each unit's `animation-delay` is a *negative* offset (`-index * staggerDelay`), which starts it already mid-cycle so the whole string reads as a travelling wave from the first frame instead of every letter bobbing in lockstep.
87
+
88
+ **Shine vs. Highlight:** both animate the whole string as a single run (no letter/word splitting, `unit` is ignored) rather than staggering units, since a highlighter mark is naturally one continuous stroke, not a letter-by-letter reveal. `Shine` loops a light gradient through the text's own color, forever. `Highlight` wipes a solid marker-style background block in behind the (already fully-opaque) text once, and stays, closer to literally highlighting the text than lighting it up.
89
+
90
+ **Replaying:** every variant automatically replays whenever `text`, `variant`, `unit`, `staggerDelay`, or `duration` changes. CSS entrance variants would otherwise just hold their `animation-fill-mode: forwards` end state and silently swap letters underneath it, so the animated markup remounts on any of those changes to keep the animation and the text in sync. To replay with the exact same text (e.g. a manual "Replay" button), change the component's own `key`; that's the only case not already covered automatically.
91
+
92
+ **Accessibility:** the full `text` is always exposed to assistive tech via a visually-hidden node; the animated markup is `aria-hidden`. Respects `prefers-reduced-motion`: every variant renders the plain, fully-visible final state immediately with no animation (no blur, no bounce, no scrambling, no bobbing, no cursor blink, no shine sweep, no highlight wipe).
93
+
94
+ **Slots:** `letter`, each animated unit's span, for every variant but `Shine`/`Highlight` (where it's the single animated text run). `cursor`, the blinking cursor, `Typewriter` only.
package/docs/CLAUDE.md CHANGED
@@ -96,6 +96,7 @@ Slot keys per component are documented in each component's doc file below.
96
96
  @ActionButtons.md
97
97
  @ActionIcon.md
98
98
  @AnimatedLogo.md
99
+ @AnimatedText.md
99
100
  @Avatar.md
100
101
  @Badge.md
101
102
  @BodyEnd.md
@@ -129,6 +130,7 @@ Slot keys per component are documented in each component's doc file below.
129
130
  @NumberInput.md
130
131
  @OptionPicker.md
131
132
  @Overscroll.md
133
+ @Popover.md
132
134
  @ProgressBar.md
133
135
  @RadioGroup.md
134
136
  @Ripple.md
@@ -49,3 +49,5 @@ import { FloatingMenu, Align } from '@ahrowe/ui';
49
49
  | `dontCloseOnChildClick` | `boolean` | Prevent close when clicking inside content |
50
50
 
51
51
  **Slots:** `root` `trigger` `menu` `menuContainer`
52
+
53
+ **Nested overlays in `content`:** a click anywhere inside `content` never closes the menu on its own — including inside a nested overlay that renders through its own portal (e.g. a `Dropdown` or another `FloatingMenu` used inside `content`), even though that overlay's DOM lives outside `content`'s own subtree. Closing on a selection is opt-in: have the handler that reacts to the selection call `onOpenChange(false)` (or your own state setter) explicitly, the way a calendar's day-selection handler would, while a nested month/year `Dropdown` inside the same menu is left alone.
@@ -0,0 +1,61 @@
1
+ # Popover
2
+
3
+ **When to use:** A floating panel anchored to a trigger — profile cards, quick forms, action menus, help panels, filter dropdowns. Reach for `Tooltip` for plain text hints, `FloatingMenu` for a menu that mirrors the trigger width; use `Popover` when you need free content on any side with an arrow.
4
+
5
+ **Import:** `import { Popover } from '@ahrowe/ui'`
6
+
7
+ ```tsx
8
+ import { Popover, PopoverPlacement, PopoverAlign, PopoverTrigger } from '@ahrowe/ui';
9
+
10
+ // Basic — children is the trigger, content is the panel body
11
+ <Popover content={<ProfileCard />}>
12
+ <button>Open</button>
13
+ </Popover>
14
+
15
+ // Placement + alignment
16
+ <Popover placement={PopoverPlacement.Right} align={PopoverAlign.Start} content={<Menu />}>
17
+ <ActionIcon icon={faEllipsis} />
18
+ </Popover>
19
+
20
+ // Hover trigger, no arrow
21
+ <Popover trigger={PopoverTrigger.Hover} withArrow={false} content="Quick info">
22
+ <span>Hover me</span>
23
+ </Popover>
24
+
25
+ // Controlled open state
26
+ <Popover isOpen={open} onOpenChange={setOpen} content={<Form onDone={() => setOpen(false)} />}>
27
+ <button>Edit</button>
28
+ </Popover>
29
+ ```
30
+
31
+ **PopoverPlacement enum:** `PopoverPlacement.Top` | `Bottom` | `Left` | `Right` — the preferred side; auto-flips to the opposite side when there's no room.
32
+
33
+ **PopoverAlign enum:** `PopoverAlign.Start` | `Center` | `End` — cross-axis alignment against the trigger.
34
+
35
+ **PopoverTrigger enum:** `PopoverTrigger.Click` | `Hover` | `Focus`.
36
+
37
+ **Key props:**
38
+
39
+ | Prop | Type | Default | Description |
40
+ |------|------|---------|-------------|
41
+ | `content` | `ReactNode` | — | Panel body |
42
+ | `children` | `ReactNode` | — | Trigger element |
43
+ | `placement` | `PopoverPlacement` | `Bottom` | Preferred side |
44
+ | `align` | `PopoverAlign` | `Center` | Cross-axis alignment |
45
+ | `trigger` | `PopoverTrigger` | `Click` | What opens the popover |
46
+ | `isOpen` | `boolean` | — | Controlled open state (omit for uncontrolled) |
47
+ | `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled |
48
+ | `onOpenChange` | `(isOpen: boolean) => void` | — | Open-state change callback |
49
+ | `offset` | `number` | `10` | Gap in px between trigger and panel |
50
+ | `withArrow` | `boolean` | `true` | Render the arrow pointing at the trigger |
51
+ | `closeOnOutsideClick` | `boolean` | `true` | Close on click outside (ignored for hover trigger) |
52
+ | `closeOnEscape` | `boolean` | `true` | Close on Escape |
53
+ | `openDelay` | `number` | `100` | Hover open delay in ms |
54
+ | `closeDelay` | `number` | `120` | Hover close delay in ms |
55
+ | `disabled` | `boolean` | `false` | Prevent opening |
56
+
57
+ **Slots:** `root` `trigger` `floating` `panel` `arrow`
58
+
59
+ Requires a `<div id="bodyEnd"></div>` at the app root — the panel is portaled there, same as `Tooltip` and `FloatingMenu`.
60
+
61
+ **Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content, opening it (click trigger only) moves focus to the first focusable element and the panel gets `role="dialog"`. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal — focus is not trapped, and hover/focus triggers don't steal focus. Popovers with only static content don't manage focus or claim the dialog role.
package/docs/Stepper.md CHANGED
@@ -1,11 +1,19 @@
1
1
  # Stepper
2
2
 
3
- **When to use:** Step progress indicator — multi-step forms, onboarding flows, checkout wizards. Shows completed, current, and upcoming steps.
3
+ **When to use:** Step progress indicator — multi-step forms, onboarding flows, checkout wizards. Shows completed, current, and upcoming steps, and (vertically) can expand a collapsible body under the active step.
4
4
 
5
5
  **Import:** `import { Stepper } from '@ahrowe/ui'`
6
+ **Types:** `import type { StepperStep, StepperProps } from '@ahrowe/ui'`
7
+
8
+ **Enums:**
9
+ - `StepperAlignment`: `Horizontal` (default) | `Vertical`
10
+ - `StepperStyleType`: `Default` (filled numbered markers) | `Outlined` (numbered markers that stay ringed when completed) | `Dots` (minimal dots, no numbers)
11
+ - `StepperCollapse`: `None` (default) | `ActiveOnly` | `Completed`
12
+ - `StepperLabelPlacement`: `Top` | `Bottom` | `Left` | `Right`
6
13
 
7
14
  ```tsx
8
- import { Stepper } from '@ahrowe/ui';
15
+ import { Stepper, StepperAlignment, StepperStyleType, StepperCollapse } from '@ahrowe/ui';
16
+ import { faUser, faCreditCard } from '@fortawesome/free-solid-svg-icons';
9
17
 
10
18
  const steps = [
11
19
  { label: 'Account' },
@@ -16,22 +24,107 @@ const steps = [
16
24
  // Horizontal (default)
17
25
  <Stepper steps={steps} currentStep={1} />
18
26
 
19
- // Vertical
27
+ // Style presets
28
+ <Stepper steps={steps} currentStep={1} styleType={StepperStyleType.Outlined} />
29
+ <Stepper steps={steps} currentStep={1} styleType={StepperStyleType.Dots} hideLabels />
30
+
31
+ // Vertical, clickable
20
32
  <Stepper
21
33
  steps={steps}
22
34
  currentStep={currentStep}
23
- alignment="vertical"
35
+ alignment={StepperAlignment.Vertical}
24
36
  onStepClicked={(stepIndex) => setCurrentStep(stepIndex)}
25
37
  />
38
+
39
+ // Rich steps — icon, description, error, optional
40
+ <Stepper
41
+ currentStep={2}
42
+ steps={[
43
+ { label: 'Account', icon: faUser, description: 'Login details' },
44
+ { label: 'Payment', icon: faCreditCard, error: true },
45
+ { label: 'Review', optional: true },
46
+ ]}
47
+ />
48
+
49
+ // Vertical wizard with collapsible bodies — only the active step's body is open
50
+ <Stepper
51
+ alignment={StepperAlignment.Vertical}
52
+ collapse={StepperCollapse.ActiveOnly}
53
+ currentStep={step}
54
+ onStepClicked={setStep}
55
+ steps={[
56
+ { label: 'Account', description: 'Login details', content: <AccountForm /> },
57
+ { label: 'Profile', description: 'About you', content: <ProfileForm /> },
58
+ { label: 'Confirm', description: 'Finish', content: <ConfirmStep /> },
59
+ ]}
60
+ />
26
61
  ```
27
62
 
63
+ **StepperStep:**
64
+
65
+ | Field | Type | Description |
66
+ |-------|------|-------------|
67
+ | `label` | `ReactNode` | Primary step label |
68
+ | `description` | `ReactNode` | Secondary line under the label |
69
+ | `content` | `ReactNode` | Collapsible body under the label (vertical only) |
70
+ | `icon` | `IconDefinition` | FontAwesome icon in the marker, replacing the number |
71
+ | `error` | `boolean` | Recolours the marker with `--stepper-error-color`; suppresses the checkmark |
72
+ | `optional` | `boolean \| ReactNode` | `true` → an "Optional" hint; a node → custom hint text |
73
+ | `disabled` | `boolean` | Never clickable, dimmed |
74
+
28
75
  **Key props:**
29
76
 
30
77
  | Prop | Type | Description |
31
78
  |------|------|-------------|
32
- | `steps` | `object[]` | Array of step objects with `label` |
79
+ | `steps` | `StepperStep[]` | The steps to render |
33
80
  | `currentStep` | `number` | Zero-based active step index |
34
- | `alignment` | `'horizontal' \| 'vertical'` | Layout direction |
35
- | `onStepClicked` | `(stepIndex: number) => void` | Navigate on step click |
81
+ | `alignment` | `StepperAlignment \| 'horizontal' \| 'vertical'` | Layout direction (default `Horizontal`) |
82
+ | `styleType` | `StepperStyleType` | Marker/connector preset (default `Default`) |
83
+ | `collapse` | `StepperCollapse` | Which `content` bodies are expanded — vertical only (default `None`) |
84
+ | `labelPlacement` | `StepperLabelPlacement` | Label position; default `Bottom` (horizontal) / `Right` (vertical). An out-of-orientation value falls back to that default |
85
+ | `onStepClicked` | `(stepIndex: number) => void` | Navigate on step click. Linear: only completed steps are clickable; with `nonLinear`, any enabled step except the current one |
86
+ | `nonLinear` | `boolean` | Allow clicking any step, not just completed ones |
87
+ | `hideLabels` | `boolean` | Hide labels, descriptions, and (vertical) content |
88
+
89
+ **Collapse (vertical `content`):** `None` keeps every body open; `ActiveOnly` opens just the current step's body (classic vertical wizard); `Completed` collapses completed bodies while keeping the current and upcoming ones open. Bodies animate their height and respect `prefers-reduced-motion`. Horizontal steppers don't render inline `content` and ignore `collapse`.
90
+
91
+ **Theming:** these are theme variables (typed on `ThemeVariables`) — set them theme-wide via `ThemeProvider`'s `variables`, or per instance via `style`, without fighting specificity. Each falls back to a built-in default when unset:
92
+
93
+ | Variable | Falls back to |
94
+ |----------|---------------|
95
+ | `--stepper-marker-size` | `40px` |
96
+ | `--stepper-marker-radius` | `var(--default-border-radius)` |
97
+ | `--stepper-idle-color` | `var(--primary-lighter)` |
98
+ | `--stepper-active-color` | `var(--primary-color)` |
99
+ | `--stepper-done-color` | `var(--primary-color)` |
100
+ | `--stepper-error-color` | `var(--error-color)` |
101
+ | `--stepper-connector-color` | `var(--primary-color)` |
102
+ | `--stepper-connector-thickness` (completed connectors) | `3px` |
103
+ | `--stepper-connector-idle-thickness` (not-yet-reached connectors) | `1px` |
104
+ | `--stepper-gap` | `12px` |
105
+ | `--stepper-text-on-marker` | `var(--text-on-primary)` |
106
+
107
+ ```tsx
108
+ // Theme-wide, via ThemeProvider — circular pink markers, thicker connectors
109
+ const theme: Theme = {
110
+ id: 'brand',
111
+ variables: {
112
+ '--stepper-marker-radius': '50%',
113
+ '--stepper-active-color': '#e91e63',
114
+ '--stepper-connector-thickness': '4px',
115
+ },
116
+ };
117
+
118
+ // Or one-off on a single stepper via style
119
+ <Stepper
120
+ steps={steps}
121
+ currentStep={1}
122
+ style={{
123
+ '--stepper-marker-radius': '50%',
124
+ '--stepper-active-color': '#e91e63',
125
+ '--stepper-connector-thickness': '4px',
126
+ } as React.CSSProperties}
127
+ />
128
+ ```
36
129
 
37
- **Note:** Legacy JSX component check [package/common/stepper/stepper.jsx](../common/stepper/stepper.jsx) for additional props.
130
+ **Slots:** `root` `entry` `entryNumber` `entryLabel` `connector` `description` `content` `optional` `icon`
package/docs/Toast.md CHANGED
@@ -65,6 +65,8 @@ showToast('Done', {
65
65
  | `allowStacking` | `boolean` | `true` | Allow multiple toasts simultaneously |
66
66
  | `pauseOnHover` | `boolean` | `true` | Pause the auto-dismiss timer while the toast is hovered or focused |
67
67
 
68
+ **Swipe to dismiss:** on touch devices, every toast can be dragged away to dismiss it, on top of the close button and auto-dismiss timer. A fast flick dismisses even without dragging far; dragging pauses the auto-dismiss timer for the duration of the gesture (regardless of `pauseOnHover`) and a vertical drag is left alone so it doesn't fight the page's own scrolling. It's gated to coarse (touch) pointers only — on mouse/trackpad devices the close button and auto-dismiss already cover it, and a drag there is more likely to be a text selection than an intentional dismiss.
69
+
68
70
  **ToastPosition:** `'top-left'` `'top-center'` `'top-right'` `'bottom-left'` `'bottom-center'` `'bottom-right'`
69
71
 
70
72
  **ToastType:** `'default'` `'info'` `'success'` `'error'` `'warn'`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },