Skip to content

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.

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

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.

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.

./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).

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.