feat(desktop-new): establish visual component foundation

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart
2026-09-04 21:45:07 -07:00
parent 2d4d36e086
commit ef1b407e02
93 changed files with 1707 additions and 126 deletions

View File

@@ -656,8 +656,8 @@ dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$BUZZ_TAURI_CONFIG" {{ARGS}}
# Run desktop-next in dev mode with the same local relay, services, ports, and identity as `dev`.
desktop-next-dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
# Run desktop-new in dev mode with the same local relay, services, ports, and identity as `dev`.
desktop-new-dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
@@ -711,7 +711,7 @@ desktop-next-dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
exit 1
fi
[[ -d desktop/node_modules ]] || pnpm --dir desktop install
[[ -d desktop-next/node_modules ]] || pnpm --dir desktop-next install
[[ -d desktop-new/node_modules ]] || pnpm --dir desktop-new install
cd {{desktop_dir}}
source ../scripts/instance-env.sh
# Keep instance-env's worktree-specific identifier, ports, icon, reset URL,
@@ -720,11 +720,11 @@ desktop-next-dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
const config = JSON.parse(process.env.BUZZ_TAURI_CONFIG);
config.productName = `Buzz Sessions (${process.env.BUZZ_WORKTREE_LABEL ?? "Dev"})`;
config.build.devUrl = `http://localhost:${process.env.BUZZ_VITE_PORT}${process.env.BUZZ_RESET_WEBVIEW_STATE === "1" ? "?resetDevState=1" : ""}`;
config.build.beforeDevCommand = `exec pnpm --dir ../desktop-next exec vite --port ${process.env.BUZZ_VITE_PORT} --strictPort`;
config.build.beforeDevCommand = `exec pnpm --dir ../desktop-new exec vite --port ${process.env.BUZZ_VITE_PORT} --strictPort`;
console.log(JSON.stringify(config));
')
INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.BUZZ_TAURI_CONFIG).identifier)")
echo "Starting desktop-next on Vite port ${BUZZ_VITE_PORT}, relay ${BUZZ_RELAY_URL}"
echo "Starting desktop-new on Vite port ${BUZZ_VITE_PORT}, relay ${BUZZ_RELAY_URL}"
FEATURES=(); [[ -n "{{mesh}}" ]] && FEATURES=(--features mesh-llm)
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$BUZZ_TAURI_CONFIG" {{ARGS}}

View File

@@ -1,4 +1,4 @@
# AGENTS.md — desktop-next
# AGENTS.md — desktop-new
This is a **new Buzz desktop client, built from scratch.** It is not a fork of
`desktop/` and not a migration target. The existing client stays untouched; this

View File

@@ -1,5 +1,5 @@
{
"name": "buzz-desktop-next",
"name": "buzz-desktop-new",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -15,10 +15,10 @@
"check:contrast": "node scripts/check-contrast.mjs",
"test": "vitest run src",
"test:e2e": "playwright test",
"tauri:dev": "node ../desktop/scripts/tauri-command.mjs dev --config ../desktop-next/tauri.conf.json --config ../desktop/src-tauri/tauri.dev.conf.json"
"tauri:dev": "node ../desktop/scripts/tauri-command.mjs dev --config ../desktop-new/tauri.conf.json --config ../desktop/src-tauri/tauri.dev.conf.json"
},
"dependencies": {
"@base-ui-components/react": "1.0.0-rc.0",
"@base-ui/react": "1.7.0",
"@fontsource-variable/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.3.0",
"@tabler/icons-react": "^3.46.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { ComponentsPage } from "@/features/design-system/ui/ComponentsPage";
export const Route = createFileRoute("/design/components")({
component: ComponentsPage,
});

View File

@@ -0,0 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import { ComponentDetailPage } from "@/features/design-system/ui/ComponentDetailPage";
export const Route = createFileRoute("/design/components_/$component")({
component: ComponentRoute,
});
function ComponentRoute() {
const { component } = Route.useParams();
return <ComponentDetailPage slug={component} />;
}

View File

@@ -0,0 +1,17 @@
import { COMPONENTS } from "@/shared/ui/registry";
import { COMPONENT_SPECIMENS } from "./componentSpecimens";
export function ComponentDetailPage({ slug }: { slug: string }) {
const component = COMPONENTS.find((candidate) => candidate.slug === slug);
if (!component) return null;
const Specimen = COMPONENT_SPECIMENS[component.slug];
return (
<>
<header className="component-page-heading">
<h1 className="text-title text-primary">{component.name}</h1>
</header>
{Specimen ? <Specimen /> : null}
</>
);
}

View File

@@ -0,0 +1,32 @@
import { Link } from "@tanstack/react-router";
import { COMPONENTS } from "@/shared/ui/registry";
import { COMPONENT_SPECIMENS } from "./componentSpecimens";
export function ComponentsPage() {
return (
<>
<header className="component-page-heading">
<h1 className="text-title text-primary">Components</h1>
</header>
<div className="component-overview-grid">
{COMPONENTS.map((component) => {
const Specimen = COMPONENT_SPECIMENS[component.slug];
return (
<article key={component.slug} className="component-overview-item">
<div className="component-overview-preview">
{Specimen ? <Specimen /> : null}
</div>
<Link
to="/design/components/$component"
params={{ component: component.slug }}
className="text-body text-primary"
>
{component.name}
</Link>
</article>
);
})}
</div>
</>
);
}

View File

@@ -2,6 +2,7 @@ import { Link, Outlet } from "@tanstack/react-router";
import { Fragment, type ReactNode } from "react";
import { useColorScheme } from "@/shared/theme/useColorScheme";
import { COMPONENTS } from "@/shared/ui/registry";
/** A nav entry, optionally with children shown indented beneath it. */
type NavItem = [label: string, to: string, children?: Array<[string, string]>];
@@ -31,7 +32,16 @@ const SECTIONS: Array<{ heading: string; items: NavItem[] }> = [
},
{
heading: "Components",
items: [],
items: [
[
"Overview",
"/design/components",
COMPONENTS.map((component) => [
component.name,
`/design/components/${component.slug}`,
]),
],
],
},
];

View File

