@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
@@ -1,7 +1,10 @@
1
1
  import Root from "./skeleton.svelte";
2
+ import Delay from "./skeleton-delay.svelte";
2
3
 
3
4
  export {
4
5
  Root,
6
+ Delay,
5
7
  //
6
8
  Root as Skeleton,
9
+ Delay as SkeletonDelay,
7
10
  };
@@ -0,0 +1,63 @@
1
+ <script lang="ts" module>
2
+ /** DESIGN.md rule 20: a skeleton waits this long, so a fast answer never shows one. */
3
+ export const SHOW_DELAY_MS = 150;
4
+ /** DESIGN.md rule 20: once shown, a skeleton stays this long, so it never blinks. */
5
+ export const MIN_VISIBLE_MS = 300;
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import { untrack, type Snippet } from "svelte";
10
+
11
+ interface Props {
12
+ /** The data is on its way. */
13
+ loading: boolean;
14
+ /** What shows while it loads: one or more `Skeleton`s shaped like the content. */
15
+ skeleton: Snippet;
16
+ /** The content, drawn once loading ends and the skeleton has had its minimum time. */
17
+ children: Snippet;
18
+ /** Ms to wait before the skeleton shows. */
19
+ showDelay?: number;
20
+ /** Ms the skeleton stays once it shows. */
21
+ minVisible?: number;
22
+ }
23
+
24
+ let {
25
+ loading,
26
+ skeleton,
27
+ children,
28
+ showDelay = SHOW_DELAY_MS,
29
+ minVisible = MIN_VISIBLE_MS,
30
+ }: Props = $props();
31
+
32
+ // `wait` draws nothing: the first 150ms of a load, which a fast answer never leaves.
33
+ let phase = $state<"wait" | "skeleton" | "content">(untrack(() => (loading ? "wait" : "content")));
34
+ let shownAt = 0;
35
+
36
+ $effect(() => {
37
+ const isLoading = loading;
38
+ let timer: ReturnType<typeof setTimeout> | undefined;
39
+ untrack(() => {
40
+ if (isLoading) {
41
+ if (phase === "skeleton") return;
42
+ phase = "wait";
43
+ timer = setTimeout(() => {
44
+ phase = "skeleton";
45
+ shownAt = performance.now();
46
+ }, showDelay);
47
+ } else if (phase === "skeleton") {
48
+ const left = minVisible - (performance.now() - shownAt);
49
+ if (left > 0) timer = setTimeout(() => (phase = "content"), left);
50
+ else phase = "content";
51
+ } else {
52
+ phase = "content";
53
+ }
54
+ });
55
+ return () => clearTimeout(timer);
56
+ });
57
+ </script>
58
+
59
+ {#if phase === "skeleton"}
60
+ {@render skeleton()}
61
+ {:else if phase === "content"}
62
+ {@render children()}
63
+ {/if}
@@ -0,0 +1,57 @@
1
+ import { createRawSnippet, flushSync, mount, unmount } from "svelte";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
3
+ import SkeletonDelay from "./skeleton-delay.svelte";
4
+
5
+ const skeleton = createRawSnippet(() => ({ render: () => "<i>skeleton</i>" }));
6
+ const children = createRawSnippet(() => ({ render: () => "<b>content</b>" }));
7
+
8
+ function render(loading: boolean) {
9
+ const props = $state({ loading, skeleton, children });
10
+ const target = document.createElement("div");
11
+ const app = mount(SkeletonDelay, { target, props });
12
+ flushSync();
13
+ return {
14
+ shows: () => target.textContent,
15
+ set: (next: boolean) => {
16
+ props.loading = next;
17
+ flushSync();
18
+ },
19
+ wait: (ms: number) => {
20
+ vi.advanceTimersByTime(ms);
21
+ flushSync();
22
+ },
23
+ done: () => void unmount(app),
24
+ };
25
+ }
26
+
27
+ describe("SkeletonDelay", () => {
28
+ beforeEach(() => vi.useFakeTimers());
29
+ afterEach(() => vi.useRealTimers());
30
+
31
+ it("shows nothing for a fast answer, then the content", () => {
32
+ const view = render(true);
33
+ expect(view.shows()).toBe("");
34
+ view.wait(100);
35
+ view.set(false);
36
+ expect(view.shows()).toBe("content");
37
+ view.done();
38
+ });
39
+
40
+ it("shows the skeleton after 150ms and holds it 300ms", () => {
41
+ const view = render(true);
42
+ view.wait(150);
43
+ expect(view.shows()).toBe("skeleton");
44
+ view.wait(50);
45
+ view.set(false);
46
+ expect(view.shows()).toBe("skeleton");
47
+ view.wait(250);
48
+ expect(view.shows()).toBe("content");
49
+ view.done();
50
+ });
51
+
52
+ it("draws the content at once when nothing is loading", () => {
53
+ const view = render(false);
54
+ expect(view.shows()).toBe("content");
55
+ view.done();
56
+ });
57
+ });
@@ -12,6 +12,10 @@
12
12
  <div
13
13
  bind:this={ref}
14
14
  data-slot="skeleton"
15
- class={cn("bg-muted rounded-md animate-pulse", className)}
15
+ class={cn(
16
+ // A foreground wash on light, because `bg-muted` (L 0.94) vanished on the 0.945 canvas.
17
+ "bg-foreground/6 dark:bg-muted rounded-sm animate-pulse motion-reduce:animate-none",
18
+ className,
19
+ )}
16
20
  {...restProps}
17
21
  ></div>
@@ -45,7 +45,7 @@
45
45
  data-slot="slider-thumb"
46
46
  aria-label={thumbLabel}
47
47
  aria-valuetext={valueText}
48
- class="bg-background focus-visible:ring-ring/50 block size-5 cursor-grab rounded-full border-2 border-current shadow-[var(--elev-btn)] transition-[transform,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] outline-none hover:scale-110 focus-visible:ring-3 active:cursor-grabbing data-active:scale-110 data-active:shadow-[var(--elev-raised)] pointer-coarse:size-6 after:absolute after:top-1/2 after:left-1/2 after:size-8 after:-translate-x-1/2 after:-translate-y-1/2 any-pointer-coarse:after:size-11"
48
+ class="bg-card block size-5 cursor-grab rounded-full border-2 border-current shadow-[var(--elev-btn)] transition-[transform,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] outline-none hover:scale-110 focus-visible:shadow-(--ring-focus) active:cursor-grabbing data-active:scale-110 data-active:shadow-[var(--elev-raised)] pointer-coarse:size-6 after:absolute after:top-1/2 after:left-1/2 after:size-8 after:-translate-x-1/2 after:-translate-y-1/2 any-pointer-coarse:after:size-11"
49
49
  />
50
50
  {/each}
51
51
  {/snippet}
@@ -19,13 +19,13 @@
19
19
  data-slot="switch"
20
20
  data-size={size}
21
21
  class={cn(
22
- "data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent shadow-xs focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-[18px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-[background-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] outline-none after:absolute after:-inset-x-3 after:top-1/2 after:h-[max(100%,2rem)] after:-translate-y-1/2 any-pointer-coarse:after:h-[max(100%,2.75rem)] data-disabled:cursor-not-allowed data-disabled:opacity-50",
22
+ "data-checked:bg-primary data-unchecked:bg-input focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent aria-invalid:ring-3 data-[size=default]:h-[18px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-[background-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] outline-none after:absolute after:-inset-x-3 after:top-1/2 after:h-[max(100%,2rem)] after:-translate-y-1/2 any-pointer-coarse:after:h-[max(100%,2.75rem)] data-disabled:cursor-not-allowed data-disabled:opacity-50",
23
23
  className,
24
24
  )}
25
25
  {...restProps}
26
26
  >
27
27
  <SwitchPrimitive.Thumb
28
28
  data-slot="switch-thumb"
29
- class="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform duration-[var(--duration-medium)] ease-[var(--ease-smooth-out)] rtl:data-[state=checked]:translate-x-[calc(-100%)]"
29
+ class="bg-card dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform duration-[var(--duration-medium)] ease-[var(--ease-smooth-out)] rtl:data-[state=checked]:translate-x-[calc(-100%)]"
30
30
  />
31
31
  </SwitchPrimitive.Root>
@@ -14,7 +14,7 @@
14
14
  bind:this={ref}
15
15
  data-slot="table-row"
16
16
  class={cn(
17
- "data-[state=selected]:bg-muted transition-[background-color,transform,box-shadow] duration-[var(--duration-quick)] ease-out hover:bg-muted/20 hover:-translate-y-px hover:shadow-[0_1px_3px_rgba(0,0,0,0.04)] motion-reduce:hover:translate-y-0 motion-reduce:hover:shadow-none",
17
+ "data-[state=selected]:bg-muted transition-[background-color] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] hover:bg-muted/20",
18
18
  className,
19
19
  )}
20
20
  {...restProps}
@@ -29,7 +29,7 @@
29
29
  shows the trigger's own active background and never an empty pill. -->
30
30
  <span
31
31
  aria-hidden="true"
32
- class="bg-background dark:border-input dark:bg-input/30 pointer-events-none absolute inset-y-[3px] left-0 z-0 w-0 rounded-md border border-transparent opacity-0 shadow-sm transition-[transform,width,opacity] duration-[var(--duration-fast,250ms)] ease-[var(--ease-smooth-out,cubic-bezier(0.22,1,0.36,1))] will-change-[transform,width] motion-reduce:transition-opacity"
32
+ class="bg-card dark:bg-input/30 pointer-events-none absolute inset-y-[3px] left-0 z-0 w-0 rounded-[calc(var(--radius-lg)-3px)] border border-transparent opacity-0 shadow-btn transition-[transform,width,opacity] duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] will-change-transform motion-reduce:transition-opacity"
33
33
  {@attach slidingPill}
34
34
  ></span>
35
35
  {@render children?.()}
@@ -13,7 +13,8 @@
13
13
  bind:ref
14
14
  data-slot="tabs-trigger"
15
15
  class={cn(
16
- "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground group-data-[pill=on]/tabs-list:data-[state=active]:border-transparent group-data-[pill=on]/tabs-list:data-[state=active]:bg-transparent group-data-[pill=on]/tabs-list:data-[state=active]:shadow-none dark:group-data-[pill=on]/tabs-list:data-[state=active]:bg-transparent relative z-[1] inline-flex h-full flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 text-sm group-data-[size=sm]/tabs-list:text-[0.8rem] font-medium whitespace-nowrap after:absolute after:top-1/2 after:left-0 after:h-[max(100%,2rem)] after:w-full after:-translate-y-1/2 any-pointer-coarse:after:h-[max(100%,2.75rem)] transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
16
+ // Muted at rest, foreground on hover and when active, so each state reads stronger than rest.
17
+ "data-[state=active]:bg-card data-[state=active]:text-foreground hover:text-foreground focus-visible:shadow-(--ring-focus) focus-visible:data-[state=active]:shadow-(--ring-focus) dark:data-[state=active]:bg-input/30 text-muted-foreground group-data-[pill=on]/tabs-list:data-[state=active]:border-transparent group-data-[pill=on]/tabs-list:data-[state=active]:bg-transparent group-data-[pill=on]/tabs-list:data-[state=active]:shadow-none dark:group-data-[pill=on]/tabs-list:data-[state=active]:bg-transparent relative z-[1] inline-flex h-full flex-1 items-center justify-center gap-1.5 rounded-[calc(var(--radius-lg)-3px)] border border-transparent px-2 text-sm group-data-[size=sm]/tabs-list:text-[0.8rem] font-medium whitespace-nowrap after:absolute after:top-1/2 after:left-0 after:h-[max(100%,2rem)] after:w-full after:-translate-y-1/2 any-pointer-coarse:after:h-[max(100%,2.75rem)] transition-[color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-btn [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
17
18
  className,
18
19
  )}
19
20
  {...restProps}
@@ -15,7 +15,7 @@
15
15
  bind:this={ref}
16
16
  data-slot={dataSlot}
17
17
  class={cn(
18
- "border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border bg-transparent px-2.5 py-2 text-base shadow-field transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3 md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
18
+ "border-input dark:bg-input/30 enabled:hover:border-foreground/20 focus-visible:border-ring focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border bg-transparent px-2.5 py-2 text-base shadow-field transition-[color,border-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] aria-invalid:ring-3 md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
19
19
  className,
20
20
  )}
21
21
  bind:value
@@ -4,7 +4,7 @@ export {
4
4
  type ToggleSize,
5
5
  type ToggleVariant,
6
6
  type ToggleVariants,
7
- } from "./toggle.svelte";
7
+ } from "./toggle-variants.js";
8
8
 
