@robr0/design-system 0.4.0 → 0.5.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 (68) hide show
  1. package/README.md +2 -2
  2. package/components/AgentStatus/AgentStatus.css +82 -22
  3. package/components/AgentStatus/AgentStatus.d.ts +6 -2
  4. package/components/AgentStatus/AgentStatus.js +38 -6
  5. package/components/AiButton/AiButton.css +163 -0
  6. package/components/AiButton/AiButton.d.ts +36 -0
  7. package/components/AiButton/AiButton.js +58 -0
  8. package/components/ChatHeader/ChatHeader.css +35 -0
  9. package/components/ChatHeader/ChatHeader.d.ts +30 -0
  10. package/components/ChatHeader/ChatHeader.js +17 -0
  11. package/components/ChatMarker/ChatMarker.css +52 -0
  12. package/components/ChatMarker/ChatMarker.d.ts +22 -0
  13. package/components/ChatMarker/ChatMarker.js +18 -0
  14. package/components/ChatMessage/ChatMessage.css +304 -0
  15. package/components/ChatMessage/ChatMessage.d.ts +68 -0
  16. package/components/ChatMessage/ChatMessage.js +69 -0
  17. package/components/ChatThread/ChatThread.css +171 -0
  18. package/components/ChatThread/ChatThread.d.ts +41 -0
  19. package/components/ChatThread/ChatThread.js +174 -0
  20. package/components/Chip/Chip.css +27 -0
  21. package/components/Chip/Chip.d.ts +2 -2
  22. package/components/CircularButton/CircularButton.d.ts +23 -8
  23. package/components/CircularButton/CircularButton.js +54 -52
  24. package/components/CodeBlock/CodeBlock.css +2 -2
  25. package/components/Composer/Composer.css +164 -0
  26. package/components/Composer/Composer.d.ts +67 -0
  27. package/components/Composer/Composer.js +120 -0
  28. package/components/DocumentChip/DocumentChip.css +181 -0
  29. package/components/DocumentChip/DocumentChip.d.ts +41 -0
  30. package/components/DocumentChip/DocumentChip.js +78 -0
  31. package/components/InterruptCard/InterruptCard.css +164 -0
  32. package/components/InterruptCard/InterruptCard.d.ts +55 -0
  33. package/components/InterruptCard/InterruptCard.js +57 -0
  34. package/components/MessageActions/MessageActions.css +76 -0
  35. package/components/MessageActions/MessageActions.d.ts +38 -0
  36. package/components/MessageActions/MessageActions.js +33 -0
  37. package/components/MessageCard/MessageCard.css +112 -0
  38. package/components/MessageCard/MessageCard.d.ts +35 -0
  39. package/components/MessageCard/MessageCard.js +28 -0
  40. package/components/NavList/NavList.css +155 -0
  41. package/components/NavList/NavList.d.ts +56 -0
  42. package/components/NavList/NavList.js +99 -0
  43. package/components/PromptSuggestions/PromptSuggestions.css +87 -0
  44. package/components/PromptSuggestions/PromptSuggestions.d.ts +51 -0
  45. package/components/PromptSuggestions/PromptSuggestions.js +34 -0
  46. package/components/Prose/Prose.css +252 -0
  47. package/components/Prose/Prose.d.ts +21 -0
  48. package/components/Prose/Prose.js +14 -0
  49. package/components/Reasoning/Reasoning.css +149 -25
  50. package/components/Reasoning/Reasoning.d.ts +19 -0
  51. package/components/Reasoning/Reasoning.js +32 -3
  52. package/components/SourceChip/SourceChip.css +102 -0
  53. package/components/SourceChip/SourceChip.d.ts +32 -0
  54. package/components/SourceChip/SourceChip.js +31 -0
  55. package/components/Stat/Stat.css +2 -2
  56. package/components/ToolCall/ToolCall.css +3 -3
  57. package/components/registry.json +113 -1
  58. package/components/registry.json.d.ts +113 -1
  59. package/components/registry.json.js +1 -1
  60. package/index.d.ts +14 -0
  61. package/index.js +28 -0
  62. package/package.json +1 -1
  63. package/tokens/registry.json +12 -0
  64. package/tokens/registry.json.d.ts +12 -0
  65. package/tokens/registry.json.js +1 -1
  66. package/tokens/tokens-dark.css +13 -0
  67. package/tokens/tokens-light.css +42 -0
  68. package/tokens/tokens-typography.css +24 -0
