@heyramzi/ui 3.1.0 → 3.3.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 (172) hide show
  1. package/dist/components/list-page/toolbar-density.js +20 -1
  2. package/dist/lib/sliding-pill.js +2 -2
  3. package/dist/record/measure-clamp.js +23 -0
  4. package/dist/shadcn/button-group/index.js +6 -0
  5. package/dist/shadcn/checkbox/checkbox-box.js +3 -0
  6. package/dist/shadcn/drawer/index.js +14 -0
  7. package/dist/shadcn/input-group/index.js +9 -0
  8. package/dist/shadcn/item/index.js +13 -0
  9. package/dist/shadcn/native-select/index.js +6 -0
  10. package/dist/shadcn/pagination/index.js +12 -0
  11. package/package.json +7 -2
  12. package/scripts/sync-shadcn.mjs +0 -6
  13. package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
  14. package/src/canvas/pen/PenOverlay.svelte +1 -30
  15. package/src/canvas/screen/PartView.svelte +13 -8
  16. package/src/canvas/screen/ScreenBody.svelte +18 -11
  17. package/src/canvas/ui/BarTooltip.svelte +46 -0
  18. package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
  19. package/src/canvas/ui/CanvasToolbar.svelte +170 -156
  20. package/src/canvas/ui/StylePanel.svelte +179 -102
  21. package/src/canvas/ui/canvas-ui.css +12 -76
  22. package/src/charts/dashboard/data-table.svelte +80 -80
  23. package/src/charts/dashboard/hint.svelte +34 -0
  24. package/src/charts/dashboard/hint.svelte.d.ts +10 -0
  25. package/src/charts/dashboard/rating-chip.svelte +4 -3
  26. package/src/charts/legend.css +0 -8
  27. package/src/charts/legend.svelte +26 -30
  28. package/src/components/copy/copy-button.svelte +10 -2
  29. package/src/components/copy/copy-button.test.ts +7 -0
  30. package/src/components/data-table/table-skeleton.svelte +15 -7
  31. package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
  32. package/src/components/docs/on-this-page.svelte +2 -2
  33. package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
  34. package/src/components/list-page/filter-item.svelte +20 -7
  35. package/src/components/list-page/filter-popover.svelte +57 -17
  36. package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
  37. package/src/components/list-page/grouped-list.svelte +41 -23
  38. package/src/components/list-page/list-page-shell.svelte +155 -69
  39. package/src/components/list-page/list-page-shell.svelte.d.ts +6 -1
  40. package/src/components/list-page/toolbar-density.ts +24 -1
  41. package/src/components/picker/picker.svelte +8 -3
  42. package/src/components/picker/picker.svelte.d.ts +2 -1
  43. package/src/components/segmented/segmented-filter.svelte +19 -10
  44. package/src/components/segmented/segmented-track.svelte +11 -3
  45. package/src/components/table-view/table-view.svelte +22 -6
  46. package/src/components/tags-input/tags-input.svelte +94 -77
  47. package/src/components/toolbar/toolbar.svelte +2 -1
  48. package/src/components/toolbar/toolbar.test.ts +5 -4
  49. package/src/components/work-parts/group-head.svelte +28 -9
  50. package/src/components/work-parts/group-head.svelte.d.ts +5 -2
  51. package/src/editor/editor-toolbar.svelte +223 -193
  52. package/src/editor/editor.svelte +56 -38
  53. package/src/editor/image-toolbar.svelte +99 -37
  54. package/src/editor/image-toolbar.svelte.d.ts +4 -5
  55. package/src/editor/url-dialog.svelte +96 -0
  56. package/src/editor/url-dialog.svelte.d.ts +15 -0
  57. package/src/effects/theme-toggle/ThemeToggle.css +8 -8
  58. package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
  59. package/src/inbox/thread-inbox.svelte +9 -1
  60. package/src/lib/live-region.svelte +12 -4
  61. package/src/lib/live-region.svelte.d.ts +5 -1
  62. package/src/lib/sliding-pill.ts +3 -2
  63. package/src/record/info-list.svelte +33 -59
  64. package/src/record/measure-clamp.ts +31 -0
  65. package/src/record/record-shell.svelte +38 -39
  66. package/src/record/record-timeline.svelte +10 -5
  67. package/src/record/steps-rail.svelte +50 -36
  68. package/src/record/where-we-are.svelte +5 -19
  69. package/src/shadcn/button-group/button-group-separator.svelte +23 -0
  70. package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
  71. package/src/shadcn/button-group/button-group-text.svelte +28 -0
  72. package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
  73. package/src/shadcn/button-group/button-group.svelte +46 -0
  74. package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
  75. package/src/shadcn/button-group/index.ts +15 -0
  76. package/src/shadcn/checkbox/checkbox-box.ts +4 -0
  77. package/src/shadcn/checkbox/checkbox.svelte +2 -4
  78. package/src/shadcn/drawer/drawer-close.svelte +7 -0
  79. package/src/shadcn/drawer/drawer-close.svelte.d.ts +4 -0
  80. package/src/shadcn/drawer/drawer-content.svelte +36 -0
  81. package/src/shadcn/drawer/drawer-content.svelte.d.ts +10 -0
  82. package/src/shadcn/drawer/drawer-description.svelte +17 -0
  83. package/src/shadcn/drawer/drawer-description.svelte.d.ts +4 -0
  84. package/src/shadcn/drawer/drawer-footer.svelte +20 -0
  85. package/src/shadcn/drawer/drawer-footer.svelte.d.ts +5 -0
  86. package/src/shadcn/drawer/drawer-header.svelte +23 -0
  87. package/src/shadcn/drawer/drawer-header.svelte.d.ts +5 -0
  88. package/src/shadcn/drawer/drawer-nested.svelte +12 -0
  89. package/src/shadcn/drawer/drawer-nested.svelte.d.ts +3 -0
  90. package/src/shadcn/drawer/drawer-overlay.svelte +20 -0
  91. package/src/shadcn/drawer/drawer-overlay.svelte.d.ts +4 -0
  92. package/src/shadcn/drawer/drawer-portal.svelte +7 -0
  93. package/src/shadcn/drawer/drawer-portal.svelte.d.ts +3 -0
  94. package/src/shadcn/drawer/drawer-title.svelte +17 -0
  95. package/src/shadcn/drawer/drawer-title.svelte.d.ts +4 -0
  96. package/src/shadcn/drawer/drawer-trigger.svelte +7 -0
  97. package/src/shadcn/drawer/drawer-trigger.svelte.d.ts +4 -0
  98. package/src/shadcn/drawer/drawer.svelte +12 -0
  99. package/src/shadcn/drawer/drawer.svelte.d.ts +3 -0
  100. package/src/shadcn/drawer/index.ts +38 -0
  101. package/src/shadcn/input-group/index.ts +22 -0
  102. package/src/shadcn/input-group/input-group-addon.svelte +51 -0
  103. package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
  104. package/src/shadcn/input-group/input-group-button.svelte +49 -0
  105. package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
  106. package/src/shadcn/input-group/input-group-input.svelte +20 -0
  107. package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
  108. package/src/shadcn/input-group/input-group-text.svelte +19 -0
  109. package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
  110. package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
  111. package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
  112. package/src/shadcn/input-group/input-group.svelte +24 -0
  113. package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
  114. package/src/shadcn/item/index.ts +34 -0
  115. package/src/shadcn/item/item-actions.svelte +20 -0
  116. package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
  117. package/src/shadcn/item/item-content.svelte +23 -0
  118. package/src/shadcn/item/item-content.svelte.d.ts +5 -0
  119. package/src/shadcn/item/item-description.svelte +23 -0
  120. package/src/shadcn/item/item-description.svelte.d.ts +5 -0
  121. package/src/shadcn/item/item-footer.svelte +20 -0
  122. package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
  123. package/src/shadcn/item/item-group.svelte +21 -0
  124. package/src/shadcn/item/item-group.svelte.d.ts +5 -0
  125. package/src/shadcn/item/item-header.svelte +20 -0
  126. package/src/shadcn/item/item-header.svelte.d.ts +5 -0
  127. package/src/shadcn/item/item-media.svelte +42 -0
  128. package/src/shadcn/item/item-media.svelte.d.ts +17 -0
  129. package/src/shadcn/item/item-separator.svelte +19 -0
  130. package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
  131. package/src/shadcn/item/item-title.svelte +20 -0
  132. package/src/shadcn/item/item-title.svelte.d.ts +5 -0
  133. package/src/shadcn/item/item.svelte +61 -0
  134. package/src/shadcn/item/item.svelte.d.ts +28 -0
  135. package/src/shadcn/native-select/index.ts +13 -0
  136. package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
  137. package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
  138. package/src/shadcn/native-select/native-select-option.svelte +20 -0
  139. package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
  140. package/src/shadcn/native-select/native-select.svelte +45 -0
  141. package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
  142. package/src/shadcn/pagination/index.ts +31 -0
  143. package/src/shadcn/pagination/pagination-content.svelte +20 -0
  144. package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
  145. package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
  146. package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
  147. package/src/shadcn/pagination/pagination-item.svelte +14 -0
  148. package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
  149. package/src/shadcn/pagination/pagination-link.svelte +41 -0
  150. package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
  151. package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
  152. package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
  153. package/src/shadcn/pagination/pagination-next.svelte +26 -0
  154. package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
  155. package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
  156. package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
  157. package/src/shadcn/pagination/pagination-previous.svelte +30 -0
  158. package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
  159. package/src/shadcn/pagination/pagination.svelte +27 -0
  160. package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
  161. package/src/slides/Controls.svelte +39 -36
  162. package/src/slides/Deck.svelte +11 -8
  163. package/src/thread/thread.svelte +15 -8
  164. package/src/work/detail/task-detail.svelte +1 -1
  165. package/src/work/gantt/gantt-task-row.svelte +3 -6
  166. package/src/work/inbox/inbox-card.svelte +7 -5
  167. package/src/work/inbox/inbox-trace.svelte +14 -10
  168. package/src/work/kanban-board.svelte +40 -25
  169. package/src/work/kanban-hidden-rail.svelte +67 -65
  170. package/src/work/task-list.svelte +145 -124
  171. package/src/work/task-list.svelte.test.ts +5 -2
  172. package/src/work/time/time-tracker.svelte +35 -10
