@ahrowe/ui 0.8.0 → 0.10.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 (77) hide show
  1. package/dist/esm/common/animatedLogo/animatedLogo.module.mjs.map +1 -1
  2. package/dist/esm/common/animatedText/animatedText.mjs +2 -0
  3. package/dist/esm/common/animatedText/animatedText.mjs.map +1 -0
  4. package/dist/esm/common/animatedText/animatedText.module.mjs +2 -0
  5. package/dist/esm/common/animatedText/animatedText.module.mjs.map +1 -0
  6. package/dist/esm/common/animatedText/animatedText.types.mjs +2 -0
  7. package/dist/esm/common/animatedText/animatedText.types.mjs.map +1 -0
  8. package/dist/esm/common/datePicker/datePicker.mjs +1 -1
  9. package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
  10. package/dist/esm/common/datePicker/datePicker.module.mjs +1 -1
  11. package/dist/esm/common/datePicker/datePicker.module.mjs.map +1 -1
  12. package/dist/esm/common/dropZone/dropZone.module.mjs.map +1 -1
  13. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +1 -1
  14. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +1 -1
  15. package/dist/esm/common/popover/popover.mjs +2 -0
  16. package/dist/esm/common/popover/popover.mjs.map +1 -0
  17. package/dist/esm/common/popover/popover.module.mjs +2 -0
  18. package/dist/esm/common/popover/popover.module.mjs.map +1 -0
  19. package/dist/esm/common/popover/popover.types.mjs +2 -0
  20. package/dist/esm/common/popover/popover.types.mjs.map +1 -0
  21. package/dist/esm/common/popover/usePopoverPosition.mjs +2 -0
  22. package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -0
  23. package/dist/esm/common/radioGroup/radioGroup.mjs +2 -0
  24. package/dist/esm/common/radioGroup/radioGroup.mjs.map +1 -0
  25. package/dist/esm/common/radioGroup/radioGroup.module.mjs +2 -0
  26. package/dist/esm/common/radioGroup/radioGroup.module.mjs.map +1 -0
  27. package/dist/esm/common/radioGroup/radioGroup.types.mjs +2 -0
  28. package/dist/esm/common/radioGroup/radioGroup.types.mjs.map +1 -0
  29. package/dist/esm/common/scrollbarProvider/scrollbarProvider.module.mjs.map +1 -1
  30. package/dist/esm/common/stepper/stepper.mjs +1 -1
  31. package/dist/esm/common/stepper/stepper.mjs.map +1 -1
  32. package/dist/esm/common/stepper/stepper.module.mjs +1 -1
  33. package/dist/esm/common/stepper/stepper.module.mjs.map +1 -1
  34. package/dist/esm/common/stepper/stepper.types.mjs +2 -0
  35. package/dist/esm/common/stepper/stepper.types.mjs.map +1 -0
  36. package/dist/esm/common/sticky/sticky.mjs +1 -1
  37. package/dist/esm/common/sticky/sticky.mjs.map +1 -1
  38. package/dist/esm/common/sticky/stickyStack.mjs +2 -0
  39. package/dist/esm/common/sticky/stickyStack.mjs.map +1 -0
  40. package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
  41. package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
  42. package/dist/esm/common/utils/scrollAncestors.mjs +2 -0
  43. package/dist/esm/common/utils/scrollAncestors.mjs.map +1 -0
  44. package/dist/esm/favicon.ico +0 -0
  45. package/dist/esm/index.mjs +1 -1
  46. package/dist/esm/logo.svg +4 -0
  47. package/dist/favicon.ico +0 -0
  48. package/dist/index.cjs +4 -4
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/logo.svg +4 -0
  51. package/dist/style.css +1 -1
  52. package/dist/types/package/common/animatedText/animatedText.d.ts +4 -0
  53. package/dist/types/package/common/animatedText/animatedText.types.d.ts +59 -0
  54. package/dist/types/package/common/animatedText/index.d.ts +2 -0
  55. package/dist/types/package/common/configProvider/configProvider.types.d.ts +2 -0
  56. package/dist/types/package/common/popover/index.d.ts +2 -0
  57. package/dist/types/package/common/popover/popover.d.ts +3 -0
  58. package/dist/types/package/common/popover/popover.types.d.ts +58 -0
  59. package/dist/types/package/common/popover/usePopoverPosition.d.ts +23 -0
  60. package/dist/types/package/common/radioGroup/index.d.ts +2 -0
  61. package/dist/types/package/common/radioGroup/radioGroup.d.ts +4 -0
  62. package/dist/types/package/common/radioGroup/radioGroup.types.d.ts +45 -0
  63. package/dist/types/package/common/stepper/index.d.ts +1 -0
  64. package/dist/types/package/common/stepper/stepper.d.ts +1 -1
  65. package/dist/types/package/common/stepper/stepper.types.d.ts +61 -5
  66. package/dist/types/package/common/sticky/stickyStack.d.ts +41 -0
  67. package/dist/types/package/common/themeProvider/theme.types.d.ts +22 -0
  68. package/dist/types/package/common/utils/scrollAncestors.d.ts +2 -0
  69. package/dist/types/package/index.d.ts +6 -0
  70. package/docs/AnimatedText.md +94 -0
  71. package/docs/CLAUDE.md +3 -0
  72. package/docs/ConfigProvider.md +1 -1
  73. package/docs/Popover.md +61 -0
  74. package/docs/RadioGroup.md +76 -0
  75. package/docs/Stepper.md +101 -8
  76. package/docs/Sticky.md +11 -0
  77. package/package.json +2 -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>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Coordinates every mounted `Sticky` so ones that overlap horizontally stack
