WebComponents UI Facade
v0.5.15
Portable iframe facade for Wippy frontend
Run
wippy run wippy/facadewippy/facade
Portable iframe facade for the Wippy frontend. Serves a thin HTML shell that loads the Wippy frontend bundle from a CDN via iframe, with all configuration driven through ns.requirement entries.
How it works
index.htmlis served as a static file viahttp.static- On load, it fetches
GET /api/public/facade/configto get runtime configuration - Checks
localStoragefor an auth token, redirects tologin_pathif missing - Loads the Web Host bundle from CDN — picks the module file based on
fe_mode:compat(default) →module.js— full Wippy host chromemanaged→managed-layout.js— declarative multi-panel host driven byhostConfig.layout
- Calls
window.initWippyApp(config)— both entries expose the same symbol; the mounted shell is the only difference - Shows
<wippy-loading>/<wippy-error>during initialization (vendored from Wippy Web Host CDN)
Modes
| Mode | Entry loaded | Mounted shell | Use case |
|---|---|---|---|
| compat (default) | module.js | Full Wippy chrome (sidebar + chat + pages + right panel) | Backwards-compatible — existing facades keep working unchanged |
| managed | managed-layout.js | Declarative multi-panel layout — no default chrome, every panel declared via hostConfig.layout | IDE-style apps, dashboards, Adobe-style multi-pane tools |
Both entries expose the same window.initWippyApp(config, rootContainer?) signature — the parent integration code does not change between modes. Set via the fe_mode requirement; unknown values normalize to compat.
When to switch to managed mode
- You want a multi-panel app with separator-drag resizing
- You want to embed a custom panel configuration driven by config (not by the standard sidebar/chat layout)
- You want breakpoint-responsive layouts (desktop vs mobile)
Leave fe_mode at compat unless you have a specific reason to opt in — the managed shell omits every piece of default Wippy chrome (no sidebar, no chat wrapper, no right panel) and expects the declaration to provide equivalents.
Vendored CDN files
public/@wippy-fe/ contains files copied from the Wippy Web Host CDN. These are loaded before the CDN URL is known (pre-config-fetch), so they must be vendored locally.
After every Wippy Web Host version bump, run:
make sync
This downloads fresh copies from the CDN. Update WEB_HOST_CDN in the Makefile when the version changes.
Derived values
These fields are NOT configurable via requirements — they are computed at runtime:
| Field | Source | Description |
|---|---|---|
| env.APP_API_URL | PUBLIC_API_URL env var | Base URL for API calls. Falls back to window.location.origin in the browser if empty. |
| env.APP_WEBSOCKET_URL | Derived from APP_API_URL | WebSocket URL — http:// → ws://, https:// → wss:// |
| iframe_origin | Extracted from fe_facade_url | Origin portion of facade URL (e.g. https://web-host.wippy.ai), used for postMessage security |
| iframe_url | fe_facade_url + fe_entry_path + ?waitForCustomConfig | Full iframe URL passed to the Web Host |
Requirements
Infrastructure
| Requirement | Default | Description |
|---|---|---|
| server | (required) | HTTP server for static serving |
| router | (required) | Public router for config endpoint |
Core
| Requirement | Default | Description |
|---|---|---|
| fe_facade_url | https://web-host.wippy.ai/webcomponents-1.0.37 | CDN base URL for the Web Host frontend bundle |
| fe_entry_path | /iframe.html | Iframe HTML entry point path (appended to fe_facade_url) |
| fe_mode | compat | compat (default — loads module.js) or managed (loads managed-layout.js for declarative multi-panel apps). See Modes above |
App Identity
Passed to the Web Host as theming.host.i18n.app — controls branding in sidebar and navigation.
| Requirement | Default | Config path | Description |
|---|---|---|---|
| app_title | Wippy | theming.host.i18n.app.title | Short title shown in sidebar header |
| app_name | Wippy AI | theming.host.i18n.app.appName | Full application name |
| app_icon | wippy:logo | theming.host.i18n.app.icon | Iconify icon reference (e.g. custom:logo, tabler:home) |
Host Config (hostConfig)
Host-only UI flags — NOT sent to child iframes.
| Requirement | Default | Type | Description |
|---|---|---|---|
| show_admin | true | bool (~= "false") | Show admin panel and keeper controls in the sidebar |
| start_nav_open | false | bool (== "true") | Navigation drawer open by default (collapsed shows icons only) |
| hide_nav_bar | false | bool (== "true") | Completely hide the left navigation sidebar |
| disable_right_panel | false | bool (== "true") | Disable the right sidebar panel |
| allow_select_model | false | bool (== "true") | Allow LLM model selection dropdown in chat |
| hide_session_selector | false | bool (== "true") | Hide the chat session selector dropdown |
| session_type | non-persistent | string | Chat session persistence (non-persistent or cookie) |
| history_mode | hash | string | Browser history mode (hash or browser) |
Boolean parsing:
show_admindefaults totrue(any value except"false"is truthy). All other boolean flags default tofalse(only"true"is truthy).
Advanced hostConfig (JSON)
These accept JSON strings for complex configuration:
| Requirement | Default | Config path | Description |
|---|---|---|---|
| api_routes | {} | apiRoutes | API route overrides — top-level, not under hostConfig (e.g. {"agents":{"list":"/custom/agents"}}) |
| additional_nav_items | [] | hostConfig.additionalNavItems | Extra sidebar nav items as JSON array |
| state_cache | {} | hostConfig.stateCache | Child state LRU config (e.g. {"maxPages":50,"maxSizePerPage":1048576}) |
| allow_additional_tags | {} | hostConfig.allowAdditionalTags | HTML sanitizer tag whitelist (e.g. {"w-chart":["data","type"]}) |
| chat | {} | hostConfig.chat | Chat config (e.g. {"convertPasteToFile":{"enabled":true,"minFileSize":1024,"allowHtml":false}}) |
| axios_defaults | {} | axiosDefaults | HTTP client defaults (e.g. {"timeout":30000}) — top-level, not under hostConfig |
| extra_scripts | [] | extraScripts | External <script> tags injected into index.html before the Web Host bundle loads. See Extra scripts. |
Auth
| Requirement | Default | Description |
|---|---|---|
| login_path | /login.html | Path to redirect unauthenticated users (no token in localStorage) |
| login_redirect_param | "" (off) | Query param name appended to login_path carrying the user's current relative URL, so the login flow can return them after auth. Empty disables. See Post-login redirect. |
Theming
Three theming scopes control which layers see which styles:
File system for fs:// references
| Requirement | Default | Description |
|---|---|---|
| content_fs | (empty) | fs.directory entry ID whose files can be referenced via fs:// in CSS and JSON requirements. When set, any value like fs://custom-css.facade.css or fs://css-variables.facade.json is resolved to that file's content at request time. Empty disables file resolution. Example: "app:app_fs". |
When content_fs is configured, CSS requirements (custom_css, host_custom_css, children_custom_css) and JSON variable requirements (css_variables, host_css_variables, children_css_variables) all accept fs://path values in addition to inline strings.
Why
fs://and notfile://? The wippy registry loader reservesfile://for its OWN load-time interpolation — it reads afile://value relative to the_index.yamlthat contains it and inlines the file content at boot, so afile://written in a YAML requirement param never reaches the facade (and a path that doesn't resolve from the YAML's own directory fails the whole file).fs://is passed through untouched, so the facade resolves it at request time againstcontent_fs. (file://is still accepted for values set via non-YAML paths — env vars,-ooverrides, runtime registry edits — where the loader does not interpolate.)
The same fs.directory can be served as a static endpoint (http.static) so pages like login.html can <link> the same CSS files directly. See File-based theming below.
Global scope (theming.global) — applied to host AND children
| Requirement | Default | Config path | Description |
|---|---|---|---|
| custom_css | Poppins font @import | theming.global.customCSS | CSS applied everywhere — host chrome and child iframes. Accepts inline CSS or fs://path (requires content_fs). |
| css_variables | {} | theming.global.cssVariables | CSS variables applied everywhere. Supports @dark/@light variants. Accepts inline JSON or fs://path. |
| icon_sets | {} | theming.global.iconSets | Iconify icon sets as {prefix: {name: {body,width,height}}} (e.g. {"custom":{"logo":{...}}}) |
Host scope (theming.host) — applied to host chrome ONLY
| Requirement | Default | Config path | Description |
|---|---|---|---|
| host_custom_css | (empty) | theming.host.customCSS | CSS for sidebar, chat, nav — NOT applied to child iframes. Accepts inline CSS or fs://path. |
| host_css_variables | {} | theming.host.cssVariables | CSS variables for host chrome only — override global vars. Accepts inline JSON or fs://path. |
| host_icon_sets | {} | theming.host.iconSets | Icon sets for host chrome only (same format as global icon_sets) |
Children scope (theming.children) — applied to child iframes ONLY
| Requirement | Default | Config path | Description |
|---|---|---|---|
| children_custom_css | (empty) | theming.children.customCSS | CSS for child iframes only — NOT applied to host chrome. Accepts inline CSS or fs://path. |
| children_css_variables | {} | theming.children.cssVariables | CSS variables for children only — override global vars. Accepts inline JSON or fs://path. |
Merge rules: Host sees
global + hostmerged. Children seeglobal + childrenmerged. Host-scope styles never leak to children and vice versa. Icons are only inglobalandhostscopes (children don't get their own icon sets).
Managed-layout
| Requirement | Default | Config path | Description |
|---|---|---|---|
| host_config_layout | {} | hostConfig.layout | Managed-layout HostLayoutDeclaration as JSON string. Only relevant when fe_mode = "managed". Empty (default) leaves hostConfig.layout unset, so the host falls back to URL-param / parent-SetConfig configuration paths. See gen-2-chat/managed-layout.md for the schema. |
Example — minimal 2-panel layout:
- name: fe_mode
value: managed
- name: host_config_layout
value: |
{
"layouts": {
"default": {
"direction": "horizontal",
"children": [
{ "panel": "nav", "size": "240px" },
{ "panel": "main", "size": "1fr", "main": true }
]
}
},
"panels": {
"nav": { "kind": "builtin", "id": "@HOST/nav-sidebar" },
"main": { "kind": "page", "id": "home", "route": "/" }
}
}
Usage
- name: facade
kind: ns.dependency
component: wippy/facade
version: "*"
parameters:
- name: server
value: app:gateway
- name: router
value: app:api.public
Only override what differs from defaults.
Themed page-only app (no chat sidebar)
- name: hide_nav_bar
value: "true"
- name: disable_right_panel
value: "true"
- name: show_admin
value: "false"
- name: custom_css
value: "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); body { font-family: 'Inter', sans-serif; }"
- name: css_variables
value: '{"--p-primary":"#6366f1"}'
Host-only styling (chat area gets custom look, children stay default)
- name: host_custom_css
value: ".chat-message { border-radius: 0; } .chat-input { font-size: 15px; }"
- name: host_css_variables
value: '{"--wippy-host-message-bg":"#f8f9fa","--wippy-host-input-bg":"#ffffff"}'
Children-only styling (embedded pages get branded, host stays default)
- name: children_css_variables
value: '{"--p-primary":"#dc2626"}'
- name: children_custom_css
value: "body { font-family: 'Comic Sans MS', cursive; }"
File-based theming
Instead of inlining CSS or JSON in YAML, store theming files in an fs.directory and reference them with fs://path. Keep them in the same folder you already serve static assets from (next to login.html), so one place backs both the facade config and the page <link>s.
Naming — name each file after the requirement it fills, with a .facade. infix:
custom-css.facade.css, css-variables.facade.json, host-custom-css.facade.css, … It keeps them self-documenting and easy to spot among other static assets.
Step 1 — reuse the static dir you already serve. For example an app that serves ./static at /app:
- name: app_fs
kind: fs.directory
directory: ./static
auto_init: true
- name: app_static
kind: http.static
meta:
server: app:gateway
fs: app:app_fs
path: /app
Step 2 — place the theming files in that folder, next to login.html:
static/
login.html
custom-css.facade.css # @import url(...); :root { ... }
css-variables.facade.json # {"--p-primary":"#6366f1","@dark":{"--p-primary":"#818cf8"}}
Step 3 — point content_fs at that fs.directory and reference the files with fs://:
- name: content_fs
value: app:app_fs
- name: custom_css
value: "fs://custom-css.facade.css"
- name: css_variables
value: "fs://css-variables.facade.json"
Result:
GET /api/public/facade/config→theming.global.customCSScontains the file content,theming.global.cssVariablescontains the parsed JSON object (resolved at request time fromcontent_fs)GET /api/public/facade/variables.css→ generates CSS fromcss-variables.facade.jsonGET /app/custom-css.facade.css→ serves the raw file (viaapp_static)login.htmlcan use both:
<link rel="stylesheet" href="/app/custom-css.facade.css">
<link rel="stylesheet" href="/api/public/facade/variables.css">
To relocate the assets later, change the one directory: line on the fs.directory — the fs:// references and content_fs follow it.
Custom icons
- name: icon_sets
value: '{"custom":{"logo":{"body":"<svg viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\"/></svg>","width":24,"height":24}}}'
- name: app_icon
value: "custom:logo"
Post-login redirect
Off by default. When enabled, the facade appends the current page's relative URL to login_path so the login flow can return the user to where they were after authenticating.
- name: login_redirect_param
value: "redirect_to"
A user opening a deep link like /c/abc-123 without a valid token will be sent to:
/login.html?redirect_to=%2Fc%2Fabc-123
The login page reads redirect_to from the query string and navigates the user there after successful auth.
Extra scripts
Inject external <script> tags into the facade index.html before the Web Host bundle loads. Useful for host-context integrations (analytics, third-party bridges) that need to run in the top-level window, not inside child iframes.
Each entry is either a string (shorthand for { src }) or an object with any of: src (required), async, defer, type, noModule, crossorigin, integrity.
- name: extra_scripts
value: '["/bridge.js"]'
Object form:
- name: extra_scripts
value: '[{"src":"/bridge.js"},{"src":"https://cdn.example.com/analytics.js","defer":true}]'
Scripts are fetched in parallel and awaited before the Web Host bundle is imported, so any globals they define are available to the bundle. Load failures are logged to console.warn and do NOT block app startup.
Security: entries come from
ns.requirementdefaults — i.e. from the application owner, not from end users — so arbitrary URLs in this list are trusted. Still, preferintegrityhashes for third-party CDN scripts.
Endpoints
GET /api/public/facade/config
Returns the full facade configuration as JSON (wippy-context-2.0 format). Used by index.html on load; see Config Response below.
GET /api/public/facade/variables.css
Returns the global CSS variables (css_variables requirement) as a text/css stylesheet. Useful for non-Wippy-Host pages (e.g. login.html) that need the same brand variables without embedding the full Web Host.
The CSS follows the same algorithm as the Web Host's createCssVariables():
- Flat string keys →
:root { --key: value; } @darkobject →@media (prefers-color-scheme: dark) { :root { ... } }@lightobject →@media (prefers-color-scheme: light) { :root { ... } }- Keys without
--prefix get--prepended automatically
Returns an empty body (200 OK) when no variables are configured. Response has Cache-Control: public, max-age=3600.
Usage in login.html:
<link rel="stylesheet" href="/api/public/facade/variables.css">
Config Response
GET /api/public/facade/config returns (wippy-context-2.0 format):
{
"facade_url": "https://web-host.wippy.ai/webcomponents-1.0.37",
"iframe_origin": "https://web-host.wippy.ai",
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.37/iframe.html?waitForCustomConfig",
"login_path": "/login.html",
"login_redirect_param": null,
"env": {
"APP_API_URL": "http://localhost:8085",
"APP_AUTH_API_URL": "http://localhost:8085",
"APP_WEBSOCKET_URL": "ws://localhost:8085"
},
"routePrefix": "http://localhost:8085",
"theming": {
"global": {
"customCSS": "@import url('https://fonts.googleapis.com/css2?family=Poppins...');"
},
"host": {
"i18n": { "app": { "title": "Wippy", "icon": "wippy:logo", "appName": "Wippy AI" } }
},
"children": null
},
"hostConfig": {
"session": { "type": "non-persistent" },
"history": "hash",
"showAdmin": true,
"allowSelectModel": false,
"startNavOpen": false,
"hideNavBar": false,
"disableRightPanel": false,
"hideSessionSelector": false
},
"extraScripts": null
}
How index.html uses the config
fetch /api/public/facade/config
→ check localStorage for auth token → redirect to login_path if missing
→ import(facade_url + '/module.js') → load Web Host bundle from CDN
→ initWippyApp({ $schema, auth, env, routePrefix, theming, hostConfig, context }, '#app')
→ listen for 'authExpired' and 'error' events → redirect to login_path
The backend returns config in wippy-context-2.0 shape. index.html only adds $schema (from facade_url), auth (from localStorage), and context (empty default). All other fields pass through from the backend unchanged.
If any step fails (config fetch, CDN import, missing initWippyApp), the page shows a themed <wippy-error> screen with title and details. No external CSS is required — both <wippy-loading> and <wippy-error> are self-contained web components with Shadow DOM styles.
Publishing
cd src/facade && wippy publish
The wippy.yaml includes embed: ["public_files"] to bundle index.html into the wapp package.