@qnroa/qtype 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -13,6 +13,100 @@ can land in any minor bump (`0.x.0`).
13
13
 
14
14
  ## [Unreleased]
15
15
 
16
+ ## [0.2.3] — 2026-09-07
17
+
18
+ Fixes long-standing bugs around Chinese IME input on the typing arena
19
+ and cleans up how "ignore punctuation" behaves. The invisible editor
20
+ is now a `<input>` instead of a contenteditable div, matching the
21
+ pattern every mainstream Chinese typing site uses; character capture
22
+ reads `el.value` on the `input` event, so IME commits arrive as the
23
+ final glyph — no more races between keydown and beforeinput.
24
+
25
+ ### Added
26
+ - **Miss-slot display setting** (Appearance → Miss display). Default
27
+ keeps the target glyph visible in red (matches Monkeytype / keybr
28
+ / ohMyType / ptype conventions — user sees *what should have been
29
+ there*). Flip to "Typed" to render the user's actual keystroke in
30
+ red at the miss slot instead — useful when reviewing exactly what
31
+ went wrong.
32
+
33
+ ### Changed
34
+ - **Editor element is now a hidden `<input>`, not `<div
35
+ contentEditable>`.** IME commits and macOS smart-punctuation
36
+ substitutions land in `el.value` as the final glyph, which the
37
+ `input` event reads and emits to the engine. Composition (pinyin
38
+ preedit) is gated on the event's own `isComposing` /
39
+ `inputType === 'insertCompositionText'` signals rather than the
40
+ hook's state machine, so browsers that fire `input` before
41
+ `compositionstart` can't leak preedit letters through as
42
+ characters.
43
+ - **`Ignore punctuation` now rewrites punctuation to space at engine
44
+ construction.** Previously the engine fast-forwarded through
45
+ punctuation slots inside `onKeyDown`, which left the arena still
46
+ showing punctuation and the on-screen keyboard still hinting the
47
+ punctuation key. Now every punctuation codepoint in the target
48
+ becomes a regular space up-front — the arena, the keyboard hint
49
+ and the engine all see the same target sequence, no divergence.
50
+ Toggling the setting mid-card rebuilds the engine via the
51
+ `useTypingEngine` memo.
52
+
53
+ ### Fixed
54
+ - **Chinese full-width punctuation shown as ASCII.** The old
55
+ keydown-based capture path picked up whichever of `keydown.key` /
56
+ `beforeinput.data` fired first and could substitute a half-width
57
+ glyph. Moving to `<input>` + `input` event and always rendering
58
+ the target glyph (default) puts an end to this class of bug.
59
+ - **Dead `softInsertText` handler removed** from `INPUT_TYPE_HANDLERS`
60
+ and the `handleBeforeInput` fast-path; character emission has a
61
+ single owner now (the `input` event).
62
+
63
+ ## [0.2.2] — 2026-09-01
64
+
65
+ A responsive footer, a convention-based way to swap the site logo
66
+ and favicon without touching code, and a housecleaning rename of the
67
+ public-facing brand assets to drop the redundant `qtype-` prefix.
68
+
69
+ ### Added
70
+ - **Custom site logo via `.qtype/assets/logo.{svg,png}`.** `qtype repo
71
+ new` seeds `<repo>/.qtype/assets/logo.svg` from the language template,
72
+ and `qtype publish build` copies that file over the default
73
+ `logo.<ext>` in `dist/` when it's present. Users edit the file in
74
+ place (keeping the filename `logo.<ext>`) — no config key, no CLI
75
+ flag. Missing file → the built-in default keeps working. Used by the
76
+ Header brand mark; the browser tab now reads a separate `favicon`
77
+ (see below).
78
+ - **Custom favicon via `.qtype/assets/favicon.{svg,png,ico}`.** Same
79
+ convention as the logo — drop a file at this path, `publish build`
80
+ ships it as `dist/favicon.<ext>` and rewrites `index.html`'s
81
+ `<link rel="icon">` to point at the resolved format. Separating
82
+ favicon from logo lets users keep a wide Header brand mark while
83
+ still having a square browser-tab icon.
84
+ - **`data-qtype-logo-ext` on `<html>`** — new boot attribute so the
85
+ Header's runtime `<img>` can point at `/logo.<ext>` with the right
86
+ extension after a build.
87
+
88
+ ### Changed
89
+ - **Public brand assets renamed** to drop the `qtype-` prefix: files
90
+ in `src/view/public/` are now `logo.svg`, `favicon.svg`,
91
+ `wordmark.svg`; the built site emits `dist/logo.<ext>`,
92
+ `dist/favicon.<ext>`, and `dist/wordmark.svg` (unused for now, kept
93
+ around for future brand surfaces). Any external consumer referencing
94
+ `qtype-mark.svg` / `qtype-logo.svg` (the old file names) in
95
+ hard-coded URLs will need to update — but the runtime `<link rel="icon">`
96
+ and Header `<img>` are both regenerated on build so a fresh
97
+ `publish build` in an existing repo is enough.
98
+ - **Footer material name is now responsive.** The left-side title
99
+ truncates with an ellipsis on all viewports (max width 240px on
100
+ desktop, 80px on narrow screens, hidden below 400px) so an unusually
101
+ long material title can't crowd the absolute-centered CardNav or
102
+ push the theme/settings buttons around.
103
+ - **Footer layout wraps the title in an inner span**, which lets the
104
+ outer flex slot keep claiming the remaining space (so the right-side
105
+ buttons stay pinned to the edge) while the inner span alone handles
106
+ the ellipsis. Fixes a mobile-only regression where the settings /
107
+ theme buttons drifted next to the title instead of hugging the right
108
+ edge.
109
+
16
110
  ## [0.2.1] — 2026-09-01
