diff --git a/.gitignore b/.gitignore index 8f0aa93..be0d98c 100644 --- a/.gitignore +++ b/.gitignore @@ -163,6 +163,10 @@ PublishScripts/ **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ +# ...and except our own source, where "packages" is a real folder of real code. +# Without this, Source/JavaScript/*/packages/ is silently dropped from every commit: +# the build stays green locally because the files are on disk, and CI fails on a fresh clone. +!Source/JavaScript/*/packages/** # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignorable files diff --git a/Documentation/blueprint-components/add-a-template.md b/Documentation/blueprint-components/add-a-template.md new file mode 100644 index 0000000..3e489b0 --- /dev/null +++ b/Documentation/blueprint-components/add-a-template.md @@ -0,0 +1,180 @@ +--- +title: Add a template of your own +description: Write an Arc-bound page template that composes with the default blueprint's shell, and prove it does. +--- + +Sooner or later your application needs a page shape this blueprint does not ship — a queried list with an +approval rail down the side, a command page that opens with a summary. This is the recipe. + +Two rules make the difference between a template that composes and one that only works where you first put +it. Both are stated up front because both are easy to get wrong in a way that looks fine. + +## Rule one: never invent a slot name + +`fitsSlot` names the slot on your parent that you fill. It is resolved against *whatever contains you*, so +the name has to be one your parent really declares. There are exactly two vocabularies: + +- `SlotName` — the default blueprint's layout regions: `topbar`, `sidebar`, `menu`, `breadcrumb`, + `content`, `footer`, `rightPanel`, `configPanel`, `aside`. +- `TemplateSlotName` — what a screen template offers to what it contains: `header`, `body`, `sidePanel`, + `toolbar`, `actions`, `stats`, `primary`, `secondary`. + +A whole page fits `SlotName.Content`, because that is the only region a layout offers a screen. Anything +nested fits one of its parent's `TemplateSlotName`s. + +## Rule two: within one chain, a fitted slot name has exactly one declarer + +`resolveScreenTemplates` places a template only when the containers in scope agree on **exactly one** home +for it. Two containers declaring the same name is not resolved by preferring the nearer one — it is +reported as unplaced, because guessing would put content in the wrong region, which is far harder to +diagnose than being told the name is ambiguous. + +So a chain that reuses `body` at three levels reads perfectly and resolves to nothing. This blueprint's +chain is built to avoid that: `body` is declared only by the module, `primary` only by the feature. + +```typescript +export const dataModulePageTemplate: ScreenTemplate = { + name: 'DataModulePage', + fitsSlot: SlotName.Content, + slots: [{ name: TemplateSlotName.Header }, { name: TemplateSlotName.Body }, { name: TemplateSlotName.SidePanel }], + // ... +}; + +export const dataFeatureSectionTemplate: ScreenTemplate = { + name: 'DataFeatureSection', + fitsSlot: TemplateSlotName.Body, + slots: [{ name: TemplateSlotName.Toolbar }, { name: TemplateSlotName.Primary }, { name: TemplateSlotName.Secondary }], + // ... +}; + +export const commandSliceSectionTemplate: ScreenTemplate = { + name: 'CommandSliceSection', + fitsSlot: TemplateSlotName.Primary, + slots: [{ name: TemplateSlotName.Header }, { name: TemplateSlotName.Actions }], + // ... +}; +``` + +`header` appears twice and that is fine — nothing in the chain *fits* `header`, so it is never ambiguous. +The constraint is only on names something fits. + +## Write the template + +Start from the element builders this package exports, so you are not rebuilding decisions it already made: + +```typescript +import { ScreenTemplate } from '@cratis/scene.model'; +import { SlotName, TemplateSlotName } from '@cratis/scene.blueprint.default'; +import { arcPageHeader, dataPage, page, toolbar, toolbarButton } from '@cratis/scene.blueprint.components'; + +export const purchaseOrderListTemplate: ScreenTemplate = { + name: 'PurchaseOrderList', + fitsSlot: SlotName.Content, + slots: [{ name: TemplateSlotName.Header }, { name: TemplateSlotName.Body }], + content: { + [TemplateSlotName.Header]: [ + arcPageHeader('po-header', { title: 'Purchase orders', subtitle: 'Awaiting approval', section: 'Procurement', query: 'AllPurchaseOrders' }, [ + toolbar('po-actions', [toolbarButton('po-approve', 'Approve the selected order', 'pi pi-check', 'Approve')]), + ]), + ], + [TemplateSlotName.Body]: [ + dataPage('po-body', 'AllPurchaseOrders', { emptyMessage: 'No purchase orders yet', dataKey: 'purchaseOrderId', globalFilterFields: ['number', 'supplier'] }, [ + { field: 'number', header: 'Number' }, + { field: 'supplier', header: 'Supplier' }, + { field: 'total', header: 'Total' }, + ]), + ], + }, + displayName: 'Purchase order list', + description: 'A dataPage bound to the purchase order query, with an approval action.', +}; +``` + +Three things in there are worth calling out. + +**The binding is a name, never a class.** A property bag carries plain values and named slots; there is no +way to put a TypeScript class in one. `resolveElementBinding` reads the name back out at render time and +looks it up in the binding registry — do not invent a second mechanism for reaching a backend, because the +one that exists is the one every adapter uses. + +**`emptyMessage` and `dataKey` are required by `TableOptions` on purpose.** The library's adapters both +default them to an empty string, and both defaults are quietly wrong in a shipped template: a table with no +empty message reads as broken when the query legitimately returns nothing, and one with no data key loses +its selection every time the query is re-performed. + +**The header carries the binding too.** Give it the same one the body is built around, and the heading, the +trail and the design-time state all follow from saying it once — instead of three copies that drift. + +## Add an arrangement only when declaration order is wrong + +A template with no `arrangement` renders its slots in declaration order, which is right more often than +not. Add one when the page has a real two-dimensional shape, and add the compact override in the same +breath: + +```typescript +arrangement: { + root: column([slotLeaf(TemplateSlotName.Header), grid([slotLeaf(TemplateSlotName.Primary, { span: 2 }), slotLeaf(TemplateSlotName.Secondary)], 3, 16)]), + overrides: [ + { + width: WidthSizeClass.Compact, + root: column([slotLeaf(TemplateSlotName.Header), slotLeaf(TemplateSlotName.Primary), slotLeaf(TemplateSlotName.Secondary)], 16), + }, + ], +}, +``` + +A leaf naming a slot you never declared positions nothing and reports nothing — the region simply is not +there. No type can express "one of whatever this container declared", so prove it with a spec instead. + +## Prove it composes + +Three checks, and every one of them is a spec this package already runs against its own templates. + +**It finds exactly one home**, with the other blueprint's templates in scope too — because that is the +situation a real profile is in: + +```typescript +const resolution = resolveScreenTemplates(appShellLayout, [...(defaultBlueprint.screenTemplates ?? []), purchaseOrderListTemplate]); +resolution.unplaced.filter(unplaced => unplaced.template === 'PurchaseOrderList').should.be.empty; +``` + +**Every component name it writes resolves**, against a catalog built from the real manifests: + +```typescript +const names = distinctComponentNames(Object.values(purchaseOrderListTemplate.content ?? {}).flat()); +names.map(name => resolveComponentName(name, componentsBlueprintProfile, componentsBlueprintCatalog)) + .filter(resolution => resolution === undefined) + .should.be.empty; +``` + +If a name does not resolve, fix the template rather than the spec. The default blueprint hit exactly this: +it referenced `chart` and `fileUpload`, which neither library declares — two names that look obvious and +render as dashed red boxes. + +**Its content is filed only under slots it declares:** + +```typescript +const declared = new Set(purchaseOrderListTemplate.slots.map(slot => slot.name)); +Object.keys(purchaseOrderListTemplate.content ?? {}).filter(name => !declared.has(name)).should.be.empty; +``` + +## When to write a component instead + +Almost never. Prefer a template every time: a template is data a host can rearrange, and a component is +code it cannot. + +The bar this blueprint holds itself to is that a content tree genuinely *cannot express* the composition — +which in practice means the thing derives or looks something up at render time rather than holding it. One +component in this package clears it, and it is worth reading +[layering on another blueprint](layering-on-a-blueprint.md) for why. If you do register one, declare it in +your manifest, or `validatePackageBundle` fails: + +```typescript +validatePackageBundle(componentsBlueprint).should.be.empty; +``` + +## Where to go next + +- [The template catalogue](template-catalogue.md) — the shapes you may not need to write at all. +- [Wire the binding registry](wiring-the-binding-registry.md) — supplying the class behind your name. +- [Ship your own blueprint](../blueprints/ship-your-own-blueprint.md) — when a template set is not enough. diff --git a/Documentation/blueprint-components/getting-started.md b/Documentation/blueprint-components/getting-started.md new file mode 100644 index 0000000..8db8fb3 --- /dev/null +++ b/Documentation/blueprint-components/getting-started.md @@ -0,0 +1,134 @@ +--- +title: Use the Components blueprint +description: Activate the blueprint, boot one of its pages through the real engine, and register the query behind it. +--- + +Let's take an Arc-bound page from "nothing" to "showing real invoices". Three steps: list the package, +render a screen, register the query. + +## List it in your profile + +A blueprint is a packaged artifact, not a language construct — it never appears in a `.play` file. A +`ui profile` lists it by name, exactly like a component library: + +```screenplay +ui profile Desktop + target platform web + target size expanded + + packages + core + Tailwind + PrimeReact + Cratis.Components + Cratis.Blueprint.Default + Cratis.Blueprint.Components +``` + +Declaration order is ascending override priority, and this order is the one the package itself resolves +against: + +```typescript +export const componentsBlueprintProfile: UiProfile = { + name: 'Arc pages', + targetPlatform: 'web', + packages: ['core', 'PrimeReact', cratisComponentsPackageManifest.name, defaultBlueprintName, componentsBlueprintName], +}; +``` + +`Cratis.Components` sits above `PrimeReact`, so a template naming `table` or `dialog` gets the Arc-aware +one and PrimeReact's is recorded as shadowed rather than discarded. Both blueprints sit above both +libraries, so `pageHeader` and `arcPageHeader` resolve to the packages that declare them. + +You do not have to write all six by hand. Declaring only the blueprint and letting the resolver expand it +gives you the same list, in a valid order: + +```typescript +const selection = resolvePackageDependencies(['Cratis.Blueprint.Components'], catalog); +isPackageSelectionValid(selection).should.be.true; +``` + +If a dependency is missing from the catalog, you get told which one while the profile is being configured, +rather than finding out when the page opens. + +## Render a page + +The package ships a `Screen` per template, so a host can boot one through the real engine with nothing else +in place: + +```tsx +import { GalleryScreenPreview } from '@cratis/scene.blueprint.components'; + + +``` + +Nothing about that path is preview-only. The screen is a real `Screen`, its bare component names go +through the real `resolveComponentName`, and it is rendered by the real `SceneElementView` against the real +merged registry — the same path a shipped application takes. + +What you see is a complete application shell — topbar, sidebar, breadcrumb, footer, the configurator — with +an invoice page inside it. The header reads: + +```text +Billing › Invoices +Invoices +Every invoice, filterable and paged against the server +No query registered as AllInvoices +``` + +…and where the table should be: + +```text +Unresolved query binding 'AllInvoices' on Cratis.Components:dataPage +``` + +That last line is not an error to fix before continuing. It is the design-time state: the template carries +a query *name*, and nothing has supplied the class behind it yet. + +## Register the query + +Only a host owns the generated Arc proxies, so only a host can close that gap. It registers every proxy a +screen can name, once, at startup: + +```typescript +import { registerQueries, registerCommands } from '@cratis/scene.components'; +import { AllInvoices, InvoiceById, InvoicesInFlight } from './Billing/proxies'; +import { RegisterInvoice, RecordAdjustment } from './Billing/commands'; + +registerQueries({ AllInvoices, InvoiceById, InvoicesInFlight }); +registerCommands({ RegisterInvoice, RecordAdjustment }); +``` + +Render the same screen again and two things change. The header now reads `Bound to query AllInvoices`, and +the placeholder is gone — the `dataPage` has its class, performs the query, and pages against your backend. + +Object shorthand is the point of the bulk form: Stage generates a module exporting every proxy it produced, +and handing that module's exports straight in stays correct as proxies are added and removed without anyone +editing a list. [Wiring the binding registry](wiring-the-binding-registry.md) covers which names this +blueprint's templates ask for. + +## Point a template at your own query + +The shipped names are defaults, not fixtures. A template ships bound to something so that it renders as a +*page* rather than a diagram of one; an application replaces the name and nothing else about the template +changes: + +```typescript +const invoiceList: ScreenTemplate = { + ...dataListPageTemplate, + name: 'PurchaseOrderList', + content: { + [TemplateSlotName.Header]: [arcPageHeader('po-header', { title: 'Purchase orders', section: 'Procurement', query: 'AllPurchaseOrders' })], + [TemplateSlotName.Body]: [dataPage('po-body', 'AllPurchaseOrders', purchaseOrderTableOptions, purchaseOrderColumns)], + }, +}; +``` + +At that point you are writing templates, not screens — which is the moment to read +[add a template of your own](add-a-template.md). + +## Where to go next + +- [Wire the binding registry](wiring-the-binding-registry.md) — every name these templates ask for. +- [The template catalogue](template-catalogue.md) — the other ten screen templates and the three dialogs. +- [Add a template of your own](add-a-template.md) — the two rules that keep a new template composable. diff --git a/Documentation/blueprint-components/index.md b/Documentation/blueprint-components/index.md new file mode 100644 index 0000000..a08a9e3 --- /dev/null +++ b/Documentation/blueprint-components/index.md @@ -0,0 +1,110 @@ +--- +title: Cratis Components blueprint +description: A blueprint of Arc-bound screen and dialog templates - whole pages built from the Cratis Components composites, for the default blueprint's shell. +--- + +You have picked the [default blueprint](../blueprints/index.md), so your application has a shell: a topbar, a +sidebar with eight menu modes, a breadcrumb, a content region and two themes. You have listed +[`Cratis.Components`](../components-package/index.md), so a screen can name `dataPage` and get a real +list screen instead of a table it has to feed by hand. + +And then you build your first list page, and you make seven decisions: which query, which columns, what the +empty message says, which property identifies a row, which fields the search box filters across, what the +header says and where its actions go. Then you build the second one, and you make those seven decisions +again — slightly differently, because it is a week later. By the tenth page an application has ten list +screens that are almost the same, and the differences are all accidents. + +`Cratis.Blueprint.Components` is the answer to that. It ships those decisions already made, as templates: + +```typescript +export const dataListPageTemplate: ScreenTemplate = { + name: 'DataListPage', + fitsSlot: SlotName.Content, + slots: [{ name: TemplateSlotName.Header }, { name: TemplateSlotName.Body }], + content: { + [TemplateSlotName.Header]: [arcPageHeader('data-list-header', { title: 'Invoices', section: 'Billing', query: SampleBindingName.AllInvoices })], + [TemplateSlotName.Body]: [dataPage('data-list-body', SampleBindingName.AllInvoices, invoiceTableOptions, invoiceColumns)], + }, +}; +``` + +You pick the template and supply a query name. The page is done. + +## What is in the box + +Eleven screen templates and three dialog templates. Every one of them is a whole page or a whole dialog, +not a fragment. + +| Template | What it is | +|---|---| +| `DataListPage` | A `dataPage` bound to one query, under a header stating the binding | +| `ObservableDataListPage` | The live variant, over an observable query | +| `DataListWithDetailPage` | The list, with the selected record's document and history beside it | +| `MasterDetailPage` | A queried list in the larger column, the record in the narrower one | +| `DashboardPage` | Four query-backed widgets, arranged the way people read a dashboard | +| `CommandFormPage` | A generated command form with its own action bar | +| `SchemaEditorPage` | An event type's schema, edited as a typed property tree | +| `ObjectEditorPage` | One document against its schema, with its trail and version history | +| `DataModulePage` · `DataFeatureSection` · `CommandSliceSection` | A worked three-level nesting chain | +| `CommandDialog` · `ConfirmDialog` · `BusyDialog` | The three dialog shapes an Arc application repeats | + +Every one is in [the template catalogue](template-catalogue.md), with the slots it declares and the +bindings it names. + +## How it differs from the default blueprint + +They are both blueprints, and they do opposite halves of the job. + +The default blueprint answers **"what does this application look like"**. It ships two layouts, sixteen +shell components, twenty-three screen templates and two themes, and its templates are built from +primitives — a `dataTable` handed rows, a form of `inputText` fields, a dialog assembled from a title, a +message and two buttons. Those templates are about *shape*. They render fully with no backend at all, +because there is no backend in them. + +This blueprint answers **"what does a page look like once it is bound to Arc"**. Its templates are built +from the Cratis Components composites, so a list page performs a real query, pages against the server, and +wires filtering and sorting back into it. They are about *behavior*, and the price of that is that most of a +page is a placeholder until a host registers the bindings — which is the normal design-time state and not a +shortcoming. + +| | Default blueprint | This blueprint | +|---|---|---| +| Layouts | Two application shells | **None** — it reuses the default's | +| Themes | Two | **None** — it reuses the default's | +| Templates built from | `core` and PrimeReact primitives | `Cratis.Components` Arc-bound composites | +| Renders without a backend | Completely | Headers and editors do; queried regions are placeholders | +| Components registered | Sixteen shell components | **One** | + +That "none" and that "one" are the design, not an unfinished list — see +[layering on another blueprint](layering-on-a-blueprint.md) for why an empty `layouts` is the whole point, +and [the template catalogue](template-catalogue.md) for the one component that earned its place. + +## What it needs underneath it + +```typescript +dependencies: [{ name: 'Cratis.Blueprint.Default' }, { name: 'Cratis.Components' }], +``` + +This is the first blueprint in Scene that depends on another blueprint. `resolvePackageDependencies` +expands that declaration transitively, so listing this package in a profile pulls in the default blueprint, +Cratis Components, and everything those two need in turn: + +```typescript +const selection = resolvePackageDependencies(['Cratis.Blueprint.Components'], catalog); +// selection.added contains 'PrimeReact' and 'Tailwind' +// 'Cratis.Blueprint.Default' and 'Cratis.Components' both come before 'Cratis.Blueprint.Components' +``` + +The ordering is not incidental. Declaration order in a profile is ascending override priority, so this +package outranking both means a template of its own can shadow one of the default blueprint's if it ever +needs to — and `Cratis.Components` outranking `PrimeReact` is what makes a template naming `table` resolve +to the Arc-aware one. + +## Where to go next + +- [Use the Components blueprint](getting-started.md) — activate it, render a page, register a binding. +- [Wire the binding registry](wiring-the-binding-registry.md) — what a host has to supply, and what happens + before it does. +- [Add a template of your own](add-a-template.md) — the recipe, and the two rules that keep it composable. +- [Layering on another blueprint](layering-on-a-blueprint.md) — why `layouts` is empty and what that buys. +- [The template catalogue](template-catalogue.md) — every template, slot and binding. diff --git a/Documentation/blueprint-components/layering-on-a-blueprint.md b/Documentation/blueprint-components/layering-on-a-blueprint.md new file mode 100644 index 0000000..e8ba29f --- /dev/null +++ b/Documentation/blueprint-components/layering-on-a-blueprint.md @@ -0,0 +1,144 @@ +--- +title: Layering on another blueprint +description: Why this blueprint ships no layout and no theme, and what depending on another blueprint buys that shipping a rival shell would have cost. +--- + +Open this package's manifest and the first thing that looks wrong is an empty array: + +```typescript +export const componentsBlueprintManifest: ScenePackage = { + name: componentsBlueprintName, + version: '1.0.0', + kind: PackageKind.Blueprint, + dependencies: [{ name: 'Cratis.Blueprint.Default' }, { name: 'Cratis.Components' }], + components: Object.values(ComponentName), + layouts: [], + // ... + themes: [], +}; +``` + +A blueprint is *the package that ships the shape of an application* — its layouts, the templates built on +them, the components that fill their slots, the themes that color all of it. This one ships no layout at +all. It is worth being clear that this is the design, because it is the first blueprint-on-blueprint +dependency in Scene and the empty array is easy to read as an unfinished list. + +## An application activates one shell + +A **layout** is an application's base navigational shell, and an application has exactly one. That is not a +limitation of the model, it is what the word means: the shell is the thing every page lives inside, and +there cannot be two. + +So a blueprint that ships a layout is, in effect, asking to be the only blueprint. Two of them are mutually +exclusive by construction, and choosing between them is choosing between everything each one holds. + +```mermaid +flowchart TB + subgraph rival["If this package shipped its own shell"] + direction TB + R1["Cratis.Blueprint.Default
AppShell · 8 modes · 2 themes"] + R2["Cratis.Blueprint.Components
ArcShell · Arc pages"] + R3{"Application
picks one"} + R1 --> R3 + R2 --> R3 + end + subgraph layered["What it does instead"] + direction TB + L1["Cratis.Blueprint.Default
AppShell · 8 modes · 2 themes"] + L2["Cratis.Blueprint.Components
Arc pages, fitsSlot: content"] + L3["Application
gets both"] + L1 --> L3 + L2 -->|"depends on"| L1 + L2 --> L3 + end +``` + +On the left, picking Arc-bound pages costs you the eight menu modes, the themes, the navigation +aggregation and the sixteen shell components the default blueprint already does well. On the right it costs +nothing. Layering is the only arrangement in which either package is worth having. + +## What the dependency actually does + +The dependency is not a formality. Almost everything structural on these pages comes from the other +blueprint: + +- **Its slot vocabulary.** Every template's `fitsSlot` is `SlotName.Content` — the default blueprint's + `AppShell` slot — and every template's own slots are `TemplateSlotName` values. No slot name here is + invented, because a name this package made up would fit nothing. +- **Its shell components.** The gallery's chrome is built from `topbar`, `sidebar`, `menu`, `menuItem`, + `breadcrumb`, `footer`, `configPanel`, `logo` and `userMenu` — all names the default blueprint declares. +- **Its element builders.** `externalComponent` is imported rather than reimplemented, because + `ExternalComponent` has fifteen required members before the two a template author cares about, and a + second copy of those defaults is a second thing to keep right. +- **Its composition rules.** `nestScreenTemplates`, `templateContentInLayout` and `composeScreenElement` + place a template inside the layout, and reimplementing them would mean re-deriving `fitsSlot` semantics. +- **Its themes and its layout modes.** Which is why there is a story rendering an Arc page in the shell's + slim mode: an Arc page dropped into `content` gets all eight modes for free. + +The one thing this package brings that the other does not is Arc binding. That is a genuinely different +axis, and it is why the two are separable packages rather than one. + +## Themes are empty for the same reason, smaller + +`@cratis/components` reads a `--cratis-*` variable layer that resolves whatever theme is active underneath +it, so the library asserts no palette. The shell being themed is the default blueprint's. A theme shipped +here would be this package asserting a look for a shell it does not own — so it ships none, and the +configurator on every gallery screen offers the default blueprint's themes: + +```typescript +function configPanel(): SceneElement { + return externalComponent(DefaultComponentName.ConfigPanel, DefaultComponentName.ConfigPanel, { + title: 'Settings', + themes: defaultBlueprintThemes.map(theme => ({ name: theme.name, label: theme.name, isDark: theme.isDark ?? false })), + }); +} +``` + +## One component, and why even that one + +The same reasoning runs down to the component registry, which has exactly one entry: + +```typescript +export const componentsBlueprintComponents: ComponentRegistry = { + [componentRegistryKey(componentsBlueprintName, ComponentName.ArcPageHeader)]: ArcPageHeader, +}; +``` + +The rule this package holds itself to is: register a component only when a template's content tree +genuinely cannot express the composition, and reach for a template every other time. A template is data a +host can rearrange; a component is code it cannot. A blueprint whose value sat in its components rather +than its templates would have misunderstood its job. + +`arcPageHeader` clears the bar because it *derives* rather than holds. Give it one binding name and it +produces the heading, the breadcrumb trail and the design-time binding state: + +```typescript +export function deriveArcHeading(properties: Record, binding: ElementBinding, kind: BindingKind): ArcHeading { + const title = stringProperty(properties, 'title') ?? (binding.name === undefined ? 'Untitled page' : humanizeBindingName(binding.name)); + const section = stringProperty(properties, 'section'); + + return { + title, + subtitle: stringProperty(properties, 'subtitle'), + trail: section === undefined ? [title] : [section, title], + bindingName: binding.name, + isBound: binding.target !== undefined, + bindingLabel: bindingLabelFor(binding, kind), + }; +} +``` + +A tree can hold a title, a subtitle and a trail as three literals — and then those three literals drift the +first time anything is renamed. It cannot derive them from one name, and it certainly cannot look that name +up in the binding registry to report whether a host has wired it. Both are behavior at render time, not +structure, which is exactly the line. + +Note also what it does *not* do: it registers `arcPageHeader`, not `pageHeader`. Shadowing the default +blueprint's page header would silently change that blueprint's own screens, which is not a decision this +package gets to make on its behalf. + +## Where to go next + +- [Add a template of your own](add-a-template.md) — the same rules, applied to your code. +- [The template catalogue](template-catalogue.md) — every template and the slots it declares. +- [Understanding blueprints](../blueprints/understanding-blueprints.md) — what a blueprint is in general. diff --git a/Documentation/blueprint-components/template-catalogue.md b/Documentation/blueprint-components/template-catalogue.md new file mode 100644 index 0000000..6e5a785 --- /dev/null +++ b/Documentation/blueprint-components/template-catalogue.md @@ -0,0 +1,154 @@ +--- +title: The template catalogue +description: Every screen template, dialog template, element builder and component this blueprint ships, with the slots each declares and the bindings each names. +--- + +Every template here fills the default blueprint's `AppShell` layout. Names are as a `Screen` refers to +them; slots are `TemplateSlotName` values. + +## Screen templates + +All eight page templates set `fitsSlot: SlotName.Content`, which is the only region a layout offers a +screen. The three nesting-chain templates fit each other. + +| Template | Slots | Bindings | What it is | +|---|---|---|---| +| `DataListPage` | `header`, `body` | `AllInvoices` | A `dataPage` filling the body, under a header stating the binding | +| `ObservableDataListPage` | `header`, `body` | `InvoicesInFlight` | The same shape over an observable query | +| `DataListWithDetailPage` | `header`, `body`, `sidePanel` | `AllInvoices` | The list, plus the record's document, trail and history | +| `MasterDetailPage` | `header`, `primary`, `secondary` | `AllInvoices` | A `dataTable` in the larger column, the record in the narrower | +| `DashboardPage` | `header`, `stats`, `primary`, `secondary` | `RevenueByMonth`, `InvoicesInFlight`, `OpenTickets`, `AllInvoices`, `AllAdjustments` | Query-backed widgets over a wide and a narrow column | +| `CommandFormPage` | `header`, `body`, `actions` | `RegisterInvoice` | A generated command form with its own action bar | +| `SchemaEditorPage` | `header`, `toolbar`, `body` | none | An event type's schema as a typed property tree | +| `ObjectEditorPage` | `header`, `toolbar`, `body`, `sidePanel` | `InvoiceById` | One document against its schema, with trail and history | +| `DataModulePage` | `header`, `body`, `sidePanel` | none | Module level — fits the layout's `content` | +| `DataFeatureSection` | `toolbar`, `primary`, `secondary` | `InvoicesInFlight` | Feature level — fits the module's `body` | +| `CommandSliceSection` | `header`, `actions` | `RecordAdjustment` | Slice level — fits the feature's `primary` | + +### Which list template to reach for + +Three of them look alike and are not. + +`DataListPage` is a whole `dataPage` — its own title bar, menubar and filtering — and is what you want when +the page *is* the list. + +`DataListWithDetailPage` is that same `dataPage` with a detail region added beside it. + +`MasterDetailPage` is a plain `dataTable` next to a detail region, and is right when the page's chrome comes +from the feature around it rather than from the list itself. Reaching for the wrong one shows up as a page +with two toolbars. + +### Which pages render without a backend + +`SchemaEditorPage` and `ObjectEditorPage` render completely with nothing registered, because the editors +they are built from read their content out of the property bag. Every other page renders its header, +toolbar and chrome, and shows a placeholder where the queried region will be. See +[wiring the binding registry](wiring-the-binding-registry.md) for the full split. + +### The nesting chain + +Three templates that demonstrate `fitsSlot` composing to depth, and the slot names are chosen so each +fitted name has exactly one declarer: + +```mermaid +flowchart TB + L["AppShell layout
declares content"] + M["DataModulePage
fitsSlot: content
declares header · body · sidePanel"] + F["DataFeatureSection
fitsSlot: body
declares toolbar · primary · secondary"] + S["CommandSliceSection
fitsSlot: primary
declares header · actions"] + L --> M --> F --> S +``` + +`resolveScreenTemplates` places all three, at depths 1, 2 and 3. A chain reusing `body` at every level would +read perfectly and resolve to nothing — see [add a template of your own](add-a-template.md) for why. + +## Dialog templates + +A dialog template has no `fitsSlot`, because an overlay occupies no slot: it is summoned, not placed. + +| Template | Slots | Binding | What it is | +|---|---|---|---| +| `CommandDialog` | `body` | `RecordAdjustment` | A short capture whose confirm button is the command's execution | +| `ConfirmDialog` | `body` | none | One question, with the consequence spelled out | +| `BusyDialog` | `body` | none | The blocking spinner for a long-running command | + +Each declares exactly **one** slot, which is a deliberate departure from how the default blueprint builds +its dialogs. That blueprint composes a dialog out of primitives — a title, a message, a row of buttons — +because primitives are all it has. Here the library ships whole dialogs that resolve their result through +Arc's dialog context, so the frame, the title bar and the buttons all belong to the composite. A template +declaring `header` and `actions` slots would be offering regions that render *outside* the modal, which is +worse than not offering them: a screen would fill them, and the content would appear somewhere nobody +expected. + +`CommandDialog` places its fields by hand rather than generating them, which is the opposite choice from +`CommandFormPage` and deliberate. A dialog is a *short* capture — three or four properties a person can +answer in an overlay. Generating every property of the command would turn an overlay into a page, which is +precisely the decision a dialog has already made. + +## Element builders + +Exported so an application can write its own templates from the same parts. + +| Builder | Produces | +|---|---| +| `arcPageHeader(id, options, actions?)` | This blueprint's page header | +| `page(id, title, content, panel?)` | The library's `page` primitive wrapping content | +| `dataPage(id, query, options, columns)` | The whole list-screen composite | +| `dataTable(id, query, options, columns)` | Server-side querying without page chrome | +| `observableDataTable(id, query, options, columns)` | The live variant | +| `columns(id, definitions)` | PrimeReact `column` children for a table | +| `commandForm(id, command, exclude?)` | A form generated from the command's own properties | +| `inputTextField` · `numberField` · `textAreaField` · `calendarField` · `dropdownField` | One field bound to one command property | +| `dialog(id, title, okLabel, cancelLabel, content)` | The Arc-aware dialog | +| `commandDialog(id, command, title, okLabel, fields)` | A dialog that submits a command | +| `busyIndicatorDialog(id, title, message)` | The blocking spinner | +| `schemaEditor` · `objectContentEditor` · `objectNavigationalBar` · `timeMachine` · `filterPanel` | The Arc-free editors | +| `toolbar` · `toolbarButton` · `toolbarGroup` · `toolbarSeparator` | The tool palette family | +| `errorBoundary(id, content)` · `icon(id, iconName)` | Region isolation, and an icon | + +Bindings are passed as plain strings, so the same builders work with your own proxy names. +`SampleBindingName` is an enum of the names the shipped templates use, not a closed set of everything a +binding may be. + +## Components + +One, registered under `componentRegistryKey('Cratis.Blueprint.Components', 'arcPageHeader')`. + +| Name | Properties | Slots | +|---|---|---| +| `arcPageHeader` | `title`, `subtitle`, `section`, `query`, `command` | `actions` | + +It derives its heading from `title`, falling back to the binding name read as a sentence; its trail from +`section` and the heading; and its design-time state from a binding-registry lookup: + +| Rendered | Meaning | +|---|---| +| `Bound to query AllInvoices` | A host registered a class under that name | +| `No query registered as AllInvoices` | Named, but nothing registered | +| `No binding` | The template names none, deliberately | + +It carries `data-scene-binding` and `data-scene-binding-state` attributes, so a preview surface can be +scanned for what is still unwired. + +## The names these templates write + +Every bare name a template references resolves against a catalog built from the real manifests of `core`, +`PrimeReact`, `Cratis.Components`, `Cratis.Blueprint.Default` and this package — and a spec proves it, so a +name renamed upstream fails here rather than rendering as a dashed box in your application. + +- **From `Cratis.Components`:** `page`, `dataPage`, `dataTable`, `observableDataTable`, `commandForm`, + `inputTextField`, `numberField`, `textAreaField`, `dropdownField`, `calendarField`, `dialog`, + `commandDialog`, `busyIndicatorDialog`, `icon`, `dropdown`, `errorBoundary`, `objectContentEditor`, + `objectNavigationalBar`, `schemaEditor`, `timeMachine`, `filterPanel`, `toolbar`, `toolbarButton`, + `toolbarGroup`, `toolbarSeparator`. +- **From `Cratis.Blueprint.Default`:** `appShell`, `topbar`, `sidebar`, `menu`, `menuItem`, `breadcrumb`, + `footer`, `configPanel`, `logo`, `userMenu`. +- **From `PrimeReact`:** `column`, and only `column` — the library's tables take PrimeReact `Column` + children and declare no `column` name of their own. +- **From this package:** `arcPageHeader`. + +## Where to go next + +- [Wire the binding registry](wiring-the-binding-registry.md) — supplying the classes these names want. +- [Add a template of your own](add-a-template.md) — when none of the above is the shape you need. +- [Layering on another blueprint](layering-on-a-blueprint.md) — why the component list is one entry long. diff --git a/Documentation/blueprint-components/toc.yml b/Documentation/blueprint-components/toc.yml new file mode 100644 index 0000000..dacad8e --- /dev/null +++ b/Documentation/blueprint-components/toc.yml @@ -0,0 +1,24 @@ +- name: Overview + href: index.md + +- name: Getting started + items: + - name: Use the Components blueprint + href: getting-started.md + +- name: Guides + items: + - name: Wire the binding registry + href: wiring-the-binding-registry.md + - name: Add a template of your own + href: add-a-template.md + +- name: Understand + items: + - name: Layering on another blueprint + href: layering-on-a-blueprint.md + +- name: Reference + items: + - name: The template catalogue + href: template-catalogue.md diff --git a/Documentation/blueprint-components/wiring-the-binding-registry.md b/Documentation/blueprint-components/wiring-the-binding-registry.md new file mode 100644 index 0000000..f908101 --- /dev/null +++ b/Documentation/blueprint-components/wiring-the-binding-registry.md @@ -0,0 +1,118 @@ +--- +title: Wire the binding registry +description: Which query and command names this blueprint's templates ask for, how a host supplies them, and what a page looks like before it does. +--- + +Every Arc-bound region on these pages is waiting on one thing: a class. The template carries a *name*, and +a host registers the generated Arc proxy under it. This page is the recipe for the host's side. + +The mechanism itself belongs to `Cratis.Components` and is explained once, in +[the binding registry](../components-package/binding-registry.md). Read that first if you have not — it +covers why a name is the only thing that survives the trip from a `.play` document to a renderer. What +follows assumes it. + +## Register at startup + +```typescript +import { registerQueries, registerCommands } from '@cratis/scene.components'; +import * as queries from './Billing/queries'; +import * as commands from './Billing/commands'; + +registerQueries(queries); +registerCommands(commands); +``` + +Do it once, at your entry point, before the first screen renders. Registering the same name twice replaces +the earlier registration, so a host can re-register on hot reload without unwinding the previous run. + +## The names these templates ask for + +The shipped templates are bound to an invoice model, so that the whole package reads as one small +application rather than eleven unrelated demonstrations. These are the names they write: + +| Name | Kind | Which templates want it | +|---|---|---| +| `AllInvoices` | Query | `DataListPage`, `DataListWithDetailPage`, `MasterDetailPage`, `DashboardPage` | +| `InvoicesInFlight` | Observable query | `ObservableDataListPage`, `DashboardPage`, `DataFeatureSection` | +| `InvoiceById` | Query | `ObjectEditorPage` | +| `RevenueByMonth` | Query | `DashboardPage` | +| `OpenTickets` | Query | `DashboardPage` | +| `AllAdjustments` | Query | `DashboardPage` | +| `RegisterInvoice` | Command | `CommandFormPage` | +| `RecordAdjustment` | Command | `CommandSliceSection`, `CommandDialog` | + +They are exported as an enum, so a host can register against the symbols rather than retyping strings: + +```typescript +import { SampleBindingName } from '@cratis/scene.blueprint.components'; + +registerQueries({ [SampleBindingName.AllInvoices]: AllInvoices }); +``` + +`InvoicesInFlight` is the one to look at twice. It is named by `observableDataTable` rather than +`dataTable`, and the difference lives in the proxy you register, not in how the element is configured: an +observable query opens a subscription and the page re-renders when the read model changes on the server. +Registering a plain query under that name will render, and will not be live. + +## What a page looks like before you register + +Three states, three different problems, three different fixes. Every page in this blueprint opens with an +`arcPageHeader`, and it tells you which one you are in: + +| The header says | What it means | What to do | +|---|---|---| +| `Bound to query AllInvoices` | A host registered a class under that name | Nothing | +| `No query registered as AllInvoices` | The template named it; nothing is registered | Register it, or fix the name | +| `No binding` | The template named nothing, deliberately | Nothing — see below | + +The third is not a defect. `SchemaEditorPage` genuinely has no query behind it: a schema is design-time +metadata about an event type, not a read model anyone queries, so its header says so rather than implying +a wiring step that does not exist. + +Below the header, an unbound region names what it wanted: + +```text +Unresolved query binding 'AllInvoices' on Cratis.Components:dataPage +``` + +A template that named nothing at all reads differently, because it is a different mistake: + +```text +Missing query binding on Cratis.Components:dataPage +``` + +The first needs the host to register that name; the second needs the template edited. + +## Half of a page renders anyway + +Not everything on these pages is Arc-bound, and the split is worth knowing because it decides what design +work you can do before a backend exists. + +**Waiting on a binding:** `dataPage`, `dataTable`, `observableDataTable`, `commandForm`, `commandDialog`. + +**Not waiting on anything:** `schemaEditor`, `objectContentEditor`, `objectNavigationalBar`, `timeMachine`, +`filterPanel`, `toolbar` and the page chrome. These read their content out of the property bag, so they +render fully with no host at all. + +That is why `SchemaEditorPage` and `ObjectEditorPage` are the two pages to open first when looking at this +blueprint — they are complete without a backend. It is also why one unbound table costs one dashed box +rather than the page: a design surface stays usable while the wiring is still missing, which is the whole +reason a missing binding is a placeholder and never a throw. + +## The Arc runtime is the host's, too + +`@cratis/arc` and `@cratis/arc.react` are peer dependencies of `@cratis/components` that the host supplies, +and a design surface is not a host. Every Arc-bound adapter therefore reaches them through a dynamic +`import()`. A page built only from the Arc-free composites never pulls the Arc client in at all, and an +unbound table never even reaches the import. + +When a binding *is* registered and the client is not installed, the chunk fails to load and the +`ArcRuntimeBoundary` around it shows the library's own error boundary — one dashed-out region, not a blank +screen. That is exactly what this package's Storybook shows in its `Bound` story, and it is honest rather +than hidden: it is what a real host without Arc installed sees. + +## Where to go next + +- [The binding registry](../components-package/binding-registry.md) — the mechanism, in full. +- [The template catalogue](template-catalogue.md) — which composite sits in which slot of which template. +- [Add a template of your own](add-a-template.md) — how to carry a binding name in a template you write. diff --git a/Documentation/blueprints/composing-screens.md b/Documentation/blueprints/composing-screens.md new file mode 100644 index 0000000..42d1a32 --- /dev/null +++ b/Documentation/blueprints/composing-screens.md @@ -0,0 +1,107 @@ +--- +title: Composing screens from templates +description: How a nested chain of screen templates is folded and placed into a layout's slots, and how an arrangement changes with the size class. +--- + +[Layouts](./layouts.md) and [screen templates](./screen-templates.md) explain what the two structures are +and how `FitsSlot` makes them nest. This page is about what happens when something actually places one - +the step between "this template fits the content slot" and "here is a screen on the screen". + +It matters because the obvious implementation is wrong in a way that produces no error at all. + +## Placing a single template + +A template that fits the layout's own `content` slot is the easy case. Its content is filed under its own +slot names, and all of it flows into `content`. + +One nuance decides where each piece lands: + +```ts +const target = layoutSlots.has(slotName) ? slotName : template.fitsSlot; +``` + +Content filed under a name the **layout** also declares stays under that name; everything else flows into +the slot `fitsSlot` names. + +That first branch is not a special case, it is a feature. The `Login` template declares an `aside` slot, and +so does the `FullPage` layout - so the branding half of a sign-in screen reaches the layout's `aside` region +rather than being buried inside the form column. A template can reach a layout region directly by naming it. + +## Placing a chain + +Now the case that is easy to get wrong. `SliceSection` fits `FeatureSection`'s `body`, which fits +`ModuleWorkspace`'s `body`, which fits the layout's `content`. + +The tempting implementation is to file the slice template's content under its own `fitsSlot` - `body` - at +the top level. It reads correctly and it is wrong: the `AppShell` layout has no `body` slot, so the content +lands in a region nothing renders. The screen comes up empty, no exception is thrown, and nothing points +back at the cause. + +:::caution +This is not hypothetical. It is the bug a spec over the gallery caught while the default blueprint was +being written, and it is exactly why "every screen fills only slots its layout declares" is worth a spec. +::: + +The correct move is to fold the chain from the inside out: place the slice in the feature, place that +result in the module, and hand the module to the layout. + +```ts +export function nestScreenTemplates(chain: ScreenTemplate[]): ScreenTemplate { + return chain.reduceRight((inner, outer) => (inner === outer ? outer : placeInside(outer, inner))); +} +``` + +`placeInside` appends the inner template's content - already folded, so it carries everything below it - +to the outer template's content under the inner one's `fitsSlot`. The result has the outermost template's +slots and `fitsSlot`, so it is placeable in the layout by the ordinary single-template rule. + +Because the fold is one operation applied repeatedly, the chain can be any depth. A chain of one is +returned unchanged, which is why almost every template pays nothing for a mechanism it does not use. + +## From a screen to an element tree + +A `Screen`'s `slotContent` is keyed by the layout's slot names, and a shell component's slots use the same +vocabulary. So rendering is a one-for-one handover rather than a translation: + +```ts +export function composeScreenElement(screen: Screen): ExternalComponent { + const shell = shellComponentForLayout(screen.layout); + if (!shell) { + throw new Error(`Screen '${screen.name}' names the layout '${screen.layout}', which this blueprint does not provide.`); + } + + return externalComponent(`screen-${screen.name}`, shell, { screenName: screen.name }, screen.slotContent); +} +``` + +Sharing one vocabulary across the layout definition, the templates and the shell components is the point. +A slot filled under a name the shell never reads renders nothing *and reports nothing*, so the names are +one enum and the rest is covered by specs. + +## What the size class does + +A layout's arrangement is a `FlowArrangement`: a tree whose leaves reference slots by name, plus overrides +targeting a width size class, a height size class, or both. `evaluateFlowArrangement` picks the most +specific match — an override naming both axes beats one naming a single axis. + +The `AppShell` layout declares three: + +| Override | What leaves the flow | Why | +| --- | --- | --- | +| Compact width | sidebar, right panel | There is no width at which an 18rem panel and a 20rem panel both fit beside content on a phone | +| Compact height | breadcrumb, footer | A landscape phone has almost no vertical room, and two horizontal strips of chrome eat most of it | +| Compact width **and** height | all four | The most specific match, so a phone in landscape gets its own answer | + +That third one is not redundant. Without it, a landscape phone would match both single-axis overrides, +`evaluateFlowArrangement` would pick whichever was declared last, and the screen would keep a footer it has +no room for — or a sidebar it has no width for, depending on declaration order. Targeting both axes is how +you stop the answer depending on the order you happened to write them in. + +Leaving the flow is not the same as not being rendered. At a compact width the sidebar is still there — +off-canvas, over the content, behind the mask — it simply no longer *occupies* anything. That is a fact +about the arrangement, and the shell's [layout modes](./layout-modes.md) are what act on it. + +## Next + +- [Regions and slots](./regions-and-slots.md) — the region vocabulary this is arranging. +- [The template set](./template-set.md) — the twenty-three templates, including the three-level chain. diff --git a/Documentation/blueprints/getting-started.md b/Documentation/blueprints/getting-started.md new file mode 100644 index 0000000..be5d708 --- /dev/null +++ b/Documentation/blueprints/getting-started.md @@ -0,0 +1,115 @@ +--- +title: Use the default blueprint +description: Render a screen inside the application shell, switch its layout mode, and switch its theme - in about ten minutes. +--- + +By the end of this you will have the Cratis default blueprint rendering a real screen in a real +application shell, with a working menu, a configurator that switches between eight layout modes, and a +theme that swaps live without a reload. + +You need a React application, `@cratis/scene.react`, and the blueprint itself. + +## Install and load the stylesheet + +The blueprint's shell is hand-written CSS - PrimeReact 10 has no application-shell primitive to lean on - +so the stylesheet has to be loaded once, at your entry point, along with PrimeReact's own. + +```ts +import 'primereact/resources/primereact.min.css'; +import 'primereact/resources/themes/lara-light-indigo/theme.css'; +import 'primeicons/primeicons.css'; +import '@cratis/scene.blueprint.default/styles'; +``` + +Load it once and never again. Every mode, every rail width and every transition in the shell comes from +this file; without it you get an unstyled column of regions and no clue why. + +## Boot a gallery screen + +The fastest way to see the whole thing working is to render one of the screens the blueprint already +ships. They are real `Screen` instances - not pictures - so this is the actual rendering path, not a demo +one. + +```tsx +import { GalleryScreenPreview } from '@cratis/scene.blueprint.default'; + +export function App() { + return ; +} +``` + +Open it. You have a fixed topbar, a docked sidebar with three menu sections, a breadcrumb, a dashboard of +four stat cards over two columns of widgets, a footer, and a cog button floating against the right edge. + +What happened under the hood: `composeScreenElement` turned the screen into an `ExternalComponent` tree +whose slots are the layout's slots, `resolveComponentName` decided which package owns each bare component +name, and the real `SceneElementView` rendered the result against the real registry. Nothing on that path +exists only for previews. + +:::note +Widgets whose names belong to packages you have not loaded render as a dashed red box naming what is +missing. That is deliberate - a blueprint previewed against half a profile should look visibly incomplete +rather than quietly wrong. +::: + +## Switch the layout mode + +Click the cog. The configurator has four axes; the second is **Menu mode**. Click **Slim** and the sidebar +becomes a 5rem rail of circular icons with the content sliding in behind it. Click **Horizontal** and the +sidebar stops being a sidebar - it flows into the topbar as a row, and submenus drop down instead of +popping out sideways. + +Nothing about the screen changed. The only thing that changed is one class on the wrapper element, which +is the whole point of the mode vocabulary: a screen cannot be written for one mode and broken in another. + +To start in a particular mode rather than clicking to it: + +```tsx +import { GalleryScreenPreview, LayoutMode } from '@cratis/scene.blueprint.default'; + +; +``` + +Now narrow the browser window past 991px. Every mode collapses to off-canvas, the mask appears behind the +sidebar, and the mode buttons grey out with a sentence saying why. Widen it again and your chosen mode is +back - it was kept, not overwritten. + +## Switch the theme + +Still in the configurator, the **Theme** section offers the two themes the blueprint ships. Click +**Scene Default Dark**. + +The page changes color and nothing remounts. `SceneThemeProvider` writes the new token values onto the +element that is already there, so scroll position, an open menu, and anything typed into a form all +survive. That is what "live re-resolution" means, and it is why theme switching is fast enough to be a +thing people actually do rather than a thing they do once. + +## Render your own screen instead + +The gallery is a starting point, not the destination. A screen of your own names the layout, names the +template whose shape it fills, and provides the content: + +```ts +import { Screen } from '@cratis/scene.model'; +import { LayoutName } from '@cratis/scene.blueprint.default'; + +const invoices: Screen = { + name: 'Invoices', + layout: LayoutName.AppShell, + screenTemplate: 'CrudList', + slotContent: {}, + forms: [], + contributions: [], +}; +``` + +Fill `slotContent` with the shell chrome and your content, keyed by the layout's slot names - `topbar`, +`sidebar`, `menu`, `breadcrumb`, `content`, `footer`, `configPanel`. The +[regions and slots reference](./regions-and-slots.md) lists all of them. + +## Where to go next + +- [Screen and dialog templates](./screen-templates.md) - the model behind what you just rendered, and how + templates nest. +- [The template set](./template-set.md) - the other twenty-two shapes you did not have to build. +- [Ship your own blueprint](./ship-your-own-blueprint.md) - when the default one is not the look you want. diff --git a/Documentation/blueprints/index.md b/Documentation/blueprints/index.md new file mode 100644 index 0000000..bf76b32 --- /dev/null +++ b/Documentation/blueprints/index.md @@ -0,0 +1,68 @@ +# Blueprints + +A blueprint is a package that ships the shape of an application: its layouts, the screen and dialog templates +built on them, and the components that fill their slots. + +An application selects **one** blueprint. That is the point — a blueprint is a coherent set, designed +together, rather than a layout from one place and templates from another that happen not to clash. In the +application's settings you pick a blueprint, and everything it holds becomes available. + +A blueprint declares the component libraries it is built from, like any other package: + +```csharp +new ScenePackage( + Name: "Cratis.Blueprint.Default", + Version: "1.0.0", + Kind: PackageKind.Blueprint, + Dependencies: [new PackageDependency("PrimeReact"), new PackageDependency("Cratis.Components")], + Components: ["appShell", "topbar", "sidebar", /* ... */], + Layouts: ["AppShell", "FullPage"], + ScreenTemplates: ["ModuleWorkspace", "FeatureList", /* ... */], + DialogTemplates: ["Confirm", /* ... */], + Themes: ["Daylight", "Midnight"]); +``` + +Because those dependencies are declared, "which blueprints can I use" is answerable from the packages a +profile already has, rather than being something you find out by trying one. + +## Layout, template, screen + +These three are easy to run together and mean different things. + +A **[layout](layouts.md)** is the application's base navigational look — the shell, with its top bar, +navigation and content region. An application has one. + +A **[screen template](screen-templates.md)** is a reusable shape that goes *inside* that shell, at module, +feature or slice level. An application has many. Each declares which of its parent's slots it fills, so a +module's template fits the layout, a feature's template fits the module's, and a slice's fits the feature's — +the same rule at every level, nesting arbitrarily deep without a second mechanism. + +A **dialog template** is the same idea for content that opens *over* the application. It declares no parent +slot, because it occupies none: a dialog is summoned, not placed. + +A **screen** is an instance. It names the structure it fills and supplies the content. + +Layouts and screen templates are structurally alike on purpose — both are slots plus an arrangement, +evaluated by the same engine. They differ in role, and a screen template additionally says where it belongs. + +## What a blueprint is not + +A blueprint is a **packaged artifact, not a language construct**. It never appears in a `.play` file. A +`ui profile` lists it by name in `packages` like anything else; the layouts and templates it provides are +then resolvable by name, exactly as its components are. + +## The default blueprint + +`@cratis/scene.blueprint.default` is the one you get for free: two layouts, eight menu modes, twenty-three +screen templates, three dialog templates and two themes. + +- **[Use the default blueprint](getting-started.md)** — render a screen, switch its mode, switch its theme. +- **[Understanding blueprints](understanding-blueprints.md)** — how a blueprint differs from a component + library, and when it is the wrong fit. +- **[Composing screens from templates](composing-screens.md)** — how a nested template chain is placed, and + what the size class does to an arrangement. +- **[Regions and slots](regions-and-slots.md)** — every region the two layouts expose. +- **[Layout modes](layout-modes.md)** — all eight modes, and the mobile regime nobody chooses. +- **[The template set](template-set.md)** — every screen and dialog template it ships. +- **[Theme tokens](theme-tokens.md)** — the thirteen shared tokens, and every theme's attribution. +- **[Ship your own blueprint](ship-your-own-blueprint.md)** — when the default is not your look. diff --git a/Documentation/blueprints/layout-modes.md b/Documentation/blueprints/layout-modes.md new file mode 100644 index 0000000..8522198 --- /dev/null +++ b/Documentation/blueprints/layout-modes.md @@ -0,0 +1,127 @@ +--- +title: Layout modes +description: All eight menu modes the default blueprint implements, the classes they map to, when to choose each, and the mobile regime nobody chooses. +--- + +The application shell has eight menu modes. Each is a `LayoutMode` value, each maps to one wrapper class, +and the class vocabulary is PrimeTek's - unchanged, so anyone who has themed a PrimeReact application +recognizes it. + +## The modes + +| Mode | `LayoutMode` | Wrapper class | Sidebar width | Choose it when | +| --- | --- | --- | --- | --- | +| Static | `Static` | `layout-static` | 18rem | Navigation is used constantly and screens are wide. The default. | +| Overlay | `Overlay` | `layout-overlay` | 18rem, off-canvas | Content needs the full width and navigation is occasional. | +| Slim | `Slim` | `layout-slim` | 5rem | The icons are learnable and every pixel of width matters. | +| Slim+ | `SlimPlus` | `layout-slim-plus` | 7rem | Same, but the icons are not self-explanatory enough to go unlabeled. | +| Compact | `Compact` | `layout-compact` | 5rem | The slim rail, with square buttons and a topbar shifted by the rail. | +| Horizontal | `Horizontal` | `layout-horizontal` | none | Few top-level areas, and vertical space is the scarce one. | +| Reveal | `Reveal` | `layout-reveal` | 4.25rem strip → 18rem | Full labels on demand without permanently paying for them. | +| Drawer | `Drawer` | `layout-drawer` | 5.25rem → 18rem | Same as reveal, but the content should never be covered mid-hover. | + +## What each one actually does + +**Static** docks the sidebar and gives the content a matching `margin-left`. The topbar toggle slides the +sidebar out and drops the margin. Content is pushed, never covered. + +**Overlay** parks the sidebar off-canvas. Opening it floats it over the content at a high z-index with a +`.layout-mask` scrim behind it; clicking the scrim closes it. + +**Slim** collapses the sidebar to an icon-only rail with root items as circular buttons. Submenus pop out +as a floating panel anchored at the rail's width. + +**Slim+** is the same idea at 7rem, with each icon's label stacked directly beneath it, and the submenu +popup anchored at 7rem. + +**Compact** is the 5rem icon rail again with square rather than circular buttons, and it also shifts the +topbar by the rail width - so the brand sits beside the rail rather than over it. + +**Horizontal** makes the sidebar `position: static` and flows it into the topbar strip as a nowrap row. +Submenus become absolute drop-downs, and the content margin goes to zero. + +**Reveal** translates the full panel off-left, leaving a 4.25rem strip of icons. Hovering slides the whole +panel in *over* the content. The pin button anchors it open, which switches it from covering the content to +pushing it out to the full 18rem. + +**Drawer** is the same interaction with a different mechanic: a collapsed 5.25rem rail that *animates its +width* to full on hover. Reveal slides; drawer grows. It pins the same way. + +## State classes + +Modes are not the only thing on the wrapper. `layoutWrapperClasses` also emits: + +| Class | Present when | +| --- | --- | +| `layout-static-inactive` | static, and the sidebar has been toggled closed | +| `layout-overlay-active` | overlay, the sidebar is open, and the viewport is not mobile | +| `layout-sidebar-active` | reveal or drawer, and the sidebar is currently out | +| `layout-sidebar-anchored` | reveal or drawer, and the sidebar is pinned | +| `layout-mobile` | the viewport is at or below the breakpoint | +| `layout-mobile-active` | mobile, and the sidebar has been opened | +| `layout-menu-light` / `-dark` / `-primary` | the chosen menu tint | +| `layout-color-scheme-light` / `-dark` | the chosen color scheme | + +The mode class always reflects the **effective** mode, so the off-canvas rules a phone needs come from the +same `layout-overlay` block a desktop overlay uses rather than a parallel mobile-only ruleset. The mode the +user actually chose is emitted separately as `data-layout-mode`, so a configurator can show it even while +the viewport overrides it. + +## Mobile is not a mode + +At or below **991px** every mode renders off-canvas. This is not user-selectable and never appears in the +picker as a ninth option. + +```ts +export function effectiveLayoutMode(state: LayoutConfigState): LayoutMode { + return state.isMobile ? LayoutMode.Overlay : state.mode; +} +``` + +Deriving it rather than overwriting `mode` is what lets the chosen mode come back untouched when the +viewport grows again. The mode picker disables its buttons and says so in a sentence rather than +disappearing - a control that vanishes reads as a bug; a disabled control with an explanation reads as a +decision. + +The breakpoint is exported as `mobileBreakpoint`, and the media query as `mobileMediaQuery`, so the resize +listener and the stylesheet cannot disagree about where the boundary is. + +## Choosing the mode at runtime + +The mode lives in one place - `LayoutConfigState` - behind one provider and one hook. + +```tsx +import { LayoutConfigProvider, LayoutMode, useLayoutConfig } from '@cratis/scene.blueprint.default'; + +function ModeButton() { + const { config, setMode } = useLayoutConfig(); + return ( + + ); +} +``` + +Wrapping the shell in `` is optional - the shell puts one around itself when a host +has not, because a gallery preview hands the renderer one `appShell` element and nothing else. Provide one +yourself when a control outside the shell needs to drive it. + +## What is remembered + +Mode, menu theme, color scheme, theme name and the pin persist to `localStorage` under +`cratis.scene.blueprint.default`. + +Whether the sidebar happened to be open, whether the pointer was over it, and whether the viewport was +narrow are deliberately **not** persisted. Those are facts about a moment rather than preferences, and +restoring them produces a shell that opens in a state nobody chose. + +Everything read back is validated against the enums rather than trusted. Storage is shared with every other +script on the origin and outlives the version of the package that wrote it, and an unrecognized mode left +in place would put a class on the wrapper that no rule matches - a shell with no sidebar at all, and no +error to explain it. + +## Next + +- [Regions and slots](./regions-and-slots.md) - what the modes are moving around. +- [Theme tokens](./theme-tokens.md) - how the modes get their colors. diff --git a/Documentation/blueprints/layouts.md b/Documentation/blueprints/layouts.md new file mode 100644 index 0000000..2963df6 --- /dev/null +++ b/Documentation/blueprints/layouts.md @@ -0,0 +1,82 @@ +--- +title: Layouts +description: An application's base navigational shell - its slots, how they are arranged, and how a layout differs from a screen template. +--- + +A layout is an application's base navigational look: the shell everything else renders inside. + +```csharp +public record Layout(string Name, IReadOnlyList Slots, Arrangement? Arrangement = null); +``` + +Three things, and the third is optional. `Slots` are the named regions the shell offers — a top bar, a +navigation area, a content region, a footer. `Arrangement` says how those slots sit relative to each other. +With no arrangement, they are simply in declaration order. + +An application has **one** layout in force, and selects it — usually from a [blueprint](index.md). + +## Slots + +```csharp +public record Slot(string Name, Arrangement? Arrangement = null); +``` + +A slot's own `Arrangement` is a different thing from the layout's, and the distinction matters: + +- The **layout's** arrangement positions the slots relative to each other. Its flow leaves are + `FlowSlotLeaf`, which reference a slot by name. +- A **slot's** arrangement positions the content filling that one slot. Its flow leaves are `FlowLeaf`, which + carry a real element. + +A layout is not uniformly one arrangement mode. Flow for most slots and freeform for one is a valid +combination, and the engine evaluates each independently. + +## Arrangement + +Two modes, both evaluated by `Cratis.Scene.Engine.Layouts` and its TypeScript twin. + +**Flow** is a tree of rows, columns and grids, with size-class overrides: + +```csharp +new FlowArrangement( + Root: new FlowColumn + { + Children = + [ + new FlowSlotLeaf("topbar"), + new FlowRow { Children = [new FlowSlotLeaf("sidebar"), new FlowSlotLeaf("content") { Grow = 1 }] }, + new FlowSlotLeaf("footer") + ] + }, + Overrides: + [ + new FlowOverride( + Width: WidthSizeClass.Compact, + Height: null, + Root: new FlowColumn { Children = [new FlowSlotLeaf("topbar"), new FlowSlotLeaf("content"), new FlowSlotLeaf("footer")] }) + ]); +``` + +The override drops the sidebar out of the flow on a compact width. `Width` and `Height` are independently +nullable, so an override can key on either axis or both. When more than one override matches a concrete size +class, the most specific wins — both dimensions beats one — and among equally specific matches, the last +declared wins. + +**Freeform** is one variant per size-class combination, each placing slots at explicit coordinates. Selection +is exact-match only: a size class with no matching variant returns nothing rather than falling back to a +variant that was never designed for it. That is deliberate — "warn, don't silently pick". + +## Size classes + +Size classes are named, not pixel breakpoints: `Compact` or `Regular` on each of width and height. A narrow +browser window and a phone in portrait are the *same* class, which is what lets one layout describe both. + +`SizeClassCalculator.Compute` (C#) and `computeSizeClass` (TypeScript) own the conversion from a real size, +with a 600dip default breakpoint per axis. It lives in the engine, shared by every renderer, so a React +renderer and a future native one agree exactly on when a boundary is crossed. + +## Layouts and screen templates + +A [screen template](screen-templates.md) has the same shape — slots plus an arrangement — and is evaluated by +the same engine. The difference is role and one field: a screen template also declares which of its parent's +slots it fills, and an application has many of them, where it has one layout. diff --git a/Documentation/blueprints/regions-and-slots.md b/Documentation/blueprints/regions-and-slots.md new file mode 100644 index 0000000..d6c6e19 --- /dev/null +++ b/Documentation/blueprints/regions-and-slots.md @@ -0,0 +1,83 @@ +--- +title: Regions and slots +description: Every region the default blueprint's two layouts expose, what fills it, and what happens to it at each size class. +--- + +The default blueprint exposes nine regions across two layouts. A screen fills them by slot name; the shell +components read the same names. + +## The AppShell layout + +| Slot | Region | Filled with | Rendered when | +| --- | --- | --- | --- | +| `topbar` | the fixed strip across the top | `topbar` | it has content | +| `sidebar` | the sidebar's own chrome, inside the panel the shell positions | `sidebar` | it or `menu` has content | +| `menu` | the navigation itself | one or more `menu` | it or `sidebar` has content | +| `breadcrumb` | the trail above the content | `breadcrumb` | it has content | +| `content` | the screen | a screen template's content | always | +| `footer` | the strip below the content | `footer` | it has content | +| `rightPanel` | the inspector column down the right edge | `rightPanel` | it has content | +| `configPanel` | the floating configurator | `configPanel` | it has content | + +## The FullPage layout + +| Slot | Region | Filled with | +| --- | --- | --- | +| `aside` | the branding half of the split | any content | +| `content` | the form, message or hero | any content | +| `configPanel` | the floating configurator | `configPanel` | + +## Why two layouts and not one + +Sign-in, register, forgotten password, new password, verification, lock, error, access-denied, not-found +and landing screens have no navigation state, no sidebar to remember and no breadcrumb to place. Hanging +them off the application shell would mean every one of the [eight layout modes](./layout-modes.md) needs an +answer for a page that has no menu. + +The split is structural in every PrimeTek template for the same reason. What survives into the full-page +layout is the configurator, because a sign-in page still has to honor the chosen theme - it is very often +the first page anyone sees. + +## Where the regions come from + +Sakai, PrimeTek's free template, establishes topbar, sidebar, menu, content and footer. The premium line - +Diamond, Atlantis, Freya, Apollo, Ultima, Avalon, Verona - adds the breadcrumb and a right panel. Both sets +are exposed here, because a blueprint covering only the free template's regions forces a fork on anyone who +wants the others. + +`aside` is not from that line; it is the branding half of the split every premium sign-in page uses, made a +slot rather than something a screen paints inside `content`. + +## What the size class does to them + +The `AppShell` arrangement declares three overrides, and `evaluateFlowArrangement` picks the most specific +match. + +| Size class | Regions in the flow | +| --- | --- | +| Regular width, regular height | all eight | +| Compact width | topbar, breadcrumb, content, footer, configPanel | +| Compact height | topbar, sidebar, menu, content, rightPanel, configPanel | +| Compact width **and** height | topbar, content, configPanel | + +At a compact width the sidebar leaves the *flow* - it is still rendered, off-canvas, over the content, +behind the mask, but it no longer occupies anything. That is a fact about the arrangement, not about CSS. + +The `FullPage` arrangement has one override: at a compact width the branding aside drops out entirely +rather than stacking above the form. A sign-in form pushed below the fold by decoration is the worst +possible first screen. + +## One vocabulary, three consumers + +`SlotName` is a single enum used by the layout definitions, the screen templates and the shell components. +That is deliberate: a slot filled under a name the shell never reads renders nothing *and reports nothing*. +Sharing the enum makes that a compile error where it can be, and the blueprint's specs cover the rest - +every gallery screen is checked against the slots its layout actually declares. + +## Nested slot names + +A screen template declares slots of its own, from a separate vocabulary (`TemplateSlotName`): `header`, +`body`, `sidePanel`, `toolbar`, `actions`, `stats`, `primary`, `secondary`. + +They never collide with the layout's names, because a template's `fitsSlot` is resolved against its direct +parent rather than globally - see [composing screens from templates](./composing-screens.md). diff --git a/Documentation/blueprints/screen-templates.md b/Documentation/blueprints/screen-templates.md new file mode 100644 index 0000000..64d7a80 --- /dev/null +++ b/Documentation/blueprints/screen-templates.md @@ -0,0 +1,114 @@ +--- +title: Screen and dialog templates +description: The reusable shapes that go inside a layout - how fitsSlot composes them into a tree, and how a screen instantiates one. +--- + +A [layout](layouts.md) is the shell. A screen template is a reusable shape that goes *inside* it. + +```csharp +public record ScreenTemplate( + string Name, + string? FitsSlot, + IReadOnlyList Slots, + Arrangement? Arrangement = null, + IReadOnlyDictionary>? Content = null, + string? DisplayName = null, + string? Description = null); +``` + +An application has one layout and many screen templates — typically one per module, feature or slice that +needs a shape of its own. + +## `FitsSlot` is what makes them nest + +A template states where it belongs. It is not told by whatever happens to host it: + +```text +AppShell (layout) + slots: topbar, sidebar, content, footer + │ + └── ModuleWorkspace (screen template) + fitsSlot: "content" + slots: moduleNav, moduleContent + │ + └── FeatureList (screen template) + fitsSlot: "moduleContent" + slots: list, details + │ + └── SliceDetail (screen template) + fitsSlot: "details" +``` + +A module's template fits a slot on the application layout. A feature's template fits a slot the module's +template declares. A slice's fits one the feature's declares. **The same rule at every level** — there is no +separate mechanism for "module-level" versus "feature-level" nesting, and no depth limit falls out of the +design. + +`FitsSlot` is nullable for a template placed explicitly rather than by declaration. + +## Qualifying a slot when the name is not enough + +`body` is a good name for a slot at every level of a chain, so several templates legitimately declare one. +A bare `body` then has no single answer, and resolution reports it as unplaced with the candidates rather +than guessing — putting a template in the wrong parent renders content in the wrong region, which is far +harder to diagnose than being told the name is ambiguous. + +Qualify it with the container to settle it: + +```csharp +new ScreenTemplate("FeatureSection", "ModuleWorkspace.body", [new Slot("body")]); +new ScreenTemplate("SliceSection", "FeatureSection.body", []); +``` + +This is the same rule component names use: a bare name searches, a qualified one goes straight to what it +names. Everything before the last `.` is the container, everything after is the slot. A qualifier naming a +container that does not declare that slot is unplaced too — it is never quietly downgraded to a search. + +Note that a bare name only becomes ambiguous once *more than one other* container declares it. A template +never competes with itself, so a two-level chain where both levels declare `body` still resolves. + +## Slots and content + +`Slots` are what this template offers to whatever it contains — the next level down. `Content` is what the +template brings with it: the chrome that is part of the template rather than part of any screen based on it. +A template with an empty `Content` is purely structural. + +That split is what makes a template reusable. Two features can share `FeatureList` and get the same +structure, header and toolbar, while each supplies its own list and detail content. + +## Screens + +A screen is the instance: + +```csharp +public record Screen( + string Name, + string Layout, + IReadOnlyDictionary> SlotContent, + IReadOnlyList
Forms, + IReadOnlyList Contributions, + string? ScreenTemplate = null); +``` + +`Layout` names the application shell the screen ultimately renders inside. `ScreenTemplate` names the +template it fills, or is null when the screen fills the layout's slots directly. The template's `FitsSlot` is +what decides *where* it lands — so a screen never has to state its own position, and moving a template moves +every screen based on it. + +## Dialog templates + +```csharp +public record DialogTemplate( + string Name, + IReadOnlyList Slots, + Arrangement? Arrangement = null, + IReadOnlyDictionary>? Content = null, + string? DisplayName = null, + string? Description = null); +``` + +Identical, minus `FitsSlot`. A dialog occupies no slot: it opens over the application, summoned by something, +rather than being placed by a containing layout. + +Everything else is the same on purpose. A confirmation dialog and a detail screen are both "slots with an +arrangement, filled with content", and there is no reason for an author to learn that twice. diff --git a/Documentation/blueprints/ship-your-own-blueprint.md b/Documentation/blueprints/ship-your-own-blueprint.md new file mode 100644 index 0000000..6c5048a --- /dev/null +++ b/Documentation/blueprints/ship-your-own-blueprint.md @@ -0,0 +1,192 @@ +--- +title: Ship your own blueprint +description: Build and publish a blueprint package - the manifest, the bundle, and the specs that stop the two from drifting apart. +--- + +You want your own application shape: your navigation, your page shapes, your palette. That is a blueprint, +and it is a package like any other. + +This guide assumes you have built a React package before and know what a `ui profile` is. If you want to +understand *why* blueprints are a separate package kind first, read +[understanding blueprints](./understanding-blueprints.md). + +## What you are producing + +Two things, and the relationship between them is the whole discipline: + +- a **`ScenePackage`** manifest - the declaration: a name, a kind, dependencies, and the *names* of + everything you contribute; +- a **`ScenePackageBundle`** - the implementation: the real React components, layouts, templates, screens + and themes behind those names. + +The manifest is platform-agnostic and lives in the model, because design-time tooling reads it without ever +loading a component. The bundle is what a renderer needs. + +## 1. Declare the manifest + +```ts +import { PackageKind, ScenePackage } from '@cratis/scene.model'; + +export const myBlueprintManifest: ScenePackage = { + name: 'Acme.Blueprint', + version: '1.0.0', + kind: PackageKind.Blueprint, + dependencies: [{ name: 'PrimeReact' }, { name: 'Cratis.Components' }], + components: ['appShell', 'topbar', 'menu'], + layouts: ['AppShell'], + screenTemplates: ['Dashboard'], + dialogTemplates: ['ConfirmDialog'], + themes: ['Acme Light'], + displayName: 'Acme Blueprint', + description: 'Acme\'s application shape.', + module: '@acme/scene.blueprint', +}; +``` + +`dependencies` is the part worth getting right. Your shells are built out of somebody else's widgets - say +so. A profile that activates your blueprint without them renders a shell whose every control is a dashed +red placeholder, and declaring the dependency is what lets that be reported while the profile is being +configured rather than discovered when someone opens the page. + +`layouts` lists **only** application shells. A dashboard is a screen template, not a layout. + +## 2. Register the components + +Registry keys pair the package name with the bare name, and are always built rather than written: + +```ts +import { ComponentRegistry, componentRegistryKey } from '@cratis/scene.react'; + +export const myBlueprintComponents: ComponentRegistry = { + [componentRegistryKey('Acme.Blueprint', 'appShell')]: AppShell, + [componentRegistryKey('Acme.Blueprint', 'topbar')]: Topbar, + [componentRegistryKey('Acme.Blueprint', 'menu')]: Menu, +}; +``` + +The separator is the registry's own business - deliberately not the `.` a screen uses to qualify a name, so +a package name containing dots stays unambiguous. Building a key by hand is how a component ends up +registered under something no lookup will ever produce. + +Every component takes the same props: + +```tsx +import { RegisteredComponentProps } from '@cratis/scene.react'; + +export function Menu({ element, slots }: RegisteredComponentProps) { + return ( + + ); +} +``` + +Read `element.properties` through typed accessors that fall back rather than casting. The bag carries +whatever a template author wrote and Scene never re-validates it, so a component that trusts it renders +`[object Object]` the first time a template has a typo. + +## 3. Define the layouts + +A layout is named slots plus an optional arrangement: + +```ts +import { Layout } from '@cratis/scene.model'; + +export const appShellLayout: Layout = { + name: 'AppShell', + slots: [{ name: 'topbar' }, { name: 'menu' }, { name: 'content' }], + arrangement: appShellArrangement, +}; +``` + +Give the arrangement overrides for the size classes that change it, and let `evaluateFlowArrangement` pick. +An override targeting both axes beats one targeting a single axis, which is what makes a phone in landscape +get its own answer rather than whichever single-axis override happened to be declared last. + +## 4. Define the templates + +Every screen template names, in `fitsSlot`, the slot on its parent that it occupies: + +```ts +import { ScreenTemplate } from '@cratis/scene.model'; + +export const dashboardTemplate: ScreenTemplate = { + name: 'Dashboard', + fitsSlot: 'content', + slots: [{ name: 'stats' }, { name: 'primary' }, { name: 'secondary' }], + content: {}, + displayName: 'Dashboard', + description: 'Four stat cards over two columns of widgets.', +}; +``` + +Dialog templates are the same minus `fitsSlot`, because a dialog is summoned rather than placed. + +## 5. Assemble the bundle + +```ts +import { ScenePackageBundle } from '@cratis/scene.react'; + +export const myBlueprint: ScenePackageBundle = { + manifest: myBlueprintManifest, + components: myBlueprintComponents, + layouts: [appShellLayout], + screenTemplates: [dashboardTemplate], + dialogTemplates: [confirmDialogTemplate], + screens: myGalleryScreens, + themes: [acmeLight], +}; +``` + +## 6. Prove the two halves agree + +This is the step that is tempting to skip and expensive to skip. + +```ts +import { validatePackageBundle } from '@cratis/scene.react'; + +describe('when validating the bundle', () => { + const problems = validatePackageBundle(myBlueprint); + + it('should report no problems', () => { + problems.should.be.empty; + }); +}); +``` + +A manifest promising a component the bundle never registered renders as a dashed red box somewhere deep +inside a screen, a long way from the declaration that caused it. A component registered but not declared is +invisible to `resolveComponentName`, so no screen can ever name it. Both are silent, which is why every +blueprint runs this. + +Three more checks are worth having, and each one caught something real while the default blueprint was +being built: + +- **Every gallery screen fills only slots its layout declares.** Filling a slot the layout does not declare + is a screen that renders empty with nothing to explain it. +- **Every component name a template references resolves** against a profile listing your dependencies. A + name nothing declares is a placeholder in the middle of a page. +- **Every theme has `author`, `authorUrl` and `license` set**, and declares compatibility with every package + the profile activates - `core` included, since there is no implicit exemption for it. + +## 7. Ship a gallery + +Ship real `Screen` instances alongside your templates, and boot them through the real engine. + +This costs almost nothing and buys something specific: when a component name in a template is wrong, the +gallery is where it goes red - during *your* specs, rather than in somebody's application three weeks +later. + +## Style it against the tokens + +Reference the [thirteen shared tokens](./theme-tokens.md), aliased once into locals of your own, and never +a component library's variables directly. That indirection is what lets one blueprint work across +PrimeReact versions and lets a host override the look without editing your rules. + +## Next + +- [Composing screens from templates](./composing-screens.md) - the placement rules in full, including the + one that fails silently. +- [Layout modes](./layout-modes.md) - what the default blueprint implements, if you want the same + vocabulary. diff --git a/Documentation/blueprints/template-set.md b/Documentation/blueprints/template-set.md new file mode 100644 index 0000000..9aa9ee5 --- /dev/null +++ b/Documentation/blueprints/template-set.md @@ -0,0 +1,120 @@ +--- +title: The template set +description: Every screen template and dialog template the default blueprint ships, what each is for, and which layout it renders in. +--- + +The default blueprint ships twenty-three screen templates and three dialog templates. Each screen template +has a matching `Screen` in the gallery, so every row below is something you can boot and look at. + +## In the application shell + +Ten shapes, each fitting the `AppShell` layout's `content` slot. + +| Template | For | Its own slots | +| --- | --- | --- | +| `Dashboard` | Four stat cards over two columns of widgets | `stats`, `primary`, `secondary` | +| `CrudList` | A searchable table with a header and a primary action | `toolbar`, `body` | +| `DetailView` | One record: header with actions, its sections, a summary panel | `header`, `body`, `sidePanel` | +| `FormPage` | A grouped form with the field types applications actually use | `header`, `body`, `actions` | +| `Empty` | The designed empty state for a list with nothing in it | `body` | +| `Documentation` | Prose with a table of contents beside it | `sidePanel`, `body` | +| `ProfileSettings` | The signed-in user editing their own account | `header`, `body`, `actions` | +| `UserManagement` | The people table, with roles and an invitation action | `toolbar`, `body` | +| `Invoice` | A printable document: parties, line items, totals | `header`, `body`, `actions` | +| `Help` | Searchable answers with a route to a human | `header`, `body`, `sidePanel` | + +`Dashboard` follows Sakai's composition - a row of four figures, then two columns of larger widgets - and +carries its own arrangement that collapses the two columns into one at a compact width. + +The last five are in the set because leaving them out is what makes a template line feel thin. Every real +application grows documentation, settings, user administration, a printable document and a help page, and +the ones nobody designed are the ones that end up looking like a different product. + +## In the full-page shell + +Ten chrome-less shapes, each fitting the `FullPage` layout's `content` slot and filling its `aside`. + +| Template | For | +| --- | --- | +| `Login` | Email and password beside the branding panel | +| `Register` | Account creation, with a strength meter and the terms checkbox | +| `ForgotPassword` | One field and one button - the point is that it asks for nothing else | +| `NewPassword` | Where a reset link lands: choose it, confirm it, done | +| `Verification` | The code step, with the progress indicator | +| `LockScreen` | One person, one password, and no way to lose what was open | +| `Error` | A server-side failure, said plainly, with a way onward | +| `AccessDenied` | A refusal that distinguishes "not signed in" from "not allowed" | +| `NotFound` | A wrong address, with a search box rather than a dead end | +| `Landing` | The marketing front door, with navigation of its own | + +The four that are nobody's plan - `Error`, `AccessDenied`, `NotFound`, `Landing` - matter more than their +frequency suggests. An error page is the screen most likely to be someone's first impression of how +carefully an application was built. + +## The nesting chain + +Three templates that exist to demonstrate, and to be asserted, rather than to be used as-is. + +| Template | `fitsSlot` | Fits into | Offers | +| --- | --- | --- | --- | +| `ModuleWorkspace` | `content` | the `AppShell` layout | `header`, `body`, `sidePanel` | +| `FeatureSection` | `body` | `ModuleWorkspace` | `toolbar`, `body` | +| `SliceSection` | `body` | `FeatureSection` | `body`, `actions` | + +Module, feature, slice - the same hierarchy an application's source is organized by. Both `FeatureSection` +and `SliceSection` name `body`, and that is not ambiguous: `fitsSlot` resolves against the direct parent. +See [composing screens from templates](./composing-screens.md) for how the chain is folded when it is +placed. + +## Dialog templates + +Three, and none has a `fitsSlot` - a dialog is summoned rather than placed, so it occupies no parent slot. + +| Template | For | Its own slots | +| --- | --- | --- | +| `ConfirmDialog` | One question with its consequence spelled out, and a way back | `header`, `body`, `actions` | +| `FormDialog` | A handful of fields captured without leaving the page underneath | `header`, `body`, `actions` | +| `DetailDialog` | A record over the list it came from, with a route to the full page | `header`, `body`, `sidePanel`, `actions` | + +## What the content is made of + +Every template carries realistic seeded content - revenue figures, product rows, real column headers - not +placeholder text. A gallery whose dashboard shows four boxes labeled "Card" proves the renderer runs; one +that shows revenue, orders, customers and a table proves the blueprint is worth starting from. + +The components are named by their **bare** names, so `resolveComponentName` decides which active package +owns each. That is what makes a template portable: the same `Dashboard` renders with PrimeReact's widgets +in one profile and somebody else's in another. + +:::note +Two component names a dashboard obviously wants - `chart` and `fileUpload` - are declared by neither +`PrimeReact` nor `Cratis.Components`. Rather than reference names that would render as placeholders, the +templates use what exists: a table of monthly figures for revenue over time, and an image beside a +"Change photo" button for the profile photo. +::: + +## Using one + +A screen names the template whose shape it fills: + +```ts +import { Screen } from '@cratis/scene.model'; +import { LayoutName } from '@cratis/scene.blueprint.default'; + +const products: Screen = { + name: 'Products', + layout: LayoutName.AppShell, + screenTemplate: 'CrudList', + slotContent: {}, + forms: [], + contributions: [], +}; +``` + +To look at the shipped one first, boot its gallery screen: + +```tsx +import { GalleryScreenPreview } from '@cratis/scene.blueprint.default'; + +; +``` diff --git a/Documentation/blueprints/theme-tokens.md b/Documentation/blueprints/theme-tokens.md new file mode 100644 index 0000000..e1b1585 --- /dev/null +++ b/Documentation/blueprints/theme-tokens.md @@ -0,0 +1,122 @@ +--- +title: Theme tokens +description: The thirteen semantic tokens every Scene package agrees on, how they become CSS, and the attribution of every theme the default blueprint ships. +--- + +A Scene `Theme` is a set of semantic tokens - `primary.color`, `surface.card` - and nothing platform +specific. A renderer decides what a token becomes: the React renderer turns each into a CSS custom +property, and a future native renderer could turn it into something else entirely. + +## The vocabulary + +Thirteen tokens, shared by every Scene package. A vocabulary that grows per package is one no package can +rely on. + +| Token | CSS custom property | Used for | +| --- | --- | --- | +| `primary.color` | `--scene-primary-color` | brand accent, active states, primary buttons | +| `primary.contrastColor` | `--scene-primary-contrast-color` | text and icons on the primary color | +| `surface.background` | `--scene-surface-background` | the page canvas behind everything | +| `surface.card` | `--scene-surface-card` | topbar, sidebar, cards, panels | +| `surface.border` | `--scene-surface-border` | every divider and outline | +| `surface.hover` | `--scene-surface-hover` | hover states on menu items and buttons | +| `surface.overlay` | `--scene-surface-overlay` | floating submenus and popups | +| `text.color` | `--scene-text-color` | body text | +| `text.mutedColor` | `--scene-text-muted-color` | secondary text, footer, section titles | +| `highlight.background` | `--scene-highlight-background` | the current menu item's background | +| `highlight.color` | `--scene-highlight-color` | the current menu item's text | +| `content.borderRadius` | `--scene-content-border-radius` | every rounded corner | +| `focus.ring` | `--scene-focus-ring` | the keyboard focus indicator | + +The name-to-property rule lives in one place, `themeTokenProperty`: dots become dashes and camelCase +splits, so `text.mutedColor` becomes `--scene-text-muted-color`. + +## The bridge + +The blueprint's stylesheet aliases those custom properties into `--layout-*` locals exactly once, at the +top, and no rule below references a `--scene-*` name: + +```css +.layout-wrapper, +.layout-full-page { + --layout-primary: var(--scene-primary-color, #4f46e5); + --layout-primary-contrast: var(--scene-primary-contrast-color, #ffffff); + --layout-background: var(--scene-surface-background, #f4f5f7); + --layout-card: var(--scene-surface-card, #ffffff); + --layout-border: var(--scene-surface-border, #e2e5e9); + --layout-hover: var(--scene-surface-hover, #eceef1); + --layout-overlay: var(--scene-surface-overlay, #ffffff); + --layout-text: var(--scene-text-color, #1f2430); + --layout-text-muted: var(--scene-text-muted-color, #6b7280); + --layout-highlight: var(--scene-highlight-background, #eef2ff); + --layout-highlight-text: var(--scene-highlight-color, #3730a3); + --layout-radius: var(--scene-content-border-radius, 0.5rem); + --layout-focus-ring: var(--scene-focus-ring, 0 0 0 2px rgb(79 70 229 / 0.4)); +} +``` + +That single seam is what makes the shell theme-agnostic. Swap the theme and every mode restyles; take over +the look by overriding tokens rather than editing a rule. The fallbacks are neutral greys so an unthemed +shell is legible rather than invisible - they are a safety net, not a palette. + +## Applying a theme + +`LayoutThemeProvider` resolves whichever theme the shell's configuration currently names and hands it to +`SceneThemeProvider`: + +```tsx +import { LayoutConfigProvider, LayoutThemeProvider } from '@cratis/scene.blueprint.default'; + + + {/* the shell */} +; +``` + +Pass your own `themes` to offer brand palettes instead of the shipped two. The configurator only ever +records a *name*, so substituting the theme set changes nothing about the switcher. + +Switching is live re-resolution rather than a reload: the new token values are written onto the element +that is already there, so nothing below it remounts and no state is lost. + +## Theme attribution + +The `Theme` record carries `author`, `authorUrl` and `license` for exactly this purpose, and a blueprint +must fill them in. + +| Theme | Author | Link | License | +| --- | --- | --- | --- | +| Scene Default Light | Cratis | | MIT | +| Scene Default Dark | Cratis | | MIT | + +Both palettes are original to Cratis - a neutral grey ramp with an indigo accent - rather than adopted from +an existing preset, which is why `author` says so rather than being left blank. A theme with empty +attribution reads as "nobody has checked", and once a palette is lifted from somewhere else without its +credit, nobody can tell afterwards which of the two it was. + +:::important +If you adopt a palette from an existing free theme, fill `author`, `authorUrl` and `license` from the +source package's own LICENSE file. Verify it rather than assuming - "it's free" and "it's MIT" are not the +same claim. +::: + +## Compatibility is declared, not assumed + +A theme lists every package it is known to work with: + +```ts +export const blueprintThemeCompatibility: string[] = ['core', defaultBlueprintName, 'PrimeReact', 'Cratis.Components', 'Tailwind']; +``` + +`core` is on that list on purpose. `incompatiblePackages` has **no implicit exemption** for it, so a theme +omitting it is reported incompatible for `core` with every profile that lists it - which is every profile, +since `core` is the fallback vocabulary. The exemption was left out of the engine deliberately, so that +"compatible with everything active" has to be stated rather than assumed. + +An incompatible pairing is a warning rather than an error: the theme might still work by coincidence, but +the gap has to be visible. + +## Both themes define the same tokens + +Deliberately the same thirteen names in both, and nothing more. A dark theme that introduced extra tokens +would work only for the parts of the shell written after it existed. The token set is the contract, and +both themes filling it identically is what makes switching a swap rather than a re-render with holes in it. diff --git a/Documentation/blueprints/toc.yml b/Documentation/blueprints/toc.yml new file mode 100644 index 0000000..3bd1d2c --- /dev/null +++ b/Documentation/blueprints/toc.yml @@ -0,0 +1,34 @@ +- name: Overview + href: index.md +- name: Layouts + href: layouts.md +- name: Screen and dialog templates + href: screen-templates.md + +- name: Getting started + items: + - name: Use the default blueprint + href: getting-started.md + +- name: Guides + items: + - name: Ship your own blueprint + href: ship-your-own-blueprint.md + +- name: Understand + items: + - name: Understanding blueprints + href: understanding-blueprints.md + - name: Composing screens from templates + href: composing-screens.md + +- name: Reference + items: + - name: Regions and slots + href: regions-and-slots.md + - name: Layout modes + href: layout-modes.md + - name: The template set + href: template-set.md + - name: Theme tokens + href: theme-tokens.md diff --git a/Documentation/blueprints/understanding-blueprints.md b/Documentation/blueprints/understanding-blueprints.md new file mode 100644 index 0000000..5a1efd6 --- /dev/null +++ b/Documentation/blueprints/understanding-blueprints.md @@ -0,0 +1,98 @@ +--- +title: Understanding blueprints +description: Why the package that gives an application its shape is a different kind of thing from the package that gives it its widgets. +--- + +A component library gives you widgets. A blueprint gives you an application. + +That sounds like a slogan, so here is the concrete version. `@cratis/scene.primereact` declares eighty-odd +component names - `inputText`, `dataTable`, `dialog`, `timeline`. Every one is a thing you can put +somewhere. None of them tells you *where*. The decisions a real application still has to make after +picking a component library are: where the navigation lives, what happens to it below 991px, what a +dashboard is shaped like, what a sign-in page is shaped like, what an error page says, and whether all of +those look like the same product. + +Those decisions are what a blueprint is. + +## The three package kinds + +`PackageKind` has three values, and they answer different questions. + +| Kind | Answers | Example | +| --- | --- | --- | +| `ComponentLibrary` | what widgets can I place? | PrimeReact, Cratis Components, `core` | +| `Styling` | what CSS system are those widgets written against? | Tailwind | +| `Blueprint` | what does the application *look like*? | the default blueprint | + +A profile mixes all three freely, and the kind never decides override priority - that is declaration +order's job. What the kind does decide is what a package is allowed to be vague about. A component library +ships no layouts and no templates, on purpose: shipping one opinionated data page nobody can rearrange is +worse than shipping a `dataTable` a blueprint can place in a slot and configure. + +```mermaid +graph TD + Profile["ui profile"] --> Core["core
ComponentLibrary"] + Profile --> Prime["PrimeReact
ComponentLibrary"] + Profile --> Tailwind["Tailwind
Styling"] + Profile --> Components["Cratis.Components
ComponentLibrary"] + Profile --> Blueprint["Cratis.Blueprint.Default
Blueprint"] + Components -->|depends on| Prime + Components -->|depends on| Tailwind + Blueprint -->|depends on| Prime + Blueprint -->|depends on| Components + Blueprint --> Layouts["Layouts"] + Blueprint --> Templates["Screen + dialog templates"] + Blueprint --> Shell["Shell components"] + Blueprint --> Themes["Themes"] +``` + +## A blueprint declares what it is built from + +This is the part that is easy to skip and expensive to skip. A blueprint's shells are built out of +somebody else's widgets, so it says so: + +```ts +export const defaultBlueprintManifest: ScenePackage = { + name: 'Cratis.Blueprint.Default', + version: '1.0.0', + kind: PackageKind.Blueprint, + dependencies: [{ name: 'PrimeReact' }, { name: 'Cratis.Components' }], + // ... +}; +``` + +A profile that activates this blueprint without PrimeReact renders a shell whose every button, breadcrumb +and overlay is a dashed red placeholder. Declaring the dependency is what lets `resolvePackageDependencies` +say so while the profile is being configured, rather than leaving it to be discovered when somebody opens +the page. + +## Why the gallery is real screens + +The default blueprint ships twenty-three `Screen` instances alongside its templates. They are not +screenshots and not fixtures for a bespoke preview pipeline - they are the same `Screen` shape a real +application produces, put through the same engine and the same React renderer. + +That costs almost nothing and buys a specific thing: when you open the gallery and the dashboard renders, +you have learned that the blueprint works, not that somebody drew a picture of it working. And when a +component name in a template is wrong, the gallery is where it goes red - during the blueprint's own +specs, rather than in your application three weeks later. + +## When a blueprint is the wrong fit + +Be honest about the limits: + +- **You only need widgets.** If your application already has a shell and a design language, a blueprint + brings decisions you have already made. Take the component library and stop. +- **Your navigation is not a sidebar.** The default blueprint's eight modes are all answers to "where does + the sidebar go". An application whose primary navigation is a command palette, a canvas, or a document + outline is not served by any of them - it wants a blueprint of its own, which is a day's work rather + than a fork. +- **You need one screen to break the rules.** That is fine and does not need a blueprint at all: a screen + can fill the layout's `content` slot directly, without naming a screen template. + +## Next + +- [Layouts](./layouts.md) and [screen and dialog templates](./screen-templates.md) - the four concepts + inside a blueprint. +- [Composing screens from templates](./composing-screens.md) - how they are actually placed. +- [Ship your own blueprint](./ship-your-own-blueprint.md) - the recipe, when the default is not your look. diff --git a/Documentation/components-package/binding-registry.md b/Documentation/components-package/binding-registry.md new file mode 100644 index 0000000..5e6f07c --- /dev/null +++ b/Documentation/components-package/binding-registry.md @@ -0,0 +1,145 @@ +--- +title: The binding registry +description: How a query or command name in a screen becomes the real Arc proxy class the Cratis Components composites need. +--- + +A screen says which data it wants: + +```screenplay +data Invoices via query AllInvoices +``` + +`AllInvoices` is a *name*. By the time that reaches a renderer it is an `ExternalComponent` whose properties +bag holds the string `'AllInvoices'`, because a `.play` document is text, Stage compiles it, and Studio reads +it back — and nothing in that round trip can carry a TypeScript class. + +`DataTableForQuery` needs the class: + +```tsx + +``` + +That gap is real and it is not closable from inside Scene. An adapter cannot conjure a query class out of a +string, and it should not try — guessing at a module path or reaching into a global would move the failure +somewhere worse. The name is the only thing that survives, so the name is what the lookup has to be keyed on. + +## The seam + +```mermaid +flowchart LR + subgraph screen["A .play screen"] + A["data Invoices
via query AllInvoices"] + end + subgraph model["Scene model"] + B["ExternalComponent
properties.query = 'AllInvoices'"] + end + subgraph host["The host application"] + C["registerQuery('AllInvoices', AllInvoices)"] + end + subgraph registry["Binding registry"] + D["'AllInvoices' → class AllInvoices"] + end + subgraph render["Render"] + E["DataTableForQuery query={AllInvoices}"] + end + A --> B --> D + C --> D + D --> E +``` + +Only the host owns the generated Arc proxies, so only the host can supply the class. It registers every proxy +a screen can name, once, at startup: + +```typescript +import { registerQueries, registerCommands } from '@cratis/scene.components'; +import { AllInvoices, InvoiceById } from './Invoices/proxies'; +import { RegisterInvoice } from './Invoices/RegisterInvoice'; + +registerQueries({ AllInvoices, InvoiceById }); +registerCommands({ RegisterInvoice }); +``` + +Object shorthand is the point of the bulk form: Stage generates a module that exports every proxy it +produced, and handing that module's exports straight in stays correct as proxies are added and removed +without anyone editing a list. + +## The API + +| Function | Purpose | +|---|---| +| `registerQuery(name, queryClass)` | Register one Arc query proxy under the name screens refer to it by. | +| `registerQueries(bindings)` | Register several at once, keyed by name. | +| `resolveQuery(name)` | The registered class, or `undefined`. | +| `registerCommand(name, commandClass)` | The command half of `registerQuery`. | +| `registerCommands(bindings)` | Register several commands at once. | +| `resolveCommand(name)` | The registered class, or `undefined`. | +| `registeredQueryNames()` | Every registered query name, sorted. | +| `registeredCommandNames()` | Every registered command name, sorted. | +| `clearBindings()` | Forget everything. | + +Queries and commands are separate namespaces. They are opposite halves of CQRS, and a screen means exactly +one of them at each site — letting a command satisfy a query binding would turn a modeling mistake into a +runtime one. + +Registering the same name twice replaces the earlier registration, so a host can re-register on hot reload +without unwinding the previous run. `clearBindings()` exists because the registry is module-level state: +right for a host that registers once at startup, wrong for Studio switching between projects or a spec that +must not inherit what the previous one registered. + +## The classes are not typed as Arc types + +`registerQuery` takes a `BoundConstructor` — "something that can be constructed" — not +`Constructor>`: + +```typescript +export type BoundConstructor = new (...args: never[]) => object; +``` + +Scene deliberately does not depend on Arc. A Scene screen is a UI model, and the whole point of the registry +is that Scene never has to know what an Arc proxy *is*. Whether a registered class really is a query is the +host's responsibility — and the host checks it where it registers, with the real Arc types in scope, which is +exactly where that check belongs. + +## A missing binding is a visible placeholder, never a throw + +`resolveQuery` returns `undefined` rather than throwing, and every Arc-bound adapter turns that into a +dashed-out box naming what it wanted: + +```text +Unresolved query binding 'AllInvoices' on Cratis.Components:dataTable +``` + +A screen that names no query at all reads differently, because it is a different mistake: + +```text +Missing query binding on Cratis.Components:dataTable +``` + +The first needs the host to register that name; the second needs the screen edited. The message is enough to +tell them apart without opening a debugger. + +This matters most in Studio. Design-time preview normally has *nothing* registered — there is no backend to +query — and the screen still has to be usable so its layout can be worked on. One unbound table costs one +dashed box, not the whole screen. The presentation deliberately matches `UnresolvedComponent` in +`@cratis/scene.react`: the same class of failure at two different depths, and a designer scanning a preview +should recognize both instantly as "something here is not wired up". + +## The Arc runtime is loaded only when it is needed + +`@cratis/arc` and `@cratis/arc.react` are peer dependencies of `@cratis/components` that the host supplies. +A design surface is not a host, so every adapter that reaches them does so through a dynamic `import()`: + +```tsx +const DataTableForQuery = lazy(async () => ({ default: (await import('@cratis/components/DataTables')).DataTableForQuery })); +``` + +A screen made only of the library's Arc-free components — pages, toolbars, editors, tooltips — therefore +never pulls the Arc client in at all, and an unbound table never even reaches the import. When a binding *is* +registered, the chunk loads behind an `ArcRuntimeBoundary`: `Suspense` while it is in flight, and +`@cratis/components`' own `ErrorBoundary` if it cannot load, which is what a host without Arc installed +sees. One dashed-out region, not a blank screen and not a thrown render. + +## Where to go next + +- [Component reference](components.md) — which components take a binding, and under which property. +- [What this package does not cover](coverage.md) — the other deliberate limits. diff --git a/Documentation/components-package/components.md b/Documentation/components-package/components.md new file mode 100644 index 0000000..57158f5 --- /dev/null +++ b/Documentation/components-package/components.md @@ -0,0 +1,152 @@ +--- +title: Component reference +description: Every abstract name Cratis.Components declares, the @cratis/components component behind it, and the properties and slots it reads. +--- + +Every name `Cratis.Components` declares, the `@cratis/components` component behind it, and the properties and +slots it reads. Registry keys are `Cratis.Components:`. + +A property whose value is absent or of the wrong type falls back to the component's own default rather than +being forced through — `title: 42` renders an empty title, not `"42"`. Names marked **binding** are looked up +in the [binding registry](binding-registry.md); an unresolved one renders a placeholder. + +## Pages + +| Name | Wraps | Properties | Slots | +|---|---|---|---| +| `page` | `Page` | `title`, `showTitle`, `panel` | `content` | +| `dataPage` | `DataPage` | **`query`** (binding), `title`, `emptyMessage`, `dataKey`, `globalFilterFields`, `clientFiltering` | `content` | +| `formElement` | `FormElement` | `icon` (icon class name) | `icon`, `content` | + +`formElement` takes its addon from the `icon` slot when there is one, and from the `icon` property otherwise +— a slot is the more specific statement, so it wins. + +## Data + +| Name | Wraps | Properties | Slots | +|---|---|---|---| +| `dataTable` | `DataTableForQuery` | **`query`** (binding), `emptyMessage`, `dataKey`, `globalFilterFields`, `clientFiltering`, `className` | `content` | +| `table` | `DataTableForQuery` | Same as `dataTable` | `content` | +| `observableDataTable` | `DataTableForObservableQuery` | Same as `dataTable` | `content` | + +Columns come from the `content` slot. `table` is the same component as `dataTable` under the bare name — see +[naming and shadowing](naming-and-shadowing.md). `observableDataTable` is a separate name because the +distinction lives in the registered proxy, not the configuration: an observable query opens a subscription +and re-renders when the read model changes. + +## Forms + +| Name | Wraps | Properties | Slots | +|---|---|---|---| +| `commandForm` | `AutoCommandForm` | **`command`** (binding), `exclude` | — | + +`AutoCommandForm` generates its fields from the command's own property descriptors, so the form follows the +command rather than going stale when a property is added on the backend. `exclude` keeps it from generating a +second copy of anything placed by hand. + +### Field types + +Every field takes the same three properties, plus its own: + +| Property | Meaning | +|---|---| +| `property` | **Required.** The command property this field binds to. Without it the field renders a placeholder rather than a field bound to nothing. | +| `title` | The label. Defaults to the property name, so a field is legible before anyone writes one. | +| `description` | Helper text under the input. | + +| Name | Wraps | Own properties | +|---|---|---| +| `inputTextField` | `InputTextField` | `type` (`text`, `email`, `password`, `color`, `date`, `datetime-local`, `time`, `url`, `tel`, `search`), `placeholder`, `className` | +| `numberField` | `NumberField` | `placeholder`, `min`, `max`, `step`, `className` | +| `checkboxField` | `CheckboxField` | `label`, `className` | +| `textAreaField` | `TextAreaField` | `placeholder`, `rows`, `cols`, `className` | +| `dropdownField` | `DropdownField` | `options`, `optionLabel`, `optionValue`, `placeholder`, `className` | +| `sliderField` | `SliderField` | `min`, `max`, `step`, `className` | +| `calendarField` | `CalendarField` | `placeholder`, `dateFormat`, `showIcon`, `showTime`, `hourFormat` (`12`, `24`), `className` | +| `colorPickerField` | `ColorPickerField` | `inline`, `defaultColor`, `className` | +| `multiSelectField` | `MultiSelectField` | `options`, `optionLabel`, `optionValue`, `placeholder`, `display` (`comma`, `chip`), `maxSelectedLabels`, `filter`, `showClear`, `className` | +| `chipsField` | `ChipsField` | `placeholder`, `max`, `separator`, `addOnBlur`, `allowDuplicate`, `className` | +| `radioButtonField` | `RadioButtonField` | `buttonValue`, `label`, `className` | +| `radioGroupField` | `RadioGroupField` | `options`, `optionLabel`, `optionValue`, `layout` (`horizontal`, `vertical`), `className` | + +`options` is a list of objects; `optionLabel` and `optionValue` name the keys to read from each, defaulting +to `label` and `value`. + +```json +{ + "property": "status", + "title": "Status", + "options": [ + { "label": "Draft", "value": "draft" }, + { "label": "Approved", "value": "approved" } + ] +} +``` + +## Dialogs + +| Name | Wraps | Properties | Slots | +|---|---|---|---| +| `dialog` | `Dialog` | `title`, `visible`, `width`, `resizable`, `isValid`, `isBusy`, `okLabel`, `cancelLabel`, `className` | `content` | +| `confirmationDialog` | `ConfirmationDialog` | — | — | +| `busyIndicatorDialog` | `BusyIndicatorDialog` | `title`, `message` | — | +| `commandDialog` | `CommandDialog` | **`command`** (binding), `title`, `visible`, `width`, `resizable`, `okLabel`, `cancelLabel`, `className` | `content` | +| `stepperCommandDialog` | `StepperCommandDialog` | **`command`** (binding), `title`, `visible`, `width`, `linear`, `orientation`, `okLabel`, `nextLabel`, `previousLabel`, `showCancel`, `cancelLabel` | `content` | + +`visible` defaults to `true` — a dialog placed on a screen is being placed to be seen; a host that controls +visibility sets the property explicitly. + +`confirmationDialog` takes no properties by design. It is not a dialog a screen configures; it is the host +that renders whatever confirmation the running application asked for through Arc's dialog service. Place it +once, near the root. + +Prefer `commandDialog` over composing `dialog` with `commandForm`: a hand-composed pair has no way to keep +the dialog open on a rejected command without reimplementing the protocol. + +## Common + +| Name | Wraps | Properties | Slots | +|---|---|---|---| +| `icon` | `IconDisplay` | `icon`, `className` | — | +| `tooltip` | `Tooltip` | `content`, `position` (`top`, `right`, `bottom`, `left`), `disabled` | `content` | +| `dropdown` | `Dropdown` | `options`, `optionLabel`, `optionValue`, `placeholder`, `disabled`, `showClear`, `filter`, `className` | — | +| `errorBoundary` | `ErrorBoundary` | — | `content` | + +`icon` normalizes the shorthand forms people write — `pi-check` on its own, or a bare name — into the class +PrimeIcons expects. `dropdown` is the standalone control; use `dropdownField` inside a `commandForm`. + +## Editors + +| Name | Wraps | Properties | Slots | +|---|---|---|---| +| `objectContentEditor` | `ObjectContentEditor` | `object`, `schema`, `editMode`, `className` | — | +| `objectNavigationalBar` | `ObjectNavigationalBar` | `navigationPath`, `className` | — | +| `schemaEditor` | `SchemaEditor` | `schema`, `eventTypeName`, `canEdit`, `canNotEditReason`, `editMode`, `className` | — | +| `timeMachine` | `TimeMachine` | `versions`, `currentVersionIndex`, `scrollSensitivity` | — | +| `filterPanel` | `FilterPanel` | `label`, `filters`, `searchPlaceholder` | `content` | + +`versions` entries take `id`, `label`, `timestamp` (ISO string or epoch number) and `content`; entries +missing any of the first three are dropped. `filters` entries take `key`, `label`, `type`, `multi`, +`options`, `searchable`, `searchPlaceholder` and `buckets`; entries without a `key` and a `label` are +dropped. + +`filterPanel` renders its own toggle button as well as the panel, because `FilterPanel` is a portal anchored +to a button and cannot be placed on its own. `label` names that button and defaults to `Filters`. + +## Toolbar + +| Name | Wraps | Properties | Slots | +|---|---|---|---| +| `toolbar` | `Toolbar` | `orientation` (`vertical`, `horizontal`), `draggable` | `content` | +| `toolbarButton` | `ToolbarButton` | `icon`, `text`, `title`, `active`, `tooltipPosition` | — | +| `toolbarGroup` | `ToolbarGroup` | `slotName`, `orientation` | `content` | +| `toolbarSeparator` | `ToolbarSeparator` | `orientation` | — | + +`draggable` belongs on the toolbar rather than each button — it is a property of the palette, and setting it +per button is how you end up with a palette that is half draggable. `toolbarButton`'s `title` is both the +tooltip and the accessible name, and defaults to `text` so an icon-only button is never nameless. + +## Where to go next + +- [The binding registry](binding-registry.md) — how the binding properties resolve. +- [What this package does not cover](coverage.md) — the deliberate omissions. diff --git a/Documentation/components-package/coverage.md b/Documentation/components-package/coverage.md new file mode 100644 index 0000000..9c45149 --- /dev/null +++ b/Documentation/components-package/coverage.md @@ -0,0 +1,68 @@ +--- +title: What this package does not cover +description: The parts of @cratis/components Cratis.Components deliberately does not expose, and the reasoning behind each omission. +--- + +Every omission below is a decision. None of them is "not done yet". + +## `PivotViewer` + +`@cratis/components/PivotViewer` is not exposed as a Scene component. + +It is a canvas-rendered faceted browser built on [pixi.js](https://pixijs.com), a full WebGL rendering +engine. Declaring the name would pull that engine into the module graph of every application that lists this +package — including Studio's design-time preview, where a canvas of animated tiles has nothing to show +because there is no data to facet. The cost is a heavy renderer in every bundle; the benefit at design time +is zero. + +An application that wants a pivot viewer imports it directly and places it in a slot. That is the right shape +for a component this specialized: it is a destination, not a building block. + +## `card` + +`@cratis/components` ships no card. See [naming and shadowing](naming-and-shadowing.md) — overriding a name +with a weaker implementation is a regression, so `card` falls through to `core` or PrimeReact. + +## Table columns + +`dataTable` and `dataPage` take their columns from the `content` slot, and this package declares no `column` +name. + +A PrimeReact `Column` is a configuration element rather than a rendered one, and it belongs to the PrimeReact +package. Declaring a second, identical `column` here would create a shadow with nothing behind it — priority +resolution doing work for no reason. + +## Event handlers + +A screen configures a component through `properties` and fills it through `slots`. There is no seam yet for +binding a click to an action, so adapters do not expose `onClick`, `onSelectionChange` or `onNavigate`. + +Where a component's callback drives its *own* visible state, the adapter holds that state so the component +behaves like the component it is — the breadcrumb trail truncates when you click a crumb, the schema editor +accepts edits, the time machine scrubs. That state is local to the preview. A host that needs the result +reads it off its own model. + +Where a callback would reach outside the component — navigating, executing something — the adapter exposes +nothing rather than a handler that quietly does nothing. + +## Date ranges on `calendarField` + +`minDate` and `maxDate` are not exposed. They would arrive from the property bag as strings and have to be +parsed here, and a bound range that silently misparses is worse than no bound range at all. A date range that +has to be enforced belongs in the command's validator, where it is authoritative for every caller rather than +only for this one field. + +## Rich content in `timeMachine` versions + +A version's `content` is rendered as text. A version whose content is a whole element tree is not something a +property bag can carry, and pretending otherwise would be the wrong seam — that belongs in a +[screen template](../blueprints/screen-templates.md). + +Entries missing an `id`, a `label` or a parseable `timestamp` are dropped rather than defaulted. A version +invented at the epoch would not be a slightly wrong entry; it would silently reorder every real one around +it. A dropped entry is visibly one item short, and a fabricated one is a timeline that lies. + +## Where to go next + +- [Component reference](components.md) — everything that *is* covered. +- [The binding registry](binding-registry.md) — the one constraint that shapes the rest of the package. diff --git a/Documentation/components-package/index.md b/Documentation/components-package/index.md new file mode 100644 index 0000000..a483ff6 --- /dev/null +++ b/Documentation/components-package/index.md @@ -0,0 +1,94 @@ +--- +title: Cratis Components package +description: The Scene package that exposes @cratis/components' Arc-bound data, form and dialog composites as resolvable Scene components. +--- + +`Cratis.Components` is the Scene package that turns [`@cratis/components`](https://www.npmjs.com/package/@cratis/components) +— Cratis' React component library — into names a screen can resolve. + +Without it, a `.play` document that says "show a table of invoices" resolves `table` to PrimeReact's +`DataTable`: a grid that renders rows it is handed, and knows nothing about where they came from. You still +have to write the query, the paging, the sorting and the filtering by hand, in code the screen cannot see. + +With it, `table` resolves to `DataTableForQuery`, and the screen names a query instead. The paging is +server-side, the sorting and filtering go back to the backend, and the whole thing is one element in the +model. The same swap happens for the form, the dialog, and the whole list page — that is the relief this +package delivers. + +## What is in the box + +Thirty-seven abstract names across seven families: + +| Family | Names | What they wrap | +|---|---|---| +| Pages | `page`, `dataPage`, `formElement` | `Page`, `DataPage`, `FormElement` | +| Data | `dataTable`, `table`, `observableDataTable` | `DataTableForQuery`, `DataTableForObservableQuery` | +| Forms | `commandForm` and twelve field types | `AutoCommandForm` and the `CommandForm` fields | +| Dialogs | `dialog`, `confirmationDialog`, `busyIndicatorDialog`, `commandDialog`, `stepperCommandDialog` | `Dialogs` and `CommandDialog` | +| Common | `icon`, `tooltip`, `dropdown`, `errorBoundary` | `IconDisplay`, `Tooltip`, `Dropdown`, `ErrorBoundary` | +| Editors | `objectContentEditor`, `objectNavigationalBar`, `schemaEditor`, `timeMachine`, `filterPanel` | The editing and inspection surfaces | +| Toolbar | `toolbar`, `toolbarButton`, `toolbarGroup`, `toolbarSeparator` | The `Toolbar` family | + +Every name, with the properties and slots it reads, is in the [component reference](components.md). + +## It is a component library, and only that + +The package declares `ComponentLibrary`, and its `layouts`, `screenTemplates`, `dialogTemplates` and `themes` +lists are all empty. That is a statement, not an oversight: + +- **No layouts or templates.** Those are decisions about what an application looks like as a whole, and they + belong to a [blueprint](../blueprints/index.md). This package provides the Arc-bound composites a template + is *built from* — so a blueprint can place a `dataPage` in a slot and configure it, rather than this + package shipping one opinionated data page nobody can rearrange. +- **No themes.** `@cratis/components` has no palette of its own. It reads a `--cratis-*` variable layer that + resolves whatever theme is active underneath it. Shipping a theme here would be this library asserting a + look it was specifically built not to have. See [theming through design tokens](theming.md). + +## What it needs underneath it + +```screenplay +ui profile Desktop + target platform web + target size expanded + + packages + core + Tailwind + PrimeReact + Cratis.Components +``` + +Every component in `@cratis/components` is a wrapper over a PrimeReact widget, and its styling is a compiled +Tailwind utility sheet plus the `--cratis-*` token layer. List it without PrimeReact and nothing renders; +list it without Tailwind and everything renders unstyled. So it declares both: + +```typescript +dependencies: [ + { name: 'PrimeReact', versionRange: '>=10.9.0' }, + { name: 'Tailwind', versionRange: '^4.0.0' }, +], +``` + +`resolvePackageDependencies(['Cratis.Components'], catalog)` expands that to +`['Tailwind', 'PrimeReact', 'Cratis.Components']` — which is also the correct override priority, since this +package layers on both. The version ranges are the real ones: `>=10.9.0` because the `--cratis-*` tokens +resolve PrimeReact 11's design tokens first and fall back to version 10's theme variables, and `^4.0.0` +because the utility sheet is compiled against Tailwind 4. + +## The one thing you have to know + +Most of what this library offers is **Arc-bound**. `DataPage`, both data tables, `AutoCommandForm`, +`CommandDialog` and `StepperCommandDialog` all take a *query or command class* and talk to a live backend. +A Scene element carries a `properties` bag of plain values and named slots — there is no way to put a +TypeScript class in one. + +The [binding registry](binding-registry.md) is how a name in a screen becomes that class. Read that page +before you build anything on this package; everything else here assumes it. + +## Where to go next + +- [The binding registry](binding-registry.md) — how a screen names a query, and a host supplies it. +- [Naming and shadowing](naming-and-shadowing.md) — why `table` resolves here and not to PrimeReact. +- [Theming through design tokens](theming.md) — how a Scene theme drives the library. +- [What this package does not cover](coverage.md) — and why each omission is deliberate. +- [Component reference](components.md) — every name, property and slot. diff --git a/Documentation/components-package/naming-and-shadowing.md b/Documentation/components-package/naming-and-shadowing.md new file mode 100644 index 0000000..fbcf5f9 --- /dev/null +++ b/Documentation/components-package/naming-and-shadowing.md @@ -0,0 +1,86 @@ +--- +title: Naming and shadowing +description: How Cratis Components names its components, and why table and dialog deliberately override the packages underneath it. +--- + +A screen writes `table`. Which `table` it gets is decided by the profile's package list, not by the screen — +and that is the whole reason component names are abstract. + +## The naming rules + +- **`lowerCamelCase`, always.** `dataTable`, `inputTextField`, `stepperCommandDialog`. +- **Abstract, not the wrapped type's name.** The name is `dataTable`, not `DataTableForQuery`. A screen that + named the implementation would be pinned to this package; a screen that names the concept resolves against + whichever package the profile ranks highest. +- **Registered as `Cratis.Components:`.** The key carries its package, so nothing collides across + packages and merge order never matters. + +A screen can also qualify a name explicitly — `Cratis.Components.table` — which resolves directly against +that package and bypasses priority entirely. An author naming the package has already disambiguated. + +## Two names are deliberate overrides + +Three packages in a typical profile declare `table`, and two declare `dialog`: + +| Name | `core` | `PrimeReact` | `Cratis.Components` | +|---|---|---|---| +| `table` | — | `DataTable` | `DataTableForQuery` | +| `dialog` | — | `Dialog` | Arc-aware `Dialog` | +| `card` | a bordered section | a `Card` | — | + +With the profile listing `core`, `PrimeReact`, `Cratis.Components` in that order, `resolveComponentName` +walks from highest priority to lowest and lands here: + +```typescript +resolveComponentName('table', profile, catalog); +// { name: 'table', package: 'Cratis.Components', shadows: ['PrimeReact'] } +``` + +The shadowed package is **recorded, not discarded** — that list is what answers "why did this resolve to +`Cratis.Components` and not PrimeReact?" without anyone having to reason it out. + +Both overrides are override priority working exactly as designed, and both are worth having: + +- **`table`** — PrimeReact's `DataTable` is handed rows. `DataTableForQuery` performs the query, pages + against the server, and wires filtering and sorting back into it. For a Cratis application that is + strictly better, so the bare name should land on it. +- **`dialog`** — PrimeReact's `Dialog` is a modal frame. This one resolves its result through Arc's dialog + context, so a caller awaits a `DialogResult` instead of threading `visible` state and callbacks by hand. + +## `card` is deliberately *not* overridden + +`@cratis/components` ships no card component. The closest thing is `Page`'s `panel` chrome, which is a page +concern rather than a card, and the `.panel` class it applies is one the consuming application defines. + +Declaring `card` here would shadow `core` and PrimeReact with something worse — an override that makes the +name resolve to a weaker implementation is a regression wearing the costume of a feature. So `card` falls +through: + +```typescript +resolveComponentName('card', profile, catalog); +// { name: 'card', package: 'core', shadows: [] } +``` + +Only override a name when you are confident the replacement is better for every screen that already writes +it. That is the test, and it is the reason two names are overridden here and a third is not. + +## `dataTable` and `table` are the same component + +`dataTable` says what it is. `table` is what a screen written against the base vocabulary already says. Both +resolve to the same adapter, so adopting this package does not require rewriting screens, and a screen that +wants to be explicit can be. + +## The declaration and the bundle must agree + +A manifest that promises a component the bundle does not register renders as an `UnresolvedComponent` at +runtime — a blank box, far from the mistake. A component registered but not declared can never be named by a +screen at all. Both are silent failures, which is why every package runs: + +```typescript +validatePackageBundle(cratisComponentsPackage).should.deep.equal([]); +``` + +## Where to go next + +- [Component reference](components.md) — the full name list. +- [Packages](../packages/index.md) — how a profile's package list is resolved and ordered. diff --git a/Documentation/components-package/theming.md b/Documentation/components-package/theming.md new file mode 100644 index 0000000..4d86aae --- /dev/null +++ b/Documentation/components-package/theming.md @@ -0,0 +1,106 @@ +--- +title: Theming through design tokens +description: How a Scene theme drives Cratis Components through a CSS variable bridge, without either side knowing the other exists. +--- + +A Scene theme is a bag of semantic tokens — `surface.card`, `text.color`, `primary.color`. It says nothing +about CSS, because a non-DOM renderer has to be able to make an entirely different choice. + +`@cratis/components` reads a `--cratis-*` CSS variable layer. It says nothing about Scene, because it was +written years before Scene existed and is used by applications that will never touch it. + +Neither side can learn about the other without one of them losing what makes it useful. So a stylesheet in +this package joins them, and both stay ignorant. + +## Three layers, resolved in order + +`@cratis/components` never references PrimeReact's variables directly. Its own `tokens.css` puts one +indirection in front of them, so a single build spans PrimeReact major versions: + +```css +--cratis-surface-card: var(--p-content-background, var(--surface-card)); +/* ^ PrimeReact v11 ^ v10 legacy */ +``` + +This package's bridge inserts Scene in front of that chain: + +```css +--cratis-surface-card: var(--scene-surface-card, var(--p-content-background, var(--surface-card))); +/* ^ Scene theme ^ PrimeReact v11 ^ v10 legacy */ +``` + +Three consequences, each a decision rather than a side effect: + +- **The Scene value is only a *first* preference.** Every mapping keeps the library's original fallback chain + behind it, so a theme that defines eight of the thirteen tokens leaves the other five exactly as the active + PrimeReact theme had them, rather than blanking them. +- **The rules are scoped to the themed element, never `:root`.** `applyThemeTokens` writes onto the + `SceneThemeProvider`'s element, so `--scene-*` does not exist at the document root. A `:root` rule would + resolve every mapping to nothing and wipe out the PrimeReact fallbacks the bridge is supposed to preserve. + The selector is `[data-scene-theme-root], [data-scene-theme]` — the provider sets the first, and a host + calling `applyThemeTokens` directly sets the second. +- **Only tokens Scene has a name for are mapped.** `--cratis-primary-500`, `--cratis-green-500` and the rest + of the primitive palette are left untouched and keep resolving against the PrimeReact theme. Inventing + Scene names for them would assert a vocabulary the themes on the other side do not have. + +## Using it + +```typescript +import '@cratis/scene.components/styles'; +``` + +Then wrap the tree in a `SceneThemeProvider` with a theme, and every `@cratis/components` component +underneath follows it: + +```tsx + + + +``` + +Switching theme rewrites the custom properties on the same element — no reload, no remount, and nothing +below it loses state. + +## The token vocabulary + +Thirteen tokens, the same set `@cratis/scene.primereact` writes its themes in. One vocabulary across both +packages is the point: a theme shipped by the PrimeReact package drives Cratis Components without knowing it +exists. + +| Scene token | CSS custom property | Maps onto | +|---|---|---| +| `primary.color` | `--scene-primary-color` | `--cratis-primary-color` | +| `primary.contrastColor` | `--scene-primary-contrast-color` | `--cratis-primary-color-text` | +| `surface.background` | `--scene-surface-background` | `--cratis-surface-ground` | +| `surface.card` | `--scene-surface-card` | `--cratis-surface-card`, `--cratis-surface-0` | +| `surface.border` | `--scene-surface-border` | `--cratis-surface-border` | +| `surface.hover` | `--scene-surface-hover` | `--cratis-surface-hover`, `--cratis-surface-section`, `--cratis-surface-100` | +| `surface.overlay` | `--scene-surface-overlay` | `--cratis-surface-overlay` | +| `text.color` | `--scene-text-color` | `--cratis-text-color` | +| `text.mutedColor` | `--scene-text-muted-color` | `--cratis-text-color-secondary` | +| `highlight.background` | `--scene-highlight-background` | `--cratis-highlight-bg` | +| `highlight.color` | `--scene-highlight-color` | `--cratis-highlight-text-color` | +| `content.borderRadius` | `--scene-content-border-radius` | `--cratis-border-radius` | +| `focus.ring` | `--scene-focus-ring` | `--cratis-focus-ring` | + +`themeTokenProperty` in `@cratis/scene.react` is the single place that decides how a semantic name becomes a +CSS custom property: it splits on `.`, kebab-cases each part, and prefixes `--scene-`. + +## A theme still has to be loaded + +The bridge tints the surfaces the Cratis wrappers own. It does not, on its own, skin PrimeReact's widgets: +in PrimeReact 10 every widget's *structural* CSS — padding, borders, the dialog frame, focus rings — ships +inside the theme file, and there is no separate primitives stylesheet. An application that loads no +PrimeReact theme has no structural CSS, and its components render as raw HTML primitives whatever the tokens +say. + +So load a PrimeReact theme underneath, and use Scene tokens to move it to your palette. The +[PrimeReact package](../primereact-package/index.md) ships every free PrimeReact 10 theme as a Scene theme, +with its tokens read verbatim out of the theme's own `:root` block — so the token layer always agrees with +what the stylesheet renders. Its [theme reference](../primereact-package/theme-reference.md) lists the +values each theme carries for the thirteen tokens above. + +## Where to go next + +- [Component reference](components.md) — every name this package declares. +- [Naming and shadowing](naming-and-shadowing.md) — how a profile decides which package a name resolves to. diff --git a/Documentation/components-package/toc.yml b/Documentation/components-package/toc.yml new file mode 100644 index 0000000..4f6d9d5 --- /dev/null +++ b/Documentation/components-package/toc.yml @@ -0,0 +1,12 @@ +- name: Overview + href: index.md +- name: The binding registry + href: binding-registry.md +- name: Naming and shadowing + href: naming-and-shadowing.md +- name: Theming through design tokens + href: theming.md +- name: What this package does not cover + href: coverage.md +- name: Component reference + href: components.md diff --git a/Documentation/index.md b/Documentation/index.md new file mode 100644 index 0000000..2c1f27e --- /dev/null +++ b/Documentation/index.md @@ -0,0 +1,54 @@ +--- +title: Scene +description: The object model and runtime for describing a user interface without describing a platform - screens, layouts, templates, packages and themes, shared by Stage at build time and Studio at design time. +--- + +Scene is the object model and runtime for describing a user interface without describing a platform. + +A `.play` document says an application has an invoice list screen with a table of invoices and an action that +opens a form. It does not say whether that renders as PrimeReact on the web, SwiftUI on a phone, or something +that does not exist yet. Scene is where that description becomes a structure a renderer can execute — and +where the decisions that make it concrete (which component library, which visual theme, which application +shell) are made explicitly rather than assumed. + +## The three halves + +Scene is deliberately split so that nothing platform-specific can leak into the model: + +| Part | Stack | Responsibility | +|---|---|---| +| `Cratis.Scene.Model` / `@cratis/scene.model` | C# (source of truth) + a TypeScript mirror | The object model. Records only — screens, layouts, templates, forms, contribution points, profiles, themes, packages. No React, no DOM, no CSS vocabulary anywhere. | +| `Cratis.Scene.Engine` / `@cratis/scene.engine` | C# + TypeScript | The algorithms over that model — package and component-name resolution, dependency ordering, layout evaluation, size classes, contribution aggregation, theme compatibility. Both implementations assert against a shared fixture corpus so they cannot drift. | +| `@cratis/scene.react` | TypeScript | One renderer. It implements the engine's renderer contract against real React and DOM, so `Scene.Native` and `Scene.Desktop` are sibling positions rather than special cases. | + +Stage executes the model to ship an application. Studio edits it. Neither is part of Scene. + +## Vocabulary + +Four words carry most of the weight, and they are easy to confuse. They are not interchangeable: + +- **[Layout](blueprints/layouts.md)** — an application's base navigational look: the shell with its top bar, + navigation and content region. An application has **one**, and selects it. +- **[Screen template](blueprints/screen-templates.md)** — a reusable shape that goes *inside* that layout, at + module, feature or slice level. An application has **many**. Each declares which of its parent's slots it + fills, so templates nest arbitrarily deep by one rule rather than several. +- **Dialog template** — the same idea for content that opens *over* an application rather than sitting inside + it. It fills no slot, because it occupies none. +- **Screen** — an instance. It names the structure it fills and provides the content that fills it. + +## Packages + +A `ui profile` lists packages by name and resolves component names against them in priority order. A +[package](packages/index.md) is the declaration behind such a name: what it contributes, and what else has to +be active for it to work. + +- A **component library** declares component names — PrimeReact, Cratis Components, and the built-in `core` + fallback. +- A **styling** package contributes a CSS system rather than components; component libraries depend on it to + say what they are written against. +- A **blueprint** ships the shape of an application: its layouts, the screen and dialog templates built on + them, and the components that fill their slots. An application selects one blueprint and gets a coherent + set, rather than assembling parts from unrelated sources. + +Dependencies between packages are declared, resolved and ordered, so "Cratis Components needs PrimeReact and +Tailwind" is a fact the tooling can check rather than a note in a README. diff --git a/Documentation/primereact-package/component-reference.md b/Documentation/primereact-package/component-reference.md new file mode 100644 index 0000000..45a3d32 --- /dev/null +++ b/Documentation/primereact-package/component-reference.md @@ -0,0 +1,262 @@ +--- +title: Component reference +description: Every abstract component name the PrimeReact package declares, the PrimeReact 10 component behind it, and what is deliberately not covered. +--- + +87 abstract names across ten families. Every name is `lowerCamelCase`; every registry key is +`PrimeReact:`. + +## Form + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `inputText` | `InputText` | `primereact/inputtext` | +| `inputTextarea` | `InputTextarea` | `primereact/inputtextarea` | +| `inputNumber` | `InputNumber` | `primereact/inputnumber` | +| `password` | `Password` | `primereact/password` | +| `inputMask` | `InputMask` | `primereact/inputmask` | +| `floatLabel` | `FloatLabel` + `InputText` | `primereact/floatlabel`, `primereact/inputtext` | +| `iconField` | `IconField` + `InputIcon` + `InputText` | `primereact/iconfield`, `primereact/inputicon`, `primereact/inputtext` | +| `dropdown` | `Dropdown` | `primereact/dropdown` | +| `multiSelect` | `MultiSelect` | `primereact/multiselect` | +| `listBox` | `ListBox` | `primereact/listbox` | +| `selectButton` | `SelectButton` | `primereact/selectbutton` | +| `checkbox` | `Checkbox` | `primereact/checkbox` | +| `radioButton` | `RadioButton` | `primereact/radiobutton` | +| `toggleSwitch` | `InputSwitch` | `primereact/inputswitch` | +| `slider` | `Slider` | `primereact/slider` | +| `rating` | `Rating` | `primereact/rating` | +| `knob` | `Knob` | `primereact/knob` | +| `calendar` | `Calendar` | `primereact/calendar` | +| `colorPicker` | `ColorPicker` | `primereact/colorpicker` | +| `chips` | `Chips` | `primereact/chips` | +| `autoComplete` | `AutoComplete` | `primereact/autocomplete` | +| `treeSelect` | `TreeSelect` | `primereact/treeselect` | +| `cascadeSelect` | `CascadeSelect` | `primereact/cascadeselect` | + +`radioButton` renders the whole group from its `options`, sharing one `name` so the browser enforces +exclusivity — a lone radio button is never what a screen means, the choice is the group. + +`autoComplete` filters the authored `options` case-insensitively. PrimeReact asks the host for suggestions +through `completeMethod` because in a real application that is a server call, and a Scene element cannot +express one. + +## Button + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `button` | `Button` | `primereact/button` | +| `splitButton` | `SplitButton` | `primereact/splitbutton` | +| `speedDial` | `SpeedDial` | `primereact/speeddial` | +| `buttonGroup` | `ButtonGroup` + `Button` | `primereact/buttongroup`, `primereact/button` | + +`button` deliberately shares its name with `core` — see +[Understanding name resolution](./understanding-name-resolution.md). + +## Data + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `dataTable` | `DataTable` + `Column` | `primereact/datatable`, `primereact/column` | +| `table` | `DataTable` + `Column` (the same adapter as `dataTable`) | `primereact/datatable`, `primereact/column` | +| `column` | `Column` | `primereact/column` | +| `dataView` | `DataView` | `primereact/dataview` | +| `tree` | `Tree` | `primereact/tree` | +| `treeTable` | `TreeTable` + `Column` | `primereact/treetable`, `primereact/column` | +| `timeline` | `Timeline` | `primereact/timeline` | +| `paginator` | `Paginator` | `primereact/paginator` | +| `orderList` | `OrderList` | `primereact/orderlist` | +| `pickList` | `PickList` | `primereact/picklist` | +| `organizationChart` | `OrganizationChart` | `primereact/organizationchart` | +| `virtualScroller` | `VirtualScroller` | `primereact/virtualscroller` | + +A table works out its columns in order of how explicitly the screen stated them: nested `column` children +first, then a `columns` property, then the keys of the first row. That last step matters more than it looks +— a table given rows and no column configuration is the most common thing an author writes first, and +inferring the columns means it renders their data instead of an empty grid. + +> [!NOTE] +> `column` renders nothing on its own. That is PrimeReact's own semantics — a bare `` outside a +> `DataTable` renders nothing either. When nested under `dataTable` or `table`, the table reads its +> `field`, `header` and `sortable` off the **model** rather than the rendered node, because PrimeReact +> identifies its columns by React element type and a Scene adapter wrapping one would not be recognized. + +`organizationChart` renders empty when no nodes are authored. PrimeReact's own `OrganizationChart` reads +the root node's `expanded` flag without checking there is a root and throws on an empty value; an element +whose data has not been authored yet is an ordinary state on a screen under construction, and taking the +whole screen down for it would be the wrong failure. + +## Panel + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `card` | `Card` | `primereact/card` | +| `panel` | `Panel` | `primereact/panel` | +| `accordion` | `Accordion` + `AccordionTab` | `primereact/accordion` | +| `fieldset` | `Fieldset` | `primereact/fieldset` | +| `divider` | `Divider` | `primereact/divider` | +| `splitter` | `Splitter` + `SplitterPanel` | `primereact/splitter` | +| `scrollPanel` | `ScrollPanel` | `primereact/scrollpanel` | +| `tabView` | `TabView` + `TabPanel` | `primereact/tabview` | +| `toolbar` | `Toolbar` | `primereact/toolbar` | +| `stepper` | `Stepper` + `StepperPanel` + `Button` | `primereact/stepper`, `primereact/stepperpanel`, `primereact/button` | + +`accordion`, `tabView`, `splitter` and `stepper` all pair a `headers` property with the `content` slot by +position — a screen puts as many children in the slot as it lists headers. This is for the same reason +`column` is read from the model: PrimeReact identifies these sections by React element type. + +`stepper` renders its own Back and Next buttons. PrimeReact's `Stepper` advances only when something calls +`nextCallback`/`prevCallback` on its ref, and a stepper that cannot step is not a stepper. + +`toolbar` takes `start`, `center` and `end` slots rather than properties, because what goes in them is other +components — buttons, a search field, a menu — not values. + +## Overlay + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `dialog` | `Dialog` + `Button` | `primereact/dialog`, `primereact/button` | +| `confirmDialog` | `ConfirmDialog` + `Button` | `primereact/confirmdialog`, `primereact/button` | +| `overlayPanel` | `OverlayPanel` + `Button` | `primereact/overlaypanel`, `primereact/button` | +| `sidebar` | `Sidebar` + `Button` | `primereact/sidebar`, `primereact/button` | +| `tooltip` | `Tooltip` | `primereact/tooltip` | + +Each of these renders its own trigger. An overlay is only interesting while it is open, and "the user +closed it" is state a Scene element has nowhere to record — so the adapter owns visibility locally and keeps +a way back. Without the trigger, dismissing a previewed dialog would leave a permanently blank spot. + +## Menu + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `menu` | `Menu` | `primereact/menu` | +| `menubar` | `Menubar` | `primereact/menubar` | +| `breadcrumb` | `BreadCrumb` | `primereact/breadcrumb` | +| `tabMenu` | `TabMenu` | `primereact/tabmenu` | +| `steps` | `Steps` | `primereact/steps` | +| `tieredMenu` | `TieredMenu` | `primereact/tieredmenu` | +| `panelMenu` | `PanelMenu` | `primereact/panelmenu` | +| `contextMenu` | `ContextMenu` | `primereact/contextmenu` | +| `megaMenu` | `MegaMenu` | `primereact/megamenu` | +| `dock` | `Dock` | `primereact/dock` | + +All ten read the same nested `{ label, icon, url, disabled, separator, items }` model to any depth, so one +authored menu can be shown as a menubar, a tiered menu or a dock without being restructured. + +## Messages + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `message` | `Message`, full width | `primereact/message` | +| `inlineMessage` | `Message`, sized to content | `primereact/message` | +| `toast` | `Toast` | `primereact/toast` | + +`message` and `inlineMessage` share one PrimeReact component but mean different things on a screen — one is +about a region, the other about the field beside it — and the width is the difference a reader sees. + +`toast` shows its message once on mount from the element's own properties. PrimeReact's `Toast` is purely +imperative and renders nothing until someone calls `show`; a Scene element cannot make that call, so the +element reads as "this screen announces this" rather than as a component that renders nothing. + +## Media + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `image` | `Image` | `primereact/image` | +| `galleria` | `Galleria` | `primereact/galleria` | +| `carousel` | `Carousel` | `primereact/carousel` | + +`galleria` and `carousel` ship no default item template, so each adapter supplies one built from the fields +the element names. Without it every item renders empty and the component looks broken rather than +unconfigured. + +## Misc + +| Name | PrimeReact 10 component | Import | +| --- | --- | --- | +| `avatar` | `Avatar` | `primereact/avatar` | +| `badge` | `Badge` | `primereact/badge` | +| `chip` | `Chip` | `primereact/chip` | +| `tag` | `Tag` | `primereact/tag` | +| `progressBar` | `ProgressBar` | `primereact/progressbar` | +| `progressSpinner` | `ProgressSpinner` | `primereact/progressspinner` | +| `skeleton` | `Skeleton` | `primereact/skeleton` | +| `scrollTop` | `ScrollTop` | `primereact/scrolltop` | +| `blockUI` | `BlockUI` | `primereact/blockui` | +| `inplace` | `Inplace` + `InplaceDisplay` + `InplaceContent` | `primereact/inplace` | +| `terminal` | `Terminal` | `primereact/terminal` | + +`progressBar` follows whether a `value` was given at all: progress you cannot measure is exactly what +indeterminate mode is for, so an element with no value animates rather than sitting at zero. + +`terminal` accepts input and answers nothing until the hosting application subscribes to PrimeReact's +`TerminalService`. Responding to a command is application behavior, not something a Scene element can +express — this is the honest shape of a terminal with no backend, not a broken one. + +## Screen + +Screenplay's screen vocabulary, plus `text`. + +| Name | Renders | PrimeReact 10 component | +| --- | --- | --- | +| `text` | a themed `` | (none — written directly) | +| `title` | a real `

`–`

` at the authored level | (none — written directly) | +| `field` | a labeled value bound by `aria-labelledby` | (none — written directly) | +| `section` | a real `
` with a heading and a rule | `Divider` | +| `summary` | a description list of label/value pairs | `Card` | +| `action` | a button whose intent maps to a severity | `Button` | + +`title` clamps its level to 1–6. Heading level is the document outline a screen reader navigates by, so it +is not a styling choice — and an out-of-range value must degrade to a valid heading, not an invalid tag. + +`action` maps intent to severity in one place: + +| `intent` | PrimeReact `severity` | +| --- | --- | +| `primary` | (default) | +| `secondary` | `secondary`, outlined | +| `destructive`, `danger` | `danger` | +| `positive`, `success` | `success` | + +## Reading properties + +Element properties arrive as untyped JSON. Every adapter narrows through the same readers, so a wrongly +typed property behaves exactly like a missing one instead of reaching PrimeReact and failing there: + +```ts +import { arrayProperty, booleanProperty, numberProperty, optionsProperty, stringProperty } from '@cratis/scene.primereact'; + +stringProperty(element, 'label'); // string | undefined +stringProperty(element, 'label', 'Save'); // string +booleanProperty(element, 'disabled', false); // boolean +numberProperty(element, 'rows', 4); // number +arrayProperty(element, 'items'); // unknown[] - never undefined +optionsProperty(element, 'options'); // SelectOption[] +``` + +`optionsProperty` accepts both shapes an author might reasonably write — `['Draft', 'Published']` and +`[{ label: 'Draft', value: 'draft' }]` — and flattens them to one type. `numberProperty` rejects `NaN` +alongside non-numbers: it is a number by `typeof` but never a usable size, count or bound. + +## Interactive state + +PrimeReact's inputs are controlled, and a Scene element has nowhere to put "what the user has typed so far" +— `properties` is authored design-time configuration. Every interactive adapter therefore holds that state +locally, seeded from its properties. The consequence is deliberate: a preview is genuinely typeable rather +than frozen, and the value stays local to the rendered component rather than being pushed back into the +model. + +## Deliberately not covered + +| Component | Why | +| --- | --- | +| `chart` | PrimeReact's `Chart` is a thin wrapper over Chart.js and does nothing without `chart.js` installed and a full Chart.js configuration object. Adding a charting library as a dependency of a component-mapping package is out of scope, and charting deserves its own Scene package with its own vocabulary. | +| `editor` | `Editor` wraps Quill and needs `quill` installed. Same reasoning — a rich-text editor is a product decision, not a mapping. | + +Both are genuinely useful, and both are omissions rather than oversights. A profile needing them should +activate a package that owns that dependency. + +## Next + +See [Theme reference](./theme-reference.md) for the themes, or +[Migrating to PrimeReact 11](./primereact-11-migration.md) for what changes when the version moves. diff --git a/Documentation/primereact-package/index.md b/Documentation/primereact-package/index.md new file mode 100644 index 0000000..8439dde --- /dev/null +++ b/Documentation/primereact-package/index.md @@ -0,0 +1,102 @@ +--- +title: PrimeReact package +description: The Scene package that maps abstract component names onto real PrimeReact components and PrimeTek's free themes onto Scene themes. +--- + +A screen written in Screenplay says `button`. It does not say `PrimeReact.Button`, and it does not import +anything. Something has to turn that name into a real React component — and that something is a package. + +`@cratis/scene.primereact` is the package that turns Scene's abstract names into PrimeReact 10 components. +Add `PrimeReact` to a `ui profile` and 87 names become resolvable, 25 themes become selectable, and every +screen you have already written renders through a real, themed component library without a single edit. + +## Without it, and with it + +Without a component library in the profile, a `ui profile` still resolves — `core` is always the final +fallback, so `button` renders as an unstyled ` + ))} + + + + + + {showsMode && ( + + )} + + {showsMenuTheme && ( +
+

Menu theme

+
+ {menuThemes.map(menuTheme => ( + + ))} +
+
+ )} + + {slots.content} + + + + ); +} diff --git a/Source/JavaScript/blueprint.default/shell/Footer.tsx b/Source/JavaScript/blueprint.default/shell/Footer.tsx new file mode 100644 index 0000000..4d66c7f --- /dev/null +++ b/Source/JavaScript/blueprint.default/shell/Footer.tsx @@ -0,0 +1,23 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { RegisteredComponentProps } from '@cratis/scene.react'; +import { readString } from './elementProperties'; + +/** + * The strip below the content. + * + * Kept as its own slot rather than folded into the content because it is application chrome, not part of + * a screen: the same footer is on every page, and a screen that had to render it would have to remember + * to. + */ +export function Footer({ element, slots }: RegisteredComponentProps) { + const text = readString(element, 'text'); + + return ( + <> + {text} + {slots.content} + + ); +} diff --git a/Source/JavaScript/blueprint.default/shell/FullPageShell.tsx b/Source/JavaScript/blueprint.default/shell/FullPageShell.tsx new file mode 100644 index 0000000..7f08da0 --- /dev/null +++ b/Source/JavaScript/blueprint.default/shell/FullPageShell.tsx @@ -0,0 +1,41 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { ReactNode } from 'react'; +import { RegisteredComponentProps } from '@cratis/scene.react'; +import { LayoutConfigProvider, useOptionalLayoutConfig } from '../configuration'; +import { SlotName } from '../layouts'; + +/** + * The chrome-less shell: content, an optional branding aside, and the configurator. + * + * Login, register, password reset, verification, lock, error, access-denied, not-found and landing screens + * all render here rather than in a stripped-down application shell. That split is structural in every + * PrimeTek template, and for a good reason: those screens have no navigation state to hold, no sidebar to + * remember, and no breadcrumb to place, so hanging them off the application shell means every one of the + * eight modes has to have an answer for a page that has no menu. + * + * The configurator stays, because a sign-in page still has to honor the chosen theme - it is very often + * the first page anyone sees. + */ +export function FullPageShell({ element, slots }: RegisteredComponentProps) { + const surface = ; + return useOptionalLayoutConfig() ? surface : {surface}; +} + +interface FullPageSurfaceProps { + elementId: string; + slots: Record; +} + +function FullPageSurface({ elementId, slots }: FullPageSurfaceProps) { + const hasAside = (slots[SlotName.Aside]?.length ?? 0) > 0; + + return ( +
+ {hasAside && } +
{slots[SlotName.Content]}
+ {slots[SlotName.ConfigPanel]} +
+ ); +} diff --git a/Source/JavaScript/blueprint.default/shell/LayoutModeSwitcher.tsx b/Source/JavaScript/blueprint.default/shell/LayoutModeSwitcher.tsx new file mode 100644 index 0000000..9aade26 --- /dev/null +++ b/Source/JavaScript/blueprint.default/shell/LayoutModeSwitcher.tsx @@ -0,0 +1,50 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { RegisteredComponentProps } from '@cratis/scene.react'; +import { LayoutMode, layoutModes, useLayoutConfig } from '../configuration'; +import { readString } from './elementProperties'; + +/** What each mode is called in the configurator - the same names the PrimeTek template line uses. */ +const modeLabels: Record = { + [LayoutMode.Static]: 'Static', + [LayoutMode.Overlay]: 'Overlay', + [LayoutMode.Slim]: 'Slim', + [LayoutMode.SlimPlus]: 'Slim+', + [LayoutMode.Compact]: 'Compact', + [LayoutMode.Horizontal]: 'Horizontal', + [LayoutMode.Reveal]: 'Reveal', + [LayoutMode.Drawer]: 'Drawer', +}; + +/** + * Switches between the layout modes. + * + * Below the mobile breakpoint every button is disabled and the panel says why, rather than the picker + * disappearing. A control that vanishes reads as a bug; a disabled control with a sentence next to it + * reads as a decision - and the choice is still recorded and still there when the window grows again. + */ +export function LayoutModeSwitcher({ element }: RegisteredComponentProps) { + const { config, setMode } = useLayoutConfig(); + const label = readString(element, 'label', 'Menu mode'); + + return ( +
+

{label}

+
+ {layoutModes.map(mode => ( + + ))} +
+ {config.isMobile &&

Below 991px every mode renders off-canvas, so the choice is kept but not applied.

} +
+ ); +} diff --git a/Source/JavaScript/blueprint.default/shell/LayoutModes.stories.tsx b/Source/JavaScript/blueprint.default/shell/LayoutModes.stories.tsx new file mode 100644 index 0000000..f0b77d5 --- /dev/null +++ b/Source/JavaScript/blueprint.default/shell/LayoutModes.stories.tsx @@ -0,0 +1,83 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from 'storybook/test'; +import { LayoutMode, MenuTheme } from '../configuration'; +import { GalleryScreenPreview } from '../gallery'; + +/** + * One story per layout mode, all showing the same dashboard. + * + * Showing the *same* screen in every mode is the point: the difference between a docked sidebar, an icon + * rail, a horizontal strip and a panel that slides in on hover is entirely in the wrapper class and the + * stylesheet, and nothing about the screen changes. A story per mode is how that claim stays true. + */ +const meta = { + title: 'Blueprint/Layout modes', + component: GalleryScreenPreview, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'The eight menu modes, each rendering the same dashboard. Below 991px every one of them is forced off-canvas - resize the preview to see it.', + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +function inMode(mode: LayoutMode, extra: Record = {}): Story['args'] { + return { screenName: 'Dashboard', initialConfig: { mode, ...extra } }; +} + +/** The sidebar is docked and the content is pushed by a matching margin. */ +export const Static: Story = { args: inMode(LayoutMode.Static) }; + +/** The sidebar is parked off-canvas; the topbar toggle floats it over the content behind the mask. */ +export const Overlay: Story = { args: inMode(LayoutMode.Overlay) }; + +/** An icon-only rail with circular buttons; submenus pop out beside it. */ +export const Slim: Story = { args: inMode(LayoutMode.Slim) }; + +/** The same rail, wider, with each label stacked under its icon. */ +export const SlimPlus: Story = { args: inMode(LayoutMode.SlimPlus) }; + +/** The icon rail again, with square buttons and a topbar shifted by the rail width. */ +export const Compact: Story = { args: inMode(LayoutMode.Compact) }; + +/** The sidebar stops being a sidebar and flows into the topbar as a row. */ +export const Horizontal: Story = { args: inMode(LayoutMode.Horizontal) }; + +/** A full panel parked off-left behind a strip of icons - hover slides it in over the content. */ +export const Reveal: Story = { args: inMode(LayoutMode.Reveal) }; + +/** A collapsed rail that grows its width on hover, and can be pinned open. */ +export const Drawer: Story = { args: inMode(LayoutMode.Drawer) }; + +/** A pinned drawer, which pushes the content out to the full sidebar width instead of covering it. */ +export const DrawerAnchored: Story = { args: inMode(LayoutMode.Drawer, { isSidebarAnchored: true, isSidebarRevealed: true }) }; + +/** A dark sidebar against a light page - the most common brand customization in the template line. */ +export const DarkMenu: Story = { args: inMode(LayoutMode.Static, { menuTheme: MenuTheme.Dark }) }; + +/** The topbar toggle opens and closes the sidebar, and the wrapper class follows it. */ +export const Interactive: Story = { + args: inMode(LayoutMode.Static), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const wrapper = canvasElement.querySelector('.layout-wrapper')!; + await expect(wrapper.className).not.toContain('layout-static-inactive'); + + await userEvent.click(canvas.getByRole('button', { name: 'Close the menu' })); + await expect(canvasElement.querySelector('.layout-wrapper')!.className).toContain('layout-static-inactive'); + + await userEvent.click(canvas.getByRole('button', { name: 'Open the menu' })); + await expect(canvasElement.querySelector('.layout-wrapper')!.className).not.toContain('layout-static-inactive'); + }, +}; diff --git a/Source/JavaScript/blueprint.default/shell/Logo.tsx b/Source/JavaScript/blueprint.default/shell/Logo.tsx new file mode 100644 index 0000000..8f3913e --- /dev/null +++ b/Source/JavaScript/blueprint.default/shell/Logo.tsx @@ -0,0 +1,30 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { RegisteredComponentProps } from '@cratis/scene.react'; +import { useSceneTheme } from '@cratis/scene.react'; +import { readOptionalString, readString } from './elementProperties'; + +/** + * The brand mark. + * + * This is the one place in the shell that reads the active theme directly rather than through a token, + * because picking between a light-background and a dark-background logo asset is a decision no CSS custom + * property can express - the two are different files. `useSceneTheme` exists for exactly this case. + */ +export function Logo({ element }: RegisteredComponentProps) { + const theme = useSceneTheme(); + const label = readString(element, 'label', 'Cratis'); + const initials = readString(element, 'initials', label.slice(0, 1).toUpperCase()); + const lightSource = readOptionalString(element, 'lightImageUrl'); + const darkSource = readOptionalString(element, 'darkImageUrl'); + const source = theme?.isDark ? darkSource ?? lightSource : lightSource ?? darkSource; + const targetScreen = readString(element, 'targetScreen', 'Dashboard'); + + return ( + + {source ? {label} : {initials}} + {label} + + ); +} diff --git a/Source/JavaScript/blueprint.default/shell/Mask.tsx b/Source/JavaScript/blueprint.default/shell/Mask.tsx new file mode 100644 index 0000000..73bb2d5 --- /dev/null +++ b/Source/JavaScript/blueprint.default/shell/Mask.tsx @@ -0,0 +1,29 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { useLayoutConfig } from '../configuration'; + +/** + * The scrim behind a floating sidebar. + * + * It is a `button` rather than a `div` because its only job is to close the sidebar, and a click target + * that is not focusable or reachable from the keyboard traps anyone not using a mouse behind an open + * overlay with no way out. It renders nothing at all when no sidebar is floating, so it never sits + * invisibly over the page swallowing clicks. + */ +export function LayoutMask() { + const { isMaskVisible, setSidebarOpen } = useLayoutConfig(); + if (!isMaskVisible) { + return undefined; + } + + return