Releases: Mustry-Solutions/mustry-perspective-component-module
Release list
v0.5.0
Open-source: Apache-2.0 + public repository
The project is now open-source under the Apache-2.0 License and public on
GitHub (Mustry-Solutions/mustry-perspective-component-module). The module is
free (isFreeModule = true), so source and binary are both open. The
install-time notice (license.html) was rewritten to align with Apache-2.0
(dropping the old no-redistribute / no-reverse-engineer clauses) and finalized:
legal entity Mustry Solutions BV (BE 0790.722.422), professional/business
use, sharpened safety-critical language (not certified for safety-instrumented
/ life-safety functions), "to the maximum extent permitted by applicable law"
and non-excludable-liability carve-outs, and a no-data-collection statement.
Added THIRD-PARTY-NOTICES.md listing the ~222 bundled open-source
components and their licenses (mostly MIT/BSD/ISC). Added community-health
files (CONTRIBUTING with a DCO flow,
CODE_OF_CONDUCT, SECURITY, issue/PR templates, CODEOWNERS), README badges and
a "Licensing & support" section, Dependabot config (minor/patch + security;
majors taken deliberately), and least-privilege CI permissions.
Documentation
Refreshed the user manual (§ Branching) and code comments to match the current
layered layout and feature set (arrows, orientation, edge styling, markdown
info card, output.warnings); fixed stale identity/links.
Dependencies
Gradle 8→9 (verified: signed build + e2e pass); GitHub Actions bumped
(checkout/setup-java/cache/upload-artifact); @inductiveautomation/perspective-client
2.3.7→2.3.8 and other npm minor/patch updates.
Branching Diagram: richer edge model (per-edge colour / style / width)
data.edgeLabels entries now carry optional color, style
(solid/dashed/dotted) and width alongside label — so an edge
can be, say, a dashed red fallback branch instead of inheriting the source
node's colour and the global config.lineWidth. Purely additive (existing
{from, to, label} entries are unchanged; no override = the old defaults),
so it passes the additive-only schema guard. Each connector SVG also gains a
data-edge="<from>-<to>" attribute for targeting. Demo /branching-labels
now colours/dashes its branches; +1 e2e.
Branching Diagram: layered layout (replaces BFS + forward-push)
The layout is now layered/Sugiyama-style: a DFS cycle-break classifies
back-edges, then longest-path layer assignment sets each node's column.
The original BFS pushed a re-referenced node and its whole subtree right to
keep every arrow pointing forward — a single rework loop inflated the graph
(a long empty connector, nodes clustered far right). Longest-path keeps the
layers compact and draws the loop as a backward connector (which already
routes clear of intervening nodes). Same data contract and category→row
model; only the column assignment changed. This is a deliberate divergence
from the ported original (whose loop handling stays as-is). +1 jest case
(a loop no longer inflates maxX); the existing demos render more compactly.
v0.4.0
Branching Diagram (new component — migrated from ignition-mustry-ui)
Fourteenth component: mustrysolutions.perspective.display.branching — a
left-to-right decision-tree / flow-path renderer, the one real component
in the ignition-mustry-ui repo, absorbed per
docs/branching-component-migration-plan.md (M0+M1 in one pass). Flat
data.nodes in (id/name/category/nextId/colour/icon/tooltip); pure BFS
layout — one column per depth, one row per category rank, re-referenced
nodes pushed forward with their subtree so arrows always point right —
with corridor-aware SVG connector routing, width-responsive column
spacing, Ignition icons, hover info cards, an inferred root
(output.hasRoot) and empty-state labels. Clicking a node writes
state.selectedNode (two-way highlight) and fires onNodeClick {id, name, category}. Ported to house standards: the ~250-line layout
extracted pure into branchingLogic.ts (12 jest cases — the source had
zero tests), React 16 class shell, all CSS namespaced mustry-branch-*
with --brn-* theme vars (source used unprefixed globals like .node),
sectioned config/data/state/output schema fixing the source's drift
(phantom rootId, undeclared nodeBorderWidth), 7-language labels.
A follow-up parity sweep against the source's shipped stylesheet and
README screenshot aligned the look 1:1: nodes CENTRE on their grid
points (connectors pass through disc centres), the label-halo pill
(padding/radius/config.backgroundColor) is back, names are absolutely
centred with the 200px cap, the info card is position:fixed, stays open
while hovered, and renders MARKDOWN again (react-markdown@4 — the
React-16-compatible major, replacing the source's React-18-only v9),
z-index layering and the original defaults (nodeSize 20, offsets 50)
restored. Two deliberate fixes over the source: the label overhang left
of the first column is reserved inside the scroll area (the source let it
spill via overflow: visible; this port scrolls, which would have clipped
it), and the info card gets width: max-content (the source's card shrank
to the ~40px node wrapper — a transformed ancestor is the containing
block for position: fixed — wrapping one word per line). Demo at /branching
(seeded production flow with a rework loop exercising duplicate
forwarding); 3 e2e tests. The source repo should now be archived.
A follow-up enhancement pass added three things, verified by loading BOTH
components (the original module built and installed alongside) on one view:
- Backward/loop connector fix. The source's split-routing only handled
forward edges; an edge whose origin sits in a LATER column than its
target (the QA→Rework rework loop) computed a negative-width SVG that,
underoverflow: visible, painted a stray diagonal + stub. The
five-segment routing is now a pure, node-testedcomputeConnector
(min/max box so width is never negative; backward edges re-routed
through the column midpoint). The original still shows the artifact; the
new component no longer does. config.showArrows(default off) — an arrowhead at each connector's
target, trimmed to the disc edge and auto-oriented, so flow direction
and loops read at a glance.config.orientationhorizontal(default) /vertical— vertical
lays depth top-to-bottom, category across; label overhang is reserved on
the cross axis so first-column labels don't clip. Widenconfig.yOffset
in vertical mode so horizontal labels don't collide.
Demos at/branching-arrowsand/branching-vertical; +4 jest connector
cases and +3 e2e tests.
Validation feedback (additive, revertible). When a dataset can't fully
draw, the reason is surfaced instead of a blank canvas: the empty state
distinguishes no-nodes / cycle (no entry point) / generic no-root, and a
new output.warnings (string[]) lists machine-readable issues — no edges,
a cycle, edges to unknown ids, and nodes unreachable from the root (which
the BFS layout silently drops). New config.labels.cyclic (7 languages);
pure diagnose() with 5 jest cases; demo at /branching-cycle; +1 e2e.
Edge labels (additive, revertible). Optional data.edgeLabels
([{from, to, label}]) tags connectors — Yes/No on a decision branch —
matched to existing edges by endpoint ids; edges without an entry stay
bare, so it's inert unless you supply labels. Rendered as a small chip on
the connector mid-point. Demo at /branching-labels (a decision tree);
+1 e2e.
Loop routing + arrow polish. Backward/loop connectors now rise close to
the TARGET instead of at the midpoint — the midpoint often landed on an
intermediate node (the rework loop visibly crossed the QA node); rising near
the target keeps the run clear of it. Arrowheads gained a halo-clearance
trim (the node's pill halo, above the connector layer, was covering the tip)
so they sit at the halo edge on the approach axis. +1 jest case.
BREAKING: full identity rename — "Mustry Perspective Components"
The module sheds the Ingots working name before any real deployments
freeze its identifiers (the sanctioned pre-1.0 window):
- Repo:
mustry-perspective-component-module(wasmustry-ingots). - Module id:
com.mustrysolutions.perspective.components(was
com.mustrysolutions.ingots) — a gateway sees this as a different
module: uninstall the old one, install the new; there is no in-place
upgrade across the rename. - Component ids:
mustrysolutions.perspective.<family>.<name>(was
mustrysolutions.ingots.…) — views built against the old ids must be
re-created (or their JSON search-replaced). The verify project is
migrated in this commit and serves as the reference. - Display name "Mustry Perspective Components", modl
Mustry-Perspective-Components.modl, Java packages/hook classes renamed
to match. The web bundle name (MustryComponents.js), URL alias
(mustry-components) and CSS prefixes (mustry-*) were already
brand-level and are unchanged.
Distribution: EULA in the module + branded user manual
The .modl now carries license.html (staged into the module content and
referenced from module.xml — the gateway shows it at install time and
requires acceptance; pattern proven in the AMQP/observability modules).
The ops toolkit seeds acceptance unattended via the CRC32 of license.html
(licenseAgreementHash in modules.json), so ops/e2e.sh --fresh and CI
keep working headlessly. NOTE: the EULA text is a first draft modeled on
the observability module's — review before any public distribution.
Releases now also attach a branded user-manual PDF built from
docs/user_manual.md by scripts/build_manual_pdf.sh (Mustry fonts,
generated cover, footer stamping — pipeline ported from the observability
module), with real component screenshots in docs/images/.
Admin family: per-row ⋯ menu with Duplicate
Every admin rail row gains a ⋯ menu — hover-revealed for mouse users and
ALWAYS visible on the selected row so touch users reach it with one tap
(hover-only affordances don't exist on touch, which is why bare hover
delete icons were rejected). It holds Duplicate (enters the existing
create flow prefilled from the source — "Day Shift (copy)", or
"jdoe-copy" dash-style for usernames — so Save fires the usual
isNew: true event and NO new events were needed) and Delete
(two-step confirm inside the menu, works on any row without selecting
it; deleting the selected row clears the selection). Duplicating a user
copies roles/schedule/contacts/adjustments but never the password;
duplicating an alternating schedule copies week A. Gated by the existing
allowCreate (duplicate) / allowDelete (delete) flags. New shared
RowMenu + labels (7 languages, moreActions/duplicate override keys
across all four components).
v0.3.0
Admin family: layout stability pass
No more layout jumps when interacting with the admin components (user
feedback: the schedule header shoved everything around when buttons
appeared). Three mechanisms, applied family-wide:
- The Save/Discard commit tail now RESERVES its space while the draft is
clean (rendered invisible and inert via a new CommitControls
reserveSpaceopt-in — the rich-text/code toolbars keep their
historical pop-in), so going dirty no longer reflows the header. - Two-step delete buttons (and the Manage-roles toggle) render BOTH
labels stacked with the inactive one invisible (shared ConfirmButton),
so arming "Delete" → "Confirm delete?" can't change the button's width. - Validation errors float as absolutely-positioned chips below their
anchor instead of reflowing siblings (name/username/date/role/
adjustment errors alike).
Verified by pixel measurement: the schedule header's Delete button, week
grid and preview strip hold identical coordinates across clean → dirty →
armed-confirm → restored; the users form holds position when an
adjustment error appears.
Follow-up structural pass (same release): actions moved out of the
headers entirely into a shared footer action bar (hairline top,
destructive Delete on the left, the reserved Save cluster on the right,
pinned to the pane's bottom) — headers are identity-only and never fight
buttons for width again. Forms got readability caps (users 640px,
holiday 420px) and hairlined section headings.
User Manager: availability adjustments
The detail form gains an "Availability adjustments" section editing the
per-user schedule overrides Ignition already models (vacation = available
false, extra cover = available true): from/until instants
(datetime-local, wire format 'YYYY-MM-DD HH:mm'), note, add/remove rows.
Blank rows drop on save; partially filled or inverted rows BLOCK save
('adjustmentInvalid' in output.validationErrors). Rides in the existing
onUserSave payload (user.scheduleAdjustments, rebuilt wholesale via
system.user.createScheduleAdjustment — see the /users reference script,
which also documents that the ScheduleAdjustment bean's getter names
vary, hence the defensive accessors). e2e covers the full round-trip
incl. validation blocking.
Holiday Manager (new component — admin family #4)
Thirteenth component: mustrysolutions.ingots.admin.holidaymanager — the
missing quarter of the schedule story: schedules can "observe holidays"
but nothing in the runtime showed or edited which dates those are. A rail
sorted by NEXT occurrence (annual repeats compute their next date, Feb-29
observes Feb 28 off-leap-years, past one-offs sink and dim) + a small
detail form (name/date/repeat) with strict calendar-checked date
validation (no silent Feb-31→March rollover). All the family patterns:
create/rename/delete with validation, draft discipline,
onHolidaySave {holiday, isNew, oldName?} / onHolidayDelete {name}
persisted via system.user.addHoliday/editHoliday/removeHoliday,
output.count/isDirty/validationErrors, 7-language labels, --adm-*
theming. Demo at /holidays (real persistence, seeded demo holidays);
the Admin Console gains a fourth tab. 13 pure-logic jest cases + 3 e2e
tests incl. a create→rename→delete lifecycle.
Admin Console demo view (composition recipe)
The verify project gains /admin: the three admin components composed in a
NATIVE tab container — the intended pattern instead of a merged
mega-component (tabs and page security are the platform's job; per-tab
capability flags do the "what do you want to use" part). Each tab is the
same live component + reference scripts as its standalone demo, sharing one
refresh tick. Documented in the README ("Composing an Admin Console");
smoke-tested end-to-end. NOTE for view authors: tab-container children
require position: {tabIndex: N} — without it only the first child renders.
User Manager: role-catalog management (opt-in)
config.allowRoleManagement (default off) adds a manage mode to the Roles
section: add, inline-rename and two-step-delete roles in the user source's
catalog, firing onRoleSave {name, oldName?} / onRoleDelete {name}
immediately (persist via system.user.addRole/editRole/removeRole).
Renames keep user assignments (the source stores role ids); the UI warns
that security policies reference roles BY NAME. Folded into the User
Manager rather than a separate component — Ignition roles are flat
strings, and role gaps are noticed while assigning them. The /users
demo persists for real (and refuses to delete 'Administrator'); e2e
covers add → assign → rename-keeps-assignment → delete.
User Manager (new component — the admin family is complete)
Twelfth component: mustrysolutions.ingots.admin.usermanager — Vision's
User Management for Perspective, closing out the admin family. A filterable
user rail + a detail form editing names, schedule (dropdown), language,
notes, role chips and contact-info rows, all against the flat PyUser mirror
(data.users, bound via system.user.getUsers(); role/schedule catalogs
bound alongside). Passwords are OPT-IN and payload-only:
config.allowPasswordChange (default off) stages a password that travels
only in the onUserSave {user, isNew, password?} event — never through
props, state or output — and the docs say loudly to put the component
behind security levels + TLS first. Create/delete flows with username
validation (onUserDelete {username}); AD/LDAP sources degrade to a
viewer via config.editable: false. The /users demo persists for real
(roles/contacts rebuilt wholesale, delete-admin refused, complexity policy
respected). output.count/isDirty/validationErrors, 7-language labels,
shared --adm-* theming. 7 pure-logic jest suites' worth of draft ops + 5
e2e tests incl. a create-with-password→edit→delete lifecycle.
Roster Manager (new component — second of the admin family)
Eleventh component: mustrysolutions.ingots.admin.rostermanager — a runtime
UI over the gateway's alarm-notification rosters (Vision's Roster
Management; Perspective has nothing). A roster is the ORDERED escalation
sequence alarm pipelines walk, so the UI treats order as the point: rows
carry "Contact N" ordinals and reorder by dragging a grip; a typeahead
picker over the bound data.availableUsers directory appends users; rows
resolve names/contact points and warn when a user has NO contact info (the
real failure mode). Create/delete flows with name validation; draft-only
edits with the shared Save/Discard tail. Controlled: system.roster is
append-only — no reorder primitive — so onRosterSave {name, users, isNew}
carries the full desired order and the author's script reconciles
(createRoster → removeUsers → addUsers); onRosterDelete {name}. The
/roster demo persists for real and seeds a small demo directory
(including a contact-less user to show the warning).
output.count/isDirty/validationErrors, 7-language labels, shared
--adm-* theming (now a SCSS mixin used by the whole family). 14 new
pure-logic jest cases + 4 e2e tests incl. a create→persist→delete
lifecycle and a reorder-persist round-trip.
Schedule Manager (new component — first of the admin family)
Tenth component: mustrysolutions.ingots.admin.schedulemanager — a runtime
UI over the gateway's user schedules, the highest-vote component-shaped gap
on the Ideas portal ("Perspective - Admin Components", 102 votes since 2019,
no native commitment; only copy-in Exchange view templates exist). Vision has
Schedule Management; Perspective has nothing. Master-detail: schedule list
with live active-now dots + a 7-day week grid where availability is
painted — drag empty space to add a range (snapped), drag block edges to
resize, click a block to remove; a red now-line marks the current time and a
preview strip answers "active now? until when?" (output.isActiveNow, 30s
tick, midnight-continuity and weekly wrap handled). Name/description/flags
edit inline; + New schedule creates from a blank draft; Delete asks twice.
Controlled: data.schedules is a flat mirror of Ignition's
BasicScheduleModel beans (bind via system.user.getSchedules()), edits stay
draft-only until Save fires onScheduleSave {schedule, isNew, oldName?} /
onScheduleDelete {name} and the author's script persists via
system.user.* — the /schedule demo ships reference scripts incl. the
rename add-then-remove reconcile. output.count/isDirty/validationErrors,
name validation, 7-language labels, --adm-* family theming. Alternating
A/B schedules deliberately render week A read-only pre-1.0 (unverified bean
layout). All range/availability/transition/paint math pure + node-tested.
Demo at /schedule; 7 e2e tests (incl. a full create→rename→delete
lifecycle against the live gateway) + 72 pure-logic jest cases. Family plan
in docs/admin-components-plan.md.
On-Screen Keyboard (new component)
Ninth component: mustrysolutions.ingots.input.keyboard — a touch keyboard for
the runtime, the gap Perspective leaves to the OS keyboard (Windows
TabTip / Linux Squeekboard), which breaks in Perspective Browser/mobile
and causes the "double-keyboard" problem; the Exchange only offers view
templates that can't stop the OS keyboard. Its value display is a <div>,
not an <input>, so tapping it never raises the OS keyboard — the actual
fix, not a workaround. Two families sharing one controlled shell: a
numeric keypad (layout: numpad, editing value.value — min/max clamp
with an out-of-range badge, decimals, units, allowNegative, setpoint-style
fresh entry) and a QWERTY keyboard (layout: text/email/url,
editing value.text — one-shot shift, a ?123 symbols layer, maxLength,
@/.com// convenience keys). mode: inline or popover (a field trigger
that opens a portalled panel; Enter commits + closes, outside-click/Escape
discards). Enter fires onCommit {value, text, isValid} (value is a number
or string by layout); live onChange; read-only output.*
(...
v0.2.0
Code / JSON Editor (new component)
Seventh component: mustrysolutions.input.codeeditor — CodeMirror-6
editing and read-only viewing of JSON / Python / SQL / XML / plain text
for config-driven apps (an open gap: only a read-only JSON viewer
existed third-party; nothing native through 8.3.8). Live JSON parse
validation (lint gutter + toolbar badge + output.isValid/errorMessage
to gate commit buttons on), Format JSON, folding, search, bracket
matching, undo/redo (bound content history-exempt), the module's
standard controlled save/dirty model (onSave), display mode,
7-language labels, --code-* theming incl. a CSS-variable syntax
palette. Demo at /code; 5 e2e tests + pure-logic jest suite.
Bundle 656K→1.25M minified (~350K gz) — CodeMirror + 4 grammars;
lazy-loading language chunks is the noted follow-up trim.
Rich Text Editor (new component)
Sixth component: mustrysolutions.input.richtexteditor — true WYSIWYG
editing and safe read-only display of operator instructions/SOPs/notes
(the top remaining validated Perspective gap; research July 2026). Built
on TipTap core (vanilla — Perspective pins React 16, so the editor
mounts imperatively, gesture-controller style). Controlled write-back
(onSave), schema-constrained sanitization with URL allowlists,
feature-flagged formatting (headings/lists/links/tables/images/
checklists), interactive display-mode checklists (onTaskToggle),
char limit, plainText/wordCount/charCount outputs, 7-language labels,
--rte-* theming, print stylesheet. Undo/redo toolbar
buttons (bound content is history-exempt — Ctrl-Z can never blank the
document), an image-library picker fed by bound data.imageLibrary
(gateway Image Management paths like /system/images/... work
directly), and a config.fonts allowlist font picker (display mode
always renders saved fonts). Demo at /rte incl. theme toggle; 10 e2e
tests + pure logic jest suites. Bundle grows 245K→656K minified (~180K gz) — first
runtime dependency, trim pass noted for later.
Build & CI
- The web bundle is now a production webpack build by default: minified JS
- CSS, no source maps (JS 508K → 240K, CSS 68K → 55K,
.modl~391K → ~110K).
./gradlew build -PwebDevrestores the unminified dev bundle with source maps.
- CSS, no source maps (JS 508K → 240K, CSS 68K → 55K,
- New Playwright e2e smoke suite (
e2e/,ops/e2e.sh): renders every
component route of the verify project in a real Perspective session — mount,
key interactions (preset write-back, view switch, group collapse, quick
filter, 50k-row virtualization, embedded-view click, fly-to), zero console
errors. Runs in CI on every push/PR. - Unattended gateway bring-up (
ops/e2e.sh --fresh): headless
commissioning + module-cert pre-acceptance by merging the dev cert's SHA-1
intodata/modules.json— no browser wizard. Used by CI; locally it's also
the quickest fix for an expired Perspective trial.
Internal
- CSS classes namespaced: every
.cal-/.tml-/.dg-/.pz-/.dtrp-
class is nowmustry--prefixed (845 renames), eliminating collision risk
with other third-party modules' global styles on a shared Perspective page.
The documented theming CSS variables (--cal-*,--tml-*, …) are
unchanged. Class names were never a documented API; anyone who targeted
them in a project stylesheet must add themustry-prefix. - Build modernized: Gradle 7.6 → 8.14.3 (zero deprecations),
io.ia.sdk.modl0.4.0 → 0.5.0, node-gradle plugin 3.5.1 → 7.1.0,
settings.gradle→ Kotlin DSL (dropping the meaningless root include),
deprecated$buildDir→layout.buildDirectory, and the:webcross-project
task mutation removed. The web bundle's double scope declaration resolved:
modlImplementation(project(":web"))in the gateway is the load-bearing one
(verified:projectScopesalone drops the jar from module.xml), so the
redundantprojectScopesentry is gone. - TypeScript full
strict(adds strictFunctionTypes/BindCallApply/
PropertyInitialization/noImplicitThis over the previous partial flags), plus
noImplicitReturnsand target es6 → es2019. Only five errors surfaced — all
the PerspectivePComponent/PlainObjectboundary variance, now explicit
casts atgetViewComponent()with the props reducer as the runtime guarantee. - One Java component registry (
comp/Components.ALL): both hooks iterate a
single list for register/remove, and descriptor assembly moved into one
factory — the per-scope hand-maintained lists (and their drift) are gone. - Uniform props-schema strictness: every schema root and section now
rejects unknown keys (additionalProperties: false), matching the picker's
standard; the resourcetimeline also gained thestylesection the other
components already declared. - God components decomposed following the calendar's pattern: picker
(1120→972 + CalendarPane/Presets/Inputs/Trigger), timeline (1019→863 +
Toolbar/Legend/Track), grid (843→741 + Toolbar/Cells);labelPacks.ts
(789) became a barrel over per-component modules inshared/labels/. - Shared drag-gesture lifecycle (
shared/dragGestureController.ts): the
calendar and timeline gesture controllers now extend one base owning pointer
capture, document listeners, the click-vs-drag threshold (incl. the larger
touch threshold), cancel semantics and commit dispatch — previously
copy-pasted per component and already drifting (the timeline had re-inlined
the movement threshold). Controllers keep only their geometry. No behavior
change; covered by new drag/click e2e tests. Pan & zoom deliberately stays
separate — its continuous multi-pointer model shares no lifecycle with the
preview/commit gestures.
Cross-component parity pass: each component's best ideas ported to the others
where they add real value.
BREAKING — prop sections harmonized (sanctioned pre-1.0, no deployments)
Both display components now follow one sections convention (the picker's
selection already embodied it): config = set-and-forget configuration,
data = bound content, state = two-way runtime state (the component
writes user interactions back; everything pre-settable/bindable), output
= read-only derived values. Moved keys (names unchanged):
- Calendar:
config.view→state.view,config.followNow→
state.followNow,output.hiddenCategories→state.hiddenCategories. - Timeline:
config.zoom→state.zoom,config.followNow→
state.followNow,config.collapsedGroups→state.collapsedGroups,
output.hiddenCategories→state.hiddenCategories. hiddenCategoriesis now genuinely two-way on both: pre-set or bind it
to open with categories pre-filtered (it was a read-only output mirror).
Calendar / Scheduler
- CSV export now includes
data.recurringEvents(series definitions, deduped
by id) — previously recurring series were silently missing from exports. - Recurring occurrences carry the same ↻ marker as the timeline (month chips,
week/day chips and all-day bars, list view, day popover). - Both-edge resize in week/day views (timeline parity): a top handle moves the
start, the bottom handle the end; multi-day segments get their one legal
handle; handles suppressed on chips too short to also grab-move. - Fixed: resize had no noop guard — a snapped-back resize fired a phantom
onChange. Both edges are now guarded (moves already were). - New
config.shifts([{label, start: 'HH:mm'}]): dashed shift-boundary
lines with gutter labels in week/day views; shared parser in
shared/shifts.ts(timeline can adopt it later). Themeable via
--cal-shift-line. onChangeevent schema now documents the recurrence context
(scope/seriesId/occurrenceDate) the payload already carried.- Follow-now live mode (timeline parity): two-way
config.followNow+
toolbar "Live" toggle with pulsing dot; re-anchors on today every
refreshSeconds(else 60s); paused mid-edit/drag; paging or a mini-nav pick
disarms, Today and view switches do not. output.visibleStartMs/visibleEndMs: the window as DST-correct epoch
instants (zone-local midnights), for bindingt_stampqueries directly.- Hover popover status badge and "All day" text are now localized via the
label packs (labels.statusTentative/statusCancelled/statusDone) — they
were hardcoded English. - Fixed (both display components): the
emptyMessagedefault ("No
events") is now treated as "unset" and follows the locale packs
(labels.noEvents) — previously a French calendar showed an English badge.
Any other value overrides;''still hides the badge.
Resource Timeline
- Empty-state badge with context-aware how-to tooltip (calendar parity) +
config.emptyMessage. - Follow-now live mode (ported from the picker's realtime mode): two-way
config.followNow+ toolbar "Live" toggle with pulsing dot; re-anchors the
window so the now-line stays in view (tick =refreshSeconds, else 60s);
paused while editing/dragging; manual paging or a mini-nav pick disarms,
Today does not. Same Designer-dirty caveat as the picker's realtime mode. - Resource
color/icon(already in the schema) are now rendered on row
labels (icon-else-dot, matching the legend). onChangeevent schema documentsscope/seriesId/occurrenceDate.- Fixed: the toolbar CSV-export icon rendered at zero size (the SVG was
never given dimensions — calendar parity); surfaced by the first fixture to
enableshowExporton a timeline. output.visibleStartMs/visibleEndMs: the window as raw epoch ms, for
bindingt_stampqueries directly.- Hover popover shows a localized status badge (tentative/cancelled/done),
calendar parity — newlabels.status*keys in all 7 languages. - Enter animation on newly-appearing bars/state bands (calendar parity, shared
EnterTracker); never animates mid-drag; respectsprefers-reduced-motion. config.snapMinutes: one snap override for drag-move, ...