@@ -0,0 +1,174 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import React, { useRef, useEffect, useState, useLayoutEffect } from "react";
3
+ import "./ChatThread.css";
4
+ import "../../fonts/material-symbols.css";
5
+ const SCROLLBAR_SETTLE_MS = 600;
6
+ const JUMP_THRESHOLD_PX = 24;
7
+ const ChatThread = React.forwardRef(
8
+ ({
9
+ anchor = true,
10
+ ariaLabel = "Conversation",
11
+ jumpLabel = "Scroll to the latest message",
12
+ className = "",
13
+ children,
14
+ onScroll,
15
+ ...rest
16
+ }, ref) => {
17
+ const baseClass = "ds-chat-thread";
18
+ const scrollRef = useRef(null);
19
+ const contentRef = useRef(null);
20
+ const spacerRef = useRef(null);
21
+ const prevCount = useRef(0);
22
+ const mounted = useRef(false);
23
+ const anchorTop = useRef(null);
24
+ const sizeSpacer = () => {
25
+ const container = scrollRef.current;
26
+ const content = contentRef.current;
27
+ const spacer = spacerRef.current;
28
+ const top = anchorTop.current;
29
+ if (!container || !content || !spacer || top == null) return;
30
+ const needed = Math.max(0, top + container.clientHeight - content.offsetHeight);
31
+ if (Math.abs(needed - spacer.offsetHeight) > 1) spacer.style.height = `${needed}px`;
32
+ };
33
+ useEffect(() => {
34
+ const container = scrollRef.current;
35
+ const content = contentRef.current;
36
+ if (!container || !content) return;
37
+ const syncGutter = () => {
38
+ const gutter = container.offsetWidth - container.clientWidth;
39
+ container.style.setProperty("--ds-chat-thread-gutter", `${gutter}px`);
40
+ };
41
+ const observer = new ResizeObserver(() => {
42
+ syncGutter();
43
+ const top = anchorTop.current;
44
+ const wasClamped = top != null && container.scrollTop < top && container.scrollTop >= container.scrollHeight - container.clientHeight - 1;
45
+ sizeSpacer();
46
+ if (top != null && wasClamped) container.scrollTo({ top, behavior: "instant" });
47
+ updateJump();
48
+ });
49
+ observer.observe(container);
50
+ observer.observe(content);
51
+ return () => observer.disconnect();
52
+ }, []);
53
+ const [scrolling, setScrolling] = useState(false);
54
+ const settleTimer = useRef(void 0);
55
+ const [canScrollDown, setCanScrollDown] = useState(false);
56
+ const updateJump = () => {
57
+ const container = scrollRef.current;
58
+ const content = contentRef.current;
59
+ if (!container || !content) return;
60
+ const remaining = content.offsetHeight - container.scrollTop - container.clientHeight;
61
+ const next = remaining > JUMP_THRESHOLD_PX;
62
+ setCanScrollDown((prev) => prev === next ? prev : next);
63
+ };
64
+ const setRef = (node) => {
65
+ scrollRef.current = node;
66
+ if (typeof ref === "function") ref(node);
67
+ else if (ref) ref.current = node;
68
+ };
69
+ useLayoutEffect(() => {
70
+ const container = scrollRef.current;
71
+ const content = contentRef.current;
72
+ const spacer = spacerRef.current;
73
+ if (!container || !content || !spacer) return;
74
+ const childCount = content.children.length;
75
+ const previous = prevCount.current;
76
+ const isFirstRun = !mounted.current;
77
+ mounted.current = true;
78
+ if (childCount === previous) return;
79
+ prevCount.current = childCount;
80
+ if (childCount < previous) {
81
+ anchorTop.current = null;
82
+ spacer.style.height = "0px";
83
+ container.scrollTo({ top: 0 });
84
+ return;
85
+ }
86
+ if (isFirstRun) {
87
+ if (childCount > 0 && anchor) {
88
+ const target2 = Math.max(0, content.offsetHeight - container.clientHeight);
89
+ anchorTop.current = target2;
90
+ sizeSpacer();
91
+ container.scrollTo({ top: target2, behavior: "instant" });
92
+ updateJump();
93
+ }
94
+ return;
95
+ }
96
+ if (!anchor) return;
97
+ const contentStyle = getComputedStyle(content);
98
+ const paddingTop = parseFloat(contentStyle.paddingTop) || 0;
99
+ if (previous === 0) {
100
+ const target2 = content.children[0];
101
+ if (!target2) return;
102
+ anchorTop.current = 0;
103
+ sizeSpacer();
104
+ const paddingBottom = parseFloat(contentStyle.paddingBottom) || 0;
105
+ const rise = Math.max(
106
+ 0,
107
+ container.clientHeight - target2.offsetHeight - paddingTop - paddingBottom
108
+ );
109
+ content.style.transition = "none";
110
+ content.style.transform = `translateY(${rise}px)`;
111
+ void content.offsetHeight;
112
+ content.style.transition = "";
113
+ content.style.transform = "";
114
+ return;
115
+ }
116
+ const target = content.children[previous];
117
+ if (!target) return;
118
+ const targetTop = target.offsetTop - paddingTop;
119
+ anchorTop.current = targetTop;
120
+ sizeSpacer();
121
+ container.scrollTo({ top: targetTop });
122
+ });
123
+ useEffect(updateJump);
124
+ const handleScroll = (e) => {
125
+ onScroll?.(e);
126
+ updateJump();
127
+ setScrolling(true);
128
+ window.clearTimeout(settleTimer.current);
129
+ settleTimer.current = window.setTimeout(
130
+ () => setScrolling(false),
131
+ SCROLLBAR_SETTLE_MS
132
+ );
133
+ };
134
+ const jumpToBottom = () => {
135
+ const container = scrollRef.current;
136
+ const content = contentRef.current;
137
+ if (!container || !content) return;
138
+ container.scrollTo({ top: content.offsetHeight - container.clientHeight });
139
+ };
140
+ useEffect(() => () => window.clearTimeout(settleTimer.current), []);
141
+ const classes = [baseClass, scrolling ? `${baseClass}--scrolling` : "", className].filter(Boolean).join(" ");
142
+ return /* @__PURE__ */ jsxs(
143
+ "div",
144
+ {
145
+ ...rest,
146
+ ref: setRef,
147
+ className: classes,
148
+ role: "region",
149
+ "aria-label": ariaLabel,
150
+ tabIndex: 0,
151
+ onScroll: handleScroll,
152
+ children: [
153
+ /* @__PURE__ */ jsx("div", { ref: contentRef, className: `${baseClass}__content`, children }),
154
+ /* @__PURE__ */ jsx("div", { ref: spacerRef, className: `${baseClass}__spacer`, "aria-hidden": "true" }),
155
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__jump-slot`, "aria-hidden": !canScrollDown, children: /* @__PURE__ */ jsx(
156
+ "button",
157
+ {
158
+ type: "button",
159
+ className: `${baseClass}__jump${canScrollDown ? ` ${baseClass}__jump--visible` : ""}`,
160
+ tabIndex: canScrollDown ? 0 : -1,
161
+ "aria-label": jumpLabel,
162
+ onClick: jumpToBottom,
163
+ children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__jump-icon material-symbols-rounded`, "aria-hidden": "true", children: "arrow_downward" })
164
+ }
165
+ ) })
166
+ ]
167
+ }
168
+ );
169
+ }
170
+ );
171
+ ChatThread.displayName = "ChatThread";
172
+ export {
173
+ ChatThread
174
+ };
@@ -32,6 +32,33 @@
32
32
  color var(--motion-duration-base) var(--motion-ease-standard), opacity var(--motion-duration-base) var(--motion-ease-standard);