@@ -23,11 +23,13 @@
23
23
  // IMPORTS
24
24
  // =========================
25
25
  import { Panel, useStore } from "@xyflow/svelte";
26
+ import { Toolbar } from "bits-ui";
26
27
  import type { CanvasOp } from "../ops/ops.js";
27
28
  import {
28
29
  FONT_SIZES,
29
30
  styleOps,
30
31
  summarizeStyle,
32
+ type FillValue,
31
33
  type FontWeight,
32
34
  type StyleNode,
33
35
  type StyleValues,
@@ -35,8 +37,10 @@
35
37
  } from "../editor/style.js";
36
38
  import { PEN_COLORS } from "../pen/types.js";
37
39
  import "../pen/pen.css";
40
+ import BarTooltip from "./BarTooltip.svelte";
38
41
  import { placeBeside, type Box } from "./place-panel.js";
39
42
  import "./canvas-ui.css";
43
+ import * as Tooltip from "../../shadcn/tooltip/index.js";
40
44
 
41
45
  // =========================
42
46
  // TYPES
@@ -70,6 +74,8 @@
70
74
  const LEVELS: readonly WidthLevel[] = [0, 1, 2];
71
75
 
72
76
  let summary = $derived(summarizeStyle(nodes, frameType));
77
+ // Roving focus remembers its tab stop by element, so a panel whose sections change is rebuilt fresh.
78
+ let controlsKey = $derived(summary ? [...summary.controls].join() : "");
73
79
  let strokeLabel = $derived(
74
80
  summary?.controls.has("fill") || summary?.controls.has("width") ? labels.stroke : labels.colour,
75
81
  );
