@operato/help 10.29.18 → 10.29.21

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.
@@ -0,0 +1,34 @@
1
+ import { LitElement, nothing, type TemplateResult } from 'lit';
2
+ export declare class OxDoc extends LitElement {
3
+ /** 토픽 경로(예: "twin/concept"). 서버가 로케일 파일로 해석. */
4
+ topic: string;
5
+ /** 콘텐츠 베이스 경로(정적 파일 서빙 루트). 기본 프레임워크 help 규약. */
6
+ base: string;
7
+ /** 인라인 스폿용 — 지정 시 해당 #anchor 섹션만 노출(풀도큐먼트는 미지정). */
8
+ section: string;
9
+ /** 인라인 컴팩트 모드(패널/오버레이용 여백 축소). */
10
+ compact: boolean;
11
+ private _html;
12
+ private _status;
13
+ private _onClick;
14
+ /** 문서를 바꾸며 지정된 절 — 렌더가 끝난 뒤에 옮겨야 하므로 한 박자 미뤄 둔다. */
15
+ private _pendingAnchor;
16
+ /** 주소에 붙은 #절은 첫 문서를 그린 뒤 한 번만 적용한다. 이후 토픽 이동에 낡은 해시가 따라가지 않게 비운다. */
17
+ private _initialAnchor;
18
+ private _scrollToAnchor;
19
+ private _onLangChange;
20
+ connectedCallback(): void;
21
+ disconnectedCallback(): void;
22
+ static styles: import("lit").CSSResult;
23
+ updated(changed: Map<string, unknown>): void;
24
+ private _load;
25
+ private _wrapTables;
26
+ /** 인라인 스폿 — id=section 인 heading 부터 다음 동급 heading 전까지만 추출(문맥 도움말용). */
27
+ private _sliceSection;
28
+ render(): TemplateResult | typeof nothing;
29
+ }
30
+ declare global {
31
+ interface HTMLElementTagNameMap {
32
+ 'ox-doc': OxDoc;
33
+ }
34
+ }
@@ -0,0 +1,361 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement, html, css } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
5
+ import { i18next } from '@operato/i18n';
6
+ /*
7
+ * ox-doc — ox-markdown 의 "리치 콘텐츠" 짝(peer). 범용 리치 문서 렌더러.
8
+ * 프레임워크 help 의 구조를 그대로 재사용: 콘텐츠 = 정적 파일(소스 아님), 로케일은 서버 라우터가 해석.
9
+ * 호출부는 topic 하나만 준다 → /helps/{topic}.html 을 fetch, 서버가 {topic}.{locale}.html 로 리라이트
10
+ * (다국어 링크 복잡함 제거). 리치 HTML 을 shadow DOM 에 렌더하고, 콘텐츠 속 범용 부품
11
+ * (ox-figure/ox-stage-ladder/…)은 등록 커스텀 엘리먼트로 upgrade 되어 살아난다.
12
+ *
13
+ * 세 소비자: 풀도큐먼트(그대로) · 인라인 스폿(section 앵커 하나) · AI(원문 텍스트는 소비자가 파일에서 추출).
14
+ *
15
+ * Moved here from operato-plant, -twin, -warehouse and -figure, which each kept a copy (2026-10-10).
16
+ * `ox-help-panel` reads the same /helps/{topic} path as markdown; whether help keeps two formats is open.
17
+ */
18
+ let OxDoc = class OxDoc extends LitElement {
19
+ constructor() {
20
+ super(...arguments);
21
+ /** 토픽 경로(예: "twin/concept"). 서버가 로케일 파일로 해석. */
22
+ this.topic = '';
23
+ /** 콘텐츠 베이스 경로(정적 파일 서빙 루트). 기본 프레임워크 help 규약. */
24
+ this.base = '/helps';
25
+ /** 인라인 스폿용 — 지정 시 해당 #anchor 섹션만 노출(풀도큐먼트는 미지정). */
26
+ this.section = '';
27
+ /** 인라인 컴팩트 모드(패널/오버레이용 여백 축소). */
28
+ this.compact = false;
29
+ this._html = '';
30
+ this._status = 'idle';
31
+ /*
32
+ * 콘텐츠 내부 토픽 링크(data-topic="twin/routing") 가로채 제자리 이동 — 앱 라우팅 무관, 자족적.
33
+ *
34
+ * 값에 앵커가 붙을 수 있다(`twin/standards-conformance#ours` = "그 문서의 그 절"). 앵커를 떼지 않으면
35
+ * 그대로 파일 경로가 되어 `…-conformance#ours.html` 을 받아 오려 하고, 브라우저가 `#` 뒤를 잘라
36
+ * 확장자 없는 주소를 요청한다 — **문서를 못 찾는다.** 실제로 세 링크가 그렇게 죽어 있었다.
37
+ */
38
+ this._onClick = (e) => {
39
+ var _a;
40
+ const link = e.composedPath().find(el => el instanceof HTMLElement && el.hasAttribute('data-topic'));
41
+ if (!link) {
42
+ const hashLink = e.composedPath().find(el => { var _a; return el instanceof HTMLAnchorElement && ((_a = el.getAttribute('href')) === null || _a === void 0 ? void 0 : _a.startsWith('#')); });
43
+ const hash = (_a = hashLink === null || hashLink === void 0 ? void 0 : hashLink.getAttribute('href')) === null || _a === void 0 ? void 0 : _a.slice(1);
44
+ if (hash) {
45
+ e.preventDefault();
46
+ this._scrollToAnchor(decodeURIComponent(hash));
47
+ }
48
+ return;
49
+ }
50
+ e.preventDefault();
51
+ const raw = link.getAttribute('data-topic');
52
+ if (!raw)
53
+ return;
54
+ const [topic, hash] = raw.split('#');
55
+ if (topic && topic !== this.topic) {
56
+ this._pendingAnchor = hash || '';
57
+ this.topic = topic;
58
+ // 스크롤 복귀는 호스트(스크롤 컨테이너 소유자)에 위임 — scrollIntoView 는 앱 전역을 밀어 헤더가 사라짐.
59
+ this.dispatchEvent(new CustomEvent('ox-doc-topic', { detail: { topic, hash: hash || '' }, bubbles: true, composed: true }));
60
+ }
61
+ else if (hash) {
62
+ this._scrollToAnchor(hash); // 같은 문서 안의 절로 — 문서를 다시 받아 오지 않는다
63
+ }
64
+ };
65
+ /** 문서를 바꾸며 지정된 절 — 렌더가 끝난 뒤에 옮겨야 하므로 한 박자 미뤄 둔다. */
66
+ this._pendingAnchor = '';
67
+ /** 주소에 붙은 #절은 첫 문서를 그린 뒤 한 번만 적용한다. 이후 토픽 이동에 낡은 해시가 따라가지 않게 비운다. */
68
+ this._initialAnchor = (() => {
69
+ try {
70
+ return location.hash ? decodeURIComponent(location.hash.slice(1)) : '';
71
+ }
72
+ catch (_a) {
73
+ return '';
74
+ }
75
+ })();
76
+ this._onLangChange = () => {
77
+ this._load();
78
+ };
79
+ }
80
+ /*
81
+ * 그 절로 옮긴다 — **가장 가까운 스크롤 조상만** 움직인다.
82
+ *
83
+ * `scrollIntoView` 를 쓰지 않는 이유: 앱 전역이 밀려 헤더가 사라진다(이 부품이 처음부터 피해 온 것).
84
+ * shadow 경계를 넘어 host 로 거슬러 올라가며 실제로 스크롤하는 상자를 찾고, 그 상자의 스크롤만 옮긴다.
85
+ */
86
+ _scrollToAnchor(id) {
87
+ var _a, _b, _c, _d, _e;
88
+ const el = (_b = (_a = this.renderRoot) === null || _a === void 0 ? void 0 : _a.querySelector) === null || _b === void 0 ? void 0 : _b.call(_a, `#${CSS.escape(id)}`);
89
+ if (!el)
90
+ return;
91
+ let cursor = this;
92
+ while (cursor) {
93
+ const next = (_c = cursor.parentElement) !== null && _c !== void 0 ? _c : (_e = (_d = cursor.getRootNode) === null || _d === void 0 ? void 0 : _d.call(cursor)) === null || _e === void 0 ? void 0 : _e.host;
94
+ if (!next)
95
+ break;
96
+ const st = getComputedStyle(next);
97
+ if (/(auto|scroll)/.test(st.overflowY) && next.scrollHeight > next.clientHeight) {
98
+ next.scrollTop += el.getBoundingClientRect().top - next.getBoundingClientRect().top - 12;
99
+ return;
100
+ }
101
+ cursor = next;
102
+ }
103
+ }
104
+ connectedCallback() {
105
+ var _a;
106
+ super.connectedCallback();
107
+ this.addEventListener('click', this._onClick);
108
+ (_a = i18next === null || i18next === void 0 ? void 0 : i18next.on) === null || _a === void 0 ? void 0 : _a.call(i18next, 'languageChanged', this._onLangChange);
109
+ }
110
+ disconnectedCallback() {
111
+ var _a;
112
+ super.disconnectedCallback();
113
+ this.removeEventListener('click', this._onClick);
114
+ (_a = i18next === null || i18next === void 0 ? void 0 : i18next.off) === null || _a === void 0 ? void 0 : _a.call(i18next, 'languageChanged', this._onLangChange);
115
+ }
116
+ updated(changed) {
117
+ if (changed.has('topic') || changed.has('base'))
118
+ this._load();
119
+ /* 새 문서가 다 그려진 뒤에야 그 절로 옮길 수 있다 — 링크에 앵커가 있었으면 여기서 처리한다. */
120
+ if (this._pendingAnchor && this._status === 'ready') {
121
+ const id = this._pendingAnchor;
122
+ this._pendingAnchor = '';
123
+ requestAnimationFrame(() => this._scrollToAnchor(id));
124
+ }
125
+ }
126
+ async _load() {
127
+ var _a;
128
+ if (!this.topic) {
129
+ this._status = 'idle';
130
+ this._html = '';
131
+ return;
132
+ }
133
+ this._status = 'loading';
134
+ try {
135
+ // 로케일은 서버 라우터가 해석(topic.html → topic.{locale}.html). 클라는 topic 만 안다.
136
+ // 쿠키 우선순위로 인해 en-US가 고정되는 현상을 방지하기 위해 쿼리스트링(?lng=)을 함께 전달한다.
137
+ const lang = (_a = i18next === null || i18next === void 0 ? void 0 : i18next.language) !== null && _a !== void 0 ? _a : 'en';
138
+ const res = await fetch(`${this.base}/${this.topic}.html?lng=${encodeURIComponent(lang)}`, {
139
+ headers: { 'accept-language': lang }
140
+ });
141
+ if (!res.ok)
142
+ throw new Error(`${res.status}`);
143
+ let content = await res.text();
144
+ if (this.section)
145
+ content = this._sliceSection(content, this.section);
146
+ this._html = this._wrapTables(content);
147
+ if (!this._pendingAnchor && this._initialAnchor) {
148
+ this._pendingAnchor = this._initialAnchor;
149
+ this._initialAnchor = '';
150
+ }
151
+ this._status = 'ready';
152
+ }
153
+ catch (e) {
154
+ this._status = 'error';
155
+ this._html = '';
156
+ }
157
+ }
158
+ /*
159
+ * 표를 가로 스크롤 상자로 감싼다 — **좁은 화면에서 표가 문서 폭을 밀어내지 않게.**
160
+ *
161
+ * 코드 블록(pre)은 이미 자기 안에서 가로로 스크롤한다. 표에는 그 장치가 없어서, 폭이 좁아지면
162
+ * (휴대폰·분할 화면·인라인 도움말 팝업) 비교표가 본문 바깥으로 삐져나갔다. 표는 열 수가 정해져
163
+ * 있어 글자만 줄여서는 들어가지 않으므로, 넘치는 쪽을 스크롤로 받는 것이 정공법이다.
164
+ *
165
+ * 문서(HTML)를 고치지 않고 렌더 시점에 감싸는 이유: 같은 파일을 세 소비자가 함께 쓴다. AI 는 태그를
166
+ * 걷어낸 본문을 읽으므로 표시용 래퍼가 원문에 섞이면 그쪽까지 지저분해진다.
167
+ */
168
+ _wrapTables(html) {
169
+ var _a;
170
+ if (!html.includes('<table'))
171
+ return html;
172
+ const tpl = document.createElement('template');
173
+ tpl.innerHTML = html;
174
+ for (const table of Array.from(tpl.content.querySelectorAll('table'))) {
175
+ if ((_a = table.parentElement) === null || _a === void 0 ? void 0 : _a.classList.contains('tablewrap'))
176
+ continue;
177
+ const wrap = document.createElement('div');
178
+ wrap.className = 'tablewrap';
179
+ table.replaceWith(wrap);
180
+ wrap.appendChild(table);
181
+ }
182
+ return tpl.innerHTML;
183
+ }
184
+ /** 인라인 스폿 — id=section 인 heading 부터 다음 동급 heading 전까지만 추출(문맥 도움말용). */
185
+ _sliceSection(full, id) {
186
+ const tpl = document.createElement('template');
187
+ tpl.innerHTML = full;
188
+ const anchor = tpl.content.querySelector(`#${CSS.escape(id)}`);
189
+ if (!anchor)
190
+ return full;
191
+ const level = anchor.tagName; // H1..H3
192
+ const frag = [anchor.outerHTML];
193
+ let n = anchor.nextElementSibling;
194
+ while (n && !(/^H[1-6]$/.test(n.tagName) && n.tagName <= level)) {
195
+ frag.push(n.outerHTML);
196
+ n = n.nextElementSibling;
197
+ }
198
+ return frag.join('');
199
+ }
200
+ render() {
201
+ var _a, _b, _c, _d;
202
+ if (this._status === 'loading')
203
+ return html `<div class="status">${(_b = (_a = i18next === null || i18next === void 0 ? void 0 : i18next.t) === null || _a === void 0 ? void 0 : _a.call(i18next, 'text.loading')) !== null && _b !== void 0 ? _b : 'Loading…'}</div>`;
204
+ if (this._status === 'error')
205
+ return html `<div class="status err">${(_d = (_c = i18next === null || i18next === void 0 ? void 0 : i18next.t) === null || _c === void 0 ? void 0 : _c.call(i18next, 'text.not found')) !== null && _d !== void 0 ? _d : 'Content not found.'} <code>${this.topic}</code></div>`;
206
+ if (this._status !== 'ready')
207
+ return html `<div class="body"><slot></slot></div>`;
208
+ return html `<div class="body">${unsafeHTML(this._html)}</div>`;
209
+ }
210
+ };
211
+ OxDoc.styles = css `
212
+ :host {
213
+ /* 테마 토큰 해소 순서: 호스트 오버라이드(--ox-doc-*) → 앱 토큰(--ui-*) → 리터럴 최후 폴백.
214
+ * 앱 안에서는 앱 테마를 그대로 따르고(모드 전환도 앱 설정에 종속), 앱 밖 단독 사용도 리터럴로 성립한다.
215
+ * OS 취향(prefers-color-scheme)으로 모드를 뒤집지 않는다 — 앱 설정과 어긋나 문서만 다크가 되던 결함.
216
+ *
217
+ * 가운데 층이 한때 --ink / --panel / --line 이었다. 주석에는 「앱 팔레트」라고 적혀 있었지만
218
+ * 그 이름을 정하는 곳이 **어디에도 없었다.** 그래서 앱 안에서도 늘 리터럴로 떨어졌고,
219
+ * 다크에서 제목이 검은 글자로 남아 바탕에 묻혔다. 앱의 실제 이름은 --ui-* 다
220
+ * (packages/shell/client/themes/ui-tokens.css). */
221
+ --_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));
222
+ --_line: var(--ox-doc-line, var(--ui-line, #dde4ea)); --_panel: var(--ox-doc-panel, var(--ui-panel, #fff)); --_ground: var(--ox-doc-ground, transparent);
223
+ --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86));
224
+ /* 링크·강조 글자. 강조색을 본문색 쪽으로 당겨 **글자로 읽힐 만큼** 대비를 준다 —
225
+ 라이트에서는 짙어지고 다크에서는 밝아진다. 한 식이 두 테마를 다 맡는다. */
226
+ --_accent-deep: var(--ox-doc-accent-deep, color-mix(in oklab, var(--_accent), var(--_ink) 22%));
227
+ --_accent-wash: var(--ox-doc-accent-wash, var(--ui-accent-wash, #e3f2f0));
228
+ /*
229
+ * 도판이 놓이는 **종이 면.** 테마를 따르지 않는다 — 따르면 안 된다.
230
+ *
231
+ * 그림의 잉크는 문서 파일 안에 박혀 있다(서른 편의 svg 안에 밝은 회색·짙은 남색이 1064곳).
232
+ * 다크에서 그 아래에 어두운 면을 깔면 선과 글자가 바탕에 묻힌다. 그래서 도판만 밝은 종이
233
+ * 위에 올린다 — 어두운 화면에 끼워 넣은 인쇄물처럼 읽힌다.
234
+ *
235
+ * 그림 자체가 테마를 따르게 하려면 서른 편의 svg 를 토큰으로 다시 그려야 한다. 그것은
236
+ * 문서를 쓰는 쪽의 일이다.
237
+ */
238
+ --_paper: var(--ox-doc-paper, #fff);
239
+ --_maxw: var(--ox-doc-maxw, 760px);
240
+ --_sans: var(--ox-doc-sans, -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Pretendard", "Malgun Gothic", "Noto Sans KR", system-ui, sans-serif);
241
+ --_mono: var(--ox-doc-mono, "SF Mono", "JetBrains Mono", ui-monospace, Menlo, monospace);
242
+ display: block; color: var(--_ink); background: var(--_ground);
243
+ font-family: var(--_sans); line-height: 1.72; font-size: 16.5px;
244
+ }
245
+ /*
246
+ * ── 문서가 쓰는 이름 ────────────────────────────────────────────────────
247
+ * 문서(helps/*.html)는 자기 style 블록에서 --x-ink · --x-line · --x-panel · --x-wash ·
248
+ * --x-accent 를 쓴다. 서른 편이 모두 그렇게 적어 두었는데 **그 이름을 정하는 곳이 없었다.**
249
+ * 그래서 늘 리터럴(#fff · #cbd5dd)로 떨어졌고, 다크에서 문서 안의 카드와 표만 흰 조각으로
250
+ * 남았다.
251
+ *
252
+ * 여기서 정한다. 문서를 한 줄도 고치지 않고 서른 편이 함께 테마를 따른다.
253
+ */
254
+ .body {
255
+ --x-ink: var(--_ink);
256
+ --x-muted: var(--_muted);
257
+ --x-faint: var(--_faint);
258
+ --x-line: var(--_line);
259
+ --x-panel: var(--_panel);
260
+ --x-wash: color-mix(in oklab, var(--_line), transparent 55%);
261
+ --x-accent: var(--_accent);
262
+ /* 글자로 쓰는 강조색. 순수 강조색은 12px 라벨에서 라이트 3.79 · 다크에서도 아슬아슬하다. */
263
+ --x-accent-deep: var(--_accent-deep);
264
+ }
265
+ .body { max-width: var(--_maxw); margin: 0 auto; padding: var(--_pad, 10px 6px 48px); }
266
+ /*
267
+ * 한글 줄바꿈 — **낱글자 단위로 끊지 않는다.**
268
+ *
269
+ * 기본값은 한글을 어느 글자에서나 끊는다. 그래서 제목이 두 줄이 되면 "시간이 지나 / 도",
270
+ * "예측 시뮬레 / 이션은" 처럼 어절 중간이 어긋났다(배우기 제목 중 두 줄로 넘어가는 11개 가운데
271
+ * 9개가 그랬다). keep-all 은 띄어쓰기 자리에서만 끊으므로 어절이 살아남는다. 영문은 원래
272
+ * 띄어쓰기로 끊으므로 달라지는 것이 없다.
273
+ *
274
+ * 끊을 자리가 없는 긴 토큰(URN·식별자)은 그대로 두면 문서 폭을 밀어내므로 overflow-wrap 으로 받는다.
275
+ */
276
+ .body { word-break: keep-all; overflow-wrap: break-word; }
277
+ :host([compact]) .body { padding: 4px 2px 12px; font-size: 15px; }
278
+ /* 렌더된 콘텐츠 타이포그래피(unsafeHTML 산출물 — shadow 내부 스코프). 견본 수준 스케일·리듬. */
279
+ /*
280
+ * 조판 예외: **읽는 문서의 눈금이다.** 화면 조판(--ui-t-*)은 관리 화면의 절 제목과 표를 위한
281
+ * 다섯 단이라 긴 글에는 단이 모자라고, 좁은 화면에서 제목이 본문을 밀어내지 않도록 반응형이
282
+ * 필요하다. 그래서 clamp 로 둔다.
283
+ */
284
+ .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; }
285
+ .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; }
286
+ .body h2:first-of-type { border-top: 0; padding-top: 0; }
287
+ .body h3 { font-size: 17px; font-weight: 680; margin: 1.4em 0 .35em; }
288
+ .body p { margin: 0 0 1.05em; }
289
+ .body h1 + p { font-size: 1.06em; color: var(--_muted); } /* 리드 문단(h1 바로 뒤) */
290
+ /* 히어로 아이브로우 — 펄스 점 + mono 라벨 */
291
+ .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; }
292
+ .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; }
293
+ @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%); } }
294
+ @media (prefers-reduced-motion: reduce) { .body .eyebrow::before { animation: none; } }
295
+ /* 섹션 키커 — 흐린 작은 라벨(제목 위). 섹션 구분선은 여기로 이동. */
296
+ /* 절 이름이다 — 문서를 훑을 때 눈이 짚는 자리. --_faint 로는 다크에서 3.64 였다(§ th 의 머리말). */
297
+ .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); }
298
+ .body .kicker + h2 { margin: .35em 0 .55em; padding-top: 0; border-top: 0; }
299
+ .body strong { font-weight: 700; }
300
+ .body em { font-style: normal; color: var(--_accent-deep); font-weight: 600; }
301
+ .body ul, .body ol { margin: 0 0 1.15em; padding-left: 1.35em; }
302
+ .body li { margin: .38em 0; }
303
+ .body ul li::marker { color: var(--_accent); }
304
+ .body ol { counter-reset: oxol; list-style: none; padding-left: 0; }
305
+ .body ol > li { counter-increment: oxol; position: relative; padding-left: 2.2em; }
306
+ .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; }
307
+ .body a { color: var(--_accent-deep); text-underline-offset: 3px; cursor: pointer; }
308
+ .body a[data-topic] { text-decoration: none; font-weight: 640; }
309
+ .body a:hover { text-decoration: underline; }
310
+ .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; }
311
+ .body blockquote::before { content: "▸"; color: var(--_accent); font-weight: 700; }
312
+ .body blockquote p { margin: 0; }
313
+ .body code { font-family: var(--_mono); font-size: .86em; background: color-mix(in oklab, var(--_line), transparent 40%); padding: .12em .4em; border-radius: 5px; }
314
+ /* 코드 블록 — 기술 개념 문서는 실물(이벤트·설정)을 보여줘야 이해된다. 없으면 예시를 그림으로
315
+ 모의 내게 되고, 그림은 복사도 검색도 안 된다. 긴 줄은 가로 스크롤(문서 폭을 밀지 않는다). */
316
+ .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); }
317
+ .body pre code { background: none; padding: 0; font-size: inherit; }
318
+ /* 표 = 패널 카드(견본 tiles 감) */
319
+ /* 표는 자기 상자 안에서 가로로 스크롤한다(_wrapTables 가 감싼다) — 좁은 화면에서 본문 폭을 밀어내지 않게. */
320
+ .body .tablewrap { overflow-x: auto; margin: 1.4em 0; }
321
+ .body .tablewrap table { margin: 0; min-width: 100%; width: auto; }
322
+ .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; }
323
+ .body thead { background: color-mix(in oklab, var(--_line), transparent 55%); }
324
+ .body th, .body td { text-align: left; padding: 10px 13px; border-bottom: 1px solid var(--_line); }
325
+ .body tbody tr:last-child td { border-bottom: 0; }
326
+ /*
327
+ * 머리칸은 --_muted 다. --_faint 로 두었더니 채워진 머리행 위에서 대비가 2.8 로 떨어져
328
+ * 열 이름이 안 읽혔다(10.5px 글자다). --_faint 는 바탕 위의 곁말에 쓰는 색이지, 면 위에
329
+ * 올리는 라벨의 색이 아니다.
330
+ */
331
+ .body th { font-family: var(--_mono); font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--_muted); font-weight: 600; }
332
+ .body td:first-child { font-weight: 640; }
333
+ .tablewrap { overflow-x: auto; }
334
+ /* 도판 — 테마 패널 위에 올려 라이트/다크 모드와 자연스럽게 조화되도록 한다. */
335
+ .body svg { display: block; max-width: 100%; box-sizing: border-box; background: var(--_panel); border: 1px solid var(--_line); border-radius: 10px; padding: 12px; }
336
+ .status { color: var(--_muted); font-size: 14px; padding: 24px 4px; }
337
+ .status.err { color: var(--ui-crit, #c0492f); }
338
+ `;
339
+ __decorate([
340
+ property({ type: String })
341
+ ], OxDoc.prototype, "topic", void 0);
342
+ __decorate([
343
+ property({ type: String })
344
+ ], OxDoc.prototype, "base", void 0);
345
+ __decorate([
346
+ property({ type: String })
347
+ ], OxDoc.prototype, "section", void 0);
348
+ __decorate([
349
+ property({ type: Boolean, reflect: true })
350
+ ], OxDoc.prototype, "compact", void 0);
351
+ __decorate([
352
+ state()
353
+ ], OxDoc.prototype, "_html", void 0);
354
+ __decorate([
355
+ state()
356
+ ], OxDoc.prototype, "_status", void 0);
357
+ OxDoc = __decorate([
358
+ customElement('ox-doc')
359
+ ], OxDoc);
360
+ export { OxDoc };
361
+ //# sourceMappingURL=ox-doc.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,14 @@
1
+ import { LitElement, type TemplateResult } from 'lit';
2
+ export declare class OxFigure extends LitElement {
3
+ /** 캡션(선택). slot="caption" 으로 리치 캡션도 가능. */
4
+ caption: string;
5
+ /** 강조 테두리(핵심 시각물). */
6
+ featured: boolean;
7
+ static styles: import("lit").CSSResult;
8
+ render(): TemplateResult;
9
+ }
10
+ declare global {
11
+ interface HTMLElementTagNameMap {
12
+ 'ox-figure': OxFigure;
13
+ }
14
+ }
@@ -0,0 +1,51 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement, html, css } from 'lit';
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ /*
5
+ * ox-figure — 범용 그림/다이어그램 컨테이너(도메인 무관). 리치 문서의 시각물을 감싸고 캡션을 붙인다.
6
+ * 콘텐츠(SVG/img/커스텀 시각물)는 slot 으로 받으므로, 컴포넌트 미등록 시에도 그대로 노출(폴백 안전).
7
+ */
8
+ let OxFigure = class OxFigure extends LitElement {
9
+ constructor() {
10
+ super(...arguments);
11
+ /** 캡션(선택). slot="caption" 으로 리치 캡션도 가능. */
12
+ this.caption = '';
13
+ /** 강조 테두리(핵심 시각물). */
14
+ this.featured = false;
15
+ }
16
+ render() {
17
+ return html `
18
+ <figure>
19
+ <div class="slot"><slot></slot></div>
20
+ ${this.caption ? html `<figcaption>${this.caption}</figcaption>` : html `<figcaption><slot name="caption"></slot></figcaption>`}
21
+ </figure>
22
+ `;
23
+ }
24
+ };
25
+ OxFigure.styles = css `
26
+ :host {
27
+ /* 해소 순서: 호스트 오버라이드 → 앱 팔레트 → 리터럴(ox-doc 와 동일 규약). */
28
+ --_line: var(--ox-doc-line, var(--ui-line, #dde4ea)); --_panel: var(--ox-doc-panel, var(--ui-panel, #fff)); --_muted: var(--ox-doc-muted, var(--ui-muted, #566573));
29
+ --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86));
30
+ --_mono: var(--ox-doc-mono, "SF Mono", ui-monospace, Menlo, monospace);
31
+ display: block; margin: 20px 0;
32
+ }
33
+ figure { margin: 0; background: var(--_panel); border: 1px solid var(--_line); border-radius: 14px; padding: 20px; }
34
+ :host([featured]) figure { border-color: color-mix(in oklab, var(--_accent), var(--_line) 55%); box-shadow: 0 8px 30px rgba(22,32,43,.06); }
35
+ .slot { display: block; }
36
+ /* 도판은 종이 위에 — 그림의 잉크가 파일 안에 박혀 있어 어두운 면 위에서는 묻힌다(ox-doc 의 --_paper). */
37
+ ::slotted(svg), ::slotted(img) { display: block; width: 100%; height: auto; box-sizing: border-box; background: var(--ox-doc-paper, #fff); border-radius: 8px; }
38
+ /* 캡션은 그림이 무엇인지 말하는 유일한 글자다 — 곁말이 아니라 읽히는 값으로 둔다. */
39
+ figcaption { margin-top: 12px; text-align: center; font-family: var(--_mono); font-size: 12.5px; color: var(--_muted); letter-spacing: .02em; line-height: 1.5; }
40
+ `;
41
+ __decorate([
42
+ property({ type: String })
43
+ ], OxFigure.prototype, "caption", void 0);
44
+ __decorate([
45
+ property({ type: Boolean })
46
+ ], OxFigure.prototype, "featured", void 0);
47
+ OxFigure = __decorate([
48
+ customElement('ox-figure')
49
+ ], OxFigure);
50
+ export { OxFigure };
51
+ //# sourceMappingURL=ox-figure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ox-figure.js","sourceRoot":"","sources":["../../../src/components/ox-figure.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAuB,MAAM,KAAK,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D;;;GAGG;AAEI,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,UAAU;IAAjC;;QACL,2CAA2C;QACf,YAAO,GAAG,EAAE,CAAA;QACxC,sBAAsB;QACO,aAAQ,GAAG,KAAK,CAAA;IA2B/C,CAAC;IARC,MAAM;QACJ,OAAO,IAAI,CAAA;;;UAGL,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,eAAe,IAAI,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,IAAI,CAAA,uDAAuD;;KAEhI,CAAA;IACH,CAAC;;AAxBM,eAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;GAelB,AAfY,CAeZ;AAnB2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;yCAAa;AAEX;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;0CAAiB;AAJlC,QAAQ;IADpB,aAAa,CAAC,WAAW,CAAC;GACd,QAAQ,CA+BpB","sourcesContent":["import { LitElement, html, css, type TemplateResult } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/*\n * ox-figure — 범용 그림/다이어그램 컨테이너(도메인 무관). 리치 문서의 시각물을 감싸고 캡션을 붙인다.\n * 콘텐츠(SVG/img/커스텀 시각물)는 slot 으로 받으므로, 컴포넌트 미등록 시에도 그대로 노출(폴백 안전).\n */\n@customElement('ox-figure')\nexport class OxFigure extends LitElement {\n /** 캡션(선택). slot=\"caption\" 으로 리치 캡션도 가능. */\n @property({ type: String }) caption = ''\n /** 강조 테두리(핵심 시각물). */\n @property({ type: Boolean }) featured = false\n\n static styles = css`\n :host {\n /* 해소 순서: 호스트 오버라이드 → 앱 팔레트 → 리터럴(ox-doc 와 동일 규약). */\n --_line: var(--ox-doc-line, var(--ui-line, #dde4ea)); --_panel: var(--ox-doc-panel, var(--ui-panel, #fff)); --_muted: var(--ox-doc-muted, var(--ui-muted, #566573));\n --_accent: var(--ox-doc-accent, var(--ui-accent, #0d8f86));\n --_mono: var(--ox-doc-mono, \"SF Mono\", ui-monospace, Menlo, monospace);\n display: block; margin: 20px 0;\n }\n figure { margin: 0; background: var(--_panel); border: 1px solid var(--_line); border-radius: 14px; padding: 20px; }\n :host([featured]) figure { border-color: color-mix(in oklab, var(--_accent), var(--_line) 55%); box-shadow: 0 8px 30px rgba(22,32,43,.06); }\n .slot { display: block; }\n /* 도판은 종이 위에 — 그림의 잉크가 파일 안에 박혀 있어 어두운 면 위에서는 묻힌다(ox-doc 의 --_paper). */\n ::slotted(svg), ::slotted(img) { display: block; width: 100%; height: auto; box-sizing: border-box; background: var(--ox-doc-paper, #fff); border-radius: 8px; }\n /* 캡션은 그림이 무엇인지 말하는 유일한 글자다 — 곁말이 아니라 읽히는 값으로 둔다. */\n figcaption { margin-top: 12px; text-align: center; font-family: var(--_mono); font-size: 12.5px; color: var(--_muted); letter-spacing: .02em; line-height: 1.5; }\n `\n\n render(): TemplateResult {\n return html`\n <figure>\n <div class=\"slot\"><slot></slot></div>\n ${this.caption ? html`<figcaption>${this.caption}</figcaption>` : html`<figcaption><slot name=\"caption\"></slot></figcaption>`}\n </figure>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap { 'ox-figure': OxFigure }\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { LitElement, type TemplateResult } from 'lit';
2
+ import './ox-doc.js';
3
+ export declare class OxHelpSpot extends LitElement {
4
+ topic: string;
5
+ section: string;
6
+ label: string;
7
+ private open;
8
+ static styles: import("lit").CSSResult;
9
+ private toggle;
10
+ private close;
11
+ private openFull;
12
+ connectedCallback(): void;
13
+ disconnectedCallback(): void;
14
+ /**
15
+ * `Escape` 로 닫는다 — **뒷막 누름의 키보드 짝**이다.
16
+ *
17
+ * 이것이 없으면 도움말을 연 사람이 탭으로 그 안을 다 지나야 빠져나온다. 마우스로 열어 보면 안
18
+ * 보이는 부류다(WCAG 2.1 §2.1.1 · §2.1.2).
19
+ */
20
+ private readonly onKeydown;
21
+ render(): TemplateResult;
22
+ }
23
+ declare global {
24
+ interface HTMLElementTagNameMap {
25
+ 'ox-help-spot': OxHelpSpot;
26
+ }
27
+ }