hocmai-feedback-widget 0.2.0 → 0.2.2

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 (57) hide show
  1. package/README.md +132 -97
  2. package/dist/capture/device-detection.d.ts +13 -0
  3. package/dist/capture/fonts.d.ts +4 -0
  4. package/dist/capture/index.d.ts +14 -0
  5. package/dist/capture/mathjax-fonts.d.ts +5 -0
  6. package/dist/capture/types.d.ts +25 -0
  7. package/dist/ckeditor.mjs +113 -0
  8. package/dist/ckeditor.mjs.map +1 -0
  9. package/dist/components/CkEditor.d.ts +14 -0
  10. package/dist/components/FeedbackWidget.d.ts +11 -0
  11. package/dist/context/FeedbackContext.d.ts +5 -0
  12. package/dist/hocmai-feedback-widget.mjs +7544 -0
  13. package/dist/hocmai-feedback-widget.mjs.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/package.json +32 -28
  16. package/src/App.tsx +143 -58
  17. package/src/capture/device-detection.ts +52 -0
  18. package/src/capture/fonts.ts +34 -0
  19. package/src/capture/index.ts +362 -0
  20. package/src/capture/mathjax-fonts.ts +143 -0
  21. package/src/capture/types.ts +34 -0
  22. package/src/ckeditor.d.ts +14 -0
  23. package/src/components/CkEditor.tsx +130 -0
  24. package/src/components/CropOverlay.tsx +29 -47
  25. package/src/components/FeedbackWidget.tsx +96 -90
  26. package/src/context/FeedbackContext.tsx +11 -4
  27. package/src/index.ts +9 -0
  28. package/src/main.tsx +7 -8
  29. package/src/utils/device.ts +14 -14
  30. package/dist/support-feedback-lib.es.js +0 -3867
  31. package/dist/support-feedback-lib.umd.js +0 -84
  32. package/dist/utils/screenshot/background-renderer.d.ts +0 -3
  33. package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
  34. package/dist/utils/screenshot/cleanup.d.ts +0 -3
  35. package/dist/utils/screenshot/device-detection.d.ts +0 -2
  36. package/dist/utils/screenshot/element-collector.d.ts +0 -10
  37. package/dist/utils/screenshot/image-renderer.d.ts +0 -21
  38. package/dist/utils/screenshot/performance.d.ts +0 -3
  39. package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
  40. package/dist/utils/screenshot/text-renderer.d.ts +0 -23
  41. package/dist/utils/screenshot/types.d.ts +0 -35
  42. package/dist/utils/screenshot.d.ts +0 -9
  43. package/dist/vite.svg +0 -1
  44. package/src/App.css +0 -42
  45. package/src/assets/react.svg +0 -1
  46. package/src/index.css +0 -68
  47. package/src/utils/screenshot/background-renderer.ts +0 -54
  48. package/src/utils/screenshot/canvas-renderer.ts +0 -217
  49. package/src/utils/screenshot/cleanup.ts +0 -32
  50. package/src/utils/screenshot/device-detection.ts +0 -38
  51. package/src/utils/screenshot/element-collector.ts +0 -83
  52. package/src/utils/screenshot/image-renderer.ts +0 -148
  53. package/src/utils/screenshot/performance.ts +0 -57
  54. package/src/utils/screenshot/svg-renderer.ts +0 -126
  55. package/src/utils/screenshot/text-renderer.ts +0 -212
  56. package/src/utils/screenshot/types.ts +0 -40
  57. package/src/utils/screenshot.ts +0 -68
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { default as FeedbackWidget } from './components/FeedbackWidget';
2
2
  export type { FeedbackData, FeedbackProps } from './components/FeedbackWidget';
3
+ export type { CkEditorProps } from './components/CkEditor';
3
4
  export { FeedbackProvider, useFeedback } from './context/FeedbackContext';
5
+ export { captureScreenshot, captureScreenshotWithOptions, prewarmCapture, } from './capture';
6
+ export type { Rect, CaptureProgress, CaptureOptions } from './capture/types';
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "hocmai-feedback-widget",
3
3
  "private": false,
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "type": "module",
6
- "main": "./dist/support-feedback-lib.umd.js",
7
- "module": "./dist/support-feedback-lib.es.js",
6
+ "description": "Feedback/report widget with MathJax-aware screenshot capture (snapdom + embedded fonts).",
7
+ "module": "./dist/hocmai-feedback-widget.mjs",
8
8
  "types": "./dist/index.d.ts",
