diff --git a/Source/JavaScript/blueprint.default/README.md b/Source/JavaScript/blueprint.default/README.md
index 07bf95ef..ac30ac6f 100644
--- a/Source/JavaScript/blueprint.default/README.md
+++ b/Source/JavaScript/blueprint.default/README.md
@@ -139,8 +139,9 @@ shell versions win the bare names while PrimeReact's stay reachable as `PrimeRea
`GalleryScreenPreview` boots one gallery screen through the real path - real `Screen`, real
`resolveComponentName`, real `SceneElementView`, real registry. Nothing in it is a preview-only code path.
-It resolves against `galleryPreviewProfile` rather than `galleryProfile`. Both describe the gallery's optional
-widget vocabulary; only a host that explicitly lists `PrimeReact` loads those widgets. A profile naming
+It resolves against `galleryProfile` when its registry implements every package that profile declares (PrimeReact by
+every component the gallery borrows from it), and against `galleryPreviewProfile` otherwise, so a partial bundle
+renders what exists instead of placeholders. Only a host that explicitly lists `PrimeReact` loads those widgets. A profile naming
only `Cratis.Blueprint.Default` resolves the shell and Cratis Components, renders the foundation templates,
and leaves templates requiring the optional package incompatible in the template catalog.
@@ -153,9 +154,16 @@ which the Cratis Breadcrumbs destination callback does not expose. Shell icons a
installed icon font. Layout modes, breakpoints, persistence, navigation policies and theme switching are
unchanged.
-The shell and foundation need no `PrimeReactProvider`, widget preset or icon font. Storybook applies none
+The shell and foundation need no `PrimeReactProvider` or widget preset. The optional `actionButton.icon`
+property renders decorative PrimeIcons classes; hosts using it supply the unchanged PrimeIcons font.
+The button's label remains its accessible name. Storybook applies no widget provider, preset or font
globally. Stories that preview optional widgets own their provider, preset and font imports instead.
+Landing, ModuleWorkspace, FeatureSection and SliceSection use blueprint-owned headers and controls rather
+than library-selected cards, text and buttons. Their primary and secondary actions keep their emphasis;
+Landing's sign-in and start actions retain their target-screen navigation. FeatureSection keeps its
+PrimeIcons. Headings are rendered once, including with the complete widget registry.
+
Older business templates still borrow widgets from `scene.primereact`. Their `templateProvenance` and the
gallery dependency vocabulary declare that requirement locally; a host opts into the package in its UI
profile when it chooses such a template. The shell layouts declare only their own component requirement.
diff --git a/Source/JavaScript/blueprint.default/for_FoundationInteractions/when_rendering_an_action_icon.tsx b/Source/JavaScript/blueprint.default/for_FoundationInteractions/when_rendering_an_action_icon.tsx
new file mode 100644
index 00000000..0730bff5
--- /dev/null
+++ b/Source/JavaScript/blueprint.default/for_FoundationInteractions/when_rendering_an_action_icon.tsx
@@ -0,0 +1,21 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { cleanup, screen } from '@testing-library/react';
+import { renderFoundation } from '../for_foundation/given';
+
+describe('when rendering an action icon', () => {
+ afterEach(cleanup);
+
+ it('should render the optional icon decoratively without changing the accessible name', () => {
+ renderFoundation('actionButton', { label: 'New', icon: 'pi pi-plus', emphasis: 'primary' });
+ const button = screen.getByRole('button', { name: 'New' });
+ expect(button.querySelector('i')?.className).toBe('pi pi-plus');
+ expect(button.querySelector('i')?.getAttribute('aria-hidden')).toBe('true');
+ });
+
+ it('should keep actions without an icon unchanged', () => {
+ renderFoundation('actionButton', { label: 'Cancel', emphasis: 'secondary' });
+ expect(screen.getByRole('button', { name: 'Cancel' }).querySelector('i')).toBeNull();
+ });
+});
diff --git a/Source/JavaScript/blueprint.default/for_galleryScreens/when_resolving_the_component_names_they_reference.ts b/Source/JavaScript/blueprint.default/for_galleryScreens/when_resolving_the_component_names_they_reference.ts
index 2646bad8..a4725a3a 100644
--- a/Source/JavaScript/blueprint.default/for_galleryScreens/when_resolving_the_component_names_they_reference.ts
+++ b/Source/JavaScript/blueprint.default/for_galleryScreens/when_resolving_the_component_names_they_reference.ts
@@ -9,6 +9,7 @@ import {
distinctComponentNames,
galleryComponentCatalog,
galleryDialogTemplates,
+ galleryPreviewProfile,
galleryProfile,
galleryScreens,
} from '../gallery';
@@ -61,4 +62,14 @@ describe('when resolving the component names they reference', () => {
.filter(name => !referenced.has(name))
.should.be.empty;
});
+
+ it('should let PrimeReact provide the shared names it declares in the full gallery profile', () => {
+ ['text', 'button', 'card'].map(name => resolveComponentName(name, galleryProfile, galleryComponentCatalog)!.package)
+ .should.deep.equal(['PrimeReact', 'PrimeReact', 'PrimeReact']);
+ });
+
+ it('should let core provide the shared names in the partial-bundle preview profile', () => {
+ ['text', 'button', 'card'].map(name => resolveComponentName(name, galleryPreviewProfile, galleryComponentCatalog)!.package)
+ .should.deep.equal(['core', 'core', 'core']);
+ });
});
diff --git a/Source/JavaScript/blueprint.default/for_previewProfile/when_choosing_the_profile_for_a_registry.tsx b/Source/JavaScript/blueprint.default/for_previewProfile/when_choosing_the_profile_for_a_registry.tsx
new file mode 100644
index 00000000..f60ca423
--- /dev/null
+++ b/Source/JavaScript/blueprint.default/for_previewProfile/when_choosing_the_profile_for_a_registry.tsx
@@ -0,0 +1,55 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { cleanup, render, screen } from '@testing-library/react';
+import { ComponentRegistry, corePackage } from '@cratis/scene.react';
+import { defaultBlueprintComponents } from '../defaultBlueprintComponents';
+import { defaultBlueprintName } from '../packageName';
+import { GalleryScreenPreview, TemplatePreviewState, galleryComponentCatalog, galleryPreviewProfile, galleryProfile, previewProfileFor } from '../gallery';
+import { primeReactComponentNames } from '../gallery/dependencyComponentNames';
+import { resolveComponentName } from '@cratis/scene.engine';
+import { provideBrowserApis } from '../for_templatePreviewStates/given';
+
+const stand = () => null;
+const partial: ComponentRegistry = { ...corePackage.components, ...defaultBlueprintComponents };
+const primeReact = Object.fromEntries(primeReactComponentNames.map(name => [`PrimeReact:${name}`, stand]));
+const full: ComponentRegistry = { ...partial, ...primeReact, 'Cratis.Components:dataPage': stand };
+const sharedNames = ['text', 'button', 'card'];
+const providers = (registry: ComponentRegistry) => sharedNames.map(name => resolveComponentName(name, previewProfileFor(registry), galleryComponentCatalog)!.package);
+
+describe('when choosing the profile for a registry', () => {
+ beforeEach(provideBrowserApis);
+ afterEach(cleanup);
+
+ it('should use the full gallery profile when every package it declares is implemented', () => previewProfileFor(full).should.equal(galleryProfile));
+ it('should let PrimeReact provide the shared names it declares with the full registry', () => providers(full).should.deep.equal(['PrimeReact', 'PrimeReact', 'PrimeReact']));
+ it('should resolve differently from the partial registry for every shared name PrimeReact declares', () => primeReactComponentNames.filter(name => sharedNames.includes(name)).forEach(name => resolveComponentName(name, previewProfileFor(full), galleryComponentCatalog)!.package.should.not.equal(resolveComponentName(name, previewProfileFor(partial), galleryComponentCatalog)!.package)));
+ it('should keep the partial profile with core and the blueprint only', () => previewProfileFor(partial).should.equal(galleryPreviewProfile));
+ it('should let core provide the shared names with a partial registry', () => providers(partial).should.deep.equal(['core', 'core', 'core']));
+ it('should keep the partial profile when a listed package implements nothing', () => previewProfileFor({ ...partial, 'PrimeReact:text': stand, 'Cratis.Components:dataPage': stand }).should.equal(galleryPreviewProfile));
+
+ for (const name of ['Landing', 'ModuleWorkspace', 'FeatureSection', 'SliceSection']) {
+ it(`should render ${name} with a partial registry and no placeholders`, () => {
+ const { container } = render();
+ (container.querySelector('[data-scene-unresolved-component]') === null).should.equal(true);
+ });
+ }
+
+ it('should render Landing without placeholders when PrimeReact has text but not card', () => {
+ const partlyLoaded = { ...partial, 'PrimeReact:text': stand, 'Cratis.Components:dataPage': stand };
+ previewProfileFor(partlyLoaded).should.equal(galleryPreviewProfile);
+ const { container } = render();
+ (container.querySelector('[data-scene-unresolved-component]') === null).should.equal(true);
+ });
+
+ it('should keep Landing headings singular and its actions independent of the complete widget registry', () => {
+ render();
+ for (const title of ['Every mode people expect', 'Themeable to the token', 'The pages you were going to build anyway']) {
+ expect(screen.getAllByText(title)).toHaveLength(1);
+ }
+ expect(screen.getByRole('button', { name: 'Sign in' }).getAttribute('data-emphasis')).toBe('secondary');
+ expect(screen.getAllByRole('button', { name: 'Start free' }).every(button => button.getAttribute('data-emphasis') === 'primary')).toBe(true);
+ });
+
+ it('should name the blueprint as a package of the full profile', () => galleryProfile.packages.should.include(defaultBlueprintName));
+});
diff --git a/Source/JavaScript/blueprint.default/for_previewProfile/when_rendering_the_neutral_gallery_templates.tsx b/Source/JavaScript/blueprint.default/for_previewProfile/when_rendering_the_neutral_gallery_templates.tsx
new file mode 100644
index 00000000..d22802b7
--- /dev/null
+++ b/Source/JavaScript/blueprint.default/for_previewProfile/when_rendering_the_neutral_gallery_templates.tsx
@@ -0,0 +1,68 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { SceneElementView } from '@cratis/scene.react';
+import { galleryComponentCatalog, galleryPreviewRegistry, galleryScreenTemplates, resolveElementComponentNames } from '../gallery';
+import { defaultBlueprintName } from '../packageName';
+
+const profile = { name: 'Neutral', targetPlatform: 'web', packages: ['core', defaultBlueprintName] };
+
+function renderTemplate(name: string) {
+ const template = galleryScreenTemplates.find(candidate => candidate.name === name)!;
+ const elements = Object.values(template.content ?? {}).flat().map(element => resolveElementComponentNames(element, profile, galleryComponentCatalog));
+ return render(<>{elements.map(element => undefined} />)}>);
+}
+
+describe('when rendering the neutral gallery templates', () => {
+ afterEach(() => {
+ cleanup();
+ window.location.hash = '';
+ });
+
+ it('should keep Landing actions named, distinguished and navigable without a widget library', () => {
+ const { container } = renderTemplate('Landing');
+ expect(container.querySelector('[data-scene-unresolved-component]')).toBeNull();
+ const signIn = screen.getByRole('button', { name: 'Sign in' });
+ expect(signIn.getAttribute('data-emphasis')).toBe('secondary');
+ fireEvent.click(signIn);
+ expect(window.location.hash).toBe('#/Login');
+ for (const start of screen.getAllByRole('button', { name: 'Start free' })) {
+ expect(start.getAttribute('data-emphasis')).toBe('primary');
+ window.location.hash = '';
+ fireEvent.click(start);
+ expect(window.location.hash).toBe('#/Register');
+ }
+ expect(screen.getByRole('button', { name: 'Talk to us' }).getAttribute('data-emphasis')).toBe('secondary');
+ for (const title of ['Ship the shell on day one', 'Every mode people expect', 'Themeable to the token', 'The pages you were going to build anyway']) {
+ expect(screen.getAllByText(title)).toHaveLength(1);
+ }
+ });
+
+ it('should keep ModuleWorkspace headings and content without inventing actions', () => {
+ const { container } = renderTemplate('ModuleWorkspace');
+ screen.getByRole('heading', { name: 'Operations' });
+ screen.getByText('Everything the operations module owns');
+ expect(screen.queryAllByRole('button')).toHaveLength(0);
+ expect(container.querySelector('[data-scene-unresolved-component]')).toBeNull();
+ });
+
+ it('should keep FeatureSection primary and secondary actions and their decorative icons', () => {
+ const { container } = renderTemplate('FeatureSection');
+ const primary = screen.getByRole('button', { name: 'New' });
+ const secondary = screen.getByRole('button', { name: 'Export' });
+ expect(primary.getAttribute('data-emphasis')).toBe('primary');
+ expect(secondary.getAttribute('data-emphasis')).toBe('secondary');
+ expect(primary.querySelector('i.pi-plus')?.getAttribute('aria-hidden')).toBe('true');
+ expect(secondary.querySelector('i.pi-download')?.getAttribute('aria-hidden')).toBe('true');
+ expect(container.querySelector('[data-scene-unresolved-component]')).toBeNull();
+ });
+
+ it('should keep SliceSection headings and content without inventing actions', () => {
+ const { container } = renderTemplate('SliceSection');
+ screen.getByRole('heading', { name: 'Adjustment' });
+ screen.getByText('One behavior, one surface');
+ expect(screen.queryAllByRole('button')).toHaveLength(0);
+ expect(container.querySelector('[data-scene-unresolved-component]')).toBeNull();
+ });
+});
diff --git a/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_choosing_the_default_or_the_widget_mode.tsx b/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_choosing_the_default_or_the_widget_mode.tsx
index 840793b0..448757aa 100644
--- a/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_choosing_the_default_or_the_widget_mode.tsx
+++ b/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_choosing_the_default_or_the_widget_mode.tsx
@@ -9,7 +9,7 @@ import { ScenePackageBundle, corePackage, resolvePackageHost } from '@cratis/sce
import { primeReactPackage } from '@cratis/scene.primereact';
import { defaultBlueprint } from '../defaultBlueprint';
import { defaultBlueprintName } from '../packageName';
-import { galleryComponentCatalog, galleryScreenTemplates, GalleryScreenPreview, TemplatePreviewState } from '../gallery';
+import { galleryComponentCatalog, galleryDialogTemplates, galleryScreenTemplates, GalleryScreenPreview, TemplatePreviewState } from '../gallery';
import { provideBrowserApis } from './given';
const empty = (name: string, kind: PackageKind): ScenePackageBundle => ({
@@ -37,6 +37,14 @@ describe('when choosing the default or the widget mode', () => {
it('should not activate PrimeReact', () => host.bundles.some(bundle => bundle.manifest.name === 'PrimeReact').should.equal(false));
it('should offer every foundation template as compatible', () => host.templates.filter(entry => foundation.some(template => template.name === entry.name) && !entry.compatible).map(entry => entry.name).should.deep.equal([]));
+ it('should offer the neutral gallery screens and confirmation dialog without PrimeReact', () => {
+ const neutral = ['Landing', 'ModuleWorkspace', 'FeatureSection', 'SliceSection'];
+ neutral.every(name => foundation.some(template => template.name === name)).should.equal(true);
+ borrowing.map(template => template.name).should.not.include.members(neutral);
+ const confirm = host.templates.find(entry => entry.name === 'ConfirmDialog' && entry.package === defaultBlueprintName);
+ (confirm?.compatible === true).should.equal(true);
+ (galleryDialogTemplates.find(template => template.name === 'ConfirmDialog')!.metadata?.compatibility?.packages ?? []).length.should.equal(0);
+ });
it('should offer every widget-borrowing template as unavailable with an actionable message', () => {
const entries = host.templates.filter(entry => borrowing.some(template => template.name === entry.name));
entries.length.should.equal(borrowing.length);
diff --git a/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_previewing_the_dialog_templates.tsx b/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_previewing_the_dialog_templates.tsx
index e572e540..001643f2 100644
--- a/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_previewing_the_dialog_templates.tsx
+++ b/Source/JavaScript/blueprint.default/for_templatePreviewStates/when_previewing_the_dialog_templates.tsx
@@ -1,10 +1,10 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-import { cleanup, render, screen } from '@testing-library/react';
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { PrimeReactProvider } from '@primereact/core';
-import { SceneElementView } from '@cratis/scene.react';
-import { galleryComponentCatalog, galleryDialogTemplates, galleryPreviewProfile, queryResultsOf, resolveElementComponentNames } from '../gallery';
+import { InteractionScope, SceneElementView, createBrowserDispatcher } from '@cratis/scene.react';
+import { galleryComponentCatalog, galleryDialogTemplates, galleryPreviewProfile, galleryProfile, previewProfileFor, queryResultsOf, resolveElementComponentNames } from '../gallery';
import { foundationPack } from '../for_foundationTemplates/given';
import { packageBackedRegistry, provideBrowserApis } from './given';
@@ -12,6 +12,24 @@ describe('when previewing the dialog templates', () => {
beforeEach(provideBrowserApis);
afterEach(cleanup);
+ it('should dispatch CloseDialog when Cancel is clicked with the complete PrimeReact registry', async () => {
+ const registry = { ...packageBackedRegistry, 'Cratis.Components:dataPage': () => undefined };
+ const profile = previewProfileFor(registry);
+ expect(profile).toBe(galleryProfile);
+ const closeDialog = vi.fn();
+ const template = galleryDialogTemplates.find(candidate => candidate.name === 'FormDialog')!;
+ const elements = Object.values(template.content ?? {}).flat()
+ .map(element => resolveElementComponentNames(element, profile, galleryComponentCatalog));
+ render(
+
+ {elements.map(element => undefined} />)}
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ await waitFor(() => expect(closeDialog).toHaveBeenCalledTimes(1));
+ });
+
for (const template of galleryDialogTemplates) {
it(`should render ${template.name} through the real packages, with a named field and an action for every choice`, () => {
const elements = Object.values(template.content ?? {}).flat()
diff --git a/Source/JavaScript/blueprint.default/foundation/ActionButtonControl.tsx b/Source/JavaScript/blueprint.default/foundation/ActionButtonControl.tsx
index 340e0859..594d8ec9 100644
--- a/Source/JavaScript/blueprint.default/foundation/ActionButtonControl.tsx
+++ b/Source/JavaScript/blueprint.default/foundation/ActionButtonControl.tsx
@@ -22,6 +22,7 @@ export function ActionButtonControl({ element, interactions }: RegisteredCompone
const requested = readOptionalString(element, 'emphasis');
const emphasis = (emphases as readonly string[]).includes(requested ?? '') ? (requested as typeof emphases[number]) : 'primary';
const target = readOptionalString(element, 'targetScreen');
+ const icon = readOptionalString(element, 'icon');
return (
@@ -30,6 +31,7 @@ export function ActionButtonControl({ element, interactions }: RegisteredCompone
if (interactions?.onClick) interactions.onClick(event);
else if (target && typeof window !== 'undefined') window.location.hash = `#/${target}`;
}}>
+ {icon && }
{readOptionalString(element, 'label') ?? ''}
diff --git a/Source/JavaScript/blueprint.default/foundation/foundationDescriptors.ts b/Source/JavaScript/blueprint.default/foundation/foundationDescriptors.ts
index 62cca9f6..9db66bb2 100644
--- a/Source/JavaScript/blueprint.default/foundation/foundationDescriptors.ts
+++ b/Source/JavaScript/blueprint.default/foundation/foundationDescriptors.ts
@@ -460,6 +460,7 @@ export const foundationDescriptors: ComponentDescriptor[] = [
description: 'One action, drawn as every action of the foundation is, that is enabled, disabled with its reason, or hidden by the permission of the user.',
properties: [
text('label', 'Label', 'Content', 'What it does - a verb, never "OK".', { constraints: { required: true } }),
+ text('icon', 'Icon', 'Appearance', 'Optional decorative PrimeIcons classes; the label remains the accessible name.'),
choice('emphasis', 'Emphasis', 'Appearance', { primary: 'Primary', secondary: 'Secondary', quiet: 'Quiet', danger: 'Destructive' }, 'primary'),
rule('permission', 'Permission', 'Whether the action works, refuses with a reason or is not shown.'),
text('disabledReason', 'Reason when disabled', 'Permissions'),
diff --git a/Source/JavaScript/blueprint.default/gallery/GalleryDialogPreview.tsx b/Source/JavaScript/blueprint.default/gallery/GalleryDialogPreview.tsx
index 1ba6d4a4..8b43f8e5 100644
--- a/Source/JavaScript/blueprint.default/gallery/GalleryDialogPreview.tsx
+++ b/Source/JavaScript/blueprint.default/gallery/GalleryDialogPreview.tsx
@@ -10,7 +10,7 @@ import { LayoutThemeProvider, defaultBlueprintThemes } from '../themes';
import { externalComponent } from './elements';
import { ForcedComponentState, forceComponentState, galleryPreviewRegistry, queryResultsOf } from './GalleryScreenPreview';
import { galleryDialogTemplates } from './dialogTemplates';
-import { galleryComponentCatalog, galleryPreviewProfile } from './previewProfile';
+import { galleryComponentCatalog, previewProfileFor } from './previewProfile';
import { resolveElementComponentNames } from './resolveElementNames';
import { TemplatePreviewState } from './TemplatePreviewState';
@@ -51,9 +51,9 @@ export function GalleryDialogPreview({ dialogName, initialConfig, themes = defau
const element = useMemo(() => {
const composed = externalComponent(`dialog-${template.name}`, previewTemplateComponent, {}, template.content ?? {});
- const resolved = resolveElementComponentNames(composed, galleryPreviewProfile, galleryComponentCatalog);
+ const resolved = resolveElementComponentNames(composed, previewProfileFor(registry), galleryComponentCatalog);
return forceState ? forceComponentState(resolved, forceState) : resolved;
- }, [template, forceState]);
+ }, [template, forceState, registry]);
const full = useMemo(() => ({ ...registry, ...previewComponents }), [registry]);
return (
diff --git a/Source/JavaScript/blueprint.default/gallery/GalleryScreenPreview.tsx b/Source/JavaScript/blueprint.default/gallery/GalleryScreenPreview.tsx
index 511b7582..cd6f16ac 100644
--- a/Source/JavaScript/blueprint.default/gallery/GalleryScreenPreview.tsx
+++ b/Source/JavaScript/blueprint.default/gallery/GalleryScreenPreview.tsx
@@ -11,7 +11,7 @@ import { ComponentState, foundationSamples } from '../foundation';
import { defaultBlueprintName } from '../packageName';
import { LayoutThemeProvider, defaultBlueprintThemes } from '../themes';
import { composeScreenElement } from './composeScreen';
-import { galleryComponentCatalog, galleryPreviewProfile } from './previewProfile';
+import { galleryComponentCatalog, previewProfileFor } from './previewProfile';
import { resolveElementComponentNames } from './resolveElementNames';
import { galleryScreen } from './screens';
import { TemplatePreviewState } from './TemplatePreviewState';
@@ -118,9 +118,9 @@ export function GalleryScreenPreview({ screenName, initialConfig, themes = defau
throw new Error(`The gallery has no screen named '${screenName}'.`);
}
- const resolved = applyTemplatePreviewState(resolveElementComponentNames(composeScreenElement(screen), galleryPreviewProfile, galleryComponentCatalog), state);
+ const resolved = applyTemplatePreviewState(resolveElementComponentNames(composeScreenElement(screen), previewProfileFor(registry), galleryComponentCatalog), state);
return forceState ? forceComponentState(resolved, forceState) : resolved;
- }, [screenName, state, forceState]);
+ }, [screenName, state, forceState, registry]);
return (
diff --git a/Source/JavaScript/blueprint.default/gallery/dependencyComponentNames.ts b/Source/JavaScript/blueprint.default/gallery/dependencyComponentNames.ts
index db7051b8..9ef3c733 100644
--- a/Source/JavaScript/blueprint.default/gallery/dependencyComponentNames.ts
+++ b/Source/JavaScript/blueprint.default/gallery/dependencyComponentNames.ts
@@ -30,6 +30,7 @@
export const primeReactComponentNames: string[] = [
'avatar',
'breadcrumb',
+ 'button',
'calendar',
'card',
'checkbox',
diff --git a/Source/JavaScript/blueprint.default/gallery/nesting.ts b/Source/JavaScript/blueprint.default/gallery/nesting.ts
index 60a40f26..6d22e8b0 100644
--- a/Source/JavaScript/blueprint.default/gallery/nesting.ts
+++ b/Source/JavaScript/blueprint.default/gallery/nesting.ts
@@ -2,12 +2,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
import { ScreenTemplate } from '@cratis/scene.model';
-import { ComponentName } from '../ComponentName';
import { SlotName, column, slotLeaf } from '../layouts';
import { TemplateSlotName } from './TemplateSlotName';
-import { button, externalComponent } from './elements';
+import { actionButton } from '../foundation/templates/dialogTemplates';
import { pageHeader } from './widgets';
-import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
+import { neutralGalleryTemplateProvenance } from '../templateProvenance';
/**
* A worked three-level chain, from the application layout down to a slice.
@@ -37,7 +36,7 @@ export const moduleWorkspaceTemplate: ScreenTemplate = {
},
displayName: 'Module workspace',
description: 'Module level: fits the application layout content slot and offers a body for one feature.',
- metadata: { ...defaultBlueprintTemplateProvenance, category: 'Foundation / Workspace' },
+ metadata: { ...neutralGalleryTemplateProvenance, category: 'Foundation / Workspace' },
};
/**
@@ -52,13 +51,13 @@ export const featureSectionTemplate: ScreenTemplate = {
slots: [{ name: TemplateSlotName.Toolbar }, { name: TemplateSlotName.Body }],
content: {
[TemplateSlotName.Toolbar]: [
- button('feature-new', 'New', { severity: 'primary', icon: 'pi pi-plus' }),
- button('feature-export', 'Export', { severity: 'secondary', icon: 'pi pi-download' }),
+ actionButton('feature-new', { label: 'New', emphasis: 'primary', icon: 'pi pi-plus' }),
+ actionButton('feature-export', { label: 'Export', emphasis: 'secondary', icon: 'pi pi-download' }),
],
},
displayName: 'Feature section',
description: 'Feature level: fits a module workspace body slot and brings the action toolbar.',
- metadata: { ...defaultBlueprintTemplateProvenance, category: 'Foundation / Section' },
+ metadata: { ...neutralGalleryTemplateProvenance, category: 'Foundation / Section' },
};
/**
@@ -75,11 +74,11 @@ export const sliceSectionTemplate: ScreenTemplate = {
root: column([slotLeaf(TemplateSlotName.Body, { grow: 1 }), slotLeaf(TemplateSlotName.Actions)]),
},
content: {
- [TemplateSlotName.Body]: [externalComponent('slice-body', ComponentName.PageHeader, { title: 'Adjustment', subtitle: 'One behavior, one surface' })],
+ [TemplateSlotName.Body]: [pageHeader('slice-body', 'Adjustment', 'One behavior, one surface')],
},
displayName: 'Slice section',
description: 'Slice level: fits a feature section body slot and hosts one behavior.',
- metadata: { ...defaultBlueprintTemplateProvenance, category: 'Foundation / Section' },
+ metadata: { ...neutralGalleryTemplateProvenance, category: 'Foundation / Section' },
};
/** The chain, outermost first - what a spec walks to prove `fitsSlot` resolves at every level. */
diff --git a/Source/JavaScript/blueprint.default/gallery/previewProfile.ts b/Source/JavaScript/blueprint.default/gallery/previewProfile.ts
index 8c27d649..4d29ae52 100644
--- a/Source/JavaScript/blueprint.default/gallery/previewProfile.ts
+++ b/Source/JavaScript/blueprint.default/gallery/previewProfile.ts
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
import { UiProfile } from '@cratis/scene.model';
+import { ComponentRegistry, componentRegistryKey } from '@cratis/scene.react';
import { PackageCatalog } from '@cratis/scene.engine';
import { ComponentName } from '../ComponentName';
import { defaultBlueprintName } from '../packageName';
@@ -23,7 +24,7 @@ export const galleryProfile: UiProfile = {
};
/**
- * The profile a *preview* resolves against, when only some of those packages are actually loaded.
+ * The profile a *preview* resolves against, when only some of those packages are actually loaded (see {@link previewProfileFor}).
*
* Same four packages, different order, and the difference is deliberate. This blueprint takes no npm
* dependency on the optional widget bundles - each borrowing template declares its own requirements - so a preview run from this package has `core` and this blueprint
@@ -33,8 +34,8 @@ export const galleryProfile: UiProfile = {
* Names only the libraries declare - `dataTable`, `timeline`, `tag` - still resolve to them, find no
* implementation, and render as `UnresolvedComponent`'s dashed box naming what is missing. That is the
* right outcome rather than a papered-over one: previewing a blueprint against half a profile *should*
- * look visibly incomplete. A host with all four bundles loaded uses {@link galleryProfile} and sees the
- * real widgets.
+ * look visibly incomplete. A preview whose registry implements all four packages uses {@link galleryProfile}
+ * instead and sees the real widgets.
*/
export const galleryPreviewProfile: UiProfile = {
name: 'Gallery preview',
@@ -42,6 +43,22 @@ export const galleryPreviewProfile: UiProfile = {
packages: ['PrimeReact', 'Cratis.Components', 'core', defaultBlueprintName],
};
+/**
+ * The profile a preview resolves against for the registry it renders with.
+ *
+ * {@link galleryProfile} when every package it declares is implemented in the registry: this blueprint, core and
+ * Cratis Components by at least one component, and PrimeReact by *every* component name the gallery borrows from it
+ * (the catalog's PrimeReact subset), so PrimeReact's `text` and `card` win over core's. Otherwise
+ * {@link galleryPreviewProfile}, which keeps partial bundles resolving to what exists. A package that is only partly
+ * registered must not win a name it cannot render, or the preview would show placeholders.
+ */
+export const previewProfileFor = (registry: ComponentRegistry): UiProfile => {
+ const keys = Object.keys(registry);
+ const loaded = (name: string) => keys.some(key => key.startsWith(componentRegistryKey(name, '')));
+ const primeReactComplete = primeReactComponentNames.every(name => componentRegistryKey('PrimeReact', name) in registry);
+ return galleryProfile.packages.every(loaded) && primeReactComplete ? galleryProfile : galleryPreviewProfile;
+};
+
/**
* The component catalog the gallery resolves against.
*
diff --git a/Source/JavaScript/blueprint.default/gallery/statusTemplates.ts b/Source/JavaScript/blueprint.default/gallery/statusTemplates.ts
index e1fbbf31..6ba3dfb6 100644
--- a/Source/JavaScript/blueprint.default/gallery/statusTemplates.ts
+++ b/Source/JavaScript/blueprint.default/gallery/statusTemplates.ts
@@ -6,8 +6,9 @@ import { ComponentName } from '../ComponentName';
import { SlotName } from '../layouts';
import { TemplateSlotName } from './TemplateSlotName';
import { button, card, externalComponent, panel, text } from './elements';
-import { widget } from './widgets';
-import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
+import { pageHeader } from './widgets';
+import { actionButton } from '../foundation/templates/dialogTemplates';
+import { defaultBlueprintTemplateProvenance, neutralGalleryTemplateProvenance } from '../templateProvenance';
/**
* The four screens that are not part of anyone's plan: a server error, a refusal, a wrong address, and the
@@ -94,29 +95,29 @@ export const landingTemplate: ScreenTemplate = {
[TemplateSlotName.Header]: [
panel('landing-nav', [
externalComponent('landing-logo', ComponentName.Logo, { label: 'Contoso', initials: 'C' }),
- button('landing-signin', 'Sign in', { severity: 'secondary', targetScreen: 'Login' }),
- button('landing-start', 'Start free', { severity: 'primary', targetScreen: 'Register' }),
+ actionButton('landing-signin', { label: 'Sign in', emphasis: 'secondary', targetScreen: 'Login' }),
+ actionButton('landing-start', { label: 'Start free', emphasis: 'primary', targetScreen: 'Register' }),
]),
],
[TemplateSlotName.Body]: [
- card('landing-hero', [
- text('landing-headline', 'Ship the shell on day one'),
- text('landing-subhead', 'Eight menu modes, twenty screen templates and two themes, in one blueprint an application picks once.'),
- button('landing-cta', 'Start free', { severity: 'primary', targetScreen: 'Register' }),
- ]),
- widget('landing-feature-modes', 'Every mode people expect', [text('landing-modes-text', 'Static, overlay, slim, slim+, compact, horizontal, reveal and drawer.')]),
- widget('landing-feature-themes', 'Themeable to the token', [text('landing-themes-text', 'Ten semantic tokens define the whole shell. Swap them and everything follows.')]),
- widget('landing-feature-templates', 'The pages you were going to build anyway', [
- text('landing-templates-text', 'Dashboard, list, detail, form, invoice, settings, help - and every screen around signing in.'),
- ]),
+ pageHeader('landing-hero', 'Ship the shell on day one',
+ 'Eight menu modes, twenty screen templates and two themes, in one blueprint an application picks once.', [
+ actionButton('landing-cta', { label: 'Start free', emphasis: 'primary', targetScreen: 'Register' }),
+ ]),
+ pageHeader('landing-feature-modes', 'Every mode people expect', 'Static, overlay, slim, slim+, compact, horizontal, reveal and drawer.'),
+ pageHeader('landing-feature-themes', 'Themeable to the token', 'Ten semantic tokens define the whole shell. Swap them and everything follows.'),
+ pageHeader('landing-feature-templates', 'The pages you were going to build anyway',
+ 'Dashboard, list, detail, form, invoice, settings, help - and every screen around signing in.'),
],
[TemplateSlotName.Actions]: [
- panel('landing-footer', [text('landing-copyright', '© Contoso'), button('landing-contact', 'Talk to us', { severity: 'secondary' })]),
+ externalComponent('landing-footer', ComponentName.Footer, { text: '© Contoso' }, {
+ content: [actionButton('landing-contact', { label: 'Talk to us', emphasis: 'secondary' })],
+ }),
],
},
displayName: 'Landing',
description: 'The marketing front door, with its own navigation because it has no application chrome.',
- metadata: { ...defaultBlueprintTemplateProvenance, category: 'Foundation / Landing' },
+ metadata: { ...neutralGalleryTemplateProvenance, category: 'Foundation / Landing' },
};
/** The four status and marketing templates. */
diff --git a/Source/JavaScript/blueprint.default/templateProvenance.ts b/Source/JavaScript/blueprint.default/templateProvenance.ts
index c49f1c0f..d26f2174 100644
--- a/Source/JavaScript/blueprint.default/templateProvenance.ts
+++ b/Source/JavaScript/blueprint.default/templateProvenance.ts
@@ -21,6 +21,15 @@ export const defaultBlueprintTemplateProvenance: Pick = {
+ ...defaultBlueprintTemplateProvenance,
+ compatibility: { scene: defaultBlueprintTemplateProvenance.compatibility!.scene },
+};
+
/**
* The provenance of the shell and the dashboard, whose arrangement follows PrimeTek's free Sakai template.
* The arrangement is followed; no Sakai source or asset is part of this package.
diff --git a/Source/JavaScript/primereact/button/PrimeButton.tsx b/Source/JavaScript/primereact/button/PrimeButton.tsx
index 5799d442..a4e2275e 100644
--- a/Source/JavaScript/primereact/button/PrimeButton.tsx
+++ b/Source/JavaScript/primereact/button/PrimeButton.tsx
@@ -23,7 +23,7 @@ import {booleanProperty, severityProperty, stringProperty} from '../properties';
* button disabled so the action cannot be fired twice, and `aria-busy` so the state is announced rather
* than only drawn. The icon is marked decorative throughout because the label already names the action.
*/
-export function PrimeButton({ element, slots }: RegisteredComponentProps) {
+export function PrimeButton({ element, slots, interactions }: RegisteredComponentProps) {
const icon = stringProperty(element, 'icon');
const severity = severityProperty(element, 'severity');
const loading = booleanProperty(element, 'loading', false);
@@ -35,6 +35,7 @@ export function PrimeButton({ element, slots }: RegisteredComponentProps) {
return (