@heyramzi/ui 2.20.0 → 2.21.1

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 (193) 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/alert/alert-variants.js +9 -0
  10. package/dist/shadcn/alert/index.js +1 -1
  11. package/dist/shadcn/badge/badge-variants.js +17 -0
  12. package/dist/shadcn/badge/index.js +1 -1
  13. package/dist/shadcn/dialog/index.js +3 -2
  14. package/dist/shadcn/dropdown-menu/menu-item.js +1 -1
  15. package/dist/shadcn/sheet/index.js +3 -2
  16. package/dist/shadcn/skeleton/index.js +3 -2
  17. package/dist/shadcn/toggle/index.js +1 -1
  18. package/dist/shadcn/toggle/toggle-variants.js +20 -0
  19. package/dist/status/index.js +2 -0
  20. package/dist/status/pill-tint.js +13 -0
  21. package/dist/work/kanban-drag.js +13 -7
  22. package/package.json +23 -23
  23. package/src/ai/confirmation/ConfirmationTitle.svelte +1 -4
  24. package/src/ai/loading-state/PixelLoader.svelte +1 -1
  25. package/src/animations/dropdown.css +19 -10
  26. package/src/animations/modal.css +7 -2
  27. package/src/charts/charts.css +3 -5
  28. package/src/charts/dashboard/dashboard-section.svelte +1 -1
  29. package/src/charts/dashboard/data-table.ts +1 -2
  30. package/src/charts/dashboard/stat-tile.svelte +1 -1
  31. package/src/charts/number-chart.css +8 -7
  32. package/src/charts/pop-value.svelte +68 -0
  33. package/src/charts/primitives/chart-tooltip.svelte +1 -1
  34. package/src/charts/reference-line.css +1 -1
  35. package/src/charts/stat-card.css +4 -4
  36. package/src/components/copy/copy-button.svelte +11 -10
  37. package/src/components/data-table/row-arrow.svelte +1 -1
  38. package/src/components/data-table/row-context-menu.svelte +1 -1
  39. package/src/components/data-table/sortable-head.svelte +2 -2
  40. package/src/components/data-table/table-skeleton.svelte +5 -5
  41. package/src/components/docs/doc-page.svelte +1 -3
  42. package/src/components/docs/on-this-page.svelte +4 -4
  43. package/src/components/field-row/field-row.svelte +1 -1
  44. package/src/components/list-page/detail-empty.svelte +3 -8
  45. package/src/components/list-page/detail-header.svelte +1 -1
  46. package/src/components/list-page/detail-row.svelte +2 -1
  47. package/src/components/list-page/empty-state.svelte +15 -7
  48. package/src/components/list-page/filter-item.svelte +2 -2
  49. package/src/components/list-page/filter-popover.svelte +1 -1
  50. package/src/components/list-page/filter-section.ts +1 -4
  51. package/src/components/list-page/grouped-list.svelte +37 -8
  52. package/src/components/list-page/list-page-shell.svelte +1 -1
  53. package/src/components/list-page/list-table.ts +6 -6
  54. package/src/components/list-page/master-detail.svelte +1 -1
  55. package/src/components/list-page/queue-rail.ts +2 -2
  56. package/src/components/list-page/rail-empty.svelte +1 -1
  57. package/src/components/list-page/rail-row.svelte +5 -5
  58. package/src/components/open-in/open-in.svelte +2 -1
  59. package/src/components/picker/trigger.ts +6 -5
  60. package/src/components/section-card/section-card.svelte +1 -1
  61. package/src/components/section-header/section-header.svelte +4 -2
  62. package/src/components/section-header/section-label.svelte +2 -2
  63. package/src/components/segmented/animated-tabs.svelte +1 -1
  64. package/src/components/segmented/section-runs.svelte +1 -1
  65. package/src/components/segmented/segment.svelte +4 -6
  66. package/src/components/segmented/segmented-track.svelte +18 -52
  67. package/src/components/segmented/segmented.ts +5 -5
  68. package/src/components/table-view/table-view.svelte +43 -24
  69. package/src/components/tags-input/tags-input.svelte +17 -31
  70. package/src/components/work-parts/looks.ts +1 -1
  71. package/src/editor/content.css +3 -3
  72. package/src/editor/editor.svelte +1 -1
  73. package/src/inbox/thread-inbox.svelte +2 -9
  74. package/src/panel/side-panel.svelte +9 -4
  75. package/src/record/info-list.svelte +7 -8
  76. package/src/record/project-list.svelte +1 -1
  77. package/src/record/record-demo.svelte +2 -4
  78. package/src/record/record-rail.svelte +5 -1
  79. package/src/record/record-shell.svelte +1 -1
  80. package/src/record/record-stats.svelte +9 -3
  81. package/src/record/steps-rail.svelte +7 -11
  82. package/src/shadcn/accordion/accordion-trigger.svelte +1 -1
  83. package/src/shadcn/alert/alert-variants.ts +17 -0
  84. package/src/shadcn/alert/alert.svelte +2 -16
  85. package/src/shadcn/alert/index.ts +1 -1
  86. package/src/shadcn/alert-dialog/alert-dialog-content.svelte +5 -1
  87. package/src/shadcn/alert-dialog/alert-dialog-footer.svelte +12 -2
  88. package/src/shadcn/alert-dialog/alert-dialog-header.svelte +1 -1
  89. package/src/shadcn/alert-dialog/alert-dialog-title.svelte +1 -1
  90. package/src/shadcn/badge/badge-variants.ts +23 -0
  91. package/src/shadcn/badge/badge.svelte +2 -20
  92. package/src/shadcn/badge/index.ts +1 -1
  93. package/src/shadcn/bubble/bubble-content.svelte +1 -1
  94. package/src/shadcn/bubble/bubble.svelte +1 -1
  95. package/src/shadcn/button/button.svelte +11 -5
  96. package/src/shadcn/calendar/calendar-month-select.svelte +1 -1
  97. package/src/shadcn/calendar/calendar-year-select.svelte +1 -1
  98. package/src/shadcn/calendar/calendar.svelte +1 -1
  99. package/src/shadcn/card/card.svelte +2 -1
  100. package/src/shadcn/checkbox/checkbox.svelte +1 -1
  101. package/src/shadcn/code-block/code-block-actions.svelte +1 -1
  102. package/src/shadcn/command/command-dialog.svelte +2 -2
  103. package/src/shadcn/command/command-group.svelte +1 -1
  104. package/src/shadcn/command/command-input.svelte +1 -1
  105. package/src/shadcn/command/command-item.svelte +1 -1
  106. package/src/shadcn/dialog/dialog-body.svelte +22 -0
  107. package/src/shadcn/dialog/dialog-content.svelte +19 -4
  108. package/src/shadcn/dialog/dialog-footer.svelte +19 -1
  109. package/src/shadcn/dialog/dialog-header.svelte +6 -1
  110. package/src/shadcn/dialog/dialog-title.svelte +2 -1
  111. package/src/shadcn/dialog/index.ts +3 -0
  112. package/src/shadcn/dropdown-menu/dropdown-menu-content.svelte +5 -1
  113. package/src/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  114. package/src/shadcn/dropdown-menu/menu-item.ts +1 -1
  115. package/src/shadcn/field/field-separator.svelte +1 -1
  116. package/src/shadcn/hover-card/hover-card-content.svelte +1 -1
  117. package/src/shadcn/input/input.svelte +2 -2
  118. package/src/shadcn/input-otp/input-otp-slot.svelte +1 -1
  119. package/src/shadcn/message-scroller/message-scroller-button.svelte +1 -1
  120. package/src/shadcn/popover/popover-content.svelte +1 -1
  121. package/src/shadcn/radio-group/radio-group-item.svelte +1 -1
  122. package/src/shadcn/scroll-area/scroll-area.svelte +1 -1
  123. package/src/shadcn/select/select-content.svelte +6 -2
  124. package/src/shadcn/select/select-item.svelte +1 -1
  125. package/src/shadcn/select/select-trigger.svelte +1 -1
  126. package/src/shadcn/sheet/index.ts +3 -0
  127. package/src/shadcn/sheet/sheet-body.svelte +22 -0
  128. package/src/shadcn/sheet/sheet-content.svelte +27 -5
  129. package/src/shadcn/sheet/sheet-footer.svelte +14 -2
  130. package/src/shadcn/sheet/sheet-header.svelte +1 -1
  131. package/src/shadcn/sheet/sheet-title.svelte +1 -1
  132. package/src/shadcn/sheet/sheet.css +11 -5
  133. package/src/shadcn/sidebar/sidebar-group-action.svelte +1 -1
  134. package/src/shadcn/sidebar/sidebar-group-label.svelte +1 -1
  135. package/src/shadcn/sidebar/sidebar-inset.svelte +1 -1
  136. package/src/shadcn/sidebar/sidebar-menu-action.svelte +1 -1
  137. package/src/shadcn/sidebar/sidebar-menu-button.svelte +2 -2
  138. package/src/shadcn/sidebar/sidebar-menu-sub-button.svelte +1 -1
  139. package/src/shadcn/sidebar/sidebar-menu.svelte +1 -1
  140. package/src/shadcn/sidebar/sidebar.svelte +1 -1
  141. package/src/shadcn/skeleton/index.ts +3 -0
  142. package/src/shadcn/skeleton/skeleton-delay.svelte +63 -0
  143. package/src/shadcn/skeleton/skeleton-delay.svelte.test.ts +57 -0
  144. package/src/shadcn/skeleton/skeleton.svelte +5 -1
  145. package/src/shadcn/slider/slider.svelte +1 -1
  146. package/src/shadcn/switch/switch.svelte +2 -2
  147. package/src/shadcn/table/table-row.svelte +1 -1
  148. package/src/shadcn/tabs/tabs-list.svelte +1 -1
  149. package/src/shadcn/tabs/tabs-trigger.svelte +2 -1
  150. package/src/shadcn/textarea/textarea.svelte +1 -1
  151. package/src/shadcn/toggle/index.ts +1 -1
  152. package/src/shadcn/toggle/toggle-variants.ts +26 -0
  153. package/src/shadcn/toggle/toggle.svelte +7 -25
  154. package/src/shadcn/toggle-group/toggle-group.svelte +1 -1
  155. package/src/shadcn/tooltip/tooltip-content.svelte +1 -1
  156. package/src/shadcn/trace/trace-collapsible-trigger.svelte +1 -1
  157. package/src/shadcn/trace/trace.svelte +1 -1
  158. package/src/status/README.md +1 -1
  159. package/src/status/index.ts +2 -0
  160. package/src/status/pill-tint.ts +16 -0
  161. package/src/status/status-badge.svelte +21 -7
  162. package/src/status/status-dot.svelte +6 -11
  163. package/src/status/status-pill.svelte +40 -0
  164. package/src/svelte-reexports.test.ts +43 -0
  165. package/src/thread/thread.svelte +54 -136
  166. package/src/tokens/craft.css +55 -36
  167. package/src/tokens/palette.css +16 -9
  168. package/src/work/demo.svelte +48 -42
  169. package/src/work/detail/detail-demo.svelte +33 -24
  170. package/src/work/detail/task-chip.svelte +4 -4
  171. package/src/work/detail/task-detail.svelte +41 -108
  172. package/src/work/detail/task-property.svelte +7 -13
  173. package/src/work/detail/task-thread.svelte +48 -119
  174. package/src/work/docs/doc-editor-page.svelte +5 -4
  175. package/src/work/docs/doc-menu.svelte +9 -4
  176. package/src/work/docs/docs-demo.svelte +4 -4
  177. package/src/work/docs/edited-chip.svelte +11 -4
  178. package/src/work/gantt/gantt-bar.svelte +18 -14
  179. package/src/work/gantt/gantt-grid.svelte +7 -7
  180. package/src/work/gantt/gantt-group-row.svelte +7 -10
  181. package/src/work/gantt/gantt-header.svelte +3 -1
  182. package/src/work/gantt/gantt-task-row.svelte +19 -9
  183. package/src/work/gantt/gantt.css +4 -5
  184. package/src/work/inbox/inbox-card.svelte +32 -10
  185. package/src/work/inbox/inbox-draft.svelte +2 -2
  186. package/src/work/kanban-column.svelte +7 -15
  187. package/src/work/kanban-drag.ts +13 -8
  188. package/src/work/task-gantt.svelte +2 -1
  189. package/src/work/task-list.svelte +54 -53
  190. package/src/work/time/time-chip.svelte +16 -19
  191. package/src/work/time/time-dot.svelte +2 -1
  192. package/src/work/time/time-ring.svelte +2 -3
  193. package/src/work/time/time-tracker.svelte +72 -166
