@operato/context 10.29.8 → 10.29.12

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.
@@ -71,21 +71,12 @@ let PageActionContextBar = class PageActionContextBar extends LitElement {
71
71
  }
72
72
 
73
73
  /*
74
- * The buttons end where the screen's content ends (ADR-0060 decision 9 addendum).
75
- *
76
- * This bar spans the window. A document screen's content does not - it is held to the
77
- * reading measure - so on a wide window the reader finished the page at one x and the
78
- * buttons sat far to the right of it. Measured on the approval screen at 1714px: the
79
- * content ended at 1050 and the buttons began at 1385, 335px away.
80
- *
81
- * A work surface fills the window, so its buttons already share its right edge and nothing
82
- * changes for it. The screen says which it is through context.frame; with nothing said,
83
- * this stays as it was.
74
+ * The buttons end where the screen's content ends. A document screen used to stop at the
75
+ * reading measure (1080px) and this bar followed it there through context.frame; since
76
+ * 2026-10-05 every frame fills the window between two equal gutters (user decision, ADR in
77
+ * operato-application/adr), so the bar spans the window like the content and no frame narrows it.
78
+ * The frame attribute is still set — it says what the screen is — but nothing here reads it.
84
79
  */
85
- :host([frame='document']) [container] {
86
- max-width: var(--ui-measure, min(1080px, 100%));
87
- margin-right: auto;
88
- }
89
80
 
