@adres/mgda-ui 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brand-knowledge.json +451 -0
- package/knowledge/accordion.md +135 -0
- package/knowledge/activity-rings.md +123 -0
- package/knowledge/alert.md +184 -0
- package/knowledge/answer-actions.md +217 -0
- package/knowledge/aspect-ratio.md +82 -0
- package/knowledge/avatar.md +158 -0
- package/knowledge/badge.md +172 -0
- package/knowledge/bar-graph.md +167 -0
- package/knowledge/bento-grid.md +115 -0
- package/knowledge/blockquote.md +153 -0
- package/knowledge/box-list.md +177 -0
- package/knowledge/bullet-list.md +128 -0
- package/knowledge/button-group.md +179 -0
- package/knowledge/button.md +231 -0
- package/knowledge/calendar.md +168 -0
- package/knowledge/card.md +321 -0
- package/knowledge/cards-nav.md +170 -0
- package/knowledge/chain-of-thought.md +164 -0
- package/knowledge/chat-container.md +135 -0
- package/knowledge/chat-message.md +224 -0
- package/knowledge/checkbox-card.md +172 -0
- package/knowledge/checkbox.md +111 -0
- package/knowledge/citations.md +241 -0
- package/knowledge/combobox.md +265 -0
- package/knowledge/countdown-pill.md +153 -0
- package/knowledge/cta.md +118 -0
- package/knowledge/data-list.md +173 -0
- package/knowledge/data-table.md +220 -0
- package/knowledge/dialog.md +249 -0
- package/knowledge/drawer.md +236 -0
- package/knowledge/dropdown-menu.md +238 -0
- package/knowledge/dropzone.md +153 -0
- package/knowledge/email-chips-input.md +161 -0
- package/knowledge/empty-state.md +106 -0
- package/knowledge/entity-header.md +166 -0
- package/knowledge/feature-grid.md +120 -0
- package/knowledge/feedback-bar.md +127 -0
- package/knowledge/footer.md +116 -0
- package/knowledge/heading.md +189 -0
- package/knowledge/hero.md +361 -0
- package/knowledge/input-button.md +187 -0
- package/knowledge/input-group.md +185 -0
- package/knowledge/input.md +256 -0
- package/knowledge/inset.md +177 -0
- package/knowledge/kpi-card.md +206 -0
- package/knowledge/list-nav.md +181 -0
- package/knowledge/logo-cloud.md +102 -0
- package/knowledge/media-gallery.md +158 -0
- package/knowledge/message-actions.md +175 -0
- package/knowledge/nav-sidebar.md +225 -0
- package/knowledge/number-stepper.md +159 -0
- package/knowledge/page-header.md +109 -0
- package/knowledge/pagination.md +225 -0
- package/knowledge/popover.md +159 -0
- package/knowledge/pricing.md +132 -0
- package/knowledge/progress.md +156 -0
- package/knowledge/prompt-input.md +337 -0
- package/knowledge/prompt-suggestions.md +168 -0
- package/knowledge/quota-bar.md +160 -0
- package/knowledge/quote.md +116 -0
- package/knowledge/radio-card.md +172 -0
- package/knowledge/radio-group.md +135 -0
- package/knowledge/reasoning.md +142 -0
- package/knowledge/search.md +131 -0
- package/knowledge/section-nav.md +162 -0
- package/knowledge/segmented-control.md +181 -0
- package/knowledge/select-button.md +205 -0
- package/knowledge/select.md +210 -0
- package/knowledge/separator.md +75 -0
- package/knowledge/shimmer-text.md +140 -0
- package/knowledge/sidebar.md +227 -0
- package/knowledge/skeleton.md +142 -0
- package/knowledge/slider.md +133 -0
- package/knowledge/slideshow.md +159 -0
- package/knowledge/source.md +156 -0
- package/knowledge/spinner.md +120 -0
- package/knowledge/stat-strip.md +128 -0
- package/knowledge/stats.md +117 -0
- package/knowledge/step-indicator.md +157 -0
- package/knowledge/switch.md +88 -0
- package/knowledge/table.md +190 -0
- package/knowledge/tabs.md +169 -0
- package/knowledge/testimonials.md +115 -0
- package/knowledge/text-link.md +192 -0
- package/knowledge/text.md +194 -0
- package/knowledge/thinking-bar.md +115 -0
- package/knowledge/thinking-steps.md +140 -0
- package/knowledge/toaster.md +127 -0
- package/knowledge/toggle.md +113 -0
- package/knowledge/tool-call-card.md +241 -0
- package/knowledge/tooltip.md +120 -0
- package/knowledge/top-nav.md +138 -0
- package/knowledge.json +19854 -0
- package/package.json +94 -4
- package/src/components/accordion.stories.tsx +46 -0
- package/src/components/accordion.tsx +74 -0
- package/src/components/activity-rings.stories.tsx +73 -0
- package/src/components/activity-rings.tsx +121 -0
- package/src/components/alert.stories.tsx +43 -0
- package/src/components/alert.tsx +132 -0
- package/src/components/answer-actions.stories.tsx +54 -0
- package/src/components/answer-actions.test.tsx +37 -0
- package/src/components/answer-actions.tsx +313 -0
- package/src/components/aspect-ratio.stories.tsx +32 -0
- package/src/components/aspect-ratio.tsx +5 -0
- package/src/components/avatar.stories.tsx +40 -0
- package/src/components/avatar.tsx +154 -0
- package/src/components/badge.stories.tsx +27 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/bar-graph.stories.tsx +28 -0
- package/src/components/bar-graph.tsx +204 -0
- package/src/components/bento-grid.stories.tsx +23 -0
- package/src/components/bento-grid.tsx +67 -0
- package/src/components/blockquote.stories.tsx +28 -0
- package/src/components/blockquote.tsx +88 -0
- package/src/components/box-list.stories.tsx +25 -0
- package/src/components/box-list.tsx +108 -0
- package/src/components/bullet-list.stories.tsx +22 -0
- package/src/components/bullet-list.tsx +101 -0
- package/src/components/button-group.stories.tsx +31 -0
- package/src/components/button-group.tsx +156 -0
- package/src/components/button.stories.tsx +66 -0
- package/src/components/button.test.ts +17 -0
- package/src/components/button.tsx +139 -0
- package/src/components/calendar.stories.tsx +25 -0
- package/src/components/calendar.tsx +367 -0
- package/src/components/card.stories.tsx +44 -0
- package/src/components/card.tsx +392 -0
- package/src/components/cards-nav.stories.tsx +35 -0
- package/src/components/cards-nav.tsx +115 -0
- package/src/components/chain-of-thought.stories.tsx +67 -0
- package/src/components/chain-of-thought.test.tsx +50 -0
- package/src/components/chain-of-thought.tsx +170 -0
- package/src/components/chat-container.stories.tsx +47 -0
- package/src/components/chat-container.test.tsx +49 -0
- package/src/components/chat-container.tsx +222 -0
- package/src/components/chat-message.stories.tsx +52 -0
- package/src/components/chat-message.test.tsx +36 -0
- package/src/components/chat-message.tsx +163 -0
- package/src/components/checkbox-card.stories.tsx +27 -0
- package/src/components/checkbox-card.tsx +200 -0
- package/src/components/checkbox.stories.tsx +26 -0
- package/src/components/checkbox.tsx +61 -0
- package/src/components/citations.stories.tsx +64 -0
- package/src/components/citations.test.tsx +61 -0
- package/src/components/citations.tsx +220 -0
- package/src/components/combobox.stories.tsx +184 -0
- package/src/components/combobox.test.tsx +82 -0
- package/src/components/combobox.tsx +407 -0
- package/src/components/countdown-pill.stories.tsx +24 -0
- package/src/components/countdown-pill.tsx +129 -0
- package/src/components/cta.stories.tsx +29 -0
- package/src/components/cta.tsx +52 -0
- package/src/components/data-list.stories.tsx +50 -0
- package/src/components/data-list.test.tsx +35 -0
- package/src/components/data-list.tsx +181 -0
- package/src/components/data-table.stories.tsx +29 -0
- package/src/components/data-table.tsx +200 -0
- package/src/components/dialog.stories.tsx +42 -0
- package/src/components/dialog.tsx +200 -0
- package/src/components/drawer.stories.tsx +48 -0
- package/src/components/drawer.tsx +385 -0
- package/src/components/dropdown-menu.stories.tsx +41 -0
- package/src/components/dropdown-menu.tsx +252 -0
- package/src/components/dropzone.stories.tsx +19 -0
- package/src/components/dropzone.tsx +250 -0
- package/src/components/email-chips-input.stories.tsx +135 -0
- package/src/components/email-chips-input.tsx +210 -0
- package/src/components/empty-state.stories.tsx +22 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/entity-header.stories.tsx +43 -0
- package/src/components/entity-header.test.tsx +35 -0
- package/src/components/entity-header.tsx +92 -0
- package/src/components/feature-grid.stories.tsx +25 -0
- package/src/components/feature-grid.tsx +87 -0
- package/src/components/feedback-bar.stories.tsx +25 -0
- package/src/components/feedback-bar.test.tsx +24 -0
- package/src/components/feedback-bar.tsx +62 -0
- package/src/components/footer.stories.tsx +23 -0
- package/src/components/footer.tsx +78 -0
- package/src/components/heading.stories.tsx +45 -0
- package/src/components/heading.test.tsx +22 -0
- package/src/components/heading.tsx +77 -0
- package/src/components/hero.stories.tsx +38 -0
- package/src/components/hero.tsx +498 -0
- package/src/components/input-button.stories.tsx +19 -0
- package/src/components/input-button.tsx +112 -0
- package/src/components/input-group.stories.tsx +38 -0
- package/src/components/input-group.tsx +157 -0
- package/src/components/input.stories.tsx +31 -0
- package/src/components/input.tsx +284 -0
- package/src/components/inset.stories.tsx +29 -0
- package/src/components/inset.tsx +70 -0
- package/src/components/kpi-card.stories.tsx +29 -0
- package/src/components/kpi-card.tsx +284 -0
- package/src/components/list-nav.stories.tsx +22 -0
- package/src/components/list-nav.tsx +145 -0
- package/src/components/logo-cloud.stories.tsx +23 -0
- package/src/components/logo-cloud.tsx +42 -0
- package/src/components/media-gallery.stories.tsx +25 -0
- package/src/components/media-gallery.tsx +347 -0
- package/src/components/message-actions.stories.tsx +45 -0
- package/src/components/message-actions.test.tsx +32 -0
- package/src/components/message-actions.tsx +119 -0
- package/src/components/nav-sidebar.stories.tsx +58 -0
- package/src/components/nav-sidebar.test.tsx +54 -0
- package/src/components/nav-sidebar.tsx +462 -0
- package/src/components/number-stepper.stories.tsx +19 -0
- package/src/components/number-stepper.tsx +158 -0
- package/src/components/page-header.stories.tsx +25 -0
- package/src/components/page-header.tsx +64 -0
- package/src/components/pagination.stories.tsx +110 -0
- package/src/components/pagination.test.tsx +46 -0
- package/src/components/pagination.tsx +144 -0
- package/src/components/popover.stories.tsx +26 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/pricing.stories.tsx +42 -0
- package/src/components/pricing.tsx +130 -0
- package/src/components/progress.stories.tsx +22 -0
- package/src/components/progress.tsx +189 -0
- package/src/components/prompt-input.stories.tsx +62 -0
- package/src/components/prompt-input.test.tsx +106 -0
- package/src/components/prompt-input.tsx +343 -0
- package/src/components/prompt-suggestions.stories.tsx +53 -0
- package/src/components/prompt-suggestions.test.tsx +37 -0
- package/src/components/prompt-suggestions.tsx +122 -0
- package/src/components/quota-bar.stories.tsx +19 -0
- package/src/components/quota-bar.tsx +191 -0
- package/src/components/quote.stories.tsx +18 -0
- package/src/components/quote.tsx +43 -0
- package/src/components/radio-card.stories.tsx +27 -0
- package/src/components/radio-card.tsx +225 -0
- package/src/components/radio-group.stories.tsx +32 -0
- package/src/components/radio-group.tsx +68 -0
- package/src/components/reasoning.stories.tsx +50 -0
- package/src/components/reasoning.test.tsx +48 -0
- package/src/components/reasoning.tsx +143 -0
- package/src/components/search.stories.tsx +18 -0
- package/src/components/search.tsx +133 -0
- package/src/components/section-nav.stories.tsx +59 -0
- package/src/components/section-nav.test.tsx +36 -0
- package/src/components/section-nav.tsx +146 -0
- package/src/components/segmented-control.stories.tsx +22 -0
- package/src/components/segmented-control.tsx +301 -0
- package/src/components/select-button.stories.tsx +27 -0
- package/src/components/select-button.tsx +174 -0
- package/src/components/select.stories.tsx +27 -0
- package/src/components/select.tsx +198 -0
- package/src/components/separator.stories.tsx +22 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/shimmer-text.stories.tsx +23 -0
- package/src/components/shimmer-text.test.tsx +35 -0
- package/src/components/shimmer-text.tsx +43 -0
- package/src/components/sidebar.stories.tsx +25 -0
- package/src/components/sidebar.tsx +234 -0
- package/src/components/skeleton.stories.tsx +22 -0
- package/src/components/skeleton.tsx +96 -0
- package/src/components/slider.stories.tsx +20 -0
- package/src/components/slider.tsx +176 -0
- package/src/components/slideshow.stories.tsx +30 -0
- package/src/components/slideshow.tsx +291 -0
- package/src/components/source.stories.tsx +32 -0
- package/src/components/source.test.tsx +40 -0
- package/src/components/source.tsx +161 -0
- package/src/components/spinner.stories.tsx +19 -0
- package/src/components/spinner.tsx +76 -0
- package/src/components/stat-strip.stories.tsx +36 -0
- package/src/components/stat-strip.test.tsx +25 -0
- package/src/components/stat-strip.tsx +85 -0
- package/src/components/stats.stories.tsx +24 -0
- package/src/components/stats.tsx +83 -0
- package/src/components/step-indicator.stories.tsx +18 -0
- package/src/components/step-indicator.test.tsx +18 -0
- package/src/components/step-indicator.tsx +286 -0
- package/src/components/switch.stories.tsx +26 -0
- package/src/components/switch.tsx +32 -0
- package/src/components/table.stories.tsx +38 -0
- package/src/components/table.tsx +220 -0
- package/src/components/tabs.stories.tsx +25 -0
- package/src/components/tabs.tsx +276 -0
- package/src/components/testimonials.stories.tsx +22 -0
- package/src/components/testimonials.tsx +109 -0
- package/src/components/text-link.stories.tsx +47 -0
- package/src/components/text-link.test.tsx +18 -0
- package/src/components/text-link.tsx +88 -0
- package/src/components/text.stories.tsx +49 -0
- package/src/components/text.tsx +105 -0
- package/src/components/thinking-bar.stories.tsx +19 -0
- package/src/components/thinking-bar.test.tsx +27 -0
- package/src/components/thinking-bar.tsx +64 -0
- package/src/components/thinking-steps.stories.tsx +44 -0
- package/src/components/thinking-steps.test.tsx +42 -0
- package/src/components/thinking-steps.tsx +92 -0
- package/src/components/toaster.stories.tsx +80 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/toggle.stories.tsx +21 -0
- package/src/components/toggle.tsx +103 -0
- package/src/components/tool-call-card.stories.tsx +97 -0
- package/src/components/tool-call-card.test.tsx +88 -0
- package/src/components/tool-call-card.tsx +231 -0
- package/src/components/tooltip.stories.tsx +25 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/top-nav.stories.tsx +20 -0
- package/src/components/top-nav.tsx +76 -0
- package/src/docs/Colors.stories.tsx +192 -0
- package/src/docs/GettingStarted.mdx +72 -0
- package/src/docs/Mcp.mdx +52 -0
- package/src/docs/Typography.stories.tsx +114 -0
- package/src/hooks/use-in-view.ts +32 -0
- package/src/hooks/use-reduced-motion.ts +26 -0
- package/src/index.ts +343 -0
- package/src/lib/cn.test.ts +67 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/motion.ts +109 -0
- package/src/lib/prefix-utilities.test.ts +56 -0
- package/src/lib/prefix-utilities.ts +50 -0
- package/src/styles/base.css +489 -0
- package/src/styles/motion.css +69 -0
- package/src/styles/overrides.css +44 -0
- package/src/styles/typography.css +173 -0
- package/src/theme/theme.css +391 -0
- package/src/theme/tokens.ts +29 -0
- package/src/theme/types.ts +26 -0
- package/README.md +0 -3
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type CalendarMode = "single" | "range"
|
|
10
|
+
|
|
11
|
+
/** A start/end pair. `to` is undefined while the user is mid-selection. */
|
|
12
|
+
export interface DateRange {
|
|
13
|
+
from: Date | undefined
|
|
14
|
+
to: Date | undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CalendarValue = Date | DateRange | undefined
|
|
18
|
+
|
|
19
|
+
export interface CalendarProps
|
|
20
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange"> {
|
|
21
|
+
/** Selection mode. `single` picks one date; `range` picks a start→end span. */
|
|
22
|
+
mode?: CalendarMode
|
|
23
|
+
/** Controlled selection. `Date | undefined` in single mode, `DateRange` in range mode. */
|
|
24
|
+
value?: CalendarValue
|
|
25
|
+
/** Uncontrolled initial selection. */
|
|
26
|
+
defaultValue?: CalendarValue
|
|
27
|
+
onValueChange?: (value: CalendarValue) => void
|
|
28
|
+
/** Controlled displayed month (any day within it). */
|
|
29
|
+
month?: Date
|
|
30
|
+
defaultMonth?: Date
|
|
31
|
+
onMonthChange?: (month: Date) => void
|
|
32
|
+
/** Selectable bounds (inclusive). */
|
|
33
|
+
min?: Date
|
|
34
|
+
max?: Date
|
|
35
|
+
/** Return true to disable a specific day (evaluated after min/max). */
|
|
36
|
+
isDateDisabled?: (date: Date) => boolean
|
|
37
|
+
/** 0 = Sunday … 6 = Saturday. Defaults to the locale's first weekday (Mon fallback). */
|
|
38
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
39
|
+
/** BCP-47 locale for month/weekday names + first weekday. Defaults to the runtime locale. */
|
|
40
|
+
locale?: string
|
|
41
|
+
/** Render leading/trailing days from adjacent months (muted). Default true. */
|
|
42
|
+
showOutsideDays?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Pure date helpers (local time, no dependency) ───────────────────────────────
|
|
46
|
+
|
|
47
|
+
const startOfDay = (d: Date) => { const x = new Date(d); x.setHours(0, 0, 0, 0); return x }
|
|
48
|
+
const startOfMonth = (d: Date) => { const x = new Date(d); x.setDate(1); x.setHours(0, 0, 0, 0); return x }
|
|
49
|
+
const addDays = (d: Date, n: number) => { const x = new Date(d); x.setDate(x.getDate() + n); return x }
|
|
50
|
+
const addMonths = (d: Date, n: number) => { const x = startOfMonth(d); x.setMonth(x.getMonth() + n); return x }
|
|
51
|
+
const isSameDay = (a?: Date, b?: Date) =>
|
|
52
|
+
!!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
53
|
+
const isSameMonth = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()
|
|
54
|
+
/** a < b < c, comparing whole days. */
|
|
55
|
+
const isBetween = (d: Date, a: Date, b: Date) => {
|
|
56
|
+
const t = startOfDay(d).getTime()
|
|
57
|
+
const lo = Math.min(startOfDay(a).getTime(), startOfDay(b).getTime())
|
|
58
|
+
const hi = Math.max(startOfDay(a).getTime(), startOfDay(b).getTime())
|
|
59
|
+
return t > lo && t < hi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Locale's first weekday (1=Mon…7=Sun via `weekInfo`) mapped to 0=Sun…6=Sat. Mon fallback. */
|
|
63
|
+
function localeWeekStart(locale: string): number {
|
|
64
|
+
try {
|
|
65
|
+
const loc = new Intl.Locale(locale)
|
|
66
|
+
// `weekInfo` is a getter in newer engines, a property in others.
|
|
67
|
+
const info = (loc as unknown as { weekInfo?: { firstDay?: number }; getWeekInfo?: () => { firstDay?: number } })
|
|
68
|
+
const first = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
69
|
+
if (first) return first % 7 // 7 (Sun) → 0
|
|
70
|
+
} catch { /* Intl.Locale/weekInfo unsupported */ }
|
|
71
|
+
return 1 // Monday
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The 6×7 grid of days for `month`, starting on `weekStartsOn`. */
|
|
75
|
+
function monthMatrix(month: Date, weekStartsOn: number): Date[] {
|
|
76
|
+
const first = startOfMonth(month)
|
|
77
|
+
const lead = (first.getDay() - weekStartsOn + 7) % 7
|
|
78
|
+
const start = addDays(first, -lead)
|
|
79
|
+
return Array.from({ length: 42 }, (_, i) => addDays(start, i))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isRange = (v: CalendarValue): v is DateRange =>
|
|
83
|
+
!!v && typeof v === "object" && !(v instanceof Date)
|
|
84
|
+
|
|
85
|
+
// ─── Component ───────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const CELL = "ax:size-9" // 36px tap target
|
|
88
|
+
const WEEKDAYS_REF = new Date(2021, 7, 1) // a Sunday, anchor for weekday name generation
|
|
89
|
+
|
|
90
|
+
export const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
|
|
91
|
+
(
|
|
92
|
+
{
|
|
93
|
+
mode = "single",
|
|
94
|
+
value: valueProp,
|
|
95
|
+
defaultValue,
|
|
96
|
+
onValueChange,
|
|
97
|
+
month: monthProp,
|
|
98
|
+
defaultMonth,
|
|
99
|
+
onMonthChange,
|
|
100
|
+
min,
|
|
101
|
+
max,
|
|
102
|
+
isDateDisabled,
|
|
103
|
+
weekStartsOn,
|
|
104
|
+
locale,
|
|
105
|
+
showOutsideDays = true,
|
|
106
|
+
className,
|
|
107
|
+
...props
|
|
108
|
+
},
|
|
109
|
+
ref,
|
|
110
|
+
) => {
|
|
111
|
+
const today = React.useMemo(() => startOfDay(new Date()), [])
|
|
112
|
+
// Resolve the browser locale only AFTER mount. SSR and the first client render
|
|
113
|
+
// must use a fixed locale ("en"); otherwise Intl weekday names + first weekday
|
|
114
|
+
// differ between server (Node default) and client (browser) and break hydration.
|
|
115
|
+
const [clientLocale, setClientLocale] = React.useState<string>()
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
if (!locale && typeof navigator !== "undefined") setClientLocale(navigator.language)
|
|
118
|
+
}, [locale])
|
|
119
|
+
const activeLocale = locale ?? clientLocale ?? "en"
|
|
120
|
+
|
|
121
|
+
const derivedWeekStart = React.useMemo(() => localeWeekStart(activeLocale), [activeLocale])
|
|
122
|
+
const weekStart = weekStartsOn ?? derivedWeekStart
|
|
123
|
+
|
|
124
|
+
// Controlled/uncontrolled selection.
|
|
125
|
+
const [valueState, setValueState] = React.useState<CalendarValue>(defaultValue)
|
|
126
|
+
const value = valueProp !== undefined ? valueProp : valueState
|
|
127
|
+
const setValue = (next: CalendarValue) => {
|
|
128
|
+
if (valueProp === undefined) setValueState(next)
|
|
129
|
+
onValueChange?.(next)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// First selected date, used to seed the displayed month + focus.
|
|
133
|
+
const anchor = isRange(value) ? value.from : (value as Date | undefined)
|
|
134
|
+
|
|
135
|
+
// Controlled/uncontrolled displayed month.
|
|
136
|
+
const [monthState, setMonthState] = React.useState<Date>(
|
|
137
|
+
() => startOfMonth(defaultMonth ?? anchor ?? today),
|
|
138
|
+
)
|
|
139
|
+
const month = monthProp ? startOfMonth(monthProp) : monthState
|
|
140
|
+
const setMonth = (next: Date) => {
|
|
141
|
+
const m = startOfMonth(next)
|
|
142
|
+
if (!monthProp) setMonthState(m)
|
|
143
|
+
onMonthChange?.(m)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Roving focus target (a day inside the displayed month).
|
|
147
|
+
const [focusedDate, setFocusedDate] = React.useState<Date>(
|
|
148
|
+
() => (anchor && isSameMonth(anchor, month) ? startOfDay(anchor)
|
|
149
|
+
: isSameMonth(today, month) ? today
|
|
150
|
+
: startOfMonth(month)),
|
|
151
|
+
)
|
|
152
|
+
// Keep the focus target inside the visible month when the month changes.
|
|
153
|
+
React.useEffect(() => {
|
|
154
|
+
if (!isSameMonth(focusedDate, month)) {
|
|
155
|
+
const day = Math.min(focusedDate.getDate(), new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate())
|
|
156
|
+
setFocusedDate(new Date(month.getFullYear(), month.getMonth(), day))
|
|
157
|
+
}
|
|
158
|
+
}, [month]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
159
|
+
|
|
160
|
+
// Move DOM focus to the focused cell only after keyboard navigation.
|
|
161
|
+
const focusBtnRef = React.useRef<HTMLButtonElement>(null)
|
|
162
|
+
const shouldFocus = React.useRef(false)
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
if (shouldFocus.current) { focusBtnRef.current?.focus(); shouldFocus.current = false }
|
|
165
|
+
}, [focusedDate])
|
|
166
|
+
|
|
167
|
+
// RTL: read the actual resolved direction so arrow keys stay intuitive.
|
|
168
|
+
const rootRef = React.useRef<HTMLDivElement>(null)
|
|
169
|
+
const isRtl = () => rootRef.current ? getComputedStyle(rootRef.current).direction === "rtl" : false
|
|
170
|
+
|
|
171
|
+
const dayDisabled = React.useCallback((d: Date) => {
|
|
172
|
+
if (min && startOfDay(d) < startOfDay(min)) return true
|
|
173
|
+
if (max && startOfDay(d) > startOfDay(max)) return true
|
|
174
|
+
return isDateDisabled?.(d) ?? false
|
|
175
|
+
}, [min, max, isDateDisabled])
|
|
176
|
+
|
|
177
|
+
const select = (d: Date) => {
|
|
178
|
+
if (dayDisabled(d)) return
|
|
179
|
+
const day = startOfDay(d)
|
|
180
|
+
if (mode === "range") {
|
|
181
|
+
const r = isRange(value) ? value : { from: undefined, to: undefined }
|
|
182
|
+
let next: DateRange
|
|
183
|
+
if (!r.from || r.to) next = { from: day, to: undefined } // start fresh
|
|
184
|
+
else next = day < r.from ? { from: day, to: r.from } : { from: r.from, to: day } // close the span
|
|
185
|
+
setValue(next)
|
|
186
|
+
} else {
|
|
187
|
+
setValue(day)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const captionId = React.useId()
|
|
192
|
+
const monthLabel = React.useMemo(
|
|
193
|
+
() => new Intl.DateTimeFormat(activeLocale, { month: "long", year: "numeric" }).format(month),
|
|
194
|
+
[month, activeLocale],
|
|
195
|
+
)
|
|
196
|
+
// Weekday headers, generated for the active locale + first weekday.
|
|
197
|
+
const weekdays = React.useMemo(() => {
|
|
198
|
+
const short = new Intl.DateTimeFormat(activeLocale, { weekday: "short" })
|
|
199
|
+
const long = new Intl.DateTimeFormat(activeLocale, { weekday: "long" })
|
|
200
|
+
const sundayOffset = WEEKDAYS_REF.getDay() // 0
|
|
201
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
202
|
+
const d = addDays(WEEKDAYS_REF, (weekStart + i - sundayOffset + 7) % 7)
|
|
203
|
+
return { short: short.format(d), long: long.format(d) }
|
|
204
|
+
})
|
|
205
|
+
}, [activeLocale, weekStart])
|
|
206
|
+
const fullDate = React.useMemo(
|
|
207
|
+
() => new Intl.DateTimeFormat(activeLocale, { weekday: "long", year: "numeric", month: "long", day: "numeric" }),
|
|
208
|
+
[activeLocale],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const days = React.useMemo(() => monthMatrix(month, weekStart), [month, weekStart])
|
|
212
|
+
|
|
213
|
+
const prevDisabled = !!min && startOfMonth(min) > addMonths(month, -1)
|
|
214
|
+
const nextDisabled = !!max && startOfMonth(max) < addMonths(month, 1)
|
|
215
|
+
|
|
216
|
+
const onKeyDown = (e: React.KeyboardEvent) => {
|
|
217
|
+
const rtl = isRtl()
|
|
218
|
+
let next: Date | null = null
|
|
219
|
+
switch (e.key) {
|
|
220
|
+
case "ArrowLeft": next = addDays(focusedDate, rtl ? 1 : -1); break
|
|
221
|
+
case "ArrowRight": next = addDays(focusedDate, rtl ? -1 : 1); break
|
|
222
|
+
case "ArrowUp": next = addDays(focusedDate, -7); break
|
|
223
|
+
case "ArrowDown": next = addDays(focusedDate, 7); break
|
|
224
|
+
case "Home": next = addDays(focusedDate, -((focusedDate.getDay() - weekStart + 7) % 7)); break
|
|
225
|
+
case "End": next = addDays(focusedDate, 6 - ((focusedDate.getDay() - weekStart + 7) % 7)); break
|
|
226
|
+
case "PageUp": next = addMonths(focusedDate, e.shiftKey ? -12 : -1); break
|
|
227
|
+
case "PageDown": next = addMonths(focusedDate, e.shiftKey ? 12 : 1); break
|
|
228
|
+
case "Enter":
|
|
229
|
+
case " ": e.preventDefault(); select(focusedDate); return
|
|
230
|
+
default: return
|
|
231
|
+
}
|
|
232
|
+
e.preventDefault()
|
|
233
|
+
const clamped = startOfDay(next)
|
|
234
|
+
shouldFocus.current = true
|
|
235
|
+
setFocusedDate(clamped)
|
|
236
|
+
if (!isSameMonth(clamped, month)) setMonth(clamped)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div
|
|
241
|
+
ref={(node) => {
|
|
242
|
+
rootRef.current = node
|
|
243
|
+
if (typeof ref === "function") ref(node)
|
|
244
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
245
|
+
}}
|
|
246
|
+
className={cn("ax:inline-block ax:select-none ax:p-sm", className)}
|
|
247
|
+
{...props}
|
|
248
|
+
>
|
|
249
|
+
{/* Caption + month navigation */}
|
|
250
|
+
<div className="ax:flex ax:items-center ax:justify-between ax:gap-sm ax:pb-sm">
|
|
251
|
+
<NavButton label="Previous month" disabled={prevDisabled} onClick={() => setMonth(addMonths(month, -1))}>
|
|
252
|
+
<ChevronLeft className="ax:size-4 ax:rtl:-scale-x-100" />
|
|
253
|
+
</NavButton>
|
|
254
|
+
<div id={captionId} aria-live="polite" className="ax:t-label-md ax:text-foreground-strong">
|
|
255
|
+
{monthLabel}
|
|
256
|
+
</div>
|
|
257
|
+
<NavButton label="Next month" disabled={nextDisabled} onClick={() => setMonth(addMonths(month, 1))}>
|
|
258
|
+
<ChevronRight className="ax:size-4 ax:rtl:-scale-x-100" />
|
|
259
|
+
</NavButton>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Day grid */}
|
|
263
|
+
<div role="grid" aria-labelledby={captionId} className="ax:w-max">
|
|
264
|
+
<div role="row" className="ax:flex">
|
|
265
|
+
{weekdays.map((w) => (
|
|
266
|
+
<div
|
|
267
|
+
key={w.long}
|
|
268
|
+
role="columnheader"
|
|
269
|
+
aria-label={w.long}
|
|
270
|
+
className={cn(CELL, "ax:flex ax:items-center ax:justify-center ax:t-label-2xs ax:text-muted-foreground")}
|
|
271
|
+
>
|
|
272
|
+
{w.short}
|
|
273
|
+
</div>
|
|
274
|
+
))}
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
|
278
|
+
{Array.from({ length: 6 }, (_, w) => (
|
|
279
|
+
<div role="row" key={w} className="ax:flex">
|
|
280
|
+
{days.slice(w * 7, w * 7 + 7).map((day) => {
|
|
281
|
+
const outside = !isSameMonth(day, month)
|
|
282
|
+
if (outside && !showOutsideDays) return <div key={+day} role="gridcell" className={CELL} />
|
|
283
|
+
|
|
284
|
+
const disabled = dayDisabled(day)
|
|
285
|
+
const isToday = isSameDay(day, today)
|
|
286
|
+
const range = isRange(value) ? value : undefined
|
|
287
|
+
const selected =
|
|
288
|
+
mode === "range"
|
|
289
|
+
? isSameDay(day, range?.from) || isSameDay(day, range?.to)
|
|
290
|
+
: isSameDay(day, value as Date | undefined)
|
|
291
|
+
const inRange = !!range?.from && !!range?.to && isBetween(day, range.from, range.to)
|
|
292
|
+
const isRangeStart = mode === "range" && isSameDay(day, range?.from) && !!range?.to
|
|
293
|
+
const isRangeEnd = mode === "range" && isSameDay(day, range?.to)
|
|
294
|
+
const isFocusTarget = isSameDay(day, focusedDate)
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div
|
|
298
|
+
key={+day}
|
|
299
|
+
role="gridcell"
|
|
300
|
+
aria-selected={selected || undefined}
|
|
301
|
+
className={cn(
|
|
302
|
+
CELL,
|
|
303
|
+
"ax:relative ax:flex ax:items-center ax:justify-center",
|
|
304
|
+
// Continuous range highlight lives on the cell so it butts up edge-to-edge.
|
|
305
|
+
(inRange || isRangeStart || isRangeEnd) && "ax:bg-primary-weakest",
|
|
306
|
+
isRangeStart && "ax:rounded-s-xs",
|
|
307
|
+
isRangeEnd && "ax:rounded-e-xs",
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
<button
|
|
311
|
+
ref={isFocusTarget ? focusBtnRef : undefined}
|
|
312
|
+
type="button"
|
|
313
|
+
tabIndex={isFocusTarget ? 0 : -1}
|
|
314
|
+
disabled={disabled}
|
|
315
|
+
aria-label={fullDate.format(day)}
|
|
316
|
+
aria-current={isToday ? "date" : undefined}
|
|
317
|
+
onClick={() => { setFocusedDate(startOfDay(day)); if (outside) setMonth(day); select(day) }}
|
|
318
|
+
onKeyDown={onKeyDown}
|
|
319
|
+
className={cn(
|
|
320
|
+
"ax:flex ax:size-8 ax:items-center ax:justify-center ax:rounded-xs ax:t-body-sm",
|
|
321
|
+
"ax:transition-colors ax:transition-snappy",
|
|
322
|
+
"ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-1 ax:focus-visible:ring-offset-background",
|
|
323
|
+
"ax:disabled:pointer-events-none ax:disabled:opacity-40",
|
|
324
|
+
selected
|
|
325
|
+
? "ax:bg-primary-weakest ax:text-primary ax:font-medium ax:hover:bg-primary-weakest"
|
|
326
|
+
: cn(
|
|
327
|
+
"ax:hover:bg-muted",
|
|
328
|
+
outside ? "ax:text-muted-foreground/70" : "ax:text-foreground",
|
|
329
|
+
isToday && "ax:font-semibold ax:text-primary",
|
|
330
|
+
),
|
|
331
|
+
)}
|
|
332
|
+
>
|
|
333
|
+
{day.getDate()}
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
)
|
|
337
|
+
})}
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
)
|
|
343
|
+
},
|
|
344
|
+
)
|
|
345
|
+
Calendar.displayName = "Calendar"
|
|
346
|
+
|
|
347
|
+
// Icon-only month-nav control - plain button (no Button dep) with the DS focus ring.
|
|
348
|
+
function NavButton({ label, disabled, onClick, children }: {
|
|
349
|
+
label: string; disabled?: boolean; onClick: () => void; children: React.ReactNode
|
|
350
|
+
}) {
|
|
351
|
+
return (
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
aria-label={label}
|
|
355
|
+
disabled={disabled}
|
|
356
|
+
onClick={onClick}
|
|
357
|
+
className={cn(
|
|
358
|
+
"ax:flex ax:size-8 ax:items-center ax:justify-center ax:rounded-md ax:text-muted-foreground",
|
|
359
|
+
"ax:hover:bg-muted ax:hover:text-foreground ax:transition-colors ax:transition-snappy ax:cursor-pointer",
|
|
360
|
+
"ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-1 ax:focus-visible:ring-offset-background",
|
|
361
|
+
"ax:disabled:pointer-events-none ax:disabled:opacity-40",
|
|
362
|
+
)}
|
|
363
|
+
>
|
|
364
|
+
{children}
|
|
365
|
+
</button>
|
|
366
|
+
)
|
|
367
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Card> = {
|
|
7
|
+
title: "Components/Card",
|
|
8
|
+
component: Card,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof Card>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<Card className="ax:max-w-sm">
|
|
18
|
+
<CardHeader>
|
|
19
|
+
<CardTitle>Project overview</CardTitle>
|
|
20
|
+
<CardDescription>Track progress across your team.</CardDescription>
|
|
21
|
+
</CardHeader>
|
|
22
|
+
<CardContent>
|
|
23
|
+
<p>12 tasks completed this week.</p>
|
|
24
|
+
</CardContent>
|
|
25
|
+
<CardFooter>
|
|
26
|
+
<Button size="sm">View details</Button>
|
|
27
|
+
</CardFooter>
|
|
28
|
+
</Card>
|
|
29
|
+
),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Sectioned: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<Card sectioned className="ax:max-w-sm">
|
|
35
|
+
<CardHeader size="lg">
|
|
36
|
+
<CardTitle>Billing</CardTitle>
|
|
37
|
+
<CardDescription>Manage your subscription.</CardDescription>
|
|
38
|
+
</CardHeader>
|
|
39
|
+
<CardContent>
|
|
40
|
+
<p>Next invoice on Sep 1.</p>
|
|
41
|
+
</CardContent>
|
|
42
|
+
</Card>
|
|
43
|
+
),
|
|
44
|
+
}
|