@heyramzi/ui 2.8.0 → 2.9.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/COMPONENTS.md +1 -1
- package/README.md +1 -1
- package/package.json +58 -74
- package/src/agents/vendor/draw.ts +4 -4
- package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
- package/src/canvas/ops/ops.ts +1 -1
- package/src/charts/area-chart.svelte +11 -1
- package/src/charts/bar-chart.svelte +18 -3
- package/src/charts/line-chart.svelte +11 -1
- package/src/components/data-table/data-table.ts +4 -5
- package/src/components/data-table/pagination.svelte +5 -3
- package/src/components/docs/on-this-page.svelte +82 -5
- package/src/components/docs/scroll-spy.svelte.ts +6 -1
- package/src/components/list-page/detail-header.svelte +7 -3
- package/src/components/list-page/empty-state.svelte +3 -1
- package/src/components/list-page/filter-popover.svelte +2 -2
- package/src/components/list-page/rail-row.svelte +5 -2
- package/src/components/mark/mark.svelte +2 -1
- package/src/components/section-card/section-card.svelte +3 -1
- package/src/components/section-header/section-header.svelte +5 -5
- package/src/components/segmented/segment.svelte +1 -1
- package/src/components/table-view/table-view.svelte +6 -1
- package/src/components/tags-input/tags-input.svelte +1 -1
- package/src/components/toolbar/toolbar.svelte +1 -1
- package/src/editor/anchors.ts +28 -0
- package/src/editor/doc-blocks.ts +168 -0
- package/src/editor/doc-page.css +389 -0
- package/src/editor/editor.svelte +47 -5
- package/src/editor/extensions.ts +26 -2
- package/src/editor/markdown.ts +27 -2
- package/src/editor/slash-items.ts +18 -0
- package/src/icons/trades.ts +1 -1
- package/src/inbox/README.md +6 -4
- package/src/inbox/inbox.test.ts +1 -1
- package/src/inbox/index.ts +2 -2
- package/src/inbox/logic.ts +29 -5
- package/src/inbox/thread-inbox.svelte +219 -92
- package/src/inbox/types.ts +27 -1
- package/src/lib/initials.test.ts +25 -0
- package/src/lib/initials.ts +29 -0
- package/src/modules.test.ts +4 -64
- package/src/panel/README.md +1 -1
- package/src/record/README.md +1 -1
- package/src/record/info-list.svelte +5 -4
- package/src/record/next-up.svelte +1 -1
- package/src/record/project-list.svelte +1 -1
- package/src/record/steps-rail.svelte +3 -3
- package/src/shadcn/badge/badge.svelte +1 -1
- package/src/shadcn/card/card-title.svelte +1 -5
- package/src/shadcn/empty/empty-description.svelte +1 -1
- package/src/shadcn/progress/progress.svelte +8 -1
- package/src/shadcn/table/table-cell.svelte +10 -2
- package/src/status/README.md +1 -1
- package/src/thread/README.md +2 -0
- package/src/thread/index.ts +9 -1
- package/src/thread/logic.ts +33 -3
- package/src/thread/thread-body.svelte +37 -0
- package/src/thread/thread-events.svelte +30 -0
- package/src/thread/thread.svelte +74 -59
- package/src/thread/types.ts +21 -0
- package/src/{tasks → work}/README.md +11 -8
- package/src/{tasks → work}/detail/task-thread.svelte +8 -15
- package/src/work/docs/doc-editor-page.svelte +194 -0
- package/src/work/docs/doc-menu.svelte +333 -0
- package/src/work/docs/docs-demo.svelte +227 -0
- package/src/work/docs/edited-chip.svelte +125 -0
- package/src/work/docs/edited.ts +54 -0
- package/src/work/docs/index.ts +38 -0
- package/src/work/docs/labels.ts +70 -0
- package/src/work/docs/menu.ts +127 -0
- package/src/work/docs/outline.ts +60 -0
- package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
- package/src/{tasks → work}/inbox/inbox-card.svelte +113 -50
- package/src/work/inbox/inbox-logic.ts +98 -0
- package/src/work/inbox/inbox-trace.svelte +58 -0
- package/src/{tasks → work}/inbox/inbox-types.ts +2 -0
- package/src/{tasks → work}/index.ts +0 -10
- package/src/{tasks → work}/kanban-board.svelte +1 -1
- package/src/{tasks → work}/task-list.svelte +1 -1
- package/src/{tasks → work}/time/time-tracker.svelte +2 -5
- package/src/tasks/inbox/inbox-logic.ts +0 -21
- package/src/tasks/inbox/inbox-trace.svelte +0 -51
- /package/src/{tasks → work}/ambient.d.ts +0 -0
- /package/src/{tasks → work}/board-order.ts +0 -0
- /package/src/{tasks → work}/dates.ts +0 -0
- /package/src/{tasks → work}/demo.svelte +0 -0
- /package/src/{tasks → work}/detail/detail-demo.svelte +0 -0
- /package/src/{tasks → work}/detail/detail-types.ts +0 -0
- /package/src/{tasks → work}/detail/task-chip.svelte +0 -0
- /package/src/{tasks → work}/detail/task-detail.svelte +0 -0
- /package/src/{tasks → work}/detail/task-property.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-bar.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-grid.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-header.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-task-row.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt.css +0 -0
- /package/src/{tasks → work}/gantt/gantt.test.ts +0 -0
- /package/src/{tasks → work}/gantt/logic.test.ts +0 -0
- /package/src/{tasks → work}/gantt/logic.ts +0 -0
- /package/src/{tasks → work}/inbox/inbox-draft.svelte +0 -0
- /package/src/{tasks → work}/inbox/inbox-statuses.ts +0 -0
- /package/src/{tasks → work}/inbox/index.ts +0 -0
- /package/src/{tasks → work}/kanban-column.svelte +0 -0
- /package/src/{tasks → work}/kanban-drag.ts +0 -0
- /package/src/{tasks → work}/kanban-types.ts +0 -0
- /package/src/{tasks → work}/presets/sales.ts +0 -0
- /package/src/{tasks → work}/reorder.ts +0 -0
- /package/src/{tasks → work}/task-gantt.svelte +0 -0
- /package/src/{tasks → work}/task-types.ts +0 -0
- /package/src/{tasks → work}/time/duration.ts +0 -0
- /package/src/{tasks → work}/time/time-chip.svelte +0 -0
- /package/src/{tasks → work}/time/time-dot.svelte +0 -0
- /package/src/{tasks → work}/time/time-ring.svelte +0 -0
- /package/src/{tasks → work}/time/time-types.ts +0 -0
- /package/src/{tasks → work}/use-kanban-dnd.svelte.ts +0 -0
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
{#snippet display(row: InfoRow, clamp = false)}
|
|
167
167
|
{#if row.chips?.length}
|
|
168
168
|
<span class="flex flex-wrap items-center gap-1">
|
|
169
|
-
{#each row.chips as chip (
|
|
169
|
+
{#each row.chips as chip, i (i)}
|
|
170
170
|
<StatusBadge tone={chip.tone ?? "neutral"} capitalize={false}>{chip.label}</StatusBadge>
|
|
171
171
|
{/each}
|
|
172
172
|
</span>
|
|
@@ -175,6 +175,7 @@
|
|
|
175
175
|
{:else if row.href && row.value}
|
|
176
176
|
<a
|
|
177
177
|
href={row.href}
|
|
178
|
+
title={row.value}
|
|
178
179
|
class={cn(
|
|
179
180
|
"min-w-0 truncate text-sm font-medium underline-offset-2 hover:underline",
|
|
180
181
|
clamp &&
|
|
@@ -262,7 +263,7 @@
|
|
|
262
263
|
<dt
|
|
263
264
|
class={cn(
|
|
264
265
|
"flex items-center gap-1.5 text-xs text-muted-foreground",
|
|
265
|
-
labelPosition === "beside"
|
|
266
|
+
labelPosition === "beside" ? "shrink-0" : "[overflow-wrap:anywhere]",
|
|
266
267
|
)}
|
|
267
268
|
>
|
|
268
269
|
{#if row.term}
|
|
@@ -301,7 +302,7 @@
|
|
|
301
302
|
<h2 class="text-xs font-medium tracking-wide text-muted-foreground uppercase">{title}</h2>
|
|
302
303
|
{/if}
|
|
303
304
|
<dl
|
|
304
|
-
class="grid grid-cols-[minmax(5rem,max-content)_minmax(0,1fr)] items-start gap-x-4 gap-y-2.5"
|
|
305
|
+
class="grid grid-cols-[minmax(5rem,min(max-content,45%))_minmax(0,1fr)] items-start gap-x-4 gap-y-2.5"
|
|
305
306
|
>
|
|
306
307
|
{#each shown as row, index (row.divider ? `divider-${index}` : keyOf(row))}
|
|
307
308
|
{#if row.divider}
|
|
@@ -311,7 +312,7 @@
|
|
|
311
312
|
{@const long = row.input === "longtext"}
|
|
312
313
|
<dt
|
|
313
314
|
class={cn(
|
|
314
|
-
"flex min-h-7 items-center gap-2 text-sm text-muted-foreground",
|
|
315
|
+
"flex min-h-7 min-w-0 items-center gap-2 text-sm [overflow-wrap:anywhere] text-muted-foreground",
|
|
315
316
|
long && "col-span-2 min-h-0",
|
|
316
317
|
)}
|
|
317
318
|
>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import StatusBadge from "../status/status-badge.svelte";
|
|
9
9
|
import { gutterClass } from "./gutter.js";
|
|
10
10
|
import { formatRange } from "./format.js";
|
|
11
|
-
import { todayIso } from "../
|
|
11
|
+
import { todayIso } from "../work/dates.js";
|
|
12
12
|
import { resolveLabels, type RecordLabels, type RecordNext } from "./types.js";
|
|
13
13
|
|
|
14
14
|
// =========================
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Progress } from "../shadcn/progress/index.js";
|
|
7
7
|
import { TONE_TEXT } from "../lib/tone.js";
|
|
8
8
|
import StatusBadge from "../status/status-badge.svelte";
|
|
9
|
-
import { todayIso } from "../
|
|
9
|
+
import { todayIso } from "../work/dates.js";
|
|
10
10
|
import { formatMoney, formatRange } from "./format.js";
|
|
11
11
|
import {
|
|
12
12
|
resolveLabels,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { Skeleton } from "../shadcn/skeleton/index.js";
|
|
19
19
|
import * as Popover from "../shadcn/popover/index.js";
|
|
20
20
|
import * as ContextMenu from "../shadcn/context-menu/index.js";
|
|
21
|
-
import { todayIso } from "../
|
|
21
|
+
import { todayIso } from "../work/dates.js";
|
|
22
22
|
import { formatRange } from "./format.js";
|
|
23
23
|
import {
|
|
24
24
|
groupSteps,
|
|
@@ -280,8 +280,8 @@
|
|
|
280
280
|
<DotsThreeVertical class="size-4" weight="bold" />
|
|
281
281
|
{#if hiddenCount > 0}
|
|
282
282
|
<span
|
|
283
|
-
class="absolute -top-0.5 -right-0.5 flex
|
|
284
|
-
>{hiddenCount}</span
|
|
283
|
+
class="absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-muted px-1 text-xs font-medium tabular-nums"
|
|
284
|
+
>{hiddenCount.toLocaleString()}</span
|
|
285
285
|
>
|
|
286
286
|
{/if}
|
|
287
287
|
</Button>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { type VariantProps, tv } from "tailwind-variants";
|
|
3
3
|
|
|
4
4
|
export const badgeVariants = tv({
|
|
5
|
-
base: "h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-xs font-medium has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-[background-color,border-color,color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
|
5
|
+
base: "h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-xs font-medium has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit max-w-full shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-[background-color,border-color,color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
|
6
6
|
variants: {
|
|
7
7
|
// Three weights a label can carry: filled, tinted, outlined. Severity is the
|
|
8
8
|
// `tone` prop, which reads `TONE_CLASS`, not a variant.
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
3
|
import { cn, type WithElementRef } from "../../lib/utils.js";
|
|
4
4
|
|
|
5
|
-
// WHY: card titles
|
|
6
|
-
|
|
7
|
-
// but a page whose sections are all cards ends up with no headings below the h1, so screen readers cannot navigate it by heading (WCAG 2.4.6).
|
|
8
|
-
|
|
9
|
-
// Pass `as="h2"` on real section titles.
|
|
5
|
+
// WHY: card titles default to a div, so pass `as="h2"` on real section titles or a page of cards has no headings for screen readers (WCAG 2.4.6).
|
|
10
6
|
let {
|
|
11
7
|
ref = $bindable(null),
|
|
12
8
|
class: className,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
bind:this={ref}
|
|
15
15
|
data-slot="empty-description"
|
|
16
16
|
class={cn(
|
|
17
|
-
"text-sm/relaxed text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
|
17
|
+
"text-sm/relaxed text-muted-foreground [overflow-wrap:anywhere] [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
|
18
18
|
className,
|
|
19
19
|
)}
|
|
20
20
|
{...restProps}
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
/** The fill's colour on the severity scale. Left out, the primary colour. */
|
|
16
16
|
tone?: Tone;
|
|
17
17
|
} = $props();
|
|
18
|
+
|
|
19
|
+
// WHY clamped: a value past max drew a positive translateX and the fill left its track.
|
|
20
|
+
let pct = $derived.by(() => {
|
|
21
|
+
const top = max && max > 0 ? max : 1;
|
|
22
|
+
const raw = (100 * (value ?? 0)) / top;
|
|
23
|
+
return Number.isFinite(raw) ? Math.min(100, Math.max(0, raw)) : 0;
|
|
24
|
+
});
|
|
18
25
|
</script>
|
|
19
26
|
|
|
20
27
|
<ProgressPrimitive.Root
|
|
@@ -31,6 +38,6 @@
|
|
|
31
38
|
<div
|
|
32
39
|
data-slot="progress-indicator"
|
|
33
40
|
class="{progressToneClass(tone)} size-full flex-1 transition-transform duration-[var(--duration-slow)] ease-[var(--ease-smooth-out)] motion-reduce:transition-none"
|
|
34
|
-
style="transform: translateX(-{100 -
|
|
41
|
+
style="transform: translateX(-{100 - pct}%)"
|
|
35
42
|
></div>
|
|
36
43
|
</ProgressPrimitive.Root>
|
|
@@ -5,15 +5,23 @@
|
|
|
5
5
|
let {
|
|
6
6
|
ref = $bindable(null),
|
|
7
7
|
class: className,
|
|
8
|
+
wrap = false,
|
|
8
9
|
children,
|
|
9
10
|
...restProps
|
|
10
|
-
}: WithElementRef<HTMLTdAttributes>
|
|
11
|
+
}: WithElementRef<HTMLTdAttributes> & {
|
|
12
|
+
/** Lets a long user string wrap, so a trailing action cell keeps its place. Off by default: amounts and dates stay on one line. */
|
|
13
|
+
wrap?: boolean;
|
|
14
|
+
} = $props();
|
|
11
15
|
</script>
|
|
12
16
|
|
|
13
17
|
<td
|
|
14
18
|
bind:this={ref}
|
|
15
19
|
data-slot="table-cell"
|
|
16
|
-
class={cn(
|
|
20
|
+
class={cn(
|
|
21
|
+
"p-2 align-middle tabular-nums [&:has([role=checkbox])]:pr-0",
|
|
22
|
+
wrap ? "whitespace-normal [overflow-wrap:anywhere]" : "whitespace-nowrap",
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
17
25
|
{...restProps}
|
|
18
26
|
>
|
|
19
27
|
{@render children?.()}
|
package/src/status/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @heyramzi/ui/status
|
|
2
2
|
|
|
3
|
-
Status badge, dot and icon from one map. `defineStatuses` builds the map, and the three parts read it, so a status looks the same everywhere. They used to live in `@heyramzi/ui/
|
|
3
|
+
Status badge, dot and icon from one map. `defineStatuses` builds the map, and the three parts read it, so a status looks the same everywhere. They used to live in `@heyramzi/ui/work`, which still re-exports every name.
|
|
4
4
|
|
|
5
5
|
```svelte
|
|
6
6
|
<script lang="ts">
|
package/src/thread/README.md
CHANGED
|
@@ -24,6 +24,8 @@ One comment thread for any subject: a task, a project, a doc or a diagram. The t
|
|
|
24
24
|
- **Resolve.** Pass `onResolve` and each root gets a check. Resolved roots fold away behind a "Show N resolved" button.
|
|
25
25
|
- **Anchor.** `anchor` carries `{ nodeId, dx, dy, x, y }` for a canvas pin. A pinned comment shows a chip, and `onAnchor` fires on a click so the canvas can move to the node. `anchorChip` replaces the chip.
|
|
26
26
|
- **Attachments.** The `attachments` snippet draws under a comment's body.
|
|
27
|
+
- **Events.** `events` takes `ThreadEvent`s (a whole sentence, a time, an optional Phosphor icon) and draws each as one `Trace` line between the conversations, in time order. `whenEvent(event)` formats its time. `interleave(nodes, events)` gives the same order without the component.
|
|
28
|
+
- **Long bodies.** A body over 6 lines folds behind `showMore` and `showLess`.
|
|
27
29
|
- **Labels.** Every string comes from `labels`, a partial of `ThreadLabels`. `showResolved` holds a `{count}` slot. `when(comment)` formats the time.
|
|
28
30
|
|
|
29
31
|
```ts
|
package/src/thread/index.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
export { default as Thread } from "./thread.svelte";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
buildThread,
|
|
4
|
+
fillCount,
|
|
5
|
+
interleave,
|
|
6
|
+
resolveThreadLabels,
|
|
7
|
+
resolvedCount,
|
|
8
|
+
} from "./logic.js";
|
|
3
9
|
export { DEFAULT_THREAD_LABELS } from "./types.js";
|
|
4
10
|
export type {
|
|
5
11
|
ThreadAnchor,
|
|
6
12
|
ThreadAuthor,
|
|
7
13
|
ThreadAuthorKind,
|
|
8
14
|
ThreadComment,
|
|
15
|
+
ThreadEvent,
|
|
16
|
+
ThreadItem,
|
|
9
17
|
ThreadLabels,
|
|
10
18
|
ThreadNode,
|
|
11
19
|
ThreadPost,
|
package/src/thread/logic.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ThreadComment,
|
|
3
|
+
ThreadEvent,
|
|
4
|
+
ThreadItem,
|
|
5
|
+
ThreadLabels,
|
|
6
|
+
ThreadNode,
|
|
7
|
+
} from "./types.js";
|
|
2
8
|
import { DEFAULT_THREAD_LABELS } from "./types.js";
|
|
3
9
|
|
|
4
|
-
|
|
5
|
-
|
|
10
|
+
/** A timestamp as a number, a malformed one last. */
|
|
11
|
+
function at(iso: string): number {
|
|
12
|
+
const ms = Date.parse(iso);
|
|
6
13
|
return Number.isNaN(ms) ? Number.POSITIVE_INFINITY : ms;
|
|
7
14
|
}
|
|
8
15
|
|
|
16
|
+
function time(comment: ThreadComment): number {
|
|
17
|
+
return at(comment.createdAt);
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
/** Oldest first, a malformed timestamp last, ties in the order they came. */
|
|
10
21
|
function byTime(comments: ThreadComment[]): ThreadComment[] {
|
|
11
22
|
return comments
|
|
@@ -59,3 +70,22 @@ export function resolveThreadLabels(overrides?: Partial<ThreadLabels>): ThreadLa
|
|
|
59
70
|
export function fillCount(label: string, count: number): string {
|
|
60
71
|
return label.replaceAll("{count}", String(count));
|
|
61
72
|
}
|
|
73
|
+
|
|
74
|
+
/** Conversations and events in time order, each run of events between two conversations as one item. */
|
|
75
|
+
export function interleave(nodes: ThreadNode[], events: ThreadEvent[]): ThreadItem[] {
|
|
76
|
+
const timed = [
|
|
77
|
+
...nodes.map((node, index) => ({ at: at(node.comment.createdAt), index, node })),
|
|
78
|
+
...events.map((event, index) => ({ at: at(event.at), index: nodes.length + index, event })),
|
|
79
|
+
].toSorted((a, b) => a.at - b.at || a.index - b.index);
|
|
80
|
+
const items: ThreadItem[] = [];
|
|
81
|
+
for (const row of timed) {
|
|
82
|
+
if ("node" in row) {
|
|
83
|
+
items.push({ type: "node", key: row.node.comment.id, node: row.node });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const last = items.at(-1);
|
|
87
|
+
if (last?.type === "events") last.events.push(row.event);
|
|
88
|
+
else items.push({ type: "events", key: row.event.id, events: [row.event] });
|
|
89
|
+
}
|
|
90
|
+
return items;
|
|
91
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
text: string;
|
|
6
|
+
more: string;
|
|
7
|
+
less: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { text, more, less, class: className }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let open = $state(false);
|
|
14
|
+
let long = $derived(text.length > 420 || text.split("\n").length > 6);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="flex flex-col items-start gap-1">
|
|
18
|
+
<p
|
|
19
|
+
class={cn(
|
|
20
|
+
"text-sm leading-relaxed font-normal whitespace-pre-wrap text-pretty",
|
|
21
|
+
long && !open && "line-clamp-6",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
{text}
|
|
26
|
+
</p>
|
|
27
|
+
{#if long}
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
|
|
31
|
+
aria-expanded={open}
|
|
32
|
+
onclick={() => (open = !open)}
|
|
33
|
+
>
|
|
34
|
+
{open ? less : more}
|
|
35
|
+
</button>
|
|
36
|
+
{/if}
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as TraceUI from "../shadcn/trace/index.js";
|
|
3
|
+
import type { ThreadEvent } from "./types.js";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
events: ThreadEvent[];
|
|
7
|
+
when: (event: ThreadEvent) => string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { events, when }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<!-- ps-1 centres each 16px icon in the 24px avatar column the comments use. -->
|
|
14
|
+
<TraceUI.Trace class="gap-1 ps-1">
|
|
15
|
+
{#each events as event (event.id)}
|
|
16
|
+
{@const Icon = event.icon}
|
|
17
|
+
<TraceUI.Line class="w-full items-start text-xs">
|
|
18
|
+
{#if Icon}
|
|
19
|
+
<TraceUI.Icon class="mt-px"><Icon size={14} /></TraceUI.Icon>
|
|
20
|
+
{/if}
|
|
21
|
+
<!-- Wraps, not truncates: a status name can be a sentence, and a touch screen has no hover title. -->
|
|
22
|
+
<TraceUI.Title class="flex-1 whitespace-normal text-pretty [overflow-wrap:anywhere]"
|
|
23
|
+
>{event.title}</TraceUI.Title
|
|
24
|
+
>
|
|
25
|
+
<TraceUI.Description>
|
|
26
|
+
<time datetime={event.at}>{when(event)}</time>
|
|
27
|
+
</TraceUI.Description>
|
|
28
|
+
</TraceUI.Line>
|
|
29
|
+
{/each}
|
|
30
|
+
</TraceUI.Trace>
|
package/src/thread/thread.svelte
CHANGED
|
@@ -2,15 +2,25 @@
|
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { ArrowBendDownRight, Check, MapPin, PencilSimple, Trash } from "phosphor-svelte";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { initial } from "../lib/initials.js";
|
|
5
6
|
import { AgentAvatar } from "../agents/index.js";
|
|
6
|
-
import { TONE_CLASS } from "../lib/tone.js";
|
|
7
7
|
import RichRenderer from "../editor/renderer.svelte";
|
|
8
8
|
import { Skeleton } from "../shadcn/skeleton/index.js";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import ThreadBody from "./thread-body.svelte";
|
|
10
|
+
import ThreadEvents from "./thread-events.svelte";
|
|
11
|
+
import {
|
|
12
|
+
buildThread,
|
|
13
|
+
fillCount,
|
|
14
|
+
interleave,
|
|
15
|
+
resolveThreadLabels,
|
|
16
|
+
resolvedCount,
|
|
17
|
+
} from "./logic.js";
|
|
18
|
+
import type { ThreadComment, ThreadEvent, ThreadLabels, ThreadPost } from "./types.js";
|
|
11
19
|
|
|
12
20
|
interface Props {
|
|
13
21
|
comments: ThreadComment[];
|
|
22
|
+
/** Status moves, assignments and the like, one quiet line each between the comments. */
|
|
23
|
+
events?: ThreadEvent[];
|
|
14
24
|
/** Rows still on their way. Draws skeleton rows in place of the thread. */
|
|
15
25
|
loading?: boolean;
|
|
16
26
|
labels?: Partial<ThreadLabels>;
|
|
@@ -22,6 +32,10 @@
|
|
|
22
32
|
anchorChip?: Snippet<[ThreadComment]>;
|
|
23
33
|
/** How a timestamp reads. Defaults to a short date. */
|
|
24
34
|
when?: (comment: ThreadComment) => string;
|
|
35
|
+
/** How an event's time reads. Defaults to a short date. */
|
|
36
|
+
whenEvent?: (event: ThreadEvent) => string;
|
|
37
|
+
/** The locale the default times read in. */
|
|
38
|
+
locale?: string;
|
|
25
39
|
/** Returns false to keep the draft. Without it no composer shows. */
|
|
26
40
|
onPost?: (post: ThreadPost) => void | boolean | Promise<void | boolean>;
|
|
27
41
|
onEdit?: (comment: ThreadComment, body: string) => void | Promise<void>;
|
|
@@ -37,13 +51,15 @@
|
|
|
37
51
|
|
|
38
52
|
let {
|
|
39
53
|
comments,
|
|
54
|
+
events = [],
|
|
55
|
+
locale = "en-GB",
|
|
40
56
|
loading = false,
|
|
41
57
|
labels: labelOverrides,
|
|
42
58
|
avatar,
|
|
43
59
|
attachments,
|
|
44
60
|
anchorChip,
|
|
45
|
-
when = (comment) =>
|
|
46
|
-
|
|
61
|
+
when = (comment) => shortDate(comment.createdAt),
|
|
62
|
+
whenEvent = (event) => shortDate(event.at),
|
|
47
63
|
onPost,
|
|
48
64
|
onEdit,
|
|
49
65
|
onDelete,
|
|
@@ -58,6 +74,11 @@
|
|
|
58
74
|
let resolved = $derived(resolvedCount(nodes));
|
|
59
75
|
let showResolved = $state(false);
|
|
60
76
|
let shown = $derived(showResolved ? nodes : nodes.filter((node) => !node.comment.resolved));
|
|
77
|
+
let items = $derived(interleave(shown, events));
|
|
78
|
+
|
|
79
|
+
function shortDate(at: string): string {
|
|
80
|
+
return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(new Date(at));
|
|
81
|
+
}
|
|
61
82
|
|
|
62
83
|
let draft = $state("");
|
|
63
84
|
let replyTo = $state<string | null>(null);
|
|
@@ -101,10 +122,6 @@
|
|
|
101
122
|
if (!body || body === comment.body) return;
|
|
102
123
|
await onEdit?.(comment, body);
|
|
103
124
|
}
|
|
104
|
-
|
|
105
|
-
function initial(name: string): string {
|
|
106
|
-
return name.trim().charAt(0).toLocaleUpperCase() || "?";
|
|
107
|
-
}
|
|
108
125
|
</script>
|
|
109
126
|
|
|
110
127
|
{#snippet composer(
|
|
@@ -164,14 +181,7 @@
|
|
|
164
181
|
<div class="flex min-h-6 flex-wrap items-center gap-x-2">
|
|
165
182
|
<span class="truncate text-[13px] font-medium">{comment.author.name}</span>
|
|
166
183
|
{#if comment.author.kind === "agent"}
|
|
167
|
-
<span
|
|
168
|
-
class={cn(
|
|
169
|
-
"inline-flex h-5 items-center rounded-sm border px-1.5 text-xs font-medium",
|
|
170
|
-
TONE_CLASS.accent,
|
|
171
|
-
)}
|
|
172
|
-
>
|
|
173
|
-
{labels.agent}
|
|
174
|
-
</span>
|
|
184
|
+
<span class="sr-only">{labels.agent}</span>
|
|
175
185
|
{/if}
|
|
176
186
|
<span class="text-xs text-muted-foreground tabular-nums">{when(comment)}</span>
|
|
177
187
|
{#if comment.edited}
|
|
@@ -254,7 +264,7 @@
|
|
|
254
264
|
{:else if comment.content}
|
|
255
265
|
<div class="text-sm leading-relaxed"><RichRenderer content={comment.content} /></div>
|
|
256
266
|
{:else}
|
|
257
|
-
<
|
|
267
|
+
<ThreadBody text={comment.body} more={labels.showMore} less={labels.showLess} />
|
|
258
268
|
{/if}
|
|
259
269
|
|
|
260
270
|
{@render attachments?.(comment)}
|
|
@@ -289,49 +299,54 @@
|
|
|
289
299
|
</li>
|
|
290
300
|
{/each}
|
|
291
301
|
</ol>
|
|
292
|
-
{:else if
|
|
302
|
+
{:else if items.length > 0}
|
|
293
303
|
<ol class="flex flex-col gap-4">
|
|
294
|
-
{#each
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<li>{@render row(reply, false)}</li>
|
|
307
|
-
{/each}
|
|
308
|
-
</ol>
|
|
309
|
-
{/if}
|
|
310
|
-
{#if onPost && !node.comment.resolved}
|
|
311
|
-
{#if replyTo === node.comment.id}
|
|
312
|
-
<div class="ms-9">
|
|
313
|
-
{@render composer(
|
|
314
|
-
replyDraft,
|
|
315
|
-
labels.replyPlaceholder,
|
|
316
|
-
sendReply,
|
|
317
|
-
(next) => (replyDraft = next),
|
|
318
|
-
true,
|
|
319
|
-
)}
|
|
320
|
-
</div>
|
|
321
|
-
{:else}
|
|
322
|
-
<button
|
|
323
|
-
type="button"
|
|
324
|
-
class="thread-button ms-9 flex w-fit items-center gap-1.5"
|
|
325
|
-
onclick={() => {
|
|
326
|
-
replyTo = node.comment.id;
|
|
327
|
-
replyDraft = "";
|
|
328
|
-
}}
|
|
329
|
-
>
|
|
330
|
-
<ArrowBendDownRight class="size-3.5" aria-hidden="true" />{labels.reply}
|
|
331
|
-
</button>
|
|
304
|
+
{#each items as item (item.key)}
|
|
305
|
+
{#if item.type === "events"}
|
|
306
|
+
<li><ThreadEvents events={item.events} when={whenEvent} /></li>
|
|
307
|
+
{:else}
|
|
308
|
+
{@const node = item.node}
|
|
309
|
+
<li
|
|
310
|
+
class={cn("flex flex-col gap-3", node.comment.resolved && "opacity-60")}
|
|
311
|
+
data-resolved={node.comment.resolved || undefined}
|
|
312
|
+
>
|
|
313
|
+
{@render row(node.comment, true)}
|
|
314
|
+
{#if node.comment.resolved}
|
|
315
|
+
<span class="ms-9 text-xs text-muted-foreground">{labels.resolved}</span>
|
|
332
316
|
{/if}
|
|
333
|
-
|
|
334
|
-
|
|
317
|
+
{#if node.replies.length > 0}
|
|
318
|
+
<ol class="ms-3 flex flex-col gap-3 border-s border-border ps-6">
|
|
319
|
+
{#each node.replies as reply (reply.id)}
|
|
320
|
+
<li>{@render row(reply, false)}</li>
|
|
321
|
+
{/each}
|
|
322
|
+
</ol>
|
|
323
|
+
{/if}
|
|
324
|
+
{#if onPost && !node.comment.resolved}
|
|
325
|
+
{#if replyTo === node.comment.id}
|
|
326
|
+
<div class="ms-9">
|
|
327
|
+
{@render composer(
|
|
328
|
+
replyDraft,
|
|
329
|
+
labels.replyPlaceholder,
|
|
330
|
+
sendReply,
|
|
331
|
+
(next) => (replyDraft = next),
|
|
332
|
+
true,
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
{:else}
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
class="thread-button ms-9 flex w-fit items-center gap-1.5"
|
|
339
|
+
onclick={() => {
|
|
340
|
+
replyTo = node.comment.id;
|
|
341
|
+
replyDraft = "";
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
<ArrowBendDownRight class="size-3.5" aria-hidden="true" />{labels.reply}
|
|
345
|
+
</button>
|
|
346
|
+
{/if}
|
|
347
|
+
{/if}
|
|
348
|
+
</li>
|
|
349
|
+
{/if}
|
|
335
350
|
{/each}
|
|
336
351
|
</ol>
|
|
337
352
|
{:else if !onPost}
|
package/src/thread/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PhosphorIcon } from "../lib/icon-types.js";
|
|
2
|
+
|
|
1
3
|
export type ThreadAuthorKind = "user" | "agent";
|
|
2
4
|
|
|
3
5
|
export type ThreadAuthor = {
|
|
@@ -47,6 +49,21 @@ export type ThreadNode = {
|
|
|
47
49
|
replies: ThreadComment[];
|
|
48
50
|
};
|
|
49
51
|
|
|
52
|
+
/** Something that happened to the subject, drawn as one quiet line between comments. */
|
|
53
|
+
export type ThreadEvent = {
|
|
54
|
+
id: string;
|
|
55
|
+
/** The whole sentence, actor first: "Project Manager moved this from Open to In progress". */
|
|
56
|
+
title: string;
|
|
57
|
+
/** ISO timestamp. */
|
|
58
|
+
at: string;
|
|
59
|
+
icon?: PhosphorIcon;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The thread in time order: a conversation, or a run of events between two of them. */
|
|
63
|
+
export type ThreadItem =
|
|
64
|
+
| { type: "node"; key: string; node: ThreadNode }
|
|
65
|
+
| { type: "events"; key: string; events: ThreadEvent[] };
|
|
66
|
+
|
|
50
67
|
export type ThreadLabels = {
|
|
51
68
|
title: string;
|
|
52
69
|
empty: string;
|
|
@@ -68,6 +85,8 @@ export type ThreadLabels = {
|
|
|
68
85
|
hideResolved: string;
|
|
69
86
|
agent: string;
|
|
70
87
|
pinned: string;
|
|
88
|
+
showMore: string;
|
|
89
|
+
showLess: string;
|
|
71
90
|
};
|
|
72
91
|
|
|
73
92
|
export const DEFAULT_THREAD_LABELS: ThreadLabels = {
|
|
@@ -90,4 +109,6 @@ export const DEFAULT_THREAD_LABELS: ThreadLabels = {
|
|
|
90
109
|
hideResolved: "Hide resolved",
|
|
91
110
|
agent: "AI",
|
|
92
111
|
pinned: "Pinned",
|
|
112
|
+
showMore: "Show more",
|
|
113
|
+
showLess: "Show less",
|
|
93
114
|
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
# @heyramzi/ui/
|
|
1
|
+
# @heyramzi/ui/work
|
|
2
2
|
|
|
3
|
-
Svelte 5
|
|
4
|
-
|
|
5
|
-
Status badges, dots and icons come
|
|
6
|
-
[ui.heyramzi.com/
|
|
3
|
+
Svelte 5 work views: tasks and the docs that sit beside them. One array of
|
|
4
|
+
tasks shows as a drag-and-drop board, a grouped list or a gantt timeline, and
|
|
5
|
+
the same column config drives all three. Status badges, dots and icons come
|
|
6
|
+
from one map too. Live at [ui.heyramzi.com/work](https://ui.heyramzi.com/work).
|
|
7
|
+
|
|
8
|
+
Status badges and the side panel live in `@heyramzi/ui/status` and
|
|
9
|
+
`@heyramzi/ui/panel`.
|
|
7
10
|
|
|
8
11
|
Every view knows nothing about your data. You pass items and a few accessors,
|
|
9
12
|
and it hands back a change to persist. It is a subpath of `@heyramzi/ui` and builds on its primitives and on
|
|
@@ -24,7 +27,7 @@ isn't a drop target (an Overdue lane, say).
|
|
|
24
27
|
|
|
25
28
|
```svelte
|
|
26
29
|
<script lang="ts">
|
|
27
|
-
import { KanbanBoard, type KanbanMove } from "@heyramzi/ui/
|
|
30
|
+
import { KanbanBoard, type KanbanMove } from "@heyramzi/ui/work";
|
|
28
31
|
|
|
29
32
|
type Task = { id: string; column: string; title: string; order: string };
|
|
30
33
|
|
|
@@ -204,7 +207,7 @@ icon, so `ChartBarHorizontal` stands in.
|
|
|
204
207
|
`SidePanel` is a sheet for one record: a title, a description, status chips, doors
|
|
205
208
|
out to other tools (`openIn`), a copy-a-prompt `handoff`, a strip of `stats`, then
|
|
206
209
|
the body and an optional pinned `footer`. You pass the tools; the panel knows none.
|
|
207
|
-
Import it from `@heyramzi/ui/panel`, or from `@heyramzi/ui/
|
|
210
|
+
Import it from `@heyramzi/ui/panel`, or from `@heyramzi/ui/work` for now.
|
|
208
211
|
|
|
209
212
|
```svelte
|
|
210
213
|
<SidePanel
|
|
@@ -263,7 +266,7 @@ and fetches nothing. You pass entries in and persist what it hands back.
|
|
|
263
266
|
| `locale`, `labels`, `class` | | `labels` overrides any string in `DEFAULT_TIME_LABELS`. |
|
|
264
267
|
|
|
265
268
|
Alt+S starts and stops the timer. A screen reader hears the start, one update a
|
|
266
|
-
minute and the stop. Import it alone from `@heyramzi/ui/
|
|
269
|
+
minute and the stop. Import it alone from `@heyramzi/ui/work/time`.
|
|
267
270
|
|
|
268
271
|
For boards and lists: `TimeChip` (`minutes`, `estimate?`, `running?`) shows the
|
|
269
272
|
logged total, with a pulsing dot while a timer runs and a small ring against an
|