@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.
- package/bundled-realms/pretui/components/agent-mark.gts +98 -0
- package/bundled-realms/pretui/components/agent-question.gts +340 -37
- package/bundled-realms/pretui/components/answer-footer.gts +249 -0
- package/bundled-realms/pretui/components/card-embed.gts +523 -0
- package/bundled-realms/pretui/components/card-pills.gts +725 -0
- package/bundled-realms/pretui/components/code-block.gts +87 -45
- package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
- package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
- package/bundled-realms/pretui/components/composer-cap.gts +516 -0
- package/bundled-realms/pretui/components/context-ring.gts +108 -0
- package/bundled-realms/pretui/components/diff-block.gts +1002 -31
- package/bundled-realms/pretui/components/diff-stat.gts +102 -0
- package/bundled-realms/pretui/components/jump-chip.gts +86 -53
- package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
- package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
- package/bundled-realms/pretui/components/plan-mark.gts +147 -0
- package/bundled-realms/pretui/components/prose.gts +185 -8
- package/bundled-realms/pretui/components/prose.md +6 -8
- package/bundled-realms/pretui/components/quick-look.gts +377 -0
- package/bundled-realms/pretui/components/reactions.gts +136 -0
- package/bundled-realms/pretui/components/recommendation.gts +67 -36
- package/bundled-realms/pretui/components/result-card.gts +58 -22
- package/bundled-realms/pretui/components/segmented-control.gts +13 -8
- package/bundled-realms/pretui/components/session-prep.gts +12 -11
- package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
- package/bundled-realms/pretui/components/speaker-label.gts +132 -0
- package/bundled-realms/pretui/components/state-glyph.gts +121 -0
- package/bundled-realms/pretui/components/step-list.gts +62 -60
- package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
- package/bundled-realms/pretui/components/system-line.gts +143 -0
- package/bundled-realms/pretui/components/task-checklist.gts +184 -0
- package/bundled-realms/pretui/components/thinking.gts +63 -94
- package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
- package/bundled-realms/pretui/components/tool-gate.gts +462 -0
- package/bundled-realms/pretui/components/tool-group.gts +920 -0
- package/bundled-realms/pretui/components/tool-icon.ts +64 -0
- package/bundled-realms/pretui/components/tool-meta.gts +82 -0
- package/bundled-realms/pretui/components/tool-plan.gts +197 -0
- package/bundled-realms/pretui/components/tool-row.gts +2087 -0
- package/bundled-realms/pretui/components/turn-changes.gts +227 -0
- package/bundled-realms/pretui/components/user-bubble.gts +575 -0
- package/bundled-realms/pretui/components/work-item.gts +185 -48
- package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
- package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
- package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
- package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
- package/bundled-realms/pretui/package.json +2 -1
- package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
- package/bundled-test-harness/assets/{app-CtezUCip.js → app-KpPqdyX3.js} +4 -4
- package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
- package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
- package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
- package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
- package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
- package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
- package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
- package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
- package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
- package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
- package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
- package/bundled-test-harness/assets/{externals-DCnB4XSS.js → externals-BWjhmCD4.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
- package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
- package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
- package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
- package/bundled-test-harness/assets/{index-query-engine-test-CY2tAkot.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
- package/bundled-test-harness/assets/{instance-filter-matcher-test-CMBTh8xn.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
- package/bundled-test-harness/assets/{main-BOZbZVem.js → main-CqAHZ2kK.js} +1 -1
- package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
- package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
- package/bundled-test-harness/assets/{operation-lowering-test-CHnjKSJb.js → operation-lowering-test-C4IaOjsA.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
- package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
- package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
- package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
- package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
- package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
- package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
- package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
- package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
- package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
- package/bundled-test-harness/assets/{searchable-option-test-Ctf8r3HZ.js → searchable-option-test-BAdOiL4U.js} +1 -1
- package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
- package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
- package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
- package/bundled-test-harness/assets/{tests-DoTEpvcF.js → tests-QYqqa6nv.js} +47 -47
- package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
- package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
- package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
- package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
- package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
- package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
- package/bundled-test-harness/index.html +8 -8
- package/bundled-test-harness/tests/index.html +8 -8
- package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
- package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Pretui — QuickLook: a sheet that shows a card or file over the transcript, with a way to open it properly.
|
|
2
|
+
import Component from '@glimmer/component';
|
|
3
|
+
import { on } from '@ember/modifier';
|
|
4
|
+
import { guidFor } from '@ember/object/internals';
|
|
5
|
+
import { modifier } from 'ember-modifier';
|
|
6
|
+
import { motion } from 'glimmer-motion';
|
|
7
|
+
import XIcon from '@cardstack/boxel-icons/x';
|
|
8
|
+
import { IconButton } from './icon-button';
|
|
9
|
+
|
|
10
|
+
// ── QuickLook ────────────────────────────────────────────────────────────
|
|
11
|
+
// A look before a commitment: the reader clicks a card or a file in the
|
|
12
|
+
// transcript and sees it here, over the transcript, without leaving the
|
|
13
|
+
// conversation. The bar says what it is (its kind, quietly, and its title)
|
|
14
|
+
// and offers the way to open it properly; Escape, Space (when the card has
|
|
15
|
+
// nothing to scroll), the close button and a click on the backdrop all put
|
|
16
|
+
// it away.
|
|
17
|
+
//
|
|
18
|
+
// It is modal: Tab cycles inside the sheet, and while it is open everything
|
|
19
|
+
// beside it in its parent is `inert`, so render it as a direct child of the
|
|
20
|
+
// region it covers (the transcript and the composer are then out of reach).
|
|
21
|
+
// Keys are handled on the sheet as they bubble, so a control inside the card
|
|
22
|
+
// (a dropdown in an edit-format card) takes its own Escape first.
|
|
23
|
+
//
|
|
24
|
+
// Presentational only. Whether it is open is the caller's state: render it
|
|
25
|
+
// to open it, stop rendering it to close it. It never waits on an animation
|
|
26
|
+
// and never keeps a "closing" state of its own (ADR 007 §1.1).
|
|
27
|
+
//
|
|
28
|
+
// Motion is declared, not performed: the sheet and the backdrop carry
|
|
29
|
+
// `{{motion id= role=}}` so a Choreo region around them can fly the sheet
|
|
30
|
+
// out of whatever opened it and fade the backdrop. Outside a region the
|
|
31
|
+
// declaration does nothing and the sheet simply appears.
|
|
32
|
+
|
|
33
|
+
export interface QuickLookSignature {
|
|
34
|
+
Args: {
|
|
35
|
+
/** the card's or the file's title: the sheet's accessible name */
|
|
36
|
+
title: string;
|
|
37
|
+
/** what it is, said quietly before the title: 'Tea lot', 'PNG image' */
|
|
38
|
+
kind?: string;
|
|
39
|
+
/** put it away: Escape, Space, the close button, the backdrop */
|
|
40
|
+
onClose: () => void;
|
|
41
|
+
/** the close button's name (default 'Close') */
|
|
42
|
+
closeLabel?: string;
|
|
43
|
+
/** where focus goes when it closes. Default: whatever had focus when
|
|
44
|
+
* it opened, the pill or embed that opened it */
|
|
45
|
+
returnFocusTo?: HTMLElement | null;
|
|
46
|
+
};
|
|
47
|
+
Blocks: {
|
|
48
|
+
/** the bar's actions: Open (the one primary action), Open in code */
|
|
49
|
+
actions: [];
|
|
50
|
+
/** the card or the file; it scrolls in its own box */
|
|
51
|
+
default: [];
|
|
52
|
+
};
|
|
53
|
+
Element: HTMLDivElement;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Space closes only when it would not do something else: typing a space,
|
|
57
|
+
// pressing a focused button or link, or toggling a control inside the card.
|
|
58
|
+
const SPACE_KEEPERS =
|
|
59
|
+
'input, textarea, select, button, a[href], summary, label, [contenteditable=""], [contenteditable="true"], [role="button"], [role="checkbox"], [role="radio"], [role="switch"], [role="textbox"], [role="combobox"], [role="slider"], [role="menuitem"], [role="option"], [role="tab"], [role="treeitem"], [role="gridcell"]';
|
|
60
|
+
|
|
61
|
+
// What Tab can land on, before the tabindex and visibility checks.
|
|
62
|
+
const TABBABLE =
|
|
63
|
+
'a[href], area[href], button, input, select, textarea, summary, iframe, audio[controls], video[controls], [contenteditable=""], [contenteditable="true"], [tabindex]';
|
|
64
|
+
|
|
65
|
+
function tabbablesIn(root: HTMLElement): HTMLElement[] {
|
|
66
|
+
return [...root.querySelectorAll<HTMLElement>(TABBABLE)].filter(
|
|
67
|
+
(el) =>
|
|
68
|
+
el.tabIndex >= 0 &&
|
|
69
|
+
!(el as HTMLButtonElement).disabled &&
|
|
70
|
+
!(el instanceof HTMLInputElement && el.type === 'hidden') &&
|
|
71
|
+
!el.closest('[inert]') &&
|
|
72
|
+
el.getClientRects().length > 0,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class QuickLook extends Component<QuickLookSignature> {
|
|
77
|
+
private guid = guidFor(this);
|
|
78
|
+
|
|
79
|
+
get titleId(): string {
|
|
80
|
+
return this.guid + '-title';
|
|
81
|
+
}
|
|
82
|
+
get closeLabel(): string {
|
|
83
|
+
return this.args.closeLabel ?? 'Close';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
close = () => {
|
|
87
|
+
this.args.onClose();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
private sheet: HTMLElement | undefined;
|
|
91
|
+
private body: HTMLElement | undefined;
|
|
92
|
+
|
|
93
|
+
/** Tab wraps between the sheet's first and last tabbable elements. */
|
|
94
|
+
private trapTab(ev: KeyboardEvent) {
|
|
95
|
+
let sheet = this.sheet;
|
|
96
|
+
if (!sheet) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let stops = tabbablesIn(sheet);
|
|
100
|
+
let first = stops[0];
|
|
101
|
+
let last = stops[stops.length - 1];
|
|
102
|
+
let active = document.activeElement;
|
|
103
|
+
if (!first || !last) {
|
|
104
|
+
ev.preventDefault();
|
|
105
|
+
sheet.focus({ preventScroll: true });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (ev.shiftKey && (active === first || active === sheet)) {
|
|
109
|
+
ev.preventDefault();
|
|
110
|
+
last.focus();
|
|
111
|
+
} else if (!ev.shiftKey && active === last) {
|
|
112
|
+
ev.preventDefault();
|
|
113
|
+
first.focus();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** the card is taller than its box: Space pages it rather than closing */
|
|
118
|
+
private get bodyScrolls(): boolean {
|
|
119
|
+
let body = this.body;
|
|
120
|
+
return Boolean(body && body.scrollHeight > body.clientHeight + 1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Tab stays in the sheet. Escape closes, unless something inside the
|
|
124
|
+
* card already used it; Space closes when focus is not somewhere a space
|
|
125
|
+
* means something else and the card has nothing to scroll. Every key the
|
|
126
|
+
* sheet uses stops here, so a host's document-level handler does not
|
|
127
|
+
* also run. */
|
|
128
|
+
onKey = (event: Event) => {
|
|
129
|
+
let ev = event as KeyboardEvent;
|
|
130
|
+
if (ev.isComposing) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (ev.key === 'Tab') {
|
|
134
|
+
this.trapTab(ev);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (ev.defaultPrevented) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (ev.key === 'Escape') {
|
|
141
|
+
ev.preventDefault();
|
|
142
|
+
ev.stopPropagation();
|
|
143
|
+
this.close();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (ev.key === ' ' || ev.key === 'Spacebar') {
|
|
147
|
+
let target = ev.target as Element | null;
|
|
148
|
+
if (target instanceof Element && target.closest(SPACE_KEEPERS)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (this.bodyScrolls) {
|
|
152
|
+
// focus inside the card: the browser pages it. Focus on the sheet
|
|
153
|
+
// itself: the card's box is not its scroller, so page it here
|
|
154
|
+
if (target === this.sheet && this.body) {
|
|
155
|
+
ev.preventDefault();
|
|
156
|
+
ev.stopPropagation();
|
|
157
|
+
let page = this.body.clientHeight * 0.875;
|
|
158
|
+
this.body.scrollBy({ top: ev.shiftKey ? -page : page });
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
ev.preventDefault();
|
|
163
|
+
ev.stopPropagation();
|
|
164
|
+
this.close();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
private registerBody = modifier((body: HTMLElement) => {
|
|
169
|
+
this.body = body;
|
|
170
|
+
return () => (this.body = undefined);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/** Everything beside the sheet in its parent is inert while it is open;
|
|
174
|
+
* what was already inert is left as it was. */
|
|
175
|
+
private inertBehind = modifier((root: HTMLElement) => {
|
|
176
|
+
let made: HTMLElement[] = [];
|
|
177
|
+
for (let sibling of root.parentElement?.children ?? []) {
|
|
178
|
+
if (
|
|
179
|
+
sibling !== root &&
|
|
180
|
+
sibling instanceof HTMLElement &&
|
|
181
|
+
!sibling.inert
|
|
182
|
+
) {
|
|
183
|
+
sibling.inert = true;
|
|
184
|
+
made.push(sibling);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return () => {
|
|
188
|
+
for (let el of made) {
|
|
189
|
+
el.inert = false;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/** Focus moves into the sheet when it is rendered, and back to what
|
|
195
|
+
* opened it when it is torn down: on the state change, never at the end
|
|
196
|
+
* of an animation (ADR 007 §1.5). */
|
|
197
|
+
private focusIn = modifier(
|
|
198
|
+
(sheet: HTMLElement, [returnTo]: [HTMLElement | null | undefined]) => {
|
|
199
|
+
this.sheet = sheet;
|
|
200
|
+
let previous =
|
|
201
|
+
returnTo ??
|
|
202
|
+
(document.activeElement instanceof HTMLElement
|
|
203
|
+
? document.activeElement
|
|
204
|
+
: null);
|
|
205
|
+
sheet.focus({ preventScroll: true });
|
|
206
|
+
return () => {
|
|
207
|
+
this.sheet = undefined;
|
|
208
|
+
let active = document.activeElement;
|
|
209
|
+
let focusWasHere =
|
|
210
|
+
!active || active === document.body || sheet.contains(active);
|
|
211
|
+
if (focusWasHere && previous?.isConnected) {
|
|
212
|
+
previous.focus({ preventScroll: true });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
<template>
|
|
219
|
+
<div class='pretui-quick-look' {{this.inertBehind}} data-test-pretui-quick-look>
|
|
220
|
+
{{! the backdrop is a pointer target only: the close button is the
|
|
221
|
+
keyboard's way out, so it stays out of the tab order and the tree }}
|
|
222
|
+
{{! template-lint-disable no-invalid-interactive }}
|
|
223
|
+
<div
|
|
224
|
+
class='pretui-quick-look-backdrop'
|
|
225
|
+
aria-hidden='true'
|
|
226
|
+
{{motion id='quick-look-backdrop' role='quick-look-backdrop'}}
|
|
227
|
+
{{on 'click' this.close}}
|
|
228
|
+
data-test-pretui-quick-look-backdrop
|
|
229
|
+
></div>
|
|
230
|
+
<div
|
|
231
|
+
class='pretui-quick-look-sheet'
|
|
232
|
+
role='dialog'
|
|
233
|
+
aria-modal='true'
|
|
234
|
+
aria-labelledby={{this.titleId}}
|
|
235
|
+
tabindex='-1'
|
|
236
|
+
{{on 'keydown' this.onKey}}
|
|
237
|
+
{{motion id='quick-look' role='quick-look'}}
|
|
238
|
+
{{this.focusIn @returnFocusTo}}
|
|
239
|
+
data-test-pretui-quick-look-sheet
|
|
240
|
+
...attributes
|
|
241
|
+
>
|
|
242
|
+
<div class='pretui-quick-look-bar'>
|
|
243
|
+
<div class='pretui-quick-look-heading'>
|
|
244
|
+
{{#if @kind}}
|
|
245
|
+
<span
|
|
246
|
+
class='pretui-quick-look-kind'
|
|
247
|
+
data-test-pretui-quick-look-kind
|
|
248
|
+
>{{@kind}}</span>
|
|
249
|
+
{{/if}}
|
|
250
|
+
<h2
|
|
251
|
+
id={{this.titleId}}
|
|
252
|
+
class='pretui-quick-look-title'
|
|
253
|
+
title={{@title}}
|
|
254
|
+
data-test-pretui-quick-look-title
|
|
255
|
+
>{{@title}}</h2>
|
|
256
|
+
</div>
|
|
257
|
+
<div
|
|
258
|
+
class='pretui-quick-look-actions'
|
|
259
|
+
data-test-pretui-quick-look-actions
|
|
260
|
+
>
|
|
261
|
+
{{yield to='actions'}}
|
|
262
|
+
</div>
|
|
263
|
+
<IconButton
|
|
264
|
+
class='pretui-quick-look-close'
|
|
265
|
+
@label={{this.closeLabel}}
|
|
266
|
+
@icon={{XIcon}}
|
|
267
|
+
@tone='neutral'
|
|
268
|
+
@appearance='plain'
|
|
269
|
+
@size='s'
|
|
270
|
+
{{on 'click' this.close}}
|
|
271
|
+
data-test-pretui-quick-look-close
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
<div
|
|
275
|
+
class='pretui-quick-look-body'
|
|
276
|
+
{{this.registerBody}}
|
|
277
|
+
data-test-pretui-quick-look-body
|
|
278
|
+
>
|
|
279
|
+
{{yield}}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<style scoped>
|
|
284
|
+
@layer PretComponent {
|
|
285
|
+
.pretui-quick-look {
|
|
286
|
+
position: absolute;
|
|
287
|
+
inset: 0;
|
|
288
|
+
z-index: var(--pretui-quick-look-z, 20);
|
|
289
|
+
}
|
|
290
|
+
.pretui-quick-look-backdrop {
|
|
291
|
+
position: absolute;
|
|
292
|
+
inset: 0;
|
|
293
|
+
background-color: var(
|
|
294
|
+
--pretui-scrim,
|
|
295
|
+
color-mix(in oklch, var(--background) 55%, transparent)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
.pretui-quick-look-sheet {
|
|
299
|
+
position: absolute;
|
|
300
|
+
inset: var(
|
|
301
|
+
--pretui-quick-look-inset,
|
|
302
|
+
var(--boxel-sp-3xl) var(--boxel-sp-sm) var(--boxel-sp-sm)
|
|
303
|
+
);
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
min-height: 0;
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
border-radius: var(--boxel-border-radius-lg);
|
|
309
|
+
background-color: var(--card);
|
|
310
|
+
color: var(--card-foreground);
|
|
311
|
+
box-shadow:
|
|
312
|
+
var(--pretui-shadow-hairline, 0 0 0 1px var(--border)),
|
|
313
|
+
var(--pretui-shadow-overlay, var(--shadow-lg));
|
|
314
|
+
}
|
|
315
|
+
.pretui-quick-look-sheet:focus {
|
|
316
|
+
outline: none;
|
|
317
|
+
}
|
|
318
|
+
.pretui-quick-look-sheet:focus-visible {
|
|
319
|
+
box-shadow:
|
|
320
|
+
0 0 0 2px var(--ring),
|
|
321
|
+
var(--pretui-shadow-overlay, var(--shadow-lg));
|
|
322
|
+
}
|
|
323
|
+
.pretui-quick-look-bar {
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
gap: var(--boxel-sp-xs);
|
|
327
|
+
min-height: var(--boxel-sp-3xl);
|
|
328
|
+
padding: var(--boxel-sp-xs) var(--boxel-sp-xs) var(--boxel-sp-xs)
|
|
329
|
+
var(--boxel-sp);
|
|
330
|
+
border-bottom: 1px solid var(--border);
|
|
331
|
+
}
|
|
332
|
+
.pretui-quick-look-heading {
|
|
333
|
+
display: flex;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
flex: 1;
|
|
336
|
+
min-width: 0;
|
|
337
|
+
}
|
|
338
|
+
.pretui-quick-look-kind {
|
|
339
|
+
font-size: var(--boxel-caption-font-size);
|
|
340
|
+
line-height: var(--boxel-caption-line-height);
|
|
341
|
+
color: var(--subtle-foreground);
|
|
342
|
+
overflow: hidden;
|
|
343
|
+
white-space: nowrap;
|
|
344
|
+
text-overflow: ellipsis;
|
|
345
|
+
}
|
|
346
|
+
.pretui-quick-look-title {
|
|
347
|
+
margin: 0;
|
|
348
|
+
font-size: var(--boxel-ui-label-font-size);
|
|
349
|
+
line-height: var(--boxel-ui-label-line-height);
|
|
350
|
+
font-weight: 600;
|
|
351
|
+
overflow: hidden;
|
|
352
|
+
white-space: nowrap;
|
|
353
|
+
text-overflow: ellipsis;
|
|
354
|
+
}
|
|
355
|
+
.pretui-quick-look-actions {
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
gap: var(--boxel-sp-xs);
|
|
359
|
+
flex-shrink: 0;
|
|
360
|
+
}
|
|
361
|
+
.pretui-quick-look-actions:empty {
|
|
362
|
+
display: none;
|
|
363
|
+
}
|
|
364
|
+
.pretui-quick-look-close {
|
|
365
|
+
flex-shrink: 0;
|
|
366
|
+
color: var(--muted-foreground);
|
|
367
|
+
}
|
|
368
|
+
.pretui-quick-look-body {
|
|
369
|
+
flex: 1;
|
|
370
|
+
min-height: 0;
|
|
371
|
+
overflow: auto;
|
|
372
|
+
overscroll-behavior: contain;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
</style>
|
|
376
|
+
</template>
|
|
377
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Pretui — Reactions: who reacted to a message or a call, as small pills under it.
|
|
2
|
+
import Component from '@glimmer/component';
|
|
3
|
+
import { on } from '@ember/modifier';
|
|
4
|
+
import { fn } from '@ember/helper';
|
|
5
|
+
|
|
6
|
+
export interface Reaction {
|
|
7
|
+
/** the emoji: '👍', '👎', '🎉' */
|
|
8
|
+
key: string;
|
|
9
|
+
/** who reacted, in order, as the room names them */
|
|
10
|
+
by: string[];
|
|
11
|
+
/** the reader is among them: the pill is pressed */
|
|
12
|
+
mine?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReactionsSignature {
|
|
16
|
+
Args: {
|
|
17
|
+
reactions: Reaction[];
|
|
18
|
+
/** toggle the reader's own reaction; omit and the pills only show */
|
|
19
|
+
onToggle?: (key: string) => void;
|
|
20
|
+
/** what the reactions are on, for the group's name: 'the read' */
|
|
21
|
+
subject?: string;
|
|
22
|
+
};
|
|
23
|
+
Element: HTMLDivElement;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* "👍 Chris" under the thing it answers, the way a chat shows a reaction:
|
|
28
|
+
* the content stays as it was, and the pill says who. Two names show, then
|
|
29
|
+
* "+N"; every name is in the pill's title and its accessible name. With
|
|
30
|
+
* `@onToggle` a pill is a pressed toggle for the reader's own reaction.
|
|
31
|
+
*/
|
|
32
|
+
export class Reactions extends Component<ReactionsSignature> {
|
|
33
|
+
names = (reaction: Reaction): string => {
|
|
34
|
+
let [a, b, ...rest] = reaction.by;
|
|
35
|
+
let shown = [a, b].filter(Boolean).join(', ');
|
|
36
|
+
return rest.length ? `${shown} +${rest.length}` : shown;
|
|
37
|
+
};
|
|
38
|
+
label = (reaction: Reaction): string =>
|
|
39
|
+
`${reaction.key} by ${reaction.by.join(', ')}`;
|
|
40
|
+
toggle = (key: string) => this.args.onToggle?.(key);
|
|
41
|
+
get groupLabel(): string {
|
|
42
|
+
return this.args.subject
|
|
43
|
+
? `Reactions to ${this.args.subject}`
|
|
44
|
+
: 'Reactions';
|
|
45
|
+
}
|
|
46
|
+
<template>
|
|
47
|
+
<div
|
|
48
|
+
class='pretui-reactions'
|
|
49
|
+
role='group'
|
|
50
|
+
aria-label={{this.groupLabel}}
|
|
51
|
+
data-test-pretui-reactions
|
|
52
|
+
...attributes
|
|
53
|
+
>
|
|
54
|
+
{{#each @reactions as |reaction|}}
|
|
55
|
+
{{#if @onToggle}}
|
|
56
|
+
<button
|
|
57
|
+
type='button'
|
|
58
|
+
class='pretui-reaction'
|
|
59
|
+
aria-pressed={{if reaction.mine 'true' 'false'}}
|
|
60
|
+
aria-label={{this.label reaction}}
|
|
61
|
+
title={{this.label reaction}}
|
|
62
|
+
{{on 'click' (fn this.toggle reaction.key)}}
|
|
63
|
+
data-test-pretui-reaction={{reaction.key}}
|
|
64
|
+
><span
|
|
65
|
+
class='pretui-reaction-key'
|
|
66
|
+
aria-hidden='true'
|
|
67
|
+
>{{reaction.key}}</span><span
|
|
68
|
+
class='pretui-reaction-by'
|
|
69
|
+
aria-hidden='true'
|
|
70
|
+
>{{this.names reaction}}</span></button>
|
|
71
|
+
{{else}}
|
|
72
|
+
<span
|
|
73
|
+
class='pretui-reaction'
|
|
74
|
+
data-mine={{if reaction.mine 'true'}}
|
|
75
|
+
role='img'
|
|
76
|
+
aria-label={{this.label reaction}}
|
|
77
|
+
title={{this.label reaction}}
|
|
78
|
+
data-test-pretui-reaction={{reaction.key}}
|
|
79
|
+
><span
|
|
80
|
+
class='pretui-reaction-key'
|
|
81
|
+
aria-hidden='true'
|
|
82
|
+
>{{reaction.key}}</span><span
|
|
83
|
+
class='pretui-reaction-by'
|
|
84
|
+
aria-hidden='true'
|
|
85
|
+
>{{this.names reaction}}</span></span>
|
|
86
|
+
{{/if}}
|
|
87
|
+
{{/each}}
|
|
88
|
+
</div>
|
|
89
|
+
<style scoped>
|
|
90
|
+
@layer PretComponent {
|
|
91
|
+
.pretui-reactions {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-wrap: wrap;
|
|
94
|
+
gap: var(--boxel-sp-4xs);
|
|
95
|
+
}
|
|
96
|
+
.pretui-reaction {
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: var(--boxel-sp-4xs);
|
|
100
|
+
/* the least a pointer target takes */
|
|
101
|
+
height: 1.5rem;
|
|
102
|
+
padding: 0 var(--boxel-sp-xs) 0 var(--boxel-sp-2xs);
|
|
103
|
+
border: 0;
|
|
104
|
+
border-radius: var(--boxel-border-radius-pill);
|
|
105
|
+
background-color: var(--muted);
|
|
106
|
+
box-shadow: inset 0 0 0 1px var(--border);
|
|
107
|
+
font-family: inherit;
|
|
108
|
+
font-size: var(--boxel-font-size-xs);
|
|
109
|
+
font-weight: 500;
|
|
110
|
+
line-height: 1;
|
|
111
|
+
color: var(--muted-foreground);
|
|
112
|
+
}
|
|
113
|
+
.pretui-reaction-key {
|
|
114
|
+
font-size: var(--boxel-font-size-xs);
|
|
115
|
+
}
|
|
116
|
+
.pretui-reaction[data-mine],
|
|
117
|
+
.pretui-reaction[aria-pressed='true'] {
|
|
118
|
+
background-color: color-mix(in oklch, var(--primary) 14%, var(--card));
|
|
119
|
+
box-shadow: inset 0 0 0 1px
|
|
120
|
+
color-mix(in oklch, var(--primary) 45%, transparent);
|
|
121
|
+
color: var(--foreground);
|
|
122
|
+
}
|
|
123
|
+
button.pretui-reaction {
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
button.pretui-reaction:hover {
|
|
127
|
+
color: var(--foreground);
|
|
128
|
+
}
|
|
129
|
+
button.pretui-reaction:focus-visible {
|
|
130
|
+
outline: 2px solid var(--ring);
|
|
131
|
+
outline-offset: 1px;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
135
|
+
</template>
|
|
136
|
+
}
|