diff --git a/AGENTS.md b/AGENTS.md index 6ddfe6df..4035d2e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,3 +26,14 @@ public registry and binds to each host through an explicit adapter. Use Node 24 LTS and pnpm 10.28.0, then run `pnpm check`, `pnpm build:pages`, and `pnpm pack:local` before release-related changes. + +## Local development + +- Docs app: `pnpm --filter blocks dev` serves `http://localhost:3005` (the + `predev` hook builds packages and checks generated demo source first). +- Open the dev server directly. Reverse proxies that do not forward Next's dev + WebSocket/streaming requests (e.g. agent browser-preview proxies) return + `502` on `/_next/webpack-hmr`, the client never hydrates, and client-only + pages such as `/blocks/create` stay on their server-rendered skeleton. +- Storybook for `@constructive-io/ui`: `pnpm --filter @constructive-io/ui sb` + (`http://localhost:6007`; the Kitchen Sink story carries the DialKit tuning panel). diff --git a/apps/blocks/package.json b/apps/blocks/package.json index 3236f2c1..7cfa7a39 100644 --- a/apps/blocks/package.json +++ b/apps/blocks/package.json @@ -40,12 +40,14 @@ "@constructive-io/schema-builder": "workspace:*", "@constructive-io/sheets": "workspace:*", "@constructive-io/ui": "workspace:*", + "@internationalized/date": "^3.10.1", "@tanstack/react-form": "^1.27.7", "@tanstack/react-query": "^5.90.16", "ai": "^7.0.26", "blocks-renderer": "workspace:*", "blocks-schema": "workspace:*", "clsx": "^2.1.1", + "dialkit": "2.0.2", "dompurify": "^3.3.1", "gql-ast": "^3.3.3", "graphql": "16.13.0", diff --git a/apps/blocks/src/app/blocks/ai/page.tsx b/apps/blocks/src/app/blocks/ai/page.tsx index 911b8238..09847d97 100644 --- a/apps/blocks/src/app/blocks/ai/page.tsx +++ b/apps/blocks/src/app/blocks/ai/page.tsx @@ -115,7 +115,7 @@ export default function AiPage() {
{doc.composition.boundaries.map((item) => (

{item.title}

@@ -160,7 +160,7 @@ export default function AiPage() {
  • {component.title} diff --git a/apps/blocks/src/app/blocks/billing/page.tsx b/apps/blocks/src/app/blocks/billing/page.tsx index ee0e1522..a6c68c81 100644 --- a/apps/blocks/src/app/blocks/billing/page.tsx +++ b/apps/blocks/src/app/blocks/billing/page.tsx @@ -38,10 +38,10 @@ export default function BillingBlocksPage() { @@ -50,7 +50,7 @@ export default function BillingBlocksPage() { diff --git a/apps/blocks/src/app/blocks/command-palette/page.tsx b/apps/blocks/src/app/blocks/command-palette/page.tsx index a11ab81c..e56bd18b 100644 --- a/apps/blocks/src/app/blocks/command-palette/page.tsx +++ b/apps/blocks/src/app/blocks/command-palette/page.tsx @@ -135,7 +135,7 @@ export default function CommandPalettePage() {
    {doc.composition.boundaries.map((item) => (

    diff --git a/apps/blocks/src/app/blocks/console-kit/page.tsx b/apps/blocks/src/app/blocks/console-kit/page.tsx index aaf35551..537e9bb5 100644 --- a/apps/blocks/src/app/blocks/console-kit/page.tsx +++ b/apps/blocks/src/app/blocks/console-kit/page.tsx @@ -2,15 +2,7 @@ import type { Metadata } from 'next'; import Link from 'next/link'; import { Badge } from '@constructive-io/ui/badge'; -import { - Table, - TableBody, - TableCaption, - TableCell, - TableHead, - TableHeader, - TableRow -} from '@constructive-io/ui/table'; +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@constructive-io/ui/table'; import { ConsoleKitShowcasePreview } from '@/components/console-kit-showcase/console-kit-showcase-preview'; import { CodeBlock } from '@/components/docs/code-block'; @@ -23,44 +15,44 @@ const INSTALL_MATRIX = [ surface: 'console-kit-nextjs', installs: 'Core + all seven modules + shell graph', evidence: 'Any tenant with routable endpoints for the modules you open', - degrades: 'Unavailable modules stay navigable with diagnostics' + degrades: 'Unavailable modules stay navigable with diagnostics', }, { surface: 'console-kit-core', installs: 'Shell, store, runtime, no feature modules', evidence: 'Host-supplied featureModules list', - degrades: 'Empty main until modules are composed' + degrades: 'Empty main until modules are composed', }, { surface: 'console-module-*', installs: 'Console integration + matching feature-pack view', evidence: 'Module-required endpoints, capabilities, and _meta', - degrades: 'Setup badge + endpoint-specific unavailable panel' + degrades: 'Setup badge + endpoint-specific unavailable panel', }, { surface: 'feature-pack-*', installs: 'Standalone provider-neutral view only', evidence: 'Host-supplied resource/actions props', - degrades: 'Loading, empty, error, ready resource states only' + degrades: 'Loading, empty, error, ready resource states only', }, { surface: 'preset-auth-hardened', installs: 'Data, Auth, Users', evidence: 'data + auth endpoints; users after sign-in', - degrades: 'Signed-out users/data show Sign in' + degrades: 'Signed-out users/data show Sign in', }, { surface: 'preset-b2b-storage', installs: 'Data, Auth, Users, Organizations, Storage', evidence: 'auth + data + admin/org contracts; storage route when public', - degrades: 'Stock storage often unavailable without a public route' + degrades: 'Stock storage often unavailable without a public route', }, { surface: 'preset-full', installs: 'All seven modules', evidence: 'Per-module public contracts', - degrades: 'Billing/notifications/storage degrade independently' - } + degrades: 'Billing/notifications/storage degrade independently', + }, ] as const; const TITLE = 'Console Kit for Next.js'; @@ -284,25 +276,20 @@ export default function ConsoleKitPage() { Console Kit for Next.js

    - {DESCRIPTION} Pass the secret-free tenant descriptor returned by - provisioning; the first-party wrapper handles standalone auth, - capability discovery, endpoint-scoped requests, and pack adapters. + {DESCRIPTION} Pass the secret-free tenant descriptor returned by provisioning; the first-party wrapper handles + standalone auth, capability discovery, endpoint-scoped requests, and pack adapters.

    -

    +

    Product states

    - Console Kit is a tenant-user console. Switch presets and signed-out, - discovering, partial, ready, incompatible, and unavailable states - without a live fixture. Missing public routes are expected degraded + Console Kit is a tenant-user console. Switch presets and signed-out, discovering, partial, ready, + incompatible, and unavailable states without a live fixture. Missing public routes are expected degraded states, not silent backend failures.

    @@ -311,28 +298,23 @@ export default function ConsoleKitPage() {
    -

    +

    Choose an install surface

    - One matrix covers the umbrella, core, Console modules, standalone - packs, and official presets. Installed code never grants authority; - each surface only becomes interactive when public evidence supports it. + One matrix covers the umbrella, core, Console modules, standalone packs, and official presets. Installed + code never grants authority; each surface only becomes interactive when public evidence supports it.

    - Install surfaces, what they install, expected public evidence, and - degradation behavior. + Install surfaces, what they install, expected public evidence, and degradation behavior. @@ -345,9 +327,7 @@ export default function ConsoleKitPage() { {INSTALL_MATRIX.map((row) => ( - - {row.surface} - + {row.surface} {row.installs} @@ -365,31 +345,22 @@ export default function ConsoleKitPage() {
    -

    +

    Install the full console

    - One registry item installs the Console Kit, all seven feature - modules, the runtime and catalog contracts, the single modular - Zustand store, and the shadcn Base UI app shell dependency graph. + One registry item installs the Console Kit, all seven feature modules, the runtime and catalog contracts, + the single modular Zustand store, and the shadcn Base UI app shell dependency graph.

    - - {registryAdd('console-kit-nextjs')} - + {registryAdd('console-kit-nextjs')}
    -

    - Install an official preset -

    +

    Install an official preset

    - Each preset registry item installs the core and the exact feature - modules mapped to its Constructive DB preset. Runtime navigation - still follows the contracts discovered from the active tenant, - so installed code never grants a capability by itself. + Each preset registry item installs the core and the exact feature modules mapped to its Constructive DB + preset. Runtime navigation still follows the contracts discovered from the active tenant, so installed + code never grants a capability by itself.

    @@ -397,29 +368,22 @@ export default function ConsoleKitPage() { { title: 'Auth hardened', registryName: 'preset-auth-hardened', - modules: 'Data, Auth, Users' + modules: 'Data, Auth, Users', }, { title: 'B2B with Storage', registryName: 'preset-b2b-storage', - modules: 'Data, Auth, Users, Organizations, Storage' + modules: 'Data, Auth, Users, Organizations, Storage', }, { title: 'Full', registryName: 'preset-full', - modules: 'All seven feature modules' - } + modules: 'All seven feature modules', + }, ].map((preset) => ( -
    -

    - {preset.title} -

    -

    - {preset.modules} -

    +
    +

    {preset.title}

    +

    {preset.modules}

    {registryAdd(preset.registryName)} @@ -428,16 +392,12 @@ export default function ConsoleKitPage() {
    -

    - Compose feature modules independently -

    +

    Compose feature modules independently

    - Install the leaf-independent core and only the Console Kit modules - your product needs. Every console module installs its standalone, - provider-neutral feature-pack view transitively, then adds - discovery, navigation, its Constructive adapter when available, - and any pack-owned store slice. Install a feature-pack item alone - when the host supplies the view props without Console Kit. + Install the leaf-independent core and only the Console Kit modules your product needs. Every console + module installs its standalone, provider-neutral feature-pack view transitively, then adds discovery, + navigation, its Constructive adapter when available, and any pack-owned store slice. Install a + feature-pack item alone when the host supplies the view props without Console Kit.

    @@ -449,34 +409,26 @@ export default function ConsoleKitPage() { 'console-module-organizations', 'console-module-storage', 'console-module-billing', - 'console-module-notifications' - ].map(registryAdd).join('\n')} + 'console-module-notifications', + ] + .map(registryAdd) + .join('\n')} - + {LOWER_LEVEL_EXAMPLE}
    -

    +

    Start from the tenant endpoint contract

    - The host supplies a database ID and its routable public GraphQL - endpoints; Console Kit does not need a preset name or a - control-plane receipt at runtime. Keep credentials on the user - side of the sign-in form, because the secret-free descriptor is - safe to pass from a server component to the client. A host-owned - session must carry the same database ID, which prevents identity - state from crossing between tenants. + The host supplies a database ID and its routable public GraphQL endpoints; Console Kit does not need a + preset name or a control-plane receipt at runtime. Keep credentials on the user side of the sign-in form, + because the secret-free descriptor is safe to pass from a server component to the client. A host-owned + session must carry the same database ID, which prevents identity state from crossing between tenants.

    @@ -485,49 +437,37 @@ export default function ConsoleKitPage() { { title: 'Endpoints', badge: 'six semantic kinds', - body: 'Pass only routable data, auth, admin, billing, storage, and notifications endpoints; no sibling endpoint is inferred.' + body: 'Pass only routable data, auth, admin, billing, storage, and notifications endpoints; no sibling endpoint is inferred.', }, { title: 'Session', badge: 'standalone default', - body: 'The first-party wrapper scopes in-memory and browser session state to the database, then resolves a fresh token per request.' + body: 'The first-party wrapper scopes in-memory and browser session state to the database, then resolves a fresh token per request.', }, { title: 'Capabilities', badge: 'discovered at runtime', - body: 'Installed modules become available only when endpoint metadata, GraphQL operations, and the active identity support them.' - } + body: 'Installed modules become available only when endpoint metadata, GraphQL operations, and the active identity support them.', + }, ].map((boundary) => ( -
    +
    -

    - {boundary.title} -

    +

    {boundary.title}

    {boundary.badge}
    -

    - {boundary.body} -

    +

    {boundary.body}

    ))}
    - + {CONSOLE_EXAMPLE}

    - Use the lower-level Console Kit endpoint resolver for embedded or - non-Constructive providers. Endpoint fallback stays opt-in, so an - admin operation cannot silently cross onto the data endpoint. + Use the lower-level Console Kit endpoint resolver for embedded or non-Constructive providers. Endpoint + fallback stays opt-in, so an admin operation cannot silently cross onto the data endpoint.

    {ENDPOINT_RESOLVER_EXAMPLE} @@ -535,30 +475,18 @@ export default function ConsoleKitPage() {
    -

    - Bootstrap CSRF for hardened auth -

    +

    Bootstrap CSRF for hardened auth

    When a tenant enables{' '} - - require_csrf_for_auth - - , pass an async token provider. Its host endpoint creates a - short-lived anonymous session with a cryptographically random - secret through a trusted backend connection and returns that - secret once; Console Kit sends it only as the auth mutation's{' '} - - csrfToken - - . Constructive consumes the anonymous session after successful - authentication, so the provider must mint a fresh token for each - attempt and must never cache it in browser storage. + require_csrf_for_auth, pass an + async token provider. Its host endpoint creates a short-lived anonymous session with a cryptographically + random secret through a trusted backend connection and returns that secret once; Console Kit sends it only + as the auth mutation's{' '} + csrfToken. Constructive + consumes the anonymous session after successful authentication, so the provider must mint a fresh token + for each attempt and must never cache it in browser storage.

    - + {CSRF_PROVIDER_EXAMPLE}
    @@ -568,42 +496,21 @@ export default function ConsoleKitPage() { Complete email verification without exposing the token

    - Unverified signed-in accounts can send a fresh verification - email from Account security. Your verification route reads the{' '} - - email_id - {' '} - and{' '} - - verification_token - {' '} - values from a URL fragment. When{' '} - - callback - {' '} - is omitted, Console Kit captures those credentials into its closure-owned - vault, scrubs the fragment, and then the auth adapter calls{' '} - - verifyEmail - {' '} - using the freshly delivered credential, including from a fresh signed-out - browser. After sign-in, Console Kit reloads the account so the - verified state is authoritative. Treat the token as a credential: - route these values through the URL's{' '} - - # - {' '} + Unverified signed-in accounts can send a fresh verification email from Account security. Your verification + route reads the email_id and{' '} + verification_token values from + a URL fragment. When callback{' '} + is omitted, Console Kit captures those credentials into its closure-owned vault, scrubs the fragment, and + then the auth adapter calls{' '} + verifyEmail using the freshly + delivered credential, including from a fresh signed-out browser. After sign-in, Console Kit reloads the + account so the verified state is authoritative. Treat the token as a credential: route these values + through the URL's #{' '} fragment so they stay out of the HTTP request. Pass{' '} - - callback={'{false}'} - {' '} - only when the host owns the complete callback lifecycle. + callback={'{false}'} only when + the host owns the complete callback lifecycle.

    - + {EMAIL_VERIFICATION_EXAMPLE} @@ -611,22 +518,16 @@ export default function ConsoleKitPage() {
    -

    +

    One store, modular slices

    - Each Console Kit instance creates one Zustand vanilla store and - composes the core navigation, tenant, session, endpoint, - discovery, runtime, and adapter slices with slices contributed by - installed feature modules. The store is scoped to the mounted - console, so server renders and multiple consoles cannot leak - identity or navigation state across instances. If the host owns - the store, it must compose the same module slices; Console Kit - verifies that composition and resets its scoped state when the - tenant or identity changes. + Each Console Kit instance creates one Zustand vanilla store and composes the core navigation, tenant, + session, endpoint, discovery, runtime, and adapter slices with slices contributed by installed feature + modules. The store is scoped to the mounted console, so server renders and multiple consoles cannot leak + identity or navigation state across instances. If the host owns the store, it must compose the same module + slices; Console Kit verifies that composition and resets its scoped state when the tenant or identity + changes.

    @@ -637,26 +538,18 @@ export default function ConsoleKitPage() {
    -

    +

    Metadata is a compatibility gate

    _meta 2026-07

    - The runtime checks every configured, reachable endpoint with - standard GraphQL introspection and Constructive's versioned{' '} - - _meta - {' '} - contract. This release accepts contract version{' '} - - 2026-07 - {' '} - only; each feature module selects its evidence from the endpoint - map, and incompatible metadata stops that feature instead of + The runtime checks every configured, reachable endpoint with standard GraphQL introspection and + Constructive's versioned{' '} + _meta contract. This release + accepts contract version{' '} + 2026-07 only; each feature + module selects its evidence from the endpoint map, and incompatible metadata stops that feature instead of guessing at tables, relations, or mutation shapes.

    @@ -666,27 +559,21 @@ export default function ConsoleKitPage() { {[ { title: 'Preflight', - body: 'Resolve each explicit semantic endpoint and read a fresh token from the active database-scoped session.' + body: 'Resolve each explicit semantic endpoint and read a fresh token from the active database-scoped session.', }, { title: 'Discover', - body: 'Load _meta plus GraphQL root operations, types, and input objects independently for every reachable endpoint.' + body: 'Load _meta plus GraphQL root operations, types, and input objects independently for every reachable endpoint.', }, { title: 'Expose', - body: 'Show a module only when its schema contract is exposed, then let PostgreSQL privileges and RLS decide each operation.' - } + body: 'Show a module only when its schema contract is exposed, then let PostgreSQL privileges and RLS decide each operation.', + }, ].map((step, index) => (
  • - - {String(index + 1).padStart(2, '0')} - -

    - {step.title} -

    -

    - {step.body} -

    + {String(index + 1).padStart(2, '0')} +

    {step.title}

    +

    {step.body}

  • ))} @@ -695,51 +582,36 @@ export default function ConsoleKitPage() {
    -

    +

    Prove compatibility against native tenants

    Start the untouched Constructive DB services with{' '} - - fun up - - , then let the Blocks fixture call the native provisioning - procedure for the three official presets and one supported - storage-routed tenant. The generated secret-free manifest feeds - the integration route, while the live suite exercises auth, - identity-scoped CRUD, cross-tenant rejection, capability - discovery, and RLS through public GraphQL endpoints. Cleanup uses - only the exact tenant database IDs recorded by that run. + fun up, then let the Blocks + fixture call the native provisioning procedure for the three official presets and one supported + storage-routed tenant. The generated secret-free manifest feeds the integration route, while the live + suite exercises auth, identity-scoped CRUD, cross-tenant rejection, capability discovery, and RLS through + public GraphQL endpoints. Cleanup uses only the exact tenant database IDs recorded by that run.

    - - {NATIVE_PROOF_EXAMPLE} - + {NATIVE_PROOF_EXAMPLE}
    -

    +

    Route-neutral app shell

    - Console Kit composes the Constructive App Shell, App Bar, and the - shadcn Base UI Sidebar. The shell renders typed navigation, - breadcrumbs, brand, account actions, search, and action slots, but - links remain plain anchors until your host supplies a Next.js or - other framework renderer. + Console Kit composes the Constructive App Shell, App Bar, and the shadcn Base UI Sidebar. The shell + renders typed navigation, breadcrumbs, brand, account actions, search, and action slots, but links remain + plain anchors until your host supplies a Next.js or other framework renderer.

    - Omit a controlled route to let Console Kit own navigation, optionally - starting from defaultRoute. - For URL-owned navigation, parse the current URL into a typed semantic - route and provide the route, href encoder, change callback, and link renderer together. + Omit a controlled route to let Console Kit own navigation, optionally starting from{' '} + defaultRoute. For URL-owned + navigation, parse the current URL into a typed semantic route and provide the route, href encoder, change + callback, and link renderer together.

    @@ -748,21 +620,16 @@ export default function ConsoleKitPage() {
    - - {registryAdd('app-shell')} - + {registryAdd('app-shell')}
    -

    - Compose only what you need -

    +

    Compose only what you need

    - Use the shell directly for a custom application, install a - focused pack for one workflow, or start with Console Kit when - you want the complete discovery and navigation layer. + Use the shell directly for a custom application, install a focused pack for one workflow, or start with + Console Kit when you want the complete discovery and navigation layer.

    Browse feature packs @@ -783,6 +650,6 @@ export const metadata: Metadata = { title: TITLE, description: DESCRIPTION, url: withBase('/blocks/console-kit'), - images: [OG_IMAGE] - } + images: [OG_IMAGE], + }, }; diff --git a/apps/blocks/src/app/blocks/create/page.tsx b/apps/blocks/src/app/blocks/create/page.tsx new file mode 100644 index 00000000..0289c5ef --- /dev/null +++ b/apps/blocks/src/app/blocks/create/page.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; + +import { ThemePlayground } from '@/components/theme-playground/theme-playground'; +import { OG_IMAGE, withBase } from '@/lib/site'; + +const TITLE = 'Create'; +const DESCRIPTION = + "Pick a theme preset or tune Constructive's neutrals, accent, radius, type, elevation and motion against every primitive and block, then copy the CSS or install it."; + +export default function CreatePage() { + return ; +} + +export const metadata: Metadata = { + title: TITLE, + description: DESCRIPTION, + alternates: { canonical: withBase('/blocks/create') }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: withBase('/blocks/create'), + images: [OG_IMAGE], + }, +}; diff --git a/apps/blocks/src/app/blocks/create/preview/layout.tsx b/apps/blocks/src/app/blocks/create/preview/layout.tsx new file mode 100644 index 00000000..72c7a42f --- /dev/null +++ b/apps/blocks/src/app/blocks/create/preview/layout.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from 'react'; +import { Geist, IBM_Plex_Sans, Open_Sans } from 'next/font/google'; + +/** + * The playground's optional preview fonts load only on this route — the host + * page and the rest of the docs never pay for them. Each variable name is a + * previewStack target in lib/theme-playground/draft.ts. Inter needs no load + * here: it is the shipped --font-sans and the root layout already loads it + * into --font-sans-loaded (which reaches this document's :root via ). + * + * `previewStyleSheet` writes `--font-sans: var(--font-open-sans), …` on + * `:root`, so the variables must be defined on `:root` too — a wrapper + * element's `*.variable` classes would leave them undefined there. + */ +const openSans = Open_Sans({ subsets: ['latin'], display: 'swap', variable: '--font-open-sans' }); +const geist = Geist({ subsets: ['latin'], display: 'swap', variable: '--font-geist' }); +const plex = IBM_Plex_Sans({ + subsets: ['latin'], + display: 'swap', + variable: '--font-plex', + weight: ['400', '500', '600'], +}); + +const rootFontVariables = `:root { + --font-open-sans: ${openSans.style.fontFamily}; + --font-geist: ${geist.style.fontFamily}; + --font-plex: ${plex.style.fontFamily}; +}`; + +export default function CreatePreviewLayout({ children }: { children: ReactNode }) { + return ( + <> + +
    {children}
    + + ); +} diff --git a/apps/blocks/src/app/blocks/create/preview/page.tsx b/apps/blocks/src/app/blocks/create/preview/page.tsx new file mode 100644 index 00000000..0f1b883f --- /dev/null +++ b/apps/blocks/src/app/blocks/create/preview/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next'; + +import { ThemePreviewDocument } from '@/components/theme-playground/preview-document'; +import { withBase } from '@/lib/site'; + +export default function CreatePreviewPage() { + return ( + <> +

    Theme playground live preview

    + + + ); +} + +export const metadata: Metadata = { + alternates: { canonical: withBase('/blocks/create') }, + robots: { follow: false, index: false }, +}; diff --git a/apps/blocks/src/app/blocks/features/page.tsx b/apps/blocks/src/app/blocks/features/page.tsx index 67cc2989..08282b10 100644 --- a/apps/blocks/src/app/blocks/features/page.tsx +++ b/apps/blocks/src/app/blocks/features/page.tsx @@ -86,10 +86,10 @@ export default function FeaturePacksPage() {
    @@ -101,7 +101,7 @@ export default function FeaturePacksPage() { {pack.endpoints}
    diff --git a/apps/blocks/src/app/blocks/page.tsx b/apps/blocks/src/app/blocks/page.tsx index 7230aa1e..fdca8ef3 100644 --- a/apps/blocks/src/app/blocks/page.tsx +++ b/apps/blocks/src/app/blocks/page.tsx @@ -90,7 +90,7 @@ export default function SetupPage() {
  • {item.title} @@ -127,7 +127,7 @@ export default function SetupPage() {
  • {component.title} diff --git a/apps/blocks/src/app/blocks/styling/page.tsx b/apps/blocks/src/app/blocks/styling/page.tsx index 407e9e66..4fc06bd2 100644 --- a/apps/blocks/src/app/blocks/styling/page.tsx +++ b/apps/blocks/src/app/blocks/styling/page.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { CodeBlock } from '@/components/docs/code-block'; import { InstallToggle } from '@/components/docs/install-toggle'; +import { ThemePresetGrid } from '@/components/docs/theme-preset-grid'; import { ColorTokenGallery, ContrastPairs, @@ -18,24 +19,17 @@ import { OG_IMAGE, withBase } from '@/lib/site'; const TITLE = 'Styling'; const DESCRIPTION = - 'Constructive theme tokens — OKLCH colors, radius scale, shadows, fonts, and z-index layers for @constructive-io/ui.'; + 'Constructive theme tokens — hsl() neutral ramp + OKLCH accents, radius scale, shadows, fonts, and z-index layers for @constructive-io/ui.'; -function DocSection({ - id, - title, - children, - lead, -}: { - id: string; - title: string; - lead?: string; - children: ReactNode; -}) { +function DocSection({ id, title, children, lead }: { id: string; title: string; lead?: string; children: ReactNode }) { return (

    - + {title}

    @@ -54,8 +48,8 @@ export default function StylingPage() {

    Styling

    A guide to the Constructive color system and CSS variables. Tokens ship with{' '} - @constructive-io/ui and - the shadcn registry theme — the same values power every primitive on this site. + @constructive-io/ui and the + shadcn registry theme — the same values power every primitive on this site.

    @@ -73,8 +67,8 @@ export default function StylingPage() {
      {[ { - title: 'OKLCH semantic colors', - body: 'Surfaces, brand, feedback, charts, and sidebar tokens in light and dark.', + title: 'hsl() neutral ramp + OKLCH accents', + body: 'Pure-neutral hsl() surfaces and text (strong 16% · default 36% · subtle 50% · selected 96% · border 95%) with OKLCH brand, feedback, and chart tokens in light and dark.', }, { title: 'Radius scale', @@ -102,14 +96,11 @@ export default function StylingPage() {
  • - Swatches below resolve against the live document theme — use the theme toggle in the top bar to - compare light and dark. Source of truth is{' '} - packages/ui/src/theme.ts - ; generated CSS is imported as{' '} - - @constructive-io/ui/globals.css - - . + Swatches below resolve against the live document theme — use the theme toggle in the top bar to compare + light and dark. Source of truth is{' '} + packages/ui/src/theme.ts; + generated CSS is imported as{' '} + @constructive-io/ui/globals.css.

    @@ -120,9 +111,7 @@ export default function StylingPage() { > {`/* from package globals */\n@custom-variant dark (&:is(.dark *));`}

    Light values live on :root; dark values on{' '} - .dark. Primary brand blue stays consistent across - modes; neutrals and surfaces shift for contrast. + .dark. Primary brand blue stays consistent across modes; + neutrals and surfaces shift for contrast.

    @@ -268,35 +257,49 @@ export default function StylingPage() {
    • Prefer redefining --primary,{' '} - --radius, and surface tokens over editing - component source. -
    • -
    • - Always set matching *-foreground pairs when you - change a surface or brand color. + --radius, and surface tokens over editing component + source.
    • - For registry installs, overrides still belong in your app CSS after generated theme CSS. + Always set matching *-foreground pairs when you change + a surface or brand color.
    • +
    • For registry installs, overrides still belong in your app CSS after generated theme CSS.
    + + +

    + Presets require the Constructive base theme and merge only color tokens — radius, shadows, and motion stay + yours. Try them live on{' '} + + Create + + , where moving a dial releases just that token family. +

    +
    +
    -