90
81
  /*
91
82
  * 액션바 위에 떠 있는 오버플로 버튼 — 배경과 구분되도록 **테마와 무관하게 어두운 표면**을 유지한다.
@@ -1 +1 @@
1
- {"version":3,"file":"ox-page-action-context-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-page-action-context-bar.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,sCAAsC,CAAA;AAE7C,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,KAAK,CAAA;AACpE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzE,OAAO,QAAQ,MAAM,uBAAuB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACtC,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAA;AAGpE,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,UAAU;IAA7C;;QAsIG,WAAM,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,CAAA;QAEf,YAAO,GAQ5B,EAAE,CAAA;QAES,cAAS,GAAY,KAAK,CAAA;IA8I7C,CAAC;IA/RC,MAAM,KAAK,MAAM;QACf,OAAO;YACL,qBAAqB;YACrB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBA0FW,iBAAiB,CAAC,8BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoC9D;SACF,CAAA;IACH,CAAC;IAoBD,IAAI,gBAAgB;QAClB,IAAI,CAAC,IAAI,CAAC,wBAAwB,EAAE,CAAC;YACnC,IAAI,CAAC,wBAAwB,GAAG,QAAQ,CAAC,GAAG,EAAE;gBAC5C,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAA;YAC9F,CAAC,EAAE,GAAG,CAAC,CAAA;QACT,CAAC;QAED,OAAO,IAAI,CAAC,wBAAwB,CAAA;IACtC,CAAC;IAED,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QAEzB,IAAI,CAAC,cAAc,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAA;QAEnD,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;IACxD,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAE5B,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;IAC3D,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;YAC5B,CAAC,CAAC,IAAI,CAAA;2BACe,IAAI,CAAC,aAAa,EAAE;;;;;;YAMnC,IAAI,CAAC,SAAS;gBACd,CAAC,CAAC,IAAI,CAAA;;;;;0BAKQ,OAAO,CAAC,CAAC,CAAC,mBAAmB,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;2BAC/D,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;6BACtB,CAAC,CAAgB,EAAE,EAAE;oBAC9B,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;wBAAE,OAAM;oBAC9C,CAAC,CAAC,cAAc,EAAE,CAAA;oBAClB,IAAI,CAAC,QAAQ,EAAE,CAAA;gBACjB,CAAC;;;eAGJ;gBACH,CAAC,CAAC,IAAI,CAAA,EAAE;SACX;YACH,CAAC,CAAC,OAAO,CAAA;IACb,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAA;QACP,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;YAClC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAc,CAAA;YACnG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,IAAI,EAAE,CAAA;YAEvC,OAAO,IAAI,IAAI,MAAM;gBACnB,CAAC,CAAC,IAAI,CAAA,iBAAiB,MAAM,IAAI,KAAK,UAAU;gBAChD,CAAC,CAAC,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;oBACjD,CAAC,CAAC,IAAI,CAAA;kCACgB,MAAM;oBACpB,MAAM,CAAC,GAAG,CAAC,CAAC,MAAW,EAAE,EAAE,CAAC,IAAI,CAAA,YAAY,MAAM,YAAY,CAAC;;eAEpE;oBACH,CAAC,CAAC,IAAI,IAAI,MAAM;wBACd,CAAC,CAAC,IAAI,CAAA,YAAY,IAAI,IAAI,KAAK,OAAO;wBACtC,CAAC,CAAC,IAAI,CAAA;;iCAEa,IAAI,IAAI,MAAM,IAAI,CAAC,KAAK;8BAC3B,MAAM;8BACN,MAAM;gCACJ,QAAQ;4BACZ,QAAQ,IAAI,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE;6BACtD,KAAK,EAAE,CAAa,EAAE,EAAE;4BAC/B,+CAA+C;4BAC/C,MAAM,MAAM,GAAG,CAAC,CAAC,aAAoB,CAAA;4BACrC,MAAM,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,CAAA;4BACnD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;4BAEtC,IAAI,WAAW,EAAE,CAAC;gCAChB,IAAI,GAAG,YAAY,CAAC,IAAI,CAAA;gCACxB,YAAY,CAAC,IAAI,GAAG,SAAS,CAAA;gCAC7B,IAAI,CAAC,aAAa,EAAE,CAAA;gCACpB,WAAW,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;4BAC3C,CAAC;4BACD,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAA;4BAEtB,IAAI,CAAC;gCACH,MAAM,MAAM,EAAE,CAAA;4BAChB,CAAC;oCAAS,CAAC;gCACT,MAAM,KAAK,CAAC,IAAI,CAAC,CAAA,CAAC,qCAAqC;gCAEvD,wEAAwE;gCACxE,IAAI,WAAW,EAAE,CAAC;oCAChB,YAAY,CAAC,IAAI,GAAG,IAAI,CAAA;oCACxB,IAAI,CAAC,aAAa,EAAE,CAAA;oCACpB,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;gCACxC,CAAC;gCACD,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAA;4BACzB,CAAC;wBACH,CAAC;;sBAEC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA,YAAY,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,OAAO;sBACrF,IAAI,IAAI,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK;;iBAE/C,CAAA;QACX,CAAC,CAAC;KACH,CAAA;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAA6B;QACzC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3B,qBAAqB,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAES,UAAU;QAClB,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAA;QACpD,2FAA2F;QAC3F,IAAI,KAAK;YAAE,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;;YAC/C,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;QAElC,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAW,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QAC1D,CAAC;IACH,CAAC;IAED,QAAQ;QACN,WAAW,CAAC,yBAAyB,EAAE;YACrC,QAAQ,EAAE,IAAI,CAAA,qEAAqE;SACpF,CAAC,CAAA;IACJ,CAAC;CACF,CAAA;AAxJ4B;IAA1B,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;qDAQlB;AAES;IAAhB,KAAK,EAAE;uDAAmC;AACrB;IAArB,KAAK,CAAC,aAAa,CAAC;uDAA2B;AAnJrC,oBAAoB;IADhC,aAAa,CAAC,4BAA4B,CAAC;GAC/B,oBAAoB,CAgShC","sourcesContent":["import '@material/web/icon/icon.js'\nimport './ox-page-action-overlay-template.js'\n\nimport { css, html, LitElement, nothing, PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport throttle from 'lodash-es/throttle.js'\nimport { i18next } from '@operato/i18n'\nimport { openOverlay } from '@operato/layout'\nimport { RouteController } from '@operato/shell'\nimport { sleep } from '@operato/utils'\nimport { ButtonContainerStyles } from '@operato/styles'\nimport { formElementHeight } from '@operato/styles/form-element-heights.js'\n\n@customElement('ox-page-action-context-bar')\nexport class PageActionContextBar extends LitElement {\n static get styles() {\n return [\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n position: relative;\n overflow-x: clip;\n overflow-y: hidden;\n\n text-transform: capitalize;\n }\n\n /*\n * ── 키보드로 온 사람에게 지금 어디인지 보인다 ──────────────────────\n * 여기 있던 것은 :host *:focus { outline: none } 한 줄이다. 그런데 이 줄의 단추는 자기\n * 스타일에 이미 초점 표시를 갖고 있었다(ButtonContainerStyles 의 button &:focus-visible,\n * 2px). **그 한 줄이 그것을 이겼다** — :host *:focus 는 특정도 (0,2,0) 이고\n * button:focus-visible 은 (0,1,1) 이다. 그래서 선언은 있는데 화면에는 아무것도 안 그려졌다.\n *\n * [확인함] 2026-09-19 :3500 — 이 줄의 단추에 초점을 주고 재니 outline-style: none 이었다.\n * (그 재기는 :focus 까지만 확인한다. 키보드 초점은 합성으로 못 만든다.)\n *\n * 마우스로 누를 때 테두리가 남는 것이 싫어서 껐던 것은 맞다. 그 뜻은 :focus-visible 이\n * 정확히 낸다. 그래서 끄는 것은 **그 밖의 초점에만** 건다.\n *\n * 굵기는 3px 로 맞춘다 — Material 단추의 링이 3px 이고, 한 줄에 우리 단추와 md 단추가 함께\n * 서므로 두 표시가 같아야 한다.\n */\n :host *:focus:not(:focus-visible) {\n outline: none;\n }\n\n :host *:focus-visible {\n outline: 3px solid var(--md-sys-color-primary);\n outline-offset: 2px;\n }\n\n [container] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n gap: var(--spacing-small);\n /*\n * 초점 링이 설 자리 — 링은 단추 **밖으로 5px** 를 쓴다(3px 굵기 + 2px 띄움). 2px 이던 때는\n * 이 줄이 overflow: hidden 이라 위아래가 잘렸다. 가로로 자르는 것은 남겨 둔다 —\n * 「더보기」가 scrollWidth > clientWidth 로 넘침을 재는데 그 값이 살아 있어야 한다.\n */\n padding: 5px;\n align-items: center;\n }\n\n /*\n * The buttons end where the screen's content ends (ADR-0060 decision 9 addendum).\n *\n * This bar spans the window. A document screen's content does not - it is held to the\n * reading measure - so on a wide window the reader finished the page at one x and the\n * buttons sat far to the right of it. Measured on the approval screen at 1714px: the\n * content ended at 1050 and the buttons began at 1385, 335px away.\n *\n * A work surface fills the window, so its buttons already share its right edge and nothing\n * changes for it. The screen says which it is through context.frame; with nothing said,\n * this stays as it was.\n */\n :host([frame='document']) [container] {\n max-width: var(--ui-measure, min(1080px, 100%));\n margin-right: auto;\n }\n\n /*\n * 액션바 위에 떠 있는 오버플로 버튼 — 배경과 구분되도록 **테마와 무관하게 어두운 표면**을 유지한다.\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 다크에서 밝게 반전되어 눈에 튀었다(ox-snack-bar 와 같은 부류).\n * 반투명 어두운 값을 기본으로 두면 브랜드 팔레트와 무관하게 안정적이고 .dark 패치도 불필요하다.\n */\n [more] {\n position: absolute;\n bottom: 0;\n right: 0;\n color: var(--ox-context-more-color, rgba(255, 255, 255, 0.95));\n background-color: var(--ox-context-more-background, rgba(0, 0, 0, 0.85));\n box-shadow: var(--ox-context-more-shadow, -2px 0px 2px 1px rgba(0, 0, 0, 0.2));\n padding: var(--context-action-bar-more-button-padding, 11px 10px);\n font-size: 2rem;\n z-index: 10;\n }\n\n select,\n input[type='text'] {\n box-sizing: border-box;\n padding: 0 var(--spacing-small);\n height: ${formElementHeight('--form-element-height-medium')};\n border: 1px solid var(--md-sys-color-outline);\n border-radius: var(--md-sys-shape-corner-small);\n color: var(--md-sys-color-on-primary);\n background-color: var(--md-sys-color-on-primary);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n\n md-icon[working] {\n animation: rotate 1s linear infinite;\n }\n\n @media screen and (max-width: 460px) {\n [container] {\n padding: 0;\n gap: 0;\n }\n\n /* 아주 좁은 화면 — 버튼을 붙여 한 덩어리로 만든다(사이 여백이 자리를 먹는다). */\n button {\n border-radius: 0;\n }\n\n button:not(:last-child) {\n border-right-width: 0;\n }\n }\n\n @keyframes rotate {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `\n ]\n }\n\n private _route = new RouteController(this)\n\n @property({ type: Array }) actions: {\n title: string\n action: () => void\n icon: string\n type: string\n select: string[]\n href?: string\n emphasis?: boolean\n }[] = []\n\n @state() private overflown: boolean = false\n @query('[container]') container?: HTMLDivElement\n\n private overflownUpdaterThrottle: any\n private resizeCallback: any\n\n get overflownUpdater() {\n if (!this.overflownUpdaterThrottle) {\n this.overflownUpdaterThrottle = throttle(() => {\n this.overflown = !!this.container && this.container.scrollWidth > this.container.clientWidth\n }, 100)\n }\n\n return this.overflownUpdaterThrottle\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n this.resizeCallback = () => this.overflownUpdater()\n\n window.addEventListener('resize', this.resizeCallback)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n\n window.removeEventListener('resize', this.resizeCallback)\n }\n\n render() {\n return this.actions.length > 0\n ? html`\n <div container>${this.renderActions()}</div>\n <!--\n 넘친 동작들에 닿는 **유일한 길**이다. 그런데 이름이 없어 낭독기는 「more_horiz」를\n 읽었고, md-icon 이라 키보드 초점도 못 받았다 — 키보드만 쓰는 사람에게는 그 동작들이\n 아예 없는 것과 같았다.\n -->\n ${this.overflown\n ? html`\n <md-icon\n more\n role=\"button\"\n tabindex=\"0\"\n title=${i18next.t('text.more-actions', { defaultValue: 'more actions' })}\n @click=${this.showMore.bind(this)}\n @keydown=${(e: KeyboardEvent) => {\n if (e.key !== 'Enter' && e.key !== ' ') return\n e.preventDefault()\n this.showMore()\n }}\n >more_horiz</md-icon\n >\n `\n : html``}\n `\n : nothing\n }\n\n renderActions() {\n return html`\n ${this.actions.map(($action, idx) => {\n let { type, title, icon, action, select, href, emphasis, disabled, disabledTitle } = $action as any\n let { raised, danger } = emphasis || {}\n\n return type == 'text'\n ? html` <span @click=${action}>${title}</span> `\n : type == 'select' || (select && select.length > 0)\n ? html`\n <select @change=${action}>\n ${select.map((option: any) => html` <option>${option}</option> `)}\n </select>\n `\n : type == 'link'\n ? html` <a href=${href}>${title}</a> `\n : html`\n <button\n ?icon-only=${type == 'icon' || !title}\n ?raised=${raised}\n ?danger=${danger}\n ?disabled=${disabled}\n title=${disabled && disabledTitle ? disabledTitle : title || ''}\n @click=${async (e: MouseEvent) => {\n /* all actions should be bloked for a second */\n const button = e.currentTarget as any\n const iconElement = button.querySelector('md-icon')\n const actionConfig = this.actions[idx]\n\n if (iconElement) {\n icon = actionConfig.icon\n actionConfig.icon = 'refresh'\n this.requestUpdate()\n iconElement.setAttribute('working', true)\n }\n button.disabled = true\n\n try {\n await action()\n } finally {\n await sleep(1000) /* waiting for additionla 1 second */\n\n /* because icon can be changed after sleep, don't use closure 'icon'. */\n if (iconElement) {\n actionConfig.icon = icon\n this.requestUpdate()\n iconElement.removeAttribute('working')\n }\n button.disabled = false\n }\n }}\n >\n ${this.actions[idx].icon ? html`<md-icon>${this.actions[idx].icon}</md-icon>` : nothing}\n ${type == 'icon' || !title ? nothing : title}\n </button>\n `\n })}\n `\n }\n\n async updated(changed: PropertyValues<this>) {\n if (changed.has('actions')) {\n requestAnimationFrame(this.overflownUpdater)\n }\n }\n\n protected willUpdate() {\n const { actions, frame } = this._route.context || {}\n /* The bar has no box of its own to measure; the attribute is what the stylesheet reads. */\n if (frame) this.setAttribute('frame', String(frame))\n else this.removeAttribute('frame')\n\n if (actions) {\n this.actions = actions.filter((action: any) => !!action)\n }\n }\n\n showMore() {\n openOverlay('context-toolbar-overlay', {\n template: html`<ox-page-action-overlay-template></ox-page-action-overlay-template>`\n })\n }\n}\n"]}
1
+ {"version":3,"file":"ox-page-action-context-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-page-action-context-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport './ox-page-action-overlay-template.js'\n\nimport { css, html, LitElement, nothing, PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport throttle from 'lodash-es/throttle.js'\nimport { i18next } from '@operato/i18n'\nimport { openOverlay } from '@operato/layout'\nimport { RouteController } from '@operato/shell'\nimport { sleep } from '@operato/utils'\nimport { ButtonContainerStyles } from '@operato/styles'\nimport { formElementHeight } from '@operato/styles/form-element-heights.js'\n\n@customElement('ox-page-action-context-bar')\nexport class PageActionContextBar extends LitElement {\n static get styles() {\n return [\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n position: relative;\n overflow-x: clip;\n overflow-y: hidden;\n\n text-transform: capitalize;\n }\n\n /*\n * ── 키보드로 온 사람에게 지금 어디인지 보인다 ──────────────────────\n * 여기 있던 것은 :host *:focus { outline: none } 한 줄이다. 그런데 이 줄의 단추는 자기\n * 스타일에 이미 초점 표시를 갖고 있었다(ButtonContainerStyles 의 button &:focus-visible,\n * 2px). **그 한 줄이 그것을 이겼다** — :host *:focus 는 특정도 (0,2,0) 이고\n * button:focus-visible 은 (0,1,1) 이다. 그래서 선언은 있는데 화면에는 아무것도 안 그려졌다.\n *\n * [확인함] 2026-09-19 :3500 — 이 줄의 단추에 초점을 주고 재니 outline-style: none 이었다.\n * (그 재기는 :focus 까지만 확인한다. 키보드 초점은 합성으로 못 만든다.)\n *\n * 마우스로 누를 때 테두리가 남는 것이 싫어서 껐던 것은 맞다. 그 뜻은 :focus-visible 이\n * 정확히 낸다. 그래서 끄는 것은 **그 밖의 초점에만** 건다.\n *\n * 굵기는 3px 로 맞춘다 — Material 단추의 링이 3px 이고, 한 줄에 우리 단추와 md 단추가 함께\n * 서므로 두 표시가 같아야 한다.\n */\n :host *:focus:not(:focus-visible) {\n outline: none;\n }\n\n :host *:focus-visible {\n outline: 3px solid var(--md-sys-color-primary);\n outline-offset: 2px;\n }\n\n [container] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n gap: var(--spacing-small);\n /*\n * 초점 링이 설 자리 — 링은 단추 **밖으로 5px** 를 쓴다(3px 굵기 + 2px 띄움). 2px 이던 때는\n * 이 줄이 overflow: hidden 이라 위아래가 잘렸다. 가로로 자르는 것은 남겨 둔다 —\n * 「더보기」가 scrollWidth > clientWidth 로 넘침을 재는데 그 값이 살아 있어야 한다.\n */\n padding: 5px;\n align-items: center;\n }\n\n /*\n * The buttons end where the screen's content ends. A document screen used to stop at the\n * reading measure (1080px) and this bar followed it there through context.frame; since\n * 2026-10-05 every frame fills the window between two equal gutters (user decision, ADR in\n * operato-application/adr), so the bar spans the window like the content and no frame narrows it.\n * The frame attribute is still set — it says what the screen is — but nothing here reads it.\n */\n\n /*\n * 액션바 위에 떠 있는 오버플로 버튼 — 배경과 구분되도록 **테마와 무관하게 어두운 표면**을 유지한다.\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 다크에서 밝게 반전되어 눈에 튀었다(ox-snack-bar 와 같은 부류).\n * 반투명 어두운 값을 기본으로 두면 브랜드 팔레트와 무관하게 안정적이고 .dark 패치도 불필요하다.\n */\n [more] {\n position: absolute;\n bottom: 0;\n right: 0;\n color: var(--ox-context-more-color, rgba(255, 255, 255, 0.95));\n background-color: var(--ox-context-more-background, rgba(0, 0, 0, 0.85));\n box-shadow: var(--ox-context-more-shadow, -2px 0px 2px 1px rgba(0, 0, 0, 0.2));\n padding: var(--context-action-bar-more-button-padding, 11px 10px);\n font-size: 2rem;\n z-index: 10;\n }\n\n select,\n input[type='text'] {\n box-sizing: border-box;\n padding: 0 var(--spacing-small);\n height: ${formElementHeight('--form-element-height-medium')};\n border: 1px solid var(--md-sys-color-outline);\n border-radius: var(--md-sys-shape-corner-small);\n color: var(--md-sys-color-on-primary);\n background-color: var(--md-sys-color-on-primary);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n\n md-icon[working] {\n animation: rotate 1s linear infinite;\n }\n\n @media screen and (max-width: 460px) {\n [container] {\n padding: 0;\n gap: 0;\n }\n\n /* 아주 좁은 화면 — 버튼을 붙여 한 덩어리로 만든다(사이 여백이 자리를 먹는다). */\n button {\n border-radius: 0;\n }\n\n button:not(:last-child) {\n border-right-width: 0;\n }\n }\n\n @keyframes rotate {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `\n ]\n }\n\n private _route = new RouteController(this)\n\n @property({ type: Array }) actions: {\n title: string\n action: () => void\n icon: string\n type: string\n select: string[]\n href?: string\n emphasis?: boolean\n }[] = []\n\n @state() private overflown: boolean = false\n @query('[container]') container?: HTMLDivElement\n\n private overflownUpdaterThrottle: any\n private resizeCallback: any\n\n get overflownUpdater() {\n if (!this.overflownUpdaterThrottle) {\n this.overflownUpdaterThrottle = throttle(() => {\n this.overflown = !!this.container && this.container.scrollWidth > this.container.clientWidth\n }, 100)\n }\n\n return this.overflownUpdaterThrottle\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n this.resizeCallback = () => this.overflownUpdater()\n\n window.addEventListener('resize', this.resizeCallback)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n\n window.removeEventListener('resize', this.resizeCallback)\n }\n\n render() {\n return this.actions.length > 0\n ? html`\n <div container>${this.renderActions()}</div>\n <!--\n 넘친 동작들에 닿는 **유일한 길**이다. 그런데 이름이 없어 낭독기는 「more_horiz」를\n 읽었고, md-icon 이라 키보드 초점도 못 받았다 — 키보드만 쓰는 사람에게는 그 동작들이\n 아예 없는 것과 같았다.\n -->\n ${this.overflown\n ? html`\n <md-icon\n more\n role=\"button\"\n tabindex=\"0\"\n title=${i18next.t('text.more-actions', { defaultValue: 'more actions' })}\n @click=${this.showMore.bind(this)}\n @keydown=${(e: KeyboardEvent) => {\n if (e.key !== 'Enter' && e.key !== ' ') return\n e.preventDefault()\n this.showMore()\n }}\n >more_horiz</md-icon\n >\n `\n : html``}\n `\n : nothing\n }\n\n renderActions() {\n return html`\n ${this.actions.map(($action, idx) => {\n let { type, title, icon, action, select, href, emphasis, disabled, disabledTitle } = $action as any\n let { raised, danger } = emphasis || {}\n\n return type == 'text'\n ? html` <span @click=${action}>${title}</span> `\n : type == 'select' || (select && select.length > 0)\n ? html`\n <select @change=${action}>\n ${select.map((option: any) => html` <option>${option}</option> `)}\n </select>\n `\n : type == 'link'\n ? html` <a href=${href}>${title}</a> `\n : html`\n <button\n ?icon-only=${type == 'icon' || !title}\n ?raised=${raised}\n ?danger=${danger}\n ?disabled=${disabled}\n title=${disabled && disabledTitle ? disabledTitle : title || ''}\n @click=${async (e: MouseEvent) => {\n /* all actions should be bloked for a second */\n const button = e.currentTarget as any\n const iconElement = button.querySelector('md-icon')\n const actionConfig = this.actions[idx]\n\n if (iconElement) {\n icon = actionConfig.icon\n actionConfig.icon = 'refresh'\n this.requestUpdate()\n iconElement.setAttribute('working', true)\n }\n button.disabled = true\n\n try {\n await action()\n } finally {\n await sleep(1000) /* waiting for additionla 1 second */\n\n /* because icon can be changed after sleep, don't use closure 'icon'. */\n if (iconElement) {\n actionConfig.icon = icon\n this.requestUpdate()\n iconElement.removeAttribute('working')\n }\n button.disabled = false\n }\n }}\n >\n ${this.actions[idx].icon ? html`<md-icon>${this.actions[idx].icon}</md-icon>` : nothing}\n ${type == 'icon' || !title ? nothing : title}\n </button>\n `\n })}\n `\n }\n\n async updated(changed: PropertyValues<this>) {\n if (changed.has('actions')) {\n requestAnimationFrame(this.overflownUpdater)\n }\n }\n\n protected willUpdate() {\n const { actions, frame } = this._route.context || {}\n /* The bar has no box of its own to measure; the attribute is what the stylesheet reads. */\n if (frame) this.setAttribute('frame', String(frame))\n else this.removeAttribute('frame')\n\n if (actions) {\n this.actions = actions.filter((action: any) => !!action)\n }\n }\n\n showMore() {\n openOverlay('context-toolbar-overlay', {\n template: html`<ox-page-action-overlay-template></ox-page-action-overlay-template>`\n })\n }\n}\n"]}
@@ -111,10 +111,12 @@ PageTitleBar.styles = [
111
111
  * 맞추자 줄이 탭 바에 붙고, 알약은 원래부터 오른쪽 끝에 붙어 있었다 — 위 0 · 오른쪽 0.
112
112
  * [확인함] 2026-09-02
113
113
  *
114
- * 왼쪽은 두지 않는다. 제목이 자기 여백(spacing-small)으로 아래 표의 첫 칸과 같은 선에 서 있고,
115
- * 여기서 더하면 그 선이 어긋난다.
114
+ * 왼쪽은 화면의 여백(--ui-gutter)에 맞춘다 — 제목 글자가 본문 첫 글자와 같은 선에 선다.
115
+ * 제목 상자가 자기 여백(spacing-small)을 가지므로 그만큼 뺀다. 전에는 0 이어서 문서 화면에서
116
+ * 제목(x214)과 본문(x229)이 15px 어긋났다. [확인함 :4000 1440 창] 2026-10-05
116
117
  */
117
- padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-small) 0;
118
+ padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-small)
119
+ calc(var(--ui-gutter, 16px) - var(--spacing-small, 4px));
118
120
  box-sizing: border-box;
