@robr0/design-system 0.4.0 → 0.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 +21 -10
- package/components/AgentStatus/AgentStatus.css +82 -22
- package/components/AgentStatus/AgentStatus.d.ts +6 -2
- package/components/AgentStatus/AgentStatus.js +38 -6
- package/components/AiButton/AiButton.css +163 -0
- package/components/AiButton/AiButton.d.ts +36 -0
- package/components/AiButton/AiButton.js +58 -0
- package/components/ChatHeader/ChatHeader.css +35 -0
- package/components/ChatHeader/ChatHeader.d.ts +30 -0
- package/components/ChatHeader/ChatHeader.js +17 -0
- package/components/ChatMarker/ChatMarker.css +52 -0
- package/components/ChatMarker/ChatMarker.d.ts +22 -0
- package/components/ChatMarker/ChatMarker.js +18 -0
- package/components/ChatMessage/ChatMessage.css +310 -0
- package/components/ChatMessage/ChatMessage.d.ts +74 -0
- package/components/ChatMessage/ChatMessage.js +71 -0
- package/components/ChatThread/ChatThread.css +171 -0
- package/components/ChatThread/ChatThread.d.ts +41 -0
- package/components/ChatThread/ChatThread.js +174 -0
- package/components/Chip/Chip.css +27 -0
- package/components/Chip/Chip.d.ts +2 -2
- package/components/CircularButton/CircularButton.d.ts +23 -8
- package/components/CircularButton/CircularButton.js +54 -52
- package/components/CodeBlock/CodeBlock.css +2 -2
- package/components/Composer/Composer.css +163 -0
- package/components/Composer/Composer.d.ts +67 -0
- package/components/Composer/Composer.js +120 -0
- package/components/DocumentChip/DocumentChip.css +181 -0
- package/components/DocumentChip/DocumentChip.d.ts +41 -0
- package/components/DocumentChip/DocumentChip.js +78 -0
- package/components/InterruptCard/InterruptCard.css +164 -0
- package/components/InterruptCard/InterruptCard.d.ts +55 -0
- package/components/InterruptCard/InterruptCard.js +57 -0
- package/components/MessageActions/MessageActions.css +76 -0
- package/components/MessageActions/MessageActions.d.ts +38 -0
- package/components/MessageActions/MessageActions.js +33 -0
- package/components/MessageCard/MessageCard.css +112 -0
- package/components/MessageCard/MessageCard.d.ts +35 -0
- package/components/MessageCard/MessageCard.js +28 -0
- package/components/NavList/NavList.css +155 -0
- package/components/NavList/NavList.d.ts +56 -0
- package/components/NavList/NavList.js +99 -0
- package/components/PromptSuggestions/PromptSuggestions.css +87 -0
- package/components/PromptSuggestions/PromptSuggestions.d.ts +51 -0
- package/components/PromptSuggestions/PromptSuggestions.js +34 -0
- package/components/Prose/Prose.css +252 -0
- package/components/Prose/Prose.d.ts +21 -0
- package/components/Prose/Prose.js +14 -0
- package/components/Reasoning/Reasoning.css +149 -25
- package/components/Reasoning/Reasoning.d.ts +19 -0
- package/components/Reasoning/Reasoning.js +32 -3
- package/components/SourceChip/SourceChip.css +102 -0
- package/components/SourceChip/SourceChip.d.ts +32 -0
- package/components/SourceChip/SourceChip.js +31 -0
- package/components/Stat/Stat.css +2 -2
- package/components/Timeline/Timeline.css +9 -1
- package/components/ToolCall/ToolCall.css +3 -3
- package/components/registry.json +113 -1
- package/components/registry.json.d.ts +113 -1
- package/components/registry.json.js +1 -1
- package/index.d.ts +14 -0
- package/index.js +28 -0
- package/package.json +1 -1
- package/tokens/registry.json +13 -0
- package/tokens/registry.json.d.ts +13 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +13 -0
- package/tokens/tokens-light.css +43 -0
- package/tokens/tokens-primitives.css +5 -0
- package/tokens/tokens-typography.css +24 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
COMPOSER COMPONENT
|
|
3
|
+
The chat input shell: attachments, auto-growing textarea, send/stop
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
.ds-composer {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
width: 100%;
|
|
10
|
+
/* The send button's diameter — CircularButton's default size, which is
|
|
11
|
+
a raw value in CircularButton.css too (icon-button sizes are not on
|
|
12
|
+
a token scale). Everything concentric derives from it. */
|
|
13
|
+
--ds-composer-send-size: 40px;
|
|
14
|
+
background-color: var(--color-input-bg-primary);
|
|
15
|
+
border: var(--border-xs) solid var(--color-input-border-primary);
|
|
16
|
+
/* The token's default is concentric with the send button: half the
|
|
17
|
+
button, plus the action bar's padding-sm ring, plus the border width
|
|
18
|
+
(border-radius applies to the border's outer edge). A token rather
|
|
19
|
+
than the calc so radius re-theming reaches this corner too. */
|
|
20
|
+
border-radius: var(--radius-composer);
|
|
21
|
+
box-shadow: var(--shadow-floating);
|
|
22
|
+
transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
23
|
+
/* The whole shell is the input affordance — clicking anywhere that is
|
|
24
|
+
not a control focuses the textarea, and the cursor says so. The
|
|
25
|
+
action bar below opts back out. */
|
|
26
|
+
cursor: text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* The shell carries the input focus/hover treatment — the textarea inside
|
|
30
|
+
is borderless, so the whole surface reads as one control */
|
|
31
|
+
.ds-composer:hover:not(:focus-within) {
|
|
32
|
+
border-color: var(--color-input-border-hover);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ds-composer:focus-within {
|
|
36
|
+
border-color: var(--color-input-border-selected);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ============================================
|
|
40
|
+
ATTACHMENTS ROW
|
|
41
|
+
============================================ */
|
|
42
|
+
|
|
43
|
+
.ds-composer__attachments {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
align-items: flex-end;
|
|
47
|
+
gap: var(--gap-xs);
|
|
48
|
+
/* Top ring only — the text zone below keeps its own full padding-md,
|
|
49
|
+
so chips sit a clear padding-md above the first text line */
|
|
50
|
+
padding: var(--padding-md) var(--padding-lg) 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ============================================
|
|
54
|
+
CONTENT ZONE
|
|
55
|
+
The text zone breathes more than the action bar: generous top and
|
|
56
|
+
inline padding, tighter toward the bar below it.
|
|
57
|
+
============================================ */
|
|
58
|
+
|
|
59
|
+
.ds-composer__content {
|
|
60
|
+
display: flex;
|
|
61
|
+
width: 100%;
|
|
62
|
+
padding: var(--padding-md) var(--padding-lg) var(--padding-sm);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ds-composer__textarea {
|
|
66
|
+
width: 100%;
|
|
67
|
+
padding: 0;
|
|
68
|
+
border: none;
|
|
69
|
+
outline: none;
|
|
70
|
+
background-color: transparent;
|
|
71
|
+
resize: none;
|
|
72
|
+
font-family: var(--font-paragraph-family);
|
|
73
|
+
font-size: var(--font-paragraph-size);
|
|
74
|
+
font-weight: var(--font-paragraph-weight);
|
|
75
|
+
line-height: var(--font-paragraph-line-height);
|
|
76
|
+
letter-spacing: var(--font-paragraph-letter-spacing);
|
|
77
|
+
color: var(--color-input-text-primary);
|
|
78
|
+
/* Growth cap: the shell owns the block padding, so the cap is rows × line-height.
|
|
79
|
+
--ds-composer-max-rows is set inline from the maxRows prop. */
|
|
80
|
+
max-height: calc(var(--ds-composer-max-rows, 8) * var(--font-paragraph-line-height));
|
|
81
|
+
overflow-y: auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.ds-composer__textarea::placeholder {
|
|
85
|
+
color: var(--color-input-text-placeholder);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Overflow scrollbar — thin, trackless, and quiet instead of the bulky
|
|
89
|
+
platform default. scrollbar-* covers Firefox and modern Chromium;
|
|
90
|
+
the -webkit rules cover Safari. */
|
|
91
|
+
.ds-composer__textarea {
|
|
92
|
+
scrollbar-width: thin;
|
|
93
|
+
scrollbar-color: var(--color-bg-container-secondary) transparent;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.ds-composer__textarea::-webkit-scrollbar {
|
|
97
|
+
width: 6px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.ds-composer__textarea::-webkit-scrollbar-track {
|
|
101
|
+
background: transparent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ds-composer__textarea::-webkit-scrollbar-thumb {
|
|
105
|
+
background-color: var(--color-bg-container-secondary);
|
|
106
|
+
border-radius: var(--radius-full);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Native auto-grow where supported; the JS measurement path stands down
|
|
110
|
+
(it checks the same capability) */
|
|
111
|
+
@supports (field-sizing: content) {
|
|
112
|
+
.ds-composer__textarea {
|
|
113
|
+
field-sizing: content;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ============================================
|
|
118
|
+
FOOTER ROW
|
|
119
|
+
============================================ */
|
|
120
|
+
|
|
121
|
+
.ds-composer__footer {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: var(--gap-xs);
|
|
125
|
+
/* The tight uniform ring the concentric corner is computed from */
|
|
126
|
+
padding: var(--padding-sm);
|
|
127
|
+
/* Empty bar space still focuses the textarea on click, but the bar is
|
|
128
|
+
control territory, so it keeps the default cursor. */
|
|
129
|
+
cursor: default;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ds-composer__actions {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: var(--gap-xs);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ds-composer__trailing {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: var(--gap-xs);
|
|
142
|
+
margin-left: auto;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ============================================
|
|
146
|
+
DISABLED
|
|
147
|
+
============================================ */
|
|
148
|
+
|
|
149
|
+
.ds-composer--disabled {
|
|
150
|
+
opacity: 0.4;
|
|
151
|
+
cursor: not-allowed;
|
|
152
|
+
background-color: var(--color-input-bg-disabled);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.ds-composer--disabled:hover:not(:focus-within),
|
|
156
|
+
.ds-composer--disabled:focus-within {
|
|
157
|
+
border-color: var(--color-input-border-disabled);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ds-composer--disabled .ds-composer__textarea {
|
|
161
|
+
cursor: not-allowed;
|
|
162
|
+
color: var(--color-input-text-disabled);
|
|
163
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by Composer itself — everything else falls through to the <textarea>. */
|
|
3
|
+
type ComposerOwnProps = {
|
|
4
|
+
/** Current value for controlled use. Pair with `onValueChange`. */
|
|
5
|
+
value?: string;
|
|
6
|
+
/** Initial value for uncontrolled use. */
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Convenience callback receiving the value directly.
|
|
10
|
+
* Fires alongside `onChange`, which keeps the standard React event signature
|
|
11
|
+
* so form libraries work unmodified.
|
|
12
|
+
*/
|
|
13
|
+
onValueChange?: (value: string) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Fires with the current value on Enter (without Shift) and on the send
|
|
16
|
+
* button — never while `streaming`, and never when the trimmed value is
|
|
17
|
+
* empty. Composer does not clear the value: the consumer owns it and clears
|
|
18
|
+
* it after a successful submit. Shadows the native `onSubmit` attribute,
|
|
19
|
+
* which never fires on a textarea anyway.
|
|
20
|
+
*/
|
|
21
|
+
onSubmit?: (value: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* A response is streaming: the send button becomes a stop button, submit
|
|
24
|
+
* is blocked, and Enter is inert.
|
|
25
|
+
*/
|
|
26
|
+
streaming?: boolean;
|
|
27
|
+
/** Fires when the stop button is pressed while `streaming`. */
|
|
28
|
+
onStop?: () => void;
|
|
29
|
+
/** Growth cap in text rows before the textarea scrolls internally. */
|
|
30
|
+
maxRows?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Attachment row rendered above the textarea (DocumentChips). Fully
|
|
33
|
+
* controlled by the caller — Composer never owns the list.
|
|
34
|
+
*/
|
|
35
|
+
attachments?: React.ReactNode;
|
|
36
|
+
/** Leading actions on the left of the action bar (attach button, model picker). */
|
|
37
|
+
actions?: React.ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Trailing actions on the right of the action bar, just before the send
|
|
40
|
+
* button (dictation, voice mode).
|
|
41
|
+
*/
|
|
42
|
+
trailingActions?: React.ReactNode;
|
|
43
|
+
/** Accessible label for the send button. */
|
|
44
|
+
sendLabel?: string;
|
|
45
|
+
/** Accessible label for the stop button. */
|
|
46
|
+
stopLabel?: string;
|
|
47
|
+
/** Additional CSS classes — applied to the shell, not the <textarea>. */
|
|
48
|
+
className?: string;
|
|
49
|
+
};
|
|
50
|
+
export interface ComposerProps extends ComposerOwnProps, Omit<React.ComponentPropsWithoutRef<'textarea'>, keyof ComposerOwnProps> {
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Composer is the chat input shell: an attachments row, an auto-growing
|
|
54
|
+
* textarea, a leading actions slot, and a trailing send button — the one
|
|
55
|
+
* sanctioned primary-action teal in the chat set, because sending a message
|
|
56
|
+
* is a genuine primary CTA. While `streaming`, send becomes stop and Enter
|
|
57
|
+
* is inert.
|
|
58
|
+
*
|
|
59
|
+
* The textarea grows with its content up to `maxRows`, then scrolls
|
|
60
|
+
* internally. Where the browser supports `field-sizing: content` the sizing
|
|
61
|
+
* is fully native; elsewhere a measurement effect keeps the height in step.
|
|
62
|
+
*
|
|
63
|
+
* Forwards a ref to the underlying `<textarea>` and spreads unrecognised
|
|
64
|
+
* props onto it; `className` lands on the shell.
|
|
65
|
+
*/
|
|
66
|
+
export declare const Composer: React.ForwardRefExoticComponent<ComposerProps & React.RefAttributes<HTMLTextAreaElement>>;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useRef, useState, useLayoutEffect } from "react";
|
|
3
|
+
import { CircularButton } from "../CircularButton/CircularButton.js";
|
|
4
|
+
import "./Composer.css";
|
|
5
|
+
const Composer = React.forwardRef(
|
|
6
|
+
({
|
|
7
|
+
value,
|
|
8
|
+
defaultValue,
|
|
9
|
+
onValueChange,
|
|
10
|
+
onSubmit,
|
|
11
|
+
streaming = false,
|
|
12
|
+
onStop,
|
|
13
|
+
maxRows = 8,
|
|
14
|
+
attachments,
|
|
15
|
+
actions,
|
|
16
|
+
trailingActions,
|
|
17
|
+
sendLabel = "Send message",
|
|
18
|
+
stopLabel = "Stop generating",
|
|
19
|
+
className = "",
|
|
20
|
+
onChange,
|
|
21
|
+
onKeyDown,
|
|
22
|
+
...rest
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const baseClass = "ds-composer";
|
|
25
|
+
const textareaRef = useRef(null);
|
|
26
|
+
const isControlled = value !== void 0;
|
|
27
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
28
|
+
const currentValue = isControlled ? value : uncontrolledValue;
|
|
29
|
+
const disabled = Boolean(rest.disabled);
|
|
30
|
+
const canSend = !disabled && currentValue.trim() !== "";
|
|
31
|
+
const setTextareaRef = (node) => {
|
|
32
|
+
textareaRef.current = node;
|
|
33
|
+
if (typeof ref === "function") ref(node);
|
|
34
|
+
else if (ref) ref.current = node;
|
|
35
|
+
};
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
const node = textareaRef.current;
|
|
38
|
+
if (!node) return;
|
|
39
|
+
if (typeof CSS !== "undefined" && CSS.supports("field-sizing", "content")) return;
|
|
40
|
+
node.style.height = "auto";
|
|
41
|
+
node.style.height = `${node.scrollHeight}px`;
|
|
42
|
+
}, [currentValue]);
|
|
43
|
+
const submit = () => {
|
|
44
|
+
if (streaming || !canSend) return;
|
|
45
|
+
onSubmit?.(currentValue);
|
|
46
|
+
};
|
|
47
|
+
const handleChange = (e) => {
|
|
48
|
+
if (!isControlled) setUncontrolledValue(e.target.value);
|
|
49
|
+
onChange?.(e);
|
|
50
|
+
onValueChange?.(e.target.value);
|
|
51
|
+
};
|
|
52
|
+
const handleKeyDown = (e) => {
|
|
53
|
+
onKeyDown?.(e);
|
|
54
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
submit();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const handleShellClick = (e) => {
|
|
60
|
+
const target = e.target;
|
|
61
|
+
if (target.closest('button, a, textarea, input, select, [role="button"]')) return;
|
|
62
|
+
textareaRef.current?.focus();
|
|
63
|
+
};
|
|
64
|
+
const classes = [baseClass, disabled ? `${baseClass}--disabled` : "", className].filter(Boolean).join(" ");
|
|
65
|
+
const ariaLabel = rest["aria-label"] ?? (rest["aria-labelledby"] !== void 0 ? void 0 : "Message");
|
|
66
|
+
return /* @__PURE__ */ jsxs(
|
|
67
|
+
"div",
|
|
68
|
+
{
|
|
69
|
+
className: classes,
|
|
70
|
+
style: { "--ds-composer-max-rows": maxRows },
|
|
71
|
+
onClick: handleShellClick,
|
|
72
|
+
children: [
|
|
73
|
+
attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
|
|
74
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children: /* @__PURE__ */ jsx(
|
|
75
|
+
"textarea",
|
|
76
|
+
{
|
|
77
|
+
...rest,
|
|
78
|
+
ref: setTextareaRef,
|
|
79
|
+
className: `${baseClass}__textarea`,
|
|
80
|
+
rows: 1,
|
|
81
|
+
value: currentValue,
|
|
82
|
+
"aria-label": ariaLabel,
|
|
83
|
+
onChange: handleChange,
|
|
84
|
+
onKeyDown: handleKeyDown
|
|
85
|
+
}
|
|
86
|
+
) }),
|
|
87
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
|
|
88
|
+
actions && /* @__PURE__ */ jsx("div", { className: `${baseClass}__actions`, children: actions }),
|
|
89
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__trailing`, children: [
|
|
90
|
+
trailingActions,
|
|
91
|
+
streaming ? /* @__PURE__ */ jsx(
|
|
92
|
+
CircularButton,
|
|
93
|
+
{
|
|
94
|
+
icon: "stop",
|
|
95
|
+
variant: "primary",
|
|
96
|
+
ariaLabel: stopLabel,
|
|
97
|
+
disabled,
|
|
98
|
+
onClick: onStop
|
|
99
|
+
}
|
|
100
|
+
) : /* @__PURE__ */ jsx(
|
|
101
|
+
CircularButton,
|
|
102
|
+
{
|
|
103
|
+
icon: "arrow_upward",
|
|
104
|
+
variant: "primary",
|
|
105
|
+
ariaLabel: sendLabel,
|
|
106
|
+
disabled: !canSend,
|
|
107
|
+
onClick: submit
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
] })
|
|
111
|
+
] })
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
Composer.displayName = "Composer";
|
|
118
|
+
export {
|
|
119
|
+
Composer
|
|
120
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
DOCUMENT CHIP COMPONENT
|
|
3
|
+
Compact file reference tile: type icon, name,
|
|
4
|
+
metadata line, upload progress, optional remove.
|
|
5
|
+
============================================ */
|
|
6
|
+
|
|
7
|
+
.ds-document-chip {
|
|
8
|
+
/* Accent indirection — the error modifier repoints one variable
|
|
9
|
+
instead of restyling each part */
|
|
10
|
+
--ds-document-chip-border: var(--color-bg-container-border);
|
|
11
|
+
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: var(--gap-sm);
|
|
15
|
+
|
|
16
|
+
padding: var(--padding-xs) var(--padding-sm-md);
|
|
17
|
+
|
|
18
|
+
border: var(--border-xs) solid var(--ds-document-chip-border);
|
|
19
|
+
border-radius: var(--radius-md);
|
|
20
|
+
background-color: var(--color-bg-container-primary);
|
|
21
|
+
|
|
22
|
+
transition: border-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ============================================
|
|
26
|
+
SIZE — COMPACT (single line for composer rows)
|
|
27
|
+
============================================ */
|
|
28
|
+
|
|
29
|
+
.ds-document-chip--compact {
|
|
30
|
+
gap: var(--gap-xs);
|
|
31
|
+
padding: var(--padding-xxs) var(--padding-sm);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ============================================
|
|
35
|
+
ERROR — repoint the border accent
|
|
36
|
+
============================================ */
|
|
37
|
+
|
|
38
|
+
.ds-document-chip--error {
|
|
39
|
+
--ds-document-chip-border: var(--color-status-error-border);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ============================================
|
|
43
|
+
MAIN — body button when the chip is clickable
|
|
44
|
+
============================================ */
|
|
45
|
+
|
|
46
|
+
.ds-document-chip__main {
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: inherit;
|
|
50
|
+
margin: 0;
|
|
51
|
+
padding: 0;
|
|
52
|
+
border: none;
|
|
53
|
+
background: none;
|
|
54
|
+
border-radius: var(--radius-sm);
|
|
55
|
+
font: inherit;
|
|
56
|
+
letter-spacing: inherit;
|
|
57
|
+
color: inherit;
|
|
58
|
+
text-align: left;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: background-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ds-document-chip__main:hover {
|
|
64
|
+
background-color: var(--color-bg-container-secondary);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ============================================
|
|
68
|
+
TYPE ICON
|
|
69
|
+
============================================ */
|
|
70
|
+
|
|
71
|
+
.ds-document-chip__icon {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
color: var(--color-icon-primary);
|
|
76
|
+
transition: color var(--motion-duration-base) var(--motion-ease-standard);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ds-document-chip__icon .material-symbols-rounded {
|
|
80
|
+
--icon-size: var(--icon-size-md);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ds-document-chip--compact .ds-document-chip__icon .material-symbols-rounded {
|
|
84
|
+
--icon-size: var(--icon-size-sm);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ds-document-chip--error .ds-document-chip__icon {
|
|
88
|
+
color: var(--color-status-error-text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ============================================
|
|
92
|
+
TEXT BLOCK — name over one detail line
|
|
93
|
+
============================================ */
|
|
94
|
+
|
|
95
|
+
.ds-document-chip__text {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
align-items: flex-start;
|
|
99
|
+
gap: var(--gap-xxs);
|
|
100
|
+
min-width: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ds-document-chip__name {
|
|
104
|
+
/* Consumer-overridable truncation width */
|
|
105
|
+
max-width: var(--ds-document-chip-max-width, 28ch);
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
text-overflow: ellipsis;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
|
|
110
|
+
font-family: var(--font-paragraph-sm-em-family);
|
|
111
|
+
font-size: var(--font-paragraph-sm-em-size);
|
|
112
|
+
font-weight: var(--font-paragraph-sm-em-weight);
|
|
113
|
+
line-height: var(--font-paragraph-sm-em-line-height);
|
|
114
|
+
letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
|
|
115
|
+
color: var(--color-text-primary);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ds-document-chip__meta,
|
|
119
|
+
.ds-document-chip__error {
|
|
120
|
+
font-family: var(--font-paragraph-sm-family);
|
|
121
|
+
font-size: var(--font-paragraph-sm-size);
|
|
122
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
123
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
124
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ds-document-chip__meta {
|
|
129
|
+
color: var(--color-text-tertiary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ds-document-chip__error {
|
|
133
|
+
color: var(--color-status-error-text);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Composed ProgressBar replacing the metadata line */
|
|
137
|
+
.ds-document-chip__progress {
|
|
138
|
+
align-self: stretch;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ============================================
|
|
142
|
+
REMOVE BUTTON
|
|
143
|
+
============================================ */
|
|
144
|
+
|
|
145
|
+
.ds-document-chip__remove {
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
justify-content: center;
|
|
149
|
+
flex-shrink: 0;
|
|
150
|
+
margin: 0;
|
|
151
|
+
padding: 0;
|
|
152
|
+
border: none;
|
|
153
|
+
background: none;
|
|
154
|
+
border-radius: var(--radius-full);
|
|
155
|
+
color: var(--color-icon-primary);
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
transition: color var(--motion-duration-base) var(--motion-ease-standard);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ds-document-chip__remove .material-symbols-rounded {
|
|
161
|
+
--icon-size: var(--icon-size-sm);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.ds-document-chip__remove:not(:disabled):hover {
|
|
165
|
+
color: var(--color-text-primary);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.ds-document-chip__remove:disabled {
|
|
169
|
+
opacity: 0.4;
|
|
170
|
+
cursor: not-allowed;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ============================================
|
|
174
|
+
FOCUS
|
|
175
|
+
============================================ */
|
|
176
|
+
|
|
177
|
+
.ds-document-chip__main:focus-visible,
|
|
178
|
+
.ds-document-chip__remove:focus-visible {
|
|
179
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
180
|
+
outline-offset: -2px;
|
|
181
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export type DocumentChipFileType = 'pdf' | 'doc' | 'sheet' | 'slide' | 'image' | 'code' | 'archive' | 'generic';
|
|
3
|
+
/** Props owned by DocumentChip itself — everything else falls through to the root element. */
|
|
4
|
+
type DocumentChipOwnProps = {
|
|
5
|
+
/** File name, truncated with an ellipsis when it outgrows the tile. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Document type — picks the leading Material Symbol. */
|
|
8
|
+
fileType?: DocumentChipFileType;
|
|
9
|
+
/** Override the type icon — a Material Symbol name, or any custom element. */
|
|
10
|
+
icon?: string | React.ReactNode;
|
|
11
|
+
/** Free-text metadata line, e.g. "1.2 MB" or "12 pages" — callers keep their own formatting. */
|
|
12
|
+
meta?: string;
|
|
13
|
+
/** Upload progress, 0–100. While set, replaces the metadata line with a progress bar. */
|
|
14
|
+
progress?: number;
|
|
15
|
+
/** Error message — replaces the metadata line and colours the tile with the error pair. */
|
|
16
|
+
error?: string;
|
|
17
|
+
/** Tile size. Compact drops the metadata line — name only — for dense composer rows. */
|
|
18
|
+
size?: 'default' | 'compact';
|
|
19
|
+
/** Click handler — presence makes the chip body (icon + text) an interactive button. */
|
|
20
|
+
onClick?: () => void;
|
|
21
|
+
/** Remove handler — renders a trailing close button. */
|
|
22
|
+
onRemove?: () => void;
|
|
23
|
+
/** Accessible label for the remove button. */
|
|
24
|
+
removeLabel?: string;
|
|
25
|
+
/** Additional CSS classes */
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
28
|
+
export interface DocumentChipProps extends DocumentChipOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof DocumentChipOwnProps> {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* DocumentChip is a compact file reference: a two-line tile with a type
|
|
32
|
+
* icon, name, metadata line, upload progress, and an optional remove
|
|
33
|
+
* button. It references documents attached to chat messages or queued
|
|
34
|
+
* above a composer — Chip stays the one-line pill for attributes, and
|
|
35
|
+
* FileInput stays the form control that owns selection.
|
|
36
|
+
*
|
|
37
|
+
* The root is always a `<div>`; passing `onClick` turns the body into a
|
|
38
|
+
* `<button>` so click and remove coexist without nesting controls.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DocumentChip: React.ForwardRefExoticComponent<DocumentChipProps & React.RefAttributes<HTMLDivElement>>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { ProgressBar } from "../ProgressBar/ProgressBar.js";
|
|
4
|
+
import "./DocumentChip.css";
|
|
5
|
+
import "../../fonts/material-symbols.css";
|
|
6
|
+
const FILE_TYPE_ICONS = {
|
|
7
|
+
pdf: "picture_as_pdf",
|
|
8
|
+
doc: "description",
|
|
9
|
+
sheet: "table",
|
|
10
|
+
slide: "co_present",
|
|
11
|
+
image: "image",
|
|
12
|
+
code: "code",
|
|
13
|
+
archive: "folder_zip",
|
|
14
|
+
generic: "draft"
|
|
15
|
+
};
|
|
16
|
+
const DocumentChip = React.forwardRef(
|
|
17
|
+
({
|
|
18
|
+
name,
|
|
19
|
+
fileType = "generic",
|
|
20
|
+
icon,
|
|
21
|
+
meta,
|
|
22
|
+
progress,
|
|
23
|
+
error,
|
|
24
|
+
size = "default",
|
|
25
|
+
onClick,
|
|
26
|
+
onRemove,
|
|
27
|
+
removeLabel = "Remove file",
|
|
28
|
+
className = "",
|
|
29
|
+
...rest
|
|
30
|
+
}, ref) => {
|
|
31
|
+
const baseClass = "ds-document-chip";
|
|
32
|
+
const classes = [
|
|
33
|
+
baseClass,
|
|
34
|
+
`${baseClass}--${size}`,
|
|
35
|
+
error ? `${baseClass}--error` : "",
|
|
36
|
+
onClick ? `${baseClass}--clickable` : "",
|
|
37
|
+
onRemove ? `${baseClass}--removable` : "",
|
|
38
|
+
className
|
|
39
|
+
].filter(Boolean).join(" ");
|
|
40
|
+
const typeIcon = icon ?? FILE_TYPE_ICONS[fileType];
|
|
41
|
+
const showDetail = size !== "compact";
|
|
42
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: typeof typeIcon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: typeIcon }) : typeIcon }),
|
|
44
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__text`, children: [
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__name`, children: name }),
|
|
46
|
+
showDetail && error && /* @__PURE__ */ jsx("span", { className: `${baseClass}__error`, children: error }),
|
|
47
|
+
showDetail && !error && progress !== void 0 && /* @__PURE__ */ jsx(
|
|
48
|
+
ProgressBar,
|
|
49
|
+
{
|
|
50
|
+
className: `${baseClass}__progress`,
|
|
51
|
+
size: "compact",
|
|
52
|
+
value: progress,
|
|
53
|
+
showLabel: true,
|
|
54
|
+
ariaLabel: `Uploading ${name}`
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
showDetail && !error && progress === void 0 && meta && /* @__PURE__ */ jsx("span", { className: `${baseClass}__meta`, children: meta })
|
|
58
|
+
] })
|
|
59
|
+
] });
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
61
|
+
onClick ? /* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__main`, onClick, children: body }) : body,
|
|
62
|
+
onRemove && /* @__PURE__ */ jsx(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
type: "button",
|
|
66
|
+
className: `${baseClass}__remove`,
|
|
67
|
+
onClick: onRemove,
|
|
68
|
+
"aria-label": removeLabel,
|
|
69
|
+
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "close" })
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
] });
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
DocumentChip.displayName = "DocumentChip";
|
|
76
|
+
export {
|
|
77
|
+
DocumentChip
|
|
78
|
+
};
|