From 789c3b0e8c331d9d07b1baf4e1cf06d6af1cd641 Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 28 May 2026 22:20:07 +0200 Subject: [PATCH 01/23] Add --cratis-* CSS variable token layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a Cratis-scoped token layer that every internal component reads from instead of PrimeReact theme variables directly. Each --cratis-* token defaults to the matching PrimeReact variable so a consumer's PrimeReact theme just works, while still allowing consumers to take full control by overriding the tokens on :root or any scope — with or without a PrimeReact theme. Tokens cover surfaces, text, primary brand, highlight, semantic accents (green/orange/red), border radius, focus ring, and overlay mask. The token file is auto-bundled into the compiled @cratis/components/styles stylesheet via the existing Tailwind build pipeline. --- Source/tailwind.css | 9 ++++--- Source/tokens.css | 65 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 Source/tokens.css diff --git a/Source/tailwind.css b/Source/tailwind.css index c31c9eb..ae4d996 100644 --- a/Source/tailwind.css +++ b/Source/tailwind.css @@ -2,12 +2,15 @@ /* Licensed under the MIT license. See LICENSE file in the project root for full license information. */ /* - * Tailwind utility classes compiled at build time. - * This stylesheet is generated during the package build and contains only - * the utility classes that are actually used across all components. + * Tailwind utility classes + Cratis CSS variable tokens compiled at build time. + * This stylesheet is generated during the package build and contains only the + * utility classes that are actually used across all components, plus the + * --cratis-* token layer that every internal component depends on for theming. + * * It intentionally excludes preflight/base resets — those are the consumer app's responsibility. * * Usage in your app: * import '@cratis/components/styles'; */ @import "tailwindcss"; +@import "./tokens.css"; diff --git a/Source/tokens.css b/Source/tokens.css new file mode 100644 index 0000000..1e2f145 --- /dev/null +++ b/Source/tokens.css @@ -0,0 +1,65 @@ +/* Copyright (c) Cratis. All rights reserved. */ +/* Licensed under the MIT license. See LICENSE file in the project root for full license information. */ + +/* + * Cratis Components — CSS variable tokens. + * + * Every internal component references the --cratis-* variables defined here + * instead of PrimeReact theme variables directly. Each token defaults to the + * matching PrimeReact CSS variable, so when a consumer loads any PrimeReact + * theme the components are fully themed out of the box. + * + * To take full control of the look and feel (with or without PrimeReact's + * unstyled / pt mechanism, Tailwind, or your own CSS), override the relevant + * --cratis-* tokens on :root (or any ancestor element): + * + * :root { + * --cratis-surface-card: #1f2937; + * --cratis-text-color: #f3f4f6; + * } + * + * Tokens are intentionally fallback-free — if neither a PrimeReact theme nor + * a consumer override is present, the variable resolves to nothing and the + * affected rule no-ops. That keeps the library theme-agnostic without + * imposing a default palette on consumers. + */ + +:root { + /* Surfaces */ + --cratis-surface-0: var(--surface-0); + --cratis-surface-100: var(--surface-100); + --cratis-surface-ground: var(--surface-ground); + --cratis-surface-section: var(--surface-section); + --cratis-surface-card: var(--surface-card); + --cratis-surface-overlay: var(--surface-overlay); + --cratis-surface-hover: var(--surface-hover); + --cratis-surface-border: var(--surface-border); + + /* Text */ + --cratis-text-color: var(--text-color); + --cratis-text-color-secondary: var(--text-color-secondary); + + /* Primary brand */ + --cratis-primary-color: var(--primary-color); + --cratis-primary-color-text: var(--primary-color-text); + --cratis-primary-300: var(--primary-300); + --cratis-primary-400: var(--primary-400); + --cratis-primary-500: var(--primary-500); + --cratis-primary-600: var(--primary-600); + + /* Highlight / selection */ + --cratis-highlight-bg: var(--highlight-bg); + --cratis-highlight-text-color: var(--highlight-text-color); + + /* Semantic accents */ + --cratis-green-500: var(--green-500); + --cratis-orange-500: var(--orange-500); + --cratis-red-500: var(--red-500); + + /* Geometry */ + --cratis-border-radius: var(--border-radius); + + /* Effects */ + --cratis-focus-ring: var(--focus-ring); + --cratis-maskbg: var(--maskbg); +} From 48131fe8d557345face92d34c9e00e4399f9f909 Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 28 May 2026 22:30:27 +0200 Subject: [PATCH 02/23] Migrate internal styles from PrimeReact theme vars to --cratis-* tokens Rename every `var(--surface-*)`, `var(--text-color*)`, `var(--primary-color*)`, `var(--highlight-*)`, `var(--border-radius)`, `var(--green-500)`, `var(--orange-500)`, and `var(--red-500)` reference in production CSS, story files, and trivial inline styles to its `--cratis-*` equivalent. The tokens cascade to the underlying PrimeReact variables by default, so behavior is unchanged when a PrimeReact theme is loaded. Consumers wanting to take control now have a stable, scoped token surface to override on `:root` or any ancestor. This commit covers the files whose only change is the rename. Files that combine var renames with code refactors are migrated in subsequent commits. --- .../CommandDialog/CommandStepper.stories.tsx | 8 +- Source/CommandDialog/CommandStepper.tsx | 6 +- Source/CommandForm/fields/Fields.stories.tsx | 12 +- Source/Common/Icon.stories.tsx | 4 +- Source/Common/Tooltip.css | 6 +- .../ObjectNavigationalBar.stories.tsx | 2 +- Source/PivotViewer/PivotViewer.css | 260 +++++++++--------- Source/PivotViewer/components/Spinner.css | 16 +- Source/SchemaEditor/NameCell.tsx | 2 +- Source/SchemaEditor/SchemaEditor.stories.tsx | 2 +- Source/Toolbar/Toolbar.css | 34 +-- Source/Toolbar/Toolbar.stories.tsx | 22 +- 12 files changed, 187 insertions(+), 187 deletions(-) diff --git a/Source/CommandDialog/CommandStepper.stories.tsx b/Source/CommandDialog/CommandStepper.stories.tsx index f4a4eba..f42729b 100644 --- a/Source/CommandDialog/CommandStepper.stories.tsx +++ b/Source/CommandDialog/CommandStepper.stories.tsx @@ -112,7 +112,7 @@ export const Default: Story = { {result && ( -
+
{result}
)} @@ -166,7 +166,7 @@ export const InDialogFrame: Story = { {result && ( -
+
{result}
)} @@ -229,7 +229,7 @@ export const InDialogFrameWithCenteredHeader: Story = { {result && ( -
+
{result}
)} @@ -289,7 +289,7 @@ export const WithValidationIndicators: Story = { {result && ( -
+
{result}
)} diff --git a/Source/CommandDialog/CommandStepper.tsx b/Source/CommandDialog/CommandStepper.tsx index f246fb0..a7c4a92 100644 --- a/Source/CommandDialog/CommandStepper.tsx +++ b/Source/CommandDialog/CommandStepper.tsx @@ -187,16 +187,16 @@ export const CommandStepperContent = ({ const isVisited = visitedSteps.has(idx); const bgColor = hasError - ? 'var(--red-500, #ef4444)' + ? 'var(--cratis-red-500, #ef4444)' : isVisited - ? 'var(--green-500, #22c55e)' + ? 'var(--cratis-green-500, #22c55e)' : null; if (!bgColor) return existing; const existingStyle = existing.style as Record | undefined; return { ...existing, - style: { ...existingStyle, backgroundColor: bgColor, color: 'var(--primary-color-text)' } + style: { ...existingStyle, backgroundColor: bgColor, color: 'var(--cratis-primary-color-text)' } }; } } diff --git a/Source/CommandForm/fields/Fields.stories.tsx b/Source/CommandForm/fields/Fields.stories.tsx index 5b2943f..eab412e 100644 --- a/Source/CommandForm/fields/Fields.stories.tsx +++ b/Source/CommandForm/fields/Fields.stories.tsx @@ -192,7 +192,7 @@ export const AllFields: Story = { description="Standard text input field with validation" /> {validationState.errors.textInput && ( -
+
{validationState.errors.textInput}
)} @@ -205,7 +205,7 @@ export const AllFields: Story = { description="Email input with email validation" /> {validationState.errors.emailInput && ( -
+
{validationState.errors.emailInput}
)} @@ -218,7 +218,7 @@ export const AllFields: Story = { description="Password input field (min 6 characters)" /> {validationState.errors.passwordInput && ( -
+
{validationState.errors.passwordInput}
)} @@ -239,7 +239,7 @@ export const AllFields: Story = { step={1} /> {validationState.errors.numberInput && ( -
+
{validationState.errors.numberInput}
)} @@ -274,7 +274,7 @@ export const AllFields: Story = { optionLabel="name" /> {validationState.errors.dropdown && ( -
+
{validationState.errors.dropdown}
)} @@ -414,7 +414,7 @@ export const AllFields: Story = { Submit Form {!validationState.canSubmit && Object.keys(validationState.errors).length > 0 && ( - + Please fix validation errors )} diff --git a/Source/Common/Icon.stories.tsx b/Source/Common/Icon.stories.tsx index 0a4fafc..76f4a35 100644 --- a/Source/Common/Icon.stories.tsx +++ b/Source/Common/Icon.stories.tsx @@ -61,7 +61,7 @@ export const StringVsReactNode: Story = {
-

+

string (CSS class)

@@ -80,7 +80,7 @@ export const StringVsReactNode: Story = { } /> -

+

ReactNode (SVG)

diff --git a/Source/Common/Tooltip.css b/Source/Common/Tooltip.css index bfc9388..0f9d6a3 100644 --- a/Source/Common/Tooltip.css +++ b/Source/Common/Tooltip.css @@ -3,8 +3,8 @@ /* ── Tooltip bubble ──────────────────────────────────────────────────────── */ .tooltip-bubble { - background: var(--surface-100); - color: var(--text-color); - border: 1px solid var(--surface-border); + background: var(--cratis-surface-100); + color: var(--cratis-text-color); + border: 1px solid var(--cratis-surface-border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } diff --git a/Source/ObjectNavigationalBar/ObjectNavigationalBar.stories.tsx b/Source/ObjectNavigationalBar/ObjectNavigationalBar.stories.tsx index d9b3ee1..261a406 100644 --- a/Source/ObjectNavigationalBar/ObjectNavigationalBar.stories.tsx +++ b/Source/ObjectNavigationalBar/ObjectNavigationalBar.stories.tsx @@ -30,7 +30,7 @@ export const Interactive: Story = { }; return ( -
+

Current Path: {navigationPath.length > 0 ? navigationPath.join(' > ') : 'Root'}

div { - background: var(--surface-section); + background: var(--cratis-surface-section); border-radius: 0.6rem; padding: 0.5rem 0.7rem; } @@ -1046,13 +1046,13 @@ font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--text-color-secondary); + color: var(--cratis-text-color-secondary); } .pv-detail-panel dd { margin: 0.15rem 0 0; font-size: 0.85rem; - color: var(--text-color); + color: var(--cratis-text-color); white-space: pre-wrap; word-break: break-word; } @@ -1072,7 +1072,7 @@ display: none; position: fixed; inset: 0; - background: var(--maskbg); + background: var(--cratis-maskbg); backdrop-filter: blur(6px); display: flex; align-items: flex-end; @@ -1083,13 +1083,13 @@ .pv-detail { width: min(720px, 100%); - background: var(--surface-overlay); - border: 1px solid var(--surface-border); + background: var(--cratis-surface-overlay); + border: 1px solid var(--cratis-surface-border); border-radius: 1.25rem; padding: 1.75rem; max-height: min(80vh, 640px); overflow-y: auto; - color: var(--text-color); + color: var(--cratis-text-color); } .pv-detail header { @@ -1107,7 +1107,7 @@ .pv-detail header p { margin: 0; font-size: 0.9rem; - color: var(--text-color-secondary); + color: var(--cratis-text-color-secondary); } .pv-detail header button { @@ -1115,7 +1115,7 @@ border: none; border-radius: 0.75rem; padding: 0.5rem 1rem; - background: var(--highlight-bg); + background: var(--cratis-highlight-bg); color: inherit; cursor: pointer; } @@ -1136,7 +1136,7 @@ } .pv-detail dl > div { - background: var(--surface-section); + background: var(--cratis-surface-section); border-radius: 0.75rem; padding: 0.55rem 0.75rem; } @@ -1146,13 +1146,13 @@ font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--text-color-secondary); + color: var(--cratis-text-color-secondary); } .pv-detail dd { margin: 0.2rem 0 0; font-size: 0.9rem; - color: var(--text-color); + color: var(--cratis-text-color); white-space: pre-wrap; word-break: break-word; } @@ -1173,10 +1173,10 @@ gap: 0.35rem; padding: 0.35rem 0.65rem; border-radius: 999px; - background: var(--highlight-bg); - color: var(--text-color); + background: var(--cratis-highlight-bg); + color: var(--cratis-text-color); font-size: 0.8rem; - border: 1px solid var(--surface-border); + border: 1px solid var(--cratis-surface-border); } /* Range Histogram Filter */ @@ -1196,7 +1196,7 @@ .pv-histogram-bar { flex: 1; min-width: 8px; - background: var(--highlight-bg); + background: var(--cratis-highlight-bg); border: none; border-radius: 2px 2px 0 0; cursor: pointer; @@ -1205,16 +1205,16 @@ } .pv-histogram-bar:hover { - background: var(--surface-hover); + background: var(--cratis-surface-hover); transform: scaleY(1.02); } .pv-histogram-bar.in-range { - background: var(--primary-color); + background: var(--cratis-primary-color); } .pv-histogram-bar.partial { - background: var(--highlight-bg); + background: var(--cratis-highlight-bg); } .pv-range-slider { @@ -1229,7 +1229,7 @@ left: 0; right: 0; height: 4px; - background: var(--surface-border); + background: var(--cratis-surface-border); border-radius: 2px; transform: translateY(-50%); } @@ -1238,7 +1238,7 @@ position: absolute; top: 50%; height: 4px; - background: var(--primary-color); + background: var(--cratis-primary-color); border-radius: 2px; transform: translateY(-50%); cursor: grab; @@ -1253,8 +1253,8 @@ top: 50%; width: 16px; height: 16px; - background: var(--primary-color); - border: 2px solid var(--text-color); + background: var(--cratis-primary-color); + border: 2px solid var(--cratis-text-color); border-radius: 50%; transform: translate(-50%, -50%); cursor: ew-resize; @@ -1264,14 +1264,14 @@ .pv-range-handle:hover { transform: translate(-50%, -50%) scale(1.15); - box-shadow: 0 4px 12px var(--primary-color); + box-shadow: 0 4px 12px var(--cratis-primary-color); } .pv-range-labels { display: flex; justify-content: space-between; font-size: 0.75rem; - color: var(--text-color-secondary); + color: var(--cratis-text-color-secondary); font-variant-numeric: tabular-nums; } @@ -1281,8 +1281,8 @@ border: none; border-radius: 0.55rem; padding: 0.4rem 0.75rem; - background: var(--highlight-bg); - color: var(--text-color); + background: var(--cratis-highlight-bg); + color: var(--cratis-text-color); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; @@ -1291,7 +1291,7 @@ } .pv-range-clear:hover { - background: var(--surface-hover); + background: var(--cratis-surface-hover); } @media (max-width: 900px) { diff --git a/Source/PivotViewer/components/Spinner.css b/Source/PivotViewer/components/Spinner.css index f3c9674..80ee30d 100644 --- a/Source/PivotViewer/components/Spinner.css +++ b/Source/PivotViewer/components/Spinner.css @@ -21,48 +21,48 @@ } .pv-spinner-ring:nth-child(1) { - border-top-color: var(--primary-300); + border-top-color: var(--cratis-primary-300); animation: spinner-rotate 1.5s linear infinite; } .pv-spinner-ring:nth-child(2) { - border-right-color: var(--primary-400); + border-right-color: var(--cratis-primary-400); animation: spinner-rotate 1.5s linear infinite; animation-delay: -0.1875s; } .pv-spinner-ring:nth-child(3) { - border-bottom-color: var(--primary-500); + border-bottom-color: var(--cratis-primary-500); animation: spinner-rotate 1.5s linear infinite; animation-delay: -0.375s; } .pv-spinner-ring:nth-child(4) { - border-left-color: var(--primary-600); + border-left-color: var(--cratis-primary-600); animation: spinner-rotate 1.5s linear infinite; animation-delay: -0.5625s; } .pv-spinner-ring:nth-child(5) { - border-top-color: var(--primary-400); + border-top-color: var(--cratis-primary-400); animation: spinner-rotate 1.5s linear infinite reverse; animation-delay: -0.75s; } .pv-spinner-ring:nth-child(6) { - border-right-color: var(--primary-500); + border-right-color: var(--cratis-primary-500); animation: spinner-rotate 1.5s linear infinite reverse; animation-delay: -0.9375s; } .pv-spinner-ring:nth-child(7) { - border-bottom-color: var(--primary-300); + border-bottom-color: var(--cratis-primary-300); animation: spinner-rotate 1.5s linear infinite reverse; animation-delay: -1.125s; } .pv-spinner-ring:nth-child(8) { - border-left-color: var(--primary-600); + border-left-color: var(--cratis-primary-600); animation: spinner-rotate 1.5s linear infinite reverse; animation-delay: -1.3125s; } diff --git a/Source/SchemaEditor/NameCell.tsx b/Source/SchemaEditor/NameCell.tsx index de9eab7..467e824 100644 --- a/Source/SchemaEditor/NameCell.tsx +++ b/Source/SchemaEditor/NameCell.tsx @@ -31,7 +31,7 @@ export const NameCell = ({ rowData, isEditMode, onUpdate, validationError }: Nam {rowData.description && ( diff --git a/Source/SchemaEditor/SchemaEditor.stories.tsx b/Source/SchemaEditor/SchemaEditor.stories.tsx index f286436..3734f98 100644 --- a/Source/SchemaEditor/SchemaEditor.stories.tsx +++ b/Source/SchemaEditor/SchemaEditor.stories.tsx @@ -89,7 +89,7 @@ export const Interactive: Story = { const [schema, setSchema] = useState(JSON.parse(JSON.stringify(sampleSchema))); return ( -
+
@@ -501,7 +501,7 @@ export const FolderGridVsList: Story = { render: () => (
- Grid (default) + Grid (default) @@ -514,7 +514,7 @@ export const FolderGridVsList: Story = {
- List + List @@ -647,7 +647,7 @@ export const WithSlotInContext: Story = {
- Context + Context
{(['drawing', 'text'] as const).map(ctx => (
); } if (property.type === 'array') { return ( -
+
Array editing not yet supported
); @@ -266,7 +304,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals if (property.type === 'object') { return ( -
+
Object editing not yet supported
); @@ -283,7 +321,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals rows={3} style={inputStyle} /> - {error && {error}} + {error && {error}}
); } @@ -295,7 +333,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals onChange={(e) => handleChange(e.target.value)} style={inputStyle} /> - {error && {error}} + {error && {error}}
); }; @@ -308,7 +346,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals
navigateToProperty(propertyName)} - style={{ color: 'var(--primary-color)', display: 'flex', alignItems: 'center' }} + style={{ color: 'var(--cratis-primary-color)', display: 'flex', alignItems: 'center' }} > Array[{value.length}] @@ -321,7 +359,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals
navigateToProperty(propertyName)} - style={{ color: 'var(--primary-color)', display: 'flex', alignItems: 'center' }} + style={{ color: 'var(--cratis-primary-color)', display: 'flex', alignItems: 'center' }} > Object @@ -334,7 +372,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals const renderTable = () => { if (Array.isArray(currentData)) { - if (currentData.length === 0) return
Empty array
; + if (currentData.length === 0) return
Empty array
; const firstItem = currentData[0]; if (typeof firstItem === 'object' && firstItem !== null && !Array.isArray(firstItem)) { @@ -346,7 +384,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals {currentData.map((item, index) => ( {index > 0 && ( - + )} @@ -422,7 +460,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals }; return ( -
+
Snapshot captured: {timestamp.toLocaleString()}
diff --git a/Source/ObjectNavigationalBar/ObjectNavigationalBar.css b/Source/ObjectNavigationalBar/ObjectNavigationalBar.css new file mode 100644 index 0000000..07c6723 --- /dev/null +++ b/Source/ObjectNavigationalBar/ObjectNavigationalBar.css @@ -0,0 +1,6 @@ +/* Copyright (c) Cratis. All rights reserved. */ +/* Licensed under the MIT license. See LICENSE file in the project root for full license information. */ + +.cratis-object-navigational-bar { + border-bottom: 1px solid var(--cratis-surface-border); +} diff --git a/Source/ObjectNavigationalBar/ObjectNavigationalBar.tsx b/Source/ObjectNavigationalBar/ObjectNavigationalBar.tsx index 864d977..fd2a2f9 100644 --- a/Source/ObjectNavigationalBar/ObjectNavigationalBar.tsx +++ b/Source/ObjectNavigationalBar/ObjectNavigationalBar.tsx @@ -5,27 +5,56 @@ import { useMemo } from 'react'; import { Button } from 'primereact/button'; import * as faIcons from 'react-icons/fa6'; import { buildNavigationBreadcrumbs } from './breadcrumbHelpers'; +import './ObjectNavigationalBar.css'; +/** + * Props for {@link ObjectNavigationalBar}. + */ export interface ObjectNavigationalBarProps { + /** + * Ordered list of property keys representing the current navigation depth + * into a nested object (e.g. `['shipping', 'address']`). An empty array + * means the navigation bar is at the root. + */ navigationPath: string[]; + + /** + * Invoked when the user clicks a breadcrumb or the back arrow. Receives + * the destination index in {@link navigationPath} (`0` means root). + */ onNavigate: (index: number) => void; + + /** Extra CSS class names appended to the navigation bar root. */ + className?: string; } -export function ObjectNavigationalBar({ navigationPath, onNavigate }: ObjectNavigationalBarProps) { +/** + * Breadcrumb-style navigation bar showing the user's path through a nested + * object structure, with a back-arrow button and clickable breadcrumb + * segments. Pairs with {@link ObjectContentEditor} but can be reused for any + * tree-like data exploration UI. + * + * @param props - {@link ObjectNavigationalBarProps}. + */ +export function ObjectNavigationalBar({ navigationPath, onNavigate, className }: ObjectNavigationalBarProps) { const breadcrumbItems = useMemo(() => buildNavigationBreadcrumbs(navigationPath), [navigationPath]); + const rootClassName = className + ? `cratis-object-navigational-bar px-4 py-2 mb-2 ${className}` + : 'cratis-object-navigational-bar px-4 py-2 mb-2'; return ( -
+
; +}; +``` + +Combine with `prefers-color-scheme` for the initial mode: + +```css +@media (prefers-color-scheme: dark) { + :root:not(.theme-light):not(.theme-dark) { + --surface-card: #1e293b; + --text-color: #f8fafc; + } +} +``` + +## Per-region brand zones + +Token overrides cascade, so any ancestor scope works for tinting Cratis-scoped surfaces in a region: + +```css +.brand-zone { + --cratis-surface-border: #c4b5fd; + --cratis-text-color-secondary: #a78bfa; + --cratis-primary-color: #7c3aed; +} +``` + +```tsx +
+ + +
+``` + +If you want PrimeReact widgets in the region to follow too, override the PrimeReact variables in the same scope: + +```css +.brand-zone { + --surface-card: #1f1147; + --text-color: #ede9fe; + --primary-color: #a78bfa; + /* …and the --cratis-* siblings above */ +} +``` + +## Per-component themed override inside unstyled mode + +When you're on Path C globally, single components can still pull in classes from a separate stylesheet via the `className` prop or per-instance `pt`: + +```tsx +import './custom-table.css'; + + + {/* All other DataTables use globalPt; this one uses a bespoke look. */} + + … + + +``` + +## What to keep in mind + +- **Provider value updates re-render**: changing `value` on `CratisComponentsProvider` rebuilds the merged config. Use a stable reference (e.g. `useMemo` or a module-level constant) to avoid spurious re-renders. +- **`pt` merging is deep**: PrimeReact merges global `pt` with per-instance `pt` by default. Set `ptOptions={{ mergeSections: false }}` on the wrapper if you need a hard replace. +- **Cratis tokens are scoped**: overriding a `--cratis-*` token only changes Cratis surfaces. To repaint PrimeReact widgets too, override the PrimeReact variable. See [Cratis token reference](cratis-tokens.md). + +## See also + +- [Path A — PrimeReact-themed](themed.md) +- [Path B — Custom palette](custom-palette.md) +- [Path C — Fully unstyled](unstyled.md) +- [Pass-through cheat sheet](pass-through.md) +- [CratisComponentsProvider](../Common/cratis-components-provider.md) diff --git a/Documentation/Styling/pass-through.md b/Documentation/Styling/pass-through.md new file mode 100644 index 0000000..d4c3bc5 --- /dev/null +++ b/Documentation/Styling/pass-through.md @@ -0,0 +1,137 @@ +# Pass-through (`pt`) cheat sheet + +Every Cratis wrapper forwards PrimeReact's `pt`, `ptOptions`, and `unstyled` props somewhere — but **where** depends on how much PrimeReact the wrapper composes. This page summarizes the pattern per component so you know which prop to reach for. + +## Three patterns + +### 1. Single-widget wrappers + +The wrapper renders exactly one PrimeReact widget and forwards `pt` / `ptOptions` / `unstyled` / `className` straight to it. The pt slot names are PrimeReact's own — see the underlying component's documentation. + +| Wrapper | Underlying widget | pt slot reference | +|---|---|---| +| `Dialog` | `primereact/dialog` Dialog | PrimeReact Dialog `pt` | +| `Dropdown` | `primereact/dropdown` Dropdown | PrimeReact Dropdown `pt` | +| `InputTextField` | `primereact/inputtext` InputText | PrimeReact InputText `pt` | +| `TextAreaField` | `primereact/inputtextarea` InputTextarea | PrimeReact InputTextarea `pt` | +| `NumberField` | `primereact/inputnumber` InputNumber | PrimeReact InputNumber `pt` | +| `DropdownField` | `primereact/dropdown` Dropdown | PrimeReact Dropdown `pt` | +| `RadioGroupField` | `primereact/radiobutton` RadioButton (one per option) | PrimeReact RadioButton `pt` | +| `RadioButtonField` | `primereact/radiobutton` RadioButton | PrimeReact RadioButton `pt` | +| `CalendarField` | `primereact/calendar` Calendar | PrimeReact Calendar `pt` | +| `CheckboxField` | `primereact/checkbox` Checkbox | PrimeReact Checkbox `pt` | +| `SliderField` | `primereact/slider` Slider | PrimeReact Slider `pt` | +| `ChipsField` | `primereact/chips` Chips | PrimeReact Chips `pt` | +| `MultiSelectField` | `primereact/multiselect` MultiSelect | PrimeReact MultiSelect `pt` | +| `ColorPickerField` | `primereact/colorpicker` ColorPicker | PrimeReact ColorPicker `pt` | + +Example: + +```tsx + c.email} + title="Email" + pt={{ root: { className: 'border-2 border-sky-500' } }} +/> +``` + +### 2. Multi-slot composites + +The wrapper composes more than one PrimeReact widget and exposes a sibling set of `*Pt` / `*PtOptions` / `*Unstyled` / `*ClassName` props per slot. + +#### `Dialog`-based dialogs + +`CommandDialog` is a single Dialog and forwards `pt`/`ptOptions`/`unstyled` to that Dialog. + +`StepperCommandDialog` composes a Dialog **and** a Stepper: + +| Prop | Targets | +|---|---| +| `pt` / `ptOptions` / `unstyled` | The inner PrimeReact Stepper. | +| `dialogPt` / `dialogPtOptions` / `dialogUnstyled` / `dialogClassName` | The outer PrimeReact Dialog. | + +```tsx + + command={RegisterAuthor} + title="Register author" + pt={{ stepperpanel: { content: { className: 'pt-6' } } }} + dialogPt={{ header: { className: 'bg-slate-900 text-slate-50' } }} + dialogClassName="shadow-2xl" +> + … + +``` + +#### Data tables and pages + +`DataTableForQuery` and `DataTableForObservableQuery` each compose a DataTable **and** a Paginator: + +| Prop | Targets | +|---|---| +| `pt` / `ptOptions` / `unstyled` / `className` | The inner DataTable. | +| `paginatorPt` / `paginatorPtOptions` / `paginatorUnstyled` | The inner Paginator. | + +`DataPage` composes a DataTable **and** a Menubar: + +| Prop | Targets | +|---|---| +| `tablePt` / `tablePtOptions` / `tableUnstyled` / `tableClassName` | The inner DataTable. | +| `menubarPt` / `menubarPtOptions` / `menubarUnstyled` / `menubarClassName` | The action Menubar. | + +```tsx + + title="Authors" + query={AllAuthors} + tablePt={{ table: { className: 'min-w-full divide-y divide-slate-700' } }} + menubarPt={{ root: { className: 'px-3 py-2 bg-slate-900' } }} +> + + + +``` + +### 3. Large composites + +These wrappers render many PrimeReact widgets internally (`InputText`, `InputNumber`, `Checkbox`, `Calendar`, `InputTextarea`, `Dropdown`, `Button`, `Menubar`, …). Exposing a `pt` prop per inner widget would be impractical; instead, they expose **`className`** on the root for layout/positioning, and you restyle their internals via the **global `pt` preset** on [`CratisComponentsProvider`](../Common/cratis-components-provider.md). + +| Wrapper | What it accepts | How to restyle internals | +|---|---|---| +| `ObjectContentEditor` | `className` | Global `pt` on `CratisComponentsProvider` covering `inputtext`, `inputnumber`, `checkbox`, `calendar`, `inputtextarea`. | +| `ObjectNavigationalBar` | `className` | Global `pt` covering `button`; `--cratis-surface-border` for the bottom border. | +| `SchemaEditor` | `className` | Global `pt` covering `menubar`, `button`, `datatable`, `dropdown`, `inputtext`. | + +Example with a global preset: + +```tsx + + + +``` + +## Where the global `pt` reaches + +A `pt` preset on `CratisComponentsProvider` flows into **every** PrimeReact widget rendered by every wrapper — including the internals of the large composites. Per-instance `pt` props on individual wrappers are *merged* with the global preset (PrimeReact's `ptOptions.mergeSections` defaults to `true`). + +To replace a slot's preset entirely on a single instance, opt out of merging: + +```tsx +