Krizaka UI
Krizaka UI
Patterns

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).

Web · ✓Mobile · n/aDesktop · ✓BetaBlocks
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 ShopPage document: the owner chooses sections, their order and how each one is drawn, within a bounded catalogue.

When not to use

  • A free-form page builder with a canvas, nested columns and any component anywhere: the model here is a list of sections, deliberately.Use instead: Shop page
  • A single form of settings.Use instead: Form

Installation

Install

npm install @krizaka/ui @krizaka/tailwind @krizaka/tokens tailwindcss

Styles

@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.

PropTypeDefaultDescription
onPublishrequired(page: ShopPage) => void | Promise<void>—Called with the draft once the person confirmed the diff.
presetsrequiredreadonly ShopComposerPreset[]—The look presets of the shop's trade.
previewUrlrequiredstring—The page of the product, in an iframe: its own route with ?draft=<id>. Its origin is the bridge's.
valuerequiredShopPage—The draft to edit. The composer keeps its own history from it.
classNamestring—Classes merged on the root (its height: h-[100dvh]).
foldedTypesreadonly ("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.
messagesShopComposerMessages{ 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.
publishedShopPage—What is live now, for the diff of « Publish ». Absent: the document the composer was opened with.
resetKeystring | 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.

PropTypeDefaultDescription
typerequired"video" | "faq" | "hero" | "services" | "booking" | "gallery" | "reviews" | "bulletin" | "team" | "visit" | "story" | "social" | "policies"—The section type.
variantrequiredstring—Its variant: one of the type's.
density"compact" | "airy"compactThe page's spacing.
presetstring—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 iframe is 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

KeysAction
↑ ↓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/⌘ ZUndoes and redoes (100 steps), except inside a text field.
Enter, SpaceSelects 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 by postMessage (connectShopPreview in @krizaka/ui/shop is 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.

Generated from the code of @krizaka/ui 3.64.0: its meta.ts, its examples and its types.Edit this documentation on GitHub

On this page