@operato/context 10.29.41 → 10.29.44

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.
@@ -4,6 +4,10 @@ import { LitElement } from 'lit';
4
4
  export declare class PageTitleBar extends LitElement {
5
5
  static styles: import("lit").CSSResult[];
6
6
  private _route;
7
+ private refused;
8
+ private onRefused;
9
+ connectedCallback(): void;
10
+ disconnectedCallback(): void;
7
11
  /**
8
12
  * 그린 것이 하나도 없으면 host 에 `blank` 를 붙인다 — 그러면 자리를 비운다.
9
13
  *
@@ -2,14 +2,28 @@ import { __decorate } from "tslib";
2
2
  import '@material/web/icon/icon.js';
3
3
  import '@operato/input/ox-input-search.js';
4
4
  import { css, html, LitElement } from 'lit';
5
- import { live } from 'lit/directives/live.js';
6
- import { customElement } from 'lit/decorators.js';
5
+ import { customElement, state } from 'lit/decorators.js';
6
+ import { keyed } from 'lit/directives/keyed.js';
7
7
  import { openHelp } from '@operato/help';
8
8
  import { RouteController } from '@operato/shell';
9
9
  let PageTitleBar = class PageTitleBar extends LitElement {
10
10
  constructor() {
11
11
  super(...arguments);
12
12
  this._route = new RouteController(this);
13
+ /*
14
+ * The search field keeps what the user is typing through every re-render. Only a list query that was refused
15
+ * (the user kept unsaved edits) draws it anew, so it shows the committed term again.
16
+ */
17
+ this.refused = 0;
18
+ this.onRefused = () => this.refused++;
19
+ }
20
+ connectedCallback() {
21
+ super.connectedCallback();
22
+ document.addEventListener('query-refused', this.onRefused);
23
+ }
24
+ disconnectedCallback() {
25
+ super.disconnectedCallback();
26
+ document.removeEventListener('query-refused', this.onRefused);
13
27
  }
