@adea-ai/ui 0.99.1 → 0.99.3
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/components/layout/orbit-layout/orbit-layout.d.ts +5 -1
- package/dist/components/layout/orbit-layout/orbit-layout.d.ts.map +1 -1
- package/dist/components/layout/orbit-layout/orbit-layout.js.map +1 -1
- package/dist/components/layout/side-rail/side-rail.d.ts +4 -1
- package/dist/components/layout/side-rail/side-rail.d.ts.map +1 -1
- package/dist/components/layout/side-rail/side-rail.js +58 -64
- package/dist/components/layout/side-rail/side-rail.js.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.js +2 -2
- package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
- package/dist/components/motion/presence.d.ts +17 -7
- package/dist/components/motion/presence.d.ts.map +1 -1
- package/dist/components/motion/presence.js.map +1 -1
- package/dist/components/theme/theme-picker.d.ts.map +1 -1
- package/dist/components/theme/theme-picker.js +1 -1
- package/dist/components/theme/theme-picker.js.map +1 -1
- package/dist/components/theme/theme-provider.d.ts.map +1 -1
- package/dist/components/theme/theme-provider.js +15 -4
- package/dist/components/theme/theme-provider.js.map +1 -1
- package/dist/components/ui/aspect-ratio/aspect-ratio.d.ts +2 -2
- package/dist/components/ui/aspect-ratio/aspect-ratio.d.ts.map +1 -1
- package/dist/components/ui/aspect-ratio/aspect-ratio.js +15 -1
- package/dist/components/ui/aspect-ratio/aspect-ratio.js.map +1 -1
- package/dist/components/ui/badge/badge.d.ts +1 -1
- package/dist/components/ui/button/button.d.ts +1 -1
- package/dist/components/ui/calendar/calendar.d.ts.map +1 -1
- package/dist/components/ui/calendar/calendar.js +1 -1
- package/dist/components/ui/calendar/calendar.js.map +1 -1
- package/dist/components/ui/card/card.d.ts +2 -2
- package/dist/components/ui/card/card.js +2 -2
- package/dist/components/ui/card/card.js.map +1 -1
- package/dist/components/ui/chart/chart-options.d.ts +50 -0
- package/dist/components/ui/chart/chart-options.d.ts.map +1 -0
- package/dist/components/ui/chart/chart-options.js +178 -0
- package/dist/components/ui/chart/chart-options.js.map +1 -0
- package/dist/components/ui/chart/chart.d.ts +21 -18
- package/dist/components/ui/chart/chart.d.ts.map +1 -1
- package/dist/components/ui/chart/chart.js +175 -155
- package/dist/components/ui/chart/chart.js.map +1 -1
- package/dist/components/ui/chart/index.js +2 -1
- package/dist/components/ui/context-menu/context-menu.d.ts +8 -0
- package/dist/components/ui/context-menu/context-menu.d.ts.map +1 -1
- package/dist/components/ui/context-menu/context-menu.js +8 -0
- package/dist/components/ui/context-menu/context-menu.js.map +1 -1
- package/dist/components/ui/drawer/drawer.d.ts +8 -1
- package/dist/components/ui/drawer/drawer.d.ts.map +1 -1
- package/dist/components/ui/drawer/drawer.js.map +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu.js +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/components/ui/input-otp/input-otp.d.ts.map +1 -1
- package/dist/components/ui/input-otp/input-otp.js +2 -2
- package/dist/components/ui/input-otp/input-otp.js.map +1 -1
- package/dist/components/ui/menubar/menubar.d.ts +8 -0
- package/dist/components/ui/menubar/menubar.d.ts.map +1 -1
- package/dist/components/ui/menubar/menubar.js +8 -0
- package/dist/components/ui/menubar/menubar.js.map +1 -1
- package/dist/components/ui/navigation-menu/navigation-menu.d.ts +14 -2
- package/dist/components/ui/navigation-menu/navigation-menu.d.ts.map +1 -1
- package/dist/components/ui/navigation-menu/navigation-menu.js +28 -23
- package/dist/components/ui/navigation-menu/navigation-menu.js.map +1 -1
- package/dist/components/ui/pagination/pagination.d.ts.map +1 -1
- package/dist/components/ui/pagination/pagination.js +1 -1
- package/dist/components/ui/pagination/pagination.js.map +1 -1
- package/dist/components/ui/resizable/resizable.d.ts +0 -12
- package/dist/components/ui/resizable/resizable.d.ts.map +1 -1
- package/dist/components/ui/resizable/resizable.js +43 -9
- package/dist/components/ui/resizable/resizable.js.map +1 -1
- package/dist/components/ui/toast/toast.d.ts.map +1 -1
- package/dist/components/ui/toast/toast.js +19 -6
- package/dist/components/ui/toast/toast.js.map +1 -1
- package/dist/lib/cn-tables.generated.js +10 -10
- package/dist/lib/cn-tables.generated.js.map +1 -1
- package/dist/r/chart.json +5 -0
- package/dist/r/registry.json +5 -0
- package/dist/r/src/components/layout/orbit-layout/orbit-layout.tsx +5 -1
- package/dist/r/src/components/layout/side-rail/side-rail.tsx +29 -16
- package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +2 -2
- package/dist/r/src/components/motion/presence.tsx +17 -7
- package/dist/r/src/components/theme/theme-picker.tsx +5 -1
- package/dist/r/src/components/theme/theme-provider.tsx +19 -9
- package/dist/r/src/components/ui/aspect-ratio/aspect-ratio.tsx +14 -3
- package/dist/r/src/components/ui/calendar/calendar.tsx +6 -1
- package/dist/r/src/components/ui/card/card.tsx +2 -2
- package/dist/r/src/components/ui/chart/chart-options.ts +196 -0
- package/dist/r/src/components/ui/chart/chart.tsx +149 -101
- package/dist/r/src/components/ui/context-menu/context-menu.tsx +8 -0
- package/dist/r/src/components/ui/drawer/drawer.tsx +8 -1
- package/dist/r/src/components/ui/dropdown-menu/dropdown-menu.tsx +8 -0
- package/dist/r/src/components/ui/input-otp/input-otp.tsx +14 -2
- package/dist/r/src/components/ui/menubar/menubar.tsx +8 -0
- package/dist/r/src/components/ui/navigation-menu/navigation-menu.tsx +42 -28
- package/dist/r/src/components/ui/pagination/pagination.tsx +8 -1
- package/dist/r/src/components/ui/resizable/resizable.tsx +46 -8
- package/dist/r/src/components/ui/toast/toast.tsx +21 -6
- package/dist/r/src/lib/cn-tables.generated.ts +10 -10
- package/dist/r/src/styles/base.css +4 -1
- package/dist/r/src/styles/theme.css +23 -43
- package/package.json +1 -1
- package/registry.json +5 -0
- package/src/components/layout/orbit-layout/orbit-layout.tsx +5 -1
- package/src/components/layout/side-rail/side-rail.tsx +29 -16
- package/src/components/layout/sidebar-nav/sidebar-nav.tsx +2 -2
- package/src/components/motion/presence.tsx +17 -7
- package/src/components/theme/theme-picker.tsx +5 -1
- package/src/components/theme/theme-provider.tsx +19 -9
- package/src/components/ui/aspect-ratio/aspect-ratio.tsx +14 -3
- package/src/components/ui/calendar/calendar.tsx +6 -1
- package/src/components/ui/card/card.tsx +2 -2
- package/src/components/ui/chart/chart-options.ts +196 -0
- package/src/components/ui/chart/chart.tsx +149 -101
- package/src/components/ui/context-menu/context-menu.tsx +8 -0
- package/src/components/ui/drawer/drawer.tsx +8 -1
- package/src/components/ui/dropdown-menu/dropdown-menu.tsx +8 -0
- package/src/components/ui/input-otp/input-otp.tsx +14 -2
- package/src/components/ui/menubar/menubar.tsx +8 -0
- package/src/components/ui/navigation-menu/navigation-menu.tsx +42 -28
- package/src/components/ui/pagination/pagination.tsx +8 -1
- package/src/components/ui/resizable/resizable.tsx +46 -8
- package/src/components/ui/toast/toast.tsx +21 -6
- package/src/lib/cn-tables.generated.ts +10 -10
- package/src/styles/base.css +4 -1
- package/src/styles/theme.css +23 -43
|
@@ -60,7 +60,11 @@ export function OrbitLayout(props: OrbitLayoutProps) {
|
|
|
60
60
|
export type OrbitItemProps = Omit<ComponentProps<'div'>, 'style'> & {
|
|
61
61
|
/** JSX style props are owned by this geometry primitive. */
|
|
62
62
|
style?: never
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Zero-based position in the rendered sequence. Index 0 sits at three o'clock
|
|
65
|
+
* (the positive x axis) and the sequence runs clockwise, because the y axis
|
|
66
|
+
* points down the screen: with four items, 0 is east, 1 south, 2 west, 3 north.
|
|
67
|
+
*/
|
|
64
68
|
index: number
|
|
65
69
|
/** Number of items sharing this orbit. */
|
|
66
70
|
count: number
|
|
@@ -205,7 +205,10 @@ export type SideRailItemProps<T extends ValidComponent = 'a'> = PolymorphicProps
|
|
|
205
205
|
* being deleted.
|
|
206
206
|
*/
|
|
207
207
|
label: string
|
|
208
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* A count or a dot. Expanded, it trails the label at the row's end; collapsed,
|
|
210
|
+
* it moves onto the icon's corner, so the rail never loses it.
|
|
211
|
+
*/
|
|
209
212
|
badge?: JSX.Element
|
|
210
213
|
/** An indicator pinned to the trailing edge in the expanded form. */
|
|
211
214
|
trailing?: JSX.Element
|
|
@@ -258,6 +261,7 @@ export function SideRailItem<T extends ValidComponent = 'a'>(props: SideRailItem
|
|
|
258
261
|
data-active={local.active ? '' : undefined}
|
|
259
262
|
class={cn(
|
|
260
263
|
sideRailItemClass,
|
|
264
|
+
'relative',
|
|
261
265
|
local.active ? sideRailItemStateClass.active : sideRailItemStateClass.idle,
|
|
262
266
|
local.class
|
|
263
267
|
)}
|
|
@@ -267,24 +271,33 @@ export function SideRailItem<T extends ValidComponent = 'a'>(props: SideRailItem
|
|
|
267
271
|
onBlur={hideTip}
|
|
268
272
|
{...(rest as Record<string, unknown>)}
|
|
269
273
|
>
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<span class="absolute -top-1 -end-1 flex items-center justify-center">
|
|
274
|
-
{local.badge}
|
|
275
|
-
</span>
|
|
276
|
-
</span>
|
|
277
|
-
</Show>
|
|
278
|
-
<Show when={!local.badge}>
|
|
279
|
-
<span class="flex size-5 shrink-0 items-center justify-center [&_svg]:size-5">
|
|
280
|
-
{local.children}
|
|
281
|
-
</span>
|
|
282
|
-
</Show>
|
|
274
|
+
<span class="flex size-5 shrink-0 items-center justify-center [&_svg]:size-5">
|
|
275
|
+
{local.children}
|
|
276
|
+
</span>
|
|
283
277
|
|
|
284
|
-
<span class="min-w-0 flex-1 truncate group-data-[collapsed=true]/rail:sr-only">
|
|
278
|
+
<span class="min-w-0 flex-1 truncate text-start group-data-[collapsed=true]/rail:sr-only">
|
|
285
279
|
{local.label}
|
|
286
280
|
</span>
|
|
287
281
|
|
|
282
|
+
{/*
|
|
283
|
+
* One element, two placements, because the badge is caller content and a DOM
|
|
284
|
+
* node cannot be in two places. Expanded, it is a trailing count in the row's
|
|
285
|
+
* flow, after the label — over the icon it would cover the glyph the row is
|
|
286
|
+
* recognised by. Collapsed, there is no label to trail, so it leaves the flow
|
|
287
|
+
* and pins to the icon's top-end corner: anchored to the row's centre (where
|
|
288
|
+
* the centred icon sits) and pushed out by 14px, the icon's half-width plus
|
|
289
|
+
* the 4px overhang, so a wider count grows back over the icon rather than past
|
|
290
|
+
* the rail's edge.
|
|
291
|
+
*/}
|
|
292
|
+
<Show when={local.badge}>
|
|
293
|
+
<span
|
|
294
|
+
data-slot="side-rail-badge"
|
|
295
|
+
class="flex shrink-0 items-center justify-center group-data-[collapsed=true]/rail:absolute group-data-[collapsed=true]/rail:end-1/2 group-data-[collapsed=true]/rail:top-1/2 group-data-[collapsed=true]/rail:-me-3.5 group-data-[collapsed=true]/rail:-mt-3.5"
|
|
296
|
+
>
|
|
297
|
+
{local.badge}
|
|
298
|
+
</span>
|
|
299
|
+
</Show>
|
|
300
|
+
|
|
288
301
|
<Show when={local.shortcut && local.trailing === undefined}>
|
|
289
302
|
<span
|
|
290
303
|
aria-hidden="true"
|
|
@@ -380,7 +393,7 @@ export function SideRailButton(
|
|
|
380
393
|
<span class="flex size-5 shrink-0 items-center justify-center [&_svg]:size-5">
|
|
381
394
|
{local.children}
|
|
382
395
|
</span>
|
|
383
|
-
<span class="min-w-0 flex-1 truncate group-data-[collapsed=true]/rail:sr-only">
|
|
396
|
+
<span class="min-w-0 flex-1 truncate text-start group-data-[collapsed=true]/rail:sr-only">
|
|
384
397
|
{local.label}
|
|
385
398
|
</span>
|
|
386
399
|
<Show when={local.shortcut}>
|
|
@@ -182,7 +182,7 @@ export function SidebarNavSection(props: SidebarNavSectionProps) {
|
|
|
182
182
|
})}
|
|
183
183
|
/>
|
|
184
184
|
</Show>
|
|
185
|
-
<span class="min-w-0 flex-1 truncate">{local.label}</span>
|
|
185
|
+
<span class="min-w-0 flex-1 truncate text-start">{local.label}</span>
|
|
186
186
|
<Show when={local.count !== undefined}>
|
|
187
187
|
<span class="shrink-0 tabular-nums text-muted-foreground">{local.count}</span>
|
|
188
188
|
</Show>
|
|
@@ -290,7 +290,7 @@ export function SidebarNavLabel(props: ComponentProps<'span'>) {
|
|
|
290
290
|
return (
|
|
291
291
|
<span
|
|
292
292
|
data-slot="sidebar-nav-label"
|
|
293
|
-
class={cn('min-w-0 flex-1 truncate', local.class)}
|
|
293
|
+
class={cn('min-w-0 flex-1 truncate text-start', local.class)}
|
|
294
294
|
{...rest}
|
|
295
295
|
/>
|
|
296
296
|
)
|
|
@@ -14,14 +14,24 @@ import { Presence as MotionPresence } from 'solid-motionone'
|
|
|
14
14
|
* this for mounting and unmounting, which is the split that keeps the common case
|
|
15
15
|
* free of a runtime while making the rare case correct.
|
|
16
16
|
*
|
|
17
|
-
* <
|
|
18
|
-
* <
|
|
19
|
-
* <div
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* <Presence exitBeforeEnter>
|
|
18
|
+
* <Show when={open()}>
|
|
19
|
+
* <Motion.div
|
|
20
|
+
* initial={{ opacity: 0, scale: 0.96 }}
|
|
21
|
+
* animate={{ opacity: 1, scale: 1 }}
|
|
22
|
+
* exit={{ opacity: 0, scale: 0.96 }}
|
|
23
|
+
* >
|
|
24
|
+
* …
|
|
25
|
+
* </Motion.div>
|
|
26
|
+
* </Show>
|
|
27
|
+
* </Presence>
|
|
22
28
|
*
|
|
23
|
-
* `Presence`
|
|
24
|
-
*
|
|
29
|
+
* `Presence` goes *outside* the condition: it has to outlive the `<Show>`'s child
|
|
30
|
+
* to keep that child mounted until its exit finishes. It takes only two booleans —
|
|
31
|
+
* `initial={false}` skips the enter animation on first render, `exitBeforeEnter`
|
|
32
|
+
* holds a replacement back until the outgoing element has left. The animation
|
|
33
|
+
* itself lives on the `Motion.*` element, which must be the direct child of the
|
|
34
|
+
* condition for its `exit` to run.
|
|
25
35
|
*/
|
|
26
36
|
export type PresenceProps = ComponentProps<typeof MotionPresence> & {
|
|
27
37
|
children?: JSX.Element
|
|
@@ -159,7 +159,11 @@ export function ThemePicker(props: ThemePickerProps) {
|
|
|
159
159
|
role="radio"
|
|
160
160
|
aria-checked={local.selectedId === theme.id}
|
|
161
161
|
aria-label={`${theme.familyLabel} ${theme.label}`}
|
|
162
|
-
|
|
162
|
+
// `justify-start`: the options in a wrapped row stretch to its tallest
|
|
163
|
+
// member, and an option whose label has a second line (the family)
|
|
164
|
+
// is that member. Centred, every shorter option's preview dropped below
|
|
165
|
+
// its neighbours'; pinned to the top, the previews share one line.
|
|
166
|
+
class="h-auto flex-col justify-start gap-1.5 p-0 hover:bg-transparent"
|
|
163
167
|
onClick={() => local.onThemeSelect(theme)}
|
|
164
168
|
>
|
|
165
169
|
<ThemePreview theme={theme} selected={local.selectedId === theme.id} />
|
|
@@ -121,11 +121,23 @@ export function useTheme(): ThemeContextValue {
|
|
|
121
121
|
return value
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The preference store, or nothing.
|
|
126
|
+
*
|
|
127
|
+
* Only ever called inside a `try`: where storage is blocked — a sandboxed iframe, a
|
|
128
|
+
* privacy mode, a denied site-data permission — merely *reading* the `localStorage`
|
|
129
|
+
* global throws ("Access is denied", `SecurityError`), so even the `typeof` guard
|
|
130
|
+
* has to sit inside the `try` rather than in front of it. Without a store the
|
|
131
|
+
* preference lives only in the provider's signal for the session.
|
|
132
|
+
*/
|
|
133
|
+
function preferenceStore(): Storage | undefined {
|
|
134
|
+
return typeof localStorage === 'undefined' ? undefined : localStorage
|
|
135
|
+
}
|
|
136
|
+
|
|
124
137
|
/** The stored preference, validated against the catalogue. */
|
|
125
138
|
function readSelection(storageKey: string): ThemeSelection {
|
|
126
|
-
if (typeof localStorage === 'undefined') return defaultThemeSelection
|
|
127
139
|
try {
|
|
128
|
-
const raw =
|
|
140
|
+
const raw = preferenceStore()?.getItem(storageKey)
|
|
129
141
|
if (!raw) return defaultThemeSelection
|
|
130
142
|
const parsed = JSON.parse(raw) as Partial<ThemeSelection>
|
|
131
143
|
return {
|
|
@@ -268,13 +280,11 @@ export function ThemeProvider(props: ThemeProviderProps) {
|
|
|
268
280
|
setSelection(patch) {
|
|
269
281
|
const next = { ...selection(), ...patch }
|
|
270
282
|
setSelectionState(next)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// simply does not survive the reload.
|
|
277
|
-
}
|
|
283
|
+
try {
|
|
284
|
+
preferenceStore()?.setItem(storageKey(), JSON.stringify(next))
|
|
285
|
+
} catch {
|
|
286
|
+
// A full, blocked or unavailable store must not break the switch: the
|
|
287
|
+
// preference simply does not survive the reload.
|
|
278
288
|
}
|
|
279
289
|
},
|
|
280
290
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentProps } from 'solid-js'
|
|
1
|
+
import type { ComponentProps, JSX } from 'solid-js'
|
|
2
2
|
import { splitProps } from 'solid-js'
|
|
3
3
|
import { cn } from '../../../lib/utils'
|
|
4
4
|
|
|
@@ -22,13 +22,24 @@ export type AspectRatioProps = ComponentProps<'div'> & {
|
|
|
22
22
|
ratio?: number
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The caller's style with the ratio laid over it. A spread `style` replaces the
|
|
27
|
+
* element's rather than merging, so passing any style used to drop the ratio and
|
|
28
|
+
* collapse the box to zero height. `ratio` is the API for the ratio, so it wins.
|
|
29
|
+
*/
|
|
30
|
+
function withRatio(style: AspectRatioProps['style'], ratio: number): JSX.CSSProperties | string {
|
|
31
|
+
const value = String(ratio)
|
|
32
|
+
if (typeof style === 'string') return `${style};aspect-ratio:${value}`
|
|
33
|
+
return { ...style, 'aspect-ratio': value }
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
export function AspectRatio(props: AspectRatioProps) {
|
|
26
|
-
const [local, rest] = splitProps(props, ['class', 'ratio', 'children'])
|
|
37
|
+
const [local, rest] = splitProps(props, ['class', 'ratio', 'style', 'children'])
|
|
27
38
|
|
|
28
39
|
return (
|
|
29
40
|
<div
|
|
30
41
|
data-slot="aspect-ratio"
|
|
31
|
-
style={
|
|
42
|
+
style={withRatio(local.style, local.ratio ?? 16 / 9)}
|
|
32
43
|
class={cn('relative w-full [&>*]:absolute [&>*]:inset-0 [&>*]:size-full', local.class)}
|
|
33
44
|
{...rest}
|
|
34
45
|
>
|
|
@@ -171,7 +171,12 @@ function CalendarGrid(props: {
|
|
|
171
171
|
// Styled from data states, so a day can be a range
|
|
172
172
|
// end and today at once without a prop per pair.
|
|
173
173
|
'data-selected:bg-primary data-selected:text-primary-foreground',
|
|
174
|
-
|
|
174
|
+
// corvu marks every day of a range selected, so the
|
|
175
|
+
// span between the ends steps back to the subtle
|
|
176
|
+
// fill and the ends take the solid one again.
|
|
177
|
+
'data-in-range:bg-primary-subtle data-in-range:text-foreground',
|
|
178
|
+
'data-range-start:bg-primary data-range-start:text-primary-foreground',
|
|
179
|
+
'data-range-end:bg-primary data-range-end:text-primary-foreground',
|
|
175
180
|
'data-range-start:rounded-r-none data-range-end:rounded-l-none',
|
|
176
181
|
'data-today:ring-1 data-today:ring-ring',
|
|
177
182
|
'data-disabled:text-muted-foreground data-disabled:line-through',
|
|
@@ -6,8 +6,8 @@ import { cn } from '../../../lib/utils'
|
|
|
6
6
|
* Card.
|
|
7
7
|
*
|
|
8
8
|
* A surface that sits on the canvas. Every card carries its own edge — a
|
|
9
|
-
* border — because in the light theme `--card`
|
|
10
|
-
*
|
|
9
|
+
* border — because in the light theme `--card` is one faint step off a white
|
|
10
|
+
* `--background`, and a borderless card there all but disappears. Making the border
|
|
11
11
|
* structural rather than optional is what stops one app's cards from reading
|
|
12
12
|
* as floating panels and another's as nothing at all.
|
|
13
13
|
*
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ChartOptions } from 'chart.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The chart's options, as data: no Chart.js runtime and no DOM, so the theme's
|
|
5
|
+
* defaults and the merge rules are testable on their own.
|
|
6
|
+
*
|
|
7
|
+
* Every colour here is written as `var(--token)`. A canvas cannot read a custom
|
|
8
|
+
* property — `fillStyle = 'var(--chart-1)'` is an invalid colour, which the
|
|
9
|
+
* canvas ignores and paints in its default black — so these strings are the
|
|
10
|
+
* *declaration* of which token a colour comes from. The components resolve them
|
|
11
|
+
* against their own element at render time (see `resolveCssVariables` and the
|
|
12
|
+
* resolver in `chart.tsx`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The five series colours, in the order a multi-series chart should use them. */
|
|
16
|
+
export const chartSeriesColors = [
|
|
17
|
+
'var(--chart-1)',
|
|
18
|
+
'var(--chart-2)',
|
|
19
|
+
'var(--chart-3)',
|
|
20
|
+
'var(--chart-4)',
|
|
21
|
+
'var(--chart-5)',
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The chart furniture, mapped onto existing semantic tokens rather than given
|
|
26
|
+
* tokens of its own. A grid line is a line, so it is `--border`; an axis tick and
|
|
27
|
+
* a legend label are secondary text, so they are `--muted-foreground`, whose
|
|
28
|
+
* contrast on `--card` (the frame's surface) is already measured in
|
|
29
|
+
* `tests/tokens.test.ts`. Dedicated `--chart-grid`/`--chart-axis`/`--chart-label`
|
|
30
|
+
* tokens would be three more values to keep in step with these in every theme
|
|
31
|
+
* for no difference anyone has asked for.
|
|
32
|
+
*/
|
|
33
|
+
const furniture = {
|
|
34
|
+
grid: 'var(--border)',
|
|
35
|
+
axis: 'var(--muted-foreground)',
|
|
36
|
+
label: 'var(--muted-foreground)',
|
|
37
|
+
surface: 'var(--card)',
|
|
38
|
+
} as const
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Which scales a chart type draws. Chart.js creates a scale for every key under
|
|
42
|
+
* `options.scales` whatever the chart type, so cartesian defaults handed to a
|
|
43
|
+
* doughnut draw an x and a y axis and a grid behind it. The shape keeps each
|
|
44
|
+
* type's defaults to the scales it actually has.
|
|
45
|
+
*/
|
|
46
|
+
export type ChartScaleShape = 'cartesian' | 'radial' | 'none'
|
|
47
|
+
|
|
48
|
+
const tickFont = { size: 10 }
|
|
49
|
+
|
|
50
|
+
function scalesFor(shape: ChartScaleShape): ChartOptions['scales'] {
|
|
51
|
+
if (shape === 'cartesian') {
|
|
52
|
+
return {
|
|
53
|
+
x: {
|
|
54
|
+
border: { color: furniture.grid },
|
|
55
|
+
grid: { color: furniture.grid, drawTicks: false },
|
|
56
|
+
ticks: { color: furniture.axis, font: tickFont },
|
|
57
|
+
},
|
|
58
|
+
y: {
|
|
59
|
+
border: { display: false },
|
|
60
|
+
grid: { color: furniture.grid, drawTicks: false },
|
|
61
|
+
ticks: { color: furniture.axis, font: tickFont },
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (shape === 'radial') {
|
|
66
|
+
return {
|
|
67
|
+
r: {
|
|
68
|
+
grid: { color: furniture.grid },
|
|
69
|
+
angleLines: { color: furniture.grid },
|
|
70
|
+
pointLabels: { color: furniture.axis, font: tickFont },
|
|
71
|
+
// The default backdrop is a translucent white box, which is a white box
|
|
72
|
+
// on a dark card. The frame's own surface makes it disappear in both.
|
|
73
|
+
ticks: { color: furniture.axis, backdropColor: furniture.surface, font: tickFont },
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
81
|
+
if (typeof value !== 'object' || value === null) return false
|
|
82
|
+
const prototype = Object.getPrototypeOf(value)
|
|
83
|
+
return prototype === Object.prototype || prototype === null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mergeInto(target: Record<string, unknown>, source: Record<string, unknown>) {
|
|
87
|
+
for (const [key, value] of Object.entries(source)) {
|
|
88
|
+
if (value === undefined) continue
|
|
89
|
+
const current = target[key]
|
|
90
|
+
target[key] =
|
|
91
|
+
isPlainObject(value) && isPlainObject(current)
|
|
92
|
+
? mergeInto({ ...current }, value)
|
|
93
|
+
: isPlainObject(value)
|
|
94
|
+
? mergeInto({}, value)
|
|
95
|
+
: value
|
|
96
|
+
}
|
|
97
|
+
return target
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Merge Chart.js options, later sources winning, object by object.
|
|
102
|
+
*
|
|
103
|
+
* Deep, because Chart.js options are deep: `{ scales: { x: { stacked: true } } }`
|
|
104
|
+
* means "also stack x", and a spread would replace the whole axis — its grid,
|
|
105
|
+
* its tick colour, its font — with that one flag. Arrays, functions (scriptable
|
|
106
|
+
* options) and class instances (gradients, patterns) are values and replace
|
|
107
|
+
* rather than merge. No source is mutated.
|
|
108
|
+
*/
|
|
109
|
+
export function mergeChartOptions(...sources: (ChartOptions | undefined)[]): ChartOptions {
|
|
110
|
+
const merged: Record<string, unknown> = {}
|
|
111
|
+
for (const source of sources) {
|
|
112
|
+
if (source) mergeInto(merged, source as Record<string, unknown>)
|
|
113
|
+
}
|
|
114
|
+
return merged as ChartOptions
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The theme's defaults as Chart.js options.
|
|
119
|
+
*
|
|
120
|
+
* Exported so a caller can spread it and change one thing, rather than
|
|
121
|
+
* restating every colour. Everything a chart draws that is not data — grid,
|
|
122
|
+
* ticks, legend, tooltip — is decided here once, which is what makes two charts
|
|
123
|
+
* in one application look like they came from the same place. `overrides` is
|
|
124
|
+
* merged deeply; `shape` selects the scales the chart type has.
|
|
125
|
+
*/
|
|
126
|
+
export function chartOptions(
|
|
127
|
+
overrides?: ChartOptions,
|
|
128
|
+
shape: ChartScaleShape = 'cartesian'
|
|
129
|
+
): ChartOptions {
|
|
130
|
+
return mergeChartOptions(
|
|
131
|
+
{
|
|
132
|
+
responsive: true,
|
|
133
|
+
maintainAspectRatio: false,
|
|
134
|
+
interaction: { mode: 'nearest', intersect: false },
|
|
135
|
+
plugins: {
|
|
136
|
+
legend: {
|
|
137
|
+
labels: {
|
|
138
|
+
color: furniture.label,
|
|
139
|
+
boxWidth: 8,
|
|
140
|
+
boxHeight: 8,
|
|
141
|
+
usePointStyle: true,
|
|
142
|
+
pointStyle: 'circle',
|
|
143
|
+
font: { size: 11 },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
tooltip: {
|
|
147
|
+
backgroundColor: 'var(--popover)',
|
|
148
|
+
titleColor: 'var(--popover-foreground)',
|
|
149
|
+
bodyColor: 'var(--popover-foreground)',
|
|
150
|
+
borderColor: 'var(--border)',
|
|
151
|
+
borderWidth: 1,
|
|
152
|
+
padding: 8,
|
|
153
|
+
cornerRadius: 6,
|
|
154
|
+
displayColors: true,
|
|
155
|
+
boxWidth: 8,
|
|
156
|
+
boxHeight: 8,
|
|
157
|
+
usePointStyle: true,
|
|
158
|
+
titleFont: { size: 11, weight: 600 },
|
|
159
|
+
bodyFont: { size: 11 },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
scales: scalesFor(shape),
|
|
163
|
+
},
|
|
164
|
+
overrides
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cssVariable = /^\s*var\(\s*(--[\w-]+)\s*(?:,\s*(.*?))?\s*\)\s*$/
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Replace every `var(--token)` string in `value` — options, datasets, nested
|
|
172
|
+
* arrays — with what `read` returns for the token. A token `read` cannot
|
|
173
|
+
* resolve falls back to the `var()`'s own fallback, then to the string as
|
|
174
|
+
* written. Functions and class instances pass through untouched, so scriptable
|
|
175
|
+
* options and gradients keep working.
|
|
176
|
+
*/
|
|
177
|
+
export function resolveCssVariables<T>(value: T, read: (name: string) => string | undefined): T {
|
|
178
|
+
if (typeof value === 'string') {
|
|
179
|
+
const match = cssVariable.exec(value)
|
|
180
|
+
if (!match) return value
|
|
181
|
+
const resolved = read(match[1]!)
|
|
182
|
+
if (resolved) return resolved as T
|
|
183
|
+
return (match[2] ? resolveCssVariables(match[2], read) : value) as T
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
return value.map((entry: unknown) => resolveCssVariables(entry, read)) as T
|
|
187
|
+
}
|
|
188
|
+
if (isPlainObject(value)) {
|
|
189
|
+
const resolved: Record<string, unknown> = {}
|
|
190
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
191
|
+
resolved[key] = resolveCssVariables(entry, read)
|
|
192
|
+
}
|
|
193
|
+
return resolved as T
|
|
194
|
+
}
|
|
195
|
+
return value
|
|
196
|
+
}
|