rift-ds 1.5.0 → 1.7.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.
Files changed (52) hide show
  1. package/README.md +8 -10
  2. package/bin/rift-ds.mjs +111 -37
  3. package/components/AgentRail/AgentRail.css +2 -2
  4. package/components/AppSidebar/AppSidebar.css +6 -0
  5. package/components/AppSidebar/AppSidebar.d.ts +12 -2
  6. package/components/AppSidebar/AppSidebar.js +11 -1
  7. package/components/Attachment/Attachment.css +14 -0
  8. package/components/Attachment/AttachmentGroup.d.ts +2 -2
  9. package/components/Attachment/AttachmentGroup.js +7 -2
  10. package/components/Composer/Composer.css +88 -4
  11. package/components/Composer/Composer.d.ts +21 -0
  12. package/components/Composer/Composer.js +199 -14
  13. package/components/Mention/Mention.css +195 -0
  14. package/components/Mention/Mention.d.ts +68 -0
  15. package/components/Mention/Mention.js +145 -0
  16. package/components/Mention/mentions.d.ts +81 -0
  17. package/components/Mention/mentions.js +83 -0
  18. package/components/PixelAvatar/PixelAvatar.css +79 -0
  19. package/components/PixelAvatar/PixelAvatar.d.ts +47 -0
  20. package/components/PixelAvatar/PixelAvatar.js +90 -0
  21. package/components/PixelAvatar/pixelInks.d.ts +27 -0
  22. package/components/PixelAvatar/pixelInks.js +45 -0
  23. package/components/PromptSuggestions/PromptSuggestions.css +218 -0
  24. package/components/PromptSuggestions/PromptSuggestions.d.ts +10 -3
  25. package/components/PromptSuggestions/PromptSuggestions.js +26 -3
  26. package/components/ShaderField/ShaderField.d.ts +3 -3
  27. package/components/SourceChip/SourceChip.css +211 -11
  28. package/components/SourceChip/SourceChip.d.ts +33 -10
  29. package/components/SourceChip/SourceChip.js +174 -22
  30. package/components/StatusDot/StatusDot.css +36 -13
  31. package/components/StatusDot/StatusDot.d.ts +16 -4
  32. package/components/StatusDot/StatusDot.js +25 -5
  33. package/components/ThreadPanel/ThreadPanel.css +26 -11
  34. package/components/ThreadPanel/ThreadPanel.d.ts +18 -4
  35. package/components/ThreadPanel/ThreadPanel.js +52 -10
  36. package/components/ThreadTabs/ThreadTabs.css +18 -8
  37. package/components/ThreadTabs/ThreadTabs.d.ts +8 -2
  38. package/components/ThreadTabs/ThreadTabs.js +24 -5
  39. package/components/Widget/Widget.css +207 -0
  40. package/components/Widget/Widget.d.ts +83 -0
  41. package/components/Widget/Widget.js +112 -0
  42. package/components/registry.json +26 -2
  43. package/components/registry.json.d.ts +26 -2
  44. package/components/registry.json.js +1 -1
  45. package/index.d.ts +3 -0
  46. package/index.js +21 -0
  47. package/package.json +5 -1
  48. package/tokens/registry.json +6 -0
  49. package/tokens/registry.json.d.ts +6 -0
  50. package/tokens/registry.json.js +1 -1
  51. package/tokens/tokens-dark.css +9 -0
  52. package/tokens/tokens-light.css +14 -0
@@ -77,6 +77,224 @@
77
77
  align-items: flex-start;
78
78
  }
79
79
 
