A Pict section that gives every Pict / Retold web application the same
out-of-the-box chrome: branded top bar, branded bottom bar, theme picker,
mode toggle, scale select, and a deterministic project-name β SVG logo
generator. One addProvider call wires it all together.
The point: stop rewriting the same boilerplate in every app. Branding, navigation chrome, theme switching, dark-mode handling, and favicons are solved problems β this module is the solved version.
npm install pict-section-themepict-section-modal is an optional peer dependency. The Theme-Button uses
it for the popup menu, and Theme-TopBar / Theme-BottomBar are designed to
mount inside its shell() panels β but every other view works without it.
In your application bootstrap:
const libPictApplication = require('pict-application');
const libPictSectionTheme = require('pict-section-theme');
const libBrand = require('./MyApp-Brand.js'); // see "Brand precompute" below
class MyApplication extends libPictApplication
{
constructor(pFable, pOptions, pServiceHash)
{
super(pFable, pOptions, pServiceHash);
// Register the standard chrome + theme machinery in one call.
// Self-bootstraps: theme runtime, catalog, views, persistence,
// brand, and the shared TopBar/BottomBar all wired up.
this.pict.addProvider('Theme-Section',
{
ApplyDefault: 'pict-default',
DefaultMode: 'system',
DefaultScale: 1.0,
Brand: libBrand,
Views: ['Picker', 'ModeToggle', 'ScaleSelect', 'Button',
'BrandMark', 'TopBar', 'BottomBar'],
ViewOptions:
{
TopBar: { NavView: 'MyApp-TopBar-Nav', UserView: 'MyApp-TopBar-User', Height: 56 },
BottomBar: { StatusView: 'MyApp-StatusBar', Height: 32 }
}
}, libPictSectionTheme);
}
}After this, the rest of your app provides:
- A small NavView that fills
#Theme-TopBar-Nav(your action buttons / links / breadcrumbs). - A small UserView that fills
#Theme-TopBar-User(account widgets, log toggles, custom indicators). - A small StatusView that fills
#Theme-BottomBar-Status(status text). - A layout view that drops the Theme-TopBar / Theme-BottomBar destinations
somewhere in the DOM (typically inside
pict-section-modal'sshell()panels).
That's it. The brand mark, theme button, light/dark mode, and the brand-tinted top + bottom stripes are all handled.
Every app has a brand: a name, a tagline, two colors, an icon, and
favicons. Don't generate these at runtime. Precompute them at build
time and persist into package.json under retold.brand. The runtime
brand module is then 5 lines and the LogoGenerator never enters your
production bundle.
The bundled CLI does this:
# manifest-driven (Retold-ecosystem apps with a Retold-Modules-Manifest.json)
npx pict-section-theme-brand \
--manifest ../../Retold-Modules-Manifest.json \
--module my-app \
--favicons web-application/favicons
# standalone (any Pict app β uses package.json's `name` + flags)
npx pict-section-theme-brand \
--palette ocean \
--display-name "My App" \
--favicons public/faviconsBoth modes write into the target package.json:
{
"retold": {
"brand": {
"Hash": "my-app",
"Name": "My App",
"Tagline": "Description from package.json",
"Palette": "ocean",
"Icon": "<svg>...</svg>",
"IconType": "svg",
"Favicon": "<svg>...</svg>",
"FaviconDark": "<svg>...</svg>",
"Colors": {
"Primary": "#2f97b4",
"Secondary": "#e07e40",
"PrimaryLight": "#2f97b4",
"PrimaryDark": "#6dbcd2",
"SecondaryLight": "#e07e40",
"SecondaryDark": "#e9b493"
}
}
}
}β¦and (when --favicons is supplied) a directory of favicon.svg,
favicon-{16,32,48,64}.png, apple-touch-icon.png, favicon-{192,512}.png.
The runtime brand module then becomes:
// MyApp-Brand.js
module.exports = require('../../package.json').retold.brand;Wire it into your build script:
{
"scripts": {
"brand": "pict-section-theme-brand --palette ocean --favicons web-application/favicons",
"prebuild": "npm run brand",
"build": "npx quack build"
}
}prebuild is an npm convention β it runs automatically before build.
| Manifest | Standalone | |
|---|---|---|
| Used by | Retold-ecosystem apps | Any Pict app |
| Source of truth | Retold-Modules-Manifest.json Branding block |
package.json name + CLI flags |
| Required flags | --manifest, --module |
none |
| Optional overrides | (Branding block) | --palette, --display-name, --tagline |
| Default palette | (none β must specify in Branding) | mix |
Standalone mode also reads optional defaults from package.json under
retold.brandConfig:
{
"retold": {
"brandConfig": {
"Palette": "ocean",
"DisplayName": "My App",
"Tagline": "What it does"
}
}
}CLI flags always win over brandConfig over package defaults. Re-running
the CLI with no flags reproduces the same brand.
mix (default), default, desert, ocean, forest, synthwave,
twilight, cosmos, carnival. mix deterministically picks one of
synthwave/ocean/desert per project β every project is internally
cohesive, the ecosystem feels varied.
Renders into #Theme-TopBar by default. Standard layout:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β [Brand-Mark] [ββ Nav slot (flex-grow) ββ] [User-slot] [β Theme] β
β βββββββββββββββββ brand-primary stripe βββββββββββββββββββββββββ β
β βββββββββββββββββ brand-secondary hairline ββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The brand stripes at the bottom are how you tell six tabs of different apps apart at a glance β every app gets a unique color combination from its deterministic logo.
Slots
#Theme-TopBar-Navβ host fills viaNavViewoption#Theme-TopBar-Userβ host fills viaUserViewoption
Options
| Option | Default | Notes |
|---|---|---|
NavView |
null |
Identifier of the host's nav slot view |
UserView |
null |
Identifier of the host's user-area slot view |
Height |
56 |
px; match this to your panel Size |
NavAlign |
'right' |
'left', 'right', or 'center' |
CompactBreakpoint |
900 |
px viewport width below which nav + user-slot collapse into a burger menu. 0 disables. Convention: ~1024 nav-heavy, ~900 default, ~768 minimal-nav, ~600 mobile-only |
MountBrandMark |
true |
Set false to skip the auto Theme-Brand-Mark mount |
MountThemeButton |
true |
Set false to skip the auto Theme-Button mount |
Per-route swapping
// In your router callback
this.pict.views['Theme-TopBar'].setNavView('MyApp-Editor-Nav');
this.pict.views['Theme-TopBar'].setUserView('MyApp-Account-Widget');Renders into #Theme-BottomBar by default. Three slots:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β βββββββββββββββββ brand-secondary hairline ββββββββββββββββββββββ β
β βββββββββββββββββ brand-primary thin line βββββββββββββββββββββββ β
β Status text [ββ Info slot (flex) ββ] [actions] β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Slots
#Theme-BottomBar-Statusβ host fills viaStatusViewoption#Theme-BottomBar-Infoβ host fills viaInfoViewoption (centered)#Theme-BottomBar-Actionsβ host fills viaActionsViewoption
Options
| Option | Default | Notes |
|---|---|---|
StatusView |
null |
Identifier of the host's status slot view |
InfoView |
null |
Identifier of the host's info slot view |
ActionsView |
null |
Identifier of the host's actions slot view |
Height |
32 |
px |
Per-route swapping: setStatusView(), setInfoView(), setActionsView().
Mark the current nav button with the W3C-standard aria-current="page".
Theme-TopBar's CSS keys off the attribute selector and applies a
brand-tinted highlight automatically:
<!-- In your NavView template -->
<button aria-current="{~D:Record.IsActive~}"
onclick="...navigateTo('/foo')">Foo</button>// In your NavView's onBeforeRender
onBeforeRender()
{
return Object.assign({}, this.pict.AppData.MyApp,
{
IsActive: (this.pict.AppData.MyApp.CurrentRoute === 'foo') ? 'page' : ''
});
}The empty string is fine β only aria-current="page" matches the
selector, blank/missing renders un-styled.
Two responsive breakpoints handle the typical "this app got too narrow" cases without the host writing any code:
Below 720px (default β the brand-mark breakpoint):
.pict-theme-brand-mark-name collapses to icon-only. The deterministic
logo is recognisable on its own.
Below 900px (default β the CompactBreakpoint option on Theme-TopBar):
The nav slot and user-slot hide; a burger button (β°) appears in their
place. Clicking the burger opens a pict-section-modal popup containing
a clone of the original nav + user DOM, so every action remains
reachable. Inline onclick="..." handlers on the cloned buttons keep
working because they resolve _Pict at click time.
900px is the conventional "narrow desktop window" breakpoint β a docked window next to another app or a half-screen split typically lands in this range. Most desktop users will hit it while shrinking, where 600px is mobile-only territory most desktops never reach.
Coordinate with the shell β ResponsiveDrawer on side panels.
If your layout uses pict-section-modal's shell() API with a sidebar
or other side panel, the topbar's burger breakpoint won't actually
trigger unless the sidebar gets out of the way too β the sidebar's
MinSize plus the workspace's min-content width pins the page
horizontally and the browser window can't shrink past that point.
Pass ResponsiveDrawer: <breakpoint> to the side panel's
addPanel() call. The shell registers a matchMedia listener and
flips the middle row's layout from row to column at that
viewport width β the side panel stretches to full width and becomes
a top drawer above the workspace (mirroring retold-remote's
content-editor pattern). Above the breakpoint it snaps back into the
docked column. The user's collapse / expand keeps working in both
modes: collapsed in drawer mode just gives the panel height: 0
(only the collapse tab is visible), expanded restores the drawer to
its DrawerHeight (default 33vh).
// Sidebar β flips to top drawer at the same threshold as the topbar
this._shell.addPanel({
Hash: 'sidebar',
Side: 'left',
Mode: 'resizable',
Size: 280,
ResponsiveDrawer: 900, // matches Theme-TopBar's CompactBreakpoint
DrawerHeight: '33vh', // optional; CSS units (px/vh/%) accepted
// ... other options
});This is the conventional "responsive sidebar" pattern: at narrow widths the sidebar moves above the workspace (giving content full width when collapsed, full height when the user opts to see the sidebar), instead of pinching the workspace into uselessness.
Override per-app from the recommended ladder:
ViewOptions:
{
// Nav-heavy app with 6+ buttons + a brand mark β earlier collapse
TopBar: { ..., CompactBreakpoint: 1024 }
// Minimal nav, less crowded β later collapse
TopBar: { ..., CompactBreakpoint: 768 }
// Mobile-only collapse β only triggers at true phone widths
TopBar: { ..., CompactBreakpoint: 600 }
// Disable compact mode entirely (desktop-only app)
TopBar: { ..., CompactBreakpoint: 0 }
}Pass 0 to disable compact mode entirely (the burger stays hidden and
the nav + user-slot always show β useful for desktop-only apps that
don't need a mobile layout).
Between 600β720px (or whatever your breakpoints are), the nav slot scrolls horizontally instead of clipping. This is silent degradation β fine for moderately-overflowed nav rows but if you have a lot of buttons, consider an in-template "More βΎ" button:
<!-- in your NavView template -->
<button class="action" onclick="document.querySelector('#my-more-menu').classList.toggle('open')">More βΎ</button>
<div id="my-more-menu" class="more-popup">
<!-- low-priority items go here -->
</div>True priority+ overflow detection (auto-detect which buttons fit and roll the rest into a "More βΎ" menu at runtime) is not currently built in β call it out as a future enhancement if you need it.
The default openBurgerMenu() clones the existing nav + user DOM into
the popup. If you want different content (e.g. a richer in-app menu,
auth widgets, a search box), override the method on the instance:
this.pict.views['Theme-TopBar'].openBurgerMenu = function ()
{
let tmpModal = this.pict.views['Pict-Section-Modal'];
return tmpModal.show({
title: 'Menu',
content: this.pict.parseTemplateByHash('MyApp-BurgerMenu', {}),
width: '320px',
closeable: true,
buttons: [],
onOpen: () => this.pict.views['MyApp-BurgerMenu-Content'].render()
});
};The default export is the PictSectionThemeProvider class, ready for
addProvider().
Named exports:
| Export | Type | What it is |
|---|---|---|
default_configuration |
object | Provider config defaults |
Provider |
class | The underlying pict-provider-theme runtime |
PictSectionThemeProvider |
class | Same as default export, named |
PickerView |
class | Theme-Picker view |
ModeToggleView |
class | Theme-ModeToggle view |
ScaleSelectView |
class | Theme-ScaleSelect view |
ButtonView |
class | Theme-Button view |
BrandStripView |
class | Theme-BrandStrip view (multi-row stripe chrome) |
BrandMarkView |
class | Theme-Brand-Mark view (inline icon+name) |
TopBarView |
class | Theme-TopBar view |
BottomBarView |
class | Theme-BottomBar view |
Catalog |
object | Theme registry singleton |
Brand |
object | Theme-Brand helper |
Scale |
object | Theme-Scale helper |
Persistence |
object | Theme-Persistence helper |
registerCatalog(pict) |
function | Push registry themes into the runtime |
listCatalog() |
function | Picker-friendly metadata for every registered theme |
install(pict, options) |
function | Legacy bootstrap (delegates to provider) |
clearPersistence(pict) |
function | Wipe the saved theme/mode/scale entry |
The deterministic logo generator lives at
pict-section-theme/source/Theme-Logo.js β require it directly when
you need it (the build CLI does), but it's intentionally kept out of
the main exports so app bundles don't ship the generator code.
| Option | Default | Notes |
|---|---|---|
ApplyDefault |
null |
Theme hash to apply at boot |
DefaultMode |
null |
'light' / 'dark' / 'system' / null |
DefaultScale |
null |
0.75β2.0 |
Persistence |
true |
Persist theme/mode/scale to localStorage |
PersistenceKey |
null |
Storage scope; null β window.location.hostname |
RegisterCatalog |
true |
Register every bundled theme |
Views |
null |
Array of view shortnames; null β all |
ViewOptions |
null |
Per-view option overrides |
Brand |
null |
The retold.brand block from your package.json |
ProviderOptions |
null |
pict-provider-theme overrides |
Precompute brand into a target package.json. See Brand precompute above.
Standalone favicon writer; takes a brand JS or JSON file and writes the favicon set. Useful when the brand is defined inline rather than coming from package.json.
pict-section-theme-favicons \
--brand path/to/MyBrand.js \
--out public/favicons \
[--print-tags]--print-tags prints the recommended <link rel="icon"> snippet for
your index.html.
You probably have something like this today:
PictView-MyApp-TopBar.js β combined brand + nav + theme button (200+ lines)
PictView-MyApp-StatusBar.js β combined status text (50 lines)
MyApp-Brand.js β hardcoded SVG / colors (60 lines)
public/favicons/ β hand-edited
css/myapp-themes.css β hand-rolled --color-* tokens
After migration:
PictView-MyApp-TopBar-Nav.js β just nav buttons (30 lines)
PictView-MyApp-TopBar-User.js β user widgets (20 lines)
PictView-MyApp-StatusBar.js β status text (15 lines)
MyApp-Brand.js β 1 require + 1 module.exports (5 lines)
public/favicons/ β generated by CLI
β theme tokens come from pict-section-theme
Concrete pilot: see retold-manager's bootstrap in
source/retold-manager/source/web/client/pict-app/Pict-Application-RetoldManager.js.
MIT β Steven Velozo