@@ -0,0 +1,22 @@
type FoundationItem = {
token: string;
value: string;
use: string;
};
export function FoundationScale({ items }: { items: FoundationItem[] }) {
return (
<div className="flex flex-col">
{items.map((item) => (
<div
key={item.token}
className="grid gap-2 border-tertiary border-b py-3 last:border-b-0 sm:grid-cols-[10rem_7rem_1fr]"
>
<code className="text-mono text-primary">{item.token}</code>
<span className="text-body-sm text-tertiary">{item.value}</span>
<span className="text-body text-secondary">{item.use}</span>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { MOTION } from "@/shared/tokens/registry";
import { FoundationScale } from "./FoundationScale";
import { PageHeader, Section } from "./primitives";
export function MotionPage() {
return (
<>
<PageHeader
title="Motion"
status="forming"
intro="State changes are quick and geometry settles deliberately. Direct manipulation itself remains immediate: a panel follows the pointer with no smoothing, then uses the settling curve only after release."
/>
<Section title="Roles">
<FoundationScale
items={MOTION.map((item) => ({
token: item.token,
value: item.value,
use: item.use,
}))}
/>
</Section>
<Section
title="Rules"
description="The values are small; the constraints are the system."
>
<ul className="flex list-disc flex-col gap-2 pl-5">
{[
"Direct manipulation follows the pointer exactly. Easing begins only after release.",
"Never animate blur. Change opacity when glass enters or leaves.",
"Reduced motion makes every transition immediate without changing state.",
"Motion explains selection, entry, exit, or changed geometry; it never decorates still content.",
].map((rule) => (
<li key={rule} className="text-body text-secondary">
{rule}
</li>
))}
</ul>
</Section>
</>
);
}

View File

@@ -0,0 +1,39 @@
import { RADII } from "@/shared/tokens/registry";
import { FoundationScale } from "./FoundationScale";
import { PageHeader, Section } from "./primitives";
export function RadiusPage() {
return (
<>
<PageHeader
title="Radius"
status="forming"
intro="Four corner roles reproduce the Messages frame without turning every measured curve into a choice: dense rows, controls, major panels, and fully round identity or chrome."
/>
<Section title="Roles">
<FoundationScale
items={RADII.map((item) => ({
token: item.token,
value: item.value,
use: item.use,
}))}
/>
</Section>
<Section title="Relationship">
<div className="component-radius-demo bg-app">
{RADII.map((item) => (
<div
key={item.token}
className="component-radius-sample bg-panel text-body-sm text-secondary"
style={{ borderRadius: `var(${item.variable})` }}
>
{item.token}
</div>
))}
</div>
</Section>
</>
);
}

View File

@@ -0,0 +1,38 @@
import { SPACE, SPACE_ROLES } from "@/shared/tokens/registry";
import { FoundationScale } from "./FoundationScale";
import { PageHeader, Section } from "./primitives";
export function SpacingPage() {
return (
<>
<PageHeader
title="Spacing"
status="forming"
intro="A short six-step rhythm extracted from the Messages frame. The first roles capture only the relationships repeated by the workspace and its dense navigator; new roles arrive when another real component earns them."
/>
<Section
title="Scale"
description="Components use these steps through roles where the distance carries product meaning."
>
<FoundationScale
items={SPACE.map((item) => ({
token: `space ${item.step}`,
value: item.value,
use: item.use,
}))}
/>
</Section>
<Section title="Roles">
<FoundationScale
items={SPACE_ROLES.map((item) => ({
token: item.token,
value: item.pointsAt,
use: item.use,
}))}
/>
</Section>
</>
);
}

View File

@@ -0,0 +1,393 @@
import {
IconDots,
IconHash,
IconMessageCircle,
IconPlus,
IconSettings,
} from "@tabler/icons-react";
import type { ReactNode } from "react";
import { useState } from "react";
import { Avatar } from "@/shared/ui/Avatar";
import { Button } from "@/shared/ui/Button";
import { IconButton } from "@/shared/ui/IconButton";
import { NavigatorRow } from "@/shared/ui/NavigatorRow";
import { NavigatorSection } from "@/shared/ui/NavigatorSection";
import { PanelHeader } from "@/shared/ui/PanelHeader";
import { SearchField } from "@/shared/ui/SearchField";
import { SegmentedNavigation } from "@/shared/ui/SegmentedNavigation";
import { WorkspaceSurface } from "@/shared/ui/WorkspaceSurface";
const DESTINATIONS = [
{ value: "home", label: "Home" },
{ value: "messages", label: "Messages" },
{ value: "projects", label: "Projects" },
] as const;
type Destination = (typeof DESTINATIONS)[number]["value"];
function SpecimenFrame({
children,
backdrop = false,
}: {
children: ReactNode;
backdrop?: boolean;
}) {
return (
<div
className="component-specimen-frame"
data-backdrop={backdrop || undefined}
>
{children}
</div>
);
}
function SpecimenGroup({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<section className="component-specimen-group">
<h2 className="text-body-sm text-tertiary">{label}</h2>
<SpecimenFrame>{children}</SpecimenFrame>
</section>
);
}
function ButtonSpecimen() {
return (
<div className="component-specimen-stack">
<SpecimenGroup label="Variants">
<div className="component-specimen-row">
<Button variant="primary">Primary</Button>
<Button variant="quiet">Quiet</Button>
<Button variant="ghost">Ghost</Button>
</div>
</SpecimenGroup>
<SpecimenGroup label="Sizes">
<div className="component-specimen-row">
<Button variant="quiet" size="compact">
Compact
</Button>
<Button variant="quiet">Default</Button>
</div>
</SpecimenGroup>
<SpecimenGroup label="States">
<div className="component-specimen-row">
<Button variant="primary">Enabled</Button>
<Button variant="quiet" disabled>
Disabled
</Button>
</div>
</SpecimenGroup>
</div>
);
}
function IconButtonSpecimen() {
const icons = {
add: <IconPlus size={16} stroke={1.7} aria-hidden="true" />,
more: <IconDots size={16} stroke={1.7} aria-hidden="true" />,
settings: <IconSettings size={16} stroke={1.7} aria-hidden="true" />,
};
return (
<div className="component-specimen-stack">
<SpecimenGroup label="Variants">
<div className="component-specimen-row">
<IconButton aria-label="Quiet add" icon={icons.add} variant="quiet" />
<IconButton
aria-label="Ghost more"
icon={icons.more}
variant="ghost"
/>
<IconButton aria-label="Solid add" icon={icons.add} variant="solid" />
<IconButton
aria-label="Chrome settings"
icon={icons.settings}
variant="chrome"
/>
</div>
</SpecimenGroup>
<SpecimenGroup label="Sizes">
<div className="component-specimen-row">
<IconButton
aria-label="Compact settings"
icon={icons.settings}
size="compact"
/>
<IconButton aria-label="Default settings" icon={icons.settings} />
<IconButton
aria-label="Large settings"
icon={icons.settings}
size="large"
/>
</div>
</SpecimenGroup>
<SpecimenGroup label="States">
<div className="component-specimen-row">
<IconButton
aria-label="Enabled add"
icon={icons.add}
variant="quiet"
/>
<IconButton
aria-label="Disabled add"
icon={icons.add}
variant="quiet"
disabled
/>
</div>
</SpecimenGroup>
</div>
);
}
function AvatarSpecimen() {
return (
<div className="component-specimen-stack">
<SpecimenGroup label="Sizes">
<div className="component-specimen-row">
<Avatar
src="/design-system/morgan.png"
alt="Morgan Martin"
fallback="Morgan"
size="small"
/>
<Avatar
src="/design-system/morgan.png"
alt="Morgan Martin"
fallback="Morgan"
/>
<Avatar
src="/design-system/morgan.png"
alt="Morgan Martin"
fallback="Morgan"
size="large"
/>
</div>
</SpecimenGroup>
<SpecimenGroup label="Fallback">
<div className="component-specimen-row">
<Avatar alt="Cynthia Chen" fallback="Cynthia" size="small" />
<Avatar alt="Cynthia Chen" fallback="Cynthia" />
<Avatar alt="Cynthia Chen" fallback="Cynthia" size="large" />
</div>
</SpecimenGroup>
</div>
);
}
function WorkspaceSurfaceSpecimen() {
return (
<div className="component-specimen-stack">
<SpecimenGroup label="Panel">
<div className="component-single-surface-demo">
<WorkspaceSurface aria-label="Standalone panel example">
<PanelHeader variant="compact" title="Standalone panel" />
</WorkspaceSurface>
</div>
</SpecimenGroup>
<SpecimenGroup label="Connected edges">
<div className="component-surface-demo">
<WorkspaceSurface
as="aside"
variant="connected-right"
aria-label="Right-connected surface example"
>
<PanelHeader variant="compact" title="Navigator" />
</WorkspaceSurface>
<WorkspaceSurface
variant="connected-left"
aria-label="Left-connected surface example"
>
<PanelHeader variant="compact" title="Conversation" />
</WorkspaceSurface>
</div>
</SpecimenGroup>
</div>
);
}
function SegmentedNavigationSpecimen() {
const [destination, setDestination] = useState<Destination>("messages");
const [iconDestination, setIconDestination] = useState<Destination>("home");
const iconItems = DESTINATIONS.map((item) => ({
...item,
icon: <IconMessageCircle size={16} stroke={1.7} aria-hidden="true" />,
}));
return (
<div className="component-specimen-stack">
<section className="component-specimen-group">
<h2 className="text-body-sm text-tertiary">Labels</h2>
<SpecimenFrame backdrop>
<SegmentedNavigation
value={destination}
items={DESTINATIONS}
label="Prototype destinations"
onValueChange={setDestination}
/>
</SpecimenFrame>
</section>
<section className="component-specimen-group">
<h2 className="text-body-sm text-tertiary">
Icons and trailing action
</h2>
<SpecimenFrame backdrop>
<SegmentedNavigation
value={iconDestination}
items={iconItems}
label="Prototype destinations with icons"
onValueChange={setIconDestination}
trailingAction={
<IconButton
aria-label="Create"
icon={<IconPlus size={16} stroke={1.7} aria-hidden="true" />}
size="compact"
/>
}
/>
</SpecimenFrame>
</section>
</div>
);
}
function PanelHeaderSpecimen() {
const actions = (
<IconButton
aria-label="More conversation actions"
icon={<IconDots size={16} stroke={1.7} aria-hidden="true" />}
size="compact"
/>
);
return (
<div className="component-specimen-stack">
<SpecimenGroup label="Default">
<WorkspaceSurface aria-label="Default panel header example">
<PanelHeader
title="Conversation"
icon={
<IconMessageCircle size={16} stroke={1.7} aria-hidden="true" />
}
actions={actions}
/>
</WorkspaceSurface>
</SpecimenGroup>
<SpecimenGroup label="Compact">
<WorkspaceSurface aria-label="Compact panel header example">
<PanelHeader variant="compact" title="Thread" actions={actions} />
</WorkspaceSurface>
</SpecimenGroup>
</div>
);
}
function SearchFieldSpecimen() {
const [emptyQuery, setEmptyQuery] = useState("");
const [filledQuery, setFilledQuery] = useState("design");
return (
<div className="component-specimen-stack">
<SpecimenGroup label="Empty">
<div className="component-field-demo">
<SearchField
value={emptyQuery}
onValueChange={setEmptyQuery}
label="Find a channel"
placeholder="Search"
/>
</div>
</SpecimenGroup>
<SpecimenGroup label="Filled and clearable">
<div className="component-field-demo">
<SearchField
value={filledQuery}
onValueChange={setFilledQuery}
label="Find a channel"
placeholder="Search"
/>
</div>
</SpecimenGroup>
</div>
);
}
function NavigatorSectionSpecimen() {
return (
<div className="component-specimen-stack">
<SpecimenGroup label="With rows">
<div className="component-navigator-section-demo">
<NavigatorSection label="Pinned">
<NavigatorRow
label="buzz-design"
icon={<IconHash size={16} stroke={1.7} aria-hidden="true" />}
/>
<NavigatorRow
label="desktop-new"
icon={<IconHash size={16} stroke={1.7} aria-hidden="true" />}
/>
</NavigatorSection>
</div>
</SpecimenGroup>
<SpecimenGroup label="Adjacent sections">
<div className="component-navigator-section-demo">
<NavigatorSection label="Projects">
<NavigatorRow label="berd-main" />
</NavigatorSection>
<NavigatorSection label="Personal">
<NavigatorRow label="design-system" />
</NavigatorSection>
</div>
</SpecimenGroup>
</div>
);
}
function NavigatorRowSpecimen() {
const [selected, setSelected] = useState("buzz-design");
return (
<div className="component-specimen-stack">
<SpecimenGroup label="Default, selected, and metadata">
<div className="component-navigator-section-demo">
<NavigatorRow
label="buzz-design"
icon={<IconHash size={16} stroke={1.7} aria-hidden="true" />}
selected={selected === "buzz-design"}
onClick={() => setSelected("buzz-design")}
/>
<NavigatorRow
label="desktop-new"
icon={<IconHash size={16} stroke={1.7} aria-hidden="true" />}
trailing={<span className="text-body-sm">3</span>}
selected={selected === "desktop-new"}
onClick={() => setSelected("desktop-new")}
/>
</div>
</SpecimenGroup>
<SpecimenGroup label="Inset">
<div className="component-navigator-section-demo">
<NavigatorRow
label="Session interaction model"
icon={
<IconMessageCircle size={16} stroke={1.7} aria-hidden="true" />
}
inset
selected={selected === "session"}
onClick={() => setSelected("session")}
/>
</div>
</SpecimenGroup>
</div>
);
}
export const COMPONENT_SPECIMENS: Record<string, () => ReactNode> = {
button: ButtonSpecimen,
"icon-button": IconButtonSpecimen,
avatar: AvatarSpecimen,
"workspace-surface": WorkspaceSurfaceSpecimen,
"segmented-navigation": SegmentedNavigationSpecimen,
"panel-header": PanelHeaderSpecimen,
"search-field": SearchFieldSpecimen,
"navigator-section": NavigatorSectionSpecimen,
"navigator-row": NavigatorRowSpecimen,
};

View File

@@ -1,4 +1,4 @@
import { Accordion } from "@base-ui-components/react/accordion";
import { Accordion } from "@base-ui/react/accordion";
import {
IconAlertCircle,
IconCheck,

View File

@@ -1,4 +1,4 @@
import { Dialog } from "@base-ui-components/react/dialog";
import { Dialog } from "@base-ui/react/dialog";
import { IconPlus, IconSearch, IconUsers, IconX } from "@tabler/icons-react";
import { useRef, useState } from "react";
import { runtime } from "@/shared/runtime/client";

View File

@@ -0,0 +1,339 @@
@layer components {
.buzz-button {
display: inline-flex;
min-width: 0;
height: var(--size-control);
align-items: center;
justify-content: center;
gap: var(--space-row-gap);
border: 0;
border-radius: var(--radius-control);
padding: 0 var(--space-control-inset);
background: transparent;
color: var(--text-primary);
font: inherit;
cursor: pointer;
transition:
color var(--duration-state) var(--easing-state),
background-color var(--duration-state) var(--easing-state),
opacity var(--duration-fast) var(--easing-state),
box-shadow var(--duration-state) var(--easing-state);
}
.buzz-button[data-size="compact"] {
height: var(--size-row);
border-radius: var(--radius-row);
padding-inline: var(--space-control-inset);
}
.buzz-button[data-variant="primary"] {
background: var(--bg-accent);
color: var(--text-on-accent);
}
.buzz-button[data-variant="primary"]:hover:not([data-disabled]) {
opacity: 0.9;
}
.buzz-button[data-variant="quiet"] {
background: var(--bg-inset);
color: var(--text-primary);
}
.buzz-button[data-variant="quiet"]:hover:not([data-disabled]),
.buzz-button[data-variant="ghost"]:hover:not([data-disabled]) {
background: var(--bg-hover);
}
.buzz-button[data-disabled] {
color: var(--text-disabled);
cursor: not-allowed;
}
.buzz-button[data-icon-variant] {
width: var(--size-control);
flex: 0 0 auto;
padding: 0;
}
.buzz-button[data-icon-size="compact"] {
width: var(--size-row);
height: var(--size-row);
border-radius: var(--radius-row);
}
.buzz-button[data-icon-size="large"] {
width: 2.5rem;
height: 2.5rem;
border-radius: var(--radius-pill);
}
.buzz-button[data-icon-variant="quiet"] {
background: var(--bg-inset);
}
.buzz-button[data-icon-variant="solid"] {
background: var(--bg-accent);
color: var(--text-on-accent);
}
.buzz-button[data-icon-variant="chrome"] {
background: var(--bg-chrome-glass);
box-shadow:
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
backdrop-filter: blur(var(--blur-md));
}
.buzz-button[data-icon-variant="chrome"]:hover {
background: var(--bg-chrome-glass-hover);
}
.buzz-avatar {
position: relative;
display: grid;
width: 2rem;
height: 2rem;
flex: 0 0 auto;
place-items: center;
overflow: hidden;
border-radius: var(--radius-pill);
background: var(--bg-inset);
color: var(--text-secondary);
}
.buzz-avatar[data-size="small"] {
width: 1.5rem;
height: 1.5rem;
}
.buzz-avatar[data-size="large"] {
width: 2.5rem;
height: 2.5rem;
}
.buzz-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.workspace-surface {
min-width: 0;
min-height: 0;
overflow: hidden;
border: 1px solid var(--border-tertiary);
border-radius: var(--radius-panel);
background: var(--bg-panel-glass);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(var(--blur-lg));
}
.workspace-surface[data-variant="connected-left"] {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.workspace-surface[data-variant="connected-right"] {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.segmented-navigation {
display: inline-flex;
height: 2.5rem;
align-items: center;
gap: var(--space-1);
padding: var(--space-1);
border-radius: var(--radius-pill);
background: var(--bg-chrome-glass);
box-shadow:
var(--shadow-xs),
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
backdrop-filter: blur(var(--blur-md));
}
.segmented-navigation-list {
position: relative;
display: inline-flex;
height: 2rem;
align-items: center;
gap: var(--space-1);
}
.segmented-navigation-indicator {
position: absolute;
top: var(--space-1);
bottom: var(--space-1);
left: 0;
width: var(--active-tab-width);
border-radius: var(--radius-pill);
background: var(--bg-chrome-selected);
box-shadow: var(--shadow-xs);
transform: translateX(var(--active-tab-left));
transition:
width var(--duration-settle) var(--easing-settle),
transform var(--duration-settle) var(--easing-settle);
}
.segmented-navigation-item {
position: relative;
z-index: 1;
display: inline-flex;
height: 2rem;
align-items: center;
justify-content: center;
gap: var(--space-2);
border: 0;
border-radius: var(--radius-pill);
padding: 0 var(--space-4);
background: transparent;
color: var(--text-secondary);
font: inherit;
cursor: pointer;
}
.segmented-navigation-item:hover:not([data-selected]) {
background: var(--bg-chrome-glass-hover);
color: var(--text-primary);
}
.segmented-navigation-item[data-selected] {
color: var(--text-primary);
}
.panel-header {
position: relative;
display: flex;
min-height: var(--size-panel-header);
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: 0 var(--space-panel-inset);
}
.panel-header::after {
position: absolute;
right: var(--space-panel-inset);
bottom: 0;
left: var(--space-panel-inset);
height: 1px;
background: var(--border-tertiary);
content: "";
pointer-events: none;
}
.panel-header[data-variant="compact"] {
min-height: 3rem;
}
.panel-header-title,
.panel-header-actions {
display: flex;
min-width: 0;
align-items: center;
gap: var(--space-row-gap);
}
.panel-header-title > h2 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-field {
display: flex;
width: 100%;
height: var(--size-control);
align-items: center;
gap: var(--space-row-gap);
border-radius: var(--radius-control);
padding: 0 var(--space-control-inset);
background: var(--bg-inset);
color: var(--text-tertiary);
}
.search-field:focus-within {
box-shadow: 0 0 0 2px var(--ring-focus);
}
.search-field input {
min-width: 0;
flex: 1;
border: 0;
outline: 0;
background: transparent;
color: var(--text-primary);
font: inherit;
}
.search-field input::placeholder {
color: var(--text-tertiary);
}
.search-field input::-webkit-search-cancel-button {
display: none;
}
.navigator-section {
display: flex;
flex-direction: column;
}
.navigator-section-label {
min-height: var(--size-row);
margin: 0;
padding: 0 var(--space-control-inset);
line-height: var(--size-row);
}
.navigator-section-rows {
display: flex;
flex-direction: column;
gap: 1px;
}
.buzz-button[data-navigator-row] {
width: 100%;
justify-content: flex-start;
color: var(--text-secondary);
text-align: left;
}
.buzz-button[data-navigator-row]:hover,
.buzz-button[data-navigator-row][data-selected] {
background: var(--bg-hover);
color: var(--text-primary);
}
.buzz-button[data-navigator-row][data-inset] {
padding-left: calc(var(--space-control-inset) + var(--space-4));
}
.navigator-row-label {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.navigator-row-trailing {
flex: 0 0 auto;
color: var(--text-tertiary);
}
.buzz-button:focus-visible,
.segmented-navigation-item:focus-visible {
outline: 2px solid var(--ring-focus);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.buzz-button,
.segmented-navigation-indicator {
transition-duration: 0ms;
}
}
}

View File

@@ -0,0 +1,159 @@
@layer components {
.component-page-heading {
margin-bottom: var(--space-6);
}
.component-specimen-stack {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.component-specimen-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.component-specimen-group > h2 {
margin: 0;
}
.component-specimen-frame {
display: flex;
min-height: 7rem;
align-items: center;
padding: var(--space-6);
border-radius: var(--radius-panel);
background: var(--bg-panel);
}
.component-specimen-frame[data-backdrop] {
background: var(--bg-app);
}
.component-specimen-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
}
.component-single-surface-demo {
min-height: 10rem;
padding: var(--space-workspace-inset);
border-radius: var(--radius-panel);
background: var(--bg-app);
}
.component-single-surface-demo .workspace-surface {
height: 100%;
}
.component-surface-demo {
display: grid;
min-height: 12rem;
grid-template-columns: minmax(10rem, var(--size-navigator)) minmax(
18rem,
1fr
);
gap: var(--space-panel-gap);
padding: var(--space-workspace-inset);
border-radius: var(--radius-panel);
background: var(--bg-app);
}
.component-surface-demo .workspace-surface {
height: 100%;
}
.workspace-surface[data-variant="navigator-demo"] {
width: min(100%, var(--size-navigator));
padding: var(--space-2);
background: var(--bg-panel);
}
.component-navigator-search {
margin-bottom: var(--space-4);
}
.workspace-surface[data-variant="navigator-demo"]
.navigator-section
+ .navigator-section {
margin-top: var(--space-4);
}
.component-field-demo {
width: min(100%, var(--size-navigator));
}
.component-navigator-section-demo {
width: min(100%, var(--size-navigator));
padding: var(--space-2);
border-radius: var(--radius-control);
background: var(--bg-panel);
}
.component-overview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-6);
}
.component-overview-item {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-3);
}
.component-overview-preview {
height: 13rem;
overflow: hidden;
border-radius: var(--radius-panel);
background: var(--bg-panel);
}
.component-overview-preview > .component-specimen-stack {
width: 180%;
transform: scale(0.55);
transform-origin: top left;
}
.component-overview-preview .component-specimen-frame {
min-height: 8rem;
}
.component-overview-item > a {
align-self: flex-start;
text-decoration: none;
}
.component-overview-item > a:hover {
color: var(--text-accent);
}
.component-radius-demo {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
padding: var(--space-6);
border-radius: var(--radius-panel);
}
.component-radius-sample {
display: grid;
width: 8rem;
height: 5rem;
place-items: center;
border: 1px solid var(--border-tertiary);
box-shadow: var(--shadow-xs);
}
@media (max-width: 48rem) {
.component-overview-grid,
.component-surface-demo {
grid-template-columns: 1fr;
}
}
}

View File

@@ -1,6 +1,8 @@
@import "tailwindcss";
@import "./tokens.css";
@import "./typography.css";
@import "./components.css";
@import "./design-components.css";
@import "./product.css";
@import "./agents.css";

View File

@@ -104,6 +104,31 @@
/* --- Elevation. From the design exploration, unchanged. -- */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 0 2px rgba(0, 0, 0, 0.1), 0 4px 8px -2px rgba(0, 0, 0, 0.06);
/* --- Space. Six steps extracted from the Messages frame. --
The scale is intentionally short: 4/8/12/16/20/24 covers the repeated
panel gap, row inset, header inset, and workspace gutter without turning
every measured distance into a token. */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
/* --- Corner. Four shapes, each with a distinct job. ------- */
--corner-row: 0.625rem;
--corner-control: 0.75rem;
--corner-panel: 1.5rem;
--corner-round: 9999rem;
/* --- Time. State is quick; geometry settles deliberately. - */
--time-immediate: 0ms;
--time-fast: 120ms;
--time-state: 150ms;
--time-settle: 220ms;
--ease-state: ease;
--ease-settle: cubic-bezier(0.645, 0.045, 0.355, 1);
}
.dark {
@@ -261,6 +286,27 @@
offset; there is no second ring token. */
--ring-focus: var(--accent-8);
/* --- Geometry. Repeated relationships from Messages. ----- */
--space-workspace-inset: var(--space-4);
--space-panel-gap: var(--space-2);
--space-panel-inset: var(--space-5);
--space-control-inset: var(--space-3);
--space-row-gap: var(--space-2);
--size-topbar: 3.5rem;
--size-panel-header: 3.5rem;
--size-control: 2.25rem;
--size-row: 1.875rem;
--size-navigator: 13.5rem;
--radius-panel: var(--corner-panel);
--radius-control: var(--corner-control);
--radius-row: var(--corner-row);
--radius-pill: var(--corner-round);
--duration-fast: var(--time-fast);
--duration-state: var(--time-state);
--duration-settle: var(--time-settle);
--easing-state: var(--ease-state);
--easing-settle: var(--ease-settle);
/* ==========================================================
DELIBERATE EXCEPTIONS — the entire list.
==========================================================
@@ -382,4 +428,10 @@
--color-tint-green: var(--tint-green);
--color-tint-red: var(--tint-red);
--color-tint-cyan: var(--tint-cyan);
--spacing-workspace-inset: var(--space-workspace-inset);
--spacing-panel-gap: var(--space-panel-gap);
--spacing-panel-inset: var(--space-panel-inset);
--spacing-control-inset: var(--space-control-inset);
--spacing-row-gap: var(--space-row-gap);
}

View File

@@ -648,6 +648,138 @@ export const TYPE_RAMPS = [
},
];
export const SPACE = [
{
step: 1,
variable: "--space-1",
value: "4px",
use: "Tight optical separation inside a control.",
},
{
step: 2,
variable: "--space-2",
value: "8px",
use: "The panel gap and default gap between related items.",
},
{
step: 3,
variable: "--space-3",
value: "12px",
use: "The horizontal inset inside rows and compact controls.",
},
{
step: 4,
variable: "--space-4",
value: "16px",
use: "The workspace edge and ordinary content inset.",
},
{
step: 5,
variable: "--space-5",
value: "20px",
use: "The inset of a full panel header or reading surface.",
},
{
step: 6,
variable: "--space-6",
value: "24px",
use: "Separation between distinct content groups.",
},
];
export const SPACE_ROLES = [
{
token: "space-workspace-inset",
variable: "--space-workspace-inset",
pointsAt: "space 4",
use: "The distance between the window edge and its workspace panels.",
},
{
token: "space-panel-gap",
variable: "--space-panel-gap",
pointsAt: "space 2",
use: "The seam between independently movable panels.",
},
{
token: "space-panel-inset",
variable: "--space-panel-inset",
pointsAt: "space 5",
use: "The horizontal inset inside a full panel.",
},
{
token: "space-control-inset",
variable: "--space-control-inset",
pointsAt: "space 3",
use: "The horizontal inset inside controls and navigation rows.",
},
{
token: "space-row-gap",
variable: "--space-row-gap",
pointsAt: "space 2",
use: "The gap between an icon, label, and trailing metadata in one row.",
},
];
export const RADII = [
{
token: "radius-row",
variable: "--radius-row",
value: "10px",
use: "Dense navigator rows and compact selected regions.",
},
{
token: "radius-control",
variable: "--radius-control",
value: "12px",
use: "Inputs, buttons, tabs, and header actions.",
},
{
token: "radius-panel",
variable: "--radius-panel",
value: "24px",
use: "Every major workspace panel.",
},
{
token: "radius-pill",
variable: "--radius-pill",
value: "round",
use: "Pills, avatars, and fully circular controls.",
},
];
export const MOTION = [
{
token: "duration-fast",
variable: "--duration-fast",
value: "120ms",
use: "A hover affordance appearing or disappearing.",
},
{
token: "duration-state",
variable: "--duration-state",
value: "150ms",
use: "A colour or opacity state changing in place.",
},
{
token: "duration-settle",
variable: "--duration-settle",
value: "220ms",
use: "A tab indicator or released panel settling into place.",
},
{
token: "easing-state",
variable: "--easing-state",
value: "ease",
use: "Small state changes that do not move geometry.",
},
{
token: "easing-settle",
variable: "--easing-settle",
value: "cubic-bezier(.645,.045,.355,1)",
use: "Geometry settling after a deliberate selection or release.",
},
];
export const ELEVATION = [
{
token: "shadow-xs",

View File

@@ -0,0 +1,24 @@
import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
type AvatarSize = "small" | "default" | "large";
export function Avatar({
src,
alt,
fallback,
size = "default",
}: {
src?: string | null;
alt: string;
fallback: string;
size?: AvatarSize;
}) {
return (
<BaseAvatar.Root className="buzz-avatar" data-size={size}>
{src ? <BaseAvatar.Image src={src} alt={alt} /> : null}
<BaseAvatar.Fallback delay={src ? 150 : 0}>
{fallback.slice(0, 1).toUpperCase()}
</BaseAvatar.Fallback>
</BaseAvatar.Root>
);
}

View File

@@ -0,0 +1,34 @@
import { Button as BaseButton } from "@base-ui/react/button";
import type { ComponentProps, ReactNode } from "react";
type ButtonVariant = "primary" | "quiet" | "ghost";
type ButtonSize = "compact" | "default";
export type ButtonProps = Omit<
ComponentProps<typeof BaseButton>,
"className"
> & {
children: ReactNode;
variant?: ButtonVariant;
size?: ButtonSize;
};
export function Button({
children,
variant = "quiet",
size = "default",
type = "button",
...props
}: ButtonProps) {
return (
<BaseButton
{...props}
type={type}
className="buzz-button"
data-variant={variant}
data-size={size}
>
{children}
</BaseButton>
);
}

View File

@@ -0,0 +1,33 @@
import type { ComponentProps, ReactElement } from "react";
import { Button } from "./Button";
type IconButtonVariant = "quiet" | "ghost" | "solid" | "chrome";
type IconButtonSize = "compact" | "default" | "large";
export type IconButtonProps = Omit<
ComponentProps<typeof Button>,
"children" | "size" | "variant"
> & {
"aria-label": string;
icon: ReactElement;
variant?: IconButtonVariant;
size?: IconButtonSize;
};
export function IconButton({
icon,
variant = "ghost",
size = "default",
...props
}: IconButtonProps) {
return (
<Button
{...props}
variant="ghost"
data-icon-variant={variant}
data-icon-size={size}
>
{icon}
</Button>
);
}

View File

@@ -0,0 +1,36 @@
import type { ReactNode } from "react";
import { Button } from "./Button";
export function NavigatorRow({
label,
icon,
trailing,
selected = false,
inset = false,
onClick,
}: {
label: string;
icon?: ReactNode;
trailing?: ReactNode;
selected?: boolean;
inset?: boolean;
onClick?: () => void;
}) {
return (
<Button
variant="ghost"
size="compact"
data-navigator-row=""
data-selected={selected || undefined}
data-inset={inset || undefined}
onClick={onClick}
aria-current={selected ? "page" : undefined}
>
{icon}
<span className="navigator-row-label">{label}</span>
{trailing ? (
<span className="navigator-row-trailing">{trailing}</span>
) : null}
</Button>
);
}

View File

@@ -0,0 +1,18 @@
import type { ReactNode } from "react";
export function NavigatorSection({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<section className="navigator-section">
<h2 className="navigator-section-label text-body text-secondary">
{label}
</h2>
<div className="navigator-section-rows">{children}</div>
</section>
);
}

View File

@@ -0,0 +1,27 @@
import type { ReactNode } from "react";
export function PanelHeader({
title,
icon,
actions,
variant = "default",
}: {
title: ReactNode;
icon?: ReactNode;
actions?: ReactNode;
variant?: "default" | "compact";
}) {
return (
<header className="panel-header" data-variant={variant}>
<div className="panel-header-title">
{icon}
{typeof title === "string" ? (
<h2 className="text-heading text-primary">{title}</h2>
) : (
title
)}
</div>
{actions ? <div className="panel-header-actions">{actions}</div> : null}
</header>
);
}

View File

@@ -0,0 +1,37 @@
import { Field } from "@base-ui/react/field";
import { Input } from "@base-ui/react/input";
import { IconSearch, IconX } from "@tabler/icons-react";
import { IconButton } from "./IconButton";
export function SearchField({
value,
onValueChange,
label = "Search",
placeholder = "Search",
}: {
value: string;
onValueChange: (value: string) => void;
label?: string;
placeholder?: string;
}) {
return (
<Field.Root className="search-field">
<Field.Label className="sr-only">{label}</Field.Label>
<IconSearch size={16} stroke={1.7} aria-hidden="true" />
<Input
type="search"
value={value}
onValueChange={onValueChange}
placeholder={placeholder}
/>
{value ? (
<IconButton
aria-label={`Clear ${label.toLowerCase()}`}
icon={<IconX size={14} stroke={1.7} aria-hidden="true" />}
size="compact"
onClick={() => onValueChange("")}
/>
) : null}
</Field.Root>
);
}

View File

@@ -0,0 +1,45 @@
import { Tabs } from "@base-ui/react/tabs";
import type { ReactNode } from "react";
export type SegmentedNavigationItem<Value extends string> = {
value: Value;
label: string;
icon?: ReactNode;
};
export function SegmentedNavigation<Value extends string>({
value,
items,
label,
onValueChange,
trailingAction,
}: {
value: Value;
items: readonly SegmentedNavigationItem<Value>[];
label: string;
onValueChange: (value: Value) => void;
trailingAction?: ReactNode;
}) {
return (
<Tabs.Root
className="segmented-navigation"
value={value}
onValueChange={(nextValue) => onValueChange(nextValue as Value)}
>
<Tabs.List className="segmented-navigation-list" aria-label={label}>
<Tabs.Indicator className="segmented-navigation-indicator" />
{items.map((item) => (
<Tabs.Tab
key={item.value}
value={item.value}
className="segmented-navigation-item"
>
{item.icon}
<span>{item.label}</span>
</Tabs.Tab>
))}
</Tabs.List>
{trailingAction}
</Tabs.Root>
);
}

View File

@@ -0,0 +1,27 @@
import type { ComponentProps, ElementType, ReactNode } from "react";
type SurfaceVariant =
| "panel"
| "connected-left"
| "connected-right"
| "navigator-demo";
type WorkspaceSurfaceProps<T extends ElementType> = {
as?: T;
children: ReactNode;
variant?: SurfaceVariant;
} & Omit<ComponentProps<T>, "as" | "children" | "className">;
export function WorkspaceSurface<T extends ElementType = "section">({
as,
children,
variant = "panel",
...props
}: WorkspaceSurfaceProps<T>) {
const Component = as ?? "section";
return (
<Component {...props} className="workspace-surface" data-variant={variant}>
{children}
</Component>
);
}

View File

@@ -0,0 +1,96 @@
export type ComponentStatus = "core" | "proposed";
export type ComponentDefinition = {
slug: string;
name: string;
purpose: string;
behavior: string;
variants: readonly string[];
status: ComponentStatus;
owner?: string;
};
export const COMPONENTS: readonly ComponentDefinition[] = [
{
slug: "button",
name: "Button",
purpose: "A labeled action with primary, quiet, or unfilled emphasis.",
behavior: "Base UI Button",
variants: ["primary", "quiet", "ghost"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "icon-button",
name: "IconButton",
purpose: "A compact icon-only action that always owns an accessible label.",
behavior: "Composes Buzz Button",
variants: ["quiet", "ghost", "solid", "chrome"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "avatar",
name: "Avatar",
purpose: "A person or agent identity image with a stable fallback.",
behavior: "Base UI Avatar",
variants: ["small", "default", "large"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "workspace-surface",
name: "WorkspaceSurface",
purpose: "A major panel sitting on the atmospheric workspace backdrop.",
behavior: "Semantic native region",
variants: ["panel", "connected-left", "connected-right"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "segmented-navigation",
name: "SegmentedNavigation",
purpose: "A single-select switch between top-level workspace destinations.",
behavior: "Base UI Tabs",
variants: ["default"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "panel-header",
name: "PanelHeader",
purpose:
"The identity and action boundary at the top of a workspace panel.",
behavior: "Semantic native header",
variants: ["default", "compact"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "search-field",
name: "SearchField",
purpose: "A compact filter field with a search cue and clear action.",
behavior: "Base UI Field and Input",
variants: ["default"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "navigator-section",
name: "NavigatorSection",
purpose: "A named group of related rows in a dense workspace navigator.",
behavior: "Semantic native section",
variants: ["default"],
status: "proposed",
owner: "desktop-new Messages",
},
{
slug: "navigator-row",
name: "NavigatorRow",
purpose: "A selectable destination row with optional icon and metadata.",
behavior: "Composes Buzz Button",
variants: ["default", "inset", "selected"],
status: "proposed",
owner: "desktop-new Messages",
},
];

View File

@@ -5,12 +5,12 @@
"build": {
"beforeDevCommand": {
"script": "exec ./node_modules/.bin/vite --host 127.0.0.1",
"cwd": "../../desktop-next",
"cwd": "../../desktop-new",
"wait": false
},
"devUrl": "http://127.0.0.1:1430",
"beforeBuildCommand": "pnpm --dir ../../desktop-next build",
"frontendDist": "../../desktop-next/dist"
"beforeBuildCommand": "pnpm --dir ../../desktop-new build",
"frontendDist": "../../desktop-new/dist"
},
"app": {
"windows": [

View File

@@ -1,41 +0,0 @@
import { PageHeader, Section, Stub } from "./primitives";
export function MotionPage() {
return (
<>
<PageHeader
title="Motion"
status="not started"
intro="No durations or easings are defined yet. The rules that govern them already exist in DESIGN.md, which is the harder half."
/>
<Section title="What this page will hold">
<Stub
what="Duration and easing tokens, and the named roles that pair them: a state change, an element entering, an overlay opening, a drag settling."
decide={[
"The duration steps, and which is the default",
"Easing curves, and which motions get a spring rather than a curve",
"How reduced-motion preferences are honoured at the token level rather than per component",
]}
/>
</Section>
<Section
title="Rules that already apply"
description="These are in DESIGN.md and do not wait for the tokens."
>
<ul className="flex list-disc flex-col gap-2 pl-5">
{[
"Direct manipulation follows the pointer exactly, with no easing — smoothing during a drag reads as lag. Spring physics belongs to what happens after release.",
"Never animate blur. Re-blurring a large surface every frame is expensive enough to feel. Animate opacity instead.",
"Motion explains a change; it does not decorate one. If removing an animation loses no information, remove it.",
].map((rule) => (
<li key={rule} className="text-body text-secondary">
{rule}
</li>
))}
</ul>
</Section>
</>
);
}

View File

@@ -1,24 +0,0 @@
import { PageHeader, Section, Stub } from "./primitives";
export function RadiusPage() {
return (
<>
<PageHeader
title="Radius"
status="not started"
intro="The design exploration is consistently soft — rounded panels, pill-shaped chrome, gently rounded chips — but the specific values have not been pulled into a scale yet."
/>
<Section title="What this page will hold">
<Stub
what="A radius scale, and the named roles that point at it: the panel corner, the control corner, the chip corner, and the fully-round pill."
decide={[
"How many steps the scale needs",
"Which roles exist, and what each one is for",
"Whether nested corners follow a rule, so a control inside a panel reads correctly",
]}
/>
</Section>
</>
);
}

View File

@@ -1,25 +0,0 @@
import { PageHeader, Section, Stub } from "./primitives";
export function SpacingPage() {
return (
<>
<PageHeader
title="Spacing"
status="not started"
intro="Tailwind's default spacing scale is currently in play, unexamined. Whether that is the right rhythm for this design is an open decision."
/>
<Section title="What this page will hold">
<Stub
what="A spacing scale with the same two-layer treatment as colour: raw steps below, and named roles for the spacings that carry meaning — the gutter between rows, the inset of a panel, the gap inside a control."
decide={[
"Whether the stock scale stays or the design needs its own rhythm",
"Which spacings are named roles rather than raw steps",
"Whether density varies per surface, and if so how that is expressed",
"The relationship between spacing and the type ramp",
]}
/>
</Section>
</>
);
}

39
pnpm-lock.yaml generated
View File

@@ -308,11 +308,11 @@ importers:
specifier: ^8.0.0
version: 8.0.16(@types/node@25.6.0)(jiti@2.7.0)(yaml@2.9.0)
desktop-next:
desktop-new:
dependencies:
'@base-ui-components/react':
specifier: 1.0.0-rc.0
version: 1.0.0-rc.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@base-ui/react':
specifier: 1.7.0
version: 1.7.0(@date-fns/tz@1.5.0)(@types/react@19.2.18)(date-fns@4.4.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@fontsource-variable/inter':
specifier: ^5.2.8
version: 5.2.8
@@ -574,21 +574,25 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@base-ui-components/react@1.0.0-rc.0':
resolution: {integrity: sha512-9lhUFbJcbXvc9KulLev1WTFxS/alJRBWDH/ibKSQaNvmDwMFS2gKp1sTeeldYSfKuS/KC1w2MZutc0wHu2hRHQ==}
'@base-ui/react@1.7.0':
resolution: {integrity: sha512-j+8QjX44C32jrXD/qyEAGpFr70FRpGL2CY61mQd9nBPWN737CK0xxD1ceJ055rW4RtdvFDT1e7otzdlfxvsYug==}
engines: {node: '>=14.0.0'}
deprecated: Package was renamed to @base-ui/react
peerDependencies:
'@date-fns/tz': ^1.2.0
'@types/react': ^17 || ^18 || ^19
date-fns: ^4.0.0
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@date-fns/tz':
optional: true
'@types/react':
optional: true
date-fns:
optional: true
'@base-ui-components/utils@0.2.2':
resolution: {integrity: sha512-rNJCD6TFy3OSRDKVHJDzLpxO3esTV1/drRtWNUpe7rCpPN9HZVHUCuP+6rdDYDGWfXnQHbqi05xOyRP2iZAlkw==}
deprecated: Package was renamed to @base-ui/utils
'@base-ui/utils@0.3.2':
resolution: {integrity: sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
@@ -3502,9 +3506,6 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tabbable@6.5.0:
resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==}
tailwind-merge@3.6.0:
resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
@@ -3982,21 +3983,21 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.29.7
'@base-ui-components/react@1.0.0-rc.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
'@base-ui/react@1.7.0(@date-fns/tz@1.5.0)(@types/react@19.2.18)(date-fns@4.4.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@babel/runtime': 7.29.7
'@base-ui-components/utils': 0.2.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@base-ui/utils': 0.3.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@floating-ui/utils': 0.2.12
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
reselect: 5.3.0
tabbable: 6.5.0
use-sync-external-store: 1.6.0(react@19.2.8)
optionalDependencies:
'@date-fns/tz': 1.5.0
'@types/react': 19.2.18
date-fns: 4.4.0
'@base-ui-components/utils@0.2.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
'@base-ui/utils@0.3.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@babel/runtime': 7.29.7
'@floating-ui/utils': 0.2.12
@@ -7006,8 +7007,6 @@ snapshots:
symbol-tree@3.2.4: {}
tabbable@6.5.0: {}
tailwind-merge@3.6.0: {}
tailwindcss@4.3.0: {}

View File

@@ -1,6 +1,6 @@
packages:
- "desktop"
- "desktop-next"
- "desktop-new"
- "web"
- "admin-web"
allowBuilds: