@tangle-network/ui 11.14.0 → 11.15.1
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/CHANGELOG.md +16 -0
- package/dist/chat/chat-container.js +31 -9
- package/dist/chat/synthetic-text.js +52 -0
- package/dist/chat/user-message.d.ts +2 -4
- package/dist/chat/user-message.js +34 -13
- package/dist/lib/control-presentation.js +11 -0
- package/dist/primitives/button.d.ts +2 -2
- package/dist/primitives/button.js +11 -9
- package/dist/primitives/input.d.ts +3 -1
- package/dist/primitives/input.js +7 -5
- package/dist/primitives/metric-strip.d.ts +6 -28
- package/dist/primitives/metric-strip.js +15 -18
- package/dist/primitives/select.d.ts +4 -1
- package/dist/primitives/select.js +7 -7
- package/dist/run/run-group.js +89 -62
- package/package.json +1 -1
- package/src/chat/chat-container.synthetic.stories.tsx +82 -0
- package/src/chat/chat-container.synthetic.test.tsx +171 -0
- package/src/chat/chat-container.tsx +33 -16
- package/src/chat/synthetic-text.test.tsx +80 -0
- package/src/chat/synthetic-text.tsx +51 -0
- package/src/chat/user-message.tsx +54 -36
- package/src/lib/control-presentation.ts +26 -0
- package/src/primitives/button.tsx +11 -9
- package/src/primitives/control-presentation.stories.tsx +79 -0
- package/src/primitives/control-presentation.test.tsx +67 -0
- package/src/primitives/input.tsx +12 -6
- package/src/primitives/metric-strip.stories.tsx +39 -0
- package/src/primitives/metric-strip.test.tsx +82 -144
- package/src/primitives/metric-strip.tsx +27 -61
- package/src/primitives/select.tsx +16 -9
- package/src/run/run-group.tsx +99 -64
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.15.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 34c2d3c: Retain non-empty synthetic text as labelled, plain-text Application notes in both transcript presentations. Preserve note ordering and synthetic-only messages without merging notes into user speech, parsing them as OpenUI, or counting them as agent responses. Existing public component props and callbacks are unchanged.
|
|
8
|
+
|
|
9
|
+
## 11.15.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 1327903: Use scoped recessed field fills, readable link-action ink and time-valued control motion. Add opt-in compact/touch control sizing and three-to-six-column MetricStrip presets that own their separators. Preserve existing sizes, imports and interaction contracts; wrap metric content and retain numeric-zero qualifiers as descriptions.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- cb72f8d: Give the shared Select its designed states by default: accent border and surface while open, a rotating chevron, the dropdown shadow on the menu, and accent focus and checked rows with an accent checkmark. Consumers no longer need per-app overrides to avoid a flat menu.
|
|
18
|
+
|
|
3
19
|
## 11.14.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -5,6 +5,7 @@ import { useRunGroups } from "../hooks/use-run-groups.js";
|
|
|
5
5
|
import { useRunCollapseState } from "../hooks/use-run-collapse-state.js";
|
|
6
6
|
import { useAutoScroll } from "../hooks/use-auto-scroll.js";
|
|
7
7
|
import { InlineThinkingItem } from "../run/inline-thinking-item.js";
|
|
8
|
+
import { SyntheticText, userTextSegments } from "./synthetic-text.js";
|
|
8
9
|
import { OpenUIArtifactRenderer } from "../openui/openui-artifact-renderer.js";
|
|
9
10
|
import { MessageList } from "./message-list.js";
|
|
10
11
|
import { AgentTimeline } from "./agent-timeline.js";
|
|
@@ -115,15 +116,25 @@ function buildTimelineItems(messages, partMap, isStreaming, onOpenUIAction, enab
|
|
|
115
116
|
for (const message of messages) {
|
|
116
117
|
const parts = partMap[message.id] ?? [];
|
|
117
118
|
if (message.role === "user") {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
const segments = userTextSegments(parts);
|
|
120
|
+
for (const segment of segments) {
|
|
121
|
+
const id = segments.length === 1 ? message.id : `${message.id}-user-${segment.index}`;
|
|
122
|
+
if (segment.synthetic) items.push({
|
|
123
|
+
id,
|
|
124
|
+
kind: "custom",
|
|
125
|
+
content: /* @__PURE__ */ jsx("div", {
|
|
126
|
+
className: "mx-3",
|
|
127
|
+
children: /* @__PURE__ */ jsx(SyntheticText, { text: segment.text })
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
else items.push({
|
|
131
|
+
id,
|
|
132
|
+
kind: "message",
|
|
133
|
+
role: "user",
|
|
134
|
+
content: segment.text.trim(),
|
|
135
|
+
timestamp: createdAtFromMessage(message)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
127
138
|
continue;
|
|
128
139
|
}
|
|
129
140
|
const toolBuffer = [];
|
|
@@ -167,6 +178,17 @@ function buildTimelineItems(messages, partMap, isStreaming, onOpenUIAction, enab
|
|
|
167
178
|
return;
|
|
168
179
|
}
|
|
169
180
|
flushToolBuffer(index);
|
|
181
|
+
if (part.type === "text" && part.synthetic) {
|
|
182
|
+
if (part.text.trim()) items.push({
|
|
183
|
+
id: itemId,
|
|
184
|
+
kind: "custom",
|
|
185
|
+
content: /* @__PURE__ */ jsx("div", {
|
|
186
|
+
className: "mx-3",
|
|
187
|
+
children: /* @__PURE__ */ jsx(SyntheticText, { text: part.text })
|
|
188
|
+
})
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
170
192
|
if (part.type === "text" && !part.synthetic && part.text.trim()) {
|
|
171
193
|
if (enableOpenUI) {
|
|
172
194
|
const jsonMatch = part.text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/chat/synthetic-text.tsx
|
|
3
|
+
/** Internal transcript chrome, never attributed to a user or model. */
|
|
4
|
+
function SyntheticText({ text }) {
|
|
5
|
+
if (!text.trim()) return null;
|
|
6
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7
|
+
role: "note",
|
|
8
|
+
"aria-label": "Application note",
|
|
9
|
+
"data-synthetic-text": "",
|
|
10
|
+
className: "min-w-0 rounded-md border border-dashed border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground",
|
|
11
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
12
|
+
className: "mb-1 font-medium",
|
|
13
|
+
children: "Application note"
|
|
14
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
15
|
+
className: "break-words whitespace-pre-wrap",
|
|
16
|
+
children: text
|
|
17
|
+
})]
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Keep the existing joined user bubble, splitting only at application notes. */
|
|
21
|
+
function userTextSegments(parts) {
|
|
22
|
+
const segments = [];
|
|
23
|
+
let pending = [];
|
|
24
|
+
let start = 0;
|
|
25
|
+
const flush = () => {
|
|
26
|
+
const text = pending.join("\n");
|
|
27
|
+
if (text.trim()) segments.push({
|
|
28
|
+
index: start,
|
|
29
|
+
synthetic: false,
|
|
30
|
+
text
|
|
31
|
+
});
|
|
32
|
+
pending = [];
|
|
33
|
+
};
|
|
34
|
+
parts.forEach((part, index) => {
|
|
35
|
+
if (part.type !== "text") return;
|
|
36
|
+
if (part.synthetic) {
|
|
37
|
+
flush();
|
|
38
|
+
if (part.text.trim()) segments.push({
|
|
39
|
+
index,
|
|
40
|
+
synthetic: true,
|
|
41
|
+
text: part.text
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
if (pending.length === 0) start = index;
|
|
45
|
+
pending.push(part.text);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
flush();
|
|
49
|
+
return segments;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { SyntheticText, userTextSegments };
|
|
@@ -12,10 +12,8 @@ export interface UserMessageProps {
|
|
|
12
12
|
actions?: ReactNode;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the run/message list (session-model `parts`) and AgentTimeline (direct
|
|
18
|
-
* `content`/`timestamp`).
|
|
15
|
+
* User speech retains the inverse-theme bubble and direct-content contract.
|
|
16
|
+
* Application notes interrupt the bubble in source order, outside its attribution.
|
|
19
17
|
*/
|
|
20
18
|
export declare const UserMessage: import("react").MemoExoticComponent<({ message: _message, parts, content, timestamp, actions }: UserMessageProps) => import("react").JSX.Element | null>;
|
|
21
19
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SyntheticText, userTextSegments } from "./synthetic-text.js";
|
|
1
2
|
import { memo } from "react";
|
|
2
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
//#region src/chat/user-message.tsx
|
|
@@ -7,15 +8,13 @@ function formatTime(date) {
|
|
|
7
8
|
minute: "2-digit"
|
|
8
9
|
});
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const text = content ?? (parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
18
|
-
if (!text.trim()) return null;
|
|
11
|
+
function MessageActions({ children }) {
|
|
12
|
+
return /* @__PURE__ */ jsx("div", {
|
|
13
|
+
className: "flex flex-wrap items-center justify-end gap-1.5 text-xs text-muted-foreground",
|
|
14
|
+
children
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function UserBubble({ text, timestamp, actions }) {
|
|
19
18
|
return /* @__PURE__ */ jsx("div", {
|
|
20
19
|
className: "flex justify-end",
|
|
21
20
|
children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -30,12 +29,34 @@ const UserMessage = memo(({ message: _message, parts, content, timestamp, action
|
|
|
30
29
|
className: "break-words whitespace-pre-wrap text-[var(--font-size-base)] leading-[1.5]",
|
|
31
30
|
children: text
|
|
32
31
|
})]
|
|
33
|
-
}), actions ? /* @__PURE__ */ jsx(
|
|
34
|
-
className: "flex flex-wrap items-center justify-end gap-1.5 text-xs text-muted-foreground",
|
|
35
|
-
children: actions
|
|
36
|
-
}) : null]
|
|
32
|
+
}), actions ? /* @__PURE__ */ jsx(MessageActions, { children: actions }) : null]
|
|
37
33
|
})
|
|
38
34
|
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* User speech retains the inverse-theme bubble and direct-content contract.
|
|
38
|
+
* Application notes interrupt the bubble in source order, outside its attribution.
|
|
39
|
+
*/
|
|
40
|
+
const UserMessage = memo(({ message: _message, parts, content, timestamp, actions }) => {
|
|
41
|
+
if (content != null) return content.trim() ? /* @__PURE__ */ jsx(UserBubble, {
|
|
42
|
+
text: content,
|
|
43
|
+
timestamp,
|
|
44
|
+
actions
|
|
45
|
+
}) : null;
|
|
46
|
+
const segments = userTextSegments(parts ?? []);
|
|
47
|
+
if (segments.length === 0) return null;
|
|
48
|
+
if (segments.length === 1 && !segments[0].synthetic) return /* @__PURE__ */ jsx(UserBubble, {
|
|
49
|
+
text: segments[0].text,
|
|
50
|
+
timestamp,
|
|
51
|
+
actions
|
|
52
|
+
});
|
|
53
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
54
|
+
className: "space-y-2",
|
|
55
|
+
children: [segments.map((segment) => segment.synthetic ? /* @__PURE__ */ jsx(SyntheticText, { text: segment.text }, segment.index) : /* @__PURE__ */ jsx(UserBubble, {
|
|
56
|
+
text: segment.text,
|
|
57
|
+
timestamp
|
|
58
|
+
}, segment.index)), actions ? /* @__PURE__ */ jsx(MessageActions, { children: actions }) : null]
|
|
59
|
+
});
|
|
39
60
|
});
|
|
40
61
|
UserMessage.displayName = "UserMessage";
|
|
41
62
|
//#endregion
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { focusField } from "./focus.js";
|
|
2
|
+
import { cn } from "./utils.js";
|
|
3
|
+
//#region src/lib/control-presentation.ts
|
|
4
|
+
const controlMotion = "duration-[var(--duration-fast)] ease-[var(--ease-standard)] motion-reduce:transition-none motion-reduce:duration-0";
|
|
5
|
+
const controlSizes = {
|
|
6
|
+
compact: "h-[var(--control-height)] px-3 py-1 text-sm",
|
|
7
|
+
touch: "h-11 min-h-11 min-w-11 px-4 py-2 text-base"
|
|
8
|
+
};
|
|
9
|
+
const fieldPresentation = cn(focusField, controlMotion, "min-w-0 bg-[var(--bg-input)] text-foreground", "aria-invalid:border-[var(--surface-danger-border)] aria-invalid:hover:border-[var(--surface-danger-border)] aria-invalid:focus:border-[var(--focus-border-danger)] aria-invalid:focus:ring-[var(--focus-halo-danger)]", "autofill:shadow-[inset_0_0_0_1000px_var(--bg-input)] autofill:[-webkit-text-fill-color:var(--text-primary)] autofill:caret-[var(--text-primary)]");
|
|
10
|
+
//#endregion
|
|
11
|
+
export { controlMotion, controlSizes, fieldPresentation };
|
|
@@ -2,11 +2,11 @@ import * as React from "react";
|
|
|
2
2
|
//#region src/primitives/button.d.ts
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
4
|
variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "sandbox" | "secondary" | null | undefined;
|
|
5
|
-
size?: "default" | "icon" | "lg" | "sm" | "xl" | null | undefined;
|
|
5
|
+
size?: "compact" | "default" | "icon" | "lg" | "sm" | "touch" | "xl" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
8
|
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "sandbox" | null;
|
|
9
|
-
size?: "default" | "sm" | "lg" | "xl" | "icon" | null;
|
|
9
|
+
size?: "default" | "sm" | "lg" | "xl" | "icon" | "compact" | "touch" | null;
|
|
10
10
|
asChild?: boolean;
|
|
11
11
|
loading?: boolean;
|
|
12
12
|
children?: React.ReactNode;
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
import { focusRing } from "../lib/focus.js";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { controlMotion, controlSizes } from "../lib/control-presentation.js";
|
|
3
4
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
5
|
import { cva } from "class-variance-authority";
|
|
5
6
|
import * as React from "react";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/primitives/button.tsx
|
|
8
|
-
const buttonVariants = cva(`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-
|
|
9
|
+
const buttonVariants = cva(`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-[color,background-color,border-color,box-shadow,transform,scale] ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`, {
|
|
9
10
|
variants: {
|
|
10
11
|
variant: {
|
|
11
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97]
|
|
12
|
-
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97]
|
|
13
|
-
outline: "border border-border bg-card hover:bg-muted active:scale-[0.97]
|
|
14
|
-
secondary: "bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97]
|
|
15
|
-
ghost: "hover:bg-muted hover:text-foreground
|
|
16
|
-
link: "text-
|
|
17
|
-
sandbox: "bg-[var(--btn-primary-bg)] text-[var(--btn-primary-text)] border border-[var(--border-accent)] hover:bg-[var(--btn-primary-hover)] active:scale-[0.97]
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97]",
|
|
13
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97]",
|
|
14
|
+
outline: "border border-border bg-card hover:bg-muted active:scale-[0.97] text-foreground",
|
|
15
|
+
secondary: "bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97]",
|
|
16
|
+
ghost: "hover:bg-muted hover:text-foreground text-muted-foreground border border-transparent",
|
|
17
|
+
link: "text-[var(--accent-text)] underline-offset-4 hover:underline",
|
|
18
|
+
sandbox: "bg-[var(--btn-primary-bg)] text-[var(--btn-primary-text)] border border-[var(--border-accent)] hover:bg-[var(--btn-primary-hover)] active:scale-[0.97]"
|
|
18
19
|
},
|
|
19
20
|
size: {
|
|
20
21
|
default: "h-[var(--control-height)] px-4 py-2",
|
|
21
22
|
sm: "h-8 rounded-md px-3 text-xs",
|
|
22
23
|
lg: "h-11 rounded-lg px-7 text-sm",
|
|
23
24
|
xl: "h-13 rounded-xl px-9 text-base",
|
|
24
|
-
icon: "h-[var(--control-height)] w-[var(--control-height)]"
|
|
25
|
+
icon: "h-[var(--control-height)] w-[var(--control-height)]",
|
|
26
|
+
...controlSizes
|
|
25
27
|
}
|
|
26
28
|
},
|
|
27
29
|
defaultVariants: {
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
//#region src/primitives/input.d.ts
|
|
4
4
|
declare const inputVariants: (props?: ({
|
|
5
5
|
variant?: "default" | "error" | "sandbox" | null | undefined;
|
|
6
|
-
size?: "default" | "lg" | "sm" | null | undefined;
|
|
6
|
+
size?: "compact" | "default" | "lg" | "sm" | "touch" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">, VariantProps<typeof inputVariants> {
|
|
9
9
|
label?: string;
|
|
@@ -14,6 +14,8 @@ declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttri
|
|
|
14
14
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
15
15
|
/** Both values render the same field. */
|
|
16
16
|
variant?: "default" | "sandbox";
|
|
17
|
+
/** Opt in to density/typing presentation without changing the default 120px well. */
|
|
18
|
+
size?: "default" | "compact" | "touch";
|
|
17
19
|
label?: string;
|
|
18
20
|
error?: string;
|
|
19
21
|
hint?: string;
|
package/dist/primitives/input.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { focusFieldInvalid } from "../lib/focus.js";
|
|
3
3
|
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { controlSizes, fieldPresentation } from "../lib/control-presentation.js";
|
|
4
5
|
import { Label } from "./label.js";
|
|
5
6
|
import { cva } from "class-variance-authority";
|
|
6
7
|
import * as React from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
//#region src/primitives/input.tsx
|
|
9
|
-
const inputVariants = cva(cn("flex w-full rounded-lg border
|
|
10
|
+
const inputVariants = cva(cn("flex w-full rounded-lg border px-4 py-2 text-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium file:text-sm", fieldPresentation), {
|
|
10
11
|
variants: {
|
|
11
12
|
variant: {
|
|
12
13
|
default: "",
|
|
@@ -16,7 +17,8 @@ const inputVariants = cva(cn("flex w-full rounded-lg border bg-card px-4 py-2 te
|
|
|
16
17
|
size: {
|
|
17
18
|
default: "h-11",
|
|
18
19
|
sm: "h-9 px-3",
|
|
19
|
-
lg: "h-12 px-5"
|
|
20
|
+
lg: "h-12 px-5",
|
|
21
|
+
...controlSizes
|
|
20
22
|
}
|
|
21
23
|
},
|
|
22
24
|
defaultVariants: {
|
|
@@ -69,14 +71,14 @@ const Input = React.forwardRef(({ className, type, variant, size, label, error,
|
|
|
69
71
|
});
|
|
70
72
|
});
|
|
71
73
|
Input.displayName = "Input";
|
|
72
|
-
const Textarea = React.forwardRef(({ className, variant: _variant, label, error, hint, id, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }, ref) => {
|
|
74
|
+
const Textarea = React.forwardRef(({ className, variant: _variant, size = "default", label, error, hint, id, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }, ref) => {
|
|
73
75
|
const generatedId = React.useId();
|
|
74
76
|
const textareaId = id ?? generatedId;
|
|
75
77
|
const hintId = hint ? `${textareaId}-hint` : void 0;
|
|
76
78
|
const errorId = error ? `${textareaId}-error` : void 0;
|
|
77
79
|
const textarea = /* @__PURE__ */ jsx("textarea", {
|
|
78
80
|
id: textareaId,
|
|
79
|
-
className: cn("flex min-h-[120px] w-full resize-y rounded-lg border
|
|
81
|
+
className: cn("flex min-h-[120px] w-full resize-y rounded-lg border px-4 py-3 text-sm", "placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", fieldPresentation, error && "border-[var(--surface-danger-border)] hover:border-[var(--surface-danger-border)] focus:border-[var(--focus-border-danger)] focus:ring-[var(--focus-halo-danger)]", size === "compact" && "min-h-24 px-3 py-2", size === "touch" && "text-base", className),
|
|
80
82
|
ref,
|
|
81
83
|
...props,
|
|
82
84
|
"aria-describedby": descriptionIds(describedBy, hintId, errorId),
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import { StatusTone } from "./status-pill.js";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
//#region src/primitives/metric-strip.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* The headline figures for a console page, as ONE statement rather than four.
|
|
6
|
-
*
|
|
7
|
-
* A row of separate stat cards is the default and it is usually wrong here.
|
|
8
|
-
* Each card is a box that must be tall enough for its longest member, so a row
|
|
9
|
-
* mixing "$248.55 / Personal wallet" against a bare "0" leaves the short ones
|
|
10
|
-
* mostly empty; and four bordered boxes read as four unrelated facts when they
|
|
11
|
-
* are four readings of one account. `MetricStrip` puts them on one plane
|
|
12
|
-
* divided by hairlines: same information, one object, no holes.
|
|
13
|
-
*
|
|
14
|
-
* The divider is a border on the item rather than a `divide-*` utility on the
|
|
15
|
-
* parent, because these wrap. `divide-x` draws from DOM order and leaves a
|
|
16
|
-
* stray rule at the start of every wrapped line; a per-item leading border
|
|
17
|
-
* suppressed at each row start does not.
|
|
18
|
-
*/
|
|
4
|
+
/** Headline readings on one plane, with native definition-list semantics. */
|
|
19
5
|
export interface MetricStripProps extends React.HTMLAttributes<HTMLDListElement> {
|
|
20
6
|
children: React.ReactNode;
|
|
7
|
+
/** Desktop columns. Default retains the existing two / sm:four layout.
|
|
8
|
+
* Five and six use three columns between sm and lg. Never inferred from data. */
|
|
9
|
+
columns?: 3 | 4 | 5 | 6;
|
|
21
10
|
}
|
|
22
11
|
declare const MetricStrip: React.ForwardRefExoticComponent<MetricStripProps & React.RefAttributes<HTMLDListElement>>;
|
|
23
12
|
export interface MetricProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -25,24 +14,13 @@ export interface MetricProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
25
14
|
value: React.ReactNode;
|
|
26
15
|
/** The qualifying line: which wallet, which window, what the limit is. */
|
|
27
16
|
hint?: React.ReactNode;
|
|
28
|
-
/**
|
|
29
|
-
* `attention` only when a person has to DO something — not merely when a
|
|
30
|
-
* number is zero. */
|
|
17
|
+
/** Attention is a caller-owned action requirement, never inferred from zero. */
|
|
31
18
|
attention?: {
|
|
32
19
|
tone: StatusTone;
|
|
33
20
|
label: string;
|
|
34
21
|
};
|
|
35
22
|
}
|
|
36
|
-
/**
|
|
37
|
-
* One reading inside a `MetricStrip`.
|
|
38
|
-
*
|
|
39
|
-
* `<dt>` carries the label and `<dd>` the value, so the pair is announced as a
|
|
40
|
-
* described term rather than as two loose strings — the semantic that makes a
|
|
41
|
-
* figure legible without the visual grouping.
|
|
42
|
-
*
|
|
43
|
-
* The value is `tabular-nums`: these sit in a row and change on a timer, and
|
|
44
|
-
* proportional digits make the column jitter every time a 1 becomes an 8.
|
|
45
|
-
*/
|
|
23
|
+
/** A described term; the strip, not this item, decides where separators belong. */
|
|
46
24
|
declare const Metric: React.ForwardRefExoticComponent<MetricProps & React.RefAttributes<HTMLDivElement>>;
|
|
47
25
|
//#endregion
|
|
48
26
|
export { Metric, MetricStrip };
|
|
@@ -3,32 +3,29 @@ import { StatusPill } from "./status-pill.js";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/primitives/metric-strip.tsx
|
|
6
|
-
const
|
|
6
|
+
const layouts = {
|
|
7
|
+
3: "grid-cols-2 sm:grid-cols-3 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(3n+1))]:border-l",
|
|
8
|
+
4: "grid-cols-2 sm:grid-cols-4 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(4n+1))]:border-l",
|
|
9
|
+
5: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(5n+1))]:border-l",
|
|
10
|
+
6: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(6n+1))]:border-l"
|
|
11
|
+
};
|
|
12
|
+
const MetricStrip = React.forwardRef(({ className, children, columns = 4, ...props }, ref) => /* @__PURE__ */ jsx("dl", {
|
|
7
13
|
ref,
|
|
8
|
-
className: cn("grid
|
|
14
|
+
className: cn("grid min-w-0 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]", layouts[columns], className),
|
|
9
15
|
...props,
|
|
10
16
|
children
|
|
11
17
|
}));
|
|
12
18
|
MetricStrip.displayName = "MetricStrip";
|
|
13
|
-
/**
|
|
14
|
-
* One reading inside a `MetricStrip`.
|
|
15
|
-
*
|
|
16
|
-
* `<dt>` carries the label and `<dd>` the value, so the pair is announced as a
|
|
17
|
-
* described term rather than as two loose strings — the semantic that makes a
|
|
18
|
-
* figure legible without the visual grouping.
|
|
19
|
-
*
|
|
20
|
-
* The value is `tabular-nums`: these sit in a row and change on a timer, and
|
|
21
|
-
* proportional digits make the column jitter every time a 1 becomes an 8.
|
|
22
|
-
*/
|
|
19
|
+
/** A described term; the strip, not this item, decides where separators belong. */
|
|
23
20
|
const Metric = React.forwardRef(({ className, label, value, hint, attention, ...props }, ref) => /* @__PURE__ */ jsxs("div", {
|
|
24
21
|
ref,
|
|
25
|
-
className: cn("border-border p-4 sm:p-5",
|
|
22
|
+
className: cn("min-w-0 border-border p-4 sm:p-5", className),
|
|
26
23
|
...props,
|
|
27
24
|
children: [
|
|
28
25
|
/* @__PURE__ */ jsxs("dt", {
|
|
29
|
-
className: "flex items-center gap-2 text-muted-foreground text-sm",
|
|
26
|
+
className: "flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm",
|
|
30
27
|
children: [/* @__PURE__ */ jsx("span", {
|
|
31
|
-
className: "
|
|
28
|
+
className: "min-w-0 whitespace-normal [overflow-wrap:anywhere]",
|
|
32
29
|
children: label
|
|
33
30
|
}), attention && /* @__PURE__ */ jsx(StatusPill, {
|
|
34
31
|
tone: attention.tone,
|
|
@@ -36,12 +33,12 @@ const Metric = React.forwardRef(({ className, label, value, hint, attention, ...
|
|
|
36
33
|
})]
|
|
37
34
|
}),
|
|
38
35
|
/* @__PURE__ */ jsx("dd", {
|
|
39
|
-
className: cn("mt-1
|
|
36
|
+
className: cn("mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl", attention?.tone === "danger" && "text-[var(--surface-danger-text)]"),
|
|
40
37
|
title: typeof value === "string" ? value : void 0,
|
|
41
38
|
children: value
|
|
42
39
|
}),
|
|
43
|
-
hint && /* @__PURE__ */ jsx("dd", {
|
|
44
|
-
className: "mt-0.5
|
|
40
|
+
hint !== void 0 && hint !== null && hint !== false && hint !== "" && /* @__PURE__ */ jsx("dd", {
|
|
41
|
+
className: "mt-0.5 whitespace-normal [overflow-wrap:anywhere] text-[var(--text-dim)] text-xs",
|
|
45
42
|
title: typeof hint === "string" ? hint : void 0,
|
|
46
43
|
children: hint
|
|
47
44
|
})
|
|
@@ -4,7 +4,10 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|
|
4
4
|
declare const Select: React.FC<SelectPrimitive.SelectProps>;
|
|
5
5
|
declare const SelectGroup: React.ForwardRefExoticComponent<SelectPrimitive.SelectGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
6
6
|
declare const SelectValue: React.ForwardRefExoticComponent<SelectPrimitive.SelectValueProps & React.RefAttributes<HTMLSpanElement>>;
|
|
7
|
-
declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> &
|
|
7
|
+
declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & {
|
|
8
|
+
/** Default remains 36px; compact follows density, touch is at least 44px. */
|
|
9
|
+
size?: "default" | "compact" | "touch";
|
|
10
|
+
} & React.RefAttributes<HTMLButtonElement>>;
|
|
8
11
|
declare const SelectScrollUpButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
9
12
|
declare const SelectScrollDownButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
10
13
|
declare const SelectContent: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { focusField } from "../lib/focus.js";
|
|
3
2
|
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { controlSizes, fieldPresentation } from "../lib/control-presentation.js";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
@@ -9,13 +9,13 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|
|
9
9
|
const Select = SelectPrimitive.Root;
|
|
10
10
|
const SelectGroup = SelectPrimitive.Group;
|
|
11
11
|
const SelectValue = SelectPrimitive.Value;
|
|
12
|
-
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(SelectPrimitive.Trigger, {
|
|
12
|
+
const SelectTrigger = React.forwardRef(({ className, children, size = "default", ...props }, ref) => /* @__PURE__ */ jsxs(SelectPrimitive.Trigger, {
|
|
13
13
|
ref,
|
|
14
|
-
className: cn("flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border
|
|
14
|
+
className: cn("flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", fieldPresentation, "data-[placeholder]:text-muted-foreground [&>span]:min-w-0 [&>span]:text-left", "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform", size !== "default" && controlSizes[size], className),
|
|
15
15
|
...props,
|
|
16
16
|
children: [children, /* @__PURE__ */ jsx(SelectPrimitive.Icon, {
|
|
17
17
|
asChild: true,
|
|
18
|
-
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 opacity-50" })
|
|
18
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 shrink-0 opacity-50" })
|
|
19
19
|
})]
|
|
20
20
|
}));
|
|
21
21
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
@@ -35,7 +35,7 @@ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) =
|
|
|
35
35
|
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
36
36
|
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(SelectPrimitive.Content, {
|
|
37
37
|
ref,
|
|
38
|
-
className: cn("data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-
|
|
38
|
+
className: cn("data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-[var(--shadow-dropdown)] data-[state=closed]:animate-out data-[state=open]:animate-in", position === "popper" && "data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1", className),
|
|
39
39
|
position,
|
|
40
40
|
...props,
|
|
41
41
|
children: [
|
|
@@ -56,11 +56,11 @@ const SelectLabel = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
|
|
|
56
56
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
57
57
|
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(SelectPrimitive.Item, {
|
|
58
58
|
ref,
|
|
59
|
-
className: cn("relative flex w-full cursor-default select-none items-center rounded-
|
|
59
|
+
className: cn("relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pr-8 pl-2 text-sm outline-none transition-colors focus:bg-[var(--accent-surface-strong)] focus:text-foreground data-[state=checked]:bg-[var(--accent-surface-strong)] data-[state=checked]:font-medium data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className),
|
|
60
60
|
...props,
|
|
61
61
|
children: [/* @__PURE__ */ jsx("span", {
|
|
62
62
|
className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center",
|
|
63
|
-
children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) })
|
|
63
|
+
children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-[var(--accent-text)]" }) })
|
|
64
64
|
}), /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })]
|
|
65
65
|
}));
|
|
66
66
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|