@@ -118,12 +124,24 @@
118
124
  const ops = styleOps(nodes, next, frameType);
119
125
  if (ops.length > 0) commit(ops);
120
126
  }
127
+
128
+ // A single-select group sends "" when its pressed item is pressed again. A style stays set.
129
+ // Each group's values are the item values written below, so a value maps back to its own type.
130
+ function pick(key: keyof StyleValues, value: string | undefined) {
131
+ if (!value) return;
132
+ if (key === "fill") change({ fill: value as FillValue });
133
+ else if (key === "stroke") change({ stroke: value as PenColor });
134
+ else if (key === "width") change({ width: Number(value) as WidthLevel });
135
+ else if (key === "fontSize") change({ fontSize: Number(value) });
136
+ else change({ fontWeight: value as FontWeight });
137
+ }
121
138
  </script>
122
139
 
123
140
  <!-- =========================== -->
124
141
  <!-- MARKUP -->
125
142
  <!-- =========================== -->
126
143
  {#if summary}
144
+ {@const { values, controls } = summary}
127
145
  <Panel
128
146
  position="center-right"
129
147
  class="cv-dock cv-style-dock {placed ? 'is-placed' : ''}"
@@ -132,111 +150,165 @@
132
150
  : undefined}
133
151
  data-side={placed?.side}
134
152
  >
