noxis-design
v0.1.0
Published
NOXIS Design — the design system of the block composer: shadcn UI primitives, composable sections with zod-first prop schemas, StyleScope (style × mode × brand).
Readme
NOXIS Design
The design system of the block composer. The same bricks — shadcn UI primitives and composable sections — dress different documents: sites and (soon) emails. Published on npm as compiled ESM + type declarations, self-contained: importing a component also loads the precompiled stylesheet — no Tailwind setup, no CSS imports on the consumer side.
src/
index.ts package entrypoint
components/
ui/ 60 shadcn primitives (Button, Dialog, Table…)
sections/ the section catalog, one file per category
section-view.tsx renders a section by type through the registry
lead-form.tsx lead-form slot (the app injects the real form)
style-scope.tsx StyleScope: style × mode × brand on a scope
lib/
sections.ts registry: SECTIONS, SECTION_LIST, WEB/EMAIL lists
section-types.ts Section/Field types, CATEGORIES, prop readers
section-fields.ts zod-first f.* helpers, props(), schemaToFields()
section-schema.ts validateBlocks(), sectionJsonSchema(), tool catalog
styles.ts the 8 styles, palettes, resolveStyle()
safe-url.ts safeUrl()/safeImg() sanitizers
styles/
entry.css build input for the SHIPPED stylesheet
noxis.css precompiled stylesheet (generated, auto-imported)
tokens.css shared design tokens (theme mapping + palettes)
globals.css dev/Storybook Tailwind entry
animations.css @theme keyframe tokens (animate-nx-*)
stories/ Storybook catalog (sections, components, demo sites)Consuming the package
npm i noxis-designThat's it. Components side-effect-import styles/noxis.css (precompiled at
build time from every class the package uses): no @source, no CSS import,
no Tailwind required in the consumer. The stylesheet ships without
preflight and its default palette sits in @layer base, so it never
overrides the host app's own tokens — and inside StyleScope the inline
variables always win.
Using sections as plain components
Every section is exported as a typed React component (props = its zod schema, all optional — renders have safe fallbacks):
import { StyleScope } from "noxis-design"
import { HeroSplit, HeroCinematic } from "noxis-design/sections/hero"
import { PricingBeam } from "noxis-design/sections/pricing"
import { Button } from "noxis-design/ui/button"
<StyleScope style="sera" mode="dark" primary="#b08d57">
<HeroSplit title="Benvenuti" subtitle="…" ctaText="Prenota" ctaHref="/book" />
<PricingBeam title="Listino" />
</StyleScope>Category entrypoints: noxis-design/sections/<category> (hero, features,
pricing, faq, …); everything is also re-exported from the root and from
noxis-design/sections. Primitives: noxis-design/ui/<name>.
Rendering a document (composer model)
A document is a JSON array of blocks. Wrap it in a StyleScope (the dress)
and render each block through the registry:
import { StyleScope, SectionView } from "noxis-design"
const blocks = [
{ type: "header-simple", props: { brand: "Acme", links: [...] } },
{ type: "hero-split", props: { title: "…" } },
{ type: "footer-columns", props: { brand: "Acme" } },
]
<StyleScope style="sera" mode="dark" primary="#b08d57">
{blocks.map((b, i) => (
<SectionView key={i} type={b.type} props={b.props} />
))}
</StyleScope>StyleScope has three independent axes:
style— one of the 8 shadcn/create styles (veganovamaialyramiralumaserarhea): ONLY the visual dress (font + radius).mode—"light" | "dark", explicit, never auto. Picks the neutral palette (real shadcn registry oklch values).primary— the brand color (hex): becomes--primary/--ringwith contrast-safe text. Optionalaccentfor a second color.
The section contract
Each section declares its props ONCE as a zod schema; everything else is derived from it:
schema: props({
brand: f.text("Brand name", { required: true }),
links: f.list("Navigation links", LINK_SHAPE, { itemLabel: "Link", max: 6 }),
logoUrl: f.image("Logo (square)"),
})schemaToFields(schema)→ the builder form spec (key, label, widget, optionality, nested sub-fields).validateBlocks(blocks)→ parse a whole document: cleaned blocks (unknown keys stripped) + actionable issues per block. Use it on AI output, imports and saves; renders stay defensive on their own.sectionToolCatalog(target)→ the exposed catalog in the standard LLM-tool shape[{ type, name, category, description, parameters }](parameters = JSON Schema).WEB_SECTION_LIST/EMAIL_SECTION_LIST— one catalog, two documents: sections declaretarget: "web" | "email" | "both"(default web).
Email documents
Email clients don't support CSS variables, oklch or webfonts, so email
sections are table-based and inline-styled, with the style axes resolved to
CONCRETE values (hex neutrals, web-safe font stacks, px radius) by
resolveEmailTheme and injected by the renderer:
import { EmailDocument, sectionToolCatalog } from "noxis-design"
// the AI composes from sectionToolCatalog("email") — 10 sections:
// email-header, email-hero, email-text, email-split, email-image,
// email-list, email-products, email-quote, email-cta, email-footer
<EmailDocument style="sera" mode="light" primary="#b08d57" blocks={blocks} />
// render to static markup and hand the HTML to your sender; merge tags and
// the unsubscribe URL (email-footer's `unsubscribeUrl`) belong to the senderStorybook: the Email page renders the full catalog through the same Style × Mode × Brand toolbar.
Section renders are PURE (no hooks, no state — <details> is fine): they
run on public sites, in the builder preview and in Storybook. Read props
defensively with ps/pobjs/pstrs; sanitize every href/src with
safeUrl/safeImg; use semantic Tailwind tokens only (bg-background,
text-muted-foreground, bg-primary… never hex).
Animations
animations.css defines keyframe tokens only (--animate-nx-* →
animate-nx-marquee, animate-nx-fade-up, animate-nx-settle,
animate-nx-clip-up, animate-nx-parallax, animate-nx-float,
animate-nx-gradient). Sections compose them inline with Tailwind:
supports-[animation-timeline:view()]: gating, [animation-range:…],
motion-reduce:animate-none, group/marquee + group/zoom hover
patterns. Marquee duration: --nx-marquee-duration; parallax intensity:
--nx-parallax-amount.
Storybook
npm run storybook # port 6012
npm run build-storybook
npm run typecheckCatalog layout: Introduction · Sections (one entry per category, "All variants" first) · Components (one Docs leaf per primitive: base + editable controls + variants) · Demo Sites (20 full Italian-market pages). The toolbar switches Style × Mode × Brand everywhere; demo sites keep their own dress until you make an explicit choice.
Adding a section
- Add the file in
src/components/sections/<category>/exportingexport const SECTION = defineSection({ … })(type, label, category, description,schema: props({…}), defaults, rich Englishsample, pure Render).defineSectionkeeps the zod shape → typed component. - Import it in the category
index.ts(SECTIONSarray), then runnpm run gen:componentsto regenerate the typed component exports. - Register the type id in
LISTED_EXTRA_TYPES(src/lib/sections.ts). - Add the story export in the category's file under
src/stories/sections/(or regenerate; keep story name = label suffix).