3
+ * underneath each other (in DOM order) instead of pinning to the same spot.
4
+ * Non-overlapping ones (e.g. side-by-side halves under a full-width one) land
5
+ * at the same top instead of being pushed down by each other.
6
+ */
7
+ export interface StickyStackMetrics {
8
+ /** Current (unstuck-flow) top of the root element, from getBoundingClientRect. */
9
+ top: number;
10
+ left: number;
11
+ width: number;
12
+ contentHeight: number;
13
+ }
14
+ export interface StickyStackEntry {
15
+ node: HTMLElement;
16
+ offsetTop: number;
17
+ disabled: boolean;
18
+ getMetrics: () => StickyStackMetrics | null;
19
+ }
20
+ export interface StickyStackResult {
21
+ isStuck: boolean;
22
+ /** Effective `top` to pin at while stuck — offsetTop plus the height of any stuck, overlapping entries above it. */
23
+ top: number;
24
+ }
25
+ type ResultListener = (result: StickyStackResult) => void;
26
+ declare class StickyStack {
27
+ private entries;
28
+ private listeners;
29
+ private results;
30
+ private rafId;
31
+ private listenerCount;
32
+ schedule: () => void;
33
+ register(entry: StickyStackEntry, onChange: ResultListener): symbol;
34
+ updateEntry(id: symbol, patch: Partial<Pick<StickyStackEntry, 'offsetTop' | 'disabled'>>): void;
35
+ unregister(id: symbol): void;
36
+ recompute: () => void;
37
+ private startListening;
38
+ private stopListening;
39
+ }
40
+ export declare const stickyStack: StickyStack;
41
+ export {};
@@ -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,8 +68,12 @@ 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';
75
+ export { default as RadioGroup } from './common/radioGroup';
76
+ export * from './common/radioGroup';
71
77
  export { default as Ripple } from './common/ripple';
72
78
  export * from './common/ripple';
73
79
  export { default as RoomDrawer } from './common/roomDrawer';
@@ -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,7 +130,9 @@ 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
135
+ @RadioGroup.md
133
136
  @Ripple.md
134
137
  @RoomDrawer.md
135
138
  @RoomViewer.md
@@ -58,7 +58,7 @@ So a global default only fills in props a given instance left out — any instan
58
58
 
59
59
  Each entry is a `Partial<...Props>`, so any of that component's props can be defaulted:
60
60
 
61
- - **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `DatePicker`
61
+ - **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker`
62
62
  - **Display:** `Button` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Accordion` · `Divider`
63
63
  - **Overlays:** `ConfirmModal`
64
64
 
