src/ui-system/tokens.cssProject-owned semantic aliases for the exact KitVersion.eec2cc2683Operate Scott’s portfolio cockpit while preserving its project intelligence, command layer, local privacy boundaries, and Tempo presentation system.
The audit exists, but no project-bound migration and agent plan has been compiled.
The bundle is concrete. The application boundary stays honest about generated files, collisions, product edits, evidence, validation, and rollback.
src/ui-system/tokens.cssProject-owned semantic aliases for the exact KitVersion.eec2cc2683src/ui-system/contracts.tsStable project contract IDs mapped to kit implementation names.66053710edsrc/ui-system/components.tsxDeclarative component binding boundary without product-screen imports.d0f92990f3src/ui-system/screen-recipes.tsComplete-composition ownership, screen-recipe, and legacy-style retirement contract.55752541dasrc/ui-system/kit-compatibility.tsMechanical contract and workflow completeness inspection.f8c76e1010src/ui-system/studio-adoption.jsonVersion, provenance, component, pattern, and source lock manifest.0dd37854e7src/ui-system/kits/tempo/tempo-components.tsxMaintained Tempo implementations for the v4 contracts and complete inventory-v2 Actions family.f2c408cd0asrc/ui-system/kits/tempo/tempo.cssScoped Tempo foundations, component states, responsive behavior, and reduced-motion rules.a7eedf673esrc/ui-system/kits/tempo/tempo-recipes.tsMaintained Tempo dashboard, library, document, agent, search, grouped-list, and sheet compositions.009090c30bsrc/ui-system/kits/tempo/index.tsStable Tempo package export surface for project-owned adapters.b2f4c92d58src/ui-system/kits/tempo/README.mdProject-local Tempo integration boundary and validation checklist.bb0f17e5acsrc/ui-system/tokens.cssmediumReview the package or evidence upgrade separately before replacing this protected project-owned file.
--- a/src/ui-system/tokens.css
+++ b/src/ui-system/tokens.css
@@ -1,27 +1,7 @@
-/* Tempo v4 · generated from Studio's locked semantic foundation roles. */
-:root {
- --studio-color-action-primary: #a99af0;
- --studio-color-surface-canvas: #f2f1ea;
- --studio-color-surface-card: #fffefb;
- --studio-color-text-primary: #1a1913;
- --studio-color-text-muted: #6f6d63;
- --studio-color-border-subtle: rgb(26 25 19 / 9%);
- --studio-type-family-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
- --studio-type-family-display: "Avenir Next Condensed", "Arial Narrow", "Helvetica Neue", Arial, sans-serif;
- --studio-type-size-body: 14px;
- --studio-type-size-heading: 42px;
- --studio-type-weight-regular: 400;
- --studio-type-weight-medium: 600;
- --studio-type-line-height-body: 1.5;
- --studio-space-base: 12px;
- --studio-space-section: 32px;
- --studio-shape-control-radius: 999px;
- --studio-shape-card-radius: 22px;
- --studio-size-control-height: 44px;
- --studio-density-interface: spacious;
- --studio-material-surface: editorial;
- --studio-motion-duration-fast: 120ms;
- --studio-motion-duration-standard: 200ms;
- --studio-motion-easing-standard: cubic-bezier(0.23, 1, 0.32, 1);
- --studio-motion-reduced: respect;
-}
+/* Tempo v2 · generated by Studio */
+:root {
+ --studio-color-action-primary: #a99af0;
+ --studio-density-interface: spacious;
+ --studio-material-surface: editorial;
+ --studio-shape-control-radius: 999px;
+}src/ui-system/contracts.tsmediumRun the observed project validation commands before continuing to product-screen edits.
--- a/src/ui-system/contracts.ts
+++ b/src/ui-system/contracts.ts
@@ -1,12 +1,13 @@
-export const studioContracts = {
- "action.button": "KitButton",
- "field.text": "KitField",
- "surface.card": "KitCard",
- "navigation.tabs": "KitSegmented",
- "feedback.status": "KitPill",
- "overlay.dialog": "KitAlert",
- "data.list": "KitList",
- "control.switch": "KitToggle"
-} as const;
-
-export type StudioContractId = keyof typeof studioContracts;
+/** Stable project contract surface for Tempo v2. */
+export const studioContracts = {
+ "action.button": "KitButton",
+ "control.switch": "KitToggle",
+ "data.list": "KitList",
+ "feedback.status": "KitPill",
+ "field.text": "KitField",
+ "navigation.tabs": "KitSegmented",
+ "overlay.dialog": "KitAlert",
+ "surface.card": "KitCard"
+} as const;
+export type StudioContractId = keyof typeof studioContracts;
+export type StudioComponentName = (typeof studioContracts)[StudioContractId];src/ui-system/components.tsxmediumRun the observed project validation commands before continuing to product-screen edits.
--- a/src/ui-system/components.tsx
+++ b/src/ui-system/components.tsx
@@ -1,21 +1,10 @@
-export {
- TempoAgentThinking as AgentThinking,
- TempoButton as Button,
- TempoCard as Card,
- TempoComposer as Composer,
- TempoDialog as Dialog,
- TempoField as Field,
- TempoList as List,
- TempoListItem as ListItem,
- TempoRoot as Root,
- TempoStatus as Status,
- TempoSwitch as Switch,
- TempoTabs as Tabs,
-} from "./kits/tempo";
-
-export type {
- TempoButtonProps as ButtonProps,
- TempoTabItem as TabItem,
- TempoTheme as Theme,
- TempoTone as Tone,
-} from "./kits/tempo";
+import { studioContracts, type StudioContractId } from "./contracts";
+export { KitRoot, KitButton, KitField, KitCard, KitSegmented, KitPill, KitAlert, KitList, KitToggle, KitComposer, KitAgentThinking, KitDeviceFrame } from "./kits/tempo/tempo-components";
+export { TempoAgentThinking as AgentThinking, TempoButton as Button, TempoCard as Card, TempoComposer as Composer, TempoDialog as Dialog, TempoField as Field, TempoList as List, TempoListItem as ListItem, TempoRoot as Root, TempoStatus as Status, TempoSwitch as Switch, TempoTabs as Tabs } from "./kits/tempo/tempo-components";
+export type { TempoButtonProps as ButtonProps, TempoTabItem as TabItem, TempoTheme as Theme, TempoTone as Tone } from "./kits/tempo/tempo-components";
+
+/** Declarative bindings only. Resolve these names through project-owned adapters. */
+export const studioComponentBindings: Record<StudioContractId, string> = studioContracts;
+export function studioComponentFor(contractId: StudioContractId) {
+ return studioComponentBindings[contractId];
+}src/ui-system/screen-recipes.tslowRun the observed project validation commands before continuing to product-screen edits.
--- /dev/null
+++ b/src/ui-system/screen-recipes.ts
@@ -0,0 +1,41 @@
+/** Complete-composition contract for Tempo v2. */
+export const studioAdoptionMode = "complete-composition" as const;
+export const studioScreenRecipes = [
+ "workflow.settings",
+ "workflow.resource-browser",
+ "workflow.review-approve",
+ "workflow.command-confirmation",
+ "workflow.empty-to-active",
+ "workflow.form-recovery"
+] as const;
+export const studioPresentationOwnership = {
+ "kitOwns": [
+ "tokens",
+ "typography",
+ "components",
+ "shell",
+ "screen-recipes",
+ "responsive-behavior",
+ "motion",
+ "interaction-density"
+ ],
+ "projectPreserves": [
+ "data",
+ "domain-state",
+ "api-contracts",
+ "command-actions",
+ "persistence",
+ "accessibility-semantics"
+ ],
+ "legacyStylesheetAuthority": false
+} as const;
+export const studioCompositionChecks = [
+ "legacy-styles-removed-from-runtime",
+ "kit-shell-applied",
+ "all-product-views-mapped",
+ "mobile-navigation-applied",
+ "empty-loading-error-states-mapped",
+ "responsive-viewports-proven",
+ "keyboard-and-focus-proven",
+ "human-visual-approval"
+] as const;src/ui-system/kit-compatibility.tsmediumRun the observed project validation commands before continuing to product-screen edits.
--- a/src/ui-system/kit-compatibility.ts
+++ b/src/ui-system/kit-compatibility.ts
@@ -1,16 +1,42 @@
-import { studioContracts, type StudioContractId } from "./contracts";
-
-export const requiredStudioContracts = Object.keys(studioContracts) as StudioContractId[];
-export const requiredStudioPatterns = ["workflow.dashboard", "workflow.resource-browser", "workflow.command-center"] as const;
-export const studioVisualProof = {
- status: "approved",
- requiredViewports: ["390x844", "430x932", "768x1024", "1440x900"],
- requiredChecks: ["canonical-package-hash", "typography", "spacing-and-alignment", "color-and-material", "radius-and-control-geometry", "bounded-list-behavior", "keyboard-and-focus", "reduced-motion", "reduced-transparency", "increased-contrast"],
-} as const;
-
-export function inspectStudioCompatibility() {
- const missingContracts = requiredStudioContracts.filter((id) => !studioContracts[id]);
- const structurallyCompatible = missingContracts.length === 0;
- const visuallyCompatible = (studioVisualProof.status as string) === "approved";
- return { compatible: structurallyCompatible && visuallyCompatible, structurallyCompatible, visuallyCompatible, missingContracts, requiredPatterns: requiredStudioPatterns, visualProof: studioVisualProof };
-}
+import { studioContracts, type StudioContractId } from "./contracts";
+import { studioAdoptionMode, studioCompositionChecks, studioPresentationOwnership, studioScreenRecipes } from "./screen-recipes";
+
+export const requiredStudioContracts = [
+ "action.button",
+ "control.switch",
+ "data.list",
+ "feedback.status",
+ "field.text",
+ "navigation.tabs",
+ "overlay.dialog",
+ "surface.card"
+] as const satisfies readonly StudioContractId[];
+export const requiredStudioPatterns = studioScreenRecipes;
+export const studioVisualProof = {
+ "status": "required",
+ "requiredViewports": [
+ "390x844",
+ "430x932",
+ "768x1024",
+ "1440x900"
+ ],
+ "requiredChecks": [
+ "canonical-package-hash",
+ "typography",
+ "spacing-and-alignment",
+ "color-and-material",
+ "radius-and-control-geometry",
+ "bounded-list-behavior",
+ "keyboard-and-focus",
+ "reduced-motion",
+ "reduced-transparency",
+ "increased-contrast"
+ ]
+} as const;
+export function inspectStudioCompatibility() {
+ const missingContracts = requiredStudioContracts.filter((id) => !studioContracts[id]);
+ const structurallyCompatible = missingContracts.length === 0;
+ const visuallyCompatible = (studioVisualProof.status as string) === 'approved';
+ const completeComposition = studioAdoptionMode === 'complete-composition' && studioPresentationOwnership.legacyStylesheetAuthority === false;
+ return { compatible: structurallyCompatible && visuallyCompatible && completeComposition, structurallyCompatible, visuallyCompatible, completeComposition, missingContracts, requiredPatterns: requiredStudioPatterns, compositionChecks: studioCompositionChecks, presentationOwnership: studioPresentationOwnership, visualProof: studioVisualProof };
+}src/ui-system/studio-adoption.jsonmediumReview the package or evidence upgrade separately before replacing this protected project-owned file.
--- a/src/ui-system/studio-adoption.json
+++ b/src/ui-system/studio-adoption.json
@@ -1,28 +1,88 @@
-{
- "schemaVersion": 1,
- "projectId": "projects",
- "kit": {
- "id": "kit_tempo",
- "name": "Tempo",
- "version": 4
- },
- "implementation": {
- "status": "complete-composition-approved",
- "mode": "complete-composition",
- "packageId": "tempo",
- "packageVersion": "1.0.0-rc.2",
- "sourceSha256": "7b256379176e2be38d42e9a4a8019b2a85cfdfeb890254deab224368dbc6515d"
- },
- "compatibility": {
- "status": "approved",
- "structuralCompatibilityIsNotVisualFidelity": true,
- "humanApprovalRequired": true,
- "humanApprovalSatisfied": true
- },
- "presentationOwnership": {
- "kitOwns": ["tokens", "components", "shell", "screen-recipes", "responsive-behavior", "motion", "interaction-density"],
- "projectPreserves": ["data", "domain-state", "api-contracts", "command-actions", "persistence"],
- "legacyStylesheetAuthority": false
- },
- "boundary": "No readiness or compatibility claim is valid until canonical-source, visual, responsive, keyboard, and accessibility proof are approved."
-}
+{
+ "schemaVersion": 1,
+ "adoptionMode": "complete-composition",
+ "projectId": "prj_c666a585-abca-476d-9ac3-79bcddf7aa4f",
+ "kit": {
+ "id": "kit_tempo",
+ "name": "Tempo",
+ "versionId": "ver_ffa97a66-7d8d-417a-8d23-fd4e0492a0b3",
+ "version": 2
+ },
+ "migrationPlanId": "mig_ee9d759f-3c80-4d62-a4ed-1ff93e475385",
+ "semanticRoles": [
+ "color.action.primary",
+ "shape.control.radius",
+ "density.interface",
+ "material.surface"
+ ],
+ "componentContracts": {
+ "action.button": "KitButton",
+ "control.switch": "KitToggle",
+ "data.list": "KitList",
+ "feedback.status": "KitPill",
+ "field.text": "KitField",
+ "navigation.tabs": "KitSegmented",
+ "overlay.dialog": "KitAlert",
+ "surface.card": "KitCard"
+ },
+ "patternIds": [
+ "workflow.settings",
+ "workflow.resource-browser",
+ "workflow.review-approve",
+ "workflow.command-confirmation",
+ "workflow.empty-to-active",
+ "workflow.form-recovery"
+ ],
+ "extensions": [
+ "KitComposer",
+ "KitAgentThinking",
+ "KitDeviceFrame"
+ ],
+ "capabilities": {
+ "textEffects": []
+ },
+ "implementation": {
+ "status": "productized-needs-human-proof",
+ "mode": "complete-composition",
+ "packageId": "tempo",
+ "packageVersion": "1.0.0-rc.4",
+ "sourceSha256": "7b256379176e2be38d42e9a4a8019b2a85cfdfeb890254deab224368dbc6515d",
+ "componentImport": "./kits/tempo/tempo-components",
+ "stylesheetImport": "./kits/tempo/tempo.css"
+ },
+ "compatibility": {
+ "status": "needs-visual-proof",
+ "structuralCompatibilityIsNotVisualFidelity": true,
+ "completeCompositionRequired": true,
+ "requiredViewports": [
+ "390x844",
+ "430x932",
+ "768x1024",
+ "1440x900"
+ ],
+ "humanApprovalRequired": true
+ },
+ "presentationOwnership": {
+ "kitOwns": [
+ "tokens",
+ "typography",
+ "components",
+ "shell",
+ "screen-recipes",
+ "responsive-behavior",
+ "motion",
+ "interaction-density"
+ ],
+ "projectPreserves": [
+ "data",
+ "domain-state",
+ "api-contracts",
+ "command-actions",
+ "persistence",
+ "accessibility-semantics"
+ ],
+ "legacyStylesheetAuthority": false
+ },
+ "sourceSha256": "cec18dd8c3bf8d51dfe3b43532bc80841f5368f8df00c069633dea306f343884",
+ "boundary": "Project behavior remains behind the facade; the selected kit replaces complete presentation composition and legacy styles lose runtime authority. Repository application remains separately approval-gated."
+}src/ui-system/kits/tempo/tempo-components.tsxmediumReview the package or evidence upgrade separately before replacing this protected project-owned file.
--- a/src/ui-system/kits/tempo/tempo-components.tsx
+++ b/src/ui-system/kits/tempo/tempo-components.tsx
@@ -1,488 +1,609 @@
-"use client";
-
-import {
- useEffect,
- useId,
- useRef,
- type ButtonHTMLAttributes,
- type CSSProperties,
- type HTMLAttributes,
- type InputHTMLAttributes,
- type KeyboardEvent,
- type ReactNode,
-} from "react";
-
-function cx(...values: Array<string | false | null | undefined>) {
- return values.filter(Boolean).join(" ");
-}
-
-export type TempoTheme = "light" | "dark";
-export type TempoTone = "neutral" | "accent" | "success" | "warning" | "danger";
-
-export function TempoRoot({
- theme = "light",
- accent,
- className,
- children,
- ...props
-}: HTMLAttributes<HTMLDivElement> & { theme?: TempoTheme; accent?: string }) {
- return (
- <div
- {...props}
- className={cx("tempo", className)}
- data-tempo-theme={theme}
- style={{ ...props.style, ...(accent ? { "--tempo-color-action-primary": accent } as CSSProperties : {}) }}
- >
- {children}
- </div>
- );
-}
-
-export type TempoButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
- tone?: "primary" | "tonal" | "outline" | "plain" | "danger";
- loading?: boolean;
- loadingLabel?: string;
- leading?: ReactNode;
- trailing?: ReactNode;
-};
-
-export function TempoButton({
- tone = "primary",
- loading = false,
- loadingLabel = "Loading",
- leading,
- trailing,
- className,
- disabled,
- children,
- type = "button",
- ...props
-}: TempoButtonProps) {
- return (
- <button
- {...props}
- aria-label={loading ? loadingLabel : props["aria-label"]}
- aria-busy={loading || undefined}
- className={cx("tempo-button", `tempo-button--${tone}`, className)}
- disabled={disabled || loading}
- type={type}
- >
- <span className="tempo-button__content" data-loading={loading || undefined}>
- {leading ? <span className="tempo-button__icon" aria-hidden>{leading}</span> : null}
- <span>{children}</span>
- {trailing ? <span className="tempo-button__icon" aria-hidden>{trailing}</span> : null}
- </span>
- {loading ? <span className="tempo-spinner" aria-hidden /> : null}
- </button>
- );
-}
-
-export type TempoFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
- label: string;
- description?: string;
- error?: string;
-};
-
-export function TempoField({ label, description, error, id, className, ...props }: TempoFieldProps) {
- const generatedId = useId();
- const inputId = id || `tempo-field-${generatedId}`;
- const descriptionId = description ? `${inputId}-description` : undefined;
- const errorId = error ? `${inputId}-error` : undefined;
- return (
- <div className={cx("tempo-field", error && "tempo-field--error", className)}>
- <label htmlFor={inputId}>{label}</label>
- {description ? <span className="tempo-field__description" id={descriptionId}>{description}</span> : null}
- <input
- {...props}
- aria-describedby={[descriptionId, errorId].filter(Boolean).join(" ") || undefined}
- aria-invalid={error ? true : props["aria-invalid"]}
- id={inputId}
- />
- {error ? <span className="tempo-field__error" id={errorId} role="alert">{error}</span> : null}
- </div>
- );
-}
-
-export function TempoCard({
- heading,
- eyebrow,
- footer,
- tone = "default",
- selected = false,
- disabled = false,
- className,
- children,
- ...props
-}: HTMLAttributes<HTMLDivElement> & {
- heading?: ReactNode;
- eyebrow?: ReactNode;
- footer?: ReactNode;
- tone?: "default" | "soft" | "accent";
- selected?: boolean;
- disabled?: boolean;
-}) {
- return (
- <div
- {...props}
- aria-disabled={disabled || undefined}
- className={cx("tempo-card", `tempo-card--${tone}`, selected && "is-selected", disabled && "is-disabled", className)}
- >
- {eyebrow || heading ? <header className="tempo-card__header">{eyebrow ? <span>{eyebrow}</span> : null}{heading ? <h3>{heading}</h3> : null}</header> : null}
- <div className="tempo-card__content">{children}</div>
- {footer ? <footer className="tempo-card__footer">{footer}</footer> : null}
- </div>
- );
-}
-
-export type TempoTabItem<T extends string> = { value: T; label: string; disabled?: boolean; panel?: ReactNode };
-
-export function TempoTabs<T extends string>({
- items,
- value,
- onValueChange,
- label = "Views",
- activation = "automatic",
- className,
-}: {
- items: Array<TempoTabItem<T>>;
- value: T;
- onValueChange: (value: T) => void;
- label?: string;
- activation?: "automatic" | "manual";
- className?: string;
-}) {
- const baseId = useId();
- const buttonRefs = useRef<Array<HTMLButtonElement | null>>([]);
- const selected = items.find((item) => item.value === value) || items[0];
-
- function moveFocus(event: KeyboardEvent<HTMLButtonElement>, index: number) {
- const enabled = items.map((item, itemIndex) => ({ item, itemIndex })).filter(({ item }) => !item.disabled);
- if (!enabled.length) return;
- const current = enabled.findIndex(({ itemIndex }) => itemIndex === index);
- let target = current;
- if (event.key === "ArrowRight" || event.key === "ArrowDown") target = (current + 1) % enabled.length;
- else if (event.key === "ArrowLeft" || event.key === "ArrowUp") target = (current - 1 + enabled.length) % enabled.length;
- else if (event.key === "Home") target = 0;
- else if (event.key === "End") target = enabled.length - 1;
- else if ((event.key === "Enter" || event.key === " ") && activation === "manual") {
- event.preventDefault();
- onValueChange(items[index].value);
- return;
- } else return;
- event.preventDefault();
- const next = enabled[target];
- buttonRefs.current[next.itemIndex]?.focus();
- if (activation === "automatic") onValueChange(next.item.value);
- }
-
- return (
- <div className={cx("tempo-tabs", className)}>
- <div className="tempo-tabs__list" role="tablist" aria-label={label} aria-orientation="horizontal">
- {items.map((item, index) => (
- <button
- aria-controls={`${baseId}-panel-${index}`}
- aria-selected={item.value === value}
- className={item.value === value ? "is-selected" : undefined}
- disabled={item.disabled}
- id={`${baseId}-tab-${index}`}
- key={item.value}
- onClick={() => onValueChange(item.value)}
- onKeyDown={(event) => moveFocus(event, index)}
- ref={(node) => { buttonRefs.current[index] = node; }}
- role="tab"
- tabIndex={item.value === value ? 0 : -1}
- type="button"
- >
- {item.label}
- </button>
- ))}
- </div>
- {selected?.panel !== undefined ? (
- <div
- aria-labelledby={`${baseId}-tab-${items.indexOf(selected)}`}
- className="tempo-tabs__panel"
- id={`${baseId}-panel-${items.indexOf(selected)}`}
- role="tabpanel"
- tabIndex={0}
- >
- {selected.panel}
- </div>
- ) : null}
- </div>
- );
-}
-
-export function TempoStatus({
- label,
- tone = "neutral",
- live = false,
- icon,
- className,
-}: {
- label: string;
- tone?: TempoTone | "loading";
- live?: boolean;
- icon?: ReactNode;
- className?: string;
-}) {
- return (
- <span
- aria-live={live ? "polite" : undefined}
- className={cx("tempo-status", `tempo-status--${tone}`, className)}
- role={live ? "status" : undefined}
- >
- {tone === "loading" ? <span className="tempo-spinner" aria-hidden /> : icon ? <span aria-hidden>{icon}</span> : <span className="tempo-status__dot" aria-hidden />}
- <span>{label}</span>
- </span>
- );
-}
-
-const focusableSelector = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])";
-
-export function TempoDialog({
- open,
- onOpenChange,
- title,
- description,
- children,
- actions,
- closeLabel = "Close dialog",
- busy = false,
- destructive = false,
- contained = false,
-}: {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- title: string;
- description?: string;
- children?: ReactNode;
- actions?: ReactNode;
- closeLabel?: string;
- busy?: boolean;
- destructive?: boolean;
- contained?: boolean;
-}) {
- const titleId = useId();
- const descriptionId = useId();
- const dialogRef = useRef<HTMLDivElement>(null);
- const returnFocusRef = useRef<HTMLElement | null>(null);
- const onOpenChangeRef = useRef(onOpenChange);
-
- useEffect(() => {
- onOpenChangeRef.current = onOpenChange;
- }, [onOpenChange]);
-
- useEffect(() => {
- if (!open) return;
- returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
- const dialog = dialogRef.current;
- const first = dialog?.querySelector<HTMLElement>(focusableSelector);
- first?.focus();
- const onKeyDown = (event: globalThis.KeyboardEvent) => {
- if (event.key === "Escape" && !busy) {
- event.preventDefault();
- onOpenChangeRef.current(false);
- }
- if (event.key !== "Tab" || !dialog) return;
- const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(focusableSelector));
- if (!focusable.length) return;
- const firstItem = focusable[0];
- const lastItem = focusable[focusable.length - 1];
- if (event.shiftKey && document.activeElement === firstItem) { event.preventDefault(); lastItem.focus(); }
- if (!event.shiftKey && document.activeElement === lastItem) { event.preventDefault(); firstItem.focus(); }
- };
- document.addEventListener("keydown", onKeyDown);
- const priorOverflow = document.body.style.overflow;
- if (!contained) document.body.style.overflow = "hidden";
- return () => {
- document.removeEventListener("keydown", onKeyDown);
- if (!contained) document.body.style.overflow = priorOverflow;
- returnFocusRef.current?.focus();
- };
- }, [busy, contained, open]);
-
- if (!open) return null;
- return (
- <div className={cx("tempo-dialog__backdrop", contained && "tempo-dialog__backdrop--contained")} onMouseDown={(event) => { if (event.target === event.currentTarget && !busy) onOpenChange(false); }}>
- <div
- aria-describedby={description ? descriptionId : undefined}
- aria-labelledby={titleId}
- aria-modal="true"
- className={cx("tempo-dialog", destructive && "tempo-dialog--destructive")}
- ref={dialogRef}
- role="dialog"
- >
- <button aria-label={closeLabel} className="tempo-dialog__close" disabled={busy} onClick={() => onOpenChange(false)} type="button">×</button>
- <TempoStatus label={destructive ? "Confirmation required" : busy ? "Working" : "Decision"} tone={destructive ? "danger" : busy ? "loading" : "accent"} />
- <h2 id={titleId}>{title}</h2>
- {description ? <p id={descriptionId}>{description}</p> : null}
- {children ? <div className="tempo-dialog__content">{children}</div> : null}
- {actions ? <div className="tempo-dialog__actions">{actions}</div> : null}
- </div>
- </div>
- );
-}
-
-export type TempoListState = "default" | "loading" | "empty" | "error";
-
-export function TempoList<T>({
- items,
- getKey,
- renderItem,
- state = "default",
- emptyState = "No items yet.",
- error = "Items could not be loaded.",
- label = "Items",
- className,
-}: {
- items: T[];
- getKey: (item: T) => string;
- renderItem: (item: T) => ReactNode;
- state?: TempoListState;
- emptyState?: ReactNode;
- error?: ReactNode;
- label?: string;
- className?: string;
-}) {
- const message = state === "loading" ? "Loading…" : state === "error" ? error : state === "empty" || !items.length ? emptyState : null;
- return (
- <div aria-busy={state === "loading" || undefined} aria-label={label} className={cx("tempo-list", className)} role="list">
- {message ? <div className={cx("tempo-list__message", state === "error" && "is-error")} role={state === "error" ? "alert" : "status"}>{message}</div> : null}
- {!message ? items.map((item) => <div className="tempo-list__row" key={getKey(item)} role="listitem">{renderItem(item)}</div>) : null}
- </div>
- );
-}
-
-export function TempoListItem({
- title,
- detail,
- leading,
- trailing,
- selected = false,
-}: {
- title: ReactNode;
- detail?: ReactNode;
- leading?: ReactNode;
- trailing?: ReactNode;
- selected?: boolean;
-}) {
- return (
- <div className={cx("tempo-list-item", selected && "is-selected")}>
- {leading ? <span className="tempo-list-item__leading" aria-hidden>{leading}</span> : null}
- <span className="tempo-list-item__copy"><strong>{title}</strong>{detail ? <small>{detail}</small> : null}</span>
- {trailing ? <span className="tempo-list-item__trailing">{trailing}</span> : null}
- </div>
- );
-}
-
-export function TempoSwitch({
- label,
- description,
- checked,
- onCheckedChange,
- disabled = false,
- className,
-}: {
- label: string;
- description?: string;
- checked: boolean;
- onCheckedChange: (checked: boolean) => void;
- disabled?: boolean;
- className?: string;
-}) {
- const labelId = useId();
- const descriptionId = useId();
- return (
- <div className={cx("tempo-switch", disabled && "is-disabled", className)}>
- <span className="tempo-switch__copy"><strong id={labelId}>{label}</strong>{description ? <small id={descriptionId}>{description}</small> : null}</span>
- <button
- aria-checked={checked}
- aria-describedby={description ? descriptionId : undefined}
- aria-labelledby={labelId}
- className="tempo-switch__control"
- disabled={disabled}
- onClick={() => onCheckedChange(!checked)}
- role="switch"
- type="button"
- ><span /></button>
- </div>
- );
-}
-
-export function TempoComposer({
- value,
- onValueChange,
- onSend,
- placeholder = "Describe what you want to do…",
- chips = [],
- disabled = false,
-}: {
- value: string;
- onValueChange: (value: string) => void;
- onSend: () => void;
- placeholder?: string;
- chips?: string[];
- disabled?: boolean;
-}) {
- const inputId = useId();
- return (
- <div className="tempo-composer">
- <label className="tempo-visually-hidden" htmlFor={inputId}>Command</label>
- <textarea id={inputId} value={value} onChange={(event) => onValueChange(event.target.value)} placeholder={placeholder} disabled={disabled} />
- <div className="tempo-composer__footer">
- <div className="tempo-composer__chips">{chips.map((chip) => <span key={chip}>{chip}</span>)}</div>
- <TempoButton aria-label="Send command" disabled={disabled || !value.trim()} onClick={onSend}>Send</TempoButton>
- </div>
- </div>
- );
-}
-
-export function TempoAgentThinking({ label = "Thinking", text, detail }: { label?: string; text: string; detail?: string }) {
- return <div className="tempo-thinking" role="status"><span>{label}</span><strong>{text}</strong>{detail ? <p>{detail}</p> : null}</div>;
-}
-
-export function TempoDeviceFrame({
- title,
- navigation = [],
- activeId,
- children,
-}: {
- title: string;
- navigation?: Array<{ id: string; label: string; icon?: ReactNode }>;
- activeId?: string;
- children: ReactNode;
-}) {
- return (
- <div className="tempo-device">
- <header><span aria-hidden><i /><i /><i /></span><strong>{title}</strong></header>
- <div className="tempo-device__body">
- {navigation.length ? <nav aria-label={`${title} navigation`}>{navigation.map((item) => <span className={item.id === activeId ? "is-active" : undefined} key={item.id}>{item.icon}{item.label}</span>)}</nav> : null}
- <main>{children}</main>
- </div>
- </div>
- );
-}
-
-// Compatibility names implement the stable contract props stored in Tempo v4.
-export const KitRoot = TempoRoot;
-export function KitButton({ label, icon, tone = "primary", ...props }: Omit<TempoButtonProps, "children" | "leading"> & { label: ReactNode; icon?: ReactNode }) {
- return <TempoButton {...props} leading={icon} tone={tone}>{label}</TempoButton>;
-}
-export const KitField = TempoField;
-export function KitCard({ content, heading, ...props }: Omit<Parameters<typeof TempoCard>[0], "children"> & { content: ReactNode; heading?: ReactNode }) {
- return <TempoCard {...props} heading={heading}>{content}</TempoCard>;
-}
-export function KitSegmented<T extends string>({ items, onChange, ...props }: Omit<Parameters<typeof TempoTabs<T>>[0], "items" | "onValueChange"> & { items: Array<TempoTabItem<T>>; onChange: (value: T) => void }) {
- return <TempoTabs {...props} items={items} onValueChange={onChange} />;
-}
-export function KitPill({ label, ...props }: Omit<Parameters<typeof TempoStatus>[0], "label"> & { label: string }) {
- return <TempoStatus {...props} label={label} />;
-}
-export const KitAlert = TempoDialog;
-export function KitList<T>({ selection, actions, renderItem, ...props }: Parameters<typeof TempoList<T>>[0] & { selection?: Set<string>; actions?: (item: T) => ReactNode }) {
- return <TempoList {...props} renderItem={(item) => <div className={selection?.has(props.getKey(item)) ? "is-selected" : undefined}>{renderItem(item)}{actions ? <span className="tempo-list__actions">{actions(item)}</span> : null}</div>} />;
-}
-export const KitToggle = TempoSwitch;
-export const KitComposer = TempoComposer;
-export const KitAgentThinking = TempoAgentThinking;
-export const KitDeviceFrame = TempoDeviceFrame;
+"use client";
+
+import {
+ useEffect,
+ useId,
+ useRef,
+ type AnchorHTMLAttributes,
+ type ButtonHTMLAttributes,
+ type CSSProperties,
+ type HTMLAttributes,
+ type InputHTMLAttributes,
+ type KeyboardEvent,
+ type ReactNode,
+} from "react";
+
+function cx(...values: Array<string | false | null | undefined>) {
+ return values.filter(Boolean).join(" ");
+}
+
+export type TempoTheme = "light" | "dark";
+export type TempoTone = "neutral" | "accent" | "success" | "warning" | "danger";
+
+export function TempoRoot({
+ theme = "light",
+ accent,
+ className,
+ children,
+ ...props
+}: HTMLAttributes<HTMLDivElement> & { theme?: TempoTheme; accent?: string }) {
+ return (
+ <div
+ {...props}
+ className={cx("tempo", className)}
+ data-tempo-theme={theme}
+ style={{ ...props.style, ...(accent ? { "--tempo-color-action-primary": accent } as CSSProperties : {}) }}
+ >
+ {children}
+ </div>
+ );
+}
+
+export type TempoButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
+ tone?: "primary" | "tonal" | "outline" | "plain" | "danger";
+ loading?: boolean;
+ loadingLabel?: string;
+ leading?: ReactNode;
+ trailing?: ReactNode;
+};
+
+export function TempoButton({
+ tone = "primary",
+ loading = false,
+ loadingLabel = "Loading",
+ leading,
+ trailing,
+ className,
+ disabled,
+ children,
+ type = "button",
+ ...props
+}: TempoButtonProps) {
+ return (
+ <button
+ {...props}
+ aria-label={loading ? loadingLabel : props["aria-label"]}
+ aria-busy={loading || undefined}
+ className={cx("tempo-button", `tempo-button--${tone}`, className)}
+ disabled={disabled || loading}
+ type={type}
+ >
+ <span className="tempo-button__content" data-loading={loading || undefined}>
+ {leading ? <span className="tempo-button__icon" aria-hidden>{leading}</span> : null}
+ <span>{children}</span>
+ {trailing ? <span className="tempo-button__icon" aria-hidden>{trailing}</span> : null}
+ </span>
+ {loading ? <span className="tempo-spinner" aria-hidden /> : null}
+ </button>
+ );
+}
+
+export type TempoIconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "aria-label"> & {
+ label: string;
+ tone?: "neutral" | "accent" | "danger";
+ size?: "small" | "medium" | "large";
+};
+
+export function TempoIconButton({
+ label,
+ tone = "neutral",
+ size = "medium",
+ className,
+ children,
+ type = "button",
+ ...props
+}: TempoIconButtonProps) {
+ return <button {...props} aria-label={label} className={cx("tempo-icon-button", `tempo-icon-button--${tone}`, `tempo-icon-button--${size}`, className)} type={type}><span aria-hidden>{children}</span></button>;
+}
+
+export type TempoLinkButtonProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
+ tone?: "primary" | "tonal" | "plain";
+ leading?: ReactNode;
+ trailing?: ReactNode;
+ disabled?: boolean;
+};
+
+export function TempoLinkButton({ tone = "plain", leading, trailing, disabled = false, className, children, onClick, tabIndex, ...props }: TempoLinkButtonProps) {
+ return <a {...props} aria-disabled={disabled || undefined} className={cx("tempo-link-button", `tempo-link-button--${tone}`, disabled && "is-disabled", className)} onClick={(event) => { if (disabled) { event.preventDefault(); return; } onClick?.(event); }} tabIndex={disabled ? -1 : tabIndex}>{leading ? <span aria-hidden>{leading}</span> : null}<span>{children}</span>{trailing ? <span aria-hidden>{trailing}</span> : null}</a>;
+}
+
+export function TempoFab({ label, className, children, type = "button", ...props }: Omit<TempoIconButtonProps, "tone" | "size">) {
+ return <button {...props} aria-label={label} className={cx("tempo-fab", className)} type={type}><span aria-hidden>{children}</span></button>;
+}
+
+export function TempoSplitButton({
+ label,
+ menuLabel = "More options",
+ onPrimaryAction,
+ onMenuAction,
+ leading,
+ disabled = false,
+ className,
+}: {
+ label: ReactNode;
+ menuLabel?: string;
+ onPrimaryAction: () => void;
+ onMenuAction: () => void;
+ leading?: ReactNode;
+ disabled?: boolean;
+ className?: string;
+}) {
+ return <div className={cx("tempo-split-button", className)} role="group" aria-label={typeof label === "string" ? label : "Split action"}>
+ <button disabled={disabled} onClick={onPrimaryAction} type="button">{leading ? <span aria-hidden>{leading}</span> : null}<span>{label}</span></button>
+ <button aria-label={menuLabel} disabled={disabled} onClick={onMenuAction} type="button"><span aria-hidden>⌄</span></button>
+ </div>;
+}
+
+export type TempoButtonGroupItem<T extends string> = { value: T; label: string; icon?: ReactNode; disabled?: boolean };
+
+export function TempoButtonGroup<T extends string>({
+ items,
+ value,
+ onValueChange,
+ label = "Actions",
+ className,
+}: {
+ items: Array<TempoButtonGroupItem<T>>;
+ value?: T;
+ onValueChange?: (value: T) => void;
+ label?: string;
+ className?: string;
+}) {
+ return <div aria-label={label} className={cx("tempo-button-group", className)} role="group">{items.map((item) => <button aria-pressed={value === undefined ? undefined : item.value === value} className={value === item.value ? "is-active" : undefined} disabled={item.disabled} key={item.value} onClick={() => onValueChange?.(item.value)} type="button">{item.icon ? <span aria-hidden>{item.icon}</span> : null}<span>{item.label}</span></button>)}</div>;
+}
+
+export function TempoToggleButton({
+ pressed,
+ onPressedChange,
+ leading,
+ className,
+ children,
+ type = "button",
+ ...props
+}: Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> & { pressed: boolean; onPressedChange: (pressed: boolean) => void; leading?: ReactNode }) {
+ return <button {...props} aria-pressed={pressed} className={cx("tempo-toggle-button", pressed && "is-pressed", className)} onClick={(event) => { props.onClick?.(event); if (!event.defaultPrevented) onPressedChange(!pressed); }} type={type}>{leading ? <span aria-hidden>{leading}</span> : null}<span>{children}</span></button>;
+}
+
+export type TempoFeedbackValue = "helpful" | "unhelpful" | null;
+
+export function TempoFeedbackRow({
+ value,
+ onValueChange,
+ onCopy,
+ onRegenerate,
+ copied = false,
+ label = "Response actions",
+ disabled = false,
+}: {
+ value: TempoFeedbackValue;
+ onValueChange: (value: TempoFeedbackValue) => void;
+ onCopy?: () => void;
+ onRegenerate?: () => void;
+ copied?: boolean;
+ label?: string;
+ disabled?: boolean;
+}) {
+ return <div aria-label={label} className="tempo-feedback-row" role="group">
+ <TempoIconButton aria-pressed={value === "helpful"} disabled={disabled} label="Helpful response" onClick={() => onValueChange(value === "helpful" ? null : "helpful")} tone={value === "helpful" ? "accent" : "neutral"}><span className="tempo-feedback-glyph">+</span></TempoIconButton>
+ <TempoIconButton aria-pressed={value === "unhelpful"} disabled={disabled} label="Unhelpful response" onClick={() => onValueChange(value === "unhelpful" ? null : "unhelpful")} tone={value === "unhelpful" ? "danger" : "neutral"}><span className="tempo-feedback-glyph">−</span></TempoIconButton>
+ {onCopy ? <TempoIconButton disabled={disabled} label={copied ? "Copied" : "Copy response"} onClick={onCopy}><span className="tempo-feedback-glyph">{copied ? "✓" : "⧉"}</span></TempoIconButton> : null}
+ {onRegenerate ? <TempoIconButton disabled={disabled} label="Regenerate response" onClick={onRegenerate}><span className="tempo-feedback-glyph">↻</span></TempoIconButton> : null}
+ </div>;
+}
+
+export type TempoFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
+ label: string;
+ description?: string;
+ error?: string;
+};
+
+export function TempoField({ label, description, error, id, className, ...props }: TempoFieldProps) {
+ const generatedId = useId();
+ const inputId = id || `tempo-field-${generatedId}`;
+ const descriptionId = description ? `${inputId}-description` : undefined;
+ const errorId = error ? `${inputId}-error` : undefined;
+ return (
+ <div className={cx("tempo-field", error && "tempo-field--error", className)}>
+ <label htmlFor={inputId}>{label}</label>
+ {description ? <span className="tempo-field__description" id={descriptionId}>{description}</span> : null}
+ <input
+ {...props}
+ aria-describedby={[descriptionId, errorId].filter(Boolean).join(" ") || undefined}
+ aria-invalid={error ? true : props["aria-invalid"]}
+ id={inputId}
+ />
+ {error ? <span className="tempo-field__error" id={errorId} role="alert">{error}</span> : null}
+ </div>
+ );
+}
+
+export function TempoCard({
+ heading,
+ eyebrow,
+ footer,
+ tone = "default",
+ selected = false,
+ disabled = false,
+ className,
+ children,
+ ...props
+}: HTMLAttributes<HTMLDivElement> & {
+ heading?: ReactNode;
+ eyebrow?: ReactNode;
+ footer?: ReactNode;
+ tone?: "default" | "soft" | "accent";
+ selected?: boolean;
+ disabled?: boolean;
+}) {
+ return (
+ <div
+ {...props}
+ aria-disabled={disabled || undefined}
+ className={cx("tempo-card", `tempo-card--${tone}`, selected && "is-selected", disabled && "is-disabled", className)}
+ >
+ {eyebrow || heading ? <header className="tempo-card__header">{eyebrow ? <span>{eyebrow}</span> : null}{heading ? <h3>{heading}</h3> : null}</header> : null}
+ <div className="tempo-card__content">{children}</div>
+ {footer ? <footer className="tempo-card__footer">{footer}</footer> : null}
+ </div>
+ );
+}
+
+export type TempoTabItem<T extends string> = { value: T; label: string; disabled?: boolean; panel?: ReactNode };
+
+export function TempoTabs<T extends string>({
+ items,
+ value,
+ onValueChange,
+ label = "Views",
+ activation = "automatic",
+ className,
+}: {
+ items: Array<TempoTabItem<T>>;
+ value: T;
+ onValueChange: (value: T) => void;
+ label?: string;
+ activation?: "automatic" | "manual";
+ className?: string;
+}) {
+ const baseId = useId();
+ const buttonRefs = useRef<Array<HTMLButtonElement | null>>([]);
+ const selected = items.find((item) => item.value === value) || items[0];
+
+ function moveFocus(event: KeyboardEvent<HTMLButtonElement>, index: number) {
+ const enabled = items.map((item, itemIndex) => ({ item, itemIndex })).filter(({ item }) => !item.disabled);
+ if (!enabled.length) return;
+ const current = enabled.findIndex(({ itemIndex }) => itemIndex === index);
+ let target = current;
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") target = (current + 1) % enabled.length;
+ else if (event.key === "ArrowLeft" || event.key === "ArrowUp") target = (current - 1 + enabled.length) % enabled.length;
+ else if (event.key === "Home") target = 0;
+ else if (event.key === "End") target = enabled.length - 1;
+ else if ((event.key === "Enter" || event.key === " ") && activation === "manual") {
+ event.preventDefault();
+ onValueChange(items[index].value);
+ return;
+ } else return;
+ event.preventDefault();
+ const next = enabled[target];
+ buttonRefs.current[next.itemIndex]?.focus();
+ if (activation === "automatic") onValueChange(next.item.value);
+ }
+
+ return (
+ <div className={cx("tempo-tabs", className)}>
+ <div className="tempo-tabs__list" role="tablist" aria-label={label} aria-orientation="horizontal">
+ {items.map((item, index) => (
+ <button
+ aria-controls={`${baseId}-panel-${index}`}
+ aria-selected={item.value === value}
+ className={item.value === value ? "is-selected" : undefined}
+ disabled={item.disabled}
+ id={`${baseId}-tab-${index}`}
+ key={item.value}
+ onClick={() => onValueChange(item.value)}
+ onKeyDown={(event) => moveFocus(event, index)}
+ ref={(node) => { buttonRefs.current[index] = node; }}
+ role="tab"
+ tabIndex={item.value === value ? 0 : -1}
+ type="button"
+ >
+ {item.label}
+ </button>
+ ))}
+ </div>
+ {selected?.panel !== undefined ? (
+ <div
+ aria-labelledby={`${baseId}-tab-${items.indexOf(selected)}`}
+ className="tempo-tabs__panel"
+ id={`${baseId}-panel-${items.indexOf(selected)}`}
+ role="tabpanel"
+ tabIndex={0}
+ >
+ {selected.panel}
+ </div>
+ ) : null}
+ </div>
+ );
+}
+
+export function TempoStatus({
+ label,
+ tone = "neutral",
+ live = false,
+ icon,
+ className,
+}: {
+ label: string;
+ tone?: TempoTone | "loading";
+ live?: boolean;
+ icon?: ReactNode;
+ className?: string;
+}) {
+ return (
+ <span
+ aria-live={live ? "polite" : undefined}
+ className={cx("tempo-status", `tempo-status--${tone}`, className)}
+ role={live ? "status" : undefined}
+ >
+ {tone === "loading" ? <span className="tempo-spinner" aria-hidden /> : icon ? <span aria-hidden>{icon}</span> : <span className="tempo-status__dot" aria-hidden />}
+ <span>{label}</span>
+ </span>
+ );
+}
+
+const focusableSelector = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])";
+
+export function TempoDialog({
+ open,
+ onOpenChange,
+ title,
+ description,
+ children,
+ actions,
+ closeLabel = "Close dialog",
+ busy = false,
+ destructive = false,
+ contained = false,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ title: string;
+ description?: string;
+ children?: ReactNode;
+ actions?: ReactNode;
+ closeLabel?: string;
+ busy?: boolean;
+ destructive?: boolean;
+ contained?: boolean;
+}) {
+ const titleId = useId();
+ const descriptionId = useId();
+ const dialogRef = useRef<HTMLDivElement>(null);
+ const returnFocusRef = useRef<HTMLElement | null>(null);
+ const onOpenChangeRef = useRef(onOpenChange);
+
+ useEffect(() => {
+ onOpenChangeRef.current = onOpenChange;
+ }, [onOpenChange]);
+
+ useEffect(() => {
+ if (!open) return;
+ returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
+ const dialog = dialogRef.current;
+ const first = dialog?.querySelector<HTMLElement>(focusableSelector);
+ first?.focus();
+ const onKeyDown = (event: globalThis.KeyboardEvent) => {
+ if (event.key === "Escape" && !busy) {
+ event.preventDefault();
+ onOpenChangeRef.current(false);
+ }
+ if (event.key !== "Tab" || !dialog) return;
+ const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(focusableSelector));
+ if (!focusable.length) return;
+ const firstItem = focusable[0];
+ const lastItem = focusable[focusable.length - 1];
+ if (event.shiftKey && document.activeElement === firstItem) { event.preventDefault(); lastItem.focus(); }
+ if (!event.shiftKey && document.activeElement === lastItem) { event.preventDefault(); firstItem.focus(); }
+ };
+ document.addEventListener("keydown", onKeyDown);
+ const priorOverflow = document.body.style.overflow;
+ if (!contained) document.body.style.overflow = "hidden";
+ return () => {
+ document.removeEventListener("keydown", onKeyDown);
+ if (!contained) document.body.style.overflow = priorOverflow;
+ returnFocusRef.current?.focus();
+ };
+ }, [busy, contained, open]);
+
+ if (!open) return null;
+ return (
+ <div className={cx("tempo-dialog__backdrop", contained && "tempo-dialog__backdrop--contained")} onMouseDown={(event) => { if (event.target === event.currentTarget && !busy) onOpenChange(false); }}>
+ <div
+ aria-describedby={description ? descriptionId : undefined}
+ aria-labelledby={titleId}
+ aria-modal="true"
+ className={cx("tempo-dialog", destructive && "tempo-dialog--destructive")}
+ ref={dialogRef}
+ role="dialog"
+ >
+ <button aria-label={closeLabel} className="tempo-dialog__close" disabled={busy} onClick={() => onOpenChange(false)} type="button">×</button>
+ <TempoStatus label={destructive ? "Confirmation required" : busy ? "Working" : "Decision"} tone={destructive ? "danger" : busy ? "loading" : "accent"} />
+ <h2 id={titleId}>{title}</h2>
+ {description ? <p id={descriptionId}>{description}</p> : null}
+ {children ? <div className="tempo-dialog__content">{children}</div> : null}
+ {actions ? <div className="tempo-dialog__actions">{actions}</div> : null}
+ </div>
+ </div>
+ );
+}
+
+export type TempoListState = "default" | "loading" | "empty" | "error";
+
+export function TempoList<T>({
+ items,
+ getKey,
+ renderItem,
+ state = "default",
+ emptyState = "No items yet.",
+ error = "Items could not be loaded.",
+ label = "Items",
+ className,
+}: {
+ items: T[];
+ getKey: (item: T) => string;
+ renderItem: (item: T) => ReactNode;
+ state?: TempoListState;
+ emptyState?: ReactNode;
+ error?: ReactNode;
+ label?: string;
+ className?: string;
+}) {
+ const message = state === "loading" ? "Loading…" : state === "error" ? error : state === "empty" || !items.length ? emptyState : null;
+ return (
+ <div aria-busy={state === "loading" || undefined} aria-label={label} className={cx("tempo-list", className)} role="list">
+ {message ? <div className={cx("tempo-list__message", state === "error" && "is-error")} role={state === "error" ? "alert" : "status"}>{message}</div> : null}
+ {!message ? items.map((item) => <div className="tempo-list__row" key={getKey(item)} role="listitem">{renderItem(item)}</div>) : null}
+ </div>
+ );
+}
+
+export function TempoListItem({
+ title,
+ detail,
+ leading,
+ trailing,
+ selected = false,
+}: {
+ title: ReactNode;
+ detail?: ReactNode;
+ leading?: ReactNode;
+ trailing?: ReactNode;
+ selected?: boolean;
+}) {
+ return (
+ <div className={cx("tempo-list-item", selected && "is-selected")}>
+ {leading ? <span className="tempo-list-item__leading" aria-hidden>{leading}</span> : null}
+ <span className="tempo-list-item__copy"><strong>{title}</strong>{detail ? <small>{detail}</small> : null}</span>
+ {trailing ? <span className="tempo-list-item__trailing">{trailing}</span> : null}
+ </div>
+ );
+}
+
+export function TempoSwitch({
+ label,
+ description,
+ checked,
+ onCheckedChange,
+ disabled = false,
+ className,
+}: {
+ label: string;
+ description?: string;
+ checked: boolean;
+ onCheckedChange: (checked: boolean) => void;
+ disabled?: boolean;
+ className?: string;
+}) {
+ const labelId = useId();
+ const descriptionId = useId();
+ return (
+ <div className={cx("tempo-switch", disabled && "is-disabled", className)}>
+ <span className="tempo-switch__copy"><strong id={labelId}>{label}</strong>{description ? <small id={descriptionId}>{description}</small> : null}</span>
+ <button
+ aria-checked={checked}
+ aria-describedby={description ? descriptionId : undefined}
+ aria-labelledby={labelId}
+ className="tempo-switch__control"
+ disabled={disabled}
+ onClick={() => onCheckedChange(!checked)}
+ role="switch"
+ type="button"
+ ><span /></button>
+ </div>
+ );
+}
+
+export function TempoComposer({
+ value,
+ onValueChange,
+ onSend,
+ placeholder = "Describe what you want to do…",
+ chips = [],
+ disabled = false,
+}: {
+ value: string;
+ onValueChange: (value: string) => void;
+ onSend: () => void;
+ placeholder?: string;
+ chips?: string[];
+ disabled?: boolean;
+}) {
+ const inputId = useId();
+ return (
+ <div className="tempo-composer">
+ <label className="tempo-visually-hidden" htmlFor={inputId}>Command</label>
+ <textarea id={inputId} value={value} onChange={(event) => onValueChange(event.target.value)} placeholder={placeholder} disabled={disabled} />
+ <div className="tempo-composer__footer">
+ <div className="tempo-composer__chips">{chips.map((chip) => <span key={chip}>{chip}</span>)}</div>
+ <TempoButton aria-label="Send command" disabled={disabled || !value.trim()} onClick={onSend}>Send</TempoButton>
+ </div>
+ </div>
+ );
+}
+
+export function TempoAgentThinking({ label = "Thinking", text, detail }: { label?: string; text: string; detail?: string }) {
+ return <div className="tempo-thinking" role="status"><span>{label}</span><strong>{text}</strong>{detail ? <p>{detail}</p> : null}</div>;
+}
+
+export function TempoDeviceFrame({
+ title,
+ navigation = [],
+ activeId,
+ children,
+}: {
+ title: string;
+ navigation?: Array<{ id: string; label: string; icon?: ReactNode }>;
+ activeId?: string;
+ children: ReactNode;
+}) {
+ return (
+ <div className="tempo-device">
+ <header><span aria-hidden><i /><i /><i /></span><strong>{title}</strong></header>
+ <div className="tempo-device__body">
+ {navigation.length ? <nav aria-label={`${title} navigation`}>{navigation.map((item) => <span className={item.id === activeId ? "is-active" : undefined} key={item.id}>{item.icon}{item.label}</span>)}</nav> : null}
+ <main>{children}</main>
+ </div>
+ </div>
+ );
+}
+
+// Compatibility names implement the stable contract props stored in Tempo v4.
+export const KitRoot = TempoRoot;
+export function KitButton({ label, icon, tone = "primary", ...props }: Omit<TempoButtonProps, "children" | "leading"> & { label: ReactNode; icon?: ReactNode }) {
+ return <TempoButton {...props} leading={icon} tone={tone}>{label}</TempoButton>;
+}
+export const KitField = TempoField;
+export function KitCard({ content, heading, ...props }: Omit<Parameters<typeof TempoCard>[0], "children"> & { content: ReactNode; heading?: ReactNode }) {
+ return <TempoCard {...props} heading={heading}>{content}</TempoCard>;
+}
+export function KitSegmented<T extends string>({ items, onChange, ...props }: Omit<Parameters<typeof TempoTabs<T>>[0], "items" | "onValueChange"> & { items: Array<TempoTabItem<T>>; onChange: (value: T) => void }) {
+ return <TempoTabs {...props} items={items} onValueChange={onChange} />;
+}
+export function KitPill({ label, ...props }: Omit<Parameters<typeof TempoStatus>[0], "label"> & { label: string }) {
+ return <TempoStatus {...props} label={label} />;
+}
+export const KitAlert = TempoDialog;
+export function KitList<T>({ selection, actions, renderItem, ...props }: Parameters<typeof TempoList<T>>[0] & { selection?: Set<string>; actions?: (item: T) => ReactNode }) {
+ return <TempoList {...props} renderItem={(item) => <div className={selection?.has(props.getKey(item)) ? "is-selected" : undefined}>{renderItem(item)}{actions ? <span className="tempo-list__actions">{actions(item)}</span> : null}</div>} />;
+}
+export const KitToggle = TempoSwitch;
+export const KitComposer = TempoComposer;
+export const KitAgentThinking = TempoAgentThinking;
+export const KitDeviceFrame = TempoDeviceFrame;
+export const KitIconButton = TempoIconButton;
+export const KitLinkButton = TempoLinkButton;
+export const KitFab = TempoFab;
+export const KitSplitButton = TempoSplitButton;
+export const KitButtonGroup = TempoButtonGroup;
+export const KitToggleButton = TempoToggleButton;
+export const KitFeedbackRow = TempoFeedbackRow;src/ui-system/kits/tempo/tempo.cssmediumReview the package or evidence upgrade separately before replacing this protected project-owned file.
--- a/src/ui-system/kits/tempo/tempo.css
+++ b/src/ui-system/kits/tempo/tempo.css
@@ -1,283 +1,405 @@
-.tempo {
- --tempo-color-action-primary: var(--studio-color-action-primary, #a99af0);
- --tempo-color-surface-canvas: var(--studio-color-surface-canvas, #f2f1ea);
- --tempo-color-surface-card: var(--studio-color-surface-card, #fffefb);
- --tempo-color-surface-soft: #f7f6ef;
- --tempo-color-text-primary: var(--studio-color-text-primary, #1a1913);
- --tempo-color-text-muted: var(--studio-color-text-muted, #6f6d63);
- --tempo-color-border-subtle: var(--studio-color-border-subtle, rgb(26 25 19 / 9%));
- --tempo-color-danger: #c74731;
- --tempo-color-success: #397653;
- --tempo-color-warning: #956516;
- --tempo-color-on-action: #181816;
- --tempo-type-family-sans: var(--studio-type-family-sans, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif);
- --tempo-type-family-display: var(--studio-type-family-display, "Avenir Next Condensed", "Arial Narrow", "Helvetica Neue", sans-serif);
- --tempo-type-size-body: var(--studio-type-size-body, 14px);
- --tempo-type-size-heading: var(--studio-type-size-heading, 42px);
- --tempo-type-weight-regular: var(--studio-type-weight-regular, 400);
- --tempo-type-weight-medium: var(--studio-type-weight-medium, 600);
- --tempo-type-line-height-body: var(--studio-type-line-height-body, 1.5);
- --tempo-space-base: var(--studio-space-base, 12px);
- --tempo-space-section: var(--studio-space-section, 32px);
- --tempo-shape-control-radius: var(--studio-shape-control-radius, 999px);
- --tempo-shape-card-radius: var(--studio-shape-card-radius, 22px);
- --tempo-size-control-height: var(--studio-size-control-height, 44px);
- --tempo-density-interface: var(--studio-density-interface, spacious);
- --tempo-material-surface: var(--studio-material-surface, editorial);
- --tempo-motion-fast: var(--studio-motion-duration-fast, 120ms);
- --tempo-motion-standard: var(--studio-motion-duration-standard, 200ms);
- --tempo-motion-ease: var(--studio-motion-easing-standard, cubic-bezier(0.23, 1, 0.32, 1));
- --tempo-motion-reduced: var(--studio-motion-reduced, respect);
- --tempo-shadow-card: 0 1px 3px rgb(26 25 19 / 5%), 0 8px 24px -18px rgb(26 25 19 / 25%);
- color: var(--tempo-color-text-primary);
- color-scheme: light;
- font-family: var(--tempo-type-family-sans);
- font-size: var(--tempo-type-size-body);
- line-height: var(--tempo-type-line-height-body);
- -webkit-font-smoothing: antialiased;
-}
-
-.tempo[data-tempo-theme="dark"] {
- --tempo-color-surface-canvas: #171711;
- --tempo-color-surface-card: #232219;
- --tempo-color-surface-soft: #2c2a20;
- --tempo-color-text-primary: #f3f1e8;
- --tempo-color-text-muted: #aaa597;
- --tempo-color-border-subtle: #3b392e;
- --tempo-color-danger: #f07a5a;
- --tempo-color-success: #75c493;
- --tempo-color-warning: #e8b64c;
- --tempo-color-on-action: #181816;
- --tempo-shadow-card: 0 1px 2px rgb(0 0 0 / 24%), 0 24px 60px -38px rgb(0 0 0 / 90%);
- color-scheme: dark;
-}
-
-.tempo,
-.tempo *,
-.tempo *::before,
-.tempo *::after {
- box-sizing: border-box;
-}
-
-.tempo button,
-.tempo input,
-.tempo textarea,
-.tempo select {
- font: inherit;
-}
-
-.tempo-visually-hidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-
-.tempo-button {
- position: relative;
- display: inline-grid;
- min-height: var(--tempo-size-control-height);
- place-items: center;
- overflow: hidden;
- border: 0;
- border-radius: var(--tempo-shape-control-radius);
- padding: 12px 22px;
- background: var(--tempo-color-action-primary);
- color: var(--tempo-color-on-action);
- cursor: pointer;
- font-size: 14.5px;
- font-weight: 700;
- touch-action: manipulation;
- transition: transform var(--tempo-motion-fast) var(--tempo-motion-ease), filter var(--tempo-motion-fast) ease, box-shadow var(--tempo-motion-fast) ease;
-}
-
-.tempo-button__content {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
-}
-
-.tempo-button__content[data-loading="true"] { visibility: hidden; }
-.tempo-button__icon { display: inline-grid; place-items: center; }
-.tempo-button--tonal { background: color-mix(in srgb, var(--tempo-color-action-primary) 19%, var(--tempo-color-surface-card)); color: var(--tempo-color-text-primary); }
-.tempo-button--outline { background: var(--tempo-color-surface-card); box-shadow: inset 0 0 0 1px var(--tempo-color-border-subtle); color: var(--tempo-color-text-primary); }
-.tempo-button--plain { background: transparent; color: var(--tempo-color-text-muted); padding-inline: 8px; }
-.tempo-button--danger { background: var(--tempo-color-danger); color: #fff; }
-.tempo-button:focus-visible,
-.tempo-field input:focus-visible,
-.tempo-tabs__list button:focus-visible,
-.tempo-switch__control:focus-visible,
-.tempo-dialog__close:focus-visible,
-.tempo-composer textarea:focus-visible {
- outline: 3px solid color-mix(in srgb, var(--tempo-color-action-primary) 42%, transparent);
- outline-offset: 2px;
-}
-.tempo-button:active:not(:disabled) { transform: scale(.97); }
-.tempo-button:disabled { cursor: not-allowed; filter: saturate(.35); opacity: .55; }
-
-.tempo-spinner {
- position: absolute;
- width: 16px;
- height: 16px;
- border: 2px solid currentColor;
- border-right-color: transparent;
- border-radius: 50%;
- animation: tempo-spin .7s linear infinite;
-}
-
-@keyframes tempo-spin { to { transform: rotate(360deg); } }
-
-.tempo-field { display: grid; gap: 7px; }
-.tempo-field label { color: var(--tempo-color-text-primary); font-size: 12px; font-weight: 700; }
-.tempo-field__description { color: var(--tempo-color-text-muted); font-size: 12px; }
-.tempo-field input {
- width: 100%;
- min-height: var(--tempo-size-control-height);
- border: 1px solid var(--tempo-color-border-subtle);
- border-radius: var(--tempo-shape-control-radius);
- padding: 10px 13px;
- background: var(--tempo-color-surface-card);
- color: var(--tempo-color-text-primary);
- transition: border-color var(--tempo-motion-fast) ease, box-shadow var(--tempo-motion-fast) ease;
-}
-.tempo-field input::placeholder { color: color-mix(in srgb, var(--tempo-color-text-muted) 72%, transparent); }
-.tempo-field--error input { border-color: var(--tempo-color-danger); }
-.tempo-field__error { color: var(--tempo-color-danger); font-size: 12px; font-weight: 600; }
-.tempo-field input:disabled { cursor: not-allowed; opacity: .55; }
-
-.tempo-card {
- overflow: hidden;
- border: 1px solid var(--tempo-color-border-subtle);
- border-radius: var(--tempo-shape-card-radius);
- background: var(--tempo-color-surface-card);
- box-shadow: var(--tempo-shadow-card);
-}
-.tempo-card--soft { background: var(--tempo-color-surface-soft); box-shadow: none; }
-.tempo-card--accent { background: color-mix(in srgb, var(--tempo-color-action-primary) 14%, var(--tempo-color-surface-card)); }
-.tempo-card.is-selected { box-shadow: inset 0 0 0 2px var(--tempo-color-action-primary), var(--tempo-shadow-card); }
-.tempo-card.is-disabled { opacity: .55; }
-.tempo-card__header { padding: 18px 18px 0; }
-.tempo-card__header > span { color: var(--tempo-color-text-muted); font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
-.tempo-card__header h3 { margin: 6px 0 0; font-family: var(--tempo-type-family-display); font-size: 20px; font-weight: 900; letter-spacing: -.01em; line-height: 1; text-transform: uppercase; }
-.tempo-card__content { padding: 18px; }
-.tempo-card__header + .tempo-card__content { padding-top: 12px; }
-.tempo-card__footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--tempo-color-border-subtle); padding: 12px 18px; }
-
-.tempo-tabs__list { display: inline-flex; max-width: 100%; gap: 3px; overflow-x: auto; border-radius: var(--tempo-shape-control-radius); padding: 3px; background: var(--tempo-color-surface-soft); scrollbar-width: none; }
-.tempo-tabs__list::-webkit-scrollbar { display: none; }
-.tempo-tabs__list button { min-height: 38px; flex: 0 0 auto; border: 0; border-radius: calc(var(--tempo-shape-control-radius) - 3px); padding: 8px 14px; background: transparent; color: var(--tempo-color-text-muted); cursor: pointer; font-weight: 600; }
-.tempo-tabs__list button.is-selected { background: var(--tempo-color-surface-card); color: var(--tempo-color-text-primary); box-shadow: 0 1px 3px rgb(24 24 22 / 10%); }
-.tempo-tabs__list button:disabled { cursor: not-allowed; opacity: .45; }
-.tempo-tabs__panel { margin-top: var(--tempo-space-base); }
-
-.tempo-status { display: inline-flex; min-height: 28px; align-items: center; gap: 7px; border-radius: 999px; padding: 5px 10px; background: var(--tempo-color-surface-soft); color: var(--tempo-color-text-primary); font-size: 12px; font-weight: 700; }
-.tempo-status__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
-.tempo-status--accent { background: color-mix(in srgb, var(--tempo-color-action-primary) 20%, var(--tempo-color-surface-card)); }
-.tempo-status--success { background: color-mix(in srgb, var(--tempo-color-success) 14%, var(--tempo-color-surface-card)); color: var(--tempo-color-success); }
-.tempo-status--warning { background: color-mix(in srgb, var(--tempo-color-warning) 15%, var(--tempo-color-surface-card)); color: var(--tempo-color-warning); }
-.tempo-status--danger { background: color-mix(in srgb, var(--tempo-color-danger) 13%, var(--tempo-color-surface-card)); color: var(--tempo-color-danger); }
-.tempo-status--loading { position: relative; padding-left: 32px; }
-.tempo-status--loading .tempo-spinner { left: 10px; width: 13px; height: 13px; }
-
-.tempo-dialog__backdrop { position: fixed; z-index: 1000; inset: 0; display: grid; place-items: center; padding: 20px; background: rgb(24 24 22 / 48%); backdrop-filter: blur(8px); }
-.tempo-dialog__backdrop--contained { position: absolute; z-index: 5; }
-.tempo-dialog { position: relative; width: min(100%, 460px); max-height: min(680px, calc(100dvh - 40px)); overflow: auto; border: 1px solid var(--tempo-color-border-subtle); border-radius: var(--tempo-shape-card-radius); padding: 22px; background: var(--tempo-color-surface-card); box-shadow: 0 32px 100px -30px rgb(0 0 0 / 58%); color: var(--tempo-color-text-primary); transform-origin: center; animation: tempo-dialog-in var(--tempo-motion-standard) var(--tempo-motion-ease); }
-.tempo-dialog--destructive { box-shadow: inset 0 3px 0 var(--tempo-color-danger), 0 32px 100px -30px rgb(0 0 0 / 58%); }
-.tempo-dialog__close { position: absolute; top: 14px; right: 14px; display: grid; width: 38px; height: 38px; place-items: center; border: 0; border-radius: 50%; background: var(--tempo-color-surface-soft); color: var(--tempo-color-text-primary); cursor: pointer; font-size: 22px; line-height: 1; }
-.tempo-dialog h2 { max-width: calc(100% - 52px); margin: 18px 0 7px; font-family: var(--tempo-type-family-display); font-size: 26px; line-height: 1.08; }
-.tempo-dialog > p { margin: 0; color: var(--tempo-color-text-muted); }
-.tempo-dialog__content { margin-top: 18px; }
-.tempo-dialog__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 22px; }
-@keyframes tempo-dialog-in { from { opacity: 0; transform: scale(.97) translateY(6px); } }
-
-.tempo-list { overflow: hidden; border: 1px solid var(--tempo-color-border-subtle); border-radius: var(--tempo-shape-card-radius); background: var(--tempo-color-surface-card); }
-.tempo-list__row + .tempo-list__row { border-top: 1px solid var(--tempo-color-border-subtle); }
-.tempo-list__message { min-height: 84px; display: grid; place-items: center; padding: 18px; color: var(--tempo-color-text-muted); text-align: center; }
-.tempo-list__message.is-error { color: var(--tempo-color-danger); }
-.tempo-list-item { display: flex; min-height: 58px; align-items: center; gap: 12px; padding: 12px 14px; }
-.tempo-list-item.is-selected { background: color-mix(in srgb, var(--tempo-color-action-primary) 13%, transparent); }
-.tempo-list-item__leading { display: grid; flex: 0 0 32px; width: 32px; height: 32px; place-items: center; border-radius: 50%; background: color-mix(in srgb, var(--tempo-color-action-primary) 24%, var(--tempo-color-surface-card)); color: var(--tempo-color-text-primary); }
-.tempo-list-item__copy { display: grid; min-width: 0; flex: 1; gap: 2px; }
-.tempo-list-item__copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.tempo-list-item__copy small { overflow: hidden; color: var(--tempo-color-text-muted); text-overflow: ellipsis; white-space: nowrap; }
-.tempo-list-item__trailing { flex: 0 0 auto; }
-.tempo-list__actions { display: flex; flex: 0 0 auto; align-items: center; gap: 6px; }
-
-.tempo-switch { display: flex; min-height: 58px; align-items: center; gap: 14px; }
-.tempo-switch__copy { display: grid; min-width: 0; flex: 1; gap: 2px; }
-.tempo-switch__copy small { color: var(--tempo-color-text-muted); }
-.tempo-switch__control { position: relative; flex: 0 0 48px; width: 48px; height: 28px; border: 0; border-radius: 999px; padding: 3px; background: var(--tempo-color-border-subtle); cursor: pointer; transition: background-color var(--tempo-motion-fast) ease; }
-.tempo-switch__control span { display: block; width: 22px; height: 22px; border-radius: 50%; background: #fff; box-shadow: 0 2px 5px rgb(0 0 0 / 25%); transform: translateX(0); transition: transform var(--tempo-motion-fast) var(--tempo-motion-ease); }
-.tempo-switch__control[aria-checked="true"] { background: var(--tempo-color-action-primary); }
-.tempo-switch__control[aria-checked="true"] span { transform: translateX(20px); }
-.tempo-switch.is-disabled { opacity: .55; }
-
-.tempo-composer { border: 1px solid var(--tempo-color-border-subtle); border-radius: var(--tempo-shape-card-radius); padding: 14px; background: var(--tempo-color-surface-card); box-shadow: var(--tempo-shadow-card); }
-.tempo-composer textarea { display: block; width: 100%; min-height: 88px; resize: vertical; border: 0; padding: 4px; background: transparent; color: var(--tempo-color-text-primary); outline: none; }
-.tempo-composer__footer { display: flex; align-items: flex-end; gap: 10px; margin-top: 10px; }
-.tempo-composer__chips { display: flex; flex: 1; flex-wrap: wrap; gap: 6px; }
-.tempo-composer__chips span { border-radius: 999px; padding: 5px 9px; background: var(--tempo-color-surface-soft); color: var(--tempo-color-text-muted); font-size: 11px; font-weight: 600; }
-
-.tempo-thinking { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; border-radius: var(--tempo-shape-card-radius); padding: 13px 14px; background: var(--tempo-color-surface-soft); }
-.tempo-thinking > span { grid-row: 1 / span 2; align-self: start; border-radius: 999px; padding: 4px 8px; background: color-mix(in srgb, var(--tempo-color-action-primary) 22%, var(--tempo-color-surface-card)); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
-.tempo-thinking strong { font-size: 13px; }
-.tempo-thinking p { grid-column: 2; margin: 0; color: var(--tempo-color-text-muted); font-size: 12px; }
-
-.tempo-device { overflow: hidden; border: 7px solid #24231d; border-radius: 28px; background: var(--tempo-color-surface-canvas); box-shadow: 0 36px 100px -40px rgb(0 0 0 / 72%); }
-.tempo-device > header { position: relative; display: flex; min-height: 38px; align-items: center; justify-content: center; border-bottom: 1px solid var(--tempo-color-border-subtle); background: var(--tempo-color-surface-soft); }
-.tempo-device > header > span { position: absolute; left: 13px; display: flex; gap: 6px; }
-.tempo-device > header i { width: 9px; height: 9px; border-radius: 50%; background: var(--tempo-color-danger); }
-.tempo-device > header i:nth-child(2) { background: var(--tempo-color-warning); }
-.tempo-device > header i:nth-child(3) { background: var(--tempo-color-success); }
-.tempo-device > header strong { font-size: 12px; }
-.tempo-device__body { display: grid; grid-template-columns: 160px minmax(0, 1fr); min-height: 420px; }
-.tempo-device__body nav { display: flex; flex-direction: column; gap: 4px; border-right: 1px solid var(--tempo-color-border-subtle); padding: 12px; background: color-mix(in srgb, var(--tempo-color-surface-soft) 70%, transparent); }
-.tempo-device__body nav span { display: flex; min-height: 38px; align-items: center; gap: 8px; border-radius: var(--tempo-shape-control-radius); padding: 7px 10px; color: var(--tempo-color-text-muted); font-size: 12px; font-weight: 600; }
-.tempo-device__body nav span.is-active { background: color-mix(in srgb, var(--tempo-color-action-primary) 18%, transparent); color: var(--tempo-color-text-primary); }
-.tempo-device__body main { min-width: 0; padding: 18px; }
-
-.tempo-proof-root { position: relative; min-height: 280px; padding: clamp(12px, 2.5vw, 28px); border-radius: var(--tempo-shape-card-radius); background: var(--tempo-color-surface-canvas); }
-.tempo-proof-root.is-compact { min-height: 220px; padding: 10px; font-size: 12px; }
-.tempo-proof-root .tempo-card { width: min(100%, 680px); margin-inline: auto; }
-.tempo-proof-root.is-compact .tempo-card__header { padding: 12px 12px 0; }
-.tempo-proof-root.is-compact .tempo-card__header h3 { font-size: 15px; }
-.tempo-proof-root.is-compact .tempo-card__content { padding: 12px; }
-.tempo-proof-root.is-compact .tempo-card__footer { padding: 10px 12px; }
-.tempo-proof-root.is-compact .tempo-button { min-height: 38px; padding: 7px 11px; font-size: 11px; }
-.tempo-proof-copy { margin: 14px 0 0; color: var(--tempo-color-text-muted); }
-.tempo-proof-stack { display: grid; gap: 16px; }
-.tempo-proof-dialog-stage { position: relative; min-height: 360px; overflow: hidden; border-radius: var(--tempo-shape-card-radius); background: var(--tempo-color-surface-soft); }
-.tempo-proof-root.is-compact .tempo-proof-dialog-stage { min-height: 280px; }
-.tempo-proof-root.is-compact .tempo-dialog { padding: 15px; transform: scale(.86); }
-
-@media (hover: hover) and (pointer: fine) {
- .tempo-button:hover:not(:disabled) { filter: brightness(1.04); }
- .tempo-tabs__list button:hover:not(:disabled) { color: var(--tempo-color-text-primary); }
-}
-
-@media (max-width: 620px) {
- .tempo-dialog__backdrop { padding: 12px; align-items: end; }
- .tempo-dialog { width: 100%; max-height: calc(100dvh - 24px); border-radius: var(--tempo-shape-card-radius) var(--tempo-shape-card-radius) 12px 12px; }
- .tempo-dialog__actions > * { flex: 1; }
- .tempo-device__body { grid-template-columns: 1fr; }
- .tempo-device__body nav { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--tempo-color-border-subtle); }
- .tempo-device__body nav span { flex: 0 0 auto; }
- .tempo-composer__footer { align-items: stretch; flex-direction: column; }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .tempo *,
- .tempo *::before,
- .tempo *::after {
- scroll-behavior: auto !important;
- animation-duration: .01ms !important;
- animation-iteration-count: 1 !important;
- transition-duration: .01ms !important;
- }
-}
+.tempo {
+ --tempo-color-action-primary: var(--studio-color-action-primary, #a99af0);
+ --tempo-color-surface-canvas: var(--studio-color-surface-canvas, #f2f1ea);
+ --tempo-color-surface-card: var(--studio-color-surface-card, #fffefb);
+ --tempo-color-surface-soft: #f7f6ef;
+ --tempo-color-text-primary: var(--studio-color-text-primary, #1a1913);
+ --tempo-color-text-muted: var(--studio-color-text-muted, #6f6d63);
+ --tempo-color-border-subtle: var(--studio-color-border-subtle, rgb(26 25 19 / 9%));
+ --tempo-color-danger: #c74731;
+ --tempo-color-success: #397653;
+ --tempo-color-warning: #956516;
+ --tempo-color-on-action: #181816;
+ --tempo-type-family-sans: var(--studio-type-family-sans, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif);
+ --tempo-type-family-display: var(--studio-type-family-display, "Avenir Next Condensed", "Arial Narrow", "Helvetica Neue", sans-serif);
+ --tempo-type-size-body: var(--studio-type-size-body, 14px);
+ --tempo-type-size-heading: var(--studio-type-size-heading, 42px);
+ --tempo-type-weight-regular: var(--studio-type-weight-regular, 400);
+ --tempo-type-weight-medium: var(--studio-type-weight-medium, 600);
+ --tempo-type-line-height-body: var(--studio-type-line-height-body, 1.5);
+ --tempo-space-base: var(--studio-space-base, 12px);
+ --tempo-space-section: var(--studio-space-section, 32px);
+ --tempo-shape-control-radius: var(--studio-shape-control-radius, 999px);
+ --tempo-shape-card-radius: var(--studio-shape-card-radius, 22px);
+ --tempo-size-control-height: var(--studio-size-control-height, 44px);
+ --tempo-density-interface: var(--studio-density-interface, spacious);
+ --tempo-material-surface: var(--studio-material-surface, editorial);
+ --tempo-motion-fast: var(--studio-motion-duration-fast, 120ms);
+ --tempo-motion-standard: var(--studio-motion-duration-standard, 200ms);
+ --tempo-motion-ease: var(--studio-motion-easing-standard, cubic-bezier(0.23, 1, 0.32, 1));
+ --tempo-motion-reduced: var(--studio-motion-reduced, respect);
+ --tempo-shadow-card: 0 1px 3px rgb(26 25 19 / 5%), 0 8px 24px -18px rgb(26 25 19 / 25%);
+ color: var(--tempo-color-text-primary);
+ color-scheme: light;
+ font-family: var(--tempo-type-family-sans);
+ font-size: var(--tempo-type-size-body);
+ line-height: var(--tempo-type-line-height-body);
+ -webkit-font-smoothing: antialiased;
+}
+
+.tempo[data-tempo-theme="dark"] {
+ --tempo-color-surface-canvas: #171711;
+ --tempo-color-surface-card: #232219;
+ --tempo-color-surface-soft: #2c2a20;
+ --tempo-color-text-primary: #f3f1e8;
+ --tempo-color-text-muted: #aaa597;
+ --tempo-color-border-subtle: #3b392e;
+ --tempo-color-danger: #f07a5a;
+ --tempo-color-success: #75c493;
+ --tempo-color-warning: #e8b64c;
+ --tempo-color-on-action: #181816;
+ --tempo-shadow-card: 0 1px 2px rgb(0 0 0 / 24%), 0 24px 60px -38px rgb(0 0 0 / 90%);
+ color-scheme: dark;
+}
+
+.tempo,
+.tempo *,
+.tempo *::before,
+.tempo *::after {
+ box-sizing: border-box;
+}
+
+.tempo button,
+.tempo input,
+.tempo textarea,
+.tempo select {
+ font: inherit;
+}
+
+.tempo-visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.tempo-button {
+ position: relative;
+ display: inline-grid;
+ min-height: var(--tempo-size-control-height);
+ place-items: center;
+ overflow: hidden;
+ border: 0;
+ border-radius: var(--tempo-shape-control-radius);
+ padding: 12px 22px;
+ background: var(--tempo-color-action-primary);
+ color: var(--tempo-color-on-action);
+ cursor: pointer;
+ font-size: 14.5px;
+ font-weight: 700;
+ touch-action: manipulation;
+ transition: transform var(--tempo-motion-fast) var(--tempo-motion-ease), filter var(--tempo-motion-fast) ease, box-shadow var(--tempo-motion-fast) ease;
+}
+
+.tempo-button__content {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+.tempo-button__content[data-loading="true"] { visibility: hidden; }
+.tempo-button__icon { display: inline-grid; place-items: center; }
+.tempo-button--tonal { background: color-mix(in srgb, var(--tempo-color-action-primary) 19%, var(--tempo-color-surface-card)); color: var(--tempo-color-text-primary); }
+.tempo-button--outline { background: var(--tempo-color-surface-card); box-shadow: inset 0 0 0 1px var(--tempo-color-border-subtle); color: var(--tempo-color-text-primary); }
+.tempo-button--plain { background: transparent; color: var(--tempo-color-text-muted); padding-inline: 8px; }
+.tempo-button--danger { background: var(--tempo-color-danger); color: #fff; }
+.tempo-button:focus-visible,
+.tempo-icon-button:focus-visible,
+.tempo-link-button:focus-visible,
+.tempo-fab:focus-visible,
+.tempo-split-button button:focus-visible,
+.tempo-button-group button:focus-visible,
+.tempo-toggle-button:focus-visible,
+.tempo-field input:focus-visible,
+.tempo-tabs__list button:focus-visible,
+.tempo-switch__control:focus-visible,
+.tempo-dialog__close:focus-visible,
+.tempo-composer textarea:focus-visible {
+ outline: 3px solid color-mix(in srgb, var(--tempo-color-action-primary) 42%, transparent);
+ outline-offset: 2px;
+}
+.tempo-button:active:not(:disabled) { transform: scale(.97); }
+.tempo-button:disabled { cursor: not-allowed; filter: saturate(.35); opacity: .55; }
+
+.tempo-icon-button,
+.tempo-fab,
+.tempo-split-button button,
+.tempo-button-group button,
+.tempo-toggle-button {
+ touch-action: manipulation;
+ transition: transform var(--tempo-motion-fast) var(--tempo-motion-ease), filter var(--tempo-motion-fast) ease, background-color var(--tempo-motion-fast) ease, box-shadow var(--tempo-motion-fast) ease, color var(--tempo-motion-fast) ease;
+}
+.tempo-icon-button {
+ display: inline-grid;
+ width: 44px;
+ height: 44px;
+ flex: 0 0 44px;
+ place-items: center;
+ border: 0;
+ border-radius: 50%;
+ background: var(--tempo-color-surface-card);
+ box-shadow: inset 0 0 0 1px var(--tempo-color-border-subtle), var(--tempo-shadow-card);
+ color: var(--tempo-color-text-primary);
+ cursor: pointer;
+}
+.tempo-icon-button > span, .tempo-fab > span { display: inline-grid; place-items: center; }
+.tempo-icon-button--small { width: 44px; height: 44px; flex-basis: 44px; }
+.tempo-icon-button--large { width: 52px; height: 52px; flex-basis: 52px; }
+.tempo-icon-button--accent { background: color-mix(in srgb, var(--tempo-color-action-primary) 24%, var(--tempo-color-surface-card)); color: var(--tempo-color-text-primary); }
+.tempo-icon-button--danger { background: color-mix(in srgb, var(--tempo-color-danger) 14%, var(--tempo-color-surface-card)); color: var(--tempo-color-danger); }
+.tempo-icon-button:active:not(:disabled), .tempo-fab:active:not(:disabled), .tempo-toggle-button:active:not(:disabled) { transform: scale(.94); }
+.tempo-icon-button:disabled, .tempo-fab:disabled, .tempo-split-button button:disabled, .tempo-button-group button:disabled, .tempo-toggle-button:disabled { cursor: not-allowed; filter: saturate(.35); opacity: .5; }
+
+.tempo-link-button {
+ display: inline-flex;
+ min-height: var(--tempo-size-control-height);
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ border-radius: var(--tempo-shape-control-radius);
+ padding: 10px 14px;
+ color: var(--tempo-color-text-primary);
+ font-weight: 700;
+ text-decoration: none;
+ touch-action: manipulation;
+ transition: transform var(--tempo-motion-fast) var(--tempo-motion-ease), background-color var(--tempo-motion-fast) ease, color var(--tempo-motion-fast) ease;
+}
+.tempo-link-button > span { display: inline-grid; place-items: center; }
+.tempo-link-button--primary { background: var(--tempo-color-action-primary); color: var(--tempo-color-on-action); }
+.tempo-link-button--tonal { background: color-mix(in srgb, var(--tempo-color-action-primary) 19%, var(--tempo-color-surface-card)); }
+.tempo-link-button--plain { color: var(--tempo-color-text-muted); }
+.tempo-link-button:active { transform: translateY(1px); }
+.tempo-link-button.is-disabled { cursor: not-allowed; filter: saturate(.35); opacity: .5; }
+
+.tempo-fab {
+ display: inline-grid;
+ width: 56px;
+ height: 56px;
+ place-items: center;
+ border: 0;
+ border-radius: 19px;
+ background: var(--tempo-color-action-primary);
+ box-shadow: 0 16px 34px -18px color-mix(in srgb, var(--tempo-color-action-primary) 75%, #000), inset 0 1px 0 rgb(255 255 255 / 38%);
+ color: var(--tempo-color-on-action);
+ cursor: pointer;
+}
+
+.tempo-split-button {
+ display: inline-flex;
+ min-height: var(--tempo-size-control-height);
+ overflow: hidden;
+ border-radius: var(--tempo-shape-control-radius);
+ background: var(--tempo-color-action-primary);
+ box-shadow: var(--tempo-shadow-card);
+}
+.tempo-split-button button { display: inline-flex; min-height: var(--tempo-size-control-height); align-items: center; justify-content: center; gap: 8px; border: 0; padding: 10px 18px; background: transparent; color: var(--tempo-color-on-action); cursor: pointer; font-weight: 700; }
+.tempo-split-button button + button { width: 46px; padding: 0; box-shadow: inset 1px 0 0 color-mix(in srgb, var(--tempo-color-on-action) 20%, transparent); }
+.tempo-split-button button:active:not(:disabled) { background: color-mix(in srgb, var(--tempo-color-on-action) 10%, transparent); }
+
+.tempo-button-group {
+ display: inline-flex;
+ max-width: 100%;
+ gap: 3px;
+ overflow-x: auto;
+ border-radius: var(--tempo-shape-control-radius);
+ padding: 3px;
+ background: var(--tempo-color-surface-soft);
+ scrollbar-width: none;
+}
+.tempo-button-group::-webkit-scrollbar { display: none; }
+.tempo-button-group button { display: inline-flex; min-height: var(--tempo-size-control-height); flex: 0 0 auto; align-items: center; gap: 7px; border: 0; border-radius: calc(var(--tempo-shape-control-radius) - 3px); padding: 9px 14px; background: transparent; color: var(--tempo-color-text-muted); cursor: pointer; font-weight: 650; }
+.tempo-button-group button.is-active, .tempo-button-group button[aria-pressed="true"] { background: var(--tempo-color-surface-card); box-shadow: 0 1px 3px rgb(24 24 22 / 10%); color: var(--tempo-color-text-primary); }
+
+.tempo-toggle-button {
+ display: inline-flex;
+ min-height: var(--tempo-size-control-height);
+ align-items: center;
+ gap: 8px;
+ border: 1px solid var(--tempo-color-border-subtle);
+ border-radius: var(--tempo-shape-control-radius);
+ padding: 10px 15px;
+ background: var(--tempo-color-surface-card);
+ color: var(--tempo-color-text-muted);
+ cursor: pointer;
+ font-weight: 700;
+}
+.tempo-toggle-button.is-pressed, .tempo-toggle-button[aria-pressed="true"] { border-color: color-mix(in srgb, var(--tempo-color-action-primary) 48%, var(--tempo-color-border-subtle)); background: color-mix(in srgb, var(--tempo-color-action-primary) 20%, var(--tempo-color-surface-card)); color: var(--tempo-color-text-primary); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tempo-color-action-primary) 18%, transparent); }
+
+.tempo-feedback-row { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--tempo-shape-control-radius); padding: 4px; background: var(--tempo-color-surface-soft); }
+.tempo-feedback-row .tempo-icon-button { box-shadow: none; background: transparent; }
+.tempo-feedback-row .tempo-icon-button[aria-pressed="true"] { background: var(--tempo-color-surface-card); box-shadow: var(--tempo-shadow-card); }
+.tempo-feedback-glyph { font-family: var(--tempo-type-family-mono, ui-monospace, monospace); font-size: 19px; font-weight: 700; line-height: 1; }
+
+.tempo-spinner {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ border: 2px solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: tempo-spin .7s linear infinite;
+}
+
+@keyframes tempo-spin { to { transform: rotate(360deg); } }
+
+.tempo-field { display: grid; gap: 7px; }
+.tempo-field label { color: var(--tempo-color-text-primary); font-size: 12px; font-weight: 700; }
+.tempo-field__description { color: var(--tempo-color-text-muted); font-size: 12px; }
+.tempo-field input {
+ width: 100%;
+ min-height: var(--tempo-size-control-height);
+ border: 1px solid var(--tempo-color-border-subtle);
+ border-radius: var(--tempo-shape-control-radius);
+ padding: 10px 13px;
+ background: var(--tempo-color-surface-card);
+ color: var(--tempo-color-text-primary);
+ transition: border-color var(--tempo-motion-fast) ease, box-shadow var(--tempo-motion-fast) ease;
+}
+.tempo-field input::placeholder { color: color-mix(in srgb, var(--tempo-color-text-muted) 72%, transparent); }
+.tempo-field--error input { border-color: var(--tempo-color-danger); }
+.tempo-field__error { color: var(--tempo-color-danger); font-size: 12px; font-weight: 600; }
+.tempo-field input:disabled { cursor: not-allowed; opacity: .55; }
+
+.tempo-card {
+ overflow: hidden;
+ border: 1px solid var(--tempo-color-border-subtle);
+ border-radius: var(--tempo-shape-card-radius);
+ background: var(--tempo-color-surface-card);
+ box-shadow: var(--tempo-shadow-card);
+}
+.tempo-card--soft { background: var(--tempo-color-surface-soft); box-shadow: none; }
+.tempo-card--accent { background: color-mix(in srgb, var(--tempo-color-action-primary) 14%, var(--tempo-color-surface-card)); }
+.tempo-card.is-selected { box-shadow: inset 0 0 0 2px var(--tempo-color-action-primary), var(--tempo-shadow-card); }
+.tempo-card.is-disabled { opacity: .55; }
+.tempo-card__header { padding: 18px 18px 0; }
+.tempo-card__header > span { color: var(--tempo-color-text-muted); font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
+.tempo-card__header h3 { margin: 6px 0 0; font-family: var(--tempo-type-family-display); font-size: 20px; font-weight: 900; letter-spacing: -.01em; line-height: 1; text-transform: uppercase; }
+.tempo-card__content { padding: 18px; }
+.tempo-card__header + .tempo-card__content { padding-top: 12px; }
+.tempo-card__footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--tempo-color-border-subtle); padding: 12px 18px; }
+
+.tempo-tabs__list { display: inline-flex; max-width: 100%; gap: 3px; overflow-x: auto; border-radius: var(--tempo-shape-control-radius); padding: 3px; background: var(--tempo-color-surface-soft); scrollbar-width: none; }
+.tempo-tabs__list::-webkit-scrollbar { display: none; }
+.tempo-tabs__list button { min-height: 38px; flex: 0 0 auto; border: 0; border-radius: calc(var(--tempo-shape-control-radius) - 3px); padding: 8px 14px; background: transparent; color: var(--tempo-color-text-muted); cursor: pointer; font-weight: 600; }
+.tempo-tabs__list button.is-selected { background: var(--tempo-color-surface-card); color: var(--tempo-color-text-primary); box-shadow: 0 1px 3px rgb(24 24 22 / 10%); }
+.tempo-tabs__list button:disabled { cursor: not-allowed; opacity: .45; }
+.tempo-tabs__panel { margin-top: var(--tempo-space-base); }
+
+.tempo-status { display: inline-flex; min-height: 28px; align-items: center; gap: 7px; border-radius: 999px; padding: 5px 10px; background: var(--tempo-color-surface-soft); color: var(--tempo-color-text-primary); font-size: 12px; font-weight: 700; }
+.tempo-status__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
+.tempo-status--accent { background: color-mix(in srgb, var(--tempo-color-action-primary) 20%, var(--tempo-color-surface-card)); }
+.tempo-status--success { background: color-mix(in srgb, var(--tempo-color-success) 14%, var(--tempo-color-surface-card)); color: var(--tempo-color-success); }
+.tempo-status--warning { background: color-mix(in srgb, var(--tempo-color-warning) 15%, var(--tempo-color-surface-card)); color: var(--tempo-color-warning); }
+.tempo-status--danger { background: color-mix(in srgb, var(--tempo-color-danger) 13%, var(--tempo-color-surface-card)); color: var(--tempo-color-danger); }
+.tempo-status--loading { position: relative; padding-left: 32px; }
+.tempo-status--loading .tempo-spinner { left: 10px; width: 13px; height: 13px; }
+
+.tempo-dialog__backdrop { position: fixed; z-index: 1000; inset: 0; display: grid; place-items: center; padding: 20px; background: rgb(24 24 22 / 48%); backdrop-filter: blur(8px); }
+.tempo-dialog__backdrop--contained { position: absolute; z-index: 5; }
+.tempo-dialog { position: relative; width: min(100%, 460px); max-height: min(680px, calc(100dvh - 40px)); overflow: auto; border: 1px solid var(--tempo-color-border-subtle); border-radius: var(--tempo-shape-card-radius); padding: 22px; background: var(--tempo-color-surface-card); box-shadow: 0 32px 100px -30px rgb(0 0 0 / 58%); color: var(--tempo-color-text-primary); transform-origin: center; animation: tempo-dialog-in var(--tempo-motion-standard) var(--tempo-motion-ease); }
+.tempo-dialog--destructive { box-shadow: inset 0 3px 0 var(--tempo-color-danger), 0 32px 100px -30px rgb(0 0 0 / 58%); }
+.tempo-dialog__close { position: absolute; top: 14px; right: 14px; display: grid; width: 38px; height: 38px; place-items: center; border: 0; border-radius: 50%; background: var(--tempo-color-surface-soft); color: var(--tempo-color-text-primary); cursor: pointer; font-size: 22px; line-height: 1; }
+.tempo-dialog h2 { max-width: calc(100% - 52px); margin: 18px 0 7px; font-family: var(--tempo-type-family-display); font-size: 26px; line-height: 1.08; }
+.tempo-dialog > p { margin: 0; color: var(--tempo-color-text-muted); }
+.tempo-dialog__content { margin-top: 18px; }
+.tempo-dialog__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 22px; }
+@keyframes tempo-dialog-in { from { opacity: 0; transform: scale(.97) translateY(6px); } }
+
+.tempo-list { overflow: hidden; border: 1px solid var(--tempo-color-border-subtle); border-radius: var(--tempo-shape-card-radius); background: var(--tempo-color-surface-card); }
+.tempo-list__row + .tempo-list__row { border-top: 1px solid var(--tempo-color-border-subtle); }
+.tempo-list__message { min-height: 84px; display: grid; place-items: center; padding: 18px; color: var(--tempo-color-text-muted); text-align: center; }
+.tempo-list__message.is-error { color: var(--tempo-color-danger); }
+.tempo-list-item { display: flex; min-height: 58px; align-items: center; gap: 12px; padding: 12px 14px; }
+.tempo-list-item.is-selected { background: color-mix(in srgb, var(--tempo-color-action-primary) 13%, transparent); }
+.tempo-list-item__leading { display: grid; flex: 0 0 32px; width: 32px; height: 32px; place-items: center; border-radius: 50%; background: color-mix(in srgb, var(--tempo-color-action-primary) 24%, var(--tempo-color-surface-card)); color: var(--tempo-color-text-primary); }
+.tempo-list-item__copy { display: grid; min-width: 0; flex: 1; gap: 2px; }
+.tempo-list-item__copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.tempo-list-item__copy small { overflow: hidden; color: var(--tempo-color-text-muted); text-overflow: ellipsis; white-space: nowrap; }
+.tempo-list-item__trailing { flex: 0 0 auto; }
+.tempo-list__actions { display: flex; flex: 0 0 auto; align-items: center; gap: 6px; }
+
+.tempo-switch { display: flex; min-height: 58px; align-items: center; gap: 14px; }
+.tempo-switch__copy { display: grid; min-width: 0; flex: 1; gap: 2px; }
+.tempo-switch__copy small { color: var(--tempo-color-text-muted); }
+.tempo-switch__control { position: relative; flex: 0 0 48px; width: 48px; height: 28px; border: 0; border-radius: 999px; padding: 3px; background: var(--tempo-color-border-subtle); cursor: pointer; transition: background-color var(--tempo-motion-fast) ease; }
+.tempo-switch__control span { display: block; width: 22px; height: 22px; border-radius: 50%; background: #fff; box-shadow: 0 2px 5px rgb(0 0 0 / 25%); transform: translateX(0); transition: transform var(--tempo-motion-fast) var(--tempo-motion-ease); }
+.tempo-switch__control[aria-checked="true"] { background: var(--tempo-color-action-primary); }
+.tempo-switch__control[aria-checked="true"] span { transform: translateX(20px); }
+.tempo-switch.is-disabled { opacity: .55; }
+
+.tempo-composer { border: 1px solid var(--tempo-color-border-subtle); border-radius: var(--tempo-shape-card-radius); padding: 14px; background: var(--tempo-color-surface-card); box-shadow: var(--tempo-shadow-card); }
+.tempo-composer textarea { display: block; width: 100%; min-height: 88px; resize: vertical; border: 0; padding: 4px; background: transparent; color: var(--tempo-color-text-primary); outline: none; }
+.tempo-composer__footer { display: flex; align-items: flex-end; gap: 10px; margin-top: 10px; }
+.tempo-composer__chips { display: flex; flex: 1; flex-wrap: wrap; gap: 6px; }
+.tempo-composer__chips span { border-radius: 999px; padding: 5px 9px; background: var(--tempo-color-surface-soft); color: var(--tempo-color-text-muted); font-size: 11px; font-weight: 600; }
+
+.tempo-thinking { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; border-radius: var(--tempo-shape-card-radius); padding: 13px 14px; background: var(--tempo-color-surface-soft); }
+.tempo-thinking > span { grid-row: 1 / span 2; align-self: start; border-radius: 999px; padding: 4px 8px; background: color-mix(in srgb, var(--tempo-color-action-primary) 22%, var(--tempo-color-surface-card)); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
+.tempo-thinking strong { font-size: 13px; }
+.tempo-thinking p { grid-column: 2; margin: 0; color: var(--tempo-color-text-muted); font-size: 12px; }
+
+.tempo-device { overflow: hidden; border: 7px solid #24231d; border-radius: 28px; background: var(--tempo-color-surface-canvas); box-shadow: 0 36px 100px -40px rgb(0 0 0 / 72%); }
+.tempo-device > header { position: relative; display: flex; min-height: 38px; align-items: center; justify-content: center; border-bottom: 1px solid var(--tempo-color-border-subtle); background: var(--tempo-color-surface-soft); }
+.tempo-device > header > span { position: absolute; left: 13px; display: flex; gap: 6px; }
+.tempo-device > header i { width: 9px; height: 9px; border-radius: 50%; background: var(--tempo-color-danger); }
+.tempo-device > header i:nth-child(2) { background: var(--tempo-color-warning); }
+.tempo-device > header i:nth-child(3) { background: var(--tempo-color-success); }
+.tempo-device > header strong { font-size: 12px; }
+.tempo-device__body { display: grid; grid-template-columns: 160px minmax(0, 1fr); min-height: 420px; }
+.tempo-device__body nav { display: flex; flex-direction: column; gap: 4px; border-right: 1px solid var(--tempo-color-border-subtle); padding: 12px; background: color-mix(in srgb, var(--tempo-color-surface-soft) 70%, transparent); }
+.tempo-device__body nav span { display: flex; min-height: 38px; align-items: center; gap: 8px; border-radius: var(--tempo-shape-control-radius); padding: 7px 10px; color: var(--tempo-color-text-muted); font-size: 12px; font-weight: 600; }
+.tempo-device__body nav span.is-active { background: color-mix(in srgb, var(--tempo-color-action-primary) 18%, transparent); color: var(--tempo-color-text-primary); }
+.tempo-device__body main { min-width: 0; padding: 18px; }
+
+.tempo-proof-root { position: relative; min-height: 280px; padding: clamp(12px, 2.5vw, 28px); border-radius: var(--tempo-shape-card-radius); background: var(--tempo-color-surface-canvas); }
+.tempo-proof-root.is-compact { min-height: 220px; padding: 10px; font-size: 12px; }
+.tempo-proof-root .tempo-card { width: min(100%, 680px); margin-inline: auto; }
+.tempo-proof-root.is-compact .tempo-card__header { padding: 12px 12px 0; }
+.tempo-proof-root.is-compact .tempo-card__header h3 { font-size: 15px; }
+.tempo-proof-root.is-compact .tempo-card__content { padding: 12px; }
+.tempo-proof-root.is-compact .tempo-card__footer { padding: 10px 12px; }
+.tempo-proof-root.is-compact .tempo-button { min-height: 38px; padding: 7px 11px; font-size: 11px; }
+.tempo-proof-copy { margin: 14px 0 0; color: var(--tempo-color-text-muted); }
+.tempo-proof-stack { display: grid; gap: 16px; }
+.tempo-proof-dialog-stage { position: relative; min-height: 360px; overflow: hidden; border-radius: var(--tempo-shape-card-radius); background: var(--tempo-color-surface-soft); }
+.tempo-proof-root.is-compact .tempo-proof-dialog-stage { min-height: 280px; }
+.tempo-proof-root.is-compact .tempo-dialog { padding: 15px; transform: scale(.86); }
+
+@media (hover: hover) and (pointer: fine) {
+ .tempo-button:hover:not(:disabled) { filter: brightness(1.04); }
+ .tempo-icon-button:hover:not(:disabled), .tempo-fab:hover:not(:disabled) { filter: brightness(1.04); transform: translateY(-1px); }
+ .tempo-link-button:hover { background: color-mix(in srgb, var(--tempo-color-action-primary) 13%, transparent); color: var(--tempo-color-text-primary); }
+ .tempo-split-button button:hover:not(:disabled) { background: color-mix(in srgb, var(--tempo-color-on-action) 8%, transparent); }
+ .tempo-button-group button:hover:not(:disabled), .tempo-toggle-button:hover:not(:disabled) { color: var(--tempo-color-text-primary); }
+ .tempo-tabs__list button:hover:not(:disabled) { color: var(--tempo-color-text-primary); }
+}
+
+@media (max-width: 620px) {
+ .tempo-dialog__backdrop { padding: 12px; align-items: end; }
+ .tempo-dialog { width: 100%; max-height: calc(100dvh - 24px); border-radius: var(--tempo-shape-card-radius) var(--tempo-shape-card-radius) 12px 12px; }
+ .tempo-dialog__actions > * { flex: 1; }
+ .tempo-device__body { grid-template-columns: 1fr; }
+ .tempo-device__body nav { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--tempo-color-border-subtle); }
+ .tempo-device__body nav span { flex: 0 0 auto; }
+ .tempo-composer__footer { align-items: stretch; flex-direction: column; }
+ .tempo-split-button { width: 100%; }
+ .tempo-split-button button:first-child { min-width: 0; flex: 1; }
+ .tempo-feedback-row { max-width: 100%; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .tempo *,
+ .tempo *::before,
+ .tempo *::after {
+ scroll-behavior: auto !important;
+ animation-duration: .01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: .01ms !important;
+ }
+}src/ui-system/kits/tempo/tempo-recipes.tsmediumReview the package or evidence upgrade separately before replacing this protected project-owned file.
--- /dev/null
+++ b/src/ui-system/kits/tempo/tempo-recipes.ts
@@ -0,0 +1,57 @@
+export type TempoScreenRecipe = {
+ id: string;
+ purpose: string;
+ composition: readonly string[];
+ mobile: readonly string[];
+};
+
+export const tempoScreenRecipes = [
+ {
+ id: "tempo.dashboard",
+ purpose: "Decision-first overview with evidence, focus, and the next meaningful action.",
+ composition: ["compact chrome", "eyebrow + condensed display title", "bounded KPI strip", "grouped evidence", "focus cards", "attention list"],
+ mobile: ["two-column KPIs", "single-column cards", "bottom tab bar", "safe-area padding"],
+ },
+ {
+ id: "tempo.library",
+ purpose: "Browse and filter durable objects as a visual asset library.",
+ composition: ["search capsule", "segmented filters", "large tinted thumbnails", "short metadata", "compact resource actions"],
+ mobile: ["one-column assets", "full-width filters", "44px resource actions", "persistent bottom tab bar"],
+ },
+ {
+ id: "tempo.document",
+ purpose: "Inspect one object as an outcome-led working document.",
+ composition: ["compact back action", "identity + status", "outcome band", "current/build/next panels", "provenance timeline", "related objects"],
+ mobile: ["stacked identity", "wrapped resource pills", "single-column panels", "sheet editing"],
+ },
+ {
+ id: "tempo.agent",
+ purpose: "Run a bounded agent workflow with readable intelligence and proof of work.",
+ composition: ["command composer", "runtime KPIs", "intelligence document", "recommended moves", "operator envelope", "receipt list"],
+ mobile: ["full-width composer", "two-column KPIs", "stacked document and moves", "single-column runtime"],
+ },
+ {
+ id: "tempo.search",
+ purpose: "Search a corpus and return source-linked results.",
+ composition: ["prominent search composer", "result summary", "source metadata", "bounded result cards", "direct actions"],
+ mobile: ["stacked submit action", "single-column results", "wrapped metadata"],
+ },
+ {
+ id: "tempo.grouped-list",
+ purpose: "Review operational items, issues, receipts, or settings without passive dashboard clutter.",
+ composition: ["section label", "optional decision KPIs", "grouped rows", "leading state mark", "trailing action"],
+ mobile: ["reduced columns", "secondary metadata wraps or collapses", "44px rows and actions"],
+ },
+ {
+ id: "tempo.sheet",
+ purpose: "Edit or confirm one bounded decision without losing context.",
+ composition: ["blurred backdrop", "decision status", "condensed title", "clear fields", "persistent actions"],
+ mobile: ["bottom-aligned sheet", "safe-area inset", "44px close and actions", "internal scroll when needed"],
+ },
+] as const satisfies readonly TempoScreenRecipe[];
+
+export const tempoPresentationOwnership = {
+ kitOwns: ["tokens", "typography", "components", "shell", "screen-recipes", "responsive-behavior", "motion", "interaction-density"],
+ projectPreserves: ["data", "domain-state", "api-contracts", "command-actions", "persistence", "accessibility-semantics"],
+ legacyStylesheetAuthority: false,
+} as const;src/ui-system/kits/tempo/index.tsmediumReview the package or evidence upgrade separately before replacing this protected project-owned file.
--- a/src/ui-system/kits/tempo/index.ts +++ b/src/ui-system/kits/tempo/index.ts @@ -1,2 +1,2 @@ -export * from "./manifest"; -export * from "./tempo-components"; +export * from "./tempo-components"; +export * from "./tempo-recipes";
src/ui-system/kits/tempo/README.mdmediumReview the package or evidence upgrade separately before replacing this protected project-owned file.
--- /dev/null +++ b/src/ui-system/kits/tempo/README.md @@ -0,0 +1,11 @@ +# Tempo project package + +Version 1.0.0-rc.4, derived from preserved source 7b256379176e2be38d42e9a4a8019b2a85cfdfeb890254deab224368dbc6515d. + +1. Load `../../tokens.css` before `./tempo.css` in the project root or global stylesheet boundary. +2. Import stable `Kit*` contract components from `./index` through the project-owned UI facade. +3. Map every meaningful view to `tempo-recipes.ts`; Tempo owns the complete shell and screen composition while product behavior remains outside this directory. +4. Remove legacy project stylesheet authority after recipe coverage. Never layer Tempo as a cosmetic override over the old UI. +5. Run the project validation commands and inspect mobile, desktop, keyboard, reduced-motion, loading, empty, error, and disabled states. + +This directory may be replaced by a later Studio-generated Tempo package. Product screens should not deep-import its internal files.
src/App.tsxmediumPreserve data flow, event handling, routing, state, copy, and accessibility semantics; verify that no legacy presentation authority survives after the screen recipe is applied.
src/tempo-runtime.cssmediumPreserve data flow, event handling, routing, state, copy, and accessibility semantics; verify that no legacy presentation authority survives after the screen recipe is applied.
scripts/sync-workspace.mjsmediumPreserve data flow, event handling, routing, state, copy, and accessibility semantics; verify that no legacy presentation authority survives after the screen recipe is applied.
scripts/validate-ui-system.mjsmediumPreserve data flow, event handling, routing, state, copy, and accessibility semantics; verify that no legacy presentation authority survives after the screen recipe is applied.
index.htmlmediumPreserve data flow, event handling, routing, state, copy, and accessibility semantics; verify that no legacy presentation authority survives after the screen recipe is applied.
npm run typechecknpm run buildApply changes only on a new source-control branch.
Keep the preserved project snapshot and every original file hash unchanged in Studio.
Revert the application commit or close the branch if validation fails.
Do not deploy or merge until project-specific visual and behavioral review passes.
4 files committed at 8a270e5e6e9b · 2 checks passed.
Studio verified the existing production lineage and provider deployment.
dpl_8gnPhqYATi1wevGy59yGVbwzbmzFStudio will build a provider-neutral prompt from only the listed source files. Agent output returns here as draft replacement content and still requires your file-by-file approval.