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
package/README.md
CHANGED
|
@@ -73,13 +73,13 @@ import 'rift-ds/tokens/presets/presets.css';
|
|
|
73
73
|
|
|
74
74
|
## Components
|
|
75
75
|
|
|
76
|
-
<!-- component-count -->
|
|
76
|
+
<!-- component-count -->149<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
|
|
77
77
|
|
|
78
78
|
<details>
|
|
79
79
|
<summary>The full list</summary>
|
|
80
80
|
|
|
81
81
|
<!-- component-list:start -->
|
|
82
|
-
Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · World map
|
|
82
|
+
Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
|
|
83
83
|
<!-- component-list:end -->
|
|
84
84
|
|
|
85
85
|
</details>
|
|
@@ -227,6 +227,12 @@
|
|
|
227
227
|
flex-shrink: 0;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/* A generated avatar in the icon seat: a block-level sprite, so the seat
|
|
231
|
+
lays it out as a box rather than on the text baseline. */
|
|
232
|
+
.ds-app-sidebar__btn-icon--avatar {
|
|
233
|
+
display: inline-flex;
|
|
234
|
+
}
|
|
235
|
+
|
|
230
236
|
.ds-app-sidebar__btn-label {
|
|
231
237
|
font-family: var(--font-paragraph-emphasis-family);
|
|
232
238
|
font-size: var(--font-paragraph-emphasis-size);
|
|
@@ -23,8 +23,18 @@ export interface AppSidebarSubItem {
|
|
|
23
23
|
export interface AppSidebarItem {
|
|
24
24
|
/** Unique key for this item */
|
|
25
25
|
key: string;
|
|
26
|
-
/** Item icon — Material Symbol name (string) or custom element (ReactNode) */
|
|
27
|
-
icon
|
|
26
|
+
/** Item icon — Material Symbol name (string) or custom element (ReactNode). Optional only when `pixelAvatar` draws the mark instead */
|
|
27
|
+
icon?: string | React.ReactNode;
|
|
28
|
+
/**
|
|
29
|
+
* Draws a generated PixelAvatar in the icon seat instead of `icon`, for
|
|
30
|
+
* rows that name a thing rather than a place — projects, workspaces,
|
|
31
|
+
* accounts. `true` seeds the character from `label`; a string seeds it
|
|
32
|
+
* from that value, so a renamed item can keep its character by passing a
|
|
33
|
+
* stable id. The sidebar spreads the inks across all of its avatar rows,
|
|
34
|
+
* so neighbours differ in colour as well as shape. The character is
|
|
35
|
+
* decorative: the label beside it is the name
|
|
36
|
+
*/
|
|
37
|
+
pixelAvatar?: boolean | string;
|
|
28
38
|
/** Display label (shown when expanded) */
|
|
29
39
|
label: string;
|
|
30
40
|
/** Click handler — also fires on a link item, so a consumer can route client-side */
|
|
@@ -5,9 +5,11 @@ import { useLayer } from "../../behaviors/useLayer.js";
|
|
|
5
5
|
import { useFocusScope } from "../../behaviors/useFocusScope.js";
|
|
6
6
|
import { useScrollLock } from "../../behaviors/useScrollLock.js";
|
|
7
7
|
import { Avatar } from "../Avatar/Avatar.js";
|
|
8
|
+
import { PixelAvatar } from "../PixelAvatar/PixelAvatar.js";
|
|
8
9
|
import { SidebarPanel } from "../SidebarPanel/SidebarPanel.js";
|
|
9
10
|
import "./AppSidebar.css";
|
|
10
11
|
import "../../fonts/material-symbols.css";
|
|
12
|
+
import { distinctPixelInks } from "../PixelAvatar/pixelInks.js";
|
|
11
13
|
function DefaultLogo() {
|
|
12
14
|
return /* @__PURE__ */ jsxs(
|
|
13
15
|
"svg",
|
|
@@ -49,6 +51,14 @@ const AppSidebar = ({
|
|
|
49
51
|
showMobileTrigger = true
|
|
50
52
|
}) => {
|
|
51
53
|
const baseClass = "ds-app-sidebar";
|
|
54
|
+
const avatarSeed = (item) => typeof item.pixelAvatar === "string" ? item.pixelAvatar : item.label;
|
|
55
|
+
const avatarItems = sections.flatMap(
|
|
56
|
+
(section) => section.items.filter((item) => item.pixelAvatar)
|
|
57
|
+
);
|
|
58
|
+
const avatarInks = /* @__PURE__ */ new Map();
|
|
59
|
+
distinctPixelInks(avatarItems.map(avatarSeed)).forEach((ink, index) => {
|
|
60
|
+
avatarInks.set(avatarItems[index].key, ink);
|
|
61
|
+
});
|
|
52
62
|
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
53
63
|
const isExpanded = controlledExpanded ?? internalExpanded;
|
|
54
64
|
const onExpandedChangeRef = useRef(onExpandedChange);
|
|
@@ -281,7 +291,7 @@ const AppSidebar = ({
|
|
|
281
291
|
].filter(Boolean).join(" ");
|
|
282
292
|
const itemContent = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
283
293
|
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__btn-left`, children: [
|
|
284
|
-
/* @__PURE__ */ jsx(
|
|
294
|
+
item.pixelAvatar ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-icon ${baseClass}__btn-icon--avatar`, children: /* @__PURE__ */ jsx(PixelAvatar, { name: avatarSeed(item), ink: avatarInks.get(item.key) }) }) : /* @__PURE__ */ jsx(
|
|
285
295
|
"span",
|
|
286
296
|
{
|
|
287
297
|
className: `${baseClass}__btn-icon${typeof item.icon === "string" ? " material-symbols-rounded" : ""}`,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
PIXEL AVATAR COMPONENT
|
|
3
|
+
A generated character mark, drawn from a name
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
/* Base — the character rides the icon scale, so it seats wherever an icon
|
|
7
|
+
does. A host that already sets --icon-size on its icon seat is matched by
|
|
8
|
+
the size classes below, which set the same variable. */
|
|
9
|
+
|
|
10
|
+
.ds-pixel-avatar {
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
width: var(--icon-size);
|
|
15
|
+
height: var(--icon-size);
|
|
16
|
+
color: var(--ds-pixel-avatar-ink);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ds-pixel-avatar__sprite {
|
|
20
|
+
display: block;
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
fill: currentColor;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Sizes */
|
|
27
|
+
|
|
28
|
+
.ds-pixel-avatar--sm {
|
|
29
|
+
--icon-size: var(--icon-size-500);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ds-pixel-avatar--md {
|
|
33
|
+
--icon-size: var(--icon-size-600);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ds-pixel-avatar--lg {
|
|
37
|
+
--icon-size: var(--icon-size-800);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ds-pixel-avatar--xl {
|
|
41
|
+
--icon-size: var(--icon-size-1200);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Inks — one rule per pixel ink token. The tokens carry a light and a dark
|
|
45
|
+
value each, so the character holds its contrast in both themes. */
|
|
46
|
+
|
|
47
|
+
.ds-pixel-avatar--ink-1 {
|
|
48
|
+
--ds-pixel-avatar-ink: var(--color-pixel-ink-1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ds-pixel-avatar--ink-2 {
|
|
52
|
+
--ds-pixel-avatar-ink: var(--color-pixel-ink-2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ds-pixel-avatar--ink-3 {
|
|
56
|
+
--ds-pixel-avatar-ink: var(--color-pixel-ink-3);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ds-pixel-avatar--ink-4 {
|
|
60
|
+
--ds-pixel-avatar-ink: var(--color-pixel-ink-4);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ds-pixel-avatar--ink-5 {
|
|
64
|
+
--ds-pixel-avatar-ink: var(--color-pixel-ink-5);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ds-pixel-avatar--ink-6 {
|
|
68
|
+
--ds-pixel-avatar-ink: var(--color-pixel-ink-6);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Variants — the tile is the same sprite on a wash of its own ink. The
|
|
72
|
+
padding comes from the size, not a percentage, which would resolve against
|
|
73
|
+
the host's width rather than the tile's. */
|
|
74
|
+
|
|
75
|
+
.ds-pixel-avatar--tile {
|
|
76
|
+
padding: calc(var(--icon-size) / 16);
|
|
77
|
+
border-radius: var(--radius-200);
|
|
78
|
+
background-color: color-mix(in srgb, var(--ds-pixel-avatar-ink) 16%, transparent);
|
|
79
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { PixelInk } from './pixelInks';
|
|
3
|
+
export { distinctPixelInks, pixelAvatarInk } from './pixelInks';
|
|
4
|
+
export type { PixelInk } from './pixelInks';
|
|
5
|
+
/** Props owned by PixelAvatar itself — everything else falls through to the root span. */
|
|
6
|
+
type PixelAvatarOwnProps = {
|
|
7
|
+
/**
|
|
8
|
+
* The seed. The same name always draws the same character in the same
|
|
9
|
+
* ink, so a project, workspace or thread has its mark the moment it has a
|
|
10
|
+
* name, with nothing designed or stored for it.
|
|
11
|
+
*/
|
|
12
|
+
name: string;
|
|
13
|
+
/** Rendered size, on the icon scale (`--icon-size-500/600/800/1200` — 20, 24, 32 and 48px), so the character seats wherever an icon does. */
|
|
14
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
15
|
+
/**
|
|
16
|
+
* Overrides the ink the name picks (1 to 6, the `--color-pixel-ink-*`
|
|
17
|
+
* tokens). A name's own ink is one of six, so two neighbours can land on
|
|
18
|
+
* the same one; a host drawing a list passes each row's entry from
|
|
19
|
+
* `distinctPixelInks` here to keep them apart. The character's shape still
|
|
20
|
+
* comes from the name.
|
|
21
|
+
*/
|
|
22
|
+
ink?: PixelInk;
|
|
23
|
+
/** `plain` draws the character alone; `tile` seats it on a rounded wash of its own ink, for a larger standalone mark. */
|
|
24
|
+
variant?: 'plain' | 'tile';
|
|
25
|
+
/**
|
|
26
|
+
* Accessible name. Omit it when the mark sits beside the name it was
|
|
27
|
+
* drawn from, which is the usual case: the character is then decorative
|
|
28
|
+
* and hidden from assistive technology. Given, the root becomes an image
|
|
29
|
+
* with this label.
|
|
30
|
+
*/
|
|
31
|
+
label?: string;
|
|
32
|
+
/** Additional CSS classes */
|
|
33
|
+
className?: string;
|
|
34
|
+
};
|
|
35
|
+
export interface PixelAvatarProps extends PixelAvatarOwnProps, Omit<React.ComponentPropsWithoutRef<'span'>, keyof PixelAvatarOwnProps> {
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* PixelAvatar — a generated character mark. The name is the seed: a small
|
|
39
|
+
* pixel creature and one of six inks are drawn from it, deterministically,
|
|
40
|
+
* so the same name is always the same character and nothing has to be
|
|
41
|
+
* designed, uploaded or stored. For the things that have a name before they
|
|
42
|
+
* have a logo — projects, workspaces, repositories, agents.
|
|
43
|
+
*
|
|
44
|
+
* Pure: no hooks, no random source and no browser API, so the server and the
|
|
45
|
+
* client draw the same sprite and it renders from a Server Component.
|
|
46
|
+
*/
|
|
47
|
+
export declare const PixelAvatar: React.ForwardRefExoticComponent<PixelAvatarProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { hashName, pixelAvatarInk } from "./pixelInks.js";
|
|
4
|
+
import { distinctPixelInks } from "./pixelInks.js";
|
|
5
|
+
import "./PixelAvatar.css";
|
|
6
|
+
const GRID = 8;
|
|
7
|
+
const MARGIN = 1;
|
|
8
|
+
const ROW_CHANCES = [
|
|
9
|
+
[0, 0.3, 0.25, 0.15],
|
|
10
|
+
// antennae
|
|
11
|
+
[0.1, 0.75, 1, 1],
|
|
12
|
+
// crown
|
|
13
|
+
[0.55, 1, 1, 1],
|
|
14
|
+
// head
|
|
15
|
+
[0.75, 1, 1, 1],
|
|
16
|
+
// eyes (one cell is carved out below)
|
|
17
|
+
[0.7, 1, 1, 1],
|
|
18
|
+
// cheeks
|
|
19
|
+
[0.35, 0.85, 0.9, 0.7],
|
|
20
|
+
// jaw
|
|
21
|
+
[0.45, 0.5, 0.45, 0.35],
|
|
22
|
+
// legs
|
|
23
|
+
[0.35, 0.3, 0.25, 0.1]
|
|
24
|
+
// feet
|
|
25
|
+
];
|
|
26
|
+
const EYE_ROW = 3;
|
|
27
|
+
function seeded(seed) {
|
|
28
|
+
let state = seed;
|
|
29
|
+
return () => {
|
|
30
|
+
state = state + 1831565813 | 0;
|
|
31
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
32
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
33
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function spriteCells(name) {
|
|
37
|
+
const next = seeded(hashName(name));
|
|
38
|
+
const eyeColumn = next() < 0.6 ? 2 : 1;
|
|
39
|
+
const cells = [];
|
|
40
|
+
ROW_CHANCES.forEach((chances, row) => {
|
|
41
|
+
chances.forEach((chance, column) => {
|
|
42
|
+
if (next() >= chance) return;
|
|
43
|
+
if (row === EYE_ROW && column === eyeColumn) return;
|
|
44
|
+
cells.push([column, row], [GRID - 1 - column, row]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return cells;
|
|
48
|
+
}
|
|
49
|
+
const PixelAvatar = React.forwardRef(
|
|
50
|
+
({ name, size = "md", variant = "plain", ink: inkOverride, label, className = "", ...rest }, ref) => {
|
|
51
|
+
const baseClass = "ds-pixel-avatar";
|
|
52
|
+
const ink = inkOverride ?? pixelAvatarInk(name);
|
|
53
|
+
const classes = [
|
|
54
|
+
baseClass,
|
|
55
|
+
`${baseClass}--${size}`,
|
|
56
|
+
`${baseClass}--${variant}`,
|
|
57
|
+
`${baseClass}--ink-${ink}`,
|
|
58
|
+
className
|
|
59
|
+
].filter(Boolean).join(" ");
|
|
60
|
+
const viewBox = `${-MARGIN} ${-MARGIN} ${GRID + MARGIN * 2} ${GRID + MARGIN * 2}`;
|
|
61
|
+
return /* @__PURE__ */ jsx(
|
|
62
|
+
"span",
|
|
63
|
+
{
|
|
64
|
+
...rest,
|
|
65
|
+
ref,
|
|
66
|
+
className: classes,
|
|
67
|
+
role: label ? "img" : void 0,
|
|
68
|
+
"aria-label": label,
|
|
69
|
+
"aria-hidden": label ? void 0 : true,
|
|
70
|
+
children: /* @__PURE__ */ jsx(
|
|
71
|
+
"svg",
|
|
72
|
+
{
|
|
73
|
+
className: `${baseClass}__sprite`,
|
|
74
|
+
viewBox,
|
|
75
|
+
shapeRendering: "crispEdges",
|
|
76
|
+
focusable: "false",
|
|
77
|
+
"aria-hidden": "true",
|
|
78
|
+
children: spriteCells(name).map(([column, row]) => /* @__PURE__ */ jsx("rect", { x: column, y: row, width: 1, height: 1 }, `${column}-${row}`))
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
PixelAvatar.displayName = "PixelAvatar";
|
|
86
|
+
export {
|
|
87
|
+
PixelAvatar,
|
|
88
|
+
distinctPixelInks,
|
|
89
|
+
pixelAvatarInk
|
|
90
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ink side of PixelAvatar, kept apart from the component so a host that
|
|
3
|
+
* lays out a list can compute its inks without rendering anything. The
|
|
4
|
+
* component re-exports everything here; this module has no subpath of its
|
|
5
|
+
* own.
|
|
6
|
+
*/
|
|
7
|
+
/** One of the six pixel inks, by token number. */
|
|
8
|
+
export type PixelInk = 1 | 2 | 3 | 4 | 5 | 6;
|
|
9
|
+
/** FNV-1a, 32-bit: a small stable hash of a name. */
|
|
10
|
+
export declare function hashName(name: string): number;
|
|
11
|
+
/** The ink a name picks for itself: stable, and one of six. */
|
|
12
|
+
export declare function pixelAvatarInk(name: string): PixelInk;
|
|
13
|
+
/**
|
|
14
|
+
* Inks for a list of names shown together, in order, so neighbours are told
|
|
15
|
+
* apart by colour as well as by shape. Each name keeps the ink it picks for
|
|
16
|
+
* itself when that ink is still among the least used in the list so far and
|
|
17
|
+
* sits clear of the row before it; otherwise it takes the next ink round the
|
|
18
|
+
* set that does. "Clear" means at least two steps away on the hue ring the
|
|
19
|
+
* inks are numbered round (red, orange, yellow, green, blue, purple), so a
|
|
20
|
+
* row never follows its own colour or the hue beside it: a muted theme
|
|
21
|
+
* draws neighbouring hues close together, and two warm tans in a row read
|
|
22
|
+
* as the same mark. The first six names therefore all differ, and a longer
|
|
23
|
+
* list repeats as evenly as it can. Deterministic
|
|
24
|
+
* for a given list, so server and client agree; a name's ink can change when
|
|
25
|
+
* the list around it does, which is the price of the guarantee.
|
|
26
|
+
*/
|
|
27
|
+
export declare function distinctPixelInks(names: readonly string[]): PixelInk[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const INKS = 6;
|
|
2
|
+
function hashName(name) {
|
|
3
|
+
let hash = 2166136261;
|
|
4
|
+
for (let i = 0; i < name.length; i += 1) {
|
|
5
|
+
hash ^= name.charCodeAt(i);
|
|
6
|
+
hash = Math.imul(hash, 16777619);
|
|
7
|
+
}
|
|
8
|
+
return hash >>> 0;
|
|
9
|
+
}
|
|
10
|
+
function pixelAvatarInk(name) {
|
|
11
|
+
return hashName(`${name}:colour`) % INKS + 1;
|
|
12
|
+
}
|
|
13
|
+
function distinctPixelInks(names) {
|
|
14
|
+
const used = new Array(INKS).fill(0);
|
|
15
|
+
const inks = [];
|
|
16
|
+
names.forEach((name) => {
|
|
17
|
+
const own = pixelAvatarInk(name) - 1;
|
|
18
|
+
const fewest = Math.min(...used);
|
|
19
|
+
const previous = inks.length > 0 ? inks[inks.length - 1] - 1 : -1;
|
|
20
|
+
let clear = -1;
|
|
21
|
+
let different = -1;
|
|
22
|
+
let any = -1;
|
|
23
|
+
for (let step = 0; step < INKS; step += 1) {
|
|
24
|
+
const candidate = (own + step) % INKS;
|
|
25
|
+
if (used[candidate] !== fewest) continue;
|
|
26
|
+
if (any === -1) any = candidate;
|
|
27
|
+
if (candidate === previous) continue;
|
|
28
|
+
if (different === -1) different = candidate;
|
|
29
|
+
const apart = Math.abs(candidate - previous);
|
|
30
|
+
if (previous === -1 || Math.min(apart, INKS - apart) >= 2) {
|
|
31
|
+
clear = candidate;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const chosen = clear !== -1 ? clear : different !== -1 ? different : any;
|
|
36
|
+
used[chosen] += 1;
|
|
37
|
+
inks.push(chosen + 1);
|
|
38
|
+
});
|
|
39
|
+
return inks;
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
distinctPixelInks,
|
|
43
|
+
hashName,
|
|
44
|
+
pixelAvatarInk
|
|
45
|
+
};
|
|
@@ -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`.
|