@cardstack/boxel-cli 0.11.0-unstable.2 → 0.11.0-unstable.3

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 (106) hide show
  1. package/bundled-realms/pretui/components/agent-mark.gts +98 -0
  2. package/bundled-realms/pretui/components/agent-question.gts +340 -37
  3. package/bundled-realms/pretui/components/answer-footer.gts +249 -0
  4. package/bundled-realms/pretui/components/card-embed.gts +523 -0
  5. package/bundled-realms/pretui/components/card-pills.gts +725 -0
  6. package/bundled-realms/pretui/components/code-block.gts +87 -45
  7. package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
  8. package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
  9. package/bundled-realms/pretui/components/composer-cap.gts +516 -0
  10. package/bundled-realms/pretui/components/context-ring.gts +108 -0
  11. package/bundled-realms/pretui/components/diff-block.gts +1002 -31
  12. package/bundled-realms/pretui/components/diff-stat.gts +102 -0
  13. package/bundled-realms/pretui/components/jump-chip.gts +86 -53
  14. package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
  15. package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
  16. package/bundled-realms/pretui/components/plan-mark.gts +147 -0
  17. package/bundled-realms/pretui/components/prose.gts +185 -8
  18. package/bundled-realms/pretui/components/prose.md +6 -8
  19. package/bundled-realms/pretui/components/quick-look.gts +377 -0
  20. package/bundled-realms/pretui/components/reactions.gts +136 -0
  21. package/bundled-realms/pretui/components/recommendation.gts +67 -36
  22. package/bundled-realms/pretui/components/result-card.gts +58 -22
  23. package/bundled-realms/pretui/components/segmented-control.gts +13 -8
  24. package/bundled-realms/pretui/components/session-prep.gts +12 -11
  25. package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
  26. package/bundled-realms/pretui/components/speaker-label.gts +132 -0
  27. package/bundled-realms/pretui/components/state-glyph.gts +121 -0
  28. package/bundled-realms/pretui/components/step-list.gts +62 -60
  29. package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
  30. package/bundled-realms/pretui/components/system-line.gts +143 -0
  31. package/bundled-realms/pretui/components/task-checklist.gts +184 -0
  32. package/bundled-realms/pretui/components/thinking.gts +63 -94
  33. package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
  34. package/bundled-realms/pretui/components/tool-gate.gts +462 -0
  35. package/bundled-realms/pretui/components/tool-group.gts +920 -0
  36. package/bundled-realms/pretui/components/tool-icon.ts +64 -0
  37. package/bundled-realms/pretui/components/tool-meta.gts +82 -0
  38. package/bundled-realms/pretui/components/tool-plan.gts +197 -0
  39. package/bundled-realms/pretui/components/tool-row.gts +2087 -0
  40. package/bundled-realms/pretui/components/turn-changes.gts +227 -0
  41. package/bundled-realms/pretui/components/user-bubble.gts +575 -0
  42. package/bundled-realms/pretui/components/work-item.gts +185 -48
  43. package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
  44. package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
  45. package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
  46. package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
  47. package/bundled-realms/pretui/package.json +2 -1
  48. package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
  49. package/bundled-test-harness/assets/{app-CtezUCip.js → app-KpPqdyX3.js} +4 -4
  50. package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
  51. package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
  52. package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
  53. package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
  54. package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
  55. package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
  56. package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
  57. package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
  58. package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
  59. package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
  60. package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
  61. package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
  62. package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
  63. package/bundled-test-harness/assets/{externals-DCnB4XSS.js → externals-BWjhmCD4.js} +1 -1
  64. package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
  65. package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
  66. package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
  67. package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
  68. package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
  69. package/bundled-test-harness/assets/{index-query-engine-test-CY2tAkot.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
  70. package/bundled-test-harness/assets/{instance-filter-matcher-test-CMBTh8xn.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
  71. package/bundled-test-harness/assets/{main-BOZbZVem.js → main-CqAHZ2kK.js} +1 -1
  72. package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
  73. package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
  74. package/bundled-test-harness/assets/{operation-lowering-test-CHnjKSJb.js → operation-lowering-test-C4IaOjsA.js} +1 -1
  75. package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
  76. package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
  77. package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
  78. package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
  79. package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
  80. package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
  81. package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
  82. package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
  83. package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
  84. package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
  85. package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
  86. package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
  87. package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
  88. package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
  89. package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
  90. package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
  91. package/bundled-test-harness/assets/{searchable-option-test-Ctf8r3HZ.js → searchable-option-test-BAdOiL4U.js} +1 -1
  92. package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
  93. package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
  94. package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
  95. package/bundled-test-harness/assets/{tests-DoTEpvcF.js → tests-QYqqa6nv.js} +47 -47
  96. package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
  97. package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
  98. package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
  99. package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
  100. package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
  101. package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
  102. package/bundled-test-harness/index.html +8 -8
  103. package/bundled-test-harness/tests/index.html +8 -8
  104. package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
  105. package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
  106. package/package.json +1 -1
@@ -0,0 +1,725 @@
1
+ // Pretui — CardPills: the cards a call found or touched, as pills or a grid, that open, select and fold.
2
+ import Component from '@glimmer/component';
3
+ import { StateGlyph } from './state-glyph';
4
+ import { tracked } from '@glimmer/tracking';
5
+ import { on } from '@ember/modifier';
6
+ import { fn } from '@ember/helper';
7
+ import { modifier } from 'ember-modifier';
8
+ import { VisuallyHidden } from './visually-hidden';
9
+
10
+ export interface CardPillItem {
11
+ /** stable: the card's id. Default: its title */
12
+ id?: string;
13
+ /** the card's title */
14
+ title: string;
15
+ /** its type, as the card shows it: 'Runner', 'Race' */
16
+ kind?: string;
17
+ /** a thumbnail; without one the pill shows the title's initial */
18
+ image?: string;
19
+ /** a quieter line, for the grid: '4 runs this month' */
20
+ sub?: string;
21
+ }
22
+
23
+ export interface CardPillsSignature {
24
+ Args: {
25
+ cards: CardPillItem[];
26
+ /** 'pills' (default): a wrap of pills, for a search row's result.
27
+ * 'grid': small tiles, for members and other sets worth seeing */
28
+ layout?: 'pills' | 'grid';
29
+ /** how many show before "Show all N". Default: all */
30
+ limit?: number;
31
+ /** the reader opened a card; the caller opens it where they work */
32
+ onOpen?: (card: CardPillItem) => void;
33
+ /** the card open on the stack, by id: its pill stays highlighted, and
34
+ * pressing it again (or Escape) closes it */
35
+ open?: string;
36
+ onClose?: (card: CardPillItem) => void;
37
+ /** pick cards instead of opening them: a press toggles a check, and a
38
+ * bar offers what to do with the picked ones */
39
+ selectable?: boolean;
40
+ /** the picked cards, by id; controlled */
41
+ selected?: string[];
42
+ onSelectionChange?: (ids: string[]) => void;
43
+ /** the bar's verb for the picked cards: 'Attach', 'Message'. With
44
+ * `@onUseSelection` */
45
+ useLabel?: string;
46
+ onUseSelection?: (cards: CardPillItem[]) => void;
47
+ /** the list's accessible name. Default 'Cards' */
48
+ label?: string;
49
+ /** hovering or focusing a pill peeks at its card: a hover card with
50
+ * the `<:peek>` block (the host's fitted card), Open on stack and Show
51
+ * here. `@onPeek` lets the host start loading it */
52
+ peek?: boolean;
53
+ onPeek?: (card: CardPillItem) => void;
54
+ /** the card shown in place, by id: the `<:expanded>` block renders it
55
+ * under the set, embedded, with Hide */
56
+ expanded?: string;
57
+ onExpand?: (card: CardPillItem) => void;
58
+ onCollapse?: (card: CardPillItem) => void;
59
+ };
60
+ Blocks: {
61
+ /** a card's fitted view, inside the peek */
62
+ peek: [card: CardPillItem];
63
+ /** a card's embedded view, in place under the set */
64
+ expanded: [card: CardPillItem];
65
+ };
66
+ Element: HTMLDivElement;
67
+ }
68
+
69
+ /**
70
+ * What a `cards.search`, `cards.get` or `entries.search` returned, as the
71
+ * cards themselves. The whole interaction is here:
72
+ *
73
+ * - **Open.** A press opens the card where the reader works; that pill
74
+ * stays highlighted as open, and a second press (or Escape) closes it.
75
+ * - **Pick.** With `@selectable` a press toggles a check instead, and a
76
+ * bar says "3 picked · Attach · Clear"; Escape clears.
77
+ * - **Move.** Arrow keys move between cards (one tab stop for the set),
78
+ * Home and End jump to the ends.
79
+ * - **Fold.** Past `@limit`, "Show all N"; once open, "Show fewer".
80
+ */
81
+ export class CardPills extends Component<CardPillsSignature> {
82
+ @tracked showAll = false;
83
+ @tracked private innerSelected: string[] = [];
84
+ @tracked private focusIndex = 0;
85
+ /** the pill being peeked at */
86
+ @tracked private peekId: string | undefined;
87
+ private list: HTMLElement | undefined;
88
+
89
+ get layout() {
90
+ return this.args.layout ?? 'pills';
91
+ }
92
+ get folds(): boolean {
93
+ let { limit, cards } = this.args;
94
+ return limit !== undefined && cards.length > limit;
95
+ }
96
+ get shown(): CardPillItem[] {
97
+ let { cards, limit } = this.args;
98
+ return this.showAll || limit === undefined ? cards : cards.slice(0, limit);
99
+ }
100
+ get hidden(): number {
101
+ return this.args.cards.length - this.shown.length;
102
+ }
103
+ get selectedIds(): string[] {
104
+ return this.args.selected ?? this.innerSelected;
105
+ }
106
+ get pickedCards(): CardPillItem[] {
107
+ let ids = new Set(this.selectedIds);
108
+ return this.args.cards.filter((c) => ids.has(this.idOf(c)));
109
+ }
110
+ get pickedLabel(): string {
111
+ return `${this.selectedIds.length} picked`;
112
+ }
113
+ get hasPicks(): boolean {
114
+ return this.selectedIds.length > 0;
115
+ }
116
+
117
+ idOf = (card: CardPillItem) => card.id ?? card.title;
118
+ initial = (card: CardPillItem) => card.title.trim()[0] ?? '?';
119
+ isOpen = (card: CardPillItem) =>
120
+ !this.args.selectable && this.args.open === this.idOf(card);
121
+ isPicked = (card: CardPillItem) => this.selectedIds.includes(this.idOf(card));
122
+ pressedOf = (card: CardPillItem) =>
123
+ this.args.selectable
124
+ ? this.isPicked(card)
125
+ ? 'true'
126
+ : 'false'
127
+ : this.isOpen(card)
128
+ ? 'true'
129
+ : 'false';
130
+ tabIndexOf = (index: number) => (index === this.focusIndex ? '0' : '-1');
131
+ stateOf = (card: CardPillItem) =>
132
+ this.isPicked(card) ? 'picked' : this.isOpen(card) ? 'open' : undefined;
133
+
134
+ private setSelected(ids: string[]) {
135
+ this.innerSelected = ids;
136
+ this.args.onSelectionChange?.(ids);
137
+ }
138
+
139
+ press = (card: CardPillItem, index: number) => {
140
+ this.focusIndex = index;
141
+ if (this.args.selectable) {
142
+ let id = this.idOf(card);
143
+ let ids = this.selectedIds;
144
+ this.setSelected(
145
+ ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id],
146
+ );
147
+ return;
148
+ }
149
+ if (this.isOpen(card)) {
150
+ this.args.onClose?.(card);
151
+ } else {
152
+ this.args.onOpen?.(card);
153
+ }
154
+ };
155
+ isPeeking = (card: CardPillItem) =>
156
+ this.args.peek === true && this.peekId === this.idOf(card);
157
+ peekIdOf = (card: CardPillItem) =>
158
+ `${this.idOf(card)}-peek`.replace(/\s+/g, '-');
159
+ startPeek = (card: CardPillItem) => {
160
+ if (!this.args.peek || this.peekId === this.idOf(card)) {
161
+ return;
162
+ }
163
+ this.peekId = this.idOf(card);
164
+ this.args.onPeek?.(card);
165
+ };
166
+ endPeek = (card: CardPillItem, event?: Event) => {
167
+ let to = (event as FocusEvent | PointerEvent | undefined)?.relatedTarget as
168
+ | Node
169
+ | null
170
+ | undefined;
171
+ let li = (event?.currentTarget as HTMLElement | undefined) ?? undefined;
172
+ if (to && li?.contains(to)) {
173
+ return; // moving into the peek keeps it
174
+ }
175
+ if (this.peekId === this.idOf(card)) {
176
+ this.peekId = undefined;
177
+ }
178
+ };
179
+ openFromPeek = (card: CardPillItem) => {
180
+ this.peekId = undefined;
181
+ this.args.onOpen?.(card);
182
+ };
183
+ expandFromPeek = (card: CardPillItem) => {
184
+ this.peekId = undefined;
185
+ this.args.onExpand?.(card);
186
+ };
187
+ get expandedCard(): CardPillItem | undefined {
188
+ return this.args.expanded
189
+ ? this.args.cards.find((c) => this.idOf(c) === this.args.expanded)
190
+ : undefined;
191
+ }
192
+ collapse_ = () => {
193
+ let card = this.expandedCard;
194
+ if (card) {
195
+ this.args.onCollapse?.(card);
196
+ }
197
+ };
198
+ clear = () => this.setSelected([]);
199
+ use = () => this.args.onUseSelection?.(this.pickedCards);
200
+ expand = () => (this.showAll = true);
201
+ collapse = () => {
202
+ this.showAll = false;
203
+ this.focusIndex = Math.min(this.focusIndex, (this.args.limit ?? 1) - 1);
204
+ };
205
+ /** one button folds and unfolds, so focus stays on it when its words
206
+ * change */
207
+ toggleAll = () => (this.showAll ? this.collapse() : this.expand());
208
+
209
+ private focusAt(index: number) {
210
+ let n = this.shown.length;
211
+ if (!n) {
212
+ return;
213
+ }
214
+ this.focusIndex = Math.min(Math.max(index, 0), n - 1);
215
+ let buttons =
216
+ this.list?.querySelectorAll<HTMLButtonElement>('[data-card-pill]');
217
+ buttons?.[this.focusIndex]?.focus();
218
+ }
219
+ /** the grid's columns, for Up and Down */
220
+ private columns(): number {
221
+ let buttons = [
222
+ ...(this.list?.querySelectorAll<HTMLElement>('[data-card-pill]') ?? []),
223
+ ];
224
+ if (buttons.length < 2) {
225
+ return 1;
226
+ }
227
+ let top = buttons[0]!.getBoundingClientRect().top;
228
+ let perRow = buttons.findIndex(
229
+ (b) => b.getBoundingClientRect().top > top + 1,
230
+ );
231
+ return perRow > 0 ? perRow : buttons.length;
232
+ }
233
+ keydown = (e: Event) => {
234
+ let event = e as KeyboardEvent;
235
+ let i = this.focusIndex;
236
+ let cols = this.layout === 'grid' ? this.columns() : 1;
237
+ let next: number | undefined;
238
+ switch (event.key) {
239
+ case 'ArrowRight':
240
+ next = i + 1;
241
+ break;
242
+ case 'ArrowLeft':
243
+ next = i - 1;
244
+ break;
245
+ case 'ArrowDown':
246
+ next = i + cols;
247
+ break;
248
+ case 'ArrowUp':
249
+ next = i - cols;
250
+ break;
251
+ case 'Home':
252
+ next = 0;
253
+ break;
254
+ case 'End':
255
+ next = this.shown.length - 1;
256
+ break;
257
+ case 'Escape': {
258
+ // an Escape the set uses stops here, so a host's document-level
259
+ // Escape does not also close what holds it
260
+ if (this.peekId) {
261
+ event.preventDefault();
262
+ event.stopPropagation();
263
+ this.peekId = undefined;
264
+ return;
265
+ }
266
+ if (this.args.selectable && this.hasPicks) {
267
+ event.preventDefault();
268
+ event.stopPropagation();
269
+ this.clear();
270
+ } else {
271
+ let open = this.args.cards.find((c) => this.isOpen(c));
272
+ if (open) {
273
+ event.preventDefault();
274
+ event.stopPropagation();
275
+ this.args.onClose?.(open);
276
+ }
277
+ }
278
+ return;
279
+ }
280
+ default:
281
+ return;
282
+ }
283
+ event.preventDefault();
284
+ this.focusAt(next);
285
+ };
286
+ setupList = modifier((element: HTMLElement) => {
287
+ this.list = element;
288
+ return () => (this.list = undefined);
289
+ });
290
+
291
+ <template>
292
+ <div
293
+ class='pretui-cardpills'
294
+ data-layout={{this.layout}}
295
+ data-selectable={{if @selectable 'true'}}
296
+ data-test-pretui-card-pills
297
+ ...attributes
298
+ >
299
+ {{! the arrow keys move a roving focus through the set, handled on
300
+ the list that holds it }}
301
+ {{! template-lint-disable no-invalid-interactive }}
302
+ <ul
303
+ class='pretui-cardpills-list'
304
+ aria-label={{if @label @label 'Cards'}}
305
+ {{this.setupList}}
306
+ {{on 'keydown' this.keydown}}
307
+ >
308
+ {{#each this.shown as |card index|}}
309
+ {{! the item holds the pill and its peek, so moving the pointer or
310
+ focus from one to the other keeps the peek open }}
311
+ {{! template-lint-disable no-invalid-interactive no-positive-tabindex }}
312
+ <li
313
+ class='pretui-cardpill-item'
314
+ {{on 'pointerenter' (fn this.startPeek card)}}
315
+ {{on 'pointerleave' (fn this.endPeek card)}}
316
+ {{on 'focusin' (fn this.startPeek card)}}
317
+ {{on 'focusout' (fn this.endPeek card)}}
318
+ >
319
+ <button
320
+ type='button'
321
+ class='pretui-cardpill'
322
+ title={{if card.kind card.kind}}
323
+ {{! a roving tabindex: 0 for the set's one stop, -1 for the rest }}
324
+ tabindex={{this.tabIndexOf index}}
325
+ aria-pressed={{this.pressedOf card}}
326
+ aria-describedby={{if (this.isPeeking card) (this.peekIdOf card)}}
327
+ data-card-pill
328
+ data-state={{this.stateOf card}}
329
+ {{on 'click' (fn this.press card index)}}
330
+ data-test-pretui-card-pill={{card.title}}
331
+ >
332
+ <span
333
+ class='pretui-cardpill-mark'
334
+ aria-hidden='true'
335
+ >{{#if (this.isPicked card)}}<StateGlyph
336
+ @glyph='done'
337
+ @size={{10}}
338
+ />{{else if card.image}}<img
339
+ class='pretui-cardpill-image'
340
+ src={{card.image}}
341
+ alt=''
342
+ data-test-pretui-card-pill-image
343
+ />{{else}}{{this.initial card}}{{/if}}</span>
344
+ <span class='pretui-cardpill-text'>
345
+ <span class='pretui-cardpill-title'>{{card.title}}</span>
346
+ {{#if card.sub}}<span
347
+ class='pretui-cardpill-sub'
348
+ >{{card.sub}}</span>{{/if}}
349
+ </span>
350
+ {{#if (this.isOpen card)}}<span
351
+ class='pretui-cardpill-open'
352
+ data-test-pretui-card-pill-open
353
+ >open</span>{{/if}}
354
+ </button>
355
+ {{#if (this.isPeeking card)}}
356
+ <div
357
+ class='pretui-cardpill-peek'
358
+ id={{this.peekIdOf card}}
359
+ role='group'
360
+ aria-label={{card.title}}
361
+ data-test-pretui-card-pill-peek={{card.title}}
362
+ >
363
+ <div class='pretui-cardpill-peek-card'>{{yield
364
+ card
365
+ to='peek'
366
+ }}</div>
367
+ <div class='pretui-cardpill-peek-actions'>
368
+ {{#if @onOpen}}<button
369
+ type='button'
370
+ class='pretui-cardpills-act'
371
+ data-test-pretui-card-pill-peek-open
372
+ {{on 'click' (fn this.openFromPeek card)}}
373
+ >Open on stack</button>{{/if}}
374
+ {{#if @onExpand}}<button
375
+ type='button'
376
+ class='pretui-cardpills-act'
377
+ data-test-pretui-card-pill-peek-expand
378
+ {{on 'click' (fn this.expandFromPeek card)}}
379
+ >Show here</button>{{/if}}
380
+ </div>
381
+ </div>
382
+ {{/if}}
383
+ </li>
384
+ {{/each}}
385
+ </ul>
386
+ {{#if this.folds}}
387
+ <button
388
+ type='button'
389
+ class='pretui-cardpills-all'
390
+ aria-expanded={{if this.showAll 'true' 'false'}}
391
+ {{on 'click' this.toggleAll}}
392
+ data-test-pretui-card-pills-all
393
+ >{{#if this.showAll}}Show fewer{{else}}Show all
394
+ {{@cards.length}}{{/if}}</button>
395
+ {{/if}}
396
+ {{#if this.expandedCard}}
397
+ <div
398
+ class='pretui-cardpills-expanded'
399
+ data-test-pretui-card-pills-expanded={{this.expandedCard.title}}
400
+ >
401
+ <div class='pretui-cardpills-expanded-head'>
402
+ <span>{{this.expandedCard.title}}</span>
403
+ <button
404
+ type='button'
405
+ class='pretui-cardpills-act'
406
+ {{on 'click' this.collapse_}}
407
+ data-test-pretui-card-pills-collapse
408
+ >Hide</button>
409
+ </div>
410
+ {{yield this.expandedCard to='expanded'}}
411
+ </div>
412
+ {{/if}}
413
+ {{#if (if @selectable this.hasPicks)}}
414
+ <div class='pretui-cardpills-bar' data-test-pretui-card-pills-bar>
415
+ <span class='pretui-cardpills-count'>{{this.pickedLabel}}</span>
416
+ {{#if @onUseSelection}}
417
+ <button
418
+ type='button'
419
+ class='pretui-cardpills-act'
420
+ data-kind='primary'
421
+ {{on 'click' this.use}}
422
+ data-test-pretui-card-pills-use
423
+ >{{if @useLabel @useLabel 'Attach'}}</button>
424
+ {{/if}}
425
+ <button
426
+ type='button'
427
+ class='pretui-cardpills-act'
428
+ {{on 'click' this.clear}}
429
+ data-test-pretui-card-pills-clear
430
+ >Clear</button>
431
+ </div>
432
+ {{/if}}
433
+ {{! the count is announced from a region that is always there, apart
434
+ from the bar's buttons, so a change says the count and nothing else }}
435
+ <VisuallyHidden
436
+ role='status'
437
+ aria-atomic='true'
438
+ data-test-pretui-card-pills-status
439
+ >{{if (if @selectable this.hasPicks) this.pickedLabel}}</VisuallyHidden>
440
+ </div>
441
+ <style scoped>
442
+ @layer PretComponent {
443
+ .pretui-cardpills {
444
+ /* the peek's distance from its pill, which the bridge over it
445
+ must span */
446
+ --_cardpills-peek-gap: var(--boxel-sp-2xs);
447
+ display: grid;
448
+ gap: var(--boxel-sp-xs);
449
+ justify-items: start;
450
+ }
451
+ .pretui-cardpills-list {
452
+ display: flex;
453
+ flex-wrap: wrap;
454
+ gap: var(--boxel-sp-4xs);
455
+ margin: 0;
456
+ padding: 0;
457
+ list-style: none;
458
+ }
459
+ .pretui-cardpills[data-layout='grid'] {
460
+ justify-items: stretch;
461
+ }
462
+ .pretui-cardpills[data-layout='grid'] .pretui-cardpills-list {
463
+ display: grid;
464
+ grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
465
+ gap: var(--boxel-sp-xs);
466
+ }
467
+ /* a pill: an outlined token on the inset, the card's mark and its
468
+ title in ink-1. It is 1.5rem tall, the least a pointer target
469
+ takes */
470
+ .pretui-cardpill {
471
+ position: relative;
472
+ display: inline-flex;
473
+ align-items: center;
474
+ gap: var(--boxel-sp-4xs);
475
+ max-width: 100%;
476
+ height: 1.5rem;
477
+ padding: 0 var(--boxel-sp-2xs) 0 var(--boxel-sp-5xs);
478
+ border: 0;
479
+ border-radius: var(--boxel-border-radius-sm);
480
+ background-color: var(--inset);
481
+ box-shadow: inset 0 0 0 1px var(--border);
482
+ font-family: inherit;
483
+ font-size: var(--boxel-ui-label-font-size);
484
+ line-height: var(--boxel-ui-label-line-height);
485
+ font-weight: 600;
486
+ color: var(--foreground);
487
+ vertical-align: middle;
488
+ cursor: pointer;
489
+ transition:
490
+ background-color 120ms ease-out,
491
+ box-shadow 120ms ease-out;
492
+ }
493
+ .pretui-cardpill:hover {
494
+ background-color: var(--hover);
495
+ box-shadow: inset 0 0 0 1px var(--border-strong);
496
+ }
497
+ .pretui-cardpill:active {
498
+ transform: translateY(0.5px);
499
+ }
500
+ .pretui-cardpill:focus-visible {
501
+ outline: 2px solid var(--ring);
502
+ outline-offset: 1px;
503
+ }
504
+ /* open on the stack: the outline and the "open" word take the live
505
+ state, never the fill */
506
+ .pretui-cardpill[data-state='open'] {
507
+ box-shadow: inset 0 0 0 1px var(--primary-ink);
508
+ }
509
+ /* picked: done is ink with a check, the outline in ink-1 */
510
+ .pretui-cardpill[data-state='picked'] {
511
+ box-shadow: inset 0 0 0 1px var(--foreground);
512
+ }
513
+ .pretui-cardpill[data-state='picked'] .pretui-cardpill-mark {
514
+ background-color: var(--foreground);
515
+ color: var(--background);
516
+ }
517
+ .pretui-cardpill-mark {
518
+ flex: none;
519
+ display: grid;
520
+ place-items: center;
521
+ width: 0.875rem;
522
+ height: 0.875rem;
523
+ overflow: hidden;
524
+ border-radius: 50%;
525
+ background-color: var(--card);
526
+ font-size: var(--boxel-caption-font-size);
527
+ line-height: 1;
528
+ font-weight: 600;
529
+ color: var(--muted-foreground);
530
+ }
531
+ .pretui-cardpill-image {
532
+ width: 100%;
533
+ height: 100%;
534
+ object-fit: cover;
535
+ }
536
+ .pretui-cardpill-text {
537
+ display: grid;
538
+ min-width: 0;
539
+ text-align: start;
540
+ }
541
+ .pretui-cardpill-title,
542
+ .pretui-cardpill-sub {
543
+ overflow: hidden;
544
+ text-overflow: ellipsis;
545
+ white-space: nowrap;
546
+ }
547
+ .pretui-cardpill-sub {
548
+ display: none;
549
+ }
550
+ .pretui-cardpill-open {
551
+ margin-inline-start: var(--boxel-sp-6xs);
552
+ font-size: var(--boxel-caption-font-size);
553
+ line-height: var(--boxel-caption-line-height);
554
+ font-weight: 400;
555
+ color: var(--primary-ink);
556
+ }
557
+ /* the grid: a tile per card, the mark larger, the type under it */
558
+ .pretui-cardpills[data-layout='grid'] .pretui-cardpill {
559
+ width: 100%;
560
+ height: auto;
561
+ gap: var(--boxel-sp-xs);
562
+ padding: var(--boxel-sp-2xs) var(--boxel-sp-xs) var(--boxel-sp-2xs)
563
+ var(--boxel-sp-2xs);
564
+ border-radius: var(--boxel-border-radius-sm);
565
+ }
566
+ .pretui-cardpills[data-layout='grid'] .pretui-cardpill-mark {
567
+ width: 1.625rem;
568
+ height: 1.625rem;
569
+ }
570
+ .pretui-cardpills[data-layout='grid'] .pretui-cardpill-sub {
571
+ display: block;
572
+ font-size: var(--boxel-caption-font-size);
573
+ line-height: var(--boxel-caption-line-height);
574
+ font-weight: 400;
575
+ color: var(--muted-foreground);
576
+ }
577
+ .pretui-cardpills[data-layout='grid'] .pretui-cardpill-open {
578
+ position: absolute;
579
+ inset-block-start: var(--boxel-sp-4xs);
580
+ inset-inline-end: var(--boxel-sp-2xs);
581
+ }
582
+ .pretui-cardpills-all {
583
+ min-height: 1.5rem;
584
+ padding: 0 var(--boxel-sp-4xs);
585
+ border: 0;
586
+ border-radius: var(--boxel-border-radius-xs);
587
+ background-color: transparent;
588
+ font-family: inherit;
589
+ font-size: var(--boxel-ui-label-font-size);
590
+ line-height: var(--boxel-ui-label-line-height);
591
+ font-weight: 400;
592
+ font-variant-numeric: tabular-nums;
593
+ color: var(--muted-foreground);
594
+ cursor: pointer;
595
+ }
596
+ .pretui-cardpills-all:hover {
597
+ color: var(--foreground);
598
+ }
599
+ .pretui-cardpills-all:focus-visible,
600
+ .pretui-cardpills-act:focus-visible {
601
+ outline: 2px solid var(--ring);
602
+ outline-offset: 1px;
603
+ }
604
+ /* the peek: a hover card under the pill with the fitted card. It
605
+ fades in after a beat (a CSS delay, no clock), so a pointer
606
+ passing over pills does not flash them */
607
+ .pretui-cardpill-item {
608
+ position: relative;
609
+ }
610
+ .pretui-cardpill-peek {
611
+ position: absolute;
612
+ inset-block-start: calc(100% + var(--_cardpills-peek-gap));
613
+ inset-inline-start: 0;
614
+ z-index: var(--pretui-z-popover, 30);
615
+ display: grid;
616
+ gap: var(--boxel-sp-xs);
617
+ width: 15rem;
618
+ padding: var(--boxel-sp-xs);
619
+ border-radius: var(--boxel-border-radius-lg);
620
+ background-color: var(--popover);
621
+ color: var(--popover-foreground);
622
+ box-shadow:
623
+ 0 0 0 1px var(--border),
624
+ var(--shadow-md);
625
+ animation: pretui-peek-in 160ms ease-out 280ms both;
626
+ }
627
+ /* a bridge over the gap, so moving from the pill into the peek
628
+ never leaves the item */
629
+ .pretui-cardpill-peek::before {
630
+ content: '';
631
+ position: absolute;
632
+ inset-inline: 0;
633
+ inset-block-start: calc(-1 * var(--_cardpills-peek-gap));
634
+ height: var(--_cardpills-peek-gap);
635
+ }
636
+ @keyframes pretui-peek-in {
637
+ from {
638
+ opacity: 0;
639
+ transform: translateY(-0.125rem);
640
+ }
641
+ to {
642
+ opacity: 1;
643
+ transform: none;
644
+ }
645
+ }
646
+ .pretui-cardpill-peek-card {
647
+ display: grid;
648
+ min-height: 4rem;
649
+ overflow: hidden;
650
+ border-radius: var(--boxel-border-radius-sm);
651
+ }
652
+ .pretui-cardpill-peek-actions {
653
+ display: flex;
654
+ gap: var(--boxel-sp-4xs);
655
+ justify-content: flex-end;
656
+ }
657
+ /* the card shown in place, embedded, under the set */
658
+ .pretui-cardpills-expanded {
659
+ display: grid;
660
+ gap: var(--boxel-sp-xs);
661
+ justify-self: stretch;
662
+ }
663
+ .pretui-cardpills-expanded-head {
664
+ display: flex;
665
+ align-items: center;
666
+ justify-content: space-between;
667
+ font-size: var(--boxel-ui-label-font-size);
668
+ line-height: var(--boxel-ui-label-line-height);
669
+ font-weight: 600;
670
+ color: var(--foreground);
671
+ }
672
+ @media (prefers-reduced-motion: reduce) {
673
+ .pretui-cardpill-peek {
674
+ animation: none;
675
+ }
676
+ }
677
+ /* what to do with the picked cards, under the set */
678
+ .pretui-cardpills-bar {
679
+ display: flex;
680
+ align-items: center;
681
+ gap: var(--boxel-sp-xs);
682
+ justify-self: stretch;
683
+ padding: var(--boxel-sp-6xs) var(--boxel-sp-6xs) var(--boxel-sp-6xs)
684
+ var(--boxel-sp-sm);
685
+ border-radius: var(--boxel-border-radius-sm);
686
+ background-color: var(--inset);
687
+ color: var(--foreground);
688
+ box-shadow: inset 0 0 0 1px var(--border);
689
+ font-size: var(--boxel-ui-label-font-size);
690
+ line-height: var(--boxel-ui-label-line-height);
691
+ }
692
+ .pretui-cardpills-count {
693
+ margin-inline-end: auto;
694
+ font-weight: 600;
695
+ font-variant-numeric: tabular-nums;
696
+ }
697
+ .pretui-cardpills-act {
698
+ height: 1.75rem;
699
+ padding: 0 var(--boxel-sp-sm);
700
+ border: 0;
701
+ border-radius: var(--boxel-border-radius-sm);
702
+ background-color: transparent;
703
+ font-family: inherit;
704
+ font-size: inherit;
705
+ font-weight: 600;
706
+ color: var(--muted-foreground);
707
+ cursor: pointer;
708
+ }
709
+ /* the bar's verb is the moment's one action */
710
+ .pretui-cardpills-act[data-kind='primary'] {
711
+ background-color: var(--primary);
712
+ color: var(--primary-foreground);
713
+ }
714
+ .pretui-cardpills-act:not([data-kind='primary']):hover {
715
+ color: var(--foreground);
716
+ }
717
+ @media (prefers-reduced-motion: reduce) {
718
+ .pretui-cardpill {
719
+ transition: none;
720
+ }
721
+ }
722
+ }
723
+ </style>
724
+ </template>
725
+ }