ptbdynamic / plinth
Plinth, the design system of BDynamic's projects: Blade components, a Filament plugin and the built CSS.
Package info
Language:TypeScript
pkg:composer/ptbdynamic/plinth
Requires
- php: ^8.3
- illuminate/support: ^12.0|^13.0
- illuminate/view: ^12.0|^13.0
Requires (Dev)
None
Suggests
- filament/filament: To register Plinth's CSS in a Filament panel with PlinthPlugin.
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.3.0
- v0.2.0
- v0.1.1
- v0.1.0
- dev-fbarrento/release-0-3-0
- dev-fbarrento/switch-checkbox
- dev-fbarrento/one-time-code
- dev-feat/field
- dev-feat/code-block-download
- dev-fbarrento/docs-width-timeout
- dev-fbarrento/rtl-mirror
- dev-fbarrento/close-glyph
- dev-fbarrento/toolbar-target
- dev-fbarrento/timeline
- dev-fbarrento/toolbar
- dev-fbarrento/list-header-132
- dev-fbarrento/nav-list
- dev-fbarrento/app-shell
- dev-fbarrento/app-nav
- dev-fbarrento/icon-set
- dev-fbarrento/empty-state
- dev-feat/stat
- dev-fbarrento/release-workflow
- dev-fbarrento/release-0.2.0
- dev-fbarrento/menu
- dev-fbarrento/ci-speed
- dev-fbarrento/tailwind-breakpoints
- dev-fbarrento/text-field
This package is auto-updated.
Last update: 2026-10-26 13:19:29 UTC
README
The design system of BDynamic's projects: design tokens, components as plain HTML and CSS, and thin Blade, Vue and React layers that print the same markup, documented and tested in Storybook.
- Tokens are the source of truth: colour (light and dark), type, spacing, radii, layers, breakpoints and motion. They are written once and built to CSS custom properties, a JS/TS export and a Tailwind v4 theme file.
- Components are plain HTML and CSS. Each one has a markup contract (its elements, classes, states and ARIA) that every framework layer prints.
- Blade, Vue and React layers print that markup and hold no styles of their own.
- Storybook documents the plain HTML and tests it: accessibility and parity between the story, Blade, Vue and React.
- Each project brings its brand through the tokens. Plinth ships neutral defaults; a project overrides the custom properties and never forks a component.
Layout
packages/
tokens/ the tokens in DTCG JSON (tokens/*.json), built with Style Dictionary to CSS custom
properties, a JS/TS export and a Tailwind theme
css/ one stylesheet per component, tokens only, mobile first; builds dist/plinth.css
vue/ Vue 3 components that print the markup contract
react/ React 19 components that print the same markup contract
blade/ Composer package: Blade components, a Filament plugin, and the built CSS
storybook/ Storybook (HTML renderer): component stories, their docs pages, parity tests
site/ Plinth's website, a Laravel app built with Plinth's components, and the guides in
site/docs (see site/README.md)
The npm workspaces are private. They are published together as one npm package and one Composer package (nothing is published yet):
| Package | Registry | Holds |
|---|---|---|
@ptbdynamic/plinth |
npm | ./tokens, ./tokens.css, ./css, ./vue, ./react and ./tailwind.css |
ptbdynamic/plinth |
Packagist | <x-plinth::…> Blade components, PlinthPlugin for Filament, the built CSS and fonts in resources/dist |
Packagist reads the Composer package from the composer.json at the root of the repository, which
points its autoload and Laravel provider at packages/blade/. .gitattributes marks everything
else export-ignore, so a composer require downloads only packages/blade/src,
packages/blade/resources, composer.json, README.md and LICENSE (and CHANGELOG.md once
there is one). packages/blade/composer.json names the same package for the tests, the dev tools
and the site's path repository.
Working on Plinth
You need Node 22 (see .nvmrc), PHP 8.4 and Composer. npm ci also runs composer install in
packages/blade.
npm ci npx playwright install chromium # once, for the Storybook test runner npm run storybook # http://127.0.0.1:6006 (builds the tokens and CSS first) npm run build-storybook # static build in storybook/storybook-static npm run check # everything CI runs (turbo run lint test build build-storybook)
Turborepo runs each task in the order the packages depend on each other
(tokens → css → vue, react and blade → storybook and site) and caches the results in .turbo/, so a
second run with nothing changed is served from the cache. On a small machine, add
--concurrency=2.
turbo.json passes the MISE_*, __MISE_* and LERD_* variables through to tasks. Turborepo
strips the environment by default, and without those variables lerd's npm shim (which runs npm
through mise) finds itself on the PATH again and starts copies of itself until the machine runs
out of memory.
What the tests cover:
packages/tokens: Vitest on the build output. The three tiers are there, semantic colours are built from primitives in both themes, text colours meet WCAG AA, and the CSS and Tailwind output are right (Tailwind itself compiles the theme).packages/vue: Vitest with Vue Test Utils.packages/react: Vitest with Testing Library.packages/blade: Pest with Testbench, including the Filament plugin (composer testthere).storybook: Vitest parity tests render every story's markup, the Blade component, the Vue component and the React one, and compare them ignoring whitespace and attribute order. Then the Storybook test runner opens every story in light and dark at 390, 768 and 1440 px and checks it with axe.
Screenshot comparison is paused until the foundations settle (#9).
Tokens
The tokens are in packages/tokens/tokens/, in three tiers (ADR 1):
| Tier | File | Custom properties | Used by |
|---|---|---|---|
| Primitive | primitives.json |
--plinth-ref-color-neutral-50 |
semantic tokens only |
| Semantic | semantic.json, semantic.light.json, .dark.json |
--plinth-color-surface |
components; projects override these |
| Component | component.json, component.light.json, .dark.json |
--plinth-page-header-height |
one component |
The website has a page for each under Foundations (colour, typography, spacing and sizes,
radii and elevation, layers and breakpoints, motion, the focus ring and component tokens), each
showing the semantic tokens and the primitives they are built from. Its Markdown is in
site/docs.
Installing Plinth in a Laravel project
Once a version is on Packagist:
composer require ptbdynamic/plinth
Laravel discovers the service provider, and the components are available as <x-plinth::badge>.
In a Filament panel, add the plugin and publish its CSS and fonts:
use BDynamic\Plinth\Filament\PlinthPlugin; $panel->plugin(PlinthPlugin::make());
php artisan filament:assets
Outside Filament, publish them with php artisan vendor:publish --tag=plinth-assets and link
vendor/plinth/fonts/index.css and vendor/plinth/plinth.css, or import them in the app's Vite
CSS from vendor/ptbdynamic/plinth/packages/blade/resources/dist/.
Using Plinth from a project while you work on it
Link your local checkout instead of a published version. Build Plinth first
(npx turbo run build) and rebuild when you change it.
Laravel: add a Composer path repository to the project's composer.json:
{
"repositories": [
{ "type": "path", "url": "../plinth/packages/blade", "options": { "symlink": true } }
],
"require": {
"ptbdynamic/plinth": "@dev"
}
}
Then composer update ptbdynamic/plinth. In a Filament panel, add the plugin and publish its
CSS:
use BDynamic\Plinth\Filament\PlinthPlugin; $panel->plugin(PlinthPlugin::make());
php artisan filament:assets
Outside Filament, publish the CSS and fonts with php artisan vendor:publish --tag=plinth-assets
and link vendor/plinth/fonts/index.css and vendor/plinth/plinth.css. The Filament plugin
registers both itself. Components are then available as <x-plinth::badge>.
Vue: point npm at the checkout with a file: link:
{
"dependencies": {
"@ptbdynamic/plinth": "file:../plinth"
}
}
import '@ptbdynamic/plinth/fonts.css'; // Geist and Geist Mono, the default typefaces import '@ptbdynamic/plinth/css'; import { PlinthBadge } from '@ptbdynamic/plinth/vue';
React (an Inertia React app, or any React 19 app): the same file: link, then
import '@ptbdynamic/plinth/fonts.css'; import '@ptbdynamic/plinth/css'; import { PlinthButton } from '@ptbdynamic/plinth/react'; <PlinthButton variant="secondary" onClick={save}> Save </PlinthButton>;
The React components take the Vue props by the same names. Vue's default slot is children, and
its named slots are props that take a node (icon, trailing). Everything else, className
and ref included, reaches the root element. They render the same on the server (Inertia's SSR)
and in the browser.
Tailwind v4: Plinth's CSS is in @layer plinth. Place that layer after Tailwind's reset and
before its utilities, so a utility class can adjust a component without !important. Then import
the theme file, so page code gets the tokens as utilities (bg-surface, text-text-muted, p-4,
rounded-md):
@layer theme, base, plinth, components, utilities; @import 'tailwindcss'; @import '@ptbdynamic/plinth/css'; @import '@ptbdynamic/plinth/tailwind.css';
Your brand: override the semantic tokens after Plinth's CSS, outside any layer (Plinth's
own are inside @layer plinth, so yours win). Storybook's example brand,
storybook/brands/example.css, is a complete example; switch to
it with Brand in the toolbar.
:root { --plinth-color-accent: #0f766e; --plinth-color-accent-hover: #115e59; /* override these two with accent */ --plinth-color-accent-active: #134e4a; --plinth-radius-pill: 0.25rem; --plinth-font-family-body: 'Your Font', system-ui, sans-serif; } [data-theme='dark'] { --plinth-color-accent: #14b8a6; }
Dark mode follows the system, or data-theme="dark" / the .dark class on any ancestor;
data-theme="light" forces light.
Releases
Plinth has one version for npm and Packagist, and one tag, v<version>
(ADR 4). What changed is in
CHANGELOG.md (from the first release on), written by Changesets:
npx changeset # in a pull request that changes a package: the bump and a line for the changelog npm run release:version # on a branch: the next version, in every package and the changelog npm run release -- --dry-run # what npm and Packagist would get, without tagging or publishing npm run release # on main, after the version pull request merges: tag and push
The dry run (CI runs it on every pull request) also checks the Composer side: the root
composer.json and packages/blade/composer.json name the same package with the same runtime
requirements and provider and have no version (Packagist takes it from the tag),
composer validate --strict passes on the root one, and git archive of the tag (or of HEAD
before tagging) holds what the package needs and nothing else.
Pushing the tag starts the release workflow (.github/workflows/release.yml). It runs the same
checks, builds the packages, publishes @ptbdynamic/plinth to npm with
trusted publishing and provenance (no npm token is
stored and no one-time code is asked for), and makes the GitHub release with the version's
section of CHANGELOG.md as its notes. Packagist publishes the tag through its GitHub hook. A
version already on npm, or a release that already exists, is skipped, so to repeat a release run
the workflow by hand with its tag:
gh workflow run release.yml -f tag=v1.2.0
A run started by hand builds the commit it runs on (main), and refuses if what the npm package ships has changed since the tag.
One-time steps for the owner
Packagist is set up through its GitHub hook. For npm, on npmjs.com open the package
@ptbdynamic/plinth → Settings → Trusted Publisher, choose GitHub Actions and enter
the organisation ptbdynamic, the repository plinth and the workflow release.yml (no
environment). Nobody needs to run npm login to release.
Contributing
See CONTRIBUTING.md for the rules every component follows.