@@ -72,6 +72,7 @@
72
72
  call site that forgot would open the row as well. -->
73
73
  <!-- svelte-ignore a11y_no_static_element_interactions -->
74
74
  <div
75
+ role="presentation"
75
76
  class="inline-flex shrink-0"
76
77
  onclick={(e) => e.stopPropagation()}
77
78
  onkeydown={(e) => e.stopPropagation()}
@@ -89,7 +90,7 @@
89
90
  ? "inline-flex shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
90
91
  : cn(
91
92
  "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
92
- size === "sm" ? "px-1.5 py-0.5 text-[11px]" : "px-2 py-1 text-xs",
93
+ size === "sm" ? "px-1.5 py-0.5 text-2xs" : "px-2 py-1 text-xs",
93
94
  ),
94
95
  variant === "icon" && (size === "sm" ? "size-5" : "size-6"),
95
96
  className,
@@ -14,10 +14,11 @@ import { tv } from "tailwind-variants";
14
14
  // box shows only under the pointer and on focus.
15
15
  export const pickerTriggerClass = tv({
16
16
  base: [
17
- // 13px at weight 500 is Linear's own property row, measured 31 Aug 2026 on a
18
- // 16px root. 14px reads looser than the reference, and it shows in the sidebar.
19
- "t-press inline-flex min-w-0 items-center gap-1.5 rounded-md text-[13px] font-medium whitespace-nowrap select-none",
20
- "hover:bg-muted data-[state=open]:bg-muted",
17
+ // `length:` stops tailwind-merge reading `text-control` as a colour (craft.css, Type).
18
+ "t-press inline-flex min-w-0 items-center gap-1.5 rounded-lg text-(length:--text-control) font-medium whitespace-nowrap select-none",
19
+ // The hover box is a foreground tint: property rows sit on the canvas too, where bg-muted
20
+ // is 0.005 L away and vanished. Radius is the control step, 10px.
21
+ "transition-colors duration-quick ease-smooth-out hover:bg-foreground/6 data-[state=open]:bg-foreground/6 motion-reduce:transition-none",
21
22
  "disabled:pointer-events-none disabled:opacity-50",
22
23
  "[&_svg]:pointer-events-none [&_svg]:shrink-0",
23
24
  ],
@@ -34,7 +35,7 @@ export const pickerTriggerClass = tv({
34
35
  */
35
36
  block: { true: "flex w-full", false: "w-fit" },
36
37
  /** No value yet: the label is a prompt, so it recedes until hovered. */
37
- empty: { true: "text-muted-foreground/70 hover:text-muted-foreground", false: "" },
38
+ empty: { true: "text-muted-foreground hover:text-foreground", false: "" },
38
39
  /** A row that carries no action still needs the column's box. */
39
40
  inert: { true: "hover:bg-transparent cursor-default", false: "" },
40
41
  },
@@ -32,7 +32,7 @@
32
32
  <!-- =========================== -->
33
33
  <Card.Root class={cn("gap-0 py-0", className)}>
34
34
  <header class="flex min-h-14 items-center justify-between gap-3 border-b px-4 py-3 sm:px-6">
35
- <Card.Title as="h3" class="min-w-0 text-base font-medium [overflow-wrap:anywhere]"
35
+ <Card.Title as="h3" class="min-w-0 text-base font-medium text-balance [overflow-wrap:anywhere]"
36
36
  >{title}</Card.Title
37
37
  >
38
38
  {#if actions}
@@ -37,7 +37,7 @@
37
37
  <SectionLabel class="min-w-0 [overflow-wrap:anywhere]">{title}</SectionLabel>
38
38
  {#if count !== undefined}
39
39
  <span
40
- class="shrink-0 rounded-md bg-muted px-1.5 py-0.5 text-xs leading-none font-medium tabular-nums text-muted-foreground"
40
+ class="shrink-0 rounded-full bg-foreground/6 px-1.5 py-0.5 text-xs leading-none font-medium tabular-nums text-muted-foreground"
41
41
  aria-label={`${count.toLocaleString()} ${countLabel}`}
42
42
  >
43
43
  {count.toLocaleString()}
@@ -45,7 +45,9 @@
45
45
  {/if}
46
46
  </svelte:element>
47
47
  {#if description}
48
- <p class="text-sm text-muted-foreground [overflow-wrap:anywhere]">{description}</p>
48
+ <p class="text-sm text-pretty text-muted-foreground [overflow-wrap:anywhere]">
49
+ {description}
50
+ </p>
49
51
  {/if}
50
52
  </div>
51
53
  {#if actions}
@@ -28,12 +28,12 @@
28
28
  <!-- =========================== -->
29
29
  <span
30
30
  class={cn(
31
- "flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground",
31
+ "flex items-center gap-2 text-xs font-semibold text-balance text-muted-foreground",
32
32
  className,
33
33
  )}
34
34
  >
35
35
  {#if Icon}
36
- <Icon class="size-4 text-brand" aria-hidden="true" />
36
+ <Icon class="size-4" weight="bold" aria-hidden="true" />
37
37
  {/if}
38
38
  {@render children()}
39
39
  </span>
@@ -157,7 +157,7 @@
157
157
  <span
158
158
  role="presentation"
159
159
  aria-hidden="true"
160
- class="flex shrink-0 items-center px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase"
160
+ class="flex shrink-0 items-center px-2 text-xs font-medium text-muted-foreground"
161
161
  >{tab.group.label}</span
162
162
  >
163
163
  {/if}
@@ -65,7 +65,7 @@
65
65
  {#each strip.runs as run, index (index)}
66
66
  <div class={cn("flex shrink-0 items-center gap-2", index === firstTitled && "ml-auto")}>
67
67
  {#if run.label}
68
- <span class="text-[10px] font-medium tracking-wider text-muted-foreground uppercase">
68
+ <span class="text-xs font-medium text-muted-foreground">
69
69
  {run.label}
70
70
  </span>
71
71
  {/if}
@@ -64,7 +64,7 @@
64
64
  src={image}
65
65
  {label}
66
66
  class={cn(
67
- "opacity-75 transition-opacity duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] group-hover/segment:opacity-100",
67
+ "opacity-75 transition-opacity duration-fast ease-smooth-out motion-reduce:transition-none group-hover/segment:opacity-100",
68
68
  on && "opacity-100",
69
69
  auto && "group-data-[state=active]/segment:opacity-100",
70
70
  )}
@@ -76,9 +76,7 @@
76
76
  {label}
77
77
 
78
78
  {#if badge}
79
- <span class="text-primary text-[10px] font-semibold tracking-wide whitespace-nowrap uppercase"
80
- >{badge}</span
81
- >
79
+ <span class="text-primary text-xs font-semibold whitespace-nowrap">{badge}</span>
82
80
  {/if}
83
81
 
84
82
  {#if count != null}
@@ -88,8 +86,8 @@
88
86
 
89
87
  // WHY min-width equals height with 4px padding: `min-w-5 px-1.5` made a digit
90
88
  // 20 by 18, so `rounded-full` drew an ellipse.
91
- "inline-flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] leading-none font-medium tabular-nums",
92
- "bg-muted-foreground/15 text-muted-foreground",
89
+ "inline-flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-xs leading-none font-medium tabular-nums",
90
+ "bg-foreground/8 text-muted-foreground",
93
91
  on && "bg-brand/14 text-brand",
94
92
  auto &&
95
93
  "group-data-[state=active]/segment:bg-brand/14 group-data-[state=active]/segment:text-brand",
@@ -16,7 +16,7 @@
16
16
  interface Props {
17
17
  /** Fill the row rather than sit at its natural width. */
18
18
  fill?: boolean;
19
- /** `pill`, a bordered well with a travelling highlight, or `underline`, a travelling rule. */
19
+ /** `pill`, a grey track with a white thumb that slides, or `underline`, a travelling rule. */
20
20
  variant?: SegmentedVariant;
21
21
  class?: string;
22
22
  children: Snippet;
@@ -64,45 +64,6 @@
64
64
  <!-- STYLES -->
65
65
  <!-- =========================== -->
66
66
  <style>
67
- /* Inset separators keep the strip one bordered object. */
68
- .track :global(.ui-segment)::before {
69
- content: "";
70
- position: absolute;
71
- top: 22%;
72
- bottom: 22%;
73
- left: 0;
74
- width: 1px;
75
- background-color: var(--color-border);
76
- transition: opacity var(--duration-fast) var(--ease-smooth-out);
77
- pointer-events: none;
78
- }
79
-
80
- .track :global(.pill + .ui-segment)::before,
81
- .track :global([data-slot="tabs-list"] > .ui-segment:first-child)::before {
82
- display: none;
83
- }
84
-
85
- /* The highlight and hover wash hide the seams on either side. */
86
- .track :global(.ui-segment[data-state="active"])::before,
87
- .track :global(.ui-segment[aria-current="page"])::before,
88
- .track :global(.ui-segment[aria-pressed="true"])::before,
89
- .track :global(.ui-segment[data-state="active"] + .ui-segment)::before,
90
- .track :global(.ui-segment[aria-current="page"] + .ui-segment)::before,
91
- .track :global(.ui-segment[aria-pressed="true"] + .ui-segment)::before,
92
- .track
93
- :global(
94
- [data-slot="tabs-list"]:has(> .ui-segment:last-child[data-state="active"]) + .ui-segment
95
- )::before {
96
- opacity: 0;
97
- }
98
-
99
- @media (hover: hover) and (pointer: fine) {
100
- .track :global(.ui-segment:hover)::before,
101
- .track :global(.ui-segment:hover + .ui-segment)::before {
102
- opacity: 0;
103
- }
104
- }
105
-
106
67
  /* The label's colour lands with the pill, on the same fast step. */
107
68
  .track :global(.ui-segment) {
108
69
  transition:
@@ -122,7 +83,7 @@
122
83
  [data-state="open"]
123
84
  )
124
85
  ) {
125
- background-color: color-mix(in oklch, var(--color-muted) 55%, transparent);
86
+ background-color: color-mix(in oklch, var(--color-foreground) 5%, transparent);
126
87
  }
127
88
  }
128
89
 
@@ -134,17 +95,18 @@
134
95
  z-index: 0;
135
96
  width: 0;
136
97
  opacity: 0;
137
- /* Concentric with the border and the 2px inset. */
138
- border-radius: calc(var(--radius-lg) - 3px);
139
- background-color: var(--color-muted);
140
- box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--color-foreground) 6%, transparent);
98
+ /* The white thumb on the grey track. Concentric: the track's control radius less its
99
+ 2px inset. The resting button shadow lifts it; no border, so no seam to fight. */
100
+ border-radius: calc(var(--radius-lg) - 2px);
101
+ background-color: var(--color-card);
102
+ box-shadow: var(--elev-btn), var(--elev-hairline);
141
103
  transform: translateX(0);
142
104
  /* transitions-dev `16-tabs-sliding`: the fast step, 250ms. */
143
105
  transition:
144
106
  transform var(--duration-fast) var(--ease-smooth-out),
145
107
  width var(--duration-fast) var(--ease-smooth-out),
146
108
  opacity var(--duration-quick) var(--ease-smooth-out);
147
- will-change: transform, width;
109
+ will-change: transform;
148
110
  pointer-events: none;
149
111
  }
150
112
 
@@ -152,16 +114,20 @@
152
114
  so a server-rendered strip is right before any JS arrives. */
153
115
  .track:not([data-pill="on"])
154
116
  :global(:is([data-state="active"], [aria-current="page"], [aria-pressed="true"])) {
155
- background-color: var(--color-muted);
156
- box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--color-foreground) 6%, transparent);
117
+ background-color: var(--color-card);
118
+ box-shadow: var(--elev-btn), var(--elev-hairline);
157
119
  }
158
120
 
159
- /* The underline: no seams, no wash, and the pill becomes a 2px rule on the
160
- hairline. `--segmented-rule` lets a brand colour it from a class. */
161
- .track[data-variant="underline"] :global(.ui-segment)::before {
162
- display: none;
121
+ /* On dark the card is the darker surface, so the thumb lifts toward the text instead. */
122
+ :global(.dark) .pill,
123
+ :global(.dark)
124
+ .track:not([data-pill="on"])
125
+ :global(:is([data-state="active"], [aria-current="page"], [aria-pressed="true"])) {
126
+ background-color: color-mix(in oklch, var(--color-foreground) 14%, var(--color-card));
163
127
  }
164
128
 
129
+ /* The underline: no wash, and the pill becomes a 2px rule on the
130
+ hairline. `--segmented-rule` lets a brand colour it from a class. */
165
131
  @media (hover: hover) {
166
132
  .track[data-variant="underline"]
167
133
  :global(
@@ -17,10 +17,10 @@ export const segmentedTrackClass = tv({
17
17
  // WHY no size variant: `sm` drew 24px, and every tab control must share one height.
18
18
  base: "h-7 any-pointer-coarse:h-11 shrink-0 items-stretch",
19
19
  variants: {
20
- // `pill`: a bordered well with a travelling highlight. `underline`: bare
20
+ // `pill`: grey track, sliding white thumb. `underline`: bare
21
21
  // labels over one hairline, the active one marked by a rule that travels.
22
22
  variant: {
23
- pill: "overflow-hidden rounded-lg border border-border p-0.5 any-pointer-coarse:border-0 any-pointer-coarse:px-[3px] any-pointer-coarse:py-0",
23
+ pill: "overflow-hidden rounded-lg bg-foreground/6 p-0.5",
24
24
  underline: "gap-5 border-b border-border",
25
25
  },
26
26
  // Fill the row rather than sit at its natural width. For a narrow rail.
@@ -46,16 +46,16 @@ export const segmentedTriggerClass = tv({
46
46
  "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
47
47
  // WHY an inset focus ring: the track clips to its rounded corners, so an
48
48
  // outside ring was cut in half on the end segments.
49
- "focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-inset focus-visible:outline-none",
49
+ "focus-visible:shadow-(--ring-focus-inset) focus-visible:outline-none",
50
50
  // WHY no height on the trigger: an icon makes a segment taller than a word.
51
51
  // `h-full` against the track's `items-stretch` keeps every segment the same box.
52
52
  "h-full text-xs",
53
53
  ],
54
54
  variants: {
55
- // The pill sits concentric in the track's 2px inset; the underline has no well, so
55
+ // The pill sits concentric in the track's 2px inset , so 8px; the underline has no well, so
56
56
  // no padding, and its rule is as wide as the label.
57
57
  variant: {
58
- pill: "rounded-[calc(var(--radius-lg)-3px)] px-2.5 any-pointer-coarse:px-3.5",
58
+ pill: "rounded-[calc(var(--radius-lg)-2px)] px-2.5 any-pointer-coarse:px-3.5",
59
59
  underline: "rounded-sm px-0",
60
60
  },
61
61
  // WHY `min-w-fit flex-auto`, not equal fractions: fractions cut "Approved" to
@@ -34,10 +34,12 @@
34
34
  /** Draws a cell its own way. Print `text` for the plain case. */
35
35
  cell?: Snippet<[TableCell<Row>]>;
36
36
  rowClass?: (row: Row) => string | undefined;
37
- /** Below this width the table scrolls sideways inside its frame. */
37
+ /**
38
+ * Below this width the table scrolls sideways inside its frame. Defaults to the columns'
39
+ * own widths (8rem a text column, 5rem a number), so a phone scrolls rather than splits words.
40
+ * Pass "0" for a table that should squeeze.
41
+ */
38
42
  minWidth?: string;
39
- /** Ignored: the panel or card under a table is its frame, so it draws none of its own. */
40
- bare?: boolean;
41
43
  class?: string;
42
44
  /** The sorted column and its direction. Bind it; columns with `sortable` set it. */
43
45
  sort?: SortState<Row>;
@@ -91,7 +93,7 @@
91
93
  footer,
92
94
  cell,
93
95
  rowClass,
94
- minWidth = "0",
96
+ minWidth,
95
97
  class: className,
96
98
  sort = $bindable(null),
97
99
  rowHref,
@@ -120,6 +122,17 @@
120
122
  // DERIVED
121
123
  // =========================
122
124
  let span = $derived(columns.length + (selectable ? 1 : 0));
125
+ // WHY a floor from the columns: with none, a 390px panel squeezed a text column until it broke
126
+ // "Handover" mid-word. A column hidden on small screens doesn't count, since it's gone there.
127
+ let floor = $derived.by(() => {
128
+ const parts = columns
129
+ .filter((c) => !c.hideBelow)
130
+ .map((c) =>
131
+ c.width && /^[\d.]+(rem|px)$/.test(c.width) ? c.width : c.numeric ? "5rem" : "8rem",
132
+ );
133
+ if (selectable) parts.push("2.5rem");
134
+ return parts.length ? `calc(${parts.join(" + ")})` : "0";
135
+ });
123
136
  let selectedCount = $derived(rows.filter((row) => selected.has(rowKey(row))).length);
124
137
  let allSelected = $derived(rows.length > 0 && selectedCount === rows.length);
125
138
  let someSelected = $derived(selectedCount > 0 && !allSelected);
@@ -230,12 +243,15 @@
230
243
  {#each columns as column (column.key)}
231
244
  <Table.Cell
232
245
  class={cn(
233
- "align-top",
234
- column.numeric ? "text-end tabular-nums" : "whitespace-normal [overflow-wrap:anywhere]",
246
+ "h-8 py-1.5 align-top any-pointer-coarse:h-11",
247
+ // WHY break-words, not overflow-wrap anywhere: anywhere shrinks a cell's min-content to one
248
+ // letter, so the table split words before it scrolled. break-words only splits a word
249
+ // longer than the whole column, a URL.
250
+ column.numeric ? "text-end tabular-nums" : "whitespace-normal break-words",
235
251
  column.muted && "text-muted-foreground",
236
252
  column.interactive && "relative z-10",
237
253
  column.hideBelow && HIDE[column.hideBelow],
238
- column.sticky && "bg-background sticky left-0 z-10",
254
+ column.sticky && "bg-card sticky left-0 z-10",
239
255
  column.class,
240
256
  )}
241
257
  >
@@ -280,9 +296,12 @@
280
296
  onclick={onRowClick ? (event: MouseEvent) => rowClick(event, row) : undefined}
281
297
  onkeydown={onRowClick ? (event: KeyboardEvent) => rowKeydown(event, row) : undefined}
282
298
  class={cn(
283
- href && "relative t-press active:bg-muted/50",
299
+ // Rows, not a card per row (DESIGN.md rule 19): a hairline under each and a flat
300
+ // hover tint. The atom's row lifts; this one does not.
301
+ "border-b border-border/70 last:border-b-0 hover:translate-y-0 hover:bg-foreground/3 hover:shadow-none",
302
+ href && "relative t-press active:bg-foreground/6",
284
303
  onRowClick &&
285
- "t-press active:bg-muted/50 focus-visible:ring-ring cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-inset",
304
+ "t-press active:bg-foreground/6 cursor-pointer outline-none focus-visible:shadow-(--ring-focus-inset)",
286
305
  rowClass?.(row),
287
306
  )}
288
307
  >
@@ -303,13 +322,13 @@
303
322
  {href}
304
323
  aria-label={linkName(row)}
305
324
  onclick={onRowLink ? (event: MouseEvent) => linkClick(event, row) : undefined}
306
- class="absolute inset-0 z-0 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
325
+ class="absolute inset-0 z-0 outline-none focus-visible:shadow-(--ring-focus-inset)"
307
326
  ></a>
308
327
  </td>
309
328
  {/if}
310
329
  </Table.Row>
311
330
  {#if subRow && expanded?.(row)}
312
- <Table.Row class="bg-muted/20 hover:bg-muted/20 hover:translate-y-0 hover:shadow-none">
331
+ <Table.Row class="bg-foreground/2 hover:bg-foreground/2 hover:translate-y-0 hover:shadow-none">
313
332
  <Table.Cell colspan={span + (rowHref ? 1 : 0)} class="whitespace-normal">
314
333
  {@render subRow(row)}
315
334
  </Table.Cell>
@@ -317,16 +336,16 @@
317
336
  {/if}
318
337
  {/snippet}
319
338
 
320
- <!-- WHY no frame: the surface under a table is its frame, the white panel or a section card. -->
339
+ <!-- WHY no frame: the surface under a table is its frame, the white panel or a section card (DESIGN.md, Patterns: tables). -->
321
340
  <div class={cn("min-w-0", className)}>
322
341
  <Table.Root
323
342
  aria-label={label}
324
343
  aria-busy={loading || undefined}
325
- style={`min-width: ${minWidth}`}
344
+ style={`min-width: ${minWidth ?? floor}`}
326
345
  class="min-w-0"
327
346
  >
328
- <Table.Header>
329
- <Table.Row>
347
+ <Table.Header class="bg-foreground/3">
348
+ <Table.Row class="hover:translate-y-0 hover:bg-transparent hover:shadow-none">
330
349
  {#if selectable}
331
350
  <Table.Head class="w-10">
332
351
  <Checkbox
@@ -343,9 +362,9 @@
343
362
  class={cn(
344
363
  "whitespace-normal",
345
364
  column.numeric && "text-end",
346
- column.sortable && "h-10 p-0",
365
+ column.sortable && "h-8 p-0",
347
366
  column.hideBelow && HIDE[column.hideBelow],
348
- column.sticky && "bg-background sticky left-0 z-20",
367
+ column.sticky && "bg-card sticky left-0 z-20",
349
368
  column.headClass ?? column.class,
350
369
  )}
351
370
  style={column.width ? `width: ${column.width}` : undefined}
@@ -356,7 +375,7 @@
356
375
  <button
357
376
  type="button"
358
377
  class={cn(
359
- "t-press hover:bg-muted/30 focus-visible:ring-ring flex h-10 w-full items-center gap-1 px-2 text-xs font-medium outline-none focus-visible:ring-2 focus-visible:ring-inset",
378
+ "t-press transition-colors duration-quick ease-smooth-out hover:bg-foreground/4 flex h-8 w-full items-center gap-1 px-2 text-xs font-medium outline-none focus-visible:shadow-(--ring-focus-inset)",
360
379
  column.numeric ? "justify-end" : "justify-start",
361
380
  )}
362
381
  onclick={() => (sort = toggleSort(sort, column.key as keyof Row & string))}
@@ -367,7 +386,7 @@
367
386
  {:else if dir === "desc"}
368
387
  <CaretDown class="text-foreground size-3.5" weight="bold" aria-hidden="true" />
369
388
  {:else}
370
- <CaretUpDown class="text-muted-foreground/75 size-3.5" aria-hidden="true" />
389
+ <CaretUpDown class="text-muted-foreground size-3.5" aria-hidden="true" />
371
390
  {/if}
372
391
  </button>
373
392
  {:else if head}
@@ -392,23 +411,23 @@
392
411
  <Table.Body>
393
412
  {#if entry.key !== null}
394
413
  <Table.Row
395
- class="bg-muted/40 hover:bg-muted/40 hover:translate-y-0 hover:shadow-none"
414
+ class="bg-foreground/4 hover:translate-y-0 hover:bg-foreground/4 hover:shadow-none"
396
415
  >
397
416
  <Table.Head
398
417
  colspan={span + (rowHref ? 1 : 0)}
399
418
  scope="colgroup"
400
- class="text-foreground h-10 p-0"
419
+ class="text-foreground h-8 p-0"
401
420
  >
402
421
  {#if collapsible}
403
422
  <button
404
423
  type="button"
405
424
  aria-expanded={!folded}
406
- class="t-press focus-visible:ring-ring flex h-10 w-full items-center gap-2 px-2 text-start outline-none focus-visible:ring-2 focus-visible:ring-inset"
425
+ class="t-press flex h-8 w-full items-center gap-2 px-2 text-start outline-none focus-visible:shadow-(--ring-focus-inset)"
407
426
  onclick={() => toggleGroup(tableGroup.key)}
408
427
  >
409
428
  <CaretDown
410
429
  class={cn(
411
- "size-3.5 shrink-0 transition-transform duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] motion-reduce:transition-none",
430
+ "size-3.5 shrink-0 transition-transform duration-fast ease-smooth-out motion-reduce:transition-none",
412
431
  folded && "-rotate-90",
413
432
  )}
414
433
  aria-hidden="true"
@@ -416,7 +435,7 @@
416
435
  {#if groupHeader}{@render groupHeader(tableGroup)}{:else}{tableGroup.key}{/if}
417
436
  </button>
418
437
  {:else}
419
- <span class="flex h-10 items-center px-2">
438
+ <span class="flex h-8 items-center px-2">
420
439
  {#if groupHeader}{@render groupHeader(tableGroup)}{:else}{tableGroup.key}{/if}
421
440
  </span>
422
441
  {/if}
@@ -2,8 +2,9 @@
2
2
  // =========================
3
3
  // IMPORTS
4
4
  // =========================
5
- import { Plus, X } from "phosphor-svelte";
5
+ import { Plus } from "phosphor-svelte";
6
6
  import { cn } from "../../lib/utils.js";
7
+ import TagChip from "../chip/tag-chip.svelte";
7
8
  import {
8
9
  addTag,
9
10
  canCreate,
@@ -105,9 +106,7 @@
105
106
  ...(creatable ? [{ kind: "create" as const, name: query.trim() }] : []),
106
107
  ],
107
108
  );
108
- const nothing = $derived(
109
- !free && editing && !full && query.trim() !== "" && rows.length === 0,
110
- );
109
+ const nothing = $derived(!free && editing && !full && query.trim() !== "" && rows.length === 0);
111
110
  const open = $derived(editing && !full && (rows.length > 0 || nothing));
112
111
 
113
112
  // =========================
@@ -129,8 +128,7 @@
129
128
  queueMicrotask(() => input?.focus());
130
129
  }
131
130
 
132
- // Leaving the field keeps what was typed only when it is an existing label said another way.
133
- // Anything else is dropped unless the field is free: free text is never saved by walking away.
131
+ // Blur keeps typed text only when it's an existing label spelt another way, or the field is free.
134
132
  function stop() {
135
133
  const typed = query;
136
134
  query = "";
@@ -219,31 +217,19 @@
219
217
 
220
218
  <!-- =========================== -->
221
219
  <!-- MARKUP -->
222
- <!-- WHY: the edit frame is --radius-lg with 4px of padding, so the chip inside is --radius-xs
223
- (8 - 4), and a read-state chip on its own takes --radius-sm. -->
220
+ <!-- WHY: the chips are TagChip at --radius-sm (6px); the edit frame is --radius-lg (10px), which
221
+ is 6 plus its 4px of padding, so the curves stay concentric. -->
224
222
  <!-- WHY text-base on the input: below 16px iOS zooms the page when it gets focus. -->
225
223
  <!-- =========================== -->
226
224
  {#snippet chip(tag: string, removable: boolean)}
227
- <span
228
- class={cn(
229
- "inline-flex h-5 max-w-full items-center gap-0.5 bg-muted px-1.5 text-xs font-medium text-foreground",
230
- removable ? "rounded-(--radius-xs) pr-0.5" : "rounded-(--radius-sm)",
231
- )}
225
+ <TagChip
226
+ title={tag}
227
+ removeLabel={text.remove(tag)}
228
+ removeFocusable={false}
229
+ onremove={removable ? () => commit(removeTag(tags, tag)) : undefined}
232
230
  >
233
- <span class="truncate" title={tag}>{tag}</span>
234
- {#if removable}
235
- <button
236
- type="button"
237
- tabindex="-1"
238
- class="relative grid size-4 shrink-0 place-items-center rounded-(--radius-xs) text-muted-foreground transition-colors hover:bg-foreground/10 hover:text-foreground before:absolute before:-inset-1.5 before:content-['']"
239
- aria-label={text.remove(tag)}
240
- onmousedown={(e) => e.preventDefault()}
241
- onclick={() => commit(removeTag(tags, tag))}
242
- >
243
- <X class="size-3" weight="bold" aria-hidden="true" />
244
- </button>
245
- {/if}
246
- </span>
231
+ {tag}
232
+ </TagChip>
247
233
  {/snippet}
248
234
 
249
235
  <div
@@ -254,7 +240,7 @@
254
240
  >
255
241
  {#if editing}
256
242
  <div
257
- class="-ml-1.5 flex min-h-7 flex-wrap items-center gap-1 rounded-lg bg-background p-1 ring-1 ring-ring/60 focus-within:ring-2 focus-within:ring-ring"
243
+ class="-ml-1.5 flex min-h-7 flex-wrap items-center gap-1 rounded-lg bg-card p-1 ring-1 ring-ring/60 focus-within:ring-2 focus-within:ring-ring"
258
244
  >
259
245
  {#each tags as tag (tag.toLowerCase())}
260
246
  {@render chip(tag, true)}
@@ -288,7 +274,7 @@
288
274
  id={listId}
289
275
  role="listbox"
290
276
  aria-label={label}
291
- class="absolute top-full left-0 z-50 mt-1 max-h-60 w-full min-w-48 overflow-y-auto rounded-xl border border-border bg-popover p-1 text-popover-foreground shadow-(--elev-overlay)"
277
+ class="absolute top-full left-0 z-50 mt-1 max-h-60 w-full min-w-48 overflow-y-auto rounded-xl bg-popover p-1 text-popover-foreground shadow-edge-raised"
292
278
  >
293
279
  {#each rows as row, index (row.kind + row.name)}
294
280
  <!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -298,7 +284,7 @@
298
284
  aria-selected={index === active}
299
285
  class={cn(
300
286
  "flex min-h-8 cursor-default items-center gap-2 rounded-lg px-2 text-sm any-pointer-coarse:min-h-11",
301
- index === active && "bg-accent text-accent-foreground",
287
+ index === active && "bg-foreground/6 text-foreground",
302
288
  )}
303
289
  onmousedown={(e) => e.preventDefault()}
304
290
  onmousemove={() => (active = index)}
@@ -321,7 +307,7 @@
321
307
  <button
322
308
  type="button"
323
309
  class={cn(
324
- "-ml-1.5 flex min-h-7 w-full flex-wrap items-center gap-1 rounded-lg px-1.5 py-1 text-left transition-colors outline-none hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none",
310
+ "-ml-1.5 flex min-h-7 w-full flex-wrap items-center gap-1 rounded-lg px-1.5 py-1 text-left transition-colors outline-none duration-quick ease-smooth-out hover:bg-foreground/4 focus-visible:shadow-(--ring-focus) disabled:pointer-events-none motion-reduce:transition-none",
325
311
  )}
326
312
  aria-label={label}
327
313
  {disabled}
@@ -31,7 +31,7 @@ export const ganttLooks = {
31
31
  bar: "rounded-lg",
32
32
  mark: "rounded-sm",
33
33
  /** The readout that follows a dragged bar. */
34
- tip: "rounded-sm bg-foreground px-2 py-1 text-[11px] font-medium whitespace-nowrap text-background tabular-nums shadow-raised",
34
+ tip: "rounded-sm bg-foreground px-2 py-1 text-2xs font-medium whitespace-nowrap text-background tabular-nums shadow-raised",
35
35
  /** A title that didn't fit its bar, set beside it. */
36
36
  outsideLabel: "rounded-sm bg-card/90 px-1 text-xs text-muted-foreground",
37
37
  track: "rounded-full",
@@ -3,8 +3,8 @@
3
3
  consuming Svelte components. */
4
4
 
5
5
  .tiptap-content {
6
- line-height: 1.75;
7
- font-size: 1rem;
6
+ line-height: var(--content-leading, 1.75);
7
+ font-size: var(--content-size, 1rem);
8
8
  color: var(--color-foreground);
9
9
  }
10
10
 
@@ -63,7 +63,7 @@
63
63
  }
64
64
 
65
65
  .tiptap-content p {
66
- margin-bottom: 1rem;
66
+ margin-bottom: var(--content-gap, 1rem);
67
67
  text-wrap: pretty;
68
68
  overflow-wrap: break-word;
69
69
  }
@@ -379,7 +379,7 @@
379
379
  .editor-wrapper {
380
380
  position: relative;
381
381
  border-radius: var(--radius-xl);
382
- background: var(--color-background);
382
+ background: var(--color-card);
383
383
  border: 1px solid var(--color-border);
384
384
  transition:
385
385
  border-color var(--duration-quick) var(--ease-smooth-out),
@@ -29,7 +29,7 @@
29
29
  import { Button } from "../shadcn/button/index.js";
30
30
  import { Kbd, KbdGroup } from "../shadcn/kbd/index.js";
31
31
  import { cn } from "../lib/utils.js";
32
- import { TONE_CLASS } from "../lib/tone.js";
32
+ import StatusPill from "../status/status-pill.svelte";
33
33
  import {
34
34
  Thread,
35
35
  type ThreadComment,
@@ -414,14 +414,7 @@
414
414
  {#if thread.subject.snoozedUntil}
415
415
  <BellSlash class="size-3.5 text-muted-foreground" aria-label={labels.snoozed} />
416
416
  {:else if thread.pending.length > 0}
417
- <span
418
- class={cn(
419
- "inline-flex h-5 items-center rounded-sm border px-1.5 font-medium",
420
- TONE_CLASS.warning,
421
- )}
422
- >
423
- {labels.needsYou}
424
- </span>
417
+ <StatusPill tone="warning">{labels.needsYou}</StatusPill>
425
418
  {:else if thread.unread > 0}
426
419
  <Circle
427
420
  weight="fill"