@things-factory/apptool-ui 10.1.27 → 10.1.28

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.
@@ -1,15 +1,30 @@
1
1
  import './layout/app-toolbar';
2
+ import './layout/app-header-band';
2
3
  import './layout/app-busybar';
3
4
  import './layout/page-mdibar';
4
5
  import './layout/page-toolbar';
5
6
  import './mdibar-setting-let';
7
+ export { setHeaderScope, getHeaderScope, subscribeHeaderScope } from './layout/header-scope';
8
+ export type { HeaderScope } from './layout/header-band-model';
6
9
  export declare function setupAppToolPart(options?: {
10
+ /**
11
+ * Which header the app gets (ADR-0060 decision 4).
12
+ *
13
+ * `'toolbar'` is the shell's own bar. `'band'` is the one row plant, twin and figure each wrote
14
+ * for themselves; it reads the same tool registry, so lite-menu's hamburger and more-ui's
15
+ * morenda button reach it without either package knowing which one is drawing. `false` leaves
16
+ * the header to the app - which is what the three did, and what this option replaces.
17
+ */
18
+ header?: 'toolbar' | 'band' | false;
19
+ /** The app's own word, drawn after `operato`. The band owns the shape (ruling ① of the addendum). */
20
+ product?: string;
21
+ /** The older spelling of `header`. `header` wins when both are given. */
7
22
  toolbar?: boolean;
8
23
  busybar?: boolean;
9
24
  /** true = 등록만 하고 설정을 따른다. { default: true } = 저장된 값이 없을 때 켜진 상태로 시작한다. */
10
25
  mdibar?: boolean | {
11
26
  default: boolean;
12
27
  };
13
- /** Removed (ADR-0060 decision 9): the page toolbar that holds the title bar is always there. */
14
- pageToolbar?: never;
28
+ /** The page toolbar is where the page title bar is drawn. On by default (ADR-0060 decision 9). */
29
+ pageToolbar?: boolean;
15
30
  }): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import './layout/app-toolbar';
2
+ import './layout/app-header-band';
2
3
  import './layout/app-busybar';
3
4
  import './layout/page-mdibar';
4
5
  import './layout/page-toolbar';
@@ -8,12 +9,13 @@ import { store, clientSettingStore } from '@operato/shell';
8
9
  import { appendViewpart, VIEWPART_POSITION, VIEWPART_LEVEL } from '@operato/layout';
9
10
  import { isMobileDevice } from '@operato/utils';
10
11
  import { ADD_SETTING } from '@things-factory/setting-base/dist-client';