17
111
 
18
112
  Discoverability pass for `qtype config`. Users no longer have to grep
package/CHANGELOG.zh.md CHANGED
@@ -12,6 +12,78 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.2.3] — 2026-09-07
16
+
17
+ 修复打字界面在中文输入法下的一系列老问题,并把"忽略标点"的行为
18
+ 理顺。隐藏的输入编辑器从 contenteditable 换成 `<input>`,和主流
19
+ 中文打字站的做法一致;字符捕获改为在 `input` 事件里读 `el.value`,
20
+ IME 提交后的最终字符直接进引擎 —— 不再有 keydown 和 beforeinput
21
+ 的竞态。
22
+
23
+ ### 新增
24
+ - **错误显示设置**(外观 → 错误显示)。默认保持目标字符可见(红色),
25
+ 和 Monkeytype / keybr / ohMyType / ptype 一致 —— 用户看到的是
26
+ "本来应该是什么"。切到"实际输入"则在错误位置显示用户实际敲入
27
+ 的字符(仍然红色),方便复盘到底敲错在哪。
28
+
29
+ ### 变更
30
+ - **编辑器元素从 `<div contentEditable>` 改为隐藏 `<input>`。** IME
31
+ 提交和 macOS 智能标点替换都会把最终字符写进 `el.value`,由
32
+ `input` 事件读到再喂给引擎。组词状态(拼音预输入)靠事件自身的
33
+ `isComposing` / `inputType === 'insertCompositionText'` 判断,
34
+ 不再依赖 hook 状态机的时序;这样即便浏览器先 `input` 后
35
+ `compositionstart`,预输入的拼音字母也不会以字符形式泄漏出去。
36
+ - **`忽略标点` 改为在引擎构造时把标点重写成空格。** 之前是引擎在
37
+ `onKeyDown` 里 fast-forward 跳过标点位置,但界面上标点还在显示,
38
+ 键盘还在提示打标点键。现在所有标点 codepoint 上来就变空格 ——
39
+ arena、键盘提示、引擎看到的目标序列完全一致,不会跑偏。中途切换
40
+ 设置时通过 `useTypingEngine` 的 memo 重建引擎。
41
+
42
+ ### 修复
43
+ - **中文全角标点显示成 ASCII。** 老的 keydown 捕获路径会挑
44
+ `keydown.key` 或 `beforeinput.data` 里先来的那个,可能替换成半角。
45
+ 改用 `<input>` + `input` 事件、并默认在打错位置显示目标字符,
46
+ 这类问题一次性解决。
47
+ - **删除死代码 `softInsertText`。** `INPUT_TYPE_HANDLERS` 和
48
+ `handleBeforeInput` 里的 `insertText` 特殊分支一起删掉,字符发射
49
+ 只由 `input` 事件负责。
50
+
51
+ ## [0.2.2] — 2026-09-01
52
+
53
+ 响应式页脚,约定式自定义 logo 和 favicon(不用改代码),以及把 view
54
+ 的公用品牌资源改名去掉 `qtype-` 前缀。
55
+
56
+ ### 新增
57
+ - **通过 `.qtype/assets/logo.{svg,png}` 自定义站点 logo。** `qtype repo
58
+ new` 会从语言模板复制 `logo.svg` 到 `<repo>/.qtype/assets/`;
59
+ `publish build` 时若该文件存在,就用它覆盖 dist 里的 `logo.<ext>`。
60
+ 用户直接编辑该文件(保留文件名 `logo.<ext>`) —— 不用 config key,
61
+ 不用 CLI 参数,文件不存在就用内置默认。用于 Header 品牌图标;浏
62
+ 览器 tab 用独立的 favicon(见下)。
63
+ - **通过 `.qtype/assets/favicon.{svg,png,ico}` 自定义浏览器 tab 图标。**
64
+ 和 logo 同样的约定式覆盖机制,`publish build` 会把用户的 favicon
65
+ 复制成 `dist/favicon.<ext>` 并重写 `index.html` 里的
66
+ `<link rel="icon">`。favicon 和 logo 分开是为了让用户可以放宽横
67
+ 的 logo,同时保留方形的 tab 图标。
68
+ - **`<html data-qtype-logo-ext>` 新 boot 属性** —— 让 Header 的
69
+ `<img src="/logo.<ext>">` 在运行时挑对扩展名。
70
+
71
+ ### 变更
72
+ - **view 的公用品牌资源改名去掉 `qtype-` 前缀。** `src/view/public/`
73
+ 下现在叫 `logo.svg`、`favicon.svg`、`wordmark.svg`;build 出来的
74
+ 站点里也是 `dist/logo.<ext>`、`dist/favicon.<ext>` 和
75
+ `dist/wordmark.svg`(暂时没被引用,保留给以后用)。外部如果硬编码
76
+ 引用了旧文件名(`qtype-mark.svg` / `qtype-logo.svg`)需要更新
77
+ —— 但运行时 `<link rel="icon">` 和 Header 的 `<img>` 都是构建时
78
+ 重新生成的,已有 repo 只要跑一次 `publish build` 就同步了。
79
+ - **页脚材料名做了响应式截断。** 左边的材料标题在所有断点上都会
80
+ ellipsis(桌面 240px 上限,窄屏 80px,超窄 400px 以下隐藏),避免过
81
+ 长的材料标题挤占 CardNav 绝对居中区域或推走右侧的主题 / 设置按钮。
82
+ - **标题外面包了一层 inner span**,让外层 flex 槽继续占满剩余空间
83
+ (右侧按钮才能贴到最右边),ellipsis 只作用在 inner span 上。修复
84
+ 了窄屏上出现的一个视觉回归 —— 设置 / 主题按钮会飘到标题旁边,而
85
+ 不是紧贴右侧。
86
+
15
87
  ## [0.2.1] — 2026-09-01
