@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,267 @@
|
|
|
1
|
+
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import "./stat-card.css";
|
|
6
|
+
import { Area, Bars, Chart, Highlight, LinearGradient, Spline, Svg, Tooltip } from "layerchart";
|
|
7
|
+
import { scaleBand } from "d3-scale";
|
|
8
|
+
import { curveBasis, curveCardinal } from "d3-shape";
|
|
9
|
+
import type { Snippet } from "svelte";
|
|
10
|
+
import Crosshair from "./crosshair.svelte";
|
|
11
|
+
import EdgeFade from "./edge-fade.svelte";
|
|
12
|
+
import Reveal from "./reveal.svelte";
|
|
13
|
+
import { resolveEdgeFade } from "./edge-fade.js";
|
|
14
|
+
import Figure from "./figure.svelte";
|
|
15
|
+
import GaugeChart from "./gauge-chart.svelte";
|
|
16
|
+
|
|
17
|
+
// =========================
|
|
18
|
+
// TYPES
|
|
19
|
+
// =========================
|
|
20
|
+
type Variant = "line" | "area" | "bar" | "gauge";
|
|
21
|
+
type Trend = "up" | "down" | "neutral";
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** Series for the line/area/bar variants. Unused by the gauge variant. */
|
|
25
|
+
data?: TData[];
|
|
26
|
+
xKey?: keyof TData & string;
|
|
27
|
+
yKey?: keyof TData & string;
|
|
28
|
+
/** Chart kind. "gauge" puts a radial gauge to the right of the figure. */
|
|
29
|
+
variant?: Variant;
|
|
30
|
+
/** Arc fill 0-100 for variant="gauge". Falls back to `value` when numeric. */
|
|
31
|
+
gaugeValue?: number;
|
|
32
|
+
/** Label under the gauge percentage. */
|
|
33
|
+
gaugeLabel?: string;
|
|
34
|
+
/** Metric name shown top-left. Omit (or set `figureOnly`) for a bare chart. */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Headline number; counts up on mount. */
|
|
37
|
+
value?: number;
|
|
38
|
+
/** Percentage change. Its sign drives the trend color/arrow unless `trend` is set. */
|
|
39
|
+
delta?: number;
|
|
40
|
+
/** Force the trend direction independent of `delta`'s sign. */
|
|
41
|
+
trend?: Trend;
|
|
42
|
+
/** Caption under the value, e.g. "Avg" or "vs last month". */
|
|
43
|
+
caption?: string;
|
|
44
|
+
/** Render only the edge-to-edge chart, no header/value/caption. */
|
|
45
|
+
figureOnly?: boolean;
|
|
46
|
+
color?: string;
|
|
47
|
+
/** Chart height in px. */
|
|
48
|
+
height?: number;
|
|
49
|
+
smooth?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Softens the left and right ends of the line and the area fill instead of cutting them off at the card edge.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* `true` fades 8% of the width per side, a number sets the fraction (0-0.45), `false` keeps the hard edges.
|
|
56
|
+
*/
|
|
57
|
+
edgeFade?: boolean | number;
|
|
58
|
+
/** Format for the headline value. */
|
|
59
|
+
formatValue?: (v: number) => string;
|
|
60
|
+
/** Format for the x value shown as the tooltip header (e.g. a date/period). */
|
|
61
|
+
formatX?: (v: unknown) => string;
|
|
62
|
+
/** Leading icon next to the label. */
|
|
63
|
+
icon?: Snippet;
|
|
64
|
+
class?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// =========================
|
|
68
|
+
// PROPS
|
|
69
|
+
// =========================
|
|
70
|
+
let {
|
|
71
|
+
data = [],
|
|
72
|
+
xKey,
|
|
73
|
+
yKey,
|
|
74
|
+
variant = "area",
|
|
75
|
+
gaugeValue,
|
|
76
|
+
gaugeLabel,
|
|
77
|
+
label,
|
|
78
|
+
value,
|
|
79
|
+
delta,
|
|
80
|
+
trend,
|
|
81
|
+
caption,
|
|
82
|
+
figureOnly = false,
|
|
83
|
+
color = "var(--chart-1)",
|
|
84
|
+
height = 150,
|
|
85
|
+
smooth = true,
|
|
86
|
+
edgeFade = true,
|
|
87
|
+
formatValue = (v) => v.toLocaleString(),
|
|
88
|
+
formatX = (v) => String(v),
|
|
89
|
+
icon,
|
|
90
|
+
class: className,
|
|
91
|
+
}: Props = $props();
|
|
92
|
+
|
|
93
|
+
// =========================
|
|
94
|
+
// DERIVED
|
|
95
|
+
// =========================
|
|
96
|
+
const uid = `hr-stat-${Math.random().toString(36).slice(2, 8)}`;
|
|
97
|
+
// WHY: match bklit's stat-card curves per variant. The area card uses cardinal
|
|
98
|
+
|
|
99
|
+
// at 0.65 tension; the line card uses curveBasis (a smoother, approximating spline) so the line flows instead of kinking through every point.
|
|
100
|
+
const areaCurve = $derived(smooth ? curveCardinal.tension(0.65) : undefined);
|
|
101
|
+
const lineCurve = $derived(smooth ? curveBasis : undefined);
|
|
102
|
+
const yMax = $derived(yKey ? Math.max(0, ...data.map((d) => Number(d[yKey] ?? 0))) : 0);
|
|
103
|
+
const arcValue = $derived(gaugeValue ?? (typeof value === "number" ? value : 0));
|
|
104
|
+
// WHY: one prop drives both ends. The stroke ramp is half the fill's so the
|
|
105
|
+
|
|
106
|
+
// line still spans nearly the full width (8% fill fade -> the 4% stroke fade this card has always used) instead of reading as side padding.
|
|
107
|
+
const fade = $derived(resolveEdgeFade(edgeFade));
|
|
108
|
+
const strokePct = $derived(Math.round(fade * 50 * 1000) / 1000);
|
|
109
|
+
const strokeStop = $derived(`${strokePct}%`);
|
|
110
|
+
const strokeEnd = $derived(`${100 - strokePct}%`);
|
|
111
|
+
const fadeMask = $derived(fade > 0 ? `url(#${uid}-fade)` : undefined);
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<!-- =========================== -->
|
|
115
|
+
<!-- STAT CARD -->
|
|
116
|
+
<!-- =========================== -->
|
|
117
|
+
<div
|
|
118
|
+
class="hr-stat-card {className ?? ''}"
|
|
119
|
+
class:hr-stat-figure-only={figureOnly}
|
|
120
|
+
class:hr-stat-has-gauge={variant === "gauge"}
|
|
121
|
+
>
|
|
122
|
+
{#if variant === "gauge"}
|
|
123
|
+
<div class="hr-stat-gauge-row">
|
|
124
|
+
{#if label != null || value != null || caption != null}
|
|
125
|
+
<Figure
|
|
126
|
+
{icon}
|
|
127
|
+
title={label}
|
|
128
|
+
{value}
|
|
129
|
+
{delta}
|
|
130
|
+
{trend}
|
|
131
|
+
{caption}
|
|
132
|
+
size="lg"
|
|
133
|
+
format={formatValue}
|
|
134
|
+
class="hr-stat-gauge-figure"
|
|
135
|
+
/>
|
|
136
|
+
{/if}
|
|
137
|
+
<GaugeChart
|
|
138
|
+
value={arcValue}
|
|
139
|
+
{color}
|
|
140
|
+
size={116}
|
|
141
|
+
minWidth={0}
|
|
142
|
+
suffix="%"
|
|
143
|
+
showValue
|
|
144
|
+
defaultLabel={gaugeLabel ?? ""}
|
|
145
|
+
ariaLabel={label}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
{:else}
|
|
149
|
+
{#if !figureOnly && (label != null || value != null || caption != null)}
|
|
150
|
+
<Figure
|
|
151
|
+
{icon}
|
|
152
|
+
title={label}
|
|
153
|
+
{value}
|
|
154
|
+
{delta}
|
|
155
|
+
{trend}
|
|
156
|
+
{caption}
|
|
157
|
+
size="lg"
|
|
158
|
+
format={formatValue}
|
|
159
|
+
/>
|
|
160
|
+
{/if}
|
|
161
|
+
|
|
162
|
+
<div
|
|
163
|
+
class="hr-stat-chart"
|
|
164
|
+
style:height="{height}px"
|
|
165
|
+
role="img"
|
|
166
|
+
aria-label="{label ?? 'Value'} trend"
|
|
167
|
+
>
|
|
168
|
+
{#if data.length > 0 && xKey && yKey}
|
|
169
|
+
{#if variant === "bar"}
|
|
170
|
+
<Chart
|
|
171
|
+
{data}
|
|
172
|
+
x={xKey}
|
|
173
|
+
xScale={scaleBand().padding(0.28)}
|
|
174
|
+
y={yKey}
|
|
175
|
+
yDomain={[0, yMax || 1]}
|
|
176
|
+
padding={{ top: 6, bottom: 0, left: 0, right: 0 }}
|
|
177
|
+
tooltipContext={{ mode: "band" }}
|
|
178
|
+
>
|
|
179
|
+
<Svg>
|
|
180
|
+
<Bars rounded="top" radius={2} style="fill: {color}" class="hr-stat-bars" />
|
|
181
|
+
<Highlight
|
|
182
|
+
bar={{ rounded: "top", radius: 2, fill: color, class: "hr-stat-bar-active" }}
|
|
183
|
+
/>
|
|
184
|
+
</Svg>
|
|
185
|
+
<Tooltip.Root>
|
|
186
|
+
{#snippet children({ data: d })}
|
|
187
|
+
<Tooltip.Header value={xKey ? formatX(d[xKey]) : ""} />
|
|
188
|
+
<Tooltip.List>
|
|
189
|
+
<Tooltip.Item label={String(yKey)} value={formatValue(Number(d[yKey]))} />
|
|
190
|
+
</Tooltip.List>
|
|
191
|
+
{/snippet}
|
|
192
|
+
</Tooltip.Root>
|
|
193
|
+
</Chart>
|
|
194
|
+
{:else}
|
|
195
|
+
<Chart
|
|
196
|
+
{data}
|
|
197
|
+
x={xKey}
|
|
198
|
+
y={yKey}
|
|
199
|
+
padding={{ top: 8, bottom: 0, left: 0, right: 0 }}
|
|
200
|
+
tooltipContext={{ mode: "quadtree-x" }}
|
|
201
|
+
>
|
|
202
|
+
<Svg>
|
|
203
|
+
<!-- WHY: bklit fades the stroke to transparent at both ends via a
|
|
204
|
+
horizontal gradient applied as the stroke paint; shared by the area
|
|
205
|
+
and line variants. -->
|
|
206
|
+
<LinearGradient id="{uid}-stroke">
|
|
207
|
+
{#snippet stopsContent()}
|
|
208
|
+
<stop offset="0%" stop-color={color} stop-opacity={fade > 0 ? 0 : 1} />
|
|
209
|
+
<stop offset={strokeStop} stop-color={color} stop-opacity="1" />
|
|
210
|
+
<stop offset={strokeEnd} stop-color={color} stop-opacity="1" />
|
|
211
|
+
<stop offset="100%" stop-color={color} stop-opacity={fade > 0 ? 0 : 1} />
|
|
212
|
+
{/snippet}
|
|
213
|
+
</LinearGradient>
|
|
214
|
+
{#if fade > 0}
|
|
215
|
+
<EdgeFade id="{uid}-fade" amount={fade} />
|
|
216
|
+
{/if}
|
|
217
|
+
<Crosshair id="{uid}-cross" />
|
|
218
|
+
<Reveal bleed={3}>
|
|
219
|
+
{#if variant === "area"}
|
|
220
|
+
<LinearGradient id={uid} vertical>
|
|
221
|
+
{#snippet stopsContent()}
|
|
222
|
+
<stop offset="0%" stop-color={color} stop-opacity="0.45" />
|
|
223
|
+
<stop offset="100%" stop-color={color} stop-opacity="0" />
|
|
224
|
+
{/snippet}
|
|
225
|
+
</LinearGradient>
|
|
226
|
+
<Area
|
|
227
|
+
y1={yKey}
|
|
228
|
+
curve={areaCurve}
|
|
229
|
+
fill="url(#{uid})"
|
|
230
|
+
mask={fadeMask}
|
|
231
|
+
line={false}
|
|
232
|
+
/>
|
|
233
|
+
{/if}
|
|
234
|
+
<Spline
|
|
235
|
+
y={yKey}
|
|
236
|
+
curve={variant === "line" ? lineCurve : areaCurve}
|
|
237
|
+
stroke="url(#{uid}-stroke)"
|
|
238
|
+
fill="none"
|
|
239
|
+
stroke-width={variant === "line" ? 2.5 : 2}
|
|
240
|
+
stroke-linecap="round"
|
|
241
|
+
class="hr-stat-spline"
|
|
242
|
+
/>
|
|
243
|
+
</Reveal>
|
|
244
|
+
<!-- WHY: the crosshair line (not just the point dot) makes hover targeting legible on the edge-to-edge chart; quadtree-x snaps to the nearest -->
|
|
245
|
+
<!-- point by cursor x (bisect-x mis-tracks a categorical x-axis), but without -->
|
|
246
|
+
<!-- the line the selected point is easy to miss. Matches AreaChart's highlight. points fill is forced to the series color (else it -->
|
|
247
|
+
<!-- falls back to layerchart's blue --color-primary); motion="none" drops the spring so the dot tracks the cursor instantly instead of lagging behind. -->
|
|
248
|
+
<Highlight
|
|
249
|
+
points={{ fill: color }}
|
|
250
|
+
lines={{ style: `stroke: url(#${uid}-cross)` }}
|
|
251
|
+
motion="none"
|
|
252
|
+
/>
|
|
253
|
+
</Svg>
|
|
254
|
+
<Tooltip.Root>
|
|
255
|
+
{#snippet children({ data: d })}
|
|
256
|
+
<Tooltip.Header value={xKey ? formatX(d[xKey]) : ""} />
|
|
257
|
+
<Tooltip.List>
|
|
258
|
+
<Tooltip.Item label={String(yKey)} value={formatValue(Number(d[yKey]))} />
|
|
259
|
+
</Tooltip.List>
|
|
260
|
+
{/snippet}
|
|
261
|
+
</Tooltip.Root>
|
|
262
|
+
</Chart>
|
|
263
|
+
{/if}
|
|
264
|
+
{/if}
|
|
265
|
+
</div>
|
|
266
|
+
{/if}
|
|
267
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* Styles for TimeSeriesChart. Shipped as a plain stylesheet (imported by the
|
|
2
|
+
component) instead of a Svelte `<style>` block: a `<style>` block becomes a
|
|
3
|
+
`?type=style&lang.css` virtual module that Tailwind v4's Vite plugin mis-parses
|
|
4
|
+
when this package is consumed as source from node_modules (tailwindlabs/
|
|
5
|
+
tailwindcss#20000). Selectors are namespaced under `.hr-ts-chart`. */
|
|
6
|
+
|
|
7
|
+
.hr-ts-chart {
|
|
8
|
+
width: 100%;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.hr-ts-chart .empty {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
height: 100%;
|
|
17
|
+
border: 1px dashed var(--chart-grid, oklch(0.5 0.01 286 / 0.2));
|
|
18
|
+
border-radius: 0.5rem;
|
|
19
|
+
}
|
|
20
|
+
.hr-ts-chart .empty p {
|
|
21
|
+
font-size: 0.875rem;
|
|
22
|
+
color: var(--chart-label, oklch(0.5 0.01 286));
|
|
23
|
+
margin: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.hr-ts-chart .hr-ts-grid {
|
|
27
|
+
stroke: var(--chart-grid, oklch(0.5 0.01 286 / 0.2));
|
|
28
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import "./time-series-chart.css";
|
|
6
|
+
import { scaleTime } from "d3-scale";
|
|
7
|
+
import { curveMonotoneX } from "d3-shape";
|
|
8
|
+
import {
|
|
9
|
+
Area,
|
|
10
|
+
Axis,
|
|
11
|
+
Chart,
|
|
12
|
+
Grid,
|
|
13
|
+
Highlight,
|
|
14
|
+
LinearGradient,
|
|
15
|
+
Spline,
|
|
16
|
+
Svg,
|
|
17
|
+
Tooltip,
|
|
18
|
+
} from "layerchart";
|
|
19
|
+
import Crosshair from "./crosshair.svelte";
|
|
20
|
+
import EdgeFade from "./edge-fade.svelte";
|
|
21
|
+
import { resolveEdgeFade } from "./edge-fade.js";
|
|
22
|
+
import { DOMAIN_MOTION } from "./motion.js";
|
|
23
|
+
import Reveal from "./reveal.svelte";
|
|
24
|
+
|
|
25
|
+
// =========================
|
|
26
|
+
// TYPES
|
|
27
|
+
// =========================
|
|
28
|
+
interface DataPoint {
|
|
29
|
+
date: string;
|
|
30
|
+
value: number;
|
|
31
|
+
value2?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Props {
|
|
35
|
+
data: DataPoint[];
|
|
36
|
+
label?: string;
|
|
37
|
+
label2?: string;
|
|
38
|
+
color?: string;
|
|
39
|
+
color2?: string;
|
|
40
|
+
formatValue?: (v: number) => string;
|
|
41
|
+
formatValue2?: (v: number) => string;
|
|
42
|
+
height?: number;
|
|
43
|
+
/** Clamp Y-axis to 95th percentile so outlier spikes don't flatten normal data */
|
|
44
|
+
softMax?: boolean;
|
|
45
|
+
/** Y-axis floor. Raise above 0 to frame variation around a baseline. */
|
|
46
|
+
yMin?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Softens the left and right ends of the filled area instead of cutting them off at the plot edge.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* `true` fades 8% of the width per side, a number sets the fraction (0-0.45), `false` keeps the hard edges.
|
|
53
|
+
*/
|
|
54
|
+
edgeFade?: boolean | number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// =========================
|
|
58
|
+
// PROPS
|
|
59
|
+
// =========================
|
|
60
|
+
let {
|
|
61
|
+
data,
|
|
62
|
+
label = "Value",
|
|
63
|
+
label2,
|
|
64
|
+
color = "var(--chart-1)",
|
|
65
|
+
color2 = "var(--muted-foreground)",
|
|
66
|
+
formatValue = (v) => v.toLocaleString(),
|
|
67
|
+
formatValue2,
|
|
68
|
+
height = 280,
|
|
69
|
+
softMax = false,
|
|
70
|
+
yMin = 0,
|
|
71
|
+
edgeFade = true,
|
|
72
|
+
}: Props = $props();
|
|
73
|
+
|
|
74
|
+
// =========================
|
|
75
|
+
// DERIVED
|
|
76
|
+
// =========================
|
|
77
|
+
const gradientId = $derived(
|
|
78
|
+
`time-series-gradient-${label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
|
|
79
|
+
);
|
|
80
|
+
const fade = $derived(resolveEdgeFade(edgeFade));
|
|
81
|
+
const fadeMask = $derived(fade > 0 ? `url(#${gradientId}-fade)` : undefined);
|
|
82
|
+
|
|
83
|
+
let chartData = $derived(
|
|
84
|
+
data.map((d) => ({
|
|
85
|
+
...d,
|
|
86
|
+
dateObj: new Date(d.date),
|
|
87
|
+
})),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
let yMax = $derived.by(() => {
|
|
91
|
+
const allValues = [
|
|
92
|
+
...data.map((d) => d.value),
|
|
93
|
+
...data.flatMap((d) => (d.value2 == null ? [] : [d.value2])),
|
|
94
|
+
];
|
|
95
|
+
const absMax = Math.max(...allValues);
|
|
96
|
+
|
|
97
|
+
if (!softMax || allValues.length < 10) return absMax;
|
|
98
|
+
|
|
99
|
+
const sorted = allValues.filter((v) => v > 0).toSorted((a, b) => a - b);
|
|
100
|
+
if (sorted.length === 0) return absMax;
|
|
101
|
+
|
|
102
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
103
|
+
// Only clamp if the max is significantly larger than p95 (2x+)
|
|
104
|
+
if (absMax > p95 * 2) return p95 * 1.3;
|
|
105
|
+
return absMax;
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<!-- =========================== -->
|
|
110
|
+
<!-- TIME SERIES CHART -->
|
|
111
|
+
<!-- =========================== -->
|
|
112
|
+
<div class="hr-ts-chart" style="height: {height}px">
|
|
113
|
+
{#if chartData.length === 0}
|
|
114
|
+
<div class="empty"><p>No historical data yet</p></div>
|
|
115
|
+
{:else}
|
|
116
|
+
<Chart
|
|
117
|
+
data={chartData}
|
|
118
|
+
x="dateObj"
|
|
119
|
+
xScale={scaleTime()}
|
|
120
|
+
y="value"
|
|
121
|
+
yDomain={[yMin, yMax]}
|
|
122
|
+
yNice
|
|
123
|
+
padding={{ left: 56, bottom: 36, top: 12, right: 16 }}
|
|
124
|
+
tooltipContext={{ mode: "bisect-x" }}
|
|
125
|
+
motion={DOMAIN_MOTION}
|
|
126
|
+
>
|
|
127
|
+
<Svg>
|
|
128
|
+
<Axis placement="left" format={(v) => formatValue(v)} ticks={6} />
|
|
129
|
+
<Axis
|
|
130
|
+
placement="bottom"
|
|
131
|
+
format={(d) => {
|
|
132
|
+
const date = d instanceof Date ? d : new Date(d);
|
|
133
|
+
return date.toLocaleDateString("en", { month: "short", day: "numeric" });
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
<Grid y class="hr-ts-grid" />
|
|
137
|
+
<LinearGradient id={gradientId} vertical>
|
|
138
|
+
{#snippet stopsContent()}
|
|
139
|
+
<stop offset="0%" stop-color={color} stop-opacity="0.35" />
|
|
140
|
+
<stop offset="100%" stop-color={color} stop-opacity="0.02" />
|
|
141
|
+
{/snippet}
|
|
142
|
+
</LinearGradient>
|
|
143
|
+
{#if fade > 0}
|
|
144
|
+
<EdgeFade id="{gradientId}-fade" amount={fade} />
|
|
145
|
+
{/if}
|
|
146
|
+
<Crosshair id="{gradientId}-cross" />
|
|
147
|
+
<Reveal>
|
|
148
|
+
<Area curve={curveMonotoneX} fill="url(#{gradientId})" mask={fadeMask} />
|
|
149
|
+
<Spline curve={curveMonotoneX} style="stroke: {color}; fill: none" stroke-width="1.5" />
|
|
150
|
+
{#if label2}
|
|
151
|
+
<Spline
|
|
152
|
+
y="value2"
|
|
153
|
+
curve={curveMonotoneX}
|
|
154
|
+
style="stroke: {color2}; stroke-dasharray: 4 4; stroke-width: 1; fill: none"
|
|
155
|
+
/>
|
|
156
|
+
{/if}
|
|
157
|
+
</Reveal>
|
|
158
|
+
<Highlight points lines={{ style: `stroke: url(#${gradientId}-cross)` }} />
|
|
159
|
+
</Svg>
|
|
160
|
+
<Tooltip.Root>
|
|
161
|
+
{#snippet children({ data: d })}
|
|
162
|
+
<Tooltip.Header
|
|
163
|
+
value={d.dateObj.toLocaleDateString("en", {
|
|
164
|
+
month: "short",
|
|
165
|
+
day: "numeric",
|
|
166
|
+
year: "numeric",
|
|
167
|
+
})}
|
|
168
|
+
/>
|
|
169
|
+
<Tooltip.List>
|
|
170
|
+
<Tooltip.Item {label} value={formatValue(d.value)} />
|
|
171
|
+
{#if label2 && d.value2 !== undefined}
|
|
172
|
+
<Tooltip.Item label={label2} value={(formatValue2 ?? formatValue)(d.value2)} />
|
|
173
|
+
{/if}
|
|
174
|
+
</Tooltip.List>
|
|
175
|
+
{/snippet}
|
|
176
|
+
</Tooltip.Root>
|
|
177
|
+
</Chart>
|
|
178
|
+
{/if}
|
|
179
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Styles for TrendPill. Plain stylesheet (see area-chart.css for why). All
|
|
2
|
+
selectors are already namespaced under `.hr-trend`. */
|
|
3
|
+
|
|
4
|
+
.hr-trend {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
/* WHY no shrink: the delta is the row's point, and it is only three characters wide. */
|
|
7
|
+
flex: 0 0 auto;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 0.1875rem;
|
|
10
|
+
padding: 0.1875rem 0.5rem;
|
|
11
|
+
border-radius: 999px;
|
|
12
|
+
border: 1px solid color-mix(in srgb, var(--stat-trend, currentColor) 22%, transparent);
|
|
13
|
+
background: color-mix(in srgb, var(--stat-trend, currentColor) 10%, transparent);
|
|
14
|
+
/* WHY the mix: a trend colour is picked to read as a mark, and at 12px on its
|
|
15
|
+
own 10% wash the raw hue measured 3.5:1. Mixing toward --foreground darkens
|
|
16
|
+
it on a light page and lightens it on a dark one, so one rule serves both
|
|
17
|
+
themes and the package needs to know nothing about which is on. */
|
|
18
|
+
color: color-mix(
|
|
19
|
+
in oklab,
|
|
20
|
+
var(--stat-trend, var(--muted-foreground, oklch(0.5 0.01 286))) 72%,
|
|
21
|
+
var(--foreground, oklch(0.2 0.01 286))
|
|
22
|
+
);
|
|
23
|
+
font-size: 0.75rem;
|
|
24
|
+
font-weight: 600;
|
|
25
|
+
font-feature-settings: "tnum";
|
|
26
|
+
line-height: 1.2;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
}
|
|
29
|
+
.hr-trend[data-trend="up"] {
|
|
30
|
+
--stat-trend: var(--stat-up, oklch(0.6 0.13 163));
|
|
31
|
+
}
|
|
32
|
+
.hr-trend[data-trend="down"] {
|
|
33
|
+
--stat-trend: var(--stat-down, oklch(0.58 0.22 25));
|
|
34
|
+
}
|
|
35
|
+
.hr-trend-arrow {
|
|
36
|
+
font-size: 0.8125rem;
|
|
37
|
+
line-height: 1;
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import "./trend-pill.css";
|
|
6
|
+
import { cn } from "../lib/utils.js";
|
|
7
|
+
|
|
8
|
+
// =========================
|
|
9
|
+
// TYPES
|
|
10
|
+
// =========================
|
|
11
|
+
type Trend = "up" | "down" | "neutral";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Percentage change. Its sign drives the color/arrow unless `trend` is set. */
|
|
15
|
+
delta: number;
|
|
16
|
+
/** Force the direction independent of `delta`'s sign. */
|
|
17
|
+
trend?: Trend;
|
|
18
|
+
/** Format the delta text. */
|
|
19
|
+
format?: (v: number) => string;
|
|
20
|
+
class?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// =========================
|
|
24
|
+
// PROPS
|
|
25
|
+
// =========================
|
|
26
|
+
let {
|
|
27
|
+
delta,
|
|
28
|
+
trend,
|
|
29
|
+
format = (v) =>
|
|
30
|
+
`${v > 0 ? "+" : ""}${v.toLocaleString(undefined, { maximumFractionDigits: 1 })}%`,
|
|
31
|
+
class: className,
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
// =========================
|
|
35
|
+
// DERIVED
|
|
36
|
+
// =========================
|
|
37
|
+
const resolved = $derived<Trend>(trend ?? (delta === 0 ? "neutral" : delta > 0 ? "up" : "down"));
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<!-- =========================== -->
|
|
41
|
+
<!-- TREND PILL -->
|
|
42
|
+
<!-- =========================== -->
|
|
43
|
+
<span class={cn("hr-trend", className)} data-trend={resolved}>
|
|
44
|
+
{#if resolved !== "neutral"}
|
|
45
|
+
<span class="hr-trend-arrow" aria-hidden="true">{resolved === "up" ? "↑" : "↓"}</span>
|
|
46
|
+
{/if}
|
|
47
|
+
{format(delta)}
|
|
48
|
+
</span>
|
|
@@ -144,7 +144,7 @@ async function copy(event: MouseEvent) {
|
|
|
144
144
|
{#if agent}
|
|
145
145
|
<StarFour
|
|
146
146
|
class={cn(
|
|
147
|
-
"col-start-1 row-start-1 size-full text-brand
|
|
147
|
+
"col-start-1 row-start-1 size-full text-brand/70 transition-[opacity,color] duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] group-hover/copy:text-brand",
|
|
148
148
|
copied ? "opacity-0" : "opacity-100",
|
|
149
149
|
)}
|
|
150
150
|
weight="bold"
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
<div class="flex items-start gap-4">
|
|
113
113
|
{#if Icon}
|
|
114
114
|
<span
|
|
115
|
-
class="flex size-11 shrink-0 items-center justify-center rounded-xl bg-
|
|
115
|
+
class="flex size-11 shrink-0 items-center justify-center rounded-xl bg-brand-100 text-brand dark:bg-brand-800/30 dark:text-brand-light"
|
|
116
116
|
>
|
|
117
117
|
<Icon class="size-5" weight="bold" />
|
|
118
118
|
</span>
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
the part of the line you're standing on. -->
|
|
111
111
|
<span
|
|
112
112
|
aria-hidden="true"
|
|
113
|
-
class="otp-marker absolute -left-px top-0 w-0.5 rounded-full bg-brand
|
|
113
|
+
class="otp-marker absolute -left-px top-0 w-0.5 rounded-full bg-brand dark:bg-brand-light"
|
|
114
114
|
class:is-ready={marker.ready}
|
|
115
115
|
style="transform: translateY({marker.y}px); height: {marker.h}px;"
|
|
116
116
|
></span>
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
<!-- How far through the reader is. The only motion here, and it is the state. -->
|
|
158
158
|
<span aria-hidden="true" class="absolute inset-x-0 bottom-0 h-px bg-border">
|
|
159
159
|
<span
|
|
160
|
-
class="otp-progress block h-full origin-left bg-brand
|
|
160
|
+
class="otp-progress block h-full origin-left bg-brand dark:bg-brand-light"
|
|
161
161
|
style="transform: scaleX({progress});"
|
|
162
162
|
></span>
|
|
163
163
|
</span>
|
|
@@ -41,7 +41,7 @@ let {
|
|
|
41
41
|
<Empty.Root class="t-empty py-16">
|
|
42
42
|
<Empty.Header class="max-w-md">
|
|
43
43
|
{#if Icon}
|
|
44
|
-
<Empty.Media class="mb-4 size-16 rounded-2xl bg-brand
|
|
44
|
+
<Empty.Media class="mb-4 size-16 rounded-2xl bg-brand/10 text-brand">
|
|
45
45
|
<Icon weight={iconWeight} class="size-8" />
|
|
46
46
|
</Empty.Media>
|
|
47
47
|
{/if}
|
|
@@ -71,7 +71,7 @@ let isActive = $derived(active ?? hasCount);
|
|
|
71
71
|
{/if}
|
|
72
72
|
{#if hasCount}
|
|
73
73
|
<span
|
|
74
|
-
class="ml-0.5 inline-flex size-4 items-center justify-center rounded-full bg-brand
|
|
74
|
+
class="ml-0.5 inline-flex size-4 items-center justify-center rounded-full bg-brand/15 text-[10px] font-semibold text-brand dark:text-brand-light"
|
|
75
75
|
>
|
|
76
76
|
{count}
|
|
77
77
|
</span>
|
|
@@ -88,8 +88,8 @@ let on = $derived(active === true);
|
|
|
88
88
|
// 20 by 18, so `rounded-full` drew an ellipse.
|
|
89
89
|
"inline-flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] leading-none font-medium tabular-nums",
|
|
90
90
|
"bg-muted-foreground/15 text-muted-foreground",
|
|
91
|
-
on && "bg-brand
|
|
92
|
-
auto && "group-data-[state=active]/segment:bg-brand
|
|
91
|
+
on && "bg-brand/14 text-brand",
|
|
92
|
+
auto && "group-data-[state=active]/segment:bg-brand/14 group-data-[state=active]/segment:text-brand",
|
|
93
93
|
)}
|
|
94
94
|
>
|
|
95
95
|
{count}
|
|
@@ -98,8 +98,8 @@ let on = $derived(active === true);
|
|
|
98
98
|
<span
|
|
99
99
|
class={cn(
|
|
100
100
|
"size-1.5 shrink-0 rounded-full bg-muted-foreground/50",
|
|
101
|
-
on && "bg-brand
|
|
102
|
-
auto && "group-data-[state=active]/segment:bg-brand
|
|
101
|
+
on && "bg-brand",
|
|
102
|
+
auto && "group-data-[state=active]/segment:bg-brand",
|
|
103
103
|
)}
|
|
104
104
|
aria-hidden="true"
|
|
105
105
|
></span>
|