diff --git a/Source/DotNET/Model/Common/Dock.cs b/Source/DotNET/Model/Common/Dock.cs
new file mode 100644
index 0000000..71eff84
--- /dev/null
+++ b/Source/DotNET/Model/Common/Dock.cs
@@ -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.
+
+namespace Cratis.Scene.Model.Common;
+
+///
+/// The edge of a dock panel a child is docked against.
+///
+public enum Dock
+{
+ ///
+ /// Docked against the left edge.
+ ///
+ Left = 0,
+
+ ///
+ /// Docked against the top edge.
+ ///
+ Top = 1,
+
+ ///
+ /// Docked against the right edge.
+ ///
+ Right = 2,
+
+ ///
+ /// Docked against the bottom edge.
+ ///
+ Bottom = 3
+}
diff --git a/Source/DotNET/Model/Common/GridLength.cs b/Source/DotNET/Model/Common/GridLength.cs
new file mode 100644
index 0000000..c990064
--- /dev/null
+++ b/Source/DotNET/Model/Common/GridLength.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Scene.Model.Common;
+
+///
+/// The length of a grid row or column, which can be absolute, sized to its content, or a weighted
+/// share of the space the absolute and content-sized tracks leave behind.
+///
+/// The numeric value, read according to .
+/// How is interpreted.
+public record GridLength(double Value = 1, GridUnitType UnitType = GridUnitType.Star)
+{
+ ///
+ /// A length sized to the content it holds.
+ ///
+ public static readonly GridLength Auto = new(0, GridUnitType.Auto);
+
+ ///
+ /// A single share of the leftover space.
+ ///
+ public static readonly GridLength Star = new(1, GridUnitType.Star);
+
+ ///
+ /// Create an absolute length.
+ ///
+ /// The absolute length.
+ /// A new .
+ public static GridLength Absolute(double value) => new(value, GridUnitType.Absolute);
+
+ ///
+ /// Create a weighted share of the leftover space.
+ ///
+ /// The weight, relative to the other starred tracks.
+ /// A new .
+ public static GridLength Stars(double weight) => new(weight, GridUnitType.Star);
+}
diff --git a/Source/DotNET/Model/Common/GridUnitType.cs b/Source/DotNET/Model/Common/GridUnitType.cs
new file mode 100644
index 0000000..00fa96b
--- /dev/null
+++ b/Source/DotNET/Model/Common/GridUnitType.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Scene.Model.Common;
+
+///
+/// How a 's value is interpreted.
+///
+public enum GridUnitType
+{
+ ///
+ /// The length is decided by the content it holds.
+ ///
+ Auto = 0,
+
+ ///
+ /// The length is an absolute measurement.
+ ///
+ Absolute = 1,
+
+ ///
+ /// The length is a weighted share of whatever space is left over.
+ ///
+ Star = 2
+}
diff --git a/Source/DotNET/Model/Common/Orientation.cs b/Source/DotNET/Model/Common/Orientation.cs
new file mode 100644
index 0000000..dd1ab23
--- /dev/null
+++ b/Source/DotNET/Model/Common/Orientation.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Scene.Model.Common;
+
+///
+/// The direction an element lays its children out in.
+///
+public enum Orientation
+{
+ ///
+ /// Children are laid out left to right.
+ ///
+ Horizontal = 0,
+
+ ///
+ /// Children are laid out top to bottom.
+ ///
+ Vertical = 1
+}
diff --git a/Source/DotNET/Model/Elements/Panels/ColumnDefinition.cs b/Source/DotNET/Model/Elements/Panels/ColumnDefinition.cs
new file mode 100644
index 0000000..3330082
--- /dev/null
+++ b/Source/DotNET/Model/Elements/Panels/ColumnDefinition.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Scene.Model.Common;
+
+namespace Cratis.Scene.Model.Elements.Panels;
+
+///
+/// One column of a .
+///
+public record ColumnDefinition
+{
+ ///
+ /// Gets how wide the column is.
+ ///
+ public GridLength Width { get; init; } = GridLength.Star;
+
+ ///
+ /// Gets the width the column never goes below.
+ ///
+ public double MinimumWidth { get; init; }
+
+ ///
+ /// Gets the width the column never goes above.
+ ///
+ public double MaximumWidth { get; init; } = double.PositiveInfinity;
+}
diff --git a/Source/DotNET/Model/Elements/Panels/DockPanel.cs b/Source/DotNET/Model/Elements/Panels/DockPanel.cs
new file mode 100644
index 0000000..3e3b412
--- /dev/null
+++ b/Source/DotNET/Model/Elements/Panels/DockPanel.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Scene.Model.Elements.Panels;
+
+///
+/// Lays its children out against its edges, each child stating which edge through the Dock key
+/// of its .
+///
+public record DockPanel : Panel
+{
+ ///
+ /// Gets whether the last child spreads into whatever space the docked children left behind.
+ ///
+ public bool LastChildFill { get; init; } = true;
+}
diff --git a/Source/DotNET/Model/Elements/Panels/Grid.cs b/Source/DotNET/Model/Elements/Panels/Grid.cs
new file mode 100644
index 0000000..de07f9b
--- /dev/null
+++ b/Source/DotNET/Model/Elements/Panels/Grid.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Scene.Model.Elements.Panels;
+
+// There is deliberately no Canvas panel here. Absolute placement is already the layout model's job -
+// FreeformArrangement carries an ElementPlacement (x, y, width, height) per element *per size class*,
+// which says strictly more than a canvas with attached Left/Top coordinates can. A Canvas panel would
+// also declare no properties of its own, and every element kind in this model is told apart by a
+// property no other kind has - so it could not be recognized at render time either.
+
+///
+/// Lays its children out in rows and columns. A child states which cell it occupies through the
+/// Grid.Row, Grid.Column, Grid.RowSpan and Grid.ColumnSpan keys of its
+/// .
+///
+public record Grid : Panel
+{
+ ///
+ /// Gets the rows, top to bottom.
+ ///
+ public IReadOnlyList Rows { get; init; } = [];
+
+ ///
+ /// Gets the columns, left to right.
+ ///
+ public IReadOnlyList Columns { get; init; } = [];
+}
diff --git a/Source/DotNET/Model/Elements/Panels/RowDefinition.cs b/Source/DotNET/Model/Elements/Panels/RowDefinition.cs
new file mode 100644
index 0000000..e2d8c5e
--- /dev/null
+++ b/Source/DotNET/Model/Elements/Panels/RowDefinition.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Scene.Model.Common;
+
+namespace Cratis.Scene.Model.Elements.Panels;
+
+///
+/// One row of a .
+///
+public record RowDefinition
+{
+ ///
+ /// Gets how tall the row is.
+ ///
+ public GridLength Height { get; init; } = GridLength.Star;
+
+ ///
+ /// Gets the height the row never goes below.
+ ///
+ public double MinimumHeight { get; init; }
+
+ ///
+ /// Gets the height the row never goes above.
+ ///
+ public double MaximumHeight { get; init; } = double.PositiveInfinity;
+}
diff --git a/Source/DotNET/Model/Elements/Panels/StackPanel.cs b/Source/DotNET/Model/Elements/Panels/StackPanel.cs
new file mode 100644
index 0000000..839dbf8
--- /dev/null
+++ b/Source/DotNET/Model/Elements/Panels/StackPanel.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Scene.Model.Common;
+
+namespace Cratis.Scene.Model.Elements.Panels;
+
+///
+/// Lays its children out in a single line.
+///
+public record StackPanel : Panel
+{
+ ///
+ /// Gets the direction the line runs in.
+ ///
+ public Orientation Orientation { get; init; } = Orientation.Vertical;
+
+ ///
+ /// Gets the space left between one child and the next.
+ ///
+ public double Spacing { get; init; }
+}
diff --git a/Source/DotNET/Model/Elements/Panels/WrapPanel.cs b/Source/DotNET/Model/Elements/Panels/WrapPanel.cs
new file mode 100644
index 0000000..7380a4d
--- /dev/null
+++ b/Source/DotNET/Model/Elements/Panels/WrapPanel.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Scene.Model.Common;
+
+namespace Cratis.Scene.Model.Elements.Panels;
+
+///
+/// Lays its children out in a line and starts a new one whenever the current line runs out of room.
+///
+public record WrapPanel : Panel
+{
+ ///
+ /// Gets the direction each line runs in.
+ ///
+ public Orientation Orientation { get; init; } = Orientation.Horizontal;
+
+ ///
+ /// Gets the width every child is laid out at, or to let each child keep its own.
+ ///
+ public double? ItemWidth { get; init; }
+
+ ///
+ /// Gets the height every child is laid out at, or to let each child keep its own.
+ ///
+ public double? ItemHeight { get; init; }
+}
diff --git a/Source/JavaScript/blueprint.components/.storybook/preview.tsx b/Source/JavaScript/blueprint.components/.storybook/preview.tsx
index e3823a3..132e979 100644
--- a/Source/JavaScript/blueprint.components/.storybook/preview.tsx
+++ b/Source/JavaScript/blueprint.components/.storybook/preview.tsx
@@ -2,20 +2,16 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
import type { Preview } from '@storybook/react';
+import { PrimeReactProvider } from '@primereact/core';
import { CratisComponentsProvider } from '@cratis/components/Common';
-// The stylesheet layers a page in this blueprint needs, in the order they resolve. PrimeReact 11 ships
-// zero CSS - `primereact/resources/themes/*` does not exist any more - and `@cratis/components` 3.0.0 took
-// its own CSS out of the JavaScript module graph, so every sheet below is an explicit import rather than
-// something a bundler injects behind an `import './Foo.css'`.
+// The stylesheet layers a page in this blueprint needs, in the order they resolve. Components 4 owns its
+// own CSS outright and keeps it out of the JavaScript module graph, so every sheet below is an explicit
+// import rather than something a bundler injects behind an `import './Foo.css'`.
//
// `tokens` defines the `--cratis-*` layer, `styles` is every component stylesheet plus the Tailwind
// utilities that consume it, and `theme` is the Cratis-authored MIT baseline that assigns those tokens
-// actual values. That order matters: the last two both read the first. `theme` is what gives this preview
-// a look at all, because with no `@primeuix/themes` preset there are no `--p-*` values for the tokens to
-// resolve to - which also means the raw PrimeReact widgets the default blueprint's shell renders come out
-// structural rather than styled. That is the honest picture of an unstyled-first host, and this package
-// takes no dependency on a preset to paper over it.
+// actual values. That order matters: the last two both read the first.
//
// The Scene package's bridge puts Scene's `--scene-*` tokens in front of that, and the default blueprint's
// `layout.css` draws the shell every one of these pages sits inside - which is the sheet this package does
@@ -31,24 +27,31 @@ import 'primeicons/primeicons.css';
import '../../components/theme/sceneTokenBridge.css';
import '../../blueprint.default/shell/layout.css';
+// The baseline theme is scoped to a `cratis-theme` ancestor rather than to `:root`, so something has to
+// carry the class, and overlays portal to the body rather than into the story's subtree.
+document.body.classList.add('cratis-theme');
+
/**
- * Every story renders inside `CratisComponentsProvider`, the library's own configuration provider over
- * PrimeReact's. Without it the wrapped components fall back to PrimeReact's defaults rather than Cratis',
- * so a story would show something subtly different from what an application renders - which defeats the
- * point of having stories at all.
+ * Two providers, because these pages are two things at once.
+ *
+ * The page bodies are Components 4, which owns its own markup and configuration and no longer sits on
+ * PrimeReact at all - `CratisComponentsProvider` now carries only what the library itself owns.
*
- * On PrimeReact 11 it is also load-bearing rather than merely advisable: every v11 component resolves its
- * configuration, theme and z-index registry through `PrimeReactProvider`, which this wraps, and throws
- * outright without one. These pages sit inside the default blueprint's shell, which reaches for PrimeReact
- * directly in five places, so removing this decorator would not degrade the stories - it would stop them
- * rendering.
+ * The shell they sit inside is the default blueprint's, and that reaches for PrimeReact directly in five
+ * places (`Topbar`, `Sidebar`, `Breadcrumb`, `UserMenu`, `ConfigPanel`). Every PrimeReact 11 component
+ * resolves its configuration, theme and z-index registry through `PrimeReactProvider` and throws without
+ * one. Until Components 3 that provider came for free, because `CratisComponentsProvider` wrapped it;
+ * under Components 4 it does not, so this preview supplies it explicitly. Dropping it would not degrade
+ * these stories - it would stop them rendering.
*/
const preview: Preview = {
decorators: [
Story => (
-
-
-
+
+
+
+
+
),
],
parameters: {
diff --git a/Source/JavaScript/blueprint.components/package.json b/Source/JavaScript/blueprint.components/package.json
index 95a5ce8..c7fa268 100644
--- a/Source/JavaScript/blueprint.components/package.json
+++ b/Source/JavaScript/blueprint.components/package.json
@@ -50,7 +50,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
- "@cratis/components": "^3.0.0",
+ "@cratis/components": "^4.1.1",
"@cratis/scene.blueprint.default": "1.0.0",
"@cratis/scene.components": "1.0.0",
"@cratis/scene.engine": "1.0.0",
@@ -58,7 +58,7 @@
"@cratis/scene.react": "1.0.0"
},
"devDependencies": {
- "@cratis/components": "^3.0.0",
+ "@cratis/components": "^4.1.1",
"@cratis/scene.blueprint.default": "1.0.0",
"@cratis/scene.components": "1.0.0",
"@cratis/scene.engine": "1.0.0",
@@ -76,7 +76,7 @@
"storybook": "^10.4.1"
},
"peerDependencies": {
- "@cratis/components": "^3.0.0",
+ "@cratis/components": "^4.1.1",
"@primereact/core": "^11.0.0",
"@primereact/headless": "^11.0.0",
"primeicons": "^8.0.0",
diff --git a/Source/JavaScript/components/.storybook/preview.tsx b/Source/JavaScript/components/.storybook/preview.tsx
index 4d47ccf..833714d 100644
--- a/Source/JavaScript/components/.storybook/preview.tsx
+++ b/Source/JavaScript/components/.storybook/preview.tsx
@@ -6,11 +6,9 @@ import { CratisComponentsProvider } from '@cratis/components/Common';
// The stylesheet stack a `@cratis/components` component needs, in the order the layers resolve.
//
-// PrimeReact 11 ships no CSS whatsoever - `primereact/resources/` does not exist, so the compiled theme
-// this file used to import has nothing left to resolve to. A look is no longer a stylesheet at all: it is
-// either a `@primeuix/themes` preset handed to the provider, which `@primeuix/styled` turns into `--p-*`
-// custom properties at runtime, or a sheet that assigns the library's own `--cratis-*` tokens directly.
-// This preview takes the second path - see the decorator below for why.
+// Components 4 owns its own markup and styling outright - there is no PrimeReact underneath it any more,
+// and so no preset and no `--p-*` values behind the tokens. A look is one thing now: a sheet that assigns
+// the library's own `--cratis-*` tokens.
//
// `tokens` declares the `--cratis-*` layer every component stylesheet reads, `styles` is that component
// CSS plus the compiled Tailwind utilities and the vendored Allotment sheet `DataPage`'s split view needs,
@@ -20,35 +18,33 @@ import { CratisComponentsProvider } from '@cratis/components/Common';
import '@cratis/components/tokens';
import '@cratis/components/styles';
import '@cratis/components/theme';
+// Components 4 renders an `Icon` given a string as `` - a consumer-owned icon font,
+// whichever one the consumer happens to load. These stories demonstrate that with `pi pi-*`, so the
+// preview loads primeicons to make them visible. It is a Storybook devDependency for exactly this reason
+// and nothing in the package's own source reaches for it.
import 'primeicons/primeicons.css';
import '../theme/sceneTokenBridge.css';
// The baseline theme is scoped to a `cratis-theme` ancestor rather than to `:root`, so something has to
-// carry the class. `` rather than a wrapper inside each story, because PrimeReact 11 portals its
-// overlays - dialogs, select panels, the filter panel - straight to `document.body`; a wrapper would
-// leave every one of them outside the themed subtree and rendering unstyled.
+// carry the class. `` rather than a wrapper inside each story, because overlays - dialogs, select
+// panels, the filter panel - are portaled to `document.body`; a wrapper would leave every one of them
+// outside the themed subtree and rendering unstyled.
document.body.classList.add('cratis-theme');
/**
- * Every story renders inside `CratisComponentsProvider`, the library's own configuration provider over
- * PrimeReact's. Without it the wrapped components fall back to PrimeReact's defaults rather than
- * Cratis', so a story would be showing something subtly different from what an application renders -
- * which defeats the point of having stories at all.
+ * Every story renders inside `CratisComponentsProvider`, so a story shows what an application shows.
*
- * `unstyled` is the posture the imported `@cratis/components/theme` is written for: it is Cratis-authored
- * MIT CSS that assigns the `--cratis-*` tokens outright, so it needs neither a `@primeuix/themes` preset
- * nor the PrimeUI license key a styled preset is gated behind. Choosing it over a preset also keeps this
- * package's dependency surface honest - `@primeuix/themes` is not one of its dependencies, and a preview
- * is the wrong place to start relying on a package nobody declared.
- *
- * It is the right choice for these stories on its own terms too. The `--cratis-*` token layer is exactly
- * what `theme/sceneTokenBridge.css` overrides, so a Scene theme visibly takes over from the baseline in
- * the `Themed` story rather than from a preset's `--p-*` values one level further down the chain.
+ * Components 4 narrowed the provider to configuration the library itself owns - `locale` and `messages`.
+ * The renderer keys it used to carry (`license`, `theme`, `defaults`, `pt`, `ripple`, `unstyled`) existed
+ * to configure PrimeReact underneath, and there is no PrimeReact underneath any more. `unstyled` in
+ * particular is gone rather than defaulted: the imported `@cratis/components/theme` is the look now, and
+ * `theme/sceneTokenBridge.css` overrides its `--cratis-*` tokens directly, which is what lets the `Themed`
+ * story visibly take over from the baseline.
*/
const preview: Preview = {
decorators: [
Story => (
-
+
),
diff --git a/Source/JavaScript/components/common/SceneTooltip.tsx b/Source/JavaScript/components/common/SceneTooltip.tsx
index 12fef10..2c6fa79 100644
--- a/Source/JavaScript/components/common/SceneTooltip.tsx
+++ b/Source/JavaScript/components/common/SceneTooltip.tsx
@@ -22,7 +22,11 @@ export function SceneTooltip({ element, slots }: RegisteredComponentProps) {
position={unionProperty(element.properties, 'position', positions)}
disabled={booleanProperty(element.properties, 'disabled')}
>
- {slots.content}
+ {/* Components 4 takes a single focusable trigger, which it clones to attach its own
+ class and part attributes, and a Scene slot is a list. The span is that one element:
+ without it a slot holding anything other than exactly one node fails to type, and a
+ slot is authored content we cannot assume the shape of. */}
+ {slots.content}
);
}
diff --git a/Source/JavaScript/components/editors/for_SceneObjectNavigationalBar/when_navigating_back_up_the_trail.tsx b/Source/JavaScript/components/editors/for_SceneObjectNavigationalBar/when_navigating_back_up_the_trail.tsx
index efcf41f..230c86a 100644
--- a/Source/JavaScript/components/editors/for_SceneObjectNavigationalBar/when_navigating_back_up_the_trail.tsx
+++ b/Source/JavaScript/components/editors/for_SceneObjectNavigationalBar/when_navigating_back_up_the_trail.tsx
@@ -2,25 +2,19 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
import { fireEvent, render, screen } from '@testing-library/react';
-import { PrimeReactProvider } from '@primereact/core';
import { externalComponent } from '../../given';
import { SceneObjectNavigationalBar } from '../SceneObjectNavigationalBar';
describe('when navigating back up the trail', () => {
/**
- * The `PrimeReactProvider` became mandatory with PrimeReact 11: every v11 component bottoms out in
- * `usePrimeReact()`, which throws when the context is absent rather than falling back to a default
- * configuration. The bar reaches one through the tooltip on its crumbs, so without this the spec fails
- * at `render` with a provider error instead of exercising the trail at all. No preset is supplied -
- * what is specified here is the navigation behavior, not how a crumb looks.
+ * No provider wrapper. Components 3 needed `PrimeReactProvider` here because the tooltip on each crumb
+ * bottomed out in a PrimeReact component that threw without one; Components 4 owns its own tooltip and
+ * this package no longer depends on PrimeReact at all. What is specified here is the navigation
+ * behavior, not how a crumb looks.
*/
beforeEach(() => {
const element = externalComponent('Cratis.Components:objectNavigationalBar', { navigationPath: ['shipping', 'address', 'street'] });
- render(
-
-
-
- );
+ render();
fireEvent.click(screen.getByText('address'));
});
diff --git a/Source/JavaScript/components/package.json b/Source/JavaScript/components/package.json
index fd8f7bf..83928f5 100644
--- a/Source/JavaScript/components/package.json
+++ b/Source/JavaScript/components/package.json
@@ -58,27 +58,20 @@
"@cratis/scene.react": "1.0.0"
},
"devDependencies": {
- "@cratis/components": "^3.0.0",
+ "@cratis/components": "^4.1.1",
"@cratis/scene.engine": "1.0.0",
"@cratis/scene.model": "1.0.0",
"@cratis/scene.react": "1.0.0",
- "@primereact/core": "11.1.0",
- "@primereact/headless": "11.1.0",
"@storybook/addon-links": "^10.4.1",
"@storybook/react": "^10.4.1",
"@storybook/react-vite": "^10.4.1",
"primeicons": "8.0.0",
- "primereact": "11.1.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"storybook": "^10.4.1"
},
"peerDependencies": {
- "@cratis/components": "^3.0.0",
- "@primereact/core": "^11.0.0",
- "@primereact/headless": "^11.0.0",
- "primeicons": "^8.0.0",
- "primereact": "^11.0.0",
+ "@cratis/components": "^4.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
diff --git a/Source/JavaScript/engine/for_panelKind/when_telling_the_panels_apart.ts b/Source/JavaScript/engine/for_panelKind/when_telling_the_panels_apart.ts
new file mode 100644
index 0000000..81cac86
--- /dev/null
+++ b/Source/JavaScript/engine/for_panelKind/when_telling_the_panels_apart.ts
@@ -0,0 +1,42 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { DockPanel, Grid, HorizontalAlignment, Orientation, Panel, StackPanel, VerticalAlignment, Visibility, WrapPanel } from '@cratis/scene.model';
+import { isDockPanel, isGrid, isStackPanel, isWrapPanel } from '../index';
+
+const base = {
+ id: 'panel',
+ properties: {},
+ visibility: Visibility.Visible,
+ isEnabled: true,
+ opacity: 1,
+ zIndex: 0,
+ size: {},
+ name: 'panel',
+ minimumSize: {},
+ maximumSize: {},
+ margin: { left: 0, top: 0, right: 0, bottom: 0 },
+ horizontalAlignment: HorizontalAlignment.Stretch,
+ verticalAlignment: VerticalAlignment.Stretch,
+ children: [],
+};
+
+const grid: Grid = { ...base, rows: [], columns: [] };
+const dock: DockPanel = { ...base, lastChildFill: true };
+const stack: StackPanel = { ...base, orientation: Orientation.Vertical, spacing: 0 };
+const wrap: WrapPanel = { ...base, orientation: Orientation.Horizontal };
+const plain: Panel = { ...base };
+
+describe('when telling the panels apart', () => {
+ it('should recognize a grid by its tracks', () => isGrid(grid).should.be.true);
+ it('should recognize a dock panel by its fill flag', () => isDockPanel(dock).should.be.true);
+ it('should recognize a stack panel by its spacing', () => isStackPanel(stack).should.be.true);
+ it('should recognize a wrap panel by an orientation with no spacing', () => isWrapPanel(wrap).should.be.true);
+
+ it('should not mistake a stack panel for a wrap panel', () => isWrapPanel(stack).should.be.false);
+ it('should not mistake a wrap panel for a stack panel', () => isStackPanel(wrap).should.be.false);
+ it('should not mistake a grid for a dock panel', () => isDockPanel(grid).should.be.false);
+
+ it('should leave a plain panel unclaimed by every guard', () =>
+ [isGrid(plain), isDockPanel(plain), isStackPanel(plain), isWrapPanel(plain)].should.have.members([false, false, false, false]));
+});
diff --git a/Source/JavaScript/engine/index.ts b/Source/JavaScript/engine/index.ts
index 323fb15..fe37e50 100644
--- a/Source/JavaScript/engine/index.ts
+++ b/Source/JavaScript/engine/index.ts
@@ -5,6 +5,7 @@ export * from './Renderer';
export * from './BindingResolver';
export * from './renderElement';
export * from './elementKind';
+export * from './panelKind';
export * from './flowNodeKind';
export * from './ComponentResolution';
export * from './resolveComponentName';
diff --git a/Source/JavaScript/engine/panelKind.ts b/Source/JavaScript/engine/panelKind.ts
new file mode 100644
index 0000000..a8dd278
--- /dev/null
+++ b/Source/JavaScript/engine/panelKind.ts
@@ -0,0 +1,33 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { DockPanel, Grid, Panel, StackPanel, WrapPanel } from '@cratis/scene.model';
+
+/**
+ * Type guards telling the concrete panels apart, by the property each one alone declares - the same way
+ * {@link isPanel} and its siblings tell the element kinds apart. The model carries no discriminator, so a
+ * panel is recognized by its own shape rather than by a tag it was serialized with.
+ *
+ * A plain {@link Panel} matches none of these. That is a real case rather than an oversight: a panel that
+ * says nothing about how it arranges its children is a grouping, and a renderer is free to lay it out in
+ * whatever way its platform considers neutral.
+ */
+
+export function isGrid(panel: Panel): panel is Grid {
+ return 'rows' in panel && 'columns' in panel;
+}
+
+export function isDockPanel(panel: Panel): panel is DockPanel {
+ return 'lastChildFill' in panel;
+}
+
+export function isStackPanel(panel: Panel): panel is StackPanel {
+ return 'spacing' in panel && 'orientation' in panel;
+}
+
+export function isWrapPanel(panel: Panel): panel is WrapPanel {
+ // A wrap panel and a stack panel both orient their line; only the stack panel spaces its children,
+ // and only the wrap panel sizes them. Checking for the absence of `spacing` is what separates a wrap
+ // panel that happens to size neither of its axes from a stack panel.
+ return 'orientation' in panel && !('spacing' in panel);
+}
diff --git a/Source/JavaScript/model/common/Dock.ts b/Source/JavaScript/model/common/Dock.ts
new file mode 100644
index 0000000..1ba25f3
--- /dev/null
+++ b/Source/JavaScript/model/common/Dock.ts
@@ -0,0 +1,12 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+/**
+ * The edge of a dock panel a child is docked against.
+ */
+export enum Dock {
+ Left = 'Left',
+ Top = 'Top',
+ Right = 'Right',
+ Bottom = 'Bottom',
+}
diff --git a/Source/JavaScript/model/common/GridLength.ts b/Source/JavaScript/model/common/GridLength.ts
new file mode 100644
index 0000000..792d5d6
--- /dev/null
+++ b/Source/JavaScript/model/common/GridLength.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { GridUnitType } from './GridUnitType';
+
+/**
+ * The length of a grid row or column, which can be absolute, sized to its content, or a weighted
+ * share of the space the absolute and content-sized tracks leave behind.
+ */
+export interface GridLength {
+ value: number;
+ unitType: GridUnitType;
+}
+
+export const GridLengthPropertyNames: (keyof GridLength)[] = ['value', 'unitType'];
diff --git a/Source/JavaScript/model/common/GridUnitType.ts b/Source/JavaScript/model/common/GridUnitType.ts
new file mode 100644
index 0000000..7eab28d
--- /dev/null
+++ b/Source/JavaScript/model/common/GridUnitType.ts
@@ -0,0 +1,11 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+/**
+ * How a {@link GridLength}'s value is interpreted.
+ */
+export enum GridUnitType {
+ Auto = 'Auto',
+ Absolute = 'Absolute',
+ Star = 'Star',
+}
diff --git a/Source/JavaScript/model/common/Orientation.ts b/Source/JavaScript/model/common/Orientation.ts
new file mode 100644
index 0000000..822d325
--- /dev/null
+++ b/Source/JavaScript/model/common/Orientation.ts
@@ -0,0 +1,10 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+/**
+ * The direction an element lays its children out in.
+ */
+export enum Orientation {
+ Horizontal = 'Horizontal',
+ Vertical = 'Vertical',
+}
diff --git a/Source/JavaScript/model/common/index.ts b/Source/JavaScript/model/common/index.ts
index 467f235..46711b0 100644
--- a/Source/JavaScript/model/common/index.ts
+++ b/Source/JavaScript/model/common/index.ts
@@ -8,3 +8,7 @@ export * from './HorizontalAlignment';
export * from './VerticalAlignment';
export * from './Visibility';
export * from './BindingExpression';
+export * from './Orientation';
+export * from './Dock';
+export * from './GridUnitType';
+export * from './GridLength';
diff --git a/Source/JavaScript/model/elements/index.ts b/Source/JavaScript/model/elements/index.ts
index 3ba3293..5016594 100644
--- a/Source/JavaScript/model/elements/index.ts
+++ b/Source/JavaScript/model/elements/index.ts
@@ -9,3 +9,4 @@ export * from './Panel';
export * from './ItemsControl';
export * from './ContentControl';
export * from './ExternalComponent';
+export * from './panels';
diff --git a/Source/JavaScript/model/elements/panels/ColumnDefinition.ts b/Source/JavaScript/model/elements/panels/ColumnDefinition.ts
new file mode 100644
index 0000000..63c3ebe
--- /dev/null
+++ b/Source/JavaScript/model/elements/panels/ColumnDefinition.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { GridLength } from '../../common';
+
+/**
+ * One column of a {@link Grid}.
+ */
+export interface ColumnDefinition {
+ width: GridLength;
+ minimumWidth: number;
+ maximumWidth: number;
+}
+
+export const ColumnDefinitionPropertyNames: (keyof ColumnDefinition)[] = ['width', 'minimumWidth', 'maximumWidth'];
diff --git a/Source/JavaScript/model/elements/panels/DockPanel.ts b/Source/JavaScript/model/elements/panels/DockPanel.ts
new file mode 100644
index 0000000..6502016
--- /dev/null
+++ b/Source/JavaScript/model/elements/panels/DockPanel.ts
@@ -0,0 +1,14 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { Panel } from '../Panel';
+
+/**
+ * Lays its children out against its edges, each child stating which edge through the `Dock` key
+ * of its `properties`.
+ */
+export interface DockPanel extends Panel {
+ lastChildFill: boolean;
+}
+
+export const DockPanelPropertyNames: (keyof DockPanel)[] = ['lastChildFill'];
diff --git a/Source/JavaScript/model/elements/panels/Grid.ts b/Source/JavaScript/model/elements/panels/Grid.ts
new file mode 100644
index 0000000..70d3f71
--- /dev/null
+++ b/Source/JavaScript/model/elements/panels/Grid.ts
@@ -0,0 +1,17 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { Panel } from '../Panel';
+import { RowDefinition } from './RowDefinition';
+import { ColumnDefinition } from './ColumnDefinition';
+
+/**
+ * Lays its children out in rows and columns. A child states which cell it occupies through the
+ * `Grid.Row`, `Grid.Column`, `Grid.RowSpan` and `Grid.ColumnSpan` keys of its `properties`.
+ */
+export interface Grid extends Panel {
+ rows: RowDefinition[];
+ columns: ColumnDefinition[];
+}
+
+export const GridPropertyNames: (keyof Grid)[] = ['rows', 'columns'];
diff --git a/Source/JavaScript/model/elements/panels/RowDefinition.ts b/Source/JavaScript/model/elements/panels/RowDefinition.ts
new file mode 100644
index 0000000..5aa1a40
--- /dev/null
+++ b/Source/JavaScript/model/elements/panels/RowDefinition.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { GridLength } from '../../common';
+
+/**
+ * One row of a {@link Grid}.
+ */
+export interface RowDefinition {
+ height: GridLength;
+ minimumHeight: number;
+ maximumHeight: number;
+}
+
+export const RowDefinitionPropertyNames: (keyof RowDefinition)[] = ['height', 'minimumHeight', 'maximumHeight'];
diff --git a/Source/JavaScript/model/elements/panels/StackPanel.ts b/Source/JavaScript/model/elements/panels/StackPanel.ts
new file mode 100644
index 0000000..5b59e79
--- /dev/null
+++ b/Source/JavaScript/model/elements/panels/StackPanel.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { Panel } from '../Panel';
+import { Orientation } from '../../common';
+
+/**
+ * Lays its children out in a single line.
+ */
+export interface StackPanel extends Panel {
+ orientation: Orientation;
+ spacing: number;
+}
+
+export const StackPanelPropertyNames: (keyof StackPanel)[] = ['orientation', 'spacing'];
diff --git a/Source/JavaScript/model/elements/panels/WrapPanel.ts b/Source/JavaScript/model/elements/panels/WrapPanel.ts
new file mode 100644
index 0000000..7ed3503
--- /dev/null
+++ b/Source/JavaScript/model/elements/panels/WrapPanel.ts
@@ -0,0 +1,16 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { Panel } from '../Panel';
+import { Orientation } from '../../common';
+
+/**
+ * Lays its children out in a line and starts a new one whenever the current line runs out of room.
+ */
+export interface WrapPanel extends Panel {
+ orientation: Orientation;
+ itemWidth?: number;
+ itemHeight?: number;
+}
+
+export const WrapPanelPropertyNames: (keyof WrapPanel)[] = ['orientation', 'itemWidth', 'itemHeight'];
diff --git a/Source/JavaScript/model/elements/panels/index.ts b/Source/JavaScript/model/elements/panels/index.ts
new file mode 100644
index 0000000..a5a6f81
--- /dev/null
+++ b/Source/JavaScript/model/elements/panels/index.ts
@@ -0,0 +1,9 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+export * from './StackPanel';
+export * from './DockPanel';
+export * from './WrapPanel';
+export * from './RowDefinition';
+export * from './ColumnDefinition';
+export * from './Grid';
diff --git a/Source/JavaScript/react/for_panelLayout/when_arranging_a_panel.ts b/Source/JavaScript/react/for_panelLayout/when_arranging_a_panel.ts
new file mode 100644
index 0000000..09f4d29
--- /dev/null
+++ b/Source/JavaScript/react/for_panelLayout/when_arranging_a_panel.ts
@@ -0,0 +1,112 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { ColumnDefinition, Dock, DockPanel, Grid, GridUnitType, HorizontalAlignment, Orientation, RowDefinition, SceneElement, StackPanel, VerticalAlignment, Visibility, WrapPanel } from '@cratis/scene.model';
+import { childStyle, panelStyle } from '../renderer/panelLayout';
+
+const base = {
+ id: 'panel',
+ properties: {},
+ visibility: Visibility.Visible,
+ isEnabled: true,
+ opacity: 1,
+ zIndex: 0,
+ size: {},
+ name: 'panel',
+ minimumSize: {},
+ maximumSize: {},
+ margin: { left: 0, top: 0, right: 0, bottom: 0 },
+ horizontalAlignment: HorizontalAlignment.Stretch,
+ verticalAlignment: VerticalAlignment.Stretch,
+ children: [] as SceneElement[],
+};
+
+const child = (properties: Record = {}): SceneElement => ({ id: 'child', properties });
+
+const star = (value: number): RowDefinition['height'] => ({ value, unitType: GridUnitType.Star });
+const row = (over: Partial = {}): RowDefinition => ({ height: star(1), minimumHeight: 0, maximumHeight: Number.POSITIVE_INFINITY, ...over });
+const column = (over: Partial = {}): ColumnDefinition => ({ width: star(1), minimumWidth: 0, maximumWidth: Number.POSITIVE_INFINITY, ...over });
+
+describe('when arranging a panel', () => {
+ it('should lay a stack panel out along its orientation', () => {
+ const panel: StackPanel = { ...base, orientation: Orientation.Horizontal, spacing: 8 };
+ panelStyle(panel)!.flexDirection!.should.equal('row');
+ });
+
+ it('should space a stack panel by its spacing', () => {
+ const panel: StackPanel = { ...base, orientation: Orientation.Vertical, spacing: 12 };
+ panelStyle(panel)!.gap!.should.equal('12px');
+ });
+
+ it('should wrap a wrap panel', () => {
+ const panel: WrapPanel = { ...base, orientation: Orientation.Horizontal };
+ panelStyle(panel)!.flexWrap!.should.equal('wrap');
+ });
+
+ it('should turn star tracks into fractions', () => {
+ const panel: Grid = { ...base, rows: [row(), row({ height: star(2) })], columns: [] };
+ panelStyle(panel)!.gridTemplateRows!.should.equal('1fr 2fr');
+ });
+
+ it('should turn an absolute track into pixels', () => {
+ const panel: Grid = { ...base, rows: [], columns: [column({ width: { value: 240, unitType: GridUnitType.Absolute } })] };
+ panelStyle(panel)!.gridTemplateColumns!.should.equal('240px');
+ });
+
+ it('should turn a content-sized track into auto', () => {
+ const panel: Grid = { ...base, rows: [], columns: [column({ width: { value: 0, unitType: GridUnitType.Auto } })] };
+ panelStyle(panel)!.gridTemplateColumns!.should.equal('auto');
+ });
+
+ it('should bound a track that states a minimum', () => {
+ const panel: Grid = { ...base, rows: [row({ minimumHeight: 40 })], columns: [] };
+ panelStyle(panel)!.gridTemplateRows!.should.equal('minmax(40px, 1fr)');
+ });
+
+ it('should leave a plain panel unstyled', () => (panelStyle({ ...base }) === undefined).should.be.true);
+});
+
+describe('when placing a child in a panel', () => {
+ it('should place a grid child in the cell it names, one-based for CSS', () => {
+ const panel: Grid = { ...base, rows: [row()], columns: [column()] };
+ childStyle(panel, child({ 'Grid.Row': 1, 'Grid.Column': 2 }), 0)!.gridRow!.should.equal('2 / span 1');
+ });
+
+ it('should span a grid child that asks for it', () => {
+ const panel: Grid = { ...base, rows: [row()], columns: [column()] };
+ childStyle(panel, child({ 'Grid.Column': 0, 'Grid.ColumnSpan': 3 }), 0)!.gridColumn!.should.equal('1 / span 3');
+ });
+
+ it('should leave a grid child that names no cell to the implicit flow', () => {
+ const panel: Grid = { ...base, rows: [row()], columns: [column()] };
+ (childStyle(panel, child(), 0) === undefined).should.be.true;
+ });
+
+ it('should let a dock panel last child fill what is left', () => {
+ const kid = child();
+ const panel: DockPanel = { ...base, lastChildFill: true, children: [kid] };
+ childStyle(panel, kid, 0)!.flex!.should.equal('1 1 auto');
+ });
+
+ it('should keep a docked child at its natural size', () => {
+ const first = child({ Dock: Dock.Top });
+ const panel: DockPanel = { ...base, lastChildFill: true, children: [first, child()] };
+ childStyle(panel, first, 0)!.flex!.should.equal('0 0 auto');
+ });
+
+ it('should not let the last child fill when the panel says not to', () => {
+ const kid = child();
+ const panel: DockPanel = { ...base, lastChildFill: false, children: [kid] };
+ childStyle(panel, kid, 0)!.flex!.should.equal('0 0 auto');
+ });
+
+ it('should size wrap panel children when the panel sizes them', () => {
+ const panel: WrapPanel = { ...base, orientation: Orientation.Horizontal, itemWidth: 120 };
+ childStyle(panel, child(), 0)!.width!.should.equal(120);
+ });
+
+ it('should leave wrap panel children their own size when the panel sizes neither axis', () => {
+ const panel: WrapPanel = { ...base, orientation: Orientation.Horizontal };
+ (childStyle(panel, child(), 0) === undefined).should.be.true;
+ });
+});
diff --git a/Source/JavaScript/react/renderer/createReactRenderer.tsx b/Source/JavaScript/react/renderer/createReactRenderer.tsx
index 129f2b6..b4b1f99 100644
--- a/Source/JavaScript/react/renderer/createReactRenderer.tsx
+++ b/Source/JavaScript/react/renderer/createReactRenderer.tsx
@@ -5,13 +5,16 @@ import { ReactNode, createElement } from 'react';
import { Renderer } from '@cratis/scene.engine';
import { ContentControl, ExternalComponent, ItemsControl, Panel } from '@cratis/scene.model';
import { ComponentRegistry } from './ComponentRegistry';
+import { childStyle, panelStyle } from './panelLayout';
import { UnresolvedComponent } from './UnresolvedComponent';
/**
- * Creates a {@link Renderer} that turns a Scene element tree into React elements. `Panel`, `ItemsControl`
- * and `ContentControl` render as plain wrapping `div`s here - arranging them according to a layout's
- * `flow`/`freeform` arrangement is Scene#4's job, layered on top of this renderer rather than folded
- * into it.
+ * Creates a {@link Renderer} that turns a Scene element tree into React elements.
+ *
+ * `ItemsControl` and `ContentControl` render as plain wrapping `div`s. A `Panel` renders as the
+ * arrangement it declares - a grid, a stack, a wrap or a dock - because that is what those panels are
+ * for; see `panelLayout` for the mapping. Arranging a *layout's slots* by `flow`/`freeform` is a
+ * different thing and stays Scene#4's, layered on top of this renderer rather than folded into it.
*/
export function createReactRenderer(registry: ComponentRegistry): Renderer {
return {
@@ -29,7 +32,20 @@ export function createReactRenderer(registry: ComponentRegistry): Renderer {
+ const style = childStyle(element, element.children[index], index);
+ return style
+ ? createElement('div', { key: `${element.id}-${index}`, 'data-scene-placement': index, style }, child)
+ : child;
+ });
+
+ return createElement(
+ 'div',
+ { key: element.id, 'data-scene-id': element.id, 'data-scene-kind': 'Panel', style: panelStyle(element) },
+ placed);
},
};
}
diff --git a/Source/JavaScript/react/renderer/panelLayout.ts b/Source/JavaScript/react/renderer/panelLayout.ts
new file mode 100644
index 0000000..d424339
--- /dev/null
+++ b/Source/JavaScript/react/renderer/panelLayout.ts
@@ -0,0 +1,109 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import type { CSSProperties } from 'react';
+import { isDockPanel, isGrid, isStackPanel, isWrapPanel } from '@cratis/scene.engine';
+import { Dock, GridUnitType, Orientation } from '@cratis/scene.model';
+import type { ColumnDefinition, GridLength, Panel, RowDefinition, SceneElement } from '@cratis/scene.model';
+
+/**
+ * Turns a panel into the CSS that arranges it, and a child into the CSS that places it inside one.
+ *
+ * The panels say what an arrangement *is*; this says what it looks like on the web. A second renderer -
+ * native, desktop - answers the same question with its own platform's layout primitives, which is why
+ * this lives in `Scene.React` rather than in the model or the engine.
+ *
+ * Where a panel needs to say something about an individual child - which grid cell, which edge - it does
+ * so through that child's own `properties` bag, because the model has no attached properties of its own.
+ * The keys are the ones the model's documentation names: `Grid.Row`, `Grid.Column`, `Grid.RowSpan`,
+ * `Grid.ColumnSpan` and `Dock`.
+ */
+
+const track = (length: GridLength): string => {
+ switch (length.unitType) {
+ case GridUnitType.Auto: return 'auto';
+ case GridUnitType.Absolute: return `${length.value}px`;
+ case GridUnitType.Star: return `${length.value}fr`;
+ }
+};
+
+const bound = (minimum: number, maximum: number, size: string): string =>
+ minimum === 0 && !Number.isFinite(maximum)
+ ? size
+ : `minmax(${minimum}px, ${Number.isFinite(maximum) ? `${maximum}px` : size})`;
+
+const rowTrack = (row: RowDefinition): string => bound(row.minimumHeight, row.maximumHeight, track(row.height));
+const columnTrack = (column: ColumnDefinition): string => bound(column.minimumWidth, column.maximumWidth, track(column.width));
+
+const number = (value: unknown): number | undefined => (typeof value === 'number' ? value : undefined);
+
+/** The CSS that arranges a panel's own children. */
+export function panelStyle(panel: Panel): CSSProperties | undefined {
+ if (isGrid(panel)) {
+ return {
+ display: 'grid',
+ gridTemplateRows: panel.rows.map(rowTrack).join(' ') || undefined,
+ gridTemplateColumns: panel.columns.map(columnTrack).join(' ') || undefined,
+ };
+ }
+
+ if (isDockPanel(panel)) {
+ // Docking has no single CSS primitive. A column of rows with a middle that grows is the shape a
+ // dock panel actually produces, and `lastChildFill` decides whether the final child is that
+ // middle or just another docked edge.
+ return { display: 'flex', flexDirection: 'column' };
+ }
+
+ if (isStackPanel(panel)) {
+ return {
+ display: 'flex',
+ flexDirection: panel.orientation === Orientation.Horizontal ? 'row' : 'column',
+ gap: panel.spacing ? `${panel.spacing}px` : undefined,
+ };
+ }
+
+ if (isWrapPanel(panel)) {
+ return {
+ display: 'flex',
+ flexWrap: 'wrap',
+ flexDirection: panel.orientation === Orientation.Horizontal ? 'row' : 'column',
+ };
+ }
+
+ return undefined;
+}
+
+/** The CSS that places one child within its panel, or `undefined` when the panel places children implicitly. */
+export function childStyle(panel: Panel, child: SceneElement, index: number): CSSProperties | undefined {
+ const properties = child.properties ?? {};
+
+ if (isGrid(panel)) {
+ const row = number(properties['Grid.Row']);
+ const column = number(properties['Grid.Column']);
+ const rowSpan = number(properties['Grid.RowSpan']) ?? 1;
+ const columnSpan = number(properties['Grid.ColumnSpan']) ?? 1;
+ if (row === undefined && column === undefined) return undefined;
+ return {
+ gridRow: row === undefined ? undefined : `${row + 1} / span ${rowSpan}`,
+ gridColumn: column === undefined ? undefined : `${column + 1} / span ${columnSpan}`,
+ };
+ }
+
+ if (isDockPanel(panel)) {
+ const dock = properties['Dock'] as Dock | undefined;
+ const isFill = panel.lastChildFill && index === (panel.children?.length ?? 0) - 1;
+ if (isFill) return { flex: '1 1 auto', minHeight: 0 };
+ // Left and right dock along the cross axis of the column the panel lays out, so they become a row
+ // of their own rather than a sibling in the column.
+ return dock === Dock.Left || dock === Dock.Right
+ ? { alignSelf: dock === Dock.Left ? 'flex-start' : 'flex-end', flex: '0 0 auto' }
+ : { flex: '0 0 auto' };
+ }
+
+ if (isWrapPanel(panel)) {
+ if (panel.itemWidth === undefined && panel.itemHeight === undefined) return undefined;
+ return { width: panel.itemWidth, height: panel.itemHeight, flex: '0 0 auto' };
+ }
+
+ return undefined;
+}
diff --git a/scene-model-shape.json b/scene-model-shape.json
index 1e1a988..ba7328d 100644
--- a/scene-model-shape.json
+++ b/scene-model-shape.json
@@ -241,6 +241,36 @@
"content",
"displayName",
"description"
+ ],
+ "GridLength": [
+ "value",
+ "unitType"
+ ],
+ "StackPanel": [
+ "orientation",
+ "spacing"
+ ],
+ "DockPanel": [
+ "lastChildFill"
+ ],
+ "WrapPanel": [
+ "orientation",
+ "itemWidth",
+ "itemHeight"
+ ],
+ "RowDefinition": [
+ "height",
+ "minimumHeight",
+ "maximumHeight"
+ ],
+ "ColumnDefinition": [
+ "width",
+ "minimumWidth",
+ "maximumWidth"
+ ],
+ "Grid": [
+ "rows",
+ "columns"
]
},
"enums": {
@@ -278,6 +308,21 @@
"Row",
"Column",
"Grid"
+ ],
+ "Orientation": [
+ "Horizontal",
+ "Vertical"
+ ],
+ "Dock": [
+ "Left",
+ "Top",
+ "Right",
+ "Bottom"
+ ],
+ "GridUnitType": [
+ "Auto",
+ "Absolute",
+ "Star"
]
}
}