Summary#
A look is a site's whole design, shipped as a kit and picked per site version at the page desk. It sits on two contracts and draws both:
| layer | package | what it says |
|---|---|---|
| a look | yours, in your own namespace — MyStore.Looks.Linen | how everything looks: the frame, the pages, the theme and fonts, a design of each block type, and blocks of its own |
| the category contract | Osysharp.Shop.Looks, inside the shop kit | what a SHOP has: a row of products, the shelves as tiles, and the parts of a store — its frame, front page, listing, grid, a piece's page, a content column, a "not here" |
| the site contract | Osysharp.Site.Looks | what ANY site has: a hero, a picture with words, a quote, a sign-up, a banner, a gallery, the strip across the top, the footer's words, links, opening hours and address |
A contract is an abstract component — parameters and no body. A look's design writes : Hero and takes exactly the
contract's parameters, checked like a class against an interface. What staff arrange is stored as the TYPE and its
values, so a store switching from one look to another keeps every word and photograph and draws them in the new design.
The worked example is the shop template's own ten looks (kits/look-atelier … kits/look-loud): the five designs
of the Vall fashion store and the five of the Second Round thrift store, each a complete look kit drawing both contracts.
This page builds a small one of your own beside them — Linen: undyed linen, ink-blue words, one rust accent — in your
own namespace, from an empty folder. Every one of the shop's looks has the same shape, so read any of them beside it.
Signature#
package MyStore.Looks.Linen { … use Osysharp.Shop@1; use Osysharp.Site.Looks@0; use Osysharp.PageBuilder@1; }
namespace MyStore.Looks.Linen;
look Linen { Label = "Linen"; Hint = "…"; Theme = LinenTheme; }
theme LinenTheme { Colors { … } Font { … } FontSize { … } Radius { … } }
component LinenFrame() : ShopFrame { … } // one design per part of a store
component LinenHero(<exactly Hero's parameters>) : Hero { … } // one design per block type you draw your way
[Block("A note from the weaver")] component LinenNote(…) { … } // a block only this look hasDescription#
What goes in the kit's manifest?#
A look kit is a folder with a package.osy, its source under model/, its fonts under fonts/ and its own
osyrin.lock. The manifest names the packages its designs read: the two contracts, the PageBuilder kit (the menu and the
look switch every frame carries), and what draws photographs.
// looks/linen/package.osy — a LOOK for a shop. It draws Osysharp.Site.Looks and Osysharp.Shop.Looks and
// carries no data and no logic of its own.
package MyStore.Looks.Linen {
version "0.1.0";
minPlatform "0.9.0";
summary "a shop look — undyed linen, ink-blue words, one rust accent.";
model "model/**/*.osy";
use Osysharp.Ui;
use Osysharp.Blocks;
use Osysharp.Storage;
use Osysharp.Shop@1;
use Osysharp.Site.Looks@0;
use Osysharp.PageBuilder@1;
}The summary says what the look is FOR — "a shop look" — because a look draws one category's contract. Run osy lock in
the folder to pin those packages.
A look kit carries no data and no logic, and asks for no egress and no secret. Its designs read what a store's page reads, as the visitor — so a design written by someone else cannot send a customer's data anywhere.
How does a store wear it?#
The store does not change shape for a look. Its pages keep what is the STORE's — the data they read, the checkout's
steps — and hand the look what it draws: its layout places ShopFrame once, its front page is FrontPage(), a piece's
page reads the piece and hands it to ProductPage. app.Look names the look the site wears until staff choose another.
app Linen {
use Osysharp.Ui;
use Osysharp.Storage;
use Osysharp.Blocks;
use Osysharp.Shop@1 { egress "api.resend.com"; secret "ResendApiKey"; }
use Osysharp.PageBuilder@1;
use Osysharp.Site.Looks@0;
model "model/**/*.osy";
}
using Osysharp.Shop;
using Osysharp.Shop.Looks;
app.Look = Linen;
[Layout]
[AllowAnonymous]
component Shop() { render { ShopFrame { Outlet(retain: 8); } } }
[Route("/")]
[Layout(Shop)]
[AllowAnonymous]
component Home() { render { FrontPage(); } }
[Route("/shop")]
[Layout(Shop)]
[AllowAnonymous]
component Catalogue() { render { ShopSearchPage(); } }
[Route("/p/{slug}")]
[Layout(Shop)]
[AllowAnonymous]
component Piece(string slug) {
live var product = Product.Include(p => p.Images).Include(p => p.Variants).SingleOrDefault(p => p.Slug == slug);
render { ProductPage(product, note: "Free returns within 30 days."); }
}
[Route("/account")]
[Layout(Shop)]
component AccountPage() { render { ShopSection { ShopAccountPanel(); } } }In a real kit, the store installs it with one line in its manifest — use MyStore.Looks.Linen@0; — and osy lock. A store
may install several; each appears in the look picker on the desk (/staff/pages), and staff pick one per version of the
site: drafted, previewed as a whole site, switched on a date ([[ui-pages-kit#looks]]).
Which look is it, and what does it wear?#
The look names itself, and its theme. Put it in a namespace of its own — everything that namespace declares is the
look's: its designs, its regions, its own blocks.
namespace MyStore.Looks.Linen;
/// Undyed linen, ink-blue words, one rust accent for the thing to press.
look Linen {
Label = "Linen";
Theme = LinenTheme;
}
theme LinenTheme {
Colors {
Bg = "#F6F2EA";
OnBg = "#1D2A3A";
Surface = "#EDE6DA";
OnSurface = "#1D2A3A";
Border = "#D9D0C1";
TextMuted = "#6B6A66";
TextSecondary = "#3E4652";
Primary = Palette.From("#A2472B");
OnPrimary = "#FFFFFF";
OnImage = "#FFFFFF";
}
Font {
Display = "\"Fraunces\", Georgia, serif";
Sans = "\"Hanken Grotesk\", system-ui, sans-serif";
}
FontSize { Mega = "clamp(48px, 7vw, 112px)"; Title = "26px"; Body = "15px"; }
Radius { Card = "6px"; Md = "6px"; }
}Label is what staff see in the picker; Hint, the line under it, defaults to the first sentence of the doc comment.
The theme is served only while the site wears this look, under :root[data-look="linen"], so several looks' palettes
live in one store without taking each other's names. It re-skins EVERYTHING on the page while the look is worn — the
store's own pages (the bag, the checkout) and any design that is not the look's. Name the tokens a store's pages read
(Bg, OnBg, Surface, Primary, TextMuted, …) so those pages fit too (theme tokens).
Which parts must a shop look draw?#
The shop contract's PARTS are the store's frame and pages. They have no plain design of their own, so a shop look
draws all seven. A part the worn look skips is drawn by the design the store's DEFAULT look (app.Look) gives it — or,
when that look skips it too, the first installed look that draws it — in the worn look's tokens. That keeps a store
whole, but it is somebody else's frame or page in your colours, so a finished look draws its own.
| part | what it is handed |
|---|---|
ShopFrame() | the page, as its slot — draw the header, the menu, the strip across the top, the footer, the account and the bag around it |
FrontPage() | nothing — it is a region staff arrange |
ShopSection() | content, as its slot — the look's column: margins and width |
ListingPage(title, lead, products, shelves, current) | a shelf, or everything; the store's own slot empty { … } for an empty one |
ProductGrid(products) | the pieces to lay out |
ProductPage(product, note) | the piece with its images, variants, specs and shelf read, or null; the store's line under it |
NotFound(message, linkLabel, to) | what is not here, and where to go instead |
namespace MyStore.Looks.Linen;
using Osysharp.Ui;
using Osysharp.Blocks;
using Osysharp.PageBuilder;
using Osysharp.Shop;
using Osysharp.Shop.Looks;
using Osysharp.Site.Looks;
/// The frame around every page: the menu, the strip across the top and the footer — each staff's to arrange.
[Composable]
component LinenFrame() : ShopFrame {
live var menu = SiteMenu("menu", [ new NavItem { Label = "Shop", To = "/", Exact = true } ]);
live var chrome = new AppChrome { Product = "Linen", Home = "/", Nav = menu };
render {
EditorialShell(chrome) {
SiteLook();
ShopVisitSync();
Slot;
slot promo {
Region("announcement", allow: [Announcement]) { Announcement(words: "Free delivery over 800 kr"); }
}
slot actions { ShopAccountButton(); }
slot footer {
EditorialFooter(chrome, note: "Linen") {
slot about {
Region("footer", allow: [FooterWords, FooterLink, OpeningHours, Address, FooterSignUp]) {
FooterWords(words: "Sheets, towels and table linen, woven slowly.");
}
}
}
}
}
}
}
/// A page's content column.
component LinenSection() : ShopSection {
variants { base { W = "100%"; MaxW = "1200px"; Mx = "auto"; Px = 5; } }
render { Box { Slot; } }
}
/// Something that is not here, and where to go instead.
component LinenNotFound(string message, string linkLabel = "", string to = "") : NotFound {
render {
LinenSection {
Stack(gap: 3, py: 16) {
Text(message, fontFamily: Font.Display, fontSize: FontSize.Title);
if (linkLabel != "" && to != "") { Link(href: to, label: linkLabel) { Text(linkLabel + " →"); } }
}
}
}
}Four calls in a frame are what make it a frame staff can run:
SiteLook()keeps the browser in the look the version wears, and moves it when staff switch or preview another.SiteMenu("menu", […])is the menu staff edit at the desk; the list you hand it is what it shows until they do.Region("announcement", …)andRegion("footer", …)are the strip and the footer staff arrange.ShopVisitSync()keeps the visitor's bag in step with the store.
⚑ Use the region and menu names every look uses — "menu", "announcement", "footer", "front". A region is
keyed by its name, so a store switching from Atelier to Linen keeps the strip, the menu and the footer staff arranged.
A region only your look names stays in the arrangement and waits; the desk lists it before a switch.
How does it draw the blocks staff arrange?#
The front page is a Region of blocks. What you write inside it is how it stands until staff arrange it. Draw each
block type your way by designing its contract — LinenHero(…) : Hero — with the contract's parameters in the same
names, types and order. A design that differs is refused, naming the difference ("'LinenHero' implements Hero, and
leaves out its parameter 'ground'").
namespace MyStore.Looks.Linen;
using Osysharp.Ui;
using Osysharp.Blocks;
using Osysharp.Shop.Looks;
using Osysharp.Site.Looks;
/// The front page: a region staff arrange. What is written inside it is how it stands until they do.
[AllowAnonymous]
component LinenFront() : FrontPage {
render {
Region("front", allow: [Hero, ProductRow, CategoryTiles, Quote, LinenNote]) {
Hero(title: "Woven slowly.", words: "Linen for the bed and the table.", button: "Shop the linen", link: "/shop");
ProductRow(title: "New in", count: 4);
LinenNote();
}
}
}
/// Linen's design of the general Hero: exactly the contract's parameters, drawn its own way.
[Composable]
component LinenHero(string title, Markdown words = "", Picture photo = "", Picture second = "", string button = "Shop now",
PageLink link = "/", Color ground = Colors.Surface) : Hero {
render {
Box(bg: ground, color: Colors.On(ground, Colors.OnSurface), label: "Hero: " + title) {
Stack(gap: 4, px: 5, py: 16, maxW: "1200px", mx: "auto") {
Text(title, fontFamily: Font.Display, fontSize: FontSize.Mega, lineHeight: "1");
if (words != "") { Markdown(words); }
if (button != "") { Row { Link(href: link, label: button) { Text(button + " →", color: Colors.Primary); } } }
}
}
}
}
/// A note from the weaver — a block only Linen has.
[Block("A note from the weaver")]
component LinenNote([Label("The note"), MaxLength(200)] string words = "Every sheet is washed once before it leaves us.") {
render { Text(words, fontFamily: Font.Display, fontStyle: FontStyle.Italic, px: 5, py: 10, textAlign: TextAlign.Center); }
}Three things to keep:
- Paint a ground staff chose with its ink. A contract with a
Color groundlets staff pick the ground;Colors.On(ground, …)sets the words in the ink the theme pairs with it, so a dark ground never gets dark words. - A hero with no photograph is still a hero. Staff may arrange a hero of words in one look and switch to yours; draw that case, not an empty frame waiting for a picture.
- A block of your own is any
[Block]component in the look's namespace. Staff place it like any other, and it is drawn only while the site wears your look: switching away, the desk lists it as not coming along, and it comes back with your look.
How does it draw a listing and a piece?#
The listing, the grid and a piece's page are handed the store's rows; the look decides how they stand. The shop's helpers
for a look — LookPrice, LookSecondCover, LookSizeName, CoverPhoto, AddToBag, ShopProductView — do the
store's arithmetic, so a design never re-implements a price or a bag.
namespace MyStore.Looks.Linen;
using Osysharp.Ui;
using Osysharp.Storage;
using Osysharp.Shop;
using Osysharp.Shop.Looks;
/// A listing: its name, a line about it, its sub-shelves as links, and the grid.
component LinenListing(string title, string lead, List<Product> products, List<Category> shelves, string current) : ListingPage {
render {
LinenSection {
Stack(gap: 4, py: 10) {
Text(title, fontFamily: Font.Display, fontSize: FontSize.Title);
if (lead != "") { Text(lead, color: Colors.TextSecondary); }
Row(gap: 4, wrap: Wrapping.Wrap) {
foreach (var s in shelves) {
Link(href: "/shop/" + s.Slug, label: s.Name) { Text(s.Name, color: s.Slug == current ? Colors.OnBg : Colors.TextMuted); }
}
}
if (products.Count == 0) { Slot("empty"); } else { LinenGrid(products); }
}
}
}
}
/// Pieces in a grid, each with its photograph, name and price.
[AllowAnonymous]
component LinenGrid(List<Product> products) : ProductGrid {
live var shop = ShopSettings.FirstOrDefault();
render {
Box(display: Display.Grid, cols: "repeat(auto-fill, minmax(220px, 1fr))", gap: 5) {
foreach (var p in products) {
Link(href: "/p/" + p.Slug, label: p.Name) {
Stack(gap: 2) {
Image(src: File.Url(CoverPhoto(p.Images)), alt: p.Name, w: "100%", aspectRatio: "4 / 5", objectFit: ObjectFit.Cover);
Text(p.Name);
Text(LookPrice(p.Price, shop), color: Colors.TextMuted);
}
}
}
}
}
}
/// One piece's page: its photograph, name and price, and the way into the bag.
[AllowAnonymous]
component LinenProductPage(Product? product, string note = "") : ProductPage {
live var shop = ShopSettings.FirstOrDefault();
live var first = product == null ? null : product.Variants.OrderBy(v => v.Rank).FirstOrDefault();
string problem = "";
action Add() { problem = first == null ? "This piece cannot be bought." : AddToBag(first, 1); }
render {
if (product == null) {
LinenNotFound("That piece is not in the shop.", linkLabel: "Back to the shop", to: "/shop");
} else {
LinenSection {
Stack(gap: 4, py: 10, maxW: "640px") {
Image(src: File.Url(CoverPhoto(product.Images)), alt: product.Name, w: "100%", objectFit: ObjectFit.Cover);
Text(product.Name, fontFamily: Font.Display, fontSize: FontSize.Title);
Text(LookPrice(product.Price, shop));
ShopProductView(product);
Button("Add to bag", onPress: Add, tone: Tone.Primary);
if (problem != "") { Text(problem, role: UiRole.Alert, color: Colors.Primary); }
if (note != "") { Text(note, color: Colors.TextMuted); }
}
}
}
}
}The shop's own looks (kits/look-*) go further on this page — sizes as chips, every photograph, the spec sheet, "notify me when
it is back" — and are the place to copy from.
What happens to a type the look skips?#
Draw only the block types you want in your own design. For the rest:
| the type | while a store wears your look |
|---|---|
| a block type with a plain design — in a shop, every type (the shop kit draws them) | drawn in that plain design, in YOUR theme's tokens |
| a block type no design draws | not offered to staff on the desk; one already placed waits in the arrangement and is listed before a switch |
| a shop PART (the frame, a page) | drawn by the store's default look's design of it, in YOUR tokens — so draw all seven (#parts) |
That is why Linen designs only the hero: the product row, the shelves, the quote and the footer's blocks come out of the shop kit's plain designs dressed in Linen's palette, type and radii. Add a design of one later and every store that installed the look draws the new one on its next compile — what staff arranged does not move.
How does it ship its fonts?#
A theme NAMES a family; the font file is what makes it render (web fonts — shipping a typeface with your app). Put the files in the kit's fonts/
and register each in the kit's own lock, from the kit's folder:
$ cd looks/linen
$ osy font add fonts/Fraunces.ttf --family "Fraunces"
$ osy font add fonts/HankenGrotesk.ttf --family "Hanken Grotesk"Keep each font's licence beside it (the looks in this repository ship OFL-*.txt). The look's fonts ship with every
compile of a store that installs it — pinned from a workspace or from the published package alike: publishing packs the
files under fonts/ with the record of which family each one is, read from the kit's own lock. Two looks naming one
face upload it once.
How do I see it in a store?#
A look has no data of its own, so it is seen in a store. Install it in one — the Vall fashion store wears five looks,
the Second Round thrift store five more — and write a test that switches the version's look and photographs every page
a customer walks through, at a phone's width and a wide screen's. This is the walk Apps/fashion-store/vall runs for
each of its looks:
void WalkThrough(string look, string front) {
runas (Staff) { Assert.Equal("", SetLook(MainVersion().Id, look)); }
Visit(look, front, 390, 844, "phone");
Visit(look, front, 1440, 900, "wide");
}
void Visit(string look, string front, int width, int height, string called) {
Ui.Viewport(width, height);
Ui.Visit("/");
Assert.Visible(front);
Ui.Shot($"{look} — the front, {called}");
Ui.Visit("/p/cotton-trench-coat");
Ui.Click("M");
Ui.Click("Add to bag");
Ui.Shot($"{look} — a piece, {called}");
}
[Test(Window)]
void the_store_in_linen() { WalkThrough("linen", "Woven slowly."); }$ osy test --pixels --filter the_store_in_linen--pixels runs the walk in a real browser and writes each Ui.Shot to osy-shots/. Open them and look: a page that
passes every assertion can still put a button off the card, and only a picture shows it. SetLook answers why not, or
""; LookLeavesBehind(version, look) lists what your look has no design or region for ([[ui-looks#switch]]).
Where would a café's contract sit?#
The shop's contract is ONE category's. Another category — a café, a salon, a restaurant — declares its own beside it,
on the same site contract: the block types only it has (a menu board, a table booking) and the parts of its site (its
frame, its front page). A look for cafés then draws Osysharp.Site.Looks and the café's contract, and a store or a café
installs the looks of its own category.
app Corner {
use Osysharp.Ui;
use Osysharp.Blocks;
use Osysharp.Site.Looks@0;
model "model/**/*.osy";
}
using Cafe.Looks;
app.Look = Morning;
[Route("/")]
[AllowAnonymous]
component Front() { render { CafeFrame { CafeFront(); } } }namespace Cafe.Looks;
using Osysharp.Blocks;
/// Today's menu on the wall — a heading and the dishes, one per line.
[Block("Menu board")]
public abstract component MenuBoard(
[Label("Heading"), MaxLength(60), Sample("Today")] string title,
[Label("The dishes"), MaxLength(800), Hint("One dish a line, with its price.")] Markdown dishes = "");
/// The frame around every page of a café.
public abstract component CafeFrame();
/// A café's front page.
public abstract component CafeFront();namespace Corner.Looks.Morning;
using Osysharp.Ui;
using Osysharp.Blocks;
using Osysharp.Site.Looks;
using Cafe.Looks;
/// Chalk on slate, and the smell of the first loaf.
look Morning { Label = "Morning"; Theme = MorningTheme; }
theme MorningTheme { Colors { Bg = "#22262A"; OnBg = "#F2EFE8"; Surface = "#2E3338"; OnSurface = "#F2EFE8"; } }
component MorningFrame() : CafeFrame {
render {
Stack(gap: 0, bg: Colors.Bg, color: Colors.OnBg, minH: "100vh") {
Region("announcement", allow: [Announcement]) { Announcement(words: "Open from seven"); }
Slot;
Region("footer", allow: [FooterWords, OpeningHours, Address]) { OpeningHours(); }
}
}
}
component MorningFront() : CafeFront {
render {
Region("front", allow: [Hero, MenuBoard, Quote]) {
Hero(title: "Bread from six, coffee from seven.");
MenuBoard(title: "Today", dishes: "Cardamom bun · 35\nSoup of the day · 95");
}
}
}
component MorningHero(string title, Markdown words = "", Picture photo = "", Picture second = "", string button = "Shop now",
PageLink link = "/", Color ground = Colors.Surface) : Hero {
render { Text(title, p: 10, fontSize: FontSize.Heading); }
}
component MorningMenuBoard(string title, Markdown dishes = "") : MenuBoard {
render { Stack(gap: 2, p: 8, bg: Colors.Surface) { Text(title, fontSize: FontSize.Title); Markdown(dishes); } }
}Here the contract sits in the app, to keep the example in one place. As a kit it ships the way the shop's does: inside the café kit, in a namespace of its own, because its types name the café's data (a dish, a table) — and the café kit carries the PLAIN design of each of its types, which is what a café without a look draws, and what a look that skips a type falls back to. A design of the site contract's types (the strip, the footer's opening hours) belongs with it too: in a café without the shop kit, nothing else draws them.
What does a finished look have?#
- a
package.osythat usesOsysharp.Site.Looksand its category's contract, and a summary that names the category; - one
lookin a namespace of its own, naming a theme that sets the tokens a store's pages read; - a design of every PART of its category (for a shop: all seven);
- a design of each block type it wants in its own hand, and blocks of its own where it has an idea no contract holds;
- regions and a menu under the names every look uses, so arrangements carry across;
- its fonts in its own lock, with their licences;
- a README whose first line says what it covers — "A look for shops: draws Osysharp.Site.Looks and Osysharp.Shop.Looks" — and a picture of a store wearing it, at a phone's width and a wide screen's.
See also#
- Looks — a site's whole design, picked per version — looks, contracts and the switch: the reference this page is the walk-through for.
- The PageBuilder kit — the desk staff arrange the site at — the desk: versions, the look each wears, the preview.
- The site's frame — a menu, a strip and a footer staff change — the menu, the strip and the footer a frame declares.
- Blocks and regions — pages the staff arrange — blocks, regions, and what a value may be.
- theme tokens — tokens, modes and variants.
- web fonts — shipping a typeface with your app — shipping a typeface.
- package.osy — a kit's manifest,
osy lockand publishing.