@heyramzi/ui 2.20.0 → 2.21.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.
Files changed (176) hide show
  1. package/README.md +2 -0
  2. package/dist/charts/dashboard/data-table.js +1 -2
  3. package/dist/components/list-page/filter-section.js +1 -1
  4. package/dist/components/list-page/list-table.js +5 -6
  5. package/dist/components/list-page/queue-rail.js +2 -1
  6. package/dist/components/picker/trigger.js +3 -3
  7. package/dist/components/segmented/segmented.js +5 -5
  8. package/dist/components/work-parts/looks.js +1 -1
  9. package/dist/shadcn/dialog/index.js +3 -2
  10. package/dist/shadcn/dropdown-menu/menu-item.js +1 -1
  11. package/dist/shadcn/sheet/index.js +3 -2
  12. package/dist/shadcn/skeleton/index.js +3 -2
  13. package/dist/status/index.js +1 -0
  14. package/dist/work/kanban-drag.js +13 -7
  15. package/package.json +1 -1
  16. package/src/ai/confirmation/ConfirmationTitle.svelte +1 -4
  17. package/src/ai/loading-state/PixelLoader.svelte +1 -1
  18. package/src/animations/dropdown.css +19 -10
  19. package/src/animations/modal.css +7 -2
  20. package/src/charts/charts.css +3 -5
  21. package/src/charts/dashboard/dashboard-section.svelte +1 -1
  22. package/src/charts/dashboard/data-table.ts +1 -2
  23. package/src/charts/dashboard/stat-tile.svelte +1 -1
  24. package/src/charts/number-chart.css +8 -7
  25. package/src/charts/pop-value.svelte +68 -0
  26. package/src/charts/primitives/chart-tooltip.svelte +1 -1
  27. package/src/charts/reference-line.css +1 -1
  28. package/src/charts/stat-card.css +4 -4
  29. package/src/components/copy/copy-button.svelte +11 -10
  30. package/src/components/data-table/row-arrow.svelte +1 -1
  31. package/src/components/data-table/row-context-menu.svelte +1 -1
  32. package/src/components/data-table/sortable-head.svelte +2 -2
  33. package/src/components/data-table/table-skeleton.svelte +5 -5
  34. package/src/components/docs/doc-page.svelte +1 -3
  35. package/src/components/docs/on-this-page.svelte +4 -4
  36. package/src/components/field-row/field-row.svelte +1 -1
  37. package/src/components/list-page/detail-empty.svelte +3 -8
  38. package/src/components/list-page/detail-header.svelte +1 -1
  39. package/src/components/list-page/detail-row.svelte +2 -1
  40. package/src/components/list-page/empty-state.svelte +15 -7
  41. package/src/components/list-page/filter-item.svelte +2 -2
  42. package/src/components/list-page/filter-popover.svelte +1 -1
  43. package/src/components/list-page/filter-section.ts +1 -4
  44. package/src/components/list-page/grouped-list.svelte +37 -8
  45. package/src/components/list-page/list-page-shell.svelte +1 -1
  46. package/src/components/list-page/list-table.ts +6 -6
  47. package/src/components/list-page/master-detail.svelte +1 -1
  48. package/src/components/list-page/queue-rail.ts +2 -2
  49. package/src/components/list-page/rail-empty.svelte +1 -1
  50. package/src/components/list-page/rail-row.svelte +5 -5
  51. package/src/components/open-in/open-in.svelte +2 -1
  52. package/src/components/picker/trigger.ts +6 -5
  53. package/src/components/section-card/section-card.svelte +1 -1
  54. package/src/components/section-header/section-header.svelte +4 -2
  55. package/src/components/section-header/section-label.svelte +2 -2
  56. package/src/components/segmented/animated-tabs.svelte +1 -1
  57. package/src/components/segmented/section-runs.svelte +1 -1
  58. package/src/components/segmented/segment.svelte +4 -6
  59. package/src/components/segmented/segmented-track.svelte +18 -52
  60. package/src/components/segmented/segmented.ts +5 -5
  61. package/src/components/table-view/table-view.svelte +43 -24
  62. package/src/components/tags-input/tags-input.svelte +17 -31
  63. package/src/components/work-parts/looks.ts +1 -1
  64. package/src/editor/content.css +3 -3
  65. package/src/editor/editor.svelte +1 -1
  66. package/src/inbox/thread-inbox.svelte +2 -9
  67. package/src/panel/side-panel.svelte +9 -4
  68. package/src/record/info-list.svelte +7 -8
  69. package/src/record/project-list.svelte +1 -1
  70. package/src/record/record-demo.svelte +2 -4
  71. package/src/record/record-shell.svelte +1 -1
  72. package/src/record/record-stats.svelte +9 -3
  73. package/src/record/steps-rail.svelte +7 -11
  74. package/src/shadcn/accordion/accordion-trigger.svelte +1 -1
  75. package/src/shadcn/alert-dialog/alert-dialog-content.svelte +5 -1
  76. package/src/shadcn/alert-dialog/alert-dialog-footer.svelte +12 -2
  77. package/src/shadcn/alert-dialog/alert-dialog-header.svelte +1 -1
  78. package/src/shadcn/alert-dialog/alert-dialog-title.svelte +1 -1
  79. package/src/shadcn/badge/badge.svelte +3 -1
  80. package/src/shadcn/bubble/bubble-content.svelte +1 -1
  81. package/src/shadcn/bubble/bubble.svelte +1 -1
  82. package/src/shadcn/button/button.svelte +11 -5
  83. package/src/shadcn/calendar/calendar-month-select.svelte +1 -1
  84. package/src/shadcn/calendar/calendar-year-select.svelte +1 -1
  85. package/src/shadcn/calendar/calendar.svelte +1 -1
  86. package/src/shadcn/card/card.svelte +2 -1
  87. package/src/shadcn/checkbox/checkbox.svelte +1 -1
  88. package/src/shadcn/code-block/code-block-actions.svelte +1 -1
  89. package/src/shadcn/command/command-dialog.svelte +2 -2
  90. package/src/shadcn/command/command-group.svelte +1 -1
  91. package/src/shadcn/command/command-input.svelte +1 -1
  92. package/src/shadcn/command/command-item.svelte +1 -1
  93. package/src/shadcn/dialog/dialog-body.svelte +22 -0
  94. package/src/shadcn/dialog/dialog-content.svelte +19 -4
  95. package/src/shadcn/dialog/dialog-footer.svelte +19 -1
  96. package/src/shadcn/dialog/dialog-header.svelte +6 -1
  97. package/src/shadcn/dialog/dialog-title.svelte +2 -1
  98. package/src/shadcn/dialog/index.ts +3 -0
  99. package/src/shadcn/dropdown-menu/dropdown-menu-content.svelte +5 -1
  100. package/src/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  101. package/src/shadcn/dropdown-menu/menu-item.ts +1 -1
  102. package/src/shadcn/field/field-separator.svelte +1 -1
  103. package/src/shadcn/hover-card/hover-card-content.svelte +1 -1
  104. package/src/shadcn/input/input.svelte +2 -2
  105. package/src/shadcn/input-otp/input-otp-slot.svelte +1 -1
  106. package/src/shadcn/message-scroller/message-scroller-button.svelte +1 -1
  107. package/src/shadcn/popover/popover-content.svelte +1 -1
  108. package/src/shadcn/radio-group/radio-group-item.svelte +1 -1
  109. package/src/shadcn/scroll-area/scroll-area.svelte +1 -1
  110. package/src/shadcn/select/select-content.svelte +6 -2
  111. package/src/shadcn/select/select-item.svelte +1 -1
  112. package/src/shadcn/select/select-trigger.svelte +1 -1
  113. package/src/shadcn/sheet/index.ts +3 -0
  114. package/src/shadcn/sheet/sheet-body.svelte +22 -0
  115. package/src/shadcn/sheet/sheet-content.svelte +27 -5
  116. package/src/shadcn/sheet/sheet-footer.svelte +14 -2
  117. package/src/shadcn/sheet/sheet-header.svelte +1 -1
  118. package/src/shadcn/sheet/sheet-title.svelte +1 -1
  119. package/src/shadcn/sheet/sheet.css +11 -5
  120. package/src/shadcn/sidebar/sidebar-group-action.svelte +1 -1
  121. package/src/shadcn/sidebar/sidebar-group-label.svelte +1 -1
  122. package/src/shadcn/sidebar/sidebar-inset.svelte +1 -1
  123. package/src/shadcn/sidebar/sidebar-menu-action.svelte +1 -1
  124. package/src/shadcn/sidebar/sidebar-menu-button.svelte +2 -2
  125. package/src/shadcn/sidebar/sidebar-menu-sub-button.svelte +1 -1
  126. package/src/shadcn/sidebar/sidebar-menu.svelte +1 -1
  127. package/src/shadcn/sidebar/sidebar.svelte +1 -1
  128. package/src/shadcn/skeleton/index.ts +3 -0
  129. package/src/shadcn/skeleton/skeleton-delay.svelte +63 -0
  130. package/src/shadcn/skeleton/skeleton-delay.svelte.test.ts +57 -0
  131. package/src/shadcn/skeleton/skeleton.svelte +5 -1
  132. package/src/shadcn/slider/slider.svelte +1 -1
  133. package/src/shadcn/switch/switch.svelte +2 -2
  134. package/src/shadcn/table/table-row.svelte +1 -1
  135. package/src/shadcn/tabs/tabs-list.svelte +1 -1
  136. package/src/shadcn/tabs/tabs-trigger.svelte +2 -1
  137. package/src/shadcn/textarea/textarea.svelte +1 -1
  138. package/src/shadcn/toggle/toggle.svelte +2 -2
  139. package/src/shadcn/toggle-group/toggle-group.svelte +1 -1
  140. package/src/shadcn/tooltip/tooltip-content.svelte +1 -1
  141. package/src/shadcn/trace/trace-collapsible-trigger.svelte +1 -1
  142. package/src/shadcn/trace/trace.svelte +1 -1
  143. package/src/status/README.md +1 -1
  144. package/src/status/index.ts +1 -0
  145. package/src/status/status-badge.svelte +21 -7
  146. package/src/status/status-dot.svelte +6 -11
  147. package/src/status/status-pill.svelte +50 -0
  148. package/src/thread/thread.svelte +54 -136
  149. package/src/tokens/craft.css +55 -36
  150. package/src/tokens/palette.css +16 -9
  151. package/src/work/demo.svelte +48 -42
  152. package/src/work/detail/detail-demo.svelte +33 -24
  153. package/src/work/detail/task-chip.svelte +4 -4
  154. package/src/work/detail/task-detail.svelte +41 -108
  155. package/src/work/detail/task-property.svelte +7 -13
  156. package/src/work/detail/task-thread.svelte +48 -119
  157. package/src/work/docs/doc-editor-page.svelte +5 -4
  158. package/src/work/docs/doc-menu.svelte +9 -4
  159. package/src/work/docs/docs-demo.svelte +4 -4
  160. package/src/work/docs/edited-chip.svelte +11 -4
  161. package/src/work/gantt/gantt-bar.svelte +18 -14
  162. package/src/work/gantt/gantt-grid.svelte +7 -7
  163. package/src/work/gantt/gantt-group-row.svelte +7 -10
  164. package/src/work/gantt/gantt-header.svelte +3 -1
  165. package/src/work/gantt/gantt-task-row.svelte +19 -9
  166. package/src/work/gantt/gantt.css +4 -5
  167. package/src/work/inbox/inbox-card.svelte +32 -10
  168. package/src/work/inbox/inbox-draft.svelte +2 -2
  169. package/src/work/kanban-column.svelte +7 -15
  170. package/src/work/kanban-drag.ts +13 -8
  171. package/src/work/task-gantt.svelte +2 -1
  172. package/src/work/task-list.svelte +54 -53
  173. package/src/work/time/time-chip.svelte +16 -19
  174. package/src/work/time/time-dot.svelte +2 -1
  175. package/src/work/time/time-ring.svelte +2 -3
  176. package/src/work/time/time-tracker.svelte +72 -166
