@xom11/whiteboard 0.7.0 → 0.10.0

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 (50) hide show
  1. package/README.md +51 -1
  2. package/dist/chunk-74VEEZBV.mjs +619 -0
  3. package/dist/chunk-74VEEZBV.mjs.map +1 -0
  4. package/dist/{chunk-BJX4YNA5.mjs → chunk-G7FR3AIV.mjs} +68 -12
  5. package/dist/chunk-G7FR3AIV.mjs.map +1 -0
  6. package/dist/{chunk-SHFOGORM.mjs → chunk-PDKKDZ4H.mjs} +4 -4
  7. package/dist/{chunk-SHFOGORM.mjs.map → chunk-PDKKDZ4H.mjs.map} +1 -1
  8. package/dist/chunk-PWIMZIB6.mjs +62 -0
  9. package/dist/chunk-PWIMZIB6.mjs.map +1 -0
  10. package/dist/{chunk-LPM4MM45.mjs → chunk-SBDMF4NQ.mjs} +3 -2
  11. package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
  12. package/dist/chunk-WQOABS6N.mjs +197 -0
  13. package/dist/chunk-WQOABS6N.mjs.map +1 -0
  14. package/dist/{chunk-3SSQKRRO.mjs → chunk-ZVN356JZ.mjs} +4 -4
  15. package/dist/{chunk-3SSQKRRO.mjs.map → chunk-ZVN356JZ.mjs.map} +1 -1
  16. package/dist/geometry-2d.js +344 -228
  17. package/dist/geometry-2d.js.map +1 -1
  18. package/dist/geometry-2d.mjs +2 -2
  19. package/dist/geometry-3d.d.mts +1 -1
  20. package/dist/geometry-3d.d.ts +1 -1
  21. package/dist/geometry-3d.js +3411 -1277
  22. package/dist/geometry-3d.js.map +1 -1
  23. package/dist/geometry-3d.mjs +3 -2
  24. package/dist/graph-2d.js +360 -66
  25. package/dist/graph-2d.js.map +1 -1
  26. package/dist/graph-2d.mjs +2 -2
  27. package/dist/{host-T2W6R6SO.mjs → host-DJETSFCG.mjs} +272 -223
  28. package/dist/host-DJETSFCG.mjs.map +1 -0
  29. package/dist/{host-2QGKMGCT.mjs → host-LZH2FZ2N.mjs} +3 -3
  30. package/dist/{host-2QGKMGCT.mjs.map → host-LZH2FZ2N.mjs.map} +1 -1
  31. package/dist/host-N6ACNJKI.mjs +3226 -0
  32. package/dist/host-N6ACNJKI.mjs.map +1 -0
  33. package/dist/index.d.mts +133 -6
  34. package/dist/index.d.ts +133 -6
  35. package/dist/index.js +5634 -1999
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +1231 -146
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +9 -6
  40. package/dist/chunk-BJX4YNA5.mjs.map +0 -1
  41. package/dist/chunk-DJTBZEAR.mjs +0 -25
  42. package/dist/chunk-DJTBZEAR.mjs.map +0 -1
  43. package/dist/chunk-HM7RIXJE.mjs +0 -331
  44. package/dist/chunk-HM7RIXJE.mjs.map +0 -1
  45. package/dist/chunk-HYXFHEDJ.mjs +0 -129
  46. package/dist/chunk-HYXFHEDJ.mjs.map +0 -1
  47. package/dist/chunk-LPM4MM45.mjs.map +0 -1
  48. package/dist/host-T2W6R6SO.mjs.map +0 -1
  49. package/dist/host-XUFON6CQ.mjs +0 -1422
  50. package/dist/host-XUFON6CQ.mjs.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/stamps/shared/useChordShortcut.ts","../src/stamps/shared/MobileToolDrawer.tsx"],"names":[],"mappings":"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","file":"chunk-LPM4MM45.mjs","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface UseChordShortcutArgs<G extends string> {\n groupOrder: readonly G[];\n tools: ReadonlyArray<{ key: string; group: G }>;\n onSelect: (toolKey: string) => void;\n enabled: boolean;\n}\n\ninterface UseChordShortcutResult<G extends string> {\n chordGroup: G | null;\n cancel: () => void;\n}\n\nconst A_CODE = 'a'.charCodeAt(0);\n\nfunction isFieldFocused(): boolean {\n const ae = (typeof document !== 'undefined'\n ? (document.activeElement as HTMLElement | null)\n : null);\n return !!(\n ae &&\n (ae.tagName === 'INPUT' ||\n ae.tagName === 'TEXTAREA' ||\n ae.isContentEditable)\n );\n}\n\nexport function useChordShortcut<G extends string>(\n args: UseChordShortcutArgs<G>,\n): UseChordShortcutResult<G> {\n const { groupOrder, tools, onSelect, enabled } = args;\n\n const [chordGroup, setChordGroup] = useState<G | null>(null);\n\n const groupOrderRef = useRef(groupOrder);\n const toolsRef = useRef(tools);\n const onSelectRef = useRef(onSelect);\n const chordGroupRef = useRef<G | null>(null);\n\n groupOrderRef.current = groupOrder;\n toolsRef.current = tools;\n onSelectRef.current = onSelect;\n // chordGroupRef được sync ngay trong handler (xem `setChord` dưới đây)\n // thay vì ghi từ render body — nếu ghi ở body sẽ bị React batch hoá khi\n // hai event xảy ra trong cùng một act() (event sau đọc giá trị cũ).\n\n const cancel = useCallback(() => {\n chordGroupRef.current = null;\n setChordGroup(null);\n }, []);\n\n useEffect(() => {\n if (!enabled) return;\n\n const setChord = (next: G | null) => {\n chordGroupRef.current = next;\n setChordGroup(next);\n };\n\n const onKey = (e: KeyboardEvent) => {\n if (e.metaKey || e.ctrlKey || e.altKey) return;\n if (isFieldFocused()) return;\n\n const key = e.key;\n const lower = key.length === 1 ? key.toLowerCase() : key;\n\n if (key === 'Escape') {\n if (chordGroupRef.current !== null) {\n e.preventDefault();\n e.stopPropagation();\n setChord(null);\n }\n return;\n }\n\n if (lower.length === 1 && lower >= 'a' && lower <= 'z') {\n const idx = lower.charCodeAt(0) - A_CODE;\n if (idx < groupOrderRef.current.length) {\n e.preventDefault();\n e.stopPropagation();\n setChord(groupOrderRef.current[idx]);\n }\n return;\n }\n\n if (key >= '1' && key <= '9') {\n const active = chordGroupRef.current;\n if (active === null) return;\n const n = key.charCodeAt(0) - '1'.charCodeAt(0); // 0-indexed\n const toolsInGroup = toolsRef.current.filter(\n (t) => t.group === active,\n );\n e.preventDefault();\n e.stopPropagation();\n if (n < toolsInGroup.length) {\n onSelectRef.current(toolsInGroup[n].key);\n }\n setChord(null);\n return;\n }\n };\n\n window.addEventListener('keydown', onKey, { capture: true });\n return () => {\n window.removeEventListener('keydown', onKey, { capture: true });\n };\n }, [enabled]);\n\n return { chordGroup, cancel };\n}\n","'use client';\n\nimport React from 'react';\n\n/**\n * Generic mobile tool drawer dùng chung cho geometry-2d + geometry-3d.\n *\n * Layout:\n * - Header: icon + title + close\n * - Sticky toolbar: chip switches (Trục/Lưới) + icon-actions (Reset, Undo)\n * - Body: section dọc, mỗi section là 1 nhóm tools, grid 3-col card có nhãn\n *\n * Style: soft-modern, emerald accent, khớp các class trong shared/stamp.css.\n */\n\nexport interface MobileChip {\n /** Label hiển thị + aria-label */\n label: string;\n icon: React.ReactNode;\n pressed: boolean;\n onToggle: (next: boolean) => void;\n /** data-testid optional (để test cũ chạy được) */\n testId?: string;\n}\n\nexport interface MobileActionButton {\n label: string;\n icon: React.ReactNode;\n onClick: () => void;\n disabled?: boolean;\n title?: string;\n}\n\nexport interface MobileTool<TKey extends string> {\n key: TKey;\n label: string;\n icon: React.ReactNode;\n}\n\nexport interface MobileToolGroup<TKey extends string, TGroup extends string> {\n group: TGroup;\n groupLabel: string;\n tools: MobileTool<TKey>[];\n}\n\ninterface MobileToolDrawerProps<TKey extends string, TGroup extends string> {\n title: string;\n headerIcon: React.ReactNode;\n chips: MobileChip[];\n actions: MobileActionButton[];\n groups: MobileToolGroup<TKey, TGroup>[];\n activeTool: TKey;\n onToolSelect: (key: TKey) => void;\n drawerOpen: boolean;\n onDrawerClose: () => void;\n isDark?: boolean;\n /** data-testid trên <aside> — giữ để test cũ tìm được panel */\n testId?: string;\n}\n\nexport function MobileToolDrawer<TKey extends string, TGroup extends string>({\n title,\n headerIcon,\n chips,\n actions,\n groups,\n activeTool,\n onToolSelect,\n drawerOpen,\n onDrawerClose,\n isDark,\n testId,\n}: MobileToolDrawerProps<TKey, TGroup>) {\n return (\n <>\n {drawerOpen && (\n <div\n className=\"stamp-drawer-backdrop\"\n onPointerDown={onDrawerClose}\n aria-hidden=\"true\"\n />\n )}\n <aside\n role=\"complementary\"\n aria-label={title}\n aria-hidden={!drawerOpen ? 'true' : undefined}\n data-testid={testId}\n data-stamp-area=\"true\"\n data-mobile-drawer=\"true\"\n data-geo-mobile=\"true\"\n data-drawer-state={drawerOpen ? 'open' : 'closed'}\n className={[\n isDark ? 'theme--dark ' : '',\n 'stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md',\n ].join('')}\n >\n {/* Header */}\n <header className=\"flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3\">\n <h3 className=\"flex items-center gap-2 text-base font-semibold text-slate-800\">\n <span className=\"inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700\">\n {headerIcon}\n </span>\n {title}\n </h3>\n <button\n type=\"button\"\n onClick={onDrawerClose}\n aria-label=\"Đóng ngăn công cụ\"\n className=\"inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n </svg>\n </button>\n </header>\n\n {/* Sticky toolbar: chips + actions */}\n <div className=\"sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur\">\n {chips.map((c) => (\n <button\n key={c.label}\n type=\"button\"\n role=\"switch\"\n aria-pressed={c.pressed}\n aria-label={c.label}\n data-testid={c.testId}\n onClick={() => c.onToggle(!c.pressed)}\n className=\"geo-mobile-chip\"\n >\n {c.icon}\n {c.label}\n </button>\n ))}\n {actions.length > 0 && <div className=\"ml-auto flex items-center gap-1\">\n {actions.map((a) => (\n <button\n key={a.label}\n type=\"button\"\n onClick={a.onClick}\n disabled={a.disabled}\n aria-label={a.label}\n title={a.title ?? a.label}\n className=\"inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent\"\n >\n {a.icon}\n </button>\n ))}\n </div>}\n </div>\n\n {/* Body: groups xếp dọc */}\n <div\n className=\"min-h-0 flex-1 overflow-y-auto\"\n style={{ paddingBottom: 'calc(0.75rem + env(safe-area-inset-bottom))' }}\n >\n {groups.map((g) => (\n <section key={g.group} className=\"px-3 pt-3 pb-1\">\n <h4 className=\"mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500\">\n <span className=\"h-1 w-1 rounded-full bg-emerald-500\" />\n {g.groupLabel}\n </h4>\n <div className=\"grid grid-cols-3 gap-2\">\n {g.tools.map((t) => {\n const active = activeTool === t.key;\n return (\n <button\n key={t.key}\n type=\"button\"\n aria-label={t.label}\n aria-pressed={active}\n data-tool={t.key}\n onClick={() => {\n onToolSelect(t.key);\n onDrawerClose();\n }}\n className={[\n 'flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95',\n active\n ? 'geo-mobile-tool-active'\n : 'bg-slate-50 text-slate-700 hover:bg-slate-100',\n ].join(' ')}\n >\n <span className=\"flex h-6 w-6 items-center justify-center\">{t.icon}</span>\n <span className=\"text-center text-[11px] font-medium leading-tight line-clamp-2\">\n {t.label}\n </span>\n </button>\n );\n })}\n </div>\n </section>\n ))}\n </div>\n </aside>\n </>\n );\n}\n"]}