12
+ import { resolveMdibarShow } from './mdibar-setting-state';
13
+ export { setHeaderScope, getHeaderScope, subscribeHeaderScope } from './layout/header-scope';
11
14
  export async function setupAppToolPart(options = {}) {
12
- const { toolbar = true, busybar = true, mdibar = true } = options;
13
- if ('pageToolbar' in options) {
14
- console.warn('setupAppToolPart: the `pageToolbar` option was removed (ADR-0060 decision 9) - the page toolbar is always registered, so the page title bar always has a place.');
15
- }
16
- if (toolbar) {
15
+ const { header, product, toolbar = true, busybar = true, mdibar = true, pageToolbar = true } = options;
16
+ /* `toolbar: false` meant "no header at all", which is how the three apps turned it off. */
17
+ const which = header !== undefined ? header : toolbar ? 'toolbar' : false;
18
+ if (which === 'toolbar') {
17
19
  appendViewpart({
18
20
  name: 'apptoolbar',
19
21
  viewpart: {
@@ -23,6 +25,18 @@ export async function setupAppToolPart(options = {}) {
23
25
  position: VIEWPART_POSITION.HEADERBAR
24
26
  });
25
27
  }
28
+ if (which === 'band') {
29
+ appendViewpart({
30
+ name: 'apptoolbar',
31
+ viewpart: {
32
+ show: true,
33
+ /* TOPMOST: the band replaces the toolbar rather than sitting under it, as the three did. */
34
+ level: VIEWPART_LEVEL.TOPMOST,
35
+ template: html ` <app-header-band .product=${product}></app-header-band> `
36
+ },
37
+ position: VIEWPART_POSITION.HEADERBAR
38
+ });
39
+ }
26
40
  if (busybar) {
27
41
  appendViewpart({
28
42
  name: 'appbusybar',
@@ -44,8 +58,7 @@ export async function setupAppToolPart(options = {}) {
44
58
  * 번 정하면 그 값이 이긴다.
45
59
  */
46
60
  const stored = ((await clientSettingStore.get('mdibar'))?.value || {}).show;
47
- const fallback = typeof mdibar === 'object' && mdibar !== null ? !!mdibar.default : false;
48
- const show = stored === undefined ? fallback : stored;
61
+ const show = resolveMdibarShow(stored, mdibar);
49
62
  appendViewpart({
50
63
  name: 'appmdibar',
51
64
  viewpart: {
@@ -64,16 +77,17 @@ export async function setupAppToolPart(options = {}) {
64
77
  });
65
78
  }
66
79
  /*
67
- * Always: it is where the page title bar lives. Apps that left it off had no screen title at all
68
- * (operato-twin's framework screens), or showed it in the app bar where search replaced it.
80
+ * On by default: it is where the page title bar lives. Apps that left it off had no screen title at
81
+ * all (operato-twin's framework screens), or showed it in the app bar where search replaced it.
69
82
  */
70
- appendViewpart({
71
- name: 'page-toolbar',
72
- viewpart: {
73
- show: true,
74
- template: html ` <page-toolbar> </page-toolbar> `
75
- },
76
- position: VIEWPART_POSITION.PAGE_HEADERBAR
77
- });
83
+ if (pageToolbar)
84
+ appendViewpart({
85
+ name: 'page-toolbar',
86
+ viewpart: {
87
+ show: true,
88
+ template: html ` <page-toolbar> </page-toolbar> `
89
+ },
90
+ position: VIEWPART_POSITION.PAGE_HEADERBAR
91
+ });
78
92
  }
79
93
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAA;AAC7B,OAAO,sBAAsB,CAAA;AAC7B,OAAO,sBAAsB,CAAA;AAC7B,OAAO,uBAAuB,CAAA;AAC9B,OAAO,sBAAsB,CAAA;AAE7B,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAE/B,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAA;AAEtE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAOI,EAAE;IAEN,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IACjE,IAAI,aAAa,IAAI,OAAO,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CACV,iKAAiK,CAClK,CAAA;IACH,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,cAAc,CAAC;YACb,IAAI,EAAE,YAAY;YAClB,QAAQ,EAAE;gBACR,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,IAAI,CAAA,+BAA+B;aAC9C;YACD,QAAQ,EAAE,iBAAiB,CAAC,SAAS;SACtC,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,cAAc,CAAC;YACb,IAAI,EAAE,YAAY;YAClB,QAAQ,EAAE;gBACR,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,IAAI,CAAA,+BAA+B;aAC9C;YACD,QAAQ,EAAE,iBAAiB,CAAC,SAAS;SACtC,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,MAAM,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;QAChC;;;;;;;;WAQG;QACH,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;QAC3E,MAAM,QAAQ,GAAG,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAE,MAAc,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAA;QAClG,MAAM,IAAI,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAA;QAErD,cAAc,CAAC;YACb,IAAI,EAAE,WAAW;YACjB,QAAQ,EAAE;gBACR,IAAI;gBACJ,KAAK,EAAE,cAAc,CAAC,OAAO;gBAC7B,QAAQ,EAAE,IAAI,CAAA,+BAA+B;aAC9C;YACD,QAAQ,EAAE,iBAAiB,CAAC,cAAc;SAC3C,CAAC,CAAA;QAEF,KAAK,CAAC,QAAQ,CAAC;YACb,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE;gBACP,GAAG,EAAE,EAAE;gBACP,QAAQ,EAAE,IAAI,CAAA,6CAA6C;aAC5D;SACF,CAAC,CAAA;IACJ,CAAC;IAED;;;OAGG;IACH,cAAc,CAAC;QACb,IAAI,EAAE,cAAc;QACpB,QAAQ,EAAE;YACR,IAAI,EAAE,IAAI;YACV,QAAQ,EAAE,IAAI,CAAA,kCAAkC;SACjD;QACD,QAAQ,EAAE,iBAAiB,CAAC,cAAc;KAC3C,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import './layout/app-toolbar'\nimport './layout/app-busybar'\nimport './layout/page-mdibar'\nimport './layout/page-toolbar'\nimport './mdibar-setting-let'\n\nimport { html } from 'lit-html'\n\nimport { store, clientSettingStore } from '@operato/shell'\nimport { appendViewpart, VIEWPART_POSITION, VIEWPART_LEVEL } from '@operato/layout'\nimport { isMobileDevice } from '@operato/utils'\n\nimport { ADD_SETTING } from '@things-factory/setting-base/dist-client'\n\nexport async function setupAppToolPart(\n options: {\n toolbar?: boolean\n busybar?: boolean\n /** true = 등록만 하고 설정을 따른다. { default: true } = 저장된 값이 없을 때 켜진 상태로 시작한다. */\n mdibar?: boolean | { default: boolean }\n /** Removed (ADR-0060 decision 9): the page toolbar that holds the title bar is always there. */\n pageToolbar?: never\n } = {}\n) {\n const { toolbar = true, busybar = true, mdibar = true } = options\n if ('pageToolbar' in options) {\n console.warn(\n 'setupAppToolPart: the `pageToolbar` option was removed (ADR-0060 decision 9) - the page toolbar is always registered, so the page title bar always has a place.'\n )\n }\n\n if (toolbar) {\n appendViewpart({\n name: 'apptoolbar',\n viewpart: {\n show: true,\n template: html` <app-toolbar></app-toolbar> `\n },\n position: VIEWPART_POSITION.HEADERBAR\n })\n }\n\n if (busybar) {\n appendViewpart({\n name: 'appbusybar',\n viewpart: {\n show: true,\n template: html` <app-busybar></app-busybar> `\n },\n position: VIEWPART_POSITION.HEADERBAR\n })\n }\n\n if (mdibar && !isMobileDevice()) {\n /*\n * 저장된 값이 없으면 **앱이 정한 기본값**을 쓴다.\n *\n * 전에는 저장된 값만 봤고, 없으면 꺼진 것으로 다뤘다. 그래서 처음 들어온 사용자는 MDI 를 쓸 수\n * 있다는 것 자체를 모른다 — 설정을 뒤져 찾아야 켤 수 있었다.\n *\n * `mdibar` 에 true 를 주면 예전과 같고, `{ default: true }` 를 주면 처음부터 켜진다. 사용자가 한\n * 번 정하면 그 값이 이긴다.\n */\n const stored = ((await clientSettingStore.get('mdibar'))?.value || {}).show\n const fallback = typeof mdibar === 'object' && mdibar !== null ? !!(mdibar as any).default : false\n const show = stored === undefined ? fallback : stored\n\n appendViewpart({\n name: 'appmdibar',\n viewpart: {\n show,\n level: VIEWPART_LEVEL.TOPMOST,\n template: html` <page-mdibar></page-mdibar> `\n },\n position: VIEWPART_POSITION.PAGE_HEADERBAR\n })\n\n store.dispatch({\n type: ADD_SETTING,\n setting: {\n seq: 21,\n template: html` <mdibar-setting-let></mdibar-setting-let> `\n }\n })\n }\n\n /*\n * Always: it is where the page title bar lives. Apps that left it off had no screen title at all\n * (operato-twin's framework screens), or showed it in the app bar where search replaced it.\n */\n appendViewpart({\n name: 'page-toolbar',\n viewpart: {\n show: true,\n template: html` <page-toolbar> </page-toolbar> `\n },\n position: VIEWPART_POSITION.PAGE_HEADERBAR\n })\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"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","sourcesContent":["import './layout/app-toolbar'\nimport './layout/app-header-band'\nimport './layout/app-busybar'\nimport './layout/page-mdibar'\nimport './layout/page-toolbar'\nimport './mdibar-setting-let'\n\nimport { html } from 'lit-html'\n\nimport { store, clientSettingStore } from '@operato/shell'\nimport { appendViewpart, VIEWPART_POSITION, VIEWPART_LEVEL } from '@operato/layout'\nimport { isMobileDevice } from '@operato/utils'\n\nimport { ADD_SETTING } from '@things-factory/setting-base/dist-client'\n\nimport { resolveMdibarShow } from './mdibar-setting-state'\n\nexport { setHeaderScope, getHeaderScope, subscribeHeaderScope } from './layout/header-scope'\nexport type { HeaderScope } from './layout/header-band-model'\n\nexport async function setupAppToolPart(\n options: {\n /**\n * Which header the app gets (ADR-0060 decision 4).\n *\n * `'toolbar'` is the shell's own bar. `'band'` is the one row plant, twin and figure each wrote\n * for themselves; it reads the same tool registry, so lite-menu's hamburger and more-ui's\n * morenda button reach it without either package knowing which one is drawing. `false` leaves\n * the header to the app - which is what the three did, and what this option replaces.\n */\n header?: 'toolbar' | 'band' | false\n /** The app's own word, drawn after `operato`. The band owns the shape (ruling ① of the addendum). */\n product?: string\n /** The older spelling of `header`. `header` wins when both are given. */\n toolbar?: boolean\n busybar?: boolean\n /** true = 등록만 하고 설정을 따른다. { default: true } = 저장된 값이 없을 때 켜진 상태로 시작한다. */\n mdibar?: boolean | { default: boolean }\n /** The page toolbar is where the page title bar is drawn. On by default (ADR-0060 decision 9). */\n pageToolbar?: boolean\n } = {}\n) {\n const { header, product, toolbar = true, busybar = true, mdibar = true, pageToolbar = true } = options\n\n /* `toolbar: false` meant \"no header at all\", which is how the three apps turned it off. */\n const which = header !== undefined ? header : toolbar ? 'toolbar' : false\n\n if (which === 'toolbar') {\n appendViewpart({\n name: 'apptoolbar',\n viewpart: {\n show: true,\n template: html` <app-toolbar></app-toolbar> `\n },\n position: VIEWPART_POSITION.HEADERBAR\n })\n }\n\n if (which === 'band') {\n appendViewpart({\n name: 'apptoolbar',\n viewpart: {\n show: true,\n /* TOPMOST: the band replaces the toolbar rather than sitting under it, as the three did. */\n level: VIEWPART_LEVEL.TOPMOST,\n template: html` <app-header-band .product=${product}></app-header-band> `\n },\n position: VIEWPART_POSITION.HEADERBAR\n })\n }\n\n if (busybar) {\n appendViewpart({\n name: 'appbusybar',\n viewpart: {\n show: true,\n template: html` <app-busybar></app-busybar> `\n },\n position: VIEWPART_POSITION.HEADERBAR\n })\n }\n\n if (mdibar && !isMobileDevice()) {\n /*\n * 저장된 값이 없으면 **앱이 정한 기본값**을 쓴다.\n *\n * 전에는 저장된 값만 봤고, 없으면 꺼진 것으로 다뤘다. 그래서 처음 들어온 사용자는 MDI 를 쓸 수\n * 있다는 것 자체를 모른다 — 설정을 뒤져 찾아야 켤 수 있었다.\n *\n * `mdibar` 에 true 를 주면 예전과 같고, `{ default: true }` 를 주면 처음부터 켜진다. 사용자가 한\n * 번 정하면 그 값이 이긴다.\n */\n const stored = ((await clientSettingStore.get('mdibar'))?.value || {}).show\n const show = resolveMdibarShow(stored, mdibar)\n\n appendViewpart({\n name: 'appmdibar',\n viewpart: {\n show,\n level: VIEWPART_LEVEL.TOPMOST,\n template: html` <page-mdibar></page-mdibar> `\n },\n position: VIEWPART_POSITION.PAGE_HEADERBAR\n })\n\n store.dispatch({\n type: ADD_SETTING,\n setting: {\n seq: 21,\n template: html` <mdibar-setting-let></mdibar-setting-let> `\n }\n })\n }\n\n /*\n * On by default: it is where the page title bar lives. Apps that left it off had no screen title at\n * all (operato-twin's framework screens), or showed it in the app bar where search replaced it.\n */\n if (pageToolbar) appendViewpart({\n name: 'page-toolbar',\n viewpart: {\n show: true,\n template: html` <page-toolbar> </page-toolbar> `\n },\n position: VIEWPART_POSITION.PAGE_HEADERBAR\n })\n}\n"]}
@@ -0,0 +1,54 @@
1
+ import '@material/web/icon/icon.js';
2
+ import { LitElement, type TemplateResult } from 'lit';
3
+ /**
4
+ * The top header, one row — the shared copy of what plant, twin and figure each wrote.
5
+ *
6
+ * ADR-0060 decision 4 and its 2026-09-18 addendum. Three apps turned the app toolbar off and wrote
7
+ * their own row, 1,174 lines between them, and the three rows disagreed on the button border, the
8
+ * ask box radius, the icon set, where the unread count sits and which colour tokens to reach for.
9
+ * Two defects came out of that: twin's row had no way into the menu when the rail hid itself, and
10
+ * plant's ask box lost the last Korean syllable because it had not learned the guard twin already
11
+ * wrote. This is one row, so what one app learns is what every app has.
12
+ *
13
+ * ── What the band gives, and what stays the app's ─────────────────────────
14
+ * It draws the menu button (while the rail is hidden), the product name, the scope control and the
15
+ * notification bell. `CENTER` and `REAR` are the app's own: plant's ask box, twin's time scrubber
16
+ * and focus chip, figure's dock button. The app puts them there through the registry every module
17
+ * already uses.
18
+ *
19
+ * ── One registry, two renderers ───────────────────────────────────────────
20
+ * This reads `store.getState().apptool.tools`, the same list `app-toolbar` reads, and splits it by
21
+ * `TOOL_POSITION`. An app chooses which of the two draws it (`setupAppToolPart({ header })`). A
22
+ * second registry would make lite-menu's hamburger and more-ui's morenda button pick a side.
23
+ */
24
+ export declare class AppHeaderBand extends LitElement {
25
+ static styles: import("lit").CSSResult[];
26
+ /** The app's one word. The band draws `operato` in front of it. */
27
+ product?: string;
28
+ private tools;
29
+ private menuPartShow;
30
+ private unread;
31
+ private scope?;
32
+ private unsubscribeStore?;
33
+ private unsubscribeScope?;
34
+ connectedCallback(): void;
35
+ disconnectedCallback(): void;
36
+ render(): TemplateResult;
37
+ /**
38
+ * The way into the menu while the rail is hidden.
39
+ *
40
+ * lite-menu registers its own hamburger in the app toolbar, which an app running this band has
41
+ * turned off; if it did register one, `menuButtonShown` sees it and this is not drawn.
42
+ */
43
+ private renderMenuButton;
44
+ /** What the app is showing. The list and the move are the app's; the shape is the band's. */
45
+ private renderScope;
46
+ /**
47
+ * Notifications arrive while you are somewhere else, so their count cannot live in a page.
48
+ *
49
+ * It opens the `notification` viewpart every sibling app registers. plant pressed this for a
50
+ * while with nothing behind it, because the app had drawn the count over a door it had never
51
+ * registered.
52
+ */
53
+ private renderBell;
54
+ }
@@ -0,0 +1,287 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import { css, html, LitElement, nothing } from 'lit';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { i18next } from '@operato/i18n';
6
+ import { toggleOverlay } from '@operato/layout';
7
+ import { store } from '@operato/shell';
8
+ import { menuButtonShown, scopeName, scopeShape, SLOT_ORDER, toolsBySlot, unreadLabel, wordmark } from './header-band-model';
9
+ import { getHeaderScope, subscribeHeaderScope } from './header-scope';
10
+ /**
11
+ * The top header, one row — the shared copy of what plant, twin and figure each wrote.
12
+ *
13
+ * ADR-0060 decision 4 and its 2026-09-18 addendum. Three apps turned the app toolbar off and wrote
14
+ * their own row, 1,174 lines between them, and the three rows disagreed on the button border, the
15
+ * ask box radius, the icon set, where the unread count sits and which colour tokens to reach for.
16
+ * Two defects came out of that: twin's row had no way into the menu when the rail hid itself, and
17
+ * plant's ask box lost the last Korean syllable because it had not learned the guard twin already
18
+ * wrote. This is one row, so what one app learns is what every app has.
19
+ *
20
+ * ── What the band gives, and what stays the app's ─────────────────────────
21
+ * It draws the menu button (while the rail is hidden), the product name, the scope control and the
22
+ * notification bell. `CENTER` and `REAR` are the app's own: plant's ask box, twin's time scrubber
23
+ * and focus chip, figure's dock button. The app puts them there through the registry every module
24
+ * already uses.
25
+ *
26
+ * ── One registry, two renderers ───────────────────────────────────────────
27
+ * This reads `store.getState().apptool.tools`, the same list `app-toolbar` reads, and splits it by
28
+ * `TOOL_POSITION`. An app chooses which of the two draws it (`setupAppToolPart({ header })`). A
29
+ * second registry would make lite-menu's hamburger and more-ui's morenda button pick a side.
30
+ */
31
+ let AppHeaderBand = class AppHeaderBand extends LitElement {
32
+ constructor() {
33
+ super(...arguments);
34
+ this.tools = [];
35
+ this.menuPartShow = undefined;
36
+ this.unread = 0;
37
+ }
38
+ static { this.styles = [
39
+ css `
40
+ :host {
41
+ display: block;
42
+ }
43
+
44
+ .band {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 10px;
48
+ padding: 7px 14px;
49
+ box-sizing: border-box;
50
+ background-color: var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));
51
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
52
+ }
53
+
54
+ /* The product name, not the screen name — the screen names itself in the page title bar. */
55
+ .wordmark {
56
+ display: inline-flex;
57
+ align-items: baseline;
58
+ gap: 5px;
59
+ white-space: nowrap;
60
+ font-size: 13px;
61
+ letter-spacing: 0.01em;
62
+ color: var(--md-sys-color-on-surface-variant);
63
+ }
64
+
65
+ .wordmark b {
66
+ font-size: 14px;
67
+ font-weight: 700;
68
+ color: var(--md-sys-color-on-surface);
69
+ }
70
+
71
+ /* The gap that pushes what follows to the right end. */
72
+ .grow {
73
+ flex: 1;
74
+ }
75
+
76
+ /*
77
+ * One pill shape for everything that can be pressed. twin and figure already agreed on 28px
78
+ * and a 7px radius; plant drew a borderless 8px one. A 1px difference in radius is not worth
79
+ * arguing about, but a button with an edge and a button without one are two different things
80
+ * on one row.
81
+ */
82
+ .ctl {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ gap: 6px;
87
+ height: 28px;
88
+ min-width: 28px;
89
+ padding: 0 8px;
90
+ box-sizing: border-box;
91
+ border: 1px solid var(--md-sys-color-outline-variant);
92
+ border-radius: 7px;
93
+ background-color: var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));
94
+ color: var(--md-sys-color-on-surface);
95
+ font: inherit;
96
+ font-size: 12px;
97
+ cursor: pointer;
98
+ }
99
+
100
+ .ctl:hover {
101
+ border-color: var(--md-sys-color-primary);
102
+ color: var(--md-sys-color-primary);
103
+ }
104
+
105
+ .ctl:focus-visible {
106
+ outline: 2px solid var(--md-sys-color-primary);
107
+ outline-offset: 2px;
108
+ }
109
+
110
+ .scope select {
111
+ border: 0;
112
+ background: transparent;
113
+ color: inherit;
114
+ font: inherit;
115
+ font-weight: 600;
116
+ outline: none;
117
+ }
118
+
119
+ /* One scope draws its name, with nothing to press. */
120
+ .scope-name {
121
+ white-space: nowrap;
122
+ font-size: 12px;
123
+ font-weight: 600;
124
+ color: var(--md-sys-color-on-surface);
125
+ }
126
+
127
+ md-icon {
128
+ font-size: 18px;
129
+ --md-icon-size: 18px;
130
+ }
131
+
132
+ /*
133
+ * The count sits **inside** the pill.
134
+ *
135
+ * twin hung it off the corner at -4px, where it lands on the button's own border; figure's
136
+ * comment records the same trouble with the notification module's badge, whose host sets
137
+ * font-size 2em and places its dot outside the box it is given.
138
+ */
139
+ .count {
140
+ min-width: 15px;
141
+ height: 15px;
142
+ padding: 0 4px;
143
+ box-sizing: border-box;
144
+ border-radius: 8px;
145
+ background-color: var(--md-sys-color-error);
146
+ color: var(--md-sys-color-on-error);
147
+ font-size: 10px;
148
+ font-weight: 700;
149
+ line-height: 15px;
150
+ text-align: center;
151
+ }
152
+
153
+ .slot {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ gap: 10px;
157
+ min-width: 0;
158
+ }
159
+ `
160
+ ]; }
161
+ connectedCallback() {
162
+ super.connectedCallback();
163
+ const read = () => {
164
+ const state = store.getState();
165
+ this.tools = state.apptool?.tools || [];
166
+ this.menuPartShow = state.layout?.viewparts?.['ox-menu-part']?.show;
167
+ this.unread = state.notification?.badge || 0;
168
+ };
169
+ read();
170
+ this.unsubscribeStore = store.subscribe(read);
171
+ this.scope = getHeaderScope();
172
+ this.unsubscribeScope = subscribeHeaderScope(() => (this.scope = getHeaderScope()));
173
+ }
174
+ disconnectedCallback() {
175
+ this.unsubscribeStore?.();
176
+ this.unsubscribeScope?.();
177
+ super.disconnectedCallback();
178
+ }
179
+ render() {
180
+ const [frontEnd, front, center, rear, rearEnd] = SLOT_ORDER.map(slot => toolsBySlot(this.tools)[slot]);
181
+ const mark = wordmark(this.product);
182
+ return html `
183
+ <div class="band">
184
+ <span class="slot">
185
+ ${menuButtonShown(this.menuPartShow, frontEnd) ? this.renderMenuButton() : nothing}
186
+ ${frontEnd.map(tool => tool.template)}
187
+ </span>
188
+
189
+ <span class="slot">
190
+ ${mark ? html `<span class="wordmark"><b>${mark.lead}</b>${mark.name}</span>` : nothing} ${this.renderScope()}
191
+ ${front.map(tool => tool.template)}
192
+ </span>
193
+
194
+ <span class="grow"></span>
195
+ <span class="slot">${center.map(tool => tool.template)}</span>
196
+ <span class="grow"></span>
197
+
198
+ <span class="slot">${rear.map(tool => tool.template)}</span>
199
+ <span class="slot">${rearEnd.map(tool => tool.template)} ${this.renderBell()}</span>
200
+ </div>
201
+ `;
202
+ }
203
+ /**
204
+ * The way into the menu while the rail is hidden.
205
+ *
206
+ * lite-menu registers its own hamburger in the app toolbar, which an app running this band has
207
+ * turned off; if it did register one, `menuButtonShown` sees it and this is not drawn.
208
+ */
209
+ renderMenuButton() {
210
+ return html `
211
+ <button
212
+ class="ctl"
213
+ title=${i18next.t('label.menu', { defaultValue: 'menu' })}
214
+ @click=${() => toggleOverlay('ox-menu-part', { backdrop: true })}
215
+ >
216
+ <md-icon>view_headline</md-icon>
217
+ </button>
218
+ `;
219
+ }
220
+ /** What the app is showing. The list and the move are the app's; the shape is the band's. */
221
+ renderScope() {
222
+ const scope = this.scope;
223
+ switch (scopeShape(scope)) {
224
+ case 'none':
225
+ return nothing;
226
+ case 'name':
227
+ return html `<span class="scope-name" title=${scope.label}>${scopeName(scope)}</span>`;
228
+ case 'picker':
229
+ return html `
230
+ <span class="ctl scope" title=${scope.label}>
231
+ <select
232
+ aria-label=${scope.label}
233
+ @change=${(event) => scope.onSelect?.(event.target.value)}
234
+ >
235
+ ${(scope.items || []).map(item => html `<option value=${item.id} ?selected=${item.id === scope.selectedId}>${item.label}</option>`)}
236
+ </select>
237
+ <md-icon>expand_more</md-icon>
238
+ </span>
239
+ `;
240
+ }
241
+ }
242
+ /**
243
+ * Notifications arrive while you are somewhere else, so their count cannot live in a page.
244
+ *
245
+ * It opens the `notification` viewpart every sibling app registers. plant pressed this for a
246
+ * while with nothing behind it, because the app had drawn the count over a door it had never
247
+ * registered.
248
+ */
249
+ renderBell() {
250
+ const count = unreadLabel(this.unread);
251
+ return html `
252
+ <button
253
+ class="ctl"
254
+ title=${i18next.t('label.notification', { defaultValue: 'notifications' })}
255
+ @click=${() => toggleOverlay('notification', { backdrop: true })}
256
+ >
257
+ <md-icon>${count ? 'notifications' : 'notifications_none'}</md-icon>
258
+ ${count ? html `<span class="count">${count}</span>` : nothing}
259
+ </button>
260
+ `;
261
+ }
262
+ };
263
+ __decorate([
264
+ property({ type: String }),
265
+ __metadata("design:type", String)
266
+ ], AppHeaderBand.prototype, "product", void 0);
267
+ __decorate([
268
+ state(),
269
+ __metadata("design:type", Array)
270
+ ], AppHeaderBand.prototype, "tools", void 0);
271
+ __decorate([
272
+ state(),
273
+ __metadata("design:type", Object)
274
+ ], AppHeaderBand.prototype, "menuPartShow", void 0);
275
+ __decorate([
276
+ state(),
277
+ __metadata("design:type", Object)
278
+ ], AppHeaderBand.prototype, "unread", void 0);
279
+ __decorate([
280
+ state(),
281
+ __metadata("design:type", Object)
282
+ ], AppHeaderBand.prototype, "scope", void 0);
283
+ AppHeaderBand = __decorate([
284
+ customElement('app-header-band')
285
+ ], AppHeaderBand);
286
+ export { AppHeaderBand };
287
+ //# sourceMappingURL=app-header-band.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-header-band.js","sourceRoot":"","sources":["../../client/layout/app-header-band.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing, type TemplateResult } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\nimport { toggleOverlay } from '@operato/layout'\nimport { store } from '@operato/shell'\n\nimport {\n menuButtonShown,\n scopeName,\n scopeShape,\n SLOT_ORDER,\n toolsBySlot,\n unreadLabel,\n wordmark,\n type AppTool,\n type HeaderScope\n} from './header-band-model'\nimport { getHeaderScope, subscribeHeaderScope } from './header-scope'\n\n/**\n * The top header, one row — the shared copy of what plant, twin and figure each wrote.\n *\n * ADR-0060 decision 4 and its 2026-09-18 addendum. Three apps turned the app toolbar off and wrote\n * their own row, 1,174 lines between them, and the three rows disagreed on the button border, the\n * ask box radius, the icon set, where the unread count sits and which colour tokens to reach for.\n * Two defects came out of that: twin's row had no way into the menu when the rail hid itself, and\n * plant's ask box lost the last Korean syllable because it had not learned the guard twin already\n * wrote. This is one row, so what one app learns is what every app has.\n *\n * ── What the band gives, and what stays the app's ─────────────────────────\n * It draws the menu button (while the rail is hidden), the product name, the scope control and the\n * notification bell. `CENTER` and `REAR` are the app's own: plant's ask box, twin's time scrubber\n * and focus chip, figure's dock button. The app puts them there through the registry every module\n * already uses.\n *\n * ── One registry, two renderers ───────────────────────────────────────────\n * This reads `store.getState().apptool.tools`, the same list `app-toolbar` reads, and splits it by\n * `TOOL_POSITION`. An app chooses which of the two draws it (`setupAppToolPart({ header })`). A\n * second registry would make lite-menu's hamburger and more-ui's morenda button pick a side.\n */\n@customElement('app-header-band')\nexport class AppHeaderBand extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n }\n\n .band {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 7px 14px;\n box-sizing: border-box;\n background-color: var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n\n /* The product name, not the screen name — the screen names itself in the page title bar. */\n .wordmark {\n display: inline-flex;\n align-items: baseline;\n gap: 5px;\n white-space: nowrap;\n font-size: 13px;\n letter-spacing: 0.01em;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n .wordmark b {\n font-size: 14px;\n font-weight: 700;\n color: var(--md-sys-color-on-surface);\n }\n\n /* The gap that pushes what follows to the right end. */\n .grow {\n flex: 1;\n }\n\n /*\n * One pill shape for everything that can be pressed. twin and figure already agreed on 28px\n * and a 7px radius; plant drew a borderless 8px one. A 1px difference in radius is not worth\n * arguing about, but a button with an edge and a button without one are two different things\n * on one row.\n */\n .ctl {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 28px;\n min-width: 28px;\n padding: 0 8px;\n box-sizing: border-box;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 7px;\n background-color: var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));\n color: var(--md-sys-color-on-surface);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n }\n\n .ctl:hover {\n border-color: var(--md-sys-color-primary);\n color: var(--md-sys-color-primary);\n }\n\n .ctl:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 2px;\n }\n\n .scope select {\n border: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n font-weight: 600;\n outline: none;\n }\n\n /* One scope draws its name, with nothing to press. */\n .scope-name {\n white-space: nowrap;\n font-size: 12px;\n font-weight: 600;\n color: var(--md-sys-color-on-surface);\n }\n\n md-icon {\n font-size: 18px;\n --md-icon-size: 18px;\n }\n\n /*\n * The count sits **inside** the pill.\n *\n * twin hung it off the corner at -4px, where it lands on the button's own border; figure's\n * comment records the same trouble with the notification module's badge, whose host sets\n * font-size 2em and places its dot outside the box it is given.\n */\n .count {\n min-width: 15px;\n height: 15px;\n padding: 0 4px;\n box-sizing: border-box;\n border-radius: 8px;\n background-color: var(--md-sys-color-error);\n color: var(--md-sys-color-on-error);\n font-size: 10px;\n font-weight: 700;\n line-height: 15px;\n text-align: center;\n }\n\n .slot {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n }\n `\n ]\n\n /** The app's one word. The band draws `operato` in front of it. */\n @property({ type: String }) product?: string\n\n @state() private tools: AppTool[] = []\n @state() private menuPartShow: boolean | undefined = undefined\n @state() private unread = 0\n @state() private scope?: HeaderScope\n\n private unsubscribeStore?: () => void\n private unsubscribeScope?: () => void\n\n connectedCallback() {\n super.connectedCallback()\n\n const read = () => {\n const state = store.getState() as any\n this.tools = state.apptool?.tools || []\n this.menuPartShow = state.layout?.viewparts?.['ox-menu-part']?.show\n this.unread = state.notification?.badge || 0\n }\n\n read()\n this.unsubscribeStore = store.subscribe(read)\n this.scope = getHeaderScope()\n this.unsubscribeScope = subscribeHeaderScope(() => (this.scope = getHeaderScope()))\n }\n\n disconnectedCallback() {\n this.unsubscribeStore?.()\n this.unsubscribeScope?.()\n super.disconnectedCallback()\n }\n\n render(): TemplateResult {\n const [frontEnd, front, center, rear, rearEnd] = SLOT_ORDER.map(slot => toolsBySlot(this.tools)[slot])\n const mark = wordmark(this.product)\n\n return html`\n <div class=\"band\">\n <span class=\"slot\">\n ${menuButtonShown(this.menuPartShow, frontEnd) ? this.renderMenuButton() : nothing}\n ${frontEnd.map(tool => tool.template)}\n </span>\n\n <span class=\"slot\">\n ${mark ? html`<span class=\"wordmark\"><b>${mark.lead}</b>${mark.name}</span>` : nothing} ${this.renderScope()}\n ${front.map(tool => tool.template)}\n </span>\n\n <span class=\"grow\"></span>\n <span class=\"slot\">${center.map(tool => tool.template)}</span>\n <span class=\"grow\"></span>\n\n <span class=\"slot\">${rear.map(tool => tool.template)}</span>\n <span class=\"slot\">${rearEnd.map(tool => tool.template)} ${this.renderBell()}</span>\n </div>\n `\n }\n\n /**\n * The way into the menu while the rail is hidden.\n *\n * lite-menu registers its own hamburger in the app toolbar, which an app running this band has\n * turned off; if it did register one, `menuButtonShown` sees it and this is not drawn.\n */\n private renderMenuButton(): TemplateResult {\n return html`\n <button\n class=\"ctl\"\n title=${i18next.t('label.menu', { defaultValue: 'menu' })}\n @click=${() => toggleOverlay('ox-menu-part', { backdrop: true })}\n >\n <md-icon>view_headline</md-icon>\n </button>\n `\n }\n\n /** What the app is showing. The list and the move are the app's; the shape is the band's. */\n private renderScope() {\n const scope = this.scope\n\n switch (scopeShape(scope)) {\n case 'none':\n return nothing\n\n case 'name':\n return html`<span class=\"scope-name\" title=${scope!.label}>${scopeName(scope)}</span>`\n\n case 'picker':\n return html`\n <span class=\"ctl scope\" title=${scope!.label}>\n <select\n aria-label=${scope!.label}\n @change=${(event: Event) => scope!.onSelect?.((event.target as HTMLSelectElement).value)}\n >\n ${(scope!.items || []).map(\n item => html`<option value=${item.id} ?selected=${item.id === scope!.selectedId}>${item.label}</option>`\n )}\n </select>\n <md-icon>expand_more</md-icon>\n </span>\n `\n }\n }\n\n /**\n * Notifications arrive while you are somewhere else, so their count cannot live in a page.\n *\n * It opens the `notification` viewpart every sibling app registers. plant pressed this for a\n * while with nothing behind it, because the app had drawn the count over a door it had never\n * registered.\n */\n private renderBell(): TemplateResult {\n const count = unreadLabel(this.unread)\n\n return html`\n <button\n class=\"ctl\"\n title=${i18next.t('label.notification', { defaultValue: 'notifications' })}\n @click=${() => toggleOverlay('notification', { backdrop: true })}\n >\n <md-icon>${count ? 'notifications' : 'notifications_none'}</md-icon>\n ${count ? html`<span class=\"count\">${count}</span>` : nothing}\n </button>\n `\n }\n}\n"]}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * What the shared header band decides, kept apart from how it draws.
3
+ *
4
+ * ADR-0060 decision 4 and its 2026-09-18 addendum. plant, twin and figure each wrote this row for
5
+ * themselves - 386, 603 and 185 lines - and the three drifted: one drew a menu button and another
6
+ * did not, one put the unread count inside the pill and another hung it off the corner, each
7
+ * reached for a different set of colour tokens. The band is one copy of the row; this file is the
8
+ * part of it a test in this repository can reach, since the rendering needs a DOM.
9
+ */
10
+ /** A tool as the registry keeps it. `template` is a lit template; this file never looks inside it. */
11
+ export type AppTool = {
12
+ name?: string;
13
+ position?: string;
14
+ template?: unknown;
15
+ };
16
+ /**
17
+ * The five places, in the order they are drawn, left to right.
18
+ *
19
+ * These are `TOOL_POSITION`'s own values, written out rather than imported: `@operato/layout` is an
20
+ * ES module and this repository's test runner does not transform node_modules, so importing the
21
+ * enum here would take the deciding out of reach of every test. `tests/header-band.test.ts` reads
22
+ * the installed declaration and checks that these five still match it.
23
+ */
24
+ export declare const SLOT_ORDER: readonly ["FRONT_END", "FRONT", "CENTER", "REAR", "REAR_END"];
25
+ export type Slot = (typeof SLOT_ORDER)[number];
26
+ /**
27
+ * The scope an app hands over: what it is showing, and how to change it.
28
+ *
29
+ * The band draws this and knows nothing else about it (ADR-0048 · 0058). plant passes factories,
30
+ * twin passes sites, warehouse passes warehouses, and none of those words reach this package.
31
+ */
32
+ export type HeaderScope = {
33
+ /** What this kind of thing is called, for the control's accessible name. */
34
+ label: string;
35
+ /** What there is to choose from. One or none means there is nothing to choose. */
36
+ items?: {
37
+ id: string;
38
+ label: string;
39
+ }[];
40
+ /** Which one is being shown. */
41
+ selectedId?: string;
42
+ /** Called with the chosen id. Entering it is the app's business, not the band's. */
43
+ onSelect?: (id: string) => void;
44
+ };
45
+ /**
46
+ * Split the one registry into the five slots.
47
+ *
48
+ * The band and `app-toolbar` read the same `store.getState().apptool.tools`, so lite-menu's
49
+ * hamburger and more-ui's morenda button land in the band without either package knowing the band
50
+ * exists. A second registry would mean every contributor choosing a side.
51
+ */
52
+ export declare function toolsBySlot(tools?: readonly AppTool[]): Record<Slot, AppTool[]>;
53
+ /**
54
+ * Whether the band draws its own way into the menu.
55
+ *
56
+ * lite-menu hides the rail on a narrow viewport and registers its hamburger in the **app toolbar**
57
+ * (`TOOL_POSITION.FRONT_END`), which an app running the band has turned off. twin turned it off and
58
+ * drew no button, so on a narrow viewport there was no way to leave the screen you were on
59
+ * (2026-09-18). With the rail on screen a second way in is noise, so the button comes and goes with
60
+ * it.
61
+ *
62
+ * `undefined` means the menu part has not registered yet. That reads as shown, so the button does
63
+ * not flash on during boot.
64
+ */
65
+ export declare function menuButtonShown(menuPartShow: boolean | undefined, frontEndTools?: readonly AppTool[]): boolean;
66
+ /**
67
+ * How much of a scope control there is to draw.
68
+ *
69
+ * A single factory draws its name and no picker: an install with one of something pays nothing for
70
+ * the ability to switch between several. plant already worked this way and the band keeps it.
71
+ */
72
+ export declare function scopeShape(scope?: HeaderScope): 'none' | 'name' | 'picker';
73
+ /** The name to draw for a scope that has no picker. */
74
+ export declare function scopeName(scope?: HeaderScope): string;
75
+ /**
76
+ * The product name, split where it is drawn in two weights: `operato` **plant**.
77
+ *
78
+ * The band owns this shape, and the app gives it one word (ruling ① of the addendum). plant and
79
+ * figure had written the same two-part wordmark by hand and twin had none at all.
80
+ */
81
+ export declare function wordmark(product?: string): {
82
+ lead: string;
83
+ name: string;
84
+ } | undefined;
85
+ /** The count as the bell shows it. Above 99 the exact number stops being worth the width. */
86
+ export declare function unreadLabel(unread: number | undefined): string | undefined;