9
9
  "files": [
10
10
  "dist",
@@ -13,44 +13,48 @@
13
13
  "exports": {
14
14
  ".": {
15
15
  "types": "./dist/index.d.ts",
16
- "import": "./dist/support-feedback-lib.es.js",
17
- "require": "./dist/support-feedback-lib.umd.js"
16
+ "import": "./dist/hocmai-feedback-widget.mjs"
17
+ },
18
+ "./ckeditor": {
19
+ "types": "./dist/components/CkEditor.d.ts",
20
+ "import": "./dist/ckeditor.mjs"
18
21
  }
19
22
  },
20
23
  "scripts": {
21
24
  "dev": "vite",
22
- "build": "tsc -b && vite build",
23
- "lint": "eslint .",
24
- "preview": "vite preview"
25
+ "build": "vite build",
26
+ "typecheck": "tsc --noEmit",
27
+ "preview": "vite preview",
28
+ "test": "playwright test"
25
29
  },
26
30
  "dependencies": {
31
+ "@zumer/snapdom": "^2.12.9",
27
32
  "lucide-react": "^0.562.0",
28
33
  "modern-screenshot": "^4.6.7",
29
34
  "ua-parser-js": "^2.0.7"
30
35
  },
31
36
  "peerDependencies": {
32
37
  "react": ">=16.8.0",
33
- "react-dom": ">=16.8.0"
38
+ "react-dom": ">=16.8.0",
39
+ "@ckeditor/ckeditor5-react": "*",
40
+ "hocmai-ckeditor-custom": "*",
41
+ "ckeditor5-classic-with-mathtype": "*"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@ckeditor/ckeditor5-react": { "optional": true },
45
+ "hocmai-ckeditor-custom": { "optional": true },
46
+ "ckeditor5-classic-with-mathtype": { "optional": true }
34
47
  },
35
48
  "devDependencies": {
36
- "@eslint/js": "^9.39.1",
37
- "@playwright/test": "^1.58.1",
38
- "@types/node": "^24.10.1",
39
- "@types/react": "^19.2.5",
40
- "@types/react-dom": "^19.2.3",
41
- "@types/ua-parser-js": "^0.7.39",
42
- "@vitejs/plugin-react": "^5.1.1",
43
- "eslint": "^9.39.1",
44
- "eslint-plugin-react-hooks": "^7.0.1",
45
- "eslint-plugin-react-refresh": "^0.4.24",
46
- "globals": "^16.5.0",
47
- "http-server": "^14.1.1",
48
- "patch-package": "^8.0.1",
49
- "react": "^19.2.0",
50
- "react-dom": "^19.2.0",
51
- "typescript": "~5.9.3",
52
- "typescript-eslint": "^8.46.4",
53
- "vite": "^7.2.4",
54
- "vite-plugin-dts": "^4.5.4"
49
+ "@playwright/test": "^1.48.0",
50
+ "@types/node": "^22.0.0",
51
+ "@types/react": "^18.3.0",
52
+ "@types/react-dom": "^18.3.0",
53
+ "@vitejs/plugin-react": "^4.3.0",
54
+ "react": "^18.3.1",
55
+ "react-dom": "^18.3.1",
56
+ "typescript": "~5.6.0",
57
+ "vite": "^5.4.0",
58
+ "vite-plugin-dts": "^4.2.0"
55
59
  }
56
60
  }
package/src/App.tsx CHANGED
@@ -1,75 +1,160 @@
1
- import { useState } from 'react'
2
- import { FeedbackProvider, useFeedback } from './context/FeedbackContext';
3
- import { type FeedbackData } from './components/FeedbackWidget';
4
- import './App.css'
1
+ import React from 'react';
2
+ import { FeedbackProvider, useFeedback, captureScreenshot, type FeedbackData } from './index';
5
3
 