9
9
  export {
10
10
  Root,
@@ -0,0 +1,26 @@
1
+ import { type VariantProps, tv } from "tailwind-variants";
2
+
3
+ export const toggleVariants = tv({
4
+ base: "relative after:absolute after:top-1/2 after:left-1/2 after:h-[max(100%,2rem)] after:w-[max(100%,2rem)] after:-translate-x-1/2 after:-translate-y-1/2 any-pointer-coarse:after:h-[max(100%,2.75rem)] any-pointer-coarse:after:w-[max(100%,2.75rem)] hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[background-color,color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
5
+ variants: {
6
+ variant: {
7
+ default: "bg-transparent",
8
+ outline:
9
+ "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-btn",
10
+ },
11
+ size: {
12
+ // Same steps as Button: sm 28, default 32, lg 36. 40 went with the scale.
13
+ default: "h-8 min-w-8 px-2",
14
+ sm: "h-7 min-w-7 px-1.5",
15
+ lg: "h-9 min-w-9 px-2.5",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ variant: "default",
20
+ size: "default",
21
+ },
22
+ });
23
+
24
+ export type ToggleVariant = VariantProps<typeof toggleVariants>["variant"];
25
+ export type ToggleSize = VariantProps<typeof toggleVariants>["size"];
26
+ export type ToggleVariants = VariantProps<typeof toggleVariants>;
@@ -1,30 +1,12 @@
1
1
  <script lang="ts" module>
2
- import { type VariantProps, tv } from "tailwind-variants";
2
+ import {
3
+ toggleVariants,
4
+ type ToggleSize,
5
+ type ToggleVariant,
6
+ type ToggleVariants,
7
+ } from "./toggle-variants.js";
3
8
 
4
- export const toggleVariants = tv({
5
- base: "relative after:absolute after:top-1/2 after:left-1/2 after:h-[max(100%,2rem)] after:w-[max(100%,2rem)] after:-translate-x-1/2 after:-translate-y-1/2 any-pointer-coarse:after:h-[max(100%,2.75rem)] any-pointer-coarse:after:w-[max(100%,2.75rem)] hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
6
- variants: {
7
- variant: {
8
- default: "bg-transparent",
9
- outline:
10
- "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-xs",
11
- },
12
- size: {
13
- // Same steps as Button: sm 28, default 32, lg 36. 40 went with the scale.
14
- default: "h-8 min-w-8 px-2",
15
- sm: "h-7 min-w-7 px-1.5",
16
- lg: "h-9 min-w-9 px-2.5",
17
- },
18
- },
19
- defaultVariants: {
20
- variant: "default",
21
- size: "default",
22
- },
23
- });
24
-
25
- export type ToggleVariant = VariantProps<typeof toggleVariants>["variant"];
26
- export type ToggleSize = VariantProps<typeof toggleVariants>["size"];
27
- export type ToggleVariants = VariantProps<typeof toggleVariants>;
9
+ export { toggleVariants, type ToggleSize, type ToggleVariant, type ToggleVariants };
28
10
  </script>
29
11
 
30
12
  <script lang="ts">
@@ -59,7 +59,7 @@ get along, so we shut typescript up by casting `value` to `never`.
59
59
  data-spacing={spacing}
60
60
  style={`--gap: ${spacing}`}
61
61
  class={cn(
62
- "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
62
+ "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-btn",
63
63
  className,
64
64
  )}
65
65
  {...restProps}
@@ -27,7 +27,7 @@
27
27
  {sideOffset}
28
28
  {side}
29
29
  class={cn(
30
- "t-dropdown z-50 w-fit max-w-xs rounded-lg border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-raised select-none outline-none",
30
+ "t-dropdown z-50 w-fit max-w-xs rounded-lg bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-edge-raised select-none outline-none",
31
31
  "has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm",
32
32
  className,
33
33
  )}
@@ -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,5 @@ export type {
9
9
  StatusMap,
10
10
  Statuses,
11
11
  } from "./status-types.js";
12
+ export { default as StatusPill } from "./status-pill.svelte";
13
+ export { PILL_TINT } from "./pill-tint.js";
@@ -0,0 +1,16 @@
1
+ import type { Tone } from "../lib/tone.js";
2
+
3
+ // A quiet tint and no border: the icon and the text carry the tone, the fill only groups
4
+ // them. Neutral reads off the foreground, not `bg-muted`, because muted sits 0.005 L from
5
+ // the canvas and vanishes there (tokens board, "what fights the new scale").
6
+ // It lives in a .ts module, not status-pill.svelte's module script, so a plain tsc consumer
7
+ // can import it: tsc's `*.svelte` shim only knows a default export.
8
+ export const PILL_TINT: Record<Tone, string> = {
9
+ neutral: "bg-foreground/6 text-muted-foreground",
10
+ accent:
11
+ "bg-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_12%,transparent)] text-[color:var(--tone-accent,var(--color-primary))]",
12
+ info: "bg-info/12 text-info-800 dark:text-info-400",
13
+ success: "bg-success/12 text-success-800 dark:text-success-400",
14
+ warning: "bg-warning/14 text-warning-800 dark:text-warning-400",
15
+ destructive: "bg-destructive/10 text-destructive-ink",
16
+ };
@@ -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,40 @@
1
+ <script lang="ts" module>
2
+ import type { Tone } from "../lib/tone.js";
3
+ import { PILL_TINT } from "./pill-tint.js";
4
+
5
+ export { PILL_TINT };
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import type { Snippet } from "svelte";
10
+ import type { HTMLAttributes } from "svelte/elements";
11
+ import { cn } from "../lib/utils.js";
12
+
13
+ interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "class" | "children"> {
14
+ tone?: Tone;
15
+ /** What sits before the label: a Phosphor icon, a `StatusIcon`, a dot or a spinner. */
16
+ icon?: Snippet;
17
+ children?: Snippet;
18
+ class?: string;
19
+ }
20
+
21
+ let { tone = "neutral", icon, children, class: className, ...restProps }: Props = $props();
22
+ </script>
23
+
24
+ <!-- A pill means status or a count (DESIGN.md rule 15), so this is the one rounded-full chip. -->
25
+ <span
26
+ data-slot="status-pill"
27
+ data-tone={tone}
28
+ class={cn(
29
+ "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",
30
+ "transition-[background-color,color] duration-quick ease-smooth-out motion-reduce:transition-none",
31
+ "[&>svg]:size-3 [&>svg]:shrink-0",
32
+ icon && "pl-1.5",
33
+ PILL_TINT[tone],
34
+ className,
35
+ )}
36
+ {...restProps}
37
+ >
38
+ {@render icon?.()}
39
+ <span class="min-w-0 truncate">{@render children?.()}</span>
40
+ </span>
@@ -0,0 +1,43 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { describe, expect, it } from "vite-plus/test";
4
+
5
+ // A plain tsc consumer (upsys app-cli) reads this package's src through a `*.svelte` shim that
6
+ // only knows a default export. A .ts file that re-exports a named value from a .svelte module
7
+ // fails there, as PILL_TINT did in 2.21.0. Named values live in a .ts module the component imports.
8
+ const root = import.meta.dirname;
9
+
10
+ function tsFiles(dir: string): string[] {
11
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
12
+ const path = join(dir, entry.name);
13
+ if (entry.isDirectory()) return tsFiles(path);
14
+ return entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") ? [path] : [];
15
+ });
16
+ }
17
+
18
+ const reexport =
19
+ /export\s+(type\s+)?(\*|\{[^}]*\})\s*(?:as\s+\w+\s*)?from\s*["']([^"']+\.svelte)["']/g;
20
+
21
+ describe("re-exports from .svelte modules", () => {
22
+ it("carry only the default export or types", () => {
23
+ const offenders: string[] = [];
24
+ for (const file of tsFiles(root)) {
25
+ for (const [, typeOnly, list, from] of readFileSync(file, "utf8").matchAll(reexport)) {
26
+ if (typeOnly) continue;
27
+ const names =
28
+ list === "*"
29
+ ? ["*"]
30
+ : list
31
+ .slice(1, -1)
32
+ .split(",")
33
+ .map((n) => n.trim());
34
+ const values = names.filter(
35
+ (n) => n && n !== "default" && !/^(type\s|default\s+as\s)/.test(n),
36
+ );
37
+ if (values.length)
38
+ offenders.push(`${relative(root, file)}: ${values.join(", ")} from ${from}`);
39
+ }
40
+ }
41
+ expect(offenders).toEqual([]);
42
+ });
43
+ });