@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
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
<!-- A labelled row in the rail. In the narrow chip row the label hides and
|
|
13
13
|
names the group for assistive tech. -->
|
|
14
|
-
<div
|
|
15
|
-
|
|
14
|
+
<div
|
|
15
|
+
class="flex min-h-7 items-center @3xl:grid @3xl:grid-cols-[6rem_minmax(0,1fr)]"
|
|
16
|
+
role="group"
|
|
17
|
+
aria-label={label}
|
|
18
|
+
>
|
|
19
|
+
<span class="hidden text-xs font-medium text-muted-foreground @3xl:block" aria-hidden="true"
|
|
20
|
+
>{label}</span
|
|
21
|
+
>
|
|
16
22
|
<div class="flex min-w-0 items-center">{@render children()}</div>
|
|
17
23
|
</div>
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
import { AgentAvatar } from "../../agents/index.js";
|
|
6
6
|
import { TONE_CLASS } from "../../lib/tone.js";
|
|
7
7
|
import { Skeleton } from "../../shadcn/skeleton/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_TASK_DETAIL_LABELS,
|
|
10
|
+
type TaskComment,
|
|
11
|
+
type TaskDetailLabels,
|
|
12
|
+
} from "./detail-types.js";
|
|
9
13
|
|
|
10
14
|
interface Props {
|
|
11
15
|
comments: TaskComment[];
|
|
@@ -31,7 +35,8 @@
|
|
|
31
35
|
labels: labelOverrides,
|
|
32
36
|
agentAvatar,
|
|
33
37
|
avatar,
|
|
34
|
-
when = (comment) =>
|
|
38
|
+
when = (comment) =>
|
|
39
|
+
new Date(comment.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric" }),
|
|
35
40
|
onSend,
|
|
36
41
|
onEdit,
|
|
37
42
|
onDelete,
|
|
@@ -100,7 +105,10 @@
|
|
|
100
105
|
{#each comments as comment (comment.id)}
|
|
101
106
|
<li class="group relative flex gap-3">
|
|
102
107
|
<!-- The rail that ties one avatar to the next, as in an activity feed. -->
|
|
103
|
-
<span
|
|
108
|
+
<span
|
|
109
|
+
class="absolute start-[11.5px] top-8 -bottom-3 w-px bg-border group-last:hidden"
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
></span>
|
|
104
112
|
<span class="relative size-6 shrink-0">
|
|
105
113
|
{#if comment.agent}
|
|
106
114
|
{#if agentAvatar}
|
|
@@ -111,7 +119,10 @@
|
|
|
111
119
|
{:else if avatar}
|
|
112
120
|
{@render avatar(comment)}
|
|
113
121
|
{:else}
|
|
114
|
-
<span
|
|
122
|
+
<span
|
|
123
|
+
class="grid size-6 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground"
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
>
|
|
115
126
|
{initial(comment.author)}
|
|
116
127
|
</span>
|
|
117
128
|
{/if}
|
|
@@ -121,7 +132,12 @@
|
|
|
121
132
|
<div class="flex h-6 items-center gap-2">
|
|
122
133
|
<span class="truncate text-[13px] font-medium">{comment.author}</span>
|
|
123
134
|
{#if comment.agent}
|
|
124
|
-
<span
|
|
135
|
+
<span
|
|
136
|
+
class={cn(
|
|
137
|
+
"inline-flex h-5 items-center rounded-sm border px-1.5 text-xs font-medium",
|
|
138
|
+
TONE_CLASS.accent,
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
125
141
|
{labels.ai}
|
|
126
142
|
</span>
|
|
127
143
|
{/if}
|
|
@@ -130,14 +146,26 @@
|
|
|
130
146
|
<span class="text-xs text-muted-foreground">{labels.commentEdited}</span>
|
|
131
147
|
{/if}
|
|
132
148
|
{#if canEdit(comment) && editingId !== comment.id}
|
|
133
|
-
<span
|
|
149
|
+
<span
|
|
150
|
+
class="ms-auto flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
|
|
151
|
+
>
|
|
134
152
|
{#if onEdit}
|
|
135
|
-
<button
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
class="thread-icon"
|
|
156
|
+
aria-label={labels.commentEdit}
|
|
157
|
+
onclick={() => startEdit(comment)}
|
|
158
|
+
>
|
|
136
159
|
<PencilSimple class="size-3.5" aria-hidden="true" />
|
|
137
160
|
</button>
|
|
138
161
|
{/if}
|
|
139
162
|
{#if onDelete}
|
|
140
|
-
<button
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
class="thread-icon"
|
|
166
|
+
aria-label={labels.commentDelete}
|
|
167
|
+
onclick={() => onDelete?.(comment)}
|
|
168
|
+
>
|
|
141
169
|
<Trash class="size-3.5" aria-hidden="true" />
|
|
142
170
|
</button>
|
|
143
171
|
{/if}
|
|
@@ -153,8 +181,14 @@
|
|
|
153
181
|
class="mt-1 w-full resize-y rounded-md border border-input bg-transparent px-2.5 py-2 text-sm leading-relaxed outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
154
182
|
></textarea>
|
|
155
183
|
<div class="mt-2 flex justify-end gap-1.5">
|
|
156
|
-
<button type="button" class="thread-button" onclick={() => (editingId = null)}
|
|
157
|
-
|
|
184
|
+
<button type="button" class="thread-button" onclick={() => (editingId = null)}
|
|
185
|
+
>{labels.cancel}</button
|
|
186
|
+
>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
class="thread-button thread-primary"
|
|
190
|
+
onclick={() => saveEdit(comment)}>{labels.save}</button
|
|
191
|
+
>
|
|
158
192
|
</div>
|
|
159
193
|
{:else}
|
|
160
194
|
<p class="text-sm leading-relaxed whitespace-pre-wrap text-pretty">{comment.body}</p>
|
|
@@ -168,7 +202,9 @@
|
|
|
168
202
|
{/if}
|
|
169
203
|
|
|
170
204
|
{#if onSend}
|
|
171
|
-
<div
|
|
205
|
+
<div
|
|
206
|
+
class="rounded-lg border border-input transition-shadow focus-within:ring-3 focus-within:ring-ring/50"
|
|
207
|
+
>
|
|
172
208
|
<textarea
|
|
173
209
|
bind:value={draft}
|
|
174
210
|
onkeydown={onComposerKeydown}
|
|
@@ -178,7 +214,12 @@
|
|
|
178
214
|
class="block max-h-60 min-h-16 w-full resize-none border-0 bg-transparent px-3 py-2 text-sm leading-relaxed outline-none [field-sizing:content] placeholder:text-muted-foreground"
|
|
179
215
|
></textarea>
|
|
180
216
|
<div class="flex justify-end px-2 pb-2">
|
|
181
|
-
<button
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
class="thread-button thread-primary"
|
|
220
|
+
onclick={send}
|
|
221
|
+
disabled={!canSend}
|
|
222
|
+
>
|
|
182
223
|
{sending ? labels.saving : labels.commentSend}
|
|
183
224
|
</button>
|
|
184
225
|
</div>
|
|
@@ -54,7 +54,9 @@
|
|
|
54
54
|
function onPointerDown(e: PointerEvent, mode: Drag["mode"]): void {
|
|
55
55
|
if (!editable || e.button !== 0) return;
|
|
56
56
|
e.stopPropagation();
|
|
57
|
-
(e.currentTarget as HTMLElement)
|
|
57
|
+
(e.currentTarget as HTMLElement)
|
|
58
|
+
.closest<HTMLElement>("[data-bar]")
|
|
59
|
+
?.setPointerCapture(e.pointerId);
|
|
58
60
|
setDrag({ id, mode, originX: e.clientX, dx: 0 });
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -95,7 +97,9 @@
|
|
|
95
97
|
role="button"
|
|
96
98
|
tabindex="0"
|
|
97
99
|
aria-roledescription="task bar"
|
|
98
|
-
aria-label="{title}, {describe(span)}.{editable
|
|
100
|
+
aria-label="{title}, {describe(span)}.{editable
|
|
101
|
+
? ' Arrow keys move it a day, Shift plus arrow keys change its end.'
|
|
102
|
+
: ''}"
|
|
99
103
|
class={cn(
|
|
100
104
|
"g-bar g-enter absolute top-1/2 flex -translate-y-1/2 items-center rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
|
|
101
105
|
editable && "cursor-grab",
|
|
@@ -117,15 +121,21 @@
|
|
|
117
121
|
{#if progress > 0}
|
|
118
122
|
<!-- The clip keeps the bar's rounded corners while the fill scales. -->
|
|
119
123
|
<span class="absolute inset-0 overflow-hidden rounded-lg" aria-hidden="true">
|
|
120
|
-
<span class="g-progress absolute inset-0 origin-left" style:transform="scaleX({progress})"
|
|
124
|
+
<span class="g-progress absolute inset-0 origin-left" style:transform="scaleX({progress})"
|
|
125
|
+
></span>
|
|
121
126
|
</span>
|
|
122
127
|
{/if}
|
|
123
128
|
{#if width >= inlineTitleMin}
|
|
124
129
|
<span class="relative truncate px-2.5 text-xs font-medium text-foreground">{title}</span>
|
|
125
130
|
{/if}
|
|
126
131
|
{#if editable}
|
|
127
|
-
<span
|
|
128
|
-
|
|
132
|
+
<span
|
|
133
|
+
class="g-handle left-0"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
onpointerdown={(e) => onPointerDown(e, "start")}
|
|
136
|
+
></span>
|
|
137
|
+
<span class="g-handle right-0" aria-hidden="true" onpointerdown={(e) => onPointerDown(e, "end")}
|
|
138
|
+
></span>
|
|
129
139
|
{/if}
|
|
130
140
|
{#if dragging && drag && drag.dx !== 0}
|
|
131
141
|
<span
|
|
@@ -182,7 +192,8 @@
|
|
|
182
192
|
|
|
183
193
|
.g-progress {
|
|
184
194
|
background: color-mix(in oklab, var(--g-hue) 32%, var(--color-card, white));
|
|
185
|
-
transition: transform var(--duration-fast, 250ms)
|
|
195
|
+
transition: transform var(--duration-fast, 250ms)
|
|
196
|
+
var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
/* Resize grips: a wide invisible hit area, a slim pill that shows on hover. */
|
|
@@ -16,26 +16,55 @@
|
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
<!-- The layer behind every row: weekends, week lines, the today line and arrows. -->
|
|
19
|
-
<div
|
|
19
|
+
<div
|
|
20
|
+
class="pointer-events-none absolute inset-y-0 left-(--g-label)"
|
|
21
|
+
style:width="{layout.timelineWidth}px"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>
|
|
20
24
|
{#each days as d (d.day)}
|
|
21
25
|
{#if d.weekend}
|
|
22
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
class="absolute inset-y-0 bg-foreground/[0.025]"
|
|
28
|
+
style:left="{d.i * layout.dayWidth}px"
|
|
29
|
+
style:width="{layout.dayWidth}px"
|
|
30
|
+
></div>
|
|
23
31
|
{/if}
|
|
24
32
|
{#if d.wd === 1}
|
|
25
|
-
<div
|
|
33
|
+
<div
|
|
34
|
+
class="absolute inset-y-0 w-px bg-border/70"
|
|
35
|
+
style:left="{d.i * layout.dayWidth}px"
|
|
36
|
+
></div>
|
|
26
37
|
{/if}
|
|
27
38
|
{/each}
|
|
28
39
|
{#if layout.todayDay >= layout.first && layout.todayDay <= layout.last}
|
|
29
|
-
<div
|
|
40
|
+
<div
|
|
41
|
+
class="g-today absolute inset-y-0 w-px bg-primary"
|
|
42
|
+
style:left="{dayX(layout, layout.todayDay) + layout.dayWidth / 2}px"
|
|
43
|
+
>
|
|
30
44
|
<span class="absolute -top-1 -left-[3px] size-[7px] rounded-full bg-primary"></span>
|
|
31
45
|
</div>
|
|
32
46
|
{/if}
|
|
33
|
-
<svg
|
|
47
|
+
<svg
|
|
48
|
+
class="absolute inset-0 overflow-visible"
|
|
49
|
+
width={layout.timelineWidth}
|
|
50
|
+
height={rowCount * layout.rowHeight}
|
|
51
|
+
>
|
|
34
52
|
{#each arrows as arrow (arrow.key)}
|
|
35
|
-
{@const lit =
|
|
53
|
+
{@const lit =
|
|
54
|
+
hoveredId === arrow.from ||
|
|
55
|
+
hoveredId === arrow.to ||
|
|
56
|
+
draggingId === arrow.from ||
|
|
57
|
+
draggingId === arrow.to}
|
|
36
58
|
<g class="g-arrow" class:g-arrow-lit={lit}>
|
|
37
59
|
<path d={arrow.d} pathLength="1" fill="none" stroke-width="1.5" class="g-arrow-line"></path>
|
|
38
|
-
<path
|
|
60
|
+
<path
|
|
61
|
+
d={arrow.head}
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke-width="1.5"
|
|
64
|
+
stroke-linecap="round"
|
|
65
|
+
stroke-linejoin="round"
|
|
66
|
+
class="g-arrow-head"
|
|
67
|
+
></path>
|
|
39
68
|
</g>
|
|
40
69
|
{/each}
|
|
41
70
|
</svg>
|
|
@@ -44,7 +73,8 @@
|
|
|
44
73
|
<style>
|
|
45
74
|
.g-today {
|
|
46
75
|
transform-origin: top;
|
|
47
|
-
animation: g-today-in var(--duration-very-slow, 500ms)
|
|
76
|
+
animation: g-today-in var(--duration-very-slow, 500ms)
|
|
77
|
+
var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
|
|
48
78
|
}
|
|
49
79
|
|
|
50
80
|
.g-arrow {
|
|
@@ -59,7 +89,8 @@
|
|
|
59
89
|
.g-arrow-line {
|
|
60
90
|
stroke-dasharray: 1;
|
|
61
91
|
stroke-dashoffset: 0;
|
|
62
|
-
animation: g-draw var(--duration-very-slow, 500ms)
|
|
92
|
+
animation: g-draw var(--duration-very-slow, 500ms)
|
|
93
|
+
var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) 300ms backwards;
|
|
63
94
|
}
|
|
64
95
|
|
|
65
96
|
.g-arrow-head {
|
|
@@ -15,13 +15,19 @@
|
|
|
15
15
|
let hue = $derived(row.group.hue ?? "var(--color-primary)");
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
|
-
<div
|
|
18
|
+
<div
|
|
19
|
+
role="listitem"
|
|
20
|
+
class="relative flex border-b border-border/60 bg-muted/30"
|
|
21
|
+
style:height="{layout.rowHeight}px"
|
|
22
|
+
>
|
|
19
23
|
<!-- Opaque card under the row's muted tint, so bars never show through the pinned column. -->
|
|
20
24
|
<div class="sticky left-0 z-10 w-(--g-label) shrink-0 border-r border-border bg-card">
|
|
21
25
|
<div class="flex h-full items-center gap-2 bg-muted/30 px-3">
|
|
22
26
|
<span class="size-2 shrink-0 rounded-full" style:background={hue}></span>
|
|
23
27
|
<span class="truncate text-sm font-medium text-foreground">{row.group.label}</span>
|
|
24
|
-
<span
|
|
28
|
+
<span
|
|
29
|
+
class="grid h-5 min-w-5 place-items-center rounded-sm bg-foreground/5 px-1 text-xs font-medium text-muted-foreground tabular-nums"
|
|
30
|
+
>
|
|
25
31
|
{row.count}
|
|
26
32
|
</span>
|
|
27
33
|
</div>
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
let { layout, days, months, labelHeading, locale, corner = $bindable() }: Props = $props();
|
|
15
15
|
|
|
16
|
-
let weekdayFormat = $derived(
|
|
16
|
+
let weekdayFormat = $derived(
|
|
17
|
+
new Intl.DateTimeFormat(locale, { weekday: "narrow", timeZone: "UTC" }),
|
|
18
|
+
);
|
|
17
19
|
</script>
|
|
18
20
|
|
|
19
21
|
<!-- Months over days, pinned to the top. -->
|
|
@@ -32,7 +34,9 @@
|
|
|
32
34
|
style:left="{month.i * layout.dayWidth}px"
|
|
33
35
|
style:width="{month.length * layout.dayWidth}px"
|
|
34
36
|
>
|
|
35
|
-
<span
|
|
37
|
+
<span
|
|
38
|
+
class="sticky left-(--g-label) inline-block px-2 pt-1.5 text-xs font-medium whitespace-nowrap text-foreground"
|
|
39
|
+
>
|
|
36
40
|
{month.label}
|
|
37
41
|
</span>
|
|
38
42
|
</div>
|
|
@@ -51,7 +55,9 @@
|
|
|
51
55
|
style:width="{layout.dayWidth}px"
|
|
52
56
|
>
|
|
53
57
|
{#if layout.dayWidth >= 26}
|
|
54
|
-
<span class="mb-0.5 opacity-70"
|
|
58
|
+
<span class="mb-0.5 opacity-70"
|
|
59
|
+
>{weekdayFormat.format(new Date(d.day * 86_400_000))}</span
|
|
60
|
+
>
|
|
55
61
|
{/if}
|
|
56
62
|
{#if showNumber}
|
|
57
63
|
<span
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
import { CaretRight, FlagPennant } from "phosphor-svelte";
|
|
4
4
|
import type { GanttChange, GanttLabelContext } from "../task-types.js";
|
|
5
5
|
import GanttBar from "./gantt-bar.svelte";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
barWidth,
|
|
8
|
+
dayX,
|
|
9
|
+
type Drag,
|
|
10
|
+
type Layout,
|
|
11
|
+
type Span,
|
|
12
|
+
type TaskRow,
|
|
13
|
+
} from "./logic.js";
|
|
7
14
|
import "./gantt.css";
|
|
8
15
|
|
|
9
16
|
interface Props<T> {
|
|
@@ -60,7 +67,12 @@
|
|
|
60
67
|
let hot = $derived(hovered || drag?.id === row.id);
|
|
61
68
|
</script>
|
|
62
69
|
|
|
63
|
-
<div
|
|
70
|
+
<div
|
|
71
|
+
role="listitem"
|
|
72
|
+
class="g-row relative flex border-b border-border/40 last:border-b-0"
|
|
73
|
+
class:g-row-hot={hot}
|
|
74
|
+
style:height="{layout.rowHeight}px"
|
|
75
|
+
>
|
|
64
76
|
<div
|
|
65
77
|
class="sticky left-0 z-10 flex w-(--g-label) shrink-0 items-center gap-2 border-r border-border bg-card px-3 text-sm"
|
|
66
78
|
style:padding-inline-start={nested ? `${row.depth * indent + 12}px` : undefined}
|
|
@@ -74,7 +86,11 @@
|
|
|
74
86
|
onclick={() => onToggle(row.id)}
|
|
75
87
|
class="-ml-1 grid size-5 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors duration-[var(--duration-quick,150ms)] hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
76
88
|
>
|
|
77
|
-
<span
|
|
89
|
+
<span
|
|
90
|
+
class="g-chevron size-3.5 [&>svg]:size-full"
|
|
91
|
+
class:g-chevron-open={row.open}
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
>
|
|
78
94
|
<CaretRight weight="bold" />
|
|
79
95
|
</span>
|
|
80
96
|
</button>
|
|
@@ -108,7 +124,9 @@
|
|
|
108
124
|
onpointerenter={() => onhover(row.id, true)}
|
|
109
125
|
onpointerleave={() => onhover(row.id, false)}
|
|
110
126
|
>
|
|
111
|
-
<span class="size-5 [&>svg]:size-full" aria-hidden="true"
|
|
127
|
+
<span class="size-5 [&>svg]:size-full" aria-hidden="true"
|
|
128
|
+
><FlagPennant weight="fill" /></span
|
|
129
|
+
>
|
|
112
130
|
</button>
|
|
113
131
|
{:else if !row.own}
|
|
114
132
|
<!-- A parent with no dates of its own: the span of what is below it. Not draggable. -->
|
|
@@ -124,7 +142,8 @@
|
|
|
124
142
|
style:width="{barWidth(span, layout)}px"
|
|
125
143
|
style:height="{Math.max(layout.rowHeight - 24, 10)}px"
|
|
126
144
|
onclick={() => onSelect?.(row.id)}
|
|
127
|
-
onkeydown={(e) =>
|
|
145
|
+
onkeydown={(e) =>
|
|
146
|
+
(e.key === "Enter" || e.key === " ") && (e.preventDefault(), onSelect?.(row.id))}
|
|
128
147
|
onpointerenter={() => onhover(row.id, true)}
|
|
129
148
|
onpointerleave={() => onhover(row.id, false)}
|
|
130
149
|
></div>
|
|
@@ -161,7 +180,8 @@
|
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
.g-chevron {
|
|
164
|
-
transition: rotate var(--duration-fast, 250ms)
|
|
183
|
+
transition: rotate var(--duration-fast, 250ms)
|
|
184
|
+
var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
.g-chevron-open {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Shared by the bar, the derived bar and the group summary: they all slide in the same way. */
|
|
2
2
|
.g-enter {
|
|
3
|
-
animation: g-bar-in var(--duration-slow, 400ms)
|
|
3
|
+
animation: g-bar-in var(--duration-slow, 400ms)
|
|
4
|
+
var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
|
|
4
5
|
animation-delay: calc(var(--g-i, 0) * var(--duration-stagger, 40ms));
|
|
5
6
|
}
|
|
6
7
|
|
|
@@ -50,7 +50,9 @@ describe("TaskGantt", () => {
|
|
|
50
50
|
const root = render({ onChange: (_id: string, change: GanttChange) => changes.push(change) });
|
|
51
51
|
const bar = root.querySelector("[data-bar]");
|
|
52
52
|
bar?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
|
|
53
|
-
bar?.dispatchEvent(
|
|
53
|
+
bar?.dispatchEvent(
|
|
54
|
+
new KeyboardEvent("keydown", { key: "ArrowRight", shiftKey: true, bubbles: true }),
|
|
55
|
+
);
|
|
54
56
|
await tick();
|
|
55
57
|
expect(changes).toEqual([
|
|
56
58
|
{ start: "2026-10-06", end: "2026-10-10" },
|
|
@@ -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");
|