@cueplusplus/ui 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2250 -0
- package/dist/agent-runtime/_glyphs.js +43 -0
- package/dist/agent-runtime/branch-picker.d.ts +45 -0
- package/dist/agent-runtime/branch-picker.js +87 -0
- package/dist/agent-runtime/index.d.ts +4 -0
- package/dist/agent-runtime/index.js +4 -0
- package/dist/agent-runtime/tool-call-card.d.ts +69 -0
- package/dist/agent-runtime/tool-call-card.js +187 -0
- package/dist/agent-runtime/use-agent-thread.d.ts +164 -0
- package/dist/agent-runtime/use-agent-thread.js +251 -0
- package/dist/brand/_chassis.d.ts +1 -1
- package/dist/brand/cue-mark.d.ts +1 -1
- package/dist/brand/cue-mark.js +1 -1
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat/message-list.d.ts +6 -0
- package/dist/chat/message-list.js +35 -0
- package/dist/chrome/_edge-scroller.js +12 -1
- package/dist/chrome/_tabs-scroll.d.ts +1 -1
- package/dist/chrome/_tabs-scroll.js +1 -1
- package/dist/chrome/app-shell.d.ts +52 -2
- package/dist/chrome/app-shell.js +80 -8
- package/dist/chrome/index.d.ts +2 -2
- package/dist/configurator/_export.js +3 -2
- package/dist/configurator/_overrides.d.ts +1 -1
- package/dist/configurator/_overrides.js +18 -6
- package/dist/configurator/token-editor.js +4 -1
- package/dist/date/_segments.js +37 -2
- package/dist/elements/activity-graph.d.ts +40 -0
- package/dist/elements/activity-graph.js +81 -0
- package/dist/elements/agent-card.d.ts +49 -0
- package/dist/elements/agent-card.js +91 -0
- package/dist/elements/agent-handoff.d.ts +24 -0
- package/dist/elements/agent-handoff.js +53 -0
- package/dist/elements/agent-mode-badge.d.ts +72 -0
- package/dist/elements/agent-mode-badge.js +110 -0
- package/dist/elements/agent-plan.d.ts +24 -0
- package/dist/elements/agent-plan.js +74 -0
- package/dist/elements/agent-status.d.ts +29 -0
- package/dist/elements/agent-status.js +46 -0
- package/dist/elements/agent-surface.d.ts +65 -0
- package/dist/elements/agent-surface.js +68 -0
- package/dist/elements/animated-number.d.ts +86 -0
- package/dist/elements/animated-number.js +118 -0
- package/dist/elements/approval-card.d.ts +40 -0
- package/dist/elements/approval-card.js +78 -0
- package/dist/elements/artifact-card.d.ts +30 -0
- package/dist/elements/artifact-card.js +58 -0
- package/dist/elements/background-inbox.d.ts +39 -0
- package/dist/elements/background-inbox.js +60 -0
- package/dist/elements/canvas-split.d.ts +77 -0
- package/dist/elements/canvas-split.js +138 -0
- package/dist/elements/chart.d.ts +41 -0
- package/dist/elements/chart.js +117 -0
- package/dist/elements/chat-panel.d.ts +46 -0
- package/dist/elements/chat-panel.js +104 -0
- package/dist/elements/checkpoint-history.d.ts +31 -0
- package/dist/elements/checkpoint-history.js +65 -0
- package/dist/elements/clamp.d.ts +90 -0
- package/dist/elements/clamp.js +109 -0
- package/dist/elements/code-diff.d.ts +42 -0
- package/dist/elements/code-diff.js +69 -0
- package/dist/elements/code-runner.d.ts +38 -0
- package/dist/elements/code-runner.js +84 -0
- package/dist/elements/command-palette.d.ts +48 -0
- package/dist/elements/command-palette.js +129 -0
- package/dist/elements/compaction-row.d.ts +63 -0
- package/dist/elements/compaction-row.js +110 -0
- package/dist/elements/comparison-card.d.ts +47 -0
- package/dist/elements/comparison-card.js +69 -0
- package/dist/elements/composer.d.ts +274 -0
- package/dist/elements/composer.js +505 -0
- package/dist/elements/computer-use.d.ts +40 -0
- package/dist/elements/computer-use.js +92 -0
- package/dist/elements/confidence-marker.d.ts +39 -0
- package/dist/elements/confidence-marker.js +64 -0
- package/dist/elements/connection-state.d.ts +29 -0
- package/dist/elements/connection-state.js +66 -0
- package/dist/elements/context-breakdown.d.ts +30 -0
- package/dist/elements/context-breakdown.js +86 -0
- package/dist/elements/context-usage.d.ts +99 -0
- package/dist/elements/context-usage.js +225 -0
- package/dist/elements/conversation-search.d.ts +48 -0
- package/dist/elements/conversation-search.js +88 -0
- package/dist/elements/cost-meter.d.ts +33 -0
- package/dist/elements/cost-meter.js +75 -0
- package/dist/elements/data-table.d.ts +31 -0
- package/dist/elements/data-table.js +67 -0
- package/dist/elements/day-separator.d.ts +34 -0
- package/dist/elements/day-separator.js +50 -0
- package/dist/elements/diagram.d.ts +34 -0
- package/dist/elements/diagram.js +75 -0
- package/dist/elements/document-reference.d.ts +37 -0
- package/dist/elements/document-reference.js +66 -0
- package/dist/elements/draft-restore.d.ts +27 -0
- package/dist/elements/draft-restore.js +54 -0
- package/dist/elements/edit-message.d.ts +36 -0
- package/dist/elements/edit-message.js +77 -0
- package/dist/elements/elements.css +459 -0
- package/dist/elements/elicitation-form.d.ts +48 -0
- package/dist/elements/elicitation-form.js +106 -0
- package/dist/elements/empty-state.d.ts +52 -0
- package/dist/elements/empty-state.js +98 -0
- package/dist/elements/end-of-turn-summary.d.ts +60 -0
- package/dist/elements/end-of-turn-summary.js +75 -0
- package/dist/elements/error-state.d.ts +29 -0
- package/dist/elements/error-state.js +55 -0
- package/dist/elements/feedback-dialog.d.ts +36 -0
- package/dist/elements/feedback-dialog.js +81 -0
- package/dist/elements/file-tree.d.ts +45 -0
- package/dist/elements/file-tree.js +84 -0
- package/dist/elements/flow-graph.d.ts +45 -0
- package/dist/elements/flow-graph.js +78 -0
- package/dist/elements/generative/chart.js +200 -0
- package/dist/elements/generative/faults.js +111 -0
- package/dist/elements/generative/icons.d.ts +41 -0
- package/dist/elements/generative/icons.js +190 -0
- package/dist/elements/generative/intrinsics.js +483 -0
- package/dist/elements/generative/spec.d.ts +110 -0
- package/dist/elements/generative/spec.js +366 -0
- package/dist/elements/generative.css +1145 -0
- package/dist/elements/generative.d.ts +144 -0
- package/dist/elements/generative.js +126 -0
- package/dist/elements/guardrail-notice.d.ts +29 -0
- package/dist/elements/guardrail-notice.js +63 -0
- package/dist/elements/image-generation.d.ts +18 -0
- package/dist/elements/image-generation.js +64 -0
- package/dist/elements/index.d.ts +116 -0
- package/dist/elements/index.js +115 -0
- package/dist/elements/inline-citation.d.ts +43 -0
- package/dist/elements/inline-citation.js +93 -0
- package/dist/elements/job-progress.d.ts +49 -0
- package/dist/elements/job-progress.js +74 -0
- package/dist/elements/launcher-bubble.d.ts +34 -0
- package/dist/elements/launcher-bubble.js +72 -0
- package/dist/elements/live-region-announcer.d.ts +130 -0
- package/dist/elements/live-region-announcer.js +230 -0
- package/dist/elements/loading-state.d.ts +27 -0
- package/dist/elements/loading-state.js +36 -0
- package/dist/elements/map-answer.d.ts +38 -0
- package/dist/elements/map-answer.js +93 -0
- package/dist/elements/markdown.d.ts +131 -0
- package/dist/elements/markdown.js +528 -0
- package/dist/elements/math-block.d.ts +51 -0
- package/dist/elements/math-block.js +70 -0
- package/dist/elements/mcp-server-panel.d.ts +41 -0
- package/dist/elements/mcp-server-panel.js +115 -0
- package/dist/elements/memory-chips.d.ts +30 -0
- package/dist/elements/memory-chips.js +43 -0
- package/dist/elements/message-actions.d.ts +38 -0
- package/dist/elements/message-actions.js +71 -0
- package/dist/elements/message-attachment.d.ts +48 -0
- package/dist/elements/message-attachment.js +63 -0
- package/dist/elements/message-branches.d.ts +25 -0
- package/dist/elements/message-branches.js +65 -0
- package/dist/elements/message-pair.d.ts +33 -0
- package/dist/elements/message-pair.js +65 -0
- package/dist/elements/message-queue.d.ts +31 -0
- package/dist/elements/message-queue.js +80 -0
- package/dist/elements/message-timing.d.ts +33 -0
- package/dist/elements/message-timing.js +37 -0
- package/dist/elements/mobile-composer.d.ts +44 -0
- package/dist/elements/mobile-composer.js +88 -0
- package/dist/elements/model-picker.d.ts +38 -0
- package/dist/elements/model-picker.js +69 -0
- package/dist/elements/number-ticker.d.ts +18 -0
- package/dist/elements/number-ticker.js +47 -0
- package/dist/elements/onboarding.d.ts +30 -0
- package/dist/elements/onboarding.js +75 -0
- package/dist/elements/permission-grant.d.ts +35 -0
- package/dist/elements/permission-grant.js +86 -0
- package/dist/elements/permission-scopes.d.ts +215 -0
- package/dist/elements/permission-scopes.js +249 -0
- package/dist/elements/prompt-library.d.ts +43 -0
- package/dist/elements/prompt-library.js +117 -0
- package/dist/elements/queue-dock.d.ts +101 -0
- package/dist/elements/queue-dock.js +132 -0
- package/dist/elements/quota-banner.d.ts +26 -0
- package/dist/elements/quota-banner.js +66 -0
- package/dist/elements/quote-reply.d.ts +44 -0
- package/dist/elements/quote-reply.js +79 -0
- package/dist/elements/range.d.ts +29 -0
- package/dist/elements/range.js +45 -0
- package/dist/elements/read-aloud.d.ts +45 -0
- package/dist/elements/read-aloud.js +75 -0
- package/dist/elements/reasoning-effort.d.ts +36 -0
- package/dist/elements/reasoning-effort.js +67 -0
- package/dist/elements/reasoning-panel.d.ts +46 -0
- package/dist/elements/reasoning-panel.js +75 -0
- package/dist/elements/recommendation-card.d.ts +45 -0
- package/dist/elements/recommendation-card.js +83 -0
- package/dist/elements/regenerate-menu.d.ts +45 -0
- package/dist/elements/regenerate-menu.js +51 -0
- package/dist/elements/replay.d.ts +217 -0
- package/dist/elements/replay.js +498 -0
- package/dist/elements/research-report.d.ts +38 -0
- package/dist/elements/research-report.js +69 -0
- package/dist/elements/retrieval-chunks.d.ts +43 -0
- package/dist/elements/retrieval-chunks.js +81 -0
- package/dist/elements/revert-dock.d.ts +159 -0
- package/dist/elements/revert-dock.js +327 -0
- package/dist/elements/reviewable-diff.d.ts +45 -0
- package/dist/elements/reviewable-diff.js +110 -0
- package/dist/elements/risk-badge.d.ts +52 -0
- package/dist/elements/risk-badge.js +91 -0
- package/dist/elements/schedule-card.d.ts +42 -0
- package/dist/elements/schedule-card.js +90 -0
- package/dist/elements/score-breakdown.d.ts +47 -0
- package/dist/elements/score-breakdown.js +82 -0
- package/dist/elements/scroll-anchor.d.ts +32 -0
- package/dist/elements/scroll-anchor.js +121 -0
- package/dist/elements/settings-panel.d.ts +52 -0
- package/dist/elements/settings-panel.js +107 -0
- package/dist/elements/shared-conversation.d.ts +39 -0
- package/dist/elements/shared-conversation.js +68 -0
- package/dist/elements/sources.d.ts +33 -0
- package/dist/elements/sources.js +61 -0
- package/dist/elements/speaker-identity.d.ts +32 -0
- package/dist/elements/speaker-identity.js +51 -0
- package/dist/elements/spec-sheet.d.ts +31 -0
- package/dist/elements/spec-sheet.js +45 -0
- package/dist/elements/stopped-run.d.ts +22 -0
- package/dist/elements/stopped-run.js +50 -0
- package/dist/elements/streaming-text.d.ts +31 -0
- package/dist/elements/streaming-text.js +45 -0
- package/dist/elements/subagent-list.d.ts +37 -0
- package/dist/elements/subagent-list.js +79 -0
- package/dist/elements/suggestions.d.ts +30 -0
- package/dist/elements/suggestions.js +36 -0
- package/dist/elements/surfaces.d.ts +85 -0
- package/dist/elements/surfaces.js +107 -0
- package/dist/elements/tail-status.d.ts +131 -0
- package/dist/elements/tail-status.js +94 -0
- package/dist/elements/terminal-block.d.ts +29 -0
- package/dist/elements/terminal-block.js +54 -0
- package/dist/elements/thinking-indicator.d.ts +18 -0
- package/dist/elements/thinking-indicator.js +36 -0
- package/dist/elements/thread-list.d.ts +35 -0
- package/dist/elements/thread-list.js +68 -0
- package/dist/elements/thread-search.d.ts +42 -0
- package/dist/elements/thread-search.js +99 -0
- package/dist/elements/timeline.d.ts +37 -0
- package/dist/elements/timeline.js +46 -0
- package/dist/elements/todo-list.d.ts +42 -0
- package/dist/elements/todo-list.js +59 -0
- package/dist/elements/tool-call.d.ts +41 -0
- package/dist/elements/tool-call.js +85 -0
- package/dist/elements/tool-error.d.ts +39 -0
- package/dist/elements/tool-error.js +76 -0
- package/dist/elements/tool-group.d.ts +45 -0
- package/dist/elements/tool-group.js +71 -0
- package/dist/elements/tool-timeline.d.ts +65 -0
- package/dist/elements/tool-timeline.js +88 -0
- package/dist/elements/trace-waterfall.d.ts +37 -0
- package/dist/elements/trace-waterfall.js +69 -0
- package/dist/elements/transcript-rules.d.ts +67 -0
- package/dist/elements/transcript-rules.js +95 -0
- package/dist/elements/turn-footer.d.ts +74 -0
- package/dist/elements/turn-footer.js +101 -0
- package/dist/elements/typing-indicator.d.ts +16 -0
- package/dist/elements/typing-indicator.js +51 -0
- package/dist/elements/verdict-row.d.ts +89 -0
- package/dist/elements/verdict-row.js +150 -0
- package/dist/elements/vocabulary.d.ts +612 -0
- package/dist/elements/vocabulary.js +611 -0
- package/dist/elements/voice-conversation.d.ts +49 -0
- package/dist/elements/voice-conversation.js +123 -0
- package/dist/elements/web-preview.d.ts +32 -0
- package/dist/elements/web-preview.js +66 -0
- package/dist/elements/web-search.d.ts +37 -0
- package/dist/elements/web-search.js +66 -0
- package/dist/elements/work-collapse.d.ts +103 -0
- package/dist/elements/work-collapse.js +131 -0
- package/dist/icons/containers.d.ts +89 -0
- package/dist/icons/containers.js +295 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/index.d.ts +9 -7
- package/dist/index.js +3 -2
- package/dist/instruments/index.d.ts +2 -2
- package/dist/instruments/index.js +2 -2
- package/dist/instruments/table.d.ts +14 -1
- package/dist/instruments/table.js +20 -1
- package/dist/instruments/two-step-button.js +25 -6
- package/dist/layout/sidebar.d.ts +19 -1
- package/dist/layout/sidebar.js +19 -1
- package/dist/primitives/button.js +11 -1
- package/dist/primitives/chip.d.ts +34 -8
- package/dist/primitives/chip.js +97 -11
- package/dist/styles.css +11 -1
- package/dist/system/agent-skin.d.ts +27 -0
- package/dist/system/agent-skin.js +20 -0
- package/dist/system/index.d.ts +3 -3
- package/dist/system/portal.d.ts +5 -3
- package/dist/system/portal.js +36 -8
- package/dist/system/prepaint.d.ts +20 -2
- package/dist/system/prepaint.js +39 -33
- package/dist/system/theme-provider.d.ts +13 -2
- package/dist/system/theme-provider.js +107 -26
- package/dist/system/use-density.d.ts +9 -0
- package/dist/system/use-density.js +9 -0
- package/dist/system/use-isomorphic-layout-effect.js +19 -1
- package/dist/theming/_presets.d.ts +1 -1
- package/dist/theming/_presets.js +124 -8
- package/dist/theming/contrast.d.ts +16 -4
- package/dist/theming/contrast.js +33 -6
- package/dist/theming/create-theme.d.ts +8 -4
- package/dist/theming/create-theme.js +56 -20
- package/dist/theming/serialize.d.ts +11 -2
- package/dist/theming/serialize.js +87 -9
- package/manifest/components/accordion.json +154 -0
- package/manifest/components/activity-graph.json +88 -0
- package/manifest/components/agent-card.json +120 -0
- package/manifest/components/agent-handoff.json +87 -0
- package/manifest/components/agent-mode-badge.json +84 -0
- package/manifest/components/agent-pile.json +116 -0
- package/manifest/components/agent-plan.json +67 -0
- package/manifest/components/agent-status.json +73 -0
- package/manifest/components/agent-surface.json +78 -0
- package/manifest/components/alert-dialog.json +190 -0
- package/manifest/components/animated-number.json +67 -0
- package/manifest/components/app-bar.json +244 -0
- package/manifest/components/app-shell.json +383 -0
- package/manifest/components/app-window-frame.json +102 -0
- package/manifest/components/approval-card.json +106 -0
- package/manifest/components/artifact-card.json +81 -0
- package/manifest/components/ask-box.json +209 -0
- package/manifest/components/audience-icon.json +52 -0
- package/manifest/components/autocomplete.json +197 -0
- package/manifest/components/avatar-group.json +72 -0
- package/manifest/components/avatar.json +95 -0
- package/manifest/components/background-inbox.json +71 -0
- package/manifest/components/branch-picker.json +124 -0
- package/manifest/components/breadcrumb.json +74 -0
- package/manifest/components/button-group.json +74 -0
- package/manifest/components/button.json +126 -0
- package/manifest/components/calendar.json +65 -0
- package/manifest/components/canvas-split-body.json +62 -0
- package/manifest/components/canvas-split-document.json +54 -0
- package/manifest/components/canvas-split-header.json +83 -0
- package/manifest/components/canvas-split-line.json +62 -0
- package/manifest/components/canvas-split-message.json +62 -0
- package/manifest/components/canvas-split-thread.json +54 -0
- package/manifest/components/canvas-split.json +60 -0
- package/manifest/components/card.json +140 -0
- package/manifest/components/carousel.json +200 -0
- package/manifest/components/catalogue-icon.json +52 -0
- package/manifest/components/channel-beta-icon.json +52 -0
- package/manifest/components/channel-matrix.json +173 -0
- package/manifest/components/channel-released-icon.json +52 -0
- package/manifest/components/chart-container.json +111 -0
- package/manifest/components/chart-ramp.json +72 -0
- package/manifest/components/chart-swatch.json +65 -0
- package/manifest/components/chart-tooltip-content.json +114 -0
- package/manifest/components/chart.json +93 -0
- package/manifest/components/chat-empty-state.json +55 -0
- package/manifest/components/chat-panel-assistant-message.json +51 -0
- package/manifest/components/chat-panel-composer.json +66 -0
- package/manifest/components/chat-panel-messages.json +51 -0
- package/manifest/components/chat-panel-typing.json +51 -0
- package/manifest/components/chat-panel-user-message.json +51 -0
- package/manifest/components/chat-panel.json +57 -0
- package/manifest/components/checkbox-group.json +68 -0
- package/manifest/components/checkbox.json +93 -0
- package/manifest/components/checkpoint-history.json +80 -0
- package/manifest/components/chip.json +144 -0
- package/manifest/components/clamp.json +102 -0
- package/manifest/components/cli-tool-icon.json +52 -0
- package/manifest/components/code-diff.json +87 -0
- package/manifest/components/code-runner.json +97 -0
- package/manifest/components/collapsible.json +120 -0
- package/manifest/components/color-area.json +59 -0
- package/manifest/components/color-field.json +79 -0
- package/manifest/components/color-picker.json +175 -0
- package/manifest/components/color-slider.json +75 -0
- package/manifest/components/color-swatch.json +68 -0
- package/manifest/components/colors-section.json +66 -0
- package/manifest/components/combobox.json +198 -0
- package/manifest/components/command-palette.json +177 -0
- package/manifest/components/compaction-row.json +100 -0
- package/manifest/components/comparison-card.json +83 -0
- package/manifest/components/composer-actions.json +65 -0
- package/manifest/components/composer-attach-button.json +65 -0
- package/manifest/components/composer-attachment-chip.json +75 -0
- package/manifest/components/composer-attachments.json +60 -0
- package/manifest/components/composer-bar.json +73 -0
- package/manifest/components/composer-command-item.json +79 -0
- package/manifest/components/composer-context.json +67 -0
- package/manifest/components/composer-input.json +73 -0
- package/manifest/components/composer-menu-item.json +72 -0
- package/manifest/components/composer-menu.json +79 -0
- package/manifest/components/composer-model-item.json +75 -0
- package/manifest/components/composer-model-trigger.json +75 -0
- package/manifest/components/composer-person-item.json +79 -0
- package/manifest/components/composer-send.json +80 -0
- package/manifest/components/composer-toolbar.json +65 -0
- package/manifest/components/composer-voice-button.json +68 -0
- package/manifest/components/composer-voice.json +75 -0
- package/manifest/components/composer.json +176 -0
- package/manifest/components/computer-use.json +84 -0
- package/manifest/components/confidence-marker.json +74 -0
- package/manifest/components/connection-state.json +84 -0
- package/manifest/components/container.json +60 -0
- package/manifest/components/context-breakdown.json +65 -0
- package/manifest/components/context-menu.json +121 -0
- package/manifest/components/context-usage.json +85 -0
- package/manifest/components/conversation-search.json +92 -0
- package/manifest/components/copy-button.json +93 -0
- package/manifest/components/cost-meter.json +74 -0
- package/manifest/components/cue-logotype.json +96 -0
- package/manifest/components/cue-mark.json +97 -0
- package/manifest/components/cue-portal-frame.json +55 -0
- package/manifest/components/data-table-pagination.json +83 -0
- package/manifest/components/data-table-toolbar.json +104 -0
- package/manifest/components/date-field.json +80 -0
- package/manifest/components/date-picker.json +191 -0
- package/manifest/components/date-range-picker.json +191 -0
- package/manifest/components/day-separator.json +58 -0
- package/manifest/components/delegation-card.json +144 -0
- package/manifest/components/density.json +58 -0
- package/manifest/components/description-list.json +84 -0
- package/manifest/components/diagram.json +100 -0
- package/manifest/components/dialog.json +198 -0
- package/manifest/components/disclosure.json +109 -0
- package/manifest/components/dmx-bar.json +94 -0
- package/manifest/components/dmx-strip.json +101 -0
- package/manifest/components/document-reference.json +90 -0
- package/manifest/components/draft-restore.json +82 -0
- package/manifest/components/drawer.json +179 -0
- package/manifest/components/dropdown-menu.json +229 -0
- package/manifest/components/edit-message.json +105 -0
- package/manifest/components/elements-command-palette.json +100 -0
- package/manifest/components/elements-composer.json +71 -0
- package/manifest/components/elements-data-table.json +68 -0
- package/manifest/components/elements-timeline.json +69 -0
- package/manifest/components/elicitation-form.json +103 -0
- package/manifest/components/empty-state-composer.json +64 -0
- package/manifest/components/empty-state-greeting.json +49 -0
- package/manifest/components/empty-state-suggestion.json +57 -0
- package/manifest/components/empty-state-suggestions.json +49 -0
- package/manifest/components/empty-state.json +90 -0
- package/manifest/components/end-of-turn-summary.json +77 -0
- package/manifest/components/env-var-input.json +150 -0
- package/manifest/components/error-state.json +80 -0
- package/manifest/components/export-dialog.json +84 -0
- package/manifest/components/eyebrow.json +56 -0
- package/manifest/components/feedback-dialog.json +107 -0
- package/manifest/components/field-description.json +63 -0
- package/manifest/components/field-error.json +77 -0
- package/manifest/components/field-label.json +63 -0
- package/manifest/components/field.json +79 -0
- package/manifest/components/file-tree.json +83 -0
- package/manifest/components/file-upload.json +159 -0
- package/manifest/components/flow-background.json +50 -0
- package/manifest/components/flow-controls.json +50 -0
- package/manifest/components/flow-graph.json +75 -0
- package/manifest/components/folder-icon.json +52 -0
- package/manifest/components/footer.json +112 -0
- package/manifest/components/frac.json +59 -0
- package/manifest/components/generation-loader.json +64 -0
- package/manifest/components/generative-ui.json +93 -0
- package/manifest/components/grid.json +81 -0
- package/manifest/components/group-bar.json +102 -0
- package/manifest/components/guardrail-notice.json +91 -0
- package/manifest/components/hover-card.json +151 -0
- package/manifest/components/icon-button.json +88 -0
- package/manifest/components/image-generation.json +67 -0
- package/manifest/components/info-tip.json +98 -0
- package/manifest/components/inline-citation.json +77 -0
- package/manifest/components/input-group.json +91 -0
- package/manifest/components/input.json +89 -0
- package/manifest/components/item.json +129 -0
- package/manifest/components/job-progress.json +96 -0
- package/manifest/components/kbd.json +52 -0
- package/manifest/components/launcher-bubble.json +105 -0
- package/manifest/components/link.json +81 -0
- package/manifest/components/live-region-announcer.json +79 -0
- package/manifest/components/log-viewer.json +139 -0
- package/manifest/components/map-answer.json +83 -0
- package/manifest/components/markdown-text.json +94 -0
- package/manifest/components/math-block.json +74 -0
- package/manifest/components/mcp-server-icon.json +52 -0
- package/manifest/components/mcp-server-panel.json +87 -0
- package/manifest/components/memory-chips.json +65 -0
- package/manifest/components/menubar.json +157 -0
- package/manifest/components/message-actions.json +99 -0
- package/manifest/components/message-attachments.json +63 -0
- package/manifest/components/message-branches.json +71 -0
- package/manifest/components/message-list.json +100 -0
- package/manifest/components/message-pair.json +86 -0
- package/manifest/components/message-queue.json +74 -0
- package/manifest/components/message-timing.json +62 -0
- package/manifest/components/message.json +116 -0
- package/manifest/components/meter.json +114 -0
- package/manifest/components/mobile-composer.json +126 -0
- package/manifest/components/model-picker.json +76 -0
- package/manifest/components/multi-select.json +190 -0
- package/manifest/components/musical-time-input.json +113 -0
- package/manifest/components/navigation-menu.json +226 -0
- package/manifest/components/node-card.json +109 -0
- package/manifest/components/node-handle.json +69 -0
- package/manifest/components/number-field.json +107 -0
- package/manifest/components/number-ticker.json +61 -0
- package/manifest/components/onboarding.json +84 -0
- package/manifest/components/otp-field.json +128 -0
- package/manifest/components/page-shell.json +96 -0
- package/manifest/components/pagination.json +92 -0
- package/manifest/components/panel-header.json +77 -0
- package/manifest/components/panel.json +71 -0
- package/manifest/components/password-input.json +119 -0
- package/manifest/components/permission-grant.json +92 -0
- package/manifest/components/permission-scopes.json +139 -0
- package/manifest/components/piano-keyboard.json +120 -0
- package/manifest/components/plussie.json +111 -0
- package/manifest/components/popover.json +193 -0
- package/manifest/components/preset-section.json +73 -0
- package/manifest/components/progress.json +95 -0
- package/manifest/components/prompt-library.json +99 -0
- package/manifest/components/queue-dock.json +110 -0
- package/manifest/components/quota-banner.json +94 -0
- package/manifest/components/quote-reply.json +105 -0
- package/manifest/components/radio-group.json +73 -0
- package/manifest/components/radio.json +80 -0
- package/manifest/components/rating.json +141 -0
- package/manifest/components/read-aloud.json +111 -0
- package/manifest/components/reasoning-effort.json +81 -0
- package/manifest/components/reasoning-panel.json +108 -0
- package/manifest/components/recommendation-card.json +107 -0
- package/manifest/components/regenerate-menu.json +89 -0
- package/manifest/components/replay-player.json +102 -0
- package/manifest/components/research-report.json +76 -0
- package/manifest/components/resizable.json +88 -0
- package/manifest/components/retrieval-chunks.json +84 -0
- package/manifest/components/revert-dock.json +150 -0
- package/manifest/components/reviewable-diff.json +93 -0
- package/manifest/components/risk-badge.json +74 -0
- package/manifest/components/row.json +109 -0
- package/manifest/components/schedule-card.json +101 -0
- package/manifest/components/score-breakdown.json +91 -0
- package/manifest/components/scroll-anchor.json +72 -0
- package/manifest/components/scroll-area.json +91 -0
- package/manifest/components/scrollable-tabs-list.json +83 -0
- package/manifest/components/scrub-input.json +200 -0
- package/manifest/components/seam-cell.json +75 -0
- package/manifest/components/seam-grid.json +75 -0
- package/manifest/components/seam-list.json +56 -0
- package/manifest/components/search-input.json +134 -0
- package/manifest/components/section-header.json +112 -0
- package/manifest/components/segmented-control.json +138 -0
- package/manifest/components/select.json +194 -0
- package/manifest/components/separator.json +53 -0
- package/manifest/components/settings-panel.json +118 -0
- package/manifest/components/shape-section.json +66 -0
- package/manifest/components/shared-conversation.json +89 -0
- package/manifest/components/sheet.json +186 -0
- package/manifest/components/shimmer-label.json +59 -0
- package/manifest/components/sidebar.json +198 -0
- package/manifest/components/signal-edge.json +56 -0
- package/manifest/components/skeleton.json +46 -0
- package/manifest/components/skill-icon.json +52 -0
- package/manifest/components/slider.json +89 -0
- package/manifest/components/sources.json +83 -0
- package/manifest/components/sparkline.json +76 -0
- package/manifest/components/speaker-identity.json +61 -0
- package/manifest/components/spec-sheet.json +80 -0
- package/manifest/components/spectrum-visualizer.json +115 -0
- package/manifest/components/spinner.json +61 -0
- package/manifest/components/stack-icon.json +52 -0
- package/manifest/components/stack.json +96 -0
- package/manifest/components/stacks-matrix-icon.json +52 -0
- package/manifest/components/stat.json +87 -0
- package/manifest/components/status-bar.json +58 -0
- package/manifest/components/status-dot.json +78 -0
- package/manifest/components/stepper.json +89 -0
- package/manifest/components/stopped-run.json +81 -0
- package/manifest/components/streaming-text.json +71 -0
- package/manifest/components/sub.json +44 -0
- package/manifest/components/subagent-list.json +88 -0
- package/manifest/components/suggestions.json +84 -0
- package/manifest/components/sup.json +44 -0
- package/manifest/components/swap-label.json +71 -0
- package/manifest/components/switch.json +87 -0
- package/manifest/components/table-scroll-region.json +69 -0
- package/manifest/components/table.json +155 -0
- package/manifest/components/tabs.json +145 -0
- package/manifest/components/tags-input.json +190 -0
- package/manifest/components/tail-status.json +109 -0
- package/manifest/components/terminal-block.json +91 -0
- package/manifest/components/terminal-frame.json +119 -0
- package/manifest/components/textarea.json +76 -0
- package/manifest/components/theme-configurator.json +124 -0
- package/manifest/components/theme-provider.json +134 -0
- package/manifest/components/thinking-indicator.json +63 -0
- package/manifest/components/thread-list.json +76 -0
- package/manifest/components/thread-search.json +90 -0
- package/manifest/components/threshold-rail.json +160 -0
- package/manifest/components/time-boundary.json +62 -0
- package/manifest/components/time-field.json +80 -0
- package/manifest/components/timeline-ruler.json +139 -0
- package/manifest/components/timeline.json +115 -0
- package/manifest/components/title-bar.json +82 -0
- package/manifest/components/toast.json +130 -0
- package/manifest/components/todo-list.json +69 -0
- package/manifest/components/toggle-group.json +106 -0
- package/manifest/components/toggle.json +109 -0
- package/manifest/components/token-editor.json +90 -0
- package/manifest/components/tool-call-card.json +200 -0
- package/manifest/components/tool-call.json +119 -0
- package/manifest/components/tool-error.json +114 -0
- package/manifest/components/tool-group.json +83 -0
- package/manifest/components/tool-timeline.json +119 -0
- package/manifest/components/toolbar.json +163 -0
- package/manifest/components/tooltip.json +164 -0
- package/manifest/components/trace-waterfall.json +74 -0
- package/manifest/components/tree-visibility-toggle.json +84 -0
- package/manifest/components/tree.json +140 -0
- package/manifest/components/turn-footer.json +105 -0
- package/manifest/components/two-step-button.json +113 -0
- package/manifest/components/typing-indicator.json +53 -0
- package/manifest/components/universe-grid.json +127 -0
- package/manifest/components/unread-divider.json +55 -0
- package/manifest/components/usage-chart.json +124 -0
- package/manifest/components/verdict-row.json +91 -0
- package/manifest/components/voice-conversation.json +109 -0
- package/manifest/components/web-preview.json +87 -0
- package/manifest/components/web-search.json +92 -0
- package/manifest/components/work-collapse.json +102 -0
- package/manifest/manifest.json +4779 -0
- package/manifest/tokens.json +159 -0
- package/package.json +54 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,2255 @@
|
|
|
1
1
|
# @cueplusplus/ui
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f71a189: ## The hooks are documented API
|
|
8
|
+
|
|
9
|
+
`useAgentThread` had no page. Neither did `useTheme`, `useToast`, `useCueTable`
|
|
10
|
+
or the eight others — not because they are private, but because the generator
|
|
11
|
+
that writes this library's documentation only ever looked for components. A
|
|
12
|
+
symbol you are told to import, whose shape decides how you write the call, was
|
|
13
|
+
reaching you as a name in a changelog and a type in your editor and nothing
|
|
14
|
+
else. An agent reading the manifest could not answer "what does `useCueTable`
|
|
15
|
+
take" at all.
|
|
16
|
+
|
|
17
|
+
`manifest.json` now carries a `hooks` array beside `components`, with every
|
|
18
|
+
`use*` an entry subpath exports:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
useAgentThread @cueplusplus/ui/agent-runtime
|
|
22
|
+
useCarousel @cueplusplus/ui/layout/carousel
|
|
23
|
+
useControlHeight @cueplusplus/ui
|
|
24
|
+
useCuePortalProps @cueplusplus/ui
|
|
25
|
+
useCueTable @cueplusplus/ui/instruments/data-table
|
|
26
|
+
useDensity @cueplusplus/ui
|
|
27
|
+
useFlowTheme @cueplusplus/ui/flow
|
|
28
|
+
useSidebar @cueplusplus/ui
|
|
29
|
+
useTheme @cueplusplus/ui
|
|
30
|
+
useToast @cueplusplus/ui
|
|
31
|
+
useZoom @cueplusplus/ui/configurator
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each entry carries what the source already knew and had nowhere to put: the
|
|
35
|
+
`signature` TypeScript resolves for it (`(size?: ControlSize): number`, not a
|
|
36
|
+
prop table with nothing in it), the description and summary, `@param`,
|
|
37
|
+
`@returns` and `@throws` where the JSDoc has them, every `@example` — all of
|
|
38
|
+
them, so `useFlowTheme`'s second one survives — plus `group`, `importPath`,
|
|
39
|
+
`peerDependencies`, `clientOnly` and the page it lives on.
|
|
40
|
+
|
|
41
|
+
The array is **inline**. A component's row points at a companion file because
|
|
42
|
+
its props, parts, variants and tokens do not fit; a hook's whole documentation
|
|
43
|
+
does, so `manifest.json` answers about a hook with no second fetch. That is why
|
|
44
|
+
a hook row has `url` and `mdUrl` but no `jsonUrl` — there is no companion to
|
|
45
|
+
name.
|
|
46
|
+
|
|
47
|
+
Every hook is `clientOnly: true`, and every one of them says so on its page. The
|
|
48
|
+
seven internal look-alikes — `useComposedRefs`, `useDebouncedValue`,
|
|
49
|
+
`useEdgeScroll` and the rest — are a component's own machinery, no entry exports
|
|
50
|
+
them, and none of them appears. A new one showing up in this array is a
|
|
51
|
+
decision, not a diff.
|
|
52
|
+
|
|
53
|
+
## Two hooks that could not have carried a page
|
|
54
|
+
|
|
55
|
+
**`useDensity` now shows the read it exists for.** Its description always said
|
|
56
|
+
this is the JS read for code that cannot ask CSS — virtualised row heights,
|
|
57
|
+
canvas drawing, layout math — and then showed you none of it. It carries the
|
|
58
|
+
virtualiser idiom now: the level, the measured row height beside it, and the
|
|
59
|
+
overscan that has to move with the level because shorter rows mean more of them
|
|
60
|
+
on screen.
|
|
61
|
+
|
|
62
|
+
**`useSidebar` was one sentence long.** It said it reads the collapse state and
|
|
63
|
+
stopped, which left the two things a caller actually needs undocumented: that it
|
|
64
|
+
hands back `toggle` as well as `collapsed` — the same valve `Sidebar.Rail`
|
|
65
|
+
pulls, so a collapse control does not have to live on the rail — and what
|
|
66
|
+
happens outside a `Sidebar.Root`. It does not throw there. It reports
|
|
67
|
+
`collapsed: false` and a `toggle` that does nothing, because a row rendered on
|
|
68
|
+
its own in a test or a docs page is a legitimate thing and the honest answer for
|
|
69
|
+
it is that nothing is collapsed. Its example swaps a label for an icon and keeps
|
|
70
|
+
the `aria-label` across both.
|
|
71
|
+
|
|
72
|
+
Behaviour is unchanged in both. This is JSDoc.
|
|
73
|
+
|
|
74
|
+
## Finding one
|
|
75
|
+
|
|
76
|
+
Hooks share the components namespace on the docs site — `/docs/components/use-agent-thread`,
|
|
77
|
+
and the markdown twin at `…/use-agent-thread.md` — so a link to a hook looks
|
|
78
|
+
like a link to anything else, the sidebar lists them under their group, and
|
|
79
|
+
`llms.txt` and `llms-full.txt` carry them. `useAgentThread`'s page mounts the
|
|
80
|
+
live scripted thread that already ran under **Agent runtime**, because the bench
|
|
81
|
+
now names the hook it was built to show.
|
|
82
|
+
|
|
83
|
+
The skills references list them per group, and the lookup script answers:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
node scripts/lookup.mjs --props useAgentThread # signature, params, returns, example
|
|
87
|
+
node scripts/lookup.mjs --hooks # all eleven, with their import paths
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## What did not change
|
|
91
|
+
|
|
92
|
+
`schemaVersion` is still `1` — this is an addition, and additions never bump it.
|
|
93
|
+
`components` still means components: every count, badge, table and `--list` that
|
|
94
|
+
said "155 components" says it still, and none of them quietly grew by eleven. No
|
|
95
|
+
component's documentation moved, no runtime code changed, and nothing new is
|
|
96
|
+
required of a consumer who never reads the manifest.
|
|
97
|
+
|
|
98
|
+
- 8991fa3: ## Every state watchable
|
|
99
|
+
|
|
100
|
+
An agent UI is mostly states nobody can get to. A tool call that is _running_
|
|
101
|
+
holds that shape for four hundred milliseconds; a run _waiting on a person_
|
|
102
|
+
holds it until somebody answers; a _cancelled_ turn is a picture you have to
|
|
103
|
+
break something to see. This library has ninety-five components for those
|
|
104
|
+
states and, until now, two ways of showing them: a still, or a script that plays
|
|
105
|
+
once and cannot be stopped, rewound or linked to.
|
|
106
|
+
|
|
107
|
+
Now every state a run passes through is watchable — the run has a transport, the
|
|
108
|
+
transport has a scrubber, and the frame you stopped on has an address. `t=8` in
|
|
109
|
+
a gallery link opens the run paused on the approval, in your theme, at your
|
|
110
|
+
density, on the device you picked.
|
|
111
|
+
|
|
112
|
+
**`ReplayPlayer`** ships on its own subpath, `@cueplusplus/ui/elements/replay`,
|
|
113
|
+
with `useReplay` beside it for a console that wants the transport and not the
|
|
114
|
+
chrome.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import {
|
|
118
|
+
ReplayPlayer,
|
|
119
|
+
type ReplayScript,
|
|
120
|
+
} from "@cueplusplus/ui/elements/replay";
|
|
121
|
+
|
|
122
|
+
const SCRIPT: ReplayScript = {
|
|
123
|
+
title: "Patching universe 3",
|
|
124
|
+
frames: [
|
|
125
|
+
{
|
|
126
|
+
scene: (
|
|
127
|
+
<ToolCall
|
|
128
|
+
label="Read"
|
|
129
|
+
activeLabel="Reading the plot"
|
|
130
|
+
query="plot.md"
|
|
131
|
+
request='{"path":"plot.md"}'
|
|
132
|
+
result=""
|
|
133
|
+
running
|
|
134
|
+
open={false}
|
|
135
|
+
onOpenChange={() => {}}
|
|
136
|
+
/>
|
|
137
|
+
),
|
|
138
|
+
hold: 600,
|
|
139
|
+
label: "tool-call",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
scene: (
|
|
143
|
+
<ApprovalCard
|
|
144
|
+
state="request"
|
|
145
|
+
command="cue write --cue 1"
|
|
146
|
+
title="Write the first cue"
|
|
147
|
+
subtitle="Universe 3, channel 1"
|
|
148
|
+
/>
|
|
149
|
+
),
|
|
150
|
+
hold: 1600,
|
|
151
|
+
label: "awaiting-approval",
|
|
152
|
+
},
|
|
153
|
+
{ scene: <MarkdownText>{answer}</MarkdownText>, hold: 0, label: "done" },
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
<ReplayPlayer script={SCRIPT} />;
|
|
158
|
+
<ReplayPlayer script={SCRIPT} startAt={17} autoPlay={false} rate={0.5} />;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### A controller over a filmstrip, not an animation
|
|
162
|
+
|
|
163
|
+
Every frame carries the **whole scene** rather than the part that changed, so
|
|
164
|
+
the component's entire rendering rule is `frames[index].scene`. That is what
|
|
165
|
+
makes the position controls honest: frame 12 reached by scrubbing is the same
|
|
166
|
+
DOM as frame 12 reached by watching, because there is no accumulated state for
|
|
167
|
+
the two paths to disagree about. A player whose frames accumulated — the way a
|
|
168
|
+
simulated terminal accumulates printed lines inside its own closure — can be
|
|
169
|
+
played and it can be restarted, but it can never be scrubbed.
|
|
170
|
+
|
|
171
|
+
### One clock, so pause is exact
|
|
172
|
+
|
|
173
|
+
The two older players in this library pace themselves with a chain of
|
|
174
|
+
`setTimeout`s, which makes pause impossible: the time already spent inside the
|
|
175
|
+
in-flight timeout is unrecoverable, so a resume can only start the hold again.
|
|
176
|
+
Here the playhead is a number of _script_ milliseconds and wall time reaches it
|
|
177
|
+
through one monotonic origin — `base + (now() - origin) * rate` — so pausing is
|
|
178
|
+
"write down the playhead", resuming is "take a new origin", and the unspent
|
|
179
|
+
380 ms of a 400 ms hold is still 380 ms on the other side of the pause. The same
|
|
180
|
+
identity is what makes the rate multiplier exact rather than approximate, and
|
|
181
|
+
what makes a seek and a play land on the same frame.
|
|
182
|
+
|
|
183
|
+
The transport is play, pause, step either way, scrub, restart and rate, built
|
|
184
|
+
from shipped cue components — a `Slider` for the scrubber, `IconButton`s, a
|
|
185
|
+
`Chip` for the state — and painted in cue's own metrics even when the scene
|
|
186
|
+
inside it is an `AgentSurface` island at upstream fidelity. The chrome belongs
|
|
187
|
+
to the console showing the run, not to the run.
|
|
188
|
+
|
|
189
|
+
### Reduced motion is a mode, not an ending
|
|
190
|
+
|
|
191
|
+
Under `prefers-reduced-motion: reduce` **nothing is ever scheduled** — not
|
|
192
|
+
scheduled and hidden, not scheduled at speed, not scheduled at all — and the
|
|
193
|
+
player opens on the resting frame. Every control except play and rate keeps
|
|
194
|
+
working, so a reader who asked for stillness can still step through the run one
|
|
195
|
+
frame at a time and read every state it passes through. That is strictly more
|
|
196
|
+
than "show the end and stop", which is what this library offered before.
|
|
197
|
+
|
|
198
|
+
### Legible from outside
|
|
199
|
+
|
|
200
|
+
The root publishes `data-replay-state` (the current frame's authored label),
|
|
201
|
+
`data-replay-playing` and `data-replay-frame`, the way `TerminalFrame` publishes
|
|
202
|
+
`data-simulating` — enough for a stylesheet, a screenshot harness or a test to
|
|
203
|
+
say "show me the awaiting-approval frame" without reaching into React.
|
|
204
|
+
|
|
205
|
+
State labels are **authored, not derived**: nothing in this tree emits a
|
|
206
|
+
run-state event and a scripted thread has no clock, so each frame carries its
|
|
207
|
+
own word from a set of seven — `queued`, `thinking`, `tool-call`,
|
|
208
|
+
`awaiting-approval`, `streaming`, `cancelled`, `done`. Two kinds of waiting, two
|
|
209
|
+
kinds of work, the one state that is waiting on a _human_, and the two endings a
|
|
210
|
+
transcript really has.
|
|
211
|
+
|
|
212
|
+
### No peer, and no icon set
|
|
213
|
+
|
|
214
|
+
`./elements/replay` declares an empty peer row in the dist contract, and that is
|
|
215
|
+
a claim being checked rather than an omission: the five transport glyphs are
|
|
216
|
+
drawn in the file at 16px. A player is chrome, and chrome that drags an icon set
|
|
217
|
+
in behind it is a dependency nobody chose.
|
|
218
|
+
|
|
219
|
+
## One run, written down once
|
|
220
|
+
|
|
221
|
+
A player needs something to play, and eight example pages hand-authoring their
|
|
222
|
+
own transcripts would have produced eight different fictions — eight inventions
|
|
223
|
+
of what a `grep` returns, eight decisions about what a cancelled call says, and
|
|
224
|
+
a reader who cannot compare two of them because they are not looking at the same
|
|
225
|
+
run.
|
|
226
|
+
|
|
227
|
+
So the run is data, in the corpus (`@repo/specimens`) rather than in the
|
|
228
|
+
package: fourteen tool payloads — read, glob, grep, bash, edit, write, patch,
|
|
229
|
+
plan, todowrite, task, webfetch, websearch, question, mcp — each readable at any
|
|
230
|
+
of six statuses through one total function, `withStatus(fixture, status)`, whose
|
|
231
|
+
totality the suite pins at all eighty-four pairs. Beside them, fourteen message
|
|
232
|
+
samples: turns with mentions and attachments, three reasoning blocks, five
|
|
233
|
+
answers including the one with a table and the one with sources, and `streamed`
|
|
234
|
+
for the part arriving a word at a time.
|
|
235
|
+
|
|
236
|
+
`CANONICAL_TRANSCRIPT` is what they compose into — the dark-mode-toggle run the
|
|
237
|
+
Cursor evidence recorded, in twenty-three beats over two turns: read, grep,
|
|
238
|
+
glob, plan, edit, shell, web search, a markdown answer, then a follow-up whose
|
|
239
|
+
edit streams in. A transcript is not a script; it is the _content_ of one, with
|
|
240
|
+
no opinion about how a beat is drawn. `transcriptScript(transcript, scene)` is
|
|
241
|
+
the join: hand it the way your surface draws a beat and it hands back a
|
|
242
|
+
`ReplayScript` with the frame count, the state labels and the captions already
|
|
243
|
+
right.
|
|
244
|
+
|
|
245
|
+
## Eight studies, at fidelity, named
|
|
246
|
+
|
|
247
|
+
The gallery gains eight pages, all playing that same twenty-three-beat run, so
|
|
248
|
+
the only thing that differs between any two of them is the chrome.
|
|
249
|
+
|
|
250
|
+
Five are **fidelity studies of named consumer products** — ChatGPT, Claude,
|
|
251
|
+
Gemini, Grok and Perplexity — drawn to the trade dress the research recorded:
|
|
252
|
+
ChatGPT's always-visible action bar against Claude's hover-and-focus one,
|
|
253
|
+
Gemini's gradient-carried surface with no avatars, Grok's near-monochrome fold
|
|
254
|
+
of consecutive calls into one group, Perplexity's answer-engine shape where the
|
|
255
|
+
question becomes a heading over a strip of sources. Three are **studies of
|
|
256
|
+
IDE-style consoles** — Claude Code's timeline rail and number-keyed permission
|
|
257
|
+
card, Cursor's five-level conversation-density switch, Hermes's composer status
|
|
258
|
+
stack with approval answered inline.
|
|
259
|
+
|
|
260
|
+
These are internal reference and they say so on their own faces: every one of
|
|
261
|
+
the eight carries a header note naming the product and its maker, stating that
|
|
262
|
+
the page is a fidelity study drawn from the elements family, that it is **not a
|
|
263
|
+
cue product surface**, and that it is not affiliated with the maker — beside a
|
|
264
|
+
chip that says whether the palette was quoted from the record or matched to it.
|
|
265
|
+
Nothing here is a template to ship a product on. They exist so the family can be
|
|
266
|
+
judged against the surfaces it will be asked to imitate.
|
|
267
|
+
|
|
268
|
+
Two gallery axes make them linkable: `surface` (`cue` or `aui`, the value-set
|
|
269
|
+
the page's `AgentSurface` island paints in) and `t`, the frame the replay opens
|
|
270
|
+
on. Both compose with theme, density, mode and device rather than overriding
|
|
271
|
+
any, and `t` is optional rather than defaulted — a stamped `t=0` pauses the run
|
|
272
|
+
on its first frame, while no `t` at all lets it play, and defaulting would have
|
|
273
|
+
turned every copied link into the paused one.
|
|
274
|
+
|
|
275
|
+
The corpus and the site are `private: true` and none of this reaches the
|
|
276
|
+
tarball. It is here because it is the evidence for the minor above: a transport
|
|
277
|
+
nobody has driven through twenty-three states on eight different chromes is a
|
|
278
|
+
transport nobody has tested.
|
|
279
|
+
|
|
280
|
+
## What did not change
|
|
281
|
+
|
|
282
|
+
The root barrel and `@cueplusplus/ui/elements` gained nothing — no export, no
|
|
283
|
+
peer, no bytes. The transport is chrome _around_ the elements family rather than
|
|
284
|
+
a member of it, so the group's import graph stays exactly as the vendor sync
|
|
285
|
+
emitted it. No component's props, structure or rendering moved; `elements.css`,
|
|
286
|
+
the island, the tokens and the sync pin are untouched.
|
|
287
|
+
|
|
288
|
+
- 7fb3004: ## Model output, rendered
|
|
289
|
+
|
|
290
|
+
Every component in this library has been able to say one thing about a string a
|
|
291
|
+
model wrote: it goes into the DOM as characters. `Message` places it,
|
|
292
|
+
`ToolCallCard` places it, all ninety-four elements place it, and the `Chat`
|
|
293
|
+
group's doctrine header said so in a line — **text is text**. That line was
|
|
294
|
+
doing real work. A component that quietly parsed model output would put every
|
|
295
|
+
consumer of this library one prompt away from an injection, because the
|
|
296
|
+
practical shape of prompt injection is not "the model turned evil", it is "the
|
|
297
|
+
model faithfully relayed bytes an attacker left on a page it fetched".
|
|
298
|
+
|
|
299
|
+
Verbatim is still the default, everywhere, unchanged. What this release adds is
|
|
300
|
+
the one exception, and everything about how it ships is an argument that it
|
|
301
|
+
should stay one: **`MarkdownText`**, on its own subpath
|
|
302
|
+
`@cueplusplus/ui/elements/markdown`, with its own two peers, not re-exported
|
|
303
|
+
from `@cueplusplus/ui/elements` and never going to be.
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { Message } from "@cueplusplus/ui";
|
|
307
|
+
import { MarkdownText } from "@cueplusplus/ui/elements/markdown";
|
|
308
|
+
|
|
309
|
+
<Message agent="researcher">
|
|
310
|
+
<MarkdownText streaming={turn.status === "running"}>
|
|
311
|
+
{turn.text}
|
|
312
|
+
</MarkdownText>
|
|
313
|
+
</Message>;
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`Message` did not change to make that work. It places what it is given, and what
|
|
317
|
+
it is given is a string until a caller writes the line above — which is the
|
|
318
|
+
whole design: rendering markup is a line of code somebody wrote, not a behaviour
|
|
319
|
+
they inherited.
|
|
320
|
+
|
|
321
|
+
### What renders
|
|
322
|
+
|
|
323
|
+
Twenty tags, written out in the source rather than inherited from
|
|
324
|
+
`hast-util-sanitize`'s `defaultSchema`, because a spread schema means the set of
|
|
325
|
+
tags this library vouches for is whatever a transitive dependency decided this
|
|
326
|
+
week.
|
|
327
|
+
|
|
328
|
+
| | |
|
|
329
|
+
| ------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
330
|
+
| blocks | paragraphs, ATX and setext headings, bullet and ordered lists, block quotes, thematic breaks, fenced and indented code |
|
|
331
|
+
| inline | emphasis, strong, strikethrough, inline code, links, images, hard breaks |
|
|
332
|
+
|
|
333
|
+
Headings start at `h2` and clamp at `h6` — model output is a passage inside a
|
|
334
|
+
page that already owns its `h1` — and `headingLevel` moves the base. Fenced
|
|
335
|
+
blocks are `<pre><code>` containing the model's characters and nothing else
|
|
336
|
+
until `renderCodeBlock` is passed: a highlighter is a second parser over the
|
|
337
|
+
same untrusted string, usually one that emits HTML, so opting into it is
|
|
338
|
+
deliberate. Every part carries a `data-slot`, so an app restyles a heading or a
|
|
339
|
+
quote from the outside without a prop for it.
|
|
340
|
+
|
|
341
|
+
### What is dropped
|
|
342
|
+
|
|
343
|
+
**Raw HTML has no path to the DOM, and the first reason is an absence rather
|
|
344
|
+
than a setting.** `react-markdown` spreads its own `allowDangerousHtml: true`
|
|
345
|
+
over whatever `remarkRehypeOptions` it is given, so that option cannot be turned
|
|
346
|
+
off from here and `<img onerror=…>` does become a `raw` node. A `raw` node is a
|
|
347
|
+
span of source text rather than an element, and the one thing that turns it back
|
|
348
|
+
into markup is `rehype-raw`. Nothing in this package depends on that plugin, and
|
|
349
|
+
a test asserts it never will — a `pnpm add rehype-raw` is a failing build, not a
|
|
350
|
+
quiet demotion of defence-in-depth to defence. `rehype-sanitize` then drops the
|
|
351
|
+
`raw` node type outright, and `skipHtml` removes whatever survived. Three gates,
|
|
352
|
+
each of them disabled in turn in the suite to prove they are redundant rather
|
|
353
|
+
than load-bearing one at a time.
|
|
354
|
+
|
|
355
|
+
None of the three is a deny-list of dangerous tags. A `<script>`, an `<iframe>`,
|
|
356
|
+
an `<svg>`, a `<form>` and an `onclick=` are the same non-event: there is
|
|
357
|
+
nothing in the tree for them to be.
|
|
358
|
+
|
|
359
|
+
**Attributes are enumerated, not filtered.** An element outside the subset
|
|
360
|
+
renders nothing; an element inside it renders only the attributes named beside
|
|
361
|
+
it — `href`/`title` on a link, `src`/`alt`/`title` on an image, `start` on an
|
|
362
|
+
ordered list, a single `language-…` class on a `code`. Everything else gets the
|
|
363
|
+
empty list. No element in the subset accepts `id`, `style`, `srcset`,
|
|
364
|
+
`formaction` or any `on*` handler, so none of them has an attribute to arrive
|
|
365
|
+
on. Comments and doctypes are refused, and DOM clobbering has no attribute to
|
|
366
|
+
travel on.
|
|
367
|
+
|
|
368
|
+
**URLs are allow-listed twice, and to three protocols.** `http`, `https` and
|
|
369
|
+
`mailto`, on both `href` and `src`. A relative URL stays relative; a
|
|
370
|
+
scheme-relative `//host` does not. A `javascript:` link, a `data:text/html`
|
|
371
|
+
image, `vbscript:`, `blob:`, `file:` and the `irc:`/`xmpp:` handlers React's
|
|
372
|
+
default pipeline permits all lose the attribute and leave an inert element
|
|
373
|
+
behind — an anchor with no destination, not an anchor to the page you are on.
|
|
374
|
+
|
|
375
|
+
**Every link carries `rel="noopener noreferrer nofollow"`, and that is not a
|
|
376
|
+
prop.** Reverse tabnabbing, a private console URL travelling in a `Referer`
|
|
377
|
+
header, and SEO laundering through somebody's transcript are not things a caller
|
|
378
|
+
should be able to turn off by accident. `target` is configurable (`_blank` by
|
|
379
|
+
default, because a transcript that navigates away loses the run); `rel` is not.
|
|
380
|
+
Images are `loading="lazy"`, `decoding="async"`, `referrerPolicy="no-referrer"`.
|
|
381
|
+
|
|
382
|
+
No GFM plugin is enabled, so there are no tables, task lists, footnotes or
|
|
383
|
+
autolink literals — an explicit `<https://…>` autolink still works. Task lists
|
|
384
|
+
are absent on purpose as much as by omission: a checkbox inside a transcript is
|
|
385
|
+
a control a model drew.
|
|
386
|
+
|
|
387
|
+
The threat model above is on the component, in its JSDoc, which means it is on
|
|
388
|
+
the docs page, in the manifest and in a consumer's hover — not in a wiki that
|
|
389
|
+
drifts from the code. It is held by 122 adversarial assertions run three ways:
|
|
390
|
+
settled, streaming, and at every prefix of a hostile stream.
|
|
391
|
+
|
|
392
|
+
### Streaming
|
|
393
|
+
|
|
394
|
+
Pass `streaming` while the turn is arriving and the token the model is halfway
|
|
395
|
+
through typing is hidden rather than rendered as punctuation. Streaming markdown
|
|
396
|
+
is not malformed markdown — CommonMark has an answer for every truncation, and
|
|
397
|
+
that answer is the problem: `**bo` is a paragraph containing two literal
|
|
398
|
+
asterisks, and three frames later it is bold text and the asterisks are gone.
|
|
399
|
+
The visible result is punctuation flickering through every emphasis, link and
|
|
400
|
+
code span as it arrives, with a reflow behind it.
|
|
401
|
+
|
|
402
|
+
Six rules, covering unclosed fences, odd inline-code runs, half-written links
|
|
403
|
+
and autolinks, and unmatched emphasis openers — the words appear the moment the
|
|
404
|
+
model writes them and only their formatting waits for the closing token. None of
|
|
405
|
+
it is a second parser and none of it can widen what renders: every rule only
|
|
406
|
+
ever _deletes_ characters before the source reaches the pipeline, so a sealed
|
|
407
|
+
stream passes the same three gates as a settled document.
|
|
408
|
+
|
|
409
|
+
### The line in `Chat` that changed
|
|
410
|
+
|
|
411
|
+
One doctrine bullet, from "**Text is text.** Nothing here parses markdown or
|
|
412
|
+
HTML" to "**Text is text, unless somebody says otherwise.**" — the same default,
|
|
413
|
+
now named as a default rather than as the only option, pointing at the renderer
|
|
414
|
+
for when a consumer wants it. No component in that group changed.
|
|
415
|
+
|
|
416
|
+
## The vocabulary a model composes
|
|
417
|
+
|
|
418
|
+
The second half of this release is the other end of the same problem. A model
|
|
419
|
+
that can only answer in prose answers a flight query in prose. `GenerativeUI`
|
|
420
|
+
takes a tree of JSON a model emitted — 27 intrinsic names, nested — and draws
|
|
421
|
+
it.
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
import { GenerativeUI } from "@cueplusplus/ui/elements/generative";
|
|
425
|
+
import "@cueplusplus/ui/elements/generative.css";
|
|
426
|
+
|
|
427
|
+
// A `GenerativeAction` carries the intrinsic that fired it as `type`, so it is
|
|
428
|
+
// named rather than spread over a message discriminator of the same name.
|
|
429
|
+
<GenerativeUI
|
|
430
|
+
spec={JSON.parse(toolResult)}
|
|
431
|
+
onAction={({ action, type, value }) =>
|
|
432
|
+
send({ type: "ui-action", action, intrinsic: type, value })
|
|
433
|
+
}
|
|
434
|
+
onFault={(fault) => telemetry.warn("generative", fault)}
|
|
435
|
+
/>;
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| | the 27 |
|
|
439
|
+
| -------------- | --------------------------------------------------------------------- |
|
|
440
|
+
| layout (7) | `Card` `Row` `Col` `Box` `Spacer` `Divider` `Carousel` |
|
|
441
|
+
| typography (4) | `Header` `Text` `Caption` `Markdown` |
|
|
442
|
+
| data (4) | `Fact` `Table` `Chart` `Badge` |
|
|
443
|
+
| media (2) | `Image` `Icon` |
|
|
444
|
+
| controls (7) | `Button` `Select` `Input` `DatePicker` `Checkbox` `RadioGroup` `Form` |
|
|
445
|
+
| lists (2) | `ListView` `ListViewItem` |
|
|
446
|
+
| feedback (1) | `Alert` |
|
|
447
|
+
|
|
448
|
+
Every other component here is handed props by a programmer, and a wrong one
|
|
449
|
+
costs a type error at the call site. This one is handed a tree by a language
|
|
450
|
+
model over a network, so `spec` is typed `unknown` — a signature demanding a
|
|
451
|
+
node would push every consumer into the same cast, which is a claim about the
|
|
452
|
+
bytes nobody is in a position to make — and the interesting question is not what
|
|
453
|
+
a correct spec renders as. It is what an incorrect one does, and the answer is
|
|
454
|
+
never an exception:
|
|
455
|
+
|
|
456
|
+
- a `$type` from a newer catalogue draws an in-place notice and its siblings
|
|
457
|
+
still render;
|
|
458
|
+
- a spec that is not a component tree draws a legible error state, never a blank
|
|
459
|
+
box that looks like a message still streaming;
|
|
460
|
+
- a tree deeper than 32 levels stops and says so rather than exhausting the
|
|
461
|
+
stack and taking the console with it;
|
|
462
|
+
- a component that throws anyway is caught by a boundary the renderer mounts
|
|
463
|
+
above its own walk.
|
|
464
|
+
|
|
465
|
+
All four also arrive as a stream on `onFault` — `not-a-node`, `unknown-type`,
|
|
466
|
+
`missing-prop`, `too-deep`, each with a dotted path — because the person who
|
|
467
|
+
needs to know this console is a version behind the model driving it is usually
|
|
468
|
+
not the person looking at it.
|
|
469
|
+
|
|
470
|
+
Two gates keep a widget from becoming a beacon. `Box` and `Card` take a
|
|
471
|
+
`background` and a size the model writes freely, and those land in an inline
|
|
472
|
+
style: colours are allow-listed by _shape_ (a hex triple, a numeric colour
|
|
473
|
+
function, a bare keyword) so `url(…)` and `image-set(…)` cannot reach a
|
|
474
|
+
`background`, and lengths must be a plain number-and-unit, which is enough for
|
|
475
|
+
`50%`, `12rem` and `320px` and not enough for `calc(…)` or `var(…)`.
|
|
476
|
+
|
|
477
|
+
### It needs no Tailwind
|
|
478
|
+
|
|
479
|
+
The stylesheet is plain `[data-aui]` CSS and the tree it draws carries no
|
|
480
|
+
`class` attribute anywhere. That is upstream's arrangement and it is worth
|
|
481
|
+
keeping precisely: a widget a model composed has to draw inside whatever console
|
|
482
|
+
it was sent to, and a console is very often somebody else's app with no utility
|
|
483
|
+
framework, no shadcn and no design system of its own. Every hook is an attribute
|
|
484
|
+
rather than a class, because a class is a claim on a name in a host's one global
|
|
485
|
+
namespace and `[data-aui="card"]` collides with nothing.
|
|
486
|
+
|
|
487
|
+
The consequence runs all the way down. The 24 icons are hand-drawn SVG rather
|
|
488
|
+
than `lucide-react`; the chart is hand-drawn SVG rather than `recharts`; the
|
|
489
|
+
carousel snaps with CSS rather than a script. **`./elements/generative` declares
|
|
490
|
+
no optional peer at all** — an empty row in the dist-contract's peer map, which
|
|
491
|
+
is that claim checked against the built package — so there is no package in the
|
|
492
|
+
tree whose absence turns a weather card into a stack trace.
|
|
493
|
+
|
|
494
|
+
The sheet has two honest modes, and one seam between them. Exactly one rule in
|
|
495
|
+
1,100 lines reads `--cue-*` tokens, and every reference in it carries a literal
|
|
496
|
+
fallback: with `@cueplusplus/ui/styles.css` present the vocabulary is cue's
|
|
497
|
+
palette, type ladder and density; without it, it is a plain legible widget that
|
|
498
|
+
a `<link>` away works in a host that has never heard of cue. Every other rule
|
|
499
|
+
spends `--aui-*` and nothing else, which is what makes a re-skin forty
|
|
500
|
+
declarations in one place rather than a hunt through two hundred rules.
|
|
501
|
+
|
|
502
|
+
### Both skins reach it, two ways
|
|
503
|
+
|
|
504
|
+
The vocabulary answers the same two axes `AgentSurface` publishes, and it
|
|
505
|
+
answers them whether they arrive as props or by inheritance:
|
|
506
|
+
|
|
507
|
+
```tsx
|
|
508
|
+
<GenerativeUI spec={spec} skin="aui" fidelity="upstream" />;
|
|
509
|
+
|
|
510
|
+
<AgentSurface skin="aui">
|
|
511
|
+
<GenerativeUI spec={spec} /> {/* no props: the stamp is on an ancestor */}
|
|
512
|
+
</AgentSurface>;
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Every island rule that lands on the root is written with both selectors — the
|
|
516
|
+
stamp on an ancestor, and the stamp on the root itself — so a tree that is its
|
|
517
|
+
own island and a tree inside a transcript paint identically.
|
|
518
|
+
|
|
519
|
+
- **`skin`** — `cue` by default. `aui` is upstream's own
|
|
520
|
+
`data-aui-theme="elements"` layer, transcribed: a 20px card, a 12px borderless
|
|
521
|
+
field on a filled ground, a pill button, a shadow instead of a hairline, and
|
|
522
|
+
labels that stop shouting. Geometry only — the colour re-pointing arrives
|
|
523
|
+
through `elements.css`, so an `aui`-skinned widget still tracks the app's
|
|
524
|
+
light and dark rather than pinning a palette of its own.
|
|
525
|
+
- **`fidelity`** — `cue-metrics` by default, which drives the five density knobs
|
|
526
|
+
upstream names (`--aui-control-height`, `--aui-control-font-size`,
|
|
527
|
+
`--aui-button-padding-x`, `--aui-field-padding-x`, `--aui-card-padding`) and
|
|
528
|
+
the five type steps off cue's ladder. `upstream` pins assistant-ui's own
|
|
529
|
+
numbers instead — a 36px control, 14px control type, `px-4`, `px-3`, `p-5` —
|
|
530
|
+
so density stops at the island edge. The two blocks are line for line, and a
|
|
531
|
+
test keeps them that way: a property one pins and the other does not restore
|
|
532
|
+
is a value that survives a nesting, and the subtree would draw at neither
|
|
533
|
+
fidelity.
|
|
534
|
+
|
|
535
|
+
### Where the two halves meet
|
|
536
|
+
|
|
537
|
+
Once, through one prop. The `Markdown` intrinsic renders its source verbatim —
|
|
538
|
+
as characters — unless `renderMarkdown` is given, because the generative subpath
|
|
539
|
+
ships no parser and will not acquire one:
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
<GenerativeUI
|
|
543
|
+
spec={spec}
|
|
544
|
+
renderMarkdown={(md) => <MarkdownText>{md}</MarkdownText>}
|
|
545
|
+
/>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Neither subpath depends on the other. A console drawing weather cards installs
|
|
549
|
+
no sanitizer; a console rendering markdown installs no vocabulary.
|
|
550
|
+
|
|
551
|
+
## Where to look
|
|
552
|
+
|
|
553
|
+
Benched at the states that matter rather than at the happy path. `MarkdownText`
|
|
554
|
+
on `elements-thread`: a settled document exercising the whole subset, whose last
|
|
555
|
+
line is a real injection shape and renders as nothing; a turn cut mid-token; a
|
|
556
|
+
fence still filling. `GenerativeUI` on `elements-knowledge`: a composed
|
|
557
|
+
dashboard, a form with all six control types, a chart, an unknown component, and
|
|
558
|
+
a spec that is not a tree at all. Pages are at `/docs/components/markdown-text`
|
|
559
|
+
and `/docs/components/generative-ui`.
|
|
560
|
+
|
|
561
|
+
## What did not change
|
|
562
|
+
|
|
563
|
+
The root barrel, and `@cueplusplus/ui/elements`. Both entries gained nothing —
|
|
564
|
+
no export, no peer, no bytes. `react-markdown` and `rehype-sanitize` are
|
|
565
|
+
optional peers of `./elements/markdown` alone, externalised rather than inlined
|
|
566
|
+
for the usual reason and one more: bundling a sanitizer would freeze a security
|
|
567
|
+
boundary at whatever version was in the store the day this package was built,
|
|
568
|
+
where a consumer's `pnpm update` could never reach it.
|
|
569
|
+
|
|
570
|
+
No component's props, structure or rendering moved. The `Chat` group is the same
|
|
571
|
+
six components it was; `Message`, `AskBox`, `ToolCallCard` and all ninety-four
|
|
572
|
+
elements still render what they are given as characters. `elements.css`, the
|
|
573
|
+
island, the tokens and the sync pin are untouched.
|
|
574
|
+
|
|
575
|
+
One accessibility fix, found by running axe over the specs a model can emit
|
|
576
|
+
rather than over the benches: a `ListViewItem` a model put outside a `ListView`
|
|
577
|
+
drew as `role="listitem"`, which requires a `list` parent it did not have. A
|
|
578
|
+
stray row now draws as a row and says nothing about where it is, which is true.
|
|
579
|
+
|
|
580
|
+
`schemaVersion` is still `1`.
|
|
581
|
+
|
|
582
|
+
- 42e0672: ## One voice at the foot of the transcript
|
|
583
|
+
|
|
584
|
+
Two elements in this family are live regions. `TailStatus` is a `role="status"`,
|
|
585
|
+
because that is what makes the row at the foot of a transcript reach a reader
|
|
586
|
+
who cannot see the spinner. `LiveRegionAnnouncer` is a polite region with a
|
|
587
|
+
queue, and the nine transitions in `ANNOUNCEMENTS` are the same states that row
|
|
588
|
+
narrates.
|
|
589
|
+
|
|
590
|
+
A console that mounted both — the ordinary shape of an agent console, and the
|
|
591
|
+
one the benches show — had every state read out twice, out of step, because the
|
|
592
|
+
two regions update at different moments. Neither file mentioned the other.
|
|
593
|
+
|
|
594
|
+
They compose now, and the rule is that **the announcer speaks and the row
|
|
595
|
+
draws**:
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
<TailStatus action={TAIL_ACTIONS.searchingCode} detail="composer.tsx" />;
|
|
599
|
+
<LiveRegionAnnouncer messages={log} />;
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Mounting an announcer takes the speech off the row automatically. The words stay
|
|
603
|
+
on the screen, the `role="status"` goes, and `data-announces="false"` says which
|
|
604
|
+
of the two it is doing. Nothing to wire: the row notices on its own, wherever in
|
|
605
|
+
the tree either of them sits.
|
|
606
|
+
|
|
607
|
+
The row is the half that defers because the announcer is the better speaker. It
|
|
608
|
+
covers transitions the row can never show — an error, a permission request, a
|
|
609
|
+
finished answer. It is mounted empty and stays mounted, which is the only way a
|
|
610
|
+
live region is announced reliably, while a tail row comes and goes with the run.
|
|
611
|
+
And its whole mechanism — 400ms of spacing, five deep, paused while the page is
|
|
612
|
+
hidden or a modal has the reader — exists because narration that changes several
|
|
613
|
+
times a second is unusable when it is spoken, which is exactly what the row is.
|
|
614
|
+
|
|
615
|
+
Two ways to decide it by hand:
|
|
616
|
+
|
|
617
|
+
- `<TailStatus announce />` — the row speaks even with an announcer mounted, for
|
|
618
|
+
a console whose announcer is fed something other than these actions.
|
|
619
|
+
- `<TailStatus announce={false} />` — the row never speaks, for a console that
|
|
620
|
+
narrates the foot of the transcript some other way.
|
|
621
|
+
|
|
622
|
+
And `useAnnouncerPresent()` is exported, so a console's own status row can ask
|
|
623
|
+
the same question before becoming a second live region.
|
|
624
|
+
|
|
625
|
+
- 51e5046: ## The amber carries its own ink too
|
|
626
|
+
|
|
627
|
+
This is the amber half of the argument `the-red-carries-its-own-ink` already
|
|
628
|
+
made, and it is the same argument: the tone a control is _filled_ with decides
|
|
629
|
+
what can be read on it, and no other token in the palette knows that.
|
|
630
|
+
|
|
631
|
+
`TwoStepButton` is the one control in the library whose whole reason for
|
|
632
|
+
existing is that the press is consequential — arm, then confirm. Its `warn`
|
|
633
|
+
confirm painted `bg-warn text-black`. Black on an amber is a good guess, and on
|
|
634
|
+
the ten dark blocks it is right by a distance: 7.11:1 to 12.41:1. It is wrong on
|
|
635
|
+
the light ones, because a light theme darkens its amber to clear the paper and a
|
|
636
|
+
darkened amber wants paper back. **3.58:1** on requestport, **3.74:1** on dusk,
|
|
637
|
+
**4.22:1** on cue's own light block — three of the twenty preset x mode blocks
|
|
638
|
+
under AA, on the half of the control that does the thing.
|
|
639
|
+
|
|
640
|
+
It could not be closed before: there was no `--cue-warn-fg` to close it with,
|
|
641
|
+
and the queued note for the red said so in as many words. There is one now.
|
|
642
|
+
|
|
643
|
+
### `--cue-warn-fg`
|
|
644
|
+
|
|
645
|
+
A new authored anchor, per preset, per mode, in the colour contract beside the
|
|
646
|
+
tone it is chosen against, with `text-warn-fg` / `bg-warn-fg` bridged into
|
|
647
|
+
Tailwind like every other colour token:
|
|
648
|
+
|
|
649
|
+
```css
|
|
650
|
+
[data-theme="cue"] {
|
|
651
|
+
--cue-warn: oklch(0.8 0.17 75);
|
|
652
|
+
--cue-warn-fg: #000000;
|
|
653
|
+
}
|
|
654
|
+
[data-theme="cue"][data-mode="light"] {
|
|
655
|
+
--cue-warn: oklch(0.55 0.115 70);
|
|
656
|
+
--cue-warn-fg: #fcfcfc;
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Not one value is hand-picked. Each is the same `pickInk` `createTheme()` already
|
|
661
|
+
uses for `accent-fg` and `danger-fg`, run over the extremes the theme itself
|
|
662
|
+
owns — its page, its deepest well, its type — falling to pure black or white
|
|
663
|
+
only where none of the three reaches AA. Run over the reds instead, it
|
|
664
|
+
reproduces all twenty shipped `danger-fg` values exactly, spelling included,
|
|
665
|
+
which is the check that this is the same derivation rather than a second one
|
|
666
|
+
wearing its name.
|
|
667
|
+
|
|
668
|
+
The amber does not answer the way the red did, and that is the argument for
|
|
669
|
+
deriving against each tone instead of writing one rule about ink on a status
|
|
670
|
+
colour. All ten dark blocks take their deepest ground. The light ten split three
|
|
671
|
+
ways: cue, dusk and requestport take their paper; hivehub, luma, signal and
|
|
672
|
+
terminal take the theme's own type; quotamate, snuffle and venu take plain black
|
|
673
|
+
because nothing they own reaches AA on their amber. The worst amber pair in the
|
|
674
|
+
system is **4.77:1** (luma, light). The worst pair anywhere is still the red's
|
|
675
|
+
4.59:1.
|
|
676
|
+
|
|
677
|
+
`CONTRAST_REQUIREMENTS` gains an eleventh pair, `warn-fg/warn` at 4.5:1, so a
|
|
678
|
+
generated palette is judged on it too, and `createTheme()` derives it for one.
|
|
679
|
+
The configurator shows it beside the amber in the Status group. The assistant-ui
|
|
680
|
+
island declares its own, because that block re-tints `--cue-warn` and an ink
|
|
681
|
+
chosen against the app's amber is an ink chosen against a colour nobody is
|
|
682
|
+
looking at in there.
|
|
683
|
+
|
|
684
|
+
### The sweep covers the amber now
|
|
685
|
+
|
|
686
|
+
`test/destructive-ink.test.ts` reads the source and measures what the components
|
|
687
|
+
_spend_ rather than what the palette _offers_. It swept the red and said in its
|
|
688
|
+
own prose that it did not sweep the amber, because sweeping a defect nobody
|
|
689
|
+
could fix turns a missing token into a red suite instead of into a decision. The
|
|
690
|
+
token exists, so the amber is in: every class string that makes `bg-warn` a
|
|
691
|
+
ground and puts an ink on it, every rule in the package's own stylesheets that
|
|
692
|
+
does the same, measured on all twenty blocks at 4.5:1, with the fills pinned by
|
|
693
|
+
name per tone so a new one has to choose its ink in the commit that paints it. A
|
|
694
|
+
tint keeps its exemption — `bg-warn/10 text-warn` and the search highlight's
|
|
695
|
+
`bg-warn/35 text-fg` are read against the page, and the report already owns
|
|
696
|
+
those pairs — and the file says out loud what that exemption does not cover.
|
|
697
|
+
|
|
698
|
+
One number beside it got more honest. Both that sweep and
|
|
699
|
+
`everywhere-the-red-is-spent` said the right ink "falls to 3.95:1 on quotamate's
|
|
700
|
+
dark" under `bg-danger/90` without naming the ground the alpha let through. It
|
|
701
|
+
is **3.94:1** over `--cue-sunken` and **3.97:1** over `--cue-bg`. That the two
|
|
702
|
+
grounds disagree at all is the reason an alpha is refused rather than measured.
|
|
703
|
+
|
|
704
|
+
### And the amber's pair is swept on its own, the way the red's is
|
|
705
|
+
|
|
706
|
+
`destructive-ink.test.ts` covers `warn-fg` on `warn` across all twenty blocks
|
|
707
|
+
for exactly as long as `TwoStepButton` keeps painting that string, because it
|
|
708
|
+
measures the call site. The red does not depend on that: it has a second,
|
|
709
|
+
unconditional sweep of the token pair beside `RevertDock` in
|
|
710
|
+
`elements/canon.test.tsx`. The amber had no equivalent, and the gap is not
|
|
711
|
+
theoretical — with the confirm moved off the amber in the way a later refactor
|
|
712
|
+
legitimately might, and that file's pinned list updated to match, a `warn-fg`
|
|
713
|
+
re-tinted down to **1.62:1** passed every contrast guard in the package.
|
|
714
|
+
|
|
715
|
+
So `instruments/two-step-button.test.tsx` gains the sweep the red has, asking
|
|
716
|
+
nothing about what any component currently paints, plus the spot check that says
|
|
717
|
+
this confirm takes the amber at full strength in the amber's own ink rather than
|
|
718
|
+
the `text-black` it used to.
|
|
719
|
+
|
|
720
|
+
### If you ship your own theme block
|
|
721
|
+
|
|
722
|
+
A hand-written `[data-theme="acme"]` block should declare `--cue-warn-fg`
|
|
723
|
+
alongside `--cue-warn`, the same way it now declares `--cue-danger-fg` alongside
|
|
724
|
+
`--cue-danger`. Without it, `text-warn-fg` resolves to nothing on your theme.
|
|
725
|
+
Themes generated by `createTheme()` and the configurator's exports already carry
|
|
726
|
+
it.
|
|
727
|
+
|
|
728
|
+
- 704f9e5: ## The canon
|
|
729
|
+
|
|
730
|
+
The elements family this library spent four releases adopting is ninety-five
|
|
731
|
+
files deep and it is one product's answer to what a transcript needs. A very
|
|
732
|
+
good one — and one. So before closing the adoption, four teardowns went reading
|
|
733
|
+
the others: Cursor and Trae's local bundles, Claude Code's editor panel, the
|
|
734
|
+
Codex extension, and the open-source field — opencode, Cline, Roo, Continue,
|
|
735
|
+
Void, Zed, OpenHands, Hermes. One question each. **Which rows do three or more
|
|
736
|
+
shipping agent UIs have, that no component catalogue ships at all?**
|
|
737
|
+
|
|
738
|
+
Seventeen answers, and they are in this release. They live in
|
|
739
|
+
`@cueplusplus/ui/elements` beside the vendored family, in the same barrel,
|
|
740
|
+
inside the same island, on the same `data-slot` conventions — but they are
|
|
741
|
+
cue's, hand-written in house style rather than fetched and rewritten, and the
|
|
742
|
+
sync codemod never sees them, so there is nothing here for a re-sync to revert.
|
|
743
|
+
Every file opens with a comment naming the products that justify it and the
|
|
744
|
+
report section that recorded it.
|
|
745
|
+
|
|
746
|
+
```tsx
|
|
747
|
+
import {
|
|
748
|
+
CompactionRow,
|
|
749
|
+
TailStatus,
|
|
750
|
+
TurnFooter,
|
|
751
|
+
WorkCollapse,
|
|
752
|
+
verbFor,
|
|
753
|
+
} from "@cueplusplus/ui/elements";
|
|
754
|
+
|
|
755
|
+
<WorkCollapse
|
|
756
|
+
durationMs={252_000}
|
|
757
|
+
toolCalls={14}
|
|
758
|
+
open={open}
|
|
759
|
+
onOpenChange={setOpen}
|
|
760
|
+
>
|
|
761
|
+
{rows}
|
|
762
|
+
</WorkCollapse>;
|
|
763
|
+
|
|
764
|
+
<CompactionRow
|
|
765
|
+
state="completed"
|
|
766
|
+
tokensBefore={128_000}
|
|
767
|
+
tokensAfter={24_000}
|
|
768
|
+
messagesBefore={214}
|
|
769
|
+
messagesAfter={31}
|
|
770
|
+
/>;
|
|
771
|
+
|
|
772
|
+
<TurnFooter
|
|
773
|
+
onCopy={copy}
|
|
774
|
+
onFork={fork}
|
|
775
|
+
timestamp={{
|
|
776
|
+
label: "2m ago",
|
|
777
|
+
title: "Today at 2:14 PM",
|
|
778
|
+
subtitle: "Worked for 2m 15s",
|
|
779
|
+
}}
|
|
780
|
+
/>;
|
|
781
|
+
|
|
782
|
+
<TailStatus
|
|
783
|
+
action={verbFor("shell", "active")}
|
|
784
|
+
detail="pnpm test"
|
|
785
|
+
onCancel={stop}
|
|
786
|
+
/>;
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### The seventeen, and who else ships them
|
|
790
|
+
|
|
791
|
+
| | | |
|
|
792
|
+
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
793
|
+
| `TailStatus` | the live row at the foot: an action, a detail that truncates on its own, an inline Cancel that exists only when there is something to cancel | Cursor's `AgentTranscriptTailStatus`, opencode's action vocabulary |
|
|
794
|
+
| `WorkCollapse` | `Worked for 4m 12s and made 14 tool calls`, expanding back to the rows it folded | Cline's `WorkActivity`, Hermes's run collapsing |
|
|
795
|
+
| `TimeBoundary` / `UnreadDivider` | the two transcript rules nobody documents — one row each, placed by the caller | Cursor's row union (the only exhaustive one in any shipping product) |
|
|
796
|
+
| `TurnFooter` / `EndOfTurnSummary` | copy / reply / fork with a three-level timestamp, and `3 files changed +48 −12` | Cursor; the diff-stat half also Zed, Roo, Cline |
|
|
797
|
+
| `CompactionRow` | `128k → 24k tokens · 214 → 31 messages`, in all five states | Cline, Zed, Roo, Continue |
|
|
798
|
+
| `QueueDock` | what you typed while it was working — foldable, with edit / send-now / remove on every entry | opencode's `followupDock`, Cursor, Cline, Roo, Zed |
|
|
799
|
+
| `RevertDock` | restore, on the files-versus-conversation axis, behind a second press | Roo's `CheckpointMenu`, Cline, Cursor, opencode |
|
|
800
|
+
| `ContextUsage` | a segmented bar, a `Category / Tokens / Usage` table, and the stat rows that turn a gauge into a bill | Cursor's tray, opencode's context tab, Claude Code's modal |
|
|
801
|
+
| `PermissionScopes` | approval whose "always allow" writes its own label and names the file it writes to | Claude Code's scope cycler, Zed's granularity dropdown, Roo, Cline, Void |
|
|
802
|
+
| `AgentModeBadge` | how much authority the run is holding, as a chip and as `data-agent-mode` | Claude Code's mode-tinted chrome, Cursor's mode tokens |
|
|
803
|
+
| `RiskBadge` | an explicit `unknown` / `low` / `medium` / `high` on the call, with the qualifier beside it | OpenHands' `SecurityRisk`; the qualifier from Cline, Roo, Zed |
|
|
804
|
+
| `VerdictRow` | a scored verification result on a finished turn — rating, score, and what is most likely wrong | OpenHands' critic display, generalised past the critic |
|
|
805
|
+
| `AnimatedNumber` | a token, cost or duration readout that counts to its new value in tabular numerals | Claude Code's live thinking-token counter |
|
|
806
|
+
| `Clamp` | the truncation trio — a height, a fixed-pixel dissolve, a way back | Claude Code's five separate truncations |
|
|
807
|
+
| `LiveRegionAnnouncer` | one polite atomic region and the queue that makes it audible | Claude Code's announcement queue, Codex's announcement strings |
|
|
808
|
+
|
|
809
|
+
Nothing here shipped on taste. The rule for the phase was that a component
|
|
810
|
+
which could not point at a report section did not ship, and the two that are
|
|
811
|
+
gaps rather than crowds say so in their own files: `RiskBadge` exists because
|
|
812
|
+
six of the seven open-source UIs encode risk _implicitly_ — you reconstruct how
|
|
813
|
+
dangerous a call is from which approval bucket its tool happened to land in —
|
|
814
|
+
and one, OpenHands, makes it a property of the action. `VerdictRow` exists
|
|
815
|
+
because every agent runtime now scores its own work somewhere and, in six of
|
|
816
|
+
seven, that judgement reaches the reader as prose or not at all.
|
|
817
|
+
|
|
818
|
+
### Copy is data, not markup
|
|
819
|
+
|
|
820
|
+
The tense triad is the single most repeated pattern in the whole study.
|
|
821
|
+
`Run` / `Running` / `Ran`. Cursor keeps it in `tool-action-labels.js`, Void as
|
|
822
|
+
`{proposed, running, done}`, Continue as `{wouldLikeTo, isCurrently,
|
|
823
|
+
hasAlready}`, Hermes as five category verbs, opencode inside an i18n dictionary
|
|
824
|
+
its own `AGENTS.md` forbids anyone to bypass. Six products, the same three
|
|
825
|
+
strings, private to the app every single time.
|
|
826
|
+
|
|
827
|
+
Here they are tables. `VERB_TRIADS` with `verbFor()` over it — which falls back
|
|
828
|
+
to `Use` / `Using` / `Used` for a kind it does not know, because an MCP server
|
|
829
|
+
names its own tools and a row forced to choose between `undefined` and the raw
|
|
830
|
+
wire name will render the wire name. Beside it: `TAIL_ACTIONS` (opencode's ten
|
|
831
|
+
status phrases), `TURN_ACTION_LABELS`, `COMPACTION_LABELS`, `QUEUE_LABELS`, the
|
|
832
|
+
revert triple, the context set, the authority ladder, the permission pair, the
|
|
833
|
+
two judgements, `CLAMP_LABELS` and the nine `ANNOUNCEMENTS`.
|
|
834
|
+
|
|
835
|
+
```tsx
|
|
836
|
+
import { VERB_TRIADS, verbFor } from "@cueplusplus/ui/elements";
|
|
837
|
+
|
|
838
|
+
// Your product's agent renders rather than uses.
|
|
839
|
+
const VERBS = {
|
|
840
|
+
...VERB_TRIADS,
|
|
841
|
+
render: { proposed: "Render", active: "Rendering", past: "Rendered" },
|
|
842
|
+
};
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
Every component that draws one of these takes the copy as a prop and defaults
|
|
846
|
+
to the table. Localising a console, or writing for a product whose agent
|
|
847
|
+
"edits" where this one "patches", is spreading a table — not forking a
|
|
848
|
+
component to change a word.
|
|
849
|
+
|
|
850
|
+
### The mode ladder is a token contract, not a colour prop
|
|
851
|
+
|
|
852
|
+
The most distinctive systemic idea in the study is not a component. Claude
|
|
853
|
+
Code's panel puts one attribute on the composer, the send button and the
|
|
854
|
+
spinner, and each re-derives its accent from it — six modes painted in four
|
|
855
|
+
colours, because what a mode _means_ is how much authority the run is holding,
|
|
856
|
+
and that ladder has four rungs everywhere. Cursor reaches the same place from
|
|
857
|
+
the token side: a quadruple per mode that the chip, the composer border and the
|
|
858
|
+
transcript accent all read.
|
|
859
|
+
|
|
860
|
+
So `@cueplusplus/ui/elements.css` gains sixteen properties — four per rung,
|
|
861
|
+
declared under `[data-agent-mode]` — and four aliases pointing at whichever
|
|
862
|
+
rung the attribute names:
|
|
863
|
+
|
|
864
|
+
```css
|
|
865
|
+
.composer:focus-within {
|
|
866
|
+
border-color: var(--cue-elements-mode-icon);
|
|
867
|
+
}
|
|
868
|
+
.send-button {
|
|
869
|
+
background: var(--cue-elements-mode-icon);
|
|
870
|
+
color: var(--cue-elements-mode-background);
|
|
871
|
+
}
|
|
872
|
+
.turn-rail {
|
|
873
|
+
background: var(--cue-elements-mode-border);
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
Stamp `data-agent-mode="unsafe"` on the composer and all three follow, with no
|
|
878
|
+
prop threading and no branch in any component. `AgentModeBadge` is the readout
|
|
879
|
+
over the same four properties, not the API.
|
|
880
|
+
|
|
881
|
+
They are declared under the attribute rather than at `:root` on purpose: a
|
|
882
|
+
custom property's `var()` references resolve where they are declared, so a rung
|
|
883
|
+
declared at `:root` would paint a badge inside an `<AgentSurface skin="aui">`
|
|
884
|
+
in the console's palette while everything around it painted in the island's.
|
|
885
|
+
The default rung is the family's neutral, not `manual`, because an unmapped
|
|
886
|
+
mode painted brand-orange would be claiming the run asks before it acts, and
|
|
887
|
+
that is the one claim nobody should make on a guess. Those same four neutrals
|
|
888
|
+
are the badge's inline fallbacks, so a consumer who never imports
|
|
889
|
+
`elements.css` gets a legible chip that makes no claim rather than an invisible
|
|
890
|
+
one — and the unpainted and the unmapped chip look alike deliberately.
|
|
891
|
+
|
|
892
|
+
### The controls that are not allowed to lie
|
|
893
|
+
|
|
894
|
+
Three of the seventeen sit where a control that misstates what it is about to
|
|
895
|
+
do is a defect rather than a polish item.
|
|
896
|
+
|
|
897
|
+
**`PermissionScopes`** is the one control in an agent UI whose label has to be
|
|
898
|
+
computed, because what it grants is different every time it appears:
|
|
899
|
+
`Yes, allow Bash(cue patch:*) for this project (just you)`, with the file that
|
|
900
|
+
grant is saved to in a sentence underneath. Two departures from the reference,
|
|
901
|
+
both in the same direction. Nothing is elided — the reference shortens a rule
|
|
902
|
+
past twenty characters and strips the `:*` off a prefix rule, and both cuts
|
|
903
|
+
land on exactly the part that says how far the grant reaches, so this label
|
|
904
|
+
wraps instead — inside the word where it has to, since a rule and a directory
|
|
905
|
+
are one word to a line breaker and an underscored path gives it nowhere else
|
|
906
|
+
to go, and so does every other string the caller hands it: the subject beside
|
|
907
|
+
the question and the sentence naming the file the grant is written into break
|
|
908
|
+
the same way, because the account of where a permission lands is no more
|
|
909
|
+
allowed off the edge of the card than the account of what it grants. And when
|
|
910
|
+
there are more rules than the sentence can name, every one of them is listed
|
|
911
|
+
underneath, because `and 3 more` is a count and a count is not an account. No scope means no always-allow answer at all: a standing
|
|
912
|
+
grant that cannot say where it is saved is not one this element will offer.
|
|
913
|
+
The scope table ships the four reaches in words and a file path for none of
|
|
914
|
+
them — where a grant lands is the product's answer, and a design system that
|
|
915
|
+
guessed at it would commit the very defect this element exists to prevent.
|
|
916
|
+
|
|
917
|
+
**`RevertDock`** is the family's one destructive control and is built like one.
|
|
918
|
+
The axis first, because a run changes the files and the conversation about them
|
|
919
|
+
and putting one back is not putting the other back. Then the gate: choosing a
|
|
920
|
+
scope only arms the dock, `onRestore` can fire from the second separate press
|
|
921
|
+
alone, and `This action cannot be undone.` is on the screen the whole time the
|
|
922
|
+
dock is open rather than in a props table somebody reads afterwards. Then the
|
|
923
|
+
geometry, which is the half a gate cannot do on its own: arming does not
|
|
924
|
+
replace the scope list — it marks the chosen scope, disables the list where it
|
|
925
|
+
stands and appends the question _underneath_ it, so the destructive control is
|
|
926
|
+
never drawn over the control that armed it and a double click cannot walk
|
|
927
|
+
through both steps. Its confirm also refuses presses for 400 ms after it
|
|
928
|
+
arrives, and fires once per arm. The way out is `TwoStepButton`'s ruling rather
|
|
929
|
+
than a second one: focus lands on `Keep everything` and never on the
|
|
930
|
+
destructive half, that button comes first in the DOM as well as on the screen,
|
|
931
|
+
`Escape` disarms and stops there rather than closing whatever the dock is
|
|
932
|
+
inside, and folding the dock away disarms it — an armed state nobody can see is
|
|
933
|
+
one somebody re-opens into.
|
|
934
|
+
|
|
935
|
+
**`RiskBadge`** treats `unknown` as a first-class value drawn in the neutral,
|
|
936
|
+
never in the low-risk green: a runtime with no scorer must not be made to look
|
|
937
|
+
like one that scored everything safe. And the qualifier renders beside the
|
|
938
|
+
label rather than hiding in a `title`, because `High risk` alone is a
|
|
939
|
+
temperature while `High risk · outside your workspace` is a fact somebody can
|
|
940
|
+
act on.
|
|
941
|
+
|
|
942
|
+
### Three that are contracts more than components
|
|
943
|
+
|
|
944
|
+
**`AnimatedNumber`** counts to its new value instead of jumping to it, in
|
|
945
|
+
tabular numerals — both halves lifted from one line of a shipping bundle, and
|
|
946
|
+
the second is not cosmetic: a proportional `1` is narrower than a `0`, so a
|
|
947
|
+
stepping counter nudges everything to its right on every update, which is what
|
|
948
|
+
makes a busy transcript feel unstable. The value stays a prop and every tween
|
|
949
|
+
lands exactly on it. Mid-tween the digits are `aria-hidden` with the settled
|
|
950
|
+
value carried beside them, so a screen reader hears what the run reported and
|
|
951
|
+
never a frame of the animation. Reduced motion is answered by _arriving_: no
|
|
952
|
+
frame is ever scheduled, because a readout that stopped where it was would be
|
|
953
|
+
wrong rather than merely still.
|
|
954
|
+
|
|
955
|
+
**`Clamp`** is a height, a dissolve of a fixed number of pixels, and a way
|
|
956
|
+
back. The fixed depth is the part worth copying — ten pixels of dissolve is the
|
|
957
|
+
same cut over a two-line row and a forty-line one, which is why the reference's
|
|
958
|
+
five separate truncations read as one idea. The fade is a **mask**, not a
|
|
959
|
+
gradient to the surface colour, which is the bug the reference shipped: its
|
|
960
|
+
diff variant hard-codes `#1e1e1e` and smears on a light theme, while a mask
|
|
961
|
+
fades the content itself and has no colour to get wrong in any theme, either
|
|
962
|
+
mode, or inside an island. The trio is published as
|
|
963
|
+
`--cue-elements-clamp-height` / `-fade`, so a height overridden in a stylesheet
|
|
964
|
+
moves the dissolve with it. The reveal is always in the DOM and always
|
|
965
|
+
focusable — a `Show more` chip that appears on hover only is unreachable for
|
|
966
|
+
anyone not holding a pointer.
|
|
967
|
+
|
|
968
|
+
**`LiveRegionAnnouncer`** is the accessibility surface, and it is a queue
|
|
969
|
+
rather than a live region wired to state, because the naive version announces
|
|
970
|
+
almost nothing: polite announcements replace each other, so a region bound to a
|
|
971
|
+
transcript that changes four times a second speaks once and swallows the rest.
|
|
972
|
+
Four rules make it audible — 400 ms of spacing, five deep with the _oldest_
|
|
973
|
+
dropped (behind a busy run the newest is the true one), held while the page is
|
|
974
|
+
hidden or a modal has the reader, and an identical sentence re-announced by
|
|
975
|
+
appending a space, which changes the string without changing a word of it. The
|
|
976
|
+
hold is in force from the first render rather than the first effect, so an
|
|
977
|
+
announcer that mounts into a background tab holding a backlog — a restored
|
|
978
|
+
session, which is the whole reason `announceOnMount` exists — does not spend a
|
|
979
|
+
line of it finding out where it is. The
|
|
980
|
+
region mounts empty and stays mounted, since a live region added at the same
|
|
981
|
+
moment as its text is one most assistive technologies never announce. One
|
|
982
|
+
decision moves to the caller: `paused` is a prop rather than an internal
|
|
983
|
+
ref-count of open overlays, because this library has its own portal contract
|
|
984
|
+
and an announcer holding a second opinion about what is on screen would be
|
|
985
|
+
wrong exactly when it mattered.
|
|
986
|
+
|
|
987
|
+
### What did not change
|
|
988
|
+
|
|
989
|
+
No new subpath, no new peer, no new group. The seventeen are exports of
|
|
990
|
+
`@cueplusplus/ui/elements`, drawn with the icon set that subpath already
|
|
991
|
+
declares, and the root barrel gained nothing. The ninety-five vendored files
|
|
992
|
+
are untouched and the sync pin, the codemod and `elements-sync.lock.json` are
|
|
993
|
+
exactly where P0 left them — a re-sync at a newer upstream SHA rewrites the
|
|
994
|
+
vendored tree and cannot touch a line of this. The tokens are untouched:
|
|
995
|
+
everything here paints from the contract the flagship release already shipped,
|
|
996
|
+
and `elements.css` gained one section rather than a new dependency for anyone
|
|
997
|
+
in cue mode.
|
|
998
|
+
|
|
999
|
+
***
|
|
1000
|
+
|
|
1001
|
+
**This closes the adoption.** `@cueplusplus/ui/elements` is now the ninety-five
|
|
1002
|
+
vendored components, the island that can paint them in either value-set, the
|
|
1003
|
+
sanitized markdown surface and generative vocabulary, the replay transport with
|
|
1004
|
+
its fixture catalogue and eight gallery studies — and the seventeen rows the
|
|
1005
|
+
field study found that nobody was shipping.
|
|
1006
|
+
|
|
1007
|
+
- 06eb4ac: ## The flagship answers in colour
|
|
1008
|
+
|
|
1009
|
+
`cue` is a monochrome brand, and until this release it carried the monochrome
|
|
1010
|
+
all the way through to the one vocabulary that exists to be told apart at a
|
|
1011
|
+
glance. On the default theme `--cue-ok`, `--cue-busy`, `--cue-warn` and
|
|
1012
|
+
`--cue-info` were all `#ffffff` on dark and all `#0a0a0a` on light: four
|
|
1013
|
+
different meanings, one colour, distinguishable only by the glyph or the word
|
|
1014
|
+
beside them. It read as restraint on a settings page. It reads as a defect on a
|
|
1015
|
+
transcript where three tool calls are in three different states and the only
|
|
1016
|
+
question is which of them is still moving.
|
|
1017
|
+
|
|
1018
|
+
They are chromatic now.
|
|
1019
|
+
|
|
1020
|
+
| token | before (dark / light) | after (dark / light) |
|
|
1021
|
+
| -------------- | --------------------- | ----------------------------------------------- |
|
|
1022
|
+
| `--cue-ok` | `#ffffff` / `#0a0a0a` | `oklch(0.72 0.17 149)` / `oklch(0.53 0.14 149)` |
|
|
1023
|
+
| `--cue-busy` | `#ffffff` / `#0a0a0a` | `oklch(0.78 0.13 220)` / `oklch(0.54 0.1 225)` |
|
|
1024
|
+
| `--cue-warn` | `#ffffff` / `#0a0a0a` | `oklch(0.8 0.17 75)` / `oklch(0.55 0.115 70)` |
|
|
1025
|
+
| `--cue-info` | `#ffffff` / `#0a0a0a` | `oklch(0.75 0.12 252)` / `oklch(0.55 0.16 258)` |
|
|
1026
|
+
| `--cue-danger` | `#ff5f57` / `#d03030` | unchanged |
|
|
1027
|
+
|
|
1028
|
+
`ok` and `warn` are assistant-ui's own semantic pair; `busy` is a console cyan
|
|
1029
|
+
for in-flight and `info` a calmer blue for news. `danger` does not move — it was
|
|
1030
|
+
the one tone already chromatic, and it is the one nobody should have to
|
|
1031
|
+
re-learn.
|
|
1032
|
+
|
|
1033
|
+
The light row is darker than these hues usually run, and that is the point: a
|
|
1034
|
+
tone is not only a dot here. `Chip`, `StatusDot`, `Toast` and the rest of the
|
|
1035
|
+
tone family draw `text-<tone>` at 11px uppercase — text, judged at 4.5:1, not at
|
|
1036
|
+
the 3:1 a signal is held to. Every light value above clears 4.5:1 on `--cue-bg`
|
|
1037
|
+
(4.79 to 4.85, beside `danger`'s 4.94) and every one of them is inside sRGB, so
|
|
1038
|
+
the measured ratio is the painted one. Dark was never in question: it runs 8.5
|
|
1039
|
+
to 10.3:1.
|
|
1040
|
+
|
|
1041
|
+
The chrome is untouched. Ink, ground, surfaces, borders, `accent`, `focus` and
|
|
1042
|
+
`selection` are the same monochrome they have always been, on both modes. This
|
|
1043
|
+
change is the status vocabulary and the live hue below it, and nothing else.
|
|
1044
|
+
|
|
1045
|
+
The other nine presets do not move at all: `terminal`, `signal`, `venu`,
|
|
1046
|
+
`hivehub`, `dusk`, `luma`, `snuffle`, `quotamate` and `requestport` were already
|
|
1047
|
+
chromatic here, and an app on any of them sees nothing new in this section.
|
|
1048
|
+
|
|
1049
|
+
### Migrating
|
|
1050
|
+
|
|
1051
|
+
**A console on the default theme that relied on monochrome statuses will now
|
|
1052
|
+
see colour.** No API moved and nothing needs editing to keep working, but
|
|
1053
|
+
pixels change, and they change in more places than a status pill: `Chip`,
|
|
1054
|
+
`StatusDot`, `Meter`, `Toast`, `Stat`, `StatusBar`, `LogViewer`, `Sparkline`,
|
|
1055
|
+
`GroupBar`, `TwoStepButton`, `Tree`, `TerminalFrame`, `AppWindowFrame`,
|
|
1056
|
+
`UsageChart`, `FileUpload`, `EnvVarInput`, `AskBox`, `DelegationCard` and
|
|
1057
|
+
`ToolCallCard` all spend these four tokens, along with the MIDI and DMX
|
|
1058
|
+
instruments. Anywhere a `tone` prop was chosen and then quietly rendered white,
|
|
1059
|
+
it now renders the tone. Screenshot tests on `data-theme="cue"` will need new
|
|
1060
|
+
baselines; a design that used `tone="info"` as a way of saying "plain" will now
|
|
1061
|
+
say "blue".
|
|
1062
|
+
|
|
1063
|
+
If a product deliberately wants the monochrome back, it is four declarations —
|
|
1064
|
+
the soft fill and the rim derive from the base with `color-mix`, so overriding
|
|
1065
|
+
the base moves the whole family with it:
|
|
1066
|
+
|
|
1067
|
+
```css
|
|
1068
|
+
@import "tailwindcss";
|
|
1069
|
+
@import "@cueplusplus/ui/styles.css";
|
|
1070
|
+
|
|
1071
|
+
[data-theme="cue"] {
|
|
1072
|
+
--cue-ok: #ffffff;
|
|
1073
|
+
--cue-busy: #ffffff;
|
|
1074
|
+
--cue-warn: #ffffff;
|
|
1075
|
+
--cue-info: #ffffff;
|
|
1076
|
+
}
|
|
1077
|
+
[data-theme="cue"][data-mode="light"] {
|
|
1078
|
+
--cue-ok: #0a0a0a;
|
|
1079
|
+
--cue-busy: #0a0a0a;
|
|
1080
|
+
--cue-warn: #0a0a0a;
|
|
1081
|
+
--cue-info: #0a0a0a;
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
Two selectors, not one, and the block goes _after_ the import — the preset's own
|
|
1086
|
+
rules are `[data-theme="cue"]` and `[data-theme="cue"][data-mode="light"]`, so
|
|
1087
|
+
an override has to match their specificity and win on source order. An app that
|
|
1088
|
+
runs `mode="system"` mirrors these onto
|
|
1089
|
+
`[data-theme="cue"][data-mode="system"]` inside the two `prefers-color-scheme`
|
|
1090
|
+
queries, exactly as the generated sheet does.
|
|
1091
|
+
|
|
1092
|
+
## `--cue-stream`, the live hue
|
|
1093
|
+
|
|
1094
|
+
The colour contract gains one token and its two derivations —
|
|
1095
|
+
`--cue-stream`, `--cue-stream-soft` and `--cue-stream-border` — on all ten
|
|
1096
|
+
presets, in both modes, at the same 12% and 35% mixes the statuses use. The
|
|
1097
|
+
colour contract is 42 custom properties where it was 39.
|
|
1098
|
+
|
|
1099
|
+
It is the colour of something still arriving: a token mid-stream, a tool still
|
|
1100
|
+
running, a cursor that has not stopped. It is deliberately **not** a sixth
|
|
1101
|
+
status. A status is something a `Chip` or a `StatusDot` can be _told_ to be, and
|
|
1102
|
+
"streaming" is not a tone a caller picks — it is a property of a surface while
|
|
1103
|
+
the model is talking. There is no `tone="stream"` and there is not going to be
|
|
1104
|
+
one. What it shares with the statuses is the shape of its family, which is why
|
|
1105
|
+
it is derived beside them and bridged beside them:
|
|
1106
|
+
|
|
1107
|
+
```
|
|
1108
|
+
--color-stream → text-stream bg-stream/15 border-stream/30
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
The bridge carries the tone at full strength and lets Tailwind's alpha modifier
|
|
1112
|
+
do the washes, the way `Chip` already writes `bg-busy/10`.
|
|
1113
|
+
|
|
1114
|
+
The flagship pair is assistant-ui's `--aui-live` — `blue-400` on ink verbatim,
|
|
1115
|
+
and on paper `blue-500`'s hue taken down to `oklch(0.55 0.21 259.815)`, because
|
|
1116
|
+
the elements draw the live hue as text and `blue-500` itself lands at 3.6:1
|
|
1117
|
+
there. Every other preset answers in its own blue, except `terminal`, which
|
|
1118
|
+
already owned a live hue and keeps it.
|
|
1119
|
+
|
|
1120
|
+
Three things that hold a second copy of the contract moved with it, so nothing
|
|
1121
|
+
needs to be told about the new token twice: `createTheme()` resolves and derives
|
|
1122
|
+
`stream` like any other tone and darkens an invented light one to the non-text
|
|
1123
|
+
AA floor; the contrast report checks `stream/bg` against that same floor — every
|
|
1124
|
+
preset clears it with room to spare, the tightest being `terminal`'s light block
|
|
1125
|
+
at 4.89:1; and the configurator edits and exports it beside the statuses rather
|
|
1126
|
+
than in the miscellany. A theme somebody generated before this release and pasted into their
|
|
1127
|
+
own CSS keeps working: `stream` falls back through the same resolution order
|
|
1128
|
+
every other invented token does.
|
|
1129
|
+
|
|
1130
|
+
## `@cueplusplus/ui/elements`
|
|
1131
|
+
|
|
1132
|
+
A new group, and the largest one this library has added at once: the agent's own
|
|
1133
|
+
surface. Fifteen elements, plus the island and the two label primitives the
|
|
1134
|
+
family is built out of, at `/docs/components/elements`.
|
|
1135
|
+
|
|
1136
|
+
| element | what it is |
|
|
1137
|
+
| ----------------- | ----------------------------------------------------------- |
|
|
1138
|
+
| `ToolCall` | one invocation, request and result behind a disclosure |
|
|
1139
|
+
| `ToolGroup` | calls that went out together, collapsed to one row |
|
|
1140
|
+
| `ToolError` | one call failed, with the error legible |
|
|
1141
|
+
| `TerminalBlock` | output streaming line by line, ending in an exit status |
|
|
1142
|
+
| `CodeDiff` | a unified diff, tinted, sized for a chat column |
|
|
1143
|
+
| `ReviewableDiff` | the same diff where each hunk is a decision |
|
|
1144
|
+
| `FileTree` | everything a run touched, with the churn per file |
|
|
1145
|
+
| `ApprovalCard` | the agent asking before it does something with side effects |
|
|
1146
|
+
| `PermissionGrant` | granting a capability rather than approving an action |
|
|
1147
|
+
| `AgentPlan` | a checklist the agent works through |
|
|
1148
|
+
| `TodoList` | the agent's working list, rewritten mid-run |
|
|
1149
|
+
| `AgentStatus` | one pill: what it is doing, and for how long |
|
|
1150
|
+
| `SubagentList` | parallel workers, their models and their progress |
|
|
1151
|
+
| `TraceWaterfall` | every span in a run on one nested time axis |
|
|
1152
|
+
| `ChatPanel` | the whole family working together, with its five parts |
|
|
1153
|
+
|
|
1154
|
+
Beside them: `ShimmerLabel` and `SwapLabel`, the eighteen surface recipes the
|
|
1155
|
+
family shares as plain class strings (`paper`, `floating`, `field`, `mono`,
|
|
1156
|
+
`live`, `codeSurface`, `collapsePanel`, `pressable`, the swap pairs and the
|
|
1157
|
+
rest), and `range`'s six functions — `at`, `clamp`, `indexIn`, `pct`,
|
|
1158
|
+
`progressOf`, `take` — which are the arithmetic a replayed transcript is made
|
|
1159
|
+
of.
|
|
1160
|
+
|
|
1161
|
+
Two conventions run through the group and both are worth knowing before you read
|
|
1162
|
+
the source:
|
|
1163
|
+
|
|
1164
|
+
**State is a prop, and time is the caller's.** Nothing here holds a run.
|
|
1165
|
+
`ToolCall` is _told_ `running` and `open`; `TerminalBlock` is told how many of
|
|
1166
|
+
its lines are visible. Replaying a conversation is arithmetic on props, which is
|
|
1167
|
+
what makes every one of these benchable at every state.
|
|
1168
|
+
|
|
1169
|
+
**These components are written upstream's way, not this library's.** Plain
|
|
1170
|
+
exported functions with no `forwardRef`, a `data-slot` attribute on every root,
|
|
1171
|
+
`className` merged last through `cn`. That is deliberate rather than sloppy: it
|
|
1172
|
+
keeps a re-synced diff readable, and `data-slot` is what lets an app restyle a
|
|
1173
|
+
part from the outside without a prop for it. `AgentSurface` — cue's own — is the
|
|
1174
|
+
one component in the group written in the house style.
|
|
1175
|
+
|
|
1176
|
+
`lucide-react` is an **optional peer** of this entry, which is why the group is
|
|
1177
|
+
its own subpath instead of part of the root barrel: the family draws its own
|
|
1178
|
+
icons rather than taking them from a prop. Import nothing from `/elements` and
|
|
1179
|
+
it costs nothing — no install, no bundle, no resolution error.
|
|
1180
|
+
|
|
1181
|
+
`@cueplusplus/ui/elements.css` ships beside it and is **optional even for this
|
|
1182
|
+
subpath**, because the family is painted by the token layer already:
|
|
1183
|
+
|
|
1184
|
+
```css
|
|
1185
|
+
@import "tailwindcss";
|
|
1186
|
+
@import "@cueplusplus/ui/styles.css";
|
|
1187
|
+
@import "@cueplusplus/ui/elements.css"; /* motion, and the supersede island */
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
It carries the two vocabularies upstream never shipped as anything but
|
|
1191
|
+
dependencies — the shimmer sweep and the nine entrance utilities, both stilled
|
|
1192
|
+
under `prefers-reduced-motion` — and the island values below. Skip it and the
|
|
1193
|
+
elements still render in cue's palette; you lose the motion and `skin="aui"`
|
|
1194
|
+
becomes a no-op.
|
|
1195
|
+
|
|
1196
|
+
**Every class it declares is namespaced `cue-elements-`**, and that is a
|
|
1197
|
+
compatibility promise rather than a house style. The entrance utilities are
|
|
1198
|
+
`tw-animate-css`'s by name — `animate-in`, `fade-in`, `slide-in-from-*` — and
|
|
1199
|
+
`tw-animate-css` is what shadcn installs by default, so a stylesheet declaring
|
|
1200
|
+
them again would give such an app two `.animate-in` rules with import order
|
|
1201
|
+
picking the winner, silently. The vendoring codemod renames them at emit, the
|
|
1202
|
+
lockfile records the table as `classRenames`, and a test fails on any class in
|
|
1203
|
+
this stylesheet that a third-party utility package could also own.
|
|
1204
|
+
|
|
1205
|
+
## The island: one DOM, two value-sets
|
|
1206
|
+
|
|
1207
|
+
```tsx
|
|
1208
|
+
import { AgentSurface, ToolCall } from "@cueplusplus/ui/elements";
|
|
1209
|
+
|
|
1210
|
+
<AgentSurface skin="aui" fidelity="upstream">
|
|
1211
|
+
<ToolCall
|
|
1212
|
+
label="Searched"
|
|
1213
|
+
activeLabel="Searching"
|
|
1214
|
+
query="composer"
|
|
1215
|
+
request={request}
|
|
1216
|
+
result={result}
|
|
1217
|
+
running
|
|
1218
|
+
open={open}
|
|
1219
|
+
onOpenChange={setOpen}
|
|
1220
|
+
/>
|
|
1221
|
+
</AgentSurface>;
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
`AgentSurface` renders a plain `div` and stamps `data-cue-skin` and
|
|
1225
|
+
`data-cue-fidelity` on it. `elements.css` re-declares, under those attributes,
|
|
1226
|
+
the `--cue-*` tokens the family reads, and custom properties inherit — so the
|
|
1227
|
+
whole subtree repaints with no prop threading and no branch inside any
|
|
1228
|
+
component.
|
|
1229
|
+
|
|
1230
|
+
The two axes are independent, which is the switchable fidelity the spec asked
|
|
1231
|
+
for:
|
|
1232
|
+
|
|
1233
|
+
- **`skin`** — `cue` (the default) or `aui`, which installs assistant-ui's four
|
|
1234
|
+
shadcn variables and four literal hues as cue tokens, in light and dark.
|
|
1235
|
+
- **`fidelity`** — `cue-metrics` (the default) keeps cue's type scale and
|
|
1236
|
+
declares exactly one metric of its own:
|
|
1237
|
+
`--spacing: calc(0.25rem * var(--cue-density, 1))`. That is the lever that
|
|
1238
|
+
carries the density ladder into five hundred-odd vendored geometry utilities,
|
|
1239
|
+
none of which names a `--cue-space-*` token. `upstream` pins assistant-ui's
|
|
1240
|
+
radii, their 13.5/12/11px type and a flat `--spacing: 0.25rem` instead, so
|
|
1241
|
+
**density stops at the island edge** and the subtree keeps upstream's
|
|
1242
|
+
proportions whatever the console around it is set to.
|
|
1243
|
+
|
|
1244
|
+
It is not a theme. `data-theme`, `data-mode` and `data-density` are untouched,
|
|
1245
|
+
and a light app gets a light island.
|
|
1246
|
+
|
|
1247
|
+
Overlays follow. The portal stamp gains both axes, so a popover or dialog opened
|
|
1248
|
+
from inside an island — which mounts on `<body>`, far from the attributes —
|
|
1249
|
+
paints like the transcript it came from rather than like the page. Outside an
|
|
1250
|
+
island the two attributes are _absent_ rather than defaulted, so nothing that
|
|
1251
|
+
does not use this pays for it.
|
|
1252
|
+
|
|
1253
|
+
## Where this code came from
|
|
1254
|
+
|
|
1255
|
+
Elements are not a package. `@assistant-ui/ui` is private at `0.0.0` and never
|
|
1256
|
+
published; the only channel is a shadcn registry whose index carries no version,
|
|
1257
|
+
no date and no hash, rebuilt from upstream `main` on every deploy. Copy-in is
|
|
1258
|
+
the intended use — so this release also ships the only thing that can answer
|
|
1259
|
+
"which Elements do we have".
|
|
1260
|
+
|
|
1261
|
+
`scripts/elements-sync/` is pinned to upstream commit
|
|
1262
|
+
`31a049fcfa846a76da7c8e2c0bcd62960825dbd4` (resolved 2026-08-22). It fetches
|
|
1263
|
+
all 96 `elements-*` registry items, verifies every allowlisted file against
|
|
1264
|
+
`raw.githubusercontent.com` at that pin, records every byte it read in
|
|
1265
|
+
`elements-sync.lock.json`, and runs a committed codemod that rewrites the fetched
|
|
1266
|
+
TSX before it lands: the ink ramp onto cue's four stops, the four literal
|
|
1267
|
+
Tailwind hues onto `stream`/`ok`/`danger`/`warn` with their alphas intact,
|
|
1268
|
+
literal pixel sizes onto the type scale, radii onto the three rungs, and every
|
|
1269
|
+
`dark:` variant resolved away because cue's mode axis is `[data-mode]` rather
|
|
1270
|
+
than `prefers-color-scheme`. Geometry is deliberately left alone: it all compiles
|
|
1271
|
+
through `--spacing`, which is the island's lever. Upstream's own 679 documented
|
|
1272
|
+
prop rows are merged into JSDoc on the way out, so the props table on each page
|
|
1273
|
+
is upstream's description of upstream's prop.
|
|
1274
|
+
|
|
1275
|
+
What that buys a consumer: the elements arrive as source this library owns and
|
|
1276
|
+
can fix, not as a dependency that can move underneath you; the "version" you have
|
|
1277
|
+
is a SHA you can read in `NOTICE.md` and diff against; and the licence obligation
|
|
1278
|
+
travels with the package. Upstream's MIT notice — Copyright (c) 2025 AgentbaseAI
|
|
1279
|
+
Inc. — is preserved verbatim in `packages/ui/src/elements/NOTICE.md` and named in
|
|
1280
|
+
the group's own documentation.
|
|
1281
|
+
|
|
1282
|
+
Two consequences worth stating plainly. **The directory is generated.** Every
|
|
1283
|
+
file in `src/elements/` except `surfaces.tsx` and `agent-surface.tsx` is output;
|
|
1284
|
+
a bug there is fixed by changing a codemod rule and re-running the sync, never by
|
|
1285
|
+
editing the file, because the next re-sync deletes a hand edit without telling
|
|
1286
|
+
anyone. And **six upstream export names are renamed at emit**, because docgen
|
|
1287
|
+
slugs are globally unique and the `Chat` family stays authoritative:
|
|
1288
|
+
`Composer → ElementsComposer`, `CommandPalette → ElementsCommandPalette`,
|
|
1289
|
+
`DataTable → ElementsDataTable`, `EmptyState → ChatEmptyState`,
|
|
1290
|
+
`Timeline → ElementsTimeline`, and `Source → CitationSource` (that one collides
|
|
1291
|
+
inside upstream's own family). None of the six is in this release — they land
|
|
1292
|
+
with the rest of the catalogue — but the table is generated by the sync, ships in
|
|
1293
|
+
the group barrel, and is the answer to "why is the import name not the one on
|
|
1294
|
+
assistant-ui's site".
|
|
1295
|
+
|
|
1296
|
+
## What did not change
|
|
1297
|
+
|
|
1298
|
+
Outside the `cue` preset's four status values and its light `stream`, no colour
|
|
1299
|
+
moved. No component's
|
|
1300
|
+
props, structure or rendering changed, with two additions that are worth naming
|
|
1301
|
+
because they are additions rather than nothing: the portal stamp — both
|
|
1302
|
+
`useCuePortalProps` and `CuePortalFrame` — now carries `data-cue-skin` and
|
|
1303
|
+
`data-cue-fidelity` alongside the four axes it already carried, and both are
|
|
1304
|
+
_absent_ outside an island rather than defaulted; and the configurator's
|
|
1305
|
+
`TokenEditor` lists `stream` in its Status group, so a generated theme edits it
|
|
1306
|
+
where you would look for it.
|
|
1307
|
+
|
|
1308
|
+
The root barrel is untouched, so an app that never imports `/elements` gets no
|
|
1309
|
+
new peer, no new stylesheet and no new bytes, and `tw-shimmer` and
|
|
1310
|
+
`tw-animate-css` were reimplemented as CSS in this package rather than taken as
|
|
1311
|
+
dependencies. The `Chat` group is unchanged and stays authoritative on every
|
|
1312
|
+
name the two families both wanted.
|
|
1313
|
+
|
|
1314
|
+
Text is still text. Nothing in this group parses model output as markdown or
|
|
1315
|
+
HTML; the elements render what they are given, for the same reason `Message`
|
|
1316
|
+
does. Sanitized markdown is a documented, opt-in surface arriving later, with its
|
|
1317
|
+
own peer and its own entry.
|
|
1318
|
+
|
|
1319
|
+
`schemaVersion` is still `1`.
|
|
1320
|
+
|
|
1321
|
+
- 4e510e7: ## The red carries its own ink
|
|
1322
|
+
|
|
1323
|
+
`RevertDock`'s confirm is the one solid destructive fill in the elements family
|
|
1324
|
+
and the one control in it that does something irreversible. It painted
|
|
1325
|
+
`bg-danger/90 text-accent-fg`, and both halves of that were wrong.
|
|
1326
|
+
|
|
1327
|
+
`--cue-accent-fg` is the ink authored against `--cue-accent`. It has no
|
|
1328
|
+
relationship to `--cue-danger`, and on the ten reds this system ships it lands
|
|
1329
|
+
anywhere between **3.58:1** (venu, dark) and **6.67:1** (requestport, light).
|
|
1330
|
+
The `/90` then let the paper through and took roughly another 0.65 off the
|
|
1331
|
+
ratio. In cue's own light theme the label on that button measured **4.29:1** at
|
|
1332
|
+
the 12px this row is set in — under AA, on the press that cannot be undone.
|
|
1333
|
+
Inside an `<AgentSurface skin="aui">` in light mode it was **3.82:1**.
|
|
1334
|
+
|
|
1335
|
+
There is no existing token that fixes it. Every candidate the palette already
|
|
1336
|
+
owns fails somewhere: `accent-fg` fails terminal and venu in dark, `bg` fails
|
|
1337
|
+
terminal and hivehub in dark, the theme's own `fg` fails every light block. The
|
|
1338
|
+
ink a red can carry is a property of _that red_, not of the mode — so the red
|
|
1339
|
+
now has one.
|
|
1340
|
+
|
|
1341
|
+
### `--cue-danger-fg`
|
|
1342
|
+
|
|
1343
|
+
A new authored anchor, per preset, per mode, in the colour contract beside the
|
|
1344
|
+
tone it is chosen against:
|
|
1345
|
+
|
|
1346
|
+
```css
|
|
1347
|
+
[data-theme="cue"] {
|
|
1348
|
+
--cue-danger: #ff5f57;
|
|
1349
|
+
--cue-danger-fg: #000000;
|
|
1350
|
+
}
|
|
1351
|
+
[data-theme="cue"][data-mode="light"] {
|
|
1352
|
+
--cue-danger: #d03030;
|
|
1353
|
+
--cue-danger-fg: #fcfcfc;
|
|
1354
|
+
}
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
with `text-danger-fg` / `bg-danger-fg` bridged into Tailwind like every other
|
|
1358
|
+
colour token. `ok`, `busy`, `info` and `stream` have no such ink, and that is
|
|
1359
|
+
deliberate: they are read as signals — a dot, a rim, a 12% wash — and the text
|
|
1360
|
+
near them sits on a surface. The red is a tone this system fills a control with,
|
|
1361
|
+
which is what makes text on it text. (The amber turns out to be the other one —
|
|
1362
|
+
_The amber carries its own ink too_, later in this same release.)
|
|
1363
|
+
|
|
1364
|
+
Each preset's value is the extreme it already owns, measured: every light block
|
|
1365
|
+
takes its paper, seven dark blocks take their deepest ground, and the two whose
|
|
1366
|
+
red sits in the middle of the range (terminal, hivehub) take white, because
|
|
1367
|
+
nothing else reaches AA on it. The worst pair in the system is now **4.59:1**;
|
|
1368
|
+
it was **3.58:1**.
|
|
1369
|
+
|
|
1370
|
+
`createTheme()` derives the same token for a generated theme the way it derives
|
|
1371
|
+
`accent-fg` — best of the theme's ground, its deepest well and its ink, with
|
|
1372
|
+
pure black or white only when none of the three can be read — and
|
|
1373
|
+
`CONTRAST_REQUIREMENTS` gained a tenth pair, `danger-fg/danger` at 4.5:1, so a
|
|
1374
|
+
generated palette is judged on it too. The configurator shows it next to the red
|
|
1375
|
+
in the Status group.
|
|
1376
|
+
|
|
1377
|
+
A sweep in `@cueplusplus/ui`'s own suite now measures the shipped pairing across
|
|
1378
|
+
all ten presets in both modes and fails under 4.5:1, and a second one measures
|
|
1379
|
+
it inside the assistant-ui island, where the two halves legitimately disagree
|
|
1380
|
+
about which ink their red wants.
|
|
1381
|
+
|
|
1382
|
+
### If you ship your own theme block
|
|
1383
|
+
|
|
1384
|
+
A hand-written `[data-theme="acme"]` block should declare `--cue-danger-fg`
|
|
1385
|
+
alongside `--cue-danger`. Without it, `text-danger-fg` resolves to nothing on
|
|
1386
|
+
your theme. Themes generated by `createTheme()` and the configurator's exports
|
|
1387
|
+
already carry it.
|
|
1388
|
+
|
|
1389
|
+
- b9ab713: ## The rest of the family
|
|
1390
|
+
|
|
1391
|
+
P0 shipped fifteen elements and the machinery that makes more of them. This
|
|
1392
|
+
release finishes the catalogue. **All 96 of assistant-ui's `elements-*` registry
|
|
1393
|
+
items now emit** — 94 elements, the eighteen shared surface recipes, and
|
|
1394
|
+
`range`'s six functions — through the same pinned sync, the same codemod and the
|
|
1395
|
+
same island. `@cueplusplus/ui/elements` goes from 23 exported components to
|
|
1396
|
+
**132**, plus two hooks, and is now the largest group in the library.
|
|
1397
|
+
|
|
1398
|
+
None of it was typed. The 79 new elements arrived the way the first fifteen did:
|
|
1399
|
+
`scripts/elements-sync/` fetches the registry at upstream
|
|
1400
|
+
`31a049fcfa846a76da7c8e2c0bcd62960825dbd4`, verifies every file against
|
|
1401
|
+
`raw.githubusercontent.com` at that pin, hashes what it read into
|
|
1402
|
+
`elements-sync.lock.json`, and runs a committed codemod that rewrites the fetched
|
|
1403
|
+
TSX before it lands. Adding an element is a line in an allowlist plus whatever
|
|
1404
|
+
rule its source needed; the emitted files are never edited by hand, because the
|
|
1405
|
+
next re-sync would delete the edit without telling anyone.
|
|
1406
|
+
|
|
1407
|
+
What that means for a consumer is the sentence the whole phase exists for: **the
|
|
1408
|
+
entire assistant-ui Elements catalogue is now cue-owned source.** Not a
|
|
1409
|
+
dependency that can move underneath you — there is no package to depend on;
|
|
1410
|
+
`@assistant-ui/ui` is private at `0.0.0` and the registry carries no version
|
|
1411
|
+
field. Every one of the 94 is painted by `--cue-*` tokens and answers to
|
|
1412
|
+
`data-theme`, `data-mode` and `data-density` like everything else here, and every
|
|
1413
|
+
one of them re-skins to assistant-ui's own values inside
|
|
1414
|
+
`<AgentSurface skin="aui">` without a prop being threaded anywhere.
|
|
1415
|
+
|
|
1416
|
+
| what landed | elements | what it covers |
|
|
1417
|
+
| ----------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1418
|
+
| thread & reasoning | 20 | reasoning revealed a beat at a time, text arriving a token at a time, and everything that hangs off one turn — actions, attachments, branches, timing, the queue, editing, quoting, regenerating, reading aloud |
|
|
1419
|
+
| knowledge & structured output | 21 | where an answer came from and how far to trust it — sources, inline citations, retrieved passages, the document it quotes, a confidence marker — and what it arrives as when prose is not enough: tables, charts, diagrams, spec sheets, maps, maths, canvases |
|
|
1420
|
+
| agents & observability | 18 | who is doing the work and what it costs — agent cards, handoffs, background runs, checkpoints, job progress, spend, context, quota — plus a session's failure modes and the four drawings it is legible in afterwards |
|
|
1421
|
+
| composer, thread list & shell | 20 | the application around the conversation — the composer and its twenty-one facets, the palette, the model picker, the prompt library, the thread list and its searches, settings, onboarding, the blank screen, the launcher bubble, the call |
|
|
1422
|
+
|
|
1423
|
+
Six upstream names are renamed at emit, because docgen slugs are globally unique
|
|
1424
|
+
and the `Chat` family stays authoritative: `Composer → ElementsComposer`,
|
|
1425
|
+
`CommandPalette → ElementsCommandPalette`, `DataTable → ElementsDataTable`,
|
|
1426
|
+
`EmptyState → ChatEmptyState`, `Timeline → ElementsTimeline`, and
|
|
1427
|
+
`Source → CitationSource`, which collides inside upstream's own family. The table
|
|
1428
|
+
is generated by the sync, ships in the group barrel's JSDoc, and is the answer to
|
|
1429
|
+
"why is the import name not the one on assistant-ui's site".
|
|
1430
|
+
|
|
1431
|
+
`composer` is the family's one compound: twenty-one exports out of a single file,
|
|
1432
|
+
all of them published, because upstream's own examples compose the facets rather
|
|
1433
|
+
than configuring a root — and a composer exported without `useSlashMatches` and
|
|
1434
|
+
`useMentionMatches` is a composer whose two menus nobody can open.
|
|
1435
|
+
|
|
1436
|
+
## Four new kitchen-sink sections
|
|
1437
|
+
|
|
1438
|
+
The corpus is split the way the batches were, not lumped into one page:
|
|
1439
|
+
`elements-thread`, `elements-knowledge`, `elements-agents` and
|
|
1440
|
+
`elements-composer` join `elements` on `/kitchen-sink`, each with its own
|
|
1441
|
+
heading and its own axe case. One section holding a hundred and thirty benches
|
|
1442
|
+
is one unreadable file and one slow accessibility run; five sections are five of
|
|
1443
|
+
each. Every element is benched at its real states — a streaming element shows
|
|
1444
|
+
streaming _and_ settled, an error element shows the error — which is what makes
|
|
1445
|
+
the props table on each component page evidence rather than a claim.
|
|
1446
|
+
|
|
1447
|
+
## `heat-graph`, a second optional peer
|
|
1448
|
+
|
|
1449
|
+
`ActivityGraph` draws a calendar of runs, and it draws it with `heat-graph` —
|
|
1450
|
+
the headless grid assistant-ui publishes beside the elements (MIT, same author).
|
|
1451
|
+
It ships as an **optional peer** at `^0.0.15`, the shape `recharts` and
|
|
1452
|
+
`@xyflow/react` already have: declared in `peerDependencies`, marked optional,
|
|
1453
|
+
externalised by the bundler, named in this entry's expected-peer list, and
|
|
1454
|
+
installed here only as a devDependency. One component of 132 reaches it, so an
|
|
1455
|
+
app that never renders a heat calendar installs nothing for it and sees no
|
|
1456
|
+
resolution error.
|
|
1457
|
+
|
|
1458
|
+
**This is a deliberate departure from the spec.** §B of
|
|
1459
|
+
`docs/specs/2026-08-22-agent-elements.md` says `heat-graph` is "vendored as a
|
|
1460
|
+
helper, not a peer". That line was written before the pipeline existed. Vendoring
|
|
1461
|
+
it now would mean hand-writing eight React components into a directory whose
|
|
1462
|
+
entire contract is that every file in it is generated — a hand-written tree
|
|
1463
|
+
inside a generated one, invisible to the lockfile, unreachable by the codemod,
|
|
1464
|
+
and the first thing a re-sync would trip over. The plan's decision 3, which asks
|
|
1465
|
+
for a peer with its four coordinated edits, is what was followed instead. The
|
|
1466
|
+
reason is recorded in `rules.mjs` beside the batch it belongs to, so the next
|
|
1467
|
+
person to read the spec finds the disagreement where the code is.
|
|
1468
|
+
|
|
1469
|
+
## Two elements carry cue runtime, and that is the point
|
|
1470
|
+
|
|
1471
|
+
Two of the 96 needed more than a class rewrite, and both were held back through
|
|
1472
|
+
their own batch for a decision rather than improvised inside one. The decision:
|
|
1473
|
+
**the codemod may write cue's own code into a vendored file.**
|
|
1474
|
+
|
|
1475
|
+
- **`InlineCitation`** is the family's one overlay — a Base UI preview card
|
|
1476
|
+
behind each numbered marker — and its `Portal` now spreads
|
|
1477
|
+
`useCuePortalProps()`. A portaled subtree mounts on `<body>`, where the token
|
|
1478
|
+
layer resolves the document's values instead of the island's, so without the
|
|
1479
|
+
stamp a hover card opened inside an `<AgentSurface skin="aui">` would paint in
|
|
1480
|
+
your console's palette while the sentence behind it painted in assistant-ui's.
|
|
1481
|
+
That is precisely the bug P0 gave `PortalStamp` its `skin` / `fidelity`
|
|
1482
|
+
carriage to prevent. `@base-ui/react` was already a required peer, so this
|
|
1483
|
+
costs nothing new to install.
|
|
1484
|
+
- **`ScrollAnchor`** is the one element in all 96 that drives its own clock — a
|
|
1485
|
+
`setInterval` appending a turn every 1.3s — and the one whose viewport scrolls
|
|
1486
|
+
itself. Its effect now returns early under `usePrefersReducedMotion()`, after
|
|
1487
|
+
painting the still frame: the _finished_ transcript, pinned to the bottom,
|
|
1488
|
+
rather than a scene frozen three messages in. Its two imperative smooth
|
|
1489
|
+
scrolls lost their `behavior: "smooth"` option, which no browser will still for
|
|
1490
|
+
`prefers-reduced-motion`; the element already carries `scroll-smooth` on the
|
|
1491
|
+
same viewport, so the scroll stays smooth from CSS and one
|
|
1492
|
+
`motion-reduce:scroll-auto` twin now turns all three of them off.
|
|
1493
|
+
|
|
1494
|
+
Read as a leak, four lines of cue runtime inside vendored source look like the
|
|
1495
|
+
vendoring failing. They are the vendoring working. These files are cue-owned
|
|
1496
|
+
source — that is the whole premise, since Elements ship as copy-in behind a
|
|
1497
|
+
registry with no version — so `packages/ui/src/elements/` is this repository's
|
|
1498
|
+
code, generated rather than typed. The codemod already rewrites every class in it
|
|
1499
|
+
onto cue's tokens; making a vendored element obey cue's _contracts_ is the same
|
|
1500
|
+
act performed one layer up. An element that portals without the island stamp, or
|
|
1501
|
+
re-arms a callback without asking whether the reader wants motion, is broken in
|
|
1502
|
+
this system's terms whatever it does upstream.
|
|
1503
|
+
|
|
1504
|
+
And it is a rule, never a hand edit. `RUNTIME_INJECTIONS` in `rules.mjs` names
|
|
1505
|
+
the file, the hook, the module, the binding, the component and the exact lines
|
|
1506
|
+
that change, each with its reason; the codemod throws on every anchor it cannot
|
|
1507
|
+
find, `sync.test.mjs` fails on an injection that stopped reaching the emitted
|
|
1508
|
+
tree, and both files carry the reasoning in a comment where a reader meets the
|
|
1509
|
+
import.
|
|
1510
|
+
|
|
1511
|
+
## Six text inputs get a focus ring back
|
|
1512
|
+
|
|
1513
|
+
`ElementsComposer`'s input, `ElementsCommandPalette`'s, `PromptLibrary`'s,
|
|
1514
|
+
`ThreadSearch`'s, `ConversationSearch`'s and `MobileComposer`'s are bare
|
|
1515
|
+
`<input>`s whose entire upstream class string is a placeholder colour, a size,
|
|
1516
|
+
`bg-transparent` and `outline-none`. They removed the browser's focus outline and
|
|
1517
|
+
put nothing in its place — a keyboard user tabbing into a command palette had no
|
|
1518
|
+
idea they had arrived. It is not a ring the codemod dropped; upstream never wrote
|
|
1519
|
+
one.
|
|
1520
|
+
|
|
1521
|
+
The rewrite restores cue's ring only where something in the class string says the
|
|
1522
|
+
element is focusable, and its signal was interaction-state variants and
|
|
1523
|
+
`group`/`peer` labels — none of which these six carry. `::placeholder` and
|
|
1524
|
+
`caret-color` apply to `input` and `textarea` and to nothing else in CSS, so
|
|
1525
|
+
either one is proof the element takes a caret, and an element that takes a caret
|
|
1526
|
+
is focusable. Both now count, and the six come back with the same
|
|
1527
|
+
`focus-visible:outline-2 / solid / offset-1 / accent` recipe every other control
|
|
1528
|
+
in this package uses.
|
|
1529
|
+
|
|
1530
|
+
**This is a WCAG 2.4.7 (Focus Visible, AA) fix you inherit by upgrading.** No
|
|
1531
|
+
prop moved and nothing needs editing; if you render any of those six, a visible
|
|
1532
|
+
focus indicator appears where there was none. The fix is in `rules.mjs`, so a
|
|
1533
|
+
re-sync reproduces it byte for byte rather than losing it.
|
|
1534
|
+
|
|
1535
|
+
## `llms.txt` is an index again
|
|
1536
|
+
|
|
1537
|
+
The machine surfaces on `ui.cueplusplus.com` re-tier, because one of them was a
|
|
1538
|
+
release away from failing its own cap: `/llms.txt` was 386 lines against a
|
|
1539
|
+
ceiling of 400, and 308 of those lines were a component's name, its URL and a
|
|
1540
|
+
one-liner. What grows that file is the catalogue, and the catalogue is what the
|
|
1541
|
+
library is for, so raising the cap would have bought exactly one release.
|
|
1542
|
+
|
|
1543
|
+
`/llms.txt` now lists **groups** — nineteen lines, each with the group's page,
|
|
1544
|
+
its component and hook counts, the specifier it imports from and what it is for —
|
|
1545
|
+
and points at the sheet for everything inside them. 61 non-blank lines, from 386,
|
|
1546
|
+
with the same facts still coming off the manifest.
|
|
1547
|
+
|
|
1548
|
+
Nothing lost information. `/llms-components.txt` was already exhaustive and stays
|
|
1549
|
+
exhaustive: one line per component with its full props signature and its import
|
|
1550
|
+
specifier, one per hook with its call signature, and a header stating the rule for
|
|
1551
|
+
reaching any component's page
|
|
1552
|
+
(`https://ui.cueplusplus.com/docs/components/<slug>.md`). The index stopped
|
|
1553
|
+
duplicating the sheet, which is the only thing it was doing with 80% of its lines.
|
|
1554
|
+
|
|
1555
|
+
If you point an agent at these files, point it at both, in this order: `/llms.txt`
|
|
1556
|
+
first — what the library is, the rules a props table cannot show, and which group
|
|
1557
|
+
a thing lives in — then `/llms-components.txt` before it writes a call, for the
|
|
1558
|
+
specifier and the full props literal. `/llms-full.txt` and `/kitchen-sink.md` are
|
|
1559
|
+
unchanged.
|
|
1560
|
+
|
|
1561
|
+
## What did not change
|
|
1562
|
+
|
|
1563
|
+
No token moved. `@cueplusplus/tokens` is untouched by this release — the colour
|
|
1564
|
+
contract is the same 42 custom properties the last one shipped — and no existing
|
|
1565
|
+
component's props, structure or rendering changed. The root barrel is still
|
|
1566
|
+
untouched, so an app that never imports `/elements` gets no new peer, no new
|
|
1567
|
+
stylesheet and no new bytes; `heat-graph` and `lucide-react` are both optional
|
|
1568
|
+
and both reached only from this subpath.
|
|
1569
|
+
|
|
1570
|
+
`@cueplusplus/ui/elements.css` gains exactly one declaration — a
|
|
1571
|
+
`cue-elements-zoom-in-95` entrance class, namespaced like the other nine so that
|
|
1572
|
+
an app which also installs `tw-animate-css` never has two stylesheets quietly
|
|
1573
|
+
arguing over `.animate-in` by import order. It remains optional even for this
|
|
1574
|
+
subpath: skip it and the elements still render in cue's palette, you lose the
|
|
1575
|
+
motion and `skin="aui"` becomes a no-op.
|
|
1576
|
+
|
|
1577
|
+
Text is still text. Nothing in this group parses model output as markdown or
|
|
1578
|
+
HTML — 132 components and not one of them decides your markup for you. Sanitized
|
|
1579
|
+
markdown remains a documented, opt-in surface arriving later, with its own peer
|
|
1580
|
+
and its own entry.
|
|
1581
|
+
|
|
1582
|
+
`schemaVersion` is still `1`.
|
|
1583
|
+
|
|
1584
|
+
- 854ad96: ## A streaming layer under the Chat group
|
|
1585
|
+
|
|
1586
|
+
The Chat group renders a turn that has finished arriving. It has never had a
|
|
1587
|
+
story for one that is _still_ arriving: no token assembly, no cancelling a run
|
|
1588
|
+
mid-sentence, no regenerating a turn beside the one it replaces, and nowhere to
|
|
1589
|
+
put the model's own tool call while the rest of the answer is still coming.
|
|
1590
|
+
|
|
1591
|
+
`@cueplusplus/ui/agent-runtime` is that story, and it is one hook wide.
|
|
1592
|
+
`useAgentThread(runtime)` takes any `@assistant-ui/react` runtime — the one you
|
|
1593
|
+
build with `useExternalStoreRuntime(adapter)`, or the AI-SDK or LangGraph ones —
|
|
1594
|
+
and gives back the props `Message`, `MessageList` and `Composer` already take.
|
|
1595
|
+
Every field is named for the prop it feeds: `busy`, `disabled`, `onSend` and
|
|
1596
|
+
`onStop` to `Composer`; `role`, `agent`, `pending` and `text` to `Message`;
|
|
1597
|
+
`toolCalls[]` onto `ToolCallCard`; `branch` and `switchBranch` to
|
|
1598
|
+
`BranchPicker`.
|
|
1599
|
+
|
|
1600
|
+
Two components ship in the entry with it. **`ToolCallCard`** is the model's own
|
|
1601
|
+
call: the tool name in mono, an optional risk chip in `AskBox`'s exact
|
|
1602
|
+
vocabulary, the status spelled in words beside a spinner rather than in colour
|
|
1603
|
+
alone, and the arguments and result behind a disclosure that mounts and unmounts
|
|
1604
|
+
instead of animating a height. It is neither of its neighbours — `AskBox` is a
|
|
1605
|
+
question put to a human, `DelegationCard` is a sub-agent's work, and a tool call
|
|
1606
|
+
is the model's own action. **`BranchPicker`** is a `‹ 2 / 3 ›` stepper for an
|
|
1607
|
+
agent turn's footer: two ghost icon buttons around a mono readout that reads
|
|
1608
|
+
itself aloud as "branch 2 of 3", clamped so it can never point past either end,
|
|
1609
|
+
and drawing nothing at all while there is only one branch.
|
|
1610
|
+
|
|
1611
|
+
`@assistant-ui/react@^0.15.16` is an **optional** peer, declared and
|
|
1612
|
+
externalised the way `@xyflow/react` is under `/flow`. Import nothing from
|
|
1613
|
+
`/agent-runtime` and it costs you nothing: no install, no bundle, no resolution
|
|
1614
|
+
error — the root barrel's import graph never reaches it, and neither does
|
|
1615
|
+
`/chat`'s. Import the entry and you do need it. Nothing in the shipped modules
|
|
1616
|
+
imports `@assistant-ui/react` at runtime — the hook's own file names only its
|
|
1617
|
+
types — but you cannot build the `AssistantRuntime` the hook takes without it,
|
|
1618
|
+
so install it. Installing it also installs its own dependencies — `radix-ui`
|
|
1619
|
+
and several `@radix-ui/*` parts, `zustand`, `zod`, `assistant-stream`,
|
|
1620
|
+
`@assistant-ui/core` — about 16 MB on disk; the subpath boundary keeps all of
|
|
1621
|
+
it out of an app that never imports the entry.
|
|
1622
|
+
|
|
1623
|
+
```tsx
|
|
1624
|
+
import { Composer, Message, MessageList } from "@cueplusplus/ui";
|
|
1625
|
+
import {
|
|
1626
|
+
BranchPicker,
|
|
1627
|
+
ToolCallCard,
|
|
1628
|
+
useAgentThread,
|
|
1629
|
+
} from "@cueplusplus/ui/agent-runtime";
|
|
1630
|
+
import {
|
|
1631
|
+
useExternalStoreRuntime,
|
|
1632
|
+
type ThreadMessageLike,
|
|
1633
|
+
} from "@assistant-ui/react";
|
|
1634
|
+
|
|
1635
|
+
// Outside the component: the runtime caches the conversion against this
|
|
1636
|
+
// function, and a new one every render is an update loop.
|
|
1637
|
+
const convertMessage = (m: ThreadMessageLike) => m;
|
|
1638
|
+
|
|
1639
|
+
const runtime = useExternalStoreRuntime({
|
|
1640
|
+
messages,
|
|
1641
|
+
isRunning,
|
|
1642
|
+
convertMessage,
|
|
1643
|
+
onNew,
|
|
1644
|
+
onCancel,
|
|
1645
|
+
onReload,
|
|
1646
|
+
setMessages,
|
|
1647
|
+
});
|
|
1648
|
+
const thread = useAgentThread(runtime, { agent: "planner" });
|
|
1649
|
+
|
|
1650
|
+
<MessageList aria-label="Thread">
|
|
1651
|
+
{thread.turns.map((t) => (
|
|
1652
|
+
<Message
|
|
1653
|
+
key={t.id}
|
|
1654
|
+
role={t.role}
|
|
1655
|
+
agent={t.agent}
|
|
1656
|
+
pending={t.pending}
|
|
1657
|
+
footer={
|
|
1658
|
+
t.branch.count > 1 && thread.switchBranch ? (
|
|
1659
|
+
<BranchPicker
|
|
1660
|
+
index={t.branch.index}
|
|
1661
|
+
count={t.branch.count}
|
|
1662
|
+
onNavigate={(d) => thread.switchBranch?.(t.id, d)}
|
|
1663
|
+
disabled={thread.busy}
|
|
1664
|
+
/>
|
|
1665
|
+
) : undefined
|
|
1666
|
+
}
|
|
1667
|
+
>
|
|
1668
|
+
{t.text}
|
|
1669
|
+
{t.toolCalls.map((c) => (
|
|
1670
|
+
<ToolCallCard
|
|
1671
|
+
key={c.id}
|
|
1672
|
+
tool={c.tool}
|
|
1673
|
+
args={c.args}
|
|
1674
|
+
result={c.result}
|
|
1675
|
+
status={c.status}
|
|
1676
|
+
/>
|
|
1677
|
+
))}
|
|
1678
|
+
</Message>
|
|
1679
|
+
))}
|
|
1680
|
+
</MessageList>;
|
|
1681
|
+
<Composer
|
|
1682
|
+
onSend={thread.onSend}
|
|
1683
|
+
busy={thread.busy}
|
|
1684
|
+
disabled={thread.disabled || thread.busy}
|
|
1685
|
+
onStop={thread.onStop}
|
|
1686
|
+
/>;
|
|
1687
|
+
```
|
|
1688
|
+
|
|
1689
|
+
## What did not change
|
|
1690
|
+
|
|
1691
|
+
The six Chat components — `AgentPile`, `AskBox`, `Composer`, `DelegationCard`,
|
|
1692
|
+
`Message`, `MessageList` — render what they rendered before, with the one
|
|
1693
|
+
exception below. Nothing is restyled and no prop moved.
|
|
1694
|
+
|
|
1695
|
+
Text is still text. `ToolCallCard` renders arguments and results verbatim, never
|
|
1696
|
+
as markdown and never as HTML, for the same reason `Message` does: an argument
|
|
1697
|
+
string is whatever a model decided to emit, and the moment a component parses it
|
|
1698
|
+
the model chooses your markup. The hook joins text parts into strings rather
|
|
1699
|
+
than handing you a node tree to trust. No markdown package is added, opt-in or
|
|
1700
|
+
otherwise.
|
|
1701
|
+
|
|
1702
|
+
Nothing of assistant-ui's rendering is adopted. The hook is a
|
|
1703
|
+
`useSyncExternalStore` over the runtime's thread and nothing else — no
|
|
1704
|
+
`AssistantRuntimeProvider` anywhere in your tree, no primitive mounted, no Radix
|
|
1705
|
+
DOM, no borrowed context. Its `asChild` composition model is not adapted and not
|
|
1706
|
+
exposed: this library's public API is still 100% `render`-prop. assistant-ui
|
|
1707
|
+
owns the state machine, this library owns 100% of the render.
|
|
1708
|
+
|
|
1709
|
+
## The one fix inside Chat
|
|
1710
|
+
|
|
1711
|
+
**`MessageList` now follows a turn that is still growing.** Its re-pin ran on the
|
|
1712
|
+
number of children changing, so a _new_ turn kept the view at the bottom but
|
|
1713
|
+
tokens streaming into a bubble that was already there did not — the list quietly
|
|
1714
|
+
stopped following mid-sentence, exactly when following matters most. It now also
|
|
1715
|
+
watches the items container and re-pins **when that container grows**, while
|
|
1716
|
+
following and pinned.
|
|
1717
|
+
|
|
1718
|
+
Only growth moves the view. A tool-call card the reader folds away, an image
|
|
1719
|
+
that failed to load, a footer that disappears — anything that makes the list
|
|
1720
|
+
shorter leaves the scroll position exactly where it was. Scrolling up still lets
|
|
1721
|
+
go, and a turn that grows while you are up there offers the "jump to latest"
|
|
1722
|
+
pill rather than pulling you down. `follow={false}` switches the whole mode off,
|
|
1723
|
+
growth included.
|
|
1724
|
+
|
|
1725
|
+
This is new behaviour on a component you already ship, so it is worth a moment
|
|
1726
|
+
if you have a `MessageList` whose children change height in place: it will now
|
|
1727
|
+
stay at the bottom where before it drifted up. There is no new prop and no API
|
|
1728
|
+
change, and an engine without `ResizeObserver` behaves exactly as it did.
|
|
1729
|
+
|
|
1730
|
+
## Things worth knowing
|
|
1731
|
+
|
|
1732
|
+
**A branch switch is host-owned under an ExternalStore runtime.** `switchBranch`
|
|
1733
|
+
does not move the thread itself. It arrives at your adapter's `setMessages` with
|
|
1734
|
+
the newly visible list, and your host writes it back into whatever holds the
|
|
1735
|
+
messages. A host that ignores `setMessages` gets a picker that clicks and does
|
|
1736
|
+
nothing — which looks like a bug in the picker and is not one.
|
|
1737
|
+
|
|
1738
|
+
**`onReload` takes two arguments**, `(parentId, config)`. Regenerating re-runs
|
|
1739
|
+
the turn's _parent_, which is what puts the new answer beside the old one rather
|
|
1740
|
+
than after it — and is what gives `BranchPicker` a second branch to show at all.
|
|
1741
|
+
The picker only moves between branches that already exist, so the control that
|
|
1742
|
+
asks for a new one is yours to draw: a ghost `Button` in the turn's footer
|
|
1743
|
+
calling `regenerate(turn.id)`, as the docs demo does.
|
|
1744
|
+
|
|
1745
|
+
**Pass `convertMessage`, and declare it outside the component.** Without it your
|
|
1746
|
+
messages have to already be complete runtime messages, and a missing `metadata`
|
|
1747
|
+
throws; `convertMessage: (m) => m` lets the runtime fill the defaults in around
|
|
1748
|
+
the messages you already have. Give it a stable identity, though: the runtime
|
|
1749
|
+
caches the conversion against that function, so an arrow written inline in the
|
|
1750
|
+
adapter re-converts the whole thread on every render — and the next time your
|
|
1751
|
+
message list changes, that becomes React's "Maximum update depth exceeded"
|
|
1752
|
+
rather than a slow render.
|
|
1753
|
+
|
|
1754
|
+
**`onStop`, `regenerate` and `switchBranch` are `undefined`** until the adapter
|
|
1755
|
+
has the matching callback — `onCancel`, `onReload`, `setMessages`. That is
|
|
1756
|
+
deliberate rather than defensive: `Composer` keeps drawing Send while `onStop`
|
|
1757
|
+
is missing, so a control that could not work is never drawn.
|
|
1758
|
+
|
|
1759
|
+
**The peer range is pinned on purpose.** `^0.15.16`, not `^0.15.0`: 0.15.9
|
|
1760
|
+
through 0.15.16 shipped inside twelve days. What this entry reads are the stable
|
|
1761
|
+
members of the runtime object — `thread.getState()`, `subscribe`, `append`,
|
|
1762
|
+
`cancelRun`, `getMessageById`, `switchToBranch` — and never an `unstable_*` one,
|
|
1763
|
+
but a floor that recent is worth saying out loud rather than discovering.
|
|
1764
|
+
|
|
1765
|
+
The live thread, the four tool-call states and the branch stepper are on the
|
|
1766
|
+
docs site under **Agent runtime**, at `/docs/components/agent-runtime`.
|
|
1767
|
+
|
|
1768
|
+
### Patch Changes
|
|
1769
|
+
|
|
1770
|
+
- e5fc161: ## Everywhere the red is spent
|
|
1771
|
+
|
|
1772
|
+
`--cue-danger-fg` arrived with `RevertDock`'s confirm and stopped there. The two
|
|
1773
|
+
destructive controls the rest of the library is actually built out of never got
|
|
1774
|
+
it, so the token shipped alongside the defect it was authored to close.
|
|
1775
|
+
|
|
1776
|
+
Three fills change, and all three are contrast fixes rather than restyles:
|
|
1777
|
+
|
|
1778
|
+
- **`Button` `variant="danger"`** — `bg-danger text-white` becomes
|
|
1779
|
+
`bg-danger text-danger-fg`.
|
|
1780
|
+
- **`TwoStepButton`'s armed `danger` confirm** — the same swap, on the control
|
|
1781
|
+
that exists because the action is irreversible.
|
|
1782
|
+
- **`VoiceConversation`'s end-call button** — `bg-danger/90 text-accent-fg`
|
|
1783
|
+
becomes `bg-danger text-danger-fg`. That element is vendored from
|
|
1784
|
+
assistant-ui, so the fix is a rule in `scripts/elements-sync/rules.mjs` and
|
|
1785
|
+
the file is re-emitted by the sync; a hand edit would have lasted until the
|
|
1786
|
+
next SHA bump.
|
|
1787
|
+
|
|
1788
|
+
`text-white` on the red measures **2.99:1** on cue's dark block — the default
|
|
1789
|
+
theme, under AA text and under the 3:1 floor a non-text control gets — and it is
|
|
1790
|
+
under AA on eight of the ten dark blocks this system ships. `text-danger-fg`
|
|
1791
|
+
measures **7.03:1** in the same place and clears 4.5:1 on all twenty
|
|
1792
|
+
preset x mode blocks. `text-accent-fg` is the ink authored against
|
|
1793
|
+
`--cue-accent`, and on the red it lands anywhere from 3.58:1 to 6.67:1; the
|
|
1794
|
+
`/90` then let the surface through and took the pair down with it.
|
|
1795
|
+
|
|
1796
|
+
Nothing you pass changes. A `danger` button, an armed confirm and an end-call
|
|
1797
|
+
button are the same elements with the same props; the label on them is legible
|
|
1798
|
+
now on every theme in both modes.
|
|
1799
|
+
|
|
1800
|
+
### The reason all three stayed green
|
|
1801
|
+
|
|
1802
|
+
The sweep that arrived with the token measures **the token pair** — twenty
|
|
1803
|
+
blocks of `danger-fg` on `danger` — and a token pair is only the answer for a
|
|
1804
|
+
control that asks for it. These three asked for something else and sat outside
|
|
1805
|
+
the measurement.
|
|
1806
|
+
|
|
1807
|
+
`test/destructive-ink.test.ts` reads the source instead. It finds every class
|
|
1808
|
+
string in the package that makes the red a ground and puts an ink on it, takes
|
|
1809
|
+
the ink out of that string, and measures it on that theme's red across all
|
|
1810
|
+
twenty blocks at 4.5:1. It refuses an alpha on the fill, because
|
|
1811
|
+
`--cue-danger-fg` on `bg-danger/90` falls to 3.94:1 on quotamate's dark over
|
|
1812
|
+
`--cue-sunken` and 3.97:1 over `--cue-bg` — and that the two grounds disagree at
|
|
1813
|
+
all is why the alpha is refused rather than measured. It fails on an ink whose
|
|
1814
|
+
colour it cannot read rather than skipping it. The list of fills is pinned by
|
|
1815
|
+
name: a new destructive fill has to choose its ink in the commit that paints
|
|
1816
|
+
it.
|
|
1817
|
+
|
|
1818
|
+
`TwoStepButton`'s `warn` confirm keeps the `text-black` literal through this
|
|
1819
|
+
change, and its JSDoc stops claiming more than it can. At this point there was
|
|
1820
|
+
no `--cue-warn-fg` to give it, that pairing floored at 3.58:1 on requestport's
|
|
1821
|
+
light block, and the sweep said in writing that the amber was the one it did not
|
|
1822
|
+
cover. _The amber carries its own ink too_, in this same release, is where that
|
|
1823
|
+
gets closed.
|
|
1824
|
+
|
|
1825
|
+
- 58eb41a: ## The red the sheets draw
|
|
1826
|
+
|
|
1827
|
+
The generative vocabulary fills a control with the red too. `GenerativeUI`
|
|
1828
|
+
renders `[data-aui="button"][data-aui-style="danger"]` from a model's spec, and
|
|
1829
|
+
that button painted `--aui-bg` — the page under the widget — on a solid
|
|
1830
|
+
`--aui-danger`. It reads **4.03:1** on terminal's dark block and **4.12:1** on
|
|
1831
|
+
hivehub's, both under AA, and **3.82:1** on the `aui` skin's light half, which
|
|
1832
|
+
is the number `elements.css` already writes down beside `--cue-danger-fg` as
|
|
1833
|
+
the reason that token exists.
|
|
1834
|
+
|
|
1835
|
+
It takes `--aui-danger-fg` now, one more line on the seam
|
|
1836
|
+
(`var(--cue-danger-fg, #000000)`, a literal fallback like every other reference
|
|
1837
|
+
in that block), and it clears 4.5:1 on all twenty shipped preset x mode blocks
|
|
1838
|
+
and on both halves of the island. Nothing a spec can say changes: a `danger`
|
|
1839
|
+
button is the same intrinsic with the same props.
|
|
1840
|
+
|
|
1841
|
+
### Why the sweep beside it did not see this one
|
|
1842
|
+
|
|
1843
|
+
`test/destructive-ink.test.ts` reads class strings, and this sheet has none.
|
|
1844
|
+
`generative.css` is plain `[data-aui]` CSS on purpose — no Tailwind, no utility
|
|
1845
|
+
to grep — so a fill drawn there sat outside a guard whose own name said it
|
|
1846
|
+
found every one.
|
|
1847
|
+
|
|
1848
|
+
It reads the package's stylesheets too now. For every rule that makes
|
|
1849
|
+
`--cue-danger` a whole-value ground and puts a `color` on it, it resolves both
|
|
1850
|
+
through the `--aui-*` seam and measures the pair on the same twenty blocks at
|
|
1851
|
+
the same 4.5:1, refuses an ink it cannot resolve to a theme colour rather than
|
|
1852
|
+
skipping it, and pins the rules it found by selector. A `color-mix()` does not
|
|
1853
|
+
resolve, which is the same exemption the class sweep gives `bg-danger/10`.
|
|
1854
|
+
|
|
1855
|
+
One hole in the class half closed with it: `text-[#7f1d1d]` was read as "not a
|
|
1856
|
+
colour" and took its whole call site out of the corpus in silence. Only lengths
|
|
1857
|
+
are excluded now — the six this package actually writes — so an arbitrary ink
|
|
1858
|
+
on the red fails by name and asks to be taught.
|
|
1859
|
+
|
|
1860
|
+
- Updated dependencies [51e5046]
|
|
1861
|
+
- Updated dependencies [06eb4ac]
|
|
1862
|
+
- Updated dependencies [4e510e7]
|
|
1863
|
+
- @cueplusplus/tokens@0.5.0
|
|
1864
|
+
|
|
1865
|
+
## 0.4.0
|
|
1866
|
+
|
|
1867
|
+
### Minor Changes
|
|
1868
|
+
|
|
1869
|
+
- e558992: Ten container icons, for the navigation column that just learned to rail
|
|
1870
|
+
|
|
1871
|
+
A navigation column is a list of places, and a place is a word until the column
|
|
1872
|
+
narrows. `AppShell` can rail now — the column drops to `3.5rem` and every row
|
|
1873
|
+
keeps its glyph and loses its name — which turns "does this row have an icon"
|
|
1874
|
+
from a styling question into whether the row still exists. This library had a
|
|
1875
|
+
public component for the marks (`CueMark`, `CueLogotype`, `Plussie`) and nine
|
|
1876
|
+
private `_glyphs` modules for its own chevrons and carets, and nothing at all
|
|
1877
|
+
for the things a catalogue actually sorts into.
|
|
1878
|
+
|
|
1879
|
+
```tsx
|
|
1880
|
+
import { CatalogueIcon, ChannelBetaIcon, SkillIcon } from "@cueplusplus/ui";
|
|
1881
|
+
|
|
1882
|
+
<Sidebar.Item icon={CatalogueIcon}>All skills</Sidebar.Item>
|
|
1883
|
+
<Sidebar.Item icon={ChannelBetaIcon}>Our skills · beta</Sidebar.Item>
|
|
1884
|
+
```
|
|
1885
|
+
|
|
1886
|
+
| Icon | The place it marks |
|
|
1887
|
+
| --------------------- | ------------------------------------------------------ |
|
|
1888
|
+
| `CatalogueIcon` | everything there is, before anything has been narrowed |
|
|
1889
|
+
| `StackIcon` | one person's stack |
|
|
1890
|
+
| `StacksMatrixIcon` | every stack at once, one column each |
|
|
1891
|
+
| `AudienceIcon` | a named group of people something is published to |
|
|
1892
|
+
| `ChannelReleasedIcon` | the released channel |
|
|
1893
|
+
| `ChannelBetaIcon` | the beta channel |
|
|
1894
|
+
| `FolderIcon` | a shelf somebody filed by hand |
|
|
1895
|
+
| `McpServerIcon` | an MCP server in the registry |
|
|
1896
|
+
| `CliToolIcon` | a CLI tool in the registry |
|
|
1897
|
+
| `SkillIcon` | a skill — the thing all the others hold |
|
|
1898
|
+
|
|
1899
|
+
## It is a set, which is a stronger claim than ten icons
|
|
1900
|
+
|
|
1901
|
+
They are read in one column, one after another, so a grid or a weight that
|
|
1902
|
+
drifts on any one of them is read as a mistake on _that row_ rather than as a
|
|
1903
|
+
style. Every one is drawn from a single shared chassis and the suite holds all
|
|
1904
|
+
three of its rules: one grid (`0 0 16 16` at `stroke-width` 1.5), no size of
|
|
1905
|
+
its own, no colour of its own. There is also a test that no two of them draw
|
|
1906
|
+
the same thing, because a set that ships one mark twice has a hole in it where
|
|
1907
|
+
a reader will look for a distinction.
|
|
1908
|
+
|
|
1909
|
+
Two pairs carry the weight of that. `ChannelReleasedIcon` and `ChannelBetaIcon`
|
|
1910
|
+
are **one ring told twice** — the same `r="5.5"` circle, filled solid and
|
|
1911
|
+
filled halfway — so nobody has to learn which of two unrelated pictures means
|
|
1912
|
+
"finished". And `StackIcon` is plates seen edge-on while `StacksMatrixIcon` is
|
|
1913
|
+
a table with a head rule and a name column, because the question the matrix
|
|
1914
|
+
answers has two axes and the question a stack answers has one.
|
|
1915
|
+
|
|
1916
|
+
## Sized and inked by the caller, on purpose
|
|
1917
|
+
|
|
1918
|
+
No `size` prop, no `tone` prop. `className="size-icon-md"` reads
|
|
1919
|
+
`--cue-icon-md`, so a glyph tracks the density island it is in — an icon that
|
|
1920
|
+
measured itself would be the one thing in a compact subtree that did not move.
|
|
1921
|
+
Ink is `currentColor` throughout, so a glyph in a muted row is muted and the
|
|
1922
|
+
same glyph in the current row is not, with nothing to pass down.
|
|
1923
|
+
|
|
1924
|
+
They are decorative by default — `aria-hidden` — because they mark a row that
|
|
1925
|
+
already carries its name, and a second announcement of the same word is noise.
|
|
1926
|
+
Props spread last, so `aria-hidden={false} role="img"` with a `<title>` turns
|
|
1927
|
+
one back into a picture where a glyph really is the only name for something.
|
|
1928
|
+
|
|
1929
|
+
## Why 16 and not 24
|
|
1930
|
+
|
|
1931
|
+
`lucide-react` draws on a 24 grid, and it remains the answer for everything
|
|
1932
|
+
outside this vocabulary — it is still an optional peer, every `icon` slot here
|
|
1933
|
+
still takes a component, and the two mix freely in one column. But `1.5` on a
|
|
1934
|
+
24 grid renders at two-thirds the weight of `1.5` on a 16 grid in the same box,
|
|
1935
|
+
and every glyph this library already draws is on 16. Matching lucide's number
|
|
1936
|
+
would have meant not matching its own line, and the line is the thing a reader
|
|
1937
|
+
sees. An app that wants these to sit at exactly lucide's weight passes
|
|
1938
|
+
`strokeWidth` — it overrides, like every other attribute.
|
|
1939
|
+
|
|
1940
|
+
## Ten exports rather than one `Icons` object
|
|
1941
|
+
|
|
1942
|
+
Deliberate, and it costs ten manifest entries instead of one with ten parts. A
|
|
1943
|
+
frozen namespace object does not tree-shake per member: a bundler cannot drop
|
|
1944
|
+
an unused property of an exported object literal, so a console that used two of
|
|
1945
|
+
these would have shipped all ten. Named exports of one module shake to exactly
|
|
1946
|
+
what is imported.
|
|
1947
|
+
|
|
1948
|
+
They ship in the root barrel with no subpath of their own, on the same ruling
|
|
1949
|
+
`brand/` records: the group pulls no optional peer and weighs two files.
|
|
1950
|
+
**Nothing here changes an existing component.** `packages/ui/manifest/**` is
|
|
1951
|
+
regenerated, not hand-edited.
|
|
1952
|
+
|
|
1953
|
+
- 85448d8: ## Three hairlines, derived from the foreground
|
|
1954
|
+
|
|
1955
|
+
A list is not a stack of boxes, and until now the token vocabulary had no way to
|
|
1956
|
+
say the difference.
|
|
1957
|
+
|
|
1958
|
+
`--cue-border` is an authored anchor: every preset picks one, and what every
|
|
1959
|
+
preset picks is a **box edge** — the line around a card, an input, a panel. It is
|
|
1960
|
+
the right weight for exactly that. Repeat it down thirty rows of a table and the
|
|
1961
|
+
page stops reading as a list and starts reading as thirty boxes stacked on each
|
|
1962
|
+
other. The line _between_ things wants to be lighter than the line _around_ a
|
|
1963
|
+
thing, and there was no token for it, so every surface that needed one either
|
|
1964
|
+
borrowed `--cue-border` and looked heavy, or invented a `color-mix()` of its own
|
|
1965
|
+
and stopped being themeable.
|
|
1966
|
+
|
|
1967
|
+
Three new tokens, in every preset, in both modes:
|
|
1968
|
+
|
|
1969
|
+
| Token | Value | What it is |
|
|
1970
|
+
| ------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
|
1971
|
+
| `--cue-hair` | `color-mix(in oklch, var(--cue-fg) 6%, transparent)` | **Hairline rule** — between rows, between sections, under a caption |
|
|
1972
|
+
| `--cue-hair-strong` | `color-mix(in oklch, var(--cue-fg) 12%, transparent)` | **Strong hairline** — the same line where it carries weight: a table head, a quiet tag's edge |
|
|
1973
|
+
| `--cue-row-hover` | `color-mix(in oklch, var(--cue-fg) 3.5%, transparent)` | **Row hover wash** — the faintest fill that still reads as one row picked out of a list |
|
|
1974
|
+
|
|
1975
|
+
They are mixed from `--cue-fg`, not authored per preset, and that is the whole
|
|
1976
|
+
point of them. A share of the foreground keeps its relationship to the type it
|
|
1977
|
+
separates whatever the palette turned out to be — the rules lighten with the text
|
|
1978
|
+
in a light block, they hold at `terminal`'s cyan-on-black and at `luma`'s warm
|
|
1979
|
+
paper, and a preset added tomorrow gets all three for free without authoring a
|
|
1980
|
+
thing. Asking ten presets to hand-pick a hairline would have produced ten
|
|
1981
|
+
slightly different answers to a question nobody should have to answer.
|
|
1982
|
+
|
|
1983
|
+
They are emitted **inside** every theme block rather than once on `:root`, like
|
|
1984
|
+
every other derived colour here: a custom property's `var()` references are
|
|
1985
|
+
substituted at the element that declares it, so a `:root`-level mix would freeze
|
|
1986
|
+
the default preset's foreground into every nested theme.
|
|
1987
|
+
|
|
1988
|
+
## Spending them
|
|
1989
|
+
|
|
1990
|
+
They are not Tailwind utilities, on the same ruling as `--cue-accent-soft`'s
|
|
1991
|
+
status siblings — arbitrary-property syntax reads the token directly:
|
|
1992
|
+
|
|
1993
|
+
```tsx
|
|
1994
|
+
<div className="border-t border-(--cue-hair) hover:bg-(--cue-row-hover)" />
|
|
1995
|
+
```
|
|
1996
|
+
|
|
1997
|
+
## `--cue-row-hover` does not replace the surface ramp
|
|
1998
|
+
|
|
1999
|
+
Worth being exact, because there are now two answers to "how does a row show
|
|
2000
|
+
hover" and only one of them is new. A row that sits **on a surface** still lifts
|
|
2001
|
+
to the next rung — `hover:bg-surface-2` — and that is what this library's own
|
|
2002
|
+
rows do: `Sidebar.Item`, `Tree`, `Pagination`, `Toggle`, `NumberField`. None of
|
|
2003
|
+
them change, and none of them should.
|
|
2004
|
+
|
|
2005
|
+
`--cue-row-hover` is for rows drawn **directly on `--cue-bg`**, where the next
|
|
2006
|
+
rung of the ramp is a step you can see from across the room and the row you are
|
|
2007
|
+
pointing at ends up looking selected rather than hovered. If the ramp fits your
|
|
2008
|
+
row, use the ramp.
|
|
2009
|
+
|
|
2010
|
+
## `createTheme()` derives them too — and now it cannot forget to
|
|
2011
|
+
|
|
2012
|
+
`@cueplusplus/ui` carries its own copy of the derivation table, because
|
|
2013
|
+
`createTheme()` runs in a browser over a palette somebody just typed while the
|
|
2014
|
+
token build runs at publish time over DTCG files under Style Dictionary, and
|
|
2015
|
+
neither package can import the other's. So a preset you generate gets the three
|
|
2016
|
+
hairlines exactly as a shipped preset does.
|
|
2017
|
+
|
|
2018
|
+
The copy was the real hazard here, and it was already sitting in the tree: **no
|
|
2019
|
+
test compared the two lists.** A derivation added to the build alone would have
|
|
2020
|
+
been missing from every theme a consumer generated, and the only symptom would
|
|
2021
|
+
have been an unstyled rule in somebody else's app weeks later. There is now a
|
|
2022
|
+
test that compares `DERIVED_TOKEN_TEMPLATES` against the compiled stylesheet in
|
|
2023
|
+
both directions, so a one-sided edit fails in this repository instead.
|
|
2024
|
+
|
|
2025
|
+
**Nothing changes for existing consumers.** Both packages gain tokens and neither
|
|
2026
|
+
loses or moves one; no component's rendered output differs.
|
|
2027
|
+
|
|
2028
|
+
- 0cf94cb: Chip takes a third variant, `tag` — a hairline and micro type, no fill at rest
|
|
2029
|
+
|
|
2030
|
+
A list row's name is usually followed by two or three flat facts about it: what
|
|
2031
|
+
kind of thing it is, which channel it came from, what scope it lives in, whether
|
|
2032
|
+
it needs an account before it will do anything. They are not states and nobody
|
|
2033
|
+
is going to click them. Until now the only shape this system offered for them
|
|
2034
|
+
was a `Chip`, and a chip is a container — a tone tint at low alpha inside a tone
|
|
2035
|
+
rim, sized for a status readout that has to be seen from across the room. Put
|
|
2036
|
+
three on a row and repeat the row thirty times and the list stops reading as a
|
|
2037
|
+
list. It reads as ninety small buttons, and the names they were annotating come
|
|
2038
|
+
second to them.
|
|
2039
|
+
|
|
2040
|
+
`variant="tag"` is the same recipe read quietly. No fill. A `--cue-hair-strong`
|
|
2041
|
+
rim instead of `--cue-border` — the token that landed for exactly this, the rule
|
|
2042
|
+
_between_ things rather than the edge _around_ one. Micro type, and the chip's
|
|
2043
|
+
`0.15em` tracking closed to `0.06em`, because tracking cut for label-size type
|
|
2044
|
+
holds a four-letter word at micro apart far enough to read as four letters.
|
|
2045
|
+
|
|
2046
|
+
```tsx
|
|
2047
|
+
<Chip variant="tag">mcp</Chip>
|
|
2048
|
+
<Chip tone="accent" variant="tag">beta</Chip>
|
|
2049
|
+
<Chip tone="danger" variant="tag">needs auth</Chip>
|
|
2050
|
+
```
|
|
2051
|
+
|
|
2052
|
+
## What it is not
|
|
2053
|
+
|
|
2054
|
+
It is not a small chip. The height is still `h-chip` and the radius is still
|
|
2055
|
+
`--cue-radius-control`, so all three variants ride the density ladder together
|
|
2056
|
+
and a row that mixes a tag with a chip has one baseline rather than two. What
|
|
2057
|
+
changed is the volume, not the size — and that is the decision to make when
|
|
2058
|
+
choosing between them. A tag whose job is to be seen is a chip.
|
|
2059
|
+
|
|
2060
|
+
The six toned rims are unchanged at `/35` across all three variants, so a
|
|
2061
|
+
`danger` tag and a `danger` outline chip agree on what danger looks like, which
|
|
2062
|
+
is the whole point of a shared tone vocabulary. Only `neutral` moves, because
|
|
2063
|
+
the neutral line is the one that has two weights now.
|
|
2064
|
+
|
|
2065
|
+
An interactive tag brightens its rim on hover and never grows a fill. The
|
|
2066
|
+
existing tone hover rows are now keyed on the variant so that stays true; a
|
|
2067
|
+
`tinted` or `outline` chip hovers exactly as it did.
|
|
2068
|
+
|
|
2069
|
+
## The invariant that made room for it
|
|
2070
|
+
|
|
2071
|
+
The recipe already refused to emit a fill and then cancel it — that is why the
|
|
2072
|
+
tinted fills live in `compoundVariants`. Adding a third variant extended the
|
|
2073
|
+
same rule to everything else that varies: the rim, the ink, the gutter, the
|
|
2074
|
+
tracking and the type size are each emitted from exactly one place now, so no
|
|
2075
|
+
call to `chipVariants` ever produces two utilities competing for one property.
|
|
2076
|
+
|
|
2077
|
+
That matters more than it looks. `chipVariants` is exported, and a caller who
|
|
2078
|
+
uses it without `cn` gets a raw class string — where the winner is decided by
|
|
2079
|
+
the order the rules happen to sit in the stylesheet, not by which utility was
|
|
2080
|
+
written last. `tailwind-merge` would not have saved that string either: it
|
|
2081
|
+
cannot see `h-chip h-4` as a conflict at all, because `chip` is a token name and
|
|
2082
|
+
not a scale step. So the base string gave up `px`, the type size and the
|
|
2083
|
+
tracking to the `variant` map, and `neutral` gave up its rim to a compound —
|
|
2084
|
+
`--cue-border` for the two chip variants, `--cue-hair-strong` for the tag. The
|
|
2085
|
+
suite holds the invariant over the whole 7 × 3 × 2 matrix.
|
|
2086
|
+
|
|
2087
|
+
**Nothing here changes an existing chip.** Every tinted and outline chip emits
|
|
2088
|
+
the same set of utilities it did before; only which line of the recipe they come
|
|
2089
|
+
from moved. `packages/ui/manifest/**` is regenerated, not hand-edited.
|
|
2090
|
+
|
|
2091
|
+
- 2a21229: AppShell rails its navigation column, and the header cell over it moves with it
|
|
2092
|
+
|
|
2093
|
+
A console's navigation is set once and then wanted out of the way, and the shape
|
|
2094
|
+
that answers that is an icon rail: the column narrows to a strip of glyphs and
|
|
2095
|
+
the work area takes the 10rem back. `Sidebar` has been able to do the narrowing
|
|
2096
|
+
since it shipped — controlled `collapsed`, `data-collapsed`, an `sr-only` label
|
|
2097
|
+
so an icon-only row keeps its accessible name, a width transition that switches
|
|
2098
|
+
off under `prefers-reduced-motion`. What it could not do was tell anything
|
|
2099
|
+
outside itself, and the thing that needs telling is the cell in the bar directly
|
|
2100
|
+
above it.
|
|
2101
|
+
|
|
2102
|
+
That cell did not exist here at all. `TitleBar` and `AppBar` are full-width
|
|
2103
|
+
strips with leading, centre and trailing slots; neither knows the column's
|
|
2104
|
+
width, and a console with an app mark over its navigation has been hand-rolling
|
|
2105
|
+
one. Hand-rolling it is where the defect lives: the cell is a child of the bar
|
|
2106
|
+
and the column is a child of the body, two different branches of the tree, and
|
|
2107
|
+
two boxes that are meant to read as one column will drift the first time one of
|
|
2108
|
+
them changes width. A rail changes it on every click.
|
|
2109
|
+
|
|
2110
|
+
## One length, written once
|
|
2111
|
+
|
|
2112
|
+
`AppShell.Root` now writes `--cue-nav-width` on the frame — `navWidth`
|
|
2113
|
+
(`"14rem"`) while expanded, `railWidth` (`"3.5rem"`) while railed — and both
|
|
2114
|
+
boxes read it. Neither holds a width of its own, so railing rewrites one length
|
|
2115
|
+
and the cell and the column arrive together rather than a frame apart. It is
|
|
2116
|
+
also the handle a consumer's own CSS aligns to: a sticky rule, a toolbar offset,
|
|
2117
|
+
a drop shadow that should stop where the column does.
|
|
2118
|
+
|
|
2119
|
+
`3.5rem` is a `--cue-icon-md` glyph centred in a `--cue-control-md` row with a
|
|
2120
|
+
gutter either side that still reads as a column rather than a strip. It is half
|
|
2121
|
+
a rem wider than the `3rem` `Sidebar.Root` collapses to on its own, which is the
|
|
2122
|
+
one visible change to an existing console: a shell whose column is collapsed
|
|
2123
|
+
from its hairline rail is now 8px wider there. Set `railWidth="3rem"` on
|
|
2124
|
+
`AppShell.Root` to keep the old measure exactly.
|
|
2125
|
+
|
|
2126
|
+
## The new surface
|
|
2127
|
+
- **`AppShell.Root`** takes `rail` / `defaultRail` / `onRailChange` — the same
|
|
2128
|
+
controlled-prop bargain `navOpen` already makes — plus `navWidth` and
|
|
2129
|
+
`railWidth`. It stamps `data-rail` on itself and declares `group/app-shell`
|
|
2130
|
+
beside it, so anything in the frame can answer in plain Tailwind:
|
|
2131
|
+
`group-data-[rail]/app-shell:hidden` on a wordmark, the way `Sidebar` already
|
|
2132
|
+
publishes `data-collapsed` for its own parts.
|
|
2133
|
+
- **`AppShell.Bar`** is the bar divided by the navigation column: a `brand` slot
|
|
2134
|
+
exactly the column's width, wearing the column's rim, and the rest of the bar
|
|
2135
|
+
beside it. Omit `brand` and it is one undivided strip — an empty bordered cell
|
|
2136
|
+
over the column is worse than no cell. It stays a `<div>`, like `TitleBar`
|
|
2137
|
+
beside it, so a frame that also carries an `AppBar` does not end up with two
|
|
2138
|
+
`banner` landmarks; a page that wants one says `role="banner"`.
|
|
2139
|
+
- **`AppShell.Sidebar`** hands the frame's rail down to `Sidebar.Root` as its
|
|
2140
|
+
controlled `collapsed` and writes it back whenever the column's own hairline
|
|
2141
|
+
rail is pressed, so the two states cannot disagree and there is nothing to
|
|
2142
|
+
keep in sync.
|
|
2143
|
+
|
|
2144
|
+
The rail is off by default and every part of this is additive. A `collapsed` or
|
|
2145
|
+
`defaultCollapsed` passed to `AppShell.Sidebar` is the pre-rail spelling and
|
|
2146
|
+
still means exactly what it did — the column keeps its own state and its own two
|
|
2147
|
+
lengths, and the frame stays out of it rather than half-driving a column it
|
|
2148
|
+
could not have seeded, because a child cannot seed its parent's state. Reach for
|
|
2149
|
+
`defaultRail` on the frame when both boxes should move. A `width` passed to
|
|
2150
|
+
`AppShell.Sidebar` likewise still wins, for the column alone, which is a
|
|
2151
|
+
misalignment worth knowing about before writing it.
|
|
2152
|
+
|
|
2153
|
+
## What is deliberately not here
|
|
2154
|
+
|
|
2155
|
+
**The toggle.** `Sidebar.Rail` — the 4px hit strip the shell already draws on
|
|
2156
|
+
the column's edge — rails the frame now, and any other control is one you render
|
|
2157
|
+
and wire, because where a rail button lives is a decision about your bar. The
|
|
2158
|
+
guidance ships the pattern: an `IconButton` with `aria-pressed` rather than
|
|
2159
|
+
`aria-expanded` (the column is still there either way, so it is a two-state
|
|
2160
|
+
toggle and not a disclosure) and `aria-controls` pointed at the `<nav>`.
|
|
2161
|
+
|
|
2162
|
+
**Persistence.** A rail is set once and expected to be remembered, and _where_
|
|
2163
|
+
it is remembered is a decision about hydration and privacy that a layout
|
|
2164
|
+
component has no business making. Read it from a cookie on the server and the
|
|
2165
|
+
first painted frame is already railed; read it from `localStorage` in an effect
|
|
2166
|
+
and the column jumps once after hydration. That is the whole reason the choice
|
|
2167
|
+
is yours and not the shell's.
|
|
2168
|
+
|
|
2169
|
+
The `md` breakpoint now wears a third face — the header cell stops measuring
|
|
2170
|
+
itself from a column that below 48rem is not in the layout at all, which keeps
|
|
2171
|
+
14rem of a 390px bar from being walled off for nothing. It is the same
|
|
2172
|
+
navigation-shape decision the column and the drawer trigger already make, and
|
|
2173
|
+
the library's breakpoint budget is still that one decision and nothing else.
|
|
2174
|
+
|
|
2175
|
+
### Patch Changes
|
|
2176
|
+
|
|
2177
|
+
- ad9a0e9: ## One space in a `TimeField` was throwing away the server render
|
|
2178
|
+
|
|
2179
|
+
A segmented date or time field prints text that `Intl.DateTimeFormat` produced,
|
|
2180
|
+
and `Intl` answers from the ICU data of whatever is running it. Today, `en-US` at
|
|
2181
|
+
half past seven in the evening is `7:30 PM` from Node 24 (CLDR 48) and
|
|
2182
|
+
`7:30 PM` from Chrome 151 — a narrow no-break space against an ordinary one.
|
|
2183
|
+
Identical on screen. Not identical to React.
|
|
2184
|
+
|
|
2185
|
+
A hydration mismatch is not a warning. React discards the server's HTML and
|
|
2186
|
+
re-renders the whole document on the client, and that re-mounts `<html>` — which
|
|
2187
|
+
React treats as a _singleton_ and therefore strips every attribute off before
|
|
2188
|
+
re-applying the ones the server rendered. `data-theme`, `data-density`,
|
|
2189
|
+
`data-font`, `data-mode` and `color-scheme` are not among them: they are written
|
|
2190
|
+
by `prepaintScript()`, client-side, before React exists. So the document spends
|
|
2191
|
+
the whole client render wearing bare `:root` — the library's default preset, no
|
|
2192
|
+
density, no pairing — and then snaps back.
|
|
2193
|
+
|
|
2194
|
+
Measured on this design system's own kitchen sink, in headless Chrome with the
|
|
2195
|
+
cache disabled: **208 ms** with no axis on the document, 6,423 elements resolving
|
|
2196
|
+
to a monospace nobody had selected, and **40 KB** of webfont fetched for it that
|
|
2197
|
+
then painted zero glyphs — on _every_ pairing, including the two that download
|
|
2198
|
+
nothing at all.
|
|
2199
|
+
|
|
2200
|
+
The literal separators now carry their text in an element of their own, marked
|
|
2201
|
+
`suppressHydrationWarning`. React only honours that flag on the element directly
|
|
2202
|
+
containing the differing text, and React Aria's `filterDOMProps` drops it before
|
|
2203
|
+
the segment's own element sees it, which is why the extra element exists. Only
|
|
2204
|
+
literal segments are wrapped — they are `aria-hidden` and not focusable. The
|
|
2205
|
+
value segments React Aria makes `contentEditable` are untouched, and digits and
|
|
2206
|
+
`AM`/`PM` are spelled the same by every ICU there has been.
|
|
2207
|
+
|
|
2208
|
+
**If you style literal separators**, `[data-type="literal"]` still selects them
|
|
2209
|
+
and still carries the text; a child-element selector under one now finds a
|
|
2210
|
+
`<span>`.
|
|
2211
|
+
|
|
2212
|
+
## `ThemeProvider` stops waiting a commit to dress the document
|
|
2213
|
+
|
|
2214
|
+
Related, and found on the way. The `<html>` stamp used to wait for the provider's
|
|
2215
|
+
persisted preferences to be restored — a commit later — because on the first one
|
|
2216
|
+
its state is still the props, and writing those would put the app's defaults on a
|
|
2217
|
+
document the visitor had already chosen for. Waiting closed that hole by leaving
|
|
2218
|
+
another one: a subtree could be measured before the stamp landed.
|
|
2219
|
+
|
|
2220
|
+
It now writes on the first commit, from the same storage `prepaintScript()` read,
|
|
2221
|
+
and it writes in an _insertion_ effect, which React runs before any layout effect
|
|
2222
|
+
in the tree — so nothing measures the document before it is dressed. A value that
|
|
2223
|
+
is already correct is not rewritten, so the usual case is no DOM mutation at all.
|
|
2224
|
+
|
|
2225
|
+
No API moved. An app that ships the pre-paint script and the provider together,
|
|
2226
|
+
as the getting-started page has always said to, sees only that the first frame is
|
|
2227
|
+
the right one more often.
|
|
2228
|
+
|
|
2229
|
+
- Updated dependencies [85448d8]
|
|
2230
|
+
- @cueplusplus/tokens@0.4.0
|
|
2231
|
+
|
|
2232
|
+
## 0.3.0
|
|
2233
|
+
|
|
2234
|
+
### Minor Changes
|
|
2235
|
+
|
|
2236
|
+
- 31e3607: Ship the linked RequestPort preset release across tokens, UI, and brand tokens.
|
|
2237
|
+
|
|
2238
|
+
The release adds the typed RequestPort preset and its generated dark, light, and
|
|
2239
|
+
system selectors; system SSR, prepaint, provider, and portal behavior; the
|
|
2240
|
+
`persistPreferences={false}` and authoritative prepaint options contracts; the
|
|
2241
|
+
typed `fontFamilies` provider/portal override; the deterministic package
|
|
2242
|
+
manifest and companion checksums; and the accessible `TableScrollRegion`.
|
|
2243
|
+
|
|
2244
|
+
All three linked tarballs receive the same minor version and changelog entry.
|
|
2245
|
+
The Version Packages path regenerates the package manifest after applying those
|
|
2246
|
+
versions and rejects stale or nondeterministic output before publication.
|
|
2247
|
+
|
|
2248
|
+
### Patch Changes
|
|
2249
|
+
|
|
2250
|
+
- Updated dependencies [31e3607]
|
|
2251
|
+
- @cueplusplus/tokens@0.3.0
|
|
2252
|
+
|
|
3
2253
|
## 0.2.0
|
|
4
2254
|
|
|
5
2255
|
### Minor Changes
|