@operato/layout 10.29.30 → 10.29.36

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.
@@ -483,6 +483,19 @@ SnackBar.styles = css `
483
483
  --md-text-button-label-text-size: var(--md-sys-typescale-label-large-size, 0.875rem);
484
484
  }
485
485
 
486
+ /*
487
+ * Material's text button turns its label and state layer to the app's primary on hover, focus and press. On this
488
+ * surface, dark whatever the theme, an app whose primary is dark loses the label the moment the pointer is on it.
489
+ * Each button keeps its own label color in every state; set here, on the button, so a quiet one keeps its own.
490
+ */
491
+ [actions] md-text-button {
492
+ --md-text-button-hover-label-text-color: var(--md-text-button-label-text-color);
493
+ --md-text-button-focus-label-text-color: var(--md-text-button-label-text-color);
494
+ --md-text-button-pressed-label-text-color: var(--md-text-button-label-text-color);
495
+ --md-text-button-hover-state-layer-color: var(--ox-snack-bar-color);
496
+ --md-text-button-pressed-state-layer-color: var(--ox-snack-bar-color);
497
+ }
498
+
486
499
  /*
487
500
  * A button to press only after thinking (emphasis 'quiet'): not in the level's accent, so the sentence above
488
501
  * it is what the eye lands on. Still a button - it reads as one and is reached by keyboard like the others.
@@ -1 +1 @@
1
- {"version":3,"file":"ox-snack-bar.js","sourceRoot":"","sources":["../../../src/layouts/ox-snack-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/text-button.js'\nimport '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { SnackbarController } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { resolveNotifyAction, resolveNotifyAssistant } from '../actions/snackbar.js'\n\n/** 복사 단추가 방금 무엇을 했는지. 잠깐 보여주고 원래 모습으로 돌아간다. */\ntype CopyState = 'idle' | 'done' | 'failed'\n\n@customElement('ox-snack-bar')\nclass SnackBar extends LitElement {\n static styles = css`\n /* 기술 원문 상자의 스크롤막대 — 앱 공통 모양을 그대로 쓴다 */\n ${ScrollbarStyles}\n\n :host {\n /*\n * 토스트는 화면 위에 잠깐 떠서 주의를 끄는 요소이므로 **테마와 무관하게 어두운 표면**을 유지한다\n * (Material 스낵바 관례).\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 **다크에서 밝게 반전**되어 눈에 튀었다.\n * → 반투명 어두운 값을 기본값으로 두면 어떤 브랜드 팔레트에서도 안정적이고, .dark 패치도 불필요하다.\n *\n * 앱이 필요하면 아래 var 로 조정한다.\n */\n /*\n * 글자색은 한 기준(--ox-snack-bar-color)에서 단계를 파생시킨다. 임의의 rgba 를 층마다\n * 따로 적으면 앱이 색을 바꿀 때 한 층만 바뀌어 위계가 깨진다.\n * 1층 기준색 · 2층 72% · 3층 60%\n */\n --ox-snack-bar-background: rgba(28, 28, 30, 0.96);\n --ox-snack-bar-color: rgba(255, 255, 255, 0.95);\n --ox-snack-bar-supporting-color: color-mix(in srgb, var(--ox-snack-bar-color) 72%, transparent);\n --ox-snack-bar-technical-color: color-mix(in srgb, var(--ox-snack-bar-color) 60%, transparent);\n --ox-snack-bar-radius: var(--md-sys-shape-corner-medium, 8px);\n --ox-snack-bar-padding: 12px 14px;\n /*\n * 방향이 있는 그림자여야 떠 있는 느낌이 난다. 이전 값(0 0 10px)은 사방으로 퍼져\n * 표면이 화면에 붙어 있는 것처럼 보였다. Material elevation 3 에 준한다.\n */\n --ox-snack-bar-shadow: 0 6px 16px rgba(0, 0, 0, 0.32), 0 2px 6px rgba(0, 0, 0, 0.24);\n /*\n * 레벨 강조색은 **아이콘에만** 쓴다. 배경은 네 레벨 공통(어두운 표면)으로 두었다 —\n * 토스트는 어느 화면 위에도 뜨므로 배경이 레벨마다 흔들리면 안 되고, 띠·패턴 같은 장식을\n * 더하면 짧은 알림에 비해 번잡해진다. 성격은 아이콘 하나로 충분히 전달된다.\n */\n --ox-snack-bar-accent: var(--status-info-color, #398ace);\n\n display: flex;\n align-items: flex-start;\n gap: 10px;\n position: fixed;\n top: 99%;\n left: 0;\n right: 0;\n overflow: hidden;\n border-radius: var(--ox-snack-bar-radius);\n padding: var(--ox-snack-bar-padding);\n color: var(--ox-snack-bar-color);\n background-color: var(--ox-snack-bar-background);\n box-shadow: var(--ox-snack-bar-shadow);\n /* 왼쪽 정렬 — 이전에는 center 라 두 줄 이상이 되면 줄마다 시작점이 달라 읽기가 나빴다 */\n text-align: left;\n /* 문장 단위로 균형있게 끊는다(지원 브라우저에서만 적용, 그 외 무시) */\n text-wrap: pretty;\n touch-action: pan-y;\n will-change: transform, opacity;\n transform: translate3d(0, 0, 0);\n transition-property: visibility, transform, opacity;\n transition-duration: 0.2s;\n visibility: hidden;\n }\n\n :host([active]) {\n visibility: visible;\n transform: translate3d(var(--snackbar-drag-x, 0), -100%, 0);\n opacity: var(--snackbar-opacity, 1);\n }\n\n /* 스와이프 중에는 손가락을 즉시 따라오도록 transition 제거 */\n :host([dragging]) {\n transition: none;\n }\n\n md-icon {\n flex: none;\n --md-icon-size: 20px;\n /*\n * 아이콘을 headline 첫 줄의 **행 상자 높이**에 맞춘다. 이전에는 margin-top 을 눈대중으로\n * 1px 준 것이라 폰트가 바뀌면 어긋났다. 행간과 같은 높이를 주고 그 안에서 가운데 두면\n * 타입스케일이 달라져도 함께 움직인다.\n */\n height: var(--md-sys-typescale-title-small-line-height, 1.25rem);\n display: flex;\n align-items: center;\n color: var(--ox-snack-bar-accent);\n }\n\n md-icon[close] {\n cursor: pointer;\n color: var(--ox-snack-bar-supporting-color);\n --md-icon-size: 18px;\n }\n md-icon[close]:hover {\n color: var(--ox-snack-bar-color);\n }\n\n [body] {\n flex: 1;\n min-width: 0;\n }\n\n /*\n * 세 층의 크기·굵기는 임의 px 이 아니라 테마의 타입스케일에서 가져온다.\n * 앱 폰트가 바뀌어도 위계가 유지되고, 다른 화면의 본문과 리듬이 맞는다.\n *\n * 1층 title-small (0.875rem / 1.25rem, 500) — 무슨 일이 일어났는가\n * 2층 body-small (0.75rem / 1rem, 400) — 왜·어디서\n * 3층 label-small (0.6875rem) — 기술 원문\n */\n [headline] {\n font-family: var(--md-sys-typescale-title-small-font, var(--theme-font, inherit));\n font-size: var(--md-sys-typescale-title-small-size, 0.875rem);\n line-height: var(--md-sys-typescale-title-small-line-height, 1.25rem);\n font-weight: var(--md-sys-typescale-title-small-weight, 500);\n /*\n * 단어 중간을 끊지 않는다. anywhere 는 필요 없는 곳에서도 글자를 쪼개 읽기가 나빠진다 —\n * 긴 토큰(URL·ID)만 끊기게 break-word 를 쓴다. 기술 원문에서만 anywhere 를 쓴다.\n */\n overflow-wrap: break-word;\n }\n\n [supporting] {\n margin-top: 2px;\n font-family: var(--md-sys-typescale-body-small-font, var(--theme-font, inherit));\n font-size: var(--md-sys-typescale-body-small-size, 0.75rem);\n line-height: var(--md-sys-typescale-body-small-line-height, 1rem);\n font-weight: var(--md-sys-typescale-body-small-weight, 400);\n color: var(--ox-snack-bar-supporting-color);\n overflow-wrap: break-word;\n /* 길어도 토스트가 화면을 삼키지 않게 — 3줄에서 자른다 */\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n }\n\n /* 3층 — 기술 원문. 접힌 상태가 기본이고 필요한 사람만 펼친다 */\n [technical] {\n margin-top: 6px;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: var(--md-sys-typescale-label-small-line-height, 1rem);\n }\n [technical] summary {\n display: flex;\n align-items: center;\n gap: 4px;\n cursor: pointer;\n color: var(--ox-snack-bar-supporting-color);\n list-style: none;\n }\n [technical] summary::-webkit-details-marker {\n display: none;\n }\n [technical] summary::before {\n content: '▸';\n display: inline-block;\n margin-right: 4px;\n transition: transform 0.15s;\n }\n [technical][open] summary::before {\n transform: rotate(90deg);\n }\n [technical] summary:hover {\n color: var(--ox-snack-bar-color);\n }\n /*\n * 원문 전체를 클립보드로. 상자가 접혀 있어도, 스크롤을 끝까지 내리지 않아도 **보이는 만큼이\n * 아니라 전부** 복사된다 — 오류를 옮겨 붙이는 사람이 실제로 원하는 것은 언제나 전문이다.\n * 그래서 요약줄에 둔다.\n */\n [technical] button[copy] {\n margin-inline-start: auto;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n border: none;\n border-radius: 4px;\n padding: 2px 5px;\n background-color: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: 1;\n cursor: pointer;\n }\n [technical] button[copy]:hover {\n color: var(--ox-snack-bar-color);\n background-color: color-mix(in srgb, var(--ox-snack-bar-color) 12%, transparent);\n }\n [technical] button[copy] md-icon {\n --md-icon-size: 14px;\n height: 14px;\n color: inherit;\n }\n [technical] pre {\n /*\n * 스크롤막대 색은 토스트 표면에서 파생시킨다. 공통 스타일의 기본값(--md-sys-color-outline)은\n * **현재 테마**의 색이라, 테마와 무관하게 어두운 이 표면 위에서는 밝기가 어긋난다.\n */\n --scrollbar-thumb-color: color-mix(in srgb, var(--ox-snack-bar-color) 28%, transparent);\n --scrollbar-thumb-hover-color: color-mix(in srgb, var(--ox-snack-bar-color) 48%, transparent);\n --scrollbar-thumb-border-radius: 3px;\n\n margin: 6px 0 0;\n padding: 8px;\n max-height: 180px;\n overflow: auto;\n border-radius: 4px;\n background-color: color-mix(in srgb, var(--ox-snack-bar-color) 8%, transparent);\n color: var(--ox-snack-bar-technical-color);\n font-family: 'SF Mono', Consolas, monospace;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: 1.5;\n white-space: pre-wrap;\n /* 원문은 긴 토큰이 흔하므로 여기서는 어디서든 끊는 것이 맞다 */\n overflow-wrap: anywhere;\n }\n\n [actions] {\n display: flex;\n gap: 4px;\n margin: 6px 0 0 -8px;\n --md-text-button-label-text-color: var(--ox-snack-bar-accent);\n --md-text-button-label-text-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n\n /*\n * A button to press only after thinking (emphasis 'quiet'): not in the level's accent, so the sentence above\n * it is what the eye lands on. Still a button - it reads as one and is reached by keyboard like the others.\n */\n [actions] md-text-button[quiet] {\n --md-text-button-label-text-color: var(--ox-snack-bar-supporting-color);\n --md-text-button-label-text-weight: 400;\n }\n\n /*\n * 레벨별 강조색 — 프레임워크 상태 색을 그대로 재사용한다.\n * 레벨은 호스트 속성으로 반영한다(:has() 에 의존하지 않는다).\n */\n :host([level='success']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-success-color, var(--status-success-color, #35a24a));\n }\n :host([level='info']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-info-color, var(--status-info-color, #398ace));\n }\n :host([level='warn']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-warn-color, var(--status-warning-color, #ee8d03));\n }\n :host([level='error']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-error-color, var(--status-danger-color, #d14946));\n }\n\n @media (min-width: 460px) {\n :host {\n /* 80% 는 큰 화면에서 한 줄이 지나치게 길어져 읽기 리듬이 깨진다 */\n min-width: 320px;\n max-width: 480px;\n margin: auto;\n width: fit-content;\n }\n }\n `\n\n private _snackbar = new SnackbarController(this)\n\n @property({ type: Boolean, reflect: true }) active: boolean = false\n /* 레벨을 호스트 속성으로 반영해 CSS 가 강조색을 고르게 한다 */\n @property({ type: String, reflect: true }) level: string = 'info'\n @property({ type: Object }) action: any\n\n // 스와이프 시작 X 좌표 (드래그 중이 아니면 null)\n private dragStartX: number | null = null\n private dragX: number = 0\n // 이 거리(px) 이상 스와이프하면 닫힘 처리\n private static readonly DISMISS_THRESHOLD = 80\n\n connectedCallback() {\n super.connectedCallback()\n // Pointer Events가 터치/마우스를 모두 커버 (스크롤 구분은 touch-action: pan-y가 담당)\n this.addEventListener('pointerdown', this.onDragStart)\n this.addEventListener('pointermove', this.onDragMove)\n this.addEventListener('pointerup', this.onDragEnd)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n /* 다시 붙을 때 같은 핸들러가 두 벌 걸리지 않게 */\n this.removeEventListener('pointerdown', this.onDragStart)\n this.removeEventListener('pointermove', this.onDragMove)\n this.removeEventListener('pointerup', this.onDragEnd)\n clearTimeout(this.copyTimer)\n }\n\n // CSS 변수로 스와이프 위치/투명도 반영\n private applyDrag(x: number, opacity: number = 1) {\n this.style.setProperty('--snackbar-drag-x', `${x}px`)\n this.style.setProperty('--snackbar-opacity', `${opacity}`)\n }\n\n private onDragStart = (e: PointerEvent) => {\n if (!this.active) return\n this.dragStartX = e.clientX\n this.dragX = 0\n this.setAttribute('dragging', '')\n }\n\n private onDragMove = (e: PointerEvent) => {\n if (this.dragStartX === null) return\n this.dragX = e.clientX - this.dragStartX\n // 스와이프 거리에 따라 점점 투명하게\n const opacity = Math.max(0, 1 - Math.abs(this.dragX) / (SnackBar.DISMISS_THRESHOLD * 2.5))\n this.applyDrag(this.dragX, opacity)\n }\n\n private onDragEnd = () => {\n if (this.dragStartX === null) return\n const moved = Math.abs(this.dragX)\n this.dragStartX = null\n this.removeAttribute('dragging')\n\n if (moved >= SnackBar.DISMISS_THRESHOLD) {\n // 스와이프한 방향으로 밀어내며 닫기\n this.applyDrag(this.dragX > 0 ? window.innerWidth : -window.innerWidth, 0)\n window.setTimeout(() => {\n // transition을 끈 채로 닫아서 아래로 떨어지는 기본 닫기 애니메이션을 숨김\n this.setAttribute('dragging', '')\n this._snackbar.close()\n requestAnimationFrame(() => {\n this.applyDrag(0)\n this.removeAttribute('dragging')\n })\n }, 200)\n } else {\n // 임계값 미달이면 원위치로 복귀\n this.applyDrag(0)\n }\n }\n\n /** 레벨별 아이콘. 성격을 한눈에 알리는 첫 신호. */\n private static readonly ICONS: Record<string, string> = {\n success: 'check_circle',\n info: 'info',\n warn: 'warning',\n error: 'error'\n }\n\n /** 복사 단추가 방금 한 일. 잠깐 보였다가 스스로 지워진다. */\n @state() private copyState: CopyState = 'idle'\n private copyTimer?: number\n /** 어느 원문을 두고 위 상태가 남았는지 — 다른 알림이 뜨면 그 상태는 남의 것이다. */\n private copiedFrom?: string\n\n private async copyAll(event: Event, text: string) {\n /* 요약줄 안의 단추다. 복사하려다 상자가 접히거나 펼쳐지면 안 된다 */\n event.preventDefault()\n event.stopPropagation()\n\n /* currentTarget 은 처리가 끝나면 비므로 여기서 잡아 둔다 */\n const button = event.currentTarget as HTMLElement | null\n\n try {\n await navigator.clipboard.writeText(text)\n this.copyState = 'done'\n } catch {\n /* 안 됐으면 안 됐다고 보인다 — 조용히 넘어가면 붙여넣기 전까지 모른다 */\n this.copyState = 'failed'\n }\n\n this.copiedFrom = text\n\n /*\n * 몇 번이든 다시 복사할 수 있다. 그런데 「복사했습니다」가 아직 떠 있는 동안 또 누르면 글자가\n * 그대로라 아무 일도 안 일어난 것처럼 보인다 — 그래서 누를 때마다 단추를 한 번 깜박인다.\n * 상태로 처리하면 두 번째 누름이 같은 값이라 다시 그려지지 않는다.\n */\n button?.animate?.([{ opacity: 0.35 }, { opacity: 1 }], { duration: 220 })\n\n clearTimeout(this.copyTimer)\n this.copyTimer = window.setTimeout(() => {\n this.copyState = 'idle'\n }, 2000)\n }\n\n /** 이 원문에 대한 상태만 인정한다 — 앞 알림에서 남은 「복사했습니다」를 물려받지 않는다. */\n private copyStateFor(text: string): CopyState {\n return this.copiedFrom === text ? this.copyState : 'idle'\n }\n\n private copyIcon(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return 'check'\n case 'failed':\n return 'error'\n default:\n return 'content_copy'\n }\n }\n\n private copyLabel(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return i18next.t('text.copied')\n case 'failed':\n return i18next.t('text.copy failed')\n default:\n return i18next.t('text.copy all')\n }\n }\n\n /** 기술 원문을 사람이 읽을 수 있는 문자열로. Error 는 message+stack, 그 외는 JSON. */\n private formatEx(ex: any): string {\n if (!ex) return ''\n if (typeof ex === 'string') return ex\n if (ex instanceof Error) return ex.stack || `${ex.name}: ${ex.message}`\n try {\n return JSON.stringify(ex, null, 2)\n } catch {\n return String(ex)\n }\n }\n\n render() {\n const { level, message, detail, ex, action } = this._snackbar\n const tone = level || 'info'\n const exText = this.formatEx(ex)\n /*\n * 주입된 도움 함수. AI 가 없는 앱이거나 설정이 죽어 있으면 null 이고, 그때는 버튼을 그리지\n * 않는다. 선택해도 아무 일이 없는 버튼을 보여주지 않는다.\n */\n const assistant = resolveNotifyAssistant()\n /*\n * 알림의 뜻을 아는 패키지가 주입한 단추 하나(registerNotifyAction). 알림이 자기 action 을 가지면\n * 그것이 이긴다 — 보낸 쪽이 정한 단추를 해석이 덮지 않는다.\n */\n const resolved = action?.label ? null : resolveNotifyAction({ level: tone as any, message, detail, ex })\n const button = action?.label ? action : resolved\n\n return html`\n <md-icon class=${tone} aria-hidden=\"true\">${SnackBar.ICONS[tone] ?? 'info'}</md-icon>\n\n <div body>\n <!-- 1층: 무슨 일이 일어났는가 -->\n <div headline>${message}</div>\n\n <!-- 2층: 왜·어디서. 낮은 강조로 구분한다 -->\n ${detail ? html`<div supporting>${detail}</div>` : nothing}\n\n <!-- 3층: 기술 원문. 접힌 채 두고 필요한 사람만 펼친다 -->\n ${exText\n ? html`\n <details technical>\n <summary>\n ${i18next.t('text.technical detail')}\n <button\n copy\n type=\"button\"\n title=${this.copyLabel(exText)}\n @click=${(event: Event) => this.copyAll(event, exText)}\n >\n <md-icon aria-hidden=\"true\">${this.copyIcon(exText)}</md-icon>\n ${this.copyLabel(exText)}\n </button>\n </summary>\n <pre>${exText}</pre>\n </details>\n `\n : nothing}\n\n ${button?.label || (assistant && (tone === 'error' || tone === 'warn'))\n ? html`\n <div actions>\n ${button?.label\n ? html`\n <md-text-button\n ?quiet=${(button as any).emphasis === 'quiet'}\n @click=${() => {\n this._snackbar.close()\n button.callback?.()\n }}\n >${button.label}</md-text-button\n >\n `\n : nothing}\n <!--\n 오류·경고에서 곧바로 AI 도움으로 넘어간다. 핸들러가 등록된 앱에서만 나타나므로\n 선택해도 아무 일이 없는 버튼이 생기지 않는다.\n -->\n ${assistant && (tone === 'error' || tone === 'warn')\n ? html`\n <md-text-button\n @click=${() => {\n assistant({ level: tone as any, message, detail, ex })\n this._snackbar.close()\n }}\n >\n <md-icon slot=\"icon\">auto_awesome</md-icon>\n ${i18next.t('text.ask ai')}\n </md-text-button>\n `\n : nothing}\n </div>\n `\n : nothing}\n </div>\n\n <md-icon\n close\n role=\"button\"\n tabindex=\"0\"\n title=${i18next.t('button.close', { defaultValue: 'close' })}\n @click=${() => this._snackbar.close()}\n >close</md-icon\n >\n `\n }\n\n stateChanged(state: any) {\n var { snackbarOpened } = state.snackbar || {}\n\n // 새로 열릴 때 이전 스와이프 잔상 초기화\n if (snackbarOpened && !this.active) {\n this.dragStartX = null\n this.dragX = 0\n this.removeAttribute('dragging')\n this.applyDrag(0)\n }\n }\n\n protected willUpdate() {\n this.active = this._snackbar.opened\n this.level = this._snackbar.level || 'info'\n }\n}\n"]}
1
+ {"version":3,"file":"ox-snack-bar.js","sourceRoot":"","sources":["../../../src/layouts/ox-snack-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/text-button.js'\nimport '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { SnackbarController } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { resolveNotifyAction, resolveNotifyAssistant } from '../actions/snackbar.js'\n\n/** 복사 단추가 방금 무엇을 했는지. 잠깐 보여주고 원래 모습으로 돌아간다. */\ntype CopyState = 'idle' | 'done' | 'failed'\n\n@customElement('ox-snack-bar')\nclass SnackBar extends LitElement {\n static styles = css`\n /* 기술 원문 상자의 스크롤막대 — 앱 공통 모양을 그대로 쓴다 */\n ${ScrollbarStyles}\n\n :host {\n /*\n * 토스트는 화면 위에 잠깐 떠서 주의를 끄는 요소이므로 **테마와 무관하게 어두운 표면**을 유지한다\n * (Material 스낵바 관례).\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 **다크에서 밝게 반전**되어 눈에 튀었다.\n * → 반투명 어두운 값을 기본값으로 두면 어떤 브랜드 팔레트에서도 안정적이고, .dark 패치도 불필요하다.\n *\n * 앱이 필요하면 아래 var 로 조정한다.\n */\n /*\n * 글자색은 한 기준(--ox-snack-bar-color)에서 단계를 파생시킨다. 임의의 rgba 를 층마다\n * 따로 적으면 앱이 색을 바꿀 때 한 층만 바뀌어 위계가 깨진다.\n * 1층 기준색 · 2층 72% · 3층 60%\n */\n --ox-snack-bar-background: rgba(28, 28, 30, 0.96);\n --ox-snack-bar-color: rgba(255, 255, 255, 0.95);\n --ox-snack-bar-supporting-color: color-mix(in srgb, var(--ox-snack-bar-color) 72%, transparent);\n --ox-snack-bar-technical-color: color-mix(in srgb, var(--ox-snack-bar-color) 60%, transparent);\n --ox-snack-bar-radius: var(--md-sys-shape-corner-medium, 8px);\n --ox-snack-bar-padding: 12px 14px;\n /*\n * 방향이 있는 그림자여야 떠 있는 느낌이 난다. 이전 값(0 0 10px)은 사방으로 퍼져\n * 표면이 화면에 붙어 있는 것처럼 보였다. Material elevation 3 에 준한다.\n */\n --ox-snack-bar-shadow: 0 6px 16px rgba(0, 0, 0, 0.32), 0 2px 6px rgba(0, 0, 0, 0.24);\n /*\n * 레벨 강조색은 **아이콘에만** 쓴다. 배경은 네 레벨 공통(어두운 표면)으로 두었다 —\n * 토스트는 어느 화면 위에도 뜨므로 배경이 레벨마다 흔들리면 안 되고, 띠·패턴 같은 장식을\n * 더하면 짧은 알림에 비해 번잡해진다. 성격은 아이콘 하나로 충분히 전달된다.\n */\n --ox-snack-bar-accent: var(--status-info-color, #398ace);\n\n display: flex;\n align-items: flex-start;\n gap: 10px;\n position: fixed;\n top: 99%;\n left: 0;\n right: 0;\n overflow: hidden;\n border-radius: var(--ox-snack-bar-radius);\n padding: var(--ox-snack-bar-padding);\n color: var(--ox-snack-bar-color);\n background-color: var(--ox-snack-bar-background);\n box-shadow: var(--ox-snack-bar-shadow);\n /* 왼쪽 정렬 — 이전에는 center 라 두 줄 이상이 되면 줄마다 시작점이 달라 읽기가 나빴다 */\n text-align: left;\n /* 문장 단위로 균형있게 끊는다(지원 브라우저에서만 적용, 그 외 무시) */\n text-wrap: pretty;\n touch-action: pan-y;\n will-change: transform, opacity;\n transform: translate3d(0, 0, 0);\n transition-property: visibility, transform, opacity;\n transition-duration: 0.2s;\n visibility: hidden;\n }\n\n :host([active]) {\n visibility: visible;\n transform: translate3d(var(--snackbar-drag-x, 0), -100%, 0);\n opacity: var(--snackbar-opacity, 1);\n }\n\n /* 스와이프 중에는 손가락을 즉시 따라오도록 transition 제거 */\n :host([dragging]) {\n transition: none;\n }\n\n md-icon {\n flex: none;\n --md-icon-size: 20px;\n /*\n * 아이콘을 headline 첫 줄의 **행 상자 높이**에 맞춘다. 이전에는 margin-top 을 눈대중으로\n * 1px 준 것이라 폰트가 바뀌면 어긋났다. 행간과 같은 높이를 주고 그 안에서 가운데 두면\n * 타입스케일이 달라져도 함께 움직인다.\n */\n height: var(--md-sys-typescale-title-small-line-height, 1.25rem);\n display: flex;\n align-items: center;\n color: var(--ox-snack-bar-accent);\n }\n\n md-icon[close] {\n cursor: pointer;\n color: var(--ox-snack-bar-supporting-color);\n --md-icon-size: 18px;\n }\n md-icon[close]:hover {\n color: var(--ox-snack-bar-color);\n }\n\n [body] {\n flex: 1;\n min-width: 0;\n }\n\n /*\n * 세 층의 크기·굵기는 임의 px 이 아니라 테마의 타입스케일에서 가져온다.\n * 앱 폰트가 바뀌어도 위계가 유지되고, 다른 화면의 본문과 리듬이 맞는다.\n *\n * 1층 title-small (0.875rem / 1.25rem, 500) — 무슨 일이 일어났는가\n * 2층 body-small (0.75rem / 1rem, 400) — 왜·어디서\n * 3층 label-small (0.6875rem) — 기술 원문\n */\n [headline] {\n font-family: var(--md-sys-typescale-title-small-font, var(--theme-font, inherit));\n font-size: var(--md-sys-typescale-title-small-size, 0.875rem);\n line-height: var(--md-sys-typescale-title-small-line-height, 1.25rem);\n font-weight: var(--md-sys-typescale-title-small-weight, 500);\n /*\n * 단어 중간을 끊지 않는다. anywhere 는 필요 없는 곳에서도 글자를 쪼개 읽기가 나빠진다 —\n * 긴 토큰(URL·ID)만 끊기게 break-word 를 쓴다. 기술 원문에서만 anywhere 를 쓴다.\n */\n overflow-wrap: break-word;\n }\n\n [supporting] {\n margin-top: 2px;\n font-family: var(--md-sys-typescale-body-small-font, var(--theme-font, inherit));\n font-size: var(--md-sys-typescale-body-small-size, 0.75rem);\n line-height: var(--md-sys-typescale-body-small-line-height, 1rem);\n font-weight: var(--md-sys-typescale-body-small-weight, 400);\n color: var(--ox-snack-bar-supporting-color);\n overflow-wrap: break-word;\n /* 길어도 토스트가 화면을 삼키지 않게 — 3줄에서 자른다 */\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n }\n\n /* 3층 — 기술 원문. 접힌 상태가 기본이고 필요한 사람만 펼친다 */\n [technical] {\n margin-top: 6px;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: var(--md-sys-typescale-label-small-line-height, 1rem);\n }\n [technical] summary {\n display: flex;\n align-items: center;\n gap: 4px;\n cursor: pointer;\n color: var(--ox-snack-bar-supporting-color);\n list-style: none;\n }\n [technical] summary::-webkit-details-marker {\n display: none;\n }\n [technical] summary::before {\n content: '▸';\n display: inline-block;\n margin-right: 4px;\n transition: transform 0.15s;\n }\n [technical][open] summary::before {\n transform: rotate(90deg);\n }\n [technical] summary:hover {\n color: var(--ox-snack-bar-color);\n }\n /*\n * 원문 전체를 클립보드로. 상자가 접혀 있어도, 스크롤을 끝까지 내리지 않아도 **보이는 만큼이\n * 아니라 전부** 복사된다 — 오류를 옮겨 붙이는 사람이 실제로 원하는 것은 언제나 전문이다.\n * 그래서 요약줄에 둔다.\n */\n [technical] button[copy] {\n margin-inline-start: auto;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n border: none;\n border-radius: 4px;\n padding: 2px 5px;\n background-color: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: 1;\n cursor: pointer;\n }\n [technical] button[copy]:hover {\n color: var(--ox-snack-bar-color);\n background-color: color-mix(in srgb, var(--ox-snack-bar-color) 12%, transparent);\n }\n [technical] button[copy] md-icon {\n --md-icon-size: 14px;\n height: 14px;\n color: inherit;\n }\n [technical] pre {\n /*\n * 스크롤막대 색은 토스트 표면에서 파생시킨다. 공통 스타일의 기본값(--md-sys-color-outline)은\n * **현재 테마**의 색이라, 테마와 무관하게 어두운 이 표면 위에서는 밝기가 어긋난다.\n */\n --scrollbar-thumb-color: color-mix(in srgb, var(--ox-snack-bar-color) 28%, transparent);\n --scrollbar-thumb-hover-color: color-mix(in srgb, var(--ox-snack-bar-color) 48%, transparent);\n --scrollbar-thumb-border-radius: 3px;\n\n margin: 6px 0 0;\n padding: 8px;\n max-height: 180px;\n overflow: auto;\n border-radius: 4px;\n background-color: color-mix(in srgb, var(--ox-snack-bar-color) 8%, transparent);\n color: var(--ox-snack-bar-technical-color);\n font-family: 'SF Mono', Consolas, monospace;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: 1.5;\n white-space: pre-wrap;\n /* 원문은 긴 토큰이 흔하므로 여기서는 어디서든 끊는 것이 맞다 */\n overflow-wrap: anywhere;\n }\n\n [actions] {\n display: flex;\n gap: 4px;\n margin: 6px 0 0 -8px;\n --md-text-button-label-text-color: var(--ox-snack-bar-accent);\n --md-text-button-label-text-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n\n /*\n * Material's text button turns its label and state layer to the app's primary on hover, focus and press. On this\n * surface, dark whatever the theme, an app whose primary is dark loses the label the moment the pointer is on it.\n * Each button keeps its own label color in every state; set here, on the button, so a quiet one keeps its own.\n */\n [actions] md-text-button {\n --md-text-button-hover-label-text-color: var(--md-text-button-label-text-color);\n --md-text-button-focus-label-text-color: var(--md-text-button-label-text-color);\n --md-text-button-pressed-label-text-color: var(--md-text-button-label-text-color);\n --md-text-button-hover-state-layer-color: var(--ox-snack-bar-color);\n --md-text-button-pressed-state-layer-color: var(--ox-snack-bar-color);\n }\n\n /*\n * A button to press only after thinking (emphasis 'quiet'): not in the level's accent, so the sentence above\n * it is what the eye lands on. Still a button - it reads as one and is reached by keyboard like the others.\n */\n [actions] md-text-button[quiet] {\n --md-text-button-label-text-color: var(--ox-snack-bar-supporting-color);\n --md-text-button-label-text-weight: 400;\n }\n\n /*\n * 레벨별 강조색 — 프레임워크 상태 색을 그대로 재사용한다.\n * 레벨은 호스트 속성으로 반영한다(:has() 에 의존하지 않는다).\n */\n :host([level='success']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-success-color, var(--status-success-color, #35a24a));\n }\n :host([level='info']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-info-color, var(--status-info-color, #398ace));\n }\n :host([level='warn']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-warn-color, var(--status-warning-color, #ee8d03));\n }\n :host([level='error']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-error-color, var(--status-danger-color, #d14946));\n }\n\n @media (min-width: 460px) {\n :host {\n /* 80% 는 큰 화면에서 한 줄이 지나치게 길어져 읽기 리듬이 깨진다 */\n min-width: 320px;\n max-width: 480px;\n margin: auto;\n width: fit-content;\n }\n }\n `\n\n private _snackbar = new SnackbarController(this)\n\n @property({ type: Boolean, reflect: true }) active: boolean = false\n /* 레벨을 호스트 속성으로 반영해 CSS 가 강조색을 고르게 한다 */\n @property({ type: String, reflect: true }) level: string = 'info'\n @property({ type: Object }) action: any\n\n // 스와이프 시작 X 좌표 (드래그 중이 아니면 null)\n private dragStartX: number | null = null\n private dragX: number = 0\n // 이 거리(px) 이상 스와이프하면 닫힘 처리\n private static readonly DISMISS_THRESHOLD = 80\n\n connectedCallback() {\n super.connectedCallback()\n // Pointer Events가 터치/마우스를 모두 커버 (스크롤 구분은 touch-action: pan-y가 담당)\n this.addEventListener('pointerdown', this.onDragStart)\n this.addEventListener('pointermove', this.onDragMove)\n this.addEventListener('pointerup', this.onDragEnd)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n /* 다시 붙을 때 같은 핸들러가 두 벌 걸리지 않게 */\n this.removeEventListener('pointerdown', this.onDragStart)\n this.removeEventListener('pointermove', this.onDragMove)\n this.removeEventListener('pointerup', this.onDragEnd)\n clearTimeout(this.copyTimer)\n }\n\n // CSS 변수로 스와이프 위치/투명도 반영\n private applyDrag(x: number, opacity: number = 1) {\n this.style.setProperty('--snackbar-drag-x', `${x}px`)\n this.style.setProperty('--snackbar-opacity', `${opacity}`)\n }\n\n private onDragStart = (e: PointerEvent) => {\n if (!this.active) return\n this.dragStartX = e.clientX\n this.dragX = 0\n this.setAttribute('dragging', '')\n }\n\n private onDragMove = (e: PointerEvent) => {\n if (this.dragStartX === null) return\n this.dragX = e.clientX - this.dragStartX\n // 스와이프 거리에 따라 점점 투명하게\n const opacity = Math.max(0, 1 - Math.abs(this.dragX) / (SnackBar.DISMISS_THRESHOLD * 2.5))\n this.applyDrag(this.dragX, opacity)\n }\n\n private onDragEnd = () => {\n if (this.dragStartX === null) return\n const moved = Math.abs(this.dragX)\n this.dragStartX = null\n this.removeAttribute('dragging')\n\n if (moved >= SnackBar.DISMISS_THRESHOLD) {\n // 스와이프한 방향으로 밀어내며 닫기\n this.applyDrag(this.dragX > 0 ? window.innerWidth : -window.innerWidth, 0)\n window.setTimeout(() => {\n // transition을 끈 채로 닫아서 아래로 떨어지는 기본 닫기 애니메이션을 숨김\n this.setAttribute('dragging', '')\n this._snackbar.close()\n requestAnimationFrame(() => {\n this.applyDrag(0)\n this.removeAttribute('dragging')\n })\n }, 200)\n } else {\n // 임계값 미달이면 원위치로 복귀\n this.applyDrag(0)\n }\n }\n\n /** 레벨별 아이콘. 성격을 한눈에 알리는 첫 신호. */\n private static readonly ICONS: Record<string, string> = {\n success: 'check_circle',\n info: 'info',\n warn: 'warning',\n error: 'error'\n }\n\n /** 복사 단추가 방금 한 일. 잠깐 보였다가 스스로 지워진다. */\n @state() private copyState: CopyState = 'idle'\n private copyTimer?: number\n /** 어느 원문을 두고 위 상태가 남았는지 — 다른 알림이 뜨면 그 상태는 남의 것이다. */\n private copiedFrom?: string\n\n private async copyAll(event: Event, text: string) {\n /* 요약줄 안의 단추다. 복사하려다 상자가 접히거나 펼쳐지면 안 된다 */\n event.preventDefault()\n event.stopPropagation()\n\n /* currentTarget 은 처리가 끝나면 비므로 여기서 잡아 둔다 */\n const button = event.currentTarget as HTMLElement | null\n\n try {\n await navigator.clipboard.writeText(text)\n this.copyState = 'done'\n } catch {\n /* 안 됐으면 안 됐다고 보인다 — 조용히 넘어가면 붙여넣기 전까지 모른다 */\n this.copyState = 'failed'\n }\n\n this.copiedFrom = text\n\n /*\n * 몇 번이든 다시 복사할 수 있다. 그런데 「복사했습니다」가 아직 떠 있는 동안 또 누르면 글자가\n * 그대로라 아무 일도 안 일어난 것처럼 보인다 — 그래서 누를 때마다 단추를 한 번 깜박인다.\n * 상태로 처리하면 두 번째 누름이 같은 값이라 다시 그려지지 않는다.\n */\n button?.animate?.([{ opacity: 0.35 }, { opacity: 1 }], { duration: 220 })\n\n clearTimeout(this.copyTimer)\n this.copyTimer = window.setTimeout(() => {\n this.copyState = 'idle'\n }, 2000)\n }\n\n /** 이 원문에 대한 상태만 인정한다 — 앞 알림에서 남은 「복사했습니다」를 물려받지 않는다. */\n private copyStateFor(text: string): CopyState {\n return this.copiedFrom === text ? this.copyState : 'idle'\n }\n\n private copyIcon(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return 'check'\n case 'failed':\n return 'error'\n default:\n return 'content_copy'\n }\n }\n\n private copyLabel(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return i18next.t('text.copied')\n case 'failed':\n return i18next.t('text.copy failed')\n default:\n return i18next.t('text.copy all')\n }\n }\n\n /** 기술 원문을 사람이 읽을 수 있는 문자열로. Error 는 message+stack, 그 외는 JSON. */\n private formatEx(ex: any): string {\n if (!ex) return ''\n if (typeof ex === 'string') return ex\n if (ex instanceof Error) return ex.stack || `${ex.name}: ${ex.message}`\n try {\n return JSON.stringify(ex, null, 2)\n } catch {\n return String(ex)\n }\n }\n\n render() {\n const { level, message, detail, ex, action } = this._snackbar\n const tone = level || 'info'\n const exText = this.formatEx(ex)\n /*\n * 주입된 도움 함수. AI 가 없는 앱이거나 설정이 죽어 있으면 null 이고, 그때는 버튼을 그리지\n * 않는다. 선택해도 아무 일이 없는 버튼을 보여주지 않는다.\n */\n const assistant = resolveNotifyAssistant()\n /*\n * 알림의 뜻을 아는 패키지가 주입한 단추 하나(registerNotifyAction). 알림이 자기 action 을 가지면\n * 그것이 이긴다 — 보낸 쪽이 정한 단추를 해석이 덮지 않는다.\n */\n const resolved = action?.label ? null : resolveNotifyAction({ level: tone as any, message, detail, ex })\n const button = action?.label ? action : resolved\n\n return html`\n <md-icon class=${tone} aria-hidden=\"true\">${SnackBar.ICONS[tone] ?? 'info'}</md-icon>\n\n <div body>\n <!-- 1층: 무슨 일이 일어났는가 -->\n <div headline>${message}</div>\n\n <!-- 2층: 왜·어디서. 낮은 강조로 구분한다 -->\n ${detail ? html`<div supporting>${detail}</div>` : nothing}\n\n <!-- 3층: 기술 원문. 접힌 채 두고 필요한 사람만 펼친다 -->\n ${exText\n ? html`\n <details technical>\n <summary>\n ${i18next.t('text.technical detail')}\n <button\n copy\n type=\"button\"\n title=${this.copyLabel(exText)}\n @click=${(event: Event) => this.copyAll(event, exText)}\n >\n <md-icon aria-hidden=\"true\">${this.copyIcon(exText)}</md-icon>\n ${this.copyLabel(exText)}\n </button>\n </summary>\n <pre>${exText}</pre>\n </details>\n `\n : nothing}\n\n ${button?.label || (assistant && (tone === 'error' || tone === 'warn'))\n ? html`\n <div actions>\n ${button?.label\n ? html`\n <md-text-button\n ?quiet=${(button as any).emphasis === 'quiet'}\n @click=${() => {\n this._snackbar.close()\n button.callback?.()\n }}\n >${button.label}</md-text-button\n >\n `\n : nothing}\n <!--\n 오류·경고에서 곧바로 AI 도움으로 넘어간다. 핸들러가 등록된 앱에서만 나타나므로\n 선택해도 아무 일이 없는 버튼이 생기지 않는다.\n -->\n ${assistant && (tone === 'error' || tone === 'warn')\n ? html`\n <md-text-button\n @click=${() => {\n assistant({ level: tone as any, message, detail, ex })\n this._snackbar.close()\n }}\n >\n <md-icon slot=\"icon\">auto_awesome</md-icon>\n ${i18next.t('text.ask ai')}\n </md-text-button>\n `\n : nothing}\n </div>\n `\n : nothing}\n </div>\n\n <md-icon\n close\n role=\"button\"\n tabindex=\"0\"\n title=${i18next.t('button.close', { defaultValue: 'close' })}\n @click=${() => this._snackbar.close()}\n >close</md-icon\n >\n `\n }\n\n stateChanged(state: any) {\n var { snackbarOpened } = state.snackbar || {}\n\n // 새로 열릴 때 이전 스와이프 잔상 초기화\n if (snackbarOpened && !this.active) {\n this.dragStartX = null\n this.dragX = 0\n this.removeAttribute('dragging')\n this.applyDrag(0)\n }\n }\n\n protected willUpdate() {\n this.active = this._snackbar.opened\n this.level = this._snackbar.level || 'info'\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ import '../src/layouts/ox-snack-bar.js';
@@ -0,0 +1,42 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import '../src/layouts/ox-snack-bar.js';
3
+ /**
4
+ * The toast is dark whatever the theme. Material's text button turns its label to the app's primary on hover, focus
5
+ * and press, so on an app whose primary is dark the button vanished under the pointer. Each button keeps its label.
6
+ */
7
+ const STATES = ['hover', 'focus', 'pressed'];
8
+ function mount() {
9
+ const host = document.createElement('div');
10
+ /* An app whose primary is as dark as the toast. */
11
+ host.style.setProperty('--md-sys-color-primary', 'rgb(30, 30, 30)');
12
+ document.body.append(host);
13
+ const root = host.attachShadow({ mode: 'open' });
14
+ const SnackBar = customElements.get('ox-snack-bar');
15
+ root.adoptedStyleSheets = [SnackBar.styles].flat().map((one) => one.styleSheet);
16
+ root.innerHTML = '<div actions><md-text-button>retry</md-text-button><md-text-button quiet>request access…</md-text-button></div>';
17
+ return { host, buttons: Array.from(root.querySelectorAll('md-text-button')) };
18
+ }
19
+ const token = (el, name) => getComputedStyle(el).getPropertyValue(name).trim();
20
+ describe('ox-snack-bar action buttons', () => {
21
+ it('★ keep their own label color when hovered, focused or pressed - a quiet one its quiet color', () => {
22
+ const { host, buttons } = mount();
23
+ for (const button of buttons) {
24
+ const label = token(button, '--md-text-button-label-text-color');
25
+ expect(label).to.not.equal('');
26
+ for (const state of STATES)
27
+ expect(token(button, `--md-text-button-${state}-label-text-color`), state).to.equal(label);
28
+ }
29
+ expect(token(buttons[0], '--md-text-button-label-text-color')).to.not.equal(token(buttons[1], '--md-text-button-label-text-color'));
30
+ host.remove();
31
+ });
32
+ it('their state layer is the toast text color, not the app primary', () => {
33
+ const { host, buttons } = mount();
34
+ const text = token(host, '--ox-snack-bar-color');
35
+ for (const button of buttons) {
36
+ expect(token(button, '--md-text-button-hover-state-layer-color')).to.equal(text);
37
+ expect(token(button, '--md-text-button-pressed-state-layer-color')).to.equal(text);
38
+ }
39
+ host.remove();
40
+ });
41
+ });
42
+ //# sourceMappingURL=snack-bar-action-color.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snack-bar-action-color.test.js","sourceRoot":"","sources":["../../test/snack-bar-action-color.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing'\n\nimport '../src/layouts/ox-snack-bar.js'\n\n/**\n * The toast is dark whatever the theme. Material's text button turns its label to the app's primary on hover, focus\n * and press, so on an app whose primary is dark the button vanished under the pointer. Each button keeps its label.\n */\n\nconst STATES = ['hover', 'focus', 'pressed']\n\nfunction mount() {\n const host = document.createElement('div')\n /* An app whose primary is as dark as the toast. */\n host.style.setProperty('--md-sys-color-primary', 'rgb(30, 30, 30)')\n document.body.append(host)\n const root = host.attachShadow({ mode: 'open' })\n const SnackBar = customElements.get('ox-snack-bar') as any\n root.adoptedStyleSheets = [SnackBar.styles].flat().map((one: any) => one.styleSheet)\n root.innerHTML = '<div actions><md-text-button>retry</md-text-button><md-text-button quiet>request access…</md-text-button></div>'\n return { host, buttons: Array.from(root.querySelectorAll('md-text-button')) as HTMLElement[] }\n}\n\nconst token = (el: HTMLElement, name: string) => getComputedStyle(el).getPropertyValue(name).trim()\n\ndescribe('ox-snack-bar action buttons', () => {\n it('★ keep their own label color when hovered, focused or pressed - a quiet one its quiet color', () => {\n const { host, buttons } = mount()\n for (const button of buttons) {\n const label = token(button, '--md-text-button-label-text-color')\n expect(label).to.not.equal('')\n for (const state of STATES) expect(token(button, `--md-text-button-${state}-label-text-color`), state).to.equal(label)\n }\n expect(token(buttons[0], '--md-text-button-label-text-color')).to.not.equal(token(buttons[1], '--md-text-button-label-text-color'))\n host.remove()\n })\n\n it('their state layer is the toast text color, not the app primary', () => {\n const { host, buttons } = mount()\n const text = token(host, '--ox-snack-bar-color')\n for (const button of buttons) {\n expect(token(button, '--md-text-button-hover-state-layer-color')).to.equal(text)\n expect(token(button, '--md-text-button-pressed-state-layer-color')).to.equal(text)\n }\n host.remove()\n })\n})\n"]}