Skip to content

Latest commit

 

History

693 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Visor — One component system. Total Control. By Low Orbit Studio

visor-core version CLI version theme-engine version CI License npm downloads bundle size Documentation


What is Visor?

Visor is a theming-first React component library built by Low Orbit Studio. It uses a two-layer distribution model that gives you full control over your components while keeping design consistency effortless:

Layer 1 — Components (copy-and-own). Run npx visor add button and the source files are copied directly into your project. You own them. Edit them freely. No runtime dependency on Visor.

Layer 2 — Tokens (@loworbitstudio/visor-core). The only npm package. It provides all the CSS custom properties that Visor components reference. Update the package and design changes cascade to every component automatically — without touching a single component file.

This model is inspired by shadcn/ui's copy-and-own approach, combined with a shared token layer that keeps multi-project consistency without locking you in.


Quick Start

One command. Runnable Next.js app. Borealis-native.

In an empty directory, run:

npx @loworbitstudio/visor init --template nextjs

That single command scaffolds a complete, runnable Next.js App Router project pre-wired with Visor:

  • package.json with next, react, TypeScript, and @loworbitstudio/visor-core + @loworbitstudio/visor-theme-engine already installed.
  • app/layout.tsx with the FOWT (Flash of Wrong Theme) prevention script inline in <head>, globals.css imported, and the theme's declared color-scheme applied at the root — a dark-only brand scaffolds <html className="dark"> + color-scheme: dark so it renders dark even on a light-preference browser; light-only is the inverse; adaptive follows prefers-color-scheme.
  • app/globals.css generated from .visor.yaml via the Visor Next.js adapter.
  • app/page.tsx, tsconfig.json, next.config.ts, .gitignore — the full create-next-app baseline.
  • .lo/borealis.json stamp recording the Visor version that initialized the project.

Then start the dev server:

cd my-app && npm run dev

Add your first component:

npx visor add button

That's it. The component source lands in your project and you own it. No FOWT flash, no missing config, no second setup step.

Heads up: visor init --template nextjs only scaffolds into empty directories — it refuses if package.json already exists so it never destructively overwrites in-flight work. For an existing app, use the manual setup below.

Manual setup (non-Next.js or retrofit)

For non-Next.js projects, or to retrofit Visor into an existing app:

1. Initialize Visor

npx @loworbitstudio/visor init

This creates a visor.json in your project root with default path mappings:

{
  "paths": {
    "components": "components/ui",
    "hooks": "hooks",
    "lib": "lib"
  }
}

2. Import tokens into your global CSS

/* app/globals.css or src/index.css */
@import "@loworbitstudio/visor-core";

3. Add your first component

npx visor add button

Adding Components

Add components one at a time or in bulk:

npx visor add input
npx visor add card
npx visor add button input label card

Available Components

The registry ships 88+ UI components across 6 categories, plus admin compounds, blocks, and hooks.

Form (24) button · calendar · checkbox · combobox · date-picker · field · fieldset · file-upload · form · input · label · number-input · otp-input · password-input · phone-input · radio-group · search-input · select · slider · slider-control · switch · tag-input · textarea · toggle-group

Data Display (12) accordion · avatar · carousel · code-block · collapsible · heading · image · progress · separator · skeleton · text · timeline

Navigation (5) breadcrumb · command · navbar · pagination · stepper

Overlay (7) context-menu · dialog · fullscreen-overlay · hover-card · lightbox · menubar · popover

Feedback (6) alert · banner · chart · table · toast · tooltip

Layout (9) badge · box · card · container · grid · inline · sheet · sidebar · stack

The five primitives box, container, grid, inline, and stack are token-typed layout building blocks: all spacing, surface, and radius props accept only Visor token names — off-system values are TypeScript errors. Pair them with Card, Sheet, and Sidebar for full-page chrome.

Add an entire category at once:

npx visor add --category form      # Add all form components
npx visor add --category overlay   # Add all overlay components

Admin Components

11 compound components for data-heavy admin UIs. Add with --category admin:

