@heyramzi/ui 2.1.0 → 2.2.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/package.json +7 -8
- package/scripts/sync-shadcn.mjs +3 -4
- package/src/agents/agent-avatar.svelte +604 -575
- package/src/agents/agent-cast.svelte +179 -183
- package/src/agents/demo.svelte +125 -113
- package/src/agents/helpers.ts +23 -17
- package/src/agents/index.ts +4 -4
- package/src/agents/page-state.ts +6 -6
- package/src/agents/types.ts +6 -6
- package/src/ai/ai-button/AiButton.svelte +4 -12
- package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
- package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
- package/src/ai/response/Response.css +2 -3
- package/src/animations/avatar-group-hover.css +2 -16
- package/src/animations/card-tilt.css +3 -6
- package/src/animations/digit-pop-in.css +2 -15
- package/src/animations/dropdown.css +4 -17
- package/src/animations/input-clear-dissolve.css +5 -18
- package/src/animations/input-shake.css +9 -41
- package/src/animations/learn-more-hover.css +17 -9
- package/src/animations/like-button.css +2 -4
- package/src/animations/page-side-by-side.css +2 -10
- package/src/animations/plus-menu-morph.css +2 -5
- package/src/animations/shimmer-text.css +5 -18
- package/src/animations/spinning-counter.css +2 -6
- package/src/animations/success-check.css +2 -15
- package/src/animations/thinking-states.css +2 -7
- package/src/brand/brand.test.ts +21 -5
- package/src/brand/index.ts +4 -10
- package/src/canvas/card/card-frame.svelte +43 -39
- package/src/canvas/card/resize-handles.svelte +89 -80
- package/src/canvas/chrome/chrome-zoom.svelte +101 -100
- package/src/canvas/chrome/lock-badge.svelte +8 -8
- package/src/canvas/chrome/side-handles.svelte +24 -19
- package/src/canvas/chrome/size-chip.svelte +30 -30
- package/src/canvas/chrome/snap-guides.svelte +20 -20
- package/src/canvas/components/icons/ToolIcon.svelte +8 -8
- package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
- package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
- package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
- package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
- package/src/canvas/edges/bend-handle.svelte +46 -46
- package/src/canvas/edges/edge-gradient.svelte +10 -10
- package/src/canvas/edges/gradient-edge.svelte +45 -29
- package/src/canvas/editor/lock.test.ts +6 -1
- package/src/canvas/editor/style.ts +7 -1
- package/src/canvas/headless/svg-figure.test.ts +5 -1
- package/src/canvas/lod/lod-switch.svelte +111 -106
- package/src/canvas/pen/PenField.svelte +87 -81
- package/src/canvas/pen/PenOverlay.svelte +127 -127
- package/src/canvas/pen/ShapeNode.svelte +31 -32
- package/src/canvas/pen/TextNode.svelte +54 -53
- package/src/canvas/pen/host-field.test.svelte +7 -7
- package/src/canvas/pen/shapes.ts +7 -1
- package/src/canvas/screen/GlyphIcon.svelte +22 -17
- package/src/canvas/screen/PartView.svelte +138 -72
- package/src/canvas/screen/ScreenBody.svelte +81 -60
- package/src/canvas/screen/ScreenCard.svelte +20 -20
- package/src/canvas/ui/CanvasToolbar.svelte +173 -85
- package/src/canvas/ui/FrameLayer.svelte +59 -59
- package/src/canvas/ui/PenLayer.svelte +39 -30
- package/src/canvas/ui/StylePanel.svelte +119 -102
- package/src/canvas/ui/canvas-grid.svelte +137 -129
- package/src/charts/animated-value.svelte +1 -1
- package/src/charts/area-chart.css +2 -5
- package/src/charts/area-chart.svelte +1 -1
- package/src/charts/bar-chart.css +2 -3
- package/src/charts/bar-chart.svelte +1 -1
- package/src/charts/charts.css +5 -17
- package/src/charts/crosshair.svelte +1 -1
- package/src/charts/dashboard/dashboard-empty.svelte +25 -25
- package/src/charts/dashboard/dashboard-section.svelte +24 -24
- package/src/charts/dashboard/data-panel.svelte +50 -50
- package/src/charts/dashboard/data-sort-head.svelte +56 -56
- package/src/charts/dashboard/data-table.svelte +138 -138
- package/src/charts/dashboard/panel-grid.svelte +30 -30
- package/src/charts/dashboard/period-filter.svelte +25 -25
- package/src/charts/dashboard/rating-chip.svelte +19 -19
- package/src/charts/dashboard/stat-band.svelte +32 -32
- package/src/charts/dashboard/stat-tile.svelte +106 -99
- package/src/charts/donut-chart.svelte +1 -1
- package/src/charts/edge-fade.svelte +1 -1
- package/src/charts/figure.svelte +1 -1
- package/src/charts/gauge-chart.css +4 -7
- package/src/charts/gauge-chart.svelte +1 -1
- package/src/charts/hover-focus.svelte +1 -1
- package/src/charts/legend.css +1 -3
- package/src/charts/legend.svelte +1 -1
- package/src/charts/line-chart.svelte +1 -1
- package/src/charts/number-chart.css +6 -9
- package/src/charts/number-chart.svelte +1 -1
- package/src/charts/reference-line.svelte +19 -19
- package/src/charts/reveal.svelte +1 -1
- package/src/charts/ring-chart.css +2 -4
- package/src/charts/ring-chart.svelte +1 -1
- package/src/charts/sparkline.svelte +5 -2
- package/src/charts/stat-card.svelte +1 -1
- package/src/charts/time-series-chart.css +2 -5
- package/src/charts/trend-pill.css +2 -4
- package/src/charts/trend-pill.svelte +1 -1
- package/src/components/copy/copy-button.svelte +93 -98
- package/src/components/copy/copy-button.test.ts +10 -2
- package/src/components/data-table/infinite-scroll.svelte +42 -42
- package/src/components/data-table/pagination.svelte +34 -22
- package/src/components/data-table/row-arrow.svelte +19 -16
- package/src/components/data-table/row-context-menu.svelte +52 -52
- package/src/components/data-table/sortable-head.svelte +25 -25
- package/src/components/data-table/table-skeleton.svelte +41 -36
- package/src/components/docs/doc-page.svelte +18 -9
- package/src/components/docs/on-this-page.svelte +16 -3
- package/src/components/docs/scroll-spy.svelte.ts +2 -3
- package/src/components/field-row/field-row.svelte +10 -8
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
- package/src/components/icon/icon.svelte +14 -14
- package/src/components/language-flag/language-flag.svelte +3 -1
- package/src/components/list-page/detail-empty.svelte +34 -34
- package/src/components/list-page/detail-header.svelte +55 -47
- package/src/components/list-page/detail-panel.svelte +51 -59
- package/src/components/list-page/detail-row.svelte +19 -19
- package/src/components/list-page/detail-stack.svelte +21 -21
- package/src/components/list-page/empty-state.svelte +30 -30
- package/src/components/list-page/filter-item.svelte +20 -20
- package/src/components/list-page/filter-popover.svelte +60 -55
- package/src/components/list-page/grouped-list.svelte +4 -2
- package/src/components/list-page/index.ts +5 -1
- package/src/components/list-page/list-page-shell.svelte +2 -5
- package/src/components/list-page/master-detail.svelte +87 -88
- package/src/components/list-page/notice-bar.svelte +38 -38
- package/src/components/list-page/prev-next.svelte +63 -63
- package/src/components/list-page/rail-body.svelte +18 -18
- package/src/components/list-page/rail-empty.svelte +23 -30
- package/src/components/list-page/rail-head.svelte +24 -24
- package/src/components/list-page/rail-row.svelte +77 -73
- package/src/components/list-page/section-host.ts +2 -3
- package/src/components/locale-switch/locale-switch.svelte +41 -41
- package/src/components/mark/mark.svelte +34 -38
- package/src/components/open-in/index.ts +1 -5
- package/src/components/open-in/open-in.svelte +40 -40
- package/src/components/picker/mark-picker.svelte +49 -49
- package/src/components/picker/picker.svelte +5 -3
- package/src/components/section-header/section-header.test.ts +7 -1
- package/src/components/section-header/section-label.svelte +20 -20
- package/src/components/segmented/animated-tabs.svelte +31 -2
- package/src/components/segmented/collapse.test.ts +2 -1
- package/src/components/segmented/index.ts +2 -0
- package/src/components/segmented/segment.svelte +52 -49
- package/src/components/segmented/segmented-filter.svelte +79 -80
- package/src/components/segmented/segmented-nav.svelte +34 -40
- package/src/components/segmented/segmented-overflow.svelte +53 -50
- package/src/components/segmented/segmented-track.svelte +62 -46
- package/src/components/segmented/segmented.ts +2 -3
- package/src/components/star-rating-input/star-rating-input.svelte +103 -103
- package/src/components/svg/svg.svelte +4 -4
- package/src/components/table-view/table-view.svelte +17 -2
- package/src/components/table-view/table-view.test.ts +7 -1
- package/src/components/table-view/types.ts +11 -11
- package/src/components/tags-input/index.ts +13 -0
- package/src/components/tags-input/tags-input.svelte +305 -0
- package/src/components/tags-input/tags.test.ts +64 -0
- package/src/components/tags-input/tags.ts +142 -0
- package/src/components/theme/index.ts +2 -4
- package/src/components/theme/theme-provider.svelte +1 -3
- package/src/components/toolbar/toolbar-search.svelte +25 -25
- package/src/components/toolbar/toolbar.svelte +11 -7
- package/src/components/toolbar/toolbar.test.ts +10 -1
- package/src/design/index.css +21 -78
- package/src/design/polish.css +26 -35
- package/src/editor/editor-toolbar.svelte +104 -99
- package/src/editor/editor.svelte +296 -266
- package/src/editor/image-toolbar.svelte +20 -22
- package/src/editor/index.ts +1 -4
- package/src/editor/inline-editor.svelte +280 -238
- package/src/editor/markdown.ts +15 -15
- package/src/editor/renderer.svelte +71 -71
- package/src/editor/slash-menu.svelte +24 -27
- package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
- package/src/effects/image-comparison/ImageComparison.svelte +11 -1
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
- package/src/effects/magic-card/MagicCard.css +14 -7
- package/src/effects/marquee/Marquee.svelte +3 -5
- package/src/effects/reveal/observe.ts +0 -3
- package/src/effects/spotlight/Spotlight.svelte +1 -1
- package/src/effects/theme-toggle/ThemeToggle.css +25 -22
- package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
- package/src/gallery.test.ts +14 -4
- package/src/inbox/README.md +1 -3
- package/src/inbox/inbox.test.ts +181 -138
- package/src/inbox/index.ts +20 -20
- package/src/inbox/logic.ts +114 -111
- package/src/inbox/thread-inbox.svelte +227 -151
- package/src/inbox/types.ts +130 -108
- package/src/lib/theme-transition.ts +2 -4
- package/src/lib/tone.ts +2 -4
- package/src/lib/utils.ts +2 -2
- package/src/modules.test.ts +79 -69
- package/src/onboarding/define-tours.test.ts +55 -40
- package/src/onboarding/define-tours.ts +23 -23
- package/src/onboarding/demo.svelte +24 -5
- package/src/onboarding/index.ts +1 -8
- package/src/onboarding/onboarding-provider.svelte +19 -7
- package/src/onboarding/onboarding.svelte.ts +16 -16
- package/src/onboarding/pick.test.ts +61 -52
- package/src/onboarding/pick.ts +16 -13
- package/src/onboarding/stores.test.ts +80 -80
- package/src/onboarding/stores.ts +41 -39
- package/src/onboarding/tour-card.svelte +30 -5
- package/src/onboarding/tour-runner.svelte +12 -3
- package/src/onboarding/types.ts +20 -12
- package/src/onboarding/wait.ts +22 -22
- package/src/panel/side-panel.svelte +94 -93
- package/src/record/demo-data.ts +291 -225
- package/src/record/field-section.svelte +38 -34
- package/src/record/fields.ts +148 -96
- package/src/record/format.ts +63 -56
- package/src/record/index.ts +49 -35
- package/src/record/info-list.svelte +145 -114
- package/src/record/kinds.ts +31 -31
- package/src/record/logic.ts +216 -214
- package/src/record/next-up.svelte +45 -34
- package/src/record/project-list.svelte +56 -50
- package/src/record/record-demo.svelte +130 -110
- package/src/record/record-header.svelte +37 -28
- package/src/record/record-shell.svelte +54 -54
- package/src/record/record-stats.svelte +25 -25
- package/src/record/record-timeline.svelte +134 -114
- package/src/record/record.test.ts +430 -309
- package/src/record/steps-rail.svelte +225 -216
- package/src/record/types.ts +234 -215
- package/src/record/where-we-are.svelte +62 -58
- package/src/shadcn/button/button.svelte +1 -1
- package/src/shadcn/card/card-title.svelte +1 -5
- package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
- package/src/shadcn/sheet/sheet-content.svelte +2 -4
- package/src/shadcn/table/table-cell.svelte +2 -3
- package/src/shadcn/utilities.css +3 -8
- package/src/slides/Controls.svelte +150 -124
- package/src/slides/Deck.svelte +243 -238
- package/src/slides/Fragment.svelte +64 -57
- package/src/slides/Overview.svelte +106 -101
- package/src/slides/Presenter.svelte +300 -266
- package/src/slides/Slide.svelte +185 -170
- package/src/slides/SlideList.svelte +16 -12
- package/src/slides/ambient.d.ts +1 -1
- package/src/slides/bridges.ts +42 -23
- package/src/slides/deck-controller.svelte.ts +47 -35
- package/src/slides/demo-slides.svelte +330 -306
- package/src/slides/demo.svelte +164 -160
- package/src/slides/export/capture.ts +20 -16
- package/src/slides/export/ffmpeg.ts +43 -38
- package/src/slides/export/index.ts +5 -5
- package/src/slides/export/launch.ts +3 -1
- package/src/slides/export/pdf.ts +30 -27
- package/src/slides/export/types.ts +10 -8
- package/src/slides/index.ts +11 -11
- package/src/slides/labels.ts +22 -19
- package/src/slides/layout.ts +9 -2
- package/src/slides/model.ts +4 -4
- package/src/slides/navigation.ts +43 -31
- package/src/slides/theme.css +5 -7
- package/src/slides/types.ts +9 -10
- package/src/status/define-statuses.ts +17 -24
- package/src/status/index.ts +5 -5
- package/src/status/status-badge.svelte +3 -1
- package/src/status/status-dot.svelte +1 -3
- package/src/status/status-icon.svelte +1 -1
- package/src/status/status-types.ts +21 -21
- package/src/tasks/board-order.ts +2 -4
- package/src/tasks/demo.svelte +189 -31
- package/src/tasks/detail/detail-demo.svelte +73 -13
- package/src/tasks/detail/detail-types.ts +45 -45
- package/src/tasks/detail/task-detail.svelte +40 -10
- package/src/tasks/detail/task-property.svelte +8 -2
- package/src/tasks/detail/task-thread.svelte +53 -12
- package/src/tasks/gantt/gantt-bar.svelte +17 -6
- package/src/tasks/gantt/gantt-grid.svelte +40 -9
- package/src/tasks/gantt/gantt-group-row.svelte +8 -2
- package/src/tasks/gantt/gantt-header.svelte +9 -3
- package/src/tasks/gantt/gantt-task-row.svelte +26 -6
- package/src/tasks/gantt/gantt.css +2 -1
- package/src/tasks/gantt/gantt.test.ts +3 -1
- package/src/tasks/gantt/logic.test.ts +110 -20
- package/src/tasks/gantt/logic.ts +42 -6
- package/src/tasks/inbox/inbox-card.svelte +13 -5
- package/src/tasks/inbox/inbox-draft.svelte +1 -1
- package/src/tasks/inbox/inbox-logic.ts +6 -40
- package/src/tasks/inbox/inbox-statuses.ts +23 -23
- package/src/tasks/inbox/inbox-trace.svelte +9 -3
- package/src/tasks/inbox/inbox-types.ts +54 -58
- package/src/tasks/inbox/index.ts +22 -21
- package/src/tasks/index.ts +49 -52
- package/src/tasks/kanban-board.svelte +16 -12
- package/src/tasks/kanban-types.ts +2 -3
- package/src/tasks/presets/sales.ts +6 -6
- package/src/tasks/task-gantt.svelte +28 -8
- package/src/tasks/task-list.svelte +41 -17
- package/src/tasks/time/duration.ts +40 -40
- package/src/tasks/time/time-chip.svelte +9 -2
- package/src/tasks/time/time-dot.svelte +1 -1
- package/src/tasks/time/time-ring.svelte +11 -3
- package/src/tasks/time/time-tracker.svelte +57 -15
- package/src/tasks/time/time-types.ts +57 -57
- package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
- package/src/thread/index.ts +7 -7
- package/src/thread/logic.ts +36 -33
- package/src/thread/thread.svelte +82 -16
- package/src/thread/thread.test.ts +58 -47
- package/src/thread/types.ts +68 -68
- package/src/tokens/craft.css +17 -53
- package/src/tokens/index.css +3 -13
- package/src/tokens/palette.css +13 -11
- package/src/tokens/primitives.css +10 -23
- package/src/tasks/inbox/inbox.svelte +0 -203
package/src/record/logic.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// The record's pure logic: grouping, merging, picking. No DOM, no clock of its own.
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
RecordEvent,
|
|
4
|
+
RecordFilterDef,
|
|
5
|
+
RecordNext,
|
|
6
|
+
RecordSummary,
|
|
7
|
+
StageDef,
|
|
8
|
+
StepDef,
|
|
9
|
+
StepGroupDef,
|
|
10
|
+
StepMarker,
|
|
11
11
|
} from "./types.js";
|
|
12
12
|
|
|
13
13
|
// =========================
|
|
@@ -16,117 +16,117 @@ import type {
|
|
|
16
16
|
|
|
17
17
|
/** The calendar day of a timestamp as `YYYY-MM-DD`, in the given zone or the viewer's own. */
|
|
18
18
|
export function localDay(iso: string, timeZone?: string): string {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
20
|
+
year: "numeric",
|
|
21
|
+
month: "2-digit",
|
|
22
|
+
day: "2-digit",
|
|
23
|
+
timeZone,
|
|
24
|
+
}).format(new Date(iso));
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export type DayGroup = { day: string; events: RecordEvent[] };
|
|
28
28
|
|
|
29
29
|
/** Newest day first, newest event first inside a day. */
|
|
30
30
|
export function groupEventsByDay(events: readonly RecordEvent[], timeZone?: string): DayGroup[] {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
const sorted = [...events].sort(
|
|
32
|
+
(a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime(),
|
|
33
|
+
);
|
|
34
|
+
const groups: DayGroup[] = [];
|
|
35
|
+
for (const event of sorted) {
|
|
36
|
+
const day = localDay(event.occurredAt, timeZone);
|
|
37
|
+
const last = groups[groups.length - 1];
|
|
38
|
+
if (last && last.day === day) last.events.push(event);
|
|
39
|
+
else groups.push({ day, events: [event] });
|
|
40
|
+
}
|
|
41
|
+
return groups;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** `today`, `yesterday` or `date`, by calendar distance from `today`. */
|
|
45
45
|
export function dayRelation(day: string, today: string): "today" | "yesterday" | "date" {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if (day === today) return "today";
|
|
47
|
+
const gap = (Date.parse(`${today}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) / 86_400_000;
|
|
48
|
+
return gap === 1 ? "yesterday" : "date";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** The events one filter chip shows. An unknown or empty key shows everything. */
|
|
52
52
|
export function eventsForFilter(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
events: readonly RecordEvent[],
|
|
54
|
+
filterKey: string,
|
|
55
|
+
filters: readonly RecordFilterDef[],
|
|
56
56
|
): RecordEvent[] {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
const filter = filters.find((f) => f.key === filterKey);
|
|
58
|
+
if (!filter) return [...events];
|
|
59
|
+
const kinds = new Set(filter.kinds);
|
|
60
|
+
return events.filter((e) => kinds.has(e.kind));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Per-chip counts. Chips with no events are the host's to hide. */
|
|
64
64
|
export function filterCounts(
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
events: readonly RecordEvent[],
|
|
66
|
+
filters: readonly RecordFilterDef[],
|
|
67
67
|
): Record<string, number> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const counts: Record<string, number> = {};
|
|
69
|
+
for (const filter of filters) {
|
|
70
|
+
const kinds = new Set(filter.kinds);
|
|
71
|
+
counts[filter.key] = events.filter((e) => kinds.has(e.kind)).length;
|
|
72
|
+
}
|
|
73
|
+
return counts;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** Keep the newest `limit` events and count the rest, cutting a day group where the limit falls. */
|
|
77
77
|
export function foldGroups(
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
groups: readonly DayGroup[],
|
|
79
|
+
limit: number,
|
|
80
80
|
): { groups: DayGroup[]; hidden: number } {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
const kept: DayGroup[] = [];
|
|
82
|
+
let used = 0;
|
|
83
|
+
let total = 0;
|
|
84
|
+
for (const group of groups) total += group.events.length;
|
|
85
|
+
for (const group of groups) {
|
|
86
|
+
if (used >= limit) break;
|
|
87
|
+
const room = limit - used;
|
|
88
|
+
const events = group.events.slice(0, room);
|
|
89
|
+
kept.push({ day: group.day, events });
|
|
90
|
+
used += events.length;
|
|
91
|
+
}
|
|
92
|
+
return { groups: kept, hidden: total - used };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export type ProjectEvents = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
href?: string | null;
|
|
99
|
+
events: readonly RecordEvent[];
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* One client's timeline
|
|
104
|
-
*
|
|
105
|
-
* passed in both lists draws once. Newest first.
|
|
103
|
+
* One client's timeline, newest first, with project events tagged. An id seen twice keeps the
|
|
104
|
+
* first, so a source passed in both lists draws once.
|
|
106
105
|
*/
|
|
107
106
|
export function mergeClientEvents(
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
clientEvents: readonly RecordEvent[],
|
|
108
|
+
projects: readonly ProjectEvents[],
|
|
110
109
|
): RecordEvent[] {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
110
|
+
const seen = new Set<string>();
|
|
111
|
+
const merged: RecordEvent[] = [];
|
|
112
|
+
const add = (event: RecordEvent) => {
|
|
113
|
+
if (seen.has(event.id)) return;
|
|
114
|
+
seen.add(event.id);
|
|
115
|
+
merged.push(event);
|
|
116
|
+
};
|
|
117
|
+
for (const project of projects) {
|
|
118
|
+
for (const event of project.events) {
|
|
119
|
+
add({
|
|
120
|
+
...event,
|
|
121
|
+
projectId: project.id,
|
|
122
|
+
projectName: project.name,
|
|
123
|
+
projectHref: project.href ?? null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const event of clientEvents)
|
|
128
|
+
add({ ...event, projectId: null, projectName: null, projectHref: null });
|
|
129
|
+
return merged.sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// =========================
|
|
@@ -137,93 +137,94 @@ const ACTIVE: ReadonlySet<string> = new Set(["open", "proposal", "ongoing"]);
|
|
|
137
137
|
|
|
138
138
|
/** Projects still in play: not closed, and in a stage that isn't won-and-done or lost. */
|
|
139
139
|
export function activeProjects(
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
projects: readonly RecordSummary[],
|
|
141
|
+
stages: readonly StageDef[],
|
|
142
142
|
): RecordSummary[] {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
const category = new Map(stages.map((s) => [s.key, s.category]));
|
|
144
|
+
return projects.filter((p) => {
|
|
145
|
+
if (p.closed) return false;
|
|
146
|
+
const c = category.get(p.stageKey);
|
|
147
|
+
return c === undefined || ACTIVE.has(c);
|
|
148
|
+
});
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/** The earliest dated next across active projects, undated ones after, ties by list order. */
|
|
152
152
|
export function nextAcrossProjects(
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
projects: readonly RecordSummary[],
|
|
154
|
+
stages: readonly StageDef[],
|
|
155
155
|
): RecordNext | null {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
156
|
+
let best: RecordNext | null = null;
|
|
157
|
+
for (const project of activeProjects(projects, stages)) {
|
|
158
|
+
const next = project.next;
|
|
159
|
+
if (!next) continue;
|
|
160
|
+
const candidate: RecordNext = { ...next, projectId: project.id, projectName: project.name };
|
|
161
|
+
if (!best) {
|
|
162
|
+
best = candidate;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!best.date && candidate.date) best = candidate;
|
|
166
|
+
else if (best.date && candidate.date && candidate.date < best.date) best = candidate;
|
|
167
|
+
}
|
|
168
|
+
return best;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
export type LatestStatus = NonNullable<RecordSummary["whereWeAre"]> & {
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
projectId: string;
|
|
173
|
+
projectName: string;
|
|
174
174
|
};
|
|
175
175
|
|
|
176
176
|
/** The newest "where we are" across projects, tagged with its project. */
|
|
177
177
|
export function latestWhereWeAre(projects: readonly RecordSummary[]): LatestStatus | null {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
178
|
+
let best: LatestStatus | null = null;
|
|
179
|
+
for (const project of projects) {
|
|
180
|
+
const status = project.whereWeAre;
|
|
181
|
+
if (!status || !status.text) continue;
|
|
182
|
+
const at = status.at ? new Date(status.at).getTime() : 0;
|
|
183
|
+
const bestAt = best?.at ? new Date(best.at).getTime() : -1;
|
|
184
|
+
if (!best || at > bestAt)
|
|
185
|
+
best = { ...status, projectId: project.id, projectName: project.name };
|
|
186
|
+
}
|
|
187
|
+
return best;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
export type MoneyTotals = Record<string, { pipeline: number; won: number; paid: number }>;
|
|
190
191
|
|
|
191
192
|
export type ClientRollup = {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
count: number;
|
|
194
|
+
activeCount: number;
|
|
195
|
+
totals: MoneyTotals;
|
|
196
|
+
lastCallAt: string | null;
|
|
196
197
|
};
|
|
197
198
|
|
|
198
199
|
/**
|
|
199
|
-
* The client's
|
|
200
|
-
*
|
|
201
|
-
* project adds to none. Totals stay per currency, because a sum across two is a lie.
|
|
200
|
+
* The client's pipeline, won and paid totals, kept per currency because a sum across two
|
|
201
|
+
* currencies is a lie.
|
|
202
202
|
*/
|
|
203
203
|
export function rollupProjects(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
projects: readonly RecordSummary[],
|
|
205
|
+
stages: readonly StageDef[],
|
|
206
|
+
fallbackCurrency = "EUR",
|
|
207
207
|
): ClientRollup {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
208
|
+
const category = new Map(stages.map((s) => [s.key, s.category]));
|
|
209
|
+
const totals: MoneyTotals = {};
|
|
210
|
+
let lastCallAt: string | null = null;
|
|
211
|
+
for (const project of projects) {
|
|
212
|
+
const currency = project.currency ?? fallbackCurrency;
|
|
213
|
+
const bucket = (totals[currency] ??= { pipeline: 0, won: 0, paid: 0 });
|
|
214
|
+
const c = category.get(project.stageKey);
|
|
215
|
+
const value = project.value ?? 0;
|
|
216
|
+
if (c === "open" || c === "proposal") bucket.pipeline += value;
|
|
217
|
+
if (c === "ongoing" || c === "completed") bucket.won += value;
|
|
218
|
+
bucket.paid += project.paid ?? 0;
|
|
219
|
+
if (project.lastCallAt && (!lastCallAt || project.lastCallAt > lastCallAt))
|
|
220
|
+
lastCallAt = project.lastCallAt;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
count: projects.length,
|
|
224
|
+
activeCount: activeProjects(projects, stages).length,
|
|
225
|
+
totals,
|
|
226
|
+
lastCallAt,
|
|
227
|
+
};
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
// =========================
|
|
@@ -232,93 +233,94 @@ export function rollupProjects(
|
|
|
232
233
|
|
|
233
234
|
/** A step is visible unless hidden. */
|
|
234
235
|
export function visibleSteps(steps: readonly StepDef[]): StepDef[] {
|
|
235
|
-
|
|
236
|
+
return steps.filter((s) => !s.hidden);
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
export function stepProgress(steps: readonly StepDef[]): { done: number; total: number } {
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
const visible = visibleSteps(steps);
|
|
241
|
+
return { done: visible.filter((s) => s.done).length, total: visible.length };
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
/**
|
|
244
|
-
* One marker per
|
|
245
|
-
*
|
|
246
|
-
* `today` is an ISO date.
|
|
245
|
+
* One marker per step. Only an open step before the current one, or past its own date, is
|
|
246
|
+
* late. `today` is an ISO date.
|
|
247
247
|
*/
|
|
248
248
|
export function stepMarkers(steps: readonly StepDef[], today: string): Map<string, StepMarker> {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
249
|
+
const visible = visibleSteps(steps);
|
|
250
|
+
const currentId = visible.find((s) => !s.done)?.id ?? null;
|
|
251
|
+
const markers = new Map<string, StepMarker>();
|
|
252
|
+
let beforeCurrent = currentId !== null;
|
|
253
|
+
for (const step of visible) {
|
|
254
|
+
if (step.done) {
|
|
255
|
+
markers.set(step.id, "complete");
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (step.id === currentId) {
|
|
259
|
+
markers.set(step.id, "current");
|
|
260
|
+
beforeCurrent = false;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const due = (step.end ?? step.start)?.slice(0, 10);
|
|
264
|
+
const overdue = due !== undefined && due < today;
|
|
265
|
+
markers.set(step.id, beforeCurrent || overdue ? "late" : "pending");
|
|
266
|
+
}
|
|
267
|
+
return markers;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
export type StepBucket = { key: string; label: string | null; steps: StepDef[] };
|
|
271
271
|
|
|
272
272
|
/**
|
|
273
|
-
* Buckets by group
|
|
274
|
-
*
|
|
275
|
-
* under no header.
|
|
273
|
+
* Buckets by declared group order, not consecutive runs, so a reordered rail still draws one
|
|
274
|
+
* header per group. Ungrouped steps go last.
|
|
276
275
|
*/
|
|
277
|
-
export function groupSteps(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
276
|
+
export function groupSteps(
|
|
277
|
+
steps: readonly StepDef[],
|
|
278
|
+
groups: readonly StepGroupDef[] = [],
|
|
279
|
+
): StepBucket[] {
|
|
280
|
+
const known = new Set(groups.map((g) => g.key));
|
|
281
|
+
const buckets: StepBucket[] = groups.map((g) => ({ key: g.key, label: g.label, steps: [] }));
|
|
282
|
+
const loose: StepBucket = { key: "", label: null, steps: [] };
|
|
283
|
+
for (const step of steps) {
|
|
284
|
+
const bucket = buckets.find((b) => b.key === step.group);
|
|
285
|
+
(step.group && known.has(step.group) && bucket ? bucket : loose).steps.push(step);
|
|
286
|
+
}
|
|
287
|
+
const result = buckets.filter((b) => b.steps.length > 0);
|
|
288
|
+
if (loose.steps.length > 0) result.push(loose);
|
|
289
|
+
return result;
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
/** Move `movedId` next to `targetId` inside the id list. Same list back when nothing moves. */
|
|
291
293
|
export function reorderIds(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
ids: readonly string[],
|
|
295
|
+
movedId: string,
|
|
296
|
+
targetId: string,
|
|
297
|
+
before: boolean,
|
|
296
298
|
): string[] {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
if (movedId === targetId) return [...ids];
|
|
300
|
+
const from = ids.indexOf(movedId);
|
|
301
|
+
if (from < 0 || !ids.includes(targetId)) return [...ids];
|
|
302
|
+
const next = [...ids];
|
|
303
|
+
next.splice(from, 1);
|
|
304
|
+
const at = next.indexOf(targetId);
|
|
305
|
+
next.splice(before ? at : at + 1, 0, movedId);
|
|
306
|
+
return next;
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
/** Swap a step with its neighbour in its own group. Same list back at an edge. */
|
|
308
310
|
export function nudgeStep(steps: readonly StepDef[], id: string, direction: -1 | 1): string[] {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
311
|
+
const ids = steps.map((s) => s.id);
|
|
312
|
+
const step = steps.find((s) => s.id === id);
|
|
313
|
+
if (!step) return ids;
|
|
314
|
+
const mates = steps.filter((s) => s.group === step.group && !s.hidden);
|
|
315
|
+
const at = mates.findIndex((s) => s.id === id);
|
|
316
|
+
const neighbour = mates[at + direction];
|
|
317
|
+
if (!neighbour) return ids;
|
|
318
|
+
const a = ids.indexOf(id);
|
|
319
|
+
const b = ids.indexOf(neighbour.id);
|
|
320
|
+
const first = ids[a];
|
|
321
|
+
const second = ids[b];
|
|
322
|
+
if (first === undefined || second === undefined) return ids;
|
|
323
|
+
ids[a] = second;
|
|
324
|
+
ids[b] = first;
|
|
325
|
+
return ids;
|
|
324
326
|
}
|
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// =========================
|
|
3
|
-
// IMPORTS
|
|
4
|
-
// =========================
|
|
5
|
-
import { ArrowRight } from "phosphor-svelte";
|
|
6
|
-
import { cn } from "../lib/utils.js";
|
|
7
|
-
import { TONE_TEXT } from "../lib/tone.js";
|
|
8
|
-
import StatusBadge from "../status/status-badge.svelte";
|
|
9
|
-
import { gutterClass } from "./gutter.js";
|
|
10
|
-
import { formatRange } from "./format.js";
|
|
11
|
-
import { todayIso } from "../tasks/dates.js";
|
|
12
|
-
import { resolveLabels, type RecordLabels, type RecordNext } from "./types.js";
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { ArrowRight } from "phosphor-svelte";
|
|
6
|
+
import { cn } from "../lib/utils.js";
|
|
7
|
+
import { TONE_TEXT } from "../lib/tone.js";
|
|
8
|
+
import StatusBadge from "../status/status-badge.svelte";
|
|
9
|
+
import { gutterClass } from "./gutter.js";
|
|
10
|
+
import { formatRange } from "./format.js";
|
|
11
|
+
import { todayIso } from "../tasks/dates.js";
|
|
12
|
+
import { resolveLabels, type RecordLabels, type RecordNext } from "./types.js";
|
|
13
13
|
|
|
14
|
-
// =========================
|
|
15
|
-
// PROPS
|
|
16
|
-
// =========================
|
|
17
|
-
interface Props {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
let {
|
|
14
|
+
// =========================
|
|
15
|
+
// PROPS
|
|
16
|
+
// =========================
|
|
17
|
+
interface Props {
|
|
18
|
+
next?: RecordNext | null;
|
|
19
|
+
/** Client mode: say which project the next step belongs to. */
|
|
20
|
+
showProject?: boolean;
|
|
21
|
+
labels?: Partial<RecordLabels>;
|
|
22
|
+
locale?: string;
|
|
23
|
+
/** ISO date. Decides "late". Defaults to the viewer's today. */
|
|
24
|
+
today?: string;
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
let {
|
|
28
|
+
next,
|
|
29
|
+
showProject = false,
|
|
30
|
+
labels,
|
|
31
|
+
locale,
|
|
32
|
+
today = todayIso(),
|
|
33
|
+
class: className,
|
|
34
|
+
}: Props = $props();
|
|
28
35
|
|
|
29
|
-
// =========================
|
|
30
|
-
// DERIVED
|
|
31
|
-
// =========================
|
|
32
|
-
let t = $derived(resolveLabels(labels));
|
|
33
|
-
let due = $derived((next?.endDate ?? next?.date)?.slice(0, 10));
|
|
34
|
-
let late = $derived(due !== undefined && due < today);
|
|
35
|
-
let when = $derived(next?.date ? formatRange(next.date, next.endDate, locale) : null);
|
|
36
|
+
// =========================
|
|
37
|
+
// DERIVED
|
|
38
|
+
// =========================
|
|
39
|
+
let t = $derived(resolveLabels(labels));
|
|
40
|
+
let due = $derived((next?.endDate ?? next?.date)?.slice(0, 10));
|
|
41
|
+
let late = $derived(due !== undefined && due < today);
|
|
42
|
+
let when = $derived(next?.date ? formatRange(next.date, next.endDate, locale) : null);
|
|
36
43
|
</script>
|
|
37
44
|
|
|
38
45
|
<!-- =========================== -->
|
|
@@ -50,10 +57,14 @@ let when = $derived(next?.date ? formatRange(next.date, next.endDate, locale) :
|
|
|
50
57
|
<ArrowRight class="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
51
58
|
<span class="font-medium">{t.next}</span>
|
|
52
59
|
{#if when}
|
|
53
|
-
<span class={cn("tabular-nums", late ? TONE_TEXT.warning : "text-muted-foreground")}
|
|
60
|
+
<span class={cn("tabular-nums", late ? TONE_TEXT.warning : "text-muted-foreground")}
|
|
61
|
+
>{when}</span
|
|
62
|
+
>
|
|
54
63
|
{/if}
|
|
55
64
|
{#if next.href}
|
|
56
|
-
<a href={next.href} class="min-w-0 truncate underline-offset-2 hover:underline"
|
|
65
|
+
<a href={next.href} class="min-w-0 truncate underline-offset-2 hover:underline"
|
|
66
|
+
>{next.label}</a
|
|
67
|
+
>
|
|
57
68
|
{:else}
|
|
58
69
|
<span class="min-w-0 truncate">{next.label}</span>
|
|
59
70
|
{/if}
|