@heyramzi/ui 1.54.2 → 2.0.1
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 -2
- package/README.md +34 -9
- package/package.json +205 -13
- package/scripts/sync-shadcn.mjs +2 -0
- package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
- package/src/ambient.d.ts +8 -0
- package/src/brand/brand.test.ts +67 -0
- package/src/brand/index.ts +137 -0
- package/src/canvas/README.md +236 -0
- package/src/canvas/card/card-frame.svelte +61 -0
- package/src/canvas/card/card.css +39 -0
- package/src/canvas/card/resize-handles.svelte +119 -0
- package/src/canvas/card/resize.ts +33 -0
- package/src/canvas/chrome/chrome-zoom.svelte +120 -0
- package/src/canvas/chrome/chrome.css +231 -0
- package/src/canvas/chrome/lock-badge.svelte +18 -0
- package/src/canvas/chrome/side-handles.svelte +30 -0
- package/src/canvas/chrome/size-chip.svelte +45 -0
- package/src/canvas/chrome/snap-guides.svelte +69 -0
- package/src/canvas/components/icons/ToolIcon.svelte +30 -0
- package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
- package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
- package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
- package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
- package/src/canvas/edges/bend-handle.svelte +76 -0
- package/src/canvas/edges/edge-gradient.svelte +30 -0
- package/src/canvas/edges/edge-path.test.ts +58 -0
- package/src/canvas/edges/edge-path.ts +293 -0
- package/src/canvas/edges/gradient-edge.svelte +43 -0
- package/src/canvas/editor/context-menu.test.ts +65 -0
- package/src/canvas/editor/context-menu.ts +48 -0
- package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
- package/src/canvas/editor/dispatch.svelte.ts +205 -0
- package/src/canvas/editor/editor-context.ts +62 -0
- package/src/canvas/editor/editor-mode.ts +67 -0
- package/src/canvas/editor/frame.ts +98 -0
- package/src/canvas/editor/hand-context.ts +15 -0
- package/src/canvas/editor/history-core.test.ts +239 -0
- package/src/canvas/editor/history-core.ts +247 -0
- package/src/canvas/editor/keymap.test.ts +161 -0
- package/src/canvas/editor/keymap.ts +151 -0
- package/src/canvas/editor/layout-actions.test.ts +156 -0
- package/src/canvas/editor/layout-actions.ts +219 -0
- package/src/canvas/editor/lock.test.ts +68 -0
- package/src/canvas/editor/lock.ts +87 -0
- package/src/canvas/editor/rebase.test.ts +208 -0
- package/src/canvas/editor/rebase.ts +272 -0
- package/src/canvas/editor/style.ts +184 -0
- package/src/canvas/editor/text-field-context.ts +41 -0
- package/src/canvas/editor/z-order.test.ts +75 -0
- package/src/canvas/editor/z-order.ts +87 -0
- package/src/canvas/headless/alignment.test.ts +59 -0
- package/src/canvas/headless/alignment.ts +23 -0
- package/src/canvas/headless/break-apart-find.ts +418 -0
- package/src/canvas/headless/break-apart-pieces.ts +104 -0
- package/src/canvas/headless/break-apart-plan.ts +439 -0
- package/src/canvas/headless/break-apart-style.ts +106 -0
- package/src/canvas/headless/break-apart.ts +244 -0
- package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
- package/src/canvas/headless/canvas-cleanup.ts +102 -0
- package/src/canvas/headless/edge-anchors.test.ts +70 -0
- package/src/canvas/headless/edge-anchors.ts +143 -0
- package/src/canvas/headless/edge-memo.test.ts +58 -0
- package/src/canvas/headless/edge-memo.ts +32 -0
- package/src/canvas/headless/edge-serialization.test.ts +55 -0
- package/src/canvas/headless/edge-serialization.ts +32 -0
- package/src/canvas/headless/figure-geometry.ts +293 -0
- package/src/canvas/headless/fit-viewport.test.ts +85 -0
- package/src/canvas/headless/fit-viewport.ts +137 -0
- package/src/canvas/headless/grid.test.ts +34 -0
- package/src/canvas/headless/grid.ts +94 -0
- package/src/canvas/headless/icon-node.test.ts +23 -0
- package/src/canvas/headless/icon-node.ts +66 -0
- package/src/canvas/headless/index.ts +9 -0
- package/src/canvas/headless/linkify.test.ts +23 -0
- package/src/canvas/headless/linkify.ts +25 -0
- package/src/canvas/headless/pen-marks.ts +133 -0
- package/src/canvas/headless/sketch.test.ts +99 -0
- package/src/canvas/headless/sketch.ts +302 -0
- package/src/canvas/headless/snap-guides.test.ts +61 -0
- package/src/canvas/headless/snap-guides.ts +146 -0
- package/src/canvas/headless/svg-figure.test.ts +49 -0
- package/src/canvas/headless/svg-figure.ts +176 -0
- package/src/canvas/headless/table-layout.test.ts +158 -0
- package/src/canvas/headless/table-layout.ts +309 -0
- package/src/canvas/index.ts +35 -0
- package/src/canvas/lod/lod-mode.svelte.ts +30 -0
- package/src/canvas/lod/lod-switch.svelte +140 -0
- package/src/canvas/lod/render-all.svelte.ts +52 -0
- package/src/canvas/ops/arrange.ts +108 -0
- package/src/canvas/ops/ops.test.ts +98 -0
- package/src/canvas/ops/ops.ts +347 -0
- package/src/canvas/pen/PenField.svelte +128 -0
- package/src/canvas/pen/PenField.test.ts +87 -0
- package/src/canvas/pen/PenOverlay.svelte +172 -0
- package/src/canvas/pen/ShapeNode.svelte +64 -0
- package/src/canvas/pen/TextNode.svelte +97 -0
- package/src/canvas/pen/colors.ts +28 -0
- package/src/canvas/pen/eraser.ts +46 -0
- package/src/canvas/pen/geometry.ts +148 -0
- package/src/canvas/pen/host-field.test.svelte +13 -0
- package/src/canvas/pen/index.ts +40 -0
- package/src/canvas/pen/ops.ts +16 -0
- package/src/canvas/pen/pen-session.ts +177 -0
- package/src/canvas/pen/pen.css +149 -0
- package/src/canvas/pen/plugin.ts +139 -0
- package/src/canvas/pen/shapes.ts +104 -0
- package/src/canvas/pen/stroke-path.ts +54 -0
- package/src/canvas/pen/types.ts +96 -0
- package/src/canvas/pen/ui.ts +26 -0
- package/src/canvas/plugin.test.ts +45 -0
- package/src/canvas/plugin.ts +159 -0
- package/src/canvas/screen/GlyphIcon.svelte +25 -0
- package/src/canvas/screen/PartView.svelte +176 -0
- package/src/canvas/screen/ScreenBody.svelte +142 -0
- package/src/canvas/screen/ScreenCard.svelte +32 -0
- package/src/canvas/screen/glyphs/button.svg +1 -0
- package/src/canvas/screen/glyphs/close.svg +1 -0
- package/src/canvas/screen/glyphs/down.svg +1 -0
- package/src/canvas/screen/glyphs/header.svg +1 -0
- package/src/canvas/screen/glyphs/hills.svg +1 -0
- package/src/canvas/screen/glyphs/image.svg +1 -0
- package/src/canvas/screen/glyphs/input.svg +1 -0
- package/src/canvas/screen/glyphs/list.svg +1 -0
- package/src/canvas/screen/glyphs/modal.svg +1 -0
- package/src/canvas/screen/glyphs/plus.svg +1 -0
- package/src/canvas/screen/glyphs/up.svg +1 -0
- package/src/canvas/screen/glyphs.ts +40 -0
- package/src/canvas/screen/labels.ts +41 -0
- package/src/canvas/screen/parts.ts +164 -0
- package/src/canvas/screen/plugin.ts +67 -0
- package/src/canvas/screen/screen.css +593 -0
- package/src/canvas/screen/types.ts +47 -0
- package/src/canvas/screen/ui.ts +13 -0
- package/src/canvas/test-utils.ts +18 -0
- package/src/canvas/types.ts +53 -0
- package/src/canvas/ui/CanvasToolbar.svelte +169 -0
- package/src/canvas/ui/FrameLayer.svelte +116 -0
- package/src/canvas/ui/PenLayer.svelte +39 -0
- package/src/canvas/ui/StylePanel.svelte +268 -0
- package/src/canvas/ui/canvas-grid.svelte +156 -0
- package/src/canvas/ui/canvas-ui.css +350 -0
- package/src/canvas/ui/forward-wheel.ts +29 -0
- package/src/canvas/ui/icons.ts +124 -0
- package/src/canvas/ui/place-panel.ts +80 -0
- package/src/canvas/ui/shortcut-tip.test.ts +16 -0
- package/src/canvas/ui/shortcut-tip.ts +17 -0
- package/src/canvas/ui/tools.ts +146 -0
- package/src/charts/README.md +104 -0
- package/src/charts/ambient.d.ts +4 -0
- package/src/charts/animated-value.css +7 -0
- package/src/charts/animated-value.svelte +110 -0
- package/src/charts/area-chart.css +29 -0
- package/src/charts/area-chart.svelte +204 -0
- package/src/charts/bar-chart.css +119 -0
- package/src/charts/bar-chart.svelte +275 -0
- package/src/charts/charts.css +201 -0
- package/src/charts/crosshair.svelte +35 -0
- package/src/charts/dashboard/chart.ts +5 -0
- package/src/charts/dashboard/dashboard-empty.svelte +55 -0
- package/src/charts/dashboard/dashboard-section.svelte +58 -0
- package/src/charts/dashboard/data-panel.svelte +81 -0
- package/src/charts/dashboard/data-sort-head.svelte +102 -0
- package/src/charts/dashboard/data-table.svelte +278 -0
- package/src/charts/dashboard/data-table.ts +26 -0
- package/src/charts/dashboard/index.ts +25 -0
- package/src/charts/dashboard/panel-grid.svelte +43 -0
- package/src/charts/dashboard/period-filter.svelte +53 -0
- package/src/charts/dashboard/rating-chip.svelte +35 -0
- package/src/charts/dashboard/stat-band.svelte +47 -0
- package/src/charts/dashboard/stat-tile.svelte +176 -0
- package/src/charts/dashboard/windows.ts +8 -0
- package/src/charts/donut-chart.css +56 -0
- package/src/charts/donut-chart.svelte +158 -0
- package/src/charts/edge-fade.svelte +43 -0
- package/src/charts/edge-fade.ts +16 -0
- package/src/charts/figure.css +100 -0
- package/src/charts/figure.svelte +122 -0
- package/src/charts/gauge-chart.css +94 -0
- package/src/charts/gauge-chart.svelte +302 -0
- package/src/charts/hover-band.test.ts +30 -0
- package/src/charts/hover-band.ts +21 -0
- package/src/charts/hover-focus.svelte +85 -0
- package/src/charts/index.ts +31 -0
- package/src/charts/legend.css +136 -0
- package/src/charts/legend.svelte +112 -0
- package/src/charts/line-chart.css +26 -0
- package/src/charts/line-chart.svelte +256 -0
- package/src/charts/motion.ts +19 -0
- package/src/charts/number-chart.css +179 -0
- package/src/charts/number-chart.svelte +187 -0
- package/src/charts/primitives/chart-container.svelte +57 -0
- package/src/charts/primitives/chart-style.svelte +36 -0
- package/src/charts/primitives/chart-tooltip.svelte +161 -0
- package/src/charts/primitives/chart-utils.ts +54 -0
- package/src/charts/primitives/index.ts +11 -0
- package/src/charts/reference-line.css +18 -0
- package/src/charts/reference-line.svelte +31 -0
- package/src/charts/reveal.svelte +61 -0
- package/src/charts/ring-chart.css +104 -0
- package/src/charts/ring-chart.svelte +166 -0
- package/src/charts/sparkline.css +45 -0
- package/src/charts/sparkline.svelte +141 -0
- package/src/charts/stat-card.css +105 -0
- package/src/charts/stat-card.svelte +267 -0
- package/src/charts/time-series-chart.css +28 -0
- package/src/charts/time-series-chart.svelte +179 -0
- package/src/charts/trend-pill.css +38 -0
- package/src/charts/trend-pill.svelte +48 -0
- package/src/components/copy/copy-button.svelte +1 -1
- package/src/components/docs/doc-page.svelte +1 -1
- package/src/components/docs/on-this-page.svelte +2 -2
- package/src/components/list-page/empty-state.svelte +1 -1
- package/src/components/list-page/filter-popover.svelte +1 -1
- package/src/components/list-page/section-label.svelte +1 -1
- package/src/components/segmented/segment.svelte +4 -4
- package/src/components/theme/theme-provider.svelte +6 -4
- package/src/components/toolbar/index.ts +5 -0
- package/src/components/toolbar/toolbar.svelte +168 -0
- package/src/components/toolbar/toolbar.test.ts +66 -0
- package/src/editor/README.md +58 -0
- package/src/editor/ambient.d.ts +1 -0
- package/src/editor/block-id.ts +77 -0
- package/src/editor/block-ops.test.ts +237 -0
- package/src/editor/block-ops.ts +157 -0
- package/src/editor/content.css +418 -0
- package/src/editor/editor-toolbar.svelte +303 -0
- package/src/editor/editor.svelte +469 -0
- package/src/editor/extensions.ts +145 -0
- package/src/editor/image-toolbar.svelte +123 -0
- package/src/editor/index.ts +17 -0
- package/src/editor/inline-editor.svelte +261 -0
- package/src/editor/markdown-escaping.test.ts +158 -0
- package/src/editor/markdown-marks.test.ts +89 -0
- package/src/editor/markdown-test-helpers.ts +13 -0
- package/src/editor/markdown-youtube.test.ts +93 -0
- package/src/editor/markdown.test.ts +250 -0
- package/src/editor/markdown.ts +511 -0
- package/src/editor/renderer.svelte +92 -0
- package/src/editor/serializer.test.ts +133 -0
- package/src/editor/serializer.ts +370 -0
- package/src/editor/slash-items.ts +115 -0
- package/src/editor/slash-menu.svelte +159 -0
- package/src/editor/youtube-url.ts +35 -0
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
- package/src/gallery.test.ts +20 -1
- package/src/lib/result.ts +19 -0
- package/src/onboarding/README.md +120 -0
- package/src/onboarding/define-tours.test.ts +50 -0
- package/src/onboarding/define-tours.ts +31 -0
- package/src/onboarding/demo.svelte +86 -0
- package/src/onboarding/index.ts +12 -0
- package/src/onboarding/onboarding-provider.svelte +120 -0
- package/src/onboarding/onboarding.svelte.ts +24 -0
- package/src/onboarding/pick.test.ts +79 -0
- package/src/onboarding/pick.ts +36 -0
- package/src/onboarding/stores.test.ts +103 -0
- package/src/onboarding/stores.ts +58 -0
- package/src/onboarding/tour-card.svelte +70 -0
- package/src/onboarding/tour-runner.svelte +197 -0
- package/src/onboarding/types.ts +34 -0
- package/src/onboarding/wait.ts +29 -0
- package/src/shadcn/command/command-input.svelte +1 -1
- package/src/slides/Controls.svelte +138 -0
- package/src/slides/Deck.svelte +271 -0
- package/src/slides/Fragment.svelte +71 -0
- package/src/slides/Overview.svelte +113 -0
- package/src/slides/Presenter.svelte +292 -0
- package/src/slides/README.md +89 -0
- package/src/slides/Slide.svelte +179 -0
- package/src/slides/SlideList.svelte +19 -0
- package/src/slides/ambient.d.ts +1 -0
- package/src/slides/bridges.ts +69 -0
- package/src/slides/deck-controller.svelte.ts +292 -0
- package/src/slides/demo-slides.svelte +358 -0
- package/src/slides/demo.svelte +176 -0
- package/src/slides/export/capture.ts +113 -0
- package/src/slides/export/ffmpeg.ts +120 -0
- package/src/slides/export/index.ts +9 -0
- package/src/slides/export/launch.ts +10 -0
- package/src/slides/export/pdf.ts +152 -0
- package/src/slides/export/types.ts +48 -0
- package/src/slides/index.ts +19 -0
- package/src/slides/labels.ts +48 -0
- package/src/slides/layout.ts +51 -0
- package/src/slides/model.ts +47 -0
- package/src/slides/navigation.ts +100 -0
- package/src/slides/theme.css +205 -0
- package/src/slides/types.ts +86 -0
- package/src/tasks/README.md +312 -0
- package/src/tasks/ambient.d.ts +3 -0
- package/src/tasks/board-order.ts +137 -0
- package/src/tasks/dates.ts +31 -0
- package/src/tasks/define-statuses.ts +32 -0
- package/src/tasks/demo.svelte +389 -0
- package/src/tasks/detail/detail-demo.svelte +186 -0
- package/src/tasks/detail/detail-types.ts +54 -0
- package/src/tasks/detail/task-chip.svelte +28 -0
- package/src/tasks/detail/task-detail.svelte +315 -0
- package/src/tasks/detail/task-property.svelte +17 -0
- package/src/tasks/detail/task-thread.svelte +258 -0
- package/src/tasks/gantt/gantt-bar.svelte +243 -0
- package/src/tasks/gantt/gantt-grid.svelte +94 -0
- package/src/tasks/gantt/gantt-group-row.svelte +55 -0
- package/src/tasks/gantt/gantt-header.svelte +71 -0
- package/src/tasks/gantt/gantt-task-row.svelte +186 -0
- package/src/tasks/gantt/gantt.css +22 -0
- package/src/tasks/gantt/gantt.test.ts +77 -0
- package/src/tasks/gantt/logic.test.ts +163 -0
- package/src/tasks/gantt/logic.ts +248 -0
- package/src/tasks/inbox/inbox-card.svelte +283 -0
- package/src/tasks/inbox/inbox-draft.svelte +84 -0
- package/src/tasks/inbox/inbox-logic.ts +55 -0
- package/src/tasks/inbox/inbox-statuses.ts +48 -0
- package/src/tasks/inbox/inbox-trace.svelte +45 -0
- package/src/tasks/inbox/inbox-types.ts +108 -0
- package/src/tasks/inbox/inbox.svelte +203 -0
- package/src/tasks/inbox/index.ts +23 -0
- package/src/tasks/index.ts +85 -0
- package/src/tasks/kanban-board.svelte +561 -0
- package/src/tasks/kanban-column.svelte +117 -0
- package/src/tasks/kanban-drag.ts +38 -0
- package/src/tasks/kanban-types.ts +65 -0
- package/src/tasks/presets/sales.ts +16 -0
- package/src/tasks/record/demo-data.ts +286 -0
- package/src/tasks/record/field-section.svelte +53 -0
- package/src/tasks/record/fields.ts +142 -0
- package/src/tasks/record/format.ts +87 -0
- package/src/tasks/record/gutter.ts +2 -0
- package/src/tasks/record/index.ts +54 -0
- package/src/tasks/record/info-list.svelte +316 -0
- package/src/tasks/record/kinds.ts +42 -0
- package/src/tasks/record/logic.ts +324 -0
- package/src/tasks/record/next-up.svelte +70 -0
- package/src/tasks/record/project-list.svelte +133 -0
- package/src/tasks/record/record-demo.svelte +324 -0
- package/src/tasks/record/record-header.svelte +70 -0
- package/src/tasks/record/record-shell.svelte +138 -0
- package/src/tasks/record/record-stats.svelte +48 -0
- package/src/tasks/record/record-timeline.svelte +231 -0
- package/src/tasks/record/record.test.ts +350 -0
- package/src/tasks/record/steps-rail.svelte +474 -0
- package/src/tasks/record/types.ts +280 -0
- package/src/tasks/record/where-we-are.svelte +121 -0
- package/src/tasks/reorder.ts +19 -0
- package/src/tasks/side-panel-types.ts +16 -0
- package/src/tasks/side-panel.svelte +193 -0
- package/src/tasks/status-badge.svelte +63 -0
- package/src/tasks/status-dot.svelte +228 -0
- package/src/tasks/status-icon.svelte +30 -0
- package/src/tasks/status-types.ts +40 -0
- package/src/tasks/task-gantt.svelte +230 -0
- package/src/tasks/task-list.svelte +470 -0
- package/src/tasks/task-types.ts +33 -0
- package/src/tasks/time/duration.ts +69 -0
- package/src/tasks/time/time-chip.svelte +62 -0
- package/src/tasks/time/time-dot.svelte +58 -0
- package/src/tasks/time/time-ring.svelte +74 -0
- package/src/tasks/time/time-tracker.svelte +565 -0
- package/src/tasks/time/time-types.ts +74 -0
- package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
- package/src/tokens/base.css +5 -5
- package/src/tokens/palette.css +115 -120
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Which way a metric has to move to be good news.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* `neutral` is the honest answer for a figure that is neither on its own: spend is not a win or a loss until the return is beside it.
|
|
8
|
+
*/
|
|
9
|
+
export type NumberDirection = "up-good" | "down-good" | "neutral";
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
// =========================
|
|
14
|
+
// IMPORTS
|
|
15
|
+
// =========================
|
|
16
|
+
import "./number-chart.css";
|
|
17
|
+
import { Minus, TrendDown, TrendUp } from "phosphor-svelte";
|
|
18
|
+
import { cn } from "../lib/utils.js";
|
|
19
|
+
import type { Snippet } from "svelte";
|
|
20
|
+
import AnimatedValue from "./animated-value.svelte";
|
|
21
|
+
|
|
22
|
+
// =========================
|
|
23
|
+
// TYPES
|
|
24
|
+
// =========================
|
|
25
|
+
interface Props {
|
|
26
|
+
/** Metric name, above the figure. */
|
|
27
|
+
label: string;
|
|
28
|
+
/**
|
|
29
|
+
* The figure.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* A number counts up and is formatted here; a string is taken as already formatted and is printed as it stands, which is the only way to draw a total
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* that mixes two currencies.
|
|
38
|
+
*/
|
|
39
|
+
value: number | string;
|
|
40
|
+
/** Formatter for a numeric `value`. Wins over `formatOptions`. */
|
|
41
|
+
format?: (v: number) => string;
|
|
42
|
+
/** Intl options for a numeric `value` (currency, compact, percent). */
|
|
43
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
44
|
+
/** BCP-47 locale for `formatOptions`. */
|
|
45
|
+
locale?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Change against the period before this one, as a percentage. `null` is
|
|
48
|
+
* "nothing to compare against", which is not zero and is drawn as a dash.
|
|
49
|
+
*/
|
|
50
|
+
delta?: number | null;
|
|
51
|
+
/** Which way `delta` has to point to be good news. */
|
|
52
|
+
direction?: NumberDirection;
|
|
53
|
+
/** Movement below this many points is noise and draws as a dash. */
|
|
54
|
+
deltaThreshold?: number;
|
|
55
|
+
/**
|
|
56
|
+
* The chart. A fraction from 0 to 1 that gives the figure its scale: a share of a total, a run against a target, a rate against its ceiling.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* Omitted, the number stands on its own and no track is drawn.
|
|
61
|
+
*/
|
|
62
|
+
share?: number;
|
|
63
|
+
/** What the track measures, e.g. "of revenue". Only with `share`. */
|
|
64
|
+
shareLabel?: string;
|
|
65
|
+
/** Track colour. Defaults to the first series token. */
|
|
66
|
+
color?: string;
|
|
67
|
+
/** md is a figure in a row; lg is the one figure the screen exists to answer. */
|
|
68
|
+
size?: "md" | "lg";
|
|
69
|
+
/** A quiet line under the figure: the window it covers, or why it is a dash. */
|
|
70
|
+
caption?: string;
|
|
71
|
+
/** A loss or an overrun: the figure turns the destructive colour. */
|
|
72
|
+
negative?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Drop the card chrome. For a figure that sits in a band or a panel of its
|
|
75
|
+
* own, where a second border would draw a box inside a box.
|
|
76
|
+
*/
|
|
77
|
+
bare?: boolean;
|
|
78
|
+
/** Leading icon before the label. */
|
|
79
|
+
icon?: Snippet;
|
|
80
|
+
/** Rendered at the right of the label row. */
|
|
81
|
+
trailing?: Snippet;
|
|
82
|
+
class?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// =========================
|
|
86
|
+
// PROPS
|
|
87
|
+
// =========================
|
|
88
|
+
let {
|
|
89
|
+
label,
|
|
90
|
+
value,
|
|
91
|
+
format,
|
|
92
|
+
formatOptions,
|
|
93
|
+
locale,
|
|
94
|
+
delta = null,
|
|
95
|
+
direction = "neutral",
|
|
96
|
+
deltaThreshold = 0.5,
|
|
97
|
+
share,
|
|
98
|
+
shareLabel,
|
|
99
|
+
color = "var(--chart-1)",
|
|
100
|
+
size = "md",
|
|
101
|
+
caption,
|
|
102
|
+
negative = false,
|
|
103
|
+
bare = false,
|
|
104
|
+
icon,
|
|
105
|
+
trailing,
|
|
106
|
+
class: className,
|
|
107
|
+
}: Props = $props();
|
|
108
|
+
|
|
109
|
+
// =========================
|
|
110
|
+
// DERIVED
|
|
111
|
+
// =========================
|
|
112
|
+
// Under half a point either way is noise, not a movement, and eight figures
|
|
113
|
+
// each reporting a 0% move is eight pieces of chrome saying nothing.
|
|
114
|
+
const move = $derived(delta != null && Math.abs(delta) >= deltaThreshold ? delta : null);
|
|
115
|
+
// The arrow follows the sign and the colour follows the news, because a cost
|
|
116
|
+
// that fell is a green figure with a down arrow. One glyph cannot say both.
|
|
117
|
+
const tone = $derived.by(() => {
|
|
118
|
+
if (move == null || direction === "neutral") return "flat";
|
|
119
|
+
const good = direction === "up-good" ? move > 0 : move < 0;
|
|
120
|
+
return good ? "up" : "down";
|
|
121
|
+
});
|
|
122
|
+
// The track stops at full; the label says the real figure, so a 111% overrun never reads 100%.
|
|
123
|
+
const pct = $derived(share == null ? null : Math.max(0, Math.min(1, share)));
|
|
124
|
+
const sharePct = $derived(share == null ? null : Math.round(Math.max(0, share) * 100));
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<!-- =========================== -->
|
|
128
|
+
<!-- NUMBER CHART -->
|
|
129
|
+
<!-- =========================== -->
|
|
130
|
+
<div
|
|
131
|
+
class={cn("hr-number-chart", className)}
|
|
132
|
+
data-size={size}
|
|
133
|
+
data-bare={bare ? "" : undefined}
|
|
134
|
+
data-negative={negative ? "" : undefined}
|
|
135
|
+
>
|
|
136
|
+
<p class="hr-number-label">
|
|
137
|
+
{#if icon}<span class="hr-number-icon" style:color>{@render icon()}</span>{/if}
|
|
138
|
+
<span class="hr-number-label-text">{label}</span>
|
|
139
|
+
{#if trailing}<span class="hr-number-trailing">{@render trailing()}</span>{/if}
|
|
140
|
+
</p>
|
|
141
|
+
|
|
142
|
+
<div class="hr-number-row">
|
|
143
|
+
<span class="hr-number-value">
|
|
144
|
+
{#if typeof value === "number"}
|
|
145
|
+
<AnimatedValue {value} {format} {formatOptions} {locale} duration={600} />
|
|
146
|
+
{:else}
|
|
147
|
+
{value}
|
|
148
|
+
{/if}
|
|
149
|
+
</span>
|
|
150
|
+
{#if move != null}
|
|
151
|
+
<span class="hr-number-delta" data-tone={tone}>
|
|
152
|
+
{#if move > 0}
|
|
153
|
+
<TrendUp class="hr-number-arrow" weight="bold" />
|
|
154
|
+
{:else}
|
|
155
|
+
<TrendDown class="hr-number-arrow" weight="bold" />
|
|
156
|
+
{/if}
|
|
157
|
+
<span class="hr-number-delta-text">{Math.abs(move).toFixed(0)}%</span>
|
|
158
|
+
</span>
|
|
159
|
+
{:else}
|
|
160
|
+
<span class="hr-number-delta" data-tone="none" aria-hidden="true">
|
|
161
|
+
<Minus class="hr-number-arrow" />
|
|
162
|
+
</span>
|
|
163
|
+
{/if}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{#if pct != null}
|
|
167
|
+
<!-- WHY the number gets a rail and not a sparkline: one figure has no series to plot, and the question a lone number cannot answer is "out of -->
|
|
168
|
+
<!-- what". The rail answers exactly that and nothing else. -->
|
|
169
|
+
<div
|
|
170
|
+
class="hr-number-track"
|
|
171
|
+
role="img"
|
|
172
|
+
aria-label="{label}: {sharePct}% {shareLabel ?? 'of the total'}"
|
|
173
|
+
>
|
|
174
|
+
<span class="hr-number-fill" style:--hr-number-share={pct} style:background={color}></span>
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
|
|
178
|
+
{#if shareLabel && pct != null}
|
|
179
|
+
<p class="hr-number-caption">
|
|
180
|
+
<span class="hr-number-share">{sharePct}%</span>
|
|
181
|
+
{shareLabel}
|
|
182
|
+
</p>
|
|
183
|
+
{/if}
|
|
184
|
+
{#if caption}
|
|
185
|
+
<p class="hr-number-caption">{caption}</p>
|
|
186
|
+
{/if}
|
|
187
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { cn, type WithElementRef } from "../../lib/utils.js";
|
|
4
|
+
import ChartStyle from "./chart-style.svelte";
|
|
5
|
+
import { type ChartConfig, setChartContext } from "./chart-utils.js";
|
|
6
|
+
|
|
7
|
+
const uid = $props.id();
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
ref = $bindable(null),
|
|
11
|
+
id = uid,
|
|
12
|
+
class: className,
|
|
13
|
+
children,
|
|
14
|
+
config,
|
|
15
|
+
...restProps
|
|
16
|
+
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
17
|
+
config: ChartConfig;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
const chartId = $derived(`chart-${id || uid.replace(/:/g, "")}`);
|
|
21
|
+
|
|
22
|
+
setChartContext({
|
|
23
|
+
get config() {
|
|
24
|
+
return config;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
bind:this={ref}
|
|
31
|
+
data-chart={chartId}
|
|
32
|
+
data-slot="chart"
|
|
33
|
+
class={cn(
|
|
34
|
+
"flex aspect-video justify-center overflow-visible text-xs",
|
|
35
|
+
"[&_.stroke-white]:stroke-transparent",
|
|
36
|
+
"[&_.lc-line]:stroke-border/50",
|
|
37
|
+
"[&_.lc-highlight-line]:stroke-0",
|
|
38
|
+
"[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text-svg]:overflow-visible [&_.lc-text]:text-xs",
|
|
39
|
+
"[&_.lc-axis-tick]:stroke-0",
|
|
40
|
+
"[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0",
|
|
41
|
+
"[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border",
|
|
42
|
+
"[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border",
|
|
43
|
+
"[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5",
|
|
44
|
+
"[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4",
|
|
45
|
+
"[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]",
|
|
46
|
+
"[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent",
|
|
47
|
+
"[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal",
|
|
48
|
+
"[&_.lc-tooltip-rects-g]:fill-transparent",
|
|
49
|
+
"[&_.lc-layout-svg-g]:fill-transparent",
|
|
50
|
+
"[&_.lc-root-container]:w-full",
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...restProps}
|
|
54
|
+
>
|
|
55
|
+
<ChartStyle id={chartId} {config} />
|
|
56
|
+
{@render children?.()}
|
|
57
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { type ChartConfig, THEMES } from "./chart-utils.js";
|
|
3
|
+
|
|
4
|
+
let { id, config }: { id: string; config: ChartConfig } = $props();
|
|
5
|
+
|
|
6
|
+
const colorConfig = $derived(
|
|
7
|
+
config ? Object.entries(config).filter(([, c]) => c.theme || c.color) : null,
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const themeContents = $derived.by(() => {
|
|
11
|
+
if (!colorConfig || !colorConfig.length) return;
|
|
12
|
+
|
|
13
|
+
const blocks: string[] = [];
|
|
14
|
+
for (const [_theme, prefix] of Object.entries(THEMES)) {
|
|
15
|
+
let content = `${prefix} [data-chart=${id}] {\n`;
|
|
16
|
+
const color = colorConfig.map(([key, itemConfig]) => {
|
|
17
|
+
const theme = _theme as keyof typeof itemConfig.theme;
|
|
18
|
+
const value = itemConfig.theme?.[theme] || itemConfig.color;
|
|
19
|
+
return value ? `\t--color-${key}: ${value};` : null;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
content += `${color.join("\n")}\n}`;
|
|
23
|
+
blocks.push(content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return blocks.join("\n");
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
{#if themeContents}
|
|
31
|
+
{#key id}
|
|
32
|
+
<svelte:element this={"style"}>
|
|
33
|
+
{themeContents}
|
|
34
|
+
</svelte:element>
|
|
35
|
+
{/key}
|
|
36
|
+
{/if}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getChartContext, Tooltip as TooltipPrimitive } from "layerchart";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
5
|
+
import { cn, type WithElementRef, type WithoutChildren } from "../../lib/utils.js";
|
|
6
|
+
import {
|
|
7
|
+
getPayloadConfigFromPayload,
|
|
8
|
+
type TooltipSeriesItem,
|
|
9
|
+
useChart,
|
|
10
|
+
} from "./chart-utils.js";
|
|
11
|
+
|
|
12
|
+
function defaultFormatter(value: unknown, _series: TooltipSeriesItem[]) {
|
|
13
|
+
return `${value}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
ref = $bindable(null),
|
|
18
|
+
class: className,
|
|
19
|
+
hideLabel = false,
|
|
20
|
+
indicator = "dot",
|
|
21
|
+
hideIndicator = false,
|
|
22
|
+
labelKey,
|
|
23
|
+
label,
|
|
24
|
+
labelFormatter = defaultFormatter,
|
|
25
|
+
labelClassName,
|
|
26
|
+
formatter,
|
|
27
|
+
nameKey,
|
|
28
|
+
color,
|
|
29
|
+
...restProps
|
|
30
|
+
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
|
|
31
|
+
hideLabel?: boolean;
|
|
32
|
+
label?: string;
|
|
33
|
+
indicator?: "line" | "dot" | "dashed";
|
|
34
|
+
nameKey?: string;
|
|
35
|
+
labelKey?: string;
|
|
36
|
+
hideIndicator?: boolean;
|
|
37
|
+
labelClassName?: string;
|
|
38
|
+
labelFormatter?:
|
|
39
|
+
| ((value: unknown, series: TooltipSeriesItem[]) => string | number | Snippet)
|
|
40
|
+
| null;
|
|
41
|
+
formatter?: Snippet<
|
|
42
|
+
[
|
|
43
|
+
{
|
|
44
|
+
value: unknown;
|
|
45
|
+
name: string;
|
|
46
|
+
item: TooltipSeriesItem;
|
|
47
|
+
index: number;
|
|
48
|
+
series: TooltipSeriesItem[];
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
>;
|
|
52
|
+
} = $props();
|
|
53
|
+
|
|
54
|
+
const chart = useChart();
|
|
55
|
+
const ctx = getChartContext();
|
|
56
|
+
|
|
57
|
+
const tooltipSeries = $derived(ctx.tooltip.series as TooltipSeriesItem[]);
|
|
58
|
+
|
|
59
|
+
const formattedLabel = $derived.by(() => {
|
|
60
|
+
if (hideLabel || !tooltipSeries?.length) return null;
|
|
61
|
+
|
|
62
|
+
const [item] = tooltipSeries;
|
|
63
|
+
const key = labelKey ?? item?.label ?? item?.key ?? "value";
|
|
64
|
+
|
|
65
|
+
const itemConfig = getPayloadConfigFromPayload(chart.config, item, key);
|
|
66
|
+
|
|
67
|
+
const value =
|
|
68
|
+
!labelKey && typeof label === "string"
|
|
69
|
+
? (chart.config[label as keyof typeof chart.config]?.label ?? label)
|
|
70
|
+
: (itemConfig?.label ?? item.label);
|
|
71
|
+
|
|
72
|
+
if (value === undefined) return null;
|
|
73
|
+
if (!labelFormatter) return value;
|
|
74
|
+
return labelFormatter(value, tooltipSeries);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const nestLabel = $derived(tooltipSeries.length === 1 && indicator !== "dot");
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
{#snippet TooltipLabel()}
|
|
81
|
+
{#if formattedLabel}
|
|
82
|
+
<div class={cn("font-medium", labelClassName)}>
|
|
83
|
+
{#if typeof formattedLabel === "function"}
|
|
84
|
+
{@render formattedLabel()}
|
|
85
|
+
{:else}
|
|
86
|
+
{formattedLabel}
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
90
|
+
{/snippet}
|
|
91
|
+
|
|
92
|
+
<TooltipPrimitive.Root variant="none">
|
|
93
|
+
<div
|
|
94
|
+
class={cn(
|
|
95
|
+
"border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
{...restProps}
|
|
99
|
+
>
|
|
100
|
+
{#if !nestLabel}
|
|
101
|
+
{@render TooltipLabel()}
|
|
102
|
+
{/if}
|
|
103
|
+
<div class="grid gap-1.5">
|
|
104
|
+
{#each tooltipSeries as item, i (item.key + i)}
|
|
105
|
+
{@const key = `${nameKey || item.key || "value"}`}
|
|
106
|
+
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)}
|
|
107
|
+
{@const indicatorColor = color || item.color}
|
|
108
|
+
<div
|
|
109
|
+
class={cn(
|
|
110
|
+
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5",
|
|
111
|
+
indicator === "dot" && "items-center",
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{#if formatter && item.value !== undefined && item.key}
|
|
115
|
+
{@render formatter({
|
|
116
|
+
value: item.value,
|
|
117
|
+
name: item.label || item.key,
|
|
118
|
+
item,
|
|
119
|
+
index: i,
|
|
120
|
+
series: tooltipSeries,
|
|
121
|
+
})}
|
|
122
|
+
{:else}
|
|
123
|
+
{#if itemConfig?.icon}
|
|
124
|
+
<itemConfig.icon />
|
|
125
|
+
{:else if !hideIndicator}
|
|
126
|
+
<div
|
|
127
|
+
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
|
|
128
|
+
class={cn("border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]", {
|
|
129
|
+
"size-2.5": indicator === "dot",
|
|
130
|
+
"h-full w-1": indicator === "line",
|
|
131
|
+
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
|
132
|
+
"my-0.5": nestLabel && indicator === "dashed",
|
|
133
|
+
})}
|
|
134
|
+
></div>
|
|
135
|
+
{/if}
|
|
136
|
+
<div
|
|
137
|
+
class={cn(
|
|
138
|
+
"flex flex-1 shrink-0 justify-between leading-none",
|
|
139
|
+
nestLabel ? "items-end" : "items-center",
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
<div class="grid gap-1.5">
|
|
143
|
+
{#if nestLabel}
|
|
144
|
+
{@render TooltipLabel()}
|
|
145
|
+
{/if}
|
|
146
|
+
<span class="text-muted-foreground">
|
|
147
|
+
{itemConfig?.label || item.label || item.key}
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
{#if item.value !== undefined}
|
|
151
|
+
<span class="text-foreground font-mono font-medium tabular-nums">
|
|
152
|
+
{typeof item.value === "number" ? item.value.toLocaleString() : item.value}
|
|
153
|
+
</span>
|
|
154
|
+
{/if}
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
</div>
|
|
158
|
+
{/each}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</TooltipPrimitive.Root>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Component, getContext, setContext } from "svelte";
|
|
2
|
+
|
|
3
|
+
export const THEMES = { light: "", dark: ".dark" } as const;
|
|
4
|
+
|
|
5
|
+
export type ChartConfig = {
|
|
6
|
+
[k in string]: {
|
|
7
|
+
label?: string;
|
|
8
|
+
icon?: Component;
|
|
9
|
+
} & (
|
|
10
|
+
| { color?: string; theme?: never }
|
|
11
|
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type TooltipSeriesItem = {
|
|
16
|
+
key: string;
|
|
17
|
+
label: string;
|
|
18
|
+
value: unknown;
|
|
19
|
+
color: string | undefined;
|
|
20
|
+
visible: boolean;
|
|
21
|
+
config: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function getPayloadConfigFromPayload(
|
|
25
|
+
config: ChartConfig,
|
|
26
|
+
item: TooltipSeriesItem,
|
|
27
|
+
key: string,
|
|
28
|
+
) {
|
|
29
|
+
if (typeof item !== "object" || item === null) return undefined;
|
|
30
|
+
|
|
31
|
+
let configLabelKey: string = key;
|
|
32
|
+
|
|
33
|
+
if (item.key === key) {
|
|
34
|
+
configLabelKey = item.key;
|
|
35
|
+
} else if (item.label === key) {
|
|
36
|
+
configLabelKey = item.label;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ChartContextValue = {
|
|
43
|
+
config: ChartConfig;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const chartContextKey = Symbol("chart-context");
|
|
47
|
+
|
|
48
|
+
export function setChartContext(value: ChartContextValue) {
|
|
49
|
+
return setContext(chartContextKey, value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useChart() {
|
|
53
|
+
return getContext<ChartContextValue>(chartContextKey);
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as ChartContainer } from "./chart-container.svelte";
|
|
2
|
+
export { default as ChartTooltip } from "./chart-tooltip.svelte";
|
|
3
|
+
export { default as ChartStyle } from "./chart-style.svelte";
|
|
4
|
+
export {
|
|
5
|
+
type ChartConfig,
|
|
6
|
+
type TooltipSeriesItem,
|
|
7
|
+
getPayloadConfigFromPayload,
|
|
8
|
+
useChart,
|
|
9
|
+
setChartContext,
|
|
10
|
+
THEMES,
|
|
11
|
+
} from "./chart-utils.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Styles for the reference line. Plain stylesheet (see area-chart.css for why). */
|
|
2
|
+
|
|
3
|
+
.hr-reference-line line,
|
|
4
|
+
.hr-reference-line path {
|
|
5
|
+
stroke: var(--hr-reference-color);
|
|
6
|
+
stroke-width: 1.5;
|
|
7
|
+
stroke-dasharray: 4 4;
|
|
8
|
+
opacity: 0.7;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.hr-reference-line text {
|
|
12
|
+
fill: var(--hr-reference-color);
|
|
13
|
+
font-size: 0.75rem;
|
|
14
|
+
font-weight: 500;
|
|
15
|
+
stroke: var(--background);
|
|
16
|
+
stroke-width: 3;
|
|
17
|
+
paint-order: stroke;
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/** A target the data is judged against: a deal, a budget, a limit. */
|
|
3
|
+
export type ChartReference = {
|
|
4
|
+
value: number;
|
|
5
|
+
/** Already formatted, figure included: "Deal €61,500". */
|
|
6
|
+
label: string;
|
|
7
|
+
/** A status token when crossing it means something, e.g. `var(--destructive)`. */
|
|
8
|
+
color?: string;
|
|
9
|
+
};
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
/*
|
|
14
|
+
* A dashed rule across the plot at a fixed value, labelled at its start.
|
|
15
|
+
* Drawn inside a layerchart `<Svg>`; the chart widens its domain to keep it in view.
|
|
16
|
+
*/
|
|
17
|
+
import "./reference-line.css";
|
|
18
|
+
import { AnnotationLine } from "layerchart";
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
reference: ChartReference;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let { reference }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const stroke = $derived(reference.color ?? "var(--foreground)");
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<g class="hr-reference-line" style:--hr-reference-color={stroke}>
|
|
30
|
+
<AnnotationLine y={reference.value} label={reference.label} labelPlacement="top-left" />
|
|
31
|
+
</g>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { getChartContext } from "layerchart";
|
|
6
|
+
import { onMount, type Snippet } from "svelte";
|
|
7
|
+
import { expoInOut } from "svelte/easing";
|
|
8
|
+
import { prefersReducedMotion, Tween } from "svelte/motion";
|
|
9
|
+
import { REVEAL_MS, tokenMs } from "./motion.js";
|
|
10
|
+
|
|
11
|
+
// =========================
|
|
12
|
+
// TYPES
|
|
13
|
+
// =========================
|
|
14
|
+
interface Props {
|
|
15
|
+
children: Snippet;
|
|
16
|
+
/** Headroom around the plot so a stroke at the series max is not cut. */
|
|
17
|
+
bleed?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// =========================
|
|
21
|
+
// PROPS
|
|
22
|
+
// =========================
|
|
23
|
+
let { children, bleed = 6 }: Props = $props();
|
|
24
|
+
|
|
25
|
+
// =========================
|
|
26
|
+
// STATE
|
|
27
|
+
// =========================
|
|
28
|
+
const ctx = getChartContext();
|
|
29
|
+
const id = `hr-reveal-${Math.random().toString(36).slice(2, 8)}`;
|
|
30
|
+
const progress = new Tween(0, { easing: expoInOut });
|
|
31
|
+
let rect = $state<SVGRectElement>();
|
|
32
|
+
|
|
33
|
+
// =========================
|
|
34
|
+
// LIFECYCLE
|
|
35
|
+
// =========================
|
|
36
|
+
onMount(() => {
|
|
37
|
+
const duration =
|
|
38
|
+
prefersReducedMotion.current || !rect ? 0 : tokenMs(rect, "--chart-motion-reveal", REVEAL_MS);
|
|
39
|
+
progress.set(1, { duration });
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<!-- =========================== -->
|
|
44
|
+
<!-- REVEAL -->
|
|
45
|
+
<!-- =========================== -->
|
|
46
|
+
<!-- WHY a tweened width and not scaleX: scaleX needs a transform origin, and the
|
|
47
|
+
rect has to keep tracking the plot width through a resize. -->
|
|
48
|
+
<defs>
|
|
49
|
+
<clipPath {id}>
|
|
50
|
+
<rect
|
|
51
|
+
bind:this={rect}
|
|
52
|
+
x={-bleed}
|
|
53
|
+
y={-bleed}
|
|
54
|
+
width={Math.max(0, (ctx.width + bleed * 2) * progress.current)}
|
|
55
|
+
height={ctx.height + bleed * 2}
|
|
56
|
+
/>
|
|
57
|
+
</clipPath>
|
|
58
|
+
</defs>
|
|
59
|
+
<g clip-path="url(#{id})">
|
|
60
|
+
{@render children()}
|
|
61
|
+
</g>
|