npx visor add --category admin
Component CLI Name Description
Activity Feed activity-feed Timestamped event stream
Bulk Action Bar bulk-action-bar Floating bar for multi-select actions
Confirm Dialog confirm-dialog Destructive action confirmation modal
Data Table data-table Sortable, filterable table with pagination
Empty State empty-state Zero-data placeholder with CTA
Filter Bar filter-bar Composable filter chip row
Infographic Bar infographic-bar Continuous stat-card band with hairline dividers
Kbd kbd Keyboard shortcut display
Matrix Table matrix-table Members×roles boolean assignment grid
Page Header page-header Title + actions header for admin pages
Stat Card stat-card KPI metric card with trend
Status Badge status-badge Semantic status indicator

Blocks

19 full-page and section-level blocks. Add with --block:

npx visor add admin-dashboard --block
npx visor add hero-section --block

A block installs only the components it imports to render itself. Slot-driven blocks (e.g. admin-shell) also declare suggested slot-fill components — example fillers that are skipped by default so a slot-only compose stays lean. Pull them with --with-suggested:

npx visor add admin-shell --block                  # hard deps only (no Radix)
npx visor add admin-shell --block --with-suggested # + breadcrumb, dropdown-menu, sidebar
Block CLI Name Category
Admin Dashboard admin-dashboard Admin
Admin Detail Drawer admin-detail-drawer Admin
Admin List Page admin-list-page Admin
Admin Settings Page admin-settings-page Admin
Admin Shell admin-shell Admin
Admin Tabbed Editor admin-tabbed-editor Admin
Admin Wizard admin-wizard Admin
CTA Section cta-section Marketing
Features Grid features-grid Marketing
Footer Section footer-section Marketing
Hero Section hero-section Marketing
Pricing Section pricing-section Marketing
Steps Section steps-section Marketing
Testimonial Section testimonial-section Marketing
Login Form login-form Auth
Configuration Panel configuration-panel Configuration
Design System Deck design-system-deck Documentation
Design System Specimen design-system-specimen Documentation
Sphere Playground sphere-playground Visual

Available Hooks

General (10) use-boolean · use-click-outside · use-currency · use-debounce · use-focus-trap · use-intersection-observer · use-keyboard-shortcut · use-local-storage · use-media-query · use-previous

Deck (4) use-intersection-animation · use-keyboard-nav · use-slide-engine · use-wheel-nav

npx visor add use-boolean
npx visor add use-debounce
npx visor add use-slide-engine

How It Works

When you run npx visor add button, two files land in your project:

your-project/
├── components/
│   └── ui/
│       └── button/
│           ├── button.tsx           ← React component (yours to edit)
│           └── button.module.css    ← Component styles (yours to edit)
└── lib/
    └── utils.ts                     ← cn() helper, added once and shared

Components use CSS Modules for scoped class names and CSS custom properties from the tokens package for all design values:

/* button.module.css */
.base {
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}

.variantDefault {
  background-color: var(--interactive-primary-bg);
  color: var(--interactive-primary-text);
}

Variants are managed with CVA:

// button.tsx
const buttonVariants = cva(styles.base, {
  variants: {
    variant: {
      default: styles.variantDefault,
      secondary: styles.variantSecondary,
    },
    size: {
      sm: styles.sizeSm,
      md: styles.sizeMd,
    },
  },
  defaultVariants: { variant: "default", size: "md" },
})

Theming

Theming is Visor's core differentiator. Every component references CSS custom properties — never hard-coded values. Swap the token values and the entire UI follows.

The 3-Tier Token Architecture

Tier 1: Primitives         Tier 2: Semantic          Tier 3: Adaptive
--color-gray-900    ──→    --text-primary      ──→   :root { --text-primary }
--color-gray-50     ──→    --surface-page      ──→   .theme-dark { ... }
--radius-lg         ──→    --border-default

Components only reference Tier 2 (semantic) tokens. This means overriding a single semantic token updates every component that uses it.

