pseudo_ui 0.8.5
pseudo_ui: ^0.8.5 copied to clipboard
Server-Driven UI rendering engine for Flutter. Define UI with JSON Schema + View JSON, render natively with Material 3 widgets. 45+ components, expression engine, conditional visibility, LOV cascading [...]
0.8.5 #
x-validation:maxField/minFieldbuilt-in rules (US 473344). A numeric bound that lives in instance data — the bank-limit ceiling on the authority screen — cannot be a JSON Schemamaximum(it takes only a literal number). The engine now evaluatesmaxField/minFielditself: the value must not exceed / fall below the reference named byparameters.field, which is a dotted form-data path or an$instance.…expression. Numeric comparison; a value or reference that is not a number passes, so a bound that is still loading never shows an error. LocalizederrorMessages, re-judged when a referenced form field changes, same asequalsField.- Cross-field dependents are found under nested properties too.
crossFieldDependentsused to scan only the top-levelproperties, so a rule onlimitDraft.accounts.timeDeposit.dailyreferencing its…transactionsibling was never re-judged when the sibling changed; it now walks the schema and returns dotted keys, the same form a nestedbindhas.
0.8.4 #
- Locale-aware numeric input (US 471659). A
TextFieldorNumberFieldbound to a numeric schema property (typeis or listsnumber/integer) groups thousands and takes the decimal separator of the view language while typing —400.000,5intr,400,000.5inen— with a numeric keyboard and the fraction digits the schema allows (integer→ none,multipleOf→ its step, else 2). The stored value is anum; the display carries the locale, the data does not. A pre-filled string is shown in the view language too ("18000,99"→18,000.99foren). Engine:deriveNumberFormat,PseudoNumberText; Flutter:PseudoNumericInput,PseudoNumberInputFormatter. validateField:pattern/minLength/maxLengthjudge strings only. JSON Schema's string keywords no longer run againstvalue.toString()of a number, so a stored400000.5is not rejected by a Turkish-text pattern on a["string", "number"]property. Strings are matched as before.
0.8.3 #
-
Dropdownopens its menu BELOW the field, capped in height. TheDropdownButtonFormFieldit was built on lays the menu over the field itself — the selected row on top of the field — and lets a long list grow to almost the full screen; on the restricted-account form the bank list covered the page title and the buttons.PseudoDropdownis now a Material 3DropdownMenu: the menu is anchored under the field (flipping above only when there is no room below), takes the field's width, and scrolls insidekPseudoDropdownMenuMaxHeight(320). It is drawn like the menu it replaces —canvasColor, elevation 8, 2px corners, 16px row inset,titleMedium— not in the M3 tintedsurfaceContainergrey. A select, not a combo box: no typing, search or filtering. Long options still wrap in the menu (#466608); the closed field stays one line. (#471456)- The field text is synced to the bound value on every build.
DropdownMenu.initialSelectionalone would keep a stale label when the value is cleared from outside (set, reset) and never show one for a value whose LOV options arrive after the first build. - The control is announced as a button carrying its label and value; the
inner read-only text field is kept out of the semantics tree. With
semantics on (the backoffice web host enables it), the browser focused
that
<input>on click and the engine sentSemanticsAction.focusto a node that cannot take focus — an assertion in debug builds. PseudoInputDecorator.buildTheme/resolveLabel/resolveHintexpress the node'sfloatingLabel/focusOutlinepolicy for controls that take anInputDecorationThemeinstead of anInputDecoration.- Host widget tests that find the field by type must look for
DropdownMenu<String>instead ofDropdownButtonFormField<String>.
- The field text is synced to the bound value on every build.
0.8.2 #
-
Text.tone— a semantic colour role for text.primary | success | error | warning | info | muted, resolved by the host theme (ColorScheme /PseudoUiThemeExt, legibility-guarded likeIcon.variant), never a colour value; orthogonal tovariant, which stays typography. A backend error message ($instance.kpsSummary.errorMessage) can now read in the error colour instead of body text. -
Icons read in the page foreground; brand emphasis is per node.
buildPseudoThemeDatanow setsiconThemetoonSurface— Material 3 painted a bareIconinonSurfaceVariant, the muted grey that made every JSONicon: "info"look disabled. Deliberately noiconButtonTheme: that theme reaches every IconButton on screen (the AppBar back arrow, a text field's suffix, host chrome). An icon that IS the call to action says so withvariant: "primary"— new onIcon.variantand onIconButton(variant, same roles as Icon) — and is painted incolorScheme.primary. -
ForEach.pagination— long lists page instead of drawing everything. Two layers that compose:pageSizewindows the rows already loaded and a 'load more' control reveals the next window; whensourceis$lookup.<name>and that x-lookup declarespagination(param,nextField, optionalsizeParam/pageSize), the same control fetches the next page from the backend once every loaded row is shown and APPENDS it to$lookup.<name>. The cursor is SDK state (never form data); a reload of the lookup (watcher /lookupaction) restarts the chain; the control hides when nothing more can be shown or fetched. Label frompagination.label, else the host'sforEach.loadMoreUI string. Engine:paginateForEach/fetchLookupPage;FormContext.lookupPaging+loadMoreLookup.
0.8.1 #
-
Dropdown: a long option WRAPS in the menu instead of being cut short. The field and the menu draw the sameDropdownMenuItem.child, so the ellipsis that keeps the closed field to one line was also cutting every row of the open menu. On the account-permission screen that made two options read identically — "Tüm hesapları izleyebilir…" and "Tüm hesapları izleyebilir ve işlem yapab…" — which is the one moment a person is trying to tell them apart.The two needs are now met separately:
selectedItemBuildergives the field its own one-line ellipsized copy, while the menu item wraps.itemHeightis released so a wrapped row may be taller than the 48px default, and each row carries that 48px back as a minimum plus its own vertical padding — without it a one-line option falls under the minimum tap target and two wrapped ones touch, turning the menu into a block of text. Flutter only; the web adapters draw a native<select>and the browser already wraps. (#466608)
0.8.0 #
-
BREAKING —
x-controlLabelsis removed, and the SDK no longer carries any of the words aFilePickersays. It shipped in0.7.1and is gone four releases later, which needs saying plainly: the design in #117 put the widget's captions on the bound schema property, beside the field's own label and messages, and rejected a host dictionary at the time. That was wrong, and the reason is a rule that only became visible once the thing was in a real flow. Three kinds of text meet on a screen and each has a different owner: the FIELD's words (its caption, why it refused THIS document) belong to the schema property; the SCREEN's words belong to the view; and the WIDGET's words — "Dosya seç", "Kaldır", "Yükleniyor…" — are identical on every file field in every flow and change only with the application's language. Writing them on a property meant every schema that ever grows aFilePickercarries its own copy of "Kaldır", and a typo in one of them is a schema deploy.What replaces it:
PseudoView.uiStrings— a host-fedkey → textmap, where a text is either a plain string or the same{tr, en}shapex-labelsuses. It is threaded ontoFormContextand reaches nestedComponentsubtrees. Seesamples/ui-strings.json.engine/ui_strings.dart—resolveUiString(dict, key, lang), which returns''for an entry the host has not written. It does not decide the fallback; the caller does.filePicker.*— the node's whole vocabulary under one prefix:tooLarge,wrongType,emptyFile,tooMany,tooLargeTotal,failed,unavailable,none,limit,limitMulti,count,choose,add,replace,remove,clear,busy.
THE KEY IS THE FALLBACK. An entry the host has not written renders as
filePicker.removeon screen. That is deliberate: an English fallback looks correct in review and ships a Turkish screen with English buttons on it, where a key is visible to whoever opens the screen and greppable in a repo.A field may still word its OWN refusals —
x-errorMessagesnow takestooLarge,wrongType,emptyFile,tooMany,tooLargeTotal,failed,unavailable— because "Onay formu 3 MB'ı aşamaz" is about that document on that field. It may NOT rename a caption.composeFilePickerTextis where that order (field → host → key) is written down, once.Migration. Delete
x-controlLabelsfrom every schema; it is ignored andgetFieldControlLabelno longer exists. Rename the refusal keys underx-errorMessages:oversize→tooLarge,empty→emptyFile,count→tooMany,total→tooLargeTotal,readFailed→failed,pickerUnavailable→unavailable. PassuiStrings:toPseudoView— a host that passes nothing gets keys on screen, which is the intended way to find out. -
BottomSheetoverlay: own chrome, capped height, scrolls inside. The sheet's children were laid out in a bareColumn(min), so a long list either overflowed (aScrollViewchild cannot bound itself in aColumn) or, withExpanded, stretched the sheet to the whole screen. The sheet now draws a drag handle (dragHandle, default true), an optional pinned header fromtitlewith a close control, and puts the children in a scroll area capped at 85% of the screen — short sheets stay short, long ones scroll inside. Authors need neitherExpandednorScrollViewin a sheet (Bank limits sheet, user-permission flow).
0.7.4 #
-
Overlay
Dialog(visibleset): a tall body scrolls inside theAlertDialog. The hosted-mode fix (#465478) left overlay mode as it was; avisible-driven Dialog with more content than the screen (Yetki Tanımı → Rol Açıklamaları, "BOTTOM OVERFLOWED BY 158 PIXELS") still overflowed. The body now sits in aFlexiblescroll area —AlertDialogalready bounds its content, so no layout probe is needed — while icon, title and actions stay put. Short bodies keep their natural height. (#466085) -
Componentreloads when itsrefchanges in place. A page whose root isComponent ref: Aupdated toComponent ref: Bby the next surface (a form composite replaced by the completion composite after cancel → retry → approve in change-password) kept A's loaded view and fed it B's params: B's title over A's password fields, an empty button where the param names differ.didUpdateWidgetnow resets the composite (view, schema, LOVs, lookups, errors) and loads the new ref. Web adapters already reloaded onref. (#465719)
0.7.3 #
-
Flutter icon map extension announced in 0.7.2 ships in this release —
arrow_forward,smartphoneand ~60 common Material names; the 0.7.2 package did not carry the change. -
Dialog.iconaccepts a host-resolved source (asset URN) for the hero, likeIcon.name/IconButton.icon. -
Card.selectionIndicator(radio|checkbox) — on a selectable card (leadingselectaction) an explicit glyph at the trailing edge mirrors the selection; the border highlight alone did not read as "pick one". Selectable cards also honourpadding. -
Hosted
Dialog: a tall body scrolls inside the popup frame. The feedback panel gave itschildrenunbounded height, so aScrollViewinside it never scrolled and the panel overflowed the host's dialog (Yetki Tanımı → Banka Limitleri: 88 cards, "BOTTOM OVERFLOWED BY 6530 PIXELS"). When the frame bounds the height the body now sits in aFlexiblescroll area while icon, title and actions stay put; under an unbounded parent (a page ScrollView) the body flows as before. Overlay mode (AlertDialog) is unchanged. (#465478) -
Bare-string texts are host localization keys first.
resolveTextContentoffers a plain string (Text.content,Button.label,AppBar.title, anytextContent) to the newPseudoViewDelegate.localizeText(key)and renders it verbatim when the host returnsnull. A view can therefore carry either{ "tr": …, "en": … }(resolved by the renderer, unchanged) or the localization keys the legacyneo_*widgets already read, so backends need one text format for both hosts.$…expressions stay data. Hosts that do not override the hook see no change. Parity: React / Vue / Angular.
0.7.2 #
-
RadioGrouprows are tappable on the label, not only on the circle. -
Skeletoncomponent — shimmering placeholder blocks (rows,variant: text | card,height,gap) for content still loading. -
ForEach.search— client-side filter of an already-loaded list:{"query": "$form.q", "fields": ["iban", "accountName"]}keeps rows where any field contains the query (case-insensitive, Turkish ı/İ folded). For$instance/$subFlowlists a SearchField needs no backend round trip; server-side search stays anx-lookupre-run by a watcher. -
ForEach.loading— node rendered whilesourcehas not resolved yet (read-through in flight), typically aSkeleton; an empty list still renders nothing.ForEach.gap— spacing between rows. -
Card.bordered(opt-in hairline, themeoutline, 12 px radius) andCard.padding(spacing token) — a flat card on a same-coloured surface can be made visible and given air without changingvariant. -
Overlays (BottomSheet / Dialog / SideSheet) are live. Content is re-rendered on data changes while open (a
RadioGroupin a sheet shows the option it just wrote) and writing thevisibleflag false from inside (a Cancel button'sset $ui.x = false) closes the route. -
Icon.nameaccepts a host-resolved source (asset URN) likeIconButton.icon— aListTile.leadingcan be a brand SVG. -
Flutter icon map extended.
arrow_forward(every wizard's Continue button) and ~60 more Material names resolve instead of the "?" fallback.
0.7.1 #
-
setaccepts a bare schema field asdata("data": "restrictionType"→$form.restrictionType), as the vocabulary always documented. It was dropped with a "must be prefixed" warning. -
Watchers fire on async sources. A watcher whose
onreads$subFlow.*/$subProcess.*or$lookup.*now runs when that read-through lands, not only on the next form / uiState change. This is how a view seeds a field from a sub-flow's current value:{"on": "$subFlow.<state>.data.x", "action": {"action": "set", "data": "$form.x", "value": "$subFlow.<state>.data.x", "mode": "fill-empty"}}— the parent's record does not carry the child's data by design, so the instance auto-seed cannot. (Vue adapter aligned; React already re-ran on its data tick.) -
Dropdownno longer overflows on long labels.isExpanded+ ellipsis on the selected item; a four-linex-enumlabel used to paint a "RIGHT OVERFLOWED BY N PIXELS" stripe over the field. -
List-valued
x-lookupresults (search lookups).fetchLookupDataandPseudoViewnow store aListatresultFieldas-is (deep-copied), so a view can iterate it withForEach source: "$lookup.<name>"— the search-box pattern (SearchField → watcher →lookup). Previously a non-Map result was dropped silently, which is why the same view listed results on web (whose loader accepts any object) and stayed empty on Flutter.FormContext.lookupData,PseudoView.onDebugDataandPseudoViewDelegate.onNestedLookupDataare typedMap<String, dynamic>accordingly (aMap<String, Map<String, dynamic>>still satisfies them). -
TextField.trailing— a text action inside the field.{ label, action?, command? }renders a compactTextButtonin the input's suffix, left of the password reveal toggle, and dispatches through the ordinary action pipeline without validating the form: a login form's "Forgot?" must not be blocked by an empty required password.actiondefaults tosubmit; the host interpretscommand(e.g.urn:vnext:flow:start:…opens another flow). Optionaltone(success/warning/error/info) colours the label from the palette's status roles viaPseudoUiThemeExt— no literal colours in JSON. Vocabulary:$defs/inputTrailingAction. Parity: React / Vue / Angular. -
FilePickerrefusals now read from the schema'sx-errorMessages, like every other field's message. The wording was a fixed English string — the one thing in this vocabulary a definition could not translate — so a Turkish form showed an English refusal beside Turkish labels. The five verdicts and the two capability failures (oversize,wrongType,empty,count,total,readFailed,pickerUnavailable) now resolve through the samegetFieldErrorMessageasrequiredandminItems, with the same{tr, en}shape.{size},{limit},{type},{accept},{max},{total}and{totalLimit}are filled in; a message written without placeholders is shown verbatim, and an undeclared key keeps the built-in English so nothing that renders today changes. The engine takes a lookup FUNCTION rather than a schema, which is what keepsfile_picker.dartfree of imports. -
x-controlLabels— the captions a widget draws itself, declared on the schema property. Every other input shows exactly two kinds of text and both already came from the bound property: the label (x-labels, whichfloatingLabel: 'never'also renders as the placeholder) and a message (x-errorMessages).FilePickeris the first control with CHROME of its own — "Choose file", "Replace file", "Remove", and the line stating the ceiling — and those had nowhere to come from, so they stayed English when the person switched the app to Turkish.They now read from
x-controlLabelsthroughgetFieldControlLabel, which isgetFieldErrorMessagewith a different block name and the same{tr, en}shape. Keys:choose,add,replace,remove,clear,busy,hint,hintMulti,limit,usage. The sentences about limits take{perFile},{maxFiles},{total},{used}and{count}; the button captions take none. An undeclared key falls back tokFilePickerControlDefaults, the exact text the adapters rendered before this existed, so a definition that declares nothing renders unchanged. No FilePicker string is hardcoded in any adapter any more.
0.7.0 #
-
FilePicker— ask the person for a document. THE ONE INPUT A DEFINITION CANNOT COMPOSE: no arrangement ofButton,ForEachandselectmakes an operating system offer its own filesystem. Everything around the dialog stays where it already lives — label from the schema'sx-labels, requiredness from itsrequired, visibility fromx-conditional— and the picked file lands inbindlike any other field's value, so a required-but-empty picker stops the submit at exactly the gate a required-but-empty text field does. There is no upload: the file becomes base64 in the form data and rides the ORDINARY TRANSITION BODY. The stored value is{name, mimeType, size, content},contentbase64 with nodata:prefix.maxSizeBytesdefaults to 1 MiB and cannot be switched off, because the base64 sits in the instance data and is carried on EVERY read of that record while inflating by 4/3 — a 1 MB document is ~1.37 MB fetched again on each state change, poll and attribute-reading list.multiple: trueasks for a SET: the bind ALWAYS holds an array, picking ADDS instead of replacing, order is arrival order, andmaxFiles(5) andmaxTotalBytes(4 MiB) join the per-file ceiling. An untouched picker writes NOTHING — absent, never[].KNOWN LIMITATION: the node's refusal wording is a fixed English string and, unlike a schema-declared message, cannot be overridden through
x-errorMessages. A host-fed string dictionary is tracked separately. -
FilePicker.fieldsrenames the four stored keys to a backend schema that already exists — IDM/DYS spell the same facts{fileName, contentType, fileContext}. Absent means the canonical shape, which is what a new schema should use. It renames ONLY; an omitted key keeps its canonical name. -
PseudoViewDelegate.readFile— the platform half of the picker. On Flutter it BOTH OPENS THE DIALOG AND READS THE BYTES, because this package takes no file-picking dependency: the plugin a host already ships (file_picker, a camera, a document provider) is the one that must be used. The consequence worth stating is that the bytes are read BEFORE the gate rather than after it; the gate itself is unchanged and still the engine's, so the same files are refused for the same reasons with the same numbers. Optional and defaulted, so no existing delegate breaks. -
Fixed: a required list field with nothing in it passed the submit gate. The gate asked for
null/ empty-string and[]is neither, so an empty array read as an answer;minItems/maxItemswere declared and never enforced. Both now run. Pre-existing and independent ofFilePicker— the multi-mode picker is what made it visible. -
Fixed: the form-data debug log published the whole record, base64 included. For a form holding a
FilePickerthat is a customer's identity document written to the host's log sink on every keystroke.withoutFileContentsnow replaces anything file-shaped with its LENGTH and leaves every other value alone.
0.6.0 #
-
Size tokens now resolve through the theme: new
PseudoMetricsThemeExtension. The pixels behindgap/padding/Icon.size/Avatar.sizewereconstmaps inside the widgets, solgmeasured 24 logical pixels for every app that embedded the renderer. They are now one overridable extension — installPseudoMetrics(iconSize: {...}, spacing: {...}, avatarRadius: {...}, hostIconSize: ...)inThemeData.extensionsand a design system on its own grid renders the same JSON at its own metrics. Defaults are exactly the previous numbers, and a PARTIAL ladder is honoured (an unmentioned token falls back through the same ladder'smd/sm, never to zero). Resolvers:pseudoSpacingOf/pseudoIconSizeOf/pseudoAvatarRadiusOf; the context-freepseudoGapstays for registered component builders and now reads the same default ladder, so the two cannot drift. -
IconButton.size— the samesm/md/lg/xltokenIcontakes, driving both a Material glyph's size and the box a host-drawn icon is given. Nothing in the widget names a pixel any more. -
Icon.sizeacceptsxl. The Flutter adapter has rendered it since the ladder was written; the vocabulary only declaredsm/md/lg, so a valid-looking view failed strict validation. Both now share oneiconSizeTokendef. -
IconButtoncan carry a host's own icon, and has an accessible name.iconis resolved against the built-in Material map first; a name that is not in it — a host's asset URN, say — is now handed to the samePseudoImageBuilderDelegateseam thatImagenodes use, so a host that owns an icon set draws it without the SDK bundling or naming any of it. Only when neither resolves does the placeholder glyph appear. This is what a backend-declaredAppBarneeds: its actions are the host's brand icons, and without the seam the bar filled with question marks.New vocabulary field
IconButton.labelis the button's ACCESSIBLE name, not visible text: it becomes the tooltip (which Flutter also exposes to the accessibility tree) andaria-label/titleon the web adapters. Drawing it would defeat the reason an app bar action is an icon in the first place — an icon has a fixed width and can never squeeze the title off the bar the way a long or unresolved text label does. -
Scaffold: a view can now declare its own PAGE FRAME. New root-only node type carryingappBar,body, and optionallybottomNavigationBar/floatingActionButton. Rendered as a real MaterialScaffold, so the top app bar — its title and its actions — comes from the view JSON instead of client code.PseudoViewcooperates: aScaffoldroot gets neither the outerpaddingnor thewrapInScrollwrap, since the frame owns the page andbodydeclares its own scrolling.PseudoView.paddingis ROUTED rather than dropped: the host's page metric belongs on the content, so it is applied to the frame'sbody(newPseudoScaffold.bodyPadding) and the chrome stays flush with the screen edges. Without this aScaffold-rooted page rendered edge-to-edge, since the vocabulary has no padding primitive of its own.Host contract:
viewDeclaresOwnFrame(view)(new, exported from the engine) is the one question a host asks before framing a surface. When it answerstruethe host must render the view bare — no app bar of its own, no page padding, no safe-area inset — or the page grows two app bars. Only full-page presenters should ask; a popup or bottom-sheet host must ignore it. -
AppBaris now a realAppBar.PseudoAppBarimplementsPreferredSizeWidgetand returns a MaterialAppBarinstead of the previousContainer+Row— that interface is what lets a JSON-declared bar occupy theScaffold.appBarslot rather than being faux content inside the body. Two new parameters carry the difference between the two call contexts, and the CALLER sets them, not the JSON: as frame chrome (primary: true,automaticallyImplyLeading: true) the bar clears the status bar and, when the view declares noleading, Flutter draws the standard route-popping back button for free; as ordinary body content both arefalse, so a bar in the middle of a scrolling page gets neither a status-bar inset nor a spurious back arrow.variant: "center"centres the title;medium/largestill render assmall(they need aSliverAppBar, which a frame slot cannot provide).Visual change: an
AppBarused as body content now looks like a real app bar (full-bleed, square) instead of a roundedsurfaceContainerHighestchip. The only known content doing that is the component-showcase sample's catalog swatch, where the new rendering is the more accurate preview. -
Component.refmay be a resource URN. The view vocabulary accepted only a plain slug;refis nowoneOftwo named defs,plainRef(^[a-z0-9-]+$) andresourceUrn(urn:<ns>:res:<res-key>:<domain>:<key>[:<version>]), so a view can embed a composite that another domain owns. Resolution stays with the host'sloadComponent; the renderer never validatedrefat runtime, so behaviour is unchanged.
0.5.1 #
-
Form auto-seeded from
instanceData(SDK rule). A bound field opens on the record's value under the same key: for every top-level schema property present ininstanceData,PseudoViewdeep-copies it into the form when the form is (re)built (seedFormFromInstance, exported from the engine). Nested maps come along, so dotted binds (limitDraft.accounts.timeDeposit.daily) resolve. Precedence: schemadefault< instance value < hostformData. This is the one rule shared by all adapters and by the workflow manager — hosts no longer pre-fill the form by hand, and the same view opens identically on web and mobile. -
PseudoViewgainedformData— host-supplied INITIAL form values (an explicit draft), keyed by bind path (nested maps for dotted binds). Parity with the TypeScript adapters'formDataprop. Applied last, over defaults and the instance seed. Deep-copied on apply (deepCopyJson), so form writes never mutate the host's map; a changedformDatainstance rebuilds the form like a changedinstanceDatadoes. -
ImagePicker.initialValueis deprecated. Still honoured (one warning is logged per process); expose the current value under the bind key in the instance data instead — the auto-seed pre-selects it. -
Composites ship inside the package.
composites/<ref>/{view,schema}.jsonat the repository root — todayflow-completion-page(success / error end screen) andupdate-password-page(old / new / confirm password form with a live rule checklist) — is embedded as generated source, so a host answers{"type": "Component", "ref": "update-password-page"}without a component endpoint, a per-domain publish or a gateway route: the backend view only names theref, the client renders the SDK's JSON.BundledComposites.load(ref)is shaped asPseudoViewDelegate.loadComponentfor a one-line hand-over;getreturnsnullfor an unknown ref so a host can fall through to its own source;has/refslist what this build knows. The delegate seam is unchanged — a host can still serve refs from anywhere. Same folder, same API on the TypeScript side (loadBundledComposite).scripts/sync-composites.mjsregenerates both embeddings (--checkfor CI).
0.5.0 #
-
setaction verb. A declarative field write:{"action": "set", "data": "<bind>", "value": <literal-or-$expr>}writesvalueinto the bound field without reaching the host and clears any stale error on that field. It is the supported way for aSwitch, awatchercase, or an action list to seed or reset form data. -
lookupaction verb +trigger: "manual".{"action": "lookup", "name": "<x-lookup>"}loads a namedx-lookupon demand and is awaited, so a followingsetin the same list reads the fresh$lookup.<name>.*. Anx-lookupdeclared withtrigger: "manual"is skipped by the initial load and fetched only when alookupaction asks for it. -
selectis deprecated in favour ofset.selectstill works unchanged (a barebinddefaults to$form), but its first use logs a one-time deprecation warning namingsetas the replacement. Existing views keep rendering; migrate at your own pace. -
$defaultexpression namespace.$default.<field>resolves to a schema property'sdefault(walking nestedproperties), so a view can seed or reset a field to its schema default without hard-coding the value. -
Finance-domain icon names now resolve everywhere. The renderer kept a private icon map missing the banking glyphs (
credit_card,security,transfer,payment,deposit,withdrawal) that the sharedpseudoIconMapalready carried, so those names drew thehelp_outlinefallback on built-in nodes. The two maps are now one; a node asking forcredit_cardgets the real icon. -
Type-safe action / operator / namespace models (public API). The engine exposes enums that were previously magic strings:
PseudoAction(reserved verbs and theirActionSpec),ConditionOperator(x-conditionaland watchercases),ExpressionRoot($form,$instance,$default, …) andPseudoComponentType(nodetype). Comparing against these instead of raw strings turns a typo into a compile error. -
Internal:
DynamicRenderersplit into per-component widgets. The ~3000-line renderer that switched onnode.typeinline is now a thin dispatch over onePseudo*widget per component, sharing aFieldBindingfor form access and action dispatch. Purely structural — no behaviour change, verified against the full test suite.
0.3.4 #
-
Imagenodes can be drawn by the host. The SDK only ever showedhttp(s)sources throughImage.network; anything else — a brand illustration, a bundled SVG, a URN the backend and its clients agree on — landed on the grey placeholder with no way for the app to step in. Two seams on the delegate the host already injects close that, consulted in this order before the built-in behaviour: a delegate that also implementsPseudoImageBuilderDelegate.buildImage(context, request) → Widget?draws ANYImage(returnnullto decline), andPseudoViewDelegate.resolveImageSource(source) → String?(also on the TypeScript delegate) maps a non-URL source to a URL. No global registration: nested components and overlays see the seam through the delegate they inherit.Image.sourceis now expression-resolved too ($instance.illustration), as the vocabulary always said. Nothing implemented ⇒ unchanged behaviour.fit: "fill"maps toBoxFit.fillinstead ofcover. -
Validation errors inside a nested
Componentpersist (Flutter). The childFormContextwas rebuilt on everybuildwith a fresherrors: {}, so an error raised by a submit INSIDE a composite (required,minLength,pattern) was wiped by the very repaint it triggered: the button did nothing and no message ever appeared under the field. Field values were unaffected — they round-trip through the parent viabind— which is why input-less composites never showed it. The error map now lives on the nested widget's State, like its LOV and lookup data, matching React'sNestedComponentWrapperthat keeps its context in auseRef. Errors still clear the moment the field becomes valid. Prerequisite for form composites such as the shared password-input page. -
validateFieldaccepts a list-typedtype. JSON Schema allows"type": ["string", "object"]— a composite's text input that takes a plain string or a{tr, en}map. Theas String?cast ontypethrew for such a property and, because the submit path validates every property of the schema, one list-typed property crashed the whole action ("Action pipeline crashed"): nothing dispatched, no error text. TS compares with===and never had the problem; Dart now does the same. -
Numeric keyboard survives lookarounds in
pattern. The digits-only scanner behindkeyboardType/inputFormattersbailed out on any group, so a business-rule pattern such as^(?!(\d)\1{5}$)(?!123456)[0-9]{6}$(6-digit PIN, not all-same, not a run) opened an alphanumeric keyboard while its sibling^[0-9]{6}$fields opened a numeric one. Lookarounds are zero-width — they decide WHICH digit strings match, never what characters make them up — so the scanner now steps over(?=…)(?!…)(?<=…)(?<!…)and lets the consuming tokens decide; other groups remain unmodelled (no restriction). Mirrored in TS (inputmode=numeric). -
commandmay be a$expression."command": "$param.submitCommand"(or$ui.…,$instance.…) is resolved on the node's context before the action reachesonAction; literals pass through untouched and the SDK still does not interpret the value. Lets a shared composite carry its own submit button while the host view supplies the flow's transition URN throughbind— instead of leaving the host to guess the transition. An expression that resolves to nothing dispatches without a command and logs a warning. Engine helperresolveCommand; mirrored in TS. -
Icongainedvariant(success/error/warning/info): a theme colour ROLE, resolved exactly like the Snackbar variants —PseudoUiThemeExtsuccess/warning/info colours (documented fallbacks) andColorScheme.error. JSON still never carries a colour; omitted → theme icon colour. Mirrored on web asd-icon--<variant>/pseudo-icon--<variant>. -
Node-level
x-conditionalis now part ofview-vocabulary.json: every component definition accepts it ($defs.nodeConditional), matching what the Flutter renderer already evaluated and what the TS adapters now evaluate. -
x-conditionalgained thematchesoperator — a regex test on the field's string form (empty never matches, an invalid pattern isfalse, not a throw). With node-levelx-conditionalthis renders a live rule checklist (✓ / •) purely from backend JSON. Mirrored in TS; vocabulary updated. -
x-validationobject form is evaluated by the engine.{ "rule": "equalsField" | "notEqualsField", "parameters": { "field": "<sibling>" }, "errorMessages": {…} }is a built-in cross-field rule — password / confirm is the canonical case — checked on change and on submit, and re-judged when the SIBLING changes so a fixednewPasswordclears the stale error under its confirm field.x-validation: true(and any unknownrule) now reaches the host'sonValidationRequeston Flutter too — it was declared on the delegate but never called — and the host's verdict survives the synchronous submit pass. Mirrored across React, Vue and Angular. -
DatePickeropens a calendar (Flutter). The field was a plain text box that expected an ISO string; it now opensshowDatePicker, shows the value asdd/MM/yyyy, keeps storingyyyy-MM-dd, handles the empty value, and a dottedbindwrites into the nested object it names. (#459236) -
View-level
watchers[](Flutter). A view may declare watchers with anonexpression and actions; the engine'sWatcherRunnerfires a watcher only when the watched value CHANGES (never on first render), writes toformData/uiStatedirectly, cascades until quiescent (bounded), and hands non-internal verbs to the host likeonAction.Switchgained anaction/commanddispatch on toggle, and text inputs keep their controller in sync when a watcher (or aselectlist) rewrites the bound value. (#459237)
0.3.3 #
-
A node's action LIST is dispatched again (Flutter).
"action": [{select}, {select}, {submit}]— the "write these values, then advance" gesture a row uses to pick a record — reached the host as the stringified list and was dropped as an unknown verb._handleActionnow fans the list out in declaration order and resolves a descriptor's ownaction/command(descriptor.command ?? command), matching the engine'sdispatchAction. Sinceselectwrites are synchronous, a trailingsubmitreads a formData that already holds them. -
Cardtaps go through the shared action path. The card had a hand-rolled handler that read at most the first TWO entries, required both to beselect, and never dispatched — so a tappable row wrote its binds and then silently went nowhere. It also read only the legacyonTap, ignoring theactionfield the vocabulary documents as preferred and as the winner when both are set; a Card written to spec had no tap handler at all. The radio-style selected outline is unchanged. -
selectresolves an expression value."value": "$item.tckn"inside aForEachtemplate was written to the bind verbatim, so the row selected the literal string rather than its own record. Only the Card's old private handler resolved this; the shared path now does, for every caller. Both the dispatch path and the Card's selected-outline check read it through one helper, so they cannot drift to different values for the same descriptor. -
A list is dispatched SYNCHRONOUSLY. An earlier revision awaited each entry so multiple host dispatches would reach the backend in declaration order. Nothing needs that: no view declares more than one host-dispatching entry, and the shape that exists (
[select, select, submit]) already works synchronously —selectwrites return before any await, so the trailingsubmitreads a formData that holds them. Awaiting only opened a window between entries in which the host could dispose or replace the surface, so aselectlanded in an orphaned form map and a trailingsubmitdispatched on a surface that was gone. Staying synchronous closes that window by construction rather than guarding against it.A rejected host dispatch is still reported (
Main dispatch rejected: <verb>) instead of surfacing as an unhandled async error, and each entry is isolated so a throw from one — a malformedx-conditionalfailing the validation loop's cast, say — drops that entry instead of abandoning the list. -
A resolved field stops blocking
submit. The validation loop wrote toctx.errorsbut never removed a key that had become valid, so the FIRST failed submit locked the screen: every later submit saw a stale error map and returned before dispatching. Matches React'sif (err) … else delete ctx.errors[key].A hidden field's error is cleared rather than skipped, along with anything keyed UNDER it. Its widget renders as
SizedBox.shrink(), so theonChangedthat would otherwise remove the error can never fire again — a field that failed while visible and was then hidden by anx-conditionalused to leave the button dead forever, with nothing on screen to explain why. Input widgets key errors by bind path, so hiding an object has to clearcontact.ibanand not onlycontact. Safe because the whole subtree is hidden: a field the user cannot see must not block submit.Still open: an
x-conditionalon a NESTED property. The loop walks top-level properties only, so it never enumerates that field to notice it is hidden.PseudoViewalso clears_errorswhen the surface changes. Only keys in the CURRENT schema's property list are recoverable, so an error left by the previous surface blocked every submit on the next one.Scope: this covers the keys the loop itself manages — top-level schema properties. Input widgets key their errors by bind PATH (
ctx.errors['selectedUser.tckn']), which the loop never enumerates, so an error a widget set on a nested bind still needs that widget to clear it. Clearing nested keys from here would be wrong, not merely incomplete: the loop validates the object-level schema, which may not enforce a nestedpattern, so removing them would let an invalid value through. -
An undispatchable action disables every control, not just
Card.IconButton,FABandMenugated onaction != null, so{},[]or[null]left an enabled control that reacted and did nothing but log. -
A failing entry no longer abandons the rest of the list. Only the host dispatch was wrapped, so a throw from anywhere else — a malformed
x-conditionalin the schema failing the validation loop's cast, for instance — escaped to the outer net, logged one generic line and dropped every remaining entry. With[{select}, {select}, {submit}]that left the selects applied and the submit silently gone. Each entry is now isolated. -
A
Cardwhose entries all get dropped stays inert. The guard covered onlynulland[], so{},[null]and a list whose every entry fails the verb guard still produced a card that rippled and did nothing. -
A tappable
Cardclips its ink splash.Carddefaults toClip.none, so the splash painted as a square past the rounded corners. -
A
selecton a namespaced bind paints the Card as selected. Routing Card writes through the shared path made them strip the namespace prefix ($ui.→ctx.uiState,$form.→ form data), but the selected-outline check still read the RAW bind, so it walked form data looking for a literal$uikey and always found null — a radio-style card with a$ui.bind could never look selected. The read now mirrors the write. -
A descriptor with no verb is dropped instead of stringified.
command,bindandvalidatewere type-checked but the verb was not, so a descriptor missing its requiredaction(or an empty{}) sent the literal string"null"to the host — ungated by validation and with nothing logged. -
Clearing errors requests a rebuild.
onFormDataChangedfired only when validation FAILED, so avalidate: truenon-submit that passed after an earlier failure left the stale error text on screen (that verb usually does not replace the surface). Fired only when the pass actually cleared errors — doing it unconditionally would add a full LOV reload to every clean submit, sinceonFormDataChanged(null)schedules_loadLovs. -
Declared action hooks warn instead of vanishing.
preHooks/postHooksare allowed byactionDescriptorand run by the engine'sdispatchMainWithHooks, but the Flutter adapter drops them — full parity needs a 4thonActionargument (ActionDispatchContext) that would break every host. They now log atwarnso a missing analytics event is diagnosable.
0.3.2 #
-
Nested (dotted)
bindpaths now work end to end. A bind carrying a.—enteredEmails.address,limitDraft.daily— used to fail on both halves:getSchemaPropertydid a flatproperties[bind]lookup and returnednull, so the field rendered with no label, no validation message and none of the derived input constraints; and the widget wrote the user's input to a FLAT key (formData["enteredEmails.address"]), so the backend saw the sub-property as missing on submit. The resolver now walkspropertiessegment by segment, and every form widget reads and writes through path-aware helpers that create the intermediate maps on first write. Single-segment binds keep the old single-map fast path. This closes the parity gap with the TypeScript engine, which has always treated a dotted bind as a path. -
ScrollViewrenders all of its children. It previously renderedchildren[0]and silently dropped every sibling, so the commonScrollView → [Column(form), Row(actions)]shape showed the form and lost the buttons — users could see the state but not submit it. A single child still renders as-is; multiple children stack in a stretchingColumnso inputs keep the full viewport width. Same class of bug that was fixed inCardearlier and left unfixed here. -
ImagePickergainedinitialValue. SeedsformData[bind]once, only while the field has no value yet, so a server-side current choice renders pre-selected and submits unchanged when the user does not touch the grid — the anti-phishing image-change flow passes"$instance.currentImage.imageDefinitionId".'$'-prefixed values are expressions; anything else is a literal. A user tap overwrites it like any selection, and an unresolvable expression leaves the field unset.
0.3.1 #
-
Typing-time input constraints, derived from the bound schema. A digits-only
pattern(^[0-9]*$,^\d{6}$) ortype: integeropens a numeric keyboard and rejects non-digit characters as they are typed;maxLength(or a fixed-width pattern) caps the field. Until now those keywords only produced an error message after the fact, so fields that are numeric by contract — password, customer no, OTP — happily accepted letters. Derivation is conservative: a pattern whose syntax the scanner does not recognise imposes no restriction, and no vocabulary change is needed, backends opt in per property. -
Text inputs are keyed by bound field + derived contract. Flutter does not push a changed
keyboardTypeonto a live input connection, so a reused element kept the previous state's numeric keyboard — and its controller text — after a surface swap. -
Dialoggained a HOSTED mode and avariant. Withvisibleomitted the node now renders INLINE as a feedback panel (hero icon, title, body, stacked full-width actions) instead of rendering nothing; that is the shape acontent.display: "popup"surface needs, because the host already supplies the frame.variant(info/success/warning/error) picks the icon and the accent colour from theColorSchemein both modes, and an expliciticonstill wins. Runtimes no longer have to hand-roll error popups out of raw Column/Text/Button trees. -
The soft keyboard closes when an action is dispatched. It used to stay up over the next surface and over the host's loading overlay until the user dismissed it by hand.
selectactions are exempt, so tab switches and radio-style pickers do not interrupt typing. -
Vocabulary fix:
Button.actionwasoneOf [buttonAction, actionDescriptor], but a plain"submit"satisfies both branches, which made strict validators reject every string-action Button. NowanyOf.
0.3.0 #
-
Brand theming is now complete in the engine.
buildPseudoThemeDatagained everything a brand previously needed a bespoke theme builder for, so a consumer supplies aPseudoColorPalette(Dart class or brand JSON) and nothing else:- Contrast-guarded foregrounds.
onPrimary,onSecondary,onInverseSurfaceand the button labels are checked against the background they are painted on (WCAG 1.4.11, ratio ≥ 3.0) and fall back to a legible tone when a brand's token pairing collides — the green-on-green / blue-on-blue class of bug where labels vanish. Pairings that already clear the threshold are untouched, so brand intent is kept. TheColorSchemerole and the button that paints on the same background are resolved once and shared, so they can no longer disagree. - Inverted chrome roles. New
PseudoColorPalette.inverseSurface/onInverseSurface(M3's snackbar / tooltip / inverted-chip surfaces), defaulting toonSurface/surface. Brands with a softer chrome tone override them;JsonPalettereadspalette.surface.inverseSurfaceandpalette.surface.onInverseSurface. Additive — existing palettes and brand JSONs are unaffected. cornerRadiusparameter (default8) drives input and button shapes, so a brand's roundness no longer requires its own builder.- Input labels now use
onSurfaceVariantinstead of the Material default.
- Contrast-guarded foregrounds.
-
Read-through child data via
$subFlow/$subProcess. A parent workflow's view can now display its sub-flow / sub-process data without that data ever being copied into the parent. Dart port of the companion TypeScript engine'sengine/subInstances.ts— same grammar, same keying, same suppression, so one backend view definition renders on both.$subFlow.<parentState> single record; the same flow may run at several points in the parent, so the STATE is what disambiguates $subProcess.<flowName> always a list (1-to-n fan-out), keyed by flow name; plus `.length` and `.$done`Which root a child lands in comes from the runtime's
subFlowType(S→$subFlow,P→$subProcess), never from where it was authored. Entries are ordered newest-first, so[0]keeps meaning "the current run" once completed correlations arrive. A group is a list of plain maps, soForEach+$item.data.<field>consume it with no renderer change.Cost is reference-driven: the SDK scans the view for the roots it actually names and asks only for those, so a parent with seven open children serving a view that mentions one fan-out issues three reads. A view with no
$sub*binding calls nothing at all. An absent child suppresses silently —$subFlowresolves tonull,$subProcessto an empty list so.lengthis0and aForEachrenders nothing — which lets a binding be written eagerly and light up when the child appears.Host wiring is one delegate method,
PseudoViewDelegate.loadSubInstances, which defaults to returning an empty list — existing delegates keep working untouched. The record's identity comes from the newPseudoView(instanceRef: ...); omit it on start surfaces, where no instance exists yet.Known platform limitation, unchanged by this: the runtime today reports only
activeCorrelations, and a sequential child is only open while it blocks its parent — so$subFlowresolves to nothing until the runtime also reports completed correlations.$subProcessis unaffected. A host implementation should prefercorrelationswhen present and fall back toactiveCorrelations, so this lights up on a runtime upgrade with no client or view change. -
Cross-adapter input default flipped. When a
TextField/TextArea/NumberField/Dropdown/SearchField/AutoComplete/DatePicker/TimePickernode omitsfloatingLabelandfocusOutline, the effective defaults are nowfloatingLabel: "never"+focusOutline: "hidden"(the legacy Neobank input pattern — label stays inside the field as a static placeholder, focus doesn't shift the outline). Backends that need Material's floating-label pattern must opt in explicitly with"auto"/"always"; the themed focused border is restored withfocusOutline: "default". This is a breaking change for consumers that relied on the previous "no override → Material default" behaviour. -
New
Timercomponent. Countdown widget for OTP / SMS verification flows. Ships in all four adapters (Flutter, Vue, React, Angular). Props:duration(seconds, required),format(mm:ss/hh:mm:ss/ss, defaultmm:ss),variant(plain/chip, defaultplain). Display-only — expiry does NOT dispatch any workflow action; the hosting workflow polling loop is the source of truth for state changes. -
TextFieldpassword support. Bound schema properties with"format": "password"(JSON Schema draft-07 standard) are now rendered masked, with a suffix eye icon that toggles visibility. Same treatment ships in the Vue, React, and Angular adapters — Vue/React use their respective PrimeVue/PrimeReactPasswordcomponents (with the strength meter disabled), Angular Material binds[type]="password | text"and adds amat-icon-buttonsuffix. The view vocabulary is unchanged — masking is a schema-owned metadata concern, not a layout one.
0.2.3 #
-
New: JSON-driven theme palette as public API. Consumers can now load a brand theme from a JSON document (Material Design Tokens / W3C Design Tokens style) without writing any Dart code:
final raw = await rootBundle.loadString('assets/themes/acme.json'); final palette = JsonPalette.fromJson(json.decode(raw)); MaterialApp( theme: buildPseudoThemeData(palette: palette), home: PseudoView(...), );New public types:
PseudoColorPalette— abstract palette contract (20 Material 3 ColorScheme roles + success / warning / info status slots).JsonPalette—PseudoColorPalettefactory that reads a JSON map and exposes the brand colors as DartColors. Missing fields fall back to the M3 baseline.DefaultMaterialPalette— built-in M3 baseline, used as the fallback forJsonPaletteand as a sample to extend from.buildPseudoThemeData(palette: ...)— Material 3ThemeDatabuilder that seeds aColorSchemefrom the palette and attachesPseudoUiThemeExtfor status colors.
-
Spacerdispatch case added toDynamicRenderer. Supports both rigid mode (setwidthand/orheightto render aSizedBox) and flex mode (setflexto expand inside aRow/Column). Previously rendered as an "unsupported" placeholder. -
Internal docs / inline references cleaned up so the package surface carries no internal project names. Publisher identifiers (
@burgan-tech/pseudo-uinpm scope, repository URL, MIT copyright) are preserved as legitimate "who shipped this" signals. -
No breaking changes for existing consumers.
0.2.2 #
- Widen
intlconstraint from^0.19.0to>=0.19.0 <0.21.0so consumers running on Flutter 3.27+ (which pinsintl: 0.20.xviaflutter_localizations) can adopt the package withoutdependency_overridesgymnastics.
0.2.1 #
PseudoViewnow supportsdelegate: nullfor flat views with no actions, LOVs, or nested components. The engine falls back to a built-inNoOpPseudoViewDelegate.instanceso consumers no longer need a stub delegate just to render JSON.NoOpPseudoViewDelegateexported from the public barrel for direct use.loadComponentstill throwsUnsupportedErrorwhen nested refs are encountered, surfacing the gap rather than rendering an incomplete tree.
0.2.0 #
- Add
PseudoComponentRegistry— pluggable extension point for third-party component sets. The renderer's default branch now delegates to the registry, so any design system can register builders without touchingDynamicRenderer. - Add
PseudoUiThemeExt—ThemeExtension<PseudoUiThemeExt>for semantic success / warning / info colors. Falls back to Material 3 defaults when not provided by the host. - Theme awareness across the renderer:
TextField/Dropdown/Cardetc. respect host-providedInputDecorationThemeandColorScheme. DynamicRenderernow exported from the public barrel so external builders can render nested children.- Internal: design-system adapters extracted into a sibling package
(not published to pub.flutter-io.cn — consumers register their own components
via
PseudoComponentRegistry).
0.1.3 #
- CI: use official dart-lang OIDC workflow for automated pub.flutter-io.cn publishing
- Package size optimized (13MB → 31KB) via .pubignore
0.1.2 #
- README: Initial data, Lookups, Data Model, Vocabularies sections added
- Code cleanup: silent catch fixed, empty action handlers wired, lint warnings resolved
- 101 unit and widget tests (up from 15)
- LOV reload optimization: skip unchanged params, skip static LOVs on field changes
- GitHub Actions CI + pub.flutter-io.cn automated publishing
0.1.1 #
- 45+ Material Design 3 component implementations
- Expression engine:
$form,$instance,$param,$ui,$lov,$lookup,$schema,$item,$context - Conditional engine:
showIf/hideIf/enableIf/disableIfwith 13 operators and compound rules - Validation: JSON Schema validation (pattern, format, minLength, min/max) plus async custom validation
- LOV & Lookup: cascade filtering, smart reload (skip unchanged params)
- Overlay surfaces: Dialog, BottomSheet, SideSheet, NavigationDrawer driven by
$uistate - Nested components with isolated contexts and two-way data flow
- 101 unit and widget tests
0.1.0 #
- Initial release with core engine and basic Flutter adapter