@heyramzi/ui 2.23.0 → 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/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/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/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 +1 -21
- 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/canvas/ui/canvas-ui.css +12 -0
- package/src/charts/README.md +2 -2
- package/src/charts/area-chart.svelte +3 -10
- package/src/charts/bar-chart.svelte +4 -12
- 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 +3 -4
- package/src/charts/line-chart.svelte +3 -10
- package/src/charts/primitives/chart-utils.test.ts +17 -0
- package/src/charts/primitives/chart-utils.ts +16 -0
- package/src/charts/time-series-chart.svelte +8 -4
- 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/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/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/status/index.ts +1 -1
- package/src/status/status-pill.svelte +2 -8
- package/src/thread/thread.svelte +3 -6
- 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/time/duration.ts +2 -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
|
@@ -1,50 +1,35 @@
|
|
|
1
1
|
import Confirmation from "./Confirmation.svelte";
|
|
2
2
|
import ConfirmationTitle from "./ConfirmationTitle.svelte";
|
|
3
|
-
import
|
|
4
|
-
import ConfirmationAccepted from "./ConfirmationAccepted.svelte";
|
|
5
|
-
import ConfirmationRejected from "./ConfirmationRejected.svelte";
|
|
6
|
-
import ConfirmationFailed from "./ConfirmationFailed.svelte";
|
|
3
|
+
import ConfirmationState from "./ConfirmationState.svelte";
|
|
7
4
|
import ConfirmationActions from "./ConfirmationActions.svelte";
|
|
8
5
|
import ConfirmationAction from "./ConfirmationAction.svelte";
|
|
9
6
|
|
|
10
7
|
import type { ConfirmationProps } from "./Confirmation.svelte";
|
|
11
8
|
import type { ConfirmationTitleProps } from "./ConfirmationTitle.svelte";
|
|
12
|
-
import type {
|
|
13
|
-
import type { ConfirmationAcceptedProps } from "./ConfirmationAccepted.svelte";
|
|
14
|
-
import type { ConfirmationRejectedProps } from "./ConfirmationRejected.svelte";
|
|
15
|
-
import type { ConfirmationFailedProps } from "./ConfirmationFailed.svelte";
|
|
9
|
+
import type { ConfirmationStateProps } from "./ConfirmationState.svelte";
|
|
16
10
|
import type { ConfirmationActionsProps } from "./ConfirmationActions.svelte";
|
|
17
11
|
import type { ConfirmationActionProps } from "./ConfirmationAction.svelte";
|
|
18
12
|
|
|
19
13
|
export type {
|
|
20
14
|
ConfirmationContextValue,
|
|
21
|
-
|
|
15
|
+
ConfirmationKind,
|
|
22
16
|
} from "./confirmation-context.svelte.js";
|
|
23
17
|
|
|
24
18
|
export {
|
|
25
19
|
Confirmation,
|
|
26
20
|
ConfirmationTitle,
|
|
27
|
-
|
|
28
|
-
ConfirmationAccepted,
|
|
29
|
-
ConfirmationRejected,
|
|
30
|
-
ConfirmationFailed,
|
|
21
|
+
ConfirmationState,
|
|
31
22
|
ConfirmationActions,
|
|
32
23
|
ConfirmationAction,
|
|
33
24
|
type ConfirmationProps,
|
|
34
25
|
type ConfirmationTitleProps,
|
|
35
|
-
type
|
|
36
|
-
type ConfirmationAcceptedProps,
|
|
37
|
-
type ConfirmationRejectedProps,
|
|
38
|
-
type ConfirmationFailedProps,
|
|
26
|
+
type ConfirmationStateProps,
|
|
39
27
|
type ConfirmationActionsProps,
|
|
40
28
|
type ConfirmationActionProps,
|
|
41
29
|
//
|
|
42
30
|
Confirmation as Root,
|
|
43
31
|
ConfirmationTitle as Title,
|
|
44
|
-
|
|
45
|
-
ConfirmationAccepted as Accepted,
|
|
46
|
-
ConfirmationRejected as Rejected,
|
|
47
|
-
ConfirmationFailed as Failed,
|
|
32
|
+
ConfirmationState as State,
|
|
48
33
|
ConfirmationActions as Actions,
|
|
49
34
|
ConfirmationAction as Action,
|
|
50
35
|
};
|
package/src/ai/index.ts
CHANGED
|
@@ -46,28 +46,18 @@ export {
|
|
|
46
46
|
export {
|
|
47
47
|
Confirmation,
|
|
48
48
|
ConfirmationTitle,
|
|
49
|
-
|
|
50
|
-
ConfirmationAccepted,
|
|
51
|
-
ConfirmationRejected,
|
|
49
|
+
ConfirmationState,
|
|
52
50
|
ConfirmationActions,
|
|
53
51
|
ConfirmationAction,
|
|
54
52
|
type ConfirmationProps,
|
|
55
53
|
type ConfirmationTitleProps,
|
|
56
|
-
type
|
|
57
|
-
type ConfirmationAcceptedProps,
|
|
58
|
-
type ConfirmationRejectedProps,
|
|
54
|
+
type ConfirmationStateProps,
|
|
59
55
|
type ConfirmationActionsProps,
|
|
60
56
|
type ConfirmationActionProps,
|
|
61
57
|
type ConfirmationContextValue,
|
|
62
|
-
type
|
|
58
|
+
type ConfirmationKind,
|
|
63
59
|
} from "./confirmation/index.js";
|
|
64
60
|
|
|
65
|
-
// =========================
|
|
66
|
-
// THINKING DOTS
|
|
67
|
-
// =========================
|
|
68
|
-
|
|
69
|
-
export { ThinkingDots, type ThinkingDotsProps } from "./thinking-dots/index.js";
|
|
70
|
-
|
|
71
61
|
// =========================
|
|
72
62
|
// VOICE WAVEFORM
|
|
73
63
|
// =========================
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
3
|
|
|
4
|
-
export interface TypingDotsProps extends HTMLAttributes<
|
|
4
|
+
export interface TypingDotsProps extends HTMLAttributes<HTMLElement> {
|
|
5
5
|
/** Dot diameter in px. */
|
|
6
6
|
size?: number;
|
|
7
|
+
/** Set, even to "", the dots become a `role="status"` line; the text shows when non-empty. */
|
|
8
|
+
label?: string;
|
|
7
9
|
class?: string;
|
|
8
10
|
}
|
|
9
11
|
</script>
|
|
@@ -12,16 +14,34 @@
|
|
|
12
14
|
import "./TypingDots.css";
|
|
13
15
|
import { cn } from "../../lib/utils.js";
|
|
14
16
|
|
|
15
|
-
let { size = 4, class: className, ...rest }: TypingDotsProps = $props();
|
|
17
|
+
let { size = 4, label, class: className, ...rest }: TypingDotsProps = $props();
|
|
16
18
|
</script>
|
|
17
19
|
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
{#snippet dots(extra?: HTMLAttributes<HTMLElement>, dotsClass?: string)}
|
|
21
|
+
<span
|
|
22
|
+
class={cn("typing-dots", dotsClass)}
|
|
23
|
+
style:--typing-dot-size="{size}px"
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
{...extra}
|
|
26
|
+
>
|
|
27
|
+
<span class="typing-dot"></span>
|
|
28
|
+
<span class="typing-dot"></span>
|
|
29
|
+
<span class="typing-dot"></span>
|
|
30
|
+
</span>
|
|
31
|
+
{/snippet}
|
|
32
|
+
|
|
33
|
+
{#if label === undefined}
|
|
34
|
+
{@render dots(rest, className)}
|
|
35
|
+
{:else}
|
|
36
|
+
<div
|
|
37
|
+
class={cn(
|
|
38
|
+
"inline-flex items-center gap-2 text-sm text-[color:var(--ai-thinking-fg,inherit)]",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
role="status"
|
|
42
|
+
{...rest}
|
|
43
|
+
>
|
|
44
|
+
{#if label}<span>{label}</span>{/if}
|
|
45
|
+
{@render dots()}
|
|
46
|
+
</div>
|
|
47
|
+
{/if}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { flushSync, mount, unmount } from "svelte";
|
|
2
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
3
|
+
import TypingDots, { type TypingDotsProps } from "./TypingDots.svelte";
|
|
4
|
+
|
|
5
|
+
function render(props: TypingDotsProps): HTMLElement {
|
|
6
|
+
const target = document.createElement("div");
|
|
7
|
+
const app = mount(TypingDots, { target, props });
|
|
8
|
+
flushSync();
|
|
9
|
+
const root = target.firstElementChild as HTMLElement;
|
|
10
|
+
void unmount(app);
|
|
11
|
+
return root;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("TypingDots", () => {
|
|
15
|
+
it("is three hidden dots with no label", () => {
|
|
16
|
+
const root = render({});
|
|
17
|
+
expect(root.getAttribute("aria-hidden")).toBe("true");
|
|
18
|
+
expect(root.getAttribute("role")).toBeNull();
|
|
19
|
+
expect(root.querySelectorAll(".typing-dot")).toHaveLength(3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("becomes a status line with its text when labelled", () => {
|
|
23
|
+
const root = render({ label: "Thinking", class: "extra" });
|
|
24
|
+
expect(root.getAttribute("role")).toBe("status");
|
|
25
|
+
expect(root.textContent?.trim()).toBe("Thinking");
|
|
26
|
+
expect(root.classList.contains("extra")).toBe(true);
|
|
27
|
+
expect(root.querySelector(".typing-dots")?.getAttribute("aria-hidden")).toBe("true");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("keeps the status role and the caller's aria-label for an empty label", () => {
|
|
31
|
+
const root = render({ label: "", "aria-label": "Thinking" });
|
|
32
|
+
expect(root.getAttribute("role")).toBe("status");
|
|
33
|
+
expect(root.getAttribute("aria-label")).toBe("Thinking");
|
|
34
|
+
expect(root.textContent?.trim()).toBe("");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -70,6 +70,18 @@
|
|
|
70
70
|
background: var(--cv-surface);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* On a phone the rail runs along the bottom edge and the credit would sit under
|
|
74
|
+
it, in the frame's rounded corner. It moves to the top right, inset off the
|
|
75
|
+
corner's curve. */
|
|
76
|
+
@media (max-width: 639px) {
|
|
77
|
+
.svelte-flow .svelte-flow__panel.svelte-flow__attribution {
|
|
78
|
+
top: 0;
|
|
79
|
+
bottom: auto;
|
|
80
|
+
margin: 6px 8px;
|
|
81
|
+
border-radius: var(--radius-sm, 4px);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
.svelte-flow .svelte-flow__attribution a {
|
|
74
86
|
color: var(--cv-ink-muted);
|
|
75
87
|
}
|
package/src/charts/README.md
CHANGED
|
@@ -71,8 +71,8 @@ The `@source` line matters: the chart markup carries Tailwind classes, and Tailw
|
|
|
71
71
|
| --- | --- |
|
|
72
72
|
| `DashboardSection`, `PanelGrid`, `StatBand` | A named area, a row of panels side by side, a row of number cards |
|
|
73
73
|
| `StatTile` | A number card on `NumberChart`: a delta, a share track, a trend shape behind the figure, an optional link |
|
|
74
|
-
| `DataPanel
|
|
75
|
-
| `DataTable`, `
|
|
74
|
+
| `DataPanel` | A titled panel for a table or chart. The dashed empty state is `EmptyState surface="dashed"` from `@heyramzi/ui/components/list-page` |
|
|
75
|
+
| `DataTable`, `RatingChip` | The one data table: sortable heads (bind `sort`, the `SortState` TableView takes; a new column starts descending), a row that opens, columns that hide on a phone, share bars, loading and empty states. An A to E grade chip |
|
|
76
76
|
| `PeriodFilter`, `DAY_WINDOWS` | The 7, 30 and 90 day window, as a filter or as links |
|
|
77
77
|
| `CHART_HEIGHT`, `dataTableClass` and friends | The one chart height and the table skin |
|
|
78
78
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import Figure from "./figure.svelte";
|
|
13
13
|
import HoverFocus from "./hover-focus.svelte";
|
|
14
14
|
import { DOMAIN_MOTION, TOOLTIP_GAP, TOOLTIP_TOP } from "./motion.js";
|
|
15
|
+
import { valueTick } from "./primitives/chart-utils.js";
|
|
15
16
|
import Reveal from "./reveal.svelte";
|
|
16
17
|
import ReferenceLine, { type ChartReference } from "./reference-line.svelte";
|
|
17
18
|
import {
|
|
@@ -153,15 +154,7 @@
|
|
|
153
154
|
const fallback = $derived(data.length > 0 ? data[data.length - 1] : null);
|
|
154
155
|
const display = $derived(active ?? fallback);
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
const compact = new Intl.NumberFormat(undefined, {
|
|
158
|
-
notation: "compact",
|
|
159
|
-
maximumFractionDigits: 1,
|
|
160
|
-
});
|
|
161
|
-
function valueTick(v: number): string {
|
|
162
|
-
const full = formatValue(v);
|
|
163
|
-
return full.length > 6 && Number.isFinite(v) ? compact.format(v) : full;
|
|
164
|
-
}
|
|
157
|
+
const tick = $derived(valueTick(formatValue));
|
|
165
158
|
const figureRows = $derived(
|
|
166
159
|
display
|
|
167
160
|
? series.map((s) => ({
|
|
@@ -212,7 +205,7 @@
|
|
|
212
205
|
motion={DOMAIN_MOTION}
|
|
213
206
|
>
|
|
214
207
|
<Svg>
|
|
215
|
-
<Axis placement="left" format={(v) =>
|
|
208
|
+
<Axis placement="left" format={(v) => tick(Number(v))} ticks={4} />
|
|
216
209
|
<Axis placement="bottom" format={(v) => formatX(v)} ticks={ticksForWidth(xTicks)} />
|
|
217
210
|
<g class="hr-area-grid">
|
|
218
211
|
<Axis placement="left" grid rule={false} ticks={4} format={() => ""} />
|
|
@@ -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
|
|
@@ -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
|
<!-- =========================== -->
|
|
@@ -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={() => ""} />
|
|
@@ -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(
|
|
@@ -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
|
// =========================
|
|
@@ -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
|
};
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
label = "Value",
|
|
64
64
|
label2,
|
|
65
65
|
color = "var(--chart-series-1)",
|
|
66
|
-
color2 = "var(--
|
|
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
|
|