Dark Mode

Visor ships with a dark theme out of the box. Apply it by adding .theme-dark to your root element:

<html class="theme-dark">

Overriding Tokens

Override any token after your @import statement — no forking required:

/* globals.css */
@import "@loworbitstudio/visor-core";

:root {
  /* Rebrand the primary color across the entire system */
  --interactive-primary-bg: #6366f1;
  --interactive-primary-bg-hover: #4f46e5;
}

.theme-dark {
  --interactive-primary-bg: #818cf8;
}

Creating a Custom Theme

/* styles/theme-brand.css */
.theme-brand {
  --surface-page: #0a0a14;
  --surface-card: #12121f;
  --text-primary: #f0f0ff;
  --text-secondary: #a0a0c0;
  --interactive-primary-bg: #6366f1;
  --interactive-primary-text: #ffffff;
  --border-default: rgba(255, 255, 255, 0.1);
}
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="en" className="theme-brand">
      <body>{children}</body>
    </html>
  )
}

Creating a Theme from .visor.yaml

Define your theme in a YAML file and generate framework-specific CSS:

# .visor.yaml
name: my-brand
version: 1
colors:
  primary: "#6366f1"
# Generate Next.js globals.css with @layer support
npx @loworbitstudio/visor theme apply .visor.yaml --adapter nextjs

# Generate fumadocs bridge tokens
npx @loworbitstudio/visor theme apply .visor.yaml --adapter fumadocs

# Generate scoped deck CSS
npx @loworbitstudio/visor theme apply .visor.yaml --adapter deck

# Generate docs-site CSS (class-scoped, includes fumadocs bridge)
npx @loworbitstudio/visor theme apply .visor.yaml --adapter docs

Register a theme in the Visor docs site in one command:

# Creates CSS file, updates globals.css and theme-config.ts
npx @loworbitstudio/visor theme register .visor.yaml --group "Client"

# Preview changes without writing
npx @loworbitstudio/visor theme register .visor.yaml --group "Client" --dry-run

# Remove a theme
npx @loworbitstudio/visor theme unregister my-brand

Or scaffold a complete themed project:

npx @loworbitstudio/visor init --template nextjs

FOWT Prevention

Already wired automatically when you use npx @loworbitstudio/visor init --template nextjs. The steps below are only needed for manual setups or non-Next.js apps.

FOWT prevention covers two orthogonal axes. Add the script(s) you need as blocking <script> tags in your <head>, before any stylesheets.

Mode axis (dark/light). Reads localStorage('visor-theme'), falls back to prefers-color-scheme, and sets .dark or .light on <html> before first paint:

import { FOWT_SCRIPT } from '@loworbitstudio/visor-theme-engine/fowt';

// In your layout.tsx <head>:
<script>{FOWT_SCRIPT}</script>

Theme-identity axis (palette). For apps that switch between N registered themes at runtime, generateThemeFowtScript validates the stored theme name against an allowlist (falling back to a default), stamps it on <html>, and enables exactly the matching inlined <style data-theme-css>:

import { generateThemeFowtScript } from '@loworbitstudio/visor-theme-engine/fowt';

const themeScript = generateThemeFowtScript({
  themes: ['entr', 'space'],   // registered-theme allowlist
  defaultTheme: 'entr',        // fallback when nothing valid is stored
  // storageKey: 'visor-theme-name',  // optional, this is the default
  // attribute: 'data-theme-name',    // optional, this is the default
});

// In your layout.tsx <head>, AFTER the inlined <style data-theme-css="..."> tags:
<script>{themeScript}</script>

Both scripts are ES5-safe and can live in the same <head> — the mode axis toggles a class, the palette axis toggles which theme stylesheet is active.

Importing Specific Token Layers

@import "@loworbitstudio/visor-core/primitives";   /* Tier 1: raw values */
@import "@loworbitstudio/visor-core/semantic";     /* Tier 2: purpose-named */
@import "@loworbitstudio/visor-core/themes/light"; /* Tier 3: light theme */
@import "@loworbitstudio/visor-core/themes/dark";  /* Tier 3: dark theme */