119
121
  text-transform: capitalize;
120
122
 
@@ -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 { customElement } from 'lit/decorators.js'\nimport { openHelp } from '@operato/help'\nimport { RouteController } from '@operato/shell'\nimport { isMobileDevice } from '@operato/utils'\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 * 왼쪽은 두지 않는다. 제목이 자기 여백(spacing-small)으로 아래 표의 첫 칸과 같은 선에 서 있고,\n * 여기서 더하면 그 선이 어긋난다.\n */\n padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-small) 0;\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(--title-text-color);\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\n private _route = new RouteController(this)\n\n mobile = isMobileDevice()\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 if (this.mobile && searchable && title) {\n placeholder ||= text\n title = ''\n }\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=${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":"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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 } from 'lit/decorators.js'\nimport { openHelp } from '@operato/help'\nimport { RouteController } from '@operato/shell'\nimport { isMobileDevice } from '@operato/utils'\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(--title-text-color);\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\n private _route = new RouteController(this)\n\n mobile = isMobileDevice()\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 if (this.mobile && searchable && title) {\n placeholder ||= text\n title = ''\n }\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=${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"]}
@@ -29,19 +29,23 @@ class WorkScreen extends LitElement {
29
29
  WorkScreen.styles = [WorkSurfacePageStyles];
30
30
  customElements.define('test-work-screen', WorkScreen);
31
31
  describe('page frames', () => {
32
- it('document: one column 1080px wide at most, 18/20/40 padding, sections 24px apart, the page scrolls', async () => {
32
+ /*
33
+ * 2026-10-05: the column fills the window between two equal gutters (user decision: fill the page,
34
+ * responsive). It used to stop at 1080px on the left and leave the right side empty.
35
+ */
36
+ it('document: the column fills the window, 16 top / 16 gutters / 40 bottom, sections 24px apart, the page scrolls', async () => {
33
37
  const host = await fixture(html `<div style="width: 1600px; height: 600px"><test-document-screen></test-document-screen></div>`);
34
38
  const screen = host.querySelector('test-document-screen');
35
39
  await screen.updateComplete;
36
40
  const page = screen.shadowRoot.querySelector('.page');
37
41
  const [first, second] = Array.from(screen.shadowRoot.querySelectorAll('.page-section'));
38
42
  const style = getComputedStyle(page);
39
- expect(Math.round(page.getBoundingClientRect().width)).to.equal(1080);
40
- expect([style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft]).to.deep.equal(['18px', '20px', '40px', '20px']);
43
+ expect(Math.round(page.getBoundingClientRect().width)).to.equal(1600);
44
+ expect([style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft]).to.deep.equal(['16px', '16px', '40px', '16px']);
41
45
  expect(Math.round(second.getBoundingClientRect().top - first.getBoundingClientRect().bottom)).to.equal(24);
42
46
  expect(getComputedStyle(screen).overflowY).to.equal('auto');
43
47
  const actions = screen.shadowRoot.querySelector('.page-section-actions');
44
- expect(Math.round(actions.getBoundingClientRect().right)).to.equal(Math.round(page.getBoundingClientRect().right) - 20);
48
+ expect(Math.round(actions.getBoundingClientRect().right)).to.equal(Math.round(page.getBoundingClientRect().right) - 16);
45
49
  });
46
50
  it('work surface: the content fills and scrolls inside itself, the page does not scroll', async () => {
47
51
  const host = await fixture(html `<div style="height: 400px"><test-work-screen></test-work-screen></div>`);
@@ -1 +1 @@
1
- {"version":3,"file":"page-frames.test.js","sourceRoot":"","sources":["../../test/page-frames.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, fixture } from '@open-wc/testing'\nimport { html, LitElement } from 'lit'\n\nimport { DocumentPageStyles, WorkSurfacePageStyles } from '@operato/styles'\n\n/**\n * Page frames (ADR-0060 decision 8): the frame owns spacing, width and scroll, so two screens of the\n * same kind come out the same. The numbers are written out, so a change to the frame shows up here as\n * a decision.\n */\n\nclass DocumentScreen extends LitElement {\n static styles = [DocumentPageStyles]\n render() {\n return html`\n <div class=\"page\">\n <section class=\"page-section\">\n <header class=\"page-section-header\"><h3>today</h3><div class=\"page-section-actions\"><button>refresh</button></div></header>\n <div class=\"page-card\">card</div>\n </section>\n <section class=\"page-section\"><header class=\"page-section-header\"><h3>later</h3></header></section>\n </div>\n `\n }\n}\ncustomElements.define('test-document-screen', DocumentScreen)\n\nclass WorkScreen extends LitElement {\n static styles = [WorkSurfacePageStyles]\n render() {\n return html`<div class=\"page-headroom\"><button>tab</button></div><div class=\"page-fill\" style=\"height: 5000px\"></div>`\n }\n}\ncustomElements.define('test-work-screen', WorkScreen)\n\ndescribe('page frames', () => {\n it('document: one column 1080px wide at most, 18/20/40 padding, sections 24px apart, the page scrolls', async () => {\n const host = await fixture<HTMLElement>(html`<div style=\"width: 1600px; height: 600px\"><test-document-screen></test-document-screen></div>`)\n const screen = host.querySelector('test-document-screen') as DocumentScreen\n await screen.updateComplete\n const page = screen.shadowRoot!.querySelector('.page') as HTMLElement\n const [first, second] = Array.from(screen.shadowRoot!.querySelectorAll('.page-section'))\n const style = getComputedStyle(page)\n\n expect(Math.round(page.getBoundingClientRect().width)).to.equal(1080)\n expect([style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft]).to.deep.equal(['18px', '20px', '40px', '20px'])\n expect(Math.round(second.getBoundingClientRect().top - first.getBoundingClientRect().bottom)).to.equal(24)\n expect(getComputedStyle(screen).overflowY).to.equal('auto')\n\n const actions = screen.shadowRoot!.querySelector('.page-section-actions') as HTMLElement\n expect(Math.round(actions.getBoundingClientRect().right)).to.equal(Math.round(page.getBoundingClientRect().right) - 20)\n })\n\n it('work surface: the content fills and scrolls inside itself, the page does not scroll', async () => {\n const host = await fixture<HTMLElement>(html`<div style=\"height: 400px\"><test-work-screen></test-work-screen></div>`)\n const screen = host.querySelector('test-work-screen') as WorkScreen\n await screen.updateComplete\n\n expect(getComputedStyle(screen).overflow).to.equal('hidden')\n expect(Math.round(screen.getBoundingClientRect().height)).to.equal(400)\n expect(getComputedStyle(screen.shadowRoot!.querySelector('.page-fill')!).minHeight).to.equal('0px')\n })\n})\n"]}
1
+ {"version":3,"file":"page-frames.test.js","sourceRoot":"","sources":["../../test/page-frames.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, fixture } from '@open-wc/testing'\nimport { html, LitElement } from 'lit'\n\nimport { DocumentPageStyles, WorkSurfacePageStyles } from '@operato/styles'\n\n/**\n * Page frames (ADR-0060 decision 8): the frame owns spacing, width and scroll, so two screens of the\n * same kind come out the same. The numbers are written out, so a change to the frame shows up here as\n * a decision.\n */\n\nclass DocumentScreen extends LitElement {\n static styles = [DocumentPageStyles]\n render() {\n return html`\n <div class=\"page\">\n <section class=\"page-section\">\n <header class=\"page-section-header\"><h3>today</h3><div class=\"page-section-actions\"><button>refresh</button></div></header>\n <div class=\"page-card\">card</div>\n </section>\n <section class=\"page-section\"><header class=\"page-section-header\"><h3>later</h3></header></section>\n </div>\n `\n }\n}\ncustomElements.define('test-document-screen', DocumentScreen)\n\nclass WorkScreen extends LitElement {\n static styles = [WorkSurfacePageStyles]\n render() {\n return html`<div class=\"page-headroom\"><button>tab</button></div><div class=\"page-fill\" style=\"height: 5000px\"></div>`\n }\n}\ncustomElements.define('test-work-screen', WorkScreen)\n\ndescribe('page frames', () => {\n /*\n * 2026-10-05: the column fills the window between two equal gutters (user decision: fill the page,\n * responsive). It used to stop at 1080px on the left and leave the right side empty.\n */\n it('document: the column fills the window, 16 top / 16 gutters / 40 bottom, sections 24px apart, the page scrolls', async () => {\n const host = await fixture<HTMLElement>(html`<div style=\"width: 1600px; height: 600px\"><test-document-screen></test-document-screen></div>`)\n const screen = host.querySelector('test-document-screen') as DocumentScreen\n await screen.updateComplete\n const page = screen.shadowRoot!.querySelector('.page') as HTMLElement\n const [first, second] = Array.from(screen.shadowRoot!.querySelectorAll('.page-section'))\n const style = getComputedStyle(page)\n\n expect(Math.round(page.getBoundingClientRect().width)).to.equal(1600)\n expect([style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft]).to.deep.equal(['16px', '16px', '40px', '16px'])\n expect(Math.round(second.getBoundingClientRect().top - first.getBoundingClientRect().bottom)).to.equal(24)\n expect(getComputedStyle(screen).overflowY).to.equal('auto')\n\n const actions = screen.shadowRoot!.querySelector('.page-section-actions') as HTMLElement\n expect(Math.round(actions.getBoundingClientRect().right)).to.equal(Math.round(page.getBoundingClientRect().right) - 16)\n })\n\n it('work surface: the content fills and scrolls inside itself, the page does not scroll', async () => {\n const host = await fixture<HTMLElement>(html`<div style=\"height: 400px\"><test-work-screen></test-work-screen></div>`)\n const screen = host.querySelector('test-work-screen') as WorkScreen\n await screen.updateComplete\n\n expect(getComputedStyle(screen).overflow).to.equal('hidden')\n expect(Math.round(screen.getBoundingClientRect().height)).to.equal(400)\n expect(getComputedStyle(screen.shadowRoot!.querySelector('.page-fill')!).minHeight).to.equal('0px')\n })\n})\n"]}
@@ -31,4 +31,21 @@ describe('ox-page-title-bar description', () => {
31
31
  expect(el.shadowRoot.querySelector('[description]') === null).to.equal(true);
32
32
  });
33
33
  });
34
+ describe('ox-page-title-bar gutter', () => {
35
+ /*
36
+ * The title's first letter stands on the same line as the page's first letter: the bar's left
37
+ * padding plus the title box's own 4px equals the page gutter. It used to be 0 + 4, and on a
38
+ * document screen the title (x214) and the content (x229) were 15px apart (2026-10-05, :4000).
39
+ */
40
+ it('puts the title text at the page gutter', async () => {
41
+ const host = await fixture(html `<div style="--ui-gutter: 16px; --spacing-small: 4px; --spacing-medium: 8px"><ox-page-title-bar></ox-page-title-bar></div>`);
42
+ store.dispatch({ type: 'UPDATE_CONTEXT', context: { title: 'maintenance' } });
43
+ const el = host.querySelector('ox-page-title-bar');
44
+ await el.updateComplete;
45
+ await el.updateComplete;
46
+ const titler = el.shadowRoot.querySelector('[titler]');
47
+ const offset = titler.getBoundingClientRect().left + parseFloat(getComputedStyle(titler).paddingLeft) - el.getBoundingClientRect().left;
48
+ expect(Math.round(offset)).to.equal(16);
49
+ });
50
+ });
34
51
  //# sourceMappingURL=page-title-description.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"page-title-description.test.js","sourceRoot":"","sources":["../../test/page-title-description.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;AAE1C;;;GAGG;AAEH,KAAK,UAAU,GAAG,CAAC,OAAgC;IACjD,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAS,CAAC,CAAA;IAC1D,MAAM,EAAE,GAAG,MAAM,OAAO,CAAqD,IAAI,CAAA,yCAAyC,CAAC,CAAA;IAC3H,MAAM,EAAE,CAAC,cAAc,CAAA;IACvB,MAAM,EAAE,CAAC,cAAc,CAAA;IACvB,OAAO,EAAE,CAAA;AACX,CAAC;AAED,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC7C,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC,CAAA;QACjG,MAAM,IAAI,GAAG,EAAE,CAAC,UAAW,CAAC,aAAa,CAAC,eAAe,CAAC,CAAA;QAE1D,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,WAAW,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAA;QACxE,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnD,MAAM,CAAC,IAAK,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,UAAW,CAAC,aAAa,CAAC,UAAU,CAAE,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC,CAAA;IACpI,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAA;QAE9C,MAAM,CAAC,EAAE,CAAC,UAAW,CAAC,aAAa,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC7E,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACtD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,wBAAwB,EAAE,CAAC,CAAA;QAE/F,MAAM,CAAC,EAAE,CAAC,UAAW,CAAC,aAAa,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC/E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","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'\n\n/**\n * `context.description` draws one line under the screen title (ADR-0060 decision 8). A screen that\n * does not give it must look exactly as before: no line, no wrapping.\n */\n\nasync function bar(context: Record<string, unknown>) {\n store.dispatch({ type: 'UPDATE_CONTEXT', context } as any)\n const el = await fixture<HTMLElement & { updateComplete: Promise<unknown> }>(html`<ox-page-title-bar></ox-page-title-bar>`)\n await el.updateComplete\n await el.updateComplete\n return el\n}\n\ndescribe('ox-page-title-bar description', () => {\n it('draws the description under the title', async () => {\n const el = await bar({ title: 'work orders', description: 'orders released to the floor today' })\n const line = el.shadowRoot!.querySelector('[description]')\n\n expect(line?.textContent).to.equal('orders released to the floor today')\n expect(el.hasAttribute('described')).to.equal(true)\n expect(line!.getBoundingClientRect().top).to.be.greaterThan(el.shadowRoot!.querySelector('[titler]')!.getBoundingClientRect().top)\n })\n\n it('★ draws nothing more when the screen gives no description', async () => {\n const el = await bar({ title: 'work orders' })\n\n expect(el.shadowRoot!.querySelector('[description]') === null).to.equal(true)\n expect(el.hasAttribute('described')).to.equal(false)\n })\n\n it('leaves the description out when the screen turns the title row off', async () => {\n const el = await bar({ title: 'learn', heading: false, description: 'what the platform does' })\n\n expect(el.shadowRoot!.querySelector('[description]') === null).to.equal(true)\n })\n})\n"]}
1
+ {"version":3,"file":"page-title-description.test.js","sourceRoot":"","sources":["../../test/page-title-description.test.ts"],"names":[],"mappings":"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","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'\n\n/**\n * `context.description` draws one line under the screen title (ADR-0060 decision 8). A screen that\n * does not give it must look exactly as before: no line, no wrapping.\n */\n\nasync function bar(context: Record<string, unknown>) {\n store.dispatch({ type: 'UPDATE_CONTEXT', context } as any)\n const el = await fixture<HTMLElement & { updateComplete: Promise<unknown> }>(html`<ox-page-title-bar></ox-page-title-bar>`)\n await el.updateComplete\n await el.updateComplete\n return el\n}\n\ndescribe('ox-page-title-bar description', () => {\n it('draws the description under the title', async () => {\n const el = await bar({ title: 'work orders', description: 'orders released to the floor today' })\n const line = el.shadowRoot!.querySelector('[description]')\n\n expect(line?.textContent).to.equal('orders released to the floor today')\n expect(el.hasAttribute('described')).to.equal(true)\n expect(line!.getBoundingClientRect().top).to.be.greaterThan(el.shadowRoot!.querySelector('[titler]')!.getBoundingClientRect().top)\n })\n\n it('★ draws nothing more when the screen gives no description', async () => {\n const el = await bar({ title: 'work orders' })\n\n expect(el.shadowRoot!.querySelector('[description]') === null).to.equal(true)\n expect(el.hasAttribute('described')).to.equal(false)\n })\n\n it('leaves the description out when the screen turns the title row off', async () => {\n const el = await bar({ title: 'learn', heading: false, description: 'what the platform does' })\n\n expect(el.shadowRoot!.querySelector('[description]') === null).to.equal(true)\n })\n})\n\ndescribe('ox-page-title-bar gutter', () => {\n /*\n * The title's first letter stands on the same line as the page's first letter: the bar's left\n * padding plus the title box's own 4px equals the page gutter. It used to be 0 + 4, and on a\n * document screen the title (x214) and the content (x229) were 15px apart (2026-10-05, :4000).\n */\n it('puts the title text at the page gutter', async () => {\n const host = await fixture<HTMLElement>(html`<div style=\"--ui-gutter: 16px; --spacing-small: 4px; --spacing-medium: 8px\"><ox-page-title-bar></ox-page-title-bar></div>`)\n store.dispatch({ type: 'UPDATE_CONTEXT', context: { title: 'maintenance' } } as any)\n const el = host.querySelector('ox-page-title-bar') as HTMLElement & { updateComplete: Promise<unknown> }\n await el.updateComplete\n await el.updateComplete\n const titler = el.shadowRoot!.querySelector('[titler]') as HTMLElement\n const offset = titler.getBoundingClientRect().left + parseFloat(getComputedStyle(titler).paddingLeft) - el.getBoundingClientRect().left\n\n expect(Math.round(offset)).to.equal(16)\n })\n})\n"]}