80
+ /* ============================================
81
+ TILES and CARDS — blocks for a home screen
82
+ Where the chip layouts are a line of prompts,
83
+ these are a row of places to start: an even
84
+ grid that folds as the row's own width narrows,
85
+ so a docked panel folds while a full-width page
86
+ does not.
87
+ ============================================ */
88
+
89
+ /* The column floor is stated on the icon scale, the one size both blocks
90
+ are drawn from: a tile is never narrower than three cover icons, a card
91
+ than four. Four tiles or three cards sit in one row on a conversation
92
+ column and fold to two columns and one on a phone, with no breakpoint. */
93
+ .ds-prompt-suggestions--tiles,
94
+ .ds-prompt-suggestions--cards {
95
+ display: grid;
96
+ }
97
+
98
+ .ds-prompt-suggestions--tiles {
99
+ grid-template-columns: repeat(auto-fit, minmax(calc(var(--icon-size-1200) * 3), 1fr));
100
+ }
101
+
102
+ .ds-prompt-suggestions--cards {
103
+ grid-template-columns: repeat(auto-fit, minmax(calc(var(--icon-size-1200) * 4), 1fr));
104
+ }
105
+
106
+ .ds-prompt-suggestions--tiles .ds-prompt-suggestions__item,
107
+ .ds-prompt-suggestions--cards .ds-prompt-suggestions__item {
108
+ flex-shrink: 1;
109
+ min-width: 0;
110
+ }
111
+
112
+ /* One surface for both blocks: a hairline round a container that lets the
113
+ page through, blurred so text stays on a calm field over an ambient
114
+ background. The navigation-tile radius, per the radius scale. */
115
+ .ds-prompt-suggestions__tile,
116
+ .ds-prompt-suggestions__card {
117
+ display: flex;
118
+ flex-direction: column;
119
+ box-sizing: border-box;
120
+ width: 100%;
121
+ min-width: 0;
122
+ margin: 0;
123
+ border: var(--border-025) solid var(--color-bg-container-border);
124
+ border-radius: var(--radius-600);
125
+ background-color: var(--color-bg-container-primary-semi);
126
+ -webkit-backdrop-filter: blur(24px);
127
+ backdrop-filter: blur(24px);
128
+ color: inherit;
129
+ font: inherit;
130
+ text-align: left;
131
+ cursor: pointer;
132
+ }
133
+
134
+ /* A tile answers the pointer quietly, in place: the fill firms up to the
135
+ solid container and the hairline steps up to the tertiary text ink, so
136
+ the block reads as picked out without moving. */
137
+ .ds-prompt-suggestions__tile {
138
+ transition:
139
+ background-color var(--motion-duration-fast) var(--motion-ease-standard),
140
+ border-color var(--motion-duration-fast) var(--motion-ease-standard);
141
+ }
142
+
143
+ .ds-prompt-suggestions__tile:hover {
144
+ background-color: var(--color-bg-container-primary);
145
+ border-color: var(--color-text-tertiary);
146
+ }
147
+
148
+ /* A card lifts: the Card component's own hover, the same rise and the same
149
+ sanctioned shadow, so a prompt card and a navigation tile answer the
150
+ pointer the same way. */
151
+ .ds-prompt-suggestions__card {
152
+ transition:
153
+ transform var(--motion-duration-slow) var(--motion-ease-standard),
154
+ box-shadow var(--motion-duration-slow) var(--motion-ease-standard);
155
+ /* ds-allow(color): transparent resting state of the sanctioned Card
156
+ hover-lift shadow (see design.md — Do's and Don'ts). */
157
+ box-shadow: 0 0 0 rgba(0, 0, 0, 0);
158
+ }
159
+
160
+ .ds-prompt-suggestions__card:hover {
161
+ transform: translateY(-4px);
162
+ /* ds-allow(color): the sanctioned Card hover-lift shadow, restated so the
163
+ two tiles lift alike (see design.md — Do's and Don'ts). */
164
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
165
+ }
166
+
167
+ /* The pending shells are not targets, so they neither lift nor light. */
168
+ .ds-prompt-suggestions__tile--pending:hover {
169
+ background-color: var(--color-bg-container-primary-semi);
170
+ border-color: var(--color-bg-container-border);
171
+ }
172
+
173
+ .ds-prompt-suggestions__card--pending:hover {
174
+ transform: none;
175
+ box-shadow: none;
176
+ }
177
+
178
+ .ds-prompt-suggestions__tile:focus-visible,
179
+ .ds-prompt-suggestions__card:focus-visible {
180
+ outline: 2px solid var(--color-action-primary-bg);
181
+ outline-offset: 2px;
182
+ }
183
+
184
+ .ds-prompt-suggestions__tile {
185
+ gap: var(--gap-300);
186
+ padding: var(--padding-400);
187
+ }
188
+
189
+ .ds-prompt-suggestions__card {
190
+ gap: var(--gap-200);
191
+ padding: var(--padding-200) var(--padding-200) var(--padding-400);
192
+ }
193
+
194
+ .ds-prompt-suggestions__text {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: var(--gap-050);
198
+ min-width: 0;
199
+ }
200
+
201
+ .ds-prompt-suggestions__card .ds-prompt-suggestions__text {
202
+ padding-inline: var(--padding-200);
203
+ }
204
+
205
+ .ds-prompt-suggestions__label {
206
+ font-family: var(--font-paragraph-sm-emphasis-family);
207
+ font-size: var(--font-paragraph-sm-emphasis-size);
208
+ font-weight: var(--font-paragraph-sm-emphasis-weight);
209
+ line-height: var(--font-paragraph-sm-emphasis-line-height);
210
+ letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
211
+ color: var(--color-text-primary);
212
+ }
213
+
214
+ .ds-prompt-suggestions__description {
215
+ font-family: var(--font-caption-family);
216
+ font-size: var(--font-caption-size);
217
+ font-weight: var(--font-caption-weight);
218
+ line-height: var(--font-caption-line-height);
219
+ letter-spacing: var(--font-caption-letter-spacing);
220
+ color: var(--color-text-tertiary);
221
+ }
222
+
223
+ .ds-prompt-suggestions__icon {
224
+ display: inline-flex;
225
+ --icon-size: var(--icon-size-600);
226
+ color: var(--color-icon-secondary);
227
+ }
228
+
229
+ /* The card's cover: a wash of one pixel ink, strong at the top and nearly
230
+ gone by the floor, with the subject's icon standing in it in the same
231
+ ink. The inks cycle by position, so a row of cards is told apart by
232
+ colour as well as by title, and they are the theme-split set, so the icon
233
+ holds its contrast in both modes. */
234
+ .ds-prompt-suggestions__cover {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ /* Twice the cover icon's own size: the icon scale sets the proportion. */
239
+ height: calc(var(--icon-size-1200) * 2);
240
+ border-radius: var(--radius-400);
241
+ background-image: linear-gradient(
242
+ to bottom,
243
+ color-mix(in srgb, var(--ds-prompt-suggestions-tint) 24%, transparent),
244
+ color-mix(in srgb, var(--ds-prompt-suggestions-tint) 6%, transparent)
245
+ );
246
+ }
247
+
248
+ .ds-prompt-suggestions__cover .ds-prompt-suggestions__icon {
249
+ --icon-size: var(--icon-size-1200);
250
+ color: var(--ds-prompt-suggestions-tint);
251
+ }
252
+
253
+ .ds-prompt-suggestions__item:nth-child(6n + 1) { --ds-prompt-suggestions-tint: var(--color-pixel-ink-4); }
254
+ .ds-prompt-suggestions__item:nth-child(6n + 2) { --ds-prompt-suggestions-tint: var(--color-pixel-ink-1); }
255
+ .ds-prompt-suggestions__item:nth-child(6n + 3) { --ds-prompt-suggestions-tint: var(--color-pixel-ink-2); }
256
+ .ds-prompt-suggestions__item:nth-child(6n + 4) { --ds-prompt-suggestions-tint: var(--color-pixel-ink-5); }
257
+ .ds-prompt-suggestions__item:nth-child(6n + 5) { --ds-prompt-suggestions-tint: var(--color-pixel-ink-6); }
258
+ .ds-prompt-suggestions__item:nth-child(6n) { --ds-prompt-suggestions-tint: var(--color-pixel-ink-3); }
259
+
260
+ /* Pending blocks: the real shell holding the label's bar, so the row keeps
261
+ its height when the suggestions land. */
262
+ .ds-prompt-suggestions__tile--pending,
263
+ .ds-prompt-suggestions__card--pending {
264
+ cursor: default;
265
+ font-size: var(--font-paragraph-sm-emphasis-size);
266
+ }
267
+
268
+ .ds-prompt-suggestions__tile--pending {
269
+ justify-content: flex-end;
270
+ min-height: calc(
271
+ var(--icon-size-600) + var(--gap-300) + var(--font-paragraph-sm-emphasis-line-height) +
272
+ var(--font-caption-line-height) + 2 * var(--padding-400)
273
+ );
274
+ }
275
+
276
+ /* A pending card stands as tall as the loaded one it holds room for: the
277
+ cover, the label line, and two lines of description (the longest a card's
278
+ question runs at this width), so the row does not grow when they land. */
279
+ .ds-prompt-suggestions__card--pending {
280
+ min-height: calc(
281
+ var(--icon-size-1200) * 2 + var(--gap-200) + var(--font-paragraph-sm-emphasis-line-height) +
282
+ var(--gap-050) + 2 * var(--font-caption-line-height) + var(--padding-200) +
283
+ var(--padding-400) + 2 * var(--border-025)
284
+ );
285
+ }
286
+
287
+ .ds-prompt-suggestions__card--pending .ds-prompt-suggestions__skeleton-bar {
288
+ margin: var(--padding-150) var(--padding-200);
289
+ width: auto;
290
+ }
291
+
292
+ /* The chosen block takes the same press-and-spring the chips do. */
293
+ .ds-prompt-suggestions__item--chosen .ds-prompt-suggestions__tile,
294
+ .ds-prompt-suggestions__item--chosen .ds-prompt-suggestions__card {
295
+ animation: ds-prompt-suggestions-choose var(--motion-duration-slow) var(--motion-ease-spring);
296
+ }
297
+
80
298
  /* ============================================
81
299
  ITEMS
82
300
  ============================================ */
