Components: @archont561/qgis-sdk-ui
The web half of the UI catalogue (decision-55) is a second npm package,
@archont561/qgis-sdk-ui (decision-47
point 3). It peer-depends on this package through a caret range,
has lit as its only dependency, and ships dist, README.md and
LICENSE - nothing else (a pack test keeps it so). Like this package it
has an unscoped alias, qgis-sdk-ui (packages/qgis-sdk-ui-alias), that
re-exports every entry and @imports both stylesheets at the exact same
version; a ui-v* tag publishes the pair.
The elements
Section titled “The elements”Every element is a Lit element rendering into itself - no shadow root -
so the page’s stylesheet reaches each control, a <label for> works, and
querySelector finds what a test or a script needs. Each carries
data-qgis-sdk="<name>" on the host element and data-part hooks inside
(label, control, error, and per element pick, cancel, clear);
an error renders as data-part="error" and is aria-describedby from the
control.
| Element | Mirrors (native) | Reads | Raises |
|---|---|---|---|
<qgis-form> |
Form |
.store (a FormStore), .qgis, .pickers, submit-label |
qgis-change, qgis-run, qgis-result |
<qgis-layer-picker> |
LayerPicker |
.qgis, geometry, nullable, required |
qgis-change |
<qgis-field-picker> |
FieldPicker |
.qgis, .layer or .of (a layer picker to follow), nullable, required |
qgis-change |
<qgis-crs-picker> |
CrsPicker |
.suggestions, .value, required |
qgis-change |
<qgis-distance> |
Distance |
unit, .min, .max, .step, .value, required |
qgis-change |
<qgis-choice> |
Choice |
.options (alias .choices), .value, nullable, required |
qgis-change (typed value) |
<qgis-file-input>, <qgis-directory-input> |
FileInput, DirectoryInput |
.pick (a FileRef picker), .filters, .value, required |
qgis-change (a FileRef or null) |
<qgis-job-progress> |
JobProgress |
status, .fraction, message, .store, .cancel, cancellable |
qgis-cancel |
<qgis-log-view> |
LogView |
.lines, limit, .store, clearable |
- |
<qgis-run-button> |
RunButton |
.form or .operation + .input, text (alias of label), pending-label |
qgis-run, qgis-result |
The keyword forms, both spellings
Section titled “The keyword forms, both spellings”A component reads the same in Python and in a page for the names both
catalogues have. The Python keyword is the constructor
argument of qgis_sdk.ui; the web spelling is the element property (the
.prop binding in Lit, the prop of a wrapper), with the attribute where one
exists. What one side has and the other does not is said so, not papered
over.
| Component | Python keyword | Web property | Note |
|---|---|---|---|
| every input | name= |
name |
the field name; data-qgis-sdk on the web, objectName form/<name> natively |
| every component | label= |
label |
derived from the name natively; empty renders none on the web |
| every input | required= |
required |
the control gets required/aria-required on the web; Form.submit() refuses an empty required field natively |
| every input | value= |
.value |
same shape as the wire model (FileRef, AUTHORITY:CODE, the typed choice) |
| - | - | disabled, error, nullable, placeholder |
web only: the page’s own state; a native component is disabled through its widget, an error is the form’s |
Choice |
options= |
.options |
.choices stays as an alias |
LayerPicker |
kind= (vector/raster/any) |
- | the web picker lists vector layers; geometry (web only) narrows by geometry type |
FieldPicker |
of=<LayerPicker> |
.of=${picker} or .layer="<id>" |
of follows the picker’s qgis-change; layer takes the id directly |
FileInput |
filters= |
.filters |
the same glob strings |
Distance |
- | unit, .min, .max, .step |
natively a Distance is metres with the host’s spin box; the web input says its unit |
JobProgress |
cancellable= |
cancellable |
the web button also needs a .cancel callback to call |
LogView |
limit= |
limit |
clearable (web) is clear() (native) |
Button/RunButton |
text= |
text (alias of label) |
the caption |
RunButton |
action_id=, values=, progress= |
.form or .operation + .input |
the web button runs a store; the native one dispatches an action id through ctx.actions |
Form |
fields=, action_id= |
.store (formStore(qgis, actionId)) |
the web form reads its fields from the action’s schema in the store |
A wrapper (./react, ./vue, ./svelte) forwards every property above;
ELEMENT_PROPS in the package lists them per tag and a test keeps it equal
to the elements’ reactive properties.
import "@archont561/qgis-sdk-ui/define" registers the whole catalogue
(DEFINED lists what it registered); defineElements(registry?) from the
root does the same on demand and leaves a tag another script took. The
host-bound pickers are layersQuery and layerFieldsQuery behind an element:
without the layers.read grant they show the refusal and its remedy, and a
host fault lands under the control as data-part="error". A page never
names a path: a file input holds a host-minted FileRef and shows its
name.
Styling: style.css, the tokens and ./uno
Section titled “Styling: style.css, the tokens and ./uno”Two stylesheets ship. tokens.css sets the --qgis-sdk-* custom
properties - the light defaults on :root, the dark ones under
[data-qgis-sdk-theme="dark"]. style.css is compiled from the package’s
own sources by UnoCSS preset-mini with every utility qs- prefixed and
no preflight, so it sits beside a page’s Tailwind or Uno classes without
a selector in common (a collision fixture proves it), and every colour, font
and radius in it is a var(--qgis-sdk-*) - a palette change is a variable
change, never a rebuild. The build refuses a style.css over its documented
byte budget.
The tokens (TOKEN_NAMES) are the theme surface decision-16 names: bg,
fg, surface, surface-fg, control, control-fg, accent,
accent-fg, border, muted, link, danger, font, font-size,
radius, space. applyHostTheme(qgis) waits for the handshake, maps the
host palette’s roles onto them (PALETTE_ROLES: window → bg,
highlight → accent, …) over the matching light or dark defaults, sets
them on <html> and data-qgis-sdk-theme; outside QGIS it resolves null
and the stylesheet defaults stay. applyTheme(tokens), clearTheme(),
tokensFromTheme(theme) and tokensStylesheet() are the pieces.
A page that runs UnoCSS itself imports the preset instead of the
stylesheet: presetQgisSdk() from ./uno (peer @unocss/preset-mini)
builds the same qs- utilities over the same token colours in the
consumer’s own pipeline.
Wrappers
Section titled “Wrappers”./react wraps every element with @lit/react (QgisForm,
QgisLayerPicker, … with onChange, onRun, onResult, onCancel
props and a ref to the element); ./vue exports components that bind
every prop as a property and re-emit change, result, run and cancel,
plus a components map for app.component; ./svelte needs no wrapper -
Svelte 5 sets properties on a registered element - and exports ON (the
event attribute names) and the use:qgisProps={props} action for a
<svelte:element>. All three pass one behaviour suite
(tests/wrapper-behaviour.ts).
Storybook and the stories as fixtures
Section titled “Storybook and the stories as fixtures”bun run storybook (in packages/qgis-sdk-ui) builds the package and
serves the catalogue; bun run storybook:build writes storybook-static/,
which CI builds on every run (pixi run -e bun storybook-build) and the docs
workflow publishes at
archont561.github.io/qgis-sdk/storybook/.
The toolbar’s Theme entry
switches the QGIS light and dark palettes through the tokens, the a11y addon
runs with test: "error", and every story renders against
createBridgeHarness - “Refused” and “Faulted” are states of a component,
not broken stories. The same stories are fixtures: tests/stories.test.ts
composes them with composeStories under the preview’s annotations and
checks, per story, light-DOM rendering, the hooks, label-to-control
association and, for the form, that the focus order follows the declaration
and ends on the run button.
Inside QGIS the page’s variables match the running palette: the WebEngine
harness (QGIS_SDK_SCAFFOLD_WEB=1, test_scaffold_web_native.py) loads the
scaffolded Tier 1 pages, which call applyHostTheme(qgis) on start, and
after the ping reads the document’s computed --qgis-sdk-bg and
--qgis-sdk-fg back: they equal the window and windowText colours
palette_theme() read from the running QApplication, and
data-qgis-sdk-theme says which side the defaults came from.