135
- <div
136
- bind:offsetWidth={barWidth}
137
- bind:offsetHeight={barHeight}
138
- class="cv-bar cv-side cv-style t-enter"
139
- role="toolbar"
140
- aria-label={labels.panel}
141
- data-testid="style-panel"
142
- >
143
- {#if summary.controls.has("fill")}
144
- <div class="cv-section" role="group" aria-label={labels.fill} data-testid="style-fill">
145
- <span class="cv-label" aria-hidden="true">{labels.fill}</span>
146
- <div class="cv-swatches">
147
- <button
148
- type="button"
149
- class="cv-swatch cv-swatch-none"
150
- aria-pressed={summary.values.fill === "none"}
151
- aria-label="{labels.fill}: {labels.none}"
152
- data-tip={labels.none}
153
- onclick={() => change({ fill: "none" })}
154
- ></button>
155
- {#each PEN_COLORS as swatch (swatch)}
156
- <button
157
- type="button"
158
- class="cv-swatch"
159
- aria-pressed={summary.values.fill === swatch}
160
- aria-label="{labels.fill}: {labels.colors[swatch]}"
161
- data-tip={labels.colors[swatch]}
162
- style:--swatch="var(--pen-{swatch})"
163
- onclick={() => change({ fill: swatch })}
164
- ></button>
165
- {/each}
166
- </div>
167
- </div>
168
- {/if}
153
+ <Tooltip.Provider>
154
+ {#key controlsKey}
155
+ <Toolbar.Root aria-label={labels.panel}>
156
+ {#snippet child({ props })}
157
+ <div
158
+ {...props}
159
+ bind:offsetWidth={barWidth}
160
+ bind:offsetHeight={barHeight}
161
+ class="cv-bar cv-style t-enter"
162
+ data-testid="style-panel"
163
+ >
164
+ {#if controls.has("fill")}
165
+ <div
166
+ class="cv-section"
167
+ role="group"
168
+ aria-label={labels.fill}
169
+ data-testid="style-fill"
170
+ >
171
+ <span class="cv-label" aria-hidden="true">{labels.fill}</span>
172
+ <Toolbar.Group
173
+ type="single"
174
+ class="cv-swatches"
175
+ bind:value={() => values.fill, (value) => pick("fill", value)}
176
+ >
177
+ <BarTooltip label={labels.none} name="{labels.fill}: {labels.none}" side="left">
178
+ {#snippet child({ props })}
179
+ <Toolbar.GroupItem
180
+ {...props}
181
+ value="none"
182
+ class="cv-swatch cv-swatch-none"
183
+ />
184
+ {/snippet}
185
+ </BarTooltip>
186
+ {#each PEN_COLORS as swatch (swatch)}
187
+ {@render colorItem(swatch, labels.fill)}
188
+ {/each}
189
+ </Toolbar.Group>
190
+ </div>
191
+ {/if}
169
192
 
170
- {#if summary.controls.has("stroke")}
171
- <div class="cv-section" role="group" aria-label={strokeLabel} data-testid="style-stroke">
172
- <span class="cv-label" aria-hidden="true">{strokeLabel}</span>
173
- <div class="cv-swatches">
174
- {#each PEN_COLORS as swatch (swatch)}
175
- <button
176
- type="button"
177
- class="cv-swatch"
178
- aria-pressed={summary.values.stroke === swatch}
179
- aria-label="{strokeLabel}: {labels.colors[swatch]}"
180
- data-tip={labels.colors[swatch]}
181
- style:--swatch="var(--pen-{swatch})"
182
- onclick={() => change({ stroke: swatch })}
183
- ></button>
184
- {/each}
185
- </div>
186
- </div>
187
- {/if}
193
+ {#if controls.has("stroke")}
194
+ <div
195
+ class="cv-section"
196
+ role="group"
197
+ aria-label={strokeLabel}
198
+ data-testid="style-stroke"
199
+ >
200
+ <span class="cv-label" aria-hidden="true">{strokeLabel}</span>
201
+ <Toolbar.Group
202
+ type="single"
203
+ class="cv-swatches"
204
+ bind:value={() => values.stroke, (value) => pick("stroke", value)}
205
+ >
206
+ {#each PEN_COLORS as swatch (swatch)}
207
+ {@render colorItem(swatch, strokeLabel)}
208
+ {/each}
209
+ </Toolbar.Group>
210
+ </div>
211
+ {/if}
188
212
 
189
- {#if summary.controls.has("width")}
190
- <div class="cv-section" role="group" aria-label={labels.width} data-testid="style-width">
191
- <span class="cv-label" aria-hidden="true">{labels.width}</span>
192
- <div class="cv-swatches">
193
- {#each LEVELS as level (level)}
194
- <button
195
- type="button"
196
- class="cv-btn"
197
- aria-pressed={summary.values.width === level}
198
- aria-label="{labels.width}: {labels.widths[level]}"
199
- data-tip={labels.widths[level]}
200
- onclick={() => change({ width: level })}
201
- >
202
- <span class="cv-line" style:--line="{[1.5, 3, 5][level]}px"></span>
203
- </button>
204
- {/each}
205
- </div>
206
- </div>
207
- {/if}
213
+ {#if controls.has("width")}
214
+ <div
215
+ class="cv-section"
216
+ role="group"
217
+ aria-label={labels.width}
218
+ data-testid="style-width"
219
+ >
220
+ <span class="cv-label" aria-hidden="true">{labels.width}</span>
221
+ <Toolbar.Group
222
+ type="single"
223
+ class="cv-swatches"
224
+ bind:value={() => String(values.width), (value) => pick("width", value)}
225
+ >
226
+ {#each LEVELS as level (level)}
227
+ <BarTooltip
228
+ label={labels.widths[level]}
229
+ name="{labels.width}: {labels.widths[level]}"
230
+ side="left"
231
+ >
232
+ {#snippet child({ props })}
233
+ <Toolbar.GroupItem {...props} value={String(level)} class="cv-btn">
234
+ <span class="cv-line" style:--line="{[1.5, 3, 5][level]}px"></span>
235
+ </Toolbar.GroupItem>
236
+ {/snippet}
237
+ </BarTooltip>
238
+ {/each}
239
+ </Toolbar.Group>
240
+ </div>
241
+ {/if}
208
242
 
209
- {#if summary.controls.has("fontSize")}
210
- <div class="cv-section" role="group" aria-label={labels.font} data-testid="style-font">
211
- <span class="cv-label" aria-hidden="true">{labels.font}</span>
212
- <div class="cv-swatches">
213
- {#each FONT_SIZES as size (size)}
214
- <button
215
- type="button"
216
- class="cv-btn cv-figure"
217
- aria-pressed={summary.values.fontSize === size}
218
- aria-label="{labels.font}: {size}"
219
- onclick={() => change({ fontSize: size })}>{size}</button
220
- >
221
- {/each}
222
- {#each WEIGHTS as weight (weight)}
223
- <button
224
- type="button"
225
- class="cv-btn cv-figure"
226
- style:font-weight={weight === "bold" ? 700 : 400}
227
- aria-pressed={summary.values.fontWeight === weight}
228
- aria-label={weight === "bold" ? labels.bold : labels.regular}
229
- data-tip={weight === "bold" ? labels.bold : labels.regular}
230
- onclick={() => change({ fontWeight: weight })}>Aa</button
231
- >
232
- {/each}
233
- </div>
234
- </div>
235
- {/if}
236
- </div>
243
+ {#if controls.has("fontSize")}
244
+ <div
245
+ class="cv-section"
246
+ role="group"
247
+ aria-label={labels.font}
248
+ data-testid="style-font"
249
+ >
250
+ <span class="cv-label" aria-hidden="true">{labels.font}</span>
251
+ <!-- Size and weight are two picks that share one grid, so each group lays out as its items. -->
252
+ <div class="cv-swatches">
253
+ <Toolbar.Group
254
+ type="single"
255
+ class="cv-contents"
256
+ bind:value={() => String(values.fontSize), (value) => pick("fontSize", value)}
257
+ >
258
+ {#each FONT_SIZES as size (size)}
259
+ <Toolbar.GroupItem
260
+ value={String(size)}
261
+ class="cv-btn cv-figure"
262
+ aria-label="{labels.font}: {size}">{size}</Toolbar.GroupItem
263
+ >
264
+ {/each}
265
+ </Toolbar.Group>
266
+ <Toolbar.Group
267
+ type="single"
268
+ class="cv-contents"
269
+ bind:value={() => values.fontWeight, (value) => pick("fontWeight", value)}
270
+ >
271
+ {#each WEIGHTS as weight (weight)}
272
+ <BarTooltip
273
+ label={weight === "bold" ? labels.bold : labels.regular}
274
+ side="left"
275
+ >
276
+ {#snippet child({ props })}
277
+ <Toolbar.GroupItem
278
+ {...props}
279
+ value={weight}
280
+ class="cv-btn cv-figure"
281
+ style="font-weight: {weight === 'bold' ? 700 : 400}"
282
+ >Aa</Toolbar.GroupItem
283
+ >
284
+ {/snippet}
285
+ </BarTooltip>
286
+ {/each}
287
+ </Toolbar.Group>
288
+ </div>
289
+ </div>
290
+ {/if}
291
+ </div>
292
+ {/snippet}
293
+ </Toolbar.Root>
294
+ {/key}
295
+ </Tooltip.Provider>
237
296
  </Panel>
238
297
  {/if}
239
298
 
299
+ {#snippet colorItem(swatch: PenColor, section: string)}
300
+ <BarTooltip label={labels.colors[swatch]} name="{section}: {labels.colors[swatch]}" side="left">
301
+ {#snippet child({ props })}
302
+ <Toolbar.GroupItem
303
+ {...props}
304
+ value={swatch}
305
+ class="cv-swatch"
306
+ style="--swatch: var(--pen-{swatch})"
307
+ />
308
+ {/snippet}
309
+ </BarTooltip>
310
+ {/snippet}
311
+
240
312
  <style>
241
313
  @layer theme, base, components, utilities;
242
314
 
@@ -261,12 +333,17 @@
261
333
  line-height: calc(16 / 12);
262
334
  }
263
335
 
264
- .cv-swatches {
336
+ /* Bits renders the groups and their items, so those classes are reached through the bar. */
337
+ .cv-style :global(.cv-swatches) {
265
338
  display: grid;
266
339
  grid-template-columns: repeat(3, var(--cv-btn));
267
340
  gap: 0;
268
341
  }
269
342
 
343
+ .cv-style :global(.cv-contents) {
344
+ display: contents;
345
+ }
346
+
270
347
  .cv-line {
271
348
  width: 18px;
272
349
  height: var(--line);
@@ -274,13 +351,13 @@
274
351
  background: currentColor;
275
352
  }
276
353
 
277
- .cv-figure {
354
+ .cv-style :global(.cv-figure) {
278
355
  font-size: 12px;
279
356
  font-variant-numeric: tabular-nums;
280
357
  }
281
358
 
282
359
  /* No fill is an empty disc struck through, since a swatch can't show the absence of a colour. */
283
- .cv-swatch-none::before {
360
+ .cv-style :global(.cv-swatch-none)::before {
284
361
  background:
285
362
  linear-gradient(135deg, transparent 46%, var(--cv-ink-muted) 46% 54%, transparent 54%),
286
363
  var(--cv-surface);
@@ -67,6 +67,14 @@
67
67
  pointer-events: none;
68
68
  }
69
69
 
70
+ /* A toolbar group lays its items out as if they sat in the bar itself. */
71
+ .cv-group {
72
+ display: flex;
73
+ flex-direction: inherit;
74
+ align-items: center;
75
+ gap: inherit;
76
+ }
77
+
70
78
  /* Groups are set apart by space, never by a rule. */
71
79
  .cv-gap {
72
80
  flex: none;
@@ -107,6 +115,7 @@
107
115
 
108
116
  /* One active style for a tool, a size, a brush and an open popover. */
109
117
  .cv-btn[aria-pressed="true"],
118
+ .cv-btn[data-state="on"],
110
119
  .cv-btn[aria-expanded="true"],
111
120
  .cv-btn[data-state="open"] {
112
121
  background: var(--cv-surface-hover);
@@ -169,7 +178,8 @@
169
178
  }
170
179
  }
171
180
 
172
- .cv-swatch[aria-pressed="true"]::before {
181
+ .cv-swatch[aria-pressed="true"]::before,
182
+ .cv-swatch[data-state="on"]::before {
173
183
  box-shadow:
174
184
  0 0 0 2px var(--cv-surface),
175
185
  0 0 0 4px var(--cv-ink);
@@ -182,64 +192,9 @@
182
192
  background: currentColor;
183
193
  }
184
194
 
185
- /* A label on hover, for a pointer that can hover. Touch gets the aria-label alone. */
186
- @media (hover: hover) {
187
- .cv-btn[data-tip]::after,
188
- .cv-swatch[data-tip]::after {
189
- content: attr(data-tip);
190
- position: absolute;
191
- bottom: calc(100% + 10px);
192
- left: 50%;
193
- translate: -50% 4px;
194
- padding: 4px 8px;
195
- border-radius: var(--radius-sm, 6px);
196
- background: var(--foreground, #1c1c24);
197
- color: var(--background, #fff);
198
- font-size: 12px;
199
- line-height: 1.2;
200
- white-space: pre;
201
- pointer-events: none;
202
- opacity: 0;
203
- z-index: 5;
204
- transition:
205
- opacity var(--duration-fast, 150ms) var(--ease-smooth-out, ease-out),
206
- translate var(--duration-fast, 150ms) var(--ease-smooth-out, ease-out);
207
- }
208
-
209
- .cv-btn[data-tip]:hover::after,
210
- .cv-btn[data-tip]:focus-visible::after,
211
- .cv-swatch[data-tip]:hover::after,
212
- .cv-swatch[data-tip]:focus-visible::after {
213
- opacity: 1;
214
- translate: -50% 0;
215
- transition-delay: 350ms;
216
- }
217
-
218
- /* On the rail the label opens to the right. */
219
- .cv-rail .cv-btn[data-tip]::after {
220
- top: 50%;
221
- bottom: auto;
222
- left: calc(100% + 10px);
223
- translate: -4px -50%;
224
- }
225
-
226
- .cv-rail .cv-btn[data-tip]:hover::after,
227
- .cv-rail .cv-btn[data-tip]:focus-visible::after {
228
- translate: 0 -50%;
229
- }
230
-
231
- .cv-btn:active::after,
232
- .cv-btn:disabled::after,
233
- .cv-btn[aria-expanded="true"]::after,
234
- .cv-btn[data-state="open"]::after {
235
- opacity: 0 !important;
236
- }
237
- }
238
-
239
195
  @media (prefers-reduced-motion: reduce) {
240
196
  .cv-btn,
241
- .cv-swatch::before,
242
- .cv-btn[data-tip]::after {
197
+ .cv-swatch::before {
243
198
  transition: none;
244
199
  }
245
200
 
@@ -247,25 +202,6 @@
247
202
  scale: 1;
248
203
  }
249
204
  }
250
-
251
- /* On a bar docked to the right edge the label opens to the left. */
252
- @media (hover: hover) {
253
- .cv-side .cv-btn[data-tip]::after,
254
- .cv-side .cv-swatch[data-tip]::after {
255
- top: 50%;
256
- bottom: auto;
257
- left: auto;
258
- right: calc(100% + 10px);
259
- translate: 4px -50%;
260
- }
261
-
262
- .cv-side .cv-btn[data-tip]:hover::after,
263
- .cv-side .cv-btn[data-tip]:focus-visible::after,
264
- .cv-side .cv-swatch[data-tip]:hover::after,
265
- .cv-side .cv-swatch[data-tip]:focus-visible::after {
266
- translate: 0 -50%;
267
- }
268
- }
269
205
  }
270
206
 
271
207
  @property --cv-fade-start {
@@ -43,9 +43,11 @@
43
43
  */
44
44
  import { cn } from "../../lib/utils.js";
45
45
  import RatingChip from "./rating-chip.svelte";
46
+ import Hint from "./hint.svelte";
46
47
  import {
47
48
  Pagination,
48
49
  SortableHead,
50
+ TableSkeleton,
49
51
  paginateRows,
50
52
  totalPages,
51
53
  type SortState,
@@ -127,6 +129,18 @@
127
129
  });
128
130
 
129
131
  const visible = $derived(pageSize ? paginateRows(sorted, page, pageSize) : sorted);
132
+
133
+ // Placeholder rows hold the 40px of a lead cell's padding, touch or not, and the rule sits
134
+ // outside it as before. Each cell keeps its column's padding, rule and breakpoint.
135
+ const loadingCell = "box-content h-10 any-pointer-coarse:h-10";
136
+ const loadingColumns = $derived([
137
+ { width: "w-2/3", class: cn("px-4", loadingCell) },
138
+ ...columns.map((column) => ({
139
+ width: "w-3/4",
140
+ align: column.align === "start" ? ("left" as const) : ("right" as const),
141
+ class: cn(columnClass(column), loadingCell),
142
+ })),
143
+ ]);
130
144
  const opens = $derived(Boolean(rowHref || onRowClick));
131
145
 
132
146
  // Fewer rows than the page you stand on would draw an empty table.
@@ -182,96 +196,82 @@
182
196
  <th
183
197
  scope="col"
184
198
  class={cn(column.align === "start" ? dataThClass : dataThNumClass, columnClass(column))}
185
- title={column.hint}
186
199
  >
187
- {column.label}
200
+ <Hint text={column.hint}>{column.label}</Hint>
188
201
  </th>
189
202
  {/if}
190
203
  {/each}
191
204
  </tr>
192
205
  </thead>
193
- <tbody>
194
- {#if loading}
195
- {#each { length: LOADING_ROWS }, index (index)}
196
- <tr class={dataTrClass} aria-hidden="true">
197
- <td class={cn(dataTdClass, "py-3")}>
198
- <span class="block h-4 w-2/3 animate-pulse rounded bg-muted"></span>
199
- </td>
200
- {#each columns as column (column.key)}
201
- <td class={cn(dataTdNumClass, columnClass(column), "align-middle")}>
202
- <span
203
- class={cn(
204
- "block h-4 w-3/4 animate-pulse rounded bg-muted",
205
- column.align !== "start" && "ml-auto",
206
- )}
207
- ></span>
208
- </td>
209
- {/each}
206
+ {#if loading}
207
+ <TableSkeleton rows={LOADING_ROWS} columns={loadingColumns} rowClass={dataTrClass} />
208
+ {:else}
209
+ <tbody>
210
+ {#if visible.length === 0 && empty}
211
+ <tr>
212
+ <td colspan={columns.length + 1}>{@render empty()}</td>
210
213
  </tr>
211
- {/each}
212
- {:else if visible.length === 0 && empty}
213
- <tr>
214
- <td colspan={columns.length + 1}>{@render empty()}</td>
215
- </tr>
216
- {:else}
217
- {#each visible as row (rowKey(row))}
218
- <tr class={cn(dataTrClass, "group", opens && "relative cursor-pointer")}>
219
- <td class={cn(dataTdClass, "py-3")}>
220
- {#if rowHref}
221
- <a href={rowHref(row)} class={stretch}>{@render lead(row)}</a>
222
- {:else if onRowClick}
223
- <button type="button" class={stretch} onclick={() => onRowClick(row)}>
224
- {@render lead(row)}
225
- </button>
226
- {:else}
227
- {@render lead(row)}
228
- {/if}
229
- </td>
230
- {#each columns as column (column.key)}
231
- <td
232
- class={cn(
233
- dataTdNumClass,
234
- columnClass(column),
235
- "align-middle whitespace-nowrap",
236
- column.strong && "font-medium",
237
- isMuted(column, row) && "text-muted-foreground",
238
- column.negative?.(row) && "text-destructive-ink",
239
- )}
240
- title={column.title?.(row)}
241
- >
242
- {#if column.cell}
243
- {@render column.cell(row)}
244
- {:else if column.rating}
245
- {@const rating = column.rating(row)}
246
- {#if rating}
247
- <RatingChip grade={rating.grade} hint={rating.hint} />
248
- {:else}
249
- <span class="text-muted-foreground">{emptyLabel}</span>
250
- {/if}
214
+ {:else}
215
+ {#each visible as row (rowKey(row))}
216
+ <tr class={cn(dataTrClass, "group", opens && "relative cursor-pointer")}>
217
+ <td class={cn(dataTdClass, "py-3")}>
218
+ {#if rowHref}
219
+ <a href={rowHref(row)} class={stretch}>{@render lead(row)}</a>
220
+ {:else if onRowClick}
221
+ <button type="button" class={stretch} onclick={() => onRowClick(row)}>
222
+ {@render lead(row)}
223
+ </button>
251
224
  {:else}
252
- {@const value = column.value?.(row) ?? null}
253
- {#if value === null}
254
- <span class="text-muted-foreground">{emptyLabel}</span>
255
- {:else if column.bar}
256
- <span class="flex items-center justify-end gap-3">
257
- <span class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-muted">
258
- <span
259
- class="block h-full origin-left rounded-full bg-primary/70"
260
- style:transform="scaleX({Math.min(1, Math.max(0, column.bar(row)))})"
261
- ></span>
262
- </span>
263
- <span>{value}</span>
264
- </span>
265
- {:else}
266
- {value}
267
- {/if}
225
+ {@render lead(row)}
268
226
  {/if}
269
227
  </td>
270
- {/each}
271
- </tr>
272
- {/each}
273
- {/if}
274
- </tbody>
228
+ {#each columns as column (column.key)}
229
+ <td
230
+ class={cn(
231
+ dataTdNumClass,
232
+ columnClass(column),
233
+ "align-middle whitespace-nowrap",
234
+ column.strong && "font-medium",
235
+ isMuted(column, row) && "text-muted-foreground",
236
+ column.negative?.(row) && "text-destructive-ink",
237
+ )}
238
+ >
239
+ <Hint text={column.title?.(row)} class="block">
240
+ {#if column.cell}
241
+ {@render column.cell(row)}
242
+ {:else if column.rating}
243
+ {@const rating = column.rating(row)}
244
+ {#if rating}
245
+ <RatingChip grade={rating.grade} hint={rating.hint} />
246
+ {:else}
247
+ <span class="text-muted-foreground">{emptyLabel}</span>
248
+ {/if}
249
+ {:else}
250
+ {@const value = column.value?.(row) ?? null}
251
+ {#if value === null}
252
+ <span class="text-muted-foreground">{emptyLabel}</span>
253
+ {:else if column.bar}
254
+ <span class="flex items-center justify-end gap-3">
255
+ <span class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-muted">
256
+ <span
257
+ class="block h-full origin-left rounded-full bg-primary/70"
258
+ style:transform="scaleX({Math.min(1, Math.max(0, column.bar(row)))})"
259
+ ></span>
260
+ </span>
261
+ <span>{value}</span>
262
+ </span>
263
+ {:else}
264
+ {value}
265
+ {/if}
266
+ {/if}
267
+ </Hint>
268
+ </td>
269
+ {/each}
270
+ </tr>
271
+ {/each}
272
+ {/if}
273
+ </tbody>
274
+ {/if}
275
275
  </table>
276
276
  {#if pageSize && !loading}
277
277
  <Pagination {page} {pageSize} total={sorted.length} onPageChange={(next) => (page = next)} />