@things-factory/apptool-ui 10.1.27 → 10.1.29

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":"AAAA,OAAO,sBAAsB,CAAA;AAC7B,OAAO,0BAA0B,CAAA;AACjC,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,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAE1D,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAG5F,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAmBI,EAAE;IAEN,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAEtG,2FAA2F;IAC3F,MAAM,KAAK,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IAEzE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,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,KAAK,KAAK,MAAM,EAAE,CAAC;QACrB,cAAc,CAAC;YACb,IAAI,EAAE,YAAY;YAClB,QAAQ,EAAE;gBACR,IAAI,EAAE,IAAI;gBACV,4FAA4F;gBAC5F,KAAK,EAAE,cAAc,CAAC,OAAO;gBAC7B,QAAQ,EAAE,IAAI,CAAA,8BAA8B,OAAO,sBAAsB;aAC1E;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,IAAI,GAAG,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAE9C,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,IAAI,WAAW;QAAE,cAAc,CAAC;YAC9B,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE;gBACR,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,IAAI,CAAA,kCAAkC;aACjD;YACD,QAAQ,EAAE,iBAAiB,CAAC,cAAc;SAC3C,CAAC,CAAA;AACJ,CAAC","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,56 @@
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. The rail
42
+ * goes away on a mobile device in portrait or when the app asks for `hovering`, not at some
43
+ * window width.
44
+ */
45
+ private renderMenuButton;
46
+ /** What the app is showing. The list and the move are the app's; the shape is the band's. */
47
+ private renderScope;
48
+ /**
49
+ * Notifications arrive while you are somewhere else, so their count cannot live in a page.
50
+ *
51
+ * It opens the `notification` viewpart every sibling app registers. plant pressed this for a
52
+ * while with nothing behind it, because the app had drawn the count over a door it had never
53
+ * registered.
54
+ */
55
+ private renderBell;
56
+ }
@@ -0,0 +1,336 @@
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
+ * A tool arrives from the registry as whatever it was written for.
162
+ *
163
+ * lite-menu's hamburger and more-ui's morenda button are a bare md-icon apiece, sized and
164
+ * spaced by the coloured app toolbar's own rules. Drawn here they came out as 18x18 marks with
165
+ * cursor auto next to this row's 28px pill buttons - measured on warehouse (:3500,
166
+ * 2026-09-18): they did not look like something you could press.
167
+ *
168
+ * Their templates render inside this shadow root, so the band can give them its own shape.
169
+ * The selector is a direct child of a slot, which is a tool; the band's own buttons keep their
170
+ * icons inside a .ctl and are not touched.
171
+ */
172
+ .slot > md-icon,
173
+ .slot > button:not(.ctl),
174
+ .slot > a {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ gap: 6px;
179
+ height: 28px;
180
+ min-width: 28px;
181
+ padding: 0 8px;
182
+ box-sizing: border-box;
183
+ border: 1px solid var(--md-sys-color-outline-variant);
184
+ border-radius: 7px;
185
+ background-color: var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));
186
+ color: var(--md-sys-color-on-surface);
187
+ font: inherit;
188
+ font-size: 12px;
189
+ text-decoration: none;
190
+ cursor: pointer;
191
+ }
192
+
193
+ .slot > md-icon:hover,
194
+ .slot > button:not(.ctl):hover,
195
+ .slot > a:hover {
196
+ border-color: var(--md-sys-color-primary);
197
+ color: var(--md-sys-color-primary);
198
+ }
199
+
200
+ .slot > md-icon:focus-visible,
201
+ .slot > button:not(.ctl):focus-visible,
202
+ .slot > a:focus-visible {
203
+ outline: 2px solid var(--md-sys-color-primary);
204
+ outline-offset: 2px;
205
+ }
206
+ `
207
+ ]; }
208
+ connectedCallback() {
209
+ super.connectedCallback();
210
+ const read = () => {
211
+ const state = store.getState();
212
+ this.tools = state.apptool?.tools || [];
213
+ this.menuPartShow = state.layout?.viewparts?.['ox-menu-part']?.show;
214
+ this.unread = state.notification?.badge || 0;
215
+ };
216
+ read();
217
+ this.unsubscribeStore = store.subscribe(read);
218
+ this.scope = getHeaderScope();
219
+ this.unsubscribeScope = subscribeHeaderScope(() => (this.scope = getHeaderScope()));
220
+ }
221
+ disconnectedCallback() {
222
+ this.unsubscribeStore?.();
223
+ this.unsubscribeScope?.();
224
+ super.disconnectedCallback();
225
+ }
226
+ render() {
227
+ const [frontEnd, front, center, rear, rearEnd] = SLOT_ORDER.map(slot => toolsBySlot(this.tools)[slot]);
228
+ const mark = wordmark(this.product);
229
+ return html `
230
+ <div class="band">
231
+ <span class="slot">
232
+ ${menuButtonShown(this.menuPartShow, frontEnd) ? this.renderMenuButton() : nothing}
233
+ ${frontEnd.map(tool => tool.template)}
234
+ </span>
235
+
236
+ <span class="slot">
237
+ ${mark ? html `<span class="wordmark"><b>${mark.lead}</b>${mark.name}</span>` : nothing} ${this.renderScope()}
238
+ ${front.map(tool => tool.template)}
239
+ </span>
240
+
241
+ <span class="grow"></span>
242
+ <span class="slot">${center.map(tool => tool.template)}</span>
243
+ <span class="grow"></span>
244
+
245
+ <span class="slot">${rear.map(tool => tool.template)}</span>
246
+ <span class="slot">${rearEnd.map(tool => tool.template)} ${this.renderBell()}</span>
247
+ </div>
248
+ `;
249
+ }
250
+ /**
251
+ * The way into the menu while the rail is hidden.
252
+ *
253
+ * lite-menu registers its own hamburger in the app toolbar, which an app running this band has
254
+ * turned off; if it did register one, `menuButtonShown` sees it and this is not drawn. The rail
255
+ * goes away on a mobile device in portrait or when the app asks for `hovering`, not at some
256
+ * window width.
257
+ */
258
+ renderMenuButton() {
259
+ return html `
260
+ <button
261
+ class="ctl"
262
+ title=${i18next.t('label.menu', { defaultValue: 'menu' })}
263
+ @click=${() => toggleOverlay('ox-menu-part', { backdrop: true })}
264
+ >
265
+ <md-icon>view_headline</md-icon>
266
+ </button>
267
+ `;
268
+ }
269
+ /** What the app is showing. The list and the move are the app's; the shape is the band's. */
270
+ renderScope() {
271
+ const scope = this.scope;
272
+ switch (scopeShape(scope)) {
273
+ case 'none':
274
+ return nothing;
275
+ case 'name':
276
+ return html `<span class="scope-name" title=${scope.label}>${scopeName(scope)}</span>`;
277
+ case 'picker':
278
+ return html `
279
+ <span class="ctl scope" title=${scope.label}>
280
+ <select
281
+ aria-label=${scope.label}
282
+ @change=${(event) => scope.onSelect?.(event.target.value)}
283
+ >
284
+ ${(scope.items || []).map(item => html `<option value=${item.id} ?selected=${item.id === scope.selectedId}>${item.label}</option>`)}
285
+ </select>
286
+ <md-icon>expand_more</md-icon>
287
+ </span>
288
+ `;
289
+ }
290
+ }
291
+ /**
292
+ * Notifications arrive while you are somewhere else, so their count cannot live in a page.
293
+ *
294
+ * It opens the `notification` viewpart every sibling app registers. plant pressed this for a
295
+ * while with nothing behind it, because the app had drawn the count over a door it had never
296
+ * registered.
297
+ */
298
+ renderBell() {
299
+ const count = unreadLabel(this.unread);
300
+ return html `
301
+ <button
302
+ class="ctl"
303
+ title=${i18next.t('label.notification', { defaultValue: 'notifications' })}
304
+ @click=${() => toggleOverlay('notification', { backdrop: true })}
305
+ >
306
+ <md-icon>${count ? 'notifications' : 'notifications_none'}</md-icon>
307
+ ${count ? html `<span class="count">${count}</span>` : nothing}
308
+ </button>
309
+ `;
310
+ }
311
+ };
312
+ __decorate([
313
+ property({ type: String }),
314
+ __metadata("design:type", String)
315
+ ], AppHeaderBand.prototype, "product", void 0);
316
+ __decorate([
317
+ state(),
318
+ __metadata("design:type", Array)
319
+ ], AppHeaderBand.prototype, "tools", void 0);
320
+ __decorate([
321
+ state(),
322
+ __metadata("design:type", Object)
323
+ ], AppHeaderBand.prototype, "menuPartShow", void 0);
324
+ __decorate([
325
+ state(),
326
+ __metadata("design:type", Object)
327
+ ], AppHeaderBand.prototype, "unread", void 0);
328
+ __decorate([
329
+ state(),
330
+ __metadata("design:type", Object)
331
+ ], AppHeaderBand.prototype, "scope", void 0);
332
+ AppHeaderBand = __decorate([
333
+ customElement('app-header-band')
334
+ ], AppHeaderBand);
335
+ export { AppHeaderBand };
336
+ //# 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":";AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAuB,MAAM,KAAK,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEtC,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,QAAQ,EAGT,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAErE;;;;;;;;;;;;;;;;;;;;GAoBG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QA+KY,UAAK,GAAc,EAAE,CAAA;QACrB,iBAAY,GAAwB,SAAS,CAAA;QAC7C,WAAM,GAAG,CAAC,CAAA;IA2H7B,CAAC;aA3SQ,WAAM,GAAG;QACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuKF;KACF,AAzKY,CAyKZ;IAaD,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QAEzB,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAS,CAAA;YACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;YACvC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,cAAc,CAAC,EAAE,IAAI,CAAA;YACnE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,YAAY,EAAE,KAAK,IAAI,CAAC,CAAA;QAC9C,CAAC,CAAA;QAED,IAAI,EAAE,CAAA;QACN,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QAC7C,IAAI,CAAC,KAAK,GAAG,cAAc,EAAE,CAAA;QAC7B,IAAI,CAAC,gBAAgB,GAAG,oBAAoB,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,cAAc,EAAE,CAAC,CAAC,CAAA;IACrF,CAAC;IAED,oBAAoB;QAClB,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAA;QACzB,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAA;QACzB,KAAK,CAAC,oBAAoB,EAAE,CAAA;IAC9B,CAAC;IAED,MAAM;QACJ,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;QACtG,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAEnC,OAAO,IAAI,CAAA;;;YAGH,eAAe,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,OAAO;YAChF,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;;;;YAInC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA,6BAA6B,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE;YAC1G,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;;;;6BAIf,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;;;6BAGjC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;6BAC/B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE;;KAE/E,CAAA;IACH,CAAC;IAED;;;;;;;OAOG;IACK,gBAAgB;QACtB,OAAO,IAAI,CAAA;;;gBAGC,OAAO,CAAC,CAAC,CAAC,YAAY,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC;iBAChD,GAAG,EAAE,CAAC,aAAa,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;;;KAInE,CAAA;IACH,CAAC;IAED,6FAA6F;IACrF,WAAW;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAA;QAExB,QAAQ,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,KAAK,MAAM;gBACT,OAAO,OAAO,CAAA;YAEhB,KAAK,MAAM;gBACT,OAAO,IAAI,CAAA,kCAAkC,KAAM,CAAC,KAAK,IAAI,SAAS,CAAC,KAAK,CAAC,SAAS,CAAA;YAExF,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAA;0CACuB,KAAM,CAAC,KAAK;;2BAE3B,KAAM,CAAC,KAAK;wBACf,CAAC,KAAY,EAAE,EAAE,CAAC,KAAM,CAAC,QAAQ,EAAE,CAAE,KAAK,CAAC,MAA4B,CAAC,KAAK,CAAC;;gBAEtF,CAAC,KAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CACxB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAA,iBAAiB,IAAI,CAAC,EAAE,cAAc,IAAI,CAAC,EAAE,KAAK,KAAM,CAAC,UAAU,IAAI,IAAI,CAAC,KAAK,WAAW,CACzG;;;;SAIN,CAAA;QACL,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACK,UAAU;QAChB,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAEtC,OAAO,IAAI,CAAA;;;gBAGC,OAAO,CAAC,CAAC,CAAC,oBAAoB,EAAE,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC;iBACjE,GAAG,EAAE,CAAC,aAAa,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;mBAErD,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,oBAAoB;UACvD,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,uBAAuB,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO;;KAEhE,CAAA;IACH,CAAC;;AA9H2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;8CAAiB;AAE3B;IAAhB,KAAK,EAAE;;4CAA8B;AACrB;IAAhB,KAAK,EAAE;;mDAAsD;AAC7C;IAAhB,KAAK,EAAE;;6CAAmB;AACV;IAAhB,KAAK,EAAE;;4CAA4B;AAlLzB,aAAa;IADzB,aAAa,CAAC,iBAAiB,CAAC;GACpB,aAAa,CA4SzB","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 * A tool arrives from the registry as whatever it was written for.\n *\n * lite-menu's hamburger and more-ui's morenda button are a bare md-icon apiece, sized and\n * spaced by the coloured app toolbar's own rules. Drawn here they came out as 18x18 marks with\n * cursor auto next to this row's 28px pill buttons - measured on warehouse (:3500,\n * 2026-09-18): they did not look like something you could press.\n *\n * Their templates render inside this shadow root, so the band can give them its own shape.\n * The selector is a direct child of a slot, which is a tool; the band's own buttons keep their\n * icons inside a .ctl and are not touched.\n */\n .slot > md-icon,\n .slot > button:not(.ctl),\n .slot > a {\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 text-decoration: none;\n cursor: pointer;\n }\n\n .slot > md-icon:hover,\n .slot > button:not(.ctl):hover,\n .slot > a:hover {\n border-color: var(--md-sys-color-primary);\n color: var(--md-sys-color-primary);\n }\n\n .slot > md-icon:focus-visible,\n .slot > button:not(.ctl):focus-visible,\n .slot > a:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 2px;\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. The rail\n * goes away on a mobile device in portrait or when the app asks for `hovering`, not at some\n * window width.\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"]}