@operato/help 10.29.33 → 10.29.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,8 @@ export declare class OxDoc extends LitElement {
6
6
  base: string;
7
7
  /** 인라인 스폿용 — 지정 시 해당 #anchor 섹션만 노출(풀도큐먼트는 미지정). */
8
8
  section: string;
9
+ /** The section to scroll to once the whole document is drawn (the help page opened at the part a `?` showed). */
10
+ anchor: string;
9
11
  /** 인라인 컴팩트 모드(패널/오버레이용 여백 축소). */
10
12
  compact: boolean;
11
13
  private _html;
@@ -24,6 +24,8 @@ let OxDoc = class OxDoc extends LitElement {
24
24
  this.base = '/helps';
25
25
  /** 인라인 스폿용 — 지정 시 해당 #anchor 섹션만 노출(풀도큐먼트는 미지정). */
26
26
  this.section = '';
27
+ /** The section to scroll to once the whole document is drawn (the help page opened at the part a `?` showed). */
28
+ this.anchor = '';
27
29
  /** 인라인 컴팩트 모드(패널/오버레이용 여백 축소). */
28
30
  this.compact = false;
29
31
  this._html = '';
@@ -114,8 +116,10 @@ let OxDoc = class OxDoc extends LitElement {
114
116
  (_a = i18next === null || i18next === void 0 ? void 0 : i18next.off) === null || _a === void 0 ? void 0 : _a.call(i18next, 'languageChanged', this._onLangChange);
115
117
  }
116
118
  updated(changed) {
117
- if (changed.has('topic') || changed.has('base'))
119
+ if (changed.has('topic') || changed.has('base') || changed.has('section'))
118
120
  this._load();
121
+ if (changed.has('anchor') && this.anchor)
122
+ this._pendingAnchor = this.anchor;
119
123
  /* 새 문서가 다 그려진 뒤에야 그 절로 옮길 수 있다 — 링크에 앵커가 있었으면 여기서 처리한다. */
120
124
  if (this._pendingAnchor && this._status === 'ready') {
121
125
  const id = this._pendingAnchor;
@@ -345,6 +349,9 @@ __decorate([
345
349
  __decorate([
346
350
  property({ type: String })
347
351
  ], OxDoc.prototype, "section", void 0);
352
+ __decorate([
353
+ property({ type: String })
354
+ ], OxDoc.prototype, "anchor", void 0);
348
355
  __decorate([
349
356
  property({ type: Boolean, reflect: true })
350
357
  ], OxDoc.prototype, "compact", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"ox-doc.js","sourceRoot":"","sources":["../../../src/components/ox-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, type TemplateResult } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { unsafeHTML } from 'lit/directives/unsafe-html.js'\nimport { i18next } from '@operato/i18n'\n\n/*\n * ox-doc — ox-markdown 의 \"리치 콘텐츠\" 짝(peer). 범용 리치 문서 렌더러.\n * 프레임워크 help 의 구조를 그대로 재사용: 콘텐츠 = 정적 파일(소스 아님), 로케일은 서버 라우터가 해석.\n * 호출부는 topic 하나만 준다 → /helps/{topic}.html 을 fetch, 서버가 {topic}.{locale}.html 로 리라이트\n * (다국어 링크 복잡함 제거). 리치 HTML 을 shadow DOM 에 렌더하고, 콘텐츠 속 범용 부품\n * (ox-figure/ox-stage-ladder/…)은 등록 커스텀 엘리먼트로 upgrade 되어 살아난다.\n *\n * 세 소비자: 풀도큐먼트(그대로) · 인라인 스폿(section 앵커 하나) · AI(원문 텍스트는 소비자가 파일에서 추출).\n *\n * Moved here from operato-plant, -twin, -warehouse and -figure, which each kept a copy (2026-10-10).\n * `ox-help-panel` reads the same /helps/{topic} path as markdown; whether help keeps two formats is open.\n */\n@customElement('ox-doc')\nexport class OxDoc extends LitElement {\n /** 토픽 경로(예: \"twin/concept\"). 서버가 로케일 파일로 해석. */\n @property({ type: String }) topic = ''\n /** 콘텐츠 베이스 경로(정적 파일 서빙 루트). 기본 프레임워크 help 규약. */\n @property({ type: String }) base = '/helps'\n /** 인라인 스폿용 — 지정 시 해당 #anchor 섹션만 노출(풀도큐먼트는 미지정). */\n @property({ type: String }) section = ''\n /** 인라인 컴팩트 모드(패널/오버레이용 여백 축소). */\n @property({ type: Boolean, reflect: true }) compact = false\n\n @state() private _html = ''\n @state() private _status: 'idle' | 'loading' | 'ready' | 'error' = 'idle'\n\n /*\n * 콘텐츠 내부 토픽 링크(data-topic=\"twin/routing\") 가로채 제자리 이동 — 앱 라우팅 무관, 자족적.\n *\n * 값에 앵커가 붙을 수 있다(`twin/standards-conformance#ours` = \"그 문서의 그 절\"). 앵커를 떼지 않으면\n * 그대로 파일 경로가 되어 `…-conformance#ours.html` 을 받아 오려 하고, 브라우저가 `#` 뒤를 잘라\n * 확장자 없는 주소를 요청한다 — **문서를 못 찾는다.** 실제로 세 링크가 그렇게 죽어 있었다.\n */\n private _onClick = (e: Event) => {\n const link = e.composedPath().find(el => el instanceof HTMLElement && el.hasAttribute('data-topic')) as HTMLElement | undefined\n if (!link) {\n const hashLink = e.composedPath().find(el => el instanceof HTMLAnchorElement && el.getAttribute('href')?.startsWith('#')) as HTMLAnchorElement | undefined\n const hash = hashLink?.getAttribute('href')?.slice(1)\n if (hash) {\n e.preventDefault()\n this._scrollToAnchor(decodeURIComponent(hash))\n }\n return\n }\n e.preventDefault()\n const raw = link.getAttribute('data-topic')\n if (!raw) return\n const [topic, hash] = raw.split('#')\n if (topic && topic !== this.topic) {\n this._pendingAnchor = hash || ''\n this.topic = topic\n // 스크롤 복귀는 호스트(스크롤 컨테이너 소유자)에 위임 — scrollIntoView 는 앱 전역을 밀어 헤더가 사라짐.\n this.dispatchEvent(new CustomEvent('ox-doc-topic', { detail: { topic, hash: hash || '' }, bubbles: true, composed: true }))\n } else if (hash) {\n this._scrollToAnchor(hash) // 같은 문서 안의 절로 — 문서를 다시 받아 오지 않는다\n }\n }\n\n /** 문서를 바꾸며 지정된 절 — 렌더가 끝난 뒤에 옮겨야 하므로 한 박자 미뤄 둔다. */\n private _pendingAnchor = ''\n /** 주소에 붙은 #절은 첫 문서를 그린 뒤 한 번만 적용한다. 이후 토픽 이동에 낡은 해시가 따라가지 않게 비운다. */\n private _initialAnchor = (() => {\n try { return location.hash ? decodeURIComponent(location.hash.slice(1)) : '' } catch { return '' }\n })()\n\n /*\n * 그 절로 옮긴다 — **가장 가까운 스크롤 조상만** 움직인다.\n *\n * `scrollIntoView` 를 쓰지 않는 이유: 앱 전역이 밀려 헤더가 사라진다(이 부품이 처음부터 피해 온 것).\n * shadow 경계를 넘어 host 로 거슬러 올라가며 실제로 스크롤하는 상자를 찾고, 그 상자의 스크롤만 옮긴다.\n */\n private _scrollToAnchor(id: string): void {\n const el = this.renderRoot?.querySelector?.(`#${CSS.escape(id)}`) as HTMLElement | null\n if (!el) return\n let cursor: any = this\n while (cursor) {\n const next = cursor.parentElement ?? cursor.getRootNode?.()?.host\n if (!next) break\n const st = getComputedStyle(next)\n if (/(auto|scroll)/.test(st.overflowY) && next.scrollHeight > next.clientHeight) {\n next.scrollTop += el.getBoundingClientRect().top - next.getBoundingClientRect().top - 12\n return\n }\n cursor = next\n }\n }\n private _onLangChange = () => {\n this._load()\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.addEventListener('click', this._onClick)\n i18next?.on?.('languageChanged', this._onLangChange)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('click', this._onClick)\n i18next?.off?.('languageChanged', this._onLangChange)\n }\n\n static styles = css`\n :host {\n /* 테마 토큰 해소 순서: 호스트 오버라이드(--ox-doc-*) → 앱 토큰(--ui-*) → 리터럴 최후 폴백.\n * 앱 안에서는 앱 테마를 그대로 따르고(모드 전환도 앱 설정에 종속), 앱 밖 단독 사용도 리터럴로 성립한다.\n * OS 취향(prefers-color-scheme)으로 모드를 뒤집지 않는다 — 앱 설정과 어긋나 문서만 다크가 되던 결함.\n *\n * 가운데 층이 한때 --ink / --panel / --line 이었다. 주석에는 「앱 팔레트」라고 적혀 있었지만\n * 그 이름을 정하는 곳이 **어디에도 없었다.** 그래서 앱 안에서도 늘 리터럴로 떨어졌고,\n * 다크에서 제목이 검은 글자로 남아 바탕에 묻혔다. 앱의 실제 이름은 --ui-* 다\n * (packages/shell/client/themes/ui-tokens.css). */\n --_ink: var(--ox-doc-ink, var(--ui-ink, #16202b)); --_muted: var(--ox-doc-muted, var(--ui-muted, #566573)); --_faint: var(--ox-doc-faint, var(--ui-faint, #8a99a6));\n --_line: var(--ox-doc-line, var(--ui-line, #dde4ea)); --_panel: var(--ox-doc-panel, var(--ui-panel, #fff)); --_ground: var(--ox-doc-ground, transparent);\n --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86));\n /* 링크·강조 글자. 강조색을 본문색 쪽으로 당겨 **글자로 읽힐 만큼** 대비를 준다 —\n 라이트에서는 짙어지고 다크에서는 밝아진다. 한 식이 두 테마를 다 맡는다. */\n --_accent-deep: var(--ox-doc-accent-deep, color-mix(in oklab, var(--_accent), var(--_ink) 22%));\n --_accent-wash: var(--ox-doc-accent-wash, var(--ui-accent-wash, #e3f2f0));\n /*\n * 도판이 놓이는 **종이 면.** 테마를 따르지 않는다 — 따르면 안 된다.\n *\n * 그림의 잉크는 문서 파일 안에 박혀 있다(서른 편의 svg 안에 밝은 회색·짙은 남색이 1064곳).\n * 다크에서 그 아래에 어두운 면을 깔면 선과 글자가 바탕에 묻힌다. 그래서 도판만 밝은 종이\n * 위에 올린다 — 어두운 화면에 끼워 넣은 인쇄물처럼 읽힌다.\n *\n * 그림 자체가 테마를 따르게 하려면 서른 편의 svg 를 토큰으로 다시 그려야 한다. 그것은\n * 문서를 쓰는 쪽의 일이다.\n */\n --_paper: var(--ox-doc-paper, #fff);\n --_maxw: var(--ox-doc-maxw, 760px);\n --_sans: var(--ox-doc-sans, -apple-system, BlinkMacSystemFont, \"Apple SD Gothic Neo\", \"Pretendard\", \"Malgun Gothic\", \"Noto Sans KR\", system-ui, sans-serif);\n --_mono: var(--ox-doc-mono, \"SF Mono\", \"JetBrains Mono\", ui-monospace, Menlo, monospace);\n display: block; color: var(--_ink); background: var(--_ground);\n font-family: var(--_sans); line-height: 1.72; font-size: 16.5px;\n }\n /*\n * ── 문서가 쓰는 이름 ────────────────────────────────────────────────────\n * 문서(helps/*.html)는 자기 style 블록에서 --x-ink · --x-line · --x-panel · --x-wash ·\n * --x-accent 를 쓴다. 서른 편이 모두 그렇게 적어 두었는데 **그 이름을 정하는 곳이 없었다.**\n * 그래서 늘 리터럴(#fff · #cbd5dd)로 떨어졌고, 다크에서 문서 안의 카드와 표만 흰 조각으로\n * 남았다.\n *\n * 여기서 정한다. 문서를 한 줄도 고치지 않고 서른 편이 함께 테마를 따른다.\n */\n .body {\n --x-ink: var(--_ink);\n --x-muted: var(--_muted);\n --x-faint: var(--_faint);\n --x-line: var(--_line);\n --x-panel: var(--_panel);\n --x-wash: color-mix(in oklab, var(--_line), transparent 55%);\n --x-accent: var(--_accent);\n /* 글자로 쓰는 강조색. 순수 강조색은 12px 라벨에서 라이트 3.79 · 다크에서도 아슬아슬하다. */\n --x-accent-deep: var(--_accent-deep);\n }\n .body { max-width: var(--_maxw); margin: 0 auto; padding: var(--_pad, 10px 6px 48px); }\n /*\n * 한글 줄바꿈 — **낱글자 단위로 끊지 않는다.**\n *\n * 기본값은 한글을 어느 글자에서나 끊는다. 그래서 제목이 두 줄이 되면 \"시간이 지나 / 도\",\n * \"예측 시뮬레 / 이션은\" 처럼 어절 중간이 어긋났다(배우기 제목 중 두 줄로 넘어가는 11개 가운데\n * 9개가 그랬다). keep-all 은 띄어쓰기 자리에서만 끊으므로 어절이 살아남는다. 영문은 원래\n * 띄어쓰기로 끊으므로 달라지는 것이 없다.\n *\n * 끊을 자리가 없는 긴 토큰(URN·식별자)은 그대로 두면 문서 폭을 밀어내므로 overflow-wrap 으로 받는다.\n */\n .body { word-break: keep-all; overflow-wrap: break-word; }\n :host([compact]) .body { padding: 4px 2px 12px; font-size: 15px; }\n /* 렌더된 콘텐츠 타이포그래피(unsafeHTML 산출물 — shadow 내부 스코프). 견본 수준 스케일·리듬. */\n /*\n * 조판 예외: **읽는 문서의 눈금이다.** 화면 조판(--ui-t-*)은 관리 화면의 절 제목과 표를 위한\n * 다섯 단이라 긴 글에는 단이 모자라고, 좁은 화면에서 제목이 본문을 밀어내지 않도록 반응형이\n * 필요하다. 그래서 clamp 로 둔다.\n */\n .body h1 { font-size: clamp(28px, 4.6vw, 40px); font-weight: 820; letter-spacing: -.022em; line-height: 1.16; margin: 0 0 .55em; text-wrap: balance; }\n .body h2 { font-size: clamp(20px, 3.2vw, 26px); font-weight: 750; letter-spacing: -.017em; line-height: 1.25; margin: 2em 0 .55em; padding-top: 1em; border-top: 1px solid var(--_line); text-wrap: balance; }\n .body h2:first-of-type { border-top: 0; padding-top: 0; }\n .body h3 { font-size: 17px; font-weight: 680; margin: 1.4em 0 .35em; }\n .body p { margin: 0 0 1.05em; }\n .body h1 + p { font-size: 1.06em; color: var(--_muted); } /* 리드 문단(h1 바로 뒤) */\n /* 히어로 아이브로우 — 펄스 점 + mono 라벨 */\n .body .eyebrow { display: inline-flex; align-items: center; gap: 8px; font-family: var(--_mono); font-size: 12px; letter-spacing: .16em; text-transform: uppercase; color: var(--_accent-deep); font-weight: 600; margin: 0 0 14px; }\n .body .eyebrow::before { content: \"\"; width: 7px; height: 7px; border-radius: 50%; background: var(--_accent); box-shadow: 0 0 0 0 var(--_accent); animation: oxpulse 2.6s infinite; }\n @keyframes oxpulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--_accent), transparent 40%); } 70% { box-shadow: 0 0 0 9px color-mix(in oklab, var(--_accent), transparent 100%); } 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--_accent), transparent 100%); } }\n @media (prefers-reduced-motion: reduce) { .body .eyebrow::before { animation: none; } }\n /* 섹션 키커 — 흐린 작은 라벨(제목 위). 섹션 구분선은 여기로 이동. */\n /* 절 이름이다 — 문서를 훑을 때 눈이 짚는 자리. --_faint 로는 다크에서 3.64 였다(§ th 의 머리말). */\n .body .kicker { font-family: var(--_mono); font-size: 12px; letter-spacing: .13em; text-transform: uppercase; color: var(--_muted); font-weight: 600; margin: 2.2em 0 0; padding-top: 1.1em; border-top: 1px solid var(--_line); }\n .body .kicker + h2 { margin: .35em 0 .55em; padding-top: 0; border-top: 0; }\n .body strong { font-weight: 700; }\n .body em { font-style: normal; color: var(--_accent-deep); font-weight: 600; }\n .body ul, .body ol { margin: 0 0 1.15em; padding-left: 1.35em; }\n .body li { margin: .38em 0; }\n .body ul li::marker { color: var(--_accent); }\n .body ol { counter-reset: oxol; list-style: none; padding-left: 0; }\n .body ol > li { counter-increment: oxol; position: relative; padding-left: 2.2em; }\n .body ol > li::before { content: counter(oxol); position: absolute; left: 0; top: .05em; width: 1.55em; height: 1.55em; border-radius: 8px; background: var(--_accent-wash); color: var(--_accent-deep); font-family: var(--_mono); font-weight: 700; font-size: .82em; display: grid; place-items: center; }\n .body a { color: var(--_accent-deep); text-underline-offset: 3px; cursor: pointer; }\n .body a[data-topic] { text-decoration: none; font-weight: 640; }\n .body a:hover { text-decoration: underline; }\n .body blockquote { margin: 1.4em 0; padding: 14px 18px; border: 1px solid color-mix(in oklab, var(--_accent), transparent 68%); background: var(--_accent-wash); border-radius: 11px; color: var(--_ink); display: flex; gap: 11px; }\n .body blockquote::before { content: \"▸\"; color: var(--_accent); font-weight: 700; }\n .body blockquote p { margin: 0; }\n .body code { font-family: var(--_mono); font-size: .86em; background: color-mix(in oklab, var(--_line), transparent 40%); padding: .12em .4em; border-radius: 5px; }\n /* 코드 블록 — 기술 개념 문서는 실물(이벤트·설정)을 보여줘야 이해된다. 없으면 예시를 그림으로\n 모의 내게 되고, 그림은 복사도 검색도 안 된다. 긴 줄은 가로 스크롤(문서 폭을 밀지 않는다). */\n .body pre { margin: 1.3em 0; padding: 15px 17px; background: var(--_panel); border: 1px solid var(--_line); border-radius: 11px; overflow-x: auto; font-family: var(--_mono); font-size: 12.5px; line-height: 1.62; color: var(--_ink); }\n .body pre code { background: none; padding: 0; font-size: inherit; }\n /* 표 = 패널 카드(견본 tiles 감) */\n /* 표는 자기 상자 안에서 가로로 스크롤한다(_wrapTables 가 감싼다) — 좁은 화면에서 본문 폭을 밀어내지 않게. */\n .body .tablewrap { overflow-x: auto; margin: 1.4em 0; }\n .body .tablewrap table { margin: 0; min-width: 100%; width: auto; }\n .body table { width: 100%; border-collapse: collapse; margin: 1.4em 0; font-size: 14px; font-variant-numeric: tabular-nums; background: var(--_panel); border: 1px solid var(--_line); border-radius: 11px; overflow: hidden; }\n .body thead { background: color-mix(in oklab, var(--_line), transparent 55%); }\n .body th, .body td { text-align: left; padding: 10px 13px; border-bottom: 1px solid var(--_line); }\n .body tbody tr:last-child td { border-bottom: 0; }\n /*\n * 머리칸은 --_muted 다. --_faint 로 두었더니 채워진 머리행 위에서 대비가 2.8 로 떨어져\n * 열 이름이 안 읽혔다(10.5px 글자다). --_faint 는 바탕 위의 곁말에 쓰는 색이지, 면 위에\n * 올리는 라벨의 색이 아니다.\n */\n .body th { font-family: var(--_mono); font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--_muted); font-weight: 600; }\n .body td:first-child { font-weight: 640; }\n .tablewrap { overflow-x: auto; }\n /* 도판 — 테마 패널 위에 올려 라이트/다크 모드와 자연스럽게 조화되도록 한다. */\n .body svg { display: block; max-width: 100%; box-sizing: border-box; background: var(--_panel); border: 1px solid var(--_line); border-radius: 10px; padding: 12px; }\n .status { color: var(--_muted); font-size: 14px; padding: 24px 4px; }\n .status.err { color: var(--ui-crit, #c0492f); }\n `\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('topic') || changed.has('base')) this._load()\n /* 새 문서가 다 그려진 뒤에야 그 절로 옮길 수 있다 — 링크에 앵커가 있었으면 여기서 처리한다. */\n if (this._pendingAnchor && this._status === 'ready') {\n const id = this._pendingAnchor\n this._pendingAnchor = ''\n requestAnimationFrame(() => this._scrollToAnchor(id))\n }\n }\n\n private async _load() {\n if (!this.topic) { this._status = 'idle'; this._html = ''; return }\n this._status = 'loading'\n try {\n // 로케일은 서버 라우터가 해석(topic.html → topic.{locale}.html). 클라는 topic 만 안다.\n // 쿠키 우선순위로 인해 en-US가 고정되는 현상을 방지하기 위해 쿼리스트링(?lng=)을 함께 전달한다.\n const lang = i18next?.language ?? 'en'\n const res = await fetch(`${this.base}/${this.topic}.html?lng=${encodeURIComponent(lang)}`, {\n headers: { 'accept-language': lang }\n })\n if (!res.ok) throw new Error(`${res.status}`)\n let content = await res.text()\n if (this.section) content = this._sliceSection(content, this.section)\n this._html = this._wrapTables(content)\n if (!this._pendingAnchor && this._initialAnchor) {\n this._pendingAnchor = this._initialAnchor\n this._initialAnchor = ''\n }\n this._status = 'ready'\n } catch (e) {\n this._status = 'error'\n this._html = ''\n }\n }\n\n /*\n * 표를 가로 스크롤 상자로 감싼다 — **좁은 화면에서 표가 문서 폭을 밀어내지 않게.**\n *\n * 코드 블록(pre)은 이미 자기 안에서 가로로 스크롤한다. 표에는 그 장치가 없어서, 폭이 좁아지면\n * (휴대폰·분할 화면·인라인 도움말 팝업) 비교표가 본문 바깥으로 삐져나갔다. 표는 열 수가 정해져\n * 있어 글자만 줄여서는 들어가지 않으므로, 넘치는 쪽을 스크롤로 받는 것이 정공법이다.\n *\n * 문서(HTML)를 고치지 않고 렌더 시점에 감싸는 이유: 같은 파일을 세 소비자가 함께 쓴다. AI 는 태그를\n * 걷어낸 본문을 읽으므로 표시용 래퍼가 원문에 섞이면 그쪽까지 지저분해진다.\n */\n private _wrapTables(html: string): string {\n if (!html.includes('<table')) return html\n const tpl = document.createElement('template')\n tpl.innerHTML = html\n for (const table of Array.from(tpl.content.querySelectorAll('table'))) {\n if ((table.parentElement as HTMLElement | null)?.classList.contains('tablewrap')) continue\n const wrap = document.createElement('div')\n wrap.className = 'tablewrap'\n table.replaceWith(wrap)\n wrap.appendChild(table)\n }\n return tpl.innerHTML\n }\n\n /** 인라인 스폿 — id=section 인 heading 부터 다음 동급 heading 전까지만 추출(문맥 도움말용). */\n private _sliceSection(full: string, id: string): string {\n const tpl = document.createElement('template')\n tpl.innerHTML = full\n const anchor = tpl.content.querySelector(`#${CSS.escape(id)}`)\n if (!anchor) return full\n const level = anchor.tagName // H1..H3\n const frag: string[] = [(anchor as HTMLElement).outerHTML]\n let n = anchor.nextElementSibling\n while (n && !(/^H[1-6]$/.test(n.tagName) && n.tagName <= level)) { frag.push((n as HTMLElement).outerHTML); n = n.nextElementSibling }\n return frag.join('')\n }\n\n render(): TemplateResult | typeof nothing {\n if (this._status === 'loading') return html`<div class=\"status\">${i18next?.t?.('text.loading') ?? 'Loading…'}</div>`\n if (this._status === 'error') return html`<div class=\"status err\">${i18next?.t?.('text.not found') ?? 'Content not found.'} <code>${this.topic}</code></div>`\n if (this._status !== 'ready') return html`<div class=\"body\"><slot></slot></div>`\n return html`<div class=\"body\">${unsafeHTML(this._html)}</div>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap { 'ox-doc': OxDoc }\n}\n"]}
1
+ {"version":3,"file":"ox-doc.js","sourceRoot":"","sources":["../../../src/components/ox-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, type TemplateResult } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { unsafeHTML } from 'lit/directives/unsafe-html.js'\nimport { i18next } from '@operato/i18n'\n\n/*\n * ox-doc — ox-markdown 의 \"리치 콘텐츠\" 짝(peer). 범용 리치 문서 렌더러.\n * 프레임워크 help 의 구조를 그대로 재사용: 콘텐츠 = 정적 파일(소스 아님), 로케일은 서버 라우터가 해석.\n * 호출부는 topic 하나만 준다 → /helps/{topic}.html 을 fetch, 서버가 {topic}.{locale}.html 로 리라이트\n * (다국어 링크 복잡함 제거). 리치 HTML 을 shadow DOM 에 렌더하고, 콘텐츠 속 범용 부품\n * (ox-figure/ox-stage-ladder/…)은 등록 커스텀 엘리먼트로 upgrade 되어 살아난다.\n *\n * 세 소비자: 풀도큐먼트(그대로) · 인라인 스폿(section 앵커 하나) · AI(원문 텍스트는 소비자가 파일에서 추출).\n *\n * Moved here from operato-plant, -twin, -warehouse and -figure, which each kept a copy (2026-10-10).\n * `ox-help-panel` reads the same /helps/{topic} path as markdown; whether help keeps two formats is open.\n */\n@customElement('ox-doc')\nexport class OxDoc extends LitElement {\n /** 토픽 경로(예: \"twin/concept\"). 서버가 로케일 파일로 해석. */\n @property({ type: String }) topic = ''\n /** 콘텐츠 베이스 경로(정적 파일 서빙 루트). 기본 프레임워크 help 규약. */\n @property({ type: String }) base = '/helps'\n /** 인라인 스폿용 — 지정 시 해당 #anchor 섹션만 노출(풀도큐먼트는 미지정). */\n @property({ type: String }) section = ''\n /** The section to scroll to once the whole document is drawn (the help page opened at the part a `?` showed). */\n @property({ type: String }) anchor = ''\n /** 인라인 컴팩트 모드(패널/오버레이용 여백 축소). */\n @property({ type: Boolean, reflect: true }) compact = false\n\n @state() private _html = ''\n @state() private _status: 'idle' | 'loading' | 'ready' | 'error' = 'idle'\n\n /*\n * 콘텐츠 내부 토픽 링크(data-topic=\"twin/routing\") 가로채 제자리 이동 — 앱 라우팅 무관, 자족적.\n *\n * 값에 앵커가 붙을 수 있다(`twin/standards-conformance#ours` = \"그 문서의 그 절\"). 앵커를 떼지 않으면\n * 그대로 파일 경로가 되어 `…-conformance#ours.html` 을 받아 오려 하고, 브라우저가 `#` 뒤를 잘라\n * 확장자 없는 주소를 요청한다 — **문서를 못 찾는다.** 실제로 세 링크가 그렇게 죽어 있었다.\n */\n private _onClick = (e: Event) => {\n const link = e.composedPath().find(el => el instanceof HTMLElement && el.hasAttribute('data-topic')) as HTMLElement | undefined\n if (!link) {\n const hashLink = e.composedPath().find(el => el instanceof HTMLAnchorElement && el.getAttribute('href')?.startsWith('#')) as HTMLAnchorElement | undefined\n const hash = hashLink?.getAttribute('href')?.slice(1)\n if (hash) {\n e.preventDefault()\n this._scrollToAnchor(decodeURIComponent(hash))\n }\n return\n }\n e.preventDefault()\n const raw = link.getAttribute('data-topic')\n if (!raw) return\n const [topic, hash] = raw.split('#')\n if (topic && topic !== this.topic) {\n this._pendingAnchor = hash || ''\n this.topic = topic\n // 스크롤 복귀는 호스트(스크롤 컨테이너 소유자)에 위임 — scrollIntoView 는 앱 전역을 밀어 헤더가 사라짐.\n this.dispatchEvent(new CustomEvent('ox-doc-topic', { detail: { topic, hash: hash || '' }, bubbles: true, composed: true }))\n } else if (hash) {\n this._scrollToAnchor(hash) // 같은 문서 안의 절로 — 문서를 다시 받아 오지 않는다\n }\n }\n\n /** 문서를 바꾸며 지정된 절 — 렌더가 끝난 뒤에 옮겨야 하므로 한 박자 미뤄 둔다. */\n private _pendingAnchor = ''\n /** 주소에 붙은 #절은 첫 문서를 그린 뒤 한 번만 적용한다. 이후 토픽 이동에 낡은 해시가 따라가지 않게 비운다. */\n private _initialAnchor = (() => {\n try { return location.hash ? decodeURIComponent(location.hash.slice(1)) : '' } catch { return '' }\n })()\n\n /*\n * 그 절로 옮긴다 — **가장 가까운 스크롤 조상만** 움직인다.\n *\n * `scrollIntoView` 를 쓰지 않는 이유: 앱 전역이 밀려 헤더가 사라진다(이 부품이 처음부터 피해 온 것).\n * shadow 경계를 넘어 host 로 거슬러 올라가며 실제로 스크롤하는 상자를 찾고, 그 상자의 스크롤만 옮긴다.\n */\n private _scrollToAnchor(id: string): void {\n const el = this.renderRoot?.querySelector?.(`#${CSS.escape(id)}`) as HTMLElement | null\n if (!el) return\n let cursor: any = this\n while (cursor) {\n const next = cursor.parentElement ?? cursor.getRootNode?.()?.host\n if (!next) break\n const st = getComputedStyle(next)\n if (/(auto|scroll)/.test(st.overflowY) && next.scrollHeight > next.clientHeight) {\n next.scrollTop += el.getBoundingClientRect().top - next.getBoundingClientRect().top - 12\n return\n }\n cursor = next\n }\n }\n private _onLangChange = () => {\n this._load()\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.addEventListener('click', this._onClick)\n i18next?.on?.('languageChanged', this._onLangChange)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('click', this._onClick)\n i18next?.off?.('languageChanged', this._onLangChange)\n }\n\n static styles = css`\n :host {\n /* 테마 토큰 해소 순서: 호스트 오버라이드(--ox-doc-*) → 앱 토큰(--ui-*) → 리터럴 최후 폴백.\n * 앱 안에서는 앱 테마를 그대로 따르고(모드 전환도 앱 설정에 종속), 앱 밖 단독 사용도 리터럴로 성립한다.\n * OS 취향(prefers-color-scheme)으로 모드를 뒤집지 않는다 — 앱 설정과 어긋나 문서만 다크가 되던 결함.\n *\n * 가운데 층이 한때 --ink / --panel / --line 이었다. 주석에는 「앱 팔레트」라고 적혀 있었지만\n * 그 이름을 정하는 곳이 **어디에도 없었다.** 그래서 앱 안에서도 늘 리터럴로 떨어졌고,\n * 다크에서 제목이 검은 글자로 남아 바탕에 묻혔다. 앱의 실제 이름은 --ui-* 다\n * (packages/shell/client/themes/ui-tokens.css). */\n --_ink: var(--ox-doc-ink, var(--ui-ink, #16202b)); --_muted: var(--ox-doc-muted, var(--ui-muted, #566573)); --_faint: var(--ox-doc-faint, var(--ui-faint, #8a99a6));\n --_line: var(--ox-doc-line, var(--ui-line, #dde4ea)); --_panel: var(--ox-doc-panel, var(--ui-panel, #fff)); --_ground: var(--ox-doc-ground, transparent);\n --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86));\n /* 링크·강조 글자. 강조색을 본문색 쪽으로 당겨 **글자로 읽힐 만큼** 대비를 준다 —\n 라이트에서는 짙어지고 다크에서는 밝아진다. 한 식이 두 테마를 다 맡는다. */\n --_accent-deep: var(--ox-doc-accent-deep, color-mix(in oklab, var(--_accent), var(--_ink) 22%));\n --_accent-wash: var(--ox-doc-accent-wash, var(--ui-accent-wash, #e3f2f0));\n /*\n * 도판이 놓이는 **종이 면.** 테마를 따르지 않는다 — 따르면 안 된다.\n *\n * 그림의 잉크는 문서 파일 안에 박혀 있다(서른 편의 svg 안에 밝은 회색·짙은 남색이 1064곳).\n * 다크에서 그 아래에 어두운 면을 깔면 선과 글자가 바탕에 묻힌다. 그래서 도판만 밝은 종이\n * 위에 올린다 — 어두운 화면에 끼워 넣은 인쇄물처럼 읽힌다.\n *\n * 그림 자체가 테마를 따르게 하려면 서른 편의 svg 를 토큰으로 다시 그려야 한다. 그것은\n * 문서를 쓰는 쪽의 일이다.\n */\n --_paper: var(--ox-doc-paper, #fff);\n --_maxw: var(--ox-doc-maxw, 760px);\n --_sans: var(--ox-doc-sans, -apple-system, BlinkMacSystemFont, \"Apple SD Gothic Neo\", \"Pretendard\", \"Malgun Gothic\", \"Noto Sans KR\", system-ui, sans-serif);\n --_mono: var(--ox-doc-mono, \"SF Mono\", \"JetBrains Mono\", ui-monospace, Menlo, monospace);\n display: block; color: var(--_ink); background: var(--_ground);\n font-family: var(--_sans); line-height: 1.72; font-size: 16.5px;\n }\n /*\n * ── 문서가 쓰는 이름 ────────────────────────────────────────────────────\n * 문서(helps/*.html)는 자기 style 블록에서 --x-ink · --x-line · --x-panel · --x-wash ·\n * --x-accent 를 쓴다. 서른 편이 모두 그렇게 적어 두었는데 **그 이름을 정하는 곳이 없었다.**\n * 그래서 늘 리터럴(#fff · #cbd5dd)로 떨어졌고, 다크에서 문서 안의 카드와 표만 흰 조각으로\n * 남았다.\n *\n * 여기서 정한다. 문서를 한 줄도 고치지 않고 서른 편이 함께 테마를 따른다.\n */\n .body {\n --x-ink: var(--_ink);\n --x-muted: var(--_muted);\n --x-faint: var(--_faint);\n --x-line: var(--_line);\n --x-panel: var(--_panel);\n --x-wash: color-mix(in oklab, var(--_line), transparent 55%);\n --x-accent: var(--_accent);\n /* 글자로 쓰는 강조색. 순수 강조색은 12px 라벨에서 라이트 3.79 · 다크에서도 아슬아슬하다. */\n --x-accent-deep: var(--_accent-deep);\n }\n .body { max-width: var(--_maxw); margin: 0 auto; padding: var(--_pad, 10px 6px 48px); }\n /*\n * 한글 줄바꿈 — **낱글자 단위로 끊지 않는다.**\n *\n * 기본값은 한글을 어느 글자에서나 끊는다. 그래서 제목이 두 줄이 되면 \"시간이 지나 / 도\",\n * \"예측 시뮬레 / 이션은\" 처럼 어절 중간이 어긋났다(배우기 제목 중 두 줄로 넘어가는 11개 가운데\n * 9개가 그랬다). keep-all 은 띄어쓰기 자리에서만 끊으므로 어절이 살아남는다. 영문은 원래\n * 띄어쓰기로 끊으므로 달라지는 것이 없다.\n *\n * 끊을 자리가 없는 긴 토큰(URN·식별자)은 그대로 두면 문서 폭을 밀어내므로 overflow-wrap 으로 받는다.\n */\n .body { word-break: keep-all; overflow-wrap: break-word; }\n :host([compact]) .body { padding: 4px 2px 12px; font-size: 15px; }\n /* 렌더된 콘텐츠 타이포그래피(unsafeHTML 산출물 — shadow 내부 스코프). 견본 수준 스케일·리듬. */\n /*\n * 조판 예외: **읽는 문서의 눈금이다.** 화면 조판(--ui-t-*)은 관리 화면의 절 제목과 표를 위한\n * 다섯 단이라 긴 글에는 단이 모자라고, 좁은 화면에서 제목이 본문을 밀어내지 않도록 반응형이\n * 필요하다. 그래서 clamp 로 둔다.\n */\n .body h1 { font-size: clamp(28px, 4.6vw, 40px); font-weight: 820; letter-spacing: -.022em; line-height: 1.16; margin: 0 0 .55em; text-wrap: balance; }\n .body h2 { font-size: clamp(20px, 3.2vw, 26px); font-weight: 750; letter-spacing: -.017em; line-height: 1.25; margin: 2em 0 .55em; padding-top: 1em; border-top: 1px solid var(--_line); text-wrap: balance; }\n .body h2:first-of-type { border-top: 0; padding-top: 0; }\n .body h3 { font-size: 17px; font-weight: 680; margin: 1.4em 0 .35em; }\n .body p { margin: 0 0 1.05em; }\n .body h1 + p { font-size: 1.06em; color: var(--_muted); } /* 리드 문단(h1 바로 뒤) */\n /* 히어로 아이브로우 — 펄스 점 + mono 라벨 */\n .body .eyebrow { display: inline-flex; align-items: center; gap: 8px; font-family: var(--_mono); font-size: 12px; letter-spacing: .16em; text-transform: uppercase; color: var(--_accent-deep); font-weight: 600; margin: 0 0 14px; }\n .body .eyebrow::before { content: \"\"; width: 7px; height: 7px; border-radius: 50%; background: var(--_accent); box-shadow: 0 0 0 0 var(--_accent); animation: oxpulse 2.6s infinite; }\n @keyframes oxpulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--_accent), transparent 40%); } 70% { box-shadow: 0 0 0 9px color-mix(in oklab, var(--_accent), transparent 100%); } 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--_accent), transparent 100%); } }\n @media (prefers-reduced-motion: reduce) { .body .eyebrow::before { animation: none; } }\n /* 섹션 키커 — 흐린 작은 라벨(제목 위). 섹션 구분선은 여기로 이동. */\n /* 절 이름이다 — 문서를 훑을 때 눈이 짚는 자리. --_faint 로는 다크에서 3.64 였다(§ th 의 머리말). */\n .body .kicker { font-family: var(--_mono); font-size: 12px; letter-spacing: .13em; text-transform: uppercase; color: var(--_muted); font-weight: 600; margin: 2.2em 0 0; padding-top: 1.1em; border-top: 1px solid var(--_line); }\n .body .kicker + h2 { margin: .35em 0 .55em; padding-top: 0; border-top: 0; }\n .body strong { font-weight: 700; }\n .body em { font-style: normal; color: var(--_accent-deep); font-weight: 600; }\n .body ul, .body ol { margin: 0 0 1.15em; padding-left: 1.35em; }\n .body li { margin: .38em 0; }\n .body ul li::marker { color: var(--_accent); }\n .body ol { counter-reset: oxol; list-style: none; padding-left: 0; }\n .body ol > li { counter-increment: oxol; position: relative; padding-left: 2.2em; }\n .body ol > li::before { content: counter(oxol); position: absolute; left: 0; top: .05em; width: 1.55em; height: 1.55em; border-radius: 8px; background: var(--_accent-wash); color: var(--_accent-deep); font-family: var(--_mono); font-weight: 700; font-size: .82em; display: grid; place-items: center; }\n .body a { color: var(--_accent-deep); text-underline-offset: 3px; cursor: pointer; }\n .body a[data-topic] { text-decoration: none; font-weight: 640; }\n .body a:hover { text-decoration: underline; }\n .body blockquote { margin: 1.4em 0; padding: 14px 18px; border: 1px solid color-mix(in oklab, var(--_accent), transparent 68%); background: var(--_accent-wash); border-radius: 11px; color: var(--_ink); display: flex; gap: 11px; }\n .body blockquote::before { content: \"▸\"; color: var(--_accent); font-weight: 700; }\n .body blockquote p { margin: 0; }\n .body code { font-family: var(--_mono); font-size: .86em; background: color-mix(in oklab, var(--_line), transparent 40%); padding: .12em .4em; border-radius: 5px; }\n /* 코드 블록 — 기술 개념 문서는 실물(이벤트·설정)을 보여줘야 이해된다. 없으면 예시를 그림으로\n 모의 내게 되고, 그림은 복사도 검색도 안 된다. 긴 줄은 가로 스크롤(문서 폭을 밀지 않는다). */\n .body pre { margin: 1.3em 0; padding: 15px 17px; background: var(--_panel); border: 1px solid var(--_line); border-radius: 11px; overflow-x: auto; font-family: var(--_mono); font-size: 12.5px; line-height: 1.62; color: var(--_ink); }\n .body pre code { background: none; padding: 0; font-size: inherit; }\n /* 표 = 패널 카드(견본 tiles 감) */\n /* 표는 자기 상자 안에서 가로로 스크롤한다(_wrapTables 가 감싼다) — 좁은 화면에서 본문 폭을 밀어내지 않게. */\n .body .tablewrap { overflow-x: auto; margin: 1.4em 0; }\n .body .tablewrap table { margin: 0; min-width: 100%; width: auto; }\n .body table { width: 100%; border-collapse: collapse; margin: 1.4em 0; font-size: 14px; font-variant-numeric: tabular-nums; background: var(--_panel); border: 1px solid var(--_line); border-radius: 11px; overflow: hidden; }\n .body thead { background: color-mix(in oklab, var(--_line), transparent 55%); }\n .body th, .body td { text-align: left; padding: 10px 13px; border-bottom: 1px solid var(--_line); }\n .body tbody tr:last-child td { border-bottom: 0; }\n /*\n * 머리칸은 --_muted 다. --_faint 로 두었더니 채워진 머리행 위에서 대비가 2.8 로 떨어져\n * 열 이름이 안 읽혔다(10.5px 글자다). --_faint 는 바탕 위의 곁말에 쓰는 색이지, 면 위에\n * 올리는 라벨의 색이 아니다.\n */\n .body th { font-family: var(--_mono); font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--_muted); font-weight: 600; }\n .body td:first-child { font-weight: 640; }\n .tablewrap { overflow-x: auto; }\n /* 도판 — 테마 패널 위에 올려 라이트/다크 모드와 자연스럽게 조화되도록 한다. */\n .body svg { display: block; max-width: 100%; box-sizing: border-box; background: var(--_panel); border: 1px solid var(--_line); border-radius: 10px; padding: 12px; }\n .status { color: var(--_muted); font-size: 14px; padding: 24px 4px; }\n .status.err { color: var(--ui-crit, #c0492f); }\n `\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('topic') || changed.has('base') || changed.has('section')) this._load()\n if (changed.has('anchor') && this.anchor) this._pendingAnchor = this.anchor\n /* 새 문서가 다 그려진 뒤에야 그 절로 옮길 수 있다 — 링크에 앵커가 있었으면 여기서 처리한다. */\n if (this._pendingAnchor && this._status === 'ready') {\n const id = this._pendingAnchor\n this._pendingAnchor = ''\n requestAnimationFrame(() => this._scrollToAnchor(id))\n }\n }\n\n private async _load() {\n if (!this.topic) { this._status = 'idle'; this._html = ''; return }\n this._status = 'loading'\n try {\n // 로케일은 서버 라우터가 해석(topic.html → topic.{locale}.html). 클라는 topic 만 안다.\n // 쿠키 우선순위로 인해 en-US가 고정되는 현상을 방지하기 위해 쿼리스트링(?lng=)을 함께 전달한다.\n const lang = i18next?.language ?? 'en'\n const res = await fetch(`${this.base}/${this.topic}.html?lng=${encodeURIComponent(lang)}`, {\n headers: { 'accept-language': lang }\n })\n if (!res.ok) throw new Error(`${res.status}`)\n let content = await res.text()\n if (this.section) content = this._sliceSection(content, this.section)\n this._html = this._wrapTables(content)\n if (!this._pendingAnchor && this._initialAnchor) {\n this._pendingAnchor = this._initialAnchor\n this._initialAnchor = ''\n }\n this._status = 'ready'\n } catch (e) {\n this._status = 'error'\n this._html = ''\n }\n }\n\n /*\n * 표를 가로 스크롤 상자로 감싼다 — **좁은 화면에서 표가 문서 폭을 밀어내지 않게.**\n *\n * 코드 블록(pre)은 이미 자기 안에서 가로로 스크롤한다. 표에는 그 장치가 없어서, 폭이 좁아지면\n * (휴대폰·분할 화면·인라인 도움말 팝업) 비교표가 본문 바깥으로 삐져나갔다. 표는 열 수가 정해져\n * 있어 글자만 줄여서는 들어가지 않으므로, 넘치는 쪽을 스크롤로 받는 것이 정공법이다.\n *\n * 문서(HTML)를 고치지 않고 렌더 시점에 감싸는 이유: 같은 파일을 세 소비자가 함께 쓴다. AI 는 태그를\n * 걷어낸 본문을 읽으므로 표시용 래퍼가 원문에 섞이면 그쪽까지 지저분해진다.\n */\n private _wrapTables(html: string): string {\n if (!html.includes('<table')) return html\n const tpl = document.createElement('template')\n tpl.innerHTML = html\n for (const table of Array.from(tpl.content.querySelectorAll('table'))) {\n if ((table.parentElement as HTMLElement | null)?.classList.contains('tablewrap')) continue\n const wrap = document.createElement('div')\n wrap.className = 'tablewrap'\n table.replaceWith(wrap)\n wrap.appendChild(table)\n }\n return tpl.innerHTML\n }\n\n /** 인라인 스폿 — id=section 인 heading 부터 다음 동급 heading 전까지만 추출(문맥 도움말용). */\n private _sliceSection(full: string, id: string): string {\n const tpl = document.createElement('template')\n tpl.innerHTML = full\n const anchor = tpl.content.querySelector(`#${CSS.escape(id)}`)\n if (!anchor) return full\n const level = anchor.tagName // H1..H3\n const frag: string[] = [(anchor as HTMLElement).outerHTML]\n let n = anchor.nextElementSibling\n while (n && !(/^H[1-6]$/.test(n.tagName) && n.tagName <= level)) { frag.push((n as HTMLElement).outerHTML); n = n.nextElementSibling }\n return frag.join('')\n }\n\n render(): TemplateResult | typeof nothing {\n if (this._status === 'loading') return html`<div class=\"status\">${i18next?.t?.('text.loading') ?? 'Loading…'}</div>`\n if (this._status === 'error') return html`<div class=\"status err\">${i18next?.t?.('text.not found') ?? 'Content not found.'} <code>${this.topic}</code></div>`\n if (this._status !== 'ready') return html`<div class=\"body\"><slot></slot></div>`\n return html`<div class=\"body\">${unsafeHTML(this._html)}</div>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap { 'ox-doc': OxDoc }\n}\n"]}
@@ -1,27 +1,17 @@
1
1
  import { LitElement, nothing, type PropertyValues, type TemplateResult } from 'lit';
2
- import './ox-doc.js';
3
2
  export declare class OxHelpSpot extends LitElement {
4
3
  topic: string;
5
4
  section: string;
6
5
  label: string;
7
- private open;
8
6
  private written;
9
7
  static styles: import("lit").CSSResult;
10
- private toggle;
11
- private close;
12
- private openFull;
8
+ private readonly keep;
9
+ private show;
13
10
  connectedCallback(): void;
14
11
  disconnectedCallback(): void;
15
12
  protected willUpdate(changed: PropertyValues<this>): void;
16
13
  private readonly onLanguageChanged;
17
14
  private check;
18
- /**
19
- * `Escape` 로 닫는다 — **뒷막 누름의 키보드 짝**이다.
20
- *
21
- * 이것이 없으면 도움말을 연 사람이 탭으로 그 안을 다 지나야 빠져나온다. 마우스로 열어 보면 안
22
- * 보이는 부류다(WCAG 2.1 §2.1.1 · §2.1.2).
23
- */
24
- private readonly onKeydown;
25
15
  render(): TemplateResult | typeof nothing;
26
16
  }
27
17
  declare global {
@@ -2,36 +2,16 @@ import { __decorate } from "tslib";
2
2
  import { LitElement, html, css, nothing } from 'lit';
3
3
  import { customElement, property, state } from 'lit/decorators.js';
4
4
  import { i18next } from '@operato/i18n';
5
- import './ox-doc.js';
5
+ import { openHelp } from '../controller/help.js';
6
+ import { documentOf, holds } from '../help-documents.js';
6
7
  /*
7
- * What each /helps/{topic}.html holds, read once per language. A spot asks before it shows its `?`:
8
- * many spots on a page share one topic, and the answer does not change while the page is open.
9
- */
10
- const documents = new Map();
11
- function documentOf(topic, lang) {
12
- const key = `${lang}:${topic}`;
13
- if (!documents.has(key)) {
14
- documents.set(key, fetch(`/helps/${topic}.html?lng=${encodeURIComponent(lang)}`, { headers: { 'accept-language': lang } })
15
- .then(res => (res.ok ? res.text() : null))
16
- .catch(() => null));
17
- }
18
- return documents.get(key);
19
- }
20
- /** The topic is written, and so is the section when one is named. */
21
- function holds(content, section) {
22
- if (!(content === null || content === void 0 ? void 0 : content.trim()))
23
- return false;
24
- if (!section)
25
- return true;
26
- const tpl = document.createElement('template');
27
- tpl.innerHTML = content;
28
- return !!tpl.content.querySelector(`#${CSS.escape(section)}`);
29
- }
30
- /*
31
- * ox-help-spot — 인라인 문맥 도움말(C). 운영 화면 어디서든 `?` 하나로 관련 개념을 "그 자리에서" 연다.
32
- * 같은 단일 소스(helps/{topic}.html)를 ox-doc 의 section 슬라이스 + compact 로 재사용 → 세 번째 소비 표면.
8
+ * ox-help-spot — 인라인 문맥 도움말(C). 운영 화면 어디서든 `?` 하나로 관련 개념을 연다.
33
9
  * 사용: <ox-help-spot topic="twin/concept" section="entities"></ox-help-spot>
34
- * full 링크는 앱이 처리하도록 이벤트(ox-help-open-full) 방출.
10
+ *
11
+ * It opens the help overlay (`openHelp`) on that section — the app's one help surface, drawn on the overlay layer.
12
+ * The spot draws nothing but the `?`: a panel of its own sat inside whatever holds the `?` (a card button, a grid
13
+ * header), so it took that parent's pointer and text settings and handed its scrolling and presses back to it.
14
+ * The overlay's 「open in new」 takes the whole document to the help page.
35
15
  */
36
16
  let OxHelpSpot = class OxHelpSpot extends LitElement {
37
17
  constructor() {
@@ -39,47 +19,30 @@ let OxHelpSpot = class OxHelpSpot extends LitElement {
39
19
  this.topic = '';
40
20
  this.section = '';
41
21
  this.label = '';
42
- this.open = false;
43
22
  /*
44
23
  * Whether there is anything to open. A `?` is placed where the concept is first read, often before
45
24
  * anyone has written its help; until then it stays out of sight. Without this the button opened
46
25
  * 「Content not found」, and a missing section opened the whole document instead of the part asked for.
47
26
  */
48
27
  this.written = false;
28
+ /* The `?` is not a press of what holds it - a card that filters, a header that sorts (as ox-help-icon). */
29
+ this.keep = (e) => e.stopPropagation();
49
30
  /* Another language is another file, which may not be written yet. */
50
31
  this.onLanguageChanged = () => this.check();
51
- /**
52
- * `Escape` 로 닫는다 — **뒷막 누름의 키보드 짝**이다.
53
- *
54
- * 이것이 없으면 도움말을 연 사람이 탭으로 그 안을 다 지나야 빠져나온다. 마우스로 열어 보면 안
55
- * 보이는 부류다(WCAG 2.1 §2.1.1 · §2.1.2).
56
- */
57
- this.onKeydown = (event) => {
58
- if (!this.open || event.key !== 'Escape') {
59
- return;
60
- }
61
- event.preventDefault();
62
- event.stopPropagation();
63
- this.close();
64
- };
65
32
  }
66
- toggle(e) { e.stopPropagation(); this.open = !this.open; }
67
- close() { this.open = false; }
68
- openFull(e) {
33
+ show(e) {
34
+ e.stopPropagation();
69
35
  e.preventDefault();
70
- this.close();
71
- this.dispatchEvent(new CustomEvent('ox-help-open-full', { detail: { topic: this.topic, section: this.section }, bubbles: true, composed: true }));
36
+ openHelp(this.topic, this.section);
72
37
  }
73
38
  connectedCallback() {
74
39
  var _a;
75
40
  super.connectedCallback();
76
- this.addEventListener('keydown', this.onKeydown);
77
41
  this.toggleAttribute('hidden', !this.written);
78
42
  (_a = i18next === null || i18next === void 0 ? void 0 : i18next.on) === null || _a === void 0 ? void 0 : _a.call(i18next, 'languageChanged', this.onLanguageChanged);
79
43
  }
80
44
  disconnectedCallback() {
81
45
  var _a;
82
- this.removeEventListener('keydown', this.onKeydown);
83
46
  (_a = i18next === null || i18next === void 0 ? void 0 : i18next.off) === null || _a === void 0 ? void 0 : _a.call(i18next, 'languageChanged', this.onLanguageChanged);
84
47
  super.disconnectedCallback();
85
48
  }
@@ -96,37 +59,20 @@ let OxHelpSpot = class OxHelpSpot extends LitElement {
96
59
  if (topic === this.topic && section === this.section) {
97
60
  this.written = written;
98
61
  this.toggleAttribute('hidden', !written);
99
- if (!written)
100
- this.open = false;
101
62
  }
102
63
  }
103
64
  render() {
104
- var _a, _b, _c, _d;
105
65
  if (!this.written)
106
66
  return nothing;
107
- return html `
108
- <button class="q" title=${this.label || 'help'} aria-label=${this.label || 'help'} @click=${(e) => this.toggle(e)}>?</button>
109
- ${this.open
110
- ? html `
111
- ${ /*
112
- * 뒷막은 **탭 순서에 넣지 않는다.** 눌러서 닫는 것은 마우스의 편의이고, 키보드로는
113
- * `Escape` 가 그 일을 한다(아래 `onKeydown`). 뒷막에 `tabindex` 를 주면 탭을 눌렀을 때
114
- * 아무것도 아닌 빈 면에 초점이 서고, 낭독기는 읽을 것이 없는 것을 읽는다.
115
- *
116
- * 그래서 보조기술에서 숨긴다 — 이 면은 그림이지 기능이 아니다.
117
- */''}
118
- <div class="backdrop" aria-hidden="true" @click=${() => this.close()}></div>
119
- ${ /* 안쪽 누름이 뒷막까지 새지 않게만 한다 — 기능이 아니라 막는 것이다. */''}
120
- <div class="panel" role="dialog" aria-modal="true" aria-label=${this.label || 'help'} tabindex="-1" @click=${(e) => e.stopPropagation()}>
121
- <div class="bar">
122
- <a href="#" @click=${(e) => this.openFull(e)}>${(_b = (_a = i18next === null || i18next === void 0 ? void 0 : i18next.t) === null || _a === void 0 ? void 0 : _a.call(i18next, 'text.help-open-full')) !== null && _b !== void 0 ? _b : 'open full'} →</a>
123
- <button @click=${() => this.close()}>✕ ${(_d = (_c = i18next === null || i18next === void 0 ? void 0 : i18next.t) === null || _c === void 0 ? void 0 : _c.call(i18next, 'button.close')) !== null && _d !== void 0 ? _d : 'close'}</button>
124
- </div>
125
- <ox-doc compact topic=${this.topic} section=${this.section}></ox-doc>
126
- </div>
127
- `
128
- : ''}
129
- `;
67
+ return html `<button
68
+ class="q"
69
+ title=${this.label || 'help'}
70
+ aria-label=${this.label || 'help'}
71
+ @click=${(e) => this.show(e)}
72
+ @pointerdown=${this.keep}
73
+ @pointerup=${this.keep}
74
+ @mousedown=${this.keep}
75
+ >?</button>`;
130
76
  }
131
77
  };
132
78
  OxHelpSpot.styles = css `
@@ -134,24 +80,8 @@ OxHelpSpot.styles = css `
134
80
  /* 중심 정렬은 담는 쪽 책임 — 제목 행을 flex(align-items:center)로 잡아 광학 중심에 맞춘다.
135
81
  inline 으로 쓰일 때의 기본 정렬만 여기서 vertical-align:middle 로 둔다. */
136
82
  :host { display: inline-flex; align-items: center; vertical-align: middle; --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86)); }
137
- button.q { width: 17px; height: 17px; border-radius: 50%; border: 1px solid color-mix(in oklab, var(--_accent), transparent 45%); background: color-mix(in oklab, var(--_accent), transparent 88%); color: var(--_accent); font: 600 11px/1 var(--ox-doc-sans, sans-serif); cursor: pointer; padding: 0; }
83
+ button.q { width: 17px; height: 17px; border-radius: 50%; border: 1px solid color-mix(in oklab, var(--_accent), transparent 45%); background: color-mix(in oklab, var(--_accent), transparent 88%); color: var(--_accent); font: 600 11px/1 var(--ox-doc-sans, sans-serif); cursor: help; padding: 0; }
138
84
  button.q:hover { background: color-mix(in oklab, var(--_accent), transparent 75%); }
139
- .backdrop { position: fixed; inset: 0; background: rgba(10,15,20,.32); z-index: 9998; }
140
- .panel { position: fixed; z-index: 9999; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(620px, 92vw); max-height: 82vh; overflow: auto; background: var(--ox-doc-panel, var(--ui-panel, #fff)); border: 1px solid var(--ox-doc-line, var(--ui-line, #dde4ea)); border-radius: 14px; box-shadow: 0 20px 60px rgba(0,0,0,.28); padding: 8px 20px 18px; }
141
- /*
142
- * The panel is a document, not part of the words the ? sits beside. Text settings inherit through the shadow
143
- * root, so a ? in a grid header (nowrap, bold) or a right-aligned number column handed them to the panel: its
144
- * paragraphs ran off the edge on one line. They start over here.
145
- */
146
- .panel { white-space: normal; text-align: start; font-weight: normal; font-style: normal; text-transform: none; letter-spacing: normal; }
147
- /* 표준 스크롤바(얇은) — board-placement-tray 와 동일 컨벤션. */
148
- .panel { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb-color, rgba(90, 107, 122, 0.4)) transparent; }
149
- .panel::-webkit-scrollbar { width: 6px; height: 6px; }
150
- .panel::-webkit-scrollbar-track { background: transparent; }
151
- .panel::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb-color, rgba(90, 107, 122, 0.4)); border-radius: 4px; }
152
- .panel::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover-color, rgba(90, 107, 122, 0.6)); }
153
- .bar { position: sticky; top: 0; display: flex; justify-content: flex-end; gap: 10px; padding: 8px 0 4px; }
154
- .bar a, .bar button { font: 600 12px/1 var(--ox-doc-mono, monospace); color: var(--ox-doc-accent, var(--ui-accent, #0d8f86)); background: none; border: 0; cursor: pointer; text-decoration: none; letter-spacing: .04em; }
155
85
  :focus-visible { outline: 2px solid var(--_accent); outline-offset: 2px; }
156
86
  `;
157
87
  __decorate([
@@ -163,9 +93,6 @@ __decorate([
163
93
  __decorate([
164
94
  property({ type: String })
165
95
  ], OxHelpSpot.prototype, "label", void 0);
166
- __decorate([
167
- state()
168
- ], OxHelpSpot.prototype, "open", void 0);
169
96
  __decorate([
170
97
  state()
171
98
  ], OxHelpSpot.prototype, "written", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"ox-help-spot.js","sourceRoot":"","sources":["../../../src/components/ox-help-spot.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, type PropertyValues, type TemplateResult } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport './ox-doc.js'\n\n/*\n * What each /helps/{topic}.html holds, read once per language. A spot asks before it shows its `?`:\n * many spots on a page share one topic, and the answer does not change while the page is open.\n */\nconst documents = new Map<string, Promise<string | null>>()\n\nfunction documentOf(topic: string, lang: string): Promise<string | null> {\n const key = `${lang}:${topic}`\n if (!documents.has(key)) {\n documents.set(\n key,\n fetch(`/helps/${topic}.html?lng=${encodeURIComponent(lang)}`, { headers: { 'accept-language': lang } })\n .then(res => (res.ok ? res.text() : null))\n .catch(() => null)\n )\n }\n return documents.get(key)!\n}\n\n/** The topic is written, and so is the section when one is named. */\nfunction holds(content: string | null, section: string): boolean {\n if (!content?.trim()) return false\n if (!section) return true\n const tpl = document.createElement('template')\n tpl.innerHTML = content\n return !!tpl.content.querySelector(`#${CSS.escape(section)}`)\n}\n\n/*\n * ox-help-spot — 인라인 문맥 도움말(C). 운영 화면 어디서든 `?` 하나로 관련 개념을 \"그 자리에서\" 연다.\n * 같은 단일 소스(helps/{topic}.html)를 ox-doc 의 section 슬라이스 + compact 로 재사용 → 세 번째 소비 표면.\n * 사용: <ox-help-spot topic=\"twin/concept\" section=\"entities\"></ox-help-spot>\n * full 링크는 앱이 처리하도록 이벤트(ox-help-open-full) 방출.\n */\n@customElement('ox-help-spot')\nexport class OxHelpSpot extends LitElement {\n @property({ type: String }) topic = ''\n @property({ type: String }) section = ''\n @property({ type: String }) label = ''\n @state() private open = false\n /*\n * Whether there is anything to open. A `?` is placed where the concept is first read, often before\n * anyone has written its help; until then it stays out of sight. Without this the button opened\n * 「Content not found」, and a missing section opened the whole document instead of the part asked for.\n */\n @state() private written = false\n\n static styles = css`\n :host([hidden]) { display: none; }\n /* 중심 정렬은 담는 쪽 책임 — 제목 행을 flex(align-items:center)로 잡아 광학 중심에 맞춘다.\n inline 으로 쓰일 때의 기본 정렬만 여기서 vertical-align:middle 로 둔다. */\n :host { display: inline-flex; align-items: center; vertical-align: middle; --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86)); }\n button.q { width: 17px; height: 17px; border-radius: 50%; border: 1px solid color-mix(in oklab, var(--_accent), transparent 45%); background: color-mix(in oklab, var(--_accent), transparent 88%); color: var(--_accent); font: 600 11px/1 var(--ox-doc-sans, sans-serif); cursor: pointer; padding: 0; }\n button.q:hover { background: color-mix(in oklab, var(--_accent), transparent 75%); }\n .backdrop { position: fixed; inset: 0; background: rgba(10,15,20,.32); z-index: 9998; }\n .panel { position: fixed; z-index: 9999; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(620px, 92vw); max-height: 82vh; overflow: auto; background: var(--ox-doc-panel, var(--ui-panel, #fff)); border: 1px solid var(--ox-doc-line, var(--ui-line, #dde4ea)); border-radius: 14px; box-shadow: 0 20px 60px rgba(0,0,0,.28); padding: 8px 20px 18px; }\n /*\n * The panel is a document, not part of the words the ? sits beside. Text settings inherit through the shadow\n * root, so a ? in a grid header (nowrap, bold) or a right-aligned number column handed them to the panel: its\n * paragraphs ran off the edge on one line. They start over here.\n */\n .panel { white-space: normal; text-align: start; font-weight: normal; font-style: normal; text-transform: none; letter-spacing: normal; }\n /* 표준 스크롤바(얇은) — board-placement-tray 와 동일 컨벤션. */\n .panel { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb-color, rgba(90, 107, 122, 0.4)) transparent; }\n .panel::-webkit-scrollbar { width: 6px; height: 6px; }\n .panel::-webkit-scrollbar-track { background: transparent; }\n .panel::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb-color, rgba(90, 107, 122, 0.4)); border-radius: 4px; }\n .panel::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover-color, rgba(90, 107, 122, 0.6)); }\n .bar { position: sticky; top: 0; display: flex; justify-content: flex-end; gap: 10px; padding: 8px 0 4px; }\n .bar a, .bar button { font: 600 12px/1 var(--ox-doc-mono, monospace); color: var(--ox-doc-accent, var(--ui-accent, #0d8f86)); background: none; border: 0; cursor: pointer; text-decoration: none; letter-spacing: .04em; }\n :focus-visible { outline: 2px solid var(--_accent); outline-offset: 2px; }\n `\n\n private toggle(e: Event) { e.stopPropagation(); this.open = !this.open }\n private close() { this.open = false }\n private openFull(e: Event) {\n e.preventDefault()\n this.close()\n this.dispatchEvent(new CustomEvent('ox-help-open-full', { detail: { topic: this.topic, section: this.section }, bubbles: true, composed: true }))\n }\n\n connectedCallback(): void {\n super.connectedCallback()\n this.addEventListener('keydown', this.onKeydown)\n this.toggleAttribute('hidden', !this.written)\n i18next?.on?.('languageChanged', this.onLanguageChanged)\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('keydown', this.onKeydown)\n i18next?.off?.('languageChanged', this.onLanguageChanged)\n super.disconnectedCallback()\n }\n\n protected willUpdate(changed: PropertyValues<this>): void {\n if (changed.has('topic') || changed.has('section')) {\n this.check()\n }\n }\n\n /* Another language is another file, which may not be written yet. */\n private readonly onLanguageChanged = () => this.check()\n\n private async check(): Promise<void> {\n const { topic, section } = this\n const lang = i18next?.language ?? 'en'\n const written = !!topic && holds(await documentOf(topic, lang), section)\n if (topic === this.topic && section === this.section) {\n this.written = written\n this.toggleAttribute('hidden', !written)\n if (!written) this.open = false\n }\n }\n\n /**\n * `Escape` 로 닫는다 — **뒷막 누름의 키보드 짝**이다.\n *\n * 이것이 없으면 도움말을 연 사람이 탭으로 그 안을 다 지나야 빠져나온다. 마우스로 열어 보면 안\n * 보이는 부류다(WCAG 2.1 §2.1.1 · §2.1.2).\n */\n private readonly onKeydown = (event: KeyboardEvent) => {\n if (!this.open || event.key !== 'Escape') {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n this.close()\n }\n\n render(): TemplateResult | typeof nothing {\n if (!this.written) return nothing\n return html`\n <button class=\"q\" title=${this.label || 'help'} aria-label=${this.label || 'help'} @click=${(e: Event) => this.toggle(e)}>?</button>\n ${this.open\n ? html`\n ${/*\n * 뒷막은 **탭 순서에 넣지 않는다.** 눌러서 닫는 것은 마우스의 편의이고, 키보드로는\n * `Escape` 가 그 일을 한다(아래 `onKeydown`). 뒷막에 `tabindex` 를 주면 탭을 눌렀을 때\n * 아무것도 아닌 빈 면에 초점이 서고, 낭독기는 읽을 것이 없는 것을 읽는다.\n *\n * 그래서 보조기술에서 숨긴다 — 이 면은 그림이지 기능이 아니다.\n */ ''}\n <div class=\"backdrop\" aria-hidden=\"true\" @click=${() => this.close()}></div>\n ${/* 안쪽 누름이 뒷막까지 새지 않게만 한다 — 기능이 아니라 막는 것이다. */ ''}\n <div class=\"panel\" role=\"dialog\" aria-modal=\"true\" aria-label=${this.label || 'help'} tabindex=\"-1\" @click=${(e: Event) => e.stopPropagation()}>\n <div class=\"bar\">\n <a href=\"#\" @click=${(e: Event) => this.openFull(e)}>${i18next?.t?.('text.help-open-full') ?? 'open full'} →</a>\n <button @click=${() => this.close()}>✕ ${i18next?.t?.('button.close') ?? 'close'}</button>\n </div>\n <ox-doc compact topic=${this.topic} section=${this.section}></ox-doc>\n </div>\n `\n : ''}\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap { 'ox-help-spot': OxHelpSpot }\n}\n"]}
1
+ {"version":3,"file":"ox-help-spot.js","sourceRoot":"","sources":["../../../src/components/ox-help-spot.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, type PropertyValues, type TemplateResult } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { openHelp } from '../controller/help.js'\nimport { documentOf, holds } from '../help-documents.js'\n\n/*\n * ox-help-spot — 인라인 문맥 도움말(C). 운영 화면 어디서든 `?` 하나로 관련 개념을 연다.\n * 사용: <ox-help-spot topic=\"twin/concept\" section=\"entities\"></ox-help-spot>\n *\n * It opens the help overlay (`openHelp`) on that section — the app's one help surface, drawn on the overlay layer.\n * The spot draws nothing but the `?`: a panel of its own sat inside whatever holds the `?` (a card button, a grid\n * header), so it took that parent's pointer and text settings and handed its scrolling and presses back to it.\n * The overlay's 「open in new」 takes the whole document to the help page.\n */\n@customElement('ox-help-spot')\nexport class OxHelpSpot extends LitElement {\n @property({ type: String }) topic = ''\n @property({ type: String }) section = ''\n @property({ type: String }) label = ''\n /*\n * Whether there is anything to open. A `?` is placed where the concept is first read, often before\n * anyone has written its help; until then it stays out of sight. Without this the button opened\n * 「Content not found」, and a missing section opened the whole document instead of the part asked for.\n */\n @state() private written = false\n\n static styles = css`\n :host([hidden]) { display: none; }\n /* 중심 정렬은 담는 쪽 책임 — 제목 행을 flex(align-items:center)로 잡아 광학 중심에 맞춘다.\n inline 으로 쓰일 때의 기본 정렬만 여기서 vertical-align:middle 로 둔다. */\n :host { display: inline-flex; align-items: center; vertical-align: middle; --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86)); }\n button.q { width: 17px; height: 17px; border-radius: 50%; border: 1px solid color-mix(in oklab, var(--_accent), transparent 45%); background: color-mix(in oklab, var(--_accent), transparent 88%); color: var(--_accent); font: 600 11px/1 var(--ox-doc-sans, sans-serif); cursor: help; padding: 0; }\n button.q:hover { background: color-mix(in oklab, var(--_accent), transparent 75%); }\n :focus-visible { outline: 2px solid var(--_accent); outline-offset: 2px; }\n `\n\n /* The `?` is not a press of what holds it - a card that filters, a header that sorts (as ox-help-icon). */\n private readonly keep = (e: Event) => e.stopPropagation()\n\n private show(e: Event) {\n e.stopPropagation()\n e.preventDefault()\n openHelp(this.topic, this.section)\n }\n\n connectedCallback(): void {\n super.connectedCallback()\n this.toggleAttribute('hidden', !this.written)\n i18next?.on?.('languageChanged', this.onLanguageChanged)\n }\n\n disconnectedCallback(): void {\n i18next?.off?.('languageChanged', this.onLanguageChanged)\n super.disconnectedCallback()\n }\n\n protected willUpdate(changed: PropertyValues<this>): void {\n if (changed.has('topic') || changed.has('section')) {\n this.check()\n }\n }\n\n /* Another language is another file, which may not be written yet. */\n private readonly onLanguageChanged = () => this.check()\n\n private async check(): Promise<void> {\n const { topic, section } = this\n const lang = i18next?.language ?? 'en'\n const written = !!topic && holds(await documentOf(topic, lang), section)\n if (topic === this.topic && section === this.section) {\n this.written = written\n this.toggleAttribute('hidden', !written)\n }\n }\n\n render(): TemplateResult | typeof nothing {\n if (!this.written) return nothing\n return html`<button\n class=\"q\"\n title=${this.label || 'help'}\n aria-label=${this.label || 'help'}\n @click=${(e: Event) => this.show(e)}\n @pointerdown=${this.keep}\n @pointerup=${this.keep}\n @mousedown=${this.keep}\n >?</button>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap { 'ox-help-spot': OxHelpSpot }\n}\n"]}
@@ -1,2 +1,6 @@
1
- export declare function openHelp(topic: string): void;
1
+ /**
2
+ * Opens the help overlay on a topic — and, from a `?`, on the one section of it the reader asked about. Its
3
+ * 「open in new」 takes the whole document to the help page, at that section.
4
+ */
5
+ export declare function openHelp(topic: string, section?: string): void;
2
6
  export declare function closeHelp(): void;
@@ -1,8 +1,13 @@
1
1
  import { closeOverlay, openOverlay } from '@operato/layout';
2
- export function openHelp(topic) {
2
+ /**
3
+ * Opens the help overlay on a topic — and, from a `?`, on the one section of it the reader asked about. Its
4
+ * 「open in new」 takes the whole document to the help page, at that section.
5
+ */
6
+ export function openHelp(topic, section = '') {
3
7
  openOverlay('help', {
4
8
  templateProperties: {
5
- topic
9
+ topic,
10
+ section
6
11
  }
7
12
  });
8
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"help.js","sourceRoot":"","sources":["../../../src/controller/help.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,WAAW,CAAC,MAAM,EAAE;QAClB,kBAAkB,EAAE;YAClB,KAAK;SACN;KACF,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,YAAY,CAAC,MAAM,CAAC,CAAA;AACtB,CAAC","sourcesContent":["import { closeOverlay, openOverlay } from '@operato/layout'\n\nexport function openHelp(topic: string) {\n openOverlay('help', {\n templateProperties: {\n topic\n }\n })\n}\n\nexport function closeHelp() {\n closeOverlay('help')\n}\n"]}
1
+ {"version":3,"file":"help.js","sourceRoot":"","sources":["../../../src/controller/help.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,OAAO,GAAG,EAAE;IAClD,WAAW,CAAC,MAAM,EAAE;QAClB,kBAAkB,EAAE;YAClB,KAAK;YACL,OAAO;SACR;KACF,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,YAAY,CAAC,MAAM,CAAC,CAAA;AACtB,CAAC","sourcesContent":["import { closeOverlay, openOverlay } from '@operato/layout'\n\n/**\n * Opens the help overlay on a topic — and, from a `?`, on the one section of it the reader asked about. Its\n * 「open in new」 takes the whole document to the help page, at that section.\n */\nexport function openHelp(topic: string, section = '') {\n openOverlay('help', {\n templateProperties: {\n topic,\n section\n }\n })\n}\n\nexport function closeHelp() {\n closeOverlay('help')\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /** /helps/{topic}.html in a language, or null when there is none. Many spots on a page share one topic. */
2
+ export declare function documentOf(topic: string, lang: string): Promise<string | null>;
3
+ /** The document is written, and so is the section when one is named. */
4
+ export declare function holds(content: string | null, section: string): boolean;
5
+ /** How a topic is written: `html` when /helps/{topic}.html exists, else the markdown the help page always read. */
6
+ export declare function helpFormatOf(topic: string, lang: string): Promise<'html' | 'md'>;
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Which help documents exist, read once per language — the one answer the `?`, the help overlay and the help page
3
+ * share. A topic is written as /helps/{topic}.html (ox-doc) or, in the older form, /helps/{topic}.md (ox-markdown).
4
+ */
5
+ const documents = new Map();
6
+ /** /helps/{topic}.html in a language, or null when there is none. Many spots on a page share one topic. */
7
+ export function documentOf(topic, lang) {
8
+ const key = `${lang}:${topic}`;
9
+ if (!documents.has(key)) {
10
+ documents.set(key, fetch(`/helps/${topic}.html?lng=${encodeURIComponent(lang)}`, { headers: { 'accept-language': lang } })
11
+ .then(res => (res.ok ? res.text() : null))
12
+ .catch(() => null));
13
+ }
14
+ return documents.get(key);
15
+ }
16
+ /** The document is written, and so is the section when one is named. */
17
+ export function holds(content, section) {
18
+ if (!(content === null || content === void 0 ? void 0 : content.trim()))
19
+ return false;
20
+ if (!section)
21
+ return true;
22
+ const tpl = document.createElement('template');
23
+ tpl.innerHTML = content;
24
+ return !!tpl.content.querySelector(`#${CSS.escape(section)}`);
25
+ }
26
+ /** How a topic is written: `html` when /helps/{topic}.html exists, else the markdown the help page always read. */
27
+ export async function helpFormatOf(topic, lang) {
28
+ var _a;
29
+ return ((_a = (await documentOf(topic, lang))) === null || _a === void 0 ? void 0 : _a.trim()) ? 'html' : 'md';
30
+ }
31
+ //# sourceMappingURL=help-documents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help-documents.js","sourceRoot":"","sources":["../../src/help-documents.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE3D,2GAA2G;AAC3G,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,IAAY;IACpD,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,KAAK,EAAE,CAAA;IAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,SAAS,CAAC,GAAG,CACX,GAAG,EACH,KAAK,CAAC,UAAU,KAAK,aAAa,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,CAAC;aACpG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;aACzC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CACrB,CAAA;IACH,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,CAAC,GAAG,CAAE,CAAA;AAC5B,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,KAAK,CAAC,OAAsB,EAAE,OAAe;IAC3D,IAAI,CAAC,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,IAAI,EAAE,CAAA;QAAE,OAAO,KAAK,CAAA;IAClC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IACzB,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAA;IAC9C,GAAG,CAAC,SAAS,GAAG,OAAO,CAAA;IACvB,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AAC/D,CAAC;AAED,mHAAmH;AACnH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAa,EAAE,IAAY;;IAC5D,OAAO,CAAA,MAAA,CAAC,MAAM,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,0CAAE,IAAI,EAAE,EAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;AAChE,CAAC","sourcesContent":["/*\n * Which help documents exist, read once per language — the one answer the `?`, the help overlay and the help page\n * share. A topic is written as /helps/{topic}.html (ox-doc) or, in the older form, /helps/{topic}.md (ox-markdown).\n */\nconst documents = new Map<string, Promise<string | null>>()\n\n/** /helps/{topic}.html in a language, or null when there is none. Many spots on a page share one topic. */\nexport function documentOf(topic: string, lang: string): Promise<string | null> {\n const key = `${lang}:${topic}`\n if (!documents.has(key)) {\n documents.set(\n key,\n fetch(`/helps/${topic}.html?lng=${encodeURIComponent(lang)}`, { headers: { 'accept-language': lang } })\n .then(res => (res.ok ? res.text() : null))\n .catch(() => null)\n )\n }\n return documents.get(key)!\n}\n\n/** The document is written, and so is the section when one is named. */\nexport function holds(content: string | null, section: string): boolean {\n if (!content?.trim()) return false\n if (!section) return true\n const tpl = document.createElement('template')\n tpl.innerHTML = content\n return !!tpl.content.querySelector(`#${CSS.escape(section)}`)\n}\n\n/** How a topic is written: `html` when /helps/{topic}.html exists, else the markdown the help page always read. */\nexport async function helpFormatOf(topic: string, lang: string): Promise<'html' | 'md'> {\n return (await documentOf(topic, lang))?.trim() ? 'html' : 'md'\n}\n"]}
@@ -1 +1,2 @@
1
1
  import '@material/web/icon/icon.js';
2
+ import '../components/ox-doc.js';
@@ -1,14 +1,18 @@
1
1
  import { __decorate } from "tslib";
2
2
  import '@material/web/icon/icon.js';
3
+ import '../components/ox-doc.js';
3
4
  import { css, html } from 'lit';
4
- import { customElement, property, query } from 'lit/decorators.js';
5
+ import { customElement, property, query, state } from 'lit/decorators.js';
5
6
  import Headroom from '@operato/headroom';
6
7
  import { i18next, localize } from '@operato/i18n';
7
8
  import { navigate, PageView } from '@operato/shell';
8
9
  import { HeadroomStyles, ScrollbarStyles } from '@operato/styles';
10
+ import { helpFormatOf } from '../help-documents.js';
9
11
  let HelpHome = class HelpHome extends localize(i18next)(PageView) {
10
12
  constructor() {
11
13
  super(...arguments);
14
+ /** The section to open at - the part a `?` showed in the help overlay. */
15
+ this.section = '';
12
16
  this.showGotoTop = false;
13
17
  }
14
18
  get context() {
@@ -19,7 +23,13 @@ let HelpHome = class HelpHome extends localize(i18next)(PageView) {
19
23
  render() {
20
24
  const src = this.topic && `/helps/${this.topic || 'index'}.md`;
21
25
  return html `
22
- <ox-markdown id="content" .src=${src} toc></ox-markdown>
26
+ <div id="content">
27
+ ${this.format === 'html'
28
+ ? html `<ox-doc topic=${this.topic} anchor=${this.section}></ox-doc>`
29
+ : this.format === 'md'
30
+ ? html `<ox-markdown .src=${src} toc></ox-markdown>`
31
+ : ''}
32
+ </div>
23
33
 
24
34
  <div id="navigation">
25
35
  <md-icon @click=${() => navigate('help')} ?disabled=${this.topic == 'index'}>home</md-icon>
@@ -48,9 +58,20 @@ let HelpHome = class HelpHome extends localize(i18next)(PageView) {
48
58
  async pageUpdated(changes, lifecycle) {
49
59
  if (this.active) {
50
60
  this.topic = lifecycle.params['topic'] || 'index';
61
+ this.section = lifecycle.params['section'] || '';
51
62
  this.content.scrollTop = 0;
52
63
  }
53
64
  }
65
+ updated(changes) {
66
+ var _a, _b;
67
+ (_a = super.updated) === null || _a === void 0 ? void 0 : _a.call(this, changes);
68
+ if (changes.has('topic')) {
69
+ const topic = this.topic;
70
+ this.format = undefined;
71
+ if (topic)
72
+ helpFormatOf(topic, (_b = i18next === null || i18next === void 0 ? void 0 : i18next.language) !== null && _b !== void 0 ? _b : 'en').then(format => topic === this.topic && (this.format = format));
73
+ }
74
+ }
54
75
  gotoTop(e) {
55
76
  this.content.scrollTop = 0;
56
77
  e.stopPropagation();
@@ -111,9 +132,15 @@ HelpHome.styles = [
111
132
  __decorate([
112
133
  property({ type: String })
113
134
  ], HelpHome.prototype, "topic", void 0);
135
+ __decorate([
136
+ property({ type: String })
137
+ ], HelpHome.prototype, "section", void 0);
114
138
  __decorate([
115
139
  property({ type: Boolean })
116
140
  ], HelpHome.prototype, "showGotoTop", void 0);
141
+ __decorate([
142
+ state()
143
+ ], HelpHome.prototype, "format", void 0);
117
144
  __decorate([
118
145
  query('#content')
119
146
  ], HelpHome.prototype, "content", void 0);