CSS Layer Architecture

Visor's distributed CSS uses CSS Cascade Layers so generated themes win the cascade without consumer intervention.

Every shipped dist/*.css file declares the same layer order and wraps its content in the matching tier:

@layer visor-base, visor-primitives, visor-semantic, visor-brand, visor-adaptive, visor-bridge;
Layer Source Purpose
visor-base @loworbitstudio/visor-core/reset (opt-in), generated themes (visor theme apply --adapter nextjs) Element baseline — binds theme tokens to html/body and normalises UA element defaults
visor-primitives @loworbitstudio/visor-core/primitives Raw token values (colors, spacing, type)
visor-semantic @loworbitstudio/visor-core/semantic Purpose-named tokens (--text-primary, --surface-card)
visor-brand Generated themes (overrides pass-through) Brand asset vars (--brand-*)
visor-adaptive @loworbitstudio/visor-core/themes/*, generated themes (visor theme apply --adapter nextjs) Light/dark-aware tokens, generated theme overrides
visor-bridge Framework integrations (e.g. fumadocs) Maps Visor tokens onto framework-native variables

Cascade rules at a glance:

  • Per the CSS spec, unlayered styles always beat layered styles. So your bare :root { ... } overrides written after @import "@loworbitstudio/visor-core" continue to win — that pattern still works as documented above.
  • visor-base is deliberately lowest. Your own unlayered body { ... } and every component .module.css (which uses no @layer at all) beat it unconditionally. Author-origin still beats the browser's UA stylesheet regardless of layer, so the reset does its job without ever fighting your code.
  • Generated themes win over visor-core defaults. Both sit in @layer visor-adaptive, and last-loaded wins within a layer, so importing a generated theme after visor-core gives the theme its expected priority.
  • Stock themes ship layered too. When you import @loworbitstudio/visor-core/themes/blackout (or any other stock theme), the .{slug}-theme class still wins on selector specificity but its rules participate in visor-adaptive so they coexist cleanly with generated themes.

Updating

Updating a Component

Re-run the CLI with --overwrite to pull the latest upstream version:

npx visor add button --overwrite

Because you own the files, the CLI shows a diff before overwriting. If you've customized the component, use git to merge:

  1. Commit your customizations.
  2. Run npx visor add button --overwrite.
  3. Use git diff to review what changed.
  4. Merge your customizations into the updated version.
  5. Commit the result.

Updating Tokens

Token updates are standard npm updates:

npm update @loworbitstudio/visor-core

Token updates propagate automatically to all components. No component files change.


CLI Reference

# Setup
npx @loworbitstudio/visor init                              # Create visor.json config
npx @loworbitstudio/visor init --template nextjs            # Initialize with Next.js template

# Components
npx @loworbitstudio/visor add <component>                   # Add a component, hook, or lib entry
npx @loworbitstudio/visor add <c1> <c2> <c3>                # Add multiple at once
npx @loworbitstudio/visor add --category <name>             # Add all items in a category
npx @loworbitstudio/visor add <component> --block           # Add a block (hard deps only)
npx @loworbitstudio/visor add <block> --block --with-suggested # + a block's suggested slot-fill components
npx @loworbitstudio/visor add <component> --overwrite       # Update an existing component
npx @loworbitstudio/visor list                              # List all available components
npx @loworbitstudio/visor list --category <name>            # List by category
npx @loworbitstudio/visor diff [component]                  # Show local vs. registry differences
npx @loworbitstudio/visor suggest --for "<use case>"        # Find components for a use case
npx @loworbitstudio/visor suggest --for "<use case>" --json # JSON output (for AI agents)

# Themes
npx @loworbitstudio/visor theme apply <file>                # Generate CSS from .visor.yaml
npx @loworbitstudio/visor theme apply <file> --adapter nextjs     # Next.js adapter
npx @loworbitstudio/visor theme apply <file> --adapter nextjs --scope-prefix 'body.my-theme'  # Body-class scoped output
npx @loworbitstudio/visor theme apply <file> --adapter fumadocs   # fumadocs adapter
npx @loworbitstudio/visor theme apply <file> --adapter deck       # Deck adapter
npx @loworbitstudio/visor theme validate <file>             # Validate a .visor.yaml theme
npx @loworbitstudio/visor theme export [file]               # Export theme to YAML/JSON
npx @loworbitstudio/visor theme extract                     # Extract .visor.yaml from existing CSS
npx @loworbitstudio/visor theme register <file>             # Register theme in the docs site
npx @loworbitstudio/visor theme unregister <slug>           # Remove a theme from the docs site
npx @loworbitstudio/visor theme sync                        # Re-generate CSS for all themes

# Fonts
npx @loworbitstudio/visor fonts add <path> --org <name>     # Upload woff2 to Visor Fonts CDN

# Render fidelity
npx @loworbitstudio/visor render <component> --theme <slug> --mode <light|dark>  # Render one real component to a PNG (serverless)
npx @loworbitstudio/visor render doc-nav --theme space --mode dark               # Real tokens + real theme CSS + real component
npx @loworbitstudio/visor render button --theme neutral --state hover            # Capture an interactive state

visor render is a per-component render-fidelity harness: it composes the real emitted tokens + real per-theme CSS (with correct @layer/mode scoping) + the real esbuild-bundled component, then screenshots it with Playwright — no next dev. A computed-style probe confirms the themed surface resolved to its mapped value, not the raw primitive. playwright and esbuild are lazy/optional deps (not bundled into the CLI); the command prompts to install them if missing.

All commands support --json for structured output (useful for AI agents and scripts).


AI Agent Consumability

Visor includes structured metadata that makes it easy for AI agents to discover, understand, and compose components without reading source code.

Per-component metadata — Each component has a .visor.yaml file alongside its source with props, variants, slots, dependencies, usage examples, and "when to use" / "when not to use" guidance.

Registry manifestvisor-manifest.json is auto-generated during build, aggregating all component metadata (including auto-extracted CSS tokens) into a single file an agent can load.

Composition recipes — Recipe files in patterns/ document how components combine for common use cases (form with validation, dashboard layout, data table with filters). Visor calls these composition recipes, distinct from a design-language pattern.

See docs/ai-consumability.md for the full spec.


Stack


Documentation

Full documentation, component previews, and a props reference are available at:

visor.loworbit.studio


Built with Visor

  • Kaiah — AI-powered marketing platform
  • Blacklight — Music industry intelligence tool

Using Visor in your project? Open a PR to add it here.


Contributing

See CONTRIBUTING.md for guidelines on submitting components, token changes, and bug fixes.

To develop locally:

git clone https://github.com/loworbit/visor.git
cd visor
npm install

npm test               # Run tests
npm run typecheck      # Type check
npm run lint           # Lint
npm run build          # Build all packages
npm run docs:dev       # Start docs site
npm run widgetbook:dev         # Start Flutter widgetbook preview (requires Flutter SDK)
npm run themes:apply-flutter  # Regenerate packages/visor_themes/ for all 11 themes

Changesets (every shipping-package change)

Every PR that touches shipping-package source needs a .changeset/*.md file. The Changeset Gate workflow blocks merge if one is missing or malformed.

Automatic generation (recommended). The pre-push git hook runs scripts/generate-changeset.mjs before every push. If the diff touches a shipping path (any directory listed in changeset-paths.jsoncomponents/, blocks/, hooks/, lib/, registry/, themes/, patterns/, assets/, or the src//lib/ trees of the published packages) and no operator-authored changeset exists yet, Claude will write .changeset/<branch-slug>.md and stage it automatically. The same changeset-paths.json drives the CI changeset gate, so the local hook and CI stay in sync.

Requirements: claude CLI must be installed globally (npm install -g @anthropic-ai/claude-code). If it's not available, the hook prints a warning and the push proceeds normally.

Manual generation. Run at any time:

node scripts/generate-changeset.mjs
# or the standard interactive way:
npm run changeset

On-demand via Claude Code. The /lo-changeset skill at .claude/skills/lo-changeset/SKILL.md wraps the same script:

/lo-changeset

Bypass the hook. Skip changeset generation for a push:

git push --no-verify

Auto-generated marker. Generated changesets include # generated-by: lo-changeset in their YAML frontmatter. If you edit the changeset and remove that marker, it becomes operator-authored — the hook will not overwrite it on subsequent pushes. Operator overrides always win.

Failure handling. If claude fails for any reason, the hook exits 0 and the push proceeds. Run npm run changeset manually if you need a minor/major bump and the auto-generation failed.

Prompt source. scripts/changeset-prompt.md contains the bump-type rules and output format. Edit it to tune the AI's behavior.

Repository Structure

visor/
├── components/ui/     # Component source + .visor.yaml metadata
├── hooks/             # Hook source (registry entries)
├── lib/               # Utility source (registry entries)
├── patterns/          # Composition recipes (.visor-pattern.yaml)
├── registry/          # Registry schema and definitions
└── packages/
    ├── cli/           # @loworbitstudio/visor CLI + manifest builder
    ├── tokens/        # @loworbitstudio/visor-core npm package
    ├── visor-flutter/ # visor_core Flutter package (pub.dev)
    ├── visor_themes/  # All 11 Visor ThemeData — generated, do not edit
    ├── widgetbook/    # Flutter widgetbook preview app
    └── docs/          # fumadocs documentation site

Flutter widget quality

Every visor_* Flutter widget is audited against the Flutter Widget Quality Contract — a tiered checklist (Required / Recommended / Stretch) covering tokens, semantics, touch targets, reduce-motion, RTL, tests, and a11y matchers. Required-tier compliance gates a widget being marked production-ready.


Operator workflows

Day-to-day publishing is automatic for the three public npm packages — .changeset/*.md files written on each PR drive the bumps, and release.yml opens a "Version Packages" PR that publishes on merge. @low-orbit-studio/visor-themes-private still auto-versions on its own merges. The commands below surface only for the rare cases where a human is in the loop: health checks and cross-repo coordinated releases.

Publishing health and coordinated releases

/lo-visor-publish (see .claude/skills/lo-visor-publish/SKILL.md) has two modes:

  • status — read-only drift report across all 4 publishable artifacts. Non-zero exit on drift, so it can gate other workflows.
    node scripts/visor-publish-status.mjs
  • coordinate <visor-PR> <themes-PR> — single-confirmation cross-repo release for the case where a feature spans Visor + visor-themes-private and both must ship together.
    node scripts/visor-publish-coordinate.mjs 369 2 --dry-run   # preview only
    node scripts/visor-publish-coordinate.mjs 369 2             # live

The skill itself contains no publish logic. Each repo's existing CI (release.yml on the Visor side, themes-private's publish.yml) remains the source of truth for what publishes. See docs/audits/publish-automation.md for the full audit.

Publish-gate audit (PR comment governance)

When the visor-publish-smoke workflow detects drift between the source on main and the latest published @loworbitstudio/visor tarball, the audit step maps each drifted primitive back to the PR that landed it and posts a comment there — so "merged" eventually catches up with "shipped." Uses the built-in GITHUB_TOKEN, no extra secrets required. Run it locally with npm run audit:publish. Full background in CLAUDE.md § Publish Gate and docs/wisdom/W029-vi-ticket-publish-governance.md.


Sustainability

Visor is free and open-source, built and maintained by Low Orbit Studio. If it's useful to you, here's how to support it:

  • Use it and share it — the best support is adoption and word of mouth.
  • Contribute — bug reports, PRs, and Discussions participation all help.
  • Hire us — Low Orbit Studio takes on product and design system work. Get in touch.

License

See LICENSE for details.


Built by Low Orbit Studio — Brooklyn, NY.