diff --git a/docs/api-reference/veryfront/chat.md b/docs/api-reference/veryfront/chat.md index 0e1f0e2a4c..acaf3ef1af 100644 --- a/docs/api-reference/veryfront/chat.md +++ b/docs/api-reference/veryfront/chat.md @@ -144,7 +144,7 @@ Result returned from use agent. | `AgentPicker` | AgentPicker - render `` for the default data-driven combobox, or compose `AgentPicker.Trigger`, `Content`, `Search`, `List`, `Item`, `Create`, and `Manage` for a custom menu. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/agent-picker.tsx#L420) | | `AppShell` | Compound AppShell. Compose: | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/app-shell.tsx#L628) | | `AttachmentPill` | AttachmentPill - render `` for the default chip, or compose `AttachmentPill.Root` + `.Thumbnail` / `.Icon` / `.Label` / `.Retry` / `.Remove` for a custom layout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachment-pill.tsx#L495) | -| `AttachmentsPanel` | AttachmentsPanel - render `` for the default panel, or compose `AttachmentsPanel.Root` + `List` / `Item` / `Empty` / `Action` for a custom layout. Mirrors the `ToolCall` / `Sources` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L611) | +| `AttachmentsPanel` | AttachmentsPanel - render `` for the default panel, or compose `AttachmentsPanel.Root` + `List` / `Item` / `Empty` / `Action` for a custom layout. Mirrors the `ToolCall` / `Sources` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L650) | | `BranchPicker` | Branch picker with addressable previous, count, and next leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/branch-picker.tsx#L192) | | `Chat` | Render chat components through the preset or its composable sub-parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/chat-preset.tsx#L37) | | `ChatActions` | ChatActions - render `` for the default preset menu, or compose `ChatActions.Trigger` / `Content` / `Item` (each reads `useChatActions()`) for a custom menu. Mirrors the `ToolCall` compound: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-actions.tsx#L197) | @@ -308,13 +308,13 @@ Result returned from use agent. | `AttachmentInfo` | Public API contract for attachment info. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachment-pill.tsx#L17) | | `AttachmentPillContextValue` | Derived per-pill view state shared with `AttachmentPill.*` sub-parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachment-pill.tsx#L152) | | `AttachmentPillProps` | Props accepted by attachment pill. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachment-pill.tsx#L41) | -| `AttachmentsPanelActionProps` | Props for `AttachmentsPanel.Action` - the upload/attach button. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L554) | +| `AttachmentsPanelActionProps` | Props for `AttachmentsPanel.Action` - the upload/attach button. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L593) | | `AttachmentsPanelContextValue` | Per-panel state shared with `AttachmentsPanel.*` sub-parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L48) | -| `AttachmentsPanelEmptyProps` | Props for `AttachmentsPanel.Empty` - the no-files state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L508) | +| `AttachmentsPanelEmptyProps` | Props for `AttachmentsPanel.Empty` - the no-files state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L547) | | `AttachmentsPanelHeaderProps` | Props for `AttachmentsPanel.Header` - the title row + close button. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L193) | | `AttachmentsPanelItemProps` | Props accepted by an individual `AttachmentsPanel.Item` (attachment card). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L272) | | `AttachmentsPanelListProps` | Props for `AttachmentsPanel.List` - the scrollable list of file rows. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L241) | -| `AttachmentsPanelLoadingProps` | Props for `AttachmentsPanel.Loading` - the initial-fetch placeholder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L465) | +| `AttachmentsPanelLoadingProps` | Props for `AttachmentsPanel.Loading` - the initial-fetch placeholder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L504) | | `AttachmentsPanelProps` | Props accepted by `AttachmentsPanel` / `AttachmentsPanel.Root`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L88) | | `BranchInfo` | Public API contract for branch info. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L76) | | `BranchPickerActionProps` | Props shared by `BranchPicker.Previous` and `BranchPicker.Next`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/branch-picker.tsx#L20) | diff --git a/docs/rfcs/29-chat-api-shape/README.md b/docs/rfcs/29-chat-api-shape/README.md index 93c268eea6..0fa36e9c78 100644 --- a/docs/rfcs/29-chat-api-shape/README.md +++ b/docs/rfcs/29-chat-api-shape/README.md @@ -42,6 +42,8 @@ The complete set, as of `main`, and checked one delta at a time: every row links | [`useChatScroll`](./hooks/use-chat-scroll.md#usechatscroll---new---partly-shipped-srcreactcomponentschatchathooksuse-stick-to-bottomts177) | `partly shipped` | `src/react/components/chat/chat/hooks/use-stick-to-bottom.ts:177` | | [`ChatInput.Field` IME guard + native surface](./components/chat-input.md#chatinputfield---changed---partly-shipped-srcreactcomponentschatchatcompositionchat-composertypests18) | `partly shipped` | `src/react/primitives/input-box.tsx:37` (guard); the native surface landed in `src/react/components/chat/chat/composition/chat-composer.types.ts:18` | | [`ChatInput` flat sub-part exports](./components/chat-input.md#chatinput-flat-sub-part-exports---new---shipped-srcchatindexts250) | `shipped` | `src/chat/index.ts:250` | +| [`AttachmentsPanel.Item.Name`](./components/attachments-panel.md#attachmentspanelitemname---new---shipped-srcreactcomponentschatchatcomponentsattachments-paneltsx363) | `shipped` | `src/react/components/chat/chat/components/attachments-panel.tsx:363` | +| [`AttachmentsPanel.Item.Size`](./components/attachments-panel.md#attachmentspanelitemsize---new---shipped-srcreactcomponentschatchatcomponentsattachments-paneltsx386) | `shipped` | `src/react/components/chat/chat/components/attachments-panel.tsx:386` | ## The three layers diff --git a/docs/rfcs/29-chat-api-shape/components/attachments-panel.md b/docs/rfcs/29-chat-api-shape/components/attachments-panel.md index bd89b5e444..20027512bc 100644 --- a/docs/rfcs/29-chat-api-shape/components/attachments-panel.md +++ b/docs/rfcs/29-chat-api-shape/components/attachments-panel.md @@ -2,10 +2,10 @@ A compound component for browsing and managing durable uploaded files, with the same compositional depth as messages. -> **Status: RFC 29 - proposed; nothing on this page has landed.** Per-symbol truth, verified against `src/` by `deno task lint:rfc-status`: +> **Status: RFC 29 - partly landed.** Per-symbol truth, verified against `src/` by `deno task lint:rfc-status`: > -> - **Exported from `veryfront/chat` today:** `AttachmentsPanel`, `AttachmentsPanel.Action`, `AttachmentsPanel.Empty`, `AttachmentsPanel.Header`, `AttachmentsPanel.Item`, `AttachmentsPanel.Item.Icon`, `AttachmentsPanel.Item.Preview`, `AttachmentsPanel.Item.Remove`, `AttachmentsPanel.List`, `AttachmentsPanel.Loading`, `AttachmentsPanel.Root`, `useAttachments` -> - **Not exported today:** `AttachmentsPanel.Item.Name`, `AttachmentsPanel.Item.Size` +> - **Exported from `veryfront/chat` today:** `AttachmentsPanel`, `AttachmentsPanel.Action`, `AttachmentsPanel.Empty`, `AttachmentsPanel.Header`, `AttachmentsPanel.Item`, `AttachmentsPanel.Item.Icon`, `AttachmentsPanel.Item.Name`, `AttachmentsPanel.Item.Preview`, `AttachmentsPanel.Item.Remove`, `AttachmentsPanel.Item.Size`, `AttachmentsPanel.List`, `AttachmentsPanel.Loading`, `AttachmentsPanel.Root`, `useAttachments` +> - **Not exported today:** none > > An exported symbol is not a landed delta - see [reading the status block](../README.md#reading-the-status-block). Full rationale: [`29-chat-api-shape.md`](../../29-chat-api-shape.md). @@ -34,8 +34,8 @@ import { - [`.Item`](#attachmentspanelitem---changed) - `changed`: pill `
- file name (shipped, #2975) */} + {/*
- formatted byte size (shipped, #2975) */} {/* - delete; null without a remove handler */} @@ -184,9 +184,9 @@ The image thumbnail - today delegates to `AttachmentPill.Thumbnail`: a 40px squa | ----------------------------------------------- | ---- | ------------ | | `asChild` + native (`ImgHTMLAttributes`, `ref`) | | Own the node | -### `AttachmentsPanel.Item.Name` _(proposed - #2975)_ - `new` +### `AttachmentsPanel.Item.Name` - `new` - `shipped` (src/react/components/chat/chat/components/attachments-panel.tsx:363) -One ``: the file's name, truncating. Does not exist today - today the source deliberately omits it (name is "plain text with no attachment-domain logic", read from the item context and rendered yourself, or via `AttachmentPill.Label` which renders a name + secondary line column). #2975 adds it so the default row is fully recomposable from leaves. Default content: `file.name`. +One `` (the default row's name line; the RFC text originally sketched a ``): the file's name, truncating. Landed per #2975 so the default row is fully recomposable from leaves. Default content: `file.name`, falling back to `"Attachment"` when the name is empty. It is deliberately plain text: it does not reproduce `AttachmentPill.Label`'s uploading shimmer or state line - compose `AttachmentPill.Label` when you want that stateful treatment. **Layout:** in-flow text span; place it in your own `min-w-0` column to get truncation (see the Composed example). @@ -194,9 +194,9 @@ One ``: the file's name, truncating. Does not exist today - today the sour | -------------------------------------------- | ---- | ------------------------------------------------------------------------------- | | `asChild` + native (`HTMLAttributes`, `ref`) | | Convention only - the shared node contract; no part-specific props are proposed | -### `AttachmentsPanel.Item.Size` _(proposed - #2975)_ - `new` +### `AttachmentsPanel.Item.Size` - `new` - `shipped` (src/react/components/chat/chat/components/attachments-panel.tsx:386) -One ``: the file's size formatted as `B` / `KB` / `MB` (the [`formatSize`](../helpers.md) helper, public). Renders `null` when `file.size` is undefined; type/extension labels belong in `.Item.Icon` or custom children, not this size leaf. +One `` (the default row's secondary line; the RFC text originally sketched a ``): the file's size formatted as `B` / `KB` / `MB` (the [`formatSize`](../helpers.md) helper, public). Renders `null` when `file.size` is undefined; type/extension labels belong in `.Item.Icon` or custom children, not this size leaf. It always renders the faint secondary color - the error/destructive treatment stays with `AttachmentPill.Label`. **Layout:** in-flow text span inside your label column. diff --git a/src/react/components/chat/chat/components/attachments-panel.test.tsx b/src/react/components/chat/chat/components/attachments-panel.test.tsx index 9de27eacb3..c5e8c2145f 100644 --- a/src/react/components/chat/chat/components/attachments-panel.test.tsx +++ b/src/react/components/chat/chat/components/attachments-panel.test.tsx @@ -140,6 +140,64 @@ describe("AttachmentsPanel — composability contract", () => { assert(!html.includes('aria-label="Remove run-analysis.csv"')); }); + it("exposes Item.Name and Item.Size leaves", () => { + assert( + typeof AttachmentsPanel.Item.Name === "function", + "AttachmentsPanel.Item.Name must be a component", + ); + assert( + typeof AttachmentsPanel.Item.Size === "function", + "AttachmentsPanel.Item.Size must be a component", + ); + }); + + it("recomposes the row from Item.Name + Item.Size leaves", () => { + const html = renderToString( + + + + + + + + + , + ); + // The Name leaf renders the file name; the Size leaf renders the + // human-formatted byte label (24424 → "24 KB"). + assertStringIncludes(html, "run-analysis.csv"); + assertStringIncludes(html, "24 KB"); + }); + + it("Item.Size renders nothing when the file has no size", () => { + const html = renderToString( + + + + + + + + , + ); + assertStringIncludes(html, "sizeless.txt"); + assert(!html.includes("vf-size-leaf"), "Size leaf must render nothing without a size"); + }); + + it("regression: a childless Item still renders its default content", () => { + const html = renderToString( + undefined}> + + + + , + ); + // Default anatomy: media + label (name + formatted size) + overflow menu. + assertStringIncludes(html, "run-analysis.csv"); + assertStringIncludes(html, "24 KB"); + assertStringIncludes(html, 'aria-label="Actions for run-analysis.csv"'); + }); + it("Item leaves read the file from context; used outside an Item they throw", () => { function Orphan() { return ; diff --git a/src/react/components/chat/chat/components/attachments-panel.tsx b/src/react/components/chat/chat/components/attachments-panel.tsx index 98d261231c..651d2ab656 100644 --- a/src/react/components/chat/chat/components/attachments-panel.tsx +++ b/src/react/components/chat/chat/components/attachments-panel.tsx @@ -16,7 +16,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "../../../ui/dropdown-menu.tsx"; -import { AttachmentPill, useAttachmentPill } from "./attachment-pill.tsx"; +import { AttachmentPill, formatSize, useAttachmentPill } from "./attachment-pill.tsx"; import { createStrictContext } from "../../../create-strict-context.ts"; import { isSafeUploadUrl, openSafeUploadUrl } from "../upload-url.ts"; @@ -275,9 +275,9 @@ export interface AttachmentsPanelItemProps { className?: string; /** * Compose the row from the `AttachmentsPanel.Item.*` leaves (`.Icon` / - * `.Preview` / `.Remove`), the `AttachmentPill.*` parts (e.g. `.Label`), or - * your own text read from `useAttachmentPill()`. Omit for the default card - * (media + label + overflow menu). + * `.Preview` / `.Name` / `.Size` / `.Remove`), the `AttachmentPill.*` parts + * (e.g. `.Label`), or your own text read from `useAttachmentPill()`. Omit for + * the default card (media + label + overflow menu). */ children?: React.ReactNode; /** React 19: ref is a regular prop (threaded to the underlying `AttachmentPill`). */ @@ -324,12 +324,10 @@ function AttachmentsPanelItemMedia( } // --------------------------------------------------------------------------- -// AttachmentsPanel.Item.* — the row leaves that carry attachment-domain logic: -// `.Icon` / `.Preview` (derived pill state) and `.Remove` (bound to the panel's -// `onRemoveUpload`). Each reads the item's file from the enclosing -// `AttachmentPill` context. Plain text (name / size) has no such logic — read it -// from `useAttachmentPill()` / `useAttachments()` and render your own, or use -// `AttachmentPill.Label`. Use these inside an `AttachmentsPanel.Item`. +// AttachmentsPanel.Item.* — the row leaves: `.Icon` / `.Preview` (derived pill +// state), `.Name` / `.Size` (the file's text lines), and `.Remove` (bound to +// the panel's `onRemoveUpload`). Each reads the item's file from the enclosing +// `AttachmentPill` context. Use these inside an `AttachmentsPanel.Item`. // --------------------------------------------------------------------------- /** `AttachmentsPanel.Item.Icon` — the file-type icon square. */ @@ -357,13 +355,52 @@ function AttachmentsPanelItemPreview( } AttachmentsPanelItemPreview.displayName = "AttachmentsPanel.Item.Preview"; -// Note: there is deliberately no `Item.Name` / `Item.Size` leaf. Those are plain -// text with no attachment-domain logic — read them from `useAttachments()` (or -// `useAttachmentPill().attachment`) and render your own text (`formatSize` is -// exported for the byte label). `AttachmentPill.Label` also renders name + a -// secondary size/type line if you want the default treatment. We only expose -// leaves that carry domain binding (`.Remove`) or derived state (`.Icon` / -// `.Preview`) — never a wrapper around a bare ``. +/** + * `AttachmentsPanel.Item.Name` — the file-name line. Deliberately plain text: + * unlike `AttachmentPill.Label`, it does not wrap the name in a shimmer while + * the attachment uploads. Compose `AttachmentPill.Label` for that treatment. + */ +function AttachmentsPanelItemName( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element { + const { attachment } = useAttachmentPill(); + return ( + + {attachment.name || "Attachment"} + + ); +} +AttachmentsPanelItemName.displayName = "AttachmentsPanel.Item.Name"; + +/** + * `AttachmentsPanel.Item.Size` — the human-formatted byte label (`formatSize`). + * Renders nothing when the file has no size. Always the faint secondary color; + * the error/destructive state line stays with `AttachmentPill.Label`. + */ +function AttachmentsPanelItemSize( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element | null { + const { attachment } = useAttachmentPill(); + if (attachment.size == null) return null; + return ( + + {formatSize(attachment.size)} + + ); +} +AttachmentsPanelItemSize.displayName = "AttachmentsPanel.Item.Size"; /** Props for `AttachmentsPanel.Item.Remove`. */ export interface AttachmentsPanelItemRemoveProps { @@ -408,6 +445,8 @@ AttachmentsPanelItemRemove.displayName = "AttachmentsPanel.Item.Remove"; const AttachmentsPanelItemCompound = Object.assign(AttachmentsPanelItem, { Icon: AttachmentsPanelItemIcon, Preview: AttachmentsPanelItemPreview, + Name: AttachmentsPanelItemName, + Size: AttachmentsPanelItemSize, Remove: AttachmentsPanelItemRemove, }); AttachmentsPanelItem.displayName = "AttachmentsPanel.Item"; diff --git a/src/server/handlers/dev/framework-candidates.generated.ts b/src/server/handlers/dev/framework-candidates.generated.ts index 64c046da29..49894794c2 100644 --- a/src/server/handlers/dev/framework-candidates.generated.ts +++ b/src/server/handlers/dev/framework-candidates.generated.ts @@ -1729,8 +1729,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanel.Item", "AttachmentsPanel.Item.*", "AttachmentsPanel.Item.Icon", + "AttachmentsPanel.Item.Name", "AttachmentsPanel.Item.Preview", "AttachmentsPanel.Item.Remove", + "AttachmentsPanel.Item.Size", "AttachmentsPanel.List", "AttachmentsPanel.Loading", "AttachmentsPanel.Root", @@ -1770,6 +1772,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemMedia(", "AttachmentsPanelItemMenu", "AttachmentsPanelItemMenu(", + "AttachmentsPanelItemName(", + "AttachmentsPanelItemName,", + "AttachmentsPanelItemName.displayName", "AttachmentsPanelItemPreview(", "AttachmentsPanelItemPreview,", "AttachmentsPanelItemPreview.displayName", @@ -1780,6 +1785,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemRemove.displayName", "AttachmentsPanelItemRemoveProps", "AttachmentsPanelItemRemoveProps,", + "AttachmentsPanelItemSize(", + "AttachmentsPanelItemSize,", + "AttachmentsPanelItemSize.displayName", "AttachmentsPanelList", "AttachmentsPanelList(", "AttachmentsPanelList,", @@ -3972,8 +3980,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Italic", "Item", "Item.*", - "Item.Name", - "Item.Size", "Item:", "Items", "Iterable)", @@ -4734,6 +4740,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "NUL", "NUMERIC_PREFIX_PATTERN", "NUMERIC_PREFIX_PATTERN.test(raw)", + "Name", "Name:", "Named", "Narrow", @@ -5476,6 +5483,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "React.Ref;", "React.Ref;", "React.Ref;", + "React.Ref", "React.Ref;", "React.Ref;", "React.Ref}", @@ -6171,6 +6179,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "SingleToggleGroupRootProps", "SingleToggleGroupRootProps,", "Sink", + "Size", + "Size:", "Sized", "Skeleton", "Skeleton(", @@ -6562,7 +6572,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "ThinkingIndicatorProps>(", "Third-party", "This", - "Those", "Thought", "Thread", "Three", @@ -8504,7 +8513,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "attaching", "attachment", "attachment,", - "attachment-domain", "attachment-pill.tsx", "attachment.name", "attachment.name}", @@ -8886,7 +8894,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "bind", "bind,", "bind:", - "binding", "binding.", "binds", "bits", @@ -9916,6 +9923,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "color-mix(in", "color-mode.tsx", "color:", + "color;", "color={presentation.color}", "colorClass", "colorClass,", @@ -12086,6 +12094,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "error-boundary.tsx", "error.", "error.message}", + "error/destructive", "error:", "error;", "error={error}", @@ -12364,6 +12373,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "failures.push(new", "failures:", "failures[0];", + "faint", "faint.", "faithful", "fall", @@ -12436,10 +12446,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "fieldset", "fight", "file", + "file's", "file,", "file-${index}", "file-extension", "file-local", + "file-name", "file-type", "file-type-badge", "file-type-thumb", @@ -12744,6 +12756,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "formatSize(attachment.size);", "formatSize(attachment.size)}", "formatSize(bytes:", + "formatSize,", "formatStatus(status);", "formatStatus(status:", "formatTimestamp(createdAt:", @@ -13473,6 +13486,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "https://upload.invalid/", "https://veryfront.com/docs/code/guides/chat-ui#render-markdown-in-chat", "hue", + "human-formatted", "humanized", "humanizedId", "humanizedId;", @@ -14480,7 +14494,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "k}:${v}", "l", "label", - "label).", "label);", "label,", "label-left", @@ -14635,6 +14648,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "leaves", "leaves,", "leaves.", + "leaves:", "leaving", "left", "left),", @@ -14730,6 +14744,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "linear-gradient(var(--soft),", "lines", "lines)", + "lines),", "lines.", "lines.join(", "lines.map((w,", @@ -14888,7 +14903,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "lockstep", "log", "log-then-run", - "logic", "logic.", "logic:", "logic;", @@ -16825,7 +16839,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "overview", "own", "own).", - "own,", "own.", "own:", "own;", @@ -19939,7 +19952,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "size-[38px]", "size-full", "size.", - "size/type", "size:", "size=", "size={size}", @@ -20515,7 +20527,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "success.", "success:", "successful", - "such", "suffix", "sugar", "sugar.", @@ -21663,6 +21674,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "unlabeled", "unlabelled", "unless", + "unlike", "unlisted", "unlistedLegacyFallback", "unlockScroll", @@ -21786,6 +21798,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "uploads", "uploads),", "uploads,", + "uploads.", "uploads.length", "uploads.map((doc)", "uploads:", @@ -21868,14 +21881,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "useAppShell]", "useAttachmentPill", "useAttachmentPill()", - "useAttachmentPill().attachment", "useAttachmentPill();", "useAttachmentPill,", "useAttachmentPillContext;", "useAttachmentPillContext]", "useAttachments", "useAttachments(", - "useAttachments()", "useAttachments,", "useAttachments;", "useAttachmentsPanel",
` (the default row's name line; the RFC text originally sketched a ``): the file's name, truncating. Landed per #2975 so the default row is fully recomposable from leaves. Default content: `file.name`, falling back to `"Attachment"` when the name is empty. It is deliberately plain text: it does not reproduce `AttachmentPill.Label`'s uploading shimmer or state line - compose `AttachmentPill.Label` when you want that stateful treatment. **Layout:** in-flow text span; place it in your own `min-w-0` column to get truncation (see the Composed example). @@ -194,9 +194,9 @@ One ``: the file's name, truncating. Does not exist today - today the sour | -------------------------------------------- | ---- | ------------------------------------------------------------------------------- | | `asChild` + native (`HTMLAttributes`, `ref`) | | Convention only - the shared node contract; no part-specific props are proposed | -### `AttachmentsPanel.Item.Size` _(proposed - #2975)_ - `new` +### `AttachmentsPanel.Item.Size` - `new` - `shipped` (src/react/components/chat/chat/components/attachments-panel.tsx:386) -One ``: the file's size formatted as `B` / `KB` / `MB` (the [`formatSize`](../helpers.md) helper, public). Renders `null` when `file.size` is undefined; type/extension labels belong in `.Item.Icon` or custom children, not this size leaf. +One `` (the default row's secondary line; the RFC text originally sketched a ``): the file's size formatted as `B` / `KB` / `MB` (the [`formatSize`](../helpers.md) helper, public). Renders `null` when `file.size` is undefined; type/extension labels belong in `.Item.Icon` or custom children, not this size leaf. It always renders the faint secondary color - the error/destructive treatment stays with `AttachmentPill.Label`. **Layout:** in-flow text span inside your label column. diff --git a/src/react/components/chat/chat/components/attachments-panel.test.tsx b/src/react/components/chat/chat/components/attachments-panel.test.tsx index 9de27eacb3..c5e8c2145f 100644 --- a/src/react/components/chat/chat/components/attachments-panel.test.tsx +++ b/src/react/components/chat/chat/components/attachments-panel.test.tsx @@ -140,6 +140,64 @@ describe("AttachmentsPanel — composability contract", () => { assert(!html.includes('aria-label="Remove run-analysis.csv"')); }); + it("exposes Item.Name and Item.Size leaves", () => { + assert( + typeof AttachmentsPanel.Item.Name === "function", + "AttachmentsPanel.Item.Name must be a component", + ); + assert( + typeof AttachmentsPanel.Item.Size === "function", + "AttachmentsPanel.Item.Size must be a component", + ); + }); + + it("recomposes the row from Item.Name + Item.Size leaves", () => { + const html = renderToString( + + + + + + + + + , + ); + // The Name leaf renders the file name; the Size leaf renders the + // human-formatted byte label (24424 → "24 KB"). + assertStringIncludes(html, "run-analysis.csv"); + assertStringIncludes(html, "24 KB"); + }); + + it("Item.Size renders nothing when the file has no size", () => { + const html = renderToString( + + + + + + + + , + ); + assertStringIncludes(html, "sizeless.txt"); + assert(!html.includes("vf-size-leaf"), "Size leaf must render nothing without a size"); + }); + + it("regression: a childless Item still renders its default content", () => { + const html = renderToString( + undefined}> + + + + , + ); + // Default anatomy: media + label (name + formatted size) + overflow menu. + assertStringIncludes(html, "run-analysis.csv"); + assertStringIncludes(html, "24 KB"); + assertStringIncludes(html, 'aria-label="Actions for run-analysis.csv"'); + }); + it("Item leaves read the file from context; used outside an Item they throw", () => { function Orphan() { return ; diff --git a/src/react/components/chat/chat/components/attachments-panel.tsx b/src/react/components/chat/chat/components/attachments-panel.tsx index 98d261231c..651d2ab656 100644 --- a/src/react/components/chat/chat/components/attachments-panel.tsx +++ b/src/react/components/chat/chat/components/attachments-panel.tsx @@ -16,7 +16,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "../../../ui/dropdown-menu.tsx"; -import { AttachmentPill, useAttachmentPill } from "./attachment-pill.tsx"; +import { AttachmentPill, formatSize, useAttachmentPill } from "./attachment-pill.tsx"; import { createStrictContext } from "../../../create-strict-context.ts"; import { isSafeUploadUrl, openSafeUploadUrl } from "../upload-url.ts"; @@ -275,9 +275,9 @@ export interface AttachmentsPanelItemProps { className?: string; /** * Compose the row from the `AttachmentsPanel.Item.*` leaves (`.Icon` / - * `.Preview` / `.Remove`), the `AttachmentPill.*` parts (e.g. `.Label`), or - * your own text read from `useAttachmentPill()`. Omit for the default card - * (media + label + overflow menu). + * `.Preview` / `.Name` / `.Size` / `.Remove`), the `AttachmentPill.*` parts + * (e.g. `.Label`), or your own text read from `useAttachmentPill()`. Omit for + * the default card (media + label + overflow menu). */ children?: React.ReactNode; /** React 19: ref is a regular prop (threaded to the underlying `AttachmentPill`). */ @@ -324,12 +324,10 @@ function AttachmentsPanelItemMedia( } // --------------------------------------------------------------------------- -// AttachmentsPanel.Item.* — the row leaves that carry attachment-domain logic: -// `.Icon` / `.Preview` (derived pill state) and `.Remove` (bound to the panel's -// `onRemoveUpload`). Each reads the item's file from the enclosing -// `AttachmentPill` context. Plain text (name / size) has no such logic — read it -// from `useAttachmentPill()` / `useAttachments()` and render your own, or use -// `AttachmentPill.Label`. Use these inside an `AttachmentsPanel.Item`. +// AttachmentsPanel.Item.* — the row leaves: `.Icon` / `.Preview` (derived pill +// state), `.Name` / `.Size` (the file's text lines), and `.Remove` (bound to +// the panel's `onRemoveUpload`). Each reads the item's file from the enclosing +// `AttachmentPill` context. Use these inside an `AttachmentsPanel.Item`. // --------------------------------------------------------------------------- /** `AttachmentsPanel.Item.Icon` — the file-type icon square. */ @@ -357,13 +355,52 @@ function AttachmentsPanelItemPreview( } AttachmentsPanelItemPreview.displayName = "AttachmentsPanel.Item.Preview"; -// Note: there is deliberately no `Item.Name` / `Item.Size` leaf. Those are plain -// text with no attachment-domain logic — read them from `useAttachments()` (or -// `useAttachmentPill().attachment`) and render your own text (`formatSize` is -// exported for the byte label). `AttachmentPill.Label` also renders name + a -// secondary size/type line if you want the default treatment. We only expose -// leaves that carry domain binding (`.Remove`) or derived state (`.Icon` / -// `.Preview`) — never a wrapper around a bare ``. +/** + * `AttachmentsPanel.Item.Name` — the file-name line. Deliberately plain text: + * unlike `AttachmentPill.Label`, it does not wrap the name in a shimmer while + * the attachment uploads. Compose `AttachmentPill.Label` for that treatment. + */ +function AttachmentsPanelItemName( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element { + const { attachment } = useAttachmentPill(); + return ( + + {attachment.name || "Attachment"} + + ); +} +AttachmentsPanelItemName.displayName = "AttachmentsPanel.Item.Name"; + +/** + * `AttachmentsPanel.Item.Size` — the human-formatted byte label (`formatSize`). + * Renders nothing when the file has no size. Always the faint secondary color; + * the error/destructive state line stays with `AttachmentPill.Label`. + */ +function AttachmentsPanelItemSize( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element | null { + const { attachment } = useAttachmentPill(); + if (attachment.size == null) return null; + return ( + + {formatSize(attachment.size)} + + ); +} +AttachmentsPanelItemSize.displayName = "AttachmentsPanel.Item.Size"; /** Props for `AttachmentsPanel.Item.Remove`. */ export interface AttachmentsPanelItemRemoveProps { @@ -408,6 +445,8 @@ AttachmentsPanelItemRemove.displayName = "AttachmentsPanel.Item.Remove"; const AttachmentsPanelItemCompound = Object.assign(AttachmentsPanelItem, { Icon: AttachmentsPanelItemIcon, Preview: AttachmentsPanelItemPreview, + Name: AttachmentsPanelItemName, + Size: AttachmentsPanelItemSize, Remove: AttachmentsPanelItemRemove, }); AttachmentsPanelItem.displayName = "AttachmentsPanel.Item"; diff --git a/src/server/handlers/dev/framework-candidates.generated.ts b/src/server/handlers/dev/framework-candidates.generated.ts index 64c046da29..49894794c2 100644 --- a/src/server/handlers/dev/framework-candidates.generated.ts +++ b/src/server/handlers/dev/framework-candidates.generated.ts @@ -1729,8 +1729,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanel.Item", "AttachmentsPanel.Item.*", "AttachmentsPanel.Item.Icon", + "AttachmentsPanel.Item.Name", "AttachmentsPanel.Item.Preview", "AttachmentsPanel.Item.Remove", + "AttachmentsPanel.Item.Size", "AttachmentsPanel.List", "AttachmentsPanel.Loading", "AttachmentsPanel.Root", @@ -1770,6 +1772,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemMedia(", "AttachmentsPanelItemMenu", "AttachmentsPanelItemMenu(", + "AttachmentsPanelItemName(", + "AttachmentsPanelItemName,", + "AttachmentsPanelItemName.displayName", "AttachmentsPanelItemPreview(", "AttachmentsPanelItemPreview,", "AttachmentsPanelItemPreview.displayName", @@ -1780,6 +1785,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemRemove.displayName", "AttachmentsPanelItemRemoveProps", "AttachmentsPanelItemRemoveProps,", + "AttachmentsPanelItemSize(", + "AttachmentsPanelItemSize,", + "AttachmentsPanelItemSize.displayName", "AttachmentsPanelList", "AttachmentsPanelList(", "AttachmentsPanelList,", @@ -3972,8 +3980,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Italic", "Item", "Item.*", - "Item.Name", - "Item.Size", "Item:", "Items", "Iterable)", @@ -4734,6 +4740,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "NUL", "NUMERIC_PREFIX_PATTERN", "NUMERIC_PREFIX_PATTERN.test(raw)", + "Name", "Name:", "Named", "Narrow", @@ -5476,6 +5483,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "React.Ref;", "React.Ref;", "React.Ref;", + "React.Ref", "React.Ref;", "React.Ref;", "React.Ref}", @@ -6171,6 +6179,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "SingleToggleGroupRootProps", "SingleToggleGroupRootProps,", "Sink", + "Size", + "Size:", "Sized", "Skeleton", "Skeleton(", @@ -6562,7 +6572,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "ThinkingIndicatorProps>(", "Third-party", "This", - "Those", "Thought", "Thread", "Three", @@ -8504,7 +8513,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "attaching", "attachment", "attachment,", - "attachment-domain", "attachment-pill.tsx", "attachment.name", "attachment.name}", @@ -8886,7 +8894,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "bind", "bind,", "bind:", - "binding", "binding.", "binds", "bits", @@ -9916,6 +9923,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "color-mix(in", "color-mode.tsx", "color:", + "color;", "color={presentation.color}", "colorClass", "colorClass,", @@ -12086,6 +12094,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "error-boundary.tsx", "error.", "error.message}", + "error/destructive", "error:", "error;", "error={error}", @@ -12364,6 +12373,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "failures.push(new", "failures:", "failures[0];", + "faint", "faint.", "faithful", "fall", @@ -12436,10 +12446,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "fieldset", "fight", "file", + "file's", "file,", "file-${index}", "file-extension", "file-local", + "file-name", "file-type", "file-type-badge", "file-type-thumb", @@ -12744,6 +12756,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "formatSize(attachment.size);", "formatSize(attachment.size)}", "formatSize(bytes:", + "formatSize,", "formatStatus(status);", "formatStatus(status:", "formatTimestamp(createdAt:", @@ -13473,6 +13486,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "https://upload.invalid/", "https://veryfront.com/docs/code/guides/chat-ui#render-markdown-in-chat", "hue", + "human-formatted", "humanized", "humanizedId", "humanizedId;", @@ -14480,7 +14494,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "k}:${v}", "l", "label", - "label).", "label);", "label,", "label-left", @@ -14635,6 +14648,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "leaves", "leaves,", "leaves.", + "leaves:", "leaving", "left", "left),", @@ -14730,6 +14744,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "linear-gradient(var(--soft),", "lines", "lines)", + "lines),", "lines.", "lines.join(", "lines.map((w,", @@ -14888,7 +14903,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "lockstep", "log", "log-then-run", - "logic", "logic.", "logic:", "logic;", @@ -16825,7 +16839,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "overview", "own", "own).", - "own,", "own.", "own:", "own;", @@ -19939,7 +19952,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "size-[38px]", "size-full", "size.", - "size/type", "size:", "size=", "size={size}", @@ -20515,7 +20527,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "success.", "success:", "successful", - "such", "suffix", "sugar", "sugar.", @@ -21663,6 +21674,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "unlabeled", "unlabelled", "unless", + "unlike", "unlisted", "unlistedLegacyFallback", "unlockScroll", @@ -21786,6 +21798,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "uploads", "uploads),", "uploads,", + "uploads.", "uploads.length", "uploads.map((doc)", "uploads:", @@ -21868,14 +21881,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "useAppShell]", "useAttachmentPill", "useAttachmentPill()", - "useAttachmentPill().attachment", "useAttachmentPill();", "useAttachmentPill,", "useAttachmentPillContext;", "useAttachmentPillContext]", "useAttachments", "useAttachments(", - "useAttachments()", "useAttachments,", "useAttachments;", "useAttachmentsPanel",
` (the default row's secondary line; the RFC text originally sketched a ``): the file's size formatted as `B` / `KB` / `MB` (the [`formatSize`](../helpers.md) helper, public). Renders `null` when `file.size` is undefined; type/extension labels belong in `.Item.Icon` or custom children, not this size leaf. It always renders the faint secondary color - the error/destructive treatment stays with `AttachmentPill.Label`. **Layout:** in-flow text span inside your label column. diff --git a/src/react/components/chat/chat/components/attachments-panel.test.tsx b/src/react/components/chat/chat/components/attachments-panel.test.tsx index 9de27eacb3..c5e8c2145f 100644 --- a/src/react/components/chat/chat/components/attachments-panel.test.tsx +++ b/src/react/components/chat/chat/components/attachments-panel.test.tsx @@ -140,6 +140,64 @@ describe("AttachmentsPanel — composability contract", () => { assert(!html.includes('aria-label="Remove run-analysis.csv"')); }); + it("exposes Item.Name and Item.Size leaves", () => { + assert( + typeof AttachmentsPanel.Item.Name === "function", + "AttachmentsPanel.Item.Name must be a component", + ); + assert( + typeof AttachmentsPanel.Item.Size === "function", + "AttachmentsPanel.Item.Size must be a component", + ); + }); + + it("recomposes the row from Item.Name + Item.Size leaves", () => { + const html = renderToString( + + + + + + + + + , + ); + // The Name leaf renders the file name; the Size leaf renders the + // human-formatted byte label (24424 → "24 KB"). + assertStringIncludes(html, "run-analysis.csv"); + assertStringIncludes(html, "24 KB"); + }); + + it("Item.Size renders nothing when the file has no size", () => { + const html = renderToString( + + + + + + + + , + ); + assertStringIncludes(html, "sizeless.txt"); + assert(!html.includes("vf-size-leaf"), "Size leaf must render nothing without a size"); + }); + + it("regression: a childless Item still renders its default content", () => { + const html = renderToString( + undefined}> + + + + , + ); + // Default anatomy: media + label (name + formatted size) + overflow menu. + assertStringIncludes(html, "run-analysis.csv"); + assertStringIncludes(html, "24 KB"); + assertStringIncludes(html, 'aria-label="Actions for run-analysis.csv"'); + }); + it("Item leaves read the file from context; used outside an Item they throw", () => { function Orphan() { return ; diff --git a/src/react/components/chat/chat/components/attachments-panel.tsx b/src/react/components/chat/chat/components/attachments-panel.tsx index 98d261231c..651d2ab656 100644 --- a/src/react/components/chat/chat/components/attachments-panel.tsx +++ b/src/react/components/chat/chat/components/attachments-panel.tsx @@ -16,7 +16,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "../../../ui/dropdown-menu.tsx"; -import { AttachmentPill, useAttachmentPill } from "./attachment-pill.tsx"; +import { AttachmentPill, formatSize, useAttachmentPill } from "./attachment-pill.tsx"; import { createStrictContext } from "../../../create-strict-context.ts"; import { isSafeUploadUrl, openSafeUploadUrl } from "../upload-url.ts"; @@ -275,9 +275,9 @@ export interface AttachmentsPanelItemProps { className?: string; /** * Compose the row from the `AttachmentsPanel.Item.*` leaves (`.Icon` / - * `.Preview` / `.Remove`), the `AttachmentPill.*` parts (e.g. `.Label`), or - * your own text read from `useAttachmentPill()`. Omit for the default card - * (media + label + overflow menu). + * `.Preview` / `.Name` / `.Size` / `.Remove`), the `AttachmentPill.*` parts + * (e.g. `.Label`), or your own text read from `useAttachmentPill()`. Omit for + * the default card (media + label + overflow menu). */ children?: React.ReactNode; /** React 19: ref is a regular prop (threaded to the underlying `AttachmentPill`). */ @@ -324,12 +324,10 @@ function AttachmentsPanelItemMedia( } // --------------------------------------------------------------------------- -// AttachmentsPanel.Item.* — the row leaves that carry attachment-domain logic: -// `.Icon` / `.Preview` (derived pill state) and `.Remove` (bound to the panel's -// `onRemoveUpload`). Each reads the item's file from the enclosing -// `AttachmentPill` context. Plain text (name / size) has no such logic — read it -// from `useAttachmentPill()` / `useAttachments()` and render your own, or use -// `AttachmentPill.Label`. Use these inside an `AttachmentsPanel.Item`. +// AttachmentsPanel.Item.* — the row leaves: `.Icon` / `.Preview` (derived pill +// state), `.Name` / `.Size` (the file's text lines), and `.Remove` (bound to +// the panel's `onRemoveUpload`). Each reads the item's file from the enclosing +// `AttachmentPill` context. Use these inside an `AttachmentsPanel.Item`. // --------------------------------------------------------------------------- /** `AttachmentsPanel.Item.Icon` — the file-type icon square. */ @@ -357,13 +355,52 @@ function AttachmentsPanelItemPreview( } AttachmentsPanelItemPreview.displayName = "AttachmentsPanel.Item.Preview"; -// Note: there is deliberately no `Item.Name` / `Item.Size` leaf. Those are plain -// text with no attachment-domain logic — read them from `useAttachments()` (or -// `useAttachmentPill().attachment`) and render your own text (`formatSize` is -// exported for the byte label). `AttachmentPill.Label` also renders name + a -// secondary size/type line if you want the default treatment. We only expose -// leaves that carry domain binding (`.Remove`) or derived state (`.Icon` / -// `.Preview`) — never a wrapper around a bare ``. +/** + * `AttachmentsPanel.Item.Name` — the file-name line. Deliberately plain text: + * unlike `AttachmentPill.Label`, it does not wrap the name in a shimmer while + * the attachment uploads. Compose `AttachmentPill.Label` for that treatment. + */ +function AttachmentsPanelItemName( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element { + const { attachment } = useAttachmentPill(); + return ( + + {attachment.name || "Attachment"} + + ); +} +AttachmentsPanelItemName.displayName = "AttachmentsPanel.Item.Name"; + +/** + * `AttachmentsPanel.Item.Size` — the human-formatted byte label (`formatSize`). + * Renders nothing when the file has no size. Always the faint secondary color; + * the error/destructive state line stays with `AttachmentPill.Label`. + */ +function AttachmentsPanelItemSize( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element | null { + const { attachment } = useAttachmentPill(); + if (attachment.size == null) return null; + return ( + + {formatSize(attachment.size)} + + ); +} +AttachmentsPanelItemSize.displayName = "AttachmentsPanel.Item.Size"; /** Props for `AttachmentsPanel.Item.Remove`. */ export interface AttachmentsPanelItemRemoveProps { @@ -408,6 +445,8 @@ AttachmentsPanelItemRemove.displayName = "AttachmentsPanel.Item.Remove"; const AttachmentsPanelItemCompound = Object.assign(AttachmentsPanelItem, { Icon: AttachmentsPanelItemIcon, Preview: AttachmentsPanelItemPreview, + Name: AttachmentsPanelItemName, + Size: AttachmentsPanelItemSize, Remove: AttachmentsPanelItemRemove, }); AttachmentsPanelItem.displayName = "AttachmentsPanel.Item"; diff --git a/src/server/handlers/dev/framework-candidates.generated.ts b/src/server/handlers/dev/framework-candidates.generated.ts index 64c046da29..49894794c2 100644 --- a/src/server/handlers/dev/framework-candidates.generated.ts +++ b/src/server/handlers/dev/framework-candidates.generated.ts @@ -1729,8 +1729,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanel.Item", "AttachmentsPanel.Item.*", "AttachmentsPanel.Item.Icon", + "AttachmentsPanel.Item.Name", "AttachmentsPanel.Item.Preview", "AttachmentsPanel.Item.Remove", + "AttachmentsPanel.Item.Size", "AttachmentsPanel.List", "AttachmentsPanel.Loading", "AttachmentsPanel.Root", @@ -1770,6 +1772,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemMedia(", "AttachmentsPanelItemMenu", "AttachmentsPanelItemMenu(", + "AttachmentsPanelItemName(", + "AttachmentsPanelItemName,", + "AttachmentsPanelItemName.displayName", "AttachmentsPanelItemPreview(", "AttachmentsPanelItemPreview,", "AttachmentsPanelItemPreview.displayName", @@ -1780,6 +1785,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemRemove.displayName", "AttachmentsPanelItemRemoveProps", "AttachmentsPanelItemRemoveProps,", + "AttachmentsPanelItemSize(", + "AttachmentsPanelItemSize,", + "AttachmentsPanelItemSize.displayName", "AttachmentsPanelList", "AttachmentsPanelList(", "AttachmentsPanelList,", @@ -3972,8 +3980,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Italic", "Item", "Item.*", - "Item.Name", - "Item.Size", "Item:", "Items", "Iterable)", @@ -4734,6 +4740,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "NUL", "NUMERIC_PREFIX_PATTERN", "NUMERIC_PREFIX_PATTERN.test(raw)", + "Name", "Name:", "Named", "Narrow", @@ -5476,6 +5483,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "React.Ref;", "React.Ref;", "React.Ref;", + "React.Ref", "React.Ref;", "React.Ref;", "React.Ref}", @@ -6171,6 +6179,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "SingleToggleGroupRootProps", "SingleToggleGroupRootProps,", "Sink", + "Size", + "Size:", "Sized", "Skeleton", "Skeleton(", @@ -6562,7 +6572,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "ThinkingIndicatorProps>(", "Third-party", "This", - "Those", "Thought", "Thread", "Three", @@ -8504,7 +8513,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "attaching", "attachment", "attachment,", - "attachment-domain", "attachment-pill.tsx", "attachment.name", "attachment.name}", @@ -8886,7 +8894,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "bind", "bind,", "bind:", - "binding", "binding.", "binds", "bits", @@ -9916,6 +9923,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "color-mix(in", "color-mode.tsx", "color:", + "color;", "color={presentation.color}", "colorClass", "colorClass,", @@ -12086,6 +12094,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "error-boundary.tsx", "error.", "error.message}", + "error/destructive", "error:", "error;", "error={error}", @@ -12364,6 +12373,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "failures.push(new", "failures:", "failures[0];", + "faint", "faint.", "faithful", "fall", @@ -12436,10 +12446,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "fieldset", "fight", "file", + "file's", "file,", "file-${index}", "file-extension", "file-local", + "file-name", "file-type", "file-type-badge", "file-type-thumb", @@ -12744,6 +12756,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "formatSize(attachment.size);", "formatSize(attachment.size)}", "formatSize(bytes:", + "formatSize,", "formatStatus(status);", "formatStatus(status:", "formatTimestamp(createdAt:", @@ -13473,6 +13486,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "https://upload.invalid/", "https://veryfront.com/docs/code/guides/chat-ui#render-markdown-in-chat", "hue", + "human-formatted", "humanized", "humanizedId", "humanizedId;", @@ -14480,7 +14494,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "k}:${v}", "l", "label", - "label).", "label);", "label,", "label-left", @@ -14635,6 +14648,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "leaves", "leaves,", "leaves.", + "leaves:", "leaving", "left", "left),", @@ -14730,6 +14744,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "linear-gradient(var(--soft),", "lines", "lines)", + "lines),", "lines.", "lines.join(", "lines.map((w,", @@ -14888,7 +14903,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "lockstep", "log", "log-then-run", - "logic", "logic.", "logic:", "logic;", @@ -16825,7 +16839,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "overview", "own", "own).", - "own,", "own.", "own:", "own;", @@ -19939,7 +19952,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "size-[38px]", "size-full", "size.", - "size/type", "size:", "size=", "size={size}", @@ -20515,7 +20527,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "success.", "success:", "successful", - "such", "suffix", "sugar", "sugar.", @@ -21663,6 +21674,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "unlabeled", "unlabelled", "unless", + "unlike", "unlisted", "unlistedLegacyFallback", "unlockScroll", @@ -21786,6 +21798,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "uploads", "uploads),", "uploads,", + "uploads.", "uploads.length", "uploads.map((doc)", "uploads:", @@ -21868,14 +21881,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "useAppShell]", "useAttachmentPill", "useAttachmentPill()", - "useAttachmentPill().attachment", "useAttachmentPill();", "useAttachmentPill,", "useAttachmentPillContext;", "useAttachmentPillContext]", "useAttachments", "useAttachments(", - "useAttachments()", "useAttachments,", "useAttachments;", "useAttachmentsPanel",
`. +/** + * `AttachmentsPanel.Item.Name` — the file-name line. Deliberately plain text: + * unlike `AttachmentPill.Label`, it does not wrap the name in a shimmer while + * the attachment uploads. Compose `AttachmentPill.Label` for that treatment. + */ +function AttachmentsPanelItemName( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element { + const { attachment } = useAttachmentPill(); + return ( + + {attachment.name || "Attachment"} + + ); +} +AttachmentsPanelItemName.displayName = "AttachmentsPanel.Item.Name"; + +/** + * `AttachmentsPanel.Item.Size` — the human-formatted byte label (`formatSize`). + * Renders nothing when the file has no size. Always the faint secondary color; + * the error/destructive state line stays with `AttachmentPill.Label`. + */ +function AttachmentsPanelItemSize( + { className, ref, ...props }: + & React.HTMLAttributes + & { ref?: React.Ref }, +): React.JSX.Element | null { + const { attachment } = useAttachmentPill(); + if (attachment.size == null) return null; + return ( + + {formatSize(attachment.size)} + + ); +} +AttachmentsPanelItemSize.displayName = "AttachmentsPanel.Item.Size"; /** Props for `AttachmentsPanel.Item.Remove`. */ export interface AttachmentsPanelItemRemoveProps { @@ -408,6 +445,8 @@ AttachmentsPanelItemRemove.displayName = "AttachmentsPanel.Item.Remove"; const AttachmentsPanelItemCompound = Object.assign(AttachmentsPanelItem, { Icon: AttachmentsPanelItemIcon, Preview: AttachmentsPanelItemPreview, + Name: AttachmentsPanelItemName, + Size: AttachmentsPanelItemSize, Remove: AttachmentsPanelItemRemove, }); AttachmentsPanelItem.displayName = "AttachmentsPanel.Item"; diff --git a/src/server/handlers/dev/framework-candidates.generated.ts b/src/server/handlers/dev/framework-candidates.generated.ts index 64c046da29..49894794c2 100644 --- a/src/server/handlers/dev/framework-candidates.generated.ts +++ b/src/server/handlers/dev/framework-candidates.generated.ts @@ -1729,8 +1729,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanel.Item", "AttachmentsPanel.Item.*", "AttachmentsPanel.Item.Icon", + "AttachmentsPanel.Item.Name", "AttachmentsPanel.Item.Preview", "AttachmentsPanel.Item.Remove", + "AttachmentsPanel.Item.Size", "AttachmentsPanel.List", "AttachmentsPanel.Loading", "AttachmentsPanel.Root", @@ -1770,6 +1772,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemMedia(", "AttachmentsPanelItemMenu", "AttachmentsPanelItemMenu(", + "AttachmentsPanelItemName(", + "AttachmentsPanelItemName,", + "AttachmentsPanelItemName.displayName", "AttachmentsPanelItemPreview(", "AttachmentsPanelItemPreview,", "AttachmentsPanelItemPreview.displayName", @@ -1780,6 +1785,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "AttachmentsPanelItemRemove.displayName", "AttachmentsPanelItemRemoveProps", "AttachmentsPanelItemRemoveProps,", + "AttachmentsPanelItemSize(", + "AttachmentsPanelItemSize,", + "AttachmentsPanelItemSize.displayName", "AttachmentsPanelList", "AttachmentsPanelList(", "AttachmentsPanelList,", @@ -3972,8 +3980,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Italic", "Item", "Item.*", - "Item.Name", - "Item.Size", "Item:", "Items", "Iterable)", @@ -4734,6 +4740,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "NUL", "NUMERIC_PREFIX_PATTERN", "NUMERIC_PREFIX_PATTERN.test(raw)", + "Name", "Name:", "Named", "Narrow", @@ -5476,6 +5483,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "React.Ref;", "React.Ref;", "React.Ref;", + "React.Ref", "React.Ref;", "React.Ref;", "React.Ref}", @@ -6171,6 +6179,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "SingleToggleGroupRootProps", "SingleToggleGroupRootProps,", "Sink", + "Size", + "Size:", "Sized", "Skeleton", "Skeleton(", @@ -6562,7 +6572,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "ThinkingIndicatorProps>(", "Third-party", "This", - "Those", "Thought", "Thread", "Three", @@ -8504,7 +8513,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "attaching", "attachment", "attachment,", - "attachment-domain", "attachment-pill.tsx", "attachment.name", "attachment.name}", @@ -8886,7 +8894,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "bind", "bind,", "bind:", - "binding", "binding.", "binds", "bits", @@ -9916,6 +9923,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "color-mix(in", "color-mode.tsx", "color:", + "color;", "color={presentation.color}", "colorClass", "colorClass,", @@ -12086,6 +12094,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "error-boundary.tsx", "error.", "error.message}", + "error/destructive", "error:", "error;", "error={error}", @@ -12364,6 +12373,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "failures.push(new", "failures:", "failures[0];", + "faint", "faint.", "faithful", "fall", @@ -12436,10 +12446,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "fieldset", "fight", "file", + "file's", "file,", "file-${index}", "file-extension", "file-local", + "file-name", "file-type", "file-type-badge", "file-type-thumb", @@ -12744,6 +12756,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "formatSize(attachment.size);", "formatSize(attachment.size)}", "formatSize(bytes:", + "formatSize,", "formatStatus(status);", "formatStatus(status:", "formatTimestamp(createdAt:", @@ -13473,6 +13486,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "https://upload.invalid/", "https://veryfront.com/docs/code/guides/chat-ui#render-markdown-in-chat", "hue", + "human-formatted", "humanized", "humanizedId", "humanizedId;", @@ -14480,7 +14494,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "k}:${v}", "l", "label", - "label).", "label);", "label,", "label-left", @@ -14635,6 +14648,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "leaves", "leaves,", "leaves.", + "leaves:", "leaving", "left", "left),", @@ -14730,6 +14744,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "linear-gradient(var(--soft),", "lines", "lines)", + "lines),", "lines.", "lines.join(", "lines.map((w,", @@ -14888,7 +14903,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "lockstep", "log", "log-then-run", - "logic", "logic.", "logic:", "logic;", @@ -16825,7 +16839,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "overview", "own", "own).", - "own,", "own.", "own:", "own;", @@ -19939,7 +19952,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "size-[38px]", "size-full", "size.", - "size/type", "size:", "size=", "size={size}", @@ -20515,7 +20527,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "success.", "success:", "successful", - "such", "suffix", "sugar", "sugar.", @@ -21663,6 +21674,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "unlabeled", "unlabelled", "unless", + "unlike", "unlisted", "unlistedLegacyFallback", "unlockScroll", @@ -21786,6 +21798,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "uploads", "uploads),", "uploads,", + "uploads.", "uploads.length", "uploads.map((doc)", "uploads:", @@ -21868,14 +21881,12 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "useAppShell]", "useAttachmentPill", "useAttachmentPill()", - "useAttachmentPill().attachment", "useAttachmentPill();", "useAttachmentPill,", "useAttachmentPillContext;", "useAttachmentPillContext]", "useAttachments", "useAttachments(", - "useAttachments()", "useAttachments,", "useAttachments;", "useAttachmentsPanel",
+ {attachment.name || "Attachment"} +
+ {formatSize(attachment.size)} +