14
28
  /**
15
29
  * 그린 것이 하나도 없으면 host 에 `blank` 를 붙인다 — 그러면 자리를 비운다.
@@ -59,14 +73,14 @@ let PageTitleBar = class PageTitleBar extends LitElement {
59
73
  ? html `
60
74
  <div search>
61
75
  ${searchable
62
- ? html ` <ox-input-search
76
+ ? keyed(this.refused, html ` <ox-input-search
63
77
  .placeholder=${placeholder}
64
- .value=${live(value || '')}
78
+ .value=${value || ''}
65
79
  ?autofocus=${autofocus}
66
80
  @change=${(e) => {
67
81
  searchHandler(e.target.value);
68
82
  }}
69
- ></ox-input-search>`
83
+ ></ox-input-search>`)
70
84
  : html ``}
71
85
  ${!title && help && searchable
72
86
  ? html `<md-icon @click=${(e) => openHelp(help)} help>help</md-icon>`
@@ -257,6 +271,9 @@ PageTitleBar.styles = [
257
271
  }
258
272
  `
259
273
  ];
274
+ __decorate([
275
+ state()
276
+ ], PageTitleBar.prototype, "refused", void 0);
260
277
  PageTitleBar = __decorate([
261
278
  customElement('ox-page-title-bar')
262
279
  ], PageTitleBar);
@@ -1 +1 @@
1
- {"version":3,"file":"ox-page-title-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-page-title-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/input/ox-input-search.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { live } from 'lit/directives/live.js'\nimport { customElement } from 'lit/decorators.js'\nimport { openHelp } from '@operato/help'\nimport { RouteController } from '@operato/shell'\n\n@customElement('ox-page-title-bar')\nexport class PageTitleBar extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n align-items: center;\n /*\n * 줄 높이를 여기서 잡는다. 전에는 제목의 위 여백을 크게 주어(padding-top: medium) 줄을\n * 벌렸는데, 그러면 **제목 글자가 자기 상자 안에서 아래로 내려앉는다.** 옆의 검색은 가운데에\n * 있으니 둘이 어긋나 보였다. [확인함] 제목 상자 41x36 여백 8px 4px 4px. 2026-09-02\n *\n * 줄의 높이는 줄이 갖고, 제목은 위아래 같은 여백을 갖는다.\n */\n min-height: 36px;\n /*\n * ── 그릴 것이 없으면 자리도 없다 ──────────────────────────────────────\n * 제목도 검색도 필터도 없는 화면에서 이 요소가 **자식 0개인 채로 36px 을 차지**했다.\n * 배우기 문서에서 눈에 걸렸다 — 제목 줄을 끄자 문서 위에 빈 띠가 남았다.\n * [확인함] 2026-09-04 · 높이 36 · 자식 0\n *\n * min-height 는 그릴 것이 있을 때의 줄 높이다. 없을 때는 아래 [blank] 가 걷는다.\n */\n /*\n * 줄의 숨 쉴 자리 — **위와 오른쪽을 같게.**\n *\n * 전에는 제목의 위 여백(padding-top: medium)이 그 일을 하고 있었다. 그것을 걷어 글자를 가운데로\n * 맞추자 줄이 탭 바에 붙고, 알약은 원래부터 오른쪽 끝에 붙어 있었다 — 위 0 · 오른쪽 0.\n * [확인함] 2026-09-02\n *\n * 왼쪽은 화면의 여백(--ui-gutter)에 맞춘다 — 제목 글자가 본문 첫 글자와 같은 선에 선다.\n * 제목 상자가 자기 여백(spacing-small)을 가지므로 그만큼 뺀다. 전에는 0 이어서 문서 화면에서\n * 제목(x214)과 본문(x229)이 15px 어긋났다. [확인함 :4000 1440 창] 2026-10-05\n */\n padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-small)\n calc(var(--ui-gutter, 16px) - var(--spacing-small, 4px));\n box-sizing: border-box;\n text-transform: capitalize;\n\n --help-icon-color: var(--md-sys-color-on-surface);\n --help-icon-hover-color: var(--md-sys-color-on-surface-variant);\n /*\n * ── 0.2 는 안 보인다 (2026-09-05) ────────────────────────────────\n * 도움말은 「있는 줄 알아야」 누른다. 20% 로 두면 야간 모드에서 대비 1.78:1 이고\n * (실측: 성숙도 화면 제목 옆), 밝은 모드도 비슷하다. 선택해 볼 생각을 못 한다.\n *\n * 0.5 면 밝은 화면 3.4 · 어두운 화면 3.9 로 UI 요소 기준(3:1)을 넘고, 제목보다는\n * 여전히 뒤에 선다.\n */\n --help-icon-opacity: 0.5;\n /* 마우스가 오면 또렷해진다. 전에는 이 이름이 선언되어 있지 않아 무효 치환 덕에\n 우연히 1 이 되고 있었다. */\n --help-icon-hover-opacity: 1;\n --help-icon-size: 20px;\n\n --md-icon-size: 20px;\n\n --input-search-padding: var(--spacing-small);\n --input-search-font: normal 14px var(--theme-font);\n }\n\n /*\n * 아무것도 그리지 않았다 — 자리를 비운다. updated() 가 그려진 것을 세어 붙인다.\n *\n * 왜 CSS 로만 못 하나: :host(:empty) 는 **바깥 DOM 의 자식**을 보므로 이 부품에서는 늘\n * 참이다(쓰는 쪽이 자식을 넣지 않는다). 그려진 것은 그림자 안에 있어서 세어야 한다.\n */\n :host([blank]) {\n display: none;\n }\n\n [titler] {\n display: flex;\n padding: var(--spacing-small);\n font: var(--context-toolbar-title);\n color: var(--context-toolbar-title-color, var(--md-sys-color-on-surface));\n }\n\n [titler] > * {\n align-self: center;\n }\n\n /*\n * One line under the title that says what the screen is for (context.description). It takes\n * the whole row, so the search pill stays on the title's line. Without the key nothing is\n * drawn and the bar is exactly as before (ADR-0060 decision 8).\n */\n :host([described]) {\n flex-wrap: wrap;\n }\n\n [description] {\n order: 10;\n flex-basis: 100%;\n padding: 0 var(--spacing-small) var(--spacing-small);\n font-size: var(--ui-t-body, 13px);\n line-height: 1.45;\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n text-transform: none;\n }\n\n [help] {\n opacity: var(--help-icon-opacity);\n font-size: var(--help-icon-size);\n color: var(--help-icon-color);\n }\n\n [help]:hover {\n cursor: help;\n opacity: var(--help-icon-hover-opacity);\n color: var(--help-icon-hover-color);\n }\n\n [search] [help] {\n color: var(--md-sys-color-on-secondary-container);\n }\n\n [search] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n\n /*\n * 알약의 높이를 정한다. 안의 입력이 정하게 두면 23px 이 되어 제목 줄보다 낮고, 알약이라\n * 그 차이가 눈에 걸린다.\n */\n min-height: 28px;\n box-sizing: border-box;\n\n align-self: center;\n /*\n * 검색은 강조 동작이 아니라 현재 목록을 좁히는 도구다. primary 의 반대색을 면으로\n * 쓰면 다크 도메인에서 밝은 알약으로 뒤집혀 제목 줄보다 먼저 읽힌다. 표준 surface\n * 단계 안에서만 분리해 다른 애플리케이션의 검색과 같은 톤을 유지한다.\n */\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-low);\n border-radius: var(--md-sys-shape-corner-full);\n margin-left: auto;\n padding: 0 var(--spacing-medium);\n }\n\n [search] > * {\n align-self: center;\n }\n\n /*\n * 알약 안의 장치는 **한 단 흐리다** — 돋보기와 필터 여닫기.\n *\n * 둘 다 값이 아니라 값을 다루는 장치다. 입력한 글자와 같은 색이면 빈 알약에서 장치만 눈에 걸린다.\n * 마우스가 오면 진해져서 누를 수 있음을 말한다.\n */\n [search] ox-input-search {\n /*\n * 검색 알약이 이미 표면과 모양을 책임진다. 입력 자체의 기본 surface-variant 배경을\n * 그대로 두면 알약 안에 또 하나의 상자가 생겨 경계가 도드라진다.\n */\n --ox-input-search-background-color: transparent;\n --ox-input-search-color: var(--md-sys-color-on-surface);\n --ox-input-search-icon-color: var(--ui-faint, #8a959c);\n }\n\n [search] > md-icon:not([help]) {\n color: var(--ui-faint, #8a959c);\n cursor: pointer;\n transition: color 0.12s ease;\n }\n\n [search] > md-icon:not([help]):hover {\n color: var(--ui-muted, #57676f);\n }\n\n /*\n * The screen's name always shows (2026-10-12). A narrow window used to drop the title for the\n * search pill; now the pill moves to the next line when the row is too short, still at the right.\n */\n :host {\n flex-wrap: wrap;\n }\n `\n ]\n\n private _route = new RouteController(this)\n\n /**\n * 그린 것이 하나도 없으면 host 에 `blank` 를 붙인다 — 그러면 자리를 비운다.\n *\n * 그리는 것은 제목 · 도움 · 검색·필터 알약 셋뿐이고 모두 요소 하나를 낸다. 무엇 때문에\n * 비었는지 따지지 않고 **그려진 것을 센다.** 조건이 늘어도 이 판단은 그대로 맞다.\n */\n updated() {\n this.toggleAttribute('blank', this.renderRoot.querySelector('div, md-icon') === null)\n this.toggleAttribute('described', this.renderRoot.querySelector('[description]') !== null)\n }\n\n render() {\n var { search, filter, title, help, heading, description } = this._route.context || ({} as any)\n var { value = '', handler: searchHandler, placeholder = '', autofocus = true } = search || ({} as any)\n const { handler: filterHandler } = filter || ({} as any)\n const { icon, text = title } = typeof title === 'object' ? title : ({} as any)\n const searchable = typeof searchHandler == 'function'\n const filterable = typeof filterHandler == 'function'\n\n /*\n * ── 화면이 자기 제목을 그리는 경우 ────────────────────────────────────────\n *\n * `context.title` 은 **화면의 이름**이고, 그 이름을 두 곳이 읽는다.\n *\n * MDI 탭 여러 화면 중 어느 것인가\n * 이 제목 줄 지금 읽는 것이 무엇인가\n *\n * 대개 둘 다 필요하다. 그런데 **화면이 이미 자기 제목을 크게 그리는 경우**가 있다 — 배우기\n * 문서가 그렇다. 문서 맨 위에 「공장의 모든 사건을 …」 같은 제목이 있고, 그 위에 이 줄이\n * 「배우기」를 또 얹고, 탭에도 「배우기」가 있다. **같은 말이 세 번이다.**\n *\n * 그때 title 을 빼면 탭 이름도 함께 빈다(page-mdibar 가 같은 값을 읽는다). 그래서 이름은\n * 그대로 두고 **이 줄만 끄는 방법**을 둔다. 이름은 하나이고, 그리는 자리마다 켜고 끈다.\n *\n * get context() { return { title: '배우기', heading: false } }\n *\n * 제목이 없으면 도움 아이콘도 걷힌다 — 아래 조건이 이미 `help && title` 이다. 문서 자체가\n * 도움인 화면에서 그 아이콘은 같은 말의 네 번째다.\n */\n if (heading === false) {\n title = ''\n }\n\n return html`\n ${title && description ? html`<div description>${description}</div>` : html``}\n ${title ? html` <div titler>${icon ? html`<md-icon>${icon}</md-icon>&nbsp;` : html``}${text}</div> ` : html``}\n ${help && title ? html` &nbsp;<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon> ` : html``}\n ${searchable || filterable\n ? html`\n <div search>\n ${searchable\n ? html` <ox-input-search\n .placeholder=${placeholder}\n .value=${live(value || '')}\n ?autofocus=${autofocus}\n @change=${(e: Event) => {\n searchHandler((e.target as any).value)\n }}\n ></ox-input-search>`\n : html``}\n ${!title && help && searchable\n ? html`<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon>`\n : html``}\n ${filterable ? html`<md-icon @click=${(e: MouseEvent) => filterHandler()}>unfold_more</md-icon>` : html``}\n </div>\n `\n : html``}\n `\n }\n}\n"]}
1
+ {"version":3,"file":"ox-page-title-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-page-title-bar.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,mCAAmC,CAAA;AAE1C,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAGzC,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,UAAU;IAArC;;QAqLG,WAAM,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,CAAA;QAE1C;;;WAGG;QACc,YAAO,GAAG,CAAC,CAAA;QACpB,cAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAA;IAiF1C,CAAC;IA/EC,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,QAAQ,CAAC,gBAAgB,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IAC5D,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,QAAQ,CAAC,mBAAmB,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IAC/D,CAAC;IAED;;;;;OAKG;IACH,OAAO;QACL,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC,CAAA;QACrF,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC,CAAA;IAC5F,CAAC;IAED,MAAM;QACJ,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,IAAK,EAAU,CAAA;QAC9F,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,GAAG,EAAE,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,MAAM,IAAK,EAAU,CAAA;QACtG,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,IAAK,EAAU,CAAA;QACxD,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,KAAK,EAAE,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,EAAU,CAAA;QAC9E,MAAM,UAAU,GAAG,OAAO,aAAa,IAAI,UAAU,CAAA;QACrD,MAAM,UAAU,GAAG,OAAO,aAAa,IAAI,UAAU,CAAA;QAErD;;;;;;;;;;;;;;;;;;;WAmBG;QACH,IAAI,OAAO,KAAK,KAAK,EAAE,CAAC;YACtB,KAAK,GAAG,EAAE,CAAA;QACZ,CAAC;QAED,OAAO,IAAI,CAAA;QACP,KAAK,IAAI,WAAW,CAAC,CAAC,CAAC,IAAI,CAAA,oBAAoB,WAAW,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE;QAC3E,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,gBAAgB,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA,YAAY,IAAI,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE,GAAG,IAAI,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE;QAC3G,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,0BAA0B,CAAC,CAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE;QAC/G,UAAU,IAAI,UAAU;YACxB,CAAC,CAAC,IAAI,CAAA;;gBAEE,UAAU;gBACV,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAA;mCACP,WAAW;6BACjB,KAAK,IAAI,EAAE;iCACP,SAAS;8BACZ,CAAC,CAAQ,EAAE,EAAE;oBACrB,aAAa,CAAE,CAAC,CAAC,MAAc,CAAC,KAAK,CAAC,CAAA;gBACxC,CAAC;sCACiB,CAAC;gBACvB,CAAC,CAAC,IAAI,CAAA,EAAE;gBACR,CAAC,KAAK,IAAI,IAAI,IAAI,UAAU;gBAC5B,CAAC,CAAC,IAAI,CAAA,mBAAmB,CAAC,CAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB;gBAChF,CAAC,CAAC,IAAI,CAAA,EAAE;gBACR,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA,mBAAmB,CAAC,CAAa,EAAE,EAAE,CAAC,aAAa,EAAE,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE;;WAE5G;YACH,CAAC,CAAC,IAAI,CAAA,EAAE;KACX,CAAA;IACH,CAAC;;AA3QM,mBAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgLF;CACF,AAlLY,CAkLZ;AAQgB;IAAhB,KAAK,EAAE;6CAAoB;AA3LjB,YAAY;IADxB,aAAa,CAAC,mBAAmB,CAAC;GACtB,YAAY,CA6QxB","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/input/ox-input-search.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\nimport { keyed } from 'lit/directives/keyed.js'\nimport { openHelp } from '@operato/help'\nimport { RouteController } from '@operato/shell'\n\n@customElement('ox-page-title-bar')\nexport class PageTitleBar extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n align-items: center;\n /*\n * 줄 높이를 여기서 잡는다. 전에는 제목의 위 여백을 크게 주어(padding-top: medium) 줄을\n * 벌렸는데, 그러면 **제목 글자가 자기 상자 안에서 아래로 내려앉는다.** 옆의 검색은 가운데에\n * 있으니 둘이 어긋나 보였다. [확인함] 제목 상자 41x36 여백 8px 4px 4px. 2026-09-02\n *\n * 줄의 높이는 줄이 갖고, 제목은 위아래 같은 여백을 갖는다.\n */\n min-height: 36px;\n /*\n * ── 그릴 것이 없으면 자리도 없다 ──────────────────────────────────────\n * 제목도 검색도 필터도 없는 화면에서 이 요소가 **자식 0개인 채로 36px 을 차지**했다.\n * 배우기 문서에서 눈에 걸렸다 — 제목 줄을 끄자 문서 위에 빈 띠가 남았다.\n * [확인함] 2026-09-04 · 높이 36 · 자식 0\n *\n * min-height 는 그릴 것이 있을 때의 줄 높이다. 없을 때는 아래 [blank] 가 걷는다.\n */\n /*\n * 줄의 숨 쉴 자리 — **위와 오른쪽을 같게.**\n *\n * 전에는 제목의 위 여백(padding-top: medium)이 그 일을 하고 있었다. 그것을 걷어 글자를 가운데로\n * 맞추자 줄이 탭 바에 붙고, 알약은 원래부터 오른쪽 끝에 붙어 있었다 — 위 0 · 오른쪽 0.\n * [확인함] 2026-09-02\n *\n * 왼쪽은 화면의 여백(--ui-gutter)에 맞춘다 — 제목 글자가 본문 첫 글자와 같은 선에 선다.\n * 제목 상자가 자기 여백(spacing-small)을 가지므로 그만큼 뺀다. 전에는 0 이어서 문서 화면에서\n * 제목(x214)과 본문(x229)이 15px 어긋났다. [확인함 :4000 1440 창] 2026-10-05\n */\n padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-small)\n calc(var(--ui-gutter, 16px) - var(--spacing-small, 4px));\n box-sizing: border-box;\n text-transform: capitalize;\n\n --help-icon-color: var(--md-sys-color-on-surface);\n --help-icon-hover-color: var(--md-sys-color-on-surface-variant);\n /*\n * ── 0.2 는 안 보인다 (2026-09-05) ────────────────────────────────\n * 도움말은 「있는 줄 알아야」 누른다. 20% 로 두면 야간 모드에서 대비 1.78:1 이고\n * (실측: 성숙도 화면 제목 옆), 밝은 모드도 비슷하다. 선택해 볼 생각을 못 한다.\n *\n * 0.5 면 밝은 화면 3.4 · 어두운 화면 3.9 로 UI 요소 기준(3:1)을 넘고, 제목보다는\n * 여전히 뒤에 선다.\n */\n --help-icon-opacity: 0.5;\n /* 마우스가 오면 또렷해진다. 전에는 이 이름이 선언되어 있지 않아 무효 치환 덕에\n 우연히 1 이 되고 있었다. */\n --help-icon-hover-opacity: 1;\n --help-icon-size: 20px;\n\n --md-icon-size: 20px;\n\n --input-search-padding: var(--spacing-small);\n --input-search-font: normal 14px var(--theme-font);\n }\n\n /*\n * 아무것도 그리지 않았다 — 자리를 비운다. updated() 가 그려진 것을 세어 붙인다.\n *\n * 왜 CSS 로만 못 하나: :host(:empty) 는 **바깥 DOM 의 자식**을 보므로 이 부품에서는 늘\n * 참이다(쓰는 쪽이 자식을 넣지 않는다). 그려진 것은 그림자 안에 있어서 세어야 한다.\n */\n :host([blank]) {\n display: none;\n }\n\n [titler] {\n display: flex;\n padding: var(--spacing-small);\n font: var(--context-toolbar-title);\n color: var(--context-toolbar-title-color, var(--md-sys-color-on-surface));\n }\n\n [titler] > * {\n align-self: center;\n }\n\n /*\n * One line under the title that says what the screen is for (context.description). It takes\n * the whole row, so the search pill stays on the title's line. Without the key nothing is\n * drawn and the bar is exactly as before (ADR-0060 decision 8).\n */\n :host([described]) {\n flex-wrap: wrap;\n }\n\n [description] {\n order: 10;\n flex-basis: 100%;\n padding: 0 var(--spacing-small) var(--spacing-small);\n font-size: var(--ui-t-body, 13px);\n line-height: 1.45;\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n text-transform: none;\n }\n\n [help] {\n opacity: var(--help-icon-opacity);\n font-size: var(--help-icon-size);\n color: var(--help-icon-color);\n }\n\n [help]:hover {\n cursor: help;\n opacity: var(--help-icon-hover-opacity);\n color: var(--help-icon-hover-color);\n }\n\n [search] [help] {\n color: var(--md-sys-color-on-secondary-container);\n }\n\n [search] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n\n /*\n * 알약의 높이를 정한다. 안의 입력이 정하게 두면 23px 이 되어 제목 줄보다 낮고, 알약이라\n * 그 차이가 눈에 걸린다.\n */\n min-height: 28px;\n box-sizing: border-box;\n\n align-self: center;\n /*\n * 검색은 강조 동작이 아니라 현재 목록을 좁히는 도구다. primary 의 반대색을 면으로\n * 쓰면 다크 도메인에서 밝은 알약으로 뒤집혀 제목 줄보다 먼저 읽힌다. 표준 surface\n * 단계 안에서만 분리해 다른 애플리케이션의 검색과 같은 톤을 유지한다.\n */\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-low);\n border-radius: var(--md-sys-shape-corner-full);\n margin-left: auto;\n padding: 0 var(--spacing-medium);\n }\n\n [search] > * {\n align-self: center;\n }\n\n /*\n * 알약 안의 장치는 **한 단 흐리다** — 돋보기와 필터 여닫기.\n *\n * 둘 다 값이 아니라 값을 다루는 장치다. 입력한 글자와 같은 색이면 빈 알약에서 장치만 눈에 걸린다.\n * 마우스가 오면 진해져서 누를 수 있음을 말한다.\n */\n [search] ox-input-search {\n /*\n * 검색 알약이 이미 표면과 모양을 책임진다. 입력 자체의 기본 surface-variant 배경을\n * 그대로 두면 알약 안에 또 하나의 상자가 생겨 경계가 도드라진다.\n */\n --ox-input-search-background-color: transparent;\n --ox-input-search-color: var(--md-sys-color-on-surface);\n --ox-input-search-icon-color: var(--ui-faint, #8a959c);\n }\n\n [search] > md-icon:not([help]) {\n color: var(--ui-faint, #8a959c);\n cursor: pointer;\n transition: color 0.12s ease;\n }\n\n [search] > md-icon:not([help]):hover {\n color: var(--ui-muted, #57676f);\n }\n\n /*\n * The screen's name always shows (2026-10-12). A narrow window used to drop the title for the\n * search pill; now the pill moves to the next line when the row is too short, still at the right.\n */\n :host {\n flex-wrap: wrap;\n }\n `\n ]\n\n private _route = new RouteController(this)\n\n /*\n * The search field keeps what the user is typing through every re-render. Only a list query that was refused\n * (the user kept unsaved edits) draws it anew, so it shows the committed term again.\n */\n @state() private refused = 0\n private onRefused = () => this.refused++\n\n connectedCallback() {\n super.connectedCallback()\n document.addEventListener('query-refused', this.onRefused)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n document.removeEventListener('query-refused', this.onRefused)\n }\n\n /**\n * 그린 것이 하나도 없으면 host 에 `blank` 를 붙인다 — 그러면 자리를 비운다.\n *\n * 그리는 것은 제목 · 도움 · 검색·필터 알약 셋뿐이고 모두 요소 하나를 낸다. 무엇 때문에\n * 비었는지 따지지 않고 **그려진 것을 센다.** 조건이 늘어도 이 판단은 그대로 맞다.\n */\n updated() {\n this.toggleAttribute('blank', this.renderRoot.querySelector('div, md-icon') === null)\n this.toggleAttribute('described', this.renderRoot.querySelector('[description]') !== null)\n }\n\n render() {\n var { search, filter, title, help, heading, description } = this._route.context || ({} as any)\n var { value = '', handler: searchHandler, placeholder = '', autofocus = true } = search || ({} as any)\n const { handler: filterHandler } = filter || ({} as any)\n const { icon, text = title } = typeof title === 'object' ? title : ({} as any)\n const searchable = typeof searchHandler == 'function'\n const filterable = typeof filterHandler == 'function'\n\n /*\n * ── 화면이 자기 제목을 그리는 경우 ────────────────────────────────────────\n *\n * `context.title` 은 **화면의 이름**이고, 그 이름을 두 곳이 읽는다.\n *\n * MDI 탭 여러 화면 중 어느 것인가\n * 이 제목 줄 지금 읽는 것이 무엇인가\n *\n * 대개 둘 다 필요하다. 그런데 **화면이 이미 자기 제목을 크게 그리는 경우**가 있다 — 배우기\n * 문서가 그렇다. 문서 맨 위에 「공장의 모든 사건을 …」 같은 제목이 있고, 그 위에 이 줄이\n * 「배우기」를 또 얹고, 탭에도 「배우기」가 있다. **같은 말이 세 번이다.**\n *\n * 그때 title 을 빼면 탭 이름도 함께 빈다(page-mdibar 가 같은 값을 읽는다). 그래서 이름은\n * 그대로 두고 **이 줄만 끄는 방법**을 둔다. 이름은 하나이고, 그리는 자리마다 켜고 끈다.\n *\n * get context() { return { title: '배우기', heading: false } }\n *\n * 제목이 없으면 도움 아이콘도 걷힌다 — 아래 조건이 이미 `help && title` 이다. 문서 자체가\n * 도움인 화면에서 그 아이콘은 같은 말의 네 번째다.\n */\n if (heading === false) {\n title = ''\n }\n\n return html`\n ${title && description ? html`<div description>${description}</div>` : html``}\n ${title ? html` <div titler>${icon ? html`<md-icon>${icon}</md-icon>&nbsp;` : html``}${text}</div> ` : html``}\n ${help && title ? html` &nbsp;<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon> ` : html``}\n ${searchable || filterable\n ? html`\n <div search>\n ${searchable\n ? keyed(this.refused, html` <ox-input-search\n .placeholder=${placeholder}\n .value=${value || ''}\n ?autofocus=${autofocus}\n @change=${(e: Event) => {\n searchHandler((e.target as any).value)\n }}\n ></ox-input-search>`)\n : html``}\n ${!title && help && searchable\n ? html`<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon>`\n : html``}\n ${filterable ? html`<md-icon @click=${(e: MouseEvent) => filterHandler()}>unfold_more</md-icon>` : html``}\n </div>\n `\n : html``}\n `\n }\n}\n"]}
@@ -4,5 +4,9 @@ import { LitElement } from 'lit';
4
4
  export declare class TitleBar extends LitElement {
5
5
  static styles: import("lit").CSSResult[];
6
6
  private _route;
7
+ private refused;
8
+ private onRefused;
9
+ connectedCallback(): void;
10
+ disconnectedCallback(): void;
7
11
  render(): import("lit-html").TemplateResult<1>;
8
12
  }
@@ -2,14 +2,28 @@ import { __decorate } from "tslib";
2
2
  import '@material/web/icon/icon.js';
3
3
  import '@operato/input/ox-input-search.js';
4
4
  import { css, html, LitElement } from 'lit';
5
- import { live } from 'lit/directives/live.js';
6
- import { customElement } from 'lit/decorators.js';
5
+ import { customElement, state } from 'lit/decorators.js';
6
+ import { keyed } from 'lit/directives/keyed.js';
7
7
  import { openHelp } from '@operato/help';
8
8
  import { RouteController } from '@operato/shell';
9
9
  let TitleBar = class TitleBar extends LitElement {
10
10
  constructor() {
11
11
  super(...arguments);
12
12
  this._route = new RouteController(this);
13
+ /*
14
+ * The search field keeps what the user is typing through every re-render. Only a list query that was refused
15
+ * (the user kept unsaved edits) draws it anew, so it shows the committed term again.
16
+ */
17
+ this.refused = 0;
18
+ this.onRefused = () => this.refused++;
19
+ }
20
+ connectedCallback() {
21
+ super.connectedCallback();
22
+ document.addEventListener('query-refused', this.onRefused);
23
+ }
24
+ disconnectedCallback() {
25
+ super.disconnectedCallback();
26
+ document.removeEventListener('query-refused', this.onRefused);
13
27
  }
14
28
  render() {
15
29
  var { search, filter, title, help } = this._route.context || {};
@@ -29,15 +43,15 @@ let TitleBar = class TitleBar extends LitElement {
29
43
  ? html `
30
44
  <div search>
31
45
  ${searchable
32
- ? html ` <ox-input-search
46
+ ? keyed(this.refused, html ` <ox-input-search
33
47
  .placeholder=${placeholder}
34
- .value=${live(value || '')}
48
+ .value=${value || ''}
35
49
  ?autofocus=${autofocus}
36
50
  .maxLength=${maxLength}
37
51
  @change=${(e) => {
38
52
  searchHandler(e.target.value);
39
53
  }}
40
- ></ox-input-search>`
54
+ ></ox-input-search>`)
41
55
  : html ``}
42
56
  ${!title && help && searchable
43
57
  ? html `<md-icon @click=${(e) => openHelp(help)} help>help</md-icon>`
@@ -118,6 +132,9 @@ TitleBar.styles = [
118
132
  }
119
133
  `
120
134
  ];
135
+ __decorate([
136
+ state()
137
+ ], TitleBar.prototype, "refused", void 0);
121
138
  TitleBar = __decorate([
122
139
  customElement('ox-title-bar')
123
140
  ], TitleBar);
@@ -1 +1 @@
1
- {"version":3,"file":"ox-title-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-title-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/input/ox-input-search.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { live } from 'lit/directives/live.js'\nimport { customElement } from 'lit/decorators.js'\nimport { openHelp } from '@operato/help'\nimport { RouteController } from '@operato/shell'\n\n@customElement('ox-title-bar')\nexport class TitleBar extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n align-items: center;\n text-transform: capitalize;\n\n --help-icon-color: var(--md-sys-color-primary);\n --help-icon-hover-color: var(--md-sys-color-primary);\n --help-icon-opacity: 0.2;\n --help-icon-size: 20px;\n\n --md-icon-size: 20px;\n\n --input-search-padding: var(--spacing-medium);\n --input-search-focus-border-bottom: none;\n --input-search-font: normal 16px var(--theme-font);\n }\n\n [titler] {\n display: flex;\n font: var(--context-toolbar-title);\n }\n\n [titler] > * {\n align-self: center;\n }\n\n [help] {\n opacity: 0.5;\n font-size: var(--help-icon-size);\n color: var(--context-toolbar-title);\n }\n\n [help]:hover {\n cursor: help;\n opacity: var(--help-icon-hover-opacity);\n color: var(--help-icon-hover-color);\n }\n\n [search] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n\n align-self: center;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-low);\n border-radius: var(--md-sys-shape-corner-full);\n padding: 0 var(--spacing-medium);\n }\n\n [search] > * {\n align-self: center;\n }\n\n ox-input-search {\n color: var(--md-sys-color-on-surface);\n /*\n * 입력 컴포넌트의 배경은 shadow DOM 안에서 custom property로 받는다. 일반\n * background-color 선언은 기본 surface-variant를 이기지 못해 알약 안에 각진\n * 상자가 남는다.\n */\n --ox-input-search-background-color: transparent;\n --ox-input-search-color: var(--md-sys-color-on-surface);\n border-radius: var(--md-sys-shape-corner-full);\n }\n `\n ]\n\n private _route = new RouteController(this)\n\n render() {\n var { search, filter, title, help } = this._route.context || ({} as any)\n var { value = '', handler: searchHandler, placeholder = '', autofocus = true, maxLength } = search || ({} as any)\n const { handler: filterHandler } = filter || ({} as any)\n const { icon, text = title } = typeof title === 'object' ? title : ({} as any)\n const searchable = typeof searchHandler == 'function'\n const filterable = typeof filterHandler == 'function'\n\n if (searchable && title) {\n placeholder ||= text\n title = ''\n }\n\n return html`\n ${title ? html` <div titler>${icon ? html`<md-icon>${icon}</md-icon>&nbsp;` : html``}${text}</div> ` : html``}\n ${help && title ? html` &nbsp;<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon> ` : html``}\n ${searchable || filterable\n ? html`\n <div search>\n ${searchable\n ? html` <ox-input-search\n .placeholder=${placeholder}\n .value=${live(value || '')}\n ?autofocus=${autofocus}\n .maxLength=${maxLength}\n @change=${(e: Event) => {\n searchHandler((e.target as any).value)\n }}\n ></ox-input-search>`\n : html``}\n ${!title && help && searchable\n ? html`<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon>`\n : html``}\n ${filterable ? html`<md-icon @click=${(e: MouseEvent) => filterHandler()}>tune</md-icon>` : html``}\n </div>\n `\n : html``}\n `\n }\n}\n"]}
1
+ {"version":3,"file":"ox-title-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-title-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/input/ox-input-search.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\nimport { keyed } from 'lit/directives/keyed.js'\nimport { openHelp } from '@operato/help'\nimport { RouteController } from '@operato/shell'\n\n@customElement('ox-title-bar')\nexport class TitleBar extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n align-items: center;\n text-transform: capitalize;\n\n --help-icon-color: var(--md-sys-color-primary);\n --help-icon-hover-color: var(--md-sys-color-primary);\n --help-icon-opacity: 0.2;\n --help-icon-size: 20px;\n\n --md-icon-size: 20px;\n\n --input-search-padding: var(--spacing-medium);\n --input-search-focus-border-bottom: none;\n --input-search-font: normal 16px var(--theme-font);\n }\n\n [titler] {\n display: flex;\n font: var(--context-toolbar-title);\n }\n\n [titler] > * {\n align-self: center;\n }\n\n [help] {\n opacity: 0.5;\n font-size: var(--help-icon-size);\n color: var(--context-toolbar-title);\n }\n\n [help]:hover {\n cursor: help;\n opacity: var(--help-icon-hover-opacity);\n color: var(--help-icon-hover-color);\n }\n\n [search] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n\n align-self: center;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-low);\n border-radius: var(--md-sys-shape-corner-full);\n padding: 0 var(--spacing-medium);\n }\n\n [search] > * {\n align-self: center;\n }\n\n ox-input-search {\n color: var(--md-sys-color-on-surface);\n /*\n * 입력 컴포넌트의 배경은 shadow DOM 안에서 custom property로 받는다. 일반\n * background-color 선언은 기본 surface-variant를 이기지 못해 알약 안에 각진\n * 상자가 남는다.\n */\n --ox-input-search-background-color: transparent;\n --ox-input-search-color: var(--md-sys-color-on-surface);\n border-radius: var(--md-sys-shape-corner-full);\n }\n `\n ]\n\n private _route = new RouteController(this)\n\n /*\n * The search field keeps what the user is typing through every re-render. Only a list query that was refused\n * (the user kept unsaved edits) draws it anew, so it shows the committed term again.\n */\n @state() private refused = 0\n private onRefused = () => this.refused++\n\n connectedCallback() {\n super.connectedCallback()\n document.addEventListener('query-refused', this.onRefused)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n document.removeEventListener('query-refused', this.onRefused)\n }\n\n render() {\n var { search, filter, title, help } = this._route.context || ({} as any)\n var { value = '', handler: searchHandler, placeholder = '', autofocus = true, maxLength } = search || ({} as any)\n const { handler: filterHandler } = filter || ({} as any)\n const { icon, text = title } = typeof title === 'object' ? title : ({} as any)\n const searchable = typeof searchHandler == 'function'\n const filterable = typeof filterHandler == 'function'\n\n if (searchable && title) {\n placeholder ||= text\n title = ''\n }\n\n return html`\n ${title ? html` <div titler>${icon ? html`<md-icon>${icon}</md-icon>&nbsp;` : html``}${text}</div> ` : html``}\n ${help && title ? html` &nbsp;<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon> ` : html``}\n ${searchable || filterable\n ? html`\n <div search>\n ${searchable\n ? keyed(this.refused, html` <ox-input-search\n .placeholder=${placeholder}\n .value=${value || ''}\n ?autofocus=${autofocus}\n .maxLength=${maxLength}\n @change=${(e: Event) => {\n searchHandler((e.target as any).value)\n }}\n ></ox-input-search>`)\n : html``}\n ${!title && help && searchable\n ? html`<md-icon @click=${(e: MouseEvent) => openHelp(help)} help>help</md-icon>`\n : html``}\n ${filterable ? html`<md-icon @click=${(e: MouseEvent) => filterHandler()}>tune</md-icon>` : html``}\n </div>\n `\n : html``}\n `\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ import '../src/tools/ox-page-title-bar.js';
2
+ import '../src/tools/ox-title-bar.js';
@@ -0,0 +1,37 @@
1
+ import { expect, fixture } from '@open-wc/testing';
2
+ import { html } from 'lit';
3
+ import { store } from '@operato/shell';
4
+ import '../src/tools/ox-page-title-bar.js';
5
+ import '../src/tools/ox-title-bar.js';
6
+ /*
7
+ * The title bar's search keeps what the user is typing through a context update - a screen that redraws its context
8
+ * while the user types (integration analysis) wiped the text when the field was bound with `live()`, 2026-10-17.
9
+ * A list query that was refused draws the field anew with the committed term.
10
+ */
11
+ const search = (el) => el.shadowRoot.querySelector('ox-input-search');
12
+ for (const tag of ['ox-page-title-bar', 'ox-title-bar']) {
13
+ describe(`${tag} search`, () => {
14
+ const mount = async () => {
15
+ store.dispatch({ type: 'UPDATE_CONTEXT', context: { title: 'list', search: { handler: () => { }, value: '' } } });
16
+ const el = document.createElement(tag);
17
+ await fixture(html `${el}`);
18
+ await el.updateComplete;
19
+ return el;
20
+ };
21
+ it('keeps the text being typed when the context is drawn again', async () => {
22
+ const el = await mount();
23
+ search(el).value = 'typing';
24
+ store.dispatch({ type: 'UPDATE_CONTEXT', context: { title: 'list', search: { handler: () => { }, value: '' } } });
25
+ await el.updateComplete;
26
+ expect(search(el).value).to.equal('typing');
27
+ });
28
+ it('shows the committed term again after a refused query', async () => {
29
+ const el = await mount();
30
+ search(el).value = 'refused';
31
+ document.dispatchEvent(new CustomEvent('query-refused'));
32
+ await el.updateComplete;
33
+ expect(search(el).value).to.equal('');
34
+ });
35
+ });
36
+ }
37
+ //# sourceMappingURL=title-search-typing.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"title-search-typing.test.js","sourceRoot":"","sources":["../../test/title-search-typing.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAE1B,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEtC,OAAO,mCAAmC,CAAA;AAC1C,OAAO,8BAA8B,CAAA;AAErC;;;;GAIG;AAEH,MAAM,MAAM,GAAG,CAAC,EAAe,EAAE,EAAE,CAAC,EAAE,CAAC,UAAW,CAAC,aAAa,CAAC,iBAAiB,CAAoC,CAAA;AAEtH,KAAK,MAAM,GAAG,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,EAAE,CAAC;IACxD,QAAQ,CAAC,GAAG,GAAG,SAAS,EAAE,GAAG,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE;YACvB,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAS,CAAC,CAAA;YACvH,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAuD,CAAA;YAC5F,MAAM,OAAO,CAAC,IAAI,CAAA,GAAG,EAAE,EAAE,CAAC,CAAA;YAC1B,MAAM,EAAE,CAAC,cAAc,CAAA;YACvB,OAAO,EAAE,CAAA;QACX,CAAC,CAAA;QAED,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;YAC1E,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAA;YACxB,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,GAAG,QAAQ,CAAA;YAC3B,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAS,CAAC,CAAA;YACvH,MAAM,EAAE,CAAC,cAAc,CAAA;YACvB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QAC7C,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YACpE,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAA;YACxB,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,GAAG,SAAS,CAAA;YAC5B,QAAQ,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,eAAe,CAAC,CAAC,CAAA;YACxD,MAAM,EAAE,CAAC,cAAc,CAAA;YACvB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QACvC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { expect, fixture } from '@open-wc/testing'\nimport { html } from 'lit'\n\nimport { store } from '@operato/shell'\n\nimport '../src/tools/ox-page-title-bar.js'\nimport '../src/tools/ox-title-bar.js'\n\n/*\n * The title bar's search keeps what the user is typing through a context update - a screen that redraws its context\n * while the user types (integration analysis) wiped the text when the field was bound with `live()`, 2026-10-17.\n * A list query that was refused draws the field anew with the committed term.\n */\n\nconst search = (el: HTMLElement) => el.shadowRoot!.querySelector('ox-input-search') as HTMLElement & { value: string }\n\nfor (const tag of ['ox-page-title-bar', 'ox-title-bar']) {\n describe(`${tag} search`, () => {\n const mount = async () => {\n store.dispatch({ type: 'UPDATE_CONTEXT', context: { title: 'list', search: { handler: () => {}, value: '' } } } as any)\n const el = document.createElement(tag) as HTMLElement & { updateComplete: Promise<unknown> }\n await fixture(html`${el}`)\n await el.updateComplete\n return el\n }\n\n it('keeps the text being typed when the context is drawn again', async () => {\n const el = await mount()\n search(el).value = 'typing'\n store.dispatch({ type: 'UPDATE_CONTEXT', context: { title: 'list', search: { handler: () => {}, value: '' } } } as any)\n await el.updateComplete\n expect(search(el).value).to.equal('typing')\n })\n\n it('shows the committed term again after a refused query', async () => {\n const el = await mount()\n search(el).value = 'refused'\n document.dispatchEvent(new CustomEvent('query-refused'))\n await el.updateComplete\n expect(search(el).value).to.equal('')\n })\n })\n}\n"]}