@heyramzi/ui 2.22.1 → 2.24.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/COMPONENTS.md +3 -6
- package/dist/ai/confirmation/index.js +3 -6
- package/dist/ai/index.js +1 -5
- package/dist/canvas/editor/rebase.js +26 -2
- package/dist/charts/dashboard/index.js +1 -3
- package/dist/charts/primitives/chart-utils.js +14 -0
- package/dist/components/chip/index.js +1 -0
- package/dist/components/language-flag/flags.js +5 -0
- package/dist/components/language-flag/index.js +1 -0
- package/dist/components/list-page/index.js +1 -3
- package/dist/components/work-parts/index.js +1 -1
- package/dist/editor/doc-blocks.js +2 -1
- package/dist/editor/safe-url.js +26 -0
- package/dist/editor/serializer.js +10 -4
- package/dist/editor/slash-query.js +10 -0
- package/dist/effects/animated-beam/path-calculator.svelte.js +13 -6
- package/dist/lib/dates.js +41 -0
- package/dist/lib/panel.js +2 -2
- package/dist/lib/tone.js +13 -0
- package/dist/record/format.js +2 -18
- package/dist/record/logic.js +3 -6
- package/dist/status/index.js +1 -1
- package/dist/triage/index.js +4 -0
- package/dist/triage/types.js +12 -0
- package/dist/work/dates.js +2 -3
- package/dist/work/detail/detail-types.js +0 -8
- package/dist/work/docs/edited.js +8 -7
- package/dist/work/inbox/inbox-logic.js +5 -20
- package/dist/work/index.js +0 -2
- package/dist/work/time/duration.js +2 -3
- package/package.json +7 -21
- package/scripts/sync-shadcn.mjs +4 -6
- package/src/ai/confirmation/Confirmation.svelte +2 -2
- package/src/ai/confirmation/{ConfirmationRequest.svelte → ConfirmationState.svelte} +6 -3
- package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
- package/src/ai/confirmation/confirmation-parts.test.svelte +17 -0
- package/src/ai/confirmation/confirmation-parts.test.ts +23 -0
- package/src/ai/confirmation/index.ts +6 -21
- package/src/ai/index.ts +3 -13
- package/src/ai/typing-dots/TypingDots.svelte +32 -12
- package/src/ai/typing-dots/typing-dots.test.ts +36 -0
- package/src/animations/modal.css +4 -1
- package/src/canvas/editor/rebase.test.ts +37 -0
- package/src/canvas/editor/rebase.ts +33 -1
- package/src/canvas/ui/canvas-ui.css +44 -1
- package/src/charts/README.md +2 -2
- package/src/charts/area-chart.svelte +5 -12
- package/src/charts/bar-chart.svelte +6 -14
- package/src/charts/chart-tokens.css +19 -0
- package/src/charts/dashboard/data-table.svelte +21 -21
- package/src/charts/dashboard/index.ts +1 -3
- package/src/charts/dashboard/rating-chip.svelte +9 -11
- package/src/charts/dashboard/stat-tile.svelte +4 -5
- package/src/charts/donut-chart.svelte +1 -1
- package/src/charts/gauge-chart.svelte +1 -1
- package/src/charts/line-chart.svelte +5 -12
- package/src/charts/number-chart.svelte +1 -1
- package/src/charts/primitives/chart-utils.test.ts +17 -0
- package/src/charts/primitives/chart-utils.ts +16 -0
- package/src/charts/ring-chart.svelte +2 -2
- package/src/charts/sparkline.svelte +1 -1
- package/src/charts/stat-card.svelte +1 -1
- package/src/charts/time-series-chart.svelte +9 -5
- package/src/components/chip/count-chip.svelte +35 -0
- package/src/components/chip/index.ts +1 -0
- package/src/components/data-table/data-table.ts +1 -1
- package/src/components/data-table/row-context-menu.svelte +3 -4
- package/src/components/data-table/sortable-head.svelte +45 -10
- package/src/components/language-flag/flags.ts +5 -0
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-flag/language-flag.svelte +3 -11
- package/src/components/list-page/empty-state.svelte +90 -12
- package/src/components/list-page/filter-popover.svelte +2 -5
- package/src/components/list-page/grouped-list.svelte +2 -4
- package/src/components/list-page/index.ts +1 -3
- package/src/components/section-header/section-header.svelte +2 -6
- package/src/components/segmented/segment.svelte +13 -19
- package/src/components/segmented/segmented-filter.svelte +1 -2
- package/src/components/table-view/table-view.svelte +31 -45
- package/src/components/table-view/table-view.test.ts +6 -3
- package/src/components/work-parts/group-head.svelte +1 -1
- package/src/components/work-parts/index.ts +1 -1
- package/src/editor/doc-blocks.ts +2 -1
- package/src/editor/editor.svelte +7 -5
- package/src/editor/safe-url.ts +32 -0
- package/src/editor/serializer.test.ts +58 -0
- package/src/editor/serializer.ts +9 -6
- package/src/editor/slash-query.test.ts +19 -0
- package/src/editor/slash-query.ts +11 -0
- package/src/effects/animated-beam/path-calculator.svelte.ts +19 -5
- package/src/effects/marquee/Marquee.css +6 -3
- package/src/effects/marquee/Marquee.svelte +6 -2
- package/src/inbox/thread-inbox.svelte +6 -4
- package/src/lib/dates.test.ts +63 -0
- package/src/lib/dates.ts +63 -0
- package/src/lib/panel.svelte +116 -0
- package/src/lib/panel.ts +2 -2
- package/src/lib/tone.ts +15 -0
- package/src/modules.test.ts +13 -1
- package/src/panel/side-panel.svelte +4 -4
- package/src/record/format.ts +3 -15
- package/src/record/logic.ts +2 -6
- package/src/record/record-stats.svelte +4 -3
- package/src/record/steps-rail.svelte +7 -4
- package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +1 -1
- package/src/shadcn/button/button.svelte +1 -1
- package/src/shadcn/button/button.test.ts +2 -2
- package/src/shadcn/command/command-dialog.svelte +5 -10
- package/src/shadcn/dialog/dialog-overlay.svelte +1 -1
- package/src/shadcn/input/input.svelte +2 -2
- package/src/shadcn/sheet/sheet-overlay.svelte +1 -1
- package/src/status/index.ts +1 -1
- package/src/status/status-pill.svelte +2 -8
- package/src/thread/thread.svelte +3 -6
- package/src/tokens/craft.css +30 -0
- package/src/triage/README.md +28 -0
- package/src/triage/index.ts +10 -0
- package/src/triage/triage-plan.svelte +132 -0
- package/src/triage/triage-queue.svelte +48 -0
- package/src/triage/triage-task.svelte +95 -0
- package/src/triage/types.ts +35 -0
- package/src/work/README.md +0 -1
- package/src/work/dates.ts +3 -3
- package/src/work/demo.svelte +9 -1
- package/src/work/detail/detail-demo.svelte +30 -27
- package/src/work/detail/detail-types.ts +1 -29
- package/src/work/detail/task-detail.svelte +18 -17
- package/src/work/docs/doc-menu.svelte +2 -1
- package/src/work/docs/edited.ts +4 -7
- package/src/work/inbox/inbox-card.svelte +4 -9
- package/src/work/inbox/inbox-logic.ts +4 -16
- package/src/work/index.ts +1 -3
- package/src/work/kanban-column.svelte +5 -3
- package/src/work/time/duration.ts +2 -3
- package/src/work/time/time-chip.svelte +1 -3
- package/dist/ai/thinking-dots/index.js +0 -3
- package/dist/components/icon/index.js +0 -1
- package/dist/components/locale-switch/index.js +0 -1
- package/dist/icons/trades.js +0 -49
- package/dist/status/pill-tint.js +0 -13
- package/dist/work/presets/sales.js +0 -39
- package/src/ai/confirmation/ConfirmationAccepted.svelte +0 -23
- package/src/ai/confirmation/ConfirmationFailed.svelte +0 -23
- package/src/ai/confirmation/ConfirmationRejected.svelte +0 -23
- package/src/ai/thinking-dots/ThinkingDots.svelte +0 -42
- package/src/ai/thinking-dots/index.ts +0 -5
- package/src/charts/dashboard/dashboard-empty.svelte +0 -55
- package/src/charts/dashboard/data-panel.svelte +0 -81
- package/src/charts/dashboard/data-sort-head.svelte +0 -102
- package/src/components/icon/icon.svelte +0 -23
- package/src/components/icon/index.ts +0 -1
- package/src/components/list-page/detail-empty.svelte +0 -45
- package/src/components/list-page/detail-panel.svelte +0 -82
- package/src/components/list-page/rail-empty.svelte +0 -44
- package/src/components/locale-switch/index.ts +0 -1
- package/src/components/locale-switch/locale-switch.svelte +0 -59
- package/src/components/work-parts/count-chip.svelte +0 -20
- package/src/icons/TradeIcon.svelte +0 -16
- package/src/icons/trades.ts +0 -89
- package/src/status/pill-tint.ts +0 -16
- package/src/work/detail/task-property.svelte +0 -17
- package/src/work/detail/task-thread.svelte +0 -221
- package/src/work/presets/sales.ts +0 -16
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { scaleBand } from "d3-scale";
|
|
9
9
|
import Figure from "./figure.svelte";
|
|
10
10
|
import { TOOLTIP_GAP, TOOLTIP_TOP } from "./motion.js";
|
|
11
|
+
import { valueTick } from "./primitives/chart-utils.js";
|
|
11
12
|
|
|
12
13
|
// =========================
|
|
13
14
|
// TYPES
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
xKey,
|
|
56
57
|
yKey,
|
|
57
58
|
series,
|
|
58
|
-
color = "var(--chart-1)",
|
|
59
|
+
color = "var(--chart-series-1)",
|
|
59
60
|
colorOf,
|
|
60
61
|
height = 240,
|
|
61
62
|
orientation = "vertical",
|
|
@@ -134,16 +135,7 @@
|
|
|
134
135
|
});
|
|
135
136
|
const activeColor = $derived(active && colorOf ? colorOf(active) : color);
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
// falls back to "1M"; the tooltip keeps the full value.
|
|
139
|
-
const compact = new Intl.NumberFormat(undefined, {
|
|
140
|
-
notation: "compact",
|
|
141
|
-
maximumFractionDigits: 1,
|
|
142
|
-
});
|
|
143
|
-
function valueTick(v: number): string {
|
|
144
|
-
const full = formatValue(v);
|
|
145
|
-
return full.length > 6 && Number.isFinite(v) ? compact.format(v) : full;
|
|
146
|
-
}
|
|
138
|
+
const tick = $derived(valueTick(formatValue));
|
|
147
139
|
</script>
|
|
148
140
|
|
|
149
141
|
<!-- =========================== -->
|
|
@@ -161,7 +153,7 @@
|
|
|
161
153
|
{#if showFigure}
|
|
162
154
|
<Figure
|
|
163
155
|
value={active ? valueOf(active) : total}
|
|
164
|
-
color="var(--chart-1)"
|
|
156
|
+
color="var(--chart-series-1)"
|
|
165
157
|
eyebrow={active ? formatX(active[xKey]) : "Total"}
|
|
166
158
|
format={formatValue}
|
|
167
159
|
size="md"
|
|
@@ -188,12 +180,12 @@
|
|
|
188
180
|
<Svg>
|
|
189
181
|
{#if horizontal}
|
|
190
182
|
<Axis placement="left" format={(v) => (formatTick ?? formatX)(v)} />
|
|
191
|
-
<Axis placement="bottom" format={(v) =>
|
|
183
|
+
<Axis placement="bottom" format={(v) => tick(Number(v))} ticks={4} />
|
|
192
184
|
<g class="hr-bar-grid">
|
|
193
185
|
<Axis placement="bottom" grid rule={false} ticks={4} format={() => ""} />
|
|
194
186
|
</g>
|
|
195
187
|
{:else}
|
|
196
|
-
<Axis placement="left" format={(v) =>
|
|
188
|
+
<Axis placement="left" format={(v) => tick(Number(v))} ticks={4} />
|
|
197
189
|
<Axis placement="bottom" format={(v) => (formatTick ?? formatX)(v)} />
|
|
198
190
|
<g class="hr-bar-grid">
|
|
199
191
|
<Axis placement="left" grid rule={false} ticks={4} format={() => ""} />
|
|
@@ -16,6 +16,25 @@
|
|
|
16
16
|
.hr-ring,
|
|
17
17
|
.hr-gauge
|
|
18
18
|
) {
|
|
19
|
+
/* The categorical series palette. The brand leads, then hues spaced round the
|
|
20
|
+
wheel so neighbours never share a family (blue and orange first, the pair that
|
|
21
|
+
survives colour blindness). Each hue is a cat token pulled 15% toward the text
|
|
22
|
+
colour: darker on a light card, lighter on a dark one, so every series clears
|
|
23
|
+
3:1 against the card in both themes (lowest: teal, 3.6:1 light, 6.2:1 dark). */
|
|
24
|
+
--chart-series-1: var(--color-brand, oklch(0.28 0 0));
|
|
25
|
+
--chart-series-2: color-mix(in oklab, var(--color-cat-blue) 85%, var(--color-foreground));
|
|
26
|
+
--chart-series-3: color-mix(in oklab, var(--color-cat-orange) 85%, var(--color-foreground));
|
|
27
|
+
--chart-series-4: color-mix(in oklab, var(--color-cat-green) 85%, var(--color-foreground));
|
|
28
|
+
--chart-series-5: color-mix(in oklab, var(--color-cat-pink) 85%, var(--color-foreground));
|
|
29
|
+
--chart-series-6: color-mix(in oklab, var(--color-cat-teal) 85%, var(--color-foreground));
|
|
30
|
+
/* The palette's `--chart-N` names point here inside a chart, so a series passed as
|
|
31
|
+
`var(--chart-3)` gets a hue, not the brand's grey highlight. */
|
|
32
|
+
--chart-1: var(--chart-series-1);
|
|
33
|
+
--chart-2: var(--chart-series-2);
|
|
34
|
+
--chart-3: var(--chart-series-3);
|
|
35
|
+
--chart-4: var(--chart-series-4);
|
|
36
|
+
--chart-5: var(--chart-series-5);
|
|
37
|
+
--chart-6: var(--chart-series-6);
|
|
19
38
|
--chart-label: var(--muted-foreground, oklch(0.5 0.01 286));
|
|
20
39
|
--chart-grid: color-mix(in srgb, var(--border, oklch(0.5 0.01 286)) 50%, transparent);
|
|
21
40
|
--chart-ease-out: var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
|
|
@@ -42,9 +42,14 @@
|
|
|
42
42
|
* optional pager. A row opens by `rowHref` or `onRowClick`, stretched over the row from the lead.
|
|
43
43
|
*/
|
|
44
44
|
import { cn } from "../../lib/utils.js";
|
|
45
|
-
import DataSortHead from "./data-sort-head.svelte";
|
|
46
45
|
import RatingChip from "./rating-chip.svelte";
|
|
47
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
Pagination,
|
|
48
|
+
SortableHead,
|
|
49
|
+
paginateRows,
|
|
50
|
+
totalPages,
|
|
51
|
+
type SortState,
|
|
52
|
+
} from "../../components/data-table/index.js";
|
|
48
53
|
import {
|
|
49
54
|
dataTableClass,
|
|
50
55
|
dataTdClass,
|
|
@@ -69,8 +74,8 @@
|
|
|
69
74
|
minWidth?: string;
|
|
70
75
|
/** Omit for every row on one page. */
|
|
71
76
|
pageSize?: number;
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
/** The ranked column and its direction. Bind it; a column with `sort` sets it. */
|
|
78
|
+
sort?: SortState;
|
|
74
79
|
page?: number;
|
|
75
80
|
/** The row opens a page. The lead becomes the link. */
|
|
76
81
|
rowHref?: (row: T) => string;
|
|
@@ -92,8 +97,7 @@
|
|
|
92
97
|
leadWidth,
|
|
93
98
|
minWidth = "min-w-[48rem]",
|
|
94
99
|
pageSize,
|
|
95
|
-
|
|
96
|
-
descending = $bindable(true),
|
|
100
|
+
sort = $bindable(null),
|
|
97
101
|
page = $bindable(1),
|
|
98
102
|
rowHref,
|
|
99
103
|
onRowClick,
|
|
@@ -116,9 +120,9 @@
|
|
|
116
120
|
"text-left outline-none after:absolute after:inset-0 after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-ring";
|
|
117
121
|
|
|
118
122
|
const sorted = $derived.by(() => {
|
|
119
|
-
const by = columns.find((column) => column.key ===
|
|
120
|
-
if (!by) return rows;
|
|
121
|
-
const direction =
|
|
123
|
+
const by = columns.find((column) => column.key === sort?.key)?.sort;
|
|
124
|
+
if (!by || !sort) return rows;
|
|
125
|
+
const direction = sort.direction === "desc" ? -1 : 1;
|
|
122
126
|
return [...rows].sort((left, right) => (by(left) - by(right)) * direction);
|
|
123
127
|
});
|
|
124
128
|
|
|
@@ -134,12 +138,8 @@
|
|
|
134
138
|
|
|
135
139
|
function rankBy(key: string) {
|
|
136
140
|
page = 1;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
sortKey = key;
|
|
142
|
-
descending = true;
|
|
141
|
+
const flip = sort?.key === key && sort.direction === "desc";
|
|
142
|
+
sort = { key, direction: flip ? "asc" : "desc" };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
function isMuted(column: DataColumn<T>, row: T) {
|
|
@@ -168,16 +168,16 @@
|
|
|
168
168
|
<th scope="col" class={dataThClass}>{leadLabel}</th>
|
|
169
169
|
{#each columns as column (column.key)}
|
|
170
170
|
{#if column.sort}
|
|
171
|
-
<
|
|
171
|
+
<SortableHead
|
|
172
|
+
key={column.key}
|
|
173
|
+
{sort}
|
|
174
|
+
onSort={rankBy}
|
|
172
175
|
numeric={column.align !== "start"}
|
|
173
|
-
class={columnClass(column)}
|
|
174
|
-
active={sortKey === column.key}
|
|
175
|
-
{descending}
|
|
176
|
-
onSort={() => rankBy(column.key)}
|
|
176
|
+
class={cn(column.align === "start" ? dataThClass : dataThNumClass, columnClass(column))}
|
|
177
177
|
title={column.hint}
|
|
178
178
|
>
|
|
179
179
|
{column.label}
|
|
180
|
-
</
|
|
180
|
+
</SortableHead>
|
|
181
181
|
{:else}
|
|
182
182
|
<th
|
|
183
183
|
scope="col"
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
export { default as DataPanel } from "
|
|
2
|
-
export { default as DataSortHead } from "./data-sort-head.svelte";
|
|
1
|
+
export { default as DataPanel } from "../../lib/panel.svelte";
|
|
3
2
|
export {
|
|
4
3
|
default as DataTable,
|
|
5
4
|
type DataColumn,
|
|
6
5
|
type DataColumnBreakpoint,
|
|
7
6
|
} from "./data-table.svelte";
|
|
8
7
|
export { default as RatingChip } from "./rating-chip.svelte";
|
|
9
|
-
export { default as DashboardEmpty } from "./dashboard-empty.svelte";
|
|
10
8
|
export { default as DashboardSection } from "./dashboard-section.svelte";
|
|
11
9
|
export { default as PanelGrid } from "./panel-grid.svelte";
|
|
12
10
|
export { default as PeriodFilter } from "./period-filter.svelte";
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Light mode takes the 800 shades, since amber on cream fails contrast.
|
|
5
|
-
*/
|
|
2
|
+
// One A to E grade on the shared tone scale, so a C means one thing everywhere.
|
|
3
|
+
import { TONE_TINT, type Tone } from "../../lib/tone.js";
|
|
6
4
|
import { cn } from "../../lib/utils.js";
|
|
7
5
|
|
|
8
6
|
interface Props {
|
|
@@ -14,19 +12,19 @@
|
|
|
14
12
|
|
|
15
13
|
let { grade, hint, class: className }: Props = $props();
|
|
16
14
|
|
|
17
|
-
const
|
|
18
|
-
A: "
|
|
19
|
-
B: "
|
|
20
|
-
C: "
|
|
21
|
-
D: "
|
|
22
|
-
E: "
|
|
15
|
+
const GRADE_TONE: Record<string, Tone> = {
|
|
16
|
+
A: "success",
|
|
17
|
+
B: "success",
|
|
18
|
+
C: "neutral",
|
|
19
|
+
D: "warning",
|
|
20
|
+
E: "destructive",
|
|
23
21
|
};
|
|
24
22
|
</script>
|
|
25
23
|
|
|
26
24
|
<span
|
|
27
25
|
class={cn(
|
|
28
26
|
"inline-flex h-6 min-w-6 items-center justify-center rounded-md px-1.5 text-xs font-semibold tabular-nums",
|
|
29
|
-
|
|
27
|
+
TONE_TINT[GRADE_TONE[grade] ?? "neutral"],
|
|
30
28
|
className,
|
|
31
29
|
)}
|
|
32
30
|
title={hint}
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import Sparkline from "../sparkline.svelte";
|
|
15
15
|
import type { Snippet } from "svelte";
|
|
16
16
|
import { cn } from "../../lib/utils.js";
|
|
17
|
-
import { Icon } from "../../components/icon/index.js";
|
|
18
17
|
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
19
18
|
|
|
20
19
|
// =========================
|
|
@@ -87,7 +86,7 @@
|
|
|
87
86
|
shareLabel,
|
|
88
87
|
color,
|
|
89
88
|
series,
|
|
90
|
-
icon,
|
|
89
|
+
icon: Icon,
|
|
91
90
|
href,
|
|
92
91
|
hrefLabel,
|
|
93
92
|
target = "_blank",
|
|
@@ -124,7 +123,7 @@
|
|
|
124
123
|
)}
|
|
125
124
|
>
|
|
126
125
|
{#snippet tileIcon()}
|
|
127
|
-
{#if
|
|
126
|
+
{#if Icon}<Icon size={14} class="shrink-0" />{/if}
|
|
128
127
|
{/snippet}
|
|
129
128
|
|
|
130
129
|
<NumberChart
|
|
@@ -135,7 +134,7 @@
|
|
|
135
134
|
{share}
|
|
136
135
|
{shareLabel}
|
|
137
136
|
{color}
|
|
138
|
-
icon={
|
|
137
|
+
icon={Icon ? tileIcon : undefined}
|
|
139
138
|
size={lead ? "lg" : "md"}
|
|
140
139
|
caption={hint}
|
|
141
140
|
class={cn(
|
|
@@ -159,7 +158,7 @@
|
|
|
159
158
|
data={points}
|
|
160
159
|
xKey="i"
|
|
161
160
|
yKey="v"
|
|
162
|
-
color={color ?? "var(--chart-1)"}
|
|
161
|
+
color={color ?? "var(--chart-series-1)"}
|
|
163
162
|
height={48}
|
|
164
163
|
bare
|
|
165
164
|
/>
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
cornerRadius={3}
|
|
131
131
|
fill={colorKey
|
|
132
132
|
? String((arc.data as TData)[colorKey])
|
|
133
|
-
: `var(--chart-${(i %
|
|
133
|
+
: `var(--chart-series-${(i % 6) + 1})`}
|
|
134
134
|
class="hr-donut-arc"
|
|
135
135
|
data-hovered={isHovered ? "" : undefined}
|
|
136
136
|
style="opacity: {dimmed ? 0.4 : 1}; translate: {dx}px {dy}px"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import Figure from "./figure.svelte";
|
|
23
23
|
import HoverFocus from "./hover-focus.svelte";
|
|
24
24
|
import { DOMAIN_MOTION, TOOLTIP_GAP, TOOLTIP_TOP } from "./motion.js";
|
|
25
|
+
import { valueTick } from "./primitives/chart-utils.js";
|
|
25
26
|
import Reveal from "./reveal.svelte";
|
|
26
27
|
|
|
27
28
|
// =========================
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
xKey,
|
|
69
70
|
yKey,
|
|
70
71
|
series: seriesInput,
|
|
71
|
-
color = "var(--chart-1)",
|
|
72
|
+
color = "var(--chart-series-1)",
|
|
72
73
|
height = 240,
|
|
73
74
|
smooth = true,
|
|
74
75
|
edgeFade = true,
|
|
@@ -104,7 +105,7 @@
|
|
|
104
105
|
? seriesInput.map((s, i) => ({
|
|
105
106
|
key: s.key,
|
|
106
107
|
label: s.label ?? s.key,
|
|
107
|
-
color: s.color ?? `var(--chart-${(i %
|
|
108
|
+
color: s.color ?? `var(--chart-series-${(i % 6) + 1})`,
|
|
108
109
|
}))
|
|
109
110
|
: yKey
|
|
110
111
|
? [{ key: yKey, label: yKey, color }]
|
|
@@ -119,15 +120,7 @@
|
|
|
119
120
|
const fallback = $derived(data.length > 0 ? data[data.length - 1] : null);
|
|
120
121
|
const display = $derived(active ?? fallback);
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
const compact = new Intl.NumberFormat(undefined, {
|
|
124
|
-
notation: "compact",
|
|
125
|
-
maximumFractionDigits: 1,
|
|
126
|
-
});
|
|
127
|
-
function valueTick(v: number): string {
|
|
128
|
-
const full = formatValue(v);
|
|
129
|
-
return full.length > 6 && Number.isFinite(v) ? compact.format(v) : full;
|
|
130
|
-
}
|
|
123
|
+
const tick = $derived(valueTick(formatValue));
|
|
131
124
|
// WHY: a dashed tail is two splines sharing the boundary point so the solid
|
|
132
125
|
// and dashed segments meet without a gap.
|
|
133
126
|
const hasTail = $derived(
|
|
@@ -200,7 +193,7 @@
|
|
|
200
193
|
motion={DOMAIN_MOTION}
|
|
201
194
|
>
|
|
202
195
|
<Svg>
|
|
203
|
-
<Axis placement="left" format={(v) =>
|
|
196
|
+
<Axis placement="left" format={(v) => tick(Number(v))} ticks={4} />
|
|
204
197
|
<Axis placement="bottom" format={(v) => formatX(v)} />
|
|
205
198
|
<g class="hr-line-grid">
|
|
206
199
|
<Axis placement="left" grid rule={false} ticks={4} format={() => ""} />
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { valueTick } from "./chart-utils.js";
|
|
3
|
+
|
|
4
|
+
describe("valueTick", () => {
|
|
5
|
+
const tick = valueTick((v) => v.toLocaleString("en-US"));
|
|
6
|
+
|
|
7
|
+
it("keeps a label of 6 characters or fewer as the formatter wrote it", () => {
|
|
8
|
+
expect(tick(1234)).toBe("1,234");
|
|
9
|
+
expect(tick(12345)).toBe("12,345");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("shortens a longer label to compact notation", () => {
|
|
13
|
+
expect(tick(1_000_000)).toBe("1M");
|
|
14
|
+
expect(tick(1_234_567)).toBe("1.2M");
|
|
15
|
+
expect(tick(-12345)).toBe("-12.3K");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -39,6 +39,22 @@ export function getPayloadConfigFromPayload(
|
|
|
39
39
|
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const compact = new Intl.NumberFormat(undefined, {
|
|
43
|
+
notation: "compact",
|
|
44
|
+
maximumFractionDigits: 1,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An axis tick formatter. "1,000,000" overflows the 48px gutter and the gap between ticks, so a
|
|
49
|
+
* label over 6 characters falls back to "1M"; the tooltip keeps the full value.
|
|
50
|
+
*/
|
|
51
|
+
export function valueTick(format: (v: number) => string): (v: number) => string {
|
|
52
|
+
return (v) => {
|
|
53
|
+
const full = format(v);
|
|
54
|
+
return full.length > 6 && Number.isFinite(v) ? compact.format(v) : full;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
type ChartContextValue = {
|
|
43
59
|
config: ChartConfig;
|
|
44
60
|
};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
value: number;
|
|
17
17
|
/** Denominator for the progress arc. */
|
|
18
18
|
maxValue: number;
|
|
19
|
-
/** CSS color or var. Falls back to --chart-1..
|
|
19
|
+
/** CSS color or var. Falls back to --chart-series-1..6. */
|
|
20
20
|
color?: string;
|
|
21
21
|
};
|
|
22
22
|
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
index: i,
|
|
83
83
|
radius,
|
|
84
84
|
progress,
|
|
85
|
-
color: d.color ?? `var(--chart-${(i %
|
|
85
|
+
color: d.color ?? `var(--chart-series-${(i % 6) + 1})`,
|
|
86
86
|
};
|
|
87
87
|
}),
|
|
88
88
|
);
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
data,
|
|
63
63
|
label = "Value",
|
|
64
64
|
label2,
|
|
65
|
-
color = "var(--chart-1)",
|
|
66
|
-
color2 = "var(--
|
|
65
|
+
color = "var(--chart-series-1)",
|
|
66
|
+
color2 = "var(--chart-series-2)",
|
|
67
67
|
formatValue = (v) => v.toLocaleString(),
|
|
68
68
|
formatValue2,
|
|
69
69
|
height = 280,
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
<Spline
|
|
153
153
|
y="value2"
|
|
154
154
|
curve={curveMonotoneX}
|
|
155
|
-
style="stroke: {color2}; stroke-dasharray: 4 4; stroke-width: 1; fill: none"
|
|
155
|
+
style="stroke: {color2}; stroke-dasharray: 4 4; stroke-width: 1.5; fill: none"
|
|
156
156
|
/>
|
|
157
157
|
{/if}
|
|
158
158
|
</Reveal>
|
|
@@ -168,9 +168,13 @@
|
|
|
168
168
|
})}
|
|
169
169
|
/>
|
|
170
170
|
<Tooltip.List>
|
|
171
|
-
<Tooltip.Item {label} value={formatValue(d.value)} />
|
|
171
|
+
<Tooltip.Item {label} {color} value={formatValue(d.value)} />
|
|
172
172
|
{#if label2 && d.value2 !== undefined}
|
|
173
|
-
<Tooltip.Item
|
|
173
|
+
<Tooltip.Item
|
|
174
|
+
label={label2}
|
|
175
|
+
color={color2}
|
|
176
|
+
value={(formatValue2 ?? formatValue)(d.value2)}
|
|
177
|
+
/>
|
|
174
178
|
{/if}
|
|
175
179
|
</Tooltip.List>
|
|
176
180
|
{/snippet}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
|
|
5
|
+
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "class" | "children"> {
|
|
6
|
+
count: number;
|
|
7
|
+
/** `sm` is 16px, for a count inside a control or on a button; `md` is 20px, beside a heading. */
|
|
8
|
+
size?: "sm" | "md";
|
|
9
|
+
/** `brand` marks a count that is on: an applied filter, the selected segment. */
|
|
10
|
+
tone?: "neutral" | "brand";
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { count, size = "md", tone = "neutral", class: className, ...restProps }: Props = $props();
|
|
15
|
+
|
|
16
|
+
const SIZE = { sm: "h-4 min-w-4", md: "h-5 min-w-5" };
|
|
17
|
+
const TONE = {
|
|
18
|
+
neutral: "bg-foreground/6 text-muted-foreground",
|
|
19
|
+
brand: "bg-brand/12 text-brand dark:text-brand-light",
|
|
20
|
+
};
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<!-- A count is a pill (DESIGN.md rule 15). Min width equals height with 4px padding, so one digit
|
|
24
|
+
draws a circle, not an ellipse. -->
|
|
25
|
+
<span
|
|
26
|
+
class={cn(
|
|
27
|
+
"inline-grid shrink-0 place-items-center rounded-full px-1 text-xs leading-none font-medium tabular-nums",
|
|
28
|
+
SIZE[size],
|
|
29
|
+
TONE[tone],
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...restProps}
|
|
33
|
+
>
|
|
34
|
+
{count.toLocaleString()}
|
|
35
|
+
</span>
|
|
@@ -26,7 +26,7 @@ export function anyContainsQuery(values: Array<string | null | undefined>, query
|
|
|
26
26
|
// ========================= TYPES
|
|
27
27
|
export type SortDirection = "asc" | "desc";
|
|
28
28
|
|
|
29
|
-
export type SortState<T
|
|
29
|
+
export type SortState<T = Record<string, unknown>> = {
|
|
30
30
|
key: keyof T & string;
|
|
31
31
|
direction: SortDirection;
|
|
32
32
|
} | null;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as ContextMenu from "../../shadcn/context-menu/index.js";
|
|
6
6
|
import { HoldToConfirm } from "../hold-to-confirm/index.js";
|
|
7
7
|
import { ArrowUpRight, Copy, LinkSimple, Trash } from "phosphor-svelte";
|
|
8
|
+
import { copyText } from "../copy/clipboard.js";
|
|
8
9
|
import type { Snippet } from "svelte";
|
|
9
10
|
|
|
10
11
|
// =========================
|
|
@@ -47,14 +48,12 @@
|
|
|
47
48
|
// message, a doc or an agent prompt, none of which can resolve "/clients/42".
|
|
48
49
|
async function handleCopyLink() {
|
|
49
50
|
if (!href) return;
|
|
50
|
-
await
|
|
51
|
-
onCopied?.();
|
|
51
|
+
if (await copyText(new URL(href, window.location.origin).href)) onCopied?.();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async function handleCopyId() {
|
|
55
55
|
if (!id) return;
|
|
56
|
-
await
|
|
57
|
-
onCopied?.();
|
|
56
|
+
if (await copyText(id)) onCopied?.();
|
|
58
57
|
}
|
|
59
58
|
</script>
|
|
60
59
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
<script lang="ts" generics="T">
|
|
2
|
+
/*
|
|
3
|
+
* The one sortable column head, for TableView, the dashboard DataTable and hand-built
|
|
4
|
+
* tables. Every sortable head shows its caret: hover-only makes people hunt.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ========================= IMPORTS
|
|
2
8
|
import type { Snippet } from "svelte";
|
|
3
9
|
import { CaretDown, CaretUp, CaretUpDown } from "phosphor-svelte";
|
|
4
10
|
import { cn } from "../../lib/utils.js";
|
|
@@ -8,21 +14,36 @@
|
|
|
8
14
|
interface Props {
|
|
9
15
|
key: keyof T & string;
|
|
10
16
|
sort: SortState<T>;
|
|
17
|
+
/** Called on click and on Enter/Space. Choosing the next direction is the caller's. */
|
|
11
18
|
onSort: (key: keyof T & string) => void;
|
|
19
|
+
/** Numbers right-align to their digits, so their caret leads the label. */
|
|
20
|
+
numeric?: boolean;
|
|
21
|
+
/** The long form of an abbreviated head, as the browser tooltip. */
|
|
22
|
+
title?: string;
|
|
12
23
|
class?: string;
|
|
24
|
+
style?: string;
|
|
13
25
|
children: Snippet;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
// ========================= PROPS
|
|
17
|
-
let {
|
|
29
|
+
let {
|
|
30
|
+
key,
|
|
31
|
+
sort,
|
|
32
|
+
onSort,
|
|
33
|
+
numeric = false,
|
|
34
|
+
title,
|
|
35
|
+
class: className,
|
|
36
|
+
style,
|
|
37
|
+
children,
|
|
38
|
+
}: Props = $props();
|
|
18
39
|
|
|
19
40
|
// ========================= DERIVED
|
|
20
|
-
let
|
|
21
|
-
let direction = $derived<SortDirection | null>(sort && active ? sort.direction : null);
|
|
41
|
+
let direction = $derived<SortDirection | null>(sort?.key === key ? sort.direction : null);
|
|
22
42
|
let ariaSort = $derived<"ascending" | "descending" | "none">(
|
|
23
43
|
direction === "asc" ? "ascending" : direction === "desc" ? "descending" : "none",
|
|
24
44
|
);
|
|
25
45
|
|
|
46
|
+
// ========================= FUNCTIONS
|
|
26
47
|
function handleKeydown(e: KeyboardEvent) {
|
|
27
48
|
if (e.key === "Enter" || e.key === " ") {
|
|
28
49
|
e.preventDefault();
|
|
@@ -33,9 +54,14 @@
|
|
|
33
54
|
|
|
34
55
|
<th
|
|
35
56
|
data-slot="table-head"
|
|
57
|
+
scope="col"
|
|
58
|
+
{title}
|
|
59
|
+
{style}
|
|
36
60
|
class={cn(
|
|
37
61
|
"text-muted-foreground h-8 px-2 text-left align-middle text-xs font-medium whitespace-nowrap select-none",
|
|
38
62
|
"cursor-pointer transition-colors duration-quick ease-smooth-out hover:bg-foreground/4 motion-reduce:transition-none",
|
|
63
|
+
"outline-none focus-visible:shadow-(--ring-focus-inset)",
|
|
64
|
+
numeric && "text-right",
|
|
39
65
|
className,
|
|
40
66
|
)}
|
|
41
67
|
tabindex="0"
|
|
@@ -43,14 +69,23 @@
|
|
|
43
69
|
onclick={() => onSort(key)}
|
|
44
70
|
onkeydown={handleKeydown}
|
|
45
71
|
>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<CaretDown class="size-3.5 text-foreground" weight="bold" />
|
|
72
|
+
<!-- Pinned to the line's top, not its baseline: a leading caret gave the span the icon's foot
|
|
73
|
+
as its baseline and lifted a numeric label 1.5px above the plain heads beside it. -->
|
|
74
|
+
<span class="inline-flex items-center gap-1 align-top">
|
|
75
|
+
{#if numeric}
|
|
76
|
+
{@render caret()}{@render children()}
|
|
52
77
|
{:else}
|
|
53
|
-
|
|
78
|
+
{@render children()}{@render caret()}
|
|
54
79
|
{/if}
|
|
55
80
|
</span>
|
|
56
81
|
</th>
|
|
82
|
+
|
|
83
|
+
{#snippet caret()}
|
|
84
|
+
{#if direction === "asc"}
|
|
85
|
+
<CaretUp class="size-3.5 shrink-0 text-foreground" weight="bold" aria-hidden="true" />
|
|
86
|
+
{:else if direction === "desc"}
|
|
87
|
+
<CaretDown class="size-3.5 shrink-0 text-foreground" weight="bold" aria-hidden="true" />
|
|
88
|
+
{:else}
|
|
89
|
+
<CaretUpDown class="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
90
|
+
{/if}
|
|
91
|
+
{/snippet}
|