@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,8 +1,28 @@
|
|
|
1
1
|
import { describe, expect, it } from "vite-plus/test";
|
|
2
2
|
import { dayNumber } from "../dates.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
barWidth,
|
|
5
|
+
buildArrows,
|
|
6
|
+
buildMonths,
|
|
7
|
+
buildDays,
|
|
8
|
+
buildRows,
|
|
9
|
+
liveSpan,
|
|
10
|
+
rangeLabel,
|
|
11
|
+
rangeOf,
|
|
12
|
+
roundedPath,
|
|
13
|
+
spanOf,
|
|
14
|
+
type Drag,
|
|
15
|
+
type Layout,
|
|
16
|
+
} from "./logic.js";
|
|
4
17
|
|
|
5
|
-
type Task = {
|
|
18
|
+
type Task = {
|
|
19
|
+
id: string;
|
|
20
|
+
start?: string;
|
|
21
|
+
end?: string;
|
|
22
|
+
parent?: string;
|
|
23
|
+
group?: string;
|
|
24
|
+
after?: string[];
|
|
25
|
+
};
|
|
6
26
|
|
|
7
27
|
const dates = { getStart: (t: Task) => t.start, getEnd: (t: Task) => t.end };
|
|
8
28
|
const source = (overrides: Partial<Parameters<typeof buildRows<Task>>[1]> = {}) => ({
|
|
@@ -19,12 +39,20 @@ describe("spanOf", () => {
|
|
|
19
39
|
expect(span && span.end - span.start).toBe(2);
|
|
20
40
|
});
|
|
21
41
|
it("lets a lone date stand for a one-day task", () => {
|
|
22
|
-
expect(spanOf({ id: "a", end: "2026-10-05" }, dates)).toEqual({
|
|
23
|
-
|
|
42
|
+
expect(spanOf({ id: "a", end: "2026-10-05" }, dates)).toEqual({
|
|
43
|
+
start: dayNumber("2026-10-05"),
|
|
44
|
+
end: dayNumber("2026-10-05"),
|
|
45
|
+
});
|
|
46
|
+
expect(spanOf({ id: "a", start: "2026-10-05" }, dates)).toEqual({
|
|
47
|
+
start: dayNumber("2026-10-05"),
|
|
48
|
+
end: dayNumber("2026-10-05"),
|
|
49
|
+
});
|
|
24
50
|
});
|
|
25
51
|
it("gives an undated task no span, and never an end before the start", () => {
|
|
26
52
|
expect(spanOf({ id: "a" }, dates)).toBeNull();
|
|
27
|
-
expect(spanOf({ id: "a", start: "2026-10-09", end: "2026-10-05" }, dates)?.end).toBe(
|
|
53
|
+
expect(spanOf({ id: "a", start: "2026-10-09", end: "2026-10-05" }, dates)?.end).toBe(
|
|
54
|
+
dayNumber("2026-10-09"),
|
|
55
|
+
);
|
|
28
56
|
});
|
|
29
57
|
});
|
|
30
58
|
|
|
@@ -46,13 +74,20 @@ describe("liveSpan", () => {
|
|
|
46
74
|
describe("rangeOf", () => {
|
|
47
75
|
const today = dayNumber("2026-10-21");
|
|
48
76
|
it("pads the tasks' span and starts on a Monday", () => {
|
|
49
|
-
const { first, last } = rangeOf(
|
|
77
|
+
const { first, last } = rangeOf(
|
|
78
|
+
[{ id: "a", start: "2026-10-14", end: "2026-10-30" }],
|
|
79
|
+
dates,
|
|
80
|
+
today,
|
|
81
|
+
);
|
|
50
82
|
expect(new Date(first * 86_400_000).getUTCDay()).toBe(1);
|
|
51
83
|
expect(first).toBeLessThanOrEqual(dayNumber("2026-10-07"));
|
|
52
84
|
expect(last).toBe(dayNumber("2026-10-30") + 21);
|
|
53
85
|
});
|
|
54
86
|
it("honours from and to, and never ends before it starts", () => {
|
|
55
|
-
expect(rangeOf([], dates, today, "2026-10-21", "2026-10-01")).toEqual({
|
|
87
|
+
expect(rangeOf([], dates, today, "2026-10-21", "2026-10-01")).toEqual({
|
|
88
|
+
first: today,
|
|
89
|
+
last: today,
|
|
90
|
+
});
|
|
56
91
|
});
|
|
57
92
|
});
|
|
58
93
|
|
|
@@ -91,15 +126,30 @@ describe("buildRows", () => {
|
|
|
91
126
|
{ id: "b", group: "one", start: "2026-10-01", end: "2026-10-02" },
|
|
92
127
|
{ id: "c", group: "one", start: "2026-10-10", end: "2026-10-12" },
|
|
93
128
|
];
|
|
94
|
-
const rows = buildRows(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
const rows = buildRows(
|
|
130
|
+
grouped,
|
|
131
|
+
source({
|
|
132
|
+
groups: [
|
|
133
|
+
{ id: "one", label: "One" },
|
|
134
|
+
{ id: "two", label: "Two" },
|
|
135
|
+
],
|
|
136
|
+
getGroup: (t) => t.group ?? "",
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
expect(rows.map((r) => (r.kind === "group" ? `#${r.group.id}:${r.count}` : r.id))).toEqual([
|
|
140
|
+
"#one:2",
|
|
141
|
+
"b",
|
|
142
|
+
"c",
|
|
143
|
+
"#two:1",
|
|
144
|
+
"a",
|
|
145
|
+
]);
|
|
99
146
|
expect(rows[0].span).toEqual({ start: dayNumber("2026-10-01"), end: dayNumber("2026-10-12") });
|
|
100
147
|
});
|
|
101
148
|
it("applies a live drag to the row it moves", () => {
|
|
102
|
-
const rows = buildRows(
|
|
149
|
+
const rows = buildRows(
|
|
150
|
+
[{ id: "a", start: "2026-10-05", end: "2026-10-06" }],
|
|
151
|
+
source({ drag: { id: "a", mode: "move", originX: 0, dx: 2 } }),
|
|
152
|
+
);
|
|
103
153
|
const row = rows[0];
|
|
104
154
|
if (row.kind !== "task") throw new Error("expected a task row");
|
|
105
155
|
expect(row.span).toEqual({ start: dayNumber("2026-10-07"), end: dayNumber("2026-10-08") });
|
|
@@ -108,7 +158,15 @@ describe("buildRows", () => {
|
|
|
108
158
|
|
|
109
159
|
describe("buildArrows", () => {
|
|
110
160
|
const first = dayNumber("2026-10-05");
|
|
111
|
-
const layout: Layout = {
|
|
161
|
+
const layout: Layout = {
|
|
162
|
+
first,
|
|
163
|
+
last: first + 30,
|
|
164
|
+
dayWidth: 32,
|
|
165
|
+
rowHeight: 40,
|
|
166
|
+
inset: 3,
|
|
167
|
+
timelineWidth: 31 * 32,
|
|
168
|
+
todayDay: first,
|
|
169
|
+
};
|
|
112
170
|
const rows = (tasks: Task[]) => buildRows(tasks, source());
|
|
113
171
|
it("draws one arrow per known dependency and skips unknown or undated ones", () => {
|
|
114
172
|
const tasks: Task[] = [
|
|
@@ -124,7 +182,11 @@ describe("buildArrows", () => {
|
|
|
124
182
|
{ id: "a", start: "2026-10-05", end: "2026-10-05" },
|
|
125
183
|
{ id: "b", start: "2026-10-12", end: "2026-10-12", after: ["a"] },
|
|
126
184
|
];
|
|
127
|
-
const [arrow] = buildArrows(
|
|
185
|
+
const [arrow] = buildArrows(
|
|
186
|
+
rows(tasks),
|
|
187
|
+
layout,
|
|
188
|
+
(t) => tasks.find((x) => x.id === t.id)?.after,
|
|
189
|
+
);
|
|
128
190
|
expect(arrow.d.match(/Q/g)).toHaveLength(2);
|
|
129
191
|
});
|
|
130
192
|
it("routes round the row gap when the bars touch", () => {
|
|
@@ -132,30 +194,58 @@ describe("buildArrows", () => {
|
|
|
132
194
|
{ id: "a", start: "2026-10-05", end: "2026-10-08" },
|
|
133
195
|
{ id: "b", start: "2026-10-06", end: "2026-10-09", after: ["a"] },
|
|
134
196
|
];
|
|
135
|
-
const [arrow] = buildArrows(
|
|
197
|
+
const [arrow] = buildArrows(
|
|
198
|
+
rows(tasks),
|
|
199
|
+
layout,
|
|
200
|
+
(t) => tasks.find((x) => x.id === t.id)?.after,
|
|
201
|
+
);
|
|
136
202
|
expect(arrow.d.match(/Q/g)).toHaveLength(4);
|
|
137
203
|
});
|
|
138
204
|
});
|
|
139
205
|
|
|
140
206
|
describe("roundedPath", () => {
|
|
141
207
|
it("rounds each corner by up to the radius and ends on the last point", () => {
|
|
142
|
-
expect(
|
|
208
|
+
expect(
|
|
209
|
+
roundedPath(
|
|
210
|
+
[
|
|
211
|
+
[0, 0],
|
|
212
|
+
[20, 0],
|
|
213
|
+
[20, 20],
|
|
214
|
+
],
|
|
215
|
+
6,
|
|
216
|
+
),
|
|
217
|
+
).toBe("M0,0 L14,0 Q20,0 20,6 L20,20");
|
|
143
218
|
});
|
|
144
219
|
});
|
|
145
220
|
|
|
146
221
|
describe("timeline helpers", () => {
|
|
147
222
|
it("groups days under their month", () => {
|
|
148
223
|
const first = dayNumber("2026-10-30");
|
|
149
|
-
const months = buildMonths(
|
|
224
|
+
const months = buildMonths(
|
|
225
|
+
buildDays(first, 4),
|
|
226
|
+
new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", timeZone: "UTC" }),
|
|
227
|
+
);
|
|
150
228
|
expect(months).toEqual([
|
|
151
229
|
{ key: "2026-10", label: "October 2026", i: 0, length: 2 },
|
|
152
230
|
{ key: "2026-11", label: "November 2026", i: 2, length: 2 },
|
|
153
231
|
]);
|
|
154
232
|
});
|
|
155
233
|
it("never draws a bar under 8px, and labels the span", () => {
|
|
156
|
-
const layout: Layout = {
|
|
234
|
+
const layout: Layout = {
|
|
235
|
+
first: 0,
|
|
236
|
+
last: 9,
|
|
237
|
+
dayWidth: 4,
|
|
238
|
+
rowHeight: 40,
|
|
239
|
+
inset: 0,
|
|
240
|
+
timelineWidth: 40,
|
|
241
|
+
todayDay: 0,
|
|
242
|
+
};
|
|
157
243
|
expect(barWidth({ start: 1, end: 1 }, layout)).toBe(8);
|
|
158
|
-
const format = new Intl.DateTimeFormat("en-US", {
|
|
244
|
+
const format = new Intl.DateTimeFormat("en-US", {
|
|
245
|
+
month: "short",
|
|
246
|
+
day: "numeric",
|
|
247
|
+
timeZone: "UTC",
|
|
248
|
+
});
|
|
159
249
|
const start = dayNumber("2026-10-05");
|
|
160
250
|
expect(rangeLabel({ start, end: start }, format)).toBe("Oct 5 to Oct 5, 1 day");
|
|
161
251
|
expect(rangeLabel({ start, end: start + 2 }, format)).toBe("Oct 5 to Oct 7, 3 days");
|
package/src/tasks/gantt/logic.ts
CHANGED
|
@@ -68,7 +68,8 @@ export function spanOf<T>(item: T, { getStart, getEnd }: Dates<T>): Span | null
|
|
|
68
68
|
export function liveSpan(span: Span | null, drag: Drag | null, id: string): Span | null {
|
|
69
69
|
if (!span || drag?.id !== id || drag.dx === 0) return span;
|
|
70
70
|
if (drag.mode === "move") return { start: span.start + drag.dx, end: span.end + drag.dx };
|
|
71
|
-
if (drag.mode === "start")
|
|
71
|
+
if (drag.mode === "start")
|
|
72
|
+
return { start: Math.min(span.start + drag.dx, span.end), end: span.end };
|
|
72
73
|
return { start: span.start, end: Math.max(span.end + drag.dx, span.start) };
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -103,7 +104,13 @@ export function buildDays(first: number, count: number) {
|
|
|
103
104
|
return Array.from({ length: count }, (_, i) => {
|
|
104
105
|
const day = first + i;
|
|
105
106
|
const wd = weekday(day);
|
|
106
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
day,
|
|
109
|
+
i,
|
|
110
|
+
date: Number(isoFromDay(day).slice(8, 10)),
|
|
111
|
+
wd,
|
|
112
|
+
weekend: wd === 0 || wd === 6,
|
|
113
|
+
};
|
|
107
114
|
});
|
|
108
115
|
}
|
|
109
116
|
export type GanttDay = ReturnType<typeof buildDays>[number];
|
|
@@ -133,7 +140,17 @@ export function buildRows<T>(items: T[], source: RowSource<T>): Row<T>[] {
|
|
|
133
140
|
for (const item of members) {
|
|
134
141
|
const id = getId(item);
|
|
135
142
|
const own = live(item, id);
|
|
136
|
-
out.push({
|
|
143
|
+
out.push({
|
|
144
|
+
kind: "task",
|
|
145
|
+
item,
|
|
146
|
+
id,
|
|
147
|
+
order: order++,
|
|
148
|
+
depth: 0,
|
|
149
|
+
hasChildren: false,
|
|
150
|
+
open: true,
|
|
151
|
+
own,
|
|
152
|
+
span: own,
|
|
153
|
+
});
|
|
137
154
|
}
|
|
138
155
|
return;
|
|
139
156
|
}
|
|
@@ -149,7 +166,17 @@ export function buildRows<T>(items: T[], source: RowSource<T>): Row<T>[] {
|
|
|
149
166
|
const children = kids.get(id) ?? [];
|
|
150
167
|
const open = !folded.has(id);
|
|
151
168
|
const own = live(item, id);
|
|
152
|
-
const row: TaskRow<T> = {
|
|
169
|
+
const row: TaskRow<T> = {
|
|
170
|
+
kind: "task",
|
|
171
|
+
item,
|
|
172
|
+
id,
|
|
173
|
+
order: 0,
|
|
174
|
+
depth,
|
|
175
|
+
hasChildren: children.length > 0,
|
|
176
|
+
open,
|
|
177
|
+
own,
|
|
178
|
+
span: own,
|
|
179
|
+
};
|
|
153
180
|
if (visible) {
|
|
154
181
|
row.order = order++;
|
|
155
182
|
out.push(row);
|
|
@@ -195,7 +222,11 @@ export function roundedPath(points: [number, number][], radius: number): string
|
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
/** Each arrow runs from the end of the task waited on to the start of the one waiting. */
|
|
198
|
-
export function buildArrows<T>(
|
|
225
|
+
export function buildArrows<T>(
|
|
226
|
+
rows: Row<T>[],
|
|
227
|
+
layout: Layout,
|
|
228
|
+
getDependsOn: (item: T) => string[] | undefined,
|
|
229
|
+
): Arrow[] {
|
|
199
230
|
const { rowHeight, inset } = layout;
|
|
200
231
|
const at = new Map<string, { row: number; span: Span }>();
|
|
201
232
|
rows.forEach((row, index) => {
|
|
@@ -216,7 +247,12 @@ export function buildArrows<T>(rows: Row<T>[], layout: Layout, getDependsOn: (it
|
|
|
216
247
|
// Room between the bars: one bend in the gap. No room: out, round the row gap, and back in.
|
|
217
248
|
const points: [number, number][] =
|
|
218
249
|
x2 - x1 >= 24
|
|
219
|
-
? [
|
|
250
|
+
? [
|
|
251
|
+
[x1, y1],
|
|
252
|
+
[x1 + 12, y1],
|
|
253
|
+
[x1 + 12, y2],
|
|
254
|
+
[x2, y2],
|
|
255
|
+
]
|
|
220
256
|
: [
|
|
221
257
|
[x1, y1],
|
|
222
258
|
[x1 + 10, y1],
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { Button } from "../../shadcn/button/index.js";
|
|
6
6
|
import { Textarea } from "../../shadcn/textarea/index.js";
|
|
7
7
|
import { cn } from "../../lib/utils.js";
|
|
8
|
-
import StatusBadge from "
|
|
9
|
-
import StatusDot from "
|
|
8
|
+
import StatusBadge from "../../status/status-badge.svelte";
|
|
9
|
+
import StatusDot from "../../status/status-dot.svelte";
|
|
10
10
|
import { TONE_TEXT } from "../../lib/tone.js";
|
|
11
11
|
import InboxDraftEditor from "./inbox-draft.svelte";
|
|
12
12
|
import { canUndo, isOpen, startDraft } from "./inbox-logic.js";
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
onverdict: (verdict: InboxVerdict) => void;
|
|
30
30
|
/** Replaces the summary lines, for a host that renders its own proposal. */
|
|
31
31
|
renderProposal?: Snippet<[InboxItem]>;
|
|
32
|
-
}
|
|
32
|
+
}
|
|
33
33
|
|
|
34
34
|
let { item, selected = false, onselect, onverdict, renderProposal }: Props = $props();
|
|
35
35
|
|
|
@@ -158,7 +158,9 @@
|
|
|
158
158
|
class={cn(
|
|
159
159
|
"flex flex-col gap-3 rounded-xl p-3 outline-none transition-[background-color,border-color,box-shadow] duration-[var(--duration-quick,120ms)] ease-[var(--ease-smooth-out,ease-out)]",
|
|
160
160
|
"focus-visible:ring-ring/50 focus-visible:ring-3",
|
|
161
|
-
open
|
|
161
|
+
open
|
|
162
|
+
? "border-border bg-card border shadow-[var(--elev-card,none)]"
|
|
163
|
+
: "bg-muted/30 border border-transparent",
|
|
162
164
|
selected && open && "border-ring",
|
|
163
165
|
)}
|
|
164
166
|
>
|
|
@@ -228,7 +230,9 @@
|
|
|
228
230
|
<div class="flex flex-col gap-1.5 pl-6">
|
|
229
231
|
<PixelLoader label="Agent is re-running with your note" />
|
|
230
232
|
{#if item.steer}
|
|
231
|
-
<p class="text-muted-foreground border-border border-l-2 pl-3 text-sm"
|
|
233
|
+
<p class="text-muted-foreground border-border border-l-2 pl-3 text-sm">
|
|
234
|
+
“{item.steer}”
|
|
235
|
+
</p>
|
|
232
236
|
{/if}
|
|
233
237
|
</div>
|
|
234
238
|
{/if}
|
|
@@ -259,14 +263,18 @@
|
|
|
259
263
|
<div class="flex flex-wrap items-center gap-2">
|
|
260
264
|
<Button size="sm" onclick={submitSteer} disabled={!note.trim()}>Send to agent</Button>
|
|
261
265
|
<Button size="sm" variant="ghost" onclick={() => (steering = false)}>Cancel</Button>
|
|
262
|
-
<span class="text-muted-foreground text-xs max-sm:hidden"
|
|
266
|
+
<span class="text-muted-foreground text-xs max-sm:hidden"
|
|
267
|
+
>The agent retries with your note.</span
|
|
268
|
+
>
|
|
263
269
|
</div>
|
|
264
270
|
</div>
|
|
265
271
|
{:else if open}
|
|
266
272
|
<div class="flex flex-wrap items-center gap-2 pl-6">
|
|
267
273
|
<Button size="sm" onclick={approve} aria-keyshortcuts="a">{sendLabel}</Button>
|
|
268
274
|
{#if !drafts.some((d) => d.outcome === "decline")}
|
|
269
|
-
<Button size="sm" variant="destructive" onclick={decline} aria-keyshortcuts="d"
|
|
275
|
+
<Button size="sm" variant="destructive" onclick={decline} aria-keyshortcuts="d"
|
|
276
|
+
>Decline</Button
|
|
277
|
+
>
|
|
270
278
|
{/if}
|
|
271
279
|
<Button size="sm" variant="outline" onclick={steer} aria-keyshortcuts="s">Steer</Button>
|
|
272
280
|
{#if drafts.length}
|
|
@@ -1,55 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
InboxDraft,
|
|
3
|
-
InboxItem,
|
|
4
|
-
InboxKindFilter,
|
|
5
|
-
InboxLane,
|
|
6
|
-
InboxProject,
|
|
7
|
-
} from "./inbox-types.js";
|
|
8
|
-
|
|
9
|
-
/** Which lane an item sits in. The agent's own work and the person's decisions never share one. */
|
|
10
|
-
export function laneOf(item: InboxItem): InboxLane {
|
|
11
|
-
if (item.tier === "auto") return "auto";
|
|
12
|
-
return item.state === "proposed" || item.state === "working" ? "needs" : "decided";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function inLane(items: InboxItem[], lane: InboxLane, kind: InboxKindFilter): InboxItem[] {
|
|
16
|
-
return items.filter((item) => laneOf(item) === lane && (kind === "all" || item.kind === kind));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type InboxGroup = { project: InboxProject; items: InboxItem[] };
|
|
20
|
-
|
|
21
|
-
/** Group by project in order of first appearance, so the oldest waiting item keeps its place. */
|
|
22
|
-
export function groupByProject(items: InboxItem[]): InboxGroup[] {
|
|
23
|
-
const groups = new Map<string, InboxGroup>();
|
|
24
|
-
for (const item of items) {
|
|
25
|
-
const group = groups.get(item.project.id);
|
|
26
|
-
if (group) group.items.push(item);
|
|
27
|
-
else groups.set(item.project.id, { project: item.project, items: [item] });
|
|
28
|
-
}
|
|
29
|
-
return [...groups.values()];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Items in the order the screen shows them, so j and k walk what the eye sees. */
|
|
33
|
-
export function flatten(groups: InboxGroup[]): InboxItem[] {
|
|
34
|
-
return groups.flatMap((group) => group.items);
|
|
35
|
-
}
|
|
1
|
+
import type { InboxDraft, InboxItem } from "./inbox-types.js";
|
|
36
2
|
|
|
37
3
|
/** The draft to open on: the agent's pick, else the first. */
|
|
38
4
|
export function startDraft(drafts: InboxDraft[], recommended?: string): InboxDraft | undefined {
|
|
39
|
-
|
|
5
|
+
return drafts.find((draft) => draft.templateId === recommended) ?? drafts[0];
|
|
40
6
|
}
|
|
41
7
|
|
|
42
8
|
/** A person can act on an item only while it waits for them. */
|
|
43
9
|
export function isOpen(item: InboxItem): boolean {
|
|
44
|
-
|
|
10
|
+
return item.tier === "ask" && item.state === "proposed";
|
|
45
11
|
}
|
|
46
12
|
|
|
47
13
|
export function canUndo(item: InboxItem): boolean {
|
|
48
|
-
|
|
14
|
+
return item.tier === "auto" && item.state === "done";
|
|
49
15
|
}
|
|
50
16
|
|
|
51
17
|
/** True when a key press belongs to a text field and so isn't a shortcut. */
|
|
52
18
|
export function isTyping(target: EventTarget | null): boolean {
|
|
53
|
-
|
|
54
|
-
|
|
19
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
20
|
+
return target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName);
|
|
55
21
|
}
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { defineStatuses } from "
|
|
1
|
+
import { defineStatuses } from "../../status/define-statuses.js";
|
|
2
2
|
import { ClipboardText, EnvelopeSimple, Hash, Phone } from "phosphor-svelte";
|
|
3
3
|
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
4
4
|
import type { InboxSourceType } from "./inbox-types.js";
|
|
5
5
|
|
|
6
6
|
/** The lifecycle of an item. The dot and the badge both read this one map. */
|
|
7
7
|
export const inboxStates = defineStatuses({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
proposed: { label: "Needs you", tone: "warning", variant: "open" },
|
|
9
|
+
working: { label: "Agent working", tone: "accent", variant: "progress", progress: 0.6 },
|
|
10
|
+
approved: { label: "Approved", tone: "success", variant: "completed" },
|
|
11
|
+
declined: { label: "Declined", tone: "neutral", variant: "lost" },
|
|
12
|
+
done: { label: "Done by agent", tone: "success", variant: "completed" },
|
|
13
|
+
undone: { label: "Undone", tone: "neutral", variant: "lost" },
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
/** What the agent made of the touchpoint. */
|
|
17
17
|
export const inboxKinds = defineStatuses({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
information: { label: "Information", tone: "info", variant: "filled" },
|
|
19
|
+
action: { label: "Action", tone: "accent", variant: "filled" },
|
|
20
|
+
decision: { label: "Decision", tone: "warning", variant: "filled" },
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
/** How sure the agent is, bucketed. The dot's fill carries the exact score. */
|
|
24
24
|
export const inboxConfidence = defineStatuses({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
high: { label: "Confident", tone: "success", variant: "progress" },
|
|
26
|
+
medium: { label: "Unsure", tone: "warning", variant: "progress" },
|
|
27
|
+
low: { label: "Guessing", tone: "destructive", variant: "progress" },
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
export function confidenceLevel(score: number): "high" | "medium" | "low" {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
if (score >= 0.8) return "high";
|
|
32
|
+
if (score >= 0.5) return "medium";
|
|
33
|
+
return "low";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export const SOURCE_ICON: Record<InboxSourceType, PhosphorIcon> = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
call: Phone,
|
|
38
|
+
email: EnvelopeSimple,
|
|
39
|
+
slack: Hash,
|
|
40
|
+
form: ClipboardText,
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
export const SOURCE_LABEL: Record<InboxSourceType, string> = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
call: "Call",
|
|
45
|
+
email: "Email",
|
|
46
|
+
slack: "Slack",
|
|
47
|
+
form: "Form",
|
|
48
48
|
};
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { SOURCE_ICON, SOURCE_LABEL } from "./inbox-statuses.js";
|
|
4
4
|
import type { InboxTrace } from "./inbox-types.js";
|
|
5
5
|
|
|
6
|
-
interface Props {
|
|
6
|
+
interface Props {
|
|
7
|
+
trace: InboxTrace;
|
|
8
|
+
}
|
|
7
9
|
|
|
8
10
|
let { trace }: Props = $props();
|
|
9
11
|
|
|
@@ -26,7 +28,9 @@
|
|
|
26
28
|
<p class="text-foreground">
|
|
27
29
|
{trace.source.label}
|
|
28
30
|
{#if trace.source.href}
|
|
29
|
-
<a href={trace.source.href} class="text-foreground ml-1 underline underline-offset-2"
|
|
31
|
+
<a href={trace.source.href} class="text-foreground ml-1 underline underline-offset-2"
|
|
32
|
+
>Open source</a
|
|
33
|
+
>
|
|
30
34
|
{/if}
|
|
31
35
|
</p>
|
|
32
36
|
<ul class="flex flex-col gap-2">
|
|
@@ -34,7 +38,9 @@
|
|
|
34
38
|
<li>
|
|
35
39
|
<blockquote class="text-foreground">“{evidence.quote}”</blockquote>
|
|
36
40
|
{#if evidence.by || evidence.at}
|
|
37
|
-
<span class="text-xs tabular-nums"
|
|
41
|
+
<span class="text-xs tabular-nums"
|
|
42
|
+
>{[evidence.by, evidence.at].filter(Boolean).join(", ")}</span
|
|
43
|
+
>
|
|
38
44
|
{/if}
|
|
39
45
|
</li>
|
|
40
46
|
{/each}
|