@heyramzi/ui 2.0.1 → 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 +48 -24
- 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/README.md +9 -12
- 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 +8 -4
- 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/index.ts +1 -0
- package/src/components/section-header/section-header.svelte +1 -1
- package/src/components/section-header/section-header.test.ts +7 -1
- package/src/components/section-header/section-label.svelte +39 -0
- 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/index.ts +2 -0
- package/src/components/{list-page → toolbar}/toolbar-search.svelte +25 -25
- package/src/components/toolbar/toolbar.svelte +13 -9
- 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 +27 -0
- package/src/inbox/inbox.test.ts +214 -0
- package/src/inbox/index.ts +31 -0
- package/src/inbox/logic.ts +169 -0
- package/src/inbox/thread-inbox.svelte +439 -0
- package/src/inbox/types.ts +161 -0
- 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 +105 -0
- 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/README.md +5 -0
- package/src/panel/index.ts +2 -0
- package/src/panel/side-panel.svelte +194 -0
- package/src/record/README.md +3 -0
- package/src/record/demo-data.ts +352 -0
- package/src/record/field-section.svelte +57 -0
- package/src/record/fields.ts +194 -0
- package/src/record/format.ts +94 -0
- package/src/{tasks/record → record}/index.ts +49 -35
- package/src/{tasks/record → record}/info-list.svelte +145 -114
- package/src/record/kinds.ts +42 -0
- package/src/record/logic.ts +326 -0
- package/src/record/next-up.svelte +81 -0
- package/src/{tasks/record → record}/project-list.svelte +56 -50
- package/src/{tasks/record → record}/record-demo.svelte +130 -110
- package/src/{tasks/record → record}/record-header.svelte +37 -28
- package/src/{tasks/record → record}/record-shell.svelte +54 -54
- package/src/record/record-stats.svelte +48 -0
- package/src/record/record-timeline.svelte +251 -0
- package/src/record/record.test.ts +471 -0
- package/src/{tasks/record → record}/steps-rail.svelte +225 -216
- package/src/record/types.ts +299 -0
- package/src/record/where-we-are.svelte +125 -0
- 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/README.md +18 -0
- package/src/status/define-statuses.ts +25 -0
- package/src/status/index.ts +11 -0
- package/src/{tasks → status}/status-badge.svelte +3 -1
- package/src/{tasks → status}/status-dot.svelte +1 -3
- package/src/{tasks → status}/status-icon.svelte +1 -1
- package/src/status/status-types.ts +40 -0
- package/src/tasks/README.md +1 -1
- package/src/tasks/board-order.ts +2 -4
- package/src/tasks/demo.svelte +192 -34
- package/src/tasks/detail/detail-demo.svelte +75 -15
- 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 +4 -2
- package/src/tasks/gantt/logic.test.ts +110 -20
- package/src/tasks/gantt/logic.ts +42 -6
- package/src/tasks/inbox/inbox-card.svelte +15 -7
- 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 +24 -24
- 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 +52 -53
- package/src/tasks/kanban-board.svelte +16 -12
- package/src/tasks/kanban-types.ts +2 -3
- package/src/tasks/presets/sales.ts +7 -7
- 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/README.md +31 -0
- package/src/thread/index.ts +12 -0
- package/src/thread/logic.ts +61 -0
- package/src/thread/thread.svelte +432 -0
- package/src/thread/thread.test.ts +73 -0
- package/src/thread/types.ts +93 -0
- 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/components/list-page/section-label.svelte +0 -39
- package/src/tasks/define-statuses.ts +0 -32
- package/src/tasks/inbox/inbox.svelte +0 -203
- package/src/tasks/record/demo-data.ts +0 -286
- package/src/tasks/record/field-section.svelte +0 -53
- package/src/tasks/record/fields.ts +0 -142
- package/src/tasks/record/format.ts +0 -87
- package/src/tasks/record/kinds.ts +0 -42
- package/src/tasks/record/logic.ts +0 -324
- package/src/tasks/record/next-up.svelte +0 -70
- package/src/tasks/record/record-stats.svelte +0 -48
- package/src/tasks/record/record-timeline.svelte +0 -231
- package/src/tasks/record/record.test.ts +0 -350
- package/src/tasks/record/types.ts +0 -280
- package/src/tasks/record/where-we-are.svelte +0 -121
- package/src/tasks/side-panel.svelte +0 -193
- package/src/tasks/status-types.ts +0 -40
- /package/src/components/{list-page → toolbar}/toolbar-trigger.ts +0 -0
- /package/src/{tasks → panel}/side-panel-types.ts +0 -0
- /package/src/{tasks/record → record}/gutter.ts +0 -0
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
// The record's pure logic: grouping, merging, picking. No DOM, no clock of its own.
|
|
2
|
-
import type {
|
|
3
|
-
RecordEvent,
|
|
4
|
-
RecordFilterDef,
|
|
5
|
-
RecordNext,
|
|
6
|
-
RecordSummary,
|
|
7
|
-
StageDef,
|
|
8
|
-
StepDef,
|
|
9
|
-
StepGroupDef,
|
|
10
|
-
StepMarker,
|
|
11
|
-
} from "./types.js";
|
|
12
|
-
|
|
13
|
-
// =========================
|
|
14
|
-
// EVENTS
|
|
15
|
-
// =========================
|
|
16
|
-
|
|
17
|
-
/** The calendar day of a timestamp as `YYYY-MM-DD`, in the given zone or the viewer's own. */
|
|
18
|
-
export function localDay(iso: string, timeZone?: string): string {
|
|
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
|
-
}
|
|
26
|
-
|
|
27
|
-
export type DayGroup = { day: string; events: RecordEvent[] };
|
|
28
|
-
|
|
29
|
-
/** Newest day first, newest event first inside a day. */
|
|
30
|
-
export function groupEventsByDay(events: readonly RecordEvent[], timeZone?: string): DayGroup[] {
|
|
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
|
-
}
|
|
43
|
-
|
|
44
|
-
/** `today`, `yesterday` or `date`, by calendar distance from `today`. */
|
|
45
|
-
export function dayRelation(day: string, today: string): "today" | "yesterday" | "date" {
|
|
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
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The events one filter chip shows. An unknown or empty key shows everything. */
|
|
52
|
-
export function eventsForFilter(
|
|
53
|
-
events: readonly RecordEvent[],
|
|
54
|
-
filterKey: string,
|
|
55
|
-
filters: readonly RecordFilterDef[],
|
|
56
|
-
): RecordEvent[] {
|
|
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
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Per-chip counts. Chips with no events are the host's to hide. */
|
|
64
|
-
export function filterCounts(
|
|
65
|
-
events: readonly RecordEvent[],
|
|
66
|
-
filters: readonly RecordFilterDef[],
|
|
67
|
-
): Record<string, number> {
|
|
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
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Keep the newest `limit` events and count the rest, cutting a day group where the limit falls. */
|
|
77
|
-
export function foldGroups(
|
|
78
|
-
groups: readonly DayGroup[],
|
|
79
|
-
limit: number,
|
|
80
|
-
): { groups: DayGroup[]; hidden: number } {
|
|
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
|
-
}
|
|
94
|
-
|
|
95
|
-
export type ProjectEvents = {
|
|
96
|
-
id: string;
|
|
97
|
-
name: string;
|
|
98
|
-
href?: string | null;
|
|
99
|
-
events: readonly RecordEvent[];
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* One client's timeline: its own events and every project's, each project event
|
|
104
|
-
* tagged with where it came from. An id seen twice keeps the first, so a source
|
|
105
|
-
* passed in both lists draws once. Newest first.
|
|
106
|
-
*/
|
|
107
|
-
export function mergeClientEvents(
|
|
108
|
-
clientEvents: readonly RecordEvent[],
|
|
109
|
-
projects: readonly ProjectEvents[],
|
|
110
|
-
): RecordEvent[] {
|
|
111
|
-
const seen = new Set<string>();
|
|
112
|
-
const merged: RecordEvent[] = [];
|
|
113
|
-
const add = (event: RecordEvent) => {
|
|
114
|
-
if (seen.has(event.id)) return;
|
|
115
|
-
seen.add(event.id);
|
|
116
|
-
merged.push(event);
|
|
117
|
-
};
|
|
118
|
-
for (const project of projects) {
|
|
119
|
-
for (const event of project.events) {
|
|
120
|
-
add({
|
|
121
|
-
...event,
|
|
122
|
-
projectId: project.id,
|
|
123
|
-
projectName: project.name,
|
|
124
|
-
projectHref: project.href ?? null,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
for (const event of clientEvents) 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
|
-
}
|
|
131
|
-
|
|
132
|
-
// =========================
|
|
133
|
-
// PROJECTS
|
|
134
|
-
// =========================
|
|
135
|
-
|
|
136
|
-
const ACTIVE: ReadonlySet<string> = new Set(["open", "proposal", "ongoing"]);
|
|
137
|
-
|
|
138
|
-
/** Projects still in play: not closed, and in a stage that isn't won-and-done or lost. */
|
|
139
|
-
export function activeProjects(
|
|
140
|
-
projects: readonly RecordSummary[],
|
|
141
|
-
stages: readonly StageDef[],
|
|
142
|
-
): RecordSummary[] {
|
|
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
|
-
}
|
|
150
|
-
|
|
151
|
-
/** The earliest dated next across active projects, undated ones after, ties by list order. */
|
|
152
|
-
export function nextAcrossProjects(
|
|
153
|
-
projects: readonly RecordSummary[],
|
|
154
|
-
stages: readonly StageDef[],
|
|
155
|
-
): RecordNext | null {
|
|
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
|
-
}
|
|
170
|
-
|
|
171
|
-
export type LatestStatus = NonNullable<RecordSummary["whereWeAre"]> & {
|
|
172
|
-
projectId: string;
|
|
173
|
-
projectName: string;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
/** The newest "where we are" across projects, tagged with its project. */
|
|
177
|
-
export function latestWhereWeAre(projects: readonly RecordSummary[]): LatestStatus | null {
|
|
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) best = { ...status, projectId: project.id, projectName: project.name };
|
|
185
|
-
}
|
|
186
|
-
return best;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export type MoneyTotals = Record<string, { pipeline: number; won: number; paid: number }>;
|
|
190
|
-
|
|
191
|
-
export type ClientRollup = {
|
|
192
|
-
count: number;
|
|
193
|
-
activeCount: number;
|
|
194
|
-
totals: MoneyTotals;
|
|
195
|
-
lastCallAt: string | null;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* The client's strip. Pipeline is the value of projects in an open or proposal
|
|
200
|
-
* stage, won the value in an ongoing or completed one, paid what came in. A lost
|
|
201
|
-
* project adds to none. Totals stay per currency, because a sum across two is a lie.
|
|
202
|
-
*/
|
|
203
|
-
export function rollupProjects(
|
|
204
|
-
projects: readonly RecordSummary[],
|
|
205
|
-
stages: readonly StageDef[],
|
|
206
|
-
fallbackCurrency = "EUR",
|
|
207
|
-
): ClientRollup {
|
|
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)) lastCallAt = project.lastCallAt;
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
count: projects.length,
|
|
223
|
-
activeCount: activeProjects(projects, stages).length,
|
|
224
|
-
totals,
|
|
225
|
-
lastCallAt,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// =========================
|
|
230
|
-
// STEPS
|
|
231
|
-
// =========================
|
|
232
|
-
|
|
233
|
-
/** A step is visible unless hidden. */
|
|
234
|
-
export function visibleSteps(steps: readonly StepDef[]): StepDef[] {
|
|
235
|
-
return steps.filter((s) => !s.hidden);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function stepProgress(steps: readonly StepDef[]): { done: number; total: number } {
|
|
239
|
-
const visible = visibleSteps(steps);
|
|
240
|
-
return { done: visible.filter((s) => s.done).length, total: visible.length };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* One marker per visible step. The current step is the first one not done. Any
|
|
245
|
-
* open step before it, or past its own date, is late. Nothing else is.
|
|
246
|
-
* `today` is an ISO date.
|
|
247
|
-
*/
|
|
248
|
-
export function stepMarkers(steps: readonly StepDef[], today: string): Map<string, StepMarker> {
|
|
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
|
-
}
|
|
269
|
-
|
|
270
|
-
export type StepBucket = { key: string; label: string | null; steps: StepDef[] };
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Buckets by group in the declared order, not by consecutive runs, so a reordered
|
|
274
|
-
* rail still draws one header per group. Steps with no or an unknown group go last,
|
|
275
|
-
* under no header.
|
|
276
|
-
*/
|
|
277
|
-
export function groupSteps(steps: readonly StepDef[], groups: readonly StepGroupDef[] = []): StepBucket[] {
|
|
278
|
-
const known = new Set(groups.map((g) => g.key));
|
|
279
|
-
const buckets: StepBucket[] = groups.map((g) => ({ key: g.key, label: g.label, steps: [] }));
|
|
280
|
-
const loose: StepBucket = { key: "", label: null, steps: [] };
|
|
281
|
-
for (const step of steps) {
|
|
282
|
-
const bucket = buckets.find((b) => b.key === step.group);
|
|
283
|
-
(step.group && known.has(step.group) && bucket ? bucket : loose).steps.push(step);
|
|
284
|
-
}
|
|
285
|
-
const result = buckets.filter((b) => b.steps.length > 0);
|
|
286
|
-
if (loose.steps.length > 0) result.push(loose);
|
|
287
|
-
return result;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Move `movedId` next to `targetId` inside the id list. Same list back when nothing moves. */
|
|
291
|
-
export function reorderIds(
|
|
292
|
-
ids: readonly string[],
|
|
293
|
-
movedId: string,
|
|
294
|
-
targetId: string,
|
|
295
|
-
before: boolean,
|
|
296
|
-
): string[] {
|
|
297
|
-
if (movedId === targetId) return [...ids];
|
|
298
|
-
const from = ids.indexOf(movedId);
|
|
299
|
-
if (from < 0 || !ids.includes(targetId)) return [...ids];
|
|
300
|
-
const next = [...ids];
|
|
301
|
-
next.splice(from, 1);
|
|
302
|
-
const at = next.indexOf(targetId);
|
|
303
|
-
next.splice(before ? at : at + 1, 0, movedId);
|
|
304
|
-
return next;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Swap a step with its neighbour in its own group. Same list back at an edge. */
|
|
308
|
-
export function nudgeStep(steps: readonly StepDef[], id: string, direction: -1 | 1): string[] {
|
|
309
|
-
const ids = steps.map((s) => s.id);
|
|
310
|
-
const step = steps.find((s) => s.id === id);
|
|
311
|
-
if (!step) return ids;
|
|
312
|
-
const mates = steps.filter((s) => s.group === step.group && !s.hidden);
|
|
313
|
-
const at = mates.findIndex((s) => s.id === id);
|
|
314
|
-
const neighbour = mates[at + direction];
|
|
315
|
-
if (!neighbour) return ids;
|
|
316
|
-
const a = ids.indexOf(id);
|
|
317
|
-
const b = ids.indexOf(neighbour.id);
|
|
318
|
-
const first = ids[a];
|
|
319
|
-
const second = ids[b];
|
|
320
|
-
if (first === undefined || second === undefined) return ids;
|
|
321
|
-
ids[a] = second;
|
|
322
|
-
ids[b] = first;
|
|
323
|
-
return ids;
|
|
324
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
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-badge.svelte";
|
|
9
|
-
import { gutterClass } from "./gutter.js";
|
|
10
|
-
import { formatRange } from "./format.js";
|
|
11
|
-
import { todayIso } from "../dates.js";
|
|
12
|
-
import { resolveLabels, type RecordLabels, type RecordNext } from "./types.js";
|
|
13
|
-
|
|
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 { next, showProject = false, labels, locale, today = todayIso(), class: className }: Props = $props();
|
|
28
|
-
|
|
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
|
-
</script>
|
|
37
|
-
|
|
38
|
-
<!-- =========================== -->
|
|
39
|
-
<!-- MARKUP -->
|
|
40
|
-
<!-- =========================== -->
|
|
41
|
-
{#if next}
|
|
42
|
-
<section
|
|
43
|
-
aria-label={t.next}
|
|
44
|
-
class={cn(
|
|
45
|
-
"flex flex-wrap items-center gap-x-2.5 gap-y-1 border-b border-border py-2 text-sm",
|
|
46
|
-
gutterClass,
|
|
47
|
-
className,
|
|
48
|
-
)}
|
|
49
|
-
>
|
|
50
|
-
<ArrowRight class="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
51
|
-
<span class="font-medium">{t.next}</span>
|
|
52
|
-
{#if when}
|
|
53
|
-
<span class={cn("tabular-nums", late ? TONE_TEXT.warning : "text-muted-foreground")}>{when}</span>
|
|
54
|
-
{/if}
|
|
55
|
-
{#if next.href}
|
|
56
|
-
<a href={next.href} class="min-w-0 truncate underline-offset-2 hover:underline">{next.label}</a>
|
|
57
|
-
{:else}
|
|
58
|
-
<span class="min-w-0 truncate">{next.label}</span>
|
|
59
|
-
{/if}
|
|
60
|
-
{#if next.owner}
|
|
61
|
-
<span class="text-muted-foreground">{next.owner}</span>
|
|
62
|
-
{/if}
|
|
63
|
-
{#if showProject && next.projectName}
|
|
64
|
-
<span class="text-muted-foreground">{next.projectName}</span>
|
|
65
|
-
{/if}
|
|
66
|
-
{#if late}
|
|
67
|
-
<StatusBadge tone="warning" capitalize={false}>{t.late}</StatusBadge>
|
|
68
|
-
{/if}
|
|
69
|
-
</section>
|
|
70
|
-
{/if}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
// =========================
|
|
3
|
-
// IMPORTS
|
|
4
|
-
// =========================
|
|
5
|
-
import { cn } from "../../lib/utils.js";
|
|
6
|
-
import type { PanelStat } from "../side-panel-types.js";
|
|
7
|
-
import { gutterClass } from "./gutter.js";
|
|
8
|
-
|
|
9
|
-
// =========================
|
|
10
|
-
// PROPS
|
|
11
|
-
// =========================
|
|
12
|
-
interface Props {
|
|
13
|
-
stats: PanelStat[];
|
|
14
|
-
/** Sits flush with the record's gutter. Off inside a card. */
|
|
15
|
-
gutter?: boolean;
|
|
16
|
-
class?: string;
|
|
17
|
-
}
|
|
18
|
-
let { stats, gutter = true, class: className }: Props = $props();
|
|
19
|
-
|
|
20
|
-
// =========================
|
|
21
|
-
// DERIVED
|
|
22
|
-
// =========================
|
|
23
|
-
const TONE = {
|
|
24
|
-
default: "",
|
|
25
|
-
muted: "text-muted-foreground",
|
|
26
|
-
good: "text-success",
|
|
27
|
-
bad: "text-destructive",
|
|
28
|
-
} as const;
|
|
29
|
-
</script>
|
|
30
|
-
|
|
31
|
-
<!-- =========================== -->
|
|
32
|
-
<!-- MARKUP -->
|
|
33
|
-
<!-- =========================== -->
|
|
34
|
-
{#if stats.length > 0}
|
|
35
|
-
<dl class={cn("flex flex-wrap gap-x-6 gap-y-2", gutter && gutterClass, className)}>
|
|
36
|
-
{#each stats as stat (stat.label)}
|
|
37
|
-
<div class="min-w-0">
|
|
38
|
-
<dt class="text-xs text-muted-foreground">{stat.label}</dt>
|
|
39
|
-
<dd class={cn("text-sm font-medium tabular-nums", TONE[stat.tone ?? "default"])}>
|
|
40
|
-
{stat.value}
|
|
41
|
-
{#if stat.sub}
|
|
42
|
-
<span class="text-xs font-normal text-muted-foreground">{stat.sub}</span>
|
|
43
|
-
{/if}
|
|
44
|
-
</dd>
|
|
45
|
-
</div>
|
|
46
|
-
{/each}
|
|
47
|
-
</dl>
|
|
48
|
-
{/if}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
// =========================
|
|
3
|
-
// IMPORTS
|
|
4
|
-
// =========================
|
|
5
|
-
import type { Snippet } from "svelte";
|
|
6
|
-
import { Circle } from "phosphor-svelte";
|
|
7
|
-
import { SvelteSet } from "svelte/reactivity";
|
|
8
|
-
import { cn } from "../../lib/utils.js";
|
|
9
|
-
import { Button } from "../../shadcn/button/index.js";
|
|
10
|
-
import { Badge } from "../../shadcn/badge/index.js";
|
|
11
|
-
import { Skeleton } from "../../shadcn/skeleton/index.js";
|
|
12
|
-
import { SegmentedFilter } from "../../components/segmented/index.js";
|
|
13
|
-
import { TONE_ICON } from "../../lib/tone.js";
|
|
14
|
-
import { DEFAULT_RECORD_FILTERS, DEFAULT_RECORD_KINDS } from "./kinds.js";
|
|
15
|
-
import { formatDayHeader, formatMoney, formatTime } from "./format.js";
|
|
16
|
-
import {
|
|
17
|
-
dayRelation,
|
|
18
|
-
eventsForFilter,
|
|
19
|
-
filterCounts,
|
|
20
|
-
foldGroups,
|
|
21
|
-
groupEventsByDay,
|
|
22
|
-
localDay,
|
|
23
|
-
} from "./logic.js";
|
|
24
|
-
import {
|
|
25
|
-
resolveLabels,
|
|
26
|
-
type RecordEvent,
|
|
27
|
-
type RecordFilterDef,
|
|
28
|
-
type RecordKindDef,
|
|
29
|
-
type RecordLabels,
|
|
30
|
-
} from "./types.js";
|
|
31
|
-
|
|
32
|
-
// =========================
|
|
33
|
-
// PROPS
|
|
34
|
-
// =========================
|
|
35
|
-
interface Props {
|
|
36
|
-
events: RecordEvent[];
|
|
37
|
-
kinds?: RecordKindDef[];
|
|
38
|
-
filters?: RecordFilterDef[];
|
|
39
|
-
/** The chip picked: `all` or a filter key. */
|
|
40
|
-
filter?: string;
|
|
41
|
-
/** Draws a kind's content in place of the default title and body. The time,
|
|
42
|
-
the project chip and the amount stay the timeline's. */
|
|
43
|
-
entry?: Record<string, Snippet<[RecordEvent]>>;
|
|
44
|
-
/** Client mode: a chip on each entry says which project it came from. */
|
|
45
|
-
showProject?: boolean;
|
|
46
|
-
/** Entries drawn before the rest fold behind one button. */
|
|
47
|
-
limit?: number;
|
|
48
|
-
/** Above the list: a note composer, a status form. */
|
|
49
|
-
composer?: Snippet;
|
|
50
|
-
loading?: boolean;
|
|
51
|
-
labels?: Partial<RecordLabels>;
|
|
52
|
-
locale?: string;
|
|
53
|
-
/** IANA zone for day boundaries. Defaults to the viewer's own. */
|
|
54
|
-
timeZone?: string;
|
|
55
|
-
/** ISO date for "Today". Defaults to the viewer's today. */
|
|
56
|
-
today?: string;
|
|
57
|
-
class?: string;
|
|
58
|
-
}
|
|
59
|
-
let {
|
|
60
|
-
events,
|
|
61
|
-
kinds = DEFAULT_RECORD_KINDS,
|
|
62
|
-
filters = DEFAULT_RECORD_FILTERS,
|
|
63
|
-
filter = $bindable("all"),
|
|
64
|
-
entry,
|
|
65
|
-
showProject = false,
|
|
66
|
-
limit = 30,
|
|
67
|
-
composer,
|
|
68
|
-
loading = false,
|
|
69
|
-
labels,
|
|
70
|
-
locale,
|
|
71
|
-
timeZone,
|
|
72
|
-
today,
|
|
73
|
-
class: className,
|
|
74
|
-
}: Props = $props();
|
|
75
|
-
|
|
76
|
-
// =========================
|
|
77
|
-
// STATE
|
|
78
|
-
// =========================
|
|
79
|
-
let showAll = $state(false);
|
|
80
|
-
const expanded = new SvelteSet<string>();
|
|
81
|
-
|
|
82
|
-
// =========================
|
|
83
|
-
// DERIVED
|
|
84
|
-
// =========================
|
|
85
|
-
let t = $derived(resolveLabels(labels));
|
|
86
|
-
let kindByKey = $derived(new Map(kinds.map((k) => [k.key, k])));
|
|
87
|
-
let counts = $derived(filterCounts(events, filters));
|
|
88
|
-
let chips = $derived([
|
|
89
|
-
{ value: "all", label: t.all, count: events.length },
|
|
90
|
-
...filters.filter((f) => (counts[f.key] ?? 0) > 0).map((f) => ({ value: f.key, label: f.label, count: counts[f.key] })),
|
|
91
|
-
]);
|
|
92
|
-
let shown = $derived(eventsForFilter(events, filter, filters));
|
|
93
|
-
let grouped = $derived(groupEventsByDay(shown, timeZone));
|
|
94
|
-
let folded = $derived(showAll ? { groups: grouped, hidden: 0 } : foldGroups(grouped, limit));
|
|
95
|
-
let todayDay = $derived(today ?? localDay(new Date().toISOString(), timeZone));
|
|
96
|
-
|
|
97
|
-
// =========================
|
|
98
|
-
// FUNCTIONS
|
|
99
|
-
// =========================
|
|
100
|
-
function dayLabel(day: string): string {
|
|
101
|
-
const relation = dayRelation(day, todayDay);
|
|
102
|
-
if (relation === "today") return t.today;
|
|
103
|
-
if (relation === "yesterday") return t.yesterday;
|
|
104
|
-
return formatDayHeader(day, locale);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function kindOf(event: RecordEvent): RecordKindDef {
|
|
108
|
-
return kindByKey.get(event.kind) ?? { key: event.kind, label: event.kind };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// A body this long, or with a line break, gets a More toggle. Measuring each
|
|
112
|
-
// row would re-layout the list on every resize.
|
|
113
|
-
function isLong(body: string | null | undefined): boolean {
|
|
114
|
-
return Boolean(body && (body.length > 180 || body.includes("\n")));
|
|
115
|
-
}
|
|
116
|
-
</script>
|
|
117
|
-
|
|
118
|
-
<!-- =========================== -->
|
|
119
|
-
<!-- MARKUP -->
|
|
120
|
-
<!-- =========================== -->
|
|
121
|
-
<div class={cn("flex min-w-0 flex-col gap-4", className)}>
|
|
122
|
-
{@render composer?.()}
|
|
123
|
-
|
|
124
|
-
{#if chips.length > 1}
|
|
125
|
-
<div class="no-scrollbar -mx-1 overflow-x-auto px-1">
|
|
126
|
-
<SegmentedFilter
|
|
127
|
-
options={chips}
|
|
128
|
-
bind:value={filter}
|
|
129
|
-
ariaLabel={t.activity}
|
|
130
|
-
block={false}
|
|
131
|
-
max="none"
|
|
132
|
-
/>
|
|
133
|
-
</div>
|
|
134
|
-
{/if}
|
|
135
|
-
|
|
136
|
-
{#if loading}
|
|
137
|
-
<div class="flex flex-col gap-3">
|
|
138
|
-
{#each [0, 1, 2] as i (i)}
|
|
139
|
-
<Skeleton class="h-12" />
|
|
140
|
-
{/each}
|
|
141
|
-
</div>
|
|
142
|
-
{:else if grouped.length === 0}
|
|
143
|
-
<p class="py-6 text-sm text-muted-foreground">{t.noEvents}</p>
|
|
144
|
-
{:else}
|
|
145
|
-
<div class="flex flex-col gap-5">
|
|
146
|
-
{#each folded.groups as group (group.day)}
|
|
147
|
-
<section class="flex flex-col gap-1">
|
|
148
|
-
<h3 class="text-xs font-medium text-muted-foreground">{dayLabel(group.day)}</h3>
|
|
149
|
-
<ul class="flex flex-col">
|
|
150
|
-
{#each group.events as event (event.id)}
|
|
151
|
-
{@const kind = kindOf(event)}
|
|
152
|
-
{@const Icon = kind.icon ?? Circle}
|
|
153
|
-
{@const custom = entry?.[event.kind]}
|
|
154
|
-
<li class="flex gap-3 border-b border-border/60 py-3 last:border-b-0">
|
|
155
|
-
<span class="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-muted">
|
|
156
|
-
<Icon class={cn("size-3.5", TONE_ICON[kind.tone ?? "neutral"])} aria-hidden="true" />
|
|
157
|
-
<span class="sr-only">{kind.label}</span>
|
|
158
|
-
</span>
|
|
159
|
-
<div class="min-w-0 flex-1">
|
|
160
|
-
{#if custom}
|
|
161
|
-
{@render custom(event)}
|
|
162
|
-
{:else}
|
|
163
|
-
<p class="text-sm leading-snug font-medium wrap-anywhere">
|
|
164
|
-
{#if event.href}
|
|
165
|
-
<a href={event.href} class="underline-offset-2 hover:underline">{event.title}</a>
|
|
166
|
-
{:else}
|
|
167
|
-
{event.title}
|
|
168
|
-
{/if}
|
|
169
|
-
</p>
|
|
170
|
-
{#if event.body}
|
|
171
|
-
<p
|
|
172
|
-
class={cn(
|
|
173
|
-
"mt-0.5 text-sm leading-snug whitespace-pre-line text-muted-foreground wrap-anywhere",
|
|
174
|
-
!expanded.has(event.id) && "line-clamp-3",
|
|
175
|
-
)}
|
|
176
|
-
>
|
|
177
|
-
{event.body}
|
|
178
|
-
</p>
|
|
179
|
-
{#if isLong(event.body)}
|
|
180
|
-
<Button
|
|
181
|
-
variant="ghost"
|
|
182
|
-
size="sm"
|
|
183
|
-
class="-ml-2 text-muted-foreground"
|
|
184
|
-
aria-expanded={expanded.has(event.id)}
|
|
185
|
-
onclick={() => (expanded.has(event.id) ? expanded.delete(event.id) : expanded.add(event.id))}
|
|
186
|
-
>
|
|
187
|
-
{expanded.has(event.id) ? t.less : t.more}
|
|
188
|
-
</Button>
|
|
189
|
-
{/if}
|
|
190
|
-
{/if}
|
|
191
|
-
{#if event.details?.length}
|
|
192
|
-
<dl class="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-xs">
|
|
193
|
-
{#each event.details as detail (detail.label)}
|
|
194
|
-
<div class="flex gap-1.5">
|
|
195
|
-
<dt class="text-muted-foreground">{detail.label}</dt>
|
|
196
|
-
<dd class="font-medium">{detail.value}</dd>
|
|
197
|
-
</div>
|
|
198
|
-
{/each}
|
|
199
|
-
</dl>
|
|
200
|
-
{/if}
|
|
201
|
-
{/if}
|
|
202
|
-
<p class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
|
203
|
-
<span class="tabular-nums">{formatTime(event.occurredAt, locale, timeZone)}</span>
|
|
204
|
-
<span>{kind.label}</span>
|
|
205
|
-
{#if event.amount != null}
|
|
206
|
-
<span class="font-medium text-foreground tabular-nums">
|
|
207
|
-
{formatMoney(event.amount, event.currency ?? undefined, locale)}
|
|
208
|
-
</span>
|
|
209
|
-
{/if}
|
|
210
|
-
{#if event.actor}<span>{t.by(event.actor)}</span>{/if}
|
|
211
|
-
{#if event.edited}<span>{t.edited}</span>{/if}
|
|
212
|
-
{#if showProject && event.projectName}
|
|
213
|
-
<Badge variant="outline" href={event.projectHref ?? undefined} class="max-w-48 truncate">
|
|
214
|
-
{event.projectName}
|
|
215
|
-
</Badge>
|
|
216
|
-
{/if}
|
|
217
|
-
</p>
|
|
218
|
-
</div>
|
|
219
|
-
</li>
|
|
220
|
-
{/each}
|
|
221
|
-
</ul>
|
|
222
|
-
</section>
|
|
223
|
-
{/each}
|
|
224
|
-
</div>
|
|
225
|
-
{#if folded.hidden > 0}
|
|
226
|
-
<Button variant="outline" class="w-fit self-start" onclick={() => (showAll = true)}>
|
|
227
|
-
{t.older(folded.hidden)}
|
|
228
|
-
</Button>
|
|
229
|
-
{/if}
|
|
230
|
-
{/if}
|
|
231
|
-
</div>
|