@operato/layout 10.27.1 → 10.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.28.0](https://github.com/hatiolab/operato/compare/v10.27.1...v10.28.0) (2026-09-24)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **layout:** ox-canvas-bar - the common canvas controls in the same places everywhere ([11a758a](https://github.com/hatiolab/operato/commit/11a758a63fbacc060434f1ac794e9d7082b4c2a3))
12
+ * **layout:** ox-canvas-bar shows the screen's undo and redo keys in their titles ([13793b4](https://github.com/hatiolab/operato/commit/13793b432b8242832b56ae8c90cd5a8e515cd740))
13
+ * **layout:** ox-three-pane - the start | middle | end work surface, once ([7f280be](https://github.com/hatiolab/operato/commit/7f280beb22006d9609871e3c478f42620b775df6))
14
+ * **layout:** ox-url-tabs - tabs whose choice lives in the address, read and written together ([a6fb40f](https://github.com/hatiolab/operato/commit/a6fb40f2db42e1b92c474af3e25a8df1038b444b))
15
+
16
+
17
+
6
18
  ### [10.27.1](https://github.com/hatiolab/operato/compare/v10.27.0...v10.27.1) (2026-09-24)
7
19
 
8
20
  **Note:** Version bump only for package @operato/layout
@@ -0,0 +1,49 @@
1
+ import '@material/web/icon/icon.js';
2
+ import { LitElement } from 'lit';
3
+ import type { OxThreePane } from './ox-three-pane.js';
4
+ /**
5
+ * The strip over a canvas - a scene, a modeller, a map (ADR-0060 decision 8 addendum, the second frame).
6
+ *
7
+ * Seven screens built this strip each their own way: twin's map and board with the same buttons and a
8
+ * copied stylesheet, and the modellers of plant, figure and twin putting undo, redo, fit and the side
9
+ * toggles in different places. A hand that learned one product looked for them in another. So the bar
10
+ * fixes where the common controls stand, and the screen fills the rest:
11
+ *
12
+ * [fold start] | mode | undo redo | fit | tools … status | commit | [fold end]
13
+ *
14
+ * - The fold toggles stand at the two edges, over the side they fold. Give the bar the ox-three-pane
15
+ * it sits over (`.pane`) and it folds that frame itself; with no pane there are no toggles.
16
+ * - Undo, redo and fit are drawn here so their icons and order are the same everywhere. Each appears
17
+ * when the screen says it has it (`history`, `fit`) and fires an event - `canvas-undo`,
18
+ * `canvas-redo`, `canvas-fit` - for the screen to answer. Shortcuts stay with the screen.
19
+ * - `undo-shortcut` / `redo-shortcut` put the screen's keys in those titles - `undo (Ctrl+Z)` - so a
20
+ * person learns them where they reach for the button. The keys themselves stay with the screen.
21
+ * - Slots: `mode` (edit / preview and the like), the default slot for the screen's own tools,
22
+ * `status` (saved, unsaved) and `commit` (save).
23
+ */
24
+ export declare class OxCanvasBar extends LitElement {
25
+ static styles: import("lit").CSSResult;
26
+ /** The frame this bar folds. Without one the bar draws no fold toggles. */
27
+ pane?: OxThreePane | null;
28
+ /** The screen keeps an undo history - draw undo and redo. */
29
+ history: boolean;
30
+ canUndo: boolean;
31
+ canRedo: boolean;
32
+ /** The key that undoes, shown in the title beside the name (the V2 modeller did: `undo (Ctrl+Z)`). */
33
+ undoShortcut: string;
34
+ /** The key that redoes, shown the same way. */
35
+ redoShortcut: string;
36
+ /** The screen can fit its canvas to the view - draw fit. */
37
+ fit: boolean;
38
+ private observer?;
39
+ updated(changed: Map<string, unknown>): void;
40
+ disconnectedCallback(): void;
41
+ render(): import("lit-html").TemplateResult<1>;
42
+ private fold;
43
+ private fire;
44
+ }
45
+ declare global {
46
+ interface HTMLElementTagNameMap {
47
+ 'ox-canvas-bar': OxCanvasBar;
48
+ }
49
+ }
@@ -0,0 +1,188 @@
1
+ import { __decorate } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import { css, html, LitElement, nothing } from 'lit';
4
+ import { customElement, property } from 'lit/decorators.js';
5
+ import { i18next } from '@operato/i18n';
6
+ /**
7
+ * The strip over a canvas - a scene, a modeller, a map (ADR-0060 decision 8 addendum, the second frame).
8
+ *
9
+ * Seven screens built this strip each their own way: twin's map and board with the same buttons and a
10
+ * copied stylesheet, and the modellers of plant, figure and twin putting undo, redo, fit and the side
11
+ * toggles in different places. A hand that learned one product looked for them in another. So the bar
12
+ * fixes where the common controls stand, and the screen fills the rest:
13
+ *
14
+ * [fold start] | mode | undo redo | fit | tools … status | commit | [fold end]
15
+ *
16
+ * - The fold toggles stand at the two edges, over the side they fold. Give the bar the ox-three-pane
17
+ * it sits over (`.pane`) and it folds that frame itself; with no pane there are no toggles.
18
+ * - Undo, redo and fit are drawn here so their icons and order are the same everywhere. Each appears
19
+ * when the screen says it has it (`history`, `fit`) and fires an event - `canvas-undo`,
20
+ * `canvas-redo`, `canvas-fit` - for the screen to answer. Shortcuts stay with the screen.
21
+ * - `undo-shortcut` / `redo-shortcut` put the screen's keys in those titles - `undo (Ctrl+Z)` - so a
22
+ * person learns them where they reach for the button. The keys themselves stay with the screen.
23
+ * - Slots: `mode` (edit / preview and the like), the default slot for the screen's own tools,
24
+ * `status` (saved, unsaved) and `commit` (save).
25
+ */
26
+ let OxCanvasBar = class OxCanvasBar extends LitElement {
27
+ constructor() {
28
+ super(...arguments);
29
+ /** The screen keeps an undo history - draw undo and redo. */
30
+ this.history = false;
31
+ this.canUndo = false;
32
+ this.canRedo = false;
33
+ /** The key that undoes, shown in the title beside the name (the V2 modeller did: `undo (Ctrl+Z)`). */
34
+ this.undoShortcut = '';
35
+ /** The key that redoes, shown the same way. */
36
+ this.redoShortcut = '';
37
+ /** The screen can fit its canvas to the view - draw fit. */
38
+ this.fit = false;
39
+ }
40
+ updated(changed) {
41
+ var _a;
42
+ if (changed.has('pane')) {
43
+ (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
44
+ /* The fold can also change from elsewhere (a shortcut, a stylesheet reading the attribute);
45
+ the toggles follow the frame, not their own memory. */
46
+ if (this.pane) {
47
+ this.observer = new MutationObserver(() => this.requestUpdate());
48
+ this.observer.observe(this.pane, { attributes: true, attributeFilter: ['fold-start', 'fold-end'] });
49
+ }
50
+ }
51
+ }
52
+ disconnectedCallback() {
53
+ var _a;
54
+ super.disconnectedCallback();
55
+ (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
56
+ }
57
+ render() {
58
+ const t = (key, fallback) => i18next.t(key, { defaultValue: fallback });
59
+ /* The shortcut goes in the title only: the name a screen reader says stays the name. */
60
+ const titled = (name, shortcut) => (shortcut ? `${name} (${shortcut})` : name);
61
+ return html `
62
+ ${this.fold('start')}
63
+ <slot name="mode"></slot>
64
+ ${this.history
65
+ ? html `<span class="group" role="group">
66
+ <button data-action="undo" title=${titled(t('button.undo', 'undo'), this.undoShortcut)} aria-label=${t('button.undo', 'undo')}
67
+ ?disabled=${!this.canUndo} @click=${() => this.fire('canvas-undo')}><md-icon>undo</md-icon></button>
68
+ <button data-action="redo" title=${titled(t('button.redo', 'redo'), this.redoShortcut)} aria-label=${t('button.redo', 'redo')}
69
+ ?disabled=${!this.canRedo} @click=${() => this.fire('canvas-redo')}><md-icon>redo</md-icon></button>
70
+ </span>`
71
+ : nothing}
72
+ ${this.fit
73
+ ? html `<button data-action="fit" title=${t('button.fit-view', 'fit to view')} aria-label=${t('button.fit-view', 'fit to view')}
74
+ @click=${() => this.fire('canvas-fit')}><md-icon>fit_screen</md-icon></button>`
75
+ : nothing}
76
+ <slot></slot>
77
+ <span class="spacer"></span>
78
+ <slot name="status"></slot>
79
+ <slot name="commit"></slot>
80
+ ${this.fold('end')}
81
+ `;
82
+ }
83
+ fold(side) {
84
+ const pane = this.pane;
85
+ if (!pane)
86
+ return nothing;
87
+ const folded = side === 'start' ? pane.foldStart : pane.foldEnd;
88
+ const icon = `${side === 'start' ? 'left' : 'right'}_panel_${folded ? 'open' : 'close'}`;
89
+ const label = i18next.t(`button.${folded ? 'show' : 'hide'}-${side}-pane`, {
90
+ defaultValue: `${folded ? 'show' : 'hide'} the ${side === 'start' ? 'left' : 'right'} pane`
91
+ });
92
+ const button = html `<button data-action="fold-${side}" aria-pressed=${String(!folded)} title=${label} aria-label=${label}
93
+ @click=${() => {
94
+ if (side === 'start')
95
+ pane.foldStart = !pane.foldStart;
96
+ else
97
+ pane.foldEnd = !pane.foldEnd;
98
+ this.requestUpdate();
99
+ }}><md-icon>${icon}</md-icon></button>`;
100
+ return side === 'start'
101
+ ? html `${button}<span class="separator"></span>`
102
+ : html `<span class="separator"></span>${button}`;
103
+ }
104
+ fire(name) {
105
+ this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }));
106
+ }
107
+ };
108
+ OxCanvasBar.styles = css `
109
+ :host {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 4px;
113
+ padding: 4px 8px;
114
+ min-height: 40px;
115
+ box-sizing: border-box;
116
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
117
+ background: var(--md-sys-color-surface);
118
+ color: var(--md-sys-color-on-surface);
119
+ flex-wrap: wrap;
120
+ }
121
+ .group {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: 2px;
125
+ }
126
+ .separator {
127
+ width: 1px;
128
+ align-self: stretch;
129
+ margin: 4px 4px;
130
+ background: var(--md-sys-color-outline-variant);
131
+ }
132
+ .spacer {
133
+ flex: 1;
134
+ }
135
+ button {
136
+ display: inline-grid;
137
+ place-items: center;
138
+ width: 32px;
139
+ height: 32px;
140
+ padding: 0;
141
+ border: none;
142
+ border-radius: var(--md-sys-shape-corner-small, 8px);
143
+ background: transparent;
144
+ color: var(--md-sys-color-on-surface-variant);
145
+ cursor: pointer;
146
+ }
147
+ button:hover:not(:disabled) {
148
+ background: var(--md-sys-color-surface-container-high);
149
+ color: var(--md-sys-color-on-surface);
150
+ }
151
+ button:focus-visible {
152
+ outline: 2px solid var(--md-sys-color-primary);
153
+ outline-offset: 1px;
154
+ }
155
+ button:disabled {
156
+ opacity: 0.38;
157
+ cursor: default;
158
+ }
159
+ md-icon {
160
+ --md-icon-size: 20px;
161
+ }
162
+ `;
163
+ __decorate([
164
+ property({ attribute: false })
165
+ ], OxCanvasBar.prototype, "pane", void 0);
166
+ __decorate([
167
+ property({ type: Boolean })
168
+ ], OxCanvasBar.prototype, "history", void 0);
169
+ __decorate([
170
+ property({ type: Boolean, attribute: 'can-undo' })
171
+ ], OxCanvasBar.prototype, "canUndo", void 0);
172
+ __decorate([
173
+ property({ type: Boolean, attribute: 'can-redo' })
174
+ ], OxCanvasBar.prototype, "canRedo", void 0);
175
+ __decorate([
176
+ property({ attribute: 'undo-shortcut' })
177
+ ], OxCanvasBar.prototype, "undoShortcut", void 0);
178
+ __decorate([
179
+ property({ attribute: 'redo-shortcut' })
180
+ ], OxCanvasBar.prototype, "redoShortcut", void 0);
181
+ __decorate([
182
+ property({ type: Boolean })
183
+ ], OxCanvasBar.prototype, "fit", void 0);
184
+ OxCanvasBar = __decorate([
185
+ customElement('ox-canvas-bar')
186
+ ], OxCanvasBar);
187
+ export { OxCanvasBar };
188
+ //# sourceMappingURL=ox-canvas-bar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ox-canvas-bar.js","sourceRoot":"","sources":["../../../src/components/ox-canvas-bar.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAIvC;;;;;;;;;;;;;;;;;;;GAmBG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,UAAU;IAApC;;QA2DL,6DAA6D;QAChC,YAAO,GAAG,KAAK,CAAA;QACQ,YAAO,GAAG,KAAK,CAAA;QACf,YAAO,GAAG,KAAK,CAAA;QACnE,sGAAsG;QAC5D,iBAAY,GAAG,EAAE,CAAA;QAC3D,+CAA+C;QACL,iBAAY,GAAG,EAAE,CAAA;QAC3D,4DAA4D;QAC/B,QAAG,GAAG,KAAK,CAAA;IA0E1C,CAAC;IAtEC,OAAO,CAAC,OAA6B;;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACxB,MAAA,IAAI,CAAC,QAAQ,0CAAE,UAAU,EAAE,CAAA;YAC3B;qEACyD;YACzD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACd,IAAI,CAAC,QAAQ,GAAG,IAAI,gBAAgB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAA;gBAChE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;YACrG,CAAC;QACH,CAAC;IACH,CAAC;IAED,oBAAoB;;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,MAAA,IAAI,CAAC,QAAQ,0CAAE,UAAU,EAAE,CAAA;IAC7B,CAAC;IAED,MAAM;QACJ,MAAM,CAAC,GAAG,CAAC,GAAW,EAAE,QAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAA;QACvF,wFAAwF;QACxF,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,QAAgB,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAE9F,OAAO,IAAI,CAAA;QACP,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;;QAElB,IAAI,CAAC,OAAO;YACZ,CAAC,CAAC,IAAI,CAAA;+CACiC,MAAM,CAAC,CAAC,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC,aAAa,EAAE,MAAM,CAAC;0BAC/G,CAAC,IAAI,CAAC,OAAO,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;+CACjC,MAAM,CAAC,CAAC,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC,aAAa,EAAE,MAAM,CAAC;0BAC/G,CAAC,IAAI,CAAC,OAAO,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;kBAC9D;YACV,CAAC,CAAC,OAAO;QACT,IAAI,CAAC,GAAG;YACR,CAAC,CAAC,IAAI,CAAA,mCAAmC,CAAC,CAAC,iBAAiB,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC,iBAAiB,EAAE,aAAa,CAAC;qBACjH,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,yCAAyC;YACnF,CAAC,CAAC,OAAO;;;;;QAKT,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;KACnB,CAAA;IACH,CAAC;IAEO,IAAI,CAAC,IAAqB;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAA;QACtB,IAAI,CAAC,IAAI;YAAE,OAAO,OAAO,CAAA;QAEzB,MAAM,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAA;QAC/D,MAAM,IAAI,GAAG,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,UAAU,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;QACxF,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,IAAI,OAAO,EAAE;YACzE,YAAY,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,QAAQ,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,OAAO;SAC5F,CAAC,CAAA;QAEF,MAAM,MAAM,GAAG,IAAI,CAAA,6BAA6B,IAAI,kBAAkB,MAAM,CAAC,CAAC,MAAM,CAAC,UAAU,KAAK,eAAe,KAAK;eAC7G,GAAG,EAAE;YACZ,IAAI,IAAI,KAAK,OAAO;gBAAE,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,SAAS,CAAA;;gBACjD,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAA;YACjC,IAAI,CAAC,aAAa,EAAE,CAAA;QACtB,CAAC,aAAa,IAAI,qBAAqB,CAAA;QAEzC,OAAO,IAAI,KAAK,OAAO;YACrB,CAAC,CAAC,IAAI,CAAA,GAAG,MAAM,iCAAiC;YAChD,CAAC,CAAC,IAAI,CAAA,kCAAkC,MAAM,EAAE,CAAA;IACpD,CAAC;IAEO,IAAI,CAAC,IAAY;QACvB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC9E,CAAC;;AA5IM,kBAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDlB,AAtDY,CAsDZ;AAG+B;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;yCAA0B;AAE5B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;4CAAgB;AACQ;IAAnD,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;4CAAgB;AACf;IAAnD,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;4CAAgB;AAEzB;IAAzC,QAAQ,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;iDAAkB;AAEjB;IAAzC,QAAQ,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;iDAAkB;AAE9B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;wCAAY;AApE7B,WAAW;IADvB,aAAa,CAAC,eAAe,CAAC;GAClB,WAAW,CA8IvB","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\n\nimport type { OxThreePane } from './ox-three-pane.js'\n\n/**\n * The strip over a canvas - a scene, a modeller, a map (ADR-0060 decision 8 addendum, the second frame).\n *\n * Seven screens built this strip each their own way: twin's map and board with the same buttons and a\n * copied stylesheet, and the modellers of plant, figure and twin putting undo, redo, fit and the side\n * toggles in different places. A hand that learned one product looked for them in another. So the bar\n * fixes where the common controls stand, and the screen fills the rest:\n *\n * [fold start] | mode | undo redo | fit | tools … status | commit | [fold end]\n *\n * - The fold toggles stand at the two edges, over the side they fold. Give the bar the ox-three-pane\n * it sits over (`.pane`) and it folds that frame itself; with no pane there are no toggles.\n * - Undo, redo and fit are drawn here so their icons and order are the same everywhere. Each appears\n * when the screen says it has it (`history`, `fit`) and fires an event - `canvas-undo`,\n * `canvas-redo`, `canvas-fit` - for the screen to answer. Shortcuts stay with the screen.\n * - `undo-shortcut` / `redo-shortcut` put the screen's keys in those titles - `undo (Ctrl+Z)` - so a\n * person learns them where they reach for the button. The keys themselves stay with the screen.\n * - Slots: `mode` (edit / preview and the like), the default slot for the screen's own tools,\n * `status` (saved, unsaved) and `commit` (save).\n */\n@customElement('ox-canvas-bar')\nexport class OxCanvasBar extends LitElement {\n static styles = css`\n :host {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px;\n min-height: 40px;\n box-sizing: border-box;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n background: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n flex-wrap: wrap;\n }\n .group {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n }\n .separator {\n width: 1px;\n align-self: stretch;\n margin: 4px 4px;\n background: var(--md-sys-color-outline-variant);\n }\n .spacer {\n flex: 1;\n }\n button {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n button:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n }\n button:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n button:disabled {\n opacity: 0.38;\n cursor: default;\n }\n md-icon {\n --md-icon-size: 20px;\n }\n `\n\n /** The frame this bar folds. Without one the bar draws no fold toggles. */\n @property({ attribute: false }) pane?: OxThreePane | null\n /** The screen keeps an undo history - draw undo and redo. */\n @property({ type: Boolean }) history = false\n @property({ type: Boolean, attribute: 'can-undo' }) canUndo = false\n @property({ type: Boolean, attribute: 'can-redo' }) canRedo = false\n /** The key that undoes, shown in the title beside the name (the V2 modeller did: `undo (Ctrl+Z)`). */\n @property({ attribute: 'undo-shortcut' }) undoShortcut = ''\n /** The key that redoes, shown the same way. */\n @property({ attribute: 'redo-shortcut' }) redoShortcut = ''\n /** The screen can fit its canvas to the view - draw fit. */\n @property({ type: Boolean }) fit = false\n\n private observer?: MutationObserver\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('pane')) {\n this.observer?.disconnect()\n /* The fold can also change from elsewhere (a shortcut, a stylesheet reading the attribute);\n the toggles follow the frame, not their own memory. */\n if (this.pane) {\n this.observer = new MutationObserver(() => this.requestUpdate())\n this.observer.observe(this.pane, { attributes: true, attributeFilter: ['fold-start', 'fold-end'] })\n }\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.observer?.disconnect()\n }\n\n render() {\n const t = (key: string, fallback: string) => i18next.t(key, { defaultValue: fallback })\n /* The shortcut goes in the title only: the name a screen reader says stays the name. */\n const titled = (name: string, shortcut: string) => (shortcut ? `${name} (${shortcut})` : name)\n\n return html`\n ${this.fold('start')}\n <slot name=\"mode\"></slot>\n ${this.history\n ? html`<span class=\"group\" role=\"group\">\n <button data-action=\"undo\" title=${titled(t('button.undo', 'undo'), this.undoShortcut)} aria-label=${t('button.undo', 'undo')}\n ?disabled=${!this.canUndo} @click=${() => this.fire('canvas-undo')}><md-icon>undo</md-icon></button>\n <button data-action=\"redo\" title=${titled(t('button.redo', 'redo'), this.redoShortcut)} aria-label=${t('button.redo', 'redo')}\n ?disabled=${!this.canRedo} @click=${() => this.fire('canvas-redo')}><md-icon>redo</md-icon></button>\n </span>`\n : nothing}\n ${this.fit\n ? html`<button data-action=\"fit\" title=${t('button.fit-view', 'fit to view')} aria-label=${t('button.fit-view', 'fit to view')}\n @click=${() => this.fire('canvas-fit')}><md-icon>fit_screen</md-icon></button>`\n : nothing}\n <slot></slot>\n <span class=\"spacer\"></span>\n <slot name=\"status\"></slot>\n <slot name=\"commit\"></slot>\n ${this.fold('end')}\n `\n }\n\n private fold(side: 'start' | 'end') {\n const pane = this.pane\n if (!pane) return nothing\n\n const folded = side === 'start' ? pane.foldStart : pane.foldEnd\n const icon = `${side === 'start' ? 'left' : 'right'}_panel_${folded ? 'open' : 'close'}`\n const label = i18next.t(`button.${folded ? 'show' : 'hide'}-${side}-pane`, {\n defaultValue: `${folded ? 'show' : 'hide'} the ${side === 'start' ? 'left' : 'right'} pane`\n })\n\n const button = html`<button data-action=\"fold-${side}\" aria-pressed=${String(!folded)} title=${label} aria-label=${label}\n @click=${() => {\n if (side === 'start') pane.foldStart = !pane.foldStart\n else pane.foldEnd = !pane.foldEnd\n this.requestUpdate()\n }}><md-icon>${icon}</md-icon></button>`\n\n return side === 'start'\n ? html`${button}<span class=\"separator\"></span>`\n : html`<span class=\"separator\"></span>${button}`\n }\n\n private fire(name: string) {\n this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }))\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-canvas-bar': OxCanvasBar\n }\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import { LitElement } from 'lit';
2
+ /**
3
+ * A work surface in three panes: a list on the start side, the thing being worked on in the middle,
4
+ * its details on the end side (ADR-0060 decision 8 and its 2026-09-24 addendum).
5
+ *
6
+ * plant's station-screen modeller and figure's V3 editor built this grid apart - 220 | 1fr | 340 and
7
+ * 200 | 1fr | 340, each folding its sides from its own toolbar. The frame is that grid once:
8
+ *
9
+ * - Slots `start` and `end`, and the default slot for the middle.
10
+ * - `fold-start` / `fold-end` take a side away; the middle takes its room. The toggles stay in the
11
+ * screen's toolbar - the frame owns the layout, not the buttons.
12
+ * - Side widths are `--three-pane-start-width` (200px) and `--three-pane-end-width` (340px). A screen
13
+ * that needs other widths sets them; it does not write the grid again.
14
+ * - It answers its own width, not the window's: narrower than 860px the sides give up some room
15
+ * (160 / 300); narrower than 560px it is one column, the middle first, and it scrolls.
16
+ */
17
+ export declare class OxThreePane extends LitElement {
18
+ static styles: import("lit").CSSResult;
19
+ /** Take the start side away; the middle takes its room. */
20
+ foldStart: boolean;
21
+ /** Take the end side away; the middle takes its room. */
22
+ foldEnd: boolean;
23
+ render(): import("lit-html").TemplateResult<1>;
24
+ }
25
+ declare global {
26
+ interface HTMLElementTagNameMap {
27
+ 'ox-three-pane': OxThreePane;
28
+ }
29
+ }
@@ -0,0 +1,132 @@
1
+ import { __decorate } from "tslib";
2
+ import { css, html, LitElement } from 'lit';
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ /**
5
+ * A work surface in three panes: a list on the start side, the thing being worked on in the middle,
6
+ * its details on the end side (ADR-0060 decision 8 and its 2026-09-24 addendum).
7
+ *
8
+ * plant's station-screen modeller and figure's V3 editor built this grid apart - 220 | 1fr | 340 and
9
+ * 200 | 1fr | 340, each folding its sides from its own toolbar. The frame is that grid once:
10
+ *
11
+ * - Slots `start` and `end`, and the default slot for the middle.
12
+ * - `fold-start` / `fold-end` take a side away; the middle takes its room. The toggles stay in the
13
+ * screen's toolbar - the frame owns the layout, not the buttons.
14
+ * - Side widths are `--three-pane-start-width` (200px) and `--three-pane-end-width` (340px). A screen
15
+ * that needs other widths sets them; it does not write the grid again.
16
+ * - It answers its own width, not the window's: narrower than 860px the sides give up some room
17
+ * (160 / 300); narrower than 560px it is one column, the middle first, and it scrolls.
18
+ */
19
+ let OxThreePane = class OxThreePane extends LitElement {
20
+ constructor() {
21
+ super(...arguments);
22
+ /** Take the start side away; the middle takes its room. */
23
+ this.foldStart = false;
24
+ /** Take the end side away; the middle takes its room. */
25
+ this.foldEnd = false;
26
+ }
27
+ render() {
28
+ return html `<div class="panes">
29
+ <div class="start" part="start"><slot name="start"></slot></div>
30
+ <div class="middle" part="middle"><slot></slot></div>
31
+ <div class="end" part="end"><slot name="end"></slot></div>
32
+ </div>`;
33
+ }
34
+ };
35
+ OxThreePane.styles = css `
36
+ :host {
37
+ display: block;
38
+ container-type: inline-size;
39
+ min-height: 0;
40
+ min-width: 0;
41
+ }
42
+
43
+ .panes {
44
+ --_start: var(--three-pane-start-width, 200px);
45
+ --_end: var(--three-pane-end-width, 340px);
46
+
47
+ display: grid;
48
+ grid-template-columns: var(--_start) minmax(0, 1fr) var(--_end);
49
+ height: 100%;
50
+ min-height: 0;
51
+ }
52
+
53
+ :host([fold-start]) .panes {
54
+ --_start: 0px;
55
+ }
56
+ :host([fold-end]) .panes {
57
+ --_end: 0px;
58
+ }
59
+ :host([fold-start]) .start,
60
+ :host([fold-end]) .end {
61
+ display: none;
62
+ }
63
+
64
+ .start,
65
+ .middle,
66
+ .end {
67
+ min-width: 0;
68
+ min-height: 0;
69
+ display: flex;
70
+ flex-direction: column;
71
+ }
72
+ /* Each pane is placed in its own column. Auto-placement would slide the middle into the start
73
+ column's zero width once the start pane is folded away (display: none leaves the flow). */
74
+ .start {
75
+ grid-column: 1;
76
+ border-right: 1px solid var(--md-sys-color-outline-variant);
77
+ }
78
+ .middle {
79
+ grid-column: 2;
80
+ }
81
+ .end {
82
+ grid-column: 3;
83
+ border-left: 1px solid var(--md-sys-color-outline-variant);
84
+ }
85
+ ::slotted(*) {
86
+ flex: 1;
87
+ min-width: 0;
88
+ min-height: 0;
89
+ }
90
+
91
+ @container (max-width: 859px) {
92
+ .panes {
93
+ --_start: var(--three-pane-start-width-narrow, 160px);
94
+ --_end: var(--three-pane-end-width-narrow, 300px);
95
+ }
96
+ }
97
+
98
+ @container (max-width: 559px) {
99
+ .panes {
100
+ grid-template-columns: minmax(0, 1fr);
101
+ grid-template-rows: minmax(260px, 45%) auto auto;
102
+ overflow-y: auto;
103
+ }
104
+ .start,
105
+ .middle,
106
+ .end {
107
+ grid-column: 1;
108
+ }
109
+ .middle {
110
+ order: -1;
111
+ }
112
+ .start {
113
+ border-right: none;
114
+ border-top: 1px solid var(--md-sys-color-outline-variant);
115
+ }
116
+ .end {
117
+ border-left: none;
118
+ border-top: 1px solid var(--md-sys-color-outline-variant);
119
+ }
120
+ }
121
+ `;
122
+ __decorate([
123
+ property({ type: Boolean, attribute: 'fold-start', reflect: true })
124
+ ], OxThreePane.prototype, "foldStart", void 0);
125
+ __decorate([
126
+ property({ type: Boolean, attribute: 'fold-end', reflect: true })
127
+ ], OxThreePane.prototype, "foldEnd", void 0);
128
+ OxThreePane = __decorate([
129
+ customElement('ox-three-pane')
130
+ ], OxThreePane);
131
+ export { OxThreePane };
132
+ //# sourceMappingURL=ox-three-pane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ox-three-pane.js","sourceRoot":"","sources":["../../../src/components/ox-three-pane.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D;;;;;;;;;;;;;;GAcG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,UAAU;IAApC;;QAyFL,2DAA2D;QACU,cAAS,GAAG,KAAK,CAAA;QACtF,yDAAyD;QACU,YAAO,GAAG,KAAK,CAAA;IASpF,CAAC;IAPC,MAAM;QACJ,OAAO,IAAI,CAAA;;;;WAIJ,CAAA;IACT,CAAC;;AAnGM,kBAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFlB,AAtFY,CAsFZ;AAGoE;IAApE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;8CAAkB;AAEnB;IAAlE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;4CAAgB;AA5FvE,WAAW;IADvB,aAAa,CAAC,eAAe,CAAC;GAClB,WAAW,CAqGvB","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/**\n * A work surface in three panes: a list on the start side, the thing being worked on in the middle,\n * its details on the end side (ADR-0060 decision 8 and its 2026-09-24 addendum).\n *\n * plant's station-screen modeller and figure's V3 editor built this grid apart - 220 | 1fr | 340 and\n * 200 | 1fr | 340, each folding its sides from its own toolbar. The frame is that grid once:\n *\n * - Slots `start` and `end`, and the default slot for the middle.\n * - `fold-start` / `fold-end` take a side away; the middle takes its room. The toggles stay in the\n * screen's toolbar - the frame owns the layout, not the buttons.\n * - Side widths are `--three-pane-start-width` (200px) and `--three-pane-end-width` (340px). A screen\n * that needs other widths sets them; it does not write the grid again.\n * - It answers its own width, not the window's: narrower than 860px the sides give up some room\n * (160 / 300); narrower than 560px it is one column, the middle first, and it scrolls.\n */\n@customElement('ox-three-pane')\nexport class OxThreePane extends LitElement {\n static styles = css`\n :host {\n display: block;\n container-type: inline-size;\n min-height: 0;\n min-width: 0;\n }\n\n .panes {\n --_start: var(--three-pane-start-width, 200px);\n --_end: var(--three-pane-end-width, 340px);\n\n display: grid;\n grid-template-columns: var(--_start) minmax(0, 1fr) var(--_end);\n height: 100%;\n min-height: 0;\n }\n\n :host([fold-start]) .panes {\n --_start: 0px;\n }\n :host([fold-end]) .panes {\n --_end: 0px;\n }\n :host([fold-start]) .start,\n :host([fold-end]) .end {\n display: none;\n }\n\n .start,\n .middle,\n .end {\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n }\n /* Each pane is placed in its own column. Auto-placement would slide the middle into the start\n column's zero width once the start pane is folded away (display: none leaves the flow). */\n .start {\n grid-column: 1;\n border-right: 1px solid var(--md-sys-color-outline-variant);\n }\n .middle {\n grid-column: 2;\n }\n .end {\n grid-column: 3;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ::slotted(*) {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n\n @container (max-width: 859px) {\n .panes {\n --_start: var(--three-pane-start-width-narrow, 160px);\n --_end: var(--three-pane-end-width-narrow, 300px);\n }\n }\n\n @container (max-width: 559px) {\n .panes {\n grid-template-columns: minmax(0, 1fr);\n grid-template-rows: minmax(260px, 45%) auto auto;\n overflow-y: auto;\n }\n .start,\n .middle,\n .end {\n grid-column: 1;\n }\n .middle {\n order: -1;\n }\n .start {\n border-right: none;\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n .end {\n border-left: none;\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n }\n `\n\n /** Take the start side away; the middle takes its room. */\n @property({ type: Boolean, attribute: 'fold-start', reflect: true }) foldStart = false\n /** Take the end side away; the middle takes its room. */\n @property({ type: Boolean, attribute: 'fold-end', reflect: true }) foldEnd = false\n\n render() {\n return html`<div class=\"panes\">\n <div class=\"start\" part=\"start\"><slot name=\"start\"></slot></div>\n <div class=\"middle\" part=\"middle\"><slot></slot></div>\n <div class=\"end\" part=\"end\"><slot name=\"end\"></slot></div>\n </div>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-three-pane': OxThreePane\n }\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import { LitElement } from 'lit';
2
+ export interface OxUrlTab {
3
+ key: string;
4
+ label: string;
5
+ }
6
+ /**
7
+ * Tabs whose choice lives in the address (ADR-0060 decision 8 addendum, the third frame).
8
+ *
9
+ * plant's quality and pending lists and its equipment screen keep their tab in the address, each its
10
+ * own way; twin's business screen reads the tab from the address on entry and never writes it back, so
11
+ * a copied link opens on the first tab. Here reading and writing always go together: open the link,
12
+ * press back after opening a row, reload - the same tab.
13
+ *
14
+ * - `?tab=` (the `param` name) is where new screens keep it. The first tab is the default and is not
15
+ * written, so a screen's plain address stays plain. Other query values are kept as they are.
16
+ * The change replaces the address rather than adding a history step - back leaves the screen, it
17
+ * does not walk through the tabs.
18
+ * - `base` puts the key in a path segment instead (`<base>/<key>`), through the shell's navigate, the
19
+ * way plant's three screens do. **Transitional** (architect ruling 2026-09-24): it is here so those
20
+ * screens can take the frame without breaking the links people have; new screens use `?tab=`.
21
+ * - `tab-change` (detail `{ key }`) fires when the tab changes - by a press, or by back / forward.
22
+ * `selected` holds the current key, read from the address when the element connects.
23
+ *
24
+ * The look is the suite's tab strip (SurfaceStyles `.ui-tabs`): an underline under the chosen one.
25
+ */
26
+ export declare class OxUrlTabs extends LitElement {
27
+ static styles: import("lit").CSSResult;
28
+ tabs: OxUrlTab[];
29
+ /** The query name the key is kept under. */
30
+ param: string;
31
+ /** Transitional: keep the key in a path segment after this page instead of the query. */
32
+ base?: string;
33
+ selected: string;
34
+ private onPop;
35
+ connectedCallback(): void;
36
+ disconnectedCallback(): void;
37
+ updated(changed: Map<string, unknown>): void;
38
+ /** The key the address names, or the first tab when it names none of them. */
39
+ keyInAddress(): string;
40
+ private read;
41
+ /** Choose a tab: the address follows, and the screen hears it. */
42
+ select(key: string): void;
43
+ private announce;
44
+ private onKey;
45
+ render(): import("lit-html").TemplateResult<1>;
46
+ }
47
+ declare global {
48
+ interface HTMLElementTagNameMap {
49
+ 'ox-url-tabs': OxUrlTabs;
50
+ }
51
+ }