@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,96 @@
|
|
|
1
|
+
export interface InlineEditorProps {
|
|
2
|
+
/** Current value */
|
|
3
|
+
value: string
|
|
4
|
+
/** Callback when saving */
|
|
5
|
+
onSave?: (value: string) => void
|
|
6
|
+
/** Callback when cancelling */
|
|
7
|
+
onCancel?: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToolbarAction {
|
|
11
|
+
name: string
|
|
12
|
+
syntax: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface InlineEditorState {
|
|
16
|
+
isEditing: boolean
|
|
17
|
+
value: string
|
|
18
|
+
preview: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface InlineEditorAPI {
|
|
22
|
+
/** Current editor state */
|
|
23
|
+
state: InlineEditorState
|
|
24
|
+
/** Enter editing mode */
|
|
25
|
+
startEditing(): void
|
|
26
|
+
/** Cancel editing, revert to original value */
|
|
27
|
+
cancel(): void
|
|
28
|
+
/** Save the current value */
|
|
29
|
+
save(): void
|
|
30
|
+
/** Update the editing value */
|
|
31
|
+
updateValue(value: string): void
|
|
32
|
+
/** Insert syntax at cursor position */
|
|
33
|
+
insertAtCursor(syntax: string): void
|
|
34
|
+
/** Available toolbar actions */
|
|
35
|
+
toolbarActions: ToolbarAction[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createInlineEditor(props: InlineEditorProps): InlineEditorAPI {
|
|
39
|
+
const { value: initialValue, onSave, onCancel } = props
|
|
40
|
+
|
|
41
|
+
let isEditing = false
|
|
42
|
+
let currentValue = initialValue
|
|
43
|
+
|
|
44
|
+
const toolbarActions: ToolbarAction[] = [
|
|
45
|
+
{ name: 'bold', syntax: '**' },
|
|
46
|
+
{ name: 'heading', syntax: '# ' },
|
|
47
|
+
{ name: 'list', syntax: '- ' },
|
|
48
|
+
{ name: 'link', syntax: '[text](url)' },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
function startEditing(): void {
|
|
52
|
+
isEditing = true
|
|
53
|
+
currentValue = initialValue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cancel(): void {
|
|
57
|
+
isEditing = false
|
|
58
|
+
currentValue = initialValue
|
|
59
|
+
onCancel?.()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function save(): void {
|
|
63
|
+
isEditing = false
|
|
64
|
+
onSave?.(currentValue)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function updateValue(value: string): void {
|
|
68
|
+
currentValue = value
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function insertAtCursor(syntax: string): void {
|
|
72
|
+
currentValue = currentValue + syntax
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
get state(): InlineEditorState {
|
|
77
|
+
return {
|
|
78
|
+
get isEditing() {
|
|
79
|
+
return isEditing
|
|
80
|
+
},
|
|
81
|
+
get value() {
|
|
82
|
+
return currentValue
|
|
83
|
+
},
|
|
84
|
+
get preview() {
|
|
85
|
+
return currentValue
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
startEditing,
|
|
90
|
+
cancel,
|
|
91
|
+
save,
|
|
92
|
+
updateValue,
|
|
93
|
+
insertAtCursor,
|
|
94
|
+
toolbarActions,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const inputVariants = cva({
|
|
4
|
+
base: 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium 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: 'h-8 text-xs',
|
|
8
|
+
default: 'h-9',
|
|
9
|
+
lg: 'h-10 text-base',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
size: 'default',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search'
|
|
4
|
+
|
|
5
|
+
export interface InputProps {
|
|
6
|
+
type?: InputType
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
readOnly?: boolean
|
|
9
|
+
required?: boolean
|
|
10
|
+
placeholder?: string
|
|
11
|
+
'aria-invalid'?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface InputAPI {
|
|
15
|
+
/** ARIA attributes to spread on the element */
|
|
16
|
+
ariaProps: Partial<AccessibilityProps> & { 'aria-invalid'?: boolean; 'aria-required'?: boolean }
|
|
17
|
+
/** Data attributes for CSS styling hooks */
|
|
18
|
+
dataAttributes: Record<string, string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createInput(props: InputProps = {}): InputAPI {
|
|
22
|
+
const {
|
|
23
|
+
disabled = false,
|
|
24
|
+
readOnly = false,
|
|
25
|
+
required = false,
|
|
26
|
+
'aria-invalid': ariaInvalid,
|
|
27
|
+
} = props
|
|
28
|
+
|
|
29
|
+
const ariaProps: InputAPI['ariaProps'] = {}
|
|
30
|
+
|
|
31
|
+
if (disabled) {
|
|
32
|
+
ariaProps['aria-disabled'] = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (ariaInvalid) {
|
|
36
|
+
ariaProps['aria-invalid'] = true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (required) {
|
|
40
|
+
ariaProps['aria-required'] = true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dataAttributes: Record<string, string> = {}
|
|
44
|
+
|
|
45
|
+
if (disabled) {
|
|
46
|
+
dataAttributes['data-disabled'] = ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (readOnly) {
|
|
50
|
+
dataAttributes['data-readonly'] = ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (ariaInvalid) {
|
|
54
|
+
dataAttributes['data-invalid'] = ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ariaProps,
|
|
59
|
+
dataAttributes,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getInputAriaProps(state: {
|
|
64
|
+
disabled?: boolean
|
|
65
|
+
readOnly?: boolean
|
|
66
|
+
required?: boolean
|
|
67
|
+
invalid?: boolean
|
|
68
|
+
}): Partial<AccessibilityProps> & { 'aria-invalid'?: boolean; 'aria-required'?: boolean } {
|
|
69
|
+
const ariaProps: Partial<AccessibilityProps> & { 'aria-invalid'?: boolean; 'aria-required'?: boolean } = {}
|
|
70
|
+
|
|
71
|
+
if (state.disabled) {
|
|
72
|
+
ariaProps['aria-disabled'] = true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (state.invalid) {
|
|
76
|
+
ariaProps['aria-invalid'] = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (state.required) {
|
|
80
|
+
ariaProps['aria-required'] = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return ariaProps
|
|
84
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createInputGroup,
|
|
3
|
+
type InputGroupProps,
|
|
4
|
+
type InputGroupAPI,
|
|
5
|
+
type InputGroupOrientation,
|
|
6
|
+
} from './input-group.js'
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
inputGroupVariants,
|
|
10
|
+
inputGroupAddonVariants,
|
|
11
|
+
inputGroupButtonVariants,
|
|
12
|
+
inputGroupTokens,
|
|
13
|
+
} from './input-group.styles.js'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
import type { TokenContract } from '../shared/index.ts'
|
|
3
|
+
|
|
4
|
+
export const inputGroupTokens: TokenContract = {
|
|
5
|
+
name: 'input-group',
|
|
6
|
+
tokens: {
|
|
7
|
+
bg: { variable: '--rfr-input-group-bg', fallback: 'hsl(var(--background))' },
|
|
8
|
+
border: { variable: '--rfr-input-group-border', fallback: 'hsl(var(--border))' },
|
|
9
|
+
radius: { variable: '--rfr-input-group-radius', fallback: 'var(--radius)' },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const inputGroupVariants = cva({
|
|
14
|
+
base: 'flex items-stretch [&>*:not(:first-child):not(:last-child)]:rounded-none',
|
|
15
|
+
variants: {
|
|
16
|
+
orientation: {
|
|
17
|
+
horizontal: 'flex-row [&>*:first-child]:rounded-r-none [&>*:last-child]:rounded-l-none',
|
|
18
|
+
vertical: 'flex-col [&>*:first-child]:rounded-b-none [&>*:last-child]:rounded-t-none',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
orientation: 'horizontal',
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const inputGroupAddonVariants = cva({
|
|
27
|
+
base: 'flex items-center justify-center border bg-muted px-3 text-sm text-muted-foreground',
|
|
28
|
+
variants: {
|
|
29
|
+
orientation: {
|
|
30
|
+
horizontal: 'border-y border-x first:border-r-0 last:border-l-0',
|
|
31
|
+
vertical: 'border-x border-y first:border-b-0 last:border-t-0',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
orientation: 'horizontal',
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export const inputGroupButtonVariants = cva({
|
|
40
|
+
base: 'relative inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
41
|
+
variants: {
|
|
42
|
+
orientation: {
|
|
43
|
+
horizontal: '',
|
|
44
|
+
vertical: '',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
defaultVariants: {
|
|
48
|
+
orientation: 'horizontal',
|
|
49
|
+
},
|
|
50
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
import { generateId } from '../shared/index.ts'
|
|
3
|
+
|
|
4
|
+
export type InputGroupOrientation = 'horizontal' | 'vertical'
|
|
5
|
+
|
|
6
|
+
export interface InputGroupProps {
|
|
7
|
+
orientation?: InputGroupOrientation
|
|
8
|
+
id?: string
|
|
9
|
+
'aria-label'?: string
|
|
10
|
+
'aria-labelledby'?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface InputGroupAPI {
|
|
14
|
+
/** ARIA attributes to spread on the group container */
|
|
15
|
+
ariaProps: Partial<AccessibilityProps>
|
|
16
|
+
/** Data attributes for CSS styling hooks */
|
|
17
|
+
dataAttributes: Record<string, string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createInputGroup(props: InputGroupProps = {}): InputGroupAPI {
|
|
21
|
+
const { orientation = 'horizontal' } = props
|
|
22
|
+
const id = props.id ?? generateId('rfr-input-group')
|
|
23
|
+
|
|
24
|
+
const ariaProps: Partial<AccessibilityProps> = {
|
|
25
|
+
role: 'group',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (props['aria-label']) {
|
|
29
|
+
ariaProps['aria-label'] = props['aria-label']
|
|
30
|
+
}
|
|
31
|
+
if (props['aria-labelledby']) {
|
|
32
|
+
ariaProps['aria-labelledby'] = props['aria-labelledby']
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dataAttributes: Record<string, string> = {
|
|
36
|
+
'data-orientation': orientation,
|
|
37
|
+
id,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
ariaProps,
|
|
42
|
+
dataAttributes,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface StorageAdapter {
|
|
2
|
+
get(key: string): string | null
|
|
3
|
+
set(key: string, value: string): void
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface InstallPromptProps {
|
|
7
|
+
/** Delay in ms before showing prompt. Default: 3000 */
|
|
8
|
+
delay?: number
|
|
9
|
+
/** Key used to persist dismissed state. Default: 'rfr-install-dismissed' */
|
|
10
|
+
storageKey?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface InstallPromptState {
|
|
14
|
+
canShow: boolean
|
|
15
|
+
isDismissed: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InstallPromptAPI {
|
|
19
|
+
state: InstallPromptState
|
|
20
|
+
/** Mark the prompt as showable (call after delay + event fire) */
|
|
21
|
+
show(): void
|
|
22
|
+
/** Dismiss and persist */
|
|
23
|
+
dismiss(): void
|
|
24
|
+
/** Trigger the browser install prompt */
|
|
25
|
+
install(promptEvent?: { prompt(): void }): void
|
|
26
|
+
/** ARIA attributes for the banner */
|
|
27
|
+
ariaProps: Record<string, string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createInstallPrompt(
|
|
31
|
+
props: InstallPromptProps = {},
|
|
32
|
+
storage?: StorageAdapter,
|
|
33
|
+
): InstallPromptAPI {
|
|
34
|
+
const { storageKey = 'rfr-install-dismissed' } = props
|
|
35
|
+
|
|
36
|
+
const dismissed = storage?.get(storageKey) === 'true'
|
|
37
|
+
|
|
38
|
+
const state: InstallPromptState = {
|
|
39
|
+
canShow: false,
|
|
40
|
+
isDismissed: dismissed,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
state,
|
|
45
|
+
|
|
46
|
+
show() {
|
|
47
|
+
if (!state.isDismissed) {
|
|
48
|
+
state.canShow = true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
dismiss() {
|
|
53
|
+
state.canShow = false
|
|
54
|
+
state.isDismissed = true
|
|
55
|
+
storage?.set(storageKey, 'true')
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
install(promptEvent?: { prompt(): void }) {
|
|
59
|
+
promptEvent?.prompt()
|
|
60
|
+
state.canShow = false
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
ariaProps: {
|
|
64
|
+
role: 'banner',
|
|
65
|
+
'aria-label': 'Install application',
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createKeyboardShortcut,
|
|
3
|
+
formatShortcut,
|
|
4
|
+
type ShortcutProps,
|
|
5
|
+
type KeyboardShortcutAPI,
|
|
6
|
+
} from './keyboard-shortcut.js'
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
shortcutBadgeStyles,
|
|
10
|
+
shortcutKeyStyles,
|
|
11
|
+
shortcutSeparatorStyles,
|
|
12
|
+
} from './keyboard-shortcut.styles.js'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const shortcutBadgeStyles =
|
|
2
|
+
'inline-flex items-center gap-0.5 rounded border bg-muted px-1.5 py-0.5 text-xs font-mono text-muted-foreground'
|
|
3
|
+
|
|
4
|
+
export const shortcutKeyStyles =
|
|
5
|
+
'inline-flex items-center justify-center min-w-[1.25rem] rounded bg-background px-1 py-0.5 text-xs font-mono border shadow-sm'
|
|
6
|
+
|
|
7
|
+
export const shortcutSeparatorStyles =
|
|
8
|
+
'text-muted-foreground text-xs'
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export interface ShortcutProps {
|
|
4
|
+
/** Key combination (e.g., ['Ctrl', 'K'] or ['Meta', 'Shift', 'P']) */
|
|
5
|
+
keys: string[]
|
|
6
|
+
/** Callback when the shortcut is triggered */
|
|
7
|
+
onTrigger: () => void
|
|
8
|
+
/** Whether the shortcut is enabled */
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
/** Prevent default browser behavior */
|
|
11
|
+
preventDefault?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface KeyboardShortcutAPI {
|
|
15
|
+
/** Handle a keyboard event (returns true if it matched) */
|
|
16
|
+
handler(event: KeyboardEvent): boolean
|
|
17
|
+
/** Human-readable display of the shortcut */
|
|
18
|
+
display: string
|
|
19
|
+
/** Platform-aware display (uses command symbol on Mac) */
|
|
20
|
+
platformDisplay: string
|
|
21
|
+
/** Whether the shortcut is currently enabled */
|
|
22
|
+
enabled: boolean
|
|
23
|
+
/** The key combination */
|
|
24
|
+
keys: string[]
|
|
25
|
+
/** ARIA props for a visual shortcut badge */
|
|
26
|
+
badgeAriaProps: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MODIFIER_KEYS = new Set(['Ctrl', 'Control', 'Alt', 'Shift', 'Meta', 'Cmd', 'Command'])
|
|
30
|
+
|
|
31
|
+
const KEY_DISPLAY: Record<string, string> = {
|
|
32
|
+
'Ctrl': 'Ctrl',
|
|
33
|
+
'Control': 'Ctrl',
|
|
34
|
+
'Alt': 'Alt',
|
|
35
|
+
'Shift': 'Shift',
|
|
36
|
+
'Meta': 'Meta',
|
|
37
|
+
'Cmd': 'Cmd',
|
|
38
|
+
'Command': 'Cmd',
|
|
39
|
+
'Enter': '\u21B5',
|
|
40
|
+
'Backspace': '\u232B',
|
|
41
|
+
'Delete': 'Del',
|
|
42
|
+
'Escape': 'Esc',
|
|
43
|
+
'ArrowUp': '\u2191',
|
|
44
|
+
'ArrowDown': '\u2193',
|
|
45
|
+
'ArrowLeft': '\u2190',
|
|
46
|
+
'ArrowRight': '\u2192',
|
|
47
|
+
'Tab': 'Tab',
|
|
48
|
+
' ': 'Space',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MAC_KEY_DISPLAY: Record<string, string> = {
|
|
52
|
+
'Ctrl': '\u2303',
|
|
53
|
+
'Control': '\u2303',
|
|
54
|
+
'Alt': '\u2325',
|
|
55
|
+
'Shift': '\u21E7',
|
|
56
|
+
'Meta': '\u2318',
|
|
57
|
+
'Cmd': '\u2318',
|
|
58
|
+
'Command': '\u2318',
|
|
59
|
+
'Enter': '\u21B5',
|
|
60
|
+
'Backspace': '\u232B',
|
|
61
|
+
'Delete': '\u2326',
|
|
62
|
+
'Escape': '\u238B',
|
|
63
|
+
'ArrowUp': '\u2191',
|
|
64
|
+
'ArrowDown': '\u2193',
|
|
65
|
+
'ArrowLeft': '\u2190',
|
|
66
|
+
'ArrowRight': '\u2192',
|
|
67
|
+
'Tab': '\u21E5',
|
|
68
|
+
' ': '\u2423',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeKey(key: string): string {
|
|
72
|
+
// Normalize modifier variants
|
|
73
|
+
if (key === 'Command' || key === 'Cmd') return 'Meta'
|
|
74
|
+
if (key === 'Control') return 'Ctrl'
|
|
75
|
+
return key
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isMac(): boolean {
|
|
79
|
+
if (typeof navigator !== 'undefined') {
|
|
80
|
+
return navigator.platform?.includes('Mac') || navigator.userAgent?.includes('Mac')
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatShortcut(keys: string[], mac?: boolean): string {
|
|
86
|
+
const displayMap = mac ? MAC_KEY_DISPLAY : KEY_DISPLAY
|
|
87
|
+
return keys.map((key) => displayMap[key] ?? key.toUpperCase()).join(mac ? '' : '+')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createKeyboardShortcut(props: ShortcutProps): KeyboardShortcutAPI {
|
|
91
|
+
const {
|
|
92
|
+
keys,
|
|
93
|
+
onTrigger,
|
|
94
|
+
enabled: enabledProp = true,
|
|
95
|
+
preventDefault: preventDefaultProp = true,
|
|
96
|
+
} = props
|
|
97
|
+
|
|
98
|
+
const normalizedKeys = keys.map(normalizeKey)
|
|
99
|
+
const modifiers = normalizedKeys.filter((k) => MODIFIER_KEYS.has(k))
|
|
100
|
+
const regularKeys = normalizedKeys.filter((k) => !MODIFIER_KEYS.has(k))
|
|
101
|
+
|
|
102
|
+
function handler(event: KeyboardEvent): boolean {
|
|
103
|
+
if (!enabledProp) return false
|
|
104
|
+
|
|
105
|
+
// Check modifiers
|
|
106
|
+
const ctrlRequired = modifiers.includes('Ctrl')
|
|
107
|
+
const altRequired = modifiers.includes('Alt')
|
|
108
|
+
const shiftRequired = modifiers.includes('Shift')
|
|
109
|
+
const metaRequired = modifiers.includes('Meta')
|
|
110
|
+
|
|
111
|
+
if (ctrlRequired !== (event.ctrlKey || event.metaKey && !metaRequired)) {
|
|
112
|
+
// For Ctrl, also accept Meta on Mac
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ctrlMatch = ctrlRequired ? event.ctrlKey : !event.ctrlKey
|
|
116
|
+
const altMatch = altRequired ? event.altKey : !event.altKey
|
|
117
|
+
const shiftMatch = shiftRequired ? event.shiftKey : !event.shiftKey
|
|
118
|
+
const metaMatch = metaRequired ? event.metaKey : !event.metaKey
|
|
119
|
+
|
|
120
|
+
if (!ctrlMatch || !altMatch || !shiftMatch || !metaMatch) return false
|
|
121
|
+
|
|
122
|
+
// Check regular key
|
|
123
|
+
if (regularKeys.length > 0) {
|
|
124
|
+
const eventKey = event.key.length === 1 ? event.key.toUpperCase() : event.key
|
|
125
|
+
const targetKey = regularKeys[0].length === 1 ? regularKeys[0].toUpperCase() : regularKeys[0]
|
|
126
|
+
if (eventKey !== targetKey) return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (preventDefaultProp) {
|
|
130
|
+
event.preventDefault()
|
|
131
|
+
}
|
|
132
|
+
onTrigger()
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const mac = isMac()
|
|
137
|
+
const display = formatShortcut(keys, false)
|
|
138
|
+
const platformDisplay = formatShortcut(keys, mac)
|
|
139
|
+
|
|
140
|
+
const badgeAriaProps: Record<string, unknown> = {
|
|
141
|
+
'aria-hidden': true,
|
|
142
|
+
role: 'presentation',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
handler,
|
|
147
|
+
display,
|
|
148
|
+
platformDisplay,
|
|
149
|
+
enabled: enabledProp,
|
|
150
|
+
keys,
|
|
151
|
+
badgeAriaProps,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const selectorVariants = cva({
|
|
4
|
+
base: 'relative inline-flex items-center justify-between rounded-md border bg-background text-foreground cursor-pointer',
|
|
5
|
+
variants: {
|
|
6
|
+
size: {
|
|
7
|
+
sm: 'h-8 text-sm px-2 min-w-[120px]',
|
|
8
|
+
md: 'h-10 text-base px-3 min-w-[160px]',
|
|
9
|
+
lg: 'h-12 text-lg px-4 min-w-[200px]',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
size: 'md',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const optionVariants = cva({
|
|
18
|
+
base: 'flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-accent',
|
|
19
|
+
variants: {
|
|
20
|
+
selected: {
|
|
21
|
+
true: 'bg-accent/50 font-medium',
|
|
22
|
+
false: '',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
selected: 'false',
|
|
27
|
+
},
|
|
28
|
+
})
|