@operato/context 10.13.0 → 10.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +491 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +15543 -0
- package/dist/src/tools/ox-page-action-context-bar.js +2 -1
- package/dist/src/tools/ox-page-action-context-bar.js.map +1 -1
- package/dist/src/tools/ox-page-title-bar.js +22 -1
- package/dist/src/tools/ox-page-title-bar.js.map +1 -1
- package/dist/test/page-frames.test.d.ts +1 -0
- package/dist/test/page-frames.test.js +55 -0
- package/dist/test/page-frames.test.js.map +1 -0
- package/dist/test/page-title-description.test.d.ts +1 -0
- package/dist/test/page-title-description.test.js +34 -0
- package/dist/test/page-title-description.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/page-frames.test.ts +63 -0
- package/test/page-title-description.test.ts +43 -0
|
@@ -9,6 +9,7 @@ import { openOverlay } from '@operato/layout';
|
|
|
9
9
|
import { RouteController } from '@operato/shell';
|
|
10
10
|
import { sleep } from '@operato/utils';
|
|
11
11
|
import { ButtonContainerStyles } from '@operato/styles';
|
|
12
|
+
import { formElementHeight } from '@operato/styles/form-element-heights.js';
|
|
12
13
|
let PageActionContextBar = class PageActionContextBar extends LitElement {
|
|
13
14
|
constructor() {
|
|
14
15
|
super(...arguments);
|
|
@@ -66,7 +67,7 @@ let PageActionContextBar = class PageActionContextBar extends LitElement {
|
|
|
66
67
|
input[type='text'] {
|
|
67
68
|
box-sizing: border-box;
|
|
68
69
|
padding: 0 var(--spacing-small);
|
|
69
|
-
height:
|
|
70
|
+
height: ${formElementHeight('--form-element-height-medium')};
|
|
70
71
|
border: 1px solid var(--md-sys-color-outline);
|
|
71
72
|
border-radius: var(--md-sys-shape-corner-small);
|
|
72
73
|
color: var(--md-sys-color-on-primary);
|
|
@@ -1 +1 @@
|
|
|
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'\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 :host *:focus {\n outline: none;\n }\n\n [container] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n gap: var(--spacing-small);\n padding: 2px; /* only for button shadow */\n align-items: center;\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: var(--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 outline: none;\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 } = this._route.context || {}\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 :host *:focus {\n outline: none;\n }\n\n [container] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n gap: var(--spacing-small);\n padding: 2px; /* only for button shadow */\n align-items: center;\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 outline: none;\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 } = this._route.context || {}\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"]}
|
|
@@ -20,9 +20,10 @@ let PageTitleBar = class PageTitleBar extends LitElement {
|
|
|
20
20
|
*/
|
|
21
21
|
updated() {
|
|
22
22
|
this.toggleAttribute('blank', this.renderRoot.querySelector('div, md-icon') === null);
|
|
23
|
+
this.toggleAttribute('described', this.renderRoot.querySelector('[description]') !== null);
|
|
23
24
|
}
|
|
24
25
|
render() {
|
|
25
|
-
var { search, filter, title, help, heading } = this._route.context || {};
|
|
26
|
+
var { search, filter, title, help, heading, description } = this._route.context || {};
|
|
26
27
|
var { value = '', handler: searchHandler, placeholder = '', autofocus = true } = search || {};
|
|
27
28
|
const { handler: filterHandler } = filter || {};
|
|
28
29
|
const { icon, text = title } = typeof title === 'object' ? title : {};
|
|
@@ -56,6 +57,7 @@ let PageTitleBar = class PageTitleBar extends LitElement {
|
|
|
56
57
|
title = '';
|
|
57
58
|
}
|
|
58
59
|
return html `
|
|
60
|
+
${title && description ? html `<div description>${description}</div>` : html ``}
|
|
59
61
|
${title ? html ` <div titler>${icon ? html `<md-icon>${icon}</md-icon> ` : html ``}${text}</div> ` : html ``}
|
|
60
62
|
${help && title ? html ` <md-icon @click=${(e) => openHelp(help)} help>help</md-icon> ` : html ``}
|
|
61
63
|
${searchable || filterable
|
|
@@ -159,6 +161,25 @@ PageTitleBar.styles = [
|
|
|
159
161
|
align-self: center;
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
/*
|
|
165
|
+
* One line under the title that says what the screen is for (context.description). It takes
|
|
166
|
+
* the whole row, so the search pill stays on the title's line. Without the key nothing is
|
|
167
|
+
* drawn and the bar is exactly as before (ADR-0060 decision 8).
|
|
168
|
+
*/
|
|
169
|
+
:host([described]) {
|
|
170
|
+
flex-wrap: wrap;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[description] {
|
|
174
|
+
order: 10;
|
|
175
|
+
flex-basis: 100%;
|
|
176
|
+
padding: 0 var(--spacing-small) var(--spacing-small);
|
|
177
|
+
font-size: var(--ui-t-body, 13px);
|
|
178
|
+
line-height: 1.45;
|
|
179
|
+
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
180
|
+
text-transform: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
162
183
|
[help] {
|
|
163
184
|
opacity: var(--help-icon-opacity);
|
|
164
185
|
font-size: var(--help-icon-size);
|
|
@@ -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 [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 }\n\n render() {\n var { search, filter, title, help, heading } = 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 ||= title\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 ? html` <div titler>${icon ? html`<md-icon>${icon}</md-icon> ` : html``}${text}</div> ` : html``}\n ${help && title ? html` <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 * 왼쪽은 두지 않는다. 제목이 자기 여백(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 ||= title\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> ` : html``}${text}</div> ` : html``}\n ${help && title ? html` <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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { expect, fixture } from '@open-wc/testing';
|
|
2
|
+
import { html, LitElement } from 'lit';
|
|
3
|
+
import { DocumentPageStyles, WorkSurfacePageStyles } from '@operato/styles';
|
|
4
|
+
/**
|
|
5
|
+
* Page frames (ADR-0060 decision 8): the frame owns spacing, width and scroll, so two screens of the
|
|
6
|
+
* same kind come out the same. The numbers are written out, so a change to the frame shows up here as
|
|
7
|
+
* a decision.
|
|
8
|
+
*/
|
|
9
|
+
class DocumentScreen extends LitElement {
|
|
10
|
+
render() {
|
|
11
|
+
return html `
|
|
12
|
+
<div class="page">
|
|
13
|
+
<section class="page-section">
|
|
14
|
+
<header class="page-section-header"><h3>today</h3><div class="page-section-actions"><button>refresh</button></div></header>
|
|
15
|
+
<div class="page-card">card</div>
|
|
16
|
+
</section>
|
|
17
|
+
<section class="page-section"><header class="page-section-header"><h3>later</h3></header></section>
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
DocumentScreen.styles = [DocumentPageStyles];
|
|
23
|
+
customElements.define('test-document-screen', DocumentScreen);
|
|
24
|
+
class WorkScreen extends LitElement {
|
|
25
|
+
render() {
|
|
26
|
+
return html `<div class="page-headroom"><button>tab</button></div><div class="page-fill" style="height: 5000px"></div>`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
WorkScreen.styles = [WorkSurfacePageStyles];
|
|
30
|
+
customElements.define('test-work-screen', WorkScreen);
|
|
31
|
+
describe('page frames', () => {
|
|
32
|
+
it('document: one column 1080px wide at most, 18/20/40 padding, sections 24px apart, the page scrolls', async () => {
|
|
33
|
+
const host = await fixture(html `<div style="width: 1600px; height: 600px"><test-document-screen></test-document-screen></div>`);
|
|
34
|
+
const screen = host.querySelector('test-document-screen');
|
|
35
|
+
await screen.updateComplete;
|
|
36
|
+
const page = screen.shadowRoot.querySelector('.page');
|
|
37
|
+
const [first, second] = Array.from(screen.shadowRoot.querySelectorAll('.page-section'));
|
|
38
|
+
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']);
|
|
41
|
+
expect(Math.round(second.getBoundingClientRect().top - first.getBoundingClientRect().bottom)).to.equal(24);
|
|
42
|
+
expect(getComputedStyle(screen).overflowY).to.equal('auto');
|
|
43
|
+
const actions = screen.shadowRoot.querySelector('.page-section-actions');
|
|
44
|
+
expect(Math.round(actions.getBoundingClientRect().right)).to.equal(Math.round(page.getBoundingClientRect().right) - 20);
|
|
45
|
+
});
|
|
46
|
+
it('work surface: the content fills and scrolls inside itself, the page does not scroll', async () => {
|
|
47
|
+
const host = await fixture(html `<div style="height: 400px"><test-work-screen></test-work-screen></div>`);
|
|
48
|
+
const screen = host.querySelector('test-work-screen');
|
|
49
|
+
await screen.updateComplete;
|
|
50
|
+
expect(getComputedStyle(screen).overflow).to.equal('hidden');
|
|
51
|
+
expect(Math.round(screen.getBoundingClientRect().height)).to.equal(400);
|
|
52
|
+
expect(getComputedStyle(screen.shadowRoot.querySelector('.page-fill')).minHeight).to.equal('0px');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=page-frames.test.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../src/tools/ox-page-title-bar.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* `context.description` draws one line under the screen title (ADR-0060 decision 8). A screen that
|
|
7
|
+
* does not give it must look exactly as before: no line, no wrapping.
|
|
8
|
+
*/
|
|
9
|
+
async function bar(context) {
|
|
10
|
+
store.dispatch({ type: 'UPDATE_CONTEXT', context });
|
|
11
|
+
const el = await fixture(html `<ox-page-title-bar></ox-page-title-bar>`);
|
|
12
|
+
await el.updateComplete;
|
|
13
|
+
await el.updateComplete;
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
describe('ox-page-title-bar description', () => {
|
|
17
|
+
it('draws the description under the title', async () => {
|
|
18
|
+
const el = await bar({ title: 'work orders', description: 'orders released to the floor today' });
|
|
19
|
+
const line = el.shadowRoot.querySelector('[description]');
|
|
20
|
+
expect(line === null || line === void 0 ? void 0 : line.textContent).to.equal('orders released to the floor today');
|
|
21
|
+
expect(el.hasAttribute('described')).to.equal(true);
|
|
22
|
+
expect(line.getBoundingClientRect().top).to.be.greaterThan(el.shadowRoot.querySelector('[titler]').getBoundingClientRect().top);
|
|
23
|
+
});
|
|
24
|
+
it('★ draws nothing more when the screen gives no description', async () => {
|
|
25
|
+
const el = await bar({ title: 'work orders' });
|
|
26
|
+
expect(el.shadowRoot.querySelector('[description]') === null).to.equal(true);
|
|
27
|
+
expect(el.hasAttribute('described')).to.equal(false);
|
|
28
|
+
});
|
|
29
|
+
it('leaves the description out when the screen turns the title row off', async () => {
|
|
30
|
+
const el = await bar({ title: 'learn', heading: false, description: 'what the platform does' });
|
|
31
|
+
expect(el.shadowRoot.querySelector('[description]') === null).to.equal(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=page-title-description.test.js.map
|
|
@@ -0,0 +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"]}
|