16
88
 
17
89
  `qtype config` 的可发现性升级。用户不再需要翻源码或 wiki 才知道能设
@@ -80,11 +80,20 @@ export async function publishBuildAction(opts = {}) {
80
80
  fs.rmSync(outDir, { recursive: true, force: true });
81
81
  fs.mkdirSync(outDir, { recursive: true });
82
82
  copyDir(viewBundle, outDir);
83
+ // Optional brand overrides: user may drop custom assets under
84
+ // `<repo>/.qtype/assets/`:
85
+ // - logo.{svg,png} → Header brand mark (dist/logo.<ext>)
86
+ // - favicon.{svg,png,ico} → browser tab (dist/favicon.<ext>)
87
+ // Whichever the user provides overrides the view bundle default;
88
+ // the losing sibling extensions are removed so we don't ship stale
89
+ // duplicates.
90
+ const logoExt = resolveCustomAsset(cwd, outDir, 'logo', ['svg', 'png']);
91
+ const faviconExt = resolveCustomAsset(cwd, outDir, 'favicon', ['svg', 'png', 'ico']);
83
92
  // Rewrite dist/index.html to carry the site's name — both in the
84
93
  // <title> (browser tab) and as a `data-qtype-repo-name` attribute on
85
94
  // <html> (read by the view's `readBootConfig()` for the header
86
95
  // brand label and anywhere else the repo name is shown).
87
- rewriteIndexHtml(path.join(outDir, 'index.html'), name);
96
+ rewriteIndexHtml(path.join(outDir, 'index.html'), name, logoExt, faviconExt);
88
97
  // Defensive: clear any stale material/ that might have snuck into the
89
98
  // view bundle. Since dev-seed content lives at repo root's examples/
90
99
  // (served only by the vite dev middleware, not the bundle), this is a
@@ -304,7 +313,43 @@ async function resolveRepoName(cwd) {
304
313
  }
305
314
  return path.basename(cwd);
306
315
  }
