@heyramzi/ui 2.16.0 → 2.17.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.
@@ -57,17 +57,28 @@ export function resolveThreadLabels(overrides) {
57
57
  export function fillCount(label, count) {
58
58
  return label.replaceAll("{count}", String(count));
59
59
  }
60
- /** Conversations and events in time order, each run of events between two conversations as one item. */
61
- export function interleave(nodes, events) {
62
- const timed = [...nodes.map((node, index) => ({
63
- at: at(node.comment.createdAt),
64
- index,
65
- node
66
- })), ...events.map((event, index) => ({
67
- at: at(event.at),
68
- index: nodes.length + index,
69
- event
70
- }))].toSorted((a, b) => a.at - b.at || a.index - b.index);
60
+ /**
61
+ * Conversations, events and the host's entries in time order, each run of events between two
62
+ * other items as one item.
63
+ */
64
+ export function interleave(nodes, events, entries = []) {
65
+ const timed = [
66
+ ...nodes.map((node, index) => ({
67
+ at: at(node.comment.createdAt),
68
+ index,
69
+ node
70
+ })),
71
+ ...events.map((event, index) => ({
72
+ at: at(event.at),
73
+ index: nodes.length + index,
74
+ event
75
+ })),
76
+ ...entries.map((entry, index) => ({
77
+ at: at(entry.at),
78
+ index: nodes.length + events.length + index,
79
+ entry
80
+ }))
81
+ ].toSorted((a, b) => a.at - b.at || a.index - b.index);
71
82
  const items = [];
72
83
  for (const row of timed) {
73
84
  if ("node" in row) {
@@ -78,6 +89,14 @@ export function interleave(nodes, events) {
78
89
  });
79
90
  continue;
80
91
  }
92
+ if ("entry" in row) {
93
+ items.push({
94
+ type: "entry",
95
+ key: `entry-${row.entry.id}`,
96
+ entry: row.entry
97
+ });
98
+ continue;
99
+ }
81
100
  const last = items.at(-1);
82
101
  if (last?.type === "events") last.events.push(row.event);
83
102
  else items.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Svelte 5 UI for apps: primitives, effects and animations, plus work (tasks and docs), charts, slides, an editor, onboarding and a canvas as subpaths.",
5
5
  "homepage": "https://ui.heyramzi.com",
6
6
  "license": "MIT",
@@ -24,7 +24,9 @@ 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.
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, entries)` gives the same order without the component.
28
+ - **Entries.** `entries` takes `{ id, at }` rows that the `entry` snippet draws, such as an agent's run or a message to it, so a comment thread and an agent conversation read as one back-and-forth. `footer` draws above the composer, for a question waiting on an answer. `onPost` decides where a root post goes.
29
+ - **Heading.** `heading={false}` keeps the title for screen readers only, for a thread its tab already names.
28
30
  - **Long bodies.** A body over 6 lines folds behind `showMore` and `showLess`.
29
31
  - **Labels.** Every string comes from `labels`, a partial of `ThreadLabels`. `showResolved` holds a `{count}` slot. `when(comment)` formats the time.
30
32
 
@@ -12,6 +12,7 @@ export type {
12
12
  ThreadAuthor,
13
13
  ThreadAuthorKind,
14
14
  ThreadComment,
15
+ ThreadEntry,
15
16
  ThreadEvent,
16
17
  ThreadItem,
17
18
  ThreadLabels,
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ThreadComment,
3
+ ThreadEntry,
3
4
  ThreadEvent,
4
5
  ThreadItem,
5
6
  ThreadLabels,
@@ -71,11 +72,23 @@ export function fillCount(label: string, count: number): string {
71
72
  return label.replaceAll("{count}", String(count));
72
73
  }
73
74
 
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[] {
75
+ /**
76
+ * Conversations, events and the host's entries in time order, each run of events between two
77
+ * other items as one item.
78
+ */
79
+ export function interleave(
80
+ nodes: ThreadNode[],
81
+ events: ThreadEvent[],
82
+ entries: ThreadEntry[] = [],
83
+ ): ThreadItem[] {
76
84
  const timed = [
77
85
  ...nodes.map((node, index) => ({ at: at(node.comment.createdAt), index, node })),
78
86
  ...events.map((event, index) => ({ at: at(event.at), index: nodes.length + index, event })),
87
+ ...entries.map((entry, index) => ({
88
+ at: at(entry.at),
89
+ index: nodes.length + events.length + index,
90
+ entry,
91
+ })),
79
92
  ].toSorted((a, b) => a.at - b.at || a.index - b.index);
80
93
  const items: ThreadItem[] = [];
81
94
  for (const row of timed) {
@@ -83,6 +96,10 @@ export function interleave(nodes: ThreadNode[], events: ThreadEvent[]): ThreadIt
83
96
  items.push({ type: "node", key: row.node.comment.id, node: row.node });
84
97
  continue;
85
98
  }
99
+ if ("entry" in row) {
100
+ items.push({ type: "entry", key: `entry-${row.entry.id}`, entry: row.entry });
101
+ continue;
102
+ }
86
103
  const last = items.at(-1);
87
104
  if (last?.type === "events") last.events.push(row.event);
88
105
  else items.push({ type: "events", key: row.event.id, events: [row.event] });
@@ -17,7 +17,8 @@
17
17
  <div class="flex flex-col items-start gap-1">
18
18
  <p
19
19
  class={cn(
20
- "text-sm leading-relaxed font-normal whitespace-pre-wrap text-pretty",
20
+ // A long unbroken word, such as a class name, otherwise widens a narrow rail.
21
+ "max-w-full text-sm leading-relaxed font-normal whitespace-pre-wrap text-pretty wrap-break-word",
21
22
  long && !open && "line-clamp-6",
22
23
  className,
23
24
  )}
@@ -15,7 +15,13 @@
15
15
  resolveThreadLabels,
16
16
  resolvedCount,
17
17
  } from "./logic.js";
18
- import type { ThreadComment, ThreadEvent, ThreadLabels, ThreadPost } from "./types.js";
18
+ import type {
19
+ ThreadComment,
20
+ ThreadEntry,
21
+ ThreadEvent,
22
+ ThreadLabels,
23
+ ThreadPost,
24
+ } from "./types.js";
19
25
 
20
26
  interface Props {
21
27
  comments: ThreadComment[];
@@ -46,6 +52,13 @@
46
52
  onAnchor?: (comment: ThreadComment) => void;
47
53
  /** Whose comments carry the edit and delete buttons. A rich comment can't be edited here. */
48
54
  canEdit?: (comment: ThreadComment) => boolean;
55
+ /** False hides the visible title, for a thread already named by its tab. Screen readers still get it. */
56
+ heading?: boolean;
57
+ /** Rows the host draws through `entry`, placed in time among the comments. */
58
+ entries?: ThreadEntry[];
59
+ entry?: Snippet<[ThreadEntry]>;
60
+ /** Drawn above the composer, such as a question waiting on an answer. */
61
+ footer?: Snippet;
49
62
  class?: string;
50
63
  }
51
64
 
@@ -66,6 +79,10 @@
66
79
  onResolve,
67
80
  onAnchor,
68
81
  canEdit = () => false,
82
+ heading = true,
83
+ entries = [],
84
+ entry,
85
+ footer,
69
86
  class: className,
70
87
  }: Props = $props();
71
88
 
@@ -74,7 +91,7 @@
74
91
  let resolved = $derived(resolvedCount(nodes));
75
92
  let showResolved = $state(false);
76
93
  let shown = $derived(showResolved ? nodes : nodes.filter((node) => !node.comment.resolved));
77
- let items = $derived(interleave(shown, events));
94
+ let items = $derived(interleave(shown, events, entry ? entries : []));
78
95
 
79
96
  function shortDate(at: string): string {
80
97
  return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(new Date(at));
@@ -273,8 +290,8 @@
273
290
  {/snippet}
274
291
 
275
292
  <section aria-label={labels.title} class={cn("flex flex-col gap-3", className)}>
276
- <div class="flex h-7 items-center justify-between">
277
- <h2 class="text-xs font-medium text-muted-foreground">{labels.title}</h2>
293
+ <div class={cn("flex h-7 items-center justify-between", !heading && resolved === 0 && "sr-only")}>
294
+ <h2 class={cn("text-xs font-medium text-muted-foreground", !heading && "sr-only")}>{labels.title}</h2>
278
295
  {#if resolved > 0}
279
296
  <button
280
297
  type="button"
@@ -304,6 +321,8 @@
304
321
  {#each items as item (item.key)}
305
322
  {#if item.type === "events"}
306
323
  <li><ThreadEvents events={item.events} when={whenEvent} /></li>
324
+ {:else if item.type === "entry"}
325
+ <li class="min-w-0">{@render entry?.(item.entry)}</li>
307
326
  {:else}
308
327
  {@const node = item.node}
309
328
  <li
@@ -353,6 +372,8 @@
353
372
  <p class="text-[13px] text-muted-foreground">{labels.empty}</p>
354
373
  {/if}
355
374
 
375
+ {@render footer?.()}
376
+
356
377
  {#if onPost}
357
378
  {@render composer(draft, labels.placeholder, sendRoot, (next) => (draft = next), false)}
358
379
  {/if}
@@ -59,10 +59,18 @@ export type ThreadEvent = {
59
59
  icon?: PhosphorIcon;
60
60
  };
61
61
 
62
- /** The thread in time order: a conversation, or a run of events between two of them. */
62
+ /** A row the host draws itself, placed in time among the comments, such as an agent's run or a message to it. */
63
+ export type ThreadEntry = {
64
+ id: string;
65
+ /** ISO timestamp. */
66
+ at: string;
67
+ };
68
+
69
+ /** The thread in time order: a conversation, a run of events between two of them, or a host's entry. */
63
70
  export type ThreadItem =
64
71
  | { type: "node"; key: string; node: ThreadNode }
65
- | { type: "events"; key: string; events: ThreadEvent[] };
72
+ | { type: "events"; key: string; events: ThreadEvent[] }
73
+ | { type: "entry"; key: string; entry: ThreadEntry };
66
74
 
67
75
  export type ThreadLabels = {
68
76
  title: string;