Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Dependencies and build output
node_modules/
dist/
storybook-static/
coverage/
*.tsbuildinfo
.cache/
Expand Down Expand Up @@ -31,3 +32,6 @@ test-results/
.DS_Store
.claude/
.amp/

# Generated from each CSS module by `bun run --cwd apps/web css-types`
*.module.css.d.ts
8 changes: 8 additions & 0 deletions apps/web/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineMain } from "@storybook/react-vite/node";

// Storybook reads apps/web/vite.config.ts, so stories build with the same
// plugins the interface does.
export default defineMain({
framework: "@storybook/react-vite",
stories: ["../src/**/*.stories.tsx"],
});
55 changes: 55 additions & 0 deletions apps/web/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, type ReactNode } from "react";
import { ThemeProvider, useTheme } from "@corbits/react-ui";
import type { Preview } from "@storybook/react-vite";
import { Toaster } from "sonner";
// The same order main.tsx imports them in: the library's sheet, then ours.
import "@corbits/react-ui/styles.css";
import "../src/styles.css";

type Mode = "light" | "dark";

// Follows the toolbar instead of the stored preference, so a story renders
// in the mode the reviewer picked.
function Mode({ mode, children }: { mode: Mode; children: ReactNode }) {
const { setMode, resolvedMode } = useTheme();
useEffect(() => setMode(mode), [mode, setMode]);
return (
<>
{children}
<Toaster theme={resolvedMode} richColors closeButton />
</>
);
}

const preview: Preview = {
globalTypes: {
mode: {
description: "Light or dark theme",
toolbar: {
title: "Theme",
icon: "mirror",
items: [
{ value: "light", title: "Light" },
{ value: "dark", title: "Dark" },
],
dynamicTitle: true,
},
},
},
initialGlobals: { mode: "light" },
decorators: [
(Story, context) => (
<ThemeProvider storageKey="builder-storybook-theme" defaultMode="light">
<Mode mode={context.globals["mode"] === "dark" ? "dark" : "light"}>
<Story />
</Mode>
</ThemeProvider>
),
],
parameters: {
layout: "padded",
controls: { expanded: true },
},
};

export default preview;
25 changes: 25 additions & 0 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ like what is already there, and extends it rather than standing beside it.
maintainers decide whether it moves.
- Wording lives in the interface's config.

## Styles

- A component's styles live in a CSS module beside it (`pane-bar.tsx`,
`pane-bar.module.css`), imported as `styles`, with camelCase class names.
`styles.css` holds only what is global: fonts, tokens, element defaults,
overrides of the library that apply everywhere, and shared keyframes.
- Each module has exact types, generated by `bun run css-types` (the `dev`
script keeps them current and `typecheck` regenerates them), so
`styles.pane` is a `string` and a misspelt class is a type error. Join
classes with `clsx` only when one is conditional.
- A look used in more than one place is a component, such as `InlineNote`,
`Field`, `Fault` or `Banner`, not a class two components share. Shared
components live in `components.tsx`, or in a file of their own when they
belong to one area.
- A shared component owns its whole look and takes no `className`. Variants
are props, rendered as `data-*` attributes (`data-tone="error"`, or an
empty `data-pill=""` for a flag); the element it renders is an `as` prop.
- Every shared component's root carries a `data-slot` naming it. A parent
positions it from its own module through that attribute, such as
`.notes > [data-slot="inline-note"]` for margin, size or placement, never
through another module's class, and never by restyling its inside.
- The library's own classes, such as `.dark`, go in `:global(...)`, and a
global keyframe in `global(...)`: `animation: global(fade-in) 200ms`.
- Code and tests find elements by `data-*` attributes, never by a class name.

## Every artifact kind is a first-class document

A new kind (a sheet, a deck, a diagram) is integrated everywhere a document
Expand Down
13 changes: 11 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
"dev": "bun run --parallel css-types:watch serve",
"serve": "vite",
"build": "vite build",
"css-types": "tcm -s -p \"src/**/*.module.css\"",
"css-types:watch": "tcm -s -w -p \"src/**/*.module.css\"",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build -o storybook-static"
},
"dependencies": {
"@corbits-builder/client": "workspace:*",
Expand All @@ -23,6 +28,7 @@
"@tanstack/react-query": "5.104.1",
"@tanstack/react-query-persist-client": "5.104.1",
"arktype": "catalog:",
"clsx": "2.1.1",
"diff": "9.0.0",
"idb-keyval": "6.2.2",
"jszip": "3.10.2",
Expand All @@ -32,9 +38,12 @@
"sonner": "2.0.8"
},
"devDependencies": {
"@storybook/react-vite": "11.0.0-beta.0",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@vitejs/plugin-react": "5.2.0",
"storybook": "11.0.0-beta.0",
"typed-css-modules": "0.9.1",
"typescript": "5.9.3",
"vite": "6.4.5"
}
Expand Down
Loading