6
- // Reusable Question Component
7
- const QuestionItem = ({ id, content }: { id: string | number, content: string }) => {
8
- const { openFeedback } = useFeedback();
9
- const [selected, setSelected] = useState(false);
4
+ declare global {
5
+ interface Window {
6
+ MathJax?: {
7
+ typesetPromise?: (els?: Element[]) => Promise<void>;
8
+ startup?: { promise?: Promise<unknown> };
9
+ };
10
+ }
11
+ }
10
12
 
11
- return (
12
- <div className="card" style={{ padding: '20px', border: '1px solid #e5e7eb', marginBottom: '16px', borderRadius: '8px', background: 'white', textAlign: 'left' }}>
13
- <h3 style={{ marginTop: 0 }}>Question {id}</h3>
14
- <p>{content}</p>
13
+ // A pile of formula-heavy questions, mirroring the host app's MathJax CHTML usage.
14
+ const FORMULAS = [
15
+ String.raw`\(\displaystyle \int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}\)`,
16
+ String.raw`\(\displaystyle \sum_{n=1}^{\infty}\frac{1}{n^2} = \frac{\pi^2}{6}\)`,
17
+ String.raw`\(\displaystyle \frac{d}{dx}\left[\frac{\sin x}{1+\cos x}\right]\)`,
18
+ String.raw`\(\displaystyle x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}\)`,
19
+ String.raw`\(\displaystyle \lim_{x\to 0}\frac{\sin x}{x} = 1\)`,
20
+ String.raw`\(\displaystyle \binom{n}{k} = \frac{n!}{k!(n-k)!}\)`,
21
+ String.raw`\(\displaystyle \begin{pmatrix} a & b \\ c & d \end{pmatrix}^{-1}\)`,
22
+ String.raw`\(\displaystyle \sqrt[3]{\frac{a^2+b^2}{c^2}}\)`,
23
+ ];
15
24
 
16
- <div style={{ display: 'flex', gap: '10px', marginTop: '12px' }}>
17
- <button
18
- onClick={() => setSelected(!selected)}
19
- style={{ background: selected ? '#dbeafe' : '#f3f4f6', color: selected ? '#1d4ed8' : '#374151' }}
20
- >
21
- {selected ? 'Selected' : 'Select'}
22
- </button>
25
+ const Question: React.FC<{ id: number }> = ({ id }) => {
26
+ const { openFeedback } = useFeedback();
27
+ const f1 = FORMULAS[id % FORMULAS.length];
28
+ const f2 = FORMULAS[(id + 3) % FORMULAS.length];
23
29
 
24
- <button
25
- onClick={() => openFeedback(id, { source: 'QuestionItem', contentSnippet: content.slice(0, 20) })}
26
- style={{ background: 'transparent', border: '1px solid #fecaca', color: '#dc2626' }}
27
- >
28
- Báo lỗi (Feedback)
29
- </button>
30
+ return (
31
+ <div
32
+ data-question-id={id}
33
+ style={{
34
+ border: '1px solid #e5e7eb',
35
+ borderRadius: 8,
36
+ padding: 16,
37
+ margin: '12px 0',
38
+ background: '#fff',
39
+ }}
40
+ >
41
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
42
+ <strong>Câu {id}</strong>
43
+ <button onClick={() => openFeedback(id)}>Báo lỗi</button>
44
+ </div>
45
+ <p
46
+ dangerouslySetInnerHTML={{
47
+ __html: `Cho biểu thức ${f1}. Tính giá trị của ${f2} và chứng minh kết quả.`,
48
+ }}
49
+ />
50
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
51
+ {['A', 'B', 'C', 'D'].map((opt, i) => (
52
+ <label key={opt} style={{ display: 'flex', gap: 8 }}>
53
+ <input type="radio" name={`q-${id}`} />
54
+ <span
55
+ dangerouslySetInnerHTML={{
56
+ __html: `${opt}. ${FORMULAS[(id + i) % FORMULAS.length]}`,
57
+ }}
58
+ />
59
+ </label>
60
+ ))}
30
61
  </div>
31
62
  </div>
32
63
  );
33
64
  };
34
65
 
35
- function AppContent() {
36
- const [count, setCount] = useState(0);
37
-
66
+ // Mirror the host app's fixed full-screen watermark overlay (position:fixed; inset:0; z-index:10).
67
+ const Watermark: React.FC = () => {
68
+ const cols = 4;
69
+ const rows = 6;
70
+ const items = [];
71
+ for (let r = 0; r < rows; r++) {
72
+ for (let c = 0; c < cols; c++) {
73
+ items.push(
74
+ <div
75
+ key={`wm-${r}-${c}`}
76
+ style={{
77
+ position: 'absolute',
78
+ left: `${(c / cols) * 100 + 100 / cols / 2}%`,
79
+ top: `${(r / rows) * 100 + 100 / rows / 2}%`,
80
+ transform: 'translate(-50%, -50%) rotate(-45deg)',
81
+ fontSize: '20px',
82
+ fontWeight: 700,
83
+ color: 'rgba(0,0,0,0.06)',
84
+ whiteSpace: 'nowrap',
85
+ pointerEvents: 'none',
86
+ letterSpacing: '4px',
87
+ }}
88
+ >
89
+ HOCMAI.VN
90
+ </div>
91
+ );
92
+ }
93
+ }
38
94
  return (
39
- <div style={{ maxWidth: '800px', margin: '0 auto', padding: '2rem' }}>
40
- <h1>Exam System Demo (Multi-Question)</h1>
41
- <p style={{ marginBottom: '2rem' }}>
42
- Support Package integrated via <code>FeedbackProvider</code>.
43
- </p>
44
-
45
- <img
46
- style={{ width: '48px', height: '48px', borderRadius: '50%', background: '#1d4ed8', marginLeft: '8px', objectFit: 'cover', }}
47
- src='https://cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg'
48
- />
49
- <QuestionItem id="101" content="What is the capital of Vietnam?" />
50
- <QuestionItem id="102" content="Calculate the area of a circle with radius 5." />
51
- <QuestionItem id="103" content="Who wrote 'Truyen Kieu'?" />
52
-
53
- <div style={{ marginTop: '40px', padding: '20px', background: '#f9fafb', borderRadius: '8px' }}>
54
- <button onClick={() => setCount(c => c + 1)}>Global Counter: {count}</button>
55
- </div>
95
+ <div style={{ position: 'fixed', inset: 0, zIndex: 10, overflow: 'hidden', pointerEvents: 'none' }} aria-hidden>
96
+ {items}
56
97
  </div>
57
- )
58
- }
98
+ );
99
+ };
59
100
 
60
- function App() {
61
- const handleFeedbackSubmit = async (data: FeedbackData) => {
62
- console.log('Received Feedback via Provider:', data);
63
- await new Promise((resolve) => setTimeout(resolve, 1000));
101
+ const COUNT = 60;
102
+
103
+ const App: React.FC = () => {
104
+ const handleSubmit = async (data: FeedbackData) => {
105
+ console.log('Feedback submitted (dev):', data);
64
106
  };
65
107
 
108
+ React.useEffect(() => {
109
+ // Typeset all the math, then flag readiness for the Playwright test.
110
+ const run = async () => {
111
+ if (window.MathJax?.startup?.promise) {
112
+ await window.MathJax.startup.promise;
113
+ }
114
+ if (window.MathJax?.typesetPromise) {
115
+ await window.MathJax.typesetPromise();
116
+ }
117
+ // Expose for the Playwright capture test.
118
+ (window as unknown as { __captureScreenshot?: typeof captureScreenshot }).__captureScreenshot =
119
+ captureScreenshot;
120
+ document.body.setAttribute('data-math-ready', 'true');
121
+ };
122
+ run();
123
+ }, []);
124
+
66
125
  return (
67
- <FeedbackProvider onSubmit={handleFeedbackSubmit}>
68
- <AppContent />
126
+ <FeedbackProvider onSubmit={handleSubmit}>
127
+ {/* Mirror host app: normal-flow root + fixed overlay watermark + fixed opaque header */}
128
+ <div style={{ width: '100vw', display: 'flex', flexDirection: 'column' }}>
129
+ <Watermark />
130
+ <div
131
+ style={{
132
+ height: 114,
133
+ width: '100%',
134
+ position: 'fixed',
135
+ top: 0,
136
+ left: 0,
137
+ zIndex: 20,
138
+ background: 'linear-gradient(90deg,#1e3a8a,#2563eb)',
139
+ color: '#fff',
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ justifyContent: 'center',
143
+ fontWeight: 700,
144
+ }}
145
+ >
146
+ ĐỀ THI THỬ — header cố định (fixed)
147
+ </div>
148
+ <div style={{ maxWidth: 760, margin: '0 auto', padding: 24, paddingTop: 140, fontFamily: 'system-ui, sans-serif' }}>
149
+ <h1>Đề thi thử (dev harness · {COUNT} câu nhiều công thức)</h1>
150
+ <p>Trang này tái hiện môi trường MathJax CHTML để kiểm thử chụp màn hình.</p>
151
+ {Array.from({ length: COUNT }, (_, i) => (
152
+ <Question key={i + 1} id={i + 1} />
153
+ ))}
154
+ </div>
155
+ </div>
69
156
  </FeedbackProvider>
70
- )
71
- }
72
-
73
- export default App
74
-
157
+ );
158
+ };
75
159
 
160
+ export default App;
@@ -0,0 +1,52 @@
1
+ export interface CaptureCapabilities {
2
+ /** Largest canvas dimension (px) the device can reliably allocate. */
3
+ maxCanvasSize: number;
4
+ /** Recommended pixel multiplier before clamping to maxCanvasSize. */
5
+ recommendedScale: number;
6
+ }
7
+
8
+ function ua(): string {
9
+ return (typeof navigator !== 'undefined' ? navigator.userAgent : '').toLowerCase();
10
+ }
11
+
12
+ export function isIOSSafari(): boolean {
13
+ const u = ua();
14
+ const isIOS = /iphone|ipad|ipod/.test(u) ||
15
+ // iPadOS 13+ reports as desktop Safari with touch support
16
+ (/macintosh/.test(u) && typeof document !== 'undefined' && 'ontouchend' in document);
17
+ const isSafari = /safari/.test(u) && !/chrome|crios|crmo|fxios|edgios/.test(u);
18
+ return isIOS && isSafari;
19
+ }
20
+
21
+ export function getCaptureCapabilities(): CaptureCapabilities {
22
+ const u = ua();
23
+ const isMobile = /iphone|ipod|android.*mobile/.test(u);
24
+ const isTablet = /ipad|android(?!.*mobile)/.test(u);
25
+ const deviceMemory = (navigator as { deviceMemory?: number }).deviceMemory ?? 4;
26
+ const isLowMemory = deviceMemory < 4;
27
+
28
+ if (isMobile) {
29
+ return { maxCanvasSize: 4096, recommendedScale: isLowMemory ? 1 : 1.5 };
30
+ }
31
+ if (isTablet) {
32
+ return { maxCanvasSize: 8192, recommendedScale: 2 };
33
+ }
34
+ return { maxCanvasSize: 16384, recommendedScale: 2 };
35
+ }
36
+
37
+ /**
38
+ * Clamp a desired scale so neither dimension of the captured element exceeds
39
+ * the device's max canvas size. Keeps capture from silently failing on big pages.
40
+ */
41
+ export function clampScale(
42
+ desiredScale: number,
43
+ contentWidth: number,
44
+ contentHeight: number,
45
+ maxCanvasSize: number
46
+ ): number {
47
+ const maxByWidth = contentWidth > 0 ? maxCanvasSize / contentWidth : desiredScale;
48
+ const maxByHeight = contentHeight > 0 ? maxCanvasSize / contentHeight : desiredScale;
49
+ const scale = Math.min(desiredScale, maxByWidth, maxByHeight);
50
+ // Never go below 0.5 — a tiny screenshot is worse than a slightly-too-big one.
51
+ return Math.max(0.5, scale);
52
+ }
@@ -0,0 +1,34 @@
1
+ interface MathJaxLike {
2
+ startup?: { promise?: Promise<unknown> };
3
+ typesetPromise?: () => Promise<unknown>;
4
+ }
5
+
6
+ function getMathJax(): MathJaxLike | undefined {
7
+ return (window as unknown as { MathJax?: MathJaxLike }).MathJax;
8
+ }
9
+
10
+ /** Wait until MathJax has finished its initial typeset so formulas have laid out. */
11
+ export async function waitForMathJax(): Promise<void> {
12
+ const MJ = getMathJax();
13
+ if (!MJ) return;
14
+ try {
15
+ if (MJ.startup?.promise) {
16
+ await MJ.startup.promise;
17
+ }
18
+ } catch {
19
+ /* ignore — capture should proceed even if MathJax errored */
20
+ }
21
+ }
22
+
23
+ /** Two animation frames: let layout/paint settle before we snapshot. */
24
+ function nextFrames(): Promise<void> {
25
+ return new Promise((resolve) => {
26
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
27
+ });
28
+ }
29
+
30
+ /** Resolve once the page is ready for a faithful snapshot. */
31
+ export async function ensureReadyForCapture(): Promise<void> {
32
+ await waitForMathJax();
33
+ await nextFrames();
34
+ }