diff --git a/Documentation/Common/cratis-components-provider.md b/Documentation/Common/cratis-components-provider.md new file mode 100644 index 0000000..7191c2c --- /dev/null +++ b/Documentation/Common/cratis-components-provider.md @@ -0,0 +1,111 @@ +# CratisComponentsProvider + +Single setup point for Cratis Components. Wraps PrimeReact's `PrimeReactProvider` so the package can layer Cratis-wide defaults on top of PrimeReact's pass-through and unstyled mechanisms while still letting the consumer take complete control. + +## Purpose + +- Hosts the PrimeReact `pt` / `unstyled` / `ptOptions` / `inputStyle` / `ripple` / `appendTo` / `zIndex` / `locale` configuration for every Cratis wrapper below it in the tree. +- Deep-merges Cratis-wide defaults with the consumer's value, so future Cratis defaults can land without breaking consumer overrides. +- Re-exported from the package root so the recommended setup is one import: + + ```ts + import { CratisComponentsProvider } from '@cratis/components'; + ``` + +## Basic usage + +Mount once at the root of your tree: + +```tsx +import '@cratis/components/styles'; +import { CratisComponentsProvider } from '@cratis/components'; + +export const App = () => ( + + + +); +``` + +## Configuring `pt` / `unstyled` globally + +The `value` prop accepts the full PrimeReact `APIOptions` shape. The most commonly used members are `unstyled`, `pt`, `ptOptions`, `inputStyle`, `ripple`, and `appendTo`: + +```tsx +import { CratisComponentsProvider } from '@cratis/components'; +import { globalPt } from './pt-preset'; + +export const App = () => ( + + + +); +``` + +The `value` is deep-merged with the Cratis defaults (currently empty) so consumer settings always win. Pass a stable reference (a module-level constant or a `useMemo` result) to avoid unnecessary re-renders. + +## Props + +### `value` + +`Partial` — Cratis-wide and PrimeReact pass-through configuration. Merged on top of the library's defaults and made available to every Cratis component below in the tree. + +The most useful members: + +| Member | Purpose | +|---|---| +| `unstyled` | When `true`, disables every PrimeReact base style. Combine with `pt` (or per-component CSS / Tailwind) to fully restyle. | +| `pt` | Per-component pass-through configuration. Keys are PrimeReact component names (`button`, `dialog`, `inputtext`, …); values are slot configuration objects. | +| `ptOptions` | Controls merge vs. replace behavior for `pt`. Default is `{ mergeSections: true }` which merges per-instance `pt` with the global preset. | +| `inputStyle` | `'outlined'` or `'filled'` — switches the default input rendering across the whole app. | +| `ripple` | Enables PrimeReact's ripple animation on supported components. | +| `appendTo` | Where overlays mount (`document.body`, `'self'`, or a DOM ref). The Cratis `Dropdown` defaults to `document.body` independently of this setting. | +| `zIndex` | Per-overlay-type z-index baseline (`{ modal: 1100, overlay: 1000, … }`). | +| `locale` | PrimeReact locale string. | + +The full type is re-exported as `CratisComponentsConfig`. + +### `children` + +`React.ReactNode` — your application tree. + +## Using `PrimeReactProvider` directly + +`CratisComponentsProvider` is optional. If you'd rather mount PrimeReact's own provider directly, that works too — every Cratis wrapper reads the same context: + +```tsx +import { PrimeReactProvider } from 'primereact/api'; + +export const App = () => ( + + + +); +``` + +The Cratis provider exists to give Cratis one place to layer in defaults later without breaking consumers, and to keep the setup discoverable from a single import path. + +## Pure helpers (testing / library extension) + +The merge logic is exported so the contract can be verified without rendering React: + +```ts +import { mergeCratisComponentsConfig, cratisDefaults } from '@cratis/components'; + +const merged = mergeCratisComponentsConfig({ unstyled: true, pt: myPt }); +// → { ...cratisDefaults, unstyled: true, pt: myPt } +``` + +| Export | Description | +|---|---| +| `CratisComponentsProvider` | The React component. | +| `CratisComponentsProviderProps` | Props type. | +| `CratisComponentsConfig` | Alias for `Partial`. | +| `cratisDefaults` | The Cratis-wide defaults that ship today (currently `{}`). | +| `mergeCratisComponentsConfig` | Pure deep-merge helper used inside the provider. | + +## See also + +- [Styling Overview](../Styling/index.md) — the supported styling options and where the provider fits +- [Pass-through cheat sheet](../Styling/pass-through.md) — what `pt` reaches in each Cratis wrapper +- [Use fully unstyled mode](../Styling/unstyled.md) — full `pt` preset walk-through diff --git a/Documentation/Common/form-element.md b/Documentation/Common/form-element.md index 3cc5d8d..37c46bc 100644 --- a/Documentation/Common/form-element.md +++ b/Documentation/Common/form-element.md @@ -1,165 +1,131 @@ # FormElement -Wrapper component for form inputs with label and validation display. +Lightweight wrapper that places an icon addon to the left of a form input, styled with the `--cratis-*` token layer. Use it to give input fields a leading icon without pulling in PrimeReact's `InputGroup` chrome. ## Purpose -FormElement provides consistent styling and structure for form input fields with labels and validation messages. +FormElement is a structural primitive — it lays out an icon addon and a child input side by side, with rounded-on-the-left chrome around the addon. It does **not** render labels, required indicators, or validation messages — those concerns live on the underlying input itself or on the surrounding command form. ## Key Features -- Label positioning -- Validation error display -- Required field indication -- Consistent spacing -- Integration with form validation +- Icon addon styled from `--cratis-*` tokens (background, border, radius). +- Independent of PrimeReact's `p-inputgroup` / `p-inputgroup-addon` classes — works the same with or without a PrimeReact theme loaded. +- Accepts any React node as the icon (PrimeIcons class, ``, third-party icon component, …). + +## Props + +| Prop | Type | Description | +|---|---|---| +| `icon` | `React.ReactNode` | Icon node displayed inside the leading addon. Can be any React node — a PrimeIcons ``, an ``, or a `react-icons` component. | +| `children` | `React.ReactNode` | The form input rendered to the right of the icon addon (typically an `InputText`, `Dropdown`, etc.). | ## Basic Usage -```typescript +```tsx import { FormElement } from '@cratis/components'; import { InputText } from 'primereact/inputtext'; function MyForm() { return ( - + }> setName(e.target.value)} /> ); } ``` -## Props - -- `label`: Label text for the field -- `required`: Show required indicator (default: false) -- `error`: Validation error message to display -- `children`: The form input component - ## Examples -### Text Input +### Email field with envelope icon -```typescript - - }> + setEmail(e.target.value)} + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="you@example.com" /> ``` -### With Validation Error - -```typescript - - setPassword(e.target.value)} - /> - -``` +### Search field with react-icons -### Dropdown +```tsx +import { FaMagnifyingGlass } from 'react-icons/fa6'; -```typescript - - setCountry(e.value)} +}> + setQuery(e.target.value)} + placeholder="Search…" /> ``` -### Checkbox +### Dropdown with category icon -```typescript - - setAgreed(e.checked)} +```tsx +import { FaLayerGroup } from 'react-icons/fa6'; + +}> + setCategory(e.value)} + optionLabel="name" + optionValue="id" /> ``` -### Text Area +## Styling -```typescript - - setDescription(e.target.value)} - rows={5} - /> - -``` +The addon's background, border, and radius are driven by the [`--cratis-*` token layer](../Styling/cratis-tokens.md): -## Complete Form Example +| Token | Surface | +|---|---| +| `--cratis-surface-100` | Addon background. | +| `--cratis-surface-border` | Addon border. | +| `--cratis-text-color-secondary` | Icon color. | +| `--cratis-border-radius` | Rounded-on-the-left corner radius. | -```typescript -function UserForm() { - const [formData, setFormData] = useState({ - name: '', - email: '', - role: null - }); - const [errors, setErrors] = useState({}); +Override the tokens to retint the addon without forking the component: - return ( -
- - handleChange('name', e.target.value)} - /> - - - - handleChange('email', e.target.value)} - /> - - - - handleChange('role', e.value)} - /> - - - -
- ); +```css +:root { + --cratis-surface-100: #1e293b; + --cratis-surface-border: #334155; + --cratis-border-radius: 10px; +} +``` + +For per-instance restyling, wrap the FormElement with your own class and target it in CSS: + +```css +.brand-form-element .cratis-form-element__addon { + background: var(--brand-accent); + color: var(--brand-on-accent); } ``` -## Best Practices +```tsx +
+ }> + + +
+``` + +For full styling control under `unstyled` mode, the addon classes are stable: `cratis-form-element` on the row and `cratis-form-element__addon` on the leading slot. + +## When to use FormElement vs CommandForm fields + +- Use **`FormElement`** for ad-hoc forms outside of a `CommandForm`, when you want the icon-addon visual on top of any input you control. +- Use **CommandForm fields** (`InputTextField`, `NumberField`, `DropdownField`, …) inside a `CommandForm` or `CommandDialog` — they bind to a command property, surface validation state, and render the appropriate input type. + +## See Also -1. Always use FormElement for consistent form layouts -2. Show required indicators on mandatory fields -3. Display validation errors clearly -4. Keep labels concise -5. Group related fields together -6. Use appropriate input types for data +- [Styling Overview](../Styling/index.md) — the supported styling options and where FormElement fits +- [Cratis token reference](../Styling/cratis-tokens.md) — every token and the surfaces it tints +- [CommandForm Field Types](../CommandForm/index.md) — command-bound field wrappers diff --git a/Documentation/Common/index.md b/Documentation/Common/index.md index 533a594..08f9996 100644 --- a/Documentation/Common/index.md +++ b/Documentation/Common/index.md @@ -1,17 +1,20 @@ # Common Components -The Common module provides reusable UI components that serve as building blocks for applications. +The Common module provides reusable UI components and the styling setup primitive that serve as building blocks for applications. ## Components -- **Icon / IconDisplay**: Unified icon type that accepts a PrimeIcons CSS class string or any React node -- **Page**: Layout component for consistent page structures -- **FormElement**: Wrapper for form inputs with labels and validation -- **ErrorBoundary**: Error handling for React component trees +- **CratisComponentsProvider**: Single setup point for Cratis Components — wraps PrimeReact's `PrimeReactProvider` and hosts the `pt`, `unstyled`, locale, and other global configuration. +- **Icon / IconDisplay**: Unified icon type that accepts a PrimeIcons CSS class string or any React node. +- **Page**: Layout primitive for consistent page structures. +- **FormElement**: Lightweight wrapper that places an icon addon to the left of a form input. +- **ErrorBoundary**: Error handling for React component trees. ## See Also +- [CratisComponentsProvider](cratis-components-provider.md) — global setup, `pt` / `unstyled` configuration - [Icon](icon.md) - Icon type and IconDisplay component - [Page](page.md) - Page layout component -- [FormElement](form-element.md) - Form field wrapper +- [FormElement](form-element.md) - Form field icon-addon wrapper - [ErrorBoundary](error-boundary.md) - Error boundary component +- [Styling Overview](../Styling/index.md) — the supported styling options and how Common fits in diff --git a/Documentation/Common/toc.yml b/Documentation/Common/toc.yml index 780c0e3..f7906fb 100644 --- a/Documentation/Common/toc.yml +++ b/Documentation/Common/toc.yml @@ -1,5 +1,7 @@ - name: Overview href: index.md +- name: CratisComponentsProvider + href: cratis-components-provider.md - name: Icon href: icon.md - name: Page diff --git a/Documentation/Styling/cratis-tokens.md b/Documentation/Styling/cratis-tokens.md new file mode 100644 index 0000000..6750c57 --- /dev/null +++ b/Documentation/Styling/cratis-tokens.md @@ -0,0 +1,161 @@ +# Cratis token reference + +The `--cratis-*` CSS variable layer is the Cratis-scoped tint surface every Cratis wrapper reads from. Each token resolves the **PrimeReact v11 design token first** (`@primeuix/themes`, e.g. `--p-content-border-color`), falling back to the **legacy v10 theme variable** (e.g. `--surface-border`) via `tokens.css`. Loading any PrimeReact theme — v10 *or* v11 — therefore gives every Cratis surface the right color without any further work, and the same build keeps working across a PrimeReact 10 → 11 upgrade. + +This indirection is deliberate: it is the single seam that insulates your code (and your consumers' `--cratis-*` overrides) from PrimeReact's token system changing underneath you. + +You override `--cratis-*` tokens when you want **just** Cratis-scoped surfaces (validation error text, FormElement addon, breadcrumb borders, …) tinted independently of PrimeReact widgets. To repaint PrimeReact widgets themselves, override the PrimeReact variables directly — see [the custom palette setup](custom-palette.md). + +## Loading the tokens + +The token layer ships at two import paths: + +```ts +import '@cratis/components/styles'; +``` + +The recommended one — ships the Tailwind utility classes used inside the package **plus** the `--cratis-*` token declarations as a single stylesheet. + +```ts +import '@cratis/components/tokens'; +``` + +Just the token declarations, with no Tailwind utilities. Use this if you bring your own utility CSS solution (or use Tailwind with custom classnames that don't match what the package ships). + +## Token catalogue + +Each token resolves the PrimeReact v11 design token first, then the v10 legacy variable (e.g. `--cratis-surface-border` → `var(--p-content-border-color, var(--surface-border))`). v11 is not a 1:1 rename of v10 — where v11's vocabulary has no direct equivalent for a v10 concept (`surface-ground`, `surface-section`, `surface-overlay`, the composite `focus-ring`), the closest durable v11 semantic token is used (see the inline notes in `tokens.css`). Set the `--cratis-*` token to override regardless of which PrimeReact version is loaded. + +### Surfaces + +| Token | Cratis surfaces tinted by it | +|---|---| +| `--cratis-surface-0` | Reserved for any Cratis-scoped surface that maps to PrimeReact's `--surface-0`. | +| `--cratis-surface-100` | `FormElement` addon background. | +| `--cratis-surface-ground` | `PivotViewer` canvas and panel backgrounds. | +| `--cratis-surface-section` | `PivotViewer` panel section backgrounds. | +| `--cratis-surface-card` | Backgrounds of the `ObjectContentEditor` snapshot card and similar panels; `PivotViewer` card gradients. | +| `--cratis-surface-overlay` | Overlay backgrounds inside Cratis wrappers. | +| `--cratis-surface-hover` | Hover state on row alternation inside `ObjectContentEditor`. | +| `--cratis-surface-border` | `FormElement` addon border, `ObjectNavigationalBar` bottom border, `SchemaEditor` bottom border, table/paginator borders inside `DataTableForQuery` / `DataTableForObservableQuery`. | + +### Text + +| Token | Cratis surfaces tinted by it | +|---|---| +| `--cratis-text-color` | Default body text inside Cratis wrappers. | +| `--cratis-text-color-secondary` | `ObjectContentEditor` label column, `ObjectNavigationalBar` breadcrumbs, `SchemaEditor` secondary labels. | + +### Brand + +| Token | Cratis surfaces tinted by it | +|---|---| +| `--cratis-primary-color` | `ObjectContentEditor` navigation links into nested objects/arrays, default brand accent. | +| `--cratis-primary-color-text` | Foreground used on top of `--cratis-primary-color` backgrounds (e.g. CommandStepper step number color). | +| `--cratis-primary-300` | `PivotViewer` loading spinner ring. | +| `--cratis-primary-400` | `PivotViewer` loading spinner ring. | +| `--cratis-primary-500` | `PivotViewer` loading spinner ring and card gradient. | +| `--cratis-primary-600` | `PivotViewer` loading spinner ring. | + +### Selection / highlight + +| Token | Cratis surfaces tinted by it | +|---|---| +| `--cratis-highlight-bg` | Background of timestamp/highlight chips inside `ObjectContentEditor`. | +| `--cratis-highlight-text-color` | Text on top of `--cratis-highlight-bg`. | + +### Semantic accents + +| Token | Cratis surfaces tinted by it | +|---|---| +| `--cratis-green-500` | `CommandStepper` visited-step indicator. | +| `--cratis-orange-500` | Reserved for warning accents. | +| `--cratis-red-500` | Inline validation error text (replaces PrimeReact's `.p-error` styling), `CommandStepper` error-step indicator, error border tint inside `ObjectContentEditor`. | + +### Geometry + +| Token | Cratis surfaces tinted by it | +|---|---| +| `--cratis-border-radius` | Border radius on `FormElement` addon and any Cratis surface that mirrors PrimeReact's `--border-radius`. | + +### Effects + +| Token | Cratis surfaces tinted by it | +|---|---| +| `--cratis-focus-ring` | Focus-ring box-shadow on interactive `PivotViewer` elements. | +| `--cratis-maskbg` | `PivotViewer` modal mask background. | + +## Overriding tokens + +Apply on `:root` for an app-wide override: + +```css +:root { + --cratis-red-500: #f97316; + --cratis-border-radius: 12px; +} +``` + +…or on an ancestor scope for a region-specific look: + +```css +.brand-region { + --cratis-surface-border: theme('colors.violet.500'); + --cratis-text-color-secondary: theme('colors.violet.300'); +} +``` + +```tsx +
+ +
+``` + +Cratis tokens cascade like any other CSS variable, so any selector that increases specificity over `:root` wins. + +## With TailwindCSS + +Tailwind's `@layer base` is the idiomatic spot — declare tokens once and let Tailwind handle cascade and dark mode: + +```css +@import "tailwindcss"; +@import "@cratis/components/tokens"; + +@layer base { + :root { + --cratis-surface-border: theme('colors.slate.700'); + --cratis-text-color: theme('colors.slate.50'); + --cratis-red-500: theme('colors.red.500'); + } + + .dark { + --cratis-surface-border: theme('colors.slate.600'); + --cratis-text-color: theme('colors.slate.100'); + } +} +``` + +## Relationship to PrimeReact variables + +The Cratis token layer is **additive** on top of PrimeReact's theme system, not a replacement for it. The cascade in `tokens.css` resolves the v11 token first, then the v10 legacy variable: + +```css +:root { + /* v11 (@primeuix/themes) first, v10 legacy fallback */ + --cratis-surface-card: var(--p-content-background, var(--surface-card)); + --cratis-text-color: var(--p-text-color, var(--text-color)); + /* … */ +} +``` + +That means: + +- Repaint PrimeReact itself — on **v11** customize the preset (`definePreset` / `--p-*` tokens), on **v10** override the legacy `--surface-*` / `--text-color` variables — and both PrimeReact widgets *and* Cratis surfaces follow. +- Override `--cratis-surface-card` (Cratis) → only Cratis surfaces follow; PrimeReact widgets keep their existing color, on either version. + +Use the PrimeReact token when you want a whole-UI repaint. Use the Cratis token when you want a Cratis-specific accent that differs from PrimeReact widgets. + +## See also + +- [Use a custom palette on top of a PrimeReact theme](custom-palette.md) — for whole-UI repainting with PrimeReact variables +- [Pass-through cheat sheet](pass-through.md) — for per-slot styling beyond what tokens reach diff --git a/Documentation/Styling/custom-palette.md b/Documentation/Styling/custom-palette.md new file mode 100644 index 0000000..c9453bc --- /dev/null +++ b/Documentation/Styling/custom-palette.md @@ -0,0 +1,150 @@ +# Use a custom palette on top of a PrimeReact theme + +You want PrimeReact's chrome — its dialog frames, button shapes, focus rings, input borders — but in your own colors. You don't want to write a PrimeReact theme from scratch. + +This setup keeps a PrimeReact theme as the **structural baseline** and overrides PrimeReact's own CSS variables on `:root` to repaint the whole UI. The `--cratis-*` tokens follow along through `tokens.css`'s cascade, so Cratis-scoped surfaces stay in sync. You can also override the Cratis tokens independently when you want Cratis surfaces to differ from PrimeReact widgets. + +> **PrimeReact version note.** The examples below override PrimeReact **v10** theme variables (`--surface-*`, `--primary-color`, …). On **PrimeReact v11**, customize the palette through `@primeuix/themes` instead — define a preset with `definePreset` (or override the `--p-*` design tokens). Either way you don't touch the `--cratis-*` layer: it resolves the v11 design token first and falls back to the v10 variable, so Cratis-scoped surfaces follow your palette on both majors. + +## Setup + +```tsx +// 1. PrimeReact theme provides the structure. +import 'primereact/resources/themes/lara-dark-blue/theme.css'; +import 'primeicons/primeicons.css'; +import '@cratis/components/styles'; +// 2. Your palette overrides — must come after the theme so they win. +import './palette.override.css'; +``` + +## With plain CSS + +Override PrimeReact's variables — these are what its widgets read directly: + +```css +/* palette.override.css */ +:root { + /* Surfaces */ + --surface-0: #1e293b; + --surface-100: #1e293b; + --surface-ground: #020617; + --surface-section: #0f172a; + --surface-card: #1e293b; + --surface-overlay: #1e293b; + --surface-hover: #334155; + --surface-border: #334155; + + /* Text */ + --text-color: #f8fafc; + --text-color-secondary: #94a3b8; + + /* Brand */ + --primary-color: #38bdf8; + --primary-color-text: #0b1220; + + /* Selection */ + --highlight-bg: #1e40af; + --highlight-text-color: #ffffff; + + /* Geometry */ + --border-radius: 10px; + + /* --cratis-* tokens default to var(--surface-*) etc. via tokens.css, so + the overrides above flow through automatically. Set these explicitly + only if you want Cratis-scoped surfaces tinted differently. */ + --cratis-red-500: #ef4444; + --cratis-green-500: #22c55e; +} +``` + +## With TailwindCSS + +Tailwind's `@layer base` is the idiomatic spot — declare the palette once and Tailwind handles cascade and dark mode: + +```css +/* app.css */ +@import "tailwindcss"; +@import "primereact/resources/themes/lara-dark-blue/theme.css"; +@import "@cratis/components/styles"; + +@layer base { + :root { + --surface-card: theme('colors.slate.800'); + --surface-border: theme('colors.slate.700'); + --text-color: theme('colors.slate.50'); + --primary-color: theme('colors.sky.400'); + --cratis-red-500: theme('colors.red.500'); + } + + .dark { + --surface-card: theme('colors.slate.900'); + --text-color: theme('colors.slate.100'); + } +} +``` + +## Scoped overrides + +PrimeReact variables cascade like any other CSS variable, so an ancestor scope works for region-specific looks: + +```css +.dark-zone { + --surface-card: #0b1220; + --text-color: #f8fafc; + --primary-color: #60a5fa; +} +``` + +```tsx +<> + + +
+ +
+ +``` + +## Shipping multiple palettes (light / dark / brand variants) + +Put each palette behind a class on the root element and toggle the class with your theme switcher: + +```css +:root.theme-light { + --surface-card: #ffffff; + --text-color: #0f172a; + /* … */ +} + +:root.theme-dark { + --surface-card: #1e293b; + --text-color: #f8fafc; + /* … */ +} + +:root.theme-brand { + --surface-card: #1f1147; + --text-color: #ede9fe; + --primary-color: #a78bfa; + /* … */ +} +``` + +```tsx +document.documentElement.classList.add('theme-dark'); +``` + +## What `--cratis-*` tokens are for in this setup + +Two override surfaces are available, with different reach: + +- **PrimeReact variables** (`--surface-card`, `--text-color`, `--primary-color`, …) — read by PrimeReact widgets directly. Override these to repaint the whole UI (PrimeReact widgets *and* Cratis-scoped surfaces, because Cratis tokens cascade to PrimeReact's via `tokens.css`). +- **`--cratis-*` tokens** — read only by Cratis-scoped surfaces (validation errors, FormElement addon, breadcrumb borders, etc.). Override these when you want Cratis surfaces to differ from PrimeReact widgets, *without* repainting PrimeReact widgets. + +See [Cratis token reference](cratis-tokens.md) for the full Cratis token list, and [Pass-through cheat sheet](pass-through.md) when you want even tighter per-component control. + +## When to use fully unstyled mode + +This setup stops being a good fit when: + +- You need to restyle the structural chrome itself — for example, a non-rectangular Dialog frame, a completely different Button shape, or a design system with a custom focus-ring system. Use [fully unstyled mode](unstyled.md) and bring the visuals yourself. diff --git a/Documentation/Styling/getting-started.md b/Documentation/Styling/getting-started.md new file mode 100644 index 0000000..00bd529 --- /dev/null +++ b/Documentation/Styling/getting-started.md @@ -0,0 +1,61 @@ +# Getting Started + +Every styling option shares the same one-line setup. The differences come from what you load **on top** of this baseline. + +## Install + +Install `@cratis/components` along with the required PrimeReact peer dependencies. You only need to add them to your own `package.json` once; npm/yarn won't pull them in transitively: + +```bash +npm install @cratis/components primereact primeicons +# or +yarn add @cratis/components primereact primeicons +``` + +The optional peers (`pixi.js`, `framer-motion`, `allotment`) are only required when you use the corresponding component: + +| Component | Optional peer | +|---|---| +| `PivotViewer` | `pixi.js` (canvas) and `framer-motion` (animated panels) | +| `DataPage` resizable layout | `allotment` | + +## Wire the provider + +Mount [`CratisComponentsProvider`](../Common/cratis-components-provider.md) once at the root of your tree. The provider is a thin wrapper around PrimeReact's own `PrimeReactProvider` and is where you configure `unstyled`, `pt`, `ptOptions`, `inputStyle`, `ripple`, `appendTo`, and the rest of PrimeReact's API options: + +```tsx +import '@cratis/components/styles'; +import { CratisComponentsProvider } from '@cratis/components'; + +export const App = () => ( + + + +); +``` + +`@cratis/components/styles` ships the Tailwind utility classes used inside the package plus the `--cratis-*` CSS variable token layer that every internal Cratis surface reads from. + +If you bring your own Tailwind setup and want only the token layer, import `@cratis/components/tokens` instead: + +```tsx +import '@cratis/components/tokens'; +``` + +## What's loaded so far + +With nothing else, you've imported: + +- The Tailwind utility classes used internally by Cratis wrappers (so layout, spacing, sizing all work) +- The `--cratis-*` token layer (so Cratis-scoped surfaces have a stable variable surface to read from) +- The provider that hosts `pt` / `unstyled` / locale / overlay z-index settings + +That's enough for the wrappers to render structurally, but PrimeReact widgets need **either** a theme or `unstyled: true` + a `pt` preset to look like anything other than raw browser primitives. Choose the setup that matches how much visual control you need: + +- [Use a PrimeReact theme](themed.md) — load a theme stylesheet and start tweaking +- [Use a custom palette on top of a PrimeReact theme](custom-palette.md) — keep the theme's structure and supply your colors +- [Use fully unstyled mode](unstyled.md) — disable PrimeReact styling and bring every visual yourself + +## Using `PrimeReactProvider` directly + +`CratisComponentsProvider` is optional. If you'd rather mount `PrimeReactProvider` from `primereact/api` directly, that works too — every Cratis wrapper reads the same context. The Cratis provider exists to give Cratis a single place to layer in defaults later without breaking consumers, and to keep the setup discoverable. See [CratisComponentsProvider](../Common/cratis-components-provider.md) for the full prop reference. diff --git a/Documentation/Styling/index.md b/Documentation/Styling/index.md new file mode 100644 index 0000000..b015d75 --- /dev/null +++ b/Documentation/Styling/index.md @@ -0,0 +1,44 @@ +# Styling + +Cratis Components is built on top of PrimeReact and stays out of your way when it comes to styling. You can use the look that PrimeReact gives you out of the box, keep PrimeReact's structure while applying your own palette, or take complete control and provide every visual yourself — all without forking the library or fighting it. + +There are three supported styling options. They are not mutually exclusive: every component still exposes the same building blocks, so you can combine them per-component or per-region of your app. + +## TL;DR — choose a styling setup + +| Setup | When | Effort | What you write | +|---|---|---|---| +| [**Use a PrimeReact theme**](themed.md) | You want components to look good immediately and tweak from there. | Lowest | Theme CSS import + provider | +| [**Use a custom palette on top of a PrimeReact theme**](custom-palette.md) | You want PrimeReact's structure but your own colors. | Low | A PrimeReact theme + CSS variable overrides | +| [**Use fully unstyled mode**](unstyled.md) | You're integrating into a tightly controlled design system. | Highest | `unstyled: true` + a `pt` preset in CSS or Tailwind | + +All three setups use the same one-line setup described in [Getting Started](getting-started.md). You can change direction later because the same provider, tokens, and `pt` hooks stay available. + +## Why the first two options still load a PrimeReact theme + +In PrimeReact 10 every widget's *structural* CSS — padding, borders, dialog frame, focus rings, button shapes — ships **inside the theme file**. There is no separate primitives stylesheet. So a setup without any PrimeReact theme has no widget chrome at all and components render as the raw HTML primitives the browser supplies by default. PrimeReact 11's styled mode (`@primeuix/themes`) works the same way — the active preset supplies the chrome — so the reasoning here is unchanged across versions. + +The `--cratis-*` token layer is an additive Cratis-scoped tint for surfaces the wrappers in this package own — validation error text, the FormElement addon background, breadcrumb borders — and **is not, by itself, sufficient to skin PrimeReact widgets**. Override PrimeReact's variables when you want the whole UI in your palette. Use `unstyled: true` and a `pt` preset when you want to replace PrimeReact's visuals entirely. + +The token layer is **version-spanning**: each `--cratis-*` token resolves the PrimeReact v11 design token (`@primeuix/themes`) first and falls back to the v10 theme variable, so the same build of this package is themed correctly whether your app is on PrimeReact 10 or 11. (Note: this covers the *theming* surface only — full PrimeReact 11 component/`pt`-slot compatibility is tracked separately.) + +## Mental model + +Every component you import from `@cratis/components` is a thin wrapper around a PrimeReact component plus a few Cratis additions (validation hooks, command-form integration, …). Styling flows in three layers: + +1. **PrimeReact theme tokens** — on v10 the theme variables `--surface-card`, `--text-color`, `--primary-color`, …; on v11 the `@primeuix/themes` design tokens (`--p-*`, customized via `definePreset`). Read directly by PrimeReact widgets. Override these to repaint the whole UI. +2. **Cratis tokens** — `--cratis-surface-card`, `--cratis-text-color`, `--cratis-primary-color`, … Read only by Cratis-scoped surfaces. In `tokens.css` each resolves the PrimeReact v11 design token (e.g. `--p-content-border-color`) first and falls back to the v10 theme variable (e.g. `--surface-border`), so they stay in sync with the loaded PrimeReact theme on either major. Override these when you want a Cratis surface tinted differently from the surrounding PrimeReact widgets. +3. **PrimeReact `pt` (pass-through)** — A per-component prop that lets you attach CSS class names (or inline styles) to every slot inside a PrimeReact widget. The strongest customization knob; works hand-in-hand with `unstyled` mode. + +The [Cratis token reference](cratis-tokens.md) lists every token and the surface it tints. The [pass-through cheat sheet](pass-through.md) lists every Cratis wrapper and which pt props it exposes. + +## See also + +- [Getting Started](getting-started.md) — the one-line setup every option shares +- [Use a PrimeReact theme](themed.md) +- [Use a custom palette on top of a PrimeReact theme](custom-palette.md) +- [Use fully unstyled mode](unstyled.md) +- [Cratis token reference](cratis-tokens.md) +- [Pass-through (pt) cheat sheet](pass-through.md) +- [Combining styling setups](mixing-paths.md) +- [CratisComponentsProvider](../Common/cratis-components-provider.md) diff --git a/Documentation/Styling/mixing-paths.md b/Documentation/Styling/mixing-paths.md new file mode 100644 index 0000000..887c24c --- /dev/null +++ b/Documentation/Styling/mixing-paths.md @@ -0,0 +1,158 @@ +# Combining styling setups + +The three styling options compose. You don't have to choose one for the whole app — every Cratis wrapper still exposes the same building blocks, so you can combine them per-component or per-region. + +## Themed app with one unstyled island + +Keep the PrimeReact theme as your global baseline and opt one specific component out with the per-instance `unstyled` prop: + +```tsx +import 'primereact/resources/themes/lara-dark-blue/theme.css'; +import '@cratis/components/styles'; +import { CratisComponentsProvider, Dialog } from '@cratis/components'; + +const brandDialogPt = { + root: { className: 'rounded-3xl bg-violet-900 text-violet-50' }, + header: { className: 'px-6 py-4 border-b border-violet-700 font-semibold' }, + content: { className: 'p-6' }, +}; + +export const App = () => ( + + + + {/* This one Dialog opts out of the theme and uses its own brand visuals. */} + + … + + +); +``` + +## Unstyled app with one themed island + +Run the app fully unstyled and restore PrimeReact's defaults inside a single subtree by nesting a second `CratisComponentsProvider`: + +```tsx +import 'primereact/resources/themes/lara-dark-blue/theme.css'; +import '@cratis/components/styles'; +import { CratisComponentsProvider } from '@cratis/components'; +import { globalPt } from './pt-preset'; + +export const App = () => ( + + + + {/* Inside this subtree, components use PrimeReact's theme defaults. */} + + + + +); +``` + +## App-wide dark mode + +Put each palette behind a class on the root element and toggle the class with your theme switcher. PrimeReact widgets and Cratis surfaces both follow because the `--cratis-*` tokens cascade from PrimeReact variables by default: + +```css +:root.theme-light { + --surface-card: #ffffff; + --surface-border: #e2e8f0; + --text-color: #0f172a; + --primary-color: #2563eb; +} + +:root.theme-dark { + --surface-card: #1e293b; + --surface-border: #334155; + --text-color: #f8fafc; + --primary-color: #38bdf8; +} +``` + +```tsx +const ThemeToggle = () => { + const toggle = () => { + const root = document.documentElement; + root.classList.toggle('theme-dark'); + root.classList.toggle('theme-light'); + }; + 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 visual override inside unstyled mode + +When you're using fully unstyled mode 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 + +- [Use a PrimeReact theme](themed.md) +- [Use a custom palette on top of a PrimeReact theme](custom-palette.md) +- [Use fully unstyled mode](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..cf422b5 --- /dev/null +++ b/Documentation/Styling/pass-through.md @@ -0,0 +1,138 @@ +# 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` | +| `EventsView` | `primereact/timeline` Timeline | PrimeReact Timeline `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 + {!validationState.canSubmit && Object.keys(validationState.errors).length > 0 && ( - + Please fix validation errors )} diff --git a/Source/CommandForm/fields/InputTextField.tsx b/Source/CommandForm/fields/InputTextField.tsx index 146e3d5..7686cae 100644 --- a/Source/CommandForm/fields/InputTextField.tsx +++ b/Source/CommandForm/fields/InputTextField.tsx @@ -1,15 +1,75 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { InputText } from 'primereact/inputtext'; +import { InputText, type InputTextProps } from 'primereact/inputtext'; import React from 'react'; import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +/** + * Component-level props for {@link InputTextField}. Combined at runtime with + * the field-level props injected by `asCommandFormField` (`value`, `onChange`, + * `onBlur`, `invalid`). + */ interface InputTextComponentProps extends WrappedFieldProps { + /** HTML input type. Defaults to `'text'`. */ type?: 'text' | 'email' | 'password' | 'color' | 'date' | 'datetime-local' | 'time' | 'url' | 'tel' | 'search'; + + /** Placeholder text shown when the field is empty. */ placeholder?: string; + + /** Extra CSS class name forwarded to the PrimeReact InputText. Combined with the default `w-full`. */ + className?: string; + + /** PrimeReact pass-through configuration applied to the underlying InputText. */ + pt?: InputTextProps['pt']; + + /** PrimeReact pass-through options applied to the underlying InputText. */ + ptOptions?: InputTextProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying InputText. */ + unstyled?: boolean; } +/** + * A single-line text field for use inside a Cratis Arc `CommandForm` (or any + * of its dialog-hosted variants like {@link CommandDialog}, + * {@link StepperCommandDialog}, {@link CommandStepper}). + * + * ## How it binds to the command + * + * The `value` prop is an accessor function — `value={c => c.name}` — where + * `c` is the typed command instance. The `asCommandFormField` HOC from + * `@cratis/arc.react/commands` reads the accessor, subscribes the field to + * that property on the form context, and threads validation state back to + * the input. You never read or write the command instance directly; the + * field handles it. + * + * The accessor pattern means the *binding* is fully typechecked end-to-end + * — if the command's `name` property is a `string`, the field's value type + * is inferred as `string` and TypeScript catches any mismatch. + * + * ## What's unique vs. PrimeReact's `InputText` + * + * - Bound to a single command property, no manual `onChange`/`setState`. + * - Validation state (`invalid` border) is driven automatically from the + * `CommandResult.validationResults` returned by the backend's `Handle()`. + * - All HTML input `type`s PrimeReact supports (`text`, `email`, + * `password`, `url`, `tel`, `date`, `datetime-local`, `time`, `color`, + * `search`) work the same way. + * + * ## Styling + * + * Forwards `pt` / `ptOptions` / `unstyled` / `className` to the underlying + * `InputText`. The default `w-full` class is preserved when consumer + * `className` is supplied. See [pass-through cheat sheet](../../../Documentation/Styling/pass-through.md). + * + * ```tsx + * c.email} + * type="email" + * title="Email" + * placeholder="you@example.com" /> + * ``` + */ export const InputTextField = asCommandFormField( (props) => ( ( onBlur={props.onBlur} invalid={props.invalid} placeholder={props.placeholder} - className="w-full" + className={props.className ? `w-full ${props.className}` : 'w-full'} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled} /> ), { diff --git a/Source/CommandForm/fields/MultiSelectField.tsx b/Source/CommandForm/fields/MultiSelectField.tsx index 518a46a..60e69f5 100644 --- a/Source/CommandForm/fields/MultiSelectField.tsx +++ b/Source/CommandForm/fields/MultiSelectField.tsx @@ -2,20 +2,62 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; -import { MultiSelect } from 'primereact/multiselect'; +import { MultiSelect, type MultiSelectProps } from 'primereact/multiselect'; import React from 'react'; +/** + * Component-level props for {@link MultiSelectField}. + */ interface MultiSelectFieldComponentProps extends WrappedFieldProps> { + /** Source array of objects to populate the multi-select options. */ options: Array>; + + /** Property name on each option object used as the underlying value. */ optionValue?: string; + + /** Property name on each option object used as the visible label. */ optionLabel?: string; + + /** Placeholder text shown when nothing is selected. */ placeholder?: string; + + /** How the selection is displayed in the field: comma-separated or as chips. */ display?: 'comma' | 'chip'; + + /** Maximum number of selected labels to show before collapsing into a count. */ maxSelectedLabels?: number; + + /** When true, shows a filter input in the dropdown panel. */ filter?: boolean; + + /** When true, shows a clear icon that resets the selection. */ showClear?: boolean; + + /** Extra CSS class name combined with the default `w-full`. */ + className?: string; + + /** PrimeReact pass-through configuration applied to the underlying MultiSelect. */ + pt?: MultiSelectProps['pt']; + + /** PrimeReact pass-through options applied to the underlying MultiSelect. */ + ptOptions?: MultiSelectProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying MultiSelect. */ + unstyled?: boolean; } +/** + * A multi-select dropdown field bound to an `Array` + * property on a Cratis Arc command. Use for "pick any subset of these" + * controls with optional in-panel filtering. See {@link InputTextField} + * for the full `value={c => c.prop}` binding model. + * + * ```tsx + * c.tagIds} + * options={tags} optionValue="id" optionLabel="name" + * display="chip" filter title="Tags" /> + * ``` + */ export const MultiSelectField = asCommandFormField( (props) => ( ), { diff --git a/Source/CommandForm/fields/NumberField.tsx b/Source/CommandForm/fields/NumberField.tsx index bad5972..72f3fce 100644 --- a/Source/CommandForm/fields/NumberField.tsx +++ b/Source/CommandForm/fields/NumberField.tsx @@ -1,17 +1,49 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { InputNumber } from 'primereact/inputnumber'; +import { InputNumber, type InputNumberProps } from 'primereact/inputnumber'; import React from 'react'; import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +/** + * Component-level props for {@link NumberField}. + */ interface NumberFieldComponentProps extends WrappedFieldProps { + /** Placeholder text shown when the field is empty. */ placeholder?: string; + + /** Minimum allowed value. */ min?: number; + + /** Maximum allowed value. */ max?: number; + + /** Increment/decrement step applied by the spinner buttons. */ step?: number; + + /** Extra CSS class name combined with the default `w-full`. */ + className?: string; + + /** PrimeReact pass-through configuration applied to the underlying InputNumber. */ + pt?: InputNumberProps['pt']; + + /** PrimeReact pass-through options applied to the underlying InputNumber. */ + ptOptions?: InputNumberProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying InputNumber. */ + unstyled?: boolean; } +/** + * A numeric input field bound to a `number` property on a Cratis Arc + * command. Defaults to integer mode without thousands grouping; pass `step` + * to enable spinner increments and `min` / `max` to clamp the range. See + * {@link InputTextField} for the full `value={c => c.prop}` binding model. + * + * ```tsx + * c.quantity} title="Quantity" min={0} step={1} /> + * ``` + */ export const NumberField = asCommandFormField( (props) => ( ( min={props.min} max={props.max} step={props.step} - className="w-full" + className={props.className ? `w-full ${props.className}` : 'w-full'} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled} /> ), { diff --git a/Source/CommandForm/fields/RadioButtonField.tsx b/Source/CommandForm/fields/RadioButtonField.tsx index d9d4296..91d96e9 100644 --- a/Source/CommandForm/fields/RadioButtonField.tsx +++ b/Source/CommandForm/fields/RadioButtonField.tsx @@ -1,24 +1,59 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { RadioButton, RadioButtonChangeEvent } from 'primereact/radiobutton'; +import { RadioButton, RadioButtonChangeEvent, type RadioButtonProps } from 'primereact/radiobutton'; import React from 'react'; import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +/** + * Component-level props for {@link RadioButtonField}. + */ interface RadioButtonFieldComponentProps extends WrappedFieldProps { + /** Optional label displayed next to the radio button. */ label?: string; + + /** + * The value this radio button represents. The field is selected when the + * bound command property equals this value. + */ buttonValue: string | number; + + /** Extra CSS class name forwarded to the underlying RadioButton. */ + className?: string; + + /** PrimeReact pass-through configuration applied to the underlying RadioButton. */ + pt?: RadioButtonProps['pt']; + + /** PrimeReact pass-through options applied to the underlying RadioButton. */ + ptOptions?: RadioButtonProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying RadioButton. */ + unstyled?: boolean; } +/** + * A single radio button bound to a `string` or `number` property on a + * Cratis Arc command. Multiple {@link RadioButtonField} instances with the + * same `value` accessor and distinct `buttonValue` props together form a + * mutually-exclusive group — useful when the radios need to be laid out + * non-contiguously in the form. For the common case where the radios sit + * in one place, prefer {@link RadioGroupField} which manages the group as + * a single field. See {@link InputTextField} for the full + * `value={c => c.prop}` binding model. + */ export const RadioButtonField = asCommandFormField( (props) => ( -
+
props.onChange(e.value)} onBlur={props.onBlur} invalid={props.invalid} + className={props.className} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled} /> {props.label && }
diff --git a/Source/CommandForm/fields/RadioGroupField.tsx b/Source/CommandForm/fields/RadioGroupField.tsx index e0665e6..3190395 100644 --- a/Source/CommandForm/fields/RadioGroupField.tsx +++ b/Source/CommandForm/fields/RadioGroupField.tsx @@ -1,33 +1,74 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { RadioButton, RadioButtonChangeEvent } from 'primereact/radiobutton'; +import { RadioButton, RadioButtonChangeEvent, type RadioButtonProps } from 'primereact/radiobutton'; import React from 'react'; import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +/** + * Component-level props for {@link RadioGroupField}. + */ interface RadioGroupFieldComponentProps extends WrappedFieldProps { + /** Source array of objects to populate the radio options. */ options: Array>; + + /** Property name on each option object used as the visible label. */ optionLabel: string; + + /** Property name on each option object used as the underlying value. */ optionValue: string; + + /** Layout orientation. Defaults to `'vertical'`. */ layout?: 'horizontal' | 'vertical'; + /** Extra CSS class name forwarded to the group container. */ + className?: string; + /** PrimeReact pass-through configuration applied to every inner RadioButton. */ + pt?: RadioButtonProps['pt']; + /** PrimeReact pass-through options applied to every inner RadioButton. */ + ptOptions?: RadioButtonProps['ptOptions']; + /** When true, disables every base PrimeReact style on the inner RadioButtons. */ + unstyled?: boolean; } +/** + * A radio-button group field bound to a `string` or `number` property on a + * Cratis Arc command. Use for small mutually-exclusive choice sets where + * all options should be visible at once; for larger sets, prefer + * {@link DropdownField}. See {@link InputTextField} for the full + * `value={c => c.prop}` binding model. + * + * ```tsx + * c.priority} + * options={priorityOptions} + * optionLabel="label" + * optionValue="value" + * layout="horizontal" + * title="Priority" /> + * ``` + */ export const RadioGroupField = asCommandFormField( (props) => { const layout = props.layout ?? 'vertical'; + const layoutClasses = layout === 'horizontal' ? 'flex-row gap-4 flex-wrap' : 'flex-col gap-2'; + const containerClassName = props.className + ? `flex ${layoutClasses} ${props.className}` + : `flex ${layoutClasses}`; return ( -
+
{props.options.map((option) => { const optValue = option[props.optionValue] as string | number; const optLabel = option[props.optionLabel] as string; return ( -
+
props.onChange(e.value)} onBlur={props.onBlur} invalid={props.invalid} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled} />
diff --git a/Source/CommandForm/fields/SliderField.tsx b/Source/CommandForm/fields/SliderField.tsx index 880911e..cb27a47 100644 --- a/Source/CommandForm/fields/SliderField.tsx +++ b/Source/CommandForm/fields/SliderField.tsx @@ -1,16 +1,45 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { Slider } from 'primereact/slider'; +import { Slider, type SliderProps } from 'primereact/slider'; import React from 'react'; import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +/** + * Component-level props for {@link SliderField}. + */ interface SliderFieldComponentProps extends WrappedFieldProps { + /** Minimum value. Defaults to `0`. */ min?: number; + + /** Maximum value. Defaults to `100`. */ max?: number; + + /** Increment between selectable values. Defaults to `1`. */ step?: number; + + /** Extra CSS class name combined with the default `w-full`. */ + className?: string; + + /** PrimeReact pass-through configuration applied to the underlying Slider. */ + pt?: SliderProps['pt']; + + /** PrimeReact pass-through options applied to the underlying Slider. */ + ptOptions?: SliderProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying Slider. */ + unstyled?: boolean; } +/** + * A horizontal slider field bound to a `number` property on a Cratis Arc + * command. The current value is rendered below the track for feedback. See + * {@link InputTextField} for the full `value={c => c.prop}` binding model. + * + * ```tsx + * c.volume} title="Volume" min={0} max={100} /> + * ``` + */ export const SliderField = asCommandFormField( (props) => (
@@ -20,7 +49,10 @@ export const SliderField = asCommandFormField( min={props.min ?? 0} max={props.max ?? 100} step={props.step ?? 1} - className="w-full" + className={props.className ? `w-full ${props.className}` : 'w-full'} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled} />
{props.value} diff --git a/Source/CommandForm/fields/TextAreaField.tsx b/Source/CommandForm/fields/TextAreaField.tsx index e1de09b..c6168da 100644 --- a/Source/CommandForm/fields/TextAreaField.tsx +++ b/Source/CommandForm/fields/TextAreaField.tsx @@ -1,16 +1,47 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { InputTextarea } from 'primereact/inputtextarea'; +import { InputTextarea, type InputTextareaProps } from 'primereact/inputtextarea'; import React from 'react'; import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +/** + * Component-level props for {@link TextAreaField}. + */ interface TextAreaFieldComponentProps extends WrappedFieldProps { + /** Placeholder text shown when the field is empty. */ placeholder?: string; + + /** Number of visible text rows. Defaults to `5`. */ rows?: number; + + /** Number of visible character columns. */ cols?: number; + + /** Extra CSS class name combined with the default `w-full`. */ + className?: string; + + /** PrimeReact pass-through configuration applied to the underlying InputTextarea. */ + pt?: InputTextareaProps['pt']; + + /** PrimeReact pass-through options applied to the underlying InputTextarea. */ + ptOptions?: InputTextareaProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying InputTextarea. */ + unstyled?: boolean; } +/** + * A multi-line text field bound to a `string` property on a Cratis Arc + * command. Use for descriptions, notes, or any free-form text longer than + * a single line. See {@link InputTextField} for the full + * `value={c => c.prop}` binding model that every field in this folder + * follows. + * + * ```tsx + * c.description} title="Description" rows={4} /> + * ``` + */ export const TextAreaField = asCommandFormField( (props) => ( ( placeholder={props.placeholder} rows={props.rows ?? 5} cols={props.cols} - className="w-full" + className={props.className ? `w-full ${props.className}` : 'w-full'} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled} /> ), { diff --git a/Source/Common/CratisComponentsProvider.tsx b/Source/Common/CratisComponentsProvider.tsx new file mode 100644 index 0000000..fe7efc7 --- /dev/null +++ b/Source/Common/CratisComponentsProvider.tsx @@ -0,0 +1,61 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import React, { useMemo } from 'react'; +import { PrimeReactProvider } from 'primereact/api'; +import type { APIOptions } from 'primereact/api'; +import { merge } from 'ts-deepmerge'; + +/** + * Configuration accepted by {@link CratisComponentsProvider}. Mirrors PrimeReact's + * {@link APIOptions} — the most commonly used members are `unstyled`, `pt`, `ptOptions`, + * `inputStyle`, `ripple`, `appendTo`, `zIndex` and `locale`. + */ +export type CratisComponentsConfig = Partial; + +export interface CratisComponentsProviderProps { + /** + * Cratis-wide and PrimeReact pass-through configuration. Merged on top of the + * library's defaults and made available to every Cratis component below in the tree. + */ + value?: CratisComponentsConfig; + + children: React.ReactNode; +} + +/** + * Default configuration applied to every consumer. Intentionally empty today — + * reserved for Cratis-wide opinions we may want to ship in the future (for example, + * a default pt preset that complements the --cratis-* token layer). Anything added + * here is deep-merged with the consumer's `value` so consumer settings always win. + * + * Exported so specs can verify the merge contract without re-rendering React. + */ +export const cratisDefaults: CratisComponentsConfig = {}; + +/** + * Pure merge of {@link cratisDefaults} and consumer-supplied config. Exposed for + * specs; the provider component uses the same logic inside its `useMemo`. + */ +export const mergeCratisComponentsConfig = (value: CratisComponentsConfig | undefined): CratisComponentsConfig => + merge(cratisDefaults, value ?? {}) as CratisComponentsConfig; + +/** + * Single setup point for Cratis Components. Wraps {@link PrimeReactProvider} so the + * library can layer Cratis-wide defaults on top of PrimeReact's pass-through and + * unstyled mechanisms while still letting the consumer take complete control: + * + * - Pass `unstyled: true` to disable every PrimeReact base style. The wrappers in + * this package then render structurally only and pick up all visuals from your + * own CSS, Tailwind, or pt definitions. + * - Pass `pt` / `ptOptions` to apply global per-component pass-through. + * + * Consumers who want to talk to PrimeReact directly may still mount + * {@link PrimeReactProvider} themselves — this component is an optional convenience, + * not a requirement. + */ +export const CratisComponentsProvider = ({ value, children }: CratisComponentsProviderProps) => { + const merged = useMemo(() => mergeCratisComponentsConfig(value), [value]); + + return {children}; +}; diff --git a/Source/Common/ErrorBoundary.tsx b/Source/Common/ErrorBoundary.tsx index 4b4c9b4..a0d5652 100644 --- a/Source/Common/ErrorBoundary.tsx +++ b/Source/Common/ErrorBoundary.tsx @@ -3,24 +3,66 @@ import { Component, ErrorInfo, ReactNode } from 'react'; +/** + * Props for {@link ErrorBoundary}. + */ interface Props { + /** The subtree the boundary protects. Rendered as-is when no error is caught. */ children: ReactNode; } + +/** + * Internal state captured when a child throws during render or commit. + */ interface State { + /** True once a child has thrown; switches the render path to the fallback UI. */ hasError: boolean; + /** The thrown error. Held so the fallback UI can show message and stack. */ error: Error; } +/** + * React error boundary that catches errors thrown by its descendants during + * render, lifecycle, and constructor calls. On error, it renders a minimal + * inline diagnostic (message + stack) so the rest of the application keeps + * working instead of crashing the whole tree. + * + * Wrap the boundary around any subtree whose failure should be isolated: + * + * ```tsx + * + * + * + * ``` + * + * Use one boundary per logical UI region rather than a single root-level one, + * so failures stay scoped to the feature that caused them. + */ export class ErrorBoundary extends Component { + /** Initial state — no error captured. */ public state: State = { hasError: false, error: new Error(), }; + /** + * React lifecycle hook invoked when a descendant throws. Returns the + * next state, which switches the boundary into its fallback render path. + * + * @param error - The error thrown by the descendant. + */ public static getDerivedStateFromError(error: Error): State { return { hasError: true, error: error }; } + /** + * React lifecycle hook invoked alongside {@link getDerivedStateFromError}. + * Forwards the error and React's component stack to the console so the + * failure is observable in DevTools. + * + * @param error - The error thrown by the descendant. + * @param errorInfo - React-supplied metadata including the component stack. + */ public componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Uncaught error:', error, errorInfo); } diff --git a/Source/Common/FormElement.css b/Source/Common/FormElement.css new file mode 100644 index 0000000..fc67611 --- /dev/null +++ b/Source/Common/FormElement.css @@ -0,0 +1,26 @@ +/* Copyright (c) Cratis. All rights reserved. */ +/* Licensed under the MIT license. See LICENSE file in the project root for full license information. */ + +.cratis-form-element { + display: flex; + flex: 1 1 auto; + align-items: stretch; +} + +.cratis-form-element__addon { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 0.75rem; + color: var(--cratis-text-color-secondary); + background: var(--cratis-surface-100); + border: 1px solid var(--cratis-surface-border); + border-right: 0; + border-top-left-radius: var(--cratis-border-radius); + border-bottom-left-radius: var(--cratis-border-radius); +} + +.cratis-form-element > *:not(.cratis-form-element__addon) { + flex: 1 1 auto; + min-width: 0; +} diff --git a/Source/Common/FormElement.tsx b/Source/Common/FormElement.tsx index e4c0b10..cbcf457 100644 --- a/Source/Common/FormElement.tsx +++ b/Source/Common/FormElement.tsx @@ -1,16 +1,40 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +import './FormElement.css'; + +/** + * Props for {@link FormElement}. + */ export interface FormElementProps { + /** The form input rendered to the right of the icon addon (typically an `InputText`, `Dropdown`, etc.). */ children: React.ReactNode; + + /** + * Icon node displayed inside the leading addon. Can be any React node — + * a PrimeIcons ``, an ``, or a `react-icons` + * component. + */ icon: React.ReactNode; } +/** + * Lightweight wrapper that places an icon addon to the left of a form input, + * styled with the `--cratis-*` token layer (background, border, radius). Use + * it to give input fields a leading icon without pulling in PrimeReact's + * `InputGroup` chrome. + * + * ```tsx + * }> + * + * + * ``` + */ export const FormElement = (props: FormElementProps) => { return (
-
- +
+ {props.icon} {props.children} 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/Page.tsx b/Source/Common/Page.tsx index 6b1f50f..5822492 100644 --- a/Source/Common/Page.tsx +++ b/Source/Common/Page.tsx @@ -3,13 +3,50 @@ import { HTMLAttributes, ReactNode } from 'react'; +/** + * Props for {@link Page}. Extends the standard `div` attributes so callers can + * forward `id`, `aria-*`, `data-*`, custom styles, and event handlers to the + * root element. + */ export interface PageProps extends HTMLAttributes { + /** + * Title of the page. Always passed in; only rendered when {@link showTitle} + * is true, but kept on every page so it's available for browser tab titles + * and accessibility tooling. + */ title: string; + + /** + * When true, renders the title as a level-1 heading at the top of the page. + * Defaults to `false` because many pages use a custom header or breadcrumb + * trail rendered inside `children` instead. + */ showTitle?: boolean; + + /** Page body content. */ children?: ReactNode; + + /** + * When true, wraps the body in a "panel" container that applies the + * surface, border, and radius styling defined by the `--cratis-*` tokens. + * Useful when the page is the sole occupant of a viewport region. + */ panel?: boolean } +/** + * Top-level page layout primitive. Renders a flex column that fills its parent + * vertically, with optional title heading and optional `panel` chrome around + * the main content area. Intended as the root element of every routable view. + * + * ```tsx + * + * + * + * ``` + * + * @param props - {@link PageProps}. + */ export const Page = ({ title, showTitle = false, children, panel, ...rest }: PageProps) => { return (
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/Common/for_mergeCratisComponentsConfig/when_merging_user_value.ts b/Source/Common/for_mergeCratisComponentsConfig/when_merging_user_value.ts new file mode 100644 index 0000000..34e1f6d --- /dev/null +++ b/Source/Common/for_mergeCratisComponentsConfig/when_merging_user_value.ts @@ -0,0 +1,35 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { mergeCratisComponentsConfig } from '../CratisComponentsProvider'; + +describe('when merging user value', () => { + const userPt = { dialog: { root: { className: 'consumer-dialog' } } }; + let withoutValue: Record; + let withValue: Record; + + beforeEach(() => { + withoutValue = mergeCratisComponentsConfig(undefined) as Record; + withValue = mergeCratisComponentsConfig({ unstyled: true, ripple: true, pt: userPt }) as Record; + }); + + it('should return an object when no value is given', () => { + withoutValue.should.not.be.undefined; + }); + + it('should pass an empty config when no value is given', () => { + Object.keys(withoutValue).should.have.lengthOf(0); + }); + + it('should forward unstyled from user value', () => { + withValue.unstyled!.should.equal(true); + }); + + it('should forward ripple from user value', () => { + withValue.ripple!.should.equal(true); + }); + + it('should deep-merge pt from user value', () => { + withValue.pt!.should.deep.equal(userPt); + }); +}); diff --git a/Source/Common/index.ts b/Source/Common/index.ts index 076e0ab..e73b42c 100644 --- a/Source/Common/index.ts +++ b/Source/Common/index.ts @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +export * from './CratisComponentsProvider'; export * from './ErrorBoundary'; export * from './Icon'; export * from './Page'; diff --git a/Source/DataPage/DataPage.tsx b/Source/DataPage/DataPage.tsx index 8835fd9..3ce4226 100644 --- a/Source/DataPage/DataPage.tsx +++ b/Source/DataPage/DataPage.tsx @@ -5,33 +5,62 @@ import { ReactNode, useMemo } from 'react'; import { Page } from '../Common/Page'; import React from 'react'; import { MenuItem as PrimeMenuItem } from 'primereact/menuitem'; -import { Menubar } from 'primereact/menubar'; +import { Menubar, type MenubarProps } from 'primereact/menubar'; import { IObservableQueryFor, IQueryFor, QueryFor } from '@cratis/arc/queries'; import { DataTableForObservableQuery } from '../DataTables/DataTableForObservableQuery'; -import { DataTableFilterMeta, DataTableSelectionSingleChangeEvent } from 'primereact/datatable'; +import { DataTableFilterMeta, DataTableSelectionSingleChangeEvent, type DataTableProps as PrimeDataTableProps } from 'primereact/datatable'; import { DataTableForQuery } from '../DataTables/DataTableForQuery'; import { Allotment } from 'allotment'; import { Constructor } from '@cratis/fundamentals'; /* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Props for {@link MenuItem}. Extends PrimeReact's `MenuItem` shape with one + * Cratis-specific flag. + */ export interface MenuItemProps extends PrimeMenuItem { + /** + * When true, the menu item is disabled while no row is selected in the + * surrounding {@link DataPage}. Use it for context-sensitive actions like + * "Edit" or "Delete" that require a selection. + */ disableOnUnselected?: boolean; } +/** + * Declarative menu item for use inside ``. Renders nothing + * directly; the surrounding {@link MenuItems} component reads its props and + * forwards them to the action `Menubar`. + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export const MenuItem = (_: MenuItemProps) => { return null; }; +/** + * Props for {@link MenuItems}. + */ export interface MenuItemsProps { + /** One or more `` elements. */ children: ReactNode; } +/** + * Props for {@link Columns}. + */ export interface ColumnProps { + /** PrimeReact `` elements describing each visible column. */ children: ReactNode; } +/** + * Renders an action `Menubar` at the top of a {@link DataPage}, populated from + * `` children. Each menu item's `disableOnUnselected` flag + * is automatically honored against the current row selection. + * + * Use as `` inside a ``. + */ export const MenuItems = ({ children }: MenuItemsProps) => { const context = useDataPageContext(); @@ -56,10 +85,26 @@ export const MenuItems = ({ children }: MenuItemsProps) => { return (
- +
); }; +/** + * Renders the data table at the body of a {@link DataPage}. Automatically + * selects between {@link DataTableForQuery} (snapshot query) and + * {@link DataTableForObservableQuery} (real-time observable) based on the + * `query` type provided to the surrounding ``. + * + * Use as `` inside a ``, with PrimeReact `` + * children defining the table columns. + */ export const Columns = ({ children }: ColumnProps) => { const context = useDataPageContext(); @@ -70,7 +115,11 @@ export const Columns = ({ children }: ColumnProps) => { {...context} selection={context.selectedItem} onSelectionChange={context.onSelectionChanged} - clientFiltering={context.clientFiltering}> + clientFiltering={context.clientFiltering} + className={context.tableClassName} + pt={context.tablePt} + ptOptions={context.tablePtOptions} + unstyled={context.tableUnstyled}> {children} ); @@ -80,14 +129,31 @@ export const Columns = ({ children }: ColumnProps) => { {...context} selection={context.selectedItem} onSelectionChange={context.onSelectionChanged} - clientFiltering={context.clientFiltering}> + clientFiltering={context.clientFiltering} + className={context.tableClassName} + pt={context.tablePt} + ptOptions={context.tablePtOptions} + unstyled={context.tableUnstyled}> {children} ); } }; +/** + * Props passed to the optional details component rendered on the right pane + * of a {@link DataPage} when a row is selected. + * + * @typeParam TDataType - The type of the selected item. + */ export interface IDetailsComponentProps { + /** The currently-selected row. */ item: TDataType; + + /** + * Callback the details component can invoke to ask the surrounding page to + * refresh its data — for example after the details panel has performed a + * mutating action. + */ onRefresh?: () => void; } @@ -107,9 +173,13 @@ function useDataPageContext(): IDataPageContext { } /** - * Props for the DataPage component + * Props for {@link DataPage}. + * + * @typeParam TQuery - The query class — either a snapshot `IQueryFor` or a real-time `IObservableQueryFor`. + * @typeParam TDataType - The row type returned by the query. + * @typeParam TArguments - The query's argument object type, or `object` if the query takes none. */ -export interface DataPageProps | IObservableQueryFor, TDataType, TArguments> { +export interface DataPageProps | IObservableQueryFor, TDataType extends object, TArguments> { /** * The title of the page */ @@ -174,14 +244,119 @@ export interface DataPageProps | IObservable * Callback triggered to signal data refresh */ onRefresh?(): void; + + /** + * Extra CSS class name forwarded to the inner DataTable root. + */ + tableClassName?: string; + + /** PrimeReact pass-through configuration applied to the inner DataTable. */ + tablePt?: PrimeDataTableProps['pt']; + + /** PrimeReact pass-through options applied to the inner DataTable. */ + tablePtOptions?: PrimeDataTableProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the inner DataTable. */ + tableUnstyled?: boolean; + + /** + * Extra CSS class name forwarded to the action Menubar root. + */ + menubarClassName?: string; + + /** PrimeReact pass-through configuration applied to the action Menubar. */ + menubarPt?: MenubarProps['pt']; + + /** PrimeReact pass-through options applied to the action Menubar. */ + menubarPtOptions?: MenubarProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the action Menubar. */ + menubarUnstyled?: boolean; } /** - * Represents a data driven page with a menu and custom defined columns for the data table. - * @param props Props for the DataPage component - * @returns Function to render the DataPage component + * A page primitive that combines an action menubar, a query-backed data + * table, and an optional details pane into one layout. Designed as the + * default rendering for "list view" pages in an Arc app. + * + * ## What `TQuery` is + * + * `TQuery` is the auto-generated TypeScript class produced by the Arc proxy + * generator from a C# read model query. Two flavors are accepted and + * **selected automatically at runtime** based on the class hierarchy: + * + * - **`IQueryFor`** — a snapshot query. Re-fetched + * when `queryArguments` change or when the page is mounted. Rendered + * through {@link DataTableForQuery} internally. + * - **`IObservableQueryFor`** — a real-time + * subscription. Connects to the backend over WebSocket and re-renders + * automatically whenever the underlying read model changes server-side. + * Rendered through {@link DataTableForObservableQuery} internally. + * + * You don't pick which inner table to use — `DataPage` inspects the + * prototype chain (`context.query.prototype instanceof QueryFor`) and + * mounts the correct one. + * + * ## Declarative composition + * + * Three children build up the page: + * + * - **``** wraps `` elements that + * become the action `Menubar` at the top. Each item declares its `icon`, + * `label`, and `command` (an `onClick` handler). Items can be marked + * `disableOnUnselected` so they automatically grey out until the user + * picks a row — useful for Edit / Delete actions that need a target. + * + * - **``** wraps PrimeReact `` elements that + * describe the visible columns. The columns themselves are + * PrimeReact's — anything supported by their `DataTable` `` is + * supported here (sorting, filtering, custom body templates, …). + * + * - **`detailsComponent`** (optional) is a React component rendered in a + * right-hand pane via Allotment when a row is selected. It receives the + * selected item as `item` and an `onRefresh` callback the parent can + * invoke to ask the page to refetch. + * + * ## Selection lifecycle + * + * `DataPage` keeps the current selection in local state and threads it to + * the inner table, the menubar (`disableOnUnselected` items follow it), + * and the optional details pane. The `onSelectionChange` prop is invoked + * after every change if the consumer also needs to react to it. + * + * ```tsx + * + * title="Authors" + * query={AllAuthors} // proxy from C# + * detailsComponent={AuthorDetails}> + * + * + * + * + * + * + * + * + * + * + * ``` + * + * ## Styling + * + * The inner DataTable and Menubar each have their own per-slot props: + * `tablePt` / `tableUnstyled` / `tableClassName` for the table; + * `menubarPt` / `menubarUnstyled` / `menubarClassName` for the action + * menubar. See the [pass-through cheat sheet](../../Documentation/Styling/pass-through.md) + * for the full slot reference. + * + * @typeParam TQuery - The query class (proxy generated from a C# read model query). + * @typeParam TDataType - The row type returned by the query. + * @typeParam TArguments - The query's argument object type. + * @param props - {@link DataPageProps}. */ -const DataPage = | IObservableQueryFor, TDataType, TArguments extends object>(props: DataPageProps) => { +const DataPage = | IObservableQueryFor, TDataType extends object, TArguments extends object>(props: DataPageProps) => { const [selectedItem, setSelectedItem] = React.useState(undefined); const selectionChanged = (e: DataTableSelectionSingleChangeEvent) => { diff --git a/Source/DataTables/DataTableForObservableQuery.tsx b/Source/DataTables/DataTableForObservableQuery.tsx index e2b2dbd..5a9008d 100644 --- a/Source/DataTables/DataTableForObservableQuery.tsx +++ b/Source/DataTables/DataTableForObservableQuery.tsx @@ -1,15 +1,19 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { DataTable, DataTableFilterMeta, DataTableSelectionSingleChangeEvent } from 'primereact/datatable'; -import { Paginator } from 'primereact/paginator'; +import { DataTable, DataTableFilterMeta, DataTableSelectionSingleChangeEvent, type DataTableProps as PrimeDataTableProps } from 'primereact/datatable'; +import { Paginator, type PaginatorProps } from 'primereact/paginator'; import { Constructor } from '@cratis/fundamentals'; import { IObservableQueryFor, Paging } from '@cratis/arc/queries'; import { useObservableQueryWithPaging } from '@cratis/arc.react/queries'; import { ReactNode, useState, useRef, useEffect } from 'react'; /** - * Props for the DataTableForQuery component + * Props for {@link DataTableForObservableQuery}. + * + * @typeParam TQuery - The query class implementing `IObservableQueryFor`. + * @typeParam TDataType - The row type returned by the query. + * @typeParam TArguments - The query's argument object type. */ export interface DataTableForObservableQueryProps, TDataType extends object, TArguments extends object> { /** @@ -61,14 +65,96 @@ export interface DataTableForObservableQueryProps['pt']; + + /** PrimeReact pass-through options applied to the underlying DataTable. */ + ptOptions?: PrimeDataTableProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying DataTable. */ + unstyled?: boolean; + + /** PrimeReact pass-through configuration applied to the inner Paginator. */ + paginatorPt?: PaginatorProps['pt']; + + /** PrimeReact pass-through options applied to the inner Paginator. */ + paginatorPtOptions?: PaginatorProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the inner Paginator. */ + paginatorUnstyled?: boolean; } const paging = new Paging(0, 20); /** - * Represents a DataTable for a query. - * @param props Props for the component - * @returns Function to render the DataTableForQuery component + * A paged data table bound to a Cratis Arc **observable** query + * (`IObservableQueryFor`). Subscribes via + * `useObservableQueryWithPaging` from `@cratis/arc.react/queries`, which + * opens a WebSocket connection to the backend and re-renders the table + * automatically whenever the underlying read model changes server-side. + * + * ## What `TQuery` is + * + * `TQuery` is the auto-generated TypeScript class produced by the Arc proxy + * generator from a C# read model's static observable query method (one + * that returns `ISubject` on the backend). The proxy hooks the + * WebSocket subscription up; you only deal with the resulting React data. + * + * ## What's unique + * + * - **Real-time updates**: server-side projection writes flow into the + * table within the same render cycle, with no manual polling or refresh + * button. The connection re-subscribes automatically when + * `queryArguments` change. + * - **Resize-aware scrollable height**: the wrapper observes its container + * via `ResizeObserver` and adjusts the inner DataTable height to fit, + * so the table behaves correctly inside flex layouts (it never overflows + * beyond its parent or shrinks below 200px). + * - **Lazy paging + optional client filtering**: same modes as + * {@link DataTableForQuery}, but the page is re-issued through the + * observable subscription when the user pages. + * + * Use {@link DataTableForQuery} for snapshot queries that don't need live + * updates. Use {@link DataPage} for a higher-level layout that combines + * this table with a menubar, selection, and a details pane. + * + * ## Children + * + * Children are PrimeReact `` elements — same as + * {@link DataTableForQuery}. + * + * ```tsx + * import { DataTableForObservableQuery } from '@cratis/components/DataTables'; + * import { Column } from 'primereact/column'; + * import { ActiveSessions } from './ActiveSessions'; // proxy from C# + * + * + * + * + * + * ``` + * + * Rows appear and disappear in this table as users log in / log out + * server-side — no manual refresh required. + * + * ## Styling + * + * Identical to {@link DataTableForQuery}: `pt` / `ptOptions` / `unstyled` / + * `className` target the inner DataTable; `paginatorPt` and friends target + * the inner Paginator. See [pass-through cheat sheet](../../Documentation/Styling/pass-through.md). + * + * @typeParam TQuery - The observable query class (proxy generated from C# `IObservableQueryFor`). + * @typeParam TDataType - The row type returned by the query. + * @typeParam TArguments - The query's argument object type. + * @param props - {@link DataTableForObservableQueryProps}. */ export const DataTableForObservableQuery = , TDataType extends object, TArguments extends object>(props: DataTableForObservableQueryProps) => { const [filters, setFilters] = useState(props.defaultFilters ?? {}); @@ -124,8 +210,8 @@ export const DataTableForObservableQuery =
@@ -150,17 +236,24 @@ export const DataTableForObservableQuery = + emptyMessage={props.emptyMessage} + className={props.className} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled}> {props.children}
{result.paging.totalItems > 0 && ( -
+
setPage(e.page)} + pt={props.paginatorPt} + ptOptions={props.paginatorPtOptions} + unstyled={props.paginatorUnstyled} />
)} diff --git a/Source/DataTables/DataTableForQuery.tsx b/Source/DataTables/DataTableForQuery.tsx index a2ee3c2..1ff309b 100644 --- a/Source/DataTables/DataTableForQuery.tsx +++ b/Source/DataTables/DataTableForQuery.tsx @@ -1,15 +1,19 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { DataTable, DataTableFilterMeta, DataTableSelectionSingleChangeEvent } from 'primereact/datatable'; -import { Paginator } from 'primereact/paginator'; +import { DataTable, DataTableFilterMeta, DataTableSelectionSingleChangeEvent, type DataTableProps as PrimeDataTableProps } from 'primereact/datatable'; +import { Paginator, type PaginatorProps } from 'primereact/paginator'; import { Constructor } from '@cratis/fundamentals'; import { IQueryFor, Paging } from '@cratis/arc/queries'; import { useQueryWithPaging } from '@cratis/arc.react/queries'; import { ReactNode, useState, useRef } from 'react'; /** - * Props for the DataTableForQuery component + * Props for {@link DataTableForQuery}. + * + * @typeParam TQuery - The query class implementing `IQueryFor`. + * @typeParam TDataType - The row type returned by the query. + * @typeParam TArguments - The query's argument object type, or `object` if it takes none. */ export interface DataTableForQueryProps, TDataType extends object, TArguments extends object> { /** @@ -61,14 +65,93 @@ export interface DataTableForQueryProps['pt']; + + /** PrimeReact pass-through options applied to the underlying DataTable. */ + ptOptions?: PrimeDataTableProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the underlying DataTable. */ + unstyled?: boolean; + + /** PrimeReact pass-through configuration applied to the inner Paginator. */ + paginatorPt?: PaginatorProps['pt']; + + /** PrimeReact pass-through options applied to the inner Paginator. */ + paginatorPtOptions?: PaginatorProps['ptOptions']; + + /** When true, disables every base PrimeReact style on the inner Paginator. */ + paginatorUnstyled?: boolean; } const paging = new Paging(0, 20); /** - * Represents a DataTable for a query. - * @param props Props for the component - * @returns Function to render the DataTableForQuery component + * A paged data table bound to a snapshot Cratis Arc query + * (`IQueryFor`). Subscribes via + * `useQueryWithPaging` from `@cratis/arc.react/queries`, renders the result + * page in a PrimeReact `DataTable`, and shows a `Paginator` when the result + * set exceeds one page. + * + * ## What `TQuery` is + * + * `TQuery` is the auto-generated TypeScript class produced by the Arc proxy + * generator from a C# read model's static query method. `dotnet build` + * writes a `.ts` file per query with the right return type and a `use()` + * hook; importing the class is all the connection-to-the-backend you need. + * + * ## What's unique + * + * - **Lazy paging**: the table runs in PrimeReact's `lazy` mode by default + * so the server returns one page at a time. The Paginator's + * `onPageChange` calls back into the Arc hook to fetch the next page. + * - **Client-side filtering toggle**: pass `clientFiltering` to keep the + * page-fetched rows in the browser and filter locally — useful for + * small result sets where you want PrimeReact's filter UI but don't want + * to round-trip every keystroke. + * - **Default filter state**: `defaultFilters` seeds the table's filter + * meta on first render so saved or URL-encoded filter state can be + * rehydrated. + * + * Use {@link DataTableForObservableQuery} for queries that should update in + * real time as the underlying read model changes server-side. Use + * {@link DataPage} for a higher-level layout that combines this table with + * a menubar, selection, and a details pane. + * + * ## Children + * + * Children are PrimeReact `` elements describing the visible + * columns — sorting, filtering, custom body templates, everything + * PrimeReact's `` supports. + * + * ```tsx + * import { DataTableForQuery } from '@cratis/components/DataTables'; + * import { Column } from 'primereact/column'; + * import { AllAuthors } from './AllAuthors'; // proxy from C# + * + * + * + * + * + * ``` + * + * ## Styling + * + * Forward `pt` / `ptOptions` / `unstyled` / `className` to the underlying + * PrimeReact DataTable. Use `paginatorPt` / `paginatorPtOptions` / + * `paginatorUnstyled` to style the inner Paginator independently. See + * [pass-through cheat sheet](../../Documentation/Styling/pass-through.md). + * + * @typeParam TQuery - The query class (proxy generated from C# `IQueryFor`). + * @typeParam TDataType - The row type returned by the query. + * @typeParam TArguments - The query's argument object type. + * @param props - {@link DataTableForQueryProps}. */ export const DataTableForQuery = , TDataType extends object, TArguments extends object>(props: DataTableForQueryProps) => { const [filters, setFilters] = useState(props.defaultFilters ?? {}); @@ -85,8 +168,8 @@ export const DataTableForQuery =
@@ -110,18 +193,25 @@ export const DataTableForQuery = + style={{ minWidth: '100%' }} + className={props.className} + pt={props.pt} + ptOptions={props.ptOptions} + unstyled={props.unstyled}> {props.children}
{result.paging.totalItems > 0 && ( -
+
setPage(e.page)} + pt={props.paginatorPt} + ptOptions={props.paginatorPtOptions} + unstyled={props.paginatorUnstyled} />
)} diff --git a/Source/Dialogs/BusyIndicatorDialog.tsx b/Source/Dialogs/BusyIndicatorDialog.tsx index 32d5f71..d6b052e 100644 --- a/Source/Dialogs/BusyIndicatorDialog.tsx +++ b/Source/Dialogs/BusyIndicatorDialog.tsx @@ -5,11 +5,58 @@ import { BusyIndicatorDialogRequest } from '@cratis/arc.react/dialogs'; import { ProgressSpinner } from 'primereact/progressspinner'; import { Dialog } from './Dialog'; +/** + * Modal "busy" dialog used by the `@cratis/arc.react` dialog host whenever a + * long-running operation needs to block user interaction. Renders a spinner + * and a message, with no confirm/cancel/X buttons — the dialog dismisses + * itself when the host removes it. + * + * ## How to use it + * + * **You do not instantiate this component directly.** It's rendered by the + * dialog host machinery in response to a request. Trigger one from anywhere + * in your tree by calling the host's `showBusyIndicator` helper: + * + * ```tsx + * import { useDialogs } from '@cratis/arc.react/dialogs'; + * + * const Save = () => { + * const { showBusyIndicator, hideBusyIndicator } = useDialogs(); + * + * const onSave = async () => { + * showBusyIndicator({ title: 'Saving', message: 'Persisting your changes…' }); + * try { + * await someLongRunningCommand.execute(); + * } finally { + * hideBusyIndicator(); + * } + * }; + * + * return
); } @@ -251,14 +294,14 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals showIcon style={inputStyle} /> - {error && {error}} + {error && {error}}
); } if (property.type === 'array') { return ( -
+
Array editing not yet supported
); @@ -266,7 +309,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals if (property.type === 'object') { return ( -
+
Object editing not yet supported
); @@ -283,7 +326,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals rows={3} style={inputStyle} /> - {error && {error}} + {error && {error}}
); } @@ -295,7 +338,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals onChange={(e) => handleChange(e.target.value)} style={inputStyle} /> - {error && {error}} + {error && {error}}
); }; @@ -308,7 +351,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 +364,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 +377,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 +389,7 @@ export const ObjectContentEditor = ({ object, timestamp, schema, editMode = fals {currentData.map((item, index) => ( {index > 0 && ( - + )} @@ -422,7 +465,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.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'}

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 ( -
+