@refraction-ui/astro 0.14.0 → 0.15.1
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/Accordion.astro +7 -0
- package/dist/astro-accordion/Accordion.stories.ts +10 -4
- package/dist/astro-ai/AIScript.astro +1 -1
- package/dist/astro-analytics/Analytics.stories.ts +5 -3
- package/dist/astro-analytics/AnalyticsScript.astro +1 -1
- package/dist/astro-animated-text/AnimatedText.stories.ts +7 -3
- package/dist/astro-app-shell/AppShell.stories.ts +14 -3
- package/dist/astro-auth/Auth.stories.ts +8 -3
- package/dist/astro-avatar/Avatar.stories.ts +6 -3
- package/dist/astro-avatar-group/AvatarGroup.stories.ts +10 -3
- package/dist/astro-badge/Badge.stories.ts +7 -7
- package/dist/astro-bottom-nav/BottomNav.stories.ts +6 -3
- package/dist/astro-breadcrumbs/Breadcrumbs.stories.ts +9 -3
- package/dist/astro-button/Button.stories.ts +9 -3
- package/dist/astro-calendar/Calendar.stories.ts +9 -3
- package/dist/astro-callout/Callout.stories.ts +6 -3
- package/dist/astro-card/Card.stories.ts +6 -3
- package/dist/astro-card-grid/CardGrid.stories.ts +5 -3
- package/dist/astro-carousel/Carousel.astro +7 -0
- package/dist/astro-carousel/Carousel.stories.ts +10 -4
- package/dist/astro-charts/Charts.stories.ts +9 -4
- package/dist/astro-checkbox/Checkbox.stories.ts +9 -3
- package/dist/astro-code-block/CodeBlock.stories.ts +5 -3
- package/dist/astro-code-editor/CodeEditor.astro +1 -1
- package/dist/astro-code-editor/CodeEditor.stories.ts +10 -3
- package/dist/astro-collapsible/Collapsible.stories.ts +7 -3
- package/dist/astro-command/Command.stories.ts +6 -3
- package/dist/astro-command-input/CommandInput.stories.ts +5 -3
- package/dist/astro-content-protection/ContentProtection.stories.ts +9 -3
- package/dist/astro-conversation/Chat.astro +1 -1
- package/dist/astro-conversation/Conversation.stories.ts +7 -3
- package/dist/astro-data-table/DataTable.astro +1 -1
- package/dist/astro-data-table/DataTable.stories.ts +15 -3
- package/dist/astro-date-picker/DatePicker.stories.ts +11 -3
- package/dist/astro-device-frame/DeviceFrame.stories.ts +7 -3
- package/dist/astro-dialog/Dialog.stories.ts +7 -3
- package/dist/astro-diff-viewer/DiffViewer.stories.ts +19 -3
- package/dist/astro-dropdown-menu/DropdownMenu.stories.ts +6 -3
- package/dist/astro-emoji-picker/EmojiPicker.stories.ts +5 -3
- package/dist/astro-feedback-dialog/FeedbackDialog.stories.ts +6 -3
- package/dist/astro-file-tree/FileTree.stories.ts +5 -3
- package/dist/astro-file-upload/FileUpload.stories.ts +9 -3
- package/dist/astro-floating-reactions/FloatingReactions.astro +1 -1
- package/dist/astro-footer/Footer.stories.ts +7 -3
- package/dist/astro-icon-system/IconSystem.stories.ts +5 -3
- package/dist/astro-inline-editor/InlineEditor.stories.ts +5 -3
- package/dist/astro-input/Input.stories.ts +11 -3
- package/dist/astro-input-group/InputGroup.stories.ts +6 -3
- package/dist/astro-install-prompt/InstallPrompt.stories.ts +9 -3
- package/dist/astro-kanban-board/KanbanBoard.astro +92 -0
- package/dist/astro-kanban-board/index.ts +1 -0
- package/dist/astro-keyboard-shortcut/KeyboardShortcut.stories.ts +7 -3
- package/dist/astro-language-selector/LanguageSelector.stories.ts +11 -3
- package/dist/astro-link-card/LinkCard.stories.ts +5 -3
- package/dist/astro-logger/Logger.stories.ts +10 -3
- package/dist/astro-logger/TelemetryScript.astro +1 -1
- package/dist/astro-markdown-renderer/MarkdownRenderer.stories.ts +8 -3
- package/dist/astro-mascot/Mascot.astro +274 -0
- package/dist/astro-mascot/Mascot.stories.ts +46 -0
- package/dist/astro-mascot/index.ts +11 -0
- package/dist/astro-mobile-nav/MobileNav.stories.ts +6 -3
- package/dist/astro-navbar/Navbar.stories.ts +16 -3
- package/dist/astro-otp-input/OtpInput.stories.ts +20 -3
- package/dist/astro-pagination/Pagination.stories.ts +3 -3
- package/dist/astro-password-input/PasswordInput.stories.ts +29 -0
- package/dist/astro-payment/Payment.stories.ts +8 -3
- package/dist/astro-popover/Popover.stories.ts +10 -3
- package/dist/astro-popover/PopoverClose.stories.ts +17 -0
- package/dist/astro-popover/PopoverContent.stories.ts +19 -0
- package/dist/astro-popover/PopoverTrigger.stories.ts +17 -0
- package/dist/astro-presence-indicator/PresenceIndicator.stories.ts +14 -3
- package/dist/astro-progress-display/BadgeDisplay.astro +1 -1
- package/dist/astro-progress-display/BadgeDisplay.stories.ts +17 -0
- package/dist/astro-progress-display/ProgressBar.stories.ts +21 -0
- package/dist/astro-progress-display/StatsGrid.astro +1 -1
- package/dist/astro-progress-display/StatsGrid.stories.ts +19 -0
- package/dist/astro-radio/RadioGroup.stories.ts +25 -0
- package/dist/astro-radio/RadioItem.stories.ts +23 -0
- package/dist/astro-reaction-bar/ReactionBar.astro +1 -1
- package/dist/astro-reaction-bar/ReactionBar.stories.ts +10 -3
- package/dist/astro-resizable-layout/ResizableDivider.stories.ts +19 -0
- package/dist/astro-resizable-layout/ResizableLayout.stories.ts +18 -3
- package/dist/astro-resizable-layout/ResizablePane.stories.ts +21 -0
- package/dist/astro-rich-editor/RichEditor.stories.ts +20 -3
- package/dist/astro-search-bar/SearchBar.stories.ts +14 -3
- package/dist/astro-search-bar/SearchResultItem.stories.ts +17 -0
- package/dist/astro-search-bar/SearchResults.stories.ts +17 -0
- package/dist/astro-segmented-control/SegmentedControl.stories.ts +23 -0
- package/dist/astro-segmented-control/SegmentedControlItem.stories.ts +25 -0
- package/dist/astro-select/Select.stories.ts +16 -3
- package/dist/astro-select/SelectContent.stories.ts +17 -0
- package/dist/astro-select/SelectItem.stories.ts +23 -0
- package/dist/astro-select/SelectTrigger.stories.ts +21 -0
- package/dist/astro-separator/Separator.stories.ts +23 -0
- package/dist/astro-sidebar/Sidebar.astro +1 -1
- package/dist/astro-sidebar/Sidebar.stories.ts +14 -3
- package/dist/astro-skeleton/Skeleton.stories.ts +14 -3
- package/dist/astro-skeleton/SkeletonText.stories.ts +19 -0
- package/dist/astro-skip-to-content/SkipToContent.stories.ts +3 -3
- package/dist/astro-slide-viewer/SlideViewer.stories.ts +14 -3
- package/dist/astro-slot-picker/SlotPicker.astro +105 -0
- package/dist/astro-slot-picker/index.ts +1 -0
- package/dist/astro-social-auth-button/SocialAuthButton.stories.ts +23 -0
- package/dist/astro-social-auth-button/SocialAuthRow.stories.ts +17 -0
- package/dist/astro-sortable-list/SortableList.astro +68 -0
- package/dist/astro-sortable-list/index.ts +1 -0
- package/dist/astro-status-indicator/StatusIndicator.stories.ts +14 -3
- package/dist/astro-steps/Steps.stories.ts +3 -3
- package/dist/astro-switch/Switch.stories.ts +14 -3
- package/dist/astro-table-of-contents/TableOfContents.stories.ts +8 -3
- package/dist/astro-tabs/Tabs.stories.ts +12 -3
- package/dist/astro-tabs/TabsContent.stories.ts +21 -0
- package/dist/astro-tabs/TabsList.stories.ts +17 -0
- package/dist/astro-tabs/TabsTrigger.stories.ts +21 -0
- package/dist/astro-textarea/Textarea.stories.ts +22 -3
- package/dist/astro-theme/ThemeScript.stories.ts +23 -0
- package/dist/astro-theme/ThemeToggle.stories.ts +21 -0
- package/dist/astro-thread-view/ThreadView.astro +1 -1
- package/dist/astro-thread-view/ThreadView.stories.ts +10 -3
- package/dist/astro-toast/Toast.stories.ts +16 -3
- package/dist/astro-toast/Toaster.stories.ts +17 -0
- package/dist/astro-tooltip/Tooltip.stories.ts +10 -3
- package/dist/astro-tooltip/TooltipContent.stories.ts +19 -0
- package/dist/astro-tooltip/TooltipTrigger.stories.ts +17 -0
- package/dist/astro-version-selector/VersionSelector.astro +2 -2
- package/dist/astro-version-selector/VersionSelector.stories.ts +12 -3
- package/dist/astro-video-grid/VideoGrid.astro +1 -1
- package/dist/astro-video-player/VideoPlayer.stories.ts +16 -3
- package/dist/astro-voice-pill/VoicePill.stories.ts +24 -3
- package/dist/astro-waveform/Waveform.stories.ts +26 -3
- package/dist/index.ts +4 -0
- package/dist/kanban-board/index.ts +20 -0
- package/dist/kanban-board/kanban-board.styles.ts +78 -0
- package/dist/kanban-board/kanban-board.ts +82 -0
- package/dist/mascot/index.ts +2 -0
- package/dist/mascot/mascot.styles.ts +33 -0
- package/dist/mascot/mascot.ts +121 -0
- package/dist/slot-picker/index.ts +18 -0
- package/dist/slot-picker/slot-picker.styles.ts +70 -0
- package/dist/slot-picker/slot-picker.ts +64 -0
- package/dist/sortable-list/index.ts +13 -0
- package/dist/sortable-list/sortable-list.styles.ts +42 -0
- package/dist/sortable-list/sortable-list.ts +90 -0
- package/package.json +5 -1
package/dist/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export * from './astro-inline-editor/index.ts';
|
|
|
56
56
|
export * from './astro-input/index.ts';
|
|
57
57
|
export * from './astro-input-group/index.ts';
|
|
58
58
|
export * from './astro-install-prompt/index.ts';
|
|
59
|
+
export * from './astro-kanban-board/index.ts';
|
|
59
60
|
export * from './astro-keyboard-shortcut/index.ts';
|
|
60
61
|
export * from './astro-language-selector/index.ts';
|
|
61
62
|
export * from './astro-link-card/index.ts';
|
|
@@ -66,6 +67,7 @@ export * from './astro-logger/index.ts';
|
|
|
66
67
|
export * from './astro-markdown-renderer/index.ts';
|
|
67
68
|
export * from './astro-marquee-strip/index.ts';
|
|
68
69
|
export * from './astro-mastery-bar/index.ts';
|
|
70
|
+
export * from './astro-mascot/index.ts';
|
|
69
71
|
export * from './astro-mini-map/index.ts';
|
|
70
72
|
export * from './astro-mobile-nav/index.ts';
|
|
71
73
|
export * from './astro-navbar/index.ts';
|
|
@@ -95,7 +97,9 @@ export * from './astro-skeleton/index.ts';
|
|
|
95
97
|
export * from './astro-skip-to-content/index.ts';
|
|
96
98
|
export * from './astro-slide-viewer/index.ts';
|
|
97
99
|
export * from './astro-slider/index.ts';
|
|
100
|
+
export * from './astro-slot-picker/index.ts';
|
|
98
101
|
export * from './astro-social-auth-button/index.ts';
|
|
102
|
+
export * from './astro-sortable-list/index.ts';
|
|
99
103
|
export * from './astro-stat-grid/index.ts';
|
|
100
104
|
export * from './astro-status-indicator/index.ts';
|
|
101
105
|
export * from './astro-steps/index.ts';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createKanbanBoard,
|
|
3
|
+
cardsByColumn,
|
|
4
|
+
visibleAndOverflow,
|
|
5
|
+
type KanbanColumnDef,
|
|
6
|
+
type KanbanBoardAPI,
|
|
7
|
+
} from './kanban-board.js'
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
kanbanBoardVariants,
|
|
11
|
+
kanbanColumnVariants,
|
|
12
|
+
kanbanColumnHeaderClass,
|
|
13
|
+
kanbanColumnTitleClass,
|
|
14
|
+
kanbanColumnCountClass,
|
|
15
|
+
kanbanAccentBarClass,
|
|
16
|
+
kanbanNoteBarClass,
|
|
17
|
+
kanbanColumnBodyClass,
|
|
18
|
+
kanbanOverflowButtonClass,
|
|
19
|
+
kanbanCardVariants,
|
|
20
|
+
} from './kanban-board.styles.js'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** The outer board: horizontal scroll, flex row, gap between columns. */
|
|
4
|
+
export const kanbanBoardVariants = cva({
|
|
5
|
+
base: 'flex flex-row gap-4 overflow-x-auto pb-4',
|
|
6
|
+
variants: {},
|
|
7
|
+
defaultVariants: {},
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
/** A single column: muted background, rounded, fixed minimum width. */
|
|
11
|
+
export const kanbanColumnVariants = cva({
|
|
12
|
+
base: 'flex flex-col min-w-[260px] max-w-[300px] flex-shrink-0 rounded-xl bg-muted/40',
|
|
13
|
+
variants: {},
|
|
14
|
+
defaultVariants: {},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/** Column header row: icon slot + title + count badge. */
|
|
18
|
+
export const kanbanColumnHeaderClass =
|
|
19
|
+
'flex items-center gap-2 px-4 pt-4 pb-2'
|
|
20
|
+
|
|
21
|
+
/** Column title text. */
|
|
22
|
+
export const kanbanColumnTitleClass = 'flex-1 text-sm font-semibold text-foreground truncate'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Column count badge: a compact pill to the right of the title.
|
|
26
|
+
* Uses muted tokens so it sits quietly against any column accent.
|
|
27
|
+
*/
|
|
28
|
+
export const kanbanColumnCountClass =
|
|
29
|
+
'ml-auto inline-flex items-center justify-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Thin accent bar directly below the header.
|
|
33
|
+
* Callers apply `style={{ '--kanban-accent': def.accent }}` on the column
|
|
34
|
+
* element; this bar reads `var(--kanban-accent)` via an inline style.
|
|
35
|
+
* When no accent is supplied the bar is invisible (transparent).
|
|
36
|
+
*/
|
|
37
|
+
export const kanbanAccentBarClass = 'h-0.5 w-full'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Note/gate bar shown beneath the accent bar.
|
|
41
|
+
* Displays brief stage notes (e.g. "Technical screen required").
|
|
42
|
+
*/
|
|
43
|
+
export const kanbanNoteBarClass =
|
|
44
|
+
'mx-4 mb-2 rounded-md bg-muted px-3 py-1.5 text-xs text-muted-foreground'
|
|
45
|
+
|
|
46
|
+
/** Column body: card stack with gap, padded sides and bottom. */
|
|
47
|
+
export const kanbanColumnBodyClass = 'flex flex-col gap-2 px-3 pb-3'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* "+N more" overflow button.
|
|
51
|
+
* Styled as a subtle full-width ghost button inside the column.
|
|
52
|
+
*/
|
|
53
|
+
export const kanbanOverflowButtonClass = [
|
|
54
|
+
'mt-1 w-full rounded-lg border border-dashed border-border',
|
|
55
|
+
'py-1.5 text-center text-xs text-muted-foreground',
|
|
56
|
+
'transition-colors hover:bg-muted hover:text-foreground',
|
|
57
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
58
|
+
].join(' ')
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default card surface.
|
|
62
|
+
*
|
|
63
|
+
* Variants:
|
|
64
|
+
* - `clickable: 'true'` — adds hover treatment for interactive cards.
|
|
65
|
+
* - `clickable: 'false'` — static (default).
|
|
66
|
+
*/
|
|
67
|
+
export const kanbanCardVariants = cva({
|
|
68
|
+
base: 'rounded-lg border border-border bg-card p-3 text-sm text-foreground',
|
|
69
|
+
variants: {
|
|
70
|
+
clickable: {
|
|
71
|
+
true: 'cursor-pointer transition-colors hover:bg-muted/60',
|
|
72
|
+
false: '',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultVariants: {
|
|
76
|
+
clickable: 'false',
|
|
77
|
+
},
|
|
78
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** A single column definition for the Kanban board. */
|
|
2
|
+
export type KanbanColumnDef = {
|
|
3
|
+
/** Unique identifier for this column (matches cards' column id). */
|
|
4
|
+
id: string
|
|
5
|
+
/** Display title of the column (e.g. stage name). */
|
|
6
|
+
title: string
|
|
7
|
+
/** Optional accent color applied via CSS var `--kanban-accent`. */
|
|
8
|
+
accent?: string
|
|
9
|
+
/** Optional note or gate description shown below the column header. */
|
|
10
|
+
note?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface KanbanBoardAPI {
|
|
14
|
+
/** ARIA attributes to spread on the board container. */
|
|
15
|
+
ariaProps: { role: 'group'; 'aria-label': string }
|
|
16
|
+
/** Data attributes for styling hooks. */
|
|
17
|
+
dataAttributes: Record<string, string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the framework-agnostic board props.
|
|
22
|
+
*
|
|
23
|
+
* Returns `role="group"` with a fixed accessible label and data attributes
|
|
24
|
+
* so adapters can style based on state without inlining logic.
|
|
25
|
+
*/
|
|
26
|
+
export function createKanbanBoard(): KanbanBoardAPI {
|
|
27
|
+
return {
|
|
28
|
+
ariaProps: {
|
|
29
|
+
role: 'group',
|
|
30
|
+
'aria-label': 'Board',
|
|
31
|
+
},
|
|
32
|
+
dataAttributes: {
|
|
33
|
+
'data-component': 'kanban-board',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Bucket cards into a Map keyed by column id, preserving column order.
|
|
40
|
+
* Columns that have no cards get an empty array entry so callers can always
|
|
41
|
+
* iterate `columns` and look up their bucket safely.
|
|
42
|
+
*
|
|
43
|
+
* @param cards - All cards to distribute.
|
|
44
|
+
* @param getColumnId - Selector that reads the column id from a card.
|
|
45
|
+
* @param columns - Column definitions (determines output order).
|
|
46
|
+
*/
|
|
47
|
+
export function cardsByColumn<T>(
|
|
48
|
+
cards: T[],
|
|
49
|
+
getColumnId: (c: T) => string,
|
|
50
|
+
columns: KanbanColumnDef[],
|
|
51
|
+
): Map<string, T[]> {
|
|
52
|
+
const map = new Map<string, T[]>()
|
|
53
|
+
// Pre-populate every column so empty columns are present.
|
|
54
|
+
for (const col of columns) {
|
|
55
|
+
map.set(col.id, [])
|
|
56
|
+
}
|
|
57
|
+
for (const card of cards) {
|
|
58
|
+
const colId = getColumnId(card)
|
|
59
|
+
const bucket = map.get(colId)
|
|
60
|
+
if (bucket !== undefined) {
|
|
61
|
+
bucket.push(card)
|
|
62
|
+
}
|
|
63
|
+
// Cards referencing unknown columns are silently dropped.
|
|
64
|
+
}
|
|
65
|
+
return map
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Split a card list into a visible slice and an overflow count.
|
|
70
|
+
*
|
|
71
|
+
* @param cards - Full list of cards in the column.
|
|
72
|
+
* @param cap - Maximum visible cards. Defaults to 5.
|
|
73
|
+
*/
|
|
74
|
+
export function visibleAndOverflow<T>(
|
|
75
|
+
cards: T[],
|
|
76
|
+
cap = 5,
|
|
77
|
+
): { visible: T[]; overflow: number } {
|
|
78
|
+
if (cards.length <= cap) {
|
|
79
|
+
return { visible: cards, overflow: 0 }
|
|
80
|
+
}
|
|
81
|
+
return { visible: cards.slice(0, cap), overflow: cards.length - cap }
|
|
82
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
import type { TokenContract } from '../shared/index.ts'
|
|
3
|
+
|
|
4
|
+
export const mascotTokens: TokenContract = {
|
|
5
|
+
name: 'mascot',
|
|
6
|
+
tokens: {
|
|
7
|
+
primary: { variable: '--rfr-mascot-primary', fallback: '138 60% 51%' },
|
|
8
|
+
secondary: { variable: '--rfr-mascot-secondary', fallback: '138 60% 83%' },
|
|
9
|
+
accent: { variable: '--rfr-mascot-accent', fallback: '26 100% 67%' },
|
|
10
|
+
stroke: { variable: '--rfr-mascot-stroke', fallback: '139 59% 16%' },
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const mascotVariants = cva({
|
|
15
|
+
base: 'inline-block select-none transition-all duration-300',
|
|
16
|
+
variants: {
|
|
17
|
+
size: {
|
|
18
|
+
sm: 'w-24 h-auto',
|
|
19
|
+
md: 'w-36 h-auto',
|
|
20
|
+
lg: 'w-48 h-auto',
|
|
21
|
+
xl: 'w-64 h-auto',
|
|
22
|
+
},
|
|
23
|
+
animation: {
|
|
24
|
+
none: '',
|
|
25
|
+
bounce: 'motion-safe:animate-bounce',
|
|
26
|
+
float: 'rfr-mascot-float',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
size: 'md',
|
|
31
|
+
animation: 'none',
|
|
32
|
+
},
|
|
33
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { AccessibilityProps } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export type MascotMood = 'happy' | 'think' | 'wave'
|
|
4
|
+
export type MascotAnimation = 'none' | 'bounce' | 'float'
|
|
5
|
+
export type MascotSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
6
|
+
|
|
7
|
+
export interface MascotProps {
|
|
8
|
+
mood?: MascotMood
|
|
9
|
+
animation?: MascotAnimation
|
|
10
|
+
size?: MascotSize
|
|
11
|
+
animate?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MascotAPI {
|
|
15
|
+
/** ARIA attributes to spread on the root element */
|
|
16
|
+
ariaProps: Record<string, string | number | boolean>
|
|
17
|
+
/** Data attributes for CSS styling hooks and states */
|
|
18
|
+
dataAttributes: Record<string, string>
|
|
19
|
+
/** Active mascot state configuration */
|
|
20
|
+
state: {
|
|
21
|
+
mood: MascotMood
|
|
22
|
+
animation: MascotAnimation
|
|
23
|
+
size: MascotSize
|
|
24
|
+
animate: boolean
|
|
25
|
+
}
|
|
26
|
+
/** Mood styles mapping moods to CSS custom properties */
|
|
27
|
+
style: Record<string, string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Maps mascot moods ('happy', 'think', 'wave') to CSS custom properties (variables).
|
|
32
|
+
*/
|
|
33
|
+
export function getMascotMoodStyles(mood: MascotMood): Record<string, string> {
|
|
34
|
+
switch (mood) {
|
|
35
|
+
case 'happy':
|
|
36
|
+
return {
|
|
37
|
+
'--rfr-mascot-shell': 'var(--primary, 138 60% 51%)',
|
|
38
|
+
'--rfr-mascot-shell-rim': 'var(--primary-700, 138 58% 36%)',
|
|
39
|
+
'--rfr-mascot-belly': 'var(--primary-200, 138 60% 83%)',
|
|
40
|
+
'--rfr-mascot-skin': 'var(--primary-400, 138 59% 63%)',
|
|
41
|
+
'--rfr-mascot-skin-d': 'var(--primary-600, 138 58% 43%)',
|
|
42
|
+
'--rfr-mascot-scute': 'var(--sunshine, 42 100% 62%)',
|
|
43
|
+
'--rfr-mascot-scute-alt': 'var(--primary-300, 138 59% 74%)',
|
|
44
|
+
'--rfr-mascot-cheek': 'var(--accent-solid, 26 100% 67%)',
|
|
45
|
+
}
|
|
46
|
+
case 'think':
|
|
47
|
+
return {
|
|
48
|
+
// Indigo / purple tones
|
|
49
|
+
'--rfr-mascot-shell': '250 65% 55%',
|
|
50
|
+
'--rfr-mascot-shell-rim': '250 65% 40%',
|
|
51
|
+
'--rfr-mascot-belly': '250 65% 85%',
|
|
52
|
+
'--rfr-mascot-skin': '250 65% 72%',
|
|
53
|
+
'--rfr-mascot-skin-d': '250 65% 62%',
|
|
54
|
+
'--rfr-mascot-scute': '280 65% 60%',
|
|
55
|
+
'--rfr-mascot-scute-alt': '280 65% 50%',
|
|
56
|
+
'--rfr-mascot-cheek': '320 80% 65%',
|
|
57
|
+
}
|
|
58
|
+
case 'wave':
|
|
59
|
+
return {
|
|
60
|
+
// Orange / warm tones
|
|
61
|
+
'--rfr-mascot-shell': '24 85% 50%',
|
|
62
|
+
'--rfr-mascot-shell-rim': '24 85% 40%',
|
|
63
|
+
'--rfr-mascot-belly': '24 85% 85%',
|
|
64
|
+
'--rfr-mascot-skin': '24 85% 68%',
|
|
65
|
+
'--rfr-mascot-skin-d': '24 85% 58%',
|
|
66
|
+
'--rfr-mascot-scute': '160 60% 45%',
|
|
67
|
+
'--rfr-mascot-scute-alt': '160 60% 35%',
|
|
68
|
+
'--rfr-mascot-cheek': '15 90% 65%',
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a cleanup-enabled interval timer for Tobi's blinking state.
|
|
75
|
+
* Blinks for 150ms every 4000ms.
|
|
76
|
+
*/
|
|
77
|
+
export function startMascotBlinkInterval(
|
|
78
|
+
onBlinkChange: (isBlinking: boolean) => void,
|
|
79
|
+
intervalMs = 4000,
|
|
80
|
+
durationMs = 150,
|
|
81
|
+
): () => void {
|
|
82
|
+
const timer = setInterval(() => {
|
|
83
|
+
onBlinkChange(true)
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
onBlinkChange(false)
|
|
86
|
+
}, durationMs)
|
|
87
|
+
}, intervalMs)
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
clearInterval(timer)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createMascot(props: MascotProps = {}): MascotAPI {
|
|
95
|
+
const { mood = 'happy', animation = 'none', size = 'md', animate = true } = props
|
|
96
|
+
|
|
97
|
+
const ariaProps: Record<string, string | number | boolean> = {
|
|
98
|
+
role: 'img',
|
|
99
|
+
'aria-label': `Tobi the Tortoise feeling ${mood}`,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const dataAttributes: Record<string, string> = {
|
|
103
|
+
'data-slot': 'mascot',
|
|
104
|
+
'data-mood': mood,
|
|
105
|
+
'data-animation': animation,
|
|
106
|
+
'data-size': size,
|
|
107
|
+
'data-animate': animate ? 'true' : 'false',
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ariaProps,
|
|
112
|
+
dataAttributes,
|
|
113
|
+
state: {
|
|
114
|
+
mood,
|
|
115
|
+
animation,
|
|
116
|
+
size,
|
|
117
|
+
animate,
|
|
118
|
+
},
|
|
119
|
+
style: getMascotMoodStyles(mood),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
slotsForDay,
|
|
3
|
+
isSlotSelected,
|
|
4
|
+
createSlotPicker,
|
|
5
|
+
type SlotDay,
|
|
6
|
+
type SlotSelection,
|
|
7
|
+
type SlotPickerAPI,
|
|
8
|
+
} from './slot-picker.js'
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
slotPickerVariants,
|
|
12
|
+
slotPickerDayRowClass,
|
|
13
|
+
slotPickerDayVariants,
|
|
14
|
+
slotPickerGridClass,
|
|
15
|
+
slotPickerSlotVariants,
|
|
16
|
+
slotPickerSectionLabelClass,
|
|
17
|
+
slotPickerTimezoneClass,
|
|
18
|
+
} from './slot-picker.styles.js'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** Root wrapper for the entire slot picker (day row + slots grid). */
|
|
4
|
+
export const slotPickerVariants = cva({
|
|
5
|
+
base: 'flex flex-col gap-4',
|
|
6
|
+
variants: {},
|
|
7
|
+
defaultVariants: {},
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
/** Scrollable row of day chips. */
|
|
11
|
+
export const slotPickerDayRowClass = 'flex items-center gap-2 overflow-x-auto pb-1'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Individual day chip.
|
|
15
|
+
*
|
|
16
|
+
* `selected` → primary treatment; `unselected` → card with border.
|
|
17
|
+
*/
|
|
18
|
+
export const slotPickerDayVariants = cva({
|
|
19
|
+
base: [
|
|
20
|
+
'inline-flex flex-col items-center justify-center rounded-lg px-3 py-2 min-w-[3.5rem]',
|
|
21
|
+
'text-sm font-medium cursor-pointer transition-colors',
|
|
22
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
23
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
24
|
+
].join(' '),
|
|
25
|
+
variants: {
|
|
26
|
+
selected: {
|
|
27
|
+
true: 'bg-primary text-primary-foreground',
|
|
28
|
+
false: 'bg-card border border-input text-foreground hover:bg-muted',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
selected: 'false',
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** Grid that holds the time-slot buttons. */
|
|
37
|
+
export const slotPickerGridClass = 'grid grid-cols-3 gap-2 sm:grid-cols-4'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Individual time-slot button.
|
|
41
|
+
*
|
|
42
|
+
* `selected` → primary; `disabled` → dimmed regardless of selection.
|
|
43
|
+
*/
|
|
44
|
+
export const slotPickerSlotVariants = cva({
|
|
45
|
+
base: [
|
|
46
|
+
'inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm font-medium',
|
|
47
|
+
'transition-colors cursor-pointer',
|
|
48
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
49
|
+
].join(' '),
|
|
50
|
+
variants: {
|
|
51
|
+
selected: {
|
|
52
|
+
true: 'bg-primary text-primary-foreground border-primary',
|
|
53
|
+
false: 'bg-card border-input text-foreground hover:bg-muted',
|
|
54
|
+
},
|
|
55
|
+
disabled: {
|
|
56
|
+
true: 'opacity-40 pointer-events-none',
|
|
57
|
+
false: '',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
defaultVariants: {
|
|
61
|
+
selected: 'false',
|
|
62
|
+
disabled: 'false',
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/** Section label above the day row or slot grid (e.g. "Pick a day"). */
|
|
67
|
+
export const slotPickerSectionLabelClass = 'text-sm font-semibold text-foreground'
|
|
68
|
+
|
|
69
|
+
/** Timezone label rendered beside the "Pick a time" heading. */
|
|
70
|
+
export const slotPickerTimezoneClass = 'text-xs text-muted-foreground ml-2'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** A single day chip shown in the day-selector row. */
|
|
2
|
+
export type SlotDay = {
|
|
3
|
+
/** Unique identifier for the day (e.g. "2024-06-14"). */
|
|
4
|
+
id: string
|
|
5
|
+
/** Short weekday label (e.g. "Fri"). */
|
|
6
|
+
weekday: string
|
|
7
|
+
/** Day-of-month label (e.g. "14" or 14). */
|
|
8
|
+
dayNum: string | number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The currently-selected day + time slot pair. */
|
|
12
|
+
export type SlotSelection = {
|
|
13
|
+
/** The `id` of the selected day. */
|
|
14
|
+
dayId: string
|
|
15
|
+
/** The selected time slot string (e.g. "10:00 AM"). */
|
|
16
|
+
slot: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Return the list of available time slots for a given day.
|
|
21
|
+
* Returns an empty array when `dayId` has no entry in `slotsByDay`.
|
|
22
|
+
*/
|
|
23
|
+
export function slotsForDay(
|
|
24
|
+
slotsByDay: Record<string, string[]>,
|
|
25
|
+
dayId: string,
|
|
26
|
+
): string[] {
|
|
27
|
+
return slotsByDay[dayId] ?? []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check whether a specific slot in a specific day is currently selected.
|
|
32
|
+
*/
|
|
33
|
+
export function isSlotSelected(
|
|
34
|
+
value: SlotSelection | undefined,
|
|
35
|
+
dayId: string,
|
|
36
|
+
slot: string,
|
|
37
|
+
): boolean {
|
|
38
|
+
return value?.dayId === dayId && value?.slot === slot
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SlotPickerAPI {
|
|
42
|
+
/** ARIA attributes to spread on the root group element. */
|
|
43
|
+
ariaProps: { role: 'group'; 'aria-label': string }
|
|
44
|
+
/** Data attributes for styling hooks. */
|
|
45
|
+
dataAttributes: Record<string, string>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the framework-agnostic root props for a slot picker.
|
|
50
|
+
*
|
|
51
|
+
* Returns `role="group"` with an accessible label and data attributes.
|
|
52
|
+
* Adapters spread these onto their container element.
|
|
53
|
+
*/
|
|
54
|
+
export function createSlotPicker(): SlotPickerAPI {
|
|
55
|
+
return {
|
|
56
|
+
ariaProps: {
|
|
57
|
+
role: 'group',
|
|
58
|
+
'aria-label': 'Pick a time',
|
|
59
|
+
},
|
|
60
|
+
dataAttributes: {
|
|
61
|
+
'data-component': 'slot-picker',
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
reorder,
|
|
3
|
+
getNextOrderIndex,
|
|
4
|
+
createSortableList,
|
|
5
|
+
type SortableListAPI,
|
|
6
|
+
} from './sortable-list.js'
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
sortableListVariants,
|
|
10
|
+
sortableListRowVariants,
|
|
11
|
+
sortableListGripClass,
|
|
12
|
+
sortableListContentClass,
|
|
13
|
+
} from './sortable-list.styles.js'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cva } from '../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
/** The outer list container. */
|
|
4
|
+
export const sortableListVariants = cva({
|
|
5
|
+
base: 'flex flex-col gap-1 w-full',
|
|
6
|
+
variants: {
|
|
7
|
+
disabled: {
|
|
8
|
+
true: 'opacity-50 pointer-events-none',
|
|
9
|
+
false: '',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
disabled: 'false',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A single row in the list. The `dragging` variant visually lifts the row
|
|
19
|
+
* while it is being dragged.
|
|
20
|
+
*/
|
|
21
|
+
export const sortableListRowVariants = cva({
|
|
22
|
+
base: [
|
|
23
|
+
'flex items-center gap-3 rounded-md border border-border bg-card px-3 py-2',
|
|
24
|
+
'transition-opacity',
|
|
25
|
+
].join(' '),
|
|
26
|
+
variants: {
|
|
27
|
+
dragging: {
|
|
28
|
+
true: 'opacity-50 ring-2 ring-ring',
|
|
29
|
+
false: '',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
dragging: 'false',
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** The drag-grip handle button/icon area. */
|
|
38
|
+
export const sortableListGripClass =
|
|
39
|
+
'cursor-grab text-muted-foreground shrink-0 flex items-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background rounded'
|
|
40
|
+
|
|
41
|
+
/** Wraps the caller-supplied row content (fills remaining space). */
|
|
42
|
+
export const sortableListContentClass = 'flex-1 min-w-0'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, JSX-free headless logic for a drag-to-reorder vertical list.
|
|
3
|
+
*
|
|
4
|
+
* Three exports:
|
|
5
|
+
* - `reorder` — immutable array move, clamped
|
|
6
|
+
* - `getNextOrderIndex` — keyboard navigation rule (clamped, no wrap)
|
|
7
|
+
* - `createSortableList` — builds ARIA props for the list container
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface SortableListAPI {
|
|
11
|
+
/** ARIA attributes to spread on the list element. */
|
|
12
|
+
ariaProps: Record<string, string | number | boolean>
|
|
13
|
+
/** Data attributes for styling hooks. */
|
|
14
|
+
dataAttributes: Record<string, string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Move an item from index `from` to index `to` in `items`, returning a new
|
|
19
|
+
* array. Out-of-range indices return a shallow copy of the original array
|
|
20
|
+
* (no mutation, no throw).
|
|
21
|
+
*/
|
|
22
|
+
export function reorder<T>(items: T[], from: number, to: number): T[] {
|
|
23
|
+
const len = items.length
|
|
24
|
+
if (
|
|
25
|
+
len === 0 ||
|
|
26
|
+
from < 0 ||
|
|
27
|
+
from >= len ||
|
|
28
|
+
to < 0 ||
|
|
29
|
+
to >= len ||
|
|
30
|
+
from === to
|
|
31
|
+
) {
|
|
32
|
+
return [...items]
|
|
33
|
+
}
|
|
34
|
+
const result = [...items]
|
|
35
|
+
const [moved] = result.splice(from, 1)
|
|
36
|
+
result.splice(to, 0, moved)
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Keyboard reorder navigation — returns the next index for a given key.
|
|
42
|
+
*
|
|
43
|
+
* - ArrowUp / ArrowLeft → max(0, current - 1)
|
|
44
|
+
* - ArrowDown / ArrowRight → min(count - 1, current + 1)
|
|
45
|
+
* - Home → 0
|
|
46
|
+
* - End → count - 1
|
|
47
|
+
* - All other keys / empty list → current (clamped to valid range)
|
|
48
|
+
*
|
|
49
|
+
* Never wraps.
|
|
50
|
+
*/
|
|
51
|
+
export function getNextOrderIndex(
|
|
52
|
+
current: number,
|
|
53
|
+
key: string,
|
|
54
|
+
count: number,
|
|
55
|
+
): number {
|
|
56
|
+
if (count <= 0) return current
|
|
57
|
+
const clamped = Math.max(0, Math.min(current, count - 1))
|
|
58
|
+
|
|
59
|
+
switch (key) {
|
|
60
|
+
case 'ArrowUp':
|
|
61
|
+
case 'ArrowLeft':
|
|
62
|
+
return Math.max(0, clamped - 1)
|
|
63
|
+
case 'ArrowDown':
|
|
64
|
+
case 'ArrowRight':
|
|
65
|
+
return Math.min(count - 1, clamped + 1)
|
|
66
|
+
case 'Home':
|
|
67
|
+
return 0
|
|
68
|
+
case 'End':
|
|
69
|
+
return count - 1
|
|
70
|
+
default:
|
|
71
|
+
return clamped
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the framework-agnostic props for a sortable list container.
|
|
77
|
+
*
|
|
78
|
+
* Returns `role="list"` so screen readers announce the number of items
|
|
79
|
+
* correctly, plus a `data-component` hook for styling.
|
|
80
|
+
*/
|
|
81
|
+
export function createSortableList(): SortableListAPI {
|
|
82
|
+
return {
|
|
83
|
+
ariaProps: {
|
|
84
|
+
role: 'list',
|
|
85
|
+
},
|
|
86
|
+
dataAttributes: {
|
|
87
|
+
'data-component': 'sortable-list',
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|