@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.
- package/dist-client/index.d.ts +17 -2
- package/dist-client/index.js +31 -17
- package/dist-client/index.js.map +1 -1
- package/dist-client/layout/app-header-band.d.ts +56 -0
- package/dist-client/layout/app-header-band.js +336 -0
- package/dist-client/layout/app-header-band.js.map +1 -0
- package/dist-client/layout/header-band-model.d.ts +93 -0
- package/dist-client/layout/header-band-model.js +100 -0
- package/dist-client/layout/header-band-model.js.map +1 -0
- package/dist-client/layout/header-scope.d.ts +12 -0
- package/dist-client/layout/header-scope.js +33 -0
- package/dist-client/layout/header-scope.js.map +1 -0
- package/dist-client/mdibar-setting-let.d.ts +3 -1
- package/dist-client/mdibar-setting-let.js +17 -3
- package/dist-client/mdibar-setting-let.js.map +1 -1
- package/dist-client/mdibar-setting-state.d.ts +29 -0
- package/dist-client/mdibar-setting-state.js +35 -0
- package/dist-client/mdibar-setting-state.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/tests/header-band.test.ts +207 -0
- package/tests/mdibar-setting-state.test.ts +78 -0
- package/translations/en.json +3 -2
- package/translations/ja.json +3 -2
- package/translations/ko.json +3 -2
- package/translations/ms.json +3 -2
- package/translations/zh.json +3 -2
package/dist-client/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
14
|
-
pageToolbar?:
|
|
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>;
|
package/dist-client/index.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
package/dist-client/index.js.map
CHANGED
|
@@ -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,
|
|
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,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"]}
|