33
33
  }
34
34
 
35
+ /* ============================================
36
+ SIZE — LARGE (40px tall)
37
+ The default Button's footprint and the body
38
+ paragraph scale, so a chip used as a primary
39
+ tap target reads at the same weight as the
40
+ text around it.
41
+ ============================================ */
42
+
43
+ .ds-chip--large {
44
+ padding: var(--padding-sm) var(--padding-lg); /* 8px 20px */
45
+ font-family: var(--font-paragraph-em-family);
46
+ font-size: var(--font-paragraph-em-size);
47
+ font-weight: var(--font-paragraph-em-weight);
48
+ line-height: var(--font-paragraph-em-line-height);
49
+ letter-spacing: var(--font-paragraph-em-letter-spacing);
50
+ }
51
+
52
+ .ds-chip--large .ds-chip__icon,
53
+ .ds-chip--large .ds-chip__remove .material-symbols-rounded {
54
+ --icon-size: var(--icon-size-md);
55
+ }
56
+
57
+ .ds-chip--large .ds-chip__icon svg {
58
+ width: 24px;
59
+ height: 24px;
60
+ }
61
+
35
62
  /* ============================================
36
63
  SIZE — COMPACT (24px tall)
37
64
  ============================================ */
@@ -8,8 +8,8 @@ export interface ChipProps {
8
8
  selected?: boolean;
9
9
  /** Disabled state */
10
10
  disabled?: boolean;
11
- /** Chip size */
12
- size?: 'default' | 'compact';
11
+ /** Chip size — `large` matches the default Button and paragraph body scale, for pills that are a primary tap target rather than metadata */
12
+ size?: 'large' | 'default' | 'compact';
13
13
  /** Click handler — presence makes the chip an interactive <button> */
14
14
  onClick?: () => void;
15
15
  /** Remove handler — renders a trailing close button (input-style chips) */
@@ -1,4 +1,6 @@
1
- export interface CircularButtonProps {
1
+ import { default as React } from 'react';
2
+ /** Props owned by CircularButton itself — everything else falls through to the DOM node. */
3
+ type CircularButtonOwnProps = {
2
4
  /** Material Symbol icon name */
3
5
  icon: string;
4
6
  /** Visual treatment */
@@ -9,7 +11,18 @@ export interface CircularButtonProps {
9
11
  * @deprecated Use `variant` instead.
10
12
  */
11
13
  priority?: 'primary' | 'secondary' | 'tertiary';
12
- /** Button state */
14
+ /** Whether the button is disabled */
15
+ disabled?: boolean;
16
+ /**
17
+ * Documentation-only interaction state.
18
+ *
19
+ * @deprecated Use `disabled` for the disabled state.
20
+ *
21
+ * Documentation-only affordance for rendering a *static* interaction state in
22
+ * Storybook and the showcase site. Real hover/active styling comes from CSS
23
+ * pseudo-classes and needs no prop — for docs, prefer
24
+ * `className="ds-circular-button--hover"`.
25
+ */
13
26
  state?: 'default' | 'hover' | 'active' | 'disabled';
14
27
  /** Button size */
15
28
  size?: 'default' | 'compact';
@@ -19,24 +32,26 @@ export interface CircularButtonProps {
19
32
  * the disabled state) and sets `aria-busy` on the rendered element.
20
33
  */
21
34
  loading?: boolean;
22
- /** Accessible label */
35
+ /** Accessible label — required, because the button has no visible text */
23
36
  ariaLabel: string;
24
- /** Optional click handler */
25
- onClick?: () => void;
26
37
  /** Optional href — renders as <a> instead of <button> */
27
38
  href?: string;
28
39
  /** Optional target attribute for links */
29
40
  target?: string;
30
41
  /** Optional rel attribute for links */
31
42
  rel?: string;
32
- /** Id of an element describing this button — set automatically by Tooltip */
33
- 'aria-describedby'?: string;
34
43
  /** Additional CSS classes */
35
44
  className?: string;
45
+ };
46
+ export interface CircularButtonProps extends CircularButtonOwnProps, Omit<React.ComponentPropsWithoutRef<'button'>, keyof CircularButtonOwnProps | 'type'> {
36
47
  }
37
48
  /**
38
49
  * Circular icon button component.
39
50
  * A round button containing a single icon, available in
40
51
  * primary, secondary and tertiary variants with default and compact sizes.
52
+ *
53
+ * Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
54
+ * whichever element it renders, and spreads unrecognised props onto it.
41
55
  */
42
- export declare const CircularButton: ({ icon, variant, priority, state, size, loading, ariaLabel, onClick, href, target, rel, "aria-describedby": ariaDescribedby, className, }: CircularButtonProps) => import("react").JSX.Element;
56
+ export declare const CircularButton: React.ForwardRefExoticComponent<CircularButtonProps & React.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
57
+ export {};
@@ -1,67 +1,69 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ import React from "react";
2
3
  import { Spinner } from "../Spinner/Spinner.js";
3
4
  import "./CircularButton.css";
4
5
  import "../../fonts/material-symbols.css";
5
- const CircularButton = ({
6
- icon,
7
- variant,
8
- priority,
9
- state = "default",
10
- size = "default",
11
- loading,
12
- ariaLabel,
13
- onClick,
14
- href,
15
- target,
16
- rel,
17
- "aria-describedby": ariaDescribedby,
18
- className = ""
19
- }) => {
20
- const baseClass = "ds-circular-button";
21
- const resolvedVariant = variant ?? priority ?? "primary";
22
- const variantClass = `${baseClass}--${resolvedVariant}`;
23
- const stateClass = `${baseClass}--${state}`;
24
- const sizeClass = `${baseClass}--${size}`;
25
- const isLoading = Boolean(loading);
26
- const classes = [
27
- baseClass,
28
- variantClass,
29
- stateClass,
30
- sizeClass,
31
- isLoading && `${baseClass}--loading`,
32
- className
33
- ].filter(Boolean).join(" ");
34
- const isDisabled = state === "disabled";
35
- const children = isLoading ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size === "compact" ? "sm" : "md", variant: "inherit" }) }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
36
- if (href && !isDisabled && !isLoading) {
6
+ const CircularButton = React.forwardRef(
7
+ ({
8
+ icon,
9
+ variant,
10
+ priority,
11
+ disabled,
12
+ state,
13
+ size = "default",
14
+ loading,
15
+ ariaLabel,
16
+ href,
17
+ target,
18
+ rel,
19
+ className = "",
20
+ ...rest
21
+ }, ref) => {
22
+ const baseClass = "ds-circular-button";
23
+ const resolvedVariant = variant ?? priority ?? "primary";
24
+ const isDisabled = disabled ?? state === "disabled";
25
+ const resolvedState = state ?? (isDisabled ? "disabled" : "default");
26
+ const isLoading = Boolean(loading);
27
+ const classes = [
28
+ baseClass,
29
+ `${baseClass}--${resolvedVariant}`,
30
+ `${baseClass}--${resolvedState}`,
31
+ `${baseClass}--${size}`,
32
+ isLoading && `${baseClass}--loading`,
33
+ className
34
+ ].filter(Boolean).join(" ");
35
+ const children = isLoading ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size === "compact" ? "sm" : "md", variant: "inherit" }) }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
36
+ if (href && !isDisabled && !isLoading) {
37
+ return /* @__PURE__ */ jsx(
38
+ "a",
39
+ {
40
+ ...rest,
41
+ ref,
42
+ className: classes,
43
+ href,
44
+ target,
45
+ rel,
46
+ "aria-label": ariaLabel,
47
+ children
48
+ }
49
+ );
50
+ }
37
51
  return /* @__PURE__ */ jsx(
38
- "a",
52
+ "button",
39
53
  {
54
+ ...rest,
55
+ ref,
56
+ type: "button",
40
57
  className: classes,
41
- href,
42
- target,
43
- rel,
44
- onClick,
58
+ disabled: isDisabled || isLoading,
59
+ "aria-busy": isLoading || void 0,
45
60
  "aria-label": ariaLabel,
46
- "aria-describedby": ariaDescribedby,
47
61
  children
48
62
  }
49
63
  );
50
64
  }
