Shop composer
Our own editor for a shop's page document (ADR-007): the outline of the sections, the live preview of the product's page and the inspector of the selected section — variant as live thumbnails, settings as a form generated from the zod schema, words, blocks — with undo and redo, a saved draft and a publish step that shows what changes. First consumer: Akinaza's studio (akinaza#402).
import { ShopComposer, defaultShopComposerMessages, applyCommand, createHistory, diffPages, describeSettings, SectionThumbnail } from "@krizaka/ui/shop-composer";Web only — React, from @krizaka/ui/shop-composer. Beta: its API may still change in a minor version.
- Mobile · n/a. The composer is the studio of the web console; a shop's page is a web page. → Shop page
Why it is here. Akinaza's studio (Boutique › Page): the owner arranges the sections of the public page, with a live preview, undo and a publish step (krizaka/akinaza#402, ADR-007, krizaka-ui#746).
When to use
- The studio of a product whose public page is a
ShopPagedocument: the owner chooses sections, their order and how each one is drawn, within a bounded catalogue.
When not to use
Installation
Install
npm install @krizaka/ui @krizaka/tailwind @krizaka/tokens tailwindcssStyles
@import "tailwindcss";
@import "@krizaka/tailwind";
@import "@krizaka/ui/tailwind.css";Import
import { ShopComposer, defaultShopComposerMessages, applyCommand, createHistory, diffPages, describeSettings, SectionThumbnail } from "@krizaka/ui/shop-composer";Examples
Composer
The salon's compact page in the composer: outline, preview, inspector.
Props
ShopComposer
The shop composer.
| Prop | Type | Default | Description |
|---|---|---|---|
onPublishrequired | (page: ShopPage) => void | Promise<void> | — | Called with the draft once the person confirmed the diff. |
presetsrequired | readonly ShopComposerPreset[] | — | The look presets of the shop's trade. |
previewUrlrequired | string | — | The page of the product, in an iframe: its own route with ?draft=<id>. Its origin is the bridge's. |
valuerequired | ShopPage | — | The draft to edit. The composer keeps its own history from it. |
className | string | — | Classes merged on the root (its height: h-[100dvh]). |
foldedTypes | readonly ("video" | "faq" | "hero" | "services" | "booking" | "gallery" | "reviews" | "bulletin" | "team" | "visit" | "story" | "social" | "policies")[] | [] | The section types the trade does not use: folded under « … » in the catalogue. |
messages | ShopComposerMessages | {
title: "Shop page",
sections: "Sections",
addSection: "Add a section",
addSectionHint: "Choose what to add to your page.",
toggleVisible: "Show {name}",
sectionMenu: "Actions for {name}",
moveUp: "Move up",
moveDown: "Move down",
moveTop: "Move to top",
remove: "Remove",
announceMoved: "{name} moved to position {n} of {total}",
announceCannotMove: "{name} cannot move further",
announceAdded: "{name} added at position {n}",
announceRemoved: "{name} removed",
announceShown: "{name} is shown",
announceHidden: "{name} is hidden",
announceUndone: "Undone",
announceRedone: "Redone",
hiddenNote: "hidden",
inspector: "Section",
noSelection: "Choose a section to change how it is drawn.",
backToSections: "Back to sections",
variant: "How it is drawn",
settings: "Settings",
words: "Your words",
titleField: "Title",
leadField: "Line under the title",
french: "French",
english: "English",
blocks: "Items",
addBlock: "Add",
removeBlock: "Remove {name}",
look: "Look",
preset: "Style",
density: "Spacing",
densityCompact: "Compact",
densityAiry: "Airy",
undo: "Undo",
redo: "Redo",
saved: "Saved",
saving: "Saving…",
saveFailed: "Not saved",
publish: "Publish",
discard: "Discard the draft",
preview: "Preview",
previewTitle: "Live preview of your page",
phone: "Phone",
desktop: "Desktop",
viewPreview: "View",
closePreview: "Close",
publishTitle: "Publish this page",
publishWhat: "What changes",
publishNothing: "Nothing has changed since the last publication.",
publishConfirm: "Publish",
cancel: "Cancel",
problemsTitle: "Fix {n} problem before publishing",
problemIn: "{name}",
sectionNames,
variantNames,
blockNames: { fact: "Fact", service: "Service", photo: "Photo", note: "Note", sign: "Sign", reel: "Reel", flyer: "Flyer", member: "Member", link: "Link", question: "Question" },
settingNames: { media: "Photo", focus: "Crop", source: "Source", groupBy: "Group by", columns: "Columns", aside: "Beside the list", serviceId: "Service", count: "How many", texture: "Wall", cabinet: "Cabinet", channel: "Channel", rule: "Which reviews", showPhone: "Show the phone number", body: "Text", show: "Show" },
optionNames: { center: "Centre", top: "Top", bottom: "Bottom", all: "All", pick: "Pick", category: "Category", none: "None", hours: "Hours", plaster: "Plaster", brick: "Brick", ridge: "Ridge", zellige: "Zellige", cabinet: "Cabinet", portable: "Portable", best: "Best", latest: "Latest", cancellation: "Cancellation", prices: "Prices", "booking-window": "Booking window" },
diff: {
added: "{name} added",
removed: "{name} removed",
moved: "{name} moved from {from} to {to}",
variant: "{name} drawn as {to} (was {from})",
settings: "{name}: settings changed",
blocks: "{name}: items changed",
text: "{name}: words changed",
disabled: "{name} hidden",
enabled: "{name} shown",
preset: "Style changed to {to} (was {from})",
density: "Spacing changed to {to} (was {from})",
},
} | The words, translated. Default: English. |
onChange | ((page: ShopPage) => void | Promise<void>) | — | Called with a valid draft after 800 ms without a change: save it. A rejection says « Not saved ». |
onDiscard | (() => void) | — | The discard of the draft (the product asks first, in its own dialog). Absent: no such button. |
published | ShopPage | — | What is live now, for the diff of « Publish ». Absent: the document the composer was opened with. |
resetKey | string | number | — | Change it to load another document (another draft): the history and the selection start again. |
SectionThumbnail
A picture of a section: 160 × 96 px of a 960 px page, inert and hidden from assistive technology.
| Prop | Type | Default | Description |
|---|---|---|---|
typerequired | "video" | "faq" | "hero" | "services" | "booking" | "gallery" | "reviews" | "bulletin" | "team" | "visit" | "story" | "social" | "policies" | — | The section type. |
variantrequired | string | — | Its variant: one of the type's. |
density | "compact" | "airy" | compact | The page's spacing. |
preset | string | — | The look preset the page wears (salon-vitrine): the thumbnail wears it too. |
Accessibility
- One polite live region says every move, addition, removal and undo; « Saved » is its own status.
- The preview
iframeis titled; the variant picker is a radio group; an invalid setting is said next to its field and the publish dialog lists every problem and stays closed to confirmation until they are fixed. - On a phone the composer has two steps (the list, then the section) and the preview behind a « View » button.
Keyboard
| Keys | Action |
|---|---|
| ↑ ↓ | Moves between the rows of the outline. |
| Alt+↑ / Alt+↓ | Moves the focused section up or down; the move is announced and focus stays on its row. |
| Ctrl/⌘ Z, Shift+Ctrl/⌘ Z | Undoes and redoes (100 steps), except inside a text field. |
| Enter, Space | Selects a section, toggles its visibility, chooses a variant. |
Best practices
- The product saves the draft in
onChange(a valid document, after 800 ms of calm) and serves its own public route with?draft=<id>for the preview; the composer talks to it bypostMessage(connectShopPreviewin@krizaka/ui/shopis the page's half). - Pass the presets of the shop's trade and the section types the trade does not use (
foldedTypes): they are folded away, not removed. - Pass translated
messages; English is the default. - Publish only what the composer hands to
onPublish: it is parsed (parseShopPage) and the product parses it again on write.
Related components
Generated from the code of @krizaka/ui 3.64.0: its meta.ts, its examples and its types.Edit this documentation on GitHub