@things-factory/apptool-ui 10.0.18 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,9 @@ import './mdibar-setting-let';
6
6
  export declare function setupAppToolPart({ toolbar, busybar, mdibar, pageToolbar }: {
7
7
  toolbar?: boolean;
8
8
  busybar?: boolean;
9
- mdibar?: boolean;
9
+ /** true = 등록만 하고 설정을 따른다. { default: true } = 저장된 값이 없을 때 켜진 상태로 시작한다. */
10
+ mdibar?: boolean | {
11
+ default: boolean;
12
+ };
10
13
  pageToolbar?: boolean;
11
14
  }): Promise<void>;
@@ -30,7 +30,18 @@ export async function setupAppToolPart({ toolbar = true, busybar = true, mdibar
30
30
  });
31
31
  }
32
32
  if (mdibar && !isMobileDevice()) {
33
- const show = ((await clientSettingStore.get('mdibar'))?.value || {}).show;
33
+ /*
34
+ * 저장된 값이 없으면 **앱이 정한 기본값**을 쓴다.
35
+ *
36
+ * 전에는 저장된 값만 봤고, 없으면 꺼진 것으로 다뤘다. 그래서 처음 들어온 사용자는 MDI 를 쓸 수
37
+ * 있다는 것 자체를 모른다 — 설정을 뒤져 찾아야 켤 수 있었다.
38
+ *
39
+ * `mdibar` 에 true 를 주면 예전과 같고, `{ default: true }` 를 주면 처음부터 켜진다. 사용자가 한
40
+ * 번 정하면 그 값이 이긴다.
41
+ */
42
+ const stored = ((await clientSettingStore.get('mdibar'))?.value || {}).show;
43
+ const fallback = typeof mdibar === 'object' && mdibar !== null ? !!mdibar.default : false;
44
+ const show = stored === undefined ? fallback : stored;
34
45
  appendViewpart({
35
46
  name: 'appmdibar',
36
47
  viewpart: {
@@ -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,CAAC,EACrC,OAAO,GAAG,IAAI,EACd,OAAO,GAAG,IAAI,EACd,MAAM,GAAG,IAAI,EACb,WAAW,GAAG,KAAK,EAMpB;IACC,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,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;QAEzE,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,IAAI,WAAW,EAAE,CAAC;QAChB,cAAc,CAAC;YACb,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;IACJ,CAAC;AACH,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 toolbar = true,\n busybar = true,\n mdibar = true,\n pageToolbar = false\n}: {\n toolbar?: boolean\n busybar?: boolean\n mdibar?: boolean\n pageToolbar?: boolean\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 const show = ((await clientSettingStore.get('mdibar'))?.value || {}).show\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 if (pageToolbar) {\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}\n"]}
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,CAAC,EACrC,OAAO,GAAG,IAAI,EACd,OAAO,GAAG,IAAI,EACd,MAAM,GAAG,IAAI,EACb,WAAW,GAAG,KAAK,EAOpB;IACC,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,IAAI,WAAW,EAAE,CAAC;QAChB,cAAc,CAAC;YACb,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;IACJ,CAAC;AACH,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 toolbar = true,\n busybar = true,\n mdibar = true,\n pageToolbar = false\n}: {\n toolbar?: boolean\n busybar?: boolean\n /** true = 등록만 하고 설정을 따른다. { default: true } = 저장된 값이 없을 때 켜진 상태로 시작한다. */\n mdibar?: boolean | { default: boolean }\n pageToolbar?: boolean\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 if (pageToolbar) {\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}\n"]}
@@ -5,9 +5,16 @@ import { PageView } from '@operato/shell';
5
5
  declare const PageMDIBar_base: (new (...args: any[]) => LitElement) & typeof LitElement;
6
6
  export declare class PageMDIBar extends PageMDIBar_base {
7
7
  static styles: import("lit").CSSResult[];
8
+ /**
9
+ * 열린 탭들. `routes[0]` 은 홈이다(render 에서 따로 그린다).
10
+ *
11
+ * `name` 은 그 탭이 맡은 **라우트 이름**이다(`job-order-list` 같은 것). 주소를 갱신할 때 「이 주소가
12
+ * 정말 이 탭의 것인가」를 판정하는 데 쓴다 — 없으면 남의 주소가 찍힌다(updated 머리말 참조).
13
+ */
8
14
  routes: {
9
15
  page?: PageView;
10
16
  url: string;
17
+ name?: string;
11
18
  }[];
12
19
  title: string;
13
20
  private _routeCtrl;
@@ -19,6 +26,30 @@ export declare class PageMDIBar extends PageMDIBar_base {
19
26
  private _prevActivePage?;
20
27
  private _prevResourceId?;
21
28
  private _prevParams?;
29
+ /**
30
+ * 열린 탭 목록을 주소와 맞춘다.
31
+ *
32
+ * ── 남의 주소가 찍히던 결함 (2026-09-02) ──────────────────────────────────
33
+ * 항해 한 번에 이 함수가 **두 번** 지나간다. 그 사이에 주소와 활성 페이지가 어긋나 있는 순간이 있다.
34
+ *
35
+ * ① navigate 주소가 새 페이지로 바뀐다. 활성 페이지는 **아직 옛 페이지**다
36
+ * ② 나중에 새 페이지가 붙고 활성 페이지가 바뀐다
37
+ *
38
+ * ①에서도 이 블록이 돌았다. `params` 는 항해마다 새 객체라(`shell/actions/route.js` 의
39
+ * `if (!params) { params = {} }`) 아래 `params !== this._prevParams` 가 **항상 참**이기 때문이다.
40
+ * 그래서 옛 탭을 찾아 새 주소를 찍었다.
41
+ *
42
+ * [확인함] 작업지시를 열고 메뉴에서 품질을 누른 뒤
43
+ *
44
+ * i=1 job-order-list → /quality-list ← 덮였다
45
+ * i=2 quality-list → /quality-list
46
+ *
47
+ * 그 결과 **「작업지시」 탭을 눌러도 아무 일이 없었다** — 품질로 가는 주소라 그대로 머문다. 탭이
48
+ * 죽어 보이는 것이 이것이다.
49
+ *
50
+ * 그래서 탭마다 자기 라우트 이름을 들리고, **주소가 자기 것일 때만** 갱신한다. `params` 비교는 그대로
51
+ * 둔다 — 검색어처럼 질의만 바뀌는 항해도 탭 주소가 따라가야 한다.
52
+ */
22
53
  updated(): void;
23
54
  onScrollByArrow(delta: number): void;
24
55
  onWheelEvent(e: Event): void;
@@ -26,17 +57,20 @@ export declare class PageMDIBar extends PageMDIBar_base {
26
57
  moveTo(route: {
27
58
  page?: PageView;
28
59
  url: string;
60
+ name?: string;
29
61
  }): void;
30
62
  moveToHome(): void;
31
63
  closePage(route: {
32
64
  page?: PageView;
33
65
  url: string;
66
+ name?: string;
34
67
  }, idx: number): void;
35
68
  closeOtherPagesFromActive(): Promise<void>;
36
69
  closeAllPage(): Promise<void>;
37
70
  closeOtherPages(route: {
38
71
  page?: PageView;
39
72
  url: string;
73
+ name?: string;
40
74
  }, idx: number): void;
41
75
  openContextMenu(e: MouseEvent): void;
42
76
  }
@@ -10,6 +10,12 @@ import { OxPrompt } from '@operato/popup';
10
10
  let PageMDIBar = class PageMDIBar extends localize(i18next)(LitElement) {
11
11
  constructor() {
12
12
  super(...arguments);
13
+ /**
14
+ * 열린 탭들. `routes[0]` 은 홈이다(render 에서 따로 그린다).
15
+ *
16
+ * `name` 은 그 탭이 맡은 **라우트 이름**이다(`job-order-list` 같은 것). 주소를 갱신할 때 「이 주소가
17
+ * 정말 이 탭의 것인가」를 판정하는 데 쓴다 — 없으면 남의 주소가 찍힌다(updated 머리말 참조).
18
+ */
13
19
  this.routes = [{ url: this.getHomeRoute() }];
14
20
  this.title = '';
15
21
  this._routeCtrl = new RouteController(this);
@@ -42,9 +48,37 @@ let PageMDIBar = class PageMDIBar extends localize(i18next)(LitElement) {
42
48
  min-width: var(--mdibar-min-width, 100px);
43
49
  max-width: var(--mdibar-max-width, 150px);
44
50
  padding: var(--mdibar-padding, 3px 22px 1px 7px);
45
- border-right: var(--border-dim-color);
46
- border-top: var(--border-dim-color);
47
- border-bottom: var(--border-dim-color);
51
+ /*
52
+ * ── 탭은 위 두 모서리만 둥글다 ──────────────────────────────────────
53
+ *
54
+ * 탭의 은유는 **앞으로 나온 종이**다. 그래서 위는 둥글고 **아래는 각져서** 아래 판에
55
+ * 붙는다. 밑변이 각진 것이 「이 탭이 저 판의 손잡이」라고 말하는 자리다.
56
+ *
57
+ * 모서리 값은 담는 면을 따른다(동심 모서리) — 이 띠는 평평한 면이므로 작은 눈금 6px 이다.
58
+ * 팝업 카드(16px) 안의 컨트롤이 알약인 것과 같은 원칙이고, 반대쪽 끝이다.
59
+ *
60
+ * ── 전에 여기 있던 것 ───────────────────────────────────────────────
61
+ * border-right / border-top / border-bottom: var(--border-dim-color)
62
+ *
63
+ * 탭마다 세 면에 선이 있었다. **걷은 것은 그 선이 죽어서가 아니라, 이 모양에서 선이
64
+ * 너무 많아서다.** 26px 띠에 탭 셋이면 세로선이 셋이고, 여기에 활성 탭의 윤곽 한 올과
65
+ * 아래 판의 경계까지 겹친다. 위가 둥근 활성 탭이 한 장으로 떠오르면 비활성 자리는
66
+ * 물러나 있는 것으로 충분하다.
67
+ *
68
+ * ── 한때 「죽은 선언」이라고 적었다. 틀렸다 ────────────────────────────
69
+ * --border-dim-color 를 못 찾아 무효 선언이라고 단정했는데, 그 토큰은 **테두리 값 한
70
+ * 벌을 통째로** 담고 있다(이름이 -color 로 끝나지만 색이 아니다).
71
+ *
72
+ * shell/client/themes/app-theme.css body { --border-dim-color: 1px solid var(--md-sys-color-outline-variant) }
73
+ *
74
+ * 못 찾은 이유는 :root 에서 읽었기 때문이다 — 선택자가 body 다. 그래서 단축 속성에
75
+ * 쓰이던 이 넷은 **살아서 칠하고 있었다.** 색 자리(border-*-color)에 쓰면 그때 죽는다.
76
+ *
77
+ * [확인함] 2026-09-04 · body 값 「1px solid rgb(214 219 224)」 ·
78
+ * border-bottom: var(--border-dim-color) 가 1px solid #d6dbe0 로 계산된다 ·
79
+ * 바 #e4e8ed · 활성 탭 #f0f2f4 · 본문 바닥 #f0f2f4
80
+ */
81
+ border-radius: var(--mdibar-tab-radius, 6px) var(--mdibar-tab-radius, 6px) 0 0;
48
82
  white-space: nowrap;
49
83
  overflow: hidden;
50
84
  text-overflow: ellipsis;
@@ -54,19 +88,32 @@ let PageMDIBar = class PageMDIBar extends localize(i18next)(LitElement) {
54
88
 
55
89
  div[arrows] {
56
90
  display: flex;
57
- border: var(--border-dim-color);
91
+ /*
92
+ * border: var(--border-dim-color) 였다 — 화살표 세 개를 상자로 둘러쌌다. 걷었다.
93
+ * 그 셋은 이미 각자 동그라미를 받으므로 무리를 상자로 또 묶을 이유가 없다.
94
+ * (그 선언은 살아 있었다. 위 div[route] 의 주석에 그 토큰 이야기를 적어 두었다.)
95
+ */
58
96
  align-items: center;
59
97
  }
60
98
 
99
+ /*
100
+ * 흐림을 여기서 걷었다. 아래에서 이 셋에 **색을 직접 주고**(평소 --ui-muted · 못 누를 때
101
+ * --ui-faint) 있어서, 흐림이 그 위에 또 얹히면 두 번 흐려진다. 「모두 닫기」가 야간 화면에서
102
+ * 1.78:1 이었다(실측 2026-09-05 · UI 요소 기준 3:1).
103
+ */
61
104
  div[arrows] * {
62
- opacity: 0.8;
63
105
  font-size: var(--mdibar-icon-size, 24px);
64
106
  flex: 1;
65
107
  }
66
108
 
109
+ /*
110
+ * 흐림은 0.5 였다. 색까지 함께 흐린 자리가 있어 **두 번 흐려졌고**, 홈 아이콘이 밝은 화면에서
111
+ * 2.93:1 이 됐다(실측 2026-09-05). 0.7 이면 4.3 으로 UI 요소 기준을 넘고, 탭 이름보다
112
+ * 뒤에 서는 것은 그대로다.
113
+ */
67
114
  md-icon {
68
115
  --md-icon-size: var(--mdibar-icon-size, 18px);
69
- opacity: 0.5;
116
+ opacity: 0.7;
70
117
  }
71
118
 
72
119
  md-icon:hover {
@@ -74,29 +121,133 @@ let PageMDIBar = class PageMDIBar extends localize(i18next)(LitElement) {
74
121
  color: var(--md-sys-color-secondary);
75
122
  }
76
123
 
124
+ /*
125
+ * 탭 줄 오른쪽의 단추들 — 모두 닫기 · 좌우 이동.
126
+ *
127
+ * 전에는 글리프가 24px 인데 상자가 18px 이라 **넘쳤고**, 색이 rgb(32,39,47) 거의 검정이라 탭 줄
128
+ * 에서 가장 진했다. 탭 이름보다 장치가 눈에 먼저 들어왔다. [확인함] 2026-09-02
129
+ *
130
+ * 상자와 글리프를 같은 값으로 맞추고, 평소에는 흐리게 두었다가 마우스가 오면 진해진다. 배경은
131
+ * 동그라미로 든다 — 셀 안 단추와 같은 어휘다.
132
+ */
77
133
  div[arrows] md-icon {
134
+ --md-icon-size: 18px;
135
+ font-size: 18px;
136
+ width: 18px;
137
+ height: 18px;
138
+ padding: 3px;
139
+ border-radius: 50%;
140
+ color: var(--ui-muted, #57676f);
78
141
  cursor: pointer;
142
+ transition:
143
+ background-color 0.12s ease,
144
+ color 0.12s ease;
145
+ }
146
+
147
+ div[arrows] md-icon:not([disabled]):hover {
148
+ color: var(--ui-ink, #17272f);
149
+ background-color: var(--ui-hover, rgba(0, 0, 0, 0.06));
79
150
  }
80
151
 
152
+ /*
153
+ * 「모두 닫기」는 채운 글리프로 그려진다. 외곽선으로 바꾼다 — 옆의 화살표가 외곽선이라 셋이 한
154
+ * 무리로 읽혀야 한다.
155
+ */
81
156
  div[arrows] md-icon[close-all] {
82
157
  margin: 0 2px;
158
+ font-variation-settings: 'FILL' 0;
83
159
  }
84
160
 
85
161
  div[arrows] md-icon[disabled] {
86
- opacity: 0.2;
87
- color: unset;
162
+ opacity: 0.35;
163
+ color: var(--ui-faint, #8a959c);
88
164
  cursor: default;
89
165
  }
90
166
 
91
- div[route] md-icon {
167
+ /*
168
+ * 탭을 닫는 × — **옆의 화살표들과 같은 어휘로** 그린다.
169
+ *
170
+ * 확인해 보니 한 26px 띠 안에서 같은 종류가 어휘가 둘이었다.
171
+ *
172
+ * 탭의 × 18×18 · padding 0 · radius 0 · #20272f 를 50% 흐림 · hover 는 흐림만
173
+ * 바의 화살표 24×24 · padding 3 · radius 50% · --ui-muted · hover 에 배경 동그라미
174
+ *
175
+ * 화살표 쪽은 이틀 전에 정리했고(위 블록) 이쪽은 받지 못했다. 그래서 셋이 걸렸다.
176
+ *
177
+ * ① 누를 자리가 18px 다. 옆의 것이 24px 이고, 탭을 닫는 것은 되돌리기 어려운 동작이다
178
+ * ② hover 가 흐림만 바꾼다 — 배경이 없으면 단추로 읽히지 않는다
179
+ * ③ 색이 자기 색이 아니다. 잉크(#20272f)를 흐린 것이라 탭 이름과 같은 무게로 읽혔고,
180
+ * 탭이 셋이면 화면에서 「× × ×」가 이름보다 먼저 한 줄로 읽혔다
181
+ *
182
+ * 글리프를 16px 로 한 단 줄여 padding 3px 을 넣어도 22×22 로 26px 띠에 든다. 위아래
183
+ * 2px 이 남는다 — 화살표와 같은 값(24)을 쓰면 띠에 꽉 차서 모서리를 먹는다.
184
+ *
185
+ * margin-left 를 걷었다 — absolute 배치라 아무 일도 하지 않던 선언이다.
186
+ * 홈 탭의 아이콘은 닫기가 아니므로 뺀다(그쪽은 아래 [home] 규칙이 맡는다).
187
+ */
188
+ div[route]:not([home]) md-icon {
189
+ position: absolute;
190
+ right: 3px;
191
+ top: 50%;
192
+ transform: translateY(-50%);
193
+
194
+ --md-icon-size: 16px;
195
+ font-size: 16px;
196
+ width: 16px;
197
+ height: 16px;
198
+ padding: 3px;
199
+ border-radius: 50%;
200
+
201
+ opacity: 1;
202
+ /*
203
+ * ── 색은 **이 띠의 짝 토큰**에서 온다 (2026-09-05) ────────────────
204
+ * --ui-faint 는 페이지 바탕(--md-sys-color-surface)에 맞춰 섞인 값이다. 탭 띠의 바탕은
205
+ * 그것이 아니라 secondary-container 라서, 여기에 그대로 쓰면 바탕에 너무 가까워진다 —
206
+ * 야간 실측 2.07:1 이었다(UI 요소 기준 3:1).
207
+ *
208
+ * 그래서 이 띠의 글자색(on-secondary-container)을 옅게 섞는다. 활성 탭(4.9) · 비활성
209
+ * 탭(5.9) 어느 쪽에 놓여도 넘는다.
210
+ */
211
+ color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 70%, transparent);
212
+ transition:
213
+ background-color 0.12s ease,
214
+ color 0.12s ease;
215
+ }
216
+
217
+ div[route]:not([home]) md-icon:hover {
218
+ opacity: 1;
219
+ color: var(--ui-ink, #17272f);
220
+ background-color: var(--ui-hover, rgba(0, 0, 0, 0.06));
221
+ }
222
+
223
+ div[route][home] md-icon {
92
224
  position: absolute;
93
225
  right: 2px;
94
- margin-left: var(--spacing-small);
95
226
  }
96
227
 
228
+ /*
229
+ * 비활성 탭도 눌리는 것이다. 마우스가 오면 면으로 말한다 — 옆의 화살표들과 같은 어휘다.
230
+ * 채움 자리(rest)와 이미 열린 탭은 뺀다.
231
+ */
232
+ div[route]:not([active]):not([rest]):hover {
233
+ background-color: var(--ui-hover, rgba(0, 0, 0, 0.05));
234
+ }
235
+
236
+ /*
237
+ * 활성 탭은 **아래 판과 한 장**이다.
238
+ *
239
+ * 채움색이 --md-sys-color-background 인 것은 그 뜻이었다 — 본문 바닥과 같은 색이라
240
+ * 이어져 보이게. [확인함] 둘 다 #f0f2f4 로 실제 같다.
241
+ *
242
+ * 거기에 두 가지를 더한다.
243
+ *
244
+ * 위·좌·우 실선 한 올 떠오른 종이의 윤곽. 밝기 5% 차이만으로는 경계가 안 읽혔다
245
+ * 밑변은 판의 색 선을 잇지 않는다 — 그 1px 이 탭과 판을 잇는 자리다
246
+ */
97
247
  div[route][active] {
98
248
  background-color: var(--md-sys-color-background);
99
- border-bottom: 1px solid var(--md-sys-color-background);
249
+ border: 1px solid var(--md-sys-color-outline-variant);
250
+ border-bottom-color: var(--md-sys-color-background);
100
251
  color: var(--md-sys-color-on-background);
101
252
  font-weight: bold;
102
253
  }
@@ -197,8 +348,32 @@ let PageMDIBar = class PageMDIBar extends localize(i18next)(LitElement) {
197
348
  </div>
198
349
  `;
199
350
  }
351
+ /**
352
+ * 열린 탭 목록을 주소와 맞춘다.
353
+ *
354
+ * ── 남의 주소가 찍히던 결함 (2026-09-02) ──────────────────────────────────
355
+ * 항해 한 번에 이 함수가 **두 번** 지나간다. 그 사이에 주소와 활성 페이지가 어긋나 있는 순간이 있다.
356
+ *
357
+ * ① navigate 주소가 새 페이지로 바뀐다. 활성 페이지는 **아직 옛 페이지**다
358
+ * ② 나중에 새 페이지가 붙고 활성 페이지가 바뀐다
359
+ *
360
+ * ①에서도 이 블록이 돌았다. `params` 는 항해마다 새 객체라(`shell/actions/route.js` 의
361
+ * `if (!params) { params = {} }`) 아래 `params !== this._prevParams` 가 **항상 참**이기 때문이다.
362
+ * 그래서 옛 탭을 찾아 새 주소를 찍었다.
363
+ *
364
+ * [확인함] 작업지시를 열고 메뉴에서 품질을 누른 뒤
365
+ *
366
+ * i=1 job-order-list → /quality-list ← 덮였다
367
+ * i=2 quality-list → /quality-list
368
+ *
369
+ * 그 결과 **「작업지시」 탭을 눌러도 아무 일이 없었다** — 품질로 가는 주소라 그대로 머문다. 탭이
370
+ * 죽어 보이는 것이 이것이다.
371
+ *
372
+ * 그래서 탭마다 자기 라우트 이름을 들리고, **주소가 자기 것일 때만** 갱신한다. `params` 비교는 그대로
373
+ * 둔다 — 검색어처럼 질의만 바뀌는 항해도 탭 주소가 따라가야 한다.
374
+ */
200
375
  updated() {
201
- const { activePage, resourceId, params, context } = this._routeCtrl;
376
+ const { activePage, resourceId, params, context, page: routeName } = this._routeCtrl;
202
377
  /* refresh를 일으키기위한 property 변경을 위한 코드임. */
203
378
  this.title = context?.title || context?.search?.placeholder || '';
204
379
  const activePageChanged = activePage !== this._prevActivePage;
@@ -217,9 +392,18 @@ let PageMDIBar = class PageMDIBar extends localize(i18next)(LitElement) {
217
392
  }
218
393
  var route = this.routes.find(route => route.page === page);
219
394
  if (!route) {
220
- this.routes = [...this.routes, { page, url: location.href }];
395
+ this.routes = [...this.routes, { page, url: location.href, name: routeName }];
396
+ }
397
+ else if (route.name === routeName) {
398
+ /* 자기 주소다 — 같은 화면 안에서 resourceId 나 질의가 바뀐 경우가 여기 온다. */
399
+ route.url = location.href;
221
400
  }
222
- else {
401
+ else if (activePageChanged) {
402
+ /*
403
+ * 같은 요소가 다른 라우트를 맡게 된 경우. 드물지만 그냥 두면 그 탭의 주소가 영구히 낡는다.
404
+ * `activePageChanged` 로 가른다 — 위 ①의 어긋난 순간에는 이 값이 거짓이다.
405
+ */
406
+ route.name = routeName;
223
407
  route.url = location.href;
224
408
  }
225
409
  }
@@ -240,7 +424,6 @@ let PageMDIBar = class PageMDIBar extends localize(i18next)(LitElement) {
240
424
  return base?.getAttribute('href') || '/';
241
425
  }
242
426
  moveTo(route) {
243
- console.log('route', route);
244
427
  navigate(route.url);
245
428
  }
246
429
  moveToHome() {
@@ -1 +1 @@
1
- {"version":3,"file":"page-mdibar.js","sourceRoot":"","sources":["../../client/layout/page-mdibar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/popup/ox-popup-menu.js'\n\nimport { css, html, nothing, LitElement } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport { navigate, PageView, RouteController } from '@operato/shell'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPopupMenu } from '@operato/popup/ox-popup-menu.js'\nimport { OxPrompt } from '@operato/popup'\n\n@customElement('page-mdibar')\nexport class PageMDIBar extends localize(i18next)(LitElement) {\n static styles = [\n css`\n :host {\n display: flex;\n background-color: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n font-size: var(--fontsize-small);\n overflow: hidden;\n align-items: stretch;\n }\n\n div {\n box-sizing: border-box;\n }\n\n div[routes] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n }\n\n div[route] {\n display: inline-block;\n position: relative;\n min-width: var(--mdibar-min-width, 100px);\n max-width: var(--mdibar-max-width, 150px);\n padding: var(--mdibar-padding, 3px 22px 1px 7px);\n border-right: var(--border-dim-color);\n border-top: var(--border-dim-color);\n border-bottom: var(--border-dim-color);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n text-transform: capitalize;\n cursor: pointer;\n }\n\n div[arrows] {\n display: flex;\n border: var(--border-dim-color);\n align-items: center;\n }\n\n div[arrows] * {\n opacity: 0.8;\n font-size: var(--mdibar-icon-size, 24px);\n flex: 1;\n }\n\n md-icon {\n --md-icon-size: var(--mdibar-icon-size, 18px);\n opacity: 0.5;\n }\n\n md-icon:hover {\n opacity: 0.8;\n color: var(--md-sys-color-secondary);\n }\n\n div[arrows] md-icon {\n cursor: pointer;\n }\n\n div[arrows] md-icon[close-all] {\n margin: 0 2px;\n }\n\n div[arrows] md-icon[disabled] {\n opacity: 0.2;\n color: unset;\n cursor: default;\n }\n\n div[route] md-icon {\n position: absolute;\n right: 2px;\n margin-left: var(--spacing-small);\n }\n\n div[route][active] {\n background-color: var(--md-sys-color-background);\n border-bottom: 1px solid var(--md-sys-color-background);\n color: var(--md-sys-color-on-background);\n font-weight: bold;\n }\n\n div[route][home] {\n padding: var(--mdibar-padding-home, 3px 5px 0 23px);\n overflow: visible;\n min-width: unset;\n max-width: unset;\n }\n\n div[route][home] md-icon {\n left: 0;\n }\n\n div[route][rest] {\n flex: 1;\n min-width: unset;\n max-width: unset;\n border-right: unset;\n }\n `\n ]\n\n @state() routes: { page?: PageView; url: string }[] = [{ url: this.getHomeRoute() }]\n @state() title: string = ''\n\n private _routeCtrl = new RouteController(this)\n\n @query('[routes]') scroller!: HTMLDivElement\n @query('#context-menu') contextMenu!: HTMLDivElement\n\n private contextMenuTargetIndex?: number\n private contextMenuTargetRoute?: { page?: PageView; url: string }\n\n render() {\n const home = this.routes[0]\n const routes = this.routes.slice(1)\n\n return html`\n <div\n home\n route\n ?active=${this._routeCtrl.activePage === home.page}\n @click=${() => this.moveToHome()}\n @contextmenu=${(e: MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n delete this.contextMenuTargetRoute\n delete this.contextMenuTargetIndex\n\n this.openContextMenu(e)\n }}\n >\n <md-icon>home</md-icon>&#8203;\n </div>\n\n <div\n routes\n @mousewheel=${this.onWheelEvent.bind(this)}\n @contextmenu=${(e: MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n delete this.contextMenuTargetRoute\n delete this.contextMenuTargetIndex\n\n this.openContextMenu(e)\n }}\n >\n ${routes.map((route, idx) => {\n const context = route.page?.context as any\n const title = context?.title || context?.search?.placeholder || ''\n\n return html`\n <div\n @contextmenu=${(e: MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n this.contextMenuTargetRoute = route\n this.contextMenuTargetIndex = idx\n\n this.openContextMenu(e)\n }}\n @click=${() => this.moveTo(route)}\n ?active=${this._routeCtrl.activePage === route.page}\n route\n >\n ${typeof title == 'object' ? title.text : title}\n <md-icon\n @click=${(e: MouseEvent) => {\n e.stopPropagation()\n this.closePage(route, idx + 1)\n }}\n >close</md-icon\n >\n </div>\n `\n })}\n <div rest route></div>\n </div>\n\n <div arrows>\n <md-icon close-all ?disabled=${this.routes.length <= 1} @click=${() => this.closeOtherPagesFromActive()}\n >tab_close</md-icon\n >\n <md-icon ?disabled=${!this.scroller || this.scroller.scrollLeft <= 0} @click=${() => this.onScrollByArrow(1)}\n >arrow_left</md-icon\n >\n <md-icon\n ?disabled=${!this.scroller ||\n this.scroller.scrollWidth <= this.scroller.offsetWidth + this.scroller.scrollLeft}\n @click=${() => this.onScrollByArrow(-1)}\n >arrow_right</md-icon\n >\n </div>\n `\n }\n\n private _prevActivePage?: any\n private _prevResourceId?: string\n private _prevParams?: any\n\n updated() {\n const { activePage, resourceId, params, context } = this._routeCtrl\n\n /* refresh를 일으키기위한 property 변경을 위한 코드임. */\n this.title = context?.title || context?.search?.placeholder || ''\n\n const activePageChanged = activePage !== this._prevActivePage\n const routeDetailsChanged = resourceId !== this._prevResourceId || params !== this._prevParams\n\n this._prevActivePage = activePage\n this._prevResourceId = resourceId\n this._prevParams = params\n\n if (activePageChanged || routeDetailsChanged) {\n if (!activePage) {\n return\n }\n\n const page = activePage\n\n if (location.pathname == this.getHomeRoute()) {\n this.routes[0].page = page\n return\n }\n\n var route = this.routes.find(route => route.page === page)\n if (!route) {\n this.routes = [...this.routes, { page, url: location.href }]\n } else {\n route.url = location.href\n }\n }\n }\n\n onScrollByArrow(delta: number) {\n this.scroller.scrollLeft -= delta * 10\n\n this.requestUpdate()\n }\n\n onWheelEvent(e: Event) {\n const target = e.currentTarget as HTMLDivElement\n\n var delta = Math.max(-1, Math.min(1, (e as WheelEvent).deltaY || -(e as WheelEvent).detail))\n target.scrollLeft -= delta * 10\n\n this.requestUpdate()\n\n e.preventDefault()\n }\n\n getHomeRoute() {\n var base = document.querySelector('base')\n return base?.getAttribute('href') || '/'\n }\n\n moveTo(route: { page?: PageView; url: string }) {\n console.log('route', route)\n navigate(route.url)\n }\n\n moveToHome() {\n navigate(this.getHomeRoute())\n }\n\n closePage(route: { page?: PageView; url: string }, idx: number) {\n this.routes = [...this.routes.slice(0, idx), ...this.routes.slice(idx + 1)]\n const page = route.page!\n\n page.parentNode?.removeChild(page)\n page.pageDispose()\n\n if (this._routeCtrl.activePage === page) {\n const next = this.routes.length > 0 ? Math.max(0, idx - 1) : -1\n navigate(next != -1 ? this.routes[next].url : this.getHomeRoute())\n }\n }\n\n /* 현재 활성 탭을 제외한 모든 탭을 닫는 함수 */\n async closeOtherPagesFromActive() {\n if (this.routes.length <= 1) return\n\n const result = await OxPrompt.open({\n type: 'info',\n title: '',\n text: i18next.t('text.confirm_close_all_tabs'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n if (!result) return\n\n const activeRoute = this.routes.find(route => route.page === this._routeCtrl.activePage)\n\n this.routes.slice(1).forEach(route => {\n if (route === activeRoute) return\n\n const page = route.page!\n page.parentNode?.removeChild(page)\n page.pageDispose()\n })\n\n this.routes = activeRoute && activeRoute !== this.routes[0] ? [this.routes[0], activeRoute] : [this.routes[0]]\n\n navigate(activeRoute?.url || this.routes[0].url)\n }\n\n async closeAllPage() {\n const result = await OxPrompt.open({\n type: 'info',\n title: '',\n text: i18next.t('text.confirm_close_all_tabs'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n if (!result) return\n\n this.routes.slice(1).forEach((route, idx) => {\n const page = route.page!\n\n page.parentNode?.removeChild(page)\n page.pageDispose()\n })\n\n this.routes = [this.routes[0]]\n\n navigate(this.routes[0].url)\n }\n\n closeOtherPages(route: { page?: PageView; url: string }, idx: number) {\n this.routes.slice(1).forEach((route, idx) => {\n if (route === route) {\n return\n }\n\n const page = route.page!\n\n page.parentNode?.removeChild(page)\n page.pageDispose()\n })\n\n this.routes = [this.routes[0], route]\n\n navigate(route.url)\n }\n\n openContextMenu(e: MouseEvent) {\n OxPopupMenu.open({\n template: html`\n ${this.contextMenuTargetRoute\n ? html`\n <ox-popup-menuitem\n label=\"close\"\n @select=${(e: Event) => {\n this.closePage(this.contextMenuTargetRoute!, this.contextMenuTargetIndex!)\n }}\n >\n <md-icon slot=\"icon\">close</md-icon>\n </ox-popup-menuitem>\n\n <ox-popup-menuitem\n label=\"close others\"\n @select=${(e: Event) => {\n this.closeOtherPages(this.contextMenuTargetRoute!, this.contextMenuTargetIndex!)\n }}\n >\n <md-icon slot=\"icon\">cancel</md-icon>\n </ox-popup-menuitem>\n `\n : nothing}\n\n <ox-popup-menuitem\n label=\"close all\"\n @select=${(e: Event) => {\n this.closeAllPage()\n }}\n >\n <md-icon slot=\"icon\">disabled_by_default</md-icon>\n </ox-popup-menuitem>\n `,\n top: e.clientY,\n left: e.clientX,\n parent: document.body\n })\n }\n}\n"]}
1
+ {"version":3,"file":"page-mdibar.js","sourceRoot":"","sources":["../../client/layout/page-mdibar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/popup/ox-popup-menu.js'\n\nimport { css, html, nothing, LitElement } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport { navigate, PageView, RouteController } from '@operato/shell'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPopupMenu } from '@operato/popup/ox-popup-menu.js'\nimport { OxPrompt } from '@operato/popup'\n\n@customElement('page-mdibar')\nexport class PageMDIBar extends localize(i18next)(LitElement) {\n static styles = [\n css`\n :host {\n display: flex;\n background-color: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n font-size: var(--fontsize-small);\n overflow: hidden;\n align-items: stretch;\n }\n\n div {\n box-sizing: border-box;\n }\n\n div[routes] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n }\n\n div[route] {\n display: inline-block;\n position: relative;\n min-width: var(--mdibar-min-width, 100px);\n max-width: var(--mdibar-max-width, 150px);\n padding: var(--mdibar-padding, 3px 22px 1px 7px);\n /*\n * ── 탭은 위 두 모서리만 둥글다 ──────────────────────────────────────\n *\n * 탭의 은유는 **앞으로 나온 종이**다. 그래서 위는 둥글고 **아래는 각져서** 아래 판에\n * 붙는다. 밑변이 각진 것이 「이 탭이 저 판의 손잡이」라고 말하는 자리다.\n *\n * 모서리 값은 담는 면을 따른다(동심 모서리) — 이 띠는 평평한 면이므로 작은 눈금 6px 이다.\n * 팝업 카드(16px) 안의 컨트롤이 알약인 것과 같은 원칙이고, 반대쪽 끝이다.\n *\n * ── 전에 여기 있던 것 ───────────────────────────────────────────────\n * border-right / border-top / border-bottom: var(--border-dim-color)\n *\n * 탭마다 세 면에 선이 있었다. **걷은 것은 그 선이 죽어서가 아니라, 이 모양에서 선이\n * 너무 많아서다.** 26px 띠에 탭 셋이면 세로선이 셋이고, 여기에 활성 탭의 윤곽 한 올과\n * 아래 판의 경계까지 겹친다. 위가 둥근 활성 탭이 한 장으로 떠오르면 비활성 자리는\n * 물러나 있는 것으로 충분하다.\n *\n * ── 한때 「죽은 선언」이라고 적었다. 틀렸다 ────────────────────────────\n * --border-dim-color 를 못 찾아 무효 선언이라고 단정했는데, 그 토큰은 **테두리 값 한\n * 벌을 통째로** 담고 있다(이름이 -color 로 끝나지만 색이 아니다).\n *\n * shell/client/themes/app-theme.css body { --border-dim-color: 1px solid var(--md-sys-color-outline-variant) }\n *\n * 못 찾은 이유는 :root 에서 읽었기 때문이다 — 선택자가 body 다. 그래서 단축 속성에\n * 쓰이던 이 넷은 **살아서 칠하고 있었다.** 색 자리(border-*-color)에 쓰면 그때 죽는다.\n *\n * [확인함] 2026-09-04 · body 값 「1px solid rgb(214 219 224)」 ·\n * border-bottom: var(--border-dim-color) 가 1px solid #d6dbe0 로 계산된다 ·\n * 바 #e4e8ed · 활성 탭 #f0f2f4 · 본문 바닥 #f0f2f4\n */\n border-radius: var(--mdibar-tab-radius, 6px) var(--mdibar-tab-radius, 6px) 0 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n text-transform: capitalize;\n cursor: pointer;\n }\n\n div[arrows] {\n display: flex;\n /*\n * border: var(--border-dim-color) 였다 — 화살표 세 개를 상자로 둘러쌌다. 걷었다.\n * 그 셋은 이미 각자 동그라미를 받으므로 무리를 상자로 또 묶을 이유가 없다.\n * (그 선언은 살아 있었다. 위 div[route] 의 주석에 그 토큰 이야기를 적어 두었다.)\n */\n align-items: center;\n }\n\n /*\n * 흐림을 여기서 걷었다. 아래에서 이 셋에 **색을 직접 주고**(평소 --ui-muted · 못 누를 때\n * --ui-faint) 있어서, 흐림이 그 위에 또 얹히면 두 번 흐려진다. 「모두 닫기」가 야간 화면에서\n * 1.78:1 이었다(실측 2026-09-05 · UI 요소 기준 3:1).\n */\n div[arrows] * {\n font-size: var(--mdibar-icon-size, 24px);\n flex: 1;\n }\n\n /*\n * 흐림은 0.5 였다. 색까지 함께 흐린 자리가 있어 **두 번 흐려졌고**, 홈 아이콘이 밝은 화면에서\n * 2.93:1 이 됐다(실측 2026-09-05). 0.7 이면 4.3 으로 UI 요소 기준을 넘고, 탭 이름보다\n * 뒤에 서는 것은 그대로다.\n */\n md-icon {\n --md-icon-size: var(--mdibar-icon-size, 18px);\n opacity: 0.7;\n }\n\n md-icon:hover {\n opacity: 0.8;\n color: var(--md-sys-color-secondary);\n }\n\n /*\n * 탭 줄 오른쪽의 단추들 — 모두 닫기 · 좌우 이동.\n *\n * 전에는 글리프가 24px 인데 상자가 18px 이라 **넘쳤고**, 색이 rgb(32,39,47) 거의 검정이라 탭 줄\n * 에서 가장 진했다. 탭 이름보다 장치가 눈에 먼저 들어왔다. [확인함] 2026-09-02\n *\n * 상자와 글리프를 같은 값으로 맞추고, 평소에는 흐리게 두었다가 마우스가 오면 진해진다. 배경은\n * 동그라미로 든다 — 셀 안 단추와 같은 어휘다.\n */\n div[arrows] md-icon {\n --md-icon-size: 18px;\n font-size: 18px;\n width: 18px;\n height: 18px;\n padding: 3px;\n border-radius: 50%;\n color: var(--ui-muted, #57676f);\n cursor: pointer;\n transition:\n background-color 0.12s ease,\n color 0.12s ease;\n }\n\n div[arrows] md-icon:not([disabled]):hover {\n color: var(--ui-ink, #17272f);\n background-color: var(--ui-hover, rgba(0, 0, 0, 0.06));\n }\n\n /*\n * 「모두 닫기」는 채운 글리프로 그려진다. 외곽선으로 바꾼다 — 옆의 화살표가 외곽선이라 셋이 한\n * 무리로 읽혀야 한다.\n */\n div[arrows] md-icon[close-all] {\n margin: 0 2px;\n font-variation-settings: 'FILL' 0;\n }\n\n div[arrows] md-icon[disabled] {\n opacity: 0.35;\n color: var(--ui-faint, #8a959c);\n cursor: default;\n }\n\n /*\n * 탭을 닫는 × — **옆의 화살표들과 같은 어휘로** 그린다.\n *\n * 확인해 보니 한 26px 띠 안에서 같은 종류가 어휘가 둘이었다.\n *\n * 탭의 × 18×18 · padding 0 · radius 0 · #20272f 를 50% 흐림 · hover 는 흐림만\n * 바의 화살표 24×24 · padding 3 · radius 50% · --ui-muted · hover 에 배경 동그라미\n *\n * 화살표 쪽은 이틀 전에 정리했고(위 블록) 이쪽은 받지 못했다. 그래서 셋이 걸렸다.\n *\n * ① 누를 자리가 18px 다. 옆의 것이 24px 이고, 탭을 닫는 것은 되돌리기 어려운 동작이다\n * ② hover 가 흐림만 바꾼다 — 배경이 없으면 단추로 읽히지 않는다\n * ③ 색이 자기 색이 아니다. 잉크(#20272f)를 흐린 것이라 탭 이름과 같은 무게로 읽혔고,\n * 탭이 셋이면 화면에서 「× × ×」가 이름보다 먼저 한 줄로 읽혔다\n *\n * 글리프를 16px 로 한 단 줄여 padding 3px 을 넣어도 22×22 로 26px 띠에 든다. 위아래\n * 2px 이 남는다 — 화살표와 같은 값(24)을 쓰면 띠에 꽉 차서 모서리를 먹는다.\n *\n * margin-left 를 걷었다 — absolute 배치라 아무 일도 하지 않던 선언이다.\n * 홈 탭의 아이콘은 닫기가 아니므로 뺀다(그쪽은 아래 [home] 규칙이 맡는다).\n */\n div[route]:not([home]) md-icon {\n position: absolute;\n right: 3px;\n top: 50%;\n transform: translateY(-50%);\n\n --md-icon-size: 16px;\n font-size: 16px;\n width: 16px;\n height: 16px;\n padding: 3px;\n border-radius: 50%;\n\n opacity: 1;\n /*\n * ── 색은 **이 띠의 짝 토큰**에서 온다 (2026-09-05) ────────────────\n * --ui-faint 는 페이지 바탕(--md-sys-color-surface)에 맞춰 섞인 값이다. 탭 띠의 바탕은\n * 그것이 아니라 secondary-container 라서, 여기에 그대로 쓰면 바탕에 너무 가까워진다 —\n * 야간 실측 2.07:1 이었다(UI 요소 기준 3:1).\n *\n * 그래서 이 띠의 글자색(on-secondary-container)을 옅게 섞는다. 활성 탭(4.9) · 비활성\n * 탭(5.9) 어느 쪽에 놓여도 넘는다.\n */\n color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 70%, transparent);\n transition:\n background-color 0.12s ease,\n color 0.12s ease;\n }\n\n div[route]:not([home]) md-icon:hover {\n opacity: 1;\n color: var(--ui-ink, #17272f);\n background-color: var(--ui-hover, rgba(0, 0, 0, 0.06));\n }\n\n div[route][home] md-icon {\n position: absolute;\n right: 2px;\n }\n\n /*\n * 비활성 탭도 눌리는 것이다. 마우스가 오면 면으로 말한다 — 옆의 화살표들과 같은 어휘다.\n * 채움 자리(rest)와 이미 열린 탭은 뺀다.\n */\n div[route]:not([active]):not([rest]):hover {\n background-color: var(--ui-hover, rgba(0, 0, 0, 0.05));\n }\n\n /*\n * 활성 탭은 **아래 판과 한 장**이다.\n *\n * 채움색이 --md-sys-color-background 인 것은 그 뜻이었다 — 본문 바닥과 같은 색이라\n * 이어져 보이게. [확인함] 둘 다 #f0f2f4 로 실제 같다.\n *\n * 거기에 두 가지를 더한다.\n *\n * 위·좌·우 실선 한 올 떠오른 종이의 윤곽. 밝기 5% 차이만으로는 경계가 안 읽혔다\n * 밑변은 판의 색 선을 잇지 않는다 — 그 1px 이 탭과 판을 잇는 자리다\n */\n div[route][active] {\n background-color: var(--md-sys-color-background);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-bottom-color: var(--md-sys-color-background);\n color: var(--md-sys-color-on-background);\n font-weight: bold;\n }\n\n div[route][home] {\n padding: var(--mdibar-padding-home, 3px 5px 0 23px);\n overflow: visible;\n min-width: unset;\n max-width: unset;\n }\n\n div[route][home] md-icon {\n left: 0;\n }\n\n div[route][rest] {\n flex: 1;\n min-width: unset;\n max-width: unset;\n border-right: unset;\n }\n `\n ]\n\n /**\n * 열린 탭들. `routes[0]` 은 홈이다(render 에서 따로 그린다).\n *\n * `name` 은 그 탭이 맡은 **라우트 이름**이다(`job-order-list` 같은 것). 주소를 갱신할 때 「이 주소가\n * 정말 이 탭의 것인가」를 판정하는 데 쓴다 — 없으면 남의 주소가 찍힌다(updated 머리말 참조).\n */\n @state() routes: { page?: PageView; url: string; name?: string }[] = [{ url: this.getHomeRoute() }]\n @state() title: string = ''\n\n private _routeCtrl = new RouteController(this)\n\n @query('[routes]') scroller!: HTMLDivElement\n @query('#context-menu') contextMenu!: HTMLDivElement\n\n private contextMenuTargetIndex?: number\n private contextMenuTargetRoute?: { page?: PageView; url: string; name?: string }\n\n render() {\n const home = this.routes[0]\n const routes = this.routes.slice(1)\n\n return html`\n <div\n home\n route\n ?active=${this._routeCtrl.activePage === home.page}\n @click=${() => this.moveToHome()}\n @contextmenu=${(e: MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n delete this.contextMenuTargetRoute\n delete this.contextMenuTargetIndex\n\n this.openContextMenu(e)\n }}\n >\n <md-icon>home</md-icon>&#8203;\n </div>\n\n <div\n routes\n @mousewheel=${this.onWheelEvent.bind(this)}\n @contextmenu=${(e: MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n delete this.contextMenuTargetRoute\n delete this.contextMenuTargetIndex\n\n this.openContextMenu(e)\n }}\n >\n ${routes.map((route, idx) => {\n const context = route.page?.context as any\n const title = context?.title || context?.search?.placeholder || ''\n\n return html`\n <div\n @contextmenu=${(e: MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n this.contextMenuTargetRoute = route\n this.contextMenuTargetIndex = idx\n\n this.openContextMenu(e)\n }}\n @click=${() => this.moveTo(route)}\n ?active=${this._routeCtrl.activePage === route.page}\n route\n >\n ${typeof title == 'object' ? title.text : title}\n <md-icon\n @click=${(e: MouseEvent) => {\n e.stopPropagation()\n this.closePage(route, idx + 1)\n }}\n >close</md-icon\n >\n </div>\n `\n })}\n <div rest route></div>\n </div>\n\n <div arrows>\n <md-icon close-all ?disabled=${this.routes.length <= 1} @click=${() => this.closeOtherPagesFromActive()}\n >tab_close</md-icon\n >\n <md-icon ?disabled=${!this.scroller || this.scroller.scrollLeft <= 0} @click=${() => this.onScrollByArrow(1)}\n >arrow_left</md-icon\n >\n <md-icon\n ?disabled=${!this.scroller ||\n this.scroller.scrollWidth <= this.scroller.offsetWidth + this.scroller.scrollLeft}\n @click=${() => this.onScrollByArrow(-1)}\n >arrow_right</md-icon\n >\n </div>\n `\n }\n\n private _prevActivePage?: any\n private _prevResourceId?: string\n private _prevParams?: any\n\n /**\n * 열린 탭 목록을 주소와 맞춘다.\n *\n * ── 남의 주소가 찍히던 결함 (2026-09-02) ──────────────────────────────────\n * 항해 한 번에 이 함수가 **두 번** 지나간다. 그 사이에 주소와 활성 페이지가 어긋나 있는 순간이 있다.\n *\n * ① navigate 주소가 새 페이지로 바뀐다. 활성 페이지는 **아직 옛 페이지**다\n * ② 나중에 새 페이지가 붙고 활성 페이지가 바뀐다\n *\n * ①에서도 이 블록이 돌았다. `params` 는 항해마다 새 객체라(`shell/actions/route.js` 의\n * `if (!params) { params = {} }`) 아래 `params !== this._prevParams` 가 **항상 참**이기 때문이다.\n * 그래서 옛 탭을 찾아 새 주소를 찍었다.\n *\n * [확인함] 작업지시를 열고 메뉴에서 품질을 누른 뒤\n *\n * i=1 job-order-list → /quality-list ← 덮였다\n * i=2 quality-list → /quality-list\n *\n * 그 결과 **「작업지시」 탭을 눌러도 아무 일이 없었다** — 품질로 가는 주소라 그대로 머문다. 탭이\n * 죽어 보이는 것이 이것이다.\n *\n * 그래서 탭마다 자기 라우트 이름을 들리고, **주소가 자기 것일 때만** 갱신한다. `params` 비교는 그대로\n * 둔다 — 검색어처럼 질의만 바뀌는 항해도 탭 주소가 따라가야 한다.\n */\n updated() {\n const { activePage, resourceId, params, context, page: routeName } = this._routeCtrl\n\n /* refresh를 일으키기위한 property 변경을 위한 코드임. */\n this.title = context?.title || context?.search?.placeholder || ''\n\n const activePageChanged = activePage !== this._prevActivePage\n const routeDetailsChanged = resourceId !== this._prevResourceId || params !== this._prevParams\n\n this._prevActivePage = activePage\n this._prevResourceId = resourceId\n this._prevParams = params\n\n if (activePageChanged || routeDetailsChanged) {\n if (!activePage) {\n return\n }\n\n const page = activePage\n\n if (location.pathname == this.getHomeRoute()) {\n this.routes[0].page = page\n return\n }\n\n var route = this.routes.find(route => route.page === page)\n\n if (!route) {\n this.routes = [...this.routes, { page, url: location.href, name: routeName }]\n } else if (route.name === routeName) {\n /* 자기 주소다 — 같은 화면 안에서 resourceId 나 질의가 바뀐 경우가 여기 온다. */\n route.url = location.href\n } else if (activePageChanged) {\n /*\n * 같은 요소가 다른 라우트를 맡게 된 경우. 드물지만 그냥 두면 그 탭의 주소가 영구히 낡는다.\n * `activePageChanged` 로 가른다 — 위 ①의 어긋난 순간에는 이 값이 거짓이다.\n */\n route.name = routeName\n route.url = location.href\n }\n }\n }\n\n onScrollByArrow(delta: number) {\n this.scroller.scrollLeft -= delta * 10\n\n this.requestUpdate()\n }\n\n onWheelEvent(e: Event) {\n const target = e.currentTarget as HTMLDivElement\n\n var delta = Math.max(-1, Math.min(1, (e as WheelEvent).deltaY || -(e as WheelEvent).detail))\n target.scrollLeft -= delta * 10\n\n this.requestUpdate()\n\n e.preventDefault()\n }\n\n getHomeRoute() {\n var base = document.querySelector('base')\n return base?.getAttribute('href') || '/'\n }\n\n moveTo(route: { page?: PageView; url: string; name?: string }) {\n navigate(route.url)\n }\n\n moveToHome() {\n navigate(this.getHomeRoute())\n }\n\n closePage(route: { page?: PageView; url: string; name?: string }, idx: number) {\n this.routes = [...this.routes.slice(0, idx), ...this.routes.slice(idx + 1)]\n const page = route.page!\n\n page.parentNode?.removeChild(page)\n page.pageDispose()\n\n if (this._routeCtrl.activePage === page) {\n const next = this.routes.length > 0 ? Math.max(0, idx - 1) : -1\n navigate(next != -1 ? this.routes[next].url : this.getHomeRoute())\n }\n }\n\n /* 현재 활성 탭을 제외한 모든 탭을 닫는 함수 */\n async closeOtherPagesFromActive() {\n if (this.routes.length <= 1) return\n\n const result = await OxPrompt.open({\n type: 'info',\n title: '',\n text: i18next.t('text.confirm_close_all_tabs'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n if (!result) return\n\n const activeRoute = this.routes.find(route => route.page === this._routeCtrl.activePage)\n\n this.routes.slice(1).forEach(route => {\n if (route === activeRoute) return\n\n const page = route.page!\n page.parentNode?.removeChild(page)\n page.pageDispose()\n })\n\n this.routes = activeRoute && activeRoute !== this.routes[0] ? [this.routes[0], activeRoute] : [this.routes[0]]\n\n navigate(activeRoute?.url || this.routes[0].url)\n }\n\n async closeAllPage() {\n const result = await OxPrompt.open({\n type: 'info',\n title: '',\n text: i18next.t('text.confirm_close_all_tabs'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n if (!result) return\n\n this.routes.slice(1).forEach((route, idx) => {\n const page = route.page!\n\n page.parentNode?.removeChild(page)\n page.pageDispose()\n })\n\n this.routes = [this.routes[0]]\n\n navigate(this.routes[0].url)\n }\n\n closeOtherPages(route: { page?: PageView; url: string; name?: string }, idx: number) {\n this.routes.slice(1).forEach((route, idx) => {\n if (route === route) {\n return\n }\n\n const page = route.page!\n\n page.parentNode?.removeChild(page)\n page.pageDispose()\n })\n\n this.routes = [this.routes[0], route]\n\n navigate(route.url)\n }\n\n openContextMenu(e: MouseEvent) {\n OxPopupMenu.open({\n template: html`\n ${this.contextMenuTargetRoute\n ? html`\n <ox-popup-menuitem\n label=\"close\"\n @select=${(e: Event) => {\n this.closePage(this.contextMenuTargetRoute!, this.contextMenuTargetIndex!)\n }}\n >\n <md-icon slot=\"icon\">close</md-icon>\n </ox-popup-menuitem>\n\n <ox-popup-menuitem\n label=\"close others\"\n @select=${(e: Event) => {\n this.closeOtherPages(this.contextMenuTargetRoute!, this.contextMenuTargetIndex!)\n }}\n >\n <md-icon slot=\"icon\">cancel</md-icon>\n </ox-popup-menuitem>\n `\n : nothing}\n\n <ox-popup-menuitem\n label=\"close all\"\n @select=${(e: Event) => {\n this.closeAllPage()\n }}\n >\n <md-icon slot=\"icon\">disabled_by_default</md-icon>\n </ox-popup-menuitem>\n `,\n top: e.clientY,\n left: e.clientX,\n parent: document.body\n })\n }\n}\n"]}