@@ -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.
@@ -0,0 +1,76 @@
1
+ # RadioGroup
2
+
3
+ **When to use:** Single-choice selection from a small, always-visible set of options — plan pickers, shipping methods, yes/no/maybe, settings with 2–5 mutually exclusive choices. Prefer `Dropdown` when the list is long, and `OptionPicker` for a compact segmented control.
4
+
5
+ **Import:** `import { RadioGroup, RadioGroupOrientation } from '@ahrowe/ui'`
6
+ **Types:** `import type { RadioOption, RadioGroupProps, RadioValue } from '@ahrowe/ui'`
7
+
8
+ Follows the ARIA `radiogroup` pattern: one roving tab stop, arrow keys move focus **and** selection (skipping disabled options and wrapping around), Space/Enter selects the focused option.
9
+
10
+ ```tsx
11
+ import { RadioGroup, RadioGroupOrientation, FormValidator } from '@ahrowe/ui';
12
+ import type { RadioOption } from '@ahrowe/ui';
13
+
14
+ const options: RadioOption[] = [
15
+ { value: 'free', label: 'Free' },
16
+ { value: 'pro', label: 'Pro' },
17
+ { value: 'team', label: 'Team', disabled: true },
18
+ ];
19
+
20
+ // Controlled
21
+ <RadioGroup options={options} value={plan} onChange={(v) => setPlan(v as string)} />
22
+
23
+ // Uncontrolled with an initial value
24
+ <RadioGroup options={options} defaultValue="free" />
25
+
26
+ // Horizontal layout
27
+ <RadioGroup options={options} value={plan} onChange={setPlan} orientation={RadioGroupOrientation.Horizontal} />
28
+
29
+ // Per-option descriptions
30
+ <RadioGroup
31
+ options={[
32
+ { value: 'standard', label: 'Standard', description: '3–5 business days, free' },
33
+ { value: 'express', label: 'Express', description: 'Next business day, €9.90' },
34
+ ]}
35
+ value={shipping}
36
+ onChange={setShipping}
37
+ />
38
+
39
+ // With FormValidator
40
+ const planValidator = new FormValidator<string | null>(null, [Validators.required()]);
41
+ <RadioGroup options={options} formValidator={planValidator} />
42
+ ```
43
+
44
+ **RadioGroupOrientation enum:** `RadioGroupOrientation.Vertical` (default) | `RadioGroupOrientation.Horizontal`
45
+
46
+ **RadioOption:**
47
+
48
+ ```ts
49
+ interface RadioOption {
50
+ value: string | number;
51
+ label?: ReactNode; // falls back to `value`
52
+ description?: ReactNode; // secondary text under the label
53
+ disabled?: boolean; // disable just this option
54
+ }
55
+ ```
56
+
57
+ **Key props:**
58
+
59
+ | Prop | Type | Description |
60
+ |------|------|-------------|
61
+ | `options` | `RadioOption[]` | The selectable options |
62
+ | `value` | `string \| number` | Controlled selected value |
63
+ | `defaultValue` | `string \| number` | Initial value when uncontrolled |
64
+ | `onChange` | `(value, event?) => void` | Fires with the newly selected value |
65
+ | `orientation` | `RadioGroupOrientation` | Layout direction (default `Vertical`) |
66
+ | `disabled` | `boolean` | Disable the whole group |
67
+ | `size` | `string` | Diameter of the radio circle (default `'18px'`) |
68
+ | `formValidator` | `FormValidator` | Connects to form validation |
69
+ | `errorMessage` | `string` | Manual error message (when not using a `formValidator`) |
70
+ | `isValid` | `boolean` | Manual valid state (default `true`) |
71
+
72
+ **Validation:** like `Input`, an errored group shows an error `Tooltip` and turns its unselected circles red. With a `formValidator` the error surfaces only after the field is *touched* (focus leaves the group), then on focus or hover; requires `<div id="bodyEnd"></div>` at the app root for the tooltip portal.
73
+
74
+ **Global defaults:** adopts `ConfigProvider` — e.g. `defaultProps={{ RadioGroup: { orientation: RadioGroupOrientation.Horizontal } }}`. See [ConfigProvider.md](ConfigProvider.md).
75
+
76
+ **Slots:** `root` `option` `radio` `dot` `label` `description`
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/Sticky.md CHANGED
@@ -26,10 +26,21 @@ import { Sticky } from '@ahrowe/ui';
26
26
  <Sticky disabled={isEditing}>
27
27
  <Header />
28
28
  </Sticky>
29
+
30
+ // Two Stickies that overlap horizontally automatically stack — the second
31
+ // pins below the first's height instead of on top of it
32
+ <Sticky offsetTop={0}>
33
+ <TopBar />
34
+ </Sticky>
35
+ <Sticky offsetTop={0}>
36
+ <FilterBar />
37
+ </Sticky>
29
38
  ```
30
39
 
31
40
  **How it works:** while stuck, the content switches to `position: fixed` (measured to keep the same width and horizontal position) and a placeholder of the same height takes its place in the flow. It tracks scroll on the window *and* any ancestor scroll container, so it works inside scrollable panels too — it always pins to the top of the viewport (offset by `offsetTop`).
32
41
 
42
+ **Stacking:** every mounted `Sticky` coordinates automatically, no extra markup needed. When more than one is stuck at the same time, a later one (in DOM order) that horizontally overlaps an earlier stuck one pins below it — `offsetTop` plus the overlapping ones' heights — instead of both landing on the same spot. Stickies that don't overlap horizontally (e.g. two half-width ones side by side under a full-width one) land at the same `top`, next to each other, rather than pushing each other down.
43
+
33
44
  **Key props:**
34
45
 
35
46
  | Prop | Type | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -50,6 +50,7 @@
50
50
  "scripts": {
51
51
  "dev": "vite",
52
52
  "build": "vite build",
53
+ "build:demo": "vite build --base=/packages/ui/demo/ --outDir dist-demo",
53
54
  "build:lib": "vite build --config vite.lib.config.ts",
54
55
  "build:mcp": "vite build --config vite.mcp.config.ts",
55
56
  "preview": "vite preview",