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
25 changes: 25 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-vega",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default-translucent",
"menuAccent": "subtle",
"rtl": false,
"registries": {}
}
12 changes: 12 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@
- Rows are `treeitem`s rather than buttons, so their keyboard behavior is the tree's to implement rather than something the platform supplies.
- The row holding the tab stop has to stay rendered even when it scrolls out of the virtualized window. Unmounting it drops focus to the document body and leaves the navigator with no tab stop at all, which would take the scroll region out of the tab sequence.

### Build UI primitives on Base UI

**Decision:** The primitives in `src/components/ui/` are built on `@base-ui/react`, and the project is managed through the shadcn CLI with `components.json`. Toast notifications use Base UI Toast rather than a separate toast dependency. The wrappers remain hand-owned; the CLI is not used to regenerate them.

**Rationale:** Radix remains maintained and unblocking, so the move is elective rather than forced. It follows the base library shadcn made the default for new projects, and it ends Leafdown's reliance on Radix positioning internals: the editor context popup drove its repositioning transition off `[data-radix-popper-content-wrapper]`, an undocumented wrapper element, from a component, a stylesheet, and a test at once. Base UI positions through an element the application owns. Keeping a separate toast dependency alongside it would leave two interaction vocabularies in the same layer.

**Consequences:**

- The CLI writes kebab-case files into `src/components/ui/`, which is why component file names follow that convention rather than React's `PascalCase`.
- Base UI Toast supplies no default styling and no fixed set of toast types, so toast presentation, types, and announcement behavior are Leafdown's to own.
- Where a Base UI default disagrees with behavior Leafdown already had, the wrapper carries the override, so consult the wrapper rather than Base UI's documentation for what a primitive does here.

## Platform Decisions

### Windows first, cross-platform aware
Expand Down
8 changes: 4 additions & 4 deletions docs/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,15 @@ Every control that can hold focus must render a visible focus indicator. Suppres

Use:

- `focus-visible:` for controls a pointer can also activate, so a click does not leave a ring behind. Radix menu items are the exception and use `focus:`, because their focus is roving and follows the pointer.
- `focus-visible:` for controls a pointer can also activate, so a click does not leave a ring behind. Menu items are the exception and use `focus:`, because their focus is roving and follows the pointer.
- The `Button` treatment, `focus-visible:border-ring` with `focus-visible:ring-3 focus-visible:ring-ring/50`, as the reference for hand-built surfaces.
- `outline-hidden` rather than `outline-none` when suppressing the native outline.
- Room for the ring wherever a control sits inside a clipping box. `overflow` and `contain: paint` both cut a `ring-*` box-shadow off at the boundary.

Avoid:

- `outline-hidden` or `outline-none` with no paired `focus-visible:` rule.
- Relying on `outline-ring/50` in `App.css` for the indicator. It sets outline color only, so it renders nothing until a style and width exist.
- Relying on `outline-ring/50` in `app.css` for the indicator. It sets outline color only, so it renders nothing until a style and width exist.
- Treating an open or active state as the focus indicator. They answer different questions and a keyboard user needs both.

Why:
Expand All @@ -239,7 +239,7 @@ Leafdown builds its own titlebar, menu shell, and navigator, so focus presentati
Example:

```tsx
<MenubarPrimitive.Trigger className="outline-hidden hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 aria-expanded:bg-muted" />
<MenuPrimitive.Trigger className="outline-hidden select-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 aria-expanded:bg-muted" />
```

### Keyboard Traversal
Expand All @@ -251,7 +251,7 @@ Use:
- One tab stop for the surface, roving to the control that last held focus, with every other control at `tabIndex={-1}`.
- Arrow keys, `Home`, and `End` for movement inside the surface, leaving `Tab` to leave it.
- A traversal model derived from the data when the surface owns its own movement, as `articleNavigatorTraversal.ts` does for the navigator's rows.
- Position data attributes read back off the DOM when a primitive already owns one axis, as the context popup does for vertical movement across Radix's horizontal roving focus.
- Position data attributes read back off the DOM when a primitive already owns one axis, as the context popup does for vertical movement across the toolbar's horizontal roving focus.
- An explicit focus return when a surface that took focus closes.

Avoid:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"prepare": "husky"
},
"dependencies": {
"@base-ui/react": "^1.7.0",
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@milkdown/kit": "7.21.3",
Expand All @@ -49,11 +50,10 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.27.0",
"radix-ui": "^1.6.7",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"shadcn": "^4.16.2",
"shiki": "4.3.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.3",
"zustand": "^5.0.14"
Expand Down
Loading