@@ -17,7 +17,7 @@
17
17
  bind:ref
18
18
  data-slot="trace-collapsible-trigger"
19
19
  class={cn(
20
- "gap-1 rounded-md focus-visible:ring-3 focus-visible:ring-ring/50 group/trace-collapsible-trigger flex w-fit max-w-full min-w-0 cursor-pointer items-center text-left text-muted-foreground transition-colors outline-none hover:text-foreground has-[[data-status=error]]:text-destructive",
20
+ "gap-1 rounded-md focus-visible:shadow-(--ring-focus) group/trace-collapsible-trigger flex w-fit max-w-full min-w-0 cursor-pointer items-center text-left text-muted-foreground transition-colors outline-none hover:text-foreground has-[[data-status=error]]:text-destructive",
21
21
  className
22
22
  )}
23
23
  {...restProps}
@@ -14,7 +14,7 @@
14
14
  bind:this={ref}
15
15
  data-slot="trace"
16
16
  class={cn(
17
- "gap-2 *:has-[>[data-slot=trace-line]]:rounded-md *:has-[>[data-slot=trace-line]]:focus-visible:ring-3 *:has-[>[data-slot=trace-line]]:focus-visible:ring-ring/50 flex w-full min-w-0 flex-col *:has-[>[data-slot=trace-line]]:flex *:has-[>[data-slot=trace-line]]:w-fit *:has-[>[data-slot=trace-line]]:max-w-full *:has-[>[data-slot=trace-line]]:min-w-0 *:has-[>[data-slot=trace-line]]:text-left *:has-[>[data-slot=trace-line]]:outline-none",
17
+ "gap-2 *:has-[>[data-slot=trace-line]]:rounded-md *:has-[>[data-slot=trace-line]]:focus-visible:shadow-(--ring-focus) flex w-full min-w-0 flex-col *:has-[>[data-slot=trace-line]]:flex *:has-[>[data-slot=trace-line]]:w-fit *:has-[>[data-slot=trace-line]]:max-w-full *:has-[>[data-slot=trace-line]]:min-w-0 *:has-[>[data-slot=trace-line]]:text-left *:has-[>[data-slot=trace-line]]:outline-none",
18
18
  className
19
19
  )}