@@ -5,8 +5,10 @@ export interface PromptSuggestion {
5
5
  id: string;
6
6
  /** The suggestion text shown in the chip. */
7
7
  label: string;
8
- /** Optional leading icon — Material Symbol name (string) or custom element (ReactNode). */
8
+ /** Optional leading icon — Material Symbol name (string) or custom element (ReactNode). In the `cards` layout it stands in the card's cover. */
9
9
  icon?: string | React.ReactNode;
10
+ /** A quiet second line under the label, drawn by the `tiles` and `cards` layouts: what the tile leads to, or the full question a card asks. The chip layouts have no room for it and leave it out. */
11
+ description?: string;
10
12
  }
11
13
  /** Props owned by PromptSuggestions itself — everything else falls through to the root element. */
12
14
  type PromptSuggestionsOwnProps = {
@@ -19,9 +21,14 @@ type PromptSuggestionsOwnProps = {
19
21
  * sideways behind edge fades. `wrap` runs them across as many lines as
20
22
  * they need, for empty-state hero placements. `stack` gives each one its
21
23
  * own line, for narrow columns where a wrapped row breaks unevenly and
22
- * the ragged right edge reads as an accident.
24
+ * the ragged right edge reads as an accident. `tiles` and `cards` leave
25
+ * the chip behind for an empty-state home screen: `tiles` is an even row
26
+ * of quiet blocks, each an icon over a label and its `description`;
27
+ * `cards` gives each suggestion a tinted cover holding its icon, with the
28
+ * label and description beneath. Both fold to fewer columns as their own
29
+ * width narrows.
23
30
  */
24
- layout?: 'scroll' | 'wrap' | 'stack';
31
+ layout?: 'scroll' | 'wrap' | 'stack' | 'tiles' | 'cards';
25
32
  /**
26
33
  * Legacy alias for `layout="wrap"`; ignored when `layout` is set.
27
34
  * @deprecated Use `layout` instead, which also covers `stack`.
@@ -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: /* @__PURE__ */ jsx("span", { className: `${baseClass}__skeleton`, children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__skeleton-bar` }) }) }, index)) : suggestions.map((suggestion) => /* @__PURE__ */ jsx(
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__ */ jsx(
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,
@@ -1,8 +1,5 @@
1
1
  import { default as React } from 'react';
2
2
  import { ShaderBlob, ShaderParams } from './useShaderField';
3
- export type { ShaderBlob, ShaderParams, ShaderFieldParams, ShaderFieldState, } from './useShaderField';
4
- export { useShaderField, DEFAULT_SHADER_BLOBS, DEFAULT_SHADER_PARAMS } from './useShaderField';
5
- export { BLOB_COUNT } from './field.glsl';
6
3
  /**
7
4
  * How the renderer has resolved.
8
5
  *
@@ -66,3 +63,6 @@ export interface ShaderFieldProps extends ShaderFieldOwnProps, Omit<React.Compon
66
63
  * the clipping and any mask.
67
64
  */
68
65
  export declare const ShaderField: React.ForwardRefExoticComponent<ShaderFieldProps & React.RefAttributes<HTMLCanvasElement>>;
66
+ export type { ShaderBlob, ShaderParams, ShaderFieldParams, ShaderFieldState, } from './useShaderField';
67
+ export { useShaderField, DEFAULT_SHADER_BLOBS, DEFAULT_SHADER_PARAMS } from './useShaderField';
68
+ export { BLOB_COUNT } from './field.glsl';
@@ -9,10 +9,14 @@
9
9
  /* Layout */
10
10
  display: inline-flex;
11
11
  align-items: center;
12
- gap: var(--gap-100); /* 4px */
12
+ gap: var(--gap-200); /* 8px */
13
13
 
14
- /* Spacing */
15
- padding: var(--padding-050) var(--padding-200); /* 2px 8px */
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
- border-radius: var(--radius-pill);
49
- background-color: var(--color-bg-container-tertiary);
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-tertiary);
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
- LINK (rendered as <a>)
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--link {
142
+ .ds-source-chip--interactive {
87
143
  cursor: pointer;
88
144
  }
89
145
 
90
- .ds-source-chip--link:hover {
91
- background-color: var(--color-bg-container-tertiary);
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--link:focus-visible {
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
- * Leading icon — a Material Symbol name (string) or custom element (ReactNode).
10
- * The icon and the index share the leading slot: when both are passed, `index` wins
11
- * and the icon is not rendered.
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 (or icon) beside a truncating
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
- * Renders an `<a>` when `href` is supplied, a plain `<span>` otherwise.
28
- * Forwards a ref to whichever element it renders, and spreads unrecognised
29
- * props onto it — `target` and `rel` pass through as native anchor attributes.
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 {};