@a4ui/core 0.11.1 → 0.12.0

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.
Files changed (126) hide show
  1. package/README.md +36 -26
  2. package/dist/full.css +3042 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +8 -1
  6. package/src/charts/BarChart.tsx +55 -0
  7. package/src/charts/DonutChart.tsx +127 -0
  8. package/src/charts/Sparkline.tsx +101 -0
  9. package/src/charts/index.ts +5 -0
  10. package/src/commerce/CartLine.tsx +84 -0
  11. package/src/commerce/CartSummary.tsx +70 -0
  12. package/src/commerce/FilterGroup.tsx +70 -0
  13. package/src/commerce/PriceTag.tsx +62 -0
  14. package/src/commerce/ProductCard.tsx +121 -0
  15. package/src/commerce/ProductGrid.tsx +27 -0
  16. package/src/commerce/QuantityStepper.tsx +59 -0
  17. package/src/commerce/index.ts +10 -0
  18. package/src/elements.tsx +167 -0
  19. package/src/index.ts +166 -0
  20. package/src/layout/AppShell.tsx +102 -0
  21. package/src/layout/ChristmasBackground.tsx +177 -0
  22. package/src/layout/EffectsToggle.tsx +41 -0
  23. package/src/layout/NavGroup.tsx +45 -0
  24. package/src/layout/SnowScenery.tsx +76 -0
  25. package/src/layout/SpaceBackground.tsx +459 -0
  26. package/src/layout/ThemeToggle.tsx +40 -0
  27. package/src/layout/ThemedScenery.tsx +179 -0
  28. package/src/layout/sceneEffects.ts +49 -0
  29. package/src/lib/cn.ts +17 -0
  30. package/src/lib/effects.ts +71 -0
  31. package/src/lib/media.ts +27 -0
  32. package/src/lib/motion.ts +191 -0
  33. package/src/lib/theme.ts +113 -0
  34. package/src/lib/virtual.ts +33 -0
  35. package/src/styles/space.css +510 -0
  36. package/src/styles/tokens.css +213 -0
  37. package/src/themes/index.ts +102 -0
  38. package/src/themes/palettes.ts +368 -0
  39. package/src/ui/Accordion.tsx +65 -0
  40. package/src/ui/Affix.tsx +72 -0
  41. package/src/ui/Alert.tsx +55 -0
  42. package/src/ui/AlertDialog.tsx +58 -0
  43. package/src/ui/Anchor.tsx +100 -0
  44. package/src/ui/Avatar.tsx +34 -0
  45. package/src/ui/AvatarGroup.tsx +55 -0
  46. package/src/ui/BackToTop.tsx +51 -0
  47. package/src/ui/Badge.tsx +46 -0
  48. package/src/ui/BottomNavigation.tsx +65 -0
  49. package/src/ui/Breadcrumb.tsx +64 -0
  50. package/src/ui/Button.tsx +49 -0
  51. package/src/ui/Calendar.tsx +40 -0
  52. package/src/ui/CalendarHeatmap.tsx +180 -0
  53. package/src/ui/Card.tsx +84 -0
  54. package/src/ui/Carousel.tsx +121 -0
  55. package/src/ui/Cascader.tsx +158 -0
  56. package/src/ui/Checkbox.tsx +35 -0
  57. package/src/ui/Clock.tsx +221 -0
  58. package/src/ui/Collapse.tsx +48 -0
  59. package/src/ui/ColorPicker.tsx +89 -0
  60. package/src/ui/Combobox.tsx +67 -0
  61. package/src/ui/Command.tsx +158 -0
  62. package/src/ui/Comment.tsx +91 -0
  63. package/src/ui/ContextMenu.tsx +58 -0
  64. package/src/ui/Countdown.tsx +123 -0
  65. package/src/ui/DataGrid.tsx +177 -0
  66. package/src/ui/DateField.tsx +177 -0
  67. package/src/ui/DateRangePicker.tsx +204 -0
  68. package/src/ui/DateTimeField.tsx +68 -0
  69. package/src/ui/Descriptions.tsx +56 -0
  70. package/src/ui/Drawer.tsx +72 -0
  71. package/src/ui/Dropdown.tsx +71 -0
  72. package/src/ui/Dropzone.tsx +88 -0
  73. package/src/ui/Empty.tsx +53 -0
  74. package/src/ui/FileUpload.tsx +198 -0
  75. package/src/ui/FloatingActionButton.tsx +48 -0
  76. package/src/ui/Form.tsx +108 -0
  77. package/src/ui/Highlight.tsx +53 -0
  78. package/src/ui/HoverCard.tsx +42 -0
  79. package/src/ui/Image.tsx +66 -0
  80. package/src/ui/Input.tsx +38 -0
  81. package/src/ui/Kbd.tsx +25 -0
  82. package/src/ui/List.tsx +71 -0
  83. package/src/ui/Marquee.tsx +48 -0
  84. package/src/ui/Mentions.tsx +170 -0
  85. package/src/ui/Meter.tsx +57 -0
  86. package/src/ui/Modal.tsx +106 -0
  87. package/src/ui/MultiSelect.tsx +236 -0
  88. package/src/ui/NotificationCenter.tsx +103 -0
  89. package/src/ui/NumberInput.tsx +48 -0
  90. package/src/ui/PageHeader.tsx +60 -0
  91. package/src/ui/Pagination.tsx +62 -0
  92. package/src/ui/Popover.tsx +42 -0
  93. package/src/ui/Portal.tsx +34 -0
  94. package/src/ui/Progress.tsx +55 -0
  95. package/src/ui/RadioGroup.tsx +65 -0
  96. package/src/ui/Rating.tsx +98 -0
  97. package/src/ui/Result.tsx +75 -0
  98. package/src/ui/RingProgress.tsx +75 -0
  99. package/src/ui/SegmentedControl.tsx +61 -0
  100. package/src/ui/Select.tsx +55 -0
  101. package/src/ui/Separator.tsx +39 -0
  102. package/src/ui/Skeleton.tsx +23 -0
  103. package/src/ui/Slider.tsx +57 -0
  104. package/src/ui/Sortable.tsx +174 -0
  105. package/src/ui/SpeedDial.tsx +75 -0
  106. package/src/ui/Spinner.tsx +33 -0
  107. package/src/ui/Splitter.tsx +115 -0
  108. package/src/ui/Stat.tsx +74 -0
  109. package/src/ui/Stepper.tsx +134 -0
  110. package/src/ui/Switch.tsx +44 -0
  111. package/src/ui/Table.tsx +128 -0
  112. package/src/ui/Tabs.tsx +72 -0
  113. package/src/ui/TagInput.tsx +85 -0
  114. package/src/ui/Textarea.tsx +38 -0
  115. package/src/ui/TimeField.tsx +298 -0
  116. package/src/ui/Timeline.tsx +89 -0
  117. package/src/ui/Toast.tsx +68 -0
  118. package/src/ui/Toggle.tsx +41 -0
  119. package/src/ui/ToggleGroup.tsx +59 -0
  120. package/src/ui/Tooltip.tsx +42 -0
  121. package/src/ui/Tour.tsx +197 -0
  122. package/src/ui/Transfer.tsx +112 -0
  123. package/src/ui/Tree.tsx +100 -0
  124. package/src/ui/TreeSelect.tsx +151 -0
  125. package/src/ui/VirtualList.tsx +81 -0
  126. package/src/ui/internal/CalendarCore.tsx +276 -0
