@refraction-ui/astro 0.4.2 → 0.4.4
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/accordion/index.ts +1 -0
- package/dist/ai/ai-manager.ts +121 -0
- package/dist/ai/index.ts +23 -0
- package/dist/ai/mock-providers.ts +68 -0
- package/dist/ai/sanitizer.ts +174 -0
- package/dist/ai/tts-manager.ts +80 -0
- package/dist/ai/types.ts +72 -0
- package/dist/animated-text/animated-text.styles.ts +28 -0
- package/dist/animated-text/animated-text.ts +83 -0
- package/dist/animated-text/index.ts +15 -0
- package/dist/app-shell/app-shell.ts +252 -0
- package/dist/app-shell/auth-shell.ts +84 -0
- package/dist/app-shell/index.ts +22 -0
- package/dist/app-shell/page-shell.ts +125 -0
- package/dist/astro-accordion/Button.astro +1 -1
- package/dist/astro-ai/index.ts +1 -1
- package/dist/astro-animated-text/AnimatedText.astro +1 -1
- package/dist/astro-animated-text/TypewriterText.astro +1 -1
- package/dist/astro-app-shell/AppShell.astro +1 -1
- package/dist/astro-app-shell/AuthShell.astro +1 -1
- package/dist/astro-app-shell/AuthShellCard.astro +1 -1
- package/dist/astro-app-shell/PageShell.astro +1 -1
- package/dist/astro-app-shell/PageShellSection.astro +1 -1
- package/dist/astro-app-shell/index.ts +1 -1
- package/dist/astro-auth/index.ts +2 -2
- package/dist/astro-avatar/Avatar.astro +1 -1
- package/dist/astro-avatar/AvatarFallback.astro +1 -1
- package/dist/astro-avatar/AvatarImage.astro +1 -1
- package/dist/astro-avatar-group/AvatarGroup.astro +1 -1
- package/dist/astro-badge/Badge.astro +1 -1
- package/dist/astro-bottom-nav/BottomNav.astro +1 -1
- package/dist/astro-breadcrumbs/Breadcrumbs.astro +1 -1
- package/dist/astro-button/Button.astro +1 -1
- package/dist/astro-calendar/Calendar.astro +1 -1
- package/dist/astro-card/Card.astro +1 -1
- package/dist/astro-card/CardContent.astro +1 -1
- package/dist/astro-card/CardDescription.astro +1 -1
- package/dist/astro-card/CardFooter.astro +1 -1
- package/dist/astro-card/CardHeader.astro +1 -1
- package/dist/astro-card/CardTitle.astro +1 -1
- package/dist/astro-card-grid/CardGrid.astro +1 -1
- package/dist/astro-carousel/Button.astro +1 -1
- package/dist/astro-charts/Bars.astro +1 -1
- package/dist/astro-charts/Chart.astro +1 -1
- package/dist/astro-charts/Circles.astro +1 -1
- package/dist/astro-charts/Histogram.astro +1 -1
- package/dist/astro-charts/Line.astro +1 -1
- package/dist/astro-charts/PieChart.astro +1 -1
- package/dist/astro-charts/ScatterPlot.astro +1 -1
- package/dist/astro-charts/index.ts +1 -1
- package/dist/astro-checkbox/Checkbox.astro +1 -1
- package/dist/astro-code-editor/CodeEditor.astro +1 -1
- package/dist/astro-collapsible/Collapsible.astro +1 -1
- package/dist/astro-collapsible/CollapsibleContent.astro +1 -1
- package/dist/astro-command/Command.astro +1 -1
- package/dist/astro-command/CommandGroup.astro +1 -1
- package/dist/astro-command/CommandInput.astro +1 -1
- package/dist/astro-command/CommandItem.astro +1 -1
- package/dist/astro-command-input/CommandInput.astro +1 -1
- package/dist/astro-content-protection/ContentProtection.astro +1 -1
- package/dist/astro-data-table/DataTable.astro +1 -1
- package/dist/astro-date-picker/DatePicker.astro +1 -1
- package/dist/astro-device-frame/DeviceFrame.astro +1 -1
- package/dist/astro-dialog/DialogContent.astro +1 -1
- package/dist/astro-dialog/DialogOverlay.astro +1 -1
- package/dist/astro-diff-viewer/DiffViewer.astro +1 -1
- package/dist/astro-dropdown-menu/DropdownMenuContent.astro +1 -1
- package/dist/astro-dropdown-menu/DropdownMenuItem.astro +1 -1
- package/dist/astro-emoji-picker/EmojiPicker.astro +1 -1
- package/dist/astro-feedback-dialog/FeedbackDialog.astro +1 -1
- package/dist/astro-file-upload/FileUpload.astro +1 -1
- package/dist/astro-footer/Footer.astro +1 -1
- package/dist/astro-inline-editor/InlineEditor.astro +1 -1
- package/dist/astro-input/Input.astro +1 -1
- package/dist/astro-input-group/InputGroup.astro +1 -1
- package/dist/astro-input-group/InputGroupAddon.astro +1 -1
- package/dist/astro-input-group/InputGroupButton.astro +1 -1
- package/dist/astro-install-prompt/InstallPrompt.astro +1 -1
- package/dist/astro-keyboard-shortcut/ShortcutBadge.astro +1 -1
- package/dist/astro-language-selector/LanguageSelector.astro +1 -1
- package/dist/astro-link-card/LinkCard.astro +1 -1
- package/dist/astro-markdown-renderer/MarkdownRenderer.astro +1 -1
- package/dist/astro-mobile-nav/MobileNav.astro +1 -1
- package/dist/astro-mobile-nav/MobileNavLink.astro +1 -1
- package/dist/astro-mobile-nav/MobileNavTrigger.astro +1 -1
- package/dist/astro-navbar/Navbar.astro +1 -1
- package/dist/astro-otp-input/OtpInput.astro +1 -1
- package/dist/astro-payment/Payment.astro +1 -1
- package/dist/astro-popover/PopoverContent.astro +1 -1
- package/dist/astro-presence-indicator/PresenceIndicator.astro +1 -1
- package/dist/astro-progress-display/BadgeDisplay.astro +1 -1
- package/dist/astro-progress-display/ProgressBar.astro +1 -1
- package/dist/astro-progress-display/StatsGrid.astro +1 -1
- package/dist/astro-radio/RadioGroup.astro +1 -1
- package/dist/astro-radio/RadioItem.astro +1 -1
- package/dist/astro-reaction-bar/ReactionBar.astro +1 -1
- package/dist/astro-resizable-layout/ResizableDivider.astro +1 -1
- package/dist/astro-resizable-layout/ResizableLayout.astro +1 -1
- package/dist/astro-resizable-layout/ResizablePane.astro +1 -1
- package/dist/astro-resizable-layout/index.ts +1 -1
- package/dist/astro-rich-editor/index.ts +2 -2
- package/dist/astro-search-bar/SearchBar.astro +1 -1
- package/dist/astro-search-bar/SearchResults.astro +1 -1
- package/dist/astro-select/Select.astro +1 -1
- package/dist/astro-select/SelectContent.astro +1 -1
- package/dist/astro-select/SelectItem.astro +1 -1
- package/dist/astro-select/SelectTrigger.astro +1 -1
- package/dist/astro-sidebar/Sidebar.astro +1 -1
- package/dist/astro-skeleton/Skeleton.astro +1 -1
- package/dist/astro-slide-viewer/SlideViewer.astro +1 -1
- package/dist/astro-status-indicator/StatusIndicator.astro +1 -1
- package/dist/astro-switch/Switch.astro +1 -1
- package/dist/astro-table-of-contents/TableOfContents.astro +1 -1
- package/dist/astro-tabs/Tabs.astro +1 -1
- package/dist/astro-tabs/TabsList.astro +1 -1
- package/dist/astro-tabs/TabsTrigger.astro +1 -1
- package/dist/astro-textarea/Textarea.astro +1 -1
- package/dist/astro-theme/ThemeScript.astro +1 -1
- package/dist/astro-thread-view/ThreadView.astro +1 -1
- package/dist/astro-toast/Toast.astro +1 -1
- package/dist/astro-tooltip/TooltipContent.astro +1 -1
- package/dist/astro-version-selector/VersionSelector.astro +1 -1
- package/dist/astro-video-player/VideoPlayer.astro +1 -1
- package/dist/auth/adapters/mock.ts +74 -0
- package/dist/auth/auth-machine.ts +120 -0
- package/dist/auth/index.ts +22 -0
- package/dist/auth/rbac.ts +44 -0
- package/dist/auth/types.ts +44 -0
- package/dist/avatar/avatar.styles.ts +47 -0
- package/dist/avatar/avatar.ts +73 -0
- package/dist/avatar/index.ts +15 -0
- package/dist/avatar-group/avatar-group.styles.ts +63 -0
- package/dist/avatar-group/avatar-group.ts +106 -0
- package/dist/avatar-group/index.ts +18 -0
- package/dist/badge/badge.styles.ts +24 -0
- package/dist/badge/badge.ts +48 -0
- package/dist/badge/index.ts +10 -0
- package/dist/bottom-nav/bottom-nav.styles.ts +18 -0
- package/dist/bottom-nav/bottom-nav.ts +33 -0
- package/dist/bottom-nav/index.ts +2 -0
- package/dist/breadcrumbs/breadcrumbs.styles.ts +20 -0
- package/dist/breadcrumbs/breadcrumbs.ts +72 -0
- package/dist/breadcrumbs/index.ts +8 -0
- package/dist/button/button.styles.ts +38 -0
- package/dist/button/button.ts +64 -0
- package/dist/button/index.ts +10 -0
- package/dist/calendar/calendar.styles.ts +21 -0
- package/dist/calendar/calendar.ts +202 -0
- package/dist/calendar/index.ts +9 -0
- package/dist/callout/callout.ts +1 -0
- package/dist/callout/index.ts +1 -0
- package/dist/card/card.styles.ts +55 -0
- package/dist/card/card.ts +45 -0
- package/dist/card/index.ts +20 -0
- package/dist/card-grid/card-grid.ts +9 -0
- package/dist/card-grid/index.ts +1 -0
- package/dist/carousel/index.ts +1 -0
- package/dist/charts/axis.ts +50 -0
- package/dist/charts/chart.ts +73 -0
- package/dist/charts/colorblind.ts +100 -0
- package/dist/charts/data.ts +72 -0
- package/dist/charts/geometry.ts +54 -0
- package/dist/charts/index.ts +61 -0
- package/dist/charts/scales.ts +151 -0
- package/dist/checkbox/checkbox.styles.ts +33 -0
- package/dist/checkbox/checkbox.ts +73 -0
- package/dist/checkbox/index.ts +10 -0
- package/dist/code-block/code-block.ts +1 -0
- package/dist/code-block/index.ts +1 -0
- package/dist/code-editor/code-editor.styles.ts +32 -0
- package/dist/code-editor/code-editor.ts +140 -0
- package/dist/code-editor/index.ts +9 -0
- package/dist/collapsible/collapsible.styles.ts +14 -0
- package/dist/collapsible/collapsible.ts +93 -0
- package/dist/collapsible/index.ts +7 -0
- package/dist/command/command.styles.ts +27 -0
- package/dist/command/command.ts +179 -0
- package/dist/command/index.ts +14 -0
- package/dist/command-input/command-input.ts +176 -0
- package/dist/command-input/index.ts +1 -0
- package/dist/content-protection/content-protection.styles.ts +9 -0
- package/dist/content-protection/content-protection.ts +68 -0
- package/dist/content-protection/index.ts +8 -0
- package/dist/data-table/data-table.styles.ts +55 -0
- package/dist/data-table/data-table.ts +183 -0
- package/dist/data-table/index.ts +15 -0
- package/dist/date-picker/date-picker.styles.ts +31 -0
- package/dist/date-picker/date-picker.ts +333 -0
- package/dist/date-picker/index.ts +18 -0
- package/dist/device-frame/device-frame.styles.ts +21 -0
- package/dist/device-frame/device-frame.ts +112 -0
- package/dist/device-frame/index.ts +10 -0
- package/dist/dialog/dialog.styles.ts +8 -0
- package/dist/dialog/dialog.ts +133 -0
- package/dist/dialog/index.ts +8 -0
- package/dist/diff-viewer/diff-viewer.styles.ts +124 -0
- package/dist/diff-viewer/diff-viewer.ts +255 -0
- package/dist/diff-viewer/index.ts +20 -0
- package/dist/dropdown-menu/dropdown-menu.styles.ts +9 -0
- package/dist/dropdown-menu/dropdown-menu.ts +139 -0
- package/dist/dropdown-menu/index.ts +9 -0
- package/dist/emoji-picker/emoji-data.ts +282 -0
- package/dist/emoji-picker/emoji-picker.styles.ts +32 -0
- package/dist/emoji-picker/emoji-picker.ts +165 -0
- package/dist/emoji-picker/index.ts +25 -0
- package/dist/feedback-dialog/feedback-dialog.styles.ts +15 -0
- package/dist/feedback-dialog/feedback-dialog.ts +147 -0
- package/dist/feedback-dialog/index.ts +11 -0
- package/dist/file-upload/file-upload.styles.ts +30 -0
- package/dist/file-upload/file-upload.ts +260 -0
- package/dist/file-upload/index.ts +19 -0
- package/dist/footer/footer.styles.ts +5 -0
- package/dist/footer/footer.ts +32 -0
- package/dist/footer/index.ts +9 -0
- package/dist/index.ts +1 -1
- package/dist/inline-editor/index.ts +13 -0
- package/dist/inline-editor/inline-editor.styles.ts +42 -0
- package/dist/inline-editor/inline-editor.ts +96 -0
- package/dist/input/index.ts +9 -0
- package/dist/input/input.styles.ts +15 -0
- package/dist/input/input.ts +84 -0
- package/dist/input-group/index.ts +13 -0
- package/dist/input-group/input-group.styles.ts +50 -0
- package/dist/input-group/input-group.ts +44 -0
- package/dist/install-prompt/index.ts +9 -0
- package/dist/install-prompt/install-prompt.styles.ts +5 -0
- package/dist/install-prompt/install-prompt.ts +68 -0
- package/dist/keyboard-shortcut/index.ts +12 -0
- package/dist/keyboard-shortcut/keyboard-shortcut.styles.ts +8 -0
- package/dist/keyboard-shortcut/keyboard-shortcut.ts +153 -0
- package/dist/language-selector/index.ts +8 -0
- package/dist/language-selector/language-selector.styles.ts +28 -0
- package/dist/language-selector/language-selector.ts +149 -0
- package/dist/link-card/index.ts +1 -0
- package/dist/link-card/link-card.ts +9 -0
- package/dist/markdown-renderer/index.ts +8 -0
- package/dist/markdown-renderer/markdown-renderer.styles.ts +31 -0
- package/dist/markdown-renderer/markdown-renderer.ts +251 -0
- package/dist/mobile-nav/index.ts +14 -0
- package/dist/mobile-nav/mobile-nav.styles.ts +39 -0
- package/dist/mobile-nav/mobile-nav.ts +74 -0
- package/dist/navbar/index.ts +9 -0
- package/dist/navbar/navbar.styles.ts +29 -0
- package/dist/navbar/navbar.ts +39 -0
- package/dist/otp-input/index.ts +13 -0
- package/dist/otp-input/otp-input.styles.ts +51 -0
- package/dist/otp-input/otp-input.ts +198 -0
- package/dist/pagination/index.ts +1 -0
- package/dist/payment/index.ts +1 -0
- package/dist/payment/payment.ts +16 -0
- package/dist/popover/index.ts +7 -0
- package/dist/popover/popover.styles.ts +16 -0
- package/dist/popover/popover.ts +89 -0
- package/dist/presence-indicator/index.ts +14 -0
- package/dist/presence-indicator/presence-indicator.styles.ts +29 -0
- package/dist/presence-indicator/presence-indicator.ts +63 -0
- package/dist/progress-display/index.ts +15 -0
- package/dist/progress-display/progress-display.styles.ts +72 -0
- package/dist/progress-display/progress-display.ts +60 -0
- package/dist/radio/index.ts +8 -0
- package/dist/radio/radio.styles.ts +34 -0
- package/dist/radio/radio.ts +70 -0
- package/dist/reaction-bar/index.ts +14 -0
- package/dist/reaction-bar/reaction-bar.styles.ts +23 -0
- package/dist/reaction-bar/reaction-bar.ts +87 -0
- package/dist/resizable-layout/index.ts +12 -0
- package/dist/resizable-layout/resizable-layout.styles.ts +50 -0
- package/dist/resizable-layout/resizable-layout.ts +139 -0
- package/dist/rich-editor/editor.ts +342 -0
- package/dist/rich-editor/emoji.ts +175 -0
- package/dist/rich-editor/history.ts +79 -0
- package/dist/rich-editor/index.ts +106 -0
- package/dist/rich-editor/markdown-shortcuts.ts +248 -0
- package/dist/rich-editor/mentions.ts +162 -0
- package/dist/rich-editor/model.ts +206 -0
- package/dist/rich-editor/operations.ts +772 -0
- package/dist/rich-editor/selection.ts +157 -0
- package/dist/rich-editor/serialization.ts +711 -0
- package/dist/rich-editor/slash-commands.ts +212 -0
- package/dist/search-bar/index.ts +7 -0
- package/dist/search-bar/search-bar.styles.ts +29 -0
- package/dist/search-bar/search-bar.ts +130 -0
- package/dist/select/index.ts +13 -0
- package/dist/select/select.styles.ts +52 -0
- package/dist/select/select.ts +152 -0
- package/dist/sidebar/index.ts +9 -0
- package/dist/sidebar/sidebar.styles.ts +27 -0
- package/dist/sidebar/sidebar.ts +59 -0
- package/dist/skeleton/index.ts +8 -0
- package/dist/skeleton/skeleton.styles.ts +16 -0
- package/dist/skeleton/skeleton.ts +39 -0
- package/dist/skip-to-content/index.ts +1 -0
- package/dist/skip-to-content/skip-to-content.ts +1 -0
- package/dist/slide-viewer/index.ts +16 -0
- package/dist/slide-viewer/slide-viewer.styles.ts +57 -0
- package/dist/slide-viewer/slide-viewer.ts +152 -0
- package/dist/slider/index.ts +1 -0
- package/dist/slider/slider.ts +2 -0
- package/dist/status-indicator/index.ts +15 -0
- package/dist/status-indicator/status-indicator.styles.ts +41 -0
- package/dist/status-indicator/status-indicator.ts +66 -0
- package/dist/steps/index.ts +1 -0
- package/dist/steps/steps.ts +1 -0
- package/dist/switch/index.ts +7 -0
- package/dist/switch/switch.styles.ts +55 -0
- package/dist/switch/switch.ts +63 -0
- package/dist/table-of-contents/index.ts +1 -0
- package/dist/table-of-contents/table-of-contents.ts +37 -0
- package/dist/tabs/index.ts +8 -0
- package/dist/tabs/tabs.styles.ts +9 -0
- package/dist/tabs/tabs.ts +133 -0
- package/dist/textarea/index.ts +7 -0
- package/dist/textarea/textarea.styles.ts +15 -0
- package/dist/textarea/textarea.ts +59 -0
- package/dist/theme/dom-adapters.ts +60 -0
- package/dist/theme/index.ts +18 -0
- package/dist/theme/theme-machine.ts +130 -0
- package/dist/theme/theme-script.ts +18 -0
- package/dist/thread-view/index.ts +26 -0
- package/dist/thread-view/thread-view.styles.ts +35 -0
- package/dist/thread-view/thread-view.ts +176 -0
- package/dist/toast/index.ts +13 -0
- package/dist/toast/toast.styles.ts +19 -0
- package/dist/toast/toast.ts +203 -0
- package/dist/tooltip/index.ts +7 -0
- package/dist/tooltip/tooltip.styles.ts +16 -0
- package/dist/tooltip/tooltip.ts +109 -0
- package/dist/version-selector/index.ts +12 -0
- package/dist/version-selector/version-selector.styles.ts +41 -0
- package/dist/version-selector/version-selector.ts +136 -0
- package/dist/video-player/index.ts +12 -0
- package/dist/video-player/video-player.styles.ts +42 -0
- package/dist/video-player/video-player.ts +100 -0
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { AccessibilityProps, KeyboardHandlerMap } from '../shared/index.ts'
|
|
2
|
+
import { Keys, generateId } from '../shared/index.ts'
|
|
3
|
+
|
|
4
|
+
export interface TabsProps {
|
|
5
|
+
/** The active tab value (controlled) */
|
|
6
|
+
value?: string
|
|
7
|
+
/** The default active tab value (uncontrolled) */
|
|
8
|
+
defaultValue?: string
|
|
9
|
+
/** Callback when active tab changes */
|
|
10
|
+
onValueChange?: (value: string) => void
|
|
11
|
+
/** Orientation of the tab list */
|
|
12
|
+
orientation?: 'horizontal' | 'vertical'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TabsState {
|
|
16
|
+
activeValue: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TabsAPI {
|
|
20
|
+
/** Current tabs state */
|
|
21
|
+
state: TabsState
|
|
22
|
+
/** ARIA attributes for the tab list */
|
|
23
|
+
listProps: Partial<AccessibilityProps> & Record<string, unknown>
|
|
24
|
+
/** Factory for individual tab trigger props */
|
|
25
|
+
getTabProps(value: string): Partial<AccessibilityProps> & Record<string, unknown>
|
|
26
|
+
/** Factory for individual tab panel props */
|
|
27
|
+
getPanelProps(value: string): Partial<AccessibilityProps> & Record<string, unknown>
|
|
28
|
+
/** Select a tab by value */
|
|
29
|
+
select(value: string): void
|
|
30
|
+
/** Keyboard handlers for the tab list */
|
|
31
|
+
keyboardHandlers: KeyboardHandlerMap
|
|
32
|
+
/** Generated ID prefix for aria linking */
|
|
33
|
+
idPrefix: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createTabs(props: TabsProps = {}): TabsAPI {
|
|
37
|
+
const {
|
|
38
|
+
value: controlledValue,
|
|
39
|
+
defaultValue = '',
|
|
40
|
+
onValueChange,
|
|
41
|
+
orientation = 'horizontal',
|
|
42
|
+
} = props
|
|
43
|
+
|
|
44
|
+
let uncontrolledValue = controlledValue ?? defaultValue
|
|
45
|
+
const idPrefix = generateId('rfr-tabs')
|
|
46
|
+
|
|
47
|
+
function getActiveValue(): string {
|
|
48
|
+
if (controlledValue !== undefined) return controlledValue
|
|
49
|
+
return uncontrolledValue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function select(value: string): void {
|
|
53
|
+
uncontrolledValue = value
|
|
54
|
+
onValueChange?.(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getTabId(value: string): string {
|
|
58
|
+
return `${idPrefix}-tab-${value}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getPanelId(value: string): string {
|
|
62
|
+
return `${idPrefix}-panel-${value}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getTabProps(value: string): Partial<AccessibilityProps> & Record<string, unknown> {
|
|
66
|
+
const isSelected = getActiveValue() === value
|
|
67
|
+
return {
|
|
68
|
+
role: 'tab',
|
|
69
|
+
'aria-selected': isSelected,
|
|
70
|
+
'aria-controls': getPanelId(value),
|
|
71
|
+
tabIndex: isSelected ? 0 : -1,
|
|
72
|
+
id: getTabId(value),
|
|
73
|
+
'data-state': isSelected ? 'active' : 'inactive',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getPanelProps(value: string): Partial<AccessibilityProps> & Record<string, unknown> {
|
|
78
|
+
const isSelected = getActiveValue() === value
|
|
79
|
+
return {
|
|
80
|
+
role: 'tabpanel',
|
|
81
|
+
'aria-labelledby': getTabId(value),
|
|
82
|
+
id: getPanelId(value),
|
|
83
|
+
hidden: !isSelected || undefined,
|
|
84
|
+
tabIndex: 0,
|
|
85
|
+
'data-state': isSelected ? 'active' : 'inactive',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const listProps: Partial<AccessibilityProps> & Record<string, unknown> = {
|
|
90
|
+
role: 'tablist',
|
|
91
|
+
'aria-orientation': orientation,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const keyboardHandlers: KeyboardHandlerMap = orientation === 'horizontal'
|
|
95
|
+
? {
|
|
96
|
+
[Keys.ArrowLeft]: (e) => {
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
},
|
|
99
|
+
[Keys.ArrowRight]: (e) => {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
},
|
|
102
|
+
[Keys.Home]: (e) => {
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
},
|
|
105
|
+
[Keys.End]: (e) => {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
[Keys.ArrowUp]: (e) => {
|
|
111
|
+
e.preventDefault()
|
|
112
|
+
},
|
|
113
|
+
[Keys.ArrowDown]: (e) => {
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
},
|
|
116
|
+
[Keys.Home]: (e) => {
|
|
117
|
+
e.preventDefault()
|
|
118
|
+
},
|
|
119
|
+
[Keys.End]: (e) => {
|
|
120
|
+
e.preventDefault()
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
state: { activeValue: getActiveValue() },
|
|
126
|
+
listProps,
|
|
127
|
+
getTabProps,
|
|
128
|
+
getPanelProps,
|
|
129
|
+
select,
|
|
130
|
+
keyboardHandlers,
|
|
131
|
+
idPrefix,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const textareaVariants = cva({
|
|
4
|
+
base: 'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
|
5
|
+
variants: {
|
|
6
|
+
size: {
|
|
7
|
+
sm: 'min-h-[40px] text-xs',
|
|
8
|
+
default: 'min-h-[60px]',
|
|
9
|
+
lg: 'min-h-[80px] text-base',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
size: 'default',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export interface TextareaProps {
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
readOnly?: boolean
|
|
6
|
+
required?: boolean
|
|
7
|
+
'aria-invalid'?: boolean
|
|
8
|
+
rows?: number
|
|
9
|
+
maxRows?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TextareaAPI {
|
|
13
|
+
/** ARIA attributes to spread on the element */
|
|
14
|
+
ariaProps: Partial<AccessibilityProps> & { 'aria-invalid'?: boolean; 'aria-required'?: boolean }
|
|
15
|
+
/** Data attributes for CSS styling hooks */
|
|
16
|
+
dataAttributes: Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createTextarea(props: TextareaProps = {}): TextareaAPI {
|
|
20
|
+
const {
|
|
21
|
+
disabled = false,
|
|
22
|
+
readOnly = false,
|
|
23
|
+
required = false,
|
|
24
|
+
'aria-invalid': ariaInvalid,
|
|
25
|
+
} = props
|
|
26
|
+
|
|
27
|
+
const ariaProps: TextareaAPI['ariaProps'] = {}
|
|
28
|
+
|
|
29
|
+
if (disabled) {
|
|
30
|
+
ariaProps['aria-disabled'] = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (ariaInvalid) {
|
|
34
|
+
ariaProps['aria-invalid'] = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (required) {
|
|
38
|
+
ariaProps['aria-required'] = true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const dataAttributes: Record<string, string> = {}
|
|
42
|
+
|
|
43
|
+
if (disabled) {
|
|
44
|
+
dataAttributes['data-disabled'] = ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (readOnly) {
|
|
48
|
+
dataAttributes['data-readonly'] = ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ariaInvalid) {
|
|
52
|
+
dataAttributes['data-invalid'] = ''
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ariaProps,
|
|
57
|
+
dataAttributes,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser DOM adapters for the theme machine.
|
|
3
|
+
* These bridge the headless core to browser APIs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { StorageAdapter, MediaQueryAdapter, ResolvedTheme } from './theme-machine.js'
|
|
7
|
+
|
|
8
|
+
/** localStorage adapter */
|
|
9
|
+
export function createLocalStorageAdapter(): StorageAdapter {
|
|
10
|
+
return {
|
|
11
|
+
get(key) {
|
|
12
|
+
try {
|
|
13
|
+
return localStorage.getItem(key)
|
|
14
|
+
} catch {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
set(key, value) {
|
|
19
|
+
try {
|
|
20
|
+
localStorage.setItem(key, value)
|
|
21
|
+
} catch {
|
|
22
|
+
// localStorage unavailable (SSR, incognito quota exceeded, etc.)
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** matchMedia adapter */
|
|
29
|
+
export function createMediaQueryAdapter(): MediaQueryAdapter {
|
|
30
|
+
return {
|
|
31
|
+
matches(query) {
|
|
32
|
+
if (typeof window === 'undefined') return false
|
|
33
|
+
return window.matchMedia(query).matches
|
|
34
|
+
},
|
|
35
|
+
subscribe(query, callback) {
|
|
36
|
+
if (typeof window === 'undefined') return () => {}
|
|
37
|
+
const mql = window.matchMedia(query)
|
|
38
|
+
const handler = (e: MediaQueryListEvent) => callback(e.matches)
|
|
39
|
+
mql.addEventListener('change', handler)
|
|
40
|
+
return () => mql.removeEventListener('change', handler)
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Apply resolved theme to the document */
|
|
46
|
+
export function applyThemeToDOM(
|
|
47
|
+
resolved: ResolvedTheme,
|
|
48
|
+
attribute: 'class' | 'data-theme' = 'class',
|
|
49
|
+
): void {
|
|
50
|
+
if (typeof document === 'undefined') return
|
|
51
|
+
|
|
52
|
+
const root = document.documentElement
|
|
53
|
+
if (attribute === 'class') {
|
|
54
|
+
root.classList.remove('light', 'dark')
|
|
55
|
+
root.classList.add(resolved)
|
|
56
|
+
} else {
|
|
57
|
+
root.setAttribute(attribute, resolved)
|
|
58
|
+
}
|
|
59
|
+
root.style.colorScheme = resolved
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createTheme,
|
|
3
|
+
type ThemeMode,
|
|
4
|
+
type ResolvedTheme,
|
|
5
|
+
type ThemeState,
|
|
6
|
+
type ThemeConfig,
|
|
7
|
+
type ThemeAPI,
|
|
8
|
+
type StorageAdapter,
|
|
9
|
+
type MediaQueryAdapter,
|
|
10
|
+
} from './theme-machine.js'
|
|
11
|
+
|
|
12
|
+
export { getThemeScript } from './theme-script.js'
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
createLocalStorageAdapter,
|
|
16
|
+
createMediaQueryAdapter,
|
|
17
|
+
applyThemeToDOM,
|
|
18
|
+
} from './dom-adapters.js'
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless theme state machine — pure TypeScript, zero DOM dependencies.
|
|
3
|
+
* Manages light/dark/system mode with system preference tracking.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ThemeMode = 'light' | 'dark' | 'system'
|
|
7
|
+
export type ResolvedTheme = 'light' | 'dark'
|
|
8
|
+
|
|
9
|
+
export interface ThemeState {
|
|
10
|
+
/** User's chosen mode */
|
|
11
|
+
mode: ThemeMode
|
|
12
|
+
/** Resolved theme after system preference detection */
|
|
13
|
+
resolved: ResolvedTheme
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ThemeConfig {
|
|
17
|
+
/** Initial mode. Default: 'system' */
|
|
18
|
+
defaultMode?: ThemeMode
|
|
19
|
+
/** localStorage key. Default: 'rfr-theme' */
|
|
20
|
+
storageKey?: string
|
|
21
|
+
/** HTML attribute to set. Default: 'class' */
|
|
22
|
+
attribute?: 'class' | 'data-theme'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StorageAdapter {
|
|
26
|
+
get(key: string): string | null
|
|
27
|
+
set(key: string, value: string): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MediaQueryAdapter {
|
|
31
|
+
matches(query: string): boolean
|
|
32
|
+
subscribe(query: string, callback: (matches: boolean) => void): () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ThemeAPI {
|
|
36
|
+
/** Get current state */
|
|
37
|
+
getState(): ThemeState
|
|
38
|
+
/** Set mode (light/dark/system) */
|
|
39
|
+
setMode(mode: ThemeMode): void
|
|
40
|
+
/** Subscribe to state changes */
|
|
41
|
+
subscribe(fn: (state: ThemeState) => void): () => void
|
|
42
|
+
/** Clean up subscriptions */
|
|
43
|
+
destroy(): void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveTheme(mode: ThemeMode, systemPrefersDark: boolean): ResolvedTheme {
|
|
47
|
+
if (mode === 'system') {
|
|
48
|
+
return systemPrefersDark ? 'dark' : 'light'
|
|
49
|
+
}
|
|
50
|
+
return mode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createTheme(
|
|
54
|
+
config: ThemeConfig = {},
|
|
55
|
+
storage?: StorageAdapter,
|
|
56
|
+
mediaQuery?: MediaQueryAdapter,
|
|
57
|
+
): ThemeAPI {
|
|
58
|
+
const {
|
|
59
|
+
defaultMode = 'system',
|
|
60
|
+
storageKey = 'rfr-theme',
|
|
61
|
+
} = config
|
|
62
|
+
|
|
63
|
+
const listeners = new Set<(state: ThemeState) => void>()
|
|
64
|
+
let cleanupMediaQuery: (() => void) | null = null
|
|
65
|
+
|
|
66
|
+
// Read persisted mode or use default
|
|
67
|
+
const persisted = storage?.get(storageKey) as ThemeMode | null
|
|
68
|
+
let mode: ThemeMode = persisted && ['light', 'dark', 'system'].includes(persisted)
|
|
69
|
+
? persisted
|
|
70
|
+
: defaultMode
|
|
71
|
+
|
|
72
|
+
// Detect system preference
|
|
73
|
+
let systemPrefersDark = mediaQuery?.matches('(prefers-color-scheme: dark)') ?? false
|
|
74
|
+
|
|
75
|
+
let state: ThemeState = {
|
|
76
|
+
mode,
|
|
77
|
+
resolved: resolveTheme(mode, systemPrefersDark),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function notify() {
|
|
81
|
+
for (const fn of listeners) {
|
|
82
|
+
fn(state)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function updateState(newMode: ThemeMode) {
|
|
87
|
+
mode = newMode
|
|
88
|
+
state = { mode, resolved: resolveTheme(mode, systemPrefersDark) }
|
|
89
|
+
storage?.set(storageKey, mode)
|
|
90
|
+
notify()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Listen for system preference changes
|
|
94
|
+
if (mediaQuery) {
|
|
95
|
+
cleanupMediaQuery = mediaQuery.subscribe(
|
|
96
|
+
'(prefers-color-scheme: dark)',
|
|
97
|
+
(matches) => {
|
|
98
|
+
systemPrefersDark = matches
|
|
99
|
+
if (mode === 'system') {
|
|
100
|
+
state = { mode, resolved: resolveTheme(mode, systemPrefersDark) }
|
|
101
|
+
notify()
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
getState() {
|
|
109
|
+
return state
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
setMode(newMode: ThemeMode) {
|
|
113
|
+
if (newMode !== mode) {
|
|
114
|
+
updateState(newMode)
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
subscribe(fn: (state: ThemeState) => void) {
|
|
119
|
+
listeners.add(fn)
|
|
120
|
+
return () => {
|
|
121
|
+
listeners.delete(fn)
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
destroy() {
|
|
126
|
+
listeners.clear()
|
|
127
|
+
cleanupMediaQuery?.()
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline script for preventing theme flash on page load.
|
|
3
|
+
* Inject this as a <script> tag in the <head> before any CSS.
|
|
4
|
+
* Works with any framework (React, Angular, Astro, plain HTML).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function getThemeScript(
|
|
8
|
+
storageKey = 'rfr-theme',
|
|
9
|
+
attribute: 'class' | 'data-theme' = 'class',
|
|
10
|
+
): string {
|
|
11
|
+
// This string is injected as innerHTML of a <script> tag.
|
|
12
|
+
// It runs before any CSS/JS loads, preventing flash of wrong theme.
|
|
13
|
+
return `(function(){try{var m=localStorage.getItem('${storageKey}');var s=window.matchMedia('(prefers-color-scheme:dark)').matches;var t=m==='dark'||(m!=='light'&&s)?'dark':'light';var d=document.documentElement;${
|
|
14
|
+
attribute === 'class'
|
|
15
|
+
? "d.classList.remove('light','dark');d.classList.add(t);"
|
|
16
|
+
: `d.setAttribute('${attribute}',t);`
|
|
17
|
+
}d.style.colorScheme=t;}catch(e){}})()`
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createThreadView,
|
|
3
|
+
formatTimestamp,
|
|
4
|
+
formatRelativeTime,
|
|
5
|
+
type ThreadViewProps,
|
|
6
|
+
type ThreadViewState,
|
|
7
|
+
type ThreadViewAPI,
|
|
8
|
+
type MessageData,
|
|
9
|
+
type MessageReaction,
|
|
10
|
+
type MessageAttachment,
|
|
11
|
+
} from './thread-view.js'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
threadContainerStyles,
|
|
15
|
+
threadMessageStyles,
|
|
16
|
+
threadAvatarStyles,
|
|
17
|
+
threadContentStyles,
|
|
18
|
+
threadAuthorStyles,
|
|
19
|
+
threadTimestampStyles,
|
|
20
|
+
threadBodyStyles,
|
|
21
|
+
threadReactionsStyles,
|
|
22
|
+
threadReplyIndicatorStyles,
|
|
23
|
+
threadActionsStyles,
|
|
24
|
+
threadAttachmentStyles,
|
|
25
|
+
threadEditedStyles,
|
|
26
|
+
} from './thread-view.styles.js'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const threadContainerStyles =
|
|
2
|
+
'flex flex-col gap-1'
|
|
3
|
+
|
|
4
|
+
export const threadMessageStyles =
|
|
5
|
+
'flex gap-3 px-4 py-2 hover:bg-accent/50 rounded-md transition-colors group'
|
|
6
|
+
|
|
7
|
+
export const threadAvatarStyles =
|
|
8
|
+
'h-9 w-9 rounded-full bg-muted flex items-center justify-center text-sm font-medium overflow-hidden flex-shrink-0'
|
|
9
|
+
|
|
10
|
+
export const threadContentStyles =
|
|
11
|
+
'flex-1 min-w-0'
|
|
12
|
+
|
|
13
|
+
export const threadAuthorStyles =
|
|
14
|
+
'font-semibold text-sm'
|
|
15
|
+
|
|
16
|
+
export const threadTimestampStyles =
|
|
17
|
+
'text-xs text-muted-foreground ml-2'
|
|
18
|
+
|
|
19
|
+
export const threadBodyStyles =
|
|
20
|
+
'text-sm mt-0.5 whitespace-pre-wrap break-words'
|
|
21
|
+
|
|
22
|
+
export const threadReactionsStyles =
|
|
23
|
+
'flex flex-wrap gap-1 mt-1'
|
|
24
|
+
|
|
25
|
+
export const threadReplyIndicatorStyles =
|
|
26
|
+
'flex items-center gap-1 mt-1 text-xs text-primary cursor-pointer hover:underline'
|
|
27
|
+
|
|
28
|
+
export const threadActionsStyles =
|
|
29
|
+
'flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity'
|
|
30
|
+
|
|
31
|
+
export const threadAttachmentStyles =
|
|
32
|
+
'flex items-center gap-2 mt-1 p-2 rounded border text-xs bg-muted/50'
|
|
33
|
+
|
|
34
|
+
export const threadEditedStyles =
|
|
35
|
+
'text-xs text-muted-foreground ml-1'
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
import { generateId } from '../shared/index.ts'
|
|
3
|
+
|
|
4
|
+
export interface MessageReaction {
|
|
5
|
+
emoji: string
|
|
6
|
+
count: number
|
|
7
|
+
userReacted: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MessageAttachment {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
url: string
|
|
14
|
+
type: string
|
|
15
|
+
size?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MessageData {
|
|
19
|
+
/** Unique message ID */
|
|
20
|
+
id: string
|
|
21
|
+
/** Author display info */
|
|
22
|
+
author: {
|
|
23
|
+
id: string
|
|
24
|
+
name: string
|
|
25
|
+
avatarUrl?: string
|
|
26
|
+
}
|
|
27
|
+
/** Message content (can contain markdown) */
|
|
28
|
+
content: string
|
|
29
|
+
/** Message timestamp */
|
|
30
|
+
timestamp: Date
|
|
31
|
+
/** Reactions on this message */
|
|
32
|
+
reactions?: MessageReaction[]
|
|
33
|
+
/** Threaded replies */
|
|
34
|
+
replies?: MessageData[]
|
|
35
|
+
/** File attachments */
|
|
36
|
+
attachments?: MessageAttachment[]
|
|
37
|
+
/** Whether this message has been edited */
|
|
38
|
+
edited?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ThreadViewProps {
|
|
42
|
+
/** Messages to display */
|
|
43
|
+
messages: MessageData[]
|
|
44
|
+
/** Callback when a reply is sent */
|
|
45
|
+
onReply?: (messageId: string, content: string) => void
|
|
46
|
+
/** Callback when a reaction is toggled */
|
|
47
|
+
onReact?: (messageId: string, emoji: string) => void
|
|
48
|
+
/** Current user ID (for highlighting own messages) */
|
|
49
|
+
currentUserId?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ThreadViewState {
|
|
53
|
+
messages: MessageData[]
|
|
54
|
+
replyingTo: string | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ThreadViewAPI {
|
|
58
|
+
/** Current state */
|
|
59
|
+
state: ThreadViewState
|
|
60
|
+
/** Start replying to a message */
|
|
61
|
+
startReply(messageId: string): void
|
|
62
|
+
/** Cancel reply */
|
|
63
|
+
cancelReply(): void
|
|
64
|
+
/** Send a reply */
|
|
65
|
+
reply(messageId: string, content: string): void
|
|
66
|
+
/** Toggle a reaction on a message */
|
|
67
|
+
react(messageId: string, emoji: string): void
|
|
68
|
+
/** Format a timestamp for display */
|
|
69
|
+
formatTimestamp(date: Date): string
|
|
70
|
+
/** Format relative time (e.g., "2 minutes ago") */
|
|
71
|
+
formatRelativeTime(date: Date): string
|
|
72
|
+
/** ARIA props for the thread container */
|
|
73
|
+
ariaProps: Partial<AccessibilityProps> & Record<string, unknown>
|
|
74
|
+
/** Get ARIA props for a message */
|
|
75
|
+
getMessageAriaProps(message: MessageData): Record<string, unknown>
|
|
76
|
+
/** Get ARIA props for a reply button */
|
|
77
|
+
getReplyButtonAriaProps(messageId: string): Record<string, unknown>
|
|
78
|
+
/** Generated IDs */
|
|
79
|
+
ids: {
|
|
80
|
+
thread: string
|
|
81
|
+
label: string
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatTimestamp(date: Date): string {
|
|
86
|
+
const hours = date.getHours()
|
|
87
|
+
const minutes = date.getMinutes()
|
|
88
|
+
const ampm = hours >= 12 ? 'PM' : 'AM'
|
|
89
|
+
const displayHours = hours % 12 || 12
|
|
90
|
+
const displayMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`
|
|
91
|
+
return `${displayHours}:${displayMinutes} ${ampm}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatRelativeTime(date: Date, now?: Date): string {
|
|
95
|
+
const reference = now ?? new Date()
|
|
96
|
+
const diffMs = reference.getTime() - date.getTime()
|
|
97
|
+
const diffSeconds = Math.floor(diffMs / 1000)
|
|
98
|
+
const diffMinutes = Math.floor(diffSeconds / 60)
|
|
99
|
+
const diffHours = Math.floor(diffMinutes / 60)
|
|
100
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
101
|
+
|
|
102
|
+
if (diffSeconds < 60) return 'just now'
|
|
103
|
+
if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`
|
|
104
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`
|
|
105
|
+
if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`
|
|
106
|
+
|
|
107
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createThreadView(props: ThreadViewProps): ThreadViewAPI {
|
|
111
|
+
const { messages, onReply, onReact, currentUserId } = props
|
|
112
|
+
|
|
113
|
+
let replyingTo: string | null = null
|
|
114
|
+
|
|
115
|
+
const threadId = generateId('rfr-thread')
|
|
116
|
+
const labelId = generateId('rfr-thread-label')
|
|
117
|
+
|
|
118
|
+
function startReply(messageId: string): void {
|
|
119
|
+
replyingTo = messageId
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cancelReply(): void {
|
|
123
|
+
replyingTo = null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function reply(messageId: string, content: string): void {
|
|
127
|
+
onReply?.(messageId, content)
|
|
128
|
+
replyingTo = null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function react(messageId: string, emoji: string): void {
|
|
132
|
+
onReact?.(messageId, emoji)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {
|
|
136
|
+
role: 'log',
|
|
137
|
+
'aria-label': 'Message thread',
|
|
138
|
+
'aria-live': 'polite',
|
|
139
|
+
id: threadId,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getMessageAriaProps(message: MessageData): Record<string, unknown> {
|
|
143
|
+
const isOwn = currentUserId && message.author.id === currentUserId
|
|
144
|
+
return {
|
|
145
|
+
role: 'article',
|
|
146
|
+
'aria-label': `Message from ${message.author.name}${isOwn ? ' (you)' : ''} at ${formatTimestamp(message.timestamp)}`,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getReplyButtonAriaProps(_messageId: string): Record<string, unknown> {
|
|
151
|
+
return {
|
|
152
|
+
role: 'button',
|
|
153
|
+
'aria-label': `Reply to message`,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
state: {
|
|
159
|
+
messages,
|
|
160
|
+
get replyingTo() { return replyingTo },
|
|
161
|
+
},
|
|
162
|
+
startReply,
|
|
163
|
+
cancelReply,
|
|
164
|
+
reply,
|
|
165
|
+
react,
|
|
166
|
+
formatTimestamp,
|
|
167
|
+
formatRelativeTime,
|
|
168
|
+
ariaProps,
|
|
169
|
+
getMessageAriaProps,
|
|
170
|
+
getReplyButtonAriaProps,
|
|
171
|
+
ids: {
|
|
172
|
+
thread: threadId,
|
|
173
|
+
label: labelId,
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createToast,
|
|
3
|
+
createToastManager,
|
|
4
|
+
getToastIcon,
|
|
5
|
+
type ToastVariant,
|
|
6
|
+
type ToastProps,
|
|
7
|
+
type ToastAPI,
|
|
8
|
+
type ToastState,
|
|
9
|
+
type ToastEntry,
|
|
10
|
+
type ToastManagerAPI,
|
|
11
|
+
} from './toast.js'
|
|
12
|
+
|
|
13
|
+
export { toastVariants } from './toast.styles.js'
|