307
- function rewriteIndexHtml(indexPath, name) {
316
+ /**
317
+ * Resolve a user-provided brand asset (`logo` or `favicon`) by walking
318
+ * the accepted extensions in preference order and copying the first
319
+ * hit to `dist/<name>.<ext>`. Losing sibling extensions in the
320
+ * output are cleaned up so the site never carries stale defaults
321
+ * alongside the user's override. Returns the winning extension so
322
+ * downstream code (index.html rewrite, Header runtime) can build the
323
+ * right URL. Missing files → keep the view bundle's default (svg).
324
+ */
325
+ function resolveCustomAsset(cwd, outDir, name, order) {
326
+ const candidates = order.map((ext) => ({
327
+ ext,
328
+ src: path.join(cwd, '.qtype', 'assets', `${name}.${ext}`),
329
+ }));
330
+ for (const c of candidates) {
331
+ if (!fs.existsSync(c.src))
332
+ continue;
333
+ fs.copyFileSync(c.src, path.join(outDir, `${name}.${c.ext}`));
334
+ for (const other of candidates) {
335
+ if (other.ext === c.ext)
336
+ continue;
337
+ const stale = path.join(outDir, `${name}.${other.ext}`);
338
+ if (fs.existsSync(stale))
339
+ fs.rmSync(stale, { force: true });
340
+ }
341
+ return c.ext;
342
+ }
343
+ return 'svg';
344
+ }
345
+ function extToMime(ext) {
346
+ if (ext === 'png')
347
+ return 'image/png';
348
+ if (ext === 'ico')
349
+ return 'image/x-icon';
350
+ return 'image/svg+xml';
351
+ }
352
+ function rewriteIndexHtml(indexPath, name, logoExt, faviconExt) {
308
353
  if (!fs.existsSync(indexPath))
309
354
  return;
310
355
  const html = fs.readFileSync(indexPath, 'utf8');
@@ -314,13 +359,22 @@ function rewriteIndexHtml(indexPath, name) {
314
359
  .replace(/>/g, '&gt;')
315
360
  .replace(/"/g, '&quot;');
316
361
  const withTitle = html.replace(/<title>[^<]*<\/title>/, `<title>${escaped}</title>`);
317
- // Add or replace data-qtype-repo-name on the root <html> tag.
318
- // Two branches so a re-run cleanly overwrites an existing attribute.
319
- const rewritten = /<html\b[^>]*\bdata-qtype-repo-name=/.test(withTitle)
320
- ? withTitle.replace(/(<html\b[^>]*\bdata-qtype-repo-name=)"[^"]*"/, `$1"${escaped}"`)
321
- : withTitle.replace(/<html\b/, `<html data-qtype-repo-name="${escaped}"`);
322
- if (rewritten !== html)
323
- fs.writeFileSync(indexPath, rewritten);
362
+ // Add or replace data-qtype-repo-name / data-qtype-logo-ext on the
363
+ // root <html> tag. Two branches per attribute so a re-run cleanly
364
+ // overwrites the existing value.
365
+ let out = withTitle;
366
+ out = /<html\b[^>]*\bdata-qtype-repo-name=/.test(out)
367
+ ? out.replace(/(<html\b[^>]*\bdata-qtype-repo-name=)"[^"]*"/, `$1"${escaped}"`)
368
+ : out.replace(/<html\b/, `<html data-qtype-repo-name="${escaped}"`);
369
+ out = /<html\b[^>]*\bdata-qtype-logo-ext=/.test(out)
370
+ ? out.replace(/(<html\b[^>]*\bdata-qtype-logo-ext=)"[^"]*"/, `$1"${logoExt}"`)
371
+ : out.replace(/<html\b/, `<html data-qtype-logo-ext="${logoExt}"`);
372
+ // Rewrite the favicon <link> to point at the resolved favicon file
373
+ // (SVG / PNG / ICO). Kept independent of the logo so the Header
374
+ // and the browser tab can carry different aspect ratios.
375
+ out = out.replace(/<link\s+rel="icon"[^>]*>/, `<link rel="icon" type="${extToMime(faviconExt)}" href="/favicon.${faviconExt}" />`);
376
+ if (out !== html)
377
+ fs.writeFileSync(indexPath, out);
324
378
  }
325
379
  async function obtainPassword() {
326
380
  const envPwd = process.env.QTYPE_PUBLISH_PASSWORD;
@@ -93,6 +93,20 @@ export async function repoNewAction(opts) {
93
93
  fs.mkdirSync(qtypeDir, { recursive: true });
94
94
  fs.writeFileSync(path.join(qtypeDir, 'config.json'), JSON.stringify({ repo: { lang: canonicalLang } }, null, 2) + '\n');
95
95
  console.log(` + .qtype/config.json`);
96
+ // 5b. Seed brand assets from the language template so users can
97
+ // rebrand by editing files in place. `publish build` picks them
98
+ // up over the defaults that ship in the view bundle.
99
+ // - logo.svg → Header brand mark
100
+ // - favicon.svg → browser tab icon
101
+ for (const rel of ['logo.svg', 'favicon.svg']) {
102
+ const src = path.join(templatesDir, '.qtype', 'assets', rel);
103
+ if (!fs.existsSync(src))
104
+ continue;
105
+ const dest = path.join(repoDir, '.qtype', 'assets', rel);
106
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
107
+ fs.copyFileSync(src, dest);
108
+ console.log(` + .qtype/assets/${rel}`);
109
+ }
96
110
  // 6. git init (optional; requires explicit --git)
97
111
  if (opts.git === true) {
98
112
  try {
@@ -31,10 +31,28 @@ export class TypingInput {
31
31
  #position;
32
32
  #listeners = new Set();
33
33
  constructor(text, settings) {
34
- this.#target = text;
35
- this.#targetCodepoints = Array.from(text, (ch) => ch.codePointAt(0) ?? 0);
36
- this.#length = this.#targetCodepoints.length;
37
34
  this.settings = { ...defaultTypingSettings, ...settings };
35
+ // ignorePunctuation replaces every punctuation codepoint with a
36
+ // regular space in the target sequence. The engine, the arena
37
+ // renderer, and the keyboard hint all just see a space — no
38
+ // fast-forward machinery, no special-case display, no divergence
39
+ // between what the user sees and what they need to type.
40
+ const raw = Array.from(text, (ch) => ch.codePointAt(0) ?? 0);
41
+ this.#targetCodepoints = this.settings.ignorePunctuation
42
+ ? raw.map((cp) => (isPunctuation(cp) ? 0x20 : cp))
43
+ : raw;
44
+ // The effective target string: rewritten if ignorePunctuation is
45
+ // on, else identical to `text`. Anything that consumes
46
+ // `engine.target` (keyboard-hint plans, mini-arena mirrors, etc.)
47
+ // sees exactly the sequence the engine expects the user to type.
48
+ // We avoid `String.fromCodePoint(...arr)` here — spreading a large
49
+ // codepoint array can blow the JS engine's argument-count ceiling
50
+ // on unusually long targets. Building char-by-char is O(n) with no
51
+ // fixed upper bound.
52
+ this.#target = this.settings.ignorePunctuation
53
+ ? this.#targetCodepoints.map((cp) => String.fromCodePoint(cp)).join('')
54
+ : text;
55
+ this.#length = this.#targetCodepoints.length;
38
56
  this.#chars = this.#buildInitialChars();
39
57
  this.#position = 0;
40
58
  }
@@ -60,20 +78,7 @@ export class TypingInput {
60
78
  if (this.#position >= this.#length) {
61
79
  return Feedback.Correct;
62
80
  }
63
- let expected = this.#targetCodepoints[this.#position];
64
- // ignorePunctuation: fast-forward through any punctuation slots
65
- // before matching the user's keystroke against the next
66
- // typable character. Uses a broad Unicode range so it works
67
- // for both ASCII and CJK punctuation.
68
- if (this.settings.ignorePunctuation) {
69
- while (this.#position < this.#length &&
70
- isPunctuation(this.#targetCodepoints[this.#position])) {
71
- this.#autoAdvance(timestamp);
72
- }
73
- if (this.#position >= this.#length)
74
- return Feedback.Correct;
75
- expected = this.#targetCodepoints[this.#position];
76
- }
81
+ const expected = this.#targetCodepoints[this.#position];
77
82
  // Normalize both sides through the same equivalence table so
78
83
  // fullwidth/half-width / smart-quote / CJK-punctuation variants
79
84
  // don't cause false misses. Display remains untouched.
@@ -195,28 +200,6 @@ export class TypingInput {
195
200
  for (const l of this.#listeners)
196
201
  l();
197
202
  }
198
- /**
199
- * Mark the current slot as auto-advanced Hit (used by
200
- * `ignorePunctuation`). Records the timestamp so stats include the
201
- * auto-advance in `startedAt` timing, but doesn't count it toward
202
- * user keystroke totals (still shows as Hit — the punctuation was
203
- * "correctly typed" via the auto-advance rule).
204
- */
205
- #autoAdvance(timestamp) {
206
- if (this.#position >= this.#length)
207
- return;
208
- const expected = this.#targetCodepoints[this.#position];
209
- const next = this.#chars.slice();
210
- const prev = next[this.#position];
211
- next[this.#position] = {
212
- target: expected,
213
- typed: expected,
214
- attrs: (prev.attrs & ~(CharAttr.Miss | CharAttr.Cursor)) | CharAttr.Hit,
215
- timestamp,
216
- };
217
- this.#chars = next;
218
- this.#position++;
219
- }
220
203
  }
221
204
  /**
222
205
  * ASCII case-insensitive comparison. Non-ASCII (CJK, accented Latin,
@@ -1,3 +1,3 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-BbG90BH8.js","assets/index-eEQRUOZD.js","assets/index-B8531CqV.css"])))=>i.map(i=>d[i]);
2
- import{r as l,_ as h,u as g,j as f,C as p,M as m,T as y,a as w}from"./index-eEQRUOZD.js";let o=null,c=null;async function L(){return o||c||(c=h(()=>import("./index-BbG90BH8.js"),__vite__mapDeps([0,1,2])).then(async e=>(o=await e.createHighlighter({themes:["github-dark","github-light"],langs:[]}),o)),c)}async function _(e,t){if(!e.getLoadedLanguages().includes(t))try{await e.loadLanguage(t)}catch{}}function x(e){const[t,n]=l.useState(o);return l.useEffect(()=>{if(!e)return;let r=!1;return(async()=>{const s=await L();await _(s,e),r||n(s)})(),()=>{r=!0}},[e]),t}const A=/```(\w+)/;function E(e){const t=e.toLowerCase();return t.includes("typescript")||t.endsWith(".ts")?"typescript":t.includes("python")||t.endsWith(".py")?"python":t.includes("cpp")||t.includes("c++")?"cpp":t.includes("java")&&!t.includes("script")?"java":t.includes("go")?"go":t.includes("rust")||t.endsWith(".rs")?"rust":t.includes("code-c")||t.endsWith(".c")?"c":"javascript"}function j(e,t){if(e){const n=e.match(A);if(n)return n[1]}return E(t)}function k(e,t,n,r){if(!r||!r.getLoadedLanguages().includes(t))return new Array(Array.from(e).length).fill(null);try{const s=r.codeToTokens(e,{lang:t,theme:n}),i=[];for(const u of s.tokens){for(const a of u)for(const d of a.content)i.push(a.color??null);i.push(null)}return!e.endsWith(`
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-Cj70GP_V.js","assets/index-CeFBpbIw.js","assets/index-VPadwWzO.css"])))=>i.map(i=>d[i]);
2
+ import{r as l,_ as h,u as g,j as f,C as p,M as m,T as y,a as w}from"./index-CeFBpbIw.js";let o=null,c=null;async function L(){return o||c||(c=h(()=>import("./index-Cj70GP_V.js"),__vite__mapDeps([0,1,2])).then(async e=>(o=await e.createHighlighter({themes:["github-dark","github-light"],langs:[]}),o)),c)}async function _(e,t){if(!e.getLoadedLanguages().includes(t))try{await e.loadLanguage(t)}catch{}}function x(e){const[t,n]=l.useState(o);return l.useEffect(()=>{if(!e)return;let r=!1;return(async()=>{const s=await L();await _(s,e),r||n(s)})(),()=>{r=!0}},[e]),t}const A=/```(\w+)/;function E(e){const t=e.toLowerCase();return t.includes("typescript")||t.endsWith(".ts")?"typescript":t.includes("python")||t.endsWith(".py")?"python":t.includes("cpp")||t.includes("c++")?"cpp":t.includes("java")&&!t.includes("script")?"java":t.includes("go")?"go":t.includes("rust")||t.endsWith(".rs")?"rust":t.includes("code-c")||t.endsWith(".c")?"c":"javascript"}function j(e,t){if(e){const n=e.match(A);if(n)return n[1]}return E(t)}function k(e,t,n,r){if(!r||!r.getLoadedLanguages().includes(t))return new Array(Array.from(e).length).fill(null);try{const s=r.codeToTokens(e,{lang:t,theme:n}),i=[];for(const u of s.tokens){for(const a of u)for(const d of a.content)i.push(a.color??null);i.push(null)}return!e.endsWith(`
3
3
  `)&&i.length>Array.from(e).length&&i.pop(),i}catch{return new Array(Array.from(e).length).fill(null)}}function v(e){var d;const{material:t,card:n}=e,[r]=g(),s=l.useMemo(()=>r?j(n.question,t.filePath):"",[n.question,t.filePath,r]),i=typeof document<"u"&&((d=document.documentElement.getAttribute("data-theme"))!=null&&d.includes("latte"))?"github-light":"github-dark",u=x(r?s:""),a=l.useMemo(()=>r?k(n.answer,s,i,u):void 0,[n.answer,s,i,u,r]);return f.jsx(p,{type:"code",variant:"flow",slots:[{role:"title",content:n.title},n.question?{role:"question",content:f.jsx(m,{source:n.question})}:{role:"question",content:null},{role:"arena",content:f.jsx(y,{...w(e),charColors:a},n.id)}]})}export{v as CodeCard};