# Ship your own variant spaday's component packages are two halves bolted together: a **Python authoring surface** (typed component classes, catalog schemas, actions, bindings) and a **JS implementation** (the bundle that registers the custom elements). The bolt is meant to come out. This guide covers the four things people want when they build on spaday rather than just using it: 1. [Theme a package from Python](#theme-a-package-from-python), without writing a stylesheet. 1. [Serve your own bundle](#serve-your-own-bundle) behind a package's Python surface. 1. [Point a component at your own element](#point-a-component-at-your-own-element) with a different tag. 1. [Share one engine](#share-one-engine-between-two-libraries) between two libraries on a page. ## Theme a package from Python `css()` sets **CSS custom properties** — any of them, not a fixed list — so a design system's own tokens are authored from Python with no stylesheet at all: ```python from spaday.components.shell import App App().css(spa_surface="#0C4253", wa_color_brand_fill_loud="#FC6B47") # → --spa-surface: #0C4253; --wa-color-brand-fill-loud: #FC6B47 ``` Kwargs are kebab-cased and `--`-prefixed (`spa_surface_2` → `--spa-surface-2`). Custom properties inherit through shadow boundaries, so setting them on the `App` root reaches every component in the tree. ### Three layers, in order A color in any spaday component package resolves through three layers: ```css var(--spa-dagre-node-fill, /* 1. the package's own token */ var(--spa-surface-2, /* 2. the shell token it belongs to */ #fafafa)) /* 3. a literal, for a standalone page */ ``` So there are two ways to theme, and you pick by how wide you want the change: ```python App().css(spa_surface_2="#1a2028") # every package's panels follow Dagre(graph=g).css(spa_dagre_node_fill="#1a2028") # only the graph ``` `spaday.theme.SHELL_TOKENS` lists the shell palette (`--spa-surface`, `--spa-surface-2`, `--spa-border`, `--spa-text`, `--spa-muted`, `--spa-accent`, `--spa-info`, `--spa-success`, `--spa-warning`, `--spa-danger`, plus layout tokens). Each package publishes its own `TOKENS` mapping. A `Token` contains the CSS property, description, and optional shell fallback: ```python from spaday_dagre import TOKENS for kwarg, token in TOKENS.items(): fallback = token.fallback or "required" print(f"{kwarg:<28} {token.property:<34} {fallback:<18} {token.description}") ``` `Token` retains the former two-item tuple interface, so existing code that unpacks `for kwarg, (prop, what) in TOKENS.items()` continues to work. Package tokens are named `--spa--`, where `` is the name you pass to `packages=[...]`. They are only ever *read* by the package's stylesheet, never defined by it — that is what lets a token set on an ancestor (an app-level theme) win over the package's default. ### Page mode The dark palette is keyed off WebAwesome's `wa-dark` class, so one binding re-themes everything: ```python App(...).bind_root_class("wa-dark", "dark") ``` `wa-light` on a subtree flips a nested island back. Both palettes are emitted at zero specificity, so your own rules always win. ### When the design system should drive `spaday-webawesome` maps WebAwesome's tokens *onto* the shell palette rather than the other way round, so restyling WebAwesome carries the shell, the graphs, the tables and the trees with it: ```python App().css(wa_color_brand_fill_loud="#0C4253") # → --spa-accent, --spa-info, and everything downstream ``` A canvas component cannot read CSS, so `spaday-lightweight-charts` samples the resolved value of its tokens and hands them to the chart. Theming it looks identical from Python. ## Render the generic controls with your own design The generic controls (`spaday.ui`) reach a design system through a `Design`: data mapping each control kind to the element that renders it and to where its generic surface lands there. A design-system package publishes one on its `ComponentPackage`; an in-house design system publishes its own the same way, and needs no Python beyond it: ```python from spaday.ui import ControlSpec, Design, Open, Options, Part, Value DESIGN = Design( name="acme", controls={ "button": ControlSpec( tag="acme-button", label=Part(kind="text"), # the label is the button's text props={"intent": "tone", "size": "size", "disabled": "disabled"}, values={"intent": {"primary": "brand"}}, # the design's word for it ), "input": ControlSpec( tag="acme-field", label=Part(kind="attr", name="label"), help=Part(kind="slot", name="hint"), # an element in the `hint` slot error=Part(kind="attr", name="error-text"), invalid={"invalid": True}, # set while an error is shown value=Value(prop="value", event="acme-change"), # written back on the design's event ), "select": ControlSpec(tag="acme-select", options=Options(kind="prop", name="items")), "dialog": ControlSpec( tag="acme-dialog", open=Open(prop="opened", event="acme-closed", methods=("show", "hide")), ), }, ) package = ComponentPackage(name="acme", ..., design=DESIGN) ``` `Part` places a label, help text or error message as an attribute, a slotted element, the control's text, a child inside it, or a sibling in a `Wrap` around it (a `bp-field`, a `fluent-field`, a plain `