51
- return /* @__PURE__ */ jsx(
52
- "button",
53
- {
54
- type: "button",
55
- className: classes,
56
- onClick,
57
- disabled: isDisabled || isLoading,
58
- "aria-busy": isLoading || void 0,
59
- "aria-label": ariaLabel,
60
- "aria-describedby": ariaDescribedby,
61
- children
62
- }
63
- );
64
- };
65
+ );
66
+ CircularButton.displayName = "CircularButton";
65
67
  export {
66
68
  CircularButton
67
69
  };
@@ -80,7 +80,7 @@
80
80
  }
81
81
 
82
82
  .ds-code-block__filename {
83
- font-family: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
83
+ font-family: var(--font-family-code);
84
84
  font-size: var(--font-paragraph-sm-size);
85
85
  line-height: var(--font-paragraph-sm-line-height);
86
86
  color: var(--color-text-secondary);
@@ -172,7 +172,7 @@
172
172
  }
173
173
 
174
174
  .ds-code-block__code {
175
- font-family: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
175
+ font-family: var(--font-family-code);
176
176
  font-size: var(--font-paragraph-sm-size);
177
177
  line-height: var(--font-paragraph-sm-line-height);
178
178
  color: var(--color-text-primary);
@@ -0,0 +1,164 @@
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
+ /* Corner concentric with the send button it wraps: half the button,
17
+ plus the action bar's padding-sm ring, plus the border width
18
+ (border-radius applies to the border's outer edge). */
19
+ border-radius: calc(
20
+ var(--ds-composer-send-size) / 2 + var(--padding-sm) + var(--border-xs)
21
+ );
22
+ box-shadow: var(--shadow-floating);
23
+ transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
24
+ /* The whole shell is the input affordance — clicking anywhere that is
25
+ not a control focuses the textarea, and the cursor says so. The
26
+ action bar below opts back out. */
27
+ cursor: text;
28
+ }
29
+
30
+ /* The shell carries the input focus/hover treatment — the textarea inside
31
+ is borderless, so the whole surface reads as one control */
32
+ .ds-composer:hover:not(:focus-within) {
33
+ border-color: var(--color-input-border-hover);
34
+ }
35
+
36
+ .ds-composer:focus-within {
37
+ border-color: var(--color-input-border-selected);
38
+ }
39
+
40
+ /* ============================================
41
+ ATTACHMENTS ROW
42
+ ============================================ */
43
+
44
+ .ds-composer__attachments {
45
+ display: flex;
46
+ flex-wrap: wrap;
47
+ align-items: flex-end;
48
+ gap: var(--gap-xs);
49
+ /* Top ring only — the text zone below keeps its own full padding-md,
50
+ so chips sit a clear padding-md above the first text line */
51
+ padding: var(--padding-md) var(--padding-lg) 0;
52
+ }
53
+
54
+ /* ============================================
55
+ CONTENT ZONE
56
+ The text zone breathes more than the action bar: generous top and
57
+ inline padding, tighter toward the bar below it.
58
+ ============================================ */
59
+
60
+ .ds-composer__content {
61
+ display: flex;
62
+ width: 100%;
63
+ padding: var(--padding-md) var(--padding-lg) var(--padding-sm);
64
+ }
65
+
66
+ .ds-composer__textarea {
67
+ width: 100%;
68
+ padding: 0;
69
+ border: none;
70
+ outline: none;
71
+ background-color: transparent;
72
+ resize: none;
73
+ font-family: var(--font-paragraph-family);
74
+ font-size: var(--font-paragraph-size);
75
+ font-weight: var(--font-paragraph-weight);
76
+ line-height: var(--font-paragraph-line-height);
77
+ letter-spacing: var(--font-paragraph-letter-spacing);
78
+ color: var(--color-input-text-primary);
79
+ /* Growth cap: the shell owns the block padding, so the cap is rows × line-height.
80
+ --ds-composer-max-rows is set inline from the maxRows prop. */
81
+ max-height: calc(var(--ds-composer-max-rows, 8) * var(--font-paragraph-line-height));
82
+ overflow-y: auto;
83
+ }
84
+
85
+ .ds-composer__textarea::placeholder {
86
+ color: var(--color-input-text-placeholder);
87
+ }
88
+
89
+ /* Overflow scrollbar — thin, trackless, and quiet instead of the bulky
90
+ platform default. scrollbar-* covers Firefox and modern Chromium;
91
+ the -webkit rules cover Safari. */
92
+ .ds-composer__textarea {
93
+ scrollbar-width: thin;
94
+ scrollbar-color: var(--color-bg-container-secondary) transparent;
95
+ }
96
+
97
+ .ds-composer__textarea::-webkit-scrollbar {
98
+ width: 6px;
99
+ }
100
+
101
+ .ds-composer__textarea::-webkit-scrollbar-track {
102
+ background: transparent;
103
+ }
104
+
105
+ .ds-composer__textarea::-webkit-scrollbar-thumb {
106
+ background-color: var(--color-bg-container-secondary);
107
+ border-radius: var(--radius-full);
108
+ }
109
+
110
+ /* Native auto-grow where supported; the JS measurement path stands down
111
+ (it checks the same capability) */
112
+ @supports (field-sizing: content) {
113
+ .ds-composer__textarea {
114
+ field-sizing: content;
115
+ }
116
+ }
117
+
118
+ /* ============================================
119
+ FOOTER ROW
120
+ ============================================ */
121
+
122
+ .ds-composer__footer {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: var(--gap-xs);
126
+ /* The tight uniform ring the concentric corner is computed from */
127
+ padding: var(--padding-sm);
128
+ /* Empty bar space still focuses the textarea on click, but the bar is
129
+ control territory, so it keeps the default cursor. */
130
+ cursor: default;
131
+ }
132
+
133
+ .ds-composer__actions {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: var(--gap-xs);
137
+ }
138
+
139
+ .ds-composer__trailing {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: var(--gap-xs);
143
+ margin-left: auto;
144
+ }
145
+
146
+ /* ============================================
147
+ DISABLED
148
+ ============================================ */
149
+
150
+ .ds-composer--disabled {
151
+ opacity: 0.4;
152
+ cursor: not-allowed;
153
+ background-color: var(--color-input-bg-disabled);
154
+ }
155
+
156
+ .ds-composer--disabled:hover:not(:focus-within),
157
+ .ds-composer--disabled:focus-within {
158
+ border-color: var(--color-input-border-disabled);
159
+ }
160
+
161
+ .ds-composer--disabled .ds-composer__textarea {
162
+ cursor: not-allowed;
163
+ color: var(--color-input-text-disabled);
164
+ }
@@ -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 {};