@@ -0,0 +1,276 @@
1
+ // Shared month-grid calendar panel used by both Calendar (always visible) and
2
+ // DateField (inside a popover). Speaks `Date`. Beyond the day grid it adds fast
3
+ // navigation: single chevrons step the month, double chevrons jump a year, and
4
+ // the month/year in the header drill down into a month picker and a paged
5
+ // year grid — so reaching a distant date is a couple of clicks, not dozens of
6
+ // month steps. Plain Solid + theme tokens (works in light/dark).
7
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-solid'
8
+ import type { JSX } from 'solid-js'
9
+ import { createMemo, createSignal, For, Show } from 'solid-js'
10
+
11
+ import { cn } from '../../lib/cn'
12
+
13
+ const MONTHS_FULL = [
14
+ 'January',
15
+ 'February',
16
+ 'March',
17
+ 'April',
18
+ 'May',
19
+ 'June',
20
+ 'July',
21
+ 'August',
22
+ 'September',
23
+ 'October',
24
+ 'November',
25
+ 'December',
26
+ ]
27
+ const WEEKDAYS_SUN = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
28
+
29
+ const YEARS_PER_PAGE = 12
30
+
31
+ export interface CalendarCoreProps {
32
+ /** Currently selected day (for highlight); undefined = none. */
33
+ selected?: Date
34
+ /** Range start — endpoints fill, days strictly between get a soft band. */
35
+ rangeStart?: Date
36
+ /** Range end (see `rangeStart`). */
37
+ rangeEnd?: Date
38
+ /** Called with the newly picked day. */
39
+ onPick: (date: Date) => void
40
+ /** 0 = Sunday-first, 1 = Monday-first. @default 0 */
41
+ weekStart?: 0 | 1
42
+ /** Full month names, January…December order (12). */
43
+ months?: string[]
44
+ /** Weekday headers in DISPLAY order, matching `weekStart` (7). */
45
+ weekdays?: string[]
46
+ /** Month first shown (its year/month seed the view). @default today */
47
+ initialView?: Date
48
+ class?: string
49
+ }
50
+
51
+ function sameDay(a: Date, b: Date): boolean {
52
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
53
+ }
54
+
55
+ /**
56
+ * The interactive calendar body: a header (year/month jumps + drill-down) and a
57
+ * grid that switches between day, month, and year views. Stateless about
58
+ * open/closed — the caller decides where it lives.
59
+ */
60
+ export function CalendarCore(props: CalendarCoreProps): JSX.Element {
61
+ const months = () => props.months ?? MONTHS_FULL
62
+ const weekdays = () => props.weekdays ?? WEEKDAYS_SUN
63
+ const weekStart = () => props.weekStart ?? 0
64
+
65
+ // eslint-disable-next-line solid/reactivity -- seed the view once; the user navigates from here
66
+ const seed = props.initialView ?? props.selected ?? new Date()
67
+ const [viewYear, setViewYear] = createSignal(seed.getFullYear())
68
+ const [viewMonth, setViewMonth] = createSignal(seed.getMonth())
69
+ const [mode, setMode] = createSignal<'days' | 'months' | 'years'>('days')
70
+ // Start of the 12-year window shown in the year view.
71
+ const [yearBase, setYearBase] = createSignal(seed.getFullYear() - (seed.getFullYear() % YEARS_PER_PAGE))
72
+
73
+ const stepMonth = (delta: number) => {
74
+ let m = viewMonth() + delta
75
+ let y = viewYear()
76
+ while (m < 0) {
77
+ m += 12
78
+ y -= 1
79
+ }
80
+ while (m > 11) {
81
+ m -= 12
82
+ y += 1
83
+ }
84
+ setViewMonth(m)
85
+ setViewYear(y)
86
+ }
87
+ const stepYear = (delta: number) => setViewYear(viewYear() + delta)
88
+
89
+ // 6×7 grid of dates starting on the first `weekStart` day on/before the 1st.
90
+ const cells = createMemo(() => {
91
+ const first = new Date(viewYear(), viewMonth(), 1)
92
+ const offset = (first.getDay() - weekStart() + 7) % 7
93
+ const start = new Date(viewYear(), viewMonth(), 1 - offset)
94
+ const out: Date[] = []
95
+ for (let i = 0; i < 42; i++) {
96
+ out.push(new Date(start.getFullYear(), start.getMonth(), start.getDate() + i))
97
+ }
98
+ return out
99
+ })
100
+
101
+ const years = createMemo(() => {
102
+ const base = yearBase()
103
+ return Array.from({ length: YEARS_PER_PAGE }, (_, i) => base + i)
104
+ })
105
+
106
+ const today = new Date()
107
+
108
+ const navBtn = 'grid h-7 w-7 place-items-center rounded-md text-foreground transition-colors hover:bg-muted'
109
+ const headerLabel =
110
+ 'rounded-md px-2 py-1 text-sm font-medium capitalize text-foreground transition-colors hover:bg-muted'
111
+
112
+ const openYears = () => {
113
+ setYearBase(viewYear() - (viewYear() % YEARS_PER_PAGE))
114
+ setMode('years')
115
+ }
116
+
117
+ return (
118
+ <div class={cn('w-full select-none', props.class)}>
119
+ {/* Header */}
120
+ <Show
121
+ when={mode() === 'days'}
122
+ fallback={
123
+ <div class="mb-2 flex items-center justify-between">
124
+ <button
125
+ type="button"
126
+ class={navBtn}
127
+ aria-label="Previous"
128
+ onClick={() => (mode() === 'months' ? stepYear(-1) : setYearBase(yearBase() - YEARS_PER_PAGE))}
129
+ >
130
+ <ChevronLeft class="h-4 w-4" />
131
+ </button>
132
+ <button type="button" class={headerLabel} onClick={() => setMode('days')}>
133
+ <Show when={mode() === 'months'} fallback={`${years()[0]} – ${years()[YEARS_PER_PAGE - 1]}`}>
134
+ {viewYear()}
135
+ </Show>
136
+ </button>
137
+ <button
138
+ type="button"
139
+ class={navBtn}
140
+ aria-label="Next"
141
+ onClick={() => (mode() === 'months' ? stepYear(1) : setYearBase(yearBase() + YEARS_PER_PAGE))}
142
+ >
143
+ <ChevronRight class="h-4 w-4" />
144
+ </button>
145
+ </div>
146
+ }
147
+ >
148
+ <div class="mb-2 flex items-center justify-between gap-1">
149
+ <div class="flex items-center">
150
+ <button type="button" class={navBtn} aria-label="Previous year" onClick={() => stepYear(-1)}>
151
+ <ChevronsLeft class="h-4 w-4" />
152
+ </button>
153
+ <button type="button" class={navBtn} aria-label="Previous month" onClick={() => stepMonth(-1)}>
154
+ <ChevronLeft class="h-4 w-4" />
155
+ </button>
156
+ </div>
157
+ <div class="flex items-center gap-0.5">
158
+ <button type="button" class={headerLabel} onClick={() => setMode('months')}>
159
+ {months()[viewMonth()]}
160
+ </button>
161
+ <button type="button" class={headerLabel} onClick={openYears}>
162
+ {viewYear()}
163
+ </button>
164
+ </div>
165
+ <div class="flex items-center">
166
+ <button type="button" class={navBtn} aria-label="Next month" onClick={() => stepMonth(1)}>
167
+ <ChevronRight class="h-4 w-4" />
168
+ </button>
169
+ <button type="button" class={navBtn} aria-label="Next year" onClick={() => stepYear(1)}>
170
+ <ChevronsRight class="h-4 w-4" />
171
+ </button>
172
+ </div>
173
+ </div>
174
+ </Show>
175
+
176
+ {/* Body */}
177
+ <Show when={mode() === 'days'}>
178
+ <div class="mb-1 grid grid-cols-7 gap-0.5 text-center text-[11px] font-medium text-muted-foreground">
179
+ <For each={weekdays()}>{(w) => <span>{w}</span>}</For>
180
+ </div>
181
+ <div class="grid grid-cols-7 gap-0.5">
182
+ <For each={cells()}>
183
+ {(d) => {
184
+ const inMonth = d.getMonth() === viewMonth()
185
+ const isEndpoint = () =>
186
+ (props.rangeStart ? sameDay(props.rangeStart, d) : false) ||
187
+ (props.rangeEnd ? sameDay(props.rangeEnd, d) : false)
188
+ const isBetween = () =>
189
+ !!props.rangeStart &&
190
+ !!props.rangeEnd &&
191
+ d > props.rangeStart &&
192
+ d < props.rangeEnd &&
193
+ !isEndpoint()
194
+ // Endpoints and a single `selected` day both render as filled.
195
+ const isSel = () => (props.selected ? sameDay(props.selected, d) : false) || isEndpoint()
196
+ const isToday = sameDay(today, d)
197
+ // No `title` here: some engines promote it to the accessible name,
198
+ // which would shadow the day number the day cells are located by.
199
+ return (
200
+ <button
201
+ type="button"
202
+ aria-pressed={isSel()}
203
+ onClick={() => props.onPick(d)}
204
+ class={cn(
205
+ 'flex h-8 items-center justify-center rounded-md text-sm transition-colors',
206
+ isSel() ? 'bg-primary font-semibold text-primary-foreground' : 'hover:bg-muted',
207
+ !isSel() && isBetween() && 'bg-primary/20 text-foreground',
208
+ !isSel() && !isBetween() && !inMonth && 'text-muted-foreground/50',
209
+ !isSel() && !isBetween() && inMonth && 'text-foreground',
210
+ !isSel() && isToday && 'ring-1 ring-inset ring-primary',
211
+ )}
212
+ >
213
+ {d.getDate()}
214
+ </button>
215
+ )
216
+ }}
217
+ </For>
218
+ </div>
219
+ </Show>
220
+
221
+ <Show when={mode() === 'months'}>
222
+ <div class="grid grid-cols-3 gap-1">
223
+ <For each={months()}>
224
+ {(name, i) => {
225
+ const isCurrent = () => i() === viewMonth()
226
+ return (
227
+ <button
228
+ type="button"
229
+ onClick={() => {
230
+ setViewMonth(i())
231
+ setMode('days')
232
+ }}
233
+ class={cn(
234
+ 'rounded-md py-2 text-sm capitalize transition-colors',
235
+ isCurrent()
236
+ ? 'bg-primary font-semibold text-primary-foreground'
237
+ : 'text-foreground hover:bg-muted',
238
+ )}
239
+ >
240
+ {name.slice(0, 3)}
241
+ </button>
242
+ )
243
+ }}
244
+ </For>
245
+ </div>
246
+ </Show>
247
+
248
+ <Show when={mode() === 'years'}>
249
+ <div class="grid grid-cols-3 gap-1">
250
+ <For each={years()}>
251
+ {(y) => {
252
+ const isCurrent = () => y === viewYear()
253
+ return (
254
+ <button
255
+ type="button"
256
+ onClick={() => {
257
+ setViewYear(y)
258
+ setMode('months')
259
+ }}
260
+ class={cn(
261
+ 'rounded-md py-2 text-sm transition-colors',
262
+ isCurrent()
263
+ ? 'bg-primary font-semibold text-primary-foreground'
264
+ : 'text-foreground hover:bg-muted',
265
+ )}
266
+ >
267
+ {y}
268
+ </button>
269
+ )
270
+ }}
271
+ </For>
272
+ </div>
273
+ </Show>
274
+ </div>
275
+ )
276
+ }