react-foundry
v0.0.16
Published
A lightweight component development environment for React
Maintainers
Readme
react-foundry
A lightweight component development environment for React. Write a preview as a plain React component and it shows up on a canvas, in a navigation tree you define. No bundler config of your own.
- Auto-discovery — finds your
.preview.tsxfiles automatically - Your navigation — declare the tree in config; array order is display order
- One primitive — a preview is just a React component, hooks and all
- Typed nav paths — a misplaced preview is a compile error, with autocomplete
- Theming — light/dark/system, driven by a few role-named color tokens
- Accessibility — an axe-core checker that highlights the offending node on the canvas
- Style isolation — foundry's own CSS stops at the canvas, so components render as they do in your app (one way)
- Fast — built on Vite; the chrome ships precompiled, so nothing builds in your
node_modules
Install
npm install --save-dev react-foundryreact and react-dom are peer dependencies you already have (React 18 or 19).
Quick start
Point your scripts at the foundry binary:
{
"scripts": {
"dev": "foundry dev",
"build": "foundry build",
"preview": "foundry preview"
}
}Add a foundry.config.ts at your project root:
import { defineConfig } from 'react-foundry'
export default defineConfig({
previews: 'src/**/*.preview.tsx',
title: 'My Components',
nav: [{ label: 'Forms', children: [{ label: 'Button' }] }],
})Write a preview next to a component:
import { createPreview, type NavPath } from 'react-foundry'
import { Button } from './button'
export const nav: NavPath = 'Forms/Button'
export const Primary = createPreview(() => <Button variant="primary">Go</Button>)Run npm run dev and it appears on the canvas under Forms → Button → Primary.
Toolbar
The floating bar in the top-left corner holds every control over the environment itself. Each one has a keyboard shortcut, and hovering or focusing a button names both.
| Control | Key | What it does |
| --- | --- | --- |
| Component list | S | Shows or hides the shelf, the navigation tree down the left |
| Controls panel | P | Shows or hides the props panel |
| Theme | T | Flips between light and dark. On system, it flips away from whatever the system resolved to, since that is the theme you are actually looking at |
| Accessibility check | A | Turns the axe-core panel on or off |
The shortcuts are bare letters, with no modifier. The obvious chords are already taken by browsers — CtrlShiftT reopens a tab, CtrlShiftP opens a private window — and this is a bar you reach for constantly.
The cost of bare letters is that they have to stay out of your component's way, so a keystroke is ignored when it lands on an input, a textarea, a select or a rich text editor on the canvas, when any of ctrl, cmd or alt is held, or when something closer to the keystroke has already handled it.
Shelf and panel state persists across reloads, which matters more than it sounds: editing a preview file reloads the page, and collapsing the whole layout on every save would be worse than the edit is worth.
CLI
Each command takes an optional [root] argument, defaulting to the current directory:
foundry dev [root]— start the dev server (alsofoundry serve, or justfoundry)foundry build [root]— build a static bundle for productionfoundry preview [root]— serve the production build locallyfoundry --version— the installed version, worth quoting in a bug report
Every command reads foundry.config.ts from that directory. If there is none, foundry says
so and falls back to defaults rather than starting a working-looking server with an empty
shelf. Worth watching for under a hoisting package manager, where the foundry binary is
installed at your workspace root and will run happily from there.
Hot reloading
Editing a component, or the preview that renders it, patches the canvas in place. What reloads instead is anything that changes the shelf or the props panel, since neither is rebuilt by a hot patch:
| You edit | What happens |
| --- | --- |
| A component, or a stylesheet it imports | Patched in place |
| A render body, in a preview file with no controls | Patched in place |
| Anything, in a preview file that declares controls | Page reloads |
| A preview's nav, its exports, or a label | Page reloads, shelf updates |
| A preview file added or deleted | Page reloads |
| nav or title in your config | Page reloads |
| theme in your config | Stylesheet swaps, no reload |
| foundry.providers.tsx | Page reloads |
A file that declares controls reloads on every edit, not just on schema edits. The props
panel renders from the schema captured when the preview loaded, and a hot patch replaces
the render without replacing that object, so patching a schema change would leave the panel
describing the previous shape. Foundry can't tell the two edits apart, because a schema can
reach the preview by name (controls: buttonControls) and then reads identically either
side of an edit to it.
A patch keeps the state held inside render, in both forms. React's refresh transform only
registers a function passed straight to createPreview, so foundry registers an options-form
render itself, under the export's name; React then treats the edited function as an update of
the one on screen rather than as a new component. Changing the hooks inside it still remounts
it, which is React's own rule.
Keeping a refresh boundary
React Fast Refresh patches a module in place only while every one of its exports is component-like, or the value of a non-component export is unchanged by the edit. So a file exporting a component alongside a helper:
export const Badge = (props: BadgeProps) => { ... }
export const badgeVariants = cva({ ... }) // not a componentkeeps its boundary while you are editing Badge, and loses it the moment badgeVariants
is what changed: the update invalidates upward and the page reloads instead. Moving the
helper into its own file avoids it. This is a React rule rather than a foundry one, and it
applies to your components as much as to previews. The browser console names the offending
export when it happens.
Configuration
Create a foundry.config.ts in your project root:
import { defineConfig } from 'react-foundry'
export default defineConfig({
previews: 'src/components/**/*.preview.tsx',
port: 5173,
title: 'My Components',
nav: [
{ label: 'Foundations', children: [{ label: 'Colors' }, { label: 'Typography' }] },
{ label: 'Forms', children: [{ label: 'Button' }, { label: 'Input' }] },
],
theme: {
colors: {
light: { accent: '#0ea5e9' },
dark: { accent: 'oklch(70% 0.15 240)' },
},
},
})| Option | Default | Notes |
| --- | --- | --- |
| previews | 'src/components/**/*.preview.tsx' | Glob for preview files. Requires restart. |
| nav | [] | The navigation tree. Hot-reloadable; the shelf and the generated NavPath union both update on save. |
| port | 5173 | Dev server port. Requires restart. |
| host | 'localhost' | Dev server host. Requires restart. |
| title | none | Display title for the instance. Hot-reloadable. |
| theme | none | Theme customization. Hot-reloadable. |
| navTypes | true | Whether to emit foundry-nav.gen.d.ts. Set false when you derive the union with NavPathsOf instead. Hot-reloadable; turning it off mid-session removes the file. |
| navTypesPath | inferred | Exact path for the generated NavPath types. Defaults next to your previews; override for layouts the inference can't reach. Hot-reloadable. |
| viteConfig | none | Vite config overrides. Requires restart. |
Changing previews, port or host logs a warning naming the key and both values, so a
restart-required edit doesn't quietly leave the server running on the old one. A changed
previews glob is the one worth watching for: without the warning it looks like HMR has
broken, when the file you're editing is simply outside the pattern being watched.
The config is bundled with esbuild into node_modules/.cache/react-foundry/ and imported
from there, so import.meta.dirname inside your config points at that cache directory,
not your project. Use process.cwd() for anything that needs a project path:
viteConfig: { plugins: [somePlugin({ root: process.cwd() })] }Config files are resolved in this order, first match wins:
foundry.config.mjs
foundry.config.js
foundry.config.ts
.foundry/config.mjs
.foundry/config.js
.foundry/config.tsNavigation
nav is optional, and the two modes behave differently enough to choose deliberately:
| | Shelf order | NavPath |
| --- | --- | --- |
| nav declared | Exactly as written, nested as deeply as you like | The union of every path in the tree |
| nav omitted | Inferred from the nav values your previews declare, sorted alphabetically | Stays string |
If your shelf is alphabetical and ignoring the order you wanted, that is the second row:
you have no nav in your config.
Either way a preview whose path is not in the config still appears, appended at the end with a warning, so nothing is ever silently dropped.
With a tree declared, every path in it including parents becomes part of a NavPath
union that preview files check against, so a mistyped path is a compile error with
autocomplete rather than a preview quietly landing in the wrong place. There are two ways
to get that union.
Generated types (the default)
Foundry writes src/foundry-nav.gen.d.ts (or the project root if you have no src/) on
every server start and whenever the config changes, and NavPath picks it up ambiently.
Add that file to your .gitignore. It carries its own eslint-disable and
prettier-ignore header, so your lint and format config does not need to know about it.
Nothing is written when nav is empty: with no tree to describe, the file would say exactly
what NavPath already falls back to.
Deriving it from the config
NavPathsOf reads the same union straight off your config, with no generated file at all:
// foundry.config.ts
import { defineConfig, defineNav, type NavPathsOf } from 'react-foundry'
const nav = defineNav([{ label: 'Forms', children: [{ label: 'Button' }] }])
const config = defineConfig({ nav, navTypes: false })
export default config
export type AppNavPath = NavPathsOf<typeof config> // 'Forms' | 'Forms/Button'// button.preview.tsx
import type { AppNavPath } from '../foundry.config'
export const nav: AppNavPath = 'Forms/Button'defineNav is what keeps the labels literal enough to flatten (as const works too, but
checks nothing where you write it). navTypes: false turns the codegen off, which is the
part that actually retires the artifact: no gitignore entry, no lint exemptions, and no
question of whether types were emitted before tsc ran. Turning it off also deletes a file
an earlier run wrote, so a stale union cannot outlive the setting.
The tradeoff is that AppNavPath is a project-local type your previews import by name,
rather than an ambient NavPath. That is arguably clearer, since its origin is visible at
the import.
If there is no tree to read, NavPathsOf resolves to string rather than to an empty
union, matching what NavPath does without a config. So adding the type before you add
the nav tree typechecks; it just does not constrain anything yet.
Monorepo layouts
This section is about placing the generated file. If you derive the union with NavPathsOf
instead, none of it applies: an imported type resolves wherever the import does, so there is
nothing to place.
Declaration merging only narrows NavPath inside a TypeScript project that compiles both the
generated file and your .preview.tsx files. When your config lives in one package but the
previews live in another (say the config is in apps/foundry and previews in
packages/react/src), Foundry writes foundry-nav.gen.d.ts next to the previews by
inferring the directory from the previews glob base, so the previews' own tsconfig picks up
the augmentation with no extra setup.
Because that augmentation binds during the previews' package compilation, react-foundry
must be a dependency of that package too, not only the app that holds foundry.config.ts. Add
it as a devDependency where the previews live, or createPreview and NavPath resolve to
Cannot find module 'react-foundry' (TS2307) across every preview file.
If that inference can't reach the right place, set navTypesPath to the exact output file,
resolved against the config root:
export default defineConfig({
previews: '../../packages/react/src/**/*.preview.tsx',
navTypesPath: '../../packages/react/src/foundry-nav.gen.d.ts',
})Either way, add the generated file to that package's .gitignore. Symlinked workspace
previews are served with no extra config: foundry detects your workspace root and adds it to
Vite's file-serving allow-list automatically. Only reach for viteConfig.server.fs.allow if
previews live outside that detected root.
Theming
Foundry's shell derives its palette from a few role-named tokens, set independently for
light and dark. Values accept any CSS color (hex, rgb(), hsl(), oklch(), named
colors) or a bare OKLCH triplet.
Anchors — set these to shift the whole ramp at once:
| Token | Role |
| --- | --- |
| bg | base surface / paper pole |
| fg | strong text / ink pole |
| accent | focus rings, links, active states |
Surfaces and text — derived from the anchors by default; override any one for precision:
| Token | Role |
| --- | --- |
| canvas | the backdrop behind your preview |
| panel | shelf, props panel, and dock backgrounds |
| border | inputs, dividers, panel edges |
| textMuted / textBody / textStrong | secondary / body / emphasis text |
theme: {
colors: {
light: { canvas: '#faf9f7', accent: '#0ea5e9' },
dark: { canvas: '#0b0b0c', accent: '#38bdf8' },
},
fonts: { sans: 'Inter, sans-serif' },
}Overriding an anchor (bg/fg/accent) recomputes every derived token in the browser,
so the whole shell shifts from one or two values. Overriding a specific token (like
canvas) pins just that one. The surfaces and text mix in OKLCH internally, but you never
have to: pass plain hex.
Fonts (sans, mono) are mode-agnostic: one value for both themes. Foundry bundles
Instrument Sans; if you point sans at another family, make sure it is actually available (a
system font, your own @font-face, or a font-host link), since foundry can't bundle it for
you.
Creating component previews
A preview is a React component that fills the canvas. Create a .preview.tsx file alongside
your component, place it in the tree with a nav export, and wrap each preview in
createPreview:
import { createPreview, type NavPath } from 'react-foundry'
import { useState } from 'react'
import { Button } from './button'
export const nav: NavPath = 'Forms/Button'
export const Primary = createPreview(() => <Button variant="primary">Go</Button>)
export const Danger = createPreview(() => <Button variant="danger">Careful</Button>)
// Hooks work. There is no separate concept for a preview that holds state.
export const Counter = createPreview(() => {
const [count, setCount] = useState(0)
return <Button onClick={() => setCount(count + 1)}>Clicked {count}</Button>
})
// Use the options form when the export name cannot express the label.
export const AllSizes = createPreview({
label: 'Every Size',
render: () => (
<>
{sizes.map((size) => (
<Button key={size} size={size} />
))}
</>
),
})
// Exported but not wrapped, so this is not a nav entry.
export const sizes = ['small', 'medium', 'large'] as constThat renders as:
Forms
Button
Primary
Danger
Counter
Every SizeThe rules:
navplaces the file. It is typed against your config, so it autocompletes. Omit it and the filename is used instead.- Only exports wrapped in
createPreviewbecome previews. Helpers, fixtures, and constants can live in the same file without leaking into the shelf. - Labels come from export names, de-camelCased:
AllSizesbecomesAll Sizes. Passlabelto override. - Order is what you wrote. Previews appear in source order, sections in config order. Nothing is sorted behind your back.
- URLs use the export name, never the label.
AllSizeslives at/Forms/Button/AllSizeswhatever you label it, so rewording a label never breaks a link. renderis a component. Foundry mounts it rather than calling it, so hooks work inside it in both forms, controls or not, and a controlled component's value can live right there with no wrapper component extracted to hold it. React keys that state onrender's identity, which is stable when it is written as a literal in a module-levelcreatePreviewcall, as above. Do not build previews in a factory that runs during render and recreatesrendereach time: every pass would be a new component to React, remounted with its state gone.
How discovery reads your files
Foundry finds previews by parsing the source, not by importing it. That is what keeps each preview in its own lazy chunk instead of the initial bundle: the whole shelf is built without evaluating a single preview module.
The cost is a small authoring convention. A preview must be written as:
export const Primary = createPreview(/* ... */)with createPreview referenced by that name. These forms parse as not a preview, and the
export simply will not appear in the shelf:
import { createPreview as preview } from 'react-foundry' // aliased import
export function Primary() {} // not a createPreview call
export { Primary } // re-export, not a declarationAn explicit label must likewise be a string literal at the top level of the options
object. A computed one (a template literal, a variable, a constant) falls back to the
de-camelCased export name rather than failing:
export const AllSizes = createPreview({
label: 'Every Size', // used
label: `Every ${thing}`, // ignored, falls back to "All Sizes"
render: () => null,
})A file that calls createPreview but yields no previews under these rules logs a warning
naming the file, so a silent miss is visible in the dev server output.
Two files cannot share both a nav path and an export name. A duplicate
(nav, exportName) pair means one of the two previews disappears, with a console warning;
the survivor is decided by lexicographic file path. This is a realistic hazard when
migrating from a tool where two files carried the same title.
Controls
Give a preview editable controls with defineControls, and render receives their live
values:
import { createPreview, defineControls } from 'react-foundry'
import { Button } from './button'
export const Playground = createPreview({
controls: defineControls({
variant: { type: 'select', options: ['primary', 'danger'], default: 'primary' },
disabled: { type: 'boolean', default: false },
label: { type: 'text', default: 'Click me' },
}),
render: (values) => (
<Button variant={values.variant} disabled={values.disabled}>
{values.label}
</Button>
),
})Values are typed from the schema, so values.variant narrows to 'primary' | 'danger'
and a typo is a compile error. Control types: text, boolean, number, range, select,
radio, color, and list for an array of any of them (see Lists).
The panel names each control after its key, humanized (onSurface reads "On Surface").
Give a control a label to name it yourself; the key still names the prop, and the
panel keeps the two linked by showing the key beside the prop's definition.
Every control carries an info mark. Hover or focus it for the prop the control drives, as
declared on the component: variant?: 'primary' | 'danger', with the prop's JSDoc under
it. The dev server reads these with the TypeScript compiler from the controlsFor call a
preview's controls came from, following a hoisted or imported schema and a spread of one
into another. Where there is nothing to read, a schema from defineControls or a project
without typescript, the mark shows the control's own definition instead: its kind,
options or range, and default. The docs for a preview are read when it is first opened,
which can take a moment while the compiler loads the project's types, and read again when
you edit a source file, so a change to a component's props shows up without a reload.
render is a component, so a preview of a controlled component keeps its value in a hook
right there, and a control edit re-renders it with the new props rather than remounting it:
export const Playground = createPreview({
controls: controlsFor(Select, {
width: { type: 'radio', options: ['auto', 'full'], default: 'auto' },
}),
render: (v) => {
const [value, setValue] = useState('a')
return <Select options={OPTIONS} value={value} onValueChange={setValue} width={v.width} />
},
})Note that a file declaring controls reloads the page on every edit rather than patching
in place, so the panel can never describe a schema the canvas has moved on from. See
Hot reloading.
Typing controls against a component
defineControls has no relationship to any component, so nothing stops a schema drifting
from the thing it claims to exercise. controlsFor takes the component too:
const cardControls = controlsFor(Card, {
title: { type: 'text', default: 'Alert rule' },
padding: { type: 'radio', options: ['small', 'medium', 'large'], default: 'medium' },
elevated: { type: 'boolean', default: false },
})A control naming no prop, a control type the prop cannot take, and an option outside the
prop's own union all stop compiling. A prop no input can express takes no plain control
at all, only a derive (below), so a text box on a render prop is a compile error rather
than a panel offering a value no call site can produce.
Either way, a controls schema is a props declaration. Written inline or through
defineControls, it declares props for the preview's own render function, which is itself a
component and free to render whatever it likes. controlsFor points that declaration at a
component you already have, so a preview cannot claim to exercise one whose props it does
not describe.
Worth knowing:
- Write the schema inline, as above. Building it separately and passing it by name
widens
type: 'select'tostringbeforecontrolsForsees it, and the error you get names the widening rather than the cause. - A
ReactNodeprop takes atextcontrol, since a string is a validReactNode. Sochildren: { type: 'text' }works. A props panel cannot author JSX; aderivecan, see below. - A prop typed
string | numbergets no plain control, and one typed2 | 3 | 4gets a plain number input rather than a dropdown, becauseoptionscurrently holds strings. - Props inherited from a DOM element come along, so a component extending
ComponentProps<'button'>offers everyaria-*attribute in autocomplete.
Deriving a prop's value from a control
Some props have types no input can express: a component (icon: LucideIcon), a node built
from a flag, an array you would rather size with a slider than edit row by row. Any scalar
control can carry a derive that maps the control's own value to the prop's type:
const selectControls = controlsFor(Select, {
options: { type: 'range', min: 1, max: 10, default: 4, derive: (n) => CLIENTS.slice(0, n) },
width: { type: 'radio', options: ['auto', 'sm', 'md', 'lg', 'full'], default: 'auto' },
})
const statCardControls = controlsFor(StatCard, {
icon: { type: 'select', options: ['globe', 'gauge'], default: 'globe', derive: (name) => ICONS[name] },
})
const pageLeadControls = controlsFor(PageLead, {
titleAccessory: { type: 'boolean', default: false, derive: (on) => (on ? <Badge content="Beta" /> : undefined) },
})The panel draws the input exactly as it would without the derive, and the URL holds the
input's value. What changes is what render receives: v.options is SelectOption[], not a
number. The key is still checked against the component, so optoins is still a compile
error; only the input type is freed, and only through the mapping. The return type is
checked against the prop, so a derive that returns a string where the prop wants a
component is a compile error too. The parameter is typed from the control: a range hands it
a number, a boolean a boolean, and a select the union of its options, so ICONS[name]
above needs no cast. A derive is accepted on a group member as well.
derive receives its own control's value and nothing else. It cannot read another control,
which keeps it a per-prop mapping rather than a place to compose the preview. That is the
line between the two helpers: derive produces one prop's value from one input, and
defineControls is for controls that drive a composition the preview assembles itself in
render. A derive on a select or radio in defineControls or an inline schema receives
string; the narrowing to the option union is controlsFor's.
Lists
An array prop takes a list: rows drawn from one of schema, which is a scalar control for
an array of strings or a group of them for an array of objects.
const selectControls = controlsFor(Select, {
options: {
type: 'list',
of: { value: { type: 'text' }, label: { type: 'text', default: 'Untitled' } },
default: [{ value: 'acme', label: 'Acme' }],
},
})
const tagControls = controlsFor(TagList, {
tags: { type: 'list', of: { type: 'select', options: ['new', 'beta'] }, default: ['new'] },
})The panel draws a section per row with the row's fields and a remove button, and an add
button that appends a row of the of defaults. render receives the array, typed from of:
v.options is { value: string; label: string }[]. The rows travel in the URL whole, as
JSON, and the list is left out of the URL while it equals its default.
controlsFor checks the row schema against the item type the same way it checks a group
against an object prop: a key the item does not have, a control the key cannot take, and a
typo in a default row are all compile errors. A row's controls may carry a derive, and a
select in a row narrows to its options as one at the top level does.
of is a control or a group, never another list, and a list cannot sit inside a group. That
is the one level of nesting below the list the schema allows, for the same reasons a group
holds no group: a deeper tree is hard to draw legibly in a panel and costs more type
instantiation than it earns.
Accessibility
Foundry runs axe-core against the canvas, scoped to your preview and nothing else: none of foundry's own chrome is in the scan.
Once enabled, the panel below the canvas scans on its own: on navigation, on a control change, and on a theme flip, each after a short debounce so the preview has settled before axe reads it back. Collapsing the panel suspends scanning... re-run check forces a scan whenever you want one.
Each violation lists the nodes that tripped it. The crosshair on a node outlines it in the canvas and scrolls it to center; click again to clear, or hover the button to preview the outline without committing to it.
Two things the panel reports rather than hides:
- Could not be checked is axe's third bucket: rules it ran but could not reach a verdict on, like text over a background image. They are counted separately so a clean Passed never claims more than was actually checked.
- Contrast is measured from rendered pixels, which bounds it twice. It describes the theme that was active at scan time, so the mode is labeled in the header, and it covers only the part of an overflowing canvas that was on screen. Scroll the rest into view and re-run.
Providers
Your components often rely on app-wide React context: a design-system theme provider, a
data client, i18n, a router. Give foundry a foundry.providers.tsx at your project root
that exports a Provider, and it wraps every preview in it, so components render the same
way they do in your real app.
import type { FoundryProvider } from 'react-foundry'
import { ThemeProvider } from '@my/design-system'
import { QueryClientProvider } from '@my/data'
export const Provider: FoundryProvider = ({ children, theme }) => (
<ThemeProvider mode={theme}>
<QueryClientProvider>{children}</QueryClientProvider>
</ThemeProvider>
)themeis foundry's resolved mode ('light' | 'dark'), so a design-system provider can track foundry's own light/dark toggle. Ignore it if you don't need it.- The provider wraps inside the preview canvas, so app context reaches your components while foundry's styling stays on foundry's side of that boundary. See Style isolation for exactly how far that guarantee reaches.
- It is mounted on every screen, including the home screen and a group landing where
no preview is selected. So a design system that does document-level work on mount (a
theme class on
<html>,dir, font loading, a portal root) has done it before you pick anything. - The file is optional. Without it, previews render unchanged.
The file may be foundry.providers.{tsx,jsx,ts,js}. Editing it reloads the page, since
every preview mounts inside it. Two things to know for a real project:
- The first time you add the file, foundry pulls its new dependencies into the graph, so Vite may re-optimize and reload once more than usual. That is expected, not a bug.
- If your provider imports workspace packages symlinked outside your project root,
foundry serves them automatically by allow-listing your detected workspace root. Only if
they live outside that root do you need to extend
viteConfig.server.fs.allow.
Extending Vite
Foundry runs its own Vite pipeline. To extend it (plugins, aliases, etc.), pass a partial
Vite config via viteConfig:
export default defineConfig({
viteConfig: {
plugins: [/* your plugins */],
resolve: { alias: { '@': '/src' } },
},
})If your components author their own styles with vanilla-extract, add its plugin here:
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'
export default defineConfig({
viteConfig: { plugins: [vanillaExtractPlugin()] },
})Provide a single React plugin instance only through viteConfig if you need to configure it;
foundry already includes @vitejs/plugin-react, so don't add a second copy.
Foundry pins Vite 8, which resolves tsconfig path aliases natively. If you reach for
vite-tsconfig-paths out of habit, Vite prints a deprecation notice on every start. Use the
built-in instead:
export default defineConfig({
viteConfig: { resolve: { tsconfigPaths: true } },
})If you do keep a plugin that needs your project directory, remember the config runs from a
cache directory: pass process.cwd(), not import.meta.dirname.
Style isolation
The guarantee runs one way: nothing foundry sets, resets, or inherits reaches your
component on the canvas. Foundry's resets are excluded from the canvas subtree, and its
typography is anchored on each piece of chrome rather than on body or html, which the
canvas would otherwise inherit from.
The reverse is not true. Your stylesheets are loaded into foundry's document, so they
reach foundry's own chrome: a CSS reset or preflight, element selectors, inherited
typography, and anything painted on body all apply to the shelf, toolbar and props panel
as well as to your components. That is usually harmless and occasionally not, so it is worth
knowing which side of the line you are on.
Two elements are foundry's:
#rootis foundry's mount point, and foundry paints its own layout root over it. A background you set there is covered rather than fighting the canvas for the viewport.bodyis painted with foundry's canvas color.
License
MIT
