rift-ds 1.5.0 → 1.6.0
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/README.md +2 -2
- package/components/AppSidebar/AppSidebar.css +6 -0
- package/components/AppSidebar/AppSidebar.d.ts +12 -2
- package/components/AppSidebar/AppSidebar.js +11 -1
- package/components/PixelAvatar/PixelAvatar.css +79 -0
- package/components/PixelAvatar/PixelAvatar.d.ts +47 -0
- package/components/PixelAvatar/PixelAvatar.js +90 -0
- package/components/PixelAvatar/pixelInks.d.ts +27 -0
- package/components/PixelAvatar/pixelInks.js +45 -0
- package/components/PromptSuggestions/PromptSuggestions.css +218 -0
- package/components/PromptSuggestions/PromptSuggestions.d.ts +10 -3
- package/components/PromptSuggestions/PromptSuggestions.js +26 -3
- package/components/SourceChip/SourceChip.css +211 -11
- package/components/SourceChip/SourceChip.d.ts +33 -10
- package/components/SourceChip/SourceChip.js +174 -22
- package/components/ThreadPanel/ThreadPanel.css +6 -0
- package/components/ThreadPanel/ThreadPanel.d.ts +3 -1
- package/components/ThreadPanel/ThreadPanel.js +22 -4
- package/components/Widget/Widget.css +207 -0
- package/components/Widget/Widget.d.ts +83 -0
- package/components/Widget/Widget.js +112 -0
- package/components/registry.json +17 -1
- package/components/registry.json.d.ts +17 -1
- package/components/registry.json.js +1 -1
- package/index.d.ts +2 -0
- package/index.js +10 -0
- package/package.json +1 -1
- package/tokens/registry.json +6 -0
- package/tokens/registry.json.d.ts +6 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +9 -0
- package/tokens/tokens-light.css +14 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { Chip } from "../Chip/Chip.js";
|
|
5
5
|
import { MOTION_EXIT_SYNC_MS } from "../../tokens/motion.js";
|
|
6
6
|
import "./PromptSuggestions.css";
|
|
7
|
+
import "../../fonts/material-symbols.css";
|
|
7
8
|
const PromptSuggestions = React.forwardRef(
|
|
8
9
|
({
|
|
9
10
|
suggestions,
|
|
@@ -37,6 +38,8 @@ const PromptSuggestions = React.forwardRef(
|
|
|
37
38
|
};
|
|
38
39
|
const arrangement = layout ?? (wrap ? "wrap" : "scroll");
|
|
39
40
|
const chipSize = size === "compact" ? "default" : "large";
|
|
41
|
+
const block = arrangement === "tiles" ? "tile" : arrangement === "cards" ? "card" : null;
|
|
42
|
+
const renderIcon = (icon) => typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: icon });
|
|
40
43
|
const classes = [
|
|
41
44
|
baseClass,
|
|
42
45
|
`${baseClass}--${size}`,
|
|
@@ -55,7 +58,13 @@ const PromptSuggestions = React.forwardRef(
|
|
|
55
58
|
"aria-label": ariaLabel,
|
|
56
59
|
"aria-busy": pending || void 0,
|
|
57
60
|
"data-pending": pending || void 0,
|
|
58
|
-
children: pending ? Array.from({ length: pendingCount }, (_, index) => /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: `${baseClass}__item`, children:
|
|
61
|
+
children: pending ? Array.from({ length: pendingCount }, (_, index) => /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: `${baseClass}__item`, children: block ? (
|
|
62
|
+
/* The block's own shell, holding the label's bar. */
|
|
63
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__${block} ${baseClass}__${block}--pending`, children: [
|
|
64
|
+
block === "card" && /* @__PURE__ */ jsx("span", { className: `${baseClass}__cover` }),
|
|
65
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__skeleton-bar` })
|
|
66
|
+
] })
|
|
67
|
+
) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__skeleton`, children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__skeleton-bar` }) }) }, index)) : suggestions.map((suggestion) => /* @__PURE__ */ jsx(
|
|
59
68
|
"div",
|
|
60
69
|
{
|
|
61
70
|
role: "listitem",
|
|
@@ -65,7 +74,21 @@ const PromptSuggestions = React.forwardRef(
|
|
|
65
74
|
setChosenId(null);
|
|
66
75
|
}
|
|
67
76
|
},
|
|
68
|
-
children: /* @__PURE__ */
|
|
77
|
+
children: block ? /* @__PURE__ */ jsxs(
|
|
78
|
+
"button",
|
|
79
|
+
{
|
|
80
|
+
type: "button",
|
|
81
|
+
className: `${baseClass}__${block}`,
|
|
82
|
+
onClick: () => choose(suggestion.id),
|
|
83
|
+
children: [
|
|
84
|
+
block === "card" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__cover`, children: suggestion.icon != null && renderIcon(suggestion.icon) }) : suggestion.icon != null && renderIcon(suggestion.icon),
|
|
85
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__text`, children: [
|
|
86
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: suggestion.label }),
|
|
87
|
+
suggestion.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__description`, children: suggestion.description })
|
|
88
|
+
] })
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
) : /* @__PURE__ */ jsx(
|
|
69
92
|
Chip,
|
|
70
93
|
{
|
|
71
94
|
label: suggestion.label,
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
/* Layout */
|
|
10
10
|
display: inline-flex;
|
|
11
11
|
align-items: center;
|
|
12
|
-
gap: var(--gap-
|
|
12
|
+
gap: var(--gap-200); /* 8px */
|
|
13
13
|
|
|
14
|
-
/* Spacing
|
|
15
|
-
|
|
14
|
+
/* Spacing — the leading slot is tucked into the pill's start: the same
|
|
15
|
+
inset above, below and before it, so its circle is concentric with the
|
|
16
|
+
pill's end cap. */
|
|
17
|
+
--ds-source-chip-inset: var(--padding-100); /* 4px */
|
|
18
|
+
padding: var(--ds-source-chip-inset);
|
|
19
|
+
padding-inline-end: var(--padding-300); /* 12px */
|
|
16
20
|
|
|
17
21
|
/* Shape — borderless pill on the secondary container */
|
|
18
22
|
border-radius: var(--radius-pill);
|
|
@@ -45,8 +49,14 @@
|
|
|
45
49
|
flex-shrink: 0;
|
|
46
50
|
min-width: var(--icon-size-500);
|
|
47
51
|
height: var(--icon-size-500);
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
/* Concentric with the pill: its radius less the inset between them. A
|
|
53
|
+
squared theme squares the badge with it. */
|
|
54
|
+
border-radius: calc(var(--radius-pill) - var(--ds-source-chip-inset));
|
|
55
|
+
/* A solid inverse disc, so the numeral carries the weight a logo's
|
|
56
|
+
full-colour disc does: dark on light in dark mode, light on dark in
|
|
57
|
+
light mode. It holds through hover and press — the pill's wash moves
|
|
58
|
+
behind it. */
|
|
59
|
+
background-color: var(--color-bg-container-inverse);
|
|
50
60
|
|
|
51
61
|
font-family: var(--font-paragraph-sm-emphasis-family);
|
|
52
62
|
font-size: var(--font-paragraph-sm-emphasis-size);
|
|
@@ -54,7 +64,17 @@
|
|
|
54
64
|
letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
|
|
55
65
|
line-height: 1;
|
|
56
66
|
font-variant-numeric: tabular-nums;
|
|
57
|
-
color: var(--color-text-
|
|
67
|
+
color: var(--color-text-on-inverse);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Centre the digit, not its line box. A line box reserves room for
|
|
71
|
+
descenders a lining figure never uses, so centring the box sets the
|
|
72
|
+
numeral visibly high in a 20px disc. Trimming the box to cap height and
|
|
73
|
+
baseline leaves only the glyph for the disc to centre. Browsers without
|
|
74
|
+
text-box keep the line-box centring. */
|
|
75
|
+
.ds-source-chip__numeral {
|
|
76
|
+
display: block;
|
|
77
|
+
text-box: trim-both cap alphabetic;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
/* ============================================
|
|
@@ -62,12 +82,45 @@
|
|
|
62
82
|
============================================ */
|
|
63
83
|
|
|
64
84
|
.ds-source-chip__icon {
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
justify-content: center;
|
|
65
88
|
flex-shrink: 0;
|
|
89
|
+
width: var(--icon-size-500);
|
|
90
|
+
height: var(--icon-size-500);
|
|
66
91
|
--icon-size: var(--icon-size-500);
|
|
67
92
|
line-height: 1;
|
|
68
93
|
color: var(--color-icon-primary);
|
|
69
94
|
}
|
|
70
95
|
|
|
96
|
+
/* ============================================
|
|
97
|
+
LOGO (leading, when no index; and the
|
|
98
|
+
preview's top line). The source's mark,
|
|
99
|
+
cropped to the badge's shape. An image
|
|
100
|
+
fills the box; a custom element is
|
|
101
|
+
stretched to it. The tertiary ground shows
|
|
102
|
+
through a transparent favicon, so it still
|
|
103
|
+
reads as a badge.
|
|
104
|
+
============================================ */
|
|
105
|
+
|
|
106
|
+
.ds-source-chip__logo {
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
width: var(--icon-size-500);
|
|
112
|
+
height: var(--icon-size-500);
|
|
113
|
+
border-radius: calc(var(--radius-pill) - var(--ds-source-chip-inset));
|
|
114
|
+
background-color: var(--color-bg-container-tertiary);
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ds-source-chip__logo > * {
|
|
119
|
+
width: 100%;
|
|
120
|
+
height: 100%;
|
|
121
|
+
object-fit: cover;
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
/* ============================================
|
|
72
125
|
TITLE — truncates with an ellipsis
|
|
73
126
|
============================================ */
|
|
@@ -80,15 +133,31 @@
|
|
|
80
133
|
}
|
|
81
134
|
|
|
82
135
|
/* ============================================
|
|
83
|
-
|
|
136
|
+
INTERACTIVE (rendered as <a> or <button>)
|
|
137
|
+
The passive wash, as on Chip: one step on
|
|
138
|
+
hover, one more while pressed. A chip whose
|
|
139
|
+
preview is showing holds the hover step.
|
|
84
140
|
============================================ */
|
|
85
141
|
|
|
86
|
-
.ds-source-chip--
|
|
142
|
+
.ds-source-chip--interactive {
|
|
87
143
|
cursor: pointer;
|
|
88
144
|
}
|
|
89
145
|
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
/* The <button> form: clear the UA chrome the anchor never had. */
|
|
147
|
+
button.ds-source-chip {
|
|
148
|
+
border: 0;
|
|
149
|
+
margin: 0;
|
|
150
|
+
appearance: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ds-source-chip--interactive:hover,
|
|
154
|
+
.ds-source-chip--open {
|
|
155
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
156
|
+
color: var(--color-text-primary);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.ds-source-chip--interactive:active {
|
|
160
|
+
background-color: var(--color-action-passive-bg-active);
|
|
92
161
|
color: var(--color-text-primary);
|
|
93
162
|
}
|
|
94
163
|
|
|
@@ -96,11 +165,142 @@
|
|
|
96
165
|
FOCUS
|
|
97
166
|
============================================ */
|
|
98
167
|
|
|
99
|
-
.ds-source-chip--
|
|
168
|
+
.ds-source-chip--interactive:focus-visible {
|
|
100
169
|
outline: 2px solid var(--color-action-primary-bg);
|
|
101
170
|
outline-offset: 2px;
|
|
102
171
|
}
|
|
103
172
|
|
|
173
|
+
/* ============================================
|
|
174
|
+
PREVIEW
|
|
175
|
+
The source behind the chip, on the floating
|
|
176
|
+
surface (see HoverCard). The host is the
|
|
177
|
+
positioning box; the panel opens above the
|
|
178
|
+
chip, flush with its leading edge. The
|
|
179
|
+
component flips it below (data-side) and
|
|
180
|
+
slides it sideways (--ds-source-chip-shift)
|
|
181
|
+
when the space runs out.
|
|
182
|
+
============================================ */
|
|
183
|
+
|
|
184
|
+
.ds-source-chip-host {
|
|
185
|
+
position: relative;
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
min-width: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.ds-source-chip__preview {
|
|
191
|
+
position: absolute;
|
|
192
|
+
z-index: 50;
|
|
193
|
+
bottom: 100%;
|
|
194
|
+
left: var(--ds-source-chip-shift, 0px);
|
|
195
|
+
margin-block: var(--gap-200);
|
|
196
|
+
width: max-content;
|
|
197
|
+
max-width: min(320px, calc(100vw - var(--padding-400)));
|
|
198
|
+
padding: var(--padding-300) var(--padding-400);
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
gap: var(--gap-100);
|
|
201
|
+
|
|
202
|
+
border-radius: var(--radius-300);
|
|
203
|
+
background-color: var(--color-bg-page-primary);
|
|
204
|
+
border: var(--border-025) solid var(--color-bg-container-border);
|
|
205
|
+
box-shadow: var(--shadow-floating);
|
|
206
|
+
|
|
207
|
+
font-family: var(--font-paragraph-sm-family);
|
|
208
|
+
font-size: var(--font-paragraph-sm-size);
|
|
209
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
210
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
211
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
212
|
+
color: var(--color-text-secondary);
|
|
213
|
+
text-align: start;
|
|
214
|
+
white-space: normal;
|
|
215
|
+
cursor: default;
|
|
216
|
+
|
|
217
|
+
/* display:none keeps the closed panel out of the tab order and the page's
|
|
218
|
+
scrollable overflow; the flip transitions discretely so the fade
|
|
219
|
+
survives (see HoverCard, which owns the explanation). */
|
|
220
|
+
display: none;
|
|
221
|
+
opacity: 0;
|
|
222
|
+
transform: translateY(4px);
|
|
223
|
+
transition-property: opacity, transform, display;
|
|
224
|
+
transition-duration: var(--motion-duration-fast);
|
|
225
|
+
transition-timing-function: var(--motion-ease-standard);
|
|
226
|
+
transition-behavior: allow-discrete;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.ds-source-chip__preview[data-side='bottom'] {
|
|
230
|
+
top: 100%;
|
|
231
|
+
bottom: auto;
|
|
232
|
+
transform: translateY(-4px);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.ds-source-chip__preview--visible,
|
|
236
|
+
.ds-source-chip__preview--visible[data-side='bottom'] {
|
|
237
|
+
display: flex;
|
|
238
|
+
opacity: 1;
|
|
239
|
+
transform: translateY(0);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@starting-style {
|
|
243
|
+
.ds-source-chip__preview--visible {
|
|
244
|
+
opacity: 0;
|
|
245
|
+
transform: translateY(4px);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.ds-source-chip__preview--visible[data-side='bottom'] {
|
|
249
|
+
opacity: 0;
|
|
250
|
+
transform: translateY(-4px);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Top line: where the source lives, a trailing fact, and the outbound
|
|
255
|
+
arrow when the chip is a link. */
|
|
256
|
+
.ds-source-chip__preview-top {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: var(--gap-200);
|
|
260
|
+
color: var(--color-text-tertiary);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.ds-source-chip__preview-source {
|
|
264
|
+
min-width: 0;
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
text-overflow: ellipsis;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.ds-source-chip__preview-meta {
|
|
271
|
+
flex-shrink: 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* A middle dot between the two, drawn only when both are present. */
|
|
275
|
+
.ds-source-chip__preview-source + .ds-source-chip__preview-meta::before {
|
|
276
|
+
content: '\00B7';
|
|
277
|
+
margin-inline-end: var(--gap-200);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.ds-source-chip__preview-open {
|
|
281
|
+
flex-shrink: 0;
|
|
282
|
+
margin-inline-start: auto;
|
|
283
|
+
--icon-size: var(--icon-size-500);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.ds-source-chip__preview-title {
|
|
287
|
+
font-family: var(--font-paragraph-sm-emphasis-family);
|
|
288
|
+
font-size: var(--font-paragraph-sm-emphasis-size);
|
|
289
|
+
font-weight: var(--font-paragraph-sm-emphasis-weight);
|
|
290
|
+
line-height: var(--font-paragraph-sm-emphasis-line-height);
|
|
291
|
+
letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
|
|
292
|
+
color: var(--color-text-primary);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* The excerpt is a glance, not the document: four lines, then an ellipsis. */
|
|
296
|
+
.ds-source-chip__preview-excerpt {
|
|
297
|
+
display: -webkit-box;
|
|
298
|
+
-webkit-box-orient: vertical;
|
|
299
|
+
-webkit-line-clamp: 4;
|
|
300
|
+
line-clamp: 4;
|
|
301
|
+
overflow: hidden;
|
|
302
|
+
}
|
|
303
|
+
|
|
104
304
|
/* Icons take the colour of the text around them. Explicit, not just
|
|
105
305
|
inherited by default: a consumer stylesheet that sets a colour on
|
|
106
306
|
.material-symbols-rounded itself (a common global default) would
|
|
@@ -3,16 +3,34 @@ import { default as React } from 'react';
|
|
|
3
3
|
type SourceChipOwnProps = {
|
|
4
4
|
/** The source name, e.g. "Design tokens quarterly". Truncates with an ellipsis when long. */
|
|
5
5
|
title: string;
|
|
6
|
-
/** Citation number, rendered as a leading numeral in its own small badge circle. */
|
|
6
|
+
/** Citation number, rendered as a leading numeral in its own small badge circle. Wins the leading slot over `logo` and `icon`. */
|
|
7
7
|
index?: number;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The glyph for a source with neither a number nor a logo — a Material Symbol
|
|
10
|
+
* name (string) or custom element (ReactNode). Defaults to a globe, so the
|
|
11
|
+
* leading slot is never empty.
|
|
12
12
|
*/
|
|
13
13
|
icon?: string | React.ReactNode;
|
|
14
14
|
/** Optional href — renders as an `<a>` instead of a `<span>`. */
|
|
15
15
|
href?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The passage the answer drew on. Setting it gives the chip a preview panel
|
|
18
|
+
* that opens on hover, focus or press: the full title over this excerpt.
|
|
19
|
+
* Without an `href` the chip then renders as a `<button>`, so the preview
|
|
20
|
+
* is reachable from the keyboard.
|
|
21
|
+
*/
|
|
22
|
+
excerpt?: string;
|
|
23
|
+
/** Where the source lives, shown as the preview's top line, e.g. "docs.acme.com". Only rendered with `excerpt`. */
|
|
24
|
+
source?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The source's logo or favicon — an image URL (string) or a custom element
|
|
27
|
+
* (ReactNode), cropped to a circle. It fills the chip's leading slot when
|
|
28
|
+
* there is no `index`, and always leads the preview's top line. Decorative:
|
|
29
|
+
* the title and `source` text carry the name.
|
|
30
|
+
*/
|
|
31
|
+
logo?: string | React.ReactNode;
|
|
32
|
+
/** A short trailing fact for the preview's top line, e.g. "Updated 3 days ago". Only rendered with `excerpt`. */
|
|
33
|
+
meta?: string;
|
|
16
34
|
/** Additional CSS classes */
|
|
17
35
|
className?: string;
|
|
18
36
|
};
|
|
@@ -21,12 +39,17 @@ export interface SourceChipProps extends SourceChipOwnProps, Omit<React.Componen
|
|
|
21
39
|
/**
|
|
22
40
|
* SourceChip is the numbered citation pill linking a claim to its source.
|
|
23
41
|
* It renders inline after a sentence or in a sources row under an assistant
|
|
24
|
-
* chat message: a leading citation number
|
|
25
|
-
* source title.
|
|
42
|
+
* chat message: a leading slot — the citation number, the source's logo in
|
|
43
|
+
* a circle, or a globe — beside a truncating source title.
|
|
44
|
+
*
|
|
45
|
+
* Renders an `<a>` when `href` is supplied, a `<button>` when it has a preview
|
|
46
|
+
* but nowhere to link, and a plain `<span>` otherwise. Forwards a ref to
|
|
47
|
+
* whichever element it renders, and spreads unrecognised props onto it —
|
|
48
|
+
* `target` and `rel` pass through as native anchor attributes.
|
|
26
49
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
50
|
+
* With an `excerpt` the chip previews its source: a floating panel that opens
|
|
51
|
+
* above the chip on hover or focus, flips below when there is no room, and
|
|
52
|
+
* slides sideways to stay inside whatever clips it. Escape dismisses it.
|
|
30
53
|
*/
|
|
31
|
-
export declare const SourceChip: React.ForwardRefExoticComponent<SourceChipProps & React.RefAttributes<HTMLSpanElement | HTMLAnchorElement>>;
|
|
54
|
+
export declare const SourceChip: React.ForwardRefExoticComponent<SourceChipProps & React.RefAttributes<HTMLButtonElement | HTMLSpanElement | HTMLAnchorElement>>;
|
|
32
55
|
export {};
|
|
@@ -1,30 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
+
import React, { useState, useRef, useId, useEffect, useLayoutEffect } from "react";
|
|
3
4
|
import "./SourceChip.css";
|
|
4
5
|
import "../../fonts/material-symbols.css";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
import { MOTION_HOVER_SHOW_DELAY_MS, MOTION_HOVER_HIDE_DELAY_MS } from "../../tokens/motion.js";
|
|
7
|
+
const PREVIEW_EDGE_MARGIN = 8;
|
|
8
|
+
let closeOpenPreview = null;
|
|
9
|
+
function clipBounds(el) {
|
|
10
|
+
const bounds = { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight };
|
|
11
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
12
|
+
const { overflowX, overflowY } = getComputedStyle(node);
|
|
13
|
+
const rect = node.getBoundingClientRect();
|
|
14
|
+
if (overflowX !== "visible") {
|
|
15
|
+
bounds.left = Math.max(bounds.left, rect.left);
|
|
16
|
+
bounds.right = Math.min(bounds.right, rect.right);
|
|
17
|
+
}
|
|
18
|
+
if (overflowY !== "visible") {
|
|
19
|
+
bounds.top = Math.max(bounds.top, rect.top);
|
|
20
|
+
bounds.bottom = Math.min(bounds.bottom, rect.bottom);
|
|
16
21
|
}
|
|
17
|
-
return /* @__PURE__ */ jsx(
|
|
18
|
-
"span",
|
|
19
|
-
{
|
|
20
|
-
...rest,
|
|
21
|
-
ref,
|
|
22
|
-
className: classes,
|
|
23
|
-
children: content
|
|
24
|
-
}
|
|
25
|
-
);
|
|
26
22
|
}
|
|
27
|
-
|
|
23
|
+
return bounds;
|
|
24
|
+
}
|
|
25
|
+
const SourceChip = React.forwardRef(({ title, index, icon = "language", href, excerpt, source, logo, meta, className = "", ...rest }, ref) => {
|
|
26
|
+
const baseClass = "ds-source-chip";
|
|
27
|
+
const hasPreview = Boolean(excerpt);
|
|
28
|
+
const [visible, setVisible] = useState(false);
|
|
29
|
+
const showTimeoutRef = useRef(void 0);
|
|
30
|
+
const hideTimeoutRef = useRef(void 0);
|
|
31
|
+
const hostRef = useRef(null);
|
|
32
|
+
const panelRef = useRef(null);
|
|
33
|
+
const panelId = useId();
|
|
34
|
+
const clearTimers = () => {
|
|
35
|
+
clearTimeout(showTimeoutRef.current);
|
|
36
|
+
clearTimeout(hideTimeoutRef.current);
|
|
37
|
+
};
|
|
38
|
+
const show = () => {
|
|
39
|
+
clearTimeout(hideTimeoutRef.current);
|
|
40
|
+
showTimeoutRef.current = setTimeout(() => setVisible(true), MOTION_HOVER_SHOW_DELAY_MS);
|
|
41
|
+
};
|
|
42
|
+
const hide = () => {
|
|
43
|
+
clearTimeout(showTimeoutRef.current);
|
|
44
|
+
hideTimeoutRef.current = setTimeout(() => setVisible(false), MOTION_HOVER_HIDE_DELAY_MS);
|
|
45
|
+
};
|
|
46
|
+
const showNow = () => {
|
|
47
|
+
clearTimers();
|
|
48
|
+
setVisible(true);
|
|
49
|
+
};
|
|
50
|
+
useEffect(() => clearTimers, []);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!visible) return;
|
|
53
|
+
const close = () => {
|
|
54
|
+
clearTimers();
|
|
55
|
+
setVisible(false);
|
|
56
|
+
};
|
|
57
|
+
closeOpenPreview?.();
|
|
58
|
+
closeOpenPreview = close;
|
|
59
|
+
const onKeyDown = (e) => {
|
|
60
|
+
if (e.key === "Escape") close();
|
|
61
|
+
};
|
|
62
|
+
const onPointerDown = (e) => {
|
|
63
|
+
if (!hostRef.current?.contains(e.target)) close();
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener("keydown", onKeyDown);
|
|
66
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
67
|
+
return () => {
|
|
68
|
+
if (closeOpenPreview === close) closeOpenPreview = null;
|
|
69
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
70
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
71
|
+
};
|
|
72
|
+
}, [visible]);
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
const host = hostRef.current;
|
|
75
|
+
const panel = panelRef.current;
|
|
76
|
+
if (!visible || !host || !panel) return;
|
|
77
|
+
const bounds = clipBounds(host);
|
|
78
|
+
const anchor = host.getBoundingClientRect();
|
|
79
|
+
const width = panel.offsetWidth;
|
|
80
|
+
const height = panel.offsetHeight;
|
|
81
|
+
const roomAbove = anchor.top - bounds.top - PREVIEW_EDGE_MARGIN;
|
|
82
|
+
const roomBelow = bounds.bottom - anchor.bottom - PREVIEW_EDGE_MARGIN;
|
|
83
|
+
const side = roomAbove >= height || roomAbove >= roomBelow ? "top" : "bottom";
|
|
84
|
+
const minLeft = bounds.left + PREVIEW_EDGE_MARGIN;
|
|
85
|
+
const maxLeft = bounds.right - PREVIEW_EDGE_MARGIN - width;
|
|
86
|
+
const left = Math.max(minLeft, Math.min(anchor.left, maxLeft));
|
|
87
|
+
panel.dataset.side = side;
|
|
88
|
+
panel.style.setProperty("--ds-source-chip-shift", `${Math.round(left - anchor.left)}px`);
|
|
89
|
+
}, [visible]);
|
|
90
|
+
const element = href ? "a" : hasPreview ? "button" : "span";
|
|
91
|
+
const classes = [
|
|
92
|
+
baseClass,
|
|
93
|
+
href ? `${baseClass}--link` : "",
|
|
94
|
+
element !== "span" ? `${baseClass}--interactive` : "",
|
|
95
|
+
visible ? `${baseClass}--open` : "",
|
|
96
|
+
className
|
|
97
|
+
].filter(Boolean).join(" ");
|
|
98
|
+
const logoMark = logo ? typeof logo === "string" ? /* @__PURE__ */ jsx("img", { src: logo, alt: "" }) : logo : null;
|
|
99
|
+
const leading = index !== void 0 ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__index`, children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__numeral`, children: index }) }) : logoMark ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__logo`, "aria-hidden": "true", children: logoMark }) : icon ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: icon }) : icon }) : null;
|
|
100
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
101
|
+
leading,
|
|
102
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title })
|
|
103
|
+
] });
|
|
104
|
+
const describedBy = hasPreview ? panelId : void 0;
|
|
105
|
+
const chip = element === "a" ? /* @__PURE__ */ jsx(
|
|
106
|
+
"a",
|
|
107
|
+
{
|
|
108
|
+
"aria-describedby": describedBy,
|
|
109
|
+
...rest,
|
|
110
|
+
ref,
|
|
111
|
+
className: classes,
|
|
112
|
+
href,
|
|
113
|
+
children: content
|
|
114
|
+
}
|
|
115
|
+
) : element === "button" ? /* @__PURE__ */ jsx(
|
|
116
|
+
"button",
|
|
117
|
+
{
|
|
118
|
+
type: "button",
|
|
119
|
+
"aria-describedby": describedBy,
|
|
120
|
+
...rest,
|
|
121
|
+
ref,
|
|
122
|
+
className: classes,
|
|
123
|
+
children: content
|
|
124
|
+
}
|
|
125
|
+
) : /* @__PURE__ */ jsx(
|
|
126
|
+
"span",
|
|
127
|
+
{
|
|
128
|
+
...rest,
|
|
129
|
+
ref,
|
|
130
|
+
className: classes,
|
|
131
|
+
children: content
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
if (!hasPreview) return chip;
|
|
135
|
+
const handleBlur = (event) => {
|
|
136
|
+
if (!event.currentTarget.contains(event.relatedTarget)) hide();
|
|
137
|
+
};
|
|
138
|
+
return /* @__PURE__ */ jsxs(
|
|
139
|
+
"span",
|
|
140
|
+
{
|
|
141
|
+
ref: hostRef,
|
|
142
|
+
className: `${baseClass}-host`,
|
|
143
|
+
onMouseEnter: show,
|
|
144
|
+
onMouseLeave: hide,
|
|
145
|
+
onFocus: show,
|
|
146
|
+
onBlur: handleBlur,
|
|
147
|
+
onClick: showNow,
|
|
148
|
+
children: [
|
|
149
|
+
chip,
|
|
150
|
+
/* @__PURE__ */ jsxs(
|
|
151
|
+
"span",
|
|
152
|
+
{
|
|
153
|
+
ref: panelRef,
|
|
154
|
+
id: panelId,
|
|
155
|
+
role: "tooltip",
|
|
156
|
+
className: `${baseClass}__preview${visible ? ` ${baseClass}__preview--visible` : ""}`,
|
|
157
|
+
children: [
|
|
158
|
+
(logo || source || meta || href) && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__preview-top`, children: [
|
|
159
|
+
logoMark && /* @__PURE__ */ jsx("span", { className: `${baseClass}__logo`, "aria-hidden": "true", children: logoMark }),
|
|
160
|
+
source && /* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-source`, children: source }),
|
|
161
|
+
meta && /* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-meta`, children: meta }),
|
|
162
|
+
href && /* @__PURE__ */ jsx(
|
|
163
|
+
"span",
|
|
164
|
+
{
|
|
165
|
+
className: `${baseClass}__preview-open material-symbols-rounded`,
|
|
166
|
+
"aria-hidden": "true",
|
|
167
|
+
children: "arrow_outward"
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
] }),
|
|
171
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-title`, children: title }),
|
|
172
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-excerpt`, children: excerpt })
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
});
|
|
28
180
|
SourceChip.displayName = "SourceChip";
|
|
29
181
|
export {
|
|
30
182
|
SourceChip
|
|
@@ -396,6 +396,12 @@
|
|
|
396
396
|
flex-shrink: 0;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
/* A generated avatar in the icon seat: a block-level sprite, so the seat
|
|
400
|
+
lays it out as a box rather than on the text baseline. */
|
|
401
|
+
.ds-thread-panel__project-icon--avatar {
|
|
402
|
+
display: inline-flex;
|
|
403
|
+
}
|
|
404
|
+
|
|
399
405
|
.ds-thread-panel__project-label {
|
|
400
406
|
flex: 1;
|
|
401
407
|
min-width: 0;
|
|
@@ -52,8 +52,10 @@ export interface ThreadPanelProject {
|
|
|
52
52
|
id: string;
|
|
53
53
|
/** The project's name. */
|
|
54
54
|
label: string;
|
|
55
|
-
/** Leading icon on the row — Material Symbol name (string) or custom element (ReactNode).
|
|
55
|
+
/** Leading icon on the row — Material Symbol name (string) or custom element (ReactNode). Omit it and the row draws the project's generated PixelAvatar, which is the default mark. */
|
|
56
56
|
icon?: string | React.ReactNode;
|
|
57
|
+
/** Seed for the generated PixelAvatar a project wears when it has no `icon`. Defaults to `label`; pass a stable id so a renamed project keeps its character. The panel spreads the inks across its projects, so neighbours differ in colour as well as shape. */
|
|
58
|
+
pixelAvatar?: string;
|
|
57
59
|
/** Small trailing annotation in the caption face, e.g. how recently the project was touched. */
|
|
58
60
|
meta?: string;
|
|
59
61
|
/** Optional href — the row renders as an `<a>` instead of a `<button>`. */
|