@hadoku/resume-bot 0.0.0-stage → 3.14.16

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.
Files changed (70) hide show
  1. package/README.md +135 -3
  2. package/dist/App.d.ts +9 -0
  3. package/dist/App.d.ts.map +1 -0
  4. package/dist/ChatInterface-mHtxUGpp.js +128 -0
  5. package/dist/ChatInterface-mHtxUGpp.js.map +1 -0
  6. package/dist/ResumeViewer-C-uxZ-7E.js +296 -0
  7. package/dist/ResumeViewer-C-uxZ-7E.js.map +1 -0
  8. package/dist/builder/BlockCard.d.ts +15 -0
  9. package/dist/builder/BlockCard.d.ts.map +1 -0
  10. package/dist/builder/BuilderApp.d.ts +3 -0
  11. package/dist/builder/BuilderApp.d.ts.map +1 -0
  12. package/dist/builder/api.d.ts +37 -0
  13. package/dist/builder/api.d.ts.map +1 -0
  14. package/dist/builder/entry.d.ts +8 -0
  15. package/dist/builder/entry.d.ts.map +1 -0
  16. package/dist/builder/useBuilderAuth.d.ts +10 -0
  17. package/dist/builder/useBuilderAuth.d.ts.map +1 -0
  18. package/dist/builder.js +537 -0
  19. package/dist/builder.js.map +1 -0
  20. package/dist/components/ChatInterface.d.ts +10 -0
  21. package/dist/components/ChatInterface.d.ts.map +1 -0
  22. package/dist/components/ResumeDocument.d.ts +30 -0
  23. package/dist/components/ResumeDocument.d.ts.map +1 -0
  24. package/dist/components/ResumeViewer.d.ts +12 -0
  25. package/dist/components/ResumeViewer.d.ts.map +1 -0
  26. package/dist/entry-BTJjyohQ.js +154 -0
  27. package/dist/entry-BTJjyohQ.js.map +1 -0
  28. package/dist/entry.d.ts +22 -0
  29. package/dist/entry.d.ts.map +1 -0
  30. package/dist/index.js +9 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/markdown-vendor-61sRGACd.js +10510 -0
  33. package/dist/markdown-vendor-61sRGACd.js.map +1 -0
  34. package/dist/rehype-vendor-C_Q-vwLE.js +5647 -0
  35. package/dist/rehype-vendor-C_Q-vwLE.js.map +1 -0
  36. package/dist/services/api.d.ts +62 -0
  37. package/dist/services/api.d.ts.map +1 -0
  38. package/dist/style.css +1 -0
  39. package/dist/worker/application-extras.d.ts +56 -0
  40. package/dist/worker/application-extras.d.ts.map +1 -0
  41. package/dist/worker/blocks.d.ts +12 -0
  42. package/dist/worker/blocks.d.ts.map +1 -0
  43. package/dist/worker/builder.d.ts +33 -0
  44. package/dist/worker/builder.d.ts.map +1 -0
  45. package/dist/worker/constants.d.ts +39 -0
  46. package/dist/worker/constants.d.ts.map +1 -0
  47. package/dist/worker/contact.d.ts +34 -0
  48. package/dist/worker/contact.d.ts.map +1 -0
  49. package/dist/worker/cover-letter.d.ts +16 -0
  50. package/dist/worker/cover-letter.d.ts.map +1 -0
  51. package/dist/worker/index.d.ts +38 -0
  52. package/dist/worker/index.d.ts.map +1 -0
  53. package/dist/worker/llm.d.ts +77 -0
  54. package/dist/worker/llm.d.ts.map +1 -0
  55. package/dist/worker/pdf.d.ts +46 -0
  56. package/dist/worker/pdf.d.ts.map +1 -0
  57. package/dist/worker/rate-limit.d.ts +10 -0
  58. package/dist/worker/rate-limit.d.ts.map +1 -0
  59. package/dist/worker/resume.d.ts +33 -0
  60. package/dist/worker/resume.d.ts.map +1 -0
  61. package/dist/worker/skeleton.d.ts +73 -0
  62. package/dist/worker/skeleton.d.ts.map +1 -0
  63. package/dist/worker/tailored-resume.d.ts +31 -0
  64. package/dist/worker/tailored-resume.d.ts.map +1 -0
  65. package/dist/worker/typography.d.ts +12 -0
  66. package/dist/worker/typography.d.ts.map +1 -0
  67. package/dist/worker/variants.d.ts +51 -0
  68. package/dist/worker/variants.d.ts.map +1 -0
  69. package/dist/worker.js +42508 -0
  70. package/package.json +43 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder.js","sources":["../src/builder/api.ts","../src/builder/BlockCard.tsx","../node_modules/.pnpm/@wolffm+prefs-client@1.1.5_react@19.2.0_zod@4.1.12/node_modules/@wolffm/prefs-client/dist/index.js","../src/builder/useBuilderAuth.ts","../src/builder/BuilderApp.tsx","../src/builder/entry.tsx"],"sourcesContent":["// Admin builder API client. All calls go through the edge-router (same-origin,\n// credentials:'include') so the caller's admin session cookie is resolved into\n// the X-Hadoku-Tier: admin stamp the worker's requireMinTier('admin') needs.\n\nexport type BlockType = 'experience' | 'project' | 'skills' | 'education' | 'summary' | 'header'\n\nexport interface ResumeBlock {\n id: string\n type: BlockType\n tags: string[]\n title: string\n content: string\n priority: number\n}\n\nexport interface FeedbackEntry {\n text: string\n updated: string\n}\nexport type Feedback = Record<string, FeedbackEntry>\n\nexport class BuilderApiError extends Error {\n constructor(\n message: string,\n readonly status: number\n ) {\n super(message)\n this.name = 'BuilderApiError'\n }\n}\n\n// The API root ('/resume/api'); builder endpoints are appended to it directly.\nlet baseUrl = ''\nexport function setBuilderApiBaseUrl(url: string): void {\n baseUrl = url.replace(/\\/+$/, '')\n}\n\nasync function req<T>(path: string, init?: RequestInit): Promise<T> {\n const res = await fetch(`${baseUrl}/builder${path}`, {\n ...init,\n credentials: 'include',\n headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) }\n })\n if (!res.ok) {\n let detail = `${res.status}`\n try {\n const body = (await res.json()) as { error?: string; message?: string }\n detail = body.message ? `${body.error}: ${body.message}` : (body.error ?? detail)\n } catch {\n /* non-JSON error body */\n }\n throw new BuilderApiError(detail, res.status)\n }\n return (await res.json()) as T\n}\n\nexport function fetchBlocks(): Promise<{ blocks: ResumeBlock[]; feedback: Feedback }> {\n return req('/blocks')\n}\n\nexport function saveBlock(block: ResumeBlock): Promise<{ block: ResumeBlock }> {\n return req(`/blocks/${encodeURIComponent(block.id)}`, {\n method: 'PUT',\n body: JSON.stringify(block)\n })\n}\n\nexport function deleteBlock(id: string): Promise<{ deleted: true; id: string }> {\n return req(`/blocks/${encodeURIComponent(id)}`, { method: 'DELETE' })\n}\n\nexport function reorderBlocks(ids: string[]): Promise<{ ids: string[] }> {\n return req('/reorder', { method: 'PUT', body: JSON.stringify({ ids }) })\n}\n\nexport function saveFeedback(id: string, text: string): Promise<{ feedback: Feedback }> {\n return req(`/feedback/${encodeURIComponent(id)}`, {\n method: 'POST',\n body: JSON.stringify({ text })\n })\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { facetOf } from './BuilderApp'\nimport type { BlockType, FeedbackEntry, ResumeBlock } from './api'\n\nconst BLOCK_TYPES: BlockType[] = [\n 'experience',\n 'project',\n 'skills',\n 'education',\n 'summary',\n 'header'\n]\n\ninterface Props {\n block: ResumeBlock\n feedback: FeedbackEntry | undefined\n existingIds: string[]\n isNew?: boolean\n onSave: (block: ResumeBlock) => Promise<void>\n onDelete?: () => Promise<void>\n onCancel?: () => void\n onFeedback: (id: string, text: string) => Promise<void>\n onTagClick: (tag: string) => void\n}\n\nexport function BlockCard({\n block,\n feedback,\n existingIds,\n isNew = false,\n onSave,\n onDelete,\n onCancel,\n onFeedback,\n onTagClick\n}: Props) {\n const [draft, setDraft] = useState<ResumeBlock>(block)\n const [tagsText, setTagsText] = useState(block.tags.join(' '))\n const [saving, setSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n const [fbText, setFbText] = useState(feedback?.text ?? '')\n const [fbStatus, setFbStatus] = useState('')\n const fbTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n\n // Keep the draft in sync if the saved block changes underneath us (e.g. after\n // a successful save re-normalises it), but not while the user is mid-edit.\n useEffect(() => {\n if (!isNew) {\n setDraft(block)\n setTagsText(block.tags.join(' '))\n }\n }, [block, isNew])\n\n const dirty =\n isNew || JSON.stringify({ ...draft, tags: parseTags(tagsText) }) !== JSON.stringify(block)\n\n function parseTags(text: string): string[] {\n return [...new Set(text.split(/[\\s,]+/).filter(Boolean))]\n }\n\n const idInvalid =\n isNew && (!/^[a-z0-9][a-z0-9-]*$/.test(draft.id) || existingIds.includes(draft.id))\n\n async function handleSave() {\n setSaving(true)\n setSaveError(null)\n try {\n await onSave({ ...draft, tags: parseTags(tagsText) })\n } catch (err) {\n setSaveError(err instanceof Error ? err.message : 'Save failed')\n } finally {\n setSaving(false)\n }\n }\n\n async function handleDelete() {\n if (!onDelete) return\n if (!confirm(`Delete block \"${block.id}\"? This removes it from the live resume.`)) return\n setSaving(true)\n try {\n await onDelete()\n } catch (err) {\n setSaveError(err instanceof Error ? err.message : 'Delete failed')\n setSaving(false)\n }\n }\n\n function onFbChange(text: string) {\n setFbText(text)\n setFbStatus('typing…')\n clearTimeout(fbTimer.current)\n fbTimer.current = setTimeout(() => {\n onFeedback(block.id, text)\n .then(() => setFbStatus(text.trim() ? 'saved ✓' : ''))\n .catch(() => setFbStatus('save failed'))\n }, 600)\n }\n\n return (\n <div\n className={`rb-block${feedback ? ' rb-block--has-feedback' : ''}${dirty ? ' rb-block--dirty' : ''}`}\n >\n <div className=\"rb-block__row\">\n {isNew ? (\n <input\n className=\"rb-block__id-input\"\n placeholder=\"block-id (kebab-case)\"\n value={draft.id}\n onChange={e => setDraft({ ...draft, id: e.target.value })}\n />\n ) : (\n <span className=\"rb-block__id\">{draft.id}</span>\n )}\n <select\n className=\"rb-block__type\"\n value={draft.type}\n onChange={e => setDraft({ ...draft, type: e.target.value as BlockType })}\n >\n {BLOCK_TYPES.map(t => (\n <option key={t} value={t}>\n {t}\n </option>\n ))}\n </select>\n <label\n className=\"rb-block__prio\"\n title=\"Priority — how strongly you want this block in a tailored resume. Higher sorts first; a ranking hint, not a hard rule.\"\n >\n prio\n <input\n type=\"number\"\n min={0}\n max={10}\n value={draft.priority}\n onChange={e => setDraft({ ...draft, priority: Number(e.target.value) })}\n />\n </label>\n </div>\n\n <input\n className=\"rb-block__title\"\n placeholder=\"title (review label — not rendered into the resume)\"\n value={draft.title}\n onChange={e => setDraft({ ...draft, title: e.target.value })}\n />\n\n <div className=\"rb-block__chips\">\n {parseTags(tagsText).map(t => (\n <button\n type=\"button\"\n key={t}\n className={`rb-tag rb-tag--${facetOf(t)}`}\n onClick={() => onTagClick(t)}\n title=\"filter by this tag\"\n >\n {t}\n </button>\n ))}\n </div>\n <input\n className=\"rb-block__tags-input\"\n placeholder=\"tags (space or comma separated, e.g. tech:python story:scale)\"\n value={tagsText}\n onChange={e => setTagsText(e.target.value)}\n />\n\n <textarea\n className=\"rb-block__content\"\n rows={8}\n placeholder=\"content (markdown)\"\n value={draft.content}\n onChange={e => setDraft({ ...draft, content: e.target.value })}\n />\n\n <div className=\"rb-block__actions\">\n <button\n type=\"button\"\n className=\"rb-builder__btn rb-builder__btn--primary\"\n onClick={() => {\n void handleSave()\n }}\n disabled={saving || !dirty || idInvalid}\n >\n {saving ? 'Saving…' : isNew ? 'Create' : 'Save'}\n </button>\n {isNew ? (\n <button type=\"button\" className=\"rb-builder__btn\" onClick={onCancel}>\n Cancel\n </button>\n ) : (\n <button\n type=\"button\"\n className=\"rb-builder__btn rb-builder__btn--danger\"\n onClick={() => {\n void handleDelete()\n }}\n disabled={saving}\n >\n Delete\n </button>\n )}\n {idInvalid && <span className=\"rb-block__hint\">id must be unique kebab-case</span>}\n {saveError && <span className=\"rb-block__hint rb-block__hint--error\">{saveError}</span>}\n </div>\n\n {!isNew && (\n <div className=\"rb-block__feedback\">\n <textarea\n rows={2}\n placeholder=\"review note (autosaves)…\"\n value={fbText}\n onChange={e => onFbChange(e.target.value)}\n />\n <span className=\"rb-block__fb-status\">{fbStatus}</span>\n </div>\n )}\n </div>\n )\n}\n","// src/constants.ts\nvar PREFS_API_BASE = \"https://hadoku.me/prefs\";\nvar CACHE_KEY_PREFIX = \"prefs-cache\";\nvar SAVE_DEBOUNCE_MS = 1e3;\n\n// src/storage.ts\nvar localStorageAdapter = {\n get(key) {\n if (typeof window === \"undefined\") return Promise.resolve(null);\n try {\n return Promise.resolve(window.localStorage.getItem(key));\n } catch {\n return Promise.resolve(null);\n }\n },\n set(key, value) {\n if (typeof window === \"undefined\") return Promise.resolve();\n try {\n window.localStorage.setItem(key, value);\n } catch {\n }\n return Promise.resolve();\n },\n remove(key) {\n if (typeof window === \"undefined\") return Promise.resolve();\n try {\n window.localStorage.removeItem(key);\n } catch {\n }\n return Promise.resolve();\n }\n};\nvar memoryStorage = () => {\n const store = /* @__PURE__ */ new Map();\n return {\n get(key) {\n return Promise.resolve(store.get(key) ?? null);\n },\n set(key, value) {\n store.set(key, value);\n return Promise.resolve();\n },\n remove(key) {\n store.delete(key);\n return Promise.resolve();\n }\n };\n};\nvar sessionStorageWrite = (key, value) => {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(key, value);\n } catch {\n }\n};\nfunction readCapacitorGlobal() {\n return globalThis.Capacitor;\n}\nfunction capacitorStorage() {\n const cap = readCapacitorGlobal();\n if (!cap?.isNativePlatform?.()) return null;\n const prefs = cap.Plugins?.Preferences;\n if (!prefs) return null;\n return {\n async get(key) {\n const { value } = await prefs.get({ key });\n return value ?? null;\n },\n async set(key, value) {\n await prefs.set({ key, value });\n },\n async remove(key) {\n await prefs.remove({ key });\n }\n };\n}\nfunction detectStorage() {\n return capacitorStorage() ?? localStorageAdapter;\n}\n\n// src/get-device-id.ts\nvar DEVICE_ID_KEY = \"hadoku_device_id\";\nasync function getDeviceId(storage = localStorageAdapter) {\n const existing = await storage.get(DEVICE_ID_KEY);\n if (existing && isUuid(existing)) {\n return existing;\n }\n const fresh = mintUuid();\n await storage.set(DEVICE_ID_KEY, fresh);\n return fresh;\n}\nfunction mintUuid() {\n const c = globalThis.crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (ch) => {\n const r = Math.random() * 16 | 0;\n const v = ch === \"x\" ? r : r & 3 | 8;\n return v.toString(16);\n });\n}\nvar UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nfunction isUuid(s) {\n return UUID_RE.test(s);\n}\n\n// src/whoami.ts\nvar ANON_USER_ID = \"anon\";\nvar WHOAMI_GLOBAL = \"__hadokuWhoami\";\nvar SESSION_SCOPE_PREFIX = \"sess:\";\nvar UNSCOPED_ID = \"unscoped\";\nfunction adaptWhoami(body) {\n if (!body || body.valid === false) {\n return { userId: ANON_USER_ID, userType: \"public\", scope: \"anon\" };\n }\n const userType = body.userType === \"admin\" || body.userType === \"wife\" || body.userType === \"service\" || body.userType === \"friend\" || body.userType === \"member\" || body.userType === \"public\" ? body.userType : \"public\";\n const name = typeof body.name === \"string\" ? body.name : null;\n if (typeof body.userId === \"string\" && body.userId) {\n return { userId: body.userId, userType, name, scope: \"user\" };\n }\n if (typeof body.sessionId === \"string\" && body.sessionId) {\n return {\n userId: `${SESSION_SCOPE_PREFIX}${body.sessionId}`,\n userType,\n name,\n scope: \"session\"\n };\n }\n return { userId: UNSCOPED_ID, userType, name, scope: \"none\" };\n}\nasync function fetchWhoamiRaw(url, userKey) {\n const headers = {};\n if (userKey) {\n headers[\"X-User-Key\"] = userKey;\n }\n try {\n const resp = await fetch(url, {\n method: \"GET\",\n headers,\n credentials: \"include\",\n cache: \"no-store\"\n });\n if (!resp.ok) return null;\n return await resp.json();\n } catch {\n return null;\n }\n}\nfunction defaultWhoamiUrl() {\n const loc = globalThis.location;\n if (loc && (loc.protocol === \"http:\" || loc.protocol === \"https:\")) return \"/session/whoami\";\n return `${PREFS_API_BASE.replace(/\\/prefs$/, \"\")}/session/whoami`;\n}\nasync function whoami(opts = {}) {\n const url = opts.whoamiUrl ?? defaultWhoamiUrl();\n if (opts.userKey) {\n return adaptWhoami(await fetchWhoamiRaw(url, opts.userKey));\n }\n const g = globalThis;\n let shared = g[WHOAMI_GLOBAL];\n if (!shared) {\n shared = fetchWhoamiRaw(url);\n g[WHOAMI_GLOBAL] = shared;\n }\n try {\n return adaptWhoami(await shared);\n } catch {\n return { userId: ANON_USER_ID, userType: \"public\", scope: \"anon\" };\n }\n}\n\n// src/client.ts\nfunction createPrefsClient(opts) {\n const apiBase = opts.apiBase ?? PREFS_API_BASE;\n const mirrorOnRead = opts.mirrorBootstrapOnRead ?? false;\n const storage = opts.storage ?? detectStorage();\n const listeners = /* @__PURE__ */ new Set();\n const migrations = opts.migrations ?? [];\n const defaults = opts.defaults ?? {};\n const bootstrapMap = opts.bootstrapToSessionStorage ?? {};\n let userId = null;\n let identityScope = \"anon\";\n let wroteWhileAnon = false;\n let deviceId = opts.deviceId ?? null;\n let currentBlob = null;\n let userVersion = 0;\n let deviceVersion = 0;\n let pendingPatch = {};\n let pendingScope = \"user\";\n const dirtyFields = /* @__PURE__ */ new Set();\n let saveTimer = null;\n let savePromise = null;\n let initPromise = null;\n let bcast = null;\n let destroyed = false;\n const broadcastId = `${Math.random().toString(36).slice(2)}-${Date.now()}`;\n function ensureInit() {\n if (initPromise) return initPromise;\n initPromise = (async () => {\n const who = await whoami({\n userKey: opts.userKey,\n // An EXPLICIT apiBase (a non-prod environment) names where identity lives\n // too. With none, leave it to whoami()'s default — same-origin in a\n // browser — rather than pinning the hadoku.me constant (#419).\n ...opts.apiBase ? { whoamiUrl: `${opts.apiBase.replace(/\\/prefs$/, \"\")}/session/whoami` } : {}\n });\n userId = who.userId;\n identityScope = who.scope;\n if (!deviceId) {\n deviceId = await getDeviceId(storage);\n }\n const cached = await readCache();\n if (cached) {\n userVersion = cached.userVersion;\n deviceVersion = cached.deviceVersion;\n currentBlob = applyDefaultsAndParse(cached.blob);\n if (mirrorOnRead) mirrorBootstrap(currentBlob);\n notify(currentBlob);\n }\n if (canPersist() && typeof BroadcastChannel !== \"undefined\") {\n bcast = new BroadcastChannel(`prefs:${userId}:${opts.appId}`);\n bcast.addEventListener(\"message\", onBroadcast);\n }\n })();\n return initPromise;\n }\n function buildHeaders() {\n const h = { \"Content-Type\": \"application/json\" };\n if (opts.userKey) h[\"X-User-Key\"] = opts.userKey;\n if (deviceId) h[\"X-Device-Id\"] = deviceId;\n return h;\n }\n async function fetchFromServer() {\n const resp = await fetch(`${apiBase}/api/v1/${opts.appId}`, {\n method: \"GET\",\n headers: buildHeaders(),\n credentials: \"include\"\n });\n if (!resp.ok) {\n throw new Error(`prefs-api GET failed: ${resp.status}`);\n }\n return await resp.json();\n }\n async function putToServer(scope, patch, expectedVersion) {\n const resp = await fetch(`${apiBase}/api/v1/${opts.appId}`, {\n method: \"PUT\",\n headers: buildHeaders(),\n credentials: \"include\",\n body: JSON.stringify({\n scope,\n patch,\n expectedVersion\n })\n });\n if (resp.status === 409) {\n const body2 = await resp.json();\n if (body2.current) {\n return { ok: false, conflict: body2.current };\n }\n throw new Error(\"prefs-api 409 without current-state body\");\n }\n if (!resp.ok) {\n throw new Error(`prefs-api PUT failed: ${resp.status}`);\n }\n const body = await resp.json();\n return { ok: true, row: body, scope: body.scope };\n }\n function cacheKey() {\n return `${CACHE_KEY_PREFIX}:${userId ?? ANON_USER_ID}:${opts.appId}`;\n }\n function canPersist() {\n return identityScope !== \"none\";\n }\n function canSync() {\n return identityScope === \"user\" || identityScope === \"session\";\n }\n async function readCache() {\n if (!canPersist()) return null;\n const raw = await storage.get(cacheKey());\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\" && parsed.blob && typeof parsed.blob === \"object\") {\n return parsed;\n }\n } catch {\n }\n return null;\n }\n async function writeCache(env) {\n if (!canPersist()) return;\n await storage.set(cacheKey(), JSON.stringify(env));\n }\n function applyDefaultsAndParse(rawBlob) {\n let withDefaults = { ...defaults, ...rawBlob };\n for (const m of migrations) {\n withDefaults = m(withDefaults);\n }\n const result = opts.schema.safeParse(withDefaults);\n if (result.success) {\n return result.data;\n }\n const fallback = opts.schema.safeParse({ ...defaults });\n return fallback.success ? fallback.data : withDefaults;\n }\n function mirrorBootstrap(blob) {\n for (const [field, sessionKey] of Object.entries(bootstrapMap)) {\n if (typeof sessionKey !== \"string\" || !sessionKey) continue;\n const value = blob[field];\n if (typeof value === \"string\") {\n sessionStorageWrite(sessionKey, value);\n }\n }\n }\n function notify(blob) {\n for (const l of listeners) {\n try {\n l(blob);\n } catch {\n }\n }\n }\n function broadcast(blob) {\n if (!bcast) return;\n try {\n bcast.postMessage({ source: broadcastId, blob });\n } catch {\n }\n }\n function onBroadcast(ev) {\n const data = ev.data;\n if (!data || data.source === broadcastId || !data.blob) return;\n const reconciled = reconcileDirty(data.blob);\n currentBlob = applyDefaultsAndParse(reconciled);\n if (mirrorOnRead) mirrorBootstrap(currentBlob);\n notify(currentBlob);\n }\n function reconcileDirty(remote) {\n if (dirtyFields.size === 0 || !currentBlob) return remote;\n const local = currentBlob;\n const reconciled = { ...remote };\n for (const field of dirtyFields) {\n if (field in local) reconciled[field] = local[field];\n }\n return reconciled;\n }\n async function refreshFromServer() {\n if (!canSync()) {\n if (currentBlob === null) {\n currentBlob = applyDefaultsAndParse({});\n if (mirrorOnRead) mirrorBootstrap(currentBlob);\n notify(currentBlob);\n }\n return;\n }\n const server = await fetchFromServer();\n userVersion = server.user?.version ?? 0;\n deviceVersion = server.device?.version ?? 0;\n const merged = reconcileDirty(server.merged ?? {});\n currentBlob = applyDefaultsAndParse(merged);\n await writeCache({\n blob: merged,\n userVersion,\n deviceVersion,\n cachedAt: Date.now()\n });\n if (mirrorOnRead) mirrorBootstrap(currentBlob);\n notify(currentBlob);\n broadcast(currentBlob);\n }\n function scheduleFlush() {\n if (saveTimer) clearTimeout(saveTimer);\n saveTimer = setTimeout(() => {\n void flushPending();\n }, SAVE_DEBOUNCE_MS);\n }\n async function flushPending() {\n if (Object.keys(pendingPatch).length === 0) return;\n if (savePromise) {\n await savePromise;\n }\n const patchSnapshot = pendingPatch;\n const scopeSnapshot = pendingScope;\n pendingPatch = {};\n if (saveTimer) {\n clearTimeout(saveTimer);\n saveTimer = null;\n }\n if (!canSync()) {\n for (const key of Object.keys(patchSnapshot)) {\n if (!(key in pendingPatch)) dirtyFields.delete(key);\n }\n return;\n }\n const expectedVersion = scopeSnapshot === \"user\" ? userVersion : deviceVersion;\n savePromise = (async () => {\n try {\n const result = await putToServer(scopeSnapshot, patchSnapshot, expectedVersion);\n if (result.ok) {\n if (result.scope === \"user\") {\n userVersion = result.row.version;\n } else {\n deviceVersion = result.row.version;\n }\n await refreshFromServer();\n } else {\n if (scopeSnapshot === \"user\") {\n userVersion = result.conflict.version;\n } else {\n deviceVersion = result.conflict.version;\n }\n await refreshFromServer();\n }\n for (const key of Object.keys(patchSnapshot)) {\n if (!(key in pendingPatch)) dirtyFields.delete(key);\n }\n } catch {\n pendingPatch = { ...patchSnapshot, ...pendingPatch };\n pendingScope = scopeSnapshot;\n } finally {\n savePromise = null;\n }\n })();\n await savePromise;\n }\n const client = {\n appId: opts.appId,\n async read() {\n await ensureInit();\n if (currentBlob === null) {\n try {\n await refreshFromServer();\n } catch {\n currentBlob = applyDefaultsAndParse({});\n }\n } else {\n void refreshFromServer().catch(() => {\n });\n }\n return currentBlob ?? applyDefaultsAndParse({});\n },\n async save(patch, saveOpts) {\n await ensureInit();\n const scope = saveOpts?.scope ?? \"user\";\n if (identityScope === \"anon\") wroteWhileAnon = true;\n const base = currentBlob ?? {};\n const nextBlob = applyDefaultsAndParse({ ...base, ...patch });\n currentBlob = nextBlob;\n await writeCache({\n blob: { ...base, ...patch },\n userVersion,\n deviceVersion,\n cachedAt: Date.now()\n });\n mirrorBootstrap(nextBlob);\n notify(nextBlob);\n broadcast(nextBlob);\n if (Object.keys(pendingPatch).length > 0 && pendingScope !== scope) {\n await flushPending();\n }\n pendingPatch = { ...pendingPatch, ...patch };\n pendingScope = scope;\n for (const key of Object.keys(patch)) dirtyFields.add(key);\n if (saveOpts?.expectedVersion !== void 0) {\n await flushPending();\n } else {\n scheduleFlush();\n }\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n async promoteAnonPrefsToUser() {\n if (!wroteWhileAnon) return;\n const anonKey = `${CACHE_KEY_PREFIX}:${ANON_USER_ID}:${opts.appId}`;\n const raw = await storage.get(anonKey);\n if (!raw) return;\n let anon;\n try {\n anon = JSON.parse(raw);\n } catch {\n return;\n }\n if (!anon.blob || Object.keys(anon.blob).length === 0) return;\n await ensureInit();\n const server = await fetchFromServer();\n if (server.user) {\n await refreshFromServer();\n await storage.remove(anonKey);\n return;\n }\n await putToServer(\"user\", anon.blob, 0);\n await refreshFromServer();\n await storage.remove(anonKey);\n },\n async flush() {\n await flushPending();\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n if (saveTimer) clearTimeout(saveTimer);\n if (bcast) bcast.close();\n listeners.clear();\n }\n };\n return client;\n}\nexport {\n ANON_USER_ID,\n CACHE_KEY_PREFIX,\n DEVICE_ID_KEY,\n PREFS_API_BASE,\n SAVE_DEBOUNCE_MS,\n capacitorStorage,\n createPrefsClient,\n detectStorage,\n getDeviceId,\n localStorageAdapter,\n memoryStorage,\n sessionStorageWrite,\n whoami\n};\n","import { useEffect, useState } from 'react'\nimport { whoami } from '@wolffm/prefs-client'\nimport { logger } from '@wolffm/logger/client'\n\n// Ecosystem auth, matching the command station (hadoku_site ContactAdmin):\n// resolve the caller's tier via edge-router's /session/whoami, and on anything\n// short of admin bounce to the shared /auth sign-in page rather than showing a\n// custom \"enter a key\" dead-end. /auth accepts a valid key (POST /session/create)\n// and redirects back to `return`. No app-specific key handling lives here.\n\n/** Send the browser to the ecosystem sign-in page, returning here afterwards. */\nexport function redirectToAuth(): void {\n if (typeof window === 'undefined') return\n const ret = encodeURIComponent(window.location.pathname + window.location.search)\n window.location.replace(`/auth?return=${ret}`)\n}\n\nexport type AuthStatus = 'checking' | 'authorized' | 'redirecting'\n\nexport interface BuilderAuth {\n status: AuthStatus\n /** Display name from the key registry once authorized (null if unnamed). */\n name: string | null\n}\n\nexport function useBuilderAuth(): BuilderAuth {\n const [status, setStatus] = useState<AuthStatus>('checking')\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n let cancelled = false\n whoami()\n .then(who => {\n if (cancelled) return\n if (who.userType === 'admin') {\n setName(who.name ?? null)\n setStatus('authorized')\n return\n }\n logger.info('[builder] not admin, redirecting to sign in', { userType: who.userType })\n setStatus('redirecting')\n redirectToAuth()\n })\n .catch((err: unknown) => {\n // whoami() itself never rejects (it degrades to public), but stay\n // defensive: any failure means \"not signed in\" → sign-in page.\n if (cancelled) return\n logger.error('[builder] whoami failed', {\n error: err instanceof Error ? err.message : String(err)\n })\n setStatus('redirecting')\n redirectToAuth()\n })\n return () => {\n cancelled = true\n }\n }, [])\n\n return { status, name }\n}\n","import { useEffect, useMemo, useState } from 'react'\nimport { logger } from '@wolffm/logger/client'\nimport {\n fetchBlocks,\n saveBlock,\n deleteBlock as apiDeleteBlock,\n saveFeedback,\n BuilderApiError,\n type ResumeBlock,\n type BlockType,\n type Feedback\n} from './api'\nimport { BlockCard } from './BlockCard'\nimport { useBuilderAuth, redirectToAuth } from './useBuilderAuth'\n\nconst BLOCK_TYPES: BlockType[] = [\n 'experience',\n 'project',\n 'skills',\n 'education',\n 'summary',\n 'header'\n]\n\nexport function facetOf(tag: string): 'tech' | 'layer' | 'story' | 'plain' {\n if (tag.startsWith('tech:')) return 'tech'\n if (tag.startsWith('layer:')) return 'layer'\n if (tag.startsWith('story:')) return 'story'\n return 'plain'\n}\n\ntype SortMode = 'order' | 'priority' | 'type' | 'feedback'\n\nfunction emptyBlock(): ResumeBlock {\n return { id: '', type: 'experience', tags: [], title: '', content: '', priority: 5 }\n}\n\nexport function BuilderApp() {\n const { status: authStatus, name: adminName } = useBuilderAuth()\n const [blocks, setBlocks] = useState<ResumeBlock[]>([])\n const [feedback, setFeedback] = useState<Feedback>({})\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n // Filters\n const [search, setSearch] = useState('')\n const [typeFilter, setTypeFilter] = useState<'' | BlockType>('')\n const [activeTags, setActiveTags] = useState<Set<string>>(new Set())\n const [andMode, setAndMode] = useState(true)\n const [sort, setSort] = useState<SortMode>('order')\n\n const [creating, setCreating] = useState<ResumeBlock | null>(null)\n\n useEffect(() => {\n if (authStatus !== 'authorized') return\n let cancelled = false\n fetchBlocks()\n .then(res => {\n if (cancelled) return\n setBlocks(res.blocks)\n setFeedback(res.feedback)\n })\n .catch((err: unknown) => {\n if (cancelled) return\n // Session lapsed between the whoami check and this call → bounce to\n // sign-in, same as the initial gate. Everything else is a real error.\n if (err instanceof BuilderApiError && (err.status === 401 || err.status === 403)) {\n redirectToAuth()\n return\n }\n const msg = err instanceof Error ? err.message : 'Failed to load blocks'\n setError(msg)\n logger.error('[builder] load failed', { error: msg })\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [authStatus])\n\n const tagCounts = useMemo(() => {\n const counts = new Map<string, number>()\n for (const b of blocks) for (const t of b.tags) counts.set(t, (counts.get(t) ?? 0) + 1)\n return counts\n }, [blocks])\n\n const sortedTags = useMemo(\n () =>\n [...tagCounts.keys()].sort(\n (a, b) => facetOf(a).localeCompare(facetOf(b)) || a.localeCompare(b)\n ),\n [tagCounts]\n )\n\n const visible = useMemo(() => {\n const q = search.trim().toLowerCase()\n let v = blocks.filter(b => {\n if (typeFilter && b.type !== typeFilter) return false\n if (q && !(b.id + b.title + b.content).toLowerCase().includes(q)) return false\n if (activeTags.size) {\n const hits = [...activeTags].filter(t => b.tags.includes(t)).length\n if (andMode ? hits !== activeTags.size : hits === 0) return false\n }\n return true\n })\n if (sort === 'priority') v = [...v].sort((a, b) => b.priority - a.priority)\n else if (sort === 'type') v = [...v].sort((a, b) => a.type.localeCompare(b.type))\n else if (sort === 'feedback')\n v = [...v].sort((a, b) => (feedback[b.id] ? 1 : 0) - (feedback[a.id] ? 1 : 0))\n return v\n }, [blocks, search, typeFilter, activeTags, andMode, sort, feedback])\n\n function toggleTag(tag: string) {\n setActiveTags(prev => {\n const next = new Set(prev)\n if (next.has(tag)) next.delete(tag)\n else next.add(tag)\n return next\n })\n }\n\n async function handleSave(block: ResumeBlock, isNew: boolean): Promise<void> {\n const { block: saved } = await saveBlock(block)\n setBlocks(prev => {\n const idx = prev.findIndex(b => b.id === saved.id)\n if (idx === -1) return [...prev, saved]\n const copy = [...prev]\n copy[idx] = saved\n return copy\n })\n if (isNew) setCreating(null)\n }\n\n async function handleDelete(id: string): Promise<void> {\n await apiDeleteBlock(id)\n setBlocks(prev => prev.filter(b => b.id !== id))\n }\n\n async function handleFeedback(id: string, text: string): Promise<void> {\n const res = await saveFeedback(id, text)\n setFeedback(res.feedback)\n }\n\n function exportJson() {\n const blob = new Blob([JSON.stringify(blocks, null, 2)], { type: 'application/json' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = 'blocks.json'\n a.click()\n URL.revokeObjectURL(url)\n }\n\n if (authStatus === 'checking') return <p className=\"rb-builder__status\">Checking access…</p>\n if (authStatus === 'redirecting')\n return <p className=\"rb-builder__status\">Redirecting to sign in…</p>\n if (loading) return <p className=\"rb-builder__status\">Loading blocks…</p>\n if (error) return <p className=\"rb-builder__status rb-builder__status--error\">{error}</p>\n\n return (\n <div className=\"rb-builder\">\n <div className=\"rb-builder__toolbar\">\n <header className=\"rb-builder__header\">\n <h1>Resume Builder</h1>\n <div className=\"rb-builder__count\">\n {visible.length} / {blocks.length} blocks\n </div>\n {adminName && (\n <div className=\"rb-builder__whoami\">\n Signed in as <span className=\"rb-builder__whoami-name\">{adminName}</span>\n </div>\n )}\n <div className=\"rb-builder__header-actions\">\n <button type=\"button\" onClick={exportJson} className=\"rb-builder__btn\">\n Export blocks.json\n </button>\n <button\n type=\"button\"\n onClick={() => setCreating(emptyBlock())}\n className=\"rb-builder__btn rb-builder__btn--primary\"\n disabled={creating !== null}\n >\n + New block\n </button>\n </div>\n </header>\n\n <div className=\"rb-builder__controls\">\n <input\n type=\"search\"\n placeholder=\"Search id, title, content…\"\n value={search}\n onChange={e => setSearch(e.target.value)}\n className=\"rb-builder__search\"\n />\n <select\n value={typeFilter}\n onChange={e => setTypeFilter(e.target.value as '' | BlockType)}\n className=\"rb-builder__select\"\n >\n <option value=\"\">all types</option>\n {BLOCK_TYPES.map(t => (\n <option key={t} value={t}>\n {t}\n </option>\n ))}\n </select>\n <select\n value={sort}\n onChange={e => setSort(e.target.value as SortMode)}\n className=\"rb-builder__select\"\n >\n <option value=\"order\">render order</option>\n <option value=\"priority\">priority ↓</option>\n <option value=\"type\">type</option>\n <option value=\"feedback\">has feedback</option>\n </select>\n <label className=\"rb-builder__andmode\">\n <input type=\"checkbox\" checked={andMode} onChange={e => setAndMode(e.target.checked)} />\n match all tags\n </label>\n </div>\n\n <div className=\"rb-builder__legend\">\n <span className=\"rb-builder__legend-item\">\n <span className=\"rb-builder__legend-dot rb-builder__legend-dot--tech\" />\n tech:\n </span>\n <span className=\"rb-builder__legend-item\">\n <span className=\"rb-builder__legend-dot rb-builder__legend-dot--layer\" />\n layer:\n </span>\n <span className=\"rb-builder__legend-item\">\n <span className=\"rb-builder__legend-dot rb-builder__legend-dot--story\" />\n story:\n </span>\n <span className=\"rb-builder__legend-item\">\n <span className=\"rb-builder__legend-dot rb-builder__legend-dot--plain\" />\n other / always\n </span>\n <span className=\"rb-builder__legend-item\">\n <strong>prio</strong>&nbsp;= how strongly you want a block (sort/rank hint)\n </span>\n </div>\n\n <div className=\"rb-builder__tagbar\">\n {sortedTags.map(t => (\n <button\n type=\"button\"\n key={t}\n onClick={() => toggleTag(t)}\n className={`rb-tag rb-tag--${facetOf(t)}${activeTags.has(t) ? ' rb-tag--active' : ''}`}\n >\n {t}\n <span className=\"rb-tag__n\">{tagCounts.get(t)}</span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"rb-builder__list\">\n {creating && (\n <BlockCard\n key=\"__new__\"\n block={creating}\n feedback={undefined}\n isNew\n existingIds={blocks.map(b => b.id)}\n onSave={b => handleSave(b, true)}\n onCancel={() => setCreating(null)}\n onFeedback={handleFeedback}\n onTagClick={toggleTag}\n />\n )}\n {visible.map(b => (\n <BlockCard\n key={b.id}\n block={b}\n feedback={feedback[b.id]}\n existingIds={blocks.map(x => x.id)}\n onSave={block => handleSave(block, false)}\n onDelete={() => handleDelete(b.id)}\n onFeedback={handleFeedback}\n onTagClick={toggleTag}\n />\n ))}\n </div>\n </div>\n )\n}\n","import { createRoot, type Root } from 'react-dom/client'\nimport { logger } from '@wolffm/logger/client'\nimport { BuilderApp } from './BuilderApp'\nimport { setBuilderApiBaseUrl } from './api'\n// CSS is inlined into the bundle (not emitted as a separate asset) so builder.js\n// is fully self-contained — it can be served from /mf/resume/builder.js and\n// mounted by a bare page with no <link> wiring. Theme tokens come from the host\n// page; builder.css references them with fallbacks so it degrades standalone.\nimport builderCss from './builder.css?inline'\n\nfunction injectStyles() {\n if (document.getElementById('rb-builder-styles')) return\n const style = document.createElement('style')\n style.id = 'rb-builder-styles'\n style.textContent = builderCss\n document.head.appendChild(style)\n}\n\nexport interface ResumeBuilderProps {\n theme?: string\n /** API root, e.g. '/resume/api' — the builder calls ${apiBaseUrl}/builder/*. */\n apiBaseUrl: string\n}\n\ninterface BuilderElement extends HTMLElement {\n __root?: Root\n}\n\nexport function mount(el: HTMLElement, props: ResumeBuilderProps) {\n injectStyles()\n setBuilderApiBaseUrl(props.apiBaseUrl)\n const root = createRoot(el)\n root.render(<BuilderApp />)\n ;(el as BuilderElement).__root = root\n logger.info('[resume-builder] Mounted', { apiBaseUrl: props.apiBaseUrl })\n}\n\nexport function unmount(el: HTMLElement) {\n ;(el as BuilderElement).__root?.unmount()\n logger.info('[resume-builder] Unmounted')\n}\n"],"names":["BuilderApiError","message","status","baseUrl","setBuilderApiBaseUrl","url","req","path","init","res","detail","body","fetchBlocks","saveBlock","block","deleteBlock","id","saveFeedback","text","BLOCK_TYPES","BlockCard","feedback","existingIds","isNew","onSave","onDelete","onCancel","onFeedback","onTagClick","draft","setDraft","useState","tagsText","setTagsText","saving","setSaving","saveError","setSaveError","fbText","setFbText","fbStatus","setFbStatus","fbTimer","useRef","useEffect","dirty","parseTags","idInvalid","handleSave","err","handleDelete","onFbChange","jsxs","jsx","e","t","facetOf","PREFS_API_BASE","ANON_USER_ID","WHOAMI_GLOBAL","SESSION_SCOPE_PREFIX","UNSCOPED_ID","adaptWhoami","userType","name","fetchWhoamiRaw","userKey","headers","resp","defaultWhoamiUrl","loc","whoami","opts","g","shared","redirectToAuth","ret","useBuilderAuth","setStatus","setName","cancelled","who","logger","tag","emptyBlock","BuilderApp","authStatus","adminName","blocks","setBlocks","setFeedback","loading","setLoading","error","setError","search","setSearch","typeFilter","setTypeFilter","activeTags","setActiveTags","andMode","setAndMode","sort","setSort","creating","setCreating","msg","tagCounts","useMemo","counts","b","sortedTags","a","visible","q","v","hits","toggleTag","prev","next","saved","idx","copy","apiDeleteBlock","handleFeedback","exportJson","blob","x","injectStyles","style","builderCss","mount","el","props","root","createRoot","unmount"],"mappings":"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","x_google_ignoreList":[2]}
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ export interface ChatInterfaceRef {
3
+ askAbout: (text: string) => void;
4
+ }
5
+ interface ChatInterfaceProps {
6
+ ownerName?: string;
7
+ }
8
+ declare const ChatInterface: React.ForwardRefExoticComponent<ChatInterfaceProps & React.RefAttributes<ChatInterfaceRef>>;
9
+ export default ChatInterface;
10
+ //# sourceMappingURL=ChatInterface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatInterface.d.ts","sourceRoot":"","sources":["../../src/components/ChatInterface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuE,MAAM,OAAO,CAAA;AAO3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAED,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,QAAA,MAAM,aAAa,6FAyMlB,CAAA;AAID,eAAe,aAAa,CAAA"}
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ export declare const PAGE_BREAK_MARKER = "<!-- page-break -->";
3
+ /**
4
+ * The assembler emits one `# Heading` per résumé section (worker/src/skeleton.ts
5
+ * SECTION_HEADINGS). We split on those headings rather than handing the whole
6
+ * document to one ReactMarkdown pass, because the same markdown construct means
7
+ * different things per section — `###` is a job title under Experience and a
8
+ * project category under Projects, and `**bold** …` is a project entry under
9
+ * Projects and a skills row under Technical Skills. Splitting gives each section
10
+ * its own wrapper class, so the stylesheet can say so without heuristics.
11
+ */
12
+ interface Section {
13
+ /** Slug of the heading text, or `masthead` for the pre-heading name block. */
14
+ id: string;
15
+ /** Heading text; null for the masthead, which is the name/contact line itself. */
16
+ heading: string | null;
17
+ body: string;
18
+ }
19
+ export declare function splitSections(page: string): Section[];
20
+ interface ResumeDocumentProps {
21
+ content: string;
22
+ /** `letter` skips the masthead treatment: a cover letter is three paragraphs
23
+ * of prose with no headings, so every one of them lands in the pre-heading
24
+ * section and none of them is a name/contact block. */
25
+ variant?: 'resume' | 'letter';
26
+ }
27
+ /** Render résumé markdown as a stack of sheets, honouring page-break markers. */
28
+ export declare function ResumeDocument({ content, variant }: ResumeDocumentProps): React.ReactElement;
29
+ export {};
30
+ //# sourceMappingURL=ResumeDocument.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResumeDocument.d.ts","sourceRoot":"","sources":["../../src/components/ResumeDocument.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmC,MAAM,OAAO,CAAA;AAQvD,eAAO,MAAM,iBAAiB,wBAAwB,CAAA;AAEtD;;;;;;;;GAQG;AACH,UAAU,OAAO;IACf,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAA;IACV,kFAAkF;IAClF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb;AAWD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAuBrD;AAkLD,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf;;4DAEwD;IACxD,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAC9B;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,OAAkB,EACnB,EAAE,mBAAmB,GAAG,KAAK,CAAC,YAAY,CAoB1C"}
@@ -0,0 +1,12 @@
1
+ interface ResumeViewerProps {
2
+ onAskAbout: (text: string) => void;
3
+ ownerName?: string;
4
+ /** Target of the toolbar's "Schedule a Meeting" button. */
5
+ contactUrl?: string;
6
+ }
7
+ /** Download filename stem: "Matthaeus Wolff" → "matthaeus-wolff-resume". Must
8
+ * stay in sync with the worker's /resume.pdf Content-Disposition filename. */
9
+ export declare function resumeFilenameBase(ownerName?: string): string;
10
+ export default function ResumeViewer({ onAskAbout, ownerName, contactUrl }: ResumeViewerProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=ResumeViewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResumeViewer.d.ts","sourceRoot":"","sources":["../../src/components/ResumeViewer.tsx"],"names":[],"mappings":"AAKA,UAAU,iBAAiB;IACzB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAOD;+EAC+E;AAC/E,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAO7D;AAED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,UAAU,EACV,SAAS,EACT,UAAgC,EACjC,EAAE,iBAAiB,2CA2PnB"}
@@ -0,0 +1,154 @@
1
+ import { jsx as t, jsxs as l } from "react/jsx-runtime";
2
+ import { createRoot as y } from "react-dom/client";
3
+ import { logger as h } from "@wolffm/logger/client";
4
+ import { lazy as f, useRef as b, useState as m, useEffect as g, Suspense as d } from "react";
5
+ import { LoadingSkeleton as C, AppHeader as N } from "@wolffm/task-ui-components";
6
+ import { HadokuThemeRoot as R, useHadokuTheme as A } from "@wolffm/themes";
7
+ const U = f(() => import("./ResumeViewer-C-uxZ-7E.js")), L = f(() => import("./ChatInterface-mHtxUGpp.js"));
8
+ function j(e) {
9
+ const n = b(null);
10
+ return /* @__PURE__ */ t(R, { theme: e.theme, containerRef: n, children: /* @__PURE__ */ t(T, { ...e, containerRef: n }) });
11
+ }
12
+ function T(e) {
13
+ const { containerRef: n } = e, r = b(null), [o] = m(() => window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)").matches : !1), { theme: u, isDarkTheme: p, isThemeReady: w, isInitialThemeLoad: _ } = A(), [a, i] = m(!1), k = (s) => {
14
+ i(!0), r.current?.askAbout(s);
15
+ };
16
+ return g(() => {
17
+ if (!a) return;
18
+ const s = (v) => {
19
+ v.key === "Escape" && i(!1);
20
+ };
21
+ return document.addEventListener("keydown", s), () => document.removeEventListener("keydown", s);
22
+ }, [a]), _ && !w ? /* @__PURE__ */ t(C, { isDarkTheme: o }) : /* @__PURE__ */ t(
23
+ "div",
24
+ {
25
+ ref: n,
26
+ className: "resume-bot-container",
27
+ "data-theme": u,
28
+ "data-dark-theme": p ? "true" : "false",
29
+ children: /* @__PURE__ */ l("div", { className: "resume-bot", children: [
30
+ /* @__PURE__ */ t(N, { title: e.appName ?? "Resume" }),
31
+ /* @__PURE__ */ l("main", { className: "resume-bot__content", children: [
32
+ /* @__PURE__ */ t("div", { className: "resume-bot__resume-section", children: /* @__PURE__ */ t(
33
+ d,
34
+ {
35
+ fallback: /* @__PURE__ */ t("div", { className: "resume-viewer resume-viewer--loading", children: /* @__PURE__ */ t("p", { children: "Loading resume..." }) }),
36
+ children: /* @__PURE__ */ t(
37
+ U,
38
+ {
39
+ onAskAbout: k,
40
+ ownerName: e.ownerName,
41
+ contactUrl: e.contactUrl
42
+ }
43
+ )
44
+ }
45
+ ) }),
46
+ /* @__PURE__ */ t(
47
+ "button",
48
+ {
49
+ type: "button",
50
+ className: a ? "resume-bot__chat-scrim resume-bot__chat-scrim--open" : "resume-bot__chat-scrim",
51
+ "aria-label": "Close chat",
52
+ onClick: () => i(!1)
53
+ }
54
+ ),
55
+ /* @__PURE__ */ t(
56
+ "div",
57
+ {
58
+ id: "resume-bot-chat",
59
+ className: a ? "resume-bot__chat-section resume-bot__chat-section--open" : "resume-bot__chat-section",
60
+ children: /* @__PURE__ */ t(d, { fallback: /* @__PURE__ */ t("div", { className: "chat-interface__loading", children: "Loading chat..." }), children: /* @__PURE__ */ t(L, { ref: r, ownerName: e.ownerName }) })
61
+ }
62
+ )
63
+ ] }),
64
+ /* @__PURE__ */ t(
65
+ "button",
66
+ {
67
+ type: "button",
68
+ className: "resume-bot__chat-fab",
69
+ "aria-expanded": a,
70
+ "aria-controls": "resume-bot-chat",
71
+ "aria-label": a ? "Close chat" : "Chat about this resume",
72
+ onClick: () => i((s) => !s),
73
+ children: a ? /* @__PURE__ */ t($, {}) : /* @__PURE__ */ t(I, {})
74
+ }
75
+ )
76
+ ] })
77
+ }
78
+ );
79
+ }
80
+ function I() {
81
+ return /* @__PURE__ */ t("svg", { viewBox: "0 0 24 24", width: "24", height: "24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ t("path", { d: "M12 3c-4.97 0-9 3.36-9 7.5 0 2.3 1.25 4.36 3.2 5.73-.14 1.2-.6 2.3-1.36 3.24a.5.5 0 0 0 .46.82c1.9-.3 3.44-1.05 4.6-1.9.67.14 1.37.21 2.1.21 4.97 0 9-3.36 9-7.5S16.97 3 12 3Z" }) });
82
+ }
83
+ function $() {
84
+ return /* @__PURE__ */ t(
85
+ "svg",
86
+ {
87
+ viewBox: "0 0 24 24",
88
+ width: "24",
89
+ height: "24",
90
+ fill: "none",
91
+ stroke: "currentColor",
92
+ strokeWidth: "2",
93
+ strokeLinecap: "round",
94
+ "aria-hidden": "true",
95
+ children: /* @__PURE__ */ t("path", { d: "M6 6l12 12M18 6L6 18" })
96
+ }
97
+ );
98
+ }
99
+ let c = "";
100
+ function B(e) {
101
+ c = e.replace(/\/+$/, "");
102
+ }
103
+ async function P(e) {
104
+ const n = await fetch(`${c}/chat`, {
105
+ method: "POST",
106
+ headers: {
107
+ "Content-Type": "application/json"
108
+ },
109
+ body: JSON.stringify({ messages: e })
110
+ });
111
+ if (!n.ok) {
112
+ const r = await n.json();
113
+ throw new Error(r.message || r.error || "Failed to send message");
114
+ }
115
+ return await n.json();
116
+ }
117
+ async function H(e) {
118
+ const n = e ? `${c}/resume?v=${encodeURIComponent(e)}` : `${c}/resume`, r = await fetch(n);
119
+ if (!r.ok) {
120
+ const u = await r.json();
121
+ throw new Error(u.error || "Failed to fetch resume");
122
+ }
123
+ const o = await r.json();
124
+ return {
125
+ content: o.content,
126
+ coverLetter: o.cover_letter ?? null,
127
+ variant: o.variant ?? null,
128
+ label: o.label ?? null,
129
+ company: o.company ?? null,
130
+ jobTitle: o.job_title ?? null
131
+ };
132
+ }
133
+ function F(e) {
134
+ return e ? `${c}/resume.pdf?v=${encodeURIComponent(e)}` : `${c}/resume.pdf`;
135
+ }
136
+ function z(e, n) {
137
+ B(n.apiBaseUrl);
138
+ const r = y(e);
139
+ r.render(/* @__PURE__ */ t(j, { ...n })), e.__root = r, h.info("[resume-bot] Mounted successfully", {
140
+ theme: n.theme,
141
+ apiBaseUrl: n.apiBaseUrl
142
+ });
143
+ }
144
+ function J(e) {
145
+ e.__root?.unmount(), h.info("[resume-bot] Unmounted successfully");
146
+ }
147
+ export {
148
+ H as f,
149
+ z as m,
150
+ F as r,
151
+ P as s,
152
+ J as u
153
+ };
154
+ //# sourceMappingURL=entry-BTJjyohQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-BTJjyohQ.js","sources":["../src/App.tsx","../src/services/api.ts","../src/entry.tsx"],"sourcesContent":["import React, { lazy, Suspense, useEffect, useRef, useState, type RefObject } from 'react'\nimport { AppHeader, LoadingSkeleton } from '@wolffm/task-ui-components'\nimport { useHadokuTheme, HadokuThemeRoot } from '@wolffm/themes'\nimport type { ResumeBotAppProps } from './entry'\nimport type { ChatInterfaceRef } from './components/ChatInterface'\n\nconst ResumeViewer = lazy(() => import('./components/ResumeViewer'))\nconst ChatInterface = lazy(() => import('./components/ChatInterface'))\n\n/**\n * Provider boundary. Theme state belongs to the platform (@wolffm/themes),\n * not to this app — the local hooks/useTheme.ts, prefs/themePrefs.ts and\n * app/themeConfig.tsx copies are gone. AppHeader renders the shared picker\n * from this context, so nothing below passes one.\n */\nexport default function App(props: ResumeBotAppProps) {\n const containerRef = useRef<HTMLDivElement>(null)\n return (\n <HadokuThemeRoot theme={props.theme} containerRef={containerRef}>\n <AppInner {...props} containerRef={containerRef} />\n </HadokuThemeRoot>\n )\n}\n\nfunction AppInner(props: ResumeBotAppProps & { containerRef: RefObject<HTMLDivElement | null> }) {\n const { containerRef } = props\n const chatRef = useRef<ChatInterfaceRef>(null)\n\n // Detect system preference for loading skeleton\n const [systemPrefersDark] = useState(() => {\n if (window.matchMedia) {\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n }\n return false\n })\n\n // Theme comes from <HadokuThemeRoot> above — one implementation for\n // every app, instead of this repo's former hooks/useTheme.ts copy.\n const { theme, isDarkTheme, isThemeReady, isInitialThemeLoad } = useHadokuTheme()\n\n /**\n * Open state of the mobile chat panel. Only the narrow layout reads it: above\n * the breakpoint the chat pane is a permanent column and the class this\n * toggles has no rules attached, so one piece of state serves both layouts and\n * the chat is never remounted (and never loses its transcript) on a resize or\n * a device rotation.\n */\n const [chatOpen, setChatOpen] = useState(false)\n\n const handleAskAbout = (text: string) => {\n // On mobile the chat is behind the bubble, so a question asked from the\n // résumé's context menu has to bring the panel with it.\n setChatOpen(true)\n chatRef.current?.askAbout(text)\n }\n\n useEffect(() => {\n if (!chatOpen) return\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setChatOpen(false)\n }\n document.addEventListener('keydown', onKeyDown)\n return () => document.removeEventListener('keydown', onKeyDown)\n }, [chatOpen])\n\n // Show loading skeleton during initial theme load to prevent FOUC\n if (isInitialThemeLoad && !isThemeReady) {\n return <LoadingSkeleton isDarkTheme={systemPrefersDark} />\n }\n\n return (\n <div\n ref={containerRef}\n className=\"resume-bot-container\"\n data-theme={theme}\n data-dark-theme={isDarkTheme ? 'true' : 'false'}\n >\n <div className=\"resume-bot\">\n {/* DERIVED, never written here: `appName` is the platform's answer from\n spec/categories.json (registry props -> mount), and __HADOKU_APP_NAME__ is\n the standalone fallback vite fills in from @wolffm/catalogue. Do not put a\n string back. */}\n <AppHeader title={props.appName ?? __HADOKU_APP_NAME__} />\n\n <main className=\"resume-bot__content\">\n <div className=\"resume-bot__resume-section\">\n <Suspense\n fallback={\n <div className=\"resume-viewer resume-viewer--loading\">\n <p>Loading resume...</p>\n </div>\n }\n >\n <ResumeViewer\n onAskAbout={handleAskAbout}\n ownerName={props.ownerName}\n contactUrl={props.contactUrl}\n />\n </Suspense>\n </div>\n {/* Mobile only: taps outside the open panel dismiss it. A button\n rather than a bare div so the same affordance is reachable from a\n keyboard; `display: none` keeps it out of the tab order whenever\n the panel is closed or the layout is wide. */}\n <button\n type=\"button\"\n className={\n chatOpen\n ? 'resume-bot__chat-scrim resume-bot__chat-scrim--open'\n : 'resume-bot__chat-scrim'\n }\n aria-label=\"Close chat\"\n onClick={() => setChatOpen(false)}\n />\n <div\n id=\"resume-bot-chat\"\n className={\n chatOpen\n ? 'resume-bot__chat-section resume-bot__chat-section--open'\n : 'resume-bot__chat-section'\n }\n >\n <Suspense fallback={<div className=\"chat-interface__loading\">Loading chat...</div>}>\n <ChatInterface ref={chatRef} ownerName={props.ownerName} />\n </Suspense>\n </div>\n </main>\n\n {/* The bubble is always in the DOM and hidden by the stylesheet above the\n mobile breakpoint, so the wide layout is untouched by any of this. */}\n <button\n type=\"button\"\n className=\"resume-bot__chat-fab\"\n aria-expanded={chatOpen}\n aria-controls=\"resume-bot-chat\"\n aria-label={chatOpen ? 'Close chat' : 'Chat about this resume'}\n onClick={() => setChatOpen(open => !open)}\n >\n {chatOpen ? <CloseIcon /> : <ChatIcon />}\n </button>\n </div>\n </div>\n )\n}\n\n/* Inline so the widget ships no icon dependency and the glyphs inherit the\n * bubble's colour. */\nfunction ChatIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M12 3c-4.97 0-9 3.36-9 7.5 0 2.3 1.25 4.36 3.2 5.73-.14 1.2-.6 2.3-1.36 3.24a.5.5 0 0 0 .46.82c1.9-.3 3.44-1.05 4.6-1.9.67.14 1.37.21 2.1.21 4.97 0 9-3.36 9-7.5S16.97 3 12 3Z\" />\n </svg>\n )\n}\n\nfunction CloseIcon() {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M6 6l12 12M18 6L6 18\" />\n </svg>\n )\n}\n","export interface ChatMessage {\r\n role: 'user' | 'assistant' | 'system'\r\n content: string\r\n}\r\n\r\nexport interface ChatResponse {\r\n message: string\r\n usage?: {\r\n prompt_tokens: number\r\n completion_tokens: number\r\n total_tokens: number\r\n }\r\n}\r\n\r\nexport interface ResumeResponse {\r\n content: string\r\n variant?: string\r\n label?: string\r\n cover_letter?: string | null\r\n company?: string | null\r\n job_title?: string | null\r\n}\r\n\r\n/** The delivered application packet: résumé + optional tailored cover letter. */\r\nexport interface ResumePacket {\r\n content: string\r\n coverLetter: string | null\r\n variant: string | null\r\n label: string | null\r\n company: string | null\r\n jobTitle: string | null\r\n}\r\n\r\nexport interface ApiError {\r\n error: string\r\n message?: string\r\n retryAfter?: number\r\n}\r\n\r\n// Runtime API root — always set by mount() via setApiBaseUrl(props.apiBaseUrl).\r\n// The dev harness (index.html) supplies https://hadoku.me/resume/api; prod passes\r\n// the real path. Empty until then — there is no local backend to default to (the\r\n// old Express server was removed).\r\nlet API_BASE_URL = ''\r\n\r\n/**\r\n * Set the API root at runtime.\r\n * Called by the mount function with the apiBaseUrl prop, which is the API root\r\n * ('/resume/api') — endpoints are appended to it directly.\r\n */\r\nexport function setApiBaseUrl(url: string): void {\r\n API_BASE_URL = url.replace(/\\/+$/, '')\r\n}\r\n\r\n/**\r\n * Send a chat message to the backend\r\n */\r\nexport async function sendChatMessage(messages: ChatMessage[]): Promise<ChatResponse> {\r\n const response = await fetch(`${API_BASE_URL}/chat`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ messages })\r\n })\r\n\r\n if (!response.ok) {\r\n const errorData = (await response.json()) as ApiError\r\n throw new Error(errorData.message || errorData.error || 'Failed to send message')\r\n }\r\n\r\n return (await response.json()) as ChatResponse\r\n}\r\n\r\n/**\r\n * Fetch the resume content from the backend.\r\n * Pass a variant slug to fetch a link-tailored resume; the API falls back to\r\n * the full resume for unknown or expired slugs.\r\n */\r\nexport async function fetchResume(variant?: string): Promise<ResumePacket> {\r\n const url = variant\r\n ? `${API_BASE_URL}/resume?v=${encodeURIComponent(variant)}`\r\n : `${API_BASE_URL}/resume`\r\n const response = await fetch(url)\r\n\r\n if (!response.ok) {\r\n const errorData = (await response.json()) as ApiError\r\n throw new Error(errorData.error || 'Failed to fetch resume')\r\n }\r\n\r\n const data = (await response.json()) as ResumeResponse\r\n return {\r\n content: data.content,\r\n coverLetter: data.cover_letter ?? null,\r\n variant: data.variant ?? null,\r\n label: data.label ?? null,\r\n company: data.company ?? null,\r\n jobTitle: data.job_title ?? null\r\n }\r\n}\r\n\r\n/**\r\n * URL of the server-rendered resume PDF (the packet, when the variant carries a\r\n * cover letter). Same variant fall-back semantics as fetchResume.\r\n *\r\n * Returned as a URL rather than a fetched Blob on purpose: the endpoint already\r\n * sends `Content-Disposition: attachment` with the owner's filename, so linking\r\n * straight at it downloads the file with no object URL in the middle — and an\r\n * attachment cannot be hijacked by a browser's \"open PDFs in Firefox\" handler\r\n * the way a `blob:` URL can.\r\n */\r\nexport function resumePdfUrl(variant?: string): string {\r\n return variant\r\n ? `${API_BASE_URL}/resume.pdf?v=${encodeURIComponent(variant)}`\r\n : `${API_BASE_URL}/resume.pdf`\r\n}\r\n","import { createRoot, type Root } from 'react-dom/client'\nimport { logger } from '@wolffm/logger/client'\nimport App from './App'\nimport { setApiBaseUrl } from './services/api'\n// REQUIRED: Import @wolffm/themes CSS - DO NOT REMOVE\nimport '@wolffm/themes/style.css'\n// REQUIRED: Import theme picker CSS\nimport '@wolffm/task-ui-components/theme-picker.css'\nimport '@wolffm/task-ui-components/app-header.css'\nimport './styles/index.css'\n\n// Props interface for configuration from parent app\nexport interface ResumeBotAppProps {\n /**\n * The app's DISPLAY NAME, resolved by the platform from hadoku_site's\n * spec/categories.json — the same file that titles the browser tab and the\n * homepage tile. Render this; never hard-code the name in this repo.\n *\n * Absent when the app runs standalone (its own vite dev server, no host),\n * which is what `__HADOKU_APP_NAME__` covers — see vite.config.ts.\n */\n appName?: string\n theme?: string // Theme passed from parent (e.g., 'default', 'ocean', 'forest')\n // API root (required), e.g. '/resume/api' or 'https://api.yourapp.com/api'.\n // Endpoints are appended directly: ${apiBaseUrl}/chat, ${apiBaseUrl}/resume.\n apiBaseUrl: string\n ownerName?: string // Name shown in chat welcome message (default: 'the candidate')\n // Where the toolbar's \"Schedule a Meeting\" button points. Defaults to the\n // host site's own /contact/ page, which is where the widget's deployment\n // (hadoku.me/resume) keeps its scheduling form.\n contactUrl?: string\n}\n\n// Extend HTMLElement to include __root property\ninterface ResumeBotAppElement extends HTMLElement {\n __root?: Root\n}\n\n// Mount function - called by parent to initialize resume bot\nexport function mount(el: HTMLElement, props: ResumeBotAppProps) {\n // Set the API base URL for all API calls\n setApiBaseUrl(props.apiBaseUrl)\n\n const root = createRoot(el)\n root.render(<App {...props} />)\n ;(el as ResumeBotAppElement).__root = root\n logger.info('[resume-bot] Mounted successfully', {\n theme: props.theme,\n apiBaseUrl: props.apiBaseUrl\n })\n}\n\n// Unmount function - called by parent to cleanup resume bot\nexport function unmount(el: HTMLElement) {\n ;(el as ResumeBotAppElement).__root?.unmount()\n logger.info('[resume-bot] Unmounted successfully')\n}\n"],"names":["ResumeViewer","lazy","ChatInterface","App","props","containerRef","useRef","HadokuThemeRoot","jsx","AppInner","chatRef","systemPrefersDark","useState","theme","isDarkTheme","isThemeReady","isInitialThemeLoad","useHadokuTheme","chatOpen","setChatOpen","handleAskAbout","text","useEffect","onKeyDown","e","LoadingSkeleton","jsxs","AppHeader","Suspense","open","CloseIcon","ChatIcon","API_BASE_URL","setApiBaseUrl","url","sendChatMessage","messages","response","errorData","fetchResume","variant","data","resumePdfUrl","mount","el","root","createRoot","logger","unmount"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import '@wolffm/themes/style.css';
2
+ import '@wolffm/task-ui-components/theme-picker.css';
3
+ import '@wolffm/task-ui-components/app-header.css';
4
+ import './styles/index.css';
5
+ export interface ResumeBotAppProps {
6
+ /**
7
+ * The app's DISPLAY NAME, resolved by the platform from hadoku_site's
8
+ * spec/categories.json — the same file that titles the browser tab and the
9
+ * homepage tile. Render this; never hard-code the name in this repo.
10
+ *
11
+ * Absent when the app runs standalone (its own vite dev server, no host),
12
+ * which is what `__HADOKU_APP_NAME__` covers — see vite.config.ts.
13
+ */
14
+ appName?: string;
15
+ theme?: string;
16
+ apiBaseUrl: string;
17
+ ownerName?: string;
18
+ contactUrl?: string;
19
+ }
20
+ export declare function mount(el: HTMLElement, props: ResumeBotAppProps): void;
21
+ export declare function unmount(el: HTMLElement): void;
22
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../src/entry.tsx"],"names":[],"mappings":"AAKA,OAAO,0BAA0B,CAAA;AAEjC,OAAO,6CAA6C,CAAA;AACpD,OAAO,2CAA2C,CAAA;AAClD,OAAO,oBAAoB,CAAA;AAG3B,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IAGd,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAIlB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAQD,wBAAgB,KAAK,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,iBAAiB,QAW9D;AAGD,wBAAgB,OAAO,CAAC,EAAE,EAAE,WAAW,QAGtC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import "react/jsx-runtime";
2
+ import "react-dom/client";
3
+ import "@wolffm/logger/client";
4
+ import { m as u, u as i } from "./entry-BTJjyohQ.js";
5
+ export {
6
+ u as mount,
7
+ i as unmount
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}