@refraction-ui/astro 0.4.0 → 0.4.2
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/dist/astro-accordion/Button.astro +55 -0
- package/dist/astro-accordion/index.ts +1 -0
- package/dist/astro-ai/AIScript.astro +69 -0
- package/dist/astro-ai/TTSScript.astro +60 -0
- package/dist/astro-ai/index.ts +16 -0
- package/dist/astro-animated-text/AnimatedText.astro +81 -0
- package/dist/astro-animated-text/TypewriterText.astro +90 -0
- package/dist/astro-animated-text/index.ts +1 -0
- package/dist/astro-app-shell/AppShell.astro +195 -0
- package/dist/astro-app-shell/AppShellContent.astro +29 -0
- package/dist/astro-app-shell/AppShellHeader.astro +54 -0
- package/dist/astro-app-shell/AppShellMain.astro +18 -0
- package/dist/astro-app-shell/AppShellMobileNav.astro +27 -0
- package/dist/astro-app-shell/AppShellOverlay.astro +21 -0
- package/dist/astro-app-shell/AppShellSidebar.astro +26 -0
- package/dist/astro-app-shell/AuthShell.astro +31 -0
- package/dist/astro-app-shell/AuthShellCard.astro +24 -0
- package/dist/astro-app-shell/PageShell.astro +36 -0
- package/dist/astro-app-shell/PageShellFooter.astro +30 -0
- package/dist/astro-app-shell/PageShellNav.astro +33 -0
- package/dist/astro-app-shell/PageShellSection.astro +32 -0
- package/dist/astro-app-shell/index.ts +21 -0
- package/dist/astro-auth/AuthGuard.astro +133 -0
- package/dist/astro-auth/index.ts +5 -0
- package/dist/astro-avatar/Avatar.astro +22 -0
- package/dist/astro-avatar/AvatarFallback.astro +18 -0
- package/dist/astro-avatar/AvatarImage.astro +48 -0
- package/dist/astro-avatar/index.ts +1 -0
- package/dist/astro-avatar-group/AvatarGroup.astro +42 -0
- package/dist/astro-avatar-group/index.ts +1 -0
- package/dist/astro-badge/Badge.astro +27 -0
- package/dist/astro-badge/index.ts +1 -0
- package/dist/astro-bottom-nav/BottomNav.astro +40 -0
- package/dist/astro-bottom-nav/index.ts +1 -0
- package/dist/astro-breadcrumbs/Breadcrumbs.astro +56 -0
- package/dist/astro-breadcrumbs/index.ts +1 -0
- package/dist/astro-button/Button.astro +55 -0
- package/dist/astro-button/index.ts +1 -0
- package/dist/astro-calendar/Calendar.astro +259 -0
- package/dist/astro-calendar/index.ts +1 -0
- package/dist/astro-callout/Callout.astro +3 -0
- package/dist/astro-callout/index.ts +1 -0
- package/dist/astro-card/Card.astro +20 -0
- package/dist/astro-card/CardContent.astro +13 -0
- package/dist/astro-card/CardDescription.astro +13 -0
- package/dist/astro-card/CardFooter.astro +13 -0
- package/dist/astro-card/CardHeader.astro +13 -0
- package/dist/astro-card/CardTitle.astro +15 -0
- package/dist/astro-card/index.ts +1 -0
- package/dist/astro-card-grid/CardGrid.astro +17 -0
- package/dist/astro-card-grid/index.ts +1 -0
- package/dist/astro-carousel/Button.astro +55 -0
- package/dist/astro-carousel/index.ts +1 -0
- package/dist/astro-charts/Bars.astro +53 -0
- package/dist/astro-charts/Chart.astro +31 -0
- package/dist/astro-charts/Circles.astro +48 -0
- package/dist/astro-charts/Gradient.astro +34 -0
- package/dist/astro-charts/Histogram.astro +107 -0
- package/dist/astro-charts/Line.astro +55 -0
- package/dist/astro-charts/PieChart.astro +63 -0
- package/dist/astro-charts/ScatterPlot.astro +95 -0
- package/dist/astro-charts/XAxis.astro +46 -0
- package/dist/astro-charts/YAxis.astro +44 -0
- package/dist/astro-charts/index.ts +26 -0
- package/dist/astro-checkbox/Checkbox.astro +129 -0
- package/dist/astro-checkbox/index.ts +1 -0
- package/dist/astro-code-block/CodeBlock.astro +3 -0
- package/dist/astro-code-block/index.ts +1 -0
- package/dist/astro-code-editor/CodeEditor.astro +123 -0
- package/dist/astro-code-editor/index.ts +1 -0
- package/dist/astro-collapsible/Collapsible.astro +58 -0
- package/dist/astro-collapsible/CollapsibleContent.astro +21 -0
- package/dist/astro-collapsible/CollapsibleTrigger.astro +17 -0
- package/dist/astro-collapsible/index.ts +1 -0
- package/dist/astro-command/Command.astro +170 -0
- package/dist/astro-command/CommandEmpty.astro +17 -0
- package/dist/astro-command/CommandGroup.astro +25 -0
- package/dist/astro-command/CommandInput.astro +21 -0
- package/dist/astro-command/CommandItem.astro +26 -0
- package/dist/astro-command/CommandList.astro +17 -0
- package/dist/astro-command/CommandSeparator.astro +14 -0
- package/dist/astro-command/index.ts +1 -0
- package/dist/astro-command-input/CommandInput.astro +6 -0
- package/dist/astro-command-input/index.ts +1 -0
- package/dist/astro-content-protection/ContentProtection.astro +104 -0
- package/dist/astro-content-protection/index.ts +1 -0
- package/dist/astro-data-table/DataTable.astro +263 -0
- package/dist/astro-data-table/index.ts +1 -0
- package/dist/astro-date-picker/DatePicker.astro +398 -0
- package/dist/astro-date-picker/index.ts +1 -0
- package/dist/astro-device-frame/DeviceFrame.astro +44 -0
- package/dist/astro-device-frame/index.ts +1 -0
- package/dist/astro-dialog/Dialog.astro +138 -0
- package/dist/astro-dialog/DialogClose.astro +16 -0
- package/dist/astro-dialog/DialogContent.astro +24 -0
- package/dist/astro-dialog/DialogDescription.astro +15 -0
- package/dist/astro-dialog/DialogFooter.astro +14 -0
- package/dist/astro-dialog/DialogHeader.astro +14 -0
- package/dist/astro-dialog/DialogOverlay.astro +17 -0
- package/dist/astro-dialog/DialogTitle.astro +15 -0
- package/dist/astro-dialog/DialogTrigger.astro +18 -0
- package/dist/astro-dialog/index.ts +1 -0
- package/dist/astro-diff-viewer/DiffViewer.astro +371 -0
- package/dist/astro-diff-viewer/index.ts +1 -0
- package/dist/astro-dropdown-menu/DropdownMenu.astro +138 -0
- package/dist/astro-dropdown-menu/DropdownMenuContent.astro +20 -0
- package/dist/astro-dropdown-menu/DropdownMenuItem.astro +21 -0
- package/dist/astro-dropdown-menu/DropdownMenuLabel.astro +14 -0
- package/dist/astro-dropdown-menu/DropdownMenuSeparator.astro +14 -0
- package/dist/astro-dropdown-menu/DropdownMenuTrigger.astro +18 -0
- package/dist/astro-dropdown-menu/index.ts +1 -0
- package/dist/astro-emoji-picker/EmojiPicker.astro +255 -0
- package/dist/astro-emoji-picker/index.ts +1 -0
- package/dist/astro-feedback-dialog/FeedbackDialog.astro +152 -0
- package/dist/astro-feedback-dialog/index.ts +1 -0
- package/dist/astro-file-tree/astro-file-tree.astro +4 -0
- package/dist/astro-file-tree/index.ts +1 -0
- package/dist/astro-file-upload/FileUpload.astro +306 -0
- package/dist/astro-file-upload/index.ts +1 -0
- package/dist/astro-footer/Footer.astro +61 -0
- package/dist/astro-footer/index.ts +1 -0
- package/dist/astro-icon-system/astro-icon-system.astro +4 -0
- package/dist/astro-icon-system/index.ts +1 -0
- package/dist/astro-inline-editor/InlineEditor.astro +169 -0
- package/dist/astro-inline-editor/index.ts +1 -0
- package/dist/astro-input/Input.astro +49 -0
- package/dist/astro-input/index.ts +1 -0
- package/dist/astro-input-group/InputGroup.astro +29 -0
- package/dist/astro-input-group/InputGroupAddon.astro +14 -0
- package/dist/astro-input-group/InputGroupButton.astro +19 -0
- package/dist/astro-input-group/InputGroupText.astro +11 -0
- package/dist/astro-input-group/index.ts +1 -0
- package/dist/astro-install-prompt/InstallPrompt.astro +115 -0
- package/dist/astro-install-prompt/index.ts +1 -0
- package/dist/astro-keyboard-shortcut/KeyboardShortcut.astro +83 -0
- package/dist/astro-keyboard-shortcut/ShortcutBadge.astro +41 -0
- package/dist/astro-keyboard-shortcut/index.ts +1 -0
- package/dist/astro-language-selector/LanguageSelector.astro +247 -0
- package/dist/astro-language-selector/index.ts +1 -0
- package/dist/astro-link-card/LinkCard.astro +17 -0
- package/dist/astro-link-card/index.ts +1 -0
- package/dist/astro-markdown-renderer/MarkdownRenderer.astro +62 -0
- package/dist/astro-markdown-renderer/index.ts +1 -0
- package/dist/astro-mobile-nav/MobileNav.astro +84 -0
- package/dist/astro-mobile-nav/MobileNavContent.astro +29 -0
- package/dist/astro-mobile-nav/MobileNavLink.astro +16 -0
- package/dist/astro-mobile-nav/MobileNavTrigger.astro +57 -0
- package/dist/astro-mobile-nav/index.ts +1 -0
- package/dist/astro-navbar/Navbar.astro +49 -0
- package/dist/astro-navbar/index.ts +1 -0
- package/dist/astro-otp-input/OtpInput.astro +253 -0
- package/dist/astro-otp-input/index.ts +1 -0
- package/dist/astro-pagination/Pagination.astro +3 -0
- package/dist/astro-pagination/index.ts +1 -0
- package/dist/astro-payment/Payment.astro +19 -0
- package/dist/astro-payment/index.ts +1 -0
- package/dist/astro-popover/Popover.astro +87 -0
- package/dist/astro-popover/PopoverClose.astro +16 -0
- package/dist/astro-popover/PopoverContent.astro +23 -0
- package/dist/astro-popover/PopoverTrigger.astro +18 -0
- package/dist/astro-popover/index.ts +1 -0
- package/dist/astro-presence-indicator/PresenceIndicator.astro +26 -0
- package/dist/astro-presence-indicator/index.ts +1 -0
- package/dist/astro-progress-display/BadgeDisplay.astro +33 -0
- package/dist/astro-progress-display/ProgressBar.astro +27 -0
- package/dist/astro-progress-display/StatsGrid.astro +32 -0
- package/dist/astro-progress-display/index.ts +1 -0
- package/dist/astro-radio/RadioGroup.astro +111 -0
- package/dist/astro-radio/RadioItem.astro +36 -0
- package/dist/astro-radio/index.ts +1 -0
- package/dist/astro-reaction-bar/ReactionBar.astro +117 -0
- package/dist/astro-reaction-bar/index.ts +1 -0
- package/dist/astro-resizable-layout/ResizableDivider.astro +27 -0
- package/dist/astro-resizable-layout/ResizableLayout.astro +162 -0
- package/dist/astro-resizable-layout/ResizablePane.astro +28 -0
- package/dist/astro-resizable-layout/index.ts +11 -0
- package/dist/astro-rich-editor/RichEditor.astro +260 -0
- package/dist/astro-rich-editor/index.ts +25 -0
- package/dist/astro-search-bar/SearchBar.astro +140 -0
- package/dist/astro-search-bar/SearchResultItem.astro +16 -0
- package/dist/astro-search-bar/SearchResults.astro +17 -0
- package/dist/astro-search-bar/index.ts +1 -0
- package/dist/astro-select/Select.astro +165 -0
- package/dist/astro-select/SelectContent.astro +19 -0
- package/dist/astro-select/SelectItem.astro +42 -0
- package/dist/astro-select/SelectTrigger.astro +36 -0
- package/dist/astro-select/index.ts +1 -0
- package/dist/astro-sidebar/Sidebar.astro +64 -0
- package/dist/astro-sidebar/index.ts +1 -0
- package/dist/astro-skeleton/Skeleton.astro +39 -0
- package/dist/astro-skeleton/SkeletonText.astro +22 -0
- package/dist/astro-skeleton/index.ts +1 -0
- package/dist/astro-skip-to-content/SkipToContent.astro +3 -0
- package/dist/astro-skip-to-content/index.ts +1 -0
- package/dist/astro-slide-viewer/SlideViewer.astro +231 -0
- package/dist/astro-slide-viewer/index.ts +1 -0
- package/dist/astro-slider/index.ts +1 -0
- package/dist/astro-status-indicator/StatusIndicator.astro +31 -0
- package/dist/astro-status-indicator/index.ts +1 -0
- package/dist/astro-steps/Steps.astro +3 -0
- package/dist/astro-steps/index.ts +1 -0
- package/dist/astro-switch/Switch.astro +89 -0
- package/dist/astro-switch/index.ts +1 -0
- package/dist/astro-table-of-contents/TableOfContents.astro +53 -0
- package/dist/astro-table-of-contents/index.ts +1 -0
- package/dist/astro-tabs/Tabs.astro +96 -0
- package/dist/astro-tabs/TabsContent.astro +20 -0
- package/dist/astro-tabs/TabsList.astro +16 -0
- package/dist/astro-tabs/TabsTrigger.astro +24 -0
- package/dist/astro-tabs/index.ts +1 -0
- package/dist/astro-textarea/Textarea.astro +51 -0
- package/dist/astro-textarea/index.ts +1 -0
- package/dist/astro-theme/ThemeScript.astro +13 -0
- package/dist/astro-theme/ThemeToggle.astro +279 -0
- package/dist/astro-theme/index.ts +1 -0
- package/dist/astro-thread-view/ThreadView.astro +159 -0
- package/dist/astro-thread-view/index.ts +1 -0
- package/dist/astro-toast/Toast.astro +49 -0
- package/dist/astro-toast/Toaster.astro +142 -0
- package/dist/astro-toast/index.ts +1 -0
- package/dist/astro-tooltip/Tooltip.astro +95 -0
- package/dist/astro-tooltip/TooltipContent.astro +22 -0
- package/dist/astro-tooltip/TooltipTrigger.astro +16 -0
- package/dist/astro-tooltip/index.ts +1 -0
- package/dist/astro-version-selector/VersionSelector.astro +174 -0
- package/dist/astro-version-selector/index.ts +1 -0
- package/dist/astro-video-player/VideoPlayer.astro +153 -0
- package/dist/astro-video-player/index.ts +1 -0
- package/dist/index.ts +77 -0
- package/dist/shared/a11y.ts +33 -0
- package/dist/shared/aria.ts +30 -0
- package/dist/shared/cn.ts +35 -0
- package/dist/shared/cva.ts +79 -0
- package/dist/shared/engine-adapter.ts +10 -0
- package/dist/shared/focus-trap.ts +123 -0
- package/dist/shared/index.ts +60 -0
- package/dist/shared/keyboard.ts +36 -0
- package/dist/shared/live-region.ts +90 -0
- package/dist/shared/motion.ts +15 -0
- package/dist/shared/skip-link.ts +35 -0
- package/dist/shared/state-machine.ts +57 -0
- package/dist/shared/tokens.ts +16 -0
- package/dist/shared/types.ts +45 -0
- package/dist/shared/variants.ts +17 -0
- package/package.json +20 -19
- package/src/index.ts +0 -94
package/dist/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Auto-generated meta-package index
|
|
2
|
+
export * from './astro-accordion/index.ts';
|
|
3
|
+
export * from './astro-ai/index.ts';
|
|
4
|
+
export * from './astro-animated-text/index.ts';
|
|
5
|
+
export * from './astro-app-shell/index.ts';
|
|
6
|
+
export * from './astro-auth/index.ts';
|
|
7
|
+
export * from './astro-avatar/index.ts';
|
|
8
|
+
export * from './astro-avatar-group/index.ts';
|
|
9
|
+
export * from './astro-badge/index.ts';
|
|
10
|
+
export * from './astro-bottom-nav/index.ts';
|
|
11
|
+
export * from './astro-breadcrumbs/index.ts';
|
|
12
|
+
export * from './astro-button/index.ts';
|
|
13
|
+
export * from './astro-calendar/index.ts';
|
|
14
|
+
export * from './astro-card/index.ts';
|
|
15
|
+
export * from './astro-charts/index.ts';
|
|
16
|
+
export * from './astro-checkbox/index.ts';
|
|
17
|
+
export * from './astro-code-editor/index.ts';
|
|
18
|
+
export * from './astro-collapsible/index.ts';
|
|
19
|
+
export * from './astro-command/index.ts';
|
|
20
|
+
export * from './astro-content-protection/index.ts';
|
|
21
|
+
export * from './astro-data-table/index.ts';
|
|
22
|
+
export * from './astro-date-picker/index.ts';
|
|
23
|
+
export * from './astro-device-frame/index.ts';
|
|
24
|
+
export * from './astro-dialog/index.ts';
|
|
25
|
+
export * from './astro-diff-viewer/index.ts';
|
|
26
|
+
export * from './astro-dropdown-menu/index.ts';
|
|
27
|
+
export * from './astro-emoji-picker/index.ts';
|
|
28
|
+
export * from './astro-feedback-dialog/index.ts';
|
|
29
|
+
export * from './astro-file-upload/index.ts';
|
|
30
|
+
export * from './astro-footer/index.ts';
|
|
31
|
+
export * from './astro-inline-editor/index.ts';
|
|
32
|
+
export * from './astro-input/index.ts';
|
|
33
|
+
export * from './astro-input-group/index.ts';
|
|
34
|
+
export * from './astro-install-prompt/index.ts';
|
|
35
|
+
export * from './astro-keyboard-shortcut/index.ts';
|
|
36
|
+
export * from './astro-language-selector/index.ts';
|
|
37
|
+
export * from './astro-markdown-renderer/index.ts';
|
|
38
|
+
export * from './astro-mobile-nav/index.ts';
|
|
39
|
+
export * from './astro-navbar/index.ts';
|
|
40
|
+
export * from './astro-otp-input/index.ts';
|
|
41
|
+
export * from './astro-popover/index.ts';
|
|
42
|
+
export * from './astro-presence-indicator/index.ts';
|
|
43
|
+
export * from './astro-progress-display/index.ts';
|
|
44
|
+
export * from './astro-radio/index.ts';
|
|
45
|
+
export * from './astro-reaction-bar/index.ts';
|
|
46
|
+
export * from './astro-resizable-layout/index.ts';
|
|
47
|
+
export * from './astro-rich-editor/index.ts';
|
|
48
|
+
export * from './astro-search-bar/index.ts';
|
|
49
|
+
export * from './astro-select/index.ts';
|
|
50
|
+
export * from './astro-sidebar/index.ts';
|
|
51
|
+
export * from './astro-skeleton/index.ts';
|
|
52
|
+
export * from './astro-slide-viewer/index.ts';
|
|
53
|
+
export * from './astro-status-indicator/index.ts';
|
|
54
|
+
export * from './astro-switch/index.ts';
|
|
55
|
+
export * from './astro-tabs/index.ts';
|
|
56
|
+
export * from './astro-textarea/index.ts';
|
|
57
|
+
export * from './astro-theme/index.ts';
|
|
58
|
+
export * from './astro-thread-view/index.ts';
|
|
59
|
+
export * from './astro-toast/index.ts';
|
|
60
|
+
export * from './astro-tooltip/index.ts';
|
|
61
|
+
export * from './astro-version-selector/index.ts';
|
|
62
|
+
export * from './astro-video-player/index.ts';
|
|
63
|
+
export * from './shared/index.ts';
|
|
64
|
+
export * from './astro-table-of-contents/index.ts';
|
|
65
|
+
export * from './astro-carousel/index.ts';
|
|
66
|
+
export * from './astro-slider/index.ts';
|
|
67
|
+
export * from './astro-pagination/index.ts';
|
|
68
|
+
export * from './astro-callout/index.ts';
|
|
69
|
+
export * from './astro-steps/index.ts';
|
|
70
|
+
export * from './astro-file-tree/index.ts';
|
|
71
|
+
export * from './astro-icon-system/index.ts';
|
|
72
|
+
export * from './astro-skip-to-content/index.ts';
|
|
73
|
+
export * from './astro-code-block/index.ts';
|
|
74
|
+
export * from './astro-link-card/index.ts';
|
|
75
|
+
export * from './astro-card-grid/index.ts';
|
|
76
|
+
export * from './astro-payment/index.ts';
|
|
77
|
+
export * from './astro-command-input/index.ts';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility constants and utilities used across all components.
|
|
3
|
+
*
|
|
4
|
+
* Refraction UI Accessibility Principles:
|
|
5
|
+
* 1. NEVER convey meaning through color alone — always provide icon, text, or pattern
|
|
6
|
+
* 2. All interactive elements are keyboard navigable
|
|
7
|
+
* 3. All dynamic content has aria-live announcements
|
|
8
|
+
* 4. All colors meet WCAG AA contrast ratios (4.5:1 normal, 3:1 large)
|
|
9
|
+
* 5. Respect prefers-reduced-motion
|
|
10
|
+
* 6. Respect prefers-contrast
|
|
11
|
+
* 7. Focus indicators are always visible on keyboard navigation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Standard icon names used across components for status indication */
|
|
15
|
+
export const STATUS_ICONS = {
|
|
16
|
+
success: 'check-circle',
|
|
17
|
+
error: 'x-circle',
|
|
18
|
+
warning: 'alert-triangle',
|
|
19
|
+
info: 'info-circle',
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
export type StatusIconName = typeof STATUS_ICONS[keyof typeof STATUS_ICONS]
|
|
23
|
+
|
|
24
|
+
/** Check if a component state relies on color alone (for testing) */
|
|
25
|
+
export function hasNonColorIndicator(config: {
|
|
26
|
+
hasIcon?: boolean
|
|
27
|
+
hasText?: boolean
|
|
28
|
+
hasPattern?: boolean
|
|
29
|
+
hasBorder?: boolean
|
|
30
|
+
hasShape?: boolean
|
|
31
|
+
}): boolean {
|
|
32
|
+
return Object.values(config).some(Boolean)
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Merge multiple ARIA prop objects, later values override earlier ones */
|
|
2
|
+
export function mergeAriaProps(
|
|
3
|
+
...propSets: Array<Record<string, unknown>>
|
|
4
|
+
): Record<string, unknown> {
|
|
5
|
+
const result: Record<string, unknown> = {}
|
|
6
|
+
for (const props of propSets) {
|
|
7
|
+
for (const [key, value] of Object.entries(props)) {
|
|
8
|
+
if (value !== undefined) {
|
|
9
|
+
result[key] = value
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let idCounter = 0
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a unique ID, safe for SSR (deterministic within a render pass).
|
|
20
|
+
* In browsers, uses crypto.randomUUID when available.
|
|
21
|
+
*/
|
|
22
|
+
export function generateId(prefix = 'rfr'): string {
|
|
23
|
+
idCounter++
|
|
24
|
+
return `${prefix}-${idCounter}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Reset the ID counter (useful for tests) */
|
|
28
|
+
export function resetIdCounter(): void {
|
|
29
|
+
idCounter = 0
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight class name utility — our own implementation.
|
|
3
|
+
* Handles conditional classes, arrays, and falsy values.
|
|
4
|
+
* No external dependencies (no clsx, no tailwind-merge).
|
|
5
|
+
*
|
|
6
|
+
* For Tailwind class conflict resolution (e.g., 'p-2 p-4' → 'p-4'),
|
|
7
|
+
* consumers can use @refraction-ui/tailwind-config which provides
|
|
8
|
+
* a tw-merge-aware variant of this function.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type ClassValue = string | number | boolean | undefined | null | ClassValue[]
|
|
12
|
+
type ClassRecord = Record<string, boolean | undefined | null>
|
|
13
|
+
|
|
14
|
+
export function cn(...inputs: Array<ClassValue | ClassRecord>): string {
|
|
15
|
+
const classes: string[] = []
|
|
16
|
+
|
|
17
|
+
for (const input of inputs) {
|
|
18
|
+
if (!input) continue
|
|
19
|
+
|
|
20
|
+
if (typeof input === 'string') {
|
|
21
|
+
classes.push(input)
|
|
22
|
+
} else if (typeof input === 'number') {
|
|
23
|
+
classes.push(String(input))
|
|
24
|
+
} else if (Array.isArray(input)) {
|
|
25
|
+
const nested = cn(...input)
|
|
26
|
+
if (nested) classes.push(nested)
|
|
27
|
+
} else if (typeof input === 'object') {
|
|
28
|
+
for (const [key, value] of Object.entries(input)) {
|
|
29
|
+
if (value) classes.push(key)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return classes.join(' ')
|
|
35
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight class-variance-authority alternative — zero dependencies.
|
|
3
|
+
* Creates variant-driven class name functions for components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface VariantConfig {
|
|
7
|
+
[variant: string]: Record<string, string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CVAConfig<V extends VariantConfig> {
|
|
11
|
+
base?: string
|
|
12
|
+
variants?: V
|
|
13
|
+
defaultVariants?: {
|
|
14
|
+
[K in keyof V]?: keyof V[K]
|
|
15
|
+
}
|
|
16
|
+
compoundVariants?: Array<
|
|
17
|
+
{
|
|
18
|
+
[K in keyof V]?: keyof V[K]
|
|
19
|
+
} & { class: string }
|
|
20
|
+
>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type VariantProps<V extends VariantConfig> = {
|
|
24
|
+
[K in keyof V]?: keyof V[K]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function cva<V extends VariantConfig>(config: CVAConfig<V>) {
|
|
28
|
+
return (props?: VariantProps<V> & { className?: string }): string => {
|
|
29
|
+
const classes: string[] = []
|
|
30
|
+
|
|
31
|
+
if (config.base) {
|
|
32
|
+
classes.push(config.base)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.variants) {
|
|
36
|
+
for (const [variantKey, variantOptions] of Object.entries(config.variants)) {
|
|
37
|
+
const selectedValue =
|
|
38
|
+
(props as Record<string, unknown> | undefined)?.[variantKey] ??
|
|
39
|
+
config.defaultVariants?.[variantKey]
|
|
40
|
+
|
|
41
|
+
if (selectedValue != null) {
|
|
42
|
+
const variantClass = (variantOptions as Record<string, string>)[
|
|
43
|
+
selectedValue as string
|
|
44
|
+
]
|
|
45
|
+
if (variantClass) {
|
|
46
|
+
classes.push(variantClass)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (config.compoundVariants) {
|
|
53
|
+
for (const compound of config.compoundVariants) {
|
|
54
|
+
const { class: compoundClass, ...conditions } = compound
|
|
55
|
+
let matches = true
|
|
56
|
+
|
|
57
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
58
|
+
const propValue =
|
|
59
|
+
(props as Record<string, unknown>)?.[key] ??
|
|
60
|
+
config.defaultVariants?.[key]
|
|
61
|
+
if (propValue !== value) {
|
|
62
|
+
matches = false
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (matches) {
|
|
68
|
+
classes.push(compoundClass as string)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (props?.className) {
|
|
74
|
+
classes.push(props.className)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return classes.filter(Boolean).join(' ')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface EngineAdapter<
|
|
2
|
+
TState = Record<string, unknown>,
|
|
3
|
+
TMethods = Record<string, unknown>,
|
|
4
|
+
TReturnProps = Record<string, unknown>
|
|
5
|
+
> {
|
|
6
|
+
name: string;
|
|
7
|
+
state: TState;
|
|
8
|
+
methods: TMethods;
|
|
9
|
+
getProps: (elementName: string, props?: Record<string, unknown>) => TReturnProps;
|
|
10
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus trap utilities — keeps keyboard focus within a container.
|
|
3
|
+
* Used by Dialog, DropdownMenu, and other overlay components.
|
|
4
|
+
* Pure TypeScript, no DOM dependency (accepts element via parameter).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Selector for all natively focusable elements */
|
|
8
|
+
export const FOCUSABLE_SELECTOR =
|
|
9
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
10
|
+
|
|
11
|
+
/** Get all focusable elements within a container */
|
|
12
|
+
export function getFocusableElements(container: Element): Element[] {
|
|
13
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Configuration for creating a focus trap */
|
|
17
|
+
export interface FocusTrapConfig {
|
|
18
|
+
/** The container element */
|
|
19
|
+
container: Element
|
|
20
|
+
/** Called to get the initial focus target */
|
|
21
|
+
initialFocus?: () => Element | null
|
|
22
|
+
/** Called when Escape is pressed */
|
|
23
|
+
onEscape?: () => void
|
|
24
|
+
/** Whether to return focus to trigger on deactivate */
|
|
25
|
+
returnFocusOnDeactivate?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A focus trap instance */
|
|
29
|
+
export interface FocusTrap {
|
|
30
|
+
activate(): void
|
|
31
|
+
deactivate(): void
|
|
32
|
+
isActive(): boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Create a focus trap that keeps Tab/Shift+Tab within a container */
|
|
36
|
+
export function createFocusTrap(config: FocusTrapConfig): FocusTrap {
|
|
37
|
+
let active = false
|
|
38
|
+
let previouslyFocused: Element | null = null
|
|
39
|
+
|
|
40
|
+
function handleKeyDown(event: KeyboardEvent): void {
|
|
41
|
+
if (event.key === 'Escape' && config.onEscape) {
|
|
42
|
+
event.preventDefault()
|
|
43
|
+
config.onEscape()
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (event.key !== 'Tab') return
|
|
48
|
+
|
|
49
|
+
const focusable = getFocusableElements(config.container)
|
|
50
|
+
if (focusable.length === 0) return
|
|
51
|
+
|
|
52
|
+
const first = focusable[0] as HTMLElement
|
|
53
|
+
const last = focusable[focusable.length - 1] as HTMLElement
|
|
54
|
+
|
|
55
|
+
if (event.shiftKey) {
|
|
56
|
+
// Shift+Tab: if on first element, wrap to last
|
|
57
|
+
if (document.activeElement === first) {
|
|
58
|
+
event.preventDefault()
|
|
59
|
+
last.focus()
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// Tab: if on last element, wrap to first
|
|
63
|
+
if (document.activeElement === last) {
|
|
64
|
+
event.preventDefault()
|
|
65
|
+
first.focus()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
activate() {
|
|
72
|
+
if (active) return
|
|
73
|
+
active = true
|
|
74
|
+
|
|
75
|
+
// Remember the currently focused element so we can restore it later
|
|
76
|
+
if (typeof document !== 'undefined') {
|
|
77
|
+
previouslyFocused = document.activeElement
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Set initial focus
|
|
81
|
+
if (config.initialFocus) {
|
|
82
|
+
const target = config.initialFocus()
|
|
83
|
+
if (target && 'focus' in target) {
|
|
84
|
+
(target as HTMLElement).focus()
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
const focusable = getFocusableElements(config.container)
|
|
88
|
+
if (focusable.length > 0) {
|
|
89
|
+
(focusable[0] as HTMLElement).focus()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Attach keydown listener to the container
|
|
94
|
+
config.container.addEventListener(
|
|
95
|
+
'keydown',
|
|
96
|
+
handleKeyDown as EventListener,
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
deactivate() {
|
|
101
|
+
if (!active) return
|
|
102
|
+
active = false
|
|
103
|
+
|
|
104
|
+
config.container.removeEventListener(
|
|
105
|
+
'keydown',
|
|
106
|
+
handleKeyDown as EventListener,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// Restore focus to the previously focused element
|
|
110
|
+
if (
|
|
111
|
+
config.returnFocusOnDeactivate !== false &&
|
|
112
|
+
previouslyFocused &&
|
|
113
|
+
'focus' in previouslyFocused
|
|
114
|
+
) {
|
|
115
|
+
(previouslyFocused as HTMLElement).focus()
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
isActive() {
|
|
120
|
+
return active
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
BaseProps,
|
|
4
|
+
AccessibilityProps,
|
|
5
|
+
ThemeProps,
|
|
6
|
+
CompositionProps,
|
|
7
|
+
} from './types.js'
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
Size,
|
|
11
|
+
Variant,
|
|
12
|
+
Orientation,
|
|
13
|
+
Side,
|
|
14
|
+
Align,
|
|
15
|
+
DataState,
|
|
16
|
+
} from './variants.js'
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
TokenContract,
|
|
20
|
+
TokenDefinition,
|
|
21
|
+
} from './tokens.js'
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
KeyboardKey,
|
|
25
|
+
KeyboardHandlerMap,
|
|
26
|
+
} from './keyboard.js'
|
|
27
|
+
|
|
28
|
+
export type {
|
|
29
|
+
MachineConfig,
|
|
30
|
+
Machine,
|
|
31
|
+
} from './state-machine.js'
|
|
32
|
+
|
|
33
|
+
export type {
|
|
34
|
+
FocusTrapConfig,
|
|
35
|
+
FocusTrap,
|
|
36
|
+
} from './focus-trap.js'
|
|
37
|
+
|
|
38
|
+
export type {
|
|
39
|
+
LiveRegionConfig,
|
|
40
|
+
LiveRegion,
|
|
41
|
+
} from './live-region.js'
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
EngineAdapter,
|
|
45
|
+
} from './engine-adapter.js'
|
|
46
|
+
|
|
47
|
+
export type {
|
|
48
|
+
SkipLinkProps,
|
|
49
|
+
} from './skip-link.js'
|
|
50
|
+
|
|
51
|
+
// Functions
|
|
52
|
+
export { mergeAriaProps, generateId, resetIdCounter } from './aria.js'
|
|
53
|
+
export { Keys, createKeyboardHandler } from './keyboard.js'
|
|
54
|
+
export { createMachine } from './state-machine.js'
|
|
55
|
+
export { cn } from './cn.js'
|
|
56
|
+
export { cva } from './cva.js'
|
|
57
|
+
export { FOCUSABLE_SELECTOR, getFocusableElements, createFocusTrap } from './focus-trap.js'
|
|
58
|
+
export { createLiveRegion } from './live-region.js'
|
|
59
|
+
export { prefersReducedMotion, getAnimationDuration } from './motion.js'
|
|
60
|
+
export { createSkipLink } from './skip-link.js'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Standard keyboard key constants */
|
|
2
|
+
export const Keys = {
|
|
3
|
+
Enter: 'Enter',
|
|
4
|
+
Space: ' ',
|
|
5
|
+
Escape: 'Escape',
|
|
6
|
+
Tab: 'Tab',
|
|
7
|
+
ArrowUp: 'ArrowUp',
|
|
8
|
+
ArrowDown: 'ArrowDown',
|
|
9
|
+
ArrowLeft: 'ArrowLeft',
|
|
10
|
+
ArrowRight: 'ArrowRight',
|
|
11
|
+
Home: 'Home',
|
|
12
|
+
End: 'End',
|
|
13
|
+
PageUp: 'PageUp',
|
|
14
|
+
PageDown: 'PageDown',
|
|
15
|
+
Backspace: 'Backspace',
|
|
16
|
+
Delete: 'Delete',
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
export type KeyboardKey = (typeof Keys)[keyof typeof Keys]
|
|
20
|
+
|
|
21
|
+
/** Map of key → handler function */
|
|
22
|
+
export type KeyboardHandlerMap = Partial<
|
|
23
|
+
Record<string, (event: KeyboardEvent) => void>
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
/** Create a keyboard event handler from a handler map */
|
|
27
|
+
export function createKeyboardHandler(
|
|
28
|
+
handlers: KeyboardHandlerMap,
|
|
29
|
+
): (event: KeyboardEvent) => void {
|
|
30
|
+
return (event: KeyboardEvent) => {
|
|
31
|
+
const handler = handlers[event.key]
|
|
32
|
+
if (handler) {
|
|
33
|
+
handler(event)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen reader announcement utility.
|
|
3
|
+
* Creates and manages an aria-live region for dynamic content updates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface LiveRegionConfig {
|
|
7
|
+
/** 'polite' (default) or 'assertive' */
|
|
8
|
+
politeness?: 'polite' | 'assertive'
|
|
9
|
+
/** Clear the announcement after this many ms. Default: 5000 */
|
|
10
|
+
clearAfterMs?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LiveRegion {
|
|
14
|
+
announce(message: string): void
|
|
15
|
+
clear(): void
|
|
16
|
+
destroy(): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Create a live region that announces messages to screen readers */
|
|
20
|
+
export function createLiveRegion(config: LiveRegionConfig = {}): LiveRegion {
|
|
21
|
+
const { politeness = 'polite', clearAfterMs = 5000 } = config
|
|
22
|
+
let element: HTMLElement | null = null
|
|
23
|
+
let clearTimeout: ReturnType<typeof setTimeout> | null = null
|
|
24
|
+
|
|
25
|
+
function getElement(): HTMLElement {
|
|
26
|
+
if (!element && typeof document !== 'undefined') {
|
|
27
|
+
element = document.createElement('div')
|
|
28
|
+
element.setAttribute('role', 'status')
|
|
29
|
+
element.setAttribute('aria-live', politeness)
|
|
30
|
+
element.setAttribute('aria-atomic', 'true')
|
|
31
|
+
// Visually hidden but accessible to screen readers
|
|
32
|
+
Object.assign(element.style, {
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
width: '1px',
|
|
35
|
+
height: '1px',
|
|
36
|
+
padding: '0',
|
|
37
|
+
margin: '-1px',
|
|
38
|
+
overflow: 'hidden',
|
|
39
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
40
|
+
whiteSpace: 'nowrap',
|
|
41
|
+
border: '0',
|
|
42
|
+
})
|
|
43
|
+
document.body.appendChild(element)
|
|
44
|
+
}
|
|
45
|
+
return element!
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
announce(message: string): void {
|
|
50
|
+
const el = getElement()
|
|
51
|
+
if (!el) return
|
|
52
|
+
|
|
53
|
+
// Clear any pending timeout
|
|
54
|
+
if (clearTimeout !== null) {
|
|
55
|
+
globalThis.clearTimeout(clearTimeout)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Set text content — screen readers will detect the change via aria-live
|
|
59
|
+
el.textContent = message
|
|
60
|
+
|
|
61
|
+
if (clearAfterMs > 0) {
|
|
62
|
+
clearTimeout = globalThis.setTimeout(() => {
|
|
63
|
+
el.textContent = ''
|
|
64
|
+
clearTimeout = null
|
|
65
|
+
}, clearAfterMs)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
clear(): void {
|
|
70
|
+
if (clearTimeout !== null) {
|
|
71
|
+
globalThis.clearTimeout(clearTimeout)
|
|
72
|
+
clearTimeout = null
|
|
73
|
+
}
|
|
74
|
+
if (element) {
|
|
75
|
+
element.textContent = ''
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
destroy(): void {
|
|
80
|
+
if (clearTimeout !== null) {
|
|
81
|
+
globalThis.clearTimeout(clearTimeout)
|
|
82
|
+
clearTimeout = null
|
|
83
|
+
}
|
|
84
|
+
if (element && element.parentNode) {
|
|
85
|
+
element.parentNode.removeChild(element)
|
|
86
|
+
element = null
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduced motion utilities.
|
|
3
|
+
* Respects user preferences for reduced motion.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Check if user prefers reduced motion */
|
|
7
|
+
export function prefersReducedMotion(): boolean {
|
|
8
|
+
if (typeof window === 'undefined') return false
|
|
9
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Get animation duration — returns '0ms' if reduced motion preferred */
|
|
13
|
+
export function getAnimationDuration(normalDuration: string): string {
|
|
14
|
+
return prefersReducedMotion() ? '0ms' : normalDuration
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip-to-content link utility.
|
|
3
|
+
* Provides a keyboard-accessible link that is visually hidden until focused.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Props for a skip-to-content link */
|
|
7
|
+
export interface SkipLinkProps {
|
|
8
|
+
/** ID of the target element. Default: 'main-content' */
|
|
9
|
+
targetId?: string
|
|
10
|
+
/** Label text for the link. Default: 'Skip to main content' */
|
|
11
|
+
label?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Create props for a skip-to-content link */
|
|
15
|
+
export function createSkipLink(props: SkipLinkProps = {}): {
|
|
16
|
+
ariaProps: Record<string, string>
|
|
17
|
+
href: string
|
|
18
|
+
label: string
|
|
19
|
+
/** CSS classes — visually hidden until focused */
|
|
20
|
+
className: string
|
|
21
|
+
} {
|
|
22
|
+
const targetId = props.targetId ?? 'main-content'
|
|
23
|
+
const label = props.label ?? 'Skip to main content'
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ariaProps: {
|
|
27
|
+
role: 'link',
|
|
28
|
+
'aria-label': label,
|
|
29
|
+
},
|
|
30
|
+
href: `#${targetId}`,
|
|
31
|
+
label,
|
|
32
|
+
className:
|
|
33
|
+
'sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-black focus:underline',
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal state machine — zero dependencies, < 1KB.
|
|
3
|
+
* Inspired by XState concepts but dramatically simpler.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface MachineConfig<TState extends string, TEvent extends string> {
|
|
7
|
+
initial: TState
|
|
8
|
+
states: Record<TState, {
|
|
9
|
+
on?: Partial<Record<TEvent, TState>>
|
|
10
|
+
}>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Machine<TState extends string, TEvent extends string> {
|
|
14
|
+
/** Current state */
|
|
15
|
+
state: TState
|
|
16
|
+
/** Send an event to transition */
|
|
17
|
+
send(event: TEvent): void
|
|
18
|
+
/** Subscribe to state changes. Returns unsubscribe function. */
|
|
19
|
+
subscribe(fn: (state: TState) => void): () => void
|
|
20
|
+
/** Check if machine is in a given state */
|
|
21
|
+
matches(state: TState): boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createMachine<TState extends string, TEvent extends string>(
|
|
25
|
+
config: MachineConfig<TState, TEvent>,
|
|
26
|
+
): Machine<TState, TEvent> {
|
|
27
|
+
let current = config.initial
|
|
28
|
+
const listeners = new Set<(state: TState) => void>()
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
get state() {
|
|
32
|
+
return current
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
send(event: TEvent) {
|
|
36
|
+
const stateConfig = config.states[current]
|
|
37
|
+
const next = stateConfig?.on?.[event]
|
|
38
|
+
if (next && next !== current) {
|
|
39
|
+
current = next
|
|
40
|
+
for (const fn of listeners) {
|
|
41
|
+
fn(current)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
subscribe(fn: (state: TState) => void) {
|
|
47
|
+
listeners.add(fn)
|
|
48
|
+
return () => {
|
|
49
|
+
listeners.delete(fn)
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
matches(state: TState) {
|
|
54
|
+
return current === state
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
}
|