mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
feat(desktop-new): establish visual component foundation
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
10
Justfile
10
Justfile
@@ -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}}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
BIN
desktop-new/public/design-system/morgan.png
Normal file
BIN
desktop-new/public/design-system/morgan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 414 KiB |
6
desktop-new/src/app/routes/design/components.tsx
Normal file
6
desktop-new/src/app/routes/design/components.tsx
Normal 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,
|
||||
});
|
||||
11
desktop-new/src/app/routes/design/components_.$component.tsx
Normal file
11
desktop-new/src/app/routes/design/components_.$component.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
desktop-new/src/features/design-system/ui/ComponentsPage.tsx
Normal file
32
desktop-new/src/features/design-system/ui/ComponentsPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}`,
|
||||
]),
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
43
desktop-new/src/features/design-system/ui/MotionPage.tsx
Normal file
43
desktop-new/src/features/design-system/ui/MotionPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
desktop-new/src/features/design-system/ui/RadiusPage.tsx
Normal file
39
desktop-new/src/features/design-system/ui/RadiusPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
desktop-new/src/features/design-system/ui/SpacingPage.tsx
Normal file
38
desktop-new/src/features/design-system/ui/SpacingPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
393
desktop-new/src/features/design-system/ui/componentSpecimens.tsx
Normal file
393
desktop-new/src/features/design-system/ui/componentSpecimens.tsx
Normal 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,
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Accordion } from "@base-ui-components/react/accordion";
|
||||
import { Accordion } from "@base-ui/react/accordion";
|
||||
import {
|
||||
IconAlertCircle,
|
||||
IconCheck,
|
||||
@@ -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";
|
||||
339
desktop-new/src/shared/styles/components.css
Normal file
339
desktop-new/src/shared/styles/components.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
desktop-new/src/shared/styles/design-components.css
Normal file
159
desktop-new/src/shared/styles/design-components.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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",
|
||||
24
desktop-new/src/shared/ui/Avatar.tsx
Normal file
24
desktop-new/src/shared/ui/Avatar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
desktop-new/src/shared/ui/Button.tsx
Normal file
34
desktop-new/src/shared/ui/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
desktop-new/src/shared/ui/IconButton.tsx
Normal file
33
desktop-new/src/shared/ui/IconButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
36
desktop-new/src/shared/ui/NavigatorRow.tsx
Normal file
36
desktop-new/src/shared/ui/NavigatorRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
18
desktop-new/src/shared/ui/NavigatorSection.tsx
Normal file
18
desktop-new/src/shared/ui/NavigatorSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
desktop-new/src/shared/ui/PanelHeader.tsx
Normal file
27
desktop-new/src/shared/ui/PanelHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
37
desktop-new/src/shared/ui/SearchField.tsx
Normal file
37
desktop-new/src/shared/ui/SearchField.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
desktop-new/src/shared/ui/SegmentedNavigation.tsx
Normal file
45
desktop-new/src/shared/ui/SegmentedNavigation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
desktop-new/src/shared/ui/WorkspaceSurface.tsx
Normal file
27
desktop-new/src/shared/ui/WorkspaceSurface.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
96
desktop-new/src/shared/ui/registry.ts
Normal file
96
desktop-new/src/shared/ui/registry.ts
Normal 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",
|
||||
},
|
||||
];
|
||||
@@ -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": [
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
39
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
packages:
|
||||
- "desktop"
|
||||
- "desktop-next"
|
||||
- "desktop-new"
|
||||
- "web"
|
||||
- "admin-web"
|
||||
allowBuilds:
|
||||
|
||||
Reference in New Issue
Block a user