20
20
  {...restProps}
@@ -15,4 +15,4 @@ Status badge, dot and icon from one map. `defineStatuses` builds the map, and th
15
15
  <StatusBadge {statuses} status="sent" />
16
16
  ```
17
17
 
18
- Exports: `StatusBadge`, `StatusDot`, `StatusIcon`, `defineStatuses` and the types `StatusDef`, `StatusDotVariant`, `StatusId`, `StatusMap`, `Statuses`. The old subpaths `tasks/status`, `tasks/status-dot` and `tasks/statuses` resolve to the same files.
18
+ Exports: `StatusBadge`, `StatusPill` (the tinted pill StatusBadge draws, for an organism that has its own icon and label), `StatusDot`, `StatusIcon`, `defineStatuses` and the types `StatusDef`, `StatusDotVariant`, `StatusId`, `StatusMap`, `Statuses`. The old subpaths `tasks/status`, `tasks/status-dot` and `tasks/statuses` resolve to the same files.
@@ -9,3 +9,4 @@ export type {
9
9
  StatusMap,
10
10
  Statuses,
11
11
  } from "./status-types.js";
12
+ export { default as StatusPill, PILL_TINT } from "./status-pill.svelte";
@@ -2,7 +2,8 @@
2
2
  import type { Snippet } from "svelte";
3
3
  import type { HTMLAttributes } from "svelte/elements";
4
4
  import { cn } from "../lib/utils.js";
5
- import { Badge } from "../shadcn/badge/index.js";
5
+ import StatusPill from "./status-pill.svelte";
6
+ import StatusIcon from "./status-icon.svelte";
6
7
  import { Spinner } from "../shadcn/spinner/index.js";
7
8
  import { TONE_COLOR, type Tone } from "../lib/tone.js";
8
9
  import type { Statuses } from "./status-types.js";
@@ -44,22 +45,35 @@
44
45
  let resolvedLabel = $derived(
45
46
  label ?? (statuses && status ? statuses.label(status) : (status ?? "")),
46
47
  );
48
+ // Every registry status: StatusIcon draws the variant's shape when the map has none (DESIGN.md, Patterns).
49
+ let hasIcon = $derived(Boolean(statuses && status && statuses.has(status)));
47
50
  </script>
48
51
 
49
- <Badge tone={resolvedTone} class={cn(capitalize && "capitalize", className)} {...restProps}>
50
- {#if dot}
52
+ <!-- The pill draws the tint; this picks what goes before the label. A registry icon wins,
53
+ so the state reads without its colour. -->
54
+ {#snippet lead()}
55
+ {#if busy}
56
+ <Spinner class="size-3" aria-hidden="true" />
57
+ {:else if hasIcon && statuses && status}
58
+ <StatusIcon {statuses} {status} size={12} animate={false} />
59
+ {:else if dot}
51
60
  <span
52
61
  class="size-1.5 shrink-0 rounded-full"
53
62
  style:background-color={TONE_COLOR[resolvedTone]}
54
63
  aria-hidden="true"
55
64
  ></span>
56
65
  {/if}
57
- {#if busy}
58
- <Spinner class="size-3" aria-hidden="true" />
59
- {/if}
66
+ {/snippet}
67
+
68
+ <StatusPill
69
+ tone={resolvedTone}
70
+ icon={busy || hasIcon || dot ? lead : undefined}
71
+ class={cn(capitalize && "capitalize", className)}
72
+ {...restProps}
73
+ >
60
74
  {#if children}
61
75
  {@render children()}
62
76
  {:else}
63
77
  {resolvedLabel}
64
78
  {/if}
65
- </Badge>
79
+ </StatusPill>
@@ -151,7 +151,7 @@
151
151
 
152
152
  .sd-fill {
153
153
  transform-origin: 7px 7px;
154
- animation: sd-fill-in var(--duration-slow, 300ms) var(--ease-smooth-out, ease-out) forwards;
154
+ animation: sd-fill-in var(--duration-slow) var(--ease-smooth-out) forwards;
155
155
  }
156
156
 
157
157
  @keyframes sd-fill-in {
@@ -167,7 +167,7 @@
167
167
 
168
168
  .sd-pop {
169
169
  transform-origin: 7px 7px;
170
- animation: sd-pop-in var(--duration-medium, 280ms) var(--ease-smooth-out, ease-out) forwards;
170
+ animation: sd-pop-in var(--duration-medium) var(--ease-smooth-out) forwards;
171
171
  }
172
172
 
173
173
  @keyframes sd-pop-in {
@@ -175,10 +175,6 @@
175
175
  transform: scale(0.4);
176
176
  opacity: 0;
177
177
  }
178
- 60% {
179
- transform: scale(1.08);
180
- opacity: 1;
181
- }
182
178
  to {
183
179
  transform: scale(1);
184
180
  opacity: 1;
@@ -188,19 +184,18 @@
188
184
  .sd-check {
189
185
  stroke-dasharray: 12;
190
186
  stroke-dashoffset: 12;
191
- animation: sd-trace var(--duration-medium, 280ms) var(--ease-smooth-out, ease-out)
192
- var(--duration-quick, 120ms) forwards;
187
+ animation: sd-trace var(--duration-medium) var(--ease-smooth-out) var(--duration-quick) forwards;
193
188
  }
194
189
 
195
190
  .sd-cross {
196
191
  stroke-dasharray: 6;
197
192
  stroke-dashoffset: 6;
198
- animation: sd-trace calc(var(--duration-fast, 160ms) / 2) var(--ease-smooth-out, ease-out)
199
- var(--duration-quick, 120ms) forwards;
193
+ animation: sd-trace calc(var(--duration-fast) / 2) var(--ease-smooth-out) var(--duration-quick)
194
+ forwards;
200
195
  }
201
196
 
202
197
  .sd-cross-second {
203
- animation-delay: calc(var(--duration-quick, 120ms) + var(--duration-fast, 160ms) / 2);
198
+ animation-delay: calc(var(--duration-quick) + var(--duration-fast) / 2);
204
199
  }
205
200
 
206
201
  @keyframes sd-trace {
@@ -0,0 +1,50 @@
1
+ <script lang="ts" module>
2
+ import type { Tone } from "../lib/tone.js";
3
+
4
+ // A quiet tint and no border: the icon and the text carry the tone, the fill only groups
5
+ // them. Neutral reads off the foreground, not `bg-muted`, because muted sits 0.005 L from
6
+ // the canvas and vanishes there (tokens board, "what fights the new scale").
7
+ export const PILL_TINT: Record<Tone, string> = {
8
+ neutral: "bg-foreground/6 text-muted-foreground",
9
+ accent:
10
+ "bg-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_12%,transparent)] text-[color:var(--tone-accent,var(--color-primary))]",
11
+ info: "bg-info/12 text-info-800 dark:text-info-400",
12
+ success: "bg-success/12 text-success-800 dark:text-success-400",
13
+ warning: "bg-warning/14 text-warning-800 dark:text-warning-400",
14
+ destructive: "bg-destructive/10 text-destructive-ink",
15
+ };
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import type { Snippet } from "svelte";
20
+ import type { HTMLAttributes } from "svelte/elements";
21
+ import { cn } from "../lib/utils.js";
22
+
23
+ interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "class" | "children"> {
24
+ tone?: Tone;
25
+ /** What sits before the label: a Phosphor icon, a `StatusIcon`, a dot or a spinner. */
26
+ icon?: Snippet;
27
+ children?: Snippet;
28
+ class?: string;
29
+ }
30
+
31
+ let { tone = "neutral", icon, children, class: className, ...restProps }: Props = $props();
32
+ </script>
33
+
34
+ <!-- A pill means status or a count (DESIGN.md rule 15), so this is the one rounded-full chip. -->
35
+ <span
36
+ data-slot="status-pill"
37
+ data-tone={tone}
38
+ class={cn(
39
+ "inline-flex h-5 max-w-full shrink-0 items-center gap-1 overflow-hidden rounded-full px-2 text-xs font-medium whitespace-nowrap",
40
+ "transition-[background-color,color] duration-quick ease-smooth-out motion-reduce:transition-none",
41
+ "[&>svg]:size-3 [&>svg]:shrink-0",
42
+ icon && "pl-1.5",
43
+ PILL_TINT[tone],
44
+ className,
45
+ )}
46
+ {...restProps}
47
+ >
48
+ {@render icon?.()}
49
+ <span class="min-w-0 truncate">{@render children?.()}</span>
50
+ </span>
@@ -6,6 +6,8 @@
6
6
  import { AgentAvatar } from "../agents/index.js";
7
7
  import RichRenderer from "../editor/renderer.svelte";
8
8
  import { Skeleton } from "../shadcn/skeleton/index.js";
9
+ import { Button } from "../shadcn/button/index.js";
10
+ import { TagChip } from "../components/chip/index.js";
9
11
  import ThreadBody from "./thread-body.svelte";
10
12
  import ThreadEvents from "./thread-events.svelte";
11
13
  import {
@@ -152,20 +154,15 @@
152
154
  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"
153
155
  ></textarea>
154
156
  <div class="flex justify-end px-2 pb-2">
155
- <button
156
- type="button"
157
- class="thread-button thread-primary"
158
- onclick={onSend}
159
- disabled={!value.trim() || sending}
160
- >
157
+ <Button size="sm" onclick={onSend} disabled={!value.trim() || sending}>
161
158
  {sending ? labels.sending : isReply ? labels.reply : labels.send}
162
- </button>
159
+ </Button>
163
160
  </div>
164
161
  </div>
165
162
  {/snippet}
166
163
 
167
164
  {#snippet row(comment: ThreadComment, isRoot: boolean)}
168
- <div class="group/row relative flex gap-3">
165
+ <div class="group/row hover-row relative flex gap-3">
169
166
  <span class="relative size-6 shrink-0">
170
167
  {#if avatar}
171
168
  {@render avatar(comment)}
@@ -185,7 +182,7 @@
185
182
 
186
183
  <div class="min-w-0 flex-1">
187
184
  <div class="flex min-h-6 flex-wrap items-center gap-x-2">
188
- <span class="truncate text-[13px] font-medium">{comment.author.name}</span>
185
+ <span class="truncate text-(length:--text-control) font-medium">{comment.author.name}</span>
189
186
  {#if comment.author.kind === "agent"}
190
187
  <span class="sr-only">{labels.agent}</span>
191
188
  {/if}
@@ -197,35 +194,40 @@
197
194
  {#if anchorChip}
198
195
  {@render anchorChip(comment)}
199
196
  {:else if onAnchor}
200
- <button type="button" class="thread-chip" onclick={() => onAnchor(comment)}>
201
- <MapPin class="size-3" aria-hidden="true" />{labels.pinned}
202
- </button>
197
+ <button
198
+ type="button"
199
+ class="rounded-sm outline-none focus-visible:shadow-[var(--ring-focus)]"
200
+ onclick={() => onAnchor(comment)}
201
+ ><TagChip
202
+ >{#snippet icon()}<MapPin aria-hidden="true" />{/snippet}{labels.pinned}</TagChip
203
+ ></button
204
+ >
203
205
  {:else}
204
- <span class="thread-chip"
205
- ><MapPin class="size-3" aria-hidden="true" />{labels.pinned}</span
206
+ <TagChip
207
+ >{#snippet icon()}<MapPin aria-hidden="true" />{/snippet}{labels.pinned}</TagChip
206
208
  >
207
209
  {/if}
208
210
  {/if}
209
211
  {#if editingId !== comment.id}
210
- <span
211
- class="ms-auto flex gap-0.5 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
212
- >
212
+ <span class="row-action ms-auto flex gap-0.5">
213
213
  {#if isRoot && onResolve}
214
- <button
215
- type="button"
216
- class="thread-icon"
214
+ <Button
215
+ variant="ghost"
216
+ size="sm"
217
+ icon
217
218
  aria-label={comment.resolved ? labels.unresolve : labels.resolve}
218
219
  aria-pressed={comment.resolved ?? false}
219
220
  onclick={() => onResolve(comment, !comment.resolved)}
220
221
  >
221
222
  <Check class="size-3.5" aria-hidden="true" />
222
- </button>
223
+ </Button>
223
224
  {/if}
224
225
  {#if canEdit(comment)}
225
226
  {#if onEdit && !comment.content}
226
- <button
227
- type="button"
228
- class="thread-icon"
227
+ <Button
228
+ variant="ghost"
229
+ size="sm"
230
+ icon
229
231
  aria-label={labels.edit}
230
232
  onclick={() => {
231
233
  editingId = comment.id;
@@ -233,17 +235,18 @@
233
235
  }}
234
236
  >
235
237
  <PencilSimple class="size-3.5" aria-hidden="true" />
236
- </button>
238
+ </Button>
237
239
  {/if}
238
240
  {#if onDelete}
239
- <button
240
- type="button"
241
- class="thread-icon"
241
+ <Button
242
+ variant="ghost"
243
+ size="sm"
244
+ icon
242
245
  aria-label={labels.delete}
243
246
  onclick={() => onDelete?.(comment)}
244
247
  >
245
248
  <Trash class="size-3.5" aria-hidden="true" />
246
- </button>
249
+ </Button>
247
250
  {/if}
248
251
  {/if}
249
252
  </span>
@@ -255,20 +258,20 @@
255
258
  bind:value={editDraft}
256
259
  rows={3}
257
260
  aria-label={labels.edit}
258
- 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"
261
+ 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:shadow-(--ring-focus)"
259
262
  ></textarea>
260
263
  <div class="mt-2 flex justify-end gap-1.5">
261
- <button type="button" class="thread-button" onclick={() => (editingId = null)}
262
- >{labels.cancel}</button
263
- >
264
- <button
265
- type="button"
266
- class="thread-button thread-primary"
267
- onclick={() => saveEdit(comment)}>{labels.save}</button
264
+ <Button variant="ghost" size="sm" onclick={() => (editingId = null)}
265
+ >{labels.cancel}</Button
268
266
  >
267
+ <Button size="sm" onclick={() => saveEdit(comment)}>{labels.save}</Button>
269
268
  </div>
270
269
  {:else if comment.content}
271
- <div class="text-sm leading-relaxed"><RichRenderer content={comment.content} /></div>
270
+ <div
271
+ class="text-sm leading-relaxed [--content-gap:0.5em] [--content-leading:1.625] [--content-size:var(--text-sm)]"
272
+ >
273
+ <RichRenderer content={comment.content} />
274
+ </div>
272
275
  {:else}
273
276
  <ThreadBody text={comment.body} more={labels.showMore} less={labels.showLess} />
274
277
  {/if}
@@ -280,16 +283,18 @@
280
283
 
281
284
  <section aria-label={labels.title} class={cn("flex flex-col gap-3", className)}>
282
285
  <div class={cn("flex h-7 items-center justify-between", !heading && resolved === 0 && "sr-only")}>
283
- <h2 class={cn("text-xs font-medium text-muted-foreground", !heading && "sr-only")}>{labels.title}</h2>
286
+ <h2 class={cn("text-xs font-medium text-muted-foreground", !heading && "sr-only")}>
287
+ {labels.title}
288
+ </h2>
284
289
  {#if resolved > 0}
285
- <button
286
- type="button"
287
- class="thread-button"
290
+ <Button
291
+ variant="ghost"
292
+ size="sm"
288
293
  aria-expanded={showResolved}
289
294
  onclick={() => (showResolved = !showResolved)}
290
295
  >
291
296
  {showResolved ? labels.hideResolved : fillCount(labels.showResolved, resolved)}
292
- </button>
297
+ </Button>
293
298
  {/if}
294
299
  </div>
295
300
 
@@ -339,16 +344,17 @@
339
344
  )}
340
345
  </div>
341
346
  {:else}
342
- <button
343
- type="button"
344
- class="thread-button ms-9 flex w-fit items-center gap-1.5"
347
+ <Button
348
+ variant="ghost"
349
+ size="sm"
350
+ class="ms-9 w-fit"
345
351
  onclick={() => {
346
352
  replyTo = node.comment.id;
347
353
  replyDraft = "";
348
354
  }}
349
355
  >
350
356
  <ArrowBendDownRight class="size-3.5" aria-hidden="true" />{labels.reply}
351
- </button>
357
+ </Button>
352
358
  {/if}
353
359
  {/if}
354
360
  </li>
@@ -356,7 +362,7 @@
356
362
  {/each}
357
363
  </ol>
358
364
  {:else if !onPost}
359
- <p class="text-[13px] text-muted-foreground">{labels.empty}</p>
365
+ <p class="text-(length:--text-control) text-muted-foreground">{labels.empty}</p>
360
366
  {/if}
361
367
 
362
368
  {@render footer?.()}
@@ -365,91 +371,3 @@
365
371
  {@render composer(draft, labels.placeholder, sendRoot, (next) => (draft = next), false)}
366
372
  {/if}
367
373
  </section>
368
-
369
- <style>
370
- .thread-icon {
371
- display: grid;
372
- place-items: center;
373
- width: 1.75rem;
374
- height: 1.75rem;
375
- border-radius: var(--radius-md, 0.5rem);
376
- color: var(--color-muted-foreground);
377
- outline: none;
378
- transition: background-color var(--duration-quick, 150ms) var(--ease-smooth-out, ease-out);
379
- }
380
-
381
- .thread-chip {
382
- display: inline-flex;
383
- align-items: center;
384
- gap: 0.25rem;
385
- height: 1.25rem;
386
- padding-inline: 0.375rem;
387
- border-radius: var(--radius-sm, 0.25rem);
388
- background: var(--color-muted);
389
- color: var(--color-muted-foreground);
390
- font-size: 0.75rem;
391
- outline: none;
392
- }
393
-
394
- button.thread-chip:focus-visible {
395
- box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-ring) 50%, transparent);
396
- }
397
-
398
- @media (hover: hover) and (pointer: fine) {
399
- .thread-icon:hover {
400
- background: var(--color-muted);
401
- color: var(--color-foreground);
402
- }
403
- }
404
-
405
- .thread-icon:focus-visible,
406
- .thread-button:focus-visible {
407
- box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-ring) 50%, transparent);
408
- }
409
-
410
- .thread-button {
411
- height: 1.75rem;
412
- padding-inline: 0.75rem;
413
- border-radius: var(--radius-md, 0.5rem);
414
- font-size: 0.8125rem;
415
- font-weight: 500;
416
- outline: none;
417
- transition: background-color var(--duration-quick, 150ms) var(--ease-smooth-out, ease-out);
418
- }
419
-
420
- @media (hover: hover) and (pointer: fine) {
421
- .thread-button:hover:not(:disabled) {
422
- background: var(--color-muted);
423
- }
424
- }
425
-
426
- .thread-button:disabled {
427
- opacity: 0.5;
428
- pointer-events: none;
429
- }
430
-
431
- .thread-primary {
432
- background: var(--color-primary);
433
- color: var(--color-primary-foreground);
434
- }
435
-
436
- /* An empty draft reads as an outlined button, not a faded fill. */
437
- .thread-primary:disabled {
438
- opacity: 1;
439
- background: transparent;
440
- box-shadow: inset 0 0 0 1px var(--color-border);
441
- color: var(--color-muted-foreground);
442
- }
443
-
444
- @media (hover: hover) and (pointer: fine) {
445
- .thread-primary:hover:not(:disabled) {
446
- background: color-mix(in oklab, var(--color-primary) 90%, transparent);
447
- }
448
- }
449
-
450
- @media (any-pointer: coarse) {
451
- .thread-button {
452
- height: 2.5rem;
453
- }
454
- }
455
- </style>
@@ -1,25 +1,31 @@
1
1
  /* ── Radius, by role not by size ─────────────────────────────
2
- These override Tailwind's own names, so `rounded-lg` on a button already
3
- resolves to the control radius. */
2
+ Tailwind's names, so `rounded-lg` is the control radius. Five steps, 4px
3
+ apart; xs and md fold into sm. */
4
4
  @theme {
5
- --radius-xs: 4px; /* tag, dot, inline code */
6
- --radius-sm: 6px; /* chip, badge, small pill */
7
- --radius-md: 7px; /* between a chip and a control */
8
- --radius-lg: 8px; /* control: button, input, select, menu item */
9
- --radius-xl: 10px; /* card, panel, stage */
10
- --radius-2xl: 14px; /* overlay: dialog, sheet, command palette */
11
- --radius-3xl: 20px; /* hero surface, full-bleed panel */
5
+ --radius-sm: 6px; /* chip, badge, tag, inline code */
6
+ --radius-xs: var(--radius-sm); /* folded, was 4px */
7
+ --radius-md: var(--radius-sm); /* folded, was 7px */
8
+ --radius-lg: 10px; /* control: button, input, select, menu item */
9
+ --radius-xl: 14px; /* card, panel, stage */
10
+ --radius-2xl: 18px; /* overlay: dialog, sheet, popover, command palette */
11
+ --radius-3xl: 22px; /* shell panel, the inset page */
12
12
  }
13
13
 
14
14
  /* ── Type, by role ───────────────────────────────────────────
15
- Tailwind's own `text-xs` to `text-base` cover the rest. */
15
+ Tailwind's own `text-xs` to `text-base` cover the rest. Control text sits
16
+ between them: 13px at weight 500 is Linear's property row, measured 31 Aug
17
+ 2026 on a 16px root, where 14px reads looser. No line height of its own, so
18
+ it inherits like the `text-[13px]` it replaces. Write it
19
+ `text-(length:--text-control)`: tailwind-merge takes a bare `text-control`
20
+ for a colour and drops it beside `text-muted-foreground`. */
16
21
  @theme {
22
+ --text-control: 0.8125rem; /* a picker trigger, a property value */
17
23
  --text-2xs: 0.6875rem; /* a count, a kbd hint, a meta line under a tile; the floor */
18
24
  }
19
25
 
20
26
  /* ── Elevation ───────────────────────────────────────────────
21
- The steps carry no ring, since a bordered box would get two edges a pixel
22
- apart; a borderless one adds `var(--elev-hairline)` in front. */
27
+ `--elev-card` and its hover carry their own hairline, so a box that also
28
+ draws `border` gets a 2px edge. The other steps carry none. */
23
29
  :root {
24
30
  /* Resting on the page. A button, a chip, a row. */
25
31
  --elev-btn:
@@ -28,21 +34,28 @@
28
34
 
29
35
  /* Sitting on the page. A card, a stage, a panel. */
30
36
  --elev-card:
31
- 0 1px 2px -1px color-mix(in oklab, var(--elev-tint) 7%, transparent),
32
- 0 4px 9px -3px color-mix(in oklab, var(--elev-tint) 6%, transparent),
33
- 0 14px 28px -10px color-mix(in oklab, var(--elev-tint) 7%, transparent);
34
-
35
- /* Lifted off the page. A hovered card, a popover, a menu. */
37
+ var(--elev-hairline), 0 1px 2px color-mix(in oklab, var(--elev-tint) 4%, transparent);
38
+
39
+ /* A hovered card: the edge darkens 3% toward the text and a soft shadow lifts it.
40
+ Pair it with a 1px rise, as `.t-lift` does. */
41
+ --elev-card-hover:
42
+ 0 0 0 1px
43
+ color-mix(
44
+ in oklab,
45
+ var(--color-border, var(--border)),
46
+ var(--color-foreground, var(--foreground)) 3%
47
+ ),
48
+ 0 6px 16px -4px color-mix(in oklab, var(--elev-tint) 8%, transparent);
49
+
50
+ /* Lifted off the page. A popover, a menu, a hovered bordered card. */
36
51
  --elev-raised:
37
- 0 1px 2px -1px color-mix(in oklab, var(--elev-tint) 8%, transparent),
38
- 0 6px 13px -4px color-mix(in oklab, var(--elev-tint) 9%, transparent),
39
- 0 20px 40px -12px color-mix(in oklab, var(--elev-tint) 12%, transparent);
52
+ 0 2px 4px -1px color-mix(in oklab, var(--elev-tint) 5%, transparent),
53
+ 0 8px 20px -6px color-mix(in oklab, var(--elev-tint) 10%, transparent);
40
54
 
41
55
  /* Floating over a dimmed page. A dialog, a sheet, a command palette. */
42
56
  --elev-overlay:
43
- 0 2px 4px -1px color-mix(in oklab, var(--elev-tint) 8%, transparent),
44
- 0 12px 26px -8px color-mix(in oklab, var(--elev-tint) 12%, transparent),
45
- 0 40px 72px -20px color-mix(in oklab, var(--elev-tint) 20%, transparent);
57
+ 0 4px 8px -2px color-mix(in oklab, var(--elev-tint) 6%, transparent),
58
+ 0 24px 48px -12px color-mix(in oklab, var(--elev-tint) 16%, transparent);
46
59
 
47
60
  /* A hairline separates a surface from what's behind it; a rim is light on the
48
61
  top edge, which makes a filled control read as a key. */
@@ -58,8 +71,11 @@
58
71
  .dark {
59
72
  --elev-btn: 0 1px 2px color-mix(in oklab, black 30%, transparent);
60
73
  --elev-card:
61
- 0 1px 2px color-mix(in oklab, black 24%, transparent),
62
- 0 4px 12px -4px color-mix(in oklab, black 30%, transparent);
74
+ 0 0 0 1px color-mix(in oklab, white 10%, transparent),
75
+ 0 1px 2px color-mix(in oklab, black 24%, transparent);
76
+ --elev-card-hover:
77
+ 0 0 0 1px color-mix(in oklab, white 16%, transparent),
78
+ 0 8px 20px -6px color-mix(in oklab, black 40%, transparent);
63
79
  --elev-raised:
64
80
  0 2px 6px color-mix(in oklab, black 30%, transparent),
65
81
  0 10px 24px -6px color-mix(in oklab, black 38%, transparent);
@@ -77,10 +93,12 @@
77
93
  then a soft halo. */
78
94
  :root {
79
95
  --ring-color: var(--color-ring, var(--ring));
80
- --ring-halo: color-mix(in oklab, var(--ring-color) 16%, transparent);
81
- /* The halo is 16% over 4px, not 30% over 2px, because a stronger band reads
82
- as a second border instead of a glow. */
83
- --ring-focus: 0 0 0 1px var(--ring-color), 0 0 0 5px var(--ring-halo);
96
+ --ring-halo: color-mix(in oklab, var(--ring-color) 10%, transparent);
97
+ /* The 1px edge carries the 3:1 contrast alone; a 5px 16% halo read as a thick grey band
98
+ (Ramzi, 24 Oct 2026), so it's 2px at 10%. */
99
+ --ring-focus: 0 0 0 1px var(--ring-color), 0 0 0 3px var(--ring-halo);
100
+ /* Rows inside a clipped list focus inward, where an outer ring would be cut off. */
101
+ --ring-focus-inset: inset 0 0 0 1px var(--ring-color), inset 0 0 0 3px var(--ring-halo);
84
102
  }
85
103
 
86
104
  /* ── Motion ──────────────────────────────────────────────────
@@ -124,6 +142,7 @@
124
142
  --shadow-hairline: var(--elev-hairline);
125
143
  --shadow-btn: var(--elev-btn);
126
144
  --shadow-card: var(--elev-card);
145
+ --shadow-card-hover: var(--elev-card-hover);
127
146
  --shadow-raised: var(--elev-raised);
128
147
  --shadow-overlay: var(--elev-overlay);
129
148
  --shadow-field: var(--elev-field);
@@ -132,7 +151,7 @@
132
151
  because the two never appear apart and utilities cannot be stacked. */
133
152
  --shadow-key: var(--elev-rim), var(--elev-btn);
134
153
  /* The composed pair, for a surface that draws no border of its own. */
135
- --shadow-edge-card: var(--elev-hairline), var(--elev-card);
154
+ --shadow-edge-card: var(--elev-card); /* the card step carries its own hairline */
136
155
  --shadow-edge-raised: var(--elev-hairline), var(--elev-raised);
137
156
  --shadow-edge-overlay: var(--elev-hairline), var(--elev-overlay);
138
157
 
@@ -161,12 +180,12 @@
161
180
 
162
181
  .hover-row:has(:focus-visible) .row-action,
163
182
  .row-action:is(
164
- :focus-visible,
165
- :has(:focus-visible),
166
- [data-state="open"],
167
- [aria-expanded="true"],
168
- :has([data-state="open"], [aria-expanded="true"])
169
- ) {
183
+ :focus-visible,
184
+ :has(:focus-visible),
185
+ [data-state="open"],
186
+ [aria-expanded="true"],
187
+ :has([data-state="open"], [aria-expanded="true"])
188
+ ) {
170
189
  opacity: 1;
171
190
  }
172
191