@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.
- package/README.md +36 -26
- package/dist/full.css +3042 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +8 -1
- package/src/charts/BarChart.tsx +55 -0
- package/src/charts/DonutChart.tsx +127 -0
- package/src/charts/Sparkline.tsx +101 -0
- package/src/charts/index.ts +5 -0
- package/src/commerce/CartLine.tsx +84 -0
- package/src/commerce/CartSummary.tsx +70 -0
- package/src/commerce/FilterGroup.tsx +70 -0
- package/src/commerce/PriceTag.tsx +62 -0
- package/src/commerce/ProductCard.tsx +121 -0
- package/src/commerce/ProductGrid.tsx +27 -0
- package/src/commerce/QuantityStepper.tsx +59 -0
- package/src/commerce/index.ts +10 -0
- package/src/elements.tsx +167 -0
- package/src/index.ts +166 -0
- package/src/layout/AppShell.tsx +102 -0
- package/src/layout/ChristmasBackground.tsx +177 -0
- package/src/layout/EffectsToggle.tsx +41 -0
- package/src/layout/NavGroup.tsx +45 -0
- package/src/layout/SnowScenery.tsx +76 -0
- package/src/layout/SpaceBackground.tsx +459 -0
- package/src/layout/ThemeToggle.tsx +40 -0
- package/src/layout/ThemedScenery.tsx +179 -0
- package/src/layout/sceneEffects.ts +49 -0
- package/src/lib/cn.ts +17 -0
- package/src/lib/effects.ts +71 -0
- package/src/lib/media.ts +27 -0
- package/src/lib/motion.ts +191 -0
- package/src/lib/theme.ts +113 -0
- package/src/lib/virtual.ts +33 -0
- package/src/styles/space.css +510 -0
- package/src/styles/tokens.css +213 -0
- package/src/themes/index.ts +102 -0
- package/src/themes/palettes.ts +368 -0
- package/src/ui/Accordion.tsx +65 -0
- package/src/ui/Affix.tsx +72 -0
- package/src/ui/Alert.tsx +55 -0
- package/src/ui/AlertDialog.tsx +58 -0
- package/src/ui/Anchor.tsx +100 -0
- package/src/ui/Avatar.tsx +34 -0
- package/src/ui/AvatarGroup.tsx +55 -0
- package/src/ui/BackToTop.tsx +51 -0
- package/src/ui/Badge.tsx +46 -0
- package/src/ui/BottomNavigation.tsx +65 -0
- package/src/ui/Breadcrumb.tsx +64 -0
- package/src/ui/Button.tsx +49 -0
- package/src/ui/Calendar.tsx +40 -0
- package/src/ui/CalendarHeatmap.tsx +180 -0
- package/src/ui/Card.tsx +84 -0
- package/src/ui/Carousel.tsx +121 -0
- package/src/ui/Cascader.tsx +158 -0
- package/src/ui/Checkbox.tsx +35 -0
- package/src/ui/Clock.tsx +221 -0
- package/src/ui/Collapse.tsx +48 -0
- package/src/ui/ColorPicker.tsx +89 -0
- package/src/ui/Combobox.tsx +67 -0
- package/src/ui/Command.tsx +158 -0
- package/src/ui/Comment.tsx +91 -0
- package/src/ui/ContextMenu.tsx +58 -0
- package/src/ui/Countdown.tsx +123 -0
- package/src/ui/DataGrid.tsx +177 -0
- package/src/ui/DateField.tsx +177 -0
- package/src/ui/DateRangePicker.tsx +204 -0
- package/src/ui/DateTimeField.tsx +68 -0
- package/src/ui/Descriptions.tsx +56 -0
- package/src/ui/Drawer.tsx +72 -0
- package/src/ui/Dropdown.tsx +71 -0
- package/src/ui/Dropzone.tsx +88 -0
- package/src/ui/Empty.tsx +53 -0
- package/src/ui/FileUpload.tsx +198 -0
- package/src/ui/FloatingActionButton.tsx +48 -0
- package/src/ui/Form.tsx +108 -0
- package/src/ui/Highlight.tsx +53 -0
- package/src/ui/HoverCard.tsx +42 -0
- package/src/ui/Image.tsx +66 -0
- package/src/ui/Input.tsx +38 -0
- package/src/ui/Kbd.tsx +25 -0
- package/src/ui/List.tsx +71 -0
- package/src/ui/Marquee.tsx +48 -0
- package/src/ui/Mentions.tsx +170 -0
- package/src/ui/Meter.tsx +57 -0
- package/src/ui/Modal.tsx +106 -0
- package/src/ui/MultiSelect.tsx +236 -0
- package/src/ui/NotificationCenter.tsx +103 -0
- package/src/ui/NumberInput.tsx +48 -0
- package/src/ui/PageHeader.tsx +60 -0
- package/src/ui/Pagination.tsx +62 -0
- package/src/ui/Popover.tsx +42 -0
- package/src/ui/Portal.tsx +34 -0
- package/src/ui/Progress.tsx +55 -0
- package/src/ui/RadioGroup.tsx +65 -0
- package/src/ui/Rating.tsx +98 -0
- package/src/ui/Result.tsx +75 -0
- package/src/ui/RingProgress.tsx +75 -0
- package/src/ui/SegmentedControl.tsx +61 -0
- package/src/ui/Select.tsx +55 -0
- package/src/ui/Separator.tsx +39 -0
- package/src/ui/Skeleton.tsx +23 -0
- package/src/ui/Slider.tsx +57 -0
- package/src/ui/Sortable.tsx +174 -0
- package/src/ui/SpeedDial.tsx +75 -0
- package/src/ui/Spinner.tsx +33 -0
- package/src/ui/Splitter.tsx +115 -0
- package/src/ui/Stat.tsx +74 -0
- package/src/ui/Stepper.tsx +134 -0
- package/src/ui/Switch.tsx +44 -0
- package/src/ui/Table.tsx +128 -0
- package/src/ui/Tabs.tsx +72 -0
- package/src/ui/TagInput.tsx +85 -0
- package/src/ui/Textarea.tsx +38 -0
- package/src/ui/TimeField.tsx +298 -0
- package/src/ui/Timeline.tsx +89 -0
- package/src/ui/Toast.tsx +68 -0
- package/src/ui/Toggle.tsx +41 -0
- package/src/ui/ToggleGroup.tsx +59 -0
- package/src/ui/Tooltip.tsx +42 -0
- package/src/ui/Tour.tsx +197 -0
- package/src/ui/Transfer.tsx +112 -0
- package/src/ui/Tree.tsx +100 -0
- package/src/ui/TreeSelect.tsx +151 -0
- package/src/ui/VirtualList.tsx +81 -0
- 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
|
+
}
|