@urbicon-ui/blocks 6.24.0 → 6.25.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 +14 -0
- package/dist/components/Calendar/CalendarTimeGrid.svelte +2 -1
- package/dist/components/Calendar/calendar.variants.js +5 -3
- package/dist/components/CommandPalette/CommandPalette.svelte +32 -16
- package/dist/components/CommandPalette/CommandPalette.svelte.d.ts +1 -1
- package/dist/components/CommandPalette/commandPalette.variants.d.ts +10 -0
- package/dist/components/CommandPalette/commandPalette.variants.js +9 -7
- package/dist/components/CommandPalette/index.d.ts +19 -0
- package/dist/components/CompositionBar/CompositionBar.svelte +1 -1
- package/dist/components/CompositionBar/composition-bar.variants.js +1 -1
- package/dist/components/Planner/planner.variants.js +1 -1
- package/dist/components/Sankey/sankey.variants.js +1 -1
- package/dist/internal/charts/variants.js +2 -2
- package/dist/internal/date-grid/DateGridScaffold.svelte +9 -2
- package/dist/internal/date-grid/date-grid.svelte.d.ts +4 -2
- package/dist/internal/date-grid/date-grid.svelte.js +63 -7
- package/dist/primitives/Avatar/Avatar.svelte +38 -24
- package/dist/primitives/Avatar/index.d.ts +1 -1
- package/dist/primitives/Button/Button.svelte +12 -25
- package/dist/primitives/ButtonGroup/ButtonGroup.svelte +5 -1
- package/dist/primitives/Collapsible/Collapsible.svelte +10 -0
- package/dist/primitives/Collapsible/index.d.ts +13 -2
- package/dist/primitives/ConfirmDialog/ConfirmDialog.svelte +14 -0
- package/dist/primitives/ConfirmDialog/index.d.ts +13 -2
- package/dist/primitives/Dialog/Dialog.svelte +31 -8
- package/dist/primitives/Drawer/Drawer.svelte +29 -7
- package/dist/primitives/Input/Input.svelte +7 -1
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.js +3 -3
- package/dist/primitives/Menu/Menu.svelte +8 -0
- package/dist/primitives/Menu/index.d.ts +11 -13
- package/dist/primitives/Popover/Popover.svelte +107 -4
- package/dist/primitives/Popover/index.d.ts +15 -2
- package/dist/primitives/Popover/index.js +1 -1
- package/dist/primitives/Popover/popover.variants.d.ts +25 -0
- package/dist/primitives/Popover/popover.variants.js +35 -0
- package/dist/primitives/Progress/progress.variants.js +1 -1
- package/dist/primitives/RadioGroup/radioGroup.variants.js +1 -1
- package/dist/primitives/Select/index.d.ts +8 -1
- package/dist/primitives/Slider/Slider.svelte +32 -3
- package/dist/primitives/Stepper/stepper.variants.js +1 -1
- package/dist/primitives/Textarea/Textarea.svelte +5 -1
- package/dist/primitives/Tooltip/tooltip.variants.js +13 -2
- package/dist/style/foundation.css +30 -3
- package/dist/style/interaction.css +19 -2
- package/dist/style/semantic.css +72 -1
- package/dist/style/themes/neutral.css +10 -1
- package/dist/style/themes/ocean.css +5 -1
- package/dist/utils/compose-handlers.d.ts +72 -0
- package/dist/utils/compose-handlers.js +65 -0
- package/dist/utils/figma-token-export.js +1 -1
- package/dist/utils/guide.svelte.js +92 -29
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/overlay-tokens.d.ts +17 -0
- package/dist/utils/overlay-tokens.js +30 -0
- package/dist/utils/use-floating-panel.svelte.js +9 -3
- package/dist/utils/variants.js +5 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -32,6 +32,20 @@ All primitives and components support `unstyled` + `slotClasses` + `preset` for
|
|
|
32
32
|
|
|
33
33
|
Components use a **custom `tv()` variant engine** (`src/lib/utils/variants.ts`, ~600 LoC, zero-dependency replacement for `tailwind-variants`). Design tokens live in `src/lib/style/` as a three-layer OKLCH system (foundation → semantic → interaction). See the [Architecture Overview](../../docs/ARCHITECTURE.md) for details.
|
|
34
34
|
|
|
35
|
+
## Theming
|
|
36
|
+
|
|
37
|
+
Import a shipped theme after the base styles — `neutral`, `ocean`, `forest`, `rose`, `sunset`:
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
@import 'tailwindcss';
|
|
41
|
+
@import '@urbicon-ui/blocks/style/index.css';
|
|
42
|
+
@import '@urbicon-ui/blocks/style/themes/forest.css';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Rolling your own: **a brand colour alone is not a theme.** `surface-*`, `text-*` and `border-*` derive from the neutral chassis (`--color-neutral-*`), not from primary — so re-tint the chassis to your accent's temperature too, or the brand button ends up on cool blue-grey cards. Typography themes the same way (`--text-*`, `--font-*`) in the same `@theme` block.
|
|
46
|
+
|
|
47
|
+
Full walkthrough: [/customization/themes](https://ui.urbicon.de/customization/themes) · canonical reference: `urbicon css-reference theming` (and `typography`).
|
|
48
|
+
|
|
35
49
|
## Mint System (Micro-Interactions)
|
|
36
50
|
|
|
37
51
|
Opt-in micro-interactions. Call `registerDefaultMints()` at app startup, then use via prop:
|
|
@@ -135,7 +135,8 @@
|
|
|
135
135
|
<!-- Current time line -->
|
|
136
136
|
{#if isToday && currentTimePercent !== null}
|
|
137
137
|
<div class={slot('currentTimeLine')} style="top: {currentTimePercent}%;">
|
|
138
|
-
|
|
138
|
+
<!-- Dot matches the line: the shared `live` ("now") accent, not danger. -->
|
|
139
|
+
<div class="absolute -top-1 -left-1 size-2 rounded-full bg-live"></div>
|
|
139
140
|
</div>
|
|
140
141
|
{/if}
|
|
141
142
|
</div>
|
|
@@ -114,7 +114,9 @@ export const calendarVariants = tv({
|
|
|
114
114
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50'
|
|
115
115
|
],
|
|
116
116
|
allDayArea: ['border-b border-border-hairline'],
|
|
117
|
-
|
|
117
|
+
// `live` is the dedicated "now" accent (red by convention, themes via
|
|
118
|
+
// --color-live) — deliberately not `danger`, which signals errors.
|
|
119
|
+
currentTimeLine: 'absolute left-0 right-0 z-10 pointer-events-none border-t-2 border-live',
|
|
118
120
|
// Week time grid mode (replaces weekGrid when showTimeGrid)
|
|
119
121
|
weekTimeLayout: ['w-full flex flex-col', 'border-t border-border-hairline'],
|
|
120
122
|
// Event popover
|
|
@@ -191,7 +193,7 @@ export const calendarVariants = tv({
|
|
|
191
193
|
yearGrid: 'gap-1 p-1',
|
|
192
194
|
yearMonth: 'p-1 gap-0.5',
|
|
193
195
|
yearMonthTitle: 'text-xs mb-0.5',
|
|
194
|
-
yearMiniDay: 'text-
|
|
196
|
+
yearMiniDay: 'text-3xs size-3',
|
|
195
197
|
yearMiniDot: 'size-0.5',
|
|
196
198
|
weekGrid: 'gap-px',
|
|
197
199
|
weekColumn: 'min-h-16',
|
|
@@ -244,7 +246,7 @@ export const calendarVariants = tv({
|
|
|
244
246
|
yearGrid: 'gap-2 p-2',
|
|
245
247
|
yearMonth: 'p-2 gap-1',
|
|
246
248
|
yearMonthTitle: 'text-sm mb-1',
|
|
247
|
-
yearMiniDay: 'text-
|
|
249
|
+
yearMiniDay: 'text-3xs size-4',
|
|
248
250
|
yearMiniDot: 'size-1',
|
|
249
251
|
weekGrid: 'gap-px',
|
|
250
252
|
weekColumn: 'min-h-24',
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
emptyText: emptyTextProp,
|
|
14
14
|
showFooter = true,
|
|
15
15
|
open = $bindable(false),
|
|
16
|
+
query = $bindable(''),
|
|
16
17
|
shortcut = 'mod+k',
|
|
17
18
|
filter,
|
|
18
19
|
onSelect,
|
|
@@ -32,7 +33,6 @@
|
|
|
32
33
|
const blocksConfig = getBlocksConfig();
|
|
33
34
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
34
35
|
|
|
35
|
-
let query = $state('');
|
|
36
36
|
let selectedIndex = $state(0);
|
|
37
37
|
let inputEl: HTMLInputElement | undefined = $state();
|
|
38
38
|
|
|
@@ -58,18 +58,25 @@
|
|
|
58
58
|
return items.filter((item) => fn(item, query));
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Entries carry their index into `filtered` so the markup does not have to
|
|
63
|
+
* look it up per row — that was an `indexOf` per item, i.e. quadratic in the
|
|
64
|
+
* item count on every render.
|
|
65
|
+
*/
|
|
66
|
+
type GroupEntry = { item: CommandPaletteItem; flatIdx: number };
|
|
67
|
+
|
|
61
68
|
const grouped = $derived.by(() => {
|
|
62
|
-
const groups: { category: string;
|
|
63
|
-
const seen: Record<string,
|
|
64
|
-
|
|
69
|
+
const groups: { category: string; entries: GroupEntry[] }[] = [];
|
|
70
|
+
const seen: Record<string, GroupEntry[]> = {};
|
|
71
|
+
filtered.forEach((item, flatIdx) => {
|
|
65
72
|
const cat = item.category ?? '';
|
|
66
73
|
if (!seen[cat]) {
|
|
67
|
-
const arr:
|
|
74
|
+
const arr: GroupEntry[] = [];
|
|
68
75
|
seen[cat] = arr;
|
|
69
|
-
groups.push({ category: cat,
|
|
76
|
+
groups.push({ category: cat, entries: arr });
|
|
70
77
|
}
|
|
71
|
-
seen[cat].push(item);
|
|
72
|
-
}
|
|
78
|
+
seen[cat].push({ item, flatIdx });
|
|
79
|
+
});
|
|
73
80
|
return groups;
|
|
74
81
|
});
|
|
75
82
|
|
|
@@ -129,10 +136,6 @@
|
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
function getFlatIndex(item: CommandPaletteItem): number {
|
|
133
|
-
return filtered.indexOf(item);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
139
|
export function toggle() {
|
|
137
140
|
setOpen(!open);
|
|
138
141
|
}
|
|
@@ -251,8 +254,7 @@
|
|
|
251
254
|
{group.category}
|
|
252
255
|
</div>
|
|
253
256
|
{/if}
|
|
254
|
-
{#each group.
|
|
255
|
-
{@const flatIdx = getFlatIndex(item)}
|
|
257
|
+
{#each group.entries as { item, flatIdx } (item.id ?? item.label)}
|
|
256
258
|
{@const isHighlighted = flatIdx === selectedIndex}
|
|
257
259
|
{@const isDisabled = item.disabled ?? false}
|
|
258
260
|
{#if customItem}
|
|
@@ -312,9 +314,23 @@
|
|
|
312
314
|
{/if}
|
|
313
315
|
<span
|
|
314
316
|
class={unstyled
|
|
315
|
-
? (slotClasses?.
|
|
316
|
-
: styles.
|
|
317
|
+
? (slotClasses?.itemText ?? '')
|
|
318
|
+
: styles.itemText({ class: slotClasses?.itemText })}
|
|
317
319
|
>
|
|
320
|
+
<span
|
|
321
|
+
class={unstyled
|
|
322
|
+
? (slotClasses?.itemLabel ?? '')
|
|
323
|
+
: styles.itemLabel({ class: slotClasses?.itemLabel })}>{item.label}</span
|
|
324
|
+
>
|
|
325
|
+
{#if item.excerpt}
|
|
326
|
+
<span
|
|
327
|
+
class={unstyled
|
|
328
|
+
? (slotClasses?.itemExcerpt ?? '')
|
|
329
|
+
: styles.itemExcerpt({ class: slotClasses?.itemExcerpt })}
|
|
330
|
+
>{item.excerpt}</span
|
|
331
|
+
>
|
|
332
|
+
{/if}
|
|
333
|
+
</span>
|
|
318
334
|
{#if item.shortcut}
|
|
319
335
|
<kbd
|
|
320
336
|
class={unstyled
|
|
@@ -3,6 +3,6 @@ declare const CommandPalette: import("svelte").Component<CommandPaletteProps, {
|
|
|
3
3
|
toggle: () => void;
|
|
4
4
|
show: () => void;
|
|
5
5
|
hide: () => void;
|
|
6
|
-
}, "open">;
|
|
6
|
+
}, "open" | "query">;
|
|
7
7
|
type CommandPalette = ReturnType<typeof CommandPalette>;
|
|
8
8
|
export default CommandPalette;
|
|
@@ -62,11 +62,21 @@ export declare const commandPaletteVariants: ((props?: {
|
|
|
62
62
|
} & {
|
|
63
63
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
64
64
|
}) | undefined) => string;
|
|
65
|
+
itemText: (props?: ({
|
|
66
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
67
|
+
} & {
|
|
68
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
69
|
+
}) | undefined) => string;
|
|
65
70
|
itemLabel: (props?: ({
|
|
66
71
|
size?: "sm" | "md" | "lg" | undefined;
|
|
67
72
|
} & {
|
|
68
73
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
69
74
|
}) | undefined) => string;
|
|
75
|
+
itemExcerpt: (props?: ({
|
|
76
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
77
|
+
} & {
|
|
78
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
79
|
+
}) | undefined) => string;
|
|
70
80
|
itemShortcut: (props?: ({
|
|
71
81
|
size?: "sm" | "md" | "lg" | undefined;
|
|
72
82
|
} & {
|
|
@@ -16,9 +16,7 @@ export const commandPaletteVariants = tv({
|
|
|
16
16
|
inputIcon: 'h-4 w-4 shrink-0 text-text-tertiary',
|
|
17
17
|
clearButton: 'text-text-quaternary hover:text-text-secondary',
|
|
18
18
|
list: 'max-h-72 overflow-y-auto p-1.5',
|
|
19
|
-
groupLabel: [
|
|
20
|
-
'px-2 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-text-quaternary'
|
|
21
|
-
],
|
|
19
|
+
groupLabel: ['px-2 py-1.5 text-2xs font-semibold uppercase tracking-wide text-text-quaternary'],
|
|
22
20
|
// tier: modify — palette items are selectable actions.
|
|
23
21
|
item: [
|
|
24
22
|
'flex w-full items-center gap-3 rounded-modify px-3 py-2 text-sm',
|
|
@@ -28,20 +26,24 @@ export const commandPaletteVariants = tv({
|
|
|
28
26
|
itemDefault: 'text-text-secondary hover:bg-surface-hover',
|
|
29
27
|
itemDisabled: 'text-text-quaternary cursor-not-allowed opacity-50',
|
|
30
28
|
itemIcon: 'h-4 w-4 shrink-0',
|
|
31
|
-
|
|
29
|
+
itemText: 'flex min-w-0 flex-1 flex-col items-start gap-0.5',
|
|
30
|
+
itemLabel: 'text-left',
|
|
31
|
+
// One line only: excerpts are prose around a search match and would
|
|
32
|
+
// otherwise reflow every row to a different height.
|
|
33
|
+
itemExcerpt: 'w-full truncate text-left text-2xs text-text-tertiary',
|
|
32
34
|
itemShortcut: [
|
|
33
35
|
'rounded border border-border-subtle bg-surface-subtle',
|
|
34
|
-
'px-1.5 py-0.5 font-mono text-
|
|
36
|
+
'px-1.5 py-0.5 font-mono text-3xs text-text-quaternary'
|
|
35
37
|
],
|
|
36
38
|
empty: 'py-8 text-center text-sm text-text-tertiary',
|
|
37
39
|
footer: [
|
|
38
40
|
'flex items-center gap-4 border-t border-border-hairline',
|
|
39
|
-
'px-4 py-2 text-
|
|
41
|
+
'px-4 py-2 text-2xs text-text-quaternary'
|
|
40
42
|
],
|
|
41
43
|
footerHint: 'flex items-center gap-1',
|
|
42
44
|
kbd: [
|
|
43
45
|
'rounded border border-border-subtle bg-surface-subtle',
|
|
44
|
-
'px-1 py-0.5 font-mono text-
|
|
46
|
+
'px-1 py-0.5 font-mono text-3xs'
|
|
45
47
|
],
|
|
46
48
|
separator: 'my-1'
|
|
47
49
|
},
|
|
@@ -18,6 +18,12 @@ export interface CommandPaletteItem {
|
|
|
18
18
|
id?: string;
|
|
19
19
|
/** Display text shown in the list. */
|
|
20
20
|
label: string;
|
|
21
|
+
/**
|
|
22
|
+
* Secondary line rendered under the label, truncated to one line. Use it for
|
|
23
|
+
* context the label cannot carry — a description, or an excerpt around a
|
|
24
|
+
* search match.
|
|
25
|
+
*/
|
|
26
|
+
excerpt?: string;
|
|
21
27
|
/** Group header this item belongs to. Items with the same category are grouped together. */
|
|
22
28
|
category?: string;
|
|
23
29
|
/** Optional keyboard shortcut displayed on the right side. */
|
|
@@ -38,6 +44,8 @@ export interface CommandPaletteItem {
|
|
|
38
44
|
*
|
|
39
45
|
* @tag action
|
|
40
46
|
* @related Dialog
|
|
47
|
+
* @related Menu
|
|
48
|
+
* @related Combobox
|
|
41
49
|
*
|
|
42
50
|
* @example
|
|
43
51
|
* ```svelte
|
|
@@ -76,6 +84,17 @@ export interface CommandPaletteProps {
|
|
|
76
84
|
showFooter?: boolean;
|
|
77
85
|
/** Controls visibility. Supports `bind:open`. @default false */
|
|
78
86
|
open?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Current search text. Supports `bind:query`. Reset to `''` whenever the
|
|
89
|
+
* palette opens.
|
|
90
|
+
*
|
|
91
|
+
* Bind it to source `items` asynchronously — derive your results from the
|
|
92
|
+
* query and pass them back in via `items`, with `filter={() => true}` so the
|
|
93
|
+
* built-in label match does not filter them a second time.
|
|
94
|
+
*
|
|
95
|
+
* @default ''
|
|
96
|
+
*/
|
|
97
|
+
query?: string;
|
|
79
98
|
/**
|
|
80
99
|
* Register a global keyboard shortcut that toggles the palette.
|
|
81
100
|
* Set to `false` to disable. @default 'mod+k' (Cmd+K / Ctrl+K)
|
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
<span
|
|
319
319
|
aria-hidden="true"
|
|
320
320
|
class={[
|
|
321
|
-
'pointer-events-none truncate px-1.5 text-
|
|
321
|
+
'pointer-events-none truncate px-1.5 text-3xs font-medium tabular-nums',
|
|
322
322
|
!unstyled && getOnColorClass(item)
|
|
323
323
|
]
|
|
324
324
|
.filter(Boolean)
|
|
@@ -14,7 +14,7 @@ export const sankeyVariants = tv({
|
|
|
14
14
|
'fill-text-primary text-xs font-medium',
|
|
15
15
|
'transition-[opacity] duration-[var(--blocks-duration-fast)]'
|
|
16
16
|
],
|
|
17
|
-
nodeValue: ['pointer-events-none select-none', 'fill-text-tertiary text-
|
|
17
|
+
nodeValue: ['pointer-events-none select-none', 'fill-text-tertiary text-3xs tabular-nums'],
|
|
18
18
|
link: [
|
|
19
19
|
'cursor-default fill-none',
|
|
20
20
|
'transition-[opacity,stroke-opacity,stroke-width] duration-[var(--blocks-duration-fast)]',
|
|
@@ -17,7 +17,7 @@ export const chartVariants = tv({
|
|
|
17
17
|
/** Tick marks. */
|
|
18
18
|
axisTick: ['stroke-border-subtle'],
|
|
19
19
|
/** Tick + category labels. */
|
|
20
|
-
axisLabel: ['fill-text-tertiary text-
|
|
20
|
+
axisLabel: ['fill-text-tertiary text-3xs tabular-nums'],
|
|
21
21
|
/** Background gridlines. */
|
|
22
22
|
grid: ['stroke-border-hairline'],
|
|
23
23
|
/** Generic data mark (line path, area, point). */
|
|
@@ -29,7 +29,7 @@ export const chartVariants = tv({
|
|
|
29
29
|
/** Donut center primary label. */
|
|
30
30
|
centerLabel: ['fill-text-primary text-sm font-medium tabular-nums'],
|
|
31
31
|
/** Donut center secondary label. */
|
|
32
|
-
centerSubLabel: ['fill-text-tertiary text-
|
|
32
|
+
centerSubLabel: ['fill-text-tertiary text-3xs'],
|
|
33
33
|
/** Legend container (HTML). */
|
|
34
34
|
legend: ['mt-2 flex flex-wrap items-center gap-x-4 gap-y-1.5', 'text-text-secondary text-xs'],
|
|
35
35
|
/** One legend entry. */
|
|
@@ -142,8 +142,15 @@
|
|
|
142
142
|
style="position: relative; overflow: hidden;"
|
|
143
143
|
onmouseleave={() => ctx.setHoveredDate(null)}
|
|
144
144
|
{@attach swipeable({
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
// Direction-gated like the header arrows: a swipe at the bound should be
|
|
146
|
+
// inert (no navDirection flip, no clamped no-op emit), matching the
|
|
147
|
+
// disabled arrow button for the same direction.
|
|
148
|
+
onSwipeLeft: () => {
|
|
149
|
+
if (ctx.canGoForward) ctx.navigate(1);
|
|
150
|
+
},
|
|
151
|
+
onSwipeRight: () => {
|
|
152
|
+
if (ctx.canGoBack) ctx.navigate(-1);
|
|
153
|
+
},
|
|
147
154
|
enabled: swipeEnabled && !ctx.disabled
|
|
148
155
|
})}
|
|
149
156
|
>
|
|
@@ -106,8 +106,10 @@ export declare class DateGridController {
|
|
|
106
106
|
isRangeEnd(date: Date): boolean;
|
|
107
107
|
isInSelectedRange(date: Date): boolean;
|
|
108
108
|
isInPreviewRange(date: Date): boolean;
|
|
109
|
-
/** Step the view by `delta` units (months / weeks / days; range
|
|
110
|
-
* own span in
|
|
109
|
+
/** Step the view by `delta` units (months / weeks / days; range slides its
|
|
110
|
+
* window by its own span in days). Every path clamps to [minDate, maxDate];
|
|
111
|
+
* the range window clamps span-preserving. Emits the next reference date and
|
|
112
|
+
* range (visible cell range, or the shifted window for `range`). */
|
|
111
113
|
navigate(delta: number): void;
|
|
112
114
|
/** Jump to today and focus it. Clamped to `[minDate, maxDate]`: the header Today
|
|
113
115
|
* button gates itself on {@link canGoToToday} (a clamped "today" is not today, so
|
|
@@ -116,7 +116,18 @@ export class DateGridController {
|
|
|
116
116
|
const { canGoBack, canGoForward } = clampMonth(referenceDate.getMonth(), referenceDate.getFullYear(), minDate, maxDate);
|
|
117
117
|
return { canGoBack, canGoForward };
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
if (view === 'range') {
|
|
120
|
+
// Range navigation slides the *explicit* window (partial shifts allowed —
|
|
121
|
+
// see the span-preserving clamp in navigate()), so gate on the window
|
|
122
|
+
// edges, not the week-padded visible cells: the padding may spill past a
|
|
123
|
+
// bound while the window itself can still move.
|
|
124
|
+
const { start, end } = this.#orderedRange(referenceDate);
|
|
125
|
+
return {
|
|
126
|
+
canGoBack: !minDate || start.getTime() > stripTime(minDate).getTime(),
|
|
127
|
+
canGoForward: !maxDate || end.getTime() < stripTime(maxDate).getTime()
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// week/day: day-granular bounds against the visible edges.
|
|
120
131
|
const canGoBack = !minDate || stripTime(this.rangeStart) > stripTime(minDate);
|
|
121
132
|
const canGoForward = !maxDate || stripTime(this.rangeEnd) < stripTime(maxDate);
|
|
122
133
|
return { canGoBack, canGoForward };
|
|
@@ -232,24 +243,38 @@ export class DateGridController {
|
|
|
232
243
|
return isInRange(date, range.start, this.hoveredDate) && !isSameDay(date, range.start);
|
|
233
244
|
}
|
|
234
245
|
// ─── Actions ─────────────────────────────────────────────────────────────
|
|
235
|
-
/** Step the view by `delta` units (months / weeks / days; range
|
|
236
|
-
* own span in
|
|
246
|
+
/** Step the view by `delta` units (months / weeks / days; range slides its
|
|
247
|
+
* window by its own span in days). Every path clamps to [minDate, maxDate];
|
|
248
|
+
* the range window clamps span-preserving. Emits the next reference date and
|
|
249
|
+
* range (visible cell range, or the shifted window for `range`). */
|
|
237
250
|
navigate(delta) {
|
|
238
251
|
if (delta === 0)
|
|
239
252
|
return;
|
|
240
253
|
const { view, referenceDate, minDate, maxDate } = this.#opts;
|
|
241
|
-
this.navDirection = delta > 0 ? 'forward' : 'backward';
|
|
242
254
|
if (view === 'range') {
|
|
243
255
|
// The range geometry follows the explicit rangeStart/rangeEnd inputs, not
|
|
244
256
|
// referenceDate, so navigation shifts that window by its own span and emits
|
|
245
|
-
// the shifted range — the wrapper rebinds rangeStart/rangeEnd from it.
|
|
257
|
+
// the shifted range — the wrapper rebinds rangeStart/rangeEnd from it. It
|
|
258
|
+
// deliberately bypasses #emitNavigate: that would emit the week-padded
|
|
259
|
+
// *visible* range, and a wrapper rebinding rangeStart/rangeEnd from it
|
|
260
|
+
// would silently grow a mid-week window to full weeks on every step.
|
|
261
|
+
// The shift is clamped span-preserving (see #clampWindowStart), so a swipe
|
|
262
|
+
// — not gated by canGoBack/canGoForward the way the header arrows are —
|
|
263
|
+
// cannot push the window past [minDate, maxDate]. A shift that cannot move
|
|
264
|
+
// at all (already at the bound) emits nothing: unlike week/day, a no-op
|
|
265
|
+
// emit here would rebind rangeStart/rangeEnd to fresh Date objects and
|
|
266
|
+
// fire the consumer's onNavigate with an unchanged window.
|
|
246
267
|
const { start, end } = this.#orderedRange(referenceDate);
|
|
247
268
|
const spanDays = daysBetween(start, end) + 1;
|
|
248
|
-
const shiftedStart = addDays(start, delta * spanDays);
|
|
249
|
-
|
|
269
|
+
const shiftedStart = this.#clampWindowStart(addDays(start, delta * spanDays), spanDays);
|
|
270
|
+
if (shiftedStart.getTime() === start.getTime())
|
|
271
|
+
return;
|
|
272
|
+
this.navDirection = shiftedStart.getTime() > start.getTime() ? 'forward' : 'backward';
|
|
273
|
+
const shiftedEnd = addDays(shiftedStart, spanDays - 1);
|
|
250
274
|
this.#opts.onNavigate?.(shiftedStart, { start: shiftedStart, end: shiftedEnd });
|
|
251
275
|
return;
|
|
252
276
|
}
|
|
277
|
+
this.navDirection = delta > 0 ? 'forward' : 'backward';
|
|
253
278
|
let next;
|
|
254
279
|
switch (view) {
|
|
255
280
|
case 'month': {
|
|
@@ -368,6 +393,37 @@ export class DateGridController {
|
|
|
368
393
|
const re = stripTime(this.#opts.rangeEnd ?? reference);
|
|
369
394
|
return rs.getTime() <= re.getTime() ? { start: rs, end: re } : { start: re, end: rs };
|
|
370
395
|
}
|
|
396
|
+
/** Span-preserving sliding-window clamp for range navigation. A window of
|
|
397
|
+
* `spanDays` days starting at `idealStart` must lie inside [minDate, maxDate]
|
|
398
|
+
* as a whole: the earliest allowed start is minDate, the latest is
|
|
399
|
+
* `maxDate − (spanDays − 1)` (inclusive bounds — the window's last day is
|
|
400
|
+
* `start + spanDays − 1` and must not pass maxDate). Clamping only the start
|
|
401
|
+
* against maxDate would collapse the span; this keeps it exact, allowing
|
|
402
|
+
* partial shifts up to the bound. Degenerate case — the span is longer than
|
|
403
|
+
* the navigable interval, so no start satisfies both bounds — pins the window
|
|
404
|
+
* to minDate (its tail spills past maxDate; matches clampDate's minDate-wins
|
|
405
|
+
* ordering) and warns in DEV instead of crashing, mirroring the inverted-
|
|
406
|
+
* bounds warning in the constructor. */
|
|
407
|
+
#clampWindowStart(idealStart, spanDays) {
|
|
408
|
+
const { minDate, maxDate } = this.#opts;
|
|
409
|
+
const earliest = minDate ? stripTime(minDate) : undefined;
|
|
410
|
+
const latest = maxDate ? addDays(stripTime(maxDate), -(spanDays - 1)) : undefined;
|
|
411
|
+
if (earliest && latest && earliest.getTime() > latest.getTime()) {
|
|
412
|
+
if (import.meta.env?.DEV) {
|
|
413
|
+
console.warn(`[DateGrid] a ${spanDays}-day range window cannot fit between minDate ` +
|
|
414
|
+
`(${minDate?.toLocaleDateString()}) and maxDate (${maxDate?.toLocaleDateString()}) ` +
|
|
415
|
+
`— pinning the window to minDate; its tail spills past maxDate. ` +
|
|
416
|
+
`Widen the bounds or shorten the window.`);
|
|
417
|
+
}
|
|
418
|
+
return earliest;
|
|
419
|
+
}
|
|
420
|
+
let next = idealStart;
|
|
421
|
+
if (latest && next.getTime() > latest.getTime())
|
|
422
|
+
next = latest;
|
|
423
|
+
if (earliest && next.getTime() < earliest.getTime())
|
|
424
|
+
next = earliest;
|
|
425
|
+
return next;
|
|
426
|
+
}
|
|
371
427
|
/** Pure cell geometry for an arbitrary reference date (no side effects). */
|
|
372
428
|
#cellsFor(reference) {
|
|
373
429
|
const wso = this.#opts.weekStartsOn;
|
|
@@ -42,35 +42,42 @@
|
|
|
42
42
|
|
|
43
43
|
const isInteractive = $derived(clickable || interactive || !!onclick);
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// The identity palette lives in `style/semantic.css` (--color-avatar-1..12),
|
|
46
|
+
// so a brand rethemes it without touching this component and dark mode
|
|
47
|
+
// resolves through light-dark() like every other token. Spelled out one
|
|
48
|
+
// `var()` per slot rather than built from a template string: Tailwind's
|
|
49
|
+
// scanner only keeps @theme variables it can see referenced in the source,
|
|
50
|
+
// and a computed `--color-avatar-${n}` is invisible to it — the same reason
|
|
51
|
+
// `internal/charts/utils.ts` enumerates its chart palette literally.
|
|
52
|
+
const identityPalette = [
|
|
53
|
+
'var(--color-avatar-1)',
|
|
54
|
+
'var(--color-avatar-2)',
|
|
55
|
+
'var(--color-avatar-3)',
|
|
56
|
+
'var(--color-avatar-4)',
|
|
57
|
+
'var(--color-avatar-5)',
|
|
58
|
+
'var(--color-avatar-6)',
|
|
59
|
+
'var(--color-avatar-7)',
|
|
60
|
+
'var(--color-avatar-8)',
|
|
61
|
+
'var(--color-avatar-9)',
|
|
62
|
+
'var(--color-avatar-10)',
|
|
63
|
+
'var(--color-avatar-11)',
|
|
64
|
+
'var(--color-avatar-12)'
|
|
65
|
+
];
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'#4d7c0f', // lime-700
|
|
56
|
-
'#15803d', // green-700
|
|
57
|
-
'#047857', // emerald-700
|
|
58
|
-
'#0e7490', // cyan-700
|
|
59
|
-
'#0369a1', // sky-700
|
|
60
|
-
'#1d4ed8', // blue-700
|
|
61
|
-
'#4338ca', // indigo-700
|
|
62
|
-
'#6d28d9', // violet-700
|
|
63
|
-
'#7e22ce', // purple-700
|
|
64
|
-
'#a21caf', // fuchsia-700
|
|
65
|
-
'#be185d' // pink-700
|
|
66
|
-
];
|
|
67
|
+
// Deterministic name → slot. The same name must always land on the same hue,
|
|
68
|
+
// so this stays a pure function of `name` with no randomness despite the
|
|
69
|
+
// `randomColor` prop name.
|
|
70
|
+
function getIdentityColor(name?: string): string | undefined {
|
|
71
|
+
// No name means no identity to encode — fall through to the `frame` slot's
|
|
72
|
+
// neutral surface tokens instead of inventing a grey.
|
|
73
|
+
if (!name) return undefined;
|
|
67
74
|
|
|
68
75
|
let hash = 0;
|
|
69
76
|
for (let i = 0; i < name.length; i++) {
|
|
70
77
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
return
|
|
80
|
+
return identityPalette[Math.abs(hash) % identityPalette.length];
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
function getInitials(name?: string): string {
|
|
@@ -83,7 +90,7 @@
|
|
|
83
90
|
.slice(0, 2);
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
const randomBg = $derived(randomColor ?
|
|
93
|
+
const randomBg = $derived(randomColor ? getIdentityColor(name) : undefined);
|
|
87
94
|
|
|
88
95
|
const variantProps: AvatarVariants = $derived({
|
|
89
96
|
size,
|
|
@@ -107,8 +114,15 @@
|
|
|
107
114
|
// Inline property names stay kebab-case — a previous camelCase
|
|
108
115
|
// `backgroundColor` was silently ignored by browsers, dropping the avatar back
|
|
109
116
|
// to surface-interactive + white text.
|
|
117
|
+
//
|
|
118
|
+
// The initials take `--color-text-on-dark`, the palette's contrast partner: it
|
|
119
|
+
// flips white → neutral-900 in step with the fills going dark → light, so the
|
|
120
|
+
// initials stay legible in both modes. A literal `white` (the previous value)
|
|
121
|
+
// only worked because the old fills were frozen dark in dark mode too.
|
|
110
122
|
const frameStyle = $derived(
|
|
111
|
-
randomColor && randomBg
|
|
123
|
+
randomColor && randomBg
|
|
124
|
+
? `background-color: ${randomBg}; color: var(--color-text-on-dark)`
|
|
125
|
+
: undefined
|
|
112
126
|
);
|
|
113
127
|
|
|
114
128
|
// `ringColor` recolours the ring on the outer `base`. The ring is a
|
|
@@ -34,7 +34,7 @@ export interface AvatarProps extends AvatarVariants, Omit<HTMLAttributes<HTMLDiv
|
|
|
34
34
|
children?: Snippet;
|
|
35
35
|
/** Custom ring color (CSS value). Overrides `ringIntent` when set. */
|
|
36
36
|
ringColor?: string;
|
|
37
|
-
/** Derive a deterministic background color from `name`. The same name always produces the same color, making it easy to visually distinguish users without images. Overrides `intent`. */
|
|
37
|
+
/** Derive a deterministic background color from `name`. The same name always produces the same color, making it easy to visually distinguish users without images. Overrides `intent`. Picks from the 12-slot identity palette (`--color-avatar-1` … `--color-avatar-12`), which resolves light/dark automatically — override those tokens to rebrand it. Without a `name` there is no identity to encode, so the avatar keeps its neutral surface. */
|
|
38
38
|
randomColor?: boolean;
|
|
39
39
|
/** Mark the avatar as clickable (adds hover/focus styles and keyboard support). Alias for the `interactive` variant. */
|
|
40
40
|
clickable?: boolean;
|
|
@@ -153,25 +153,15 @@
|
|
|
153
153
|
`blocks-intent-{name}` class on the button root scopes the glow color
|
|
154
154
|
via `mint/styles.css`. */
|
|
155
155
|
|
|
156
|
-
:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@keyframes ripple {
|
|
166
|
-
0% {
|
|
167
|
-
transform: scale(0);
|
|
168
|
-
opacity: 1;
|
|
169
|
-
}
|
|
170
|
-
100% {
|
|
171
|
-
transform: scale(1);
|
|
172
|
-
opacity: 0;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
156
|
+
/* The ripple mint owns the ripple end-to-end: `mint/ripple.ts` appends a
|
|
157
|
+
`<span class="blocks-mint-ripple">` on click and drives it with the Web
|
|
158
|
+
Animations API, tinted `currentColor` so it resolves per variant/intent —
|
|
159
|
+
white on a filled button, the intent hue on outlined/ghost. Button used to
|
|
160
|
+
carry a competing `.blocks-ripple` rule here (a hardcoded
|
|
161
|
+
`rgba(255,255,255,0.4)` plus its own @keyframes) that no element ever
|
|
162
|
+
matched — nothing has emitted that class since the initial commit, and it
|
|
163
|
+
would have been invisible on light variants had it matched. Removed rather
|
|
164
|
+
than tokenised; see the same note in `mint/styles.css` (XC-12/PAG-1). */
|
|
175
165
|
|
|
176
166
|
:global(.sr-only) {
|
|
177
167
|
position: absolute;
|
|
@@ -185,10 +175,7 @@
|
|
|
185
175
|
border-width: 0;
|
|
186
176
|
}
|
|
187
177
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
opacity: 0 !important;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
178
|
+
/* No reduced-motion rule for the ripple here either: `mint/ripple.ts` bails at
|
|
179
|
+
click time on `prefers-reduced-motion`, and `mint/styles.css` hides
|
|
180
|
+
`.blocks-mint-ripple` outright — both on the class that actually exists. */
|
|
194
181
|
</style>
|