hocmai-feedback-widget 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +78 -113
  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 +13 -1
  11. package/dist/context/FeedbackContext.d.ts +14 -2
  12. package/dist/hocmai-feedback-widget.mjs +7550 -0
  13. package/dist/hocmai-feedback-widget.mjs.map +1 -0
  14. package/dist/index.d.ts +4 -2
  15. package/package.json +39 -29
  16. package/src/App.tsx +147 -58
  17. package/src/capture/device-detection.ts +52 -0
  18. package/src/capture/fonts.ts +34 -0
  19. package/src/capture/index.ts +361 -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 +105 -93
  26. package/src/context/FeedbackContext.tsx +49 -9
  27. package/src/index.ts +9 -3
  28. package/src/main.tsx +7 -9
  29. package/src/utils/device.ts +14 -14
  30. package/dist/support-feedback-lib.es.js +0 -3946
  31. package/dist/support-feedback-lib.umd.js +0 -84
  32. package/dist/test-mobile-screenshot.html +0 -211
  33. package/dist/test-simple-capture.html +0 -107
  34. package/dist/utils/screenshot/background-renderer.d.ts +0 -3
  35. package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
  36. package/dist/utils/screenshot/cleanup.d.ts +0 -3
  37. package/dist/utils/screenshot/device-detection.d.ts +0 -16
  38. package/dist/utils/screenshot/element-collector.d.ts +0 -10
  39. package/dist/utils/screenshot/image-renderer.d.ts +0 -21
  40. package/dist/utils/screenshot/performance.d.ts +0 -3
  41. package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
  42. package/dist/utils/screenshot/text-renderer.d.ts +0 -23
  43. package/dist/utils/screenshot/types.d.ts +0 -35
  44. package/dist/utils/screenshot.d.ts +0 -9
  45. package/dist/vite.svg +0 -1
  46. package/src/App.css +0 -42
  47. package/src/assets/react.svg +0 -1
  48. package/src/index.css +0 -68
  49. package/src/utils/screenshot/background-renderer.ts +0 -54
  50. package/src/utils/screenshot/canvas-renderer.ts +0 -343
  51. package/src/utils/screenshot/cleanup.ts +0 -32
  52. package/src/utils/screenshot/device-detection.ts +0 -90
  53. package/src/utils/screenshot/element-collector.ts +0 -134
  54. package/src/utils/screenshot/image-renderer.ts +0 -148
  55. package/src/utils/screenshot/performance.ts +0 -57
  56. package/src/utils/screenshot/svg-renderer.ts +0 -149
  57. package/src/utils/screenshot/text-renderer.ts +0 -212
  58. package/src/utils/screenshot/types.ts +0 -40
  59. package/src/utils/screenshot.ts +0 -83
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
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';
4
- export { captureScreenshot } from './utils/screenshot';
5
- export type { Rect, CaptureProgress } from './utils/screenshot/types';
5
+ export type { FeedbackSubmitHandler } from './context/FeedbackContext';
6
+ export { captureScreenshot, captureScreenshotWithOptions, prewarmCapture, } from './capture';
7
+ 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.1",
4
+ "version": "0.2.3",
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,54 @@
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": {
45
+ "optional": true
46
+ },
47
+ "hocmai-ckeditor-custom": {
48
+ "optional": true
49
+ },
50
+ "ckeditor5-classic-with-mathtype": {
51
+ "optional": true
52
+ }
34
53
  },
35
54
  "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"
55
+ "@playwright/test": "^1.48.0",
56
+ "@types/node": "^22.0.0",
57
+ "@types/react": "^18.3.0",
58
+ "@types/react-dom": "^18.3.0",
59
+ "@vitejs/plugin-react": "^4.3.0",
60
+ "react": "^18.3.1",
61
+ "react-dom": "^18.3.1",
62
+ "typescript": "~5.6.0",
63
+ "vite": "^5.4.0",
64
+ "vite-plugin-dts": "^4.2.0"
55
65
  }
56
- }
66
+ }
package/src/App.tsx CHANGED
@@ -1,75 +1,164 @@
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';
3
+ import CkEditor from './components/CkEditor';
5
4
 
6
- // Reusable Question Component
7
- const QuestionItem = ({ id, content }: { id: string | number, content: string }) => {
8
- const { openFeedback } = useFeedback();
9
- const [selected, setSelected] = useState(false);
5
+ declare global {
6
+ interface Window {
7
+ MathJax?: {
8
+ typesetPromise?: (els?: Element[]) => Promise<void>;
9
+ startup?: { promise?: Promise<unknown> };
10
+ };
11
+ }
12
+ }
10
13
 
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>
14
+ // A pile of formula-heavy questions, mirroring the host app's MathJax CHTML usage.
15
+ const FORMULAS = [
16
+ String.raw`\(\displaystyle \int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}\)`,
17
+ String.raw`\(\displaystyle \sum_{n=1}^{\infty}\frac{1}{n^2} = \frac{\pi^2}{6}\)`,
18
+ String.raw`\(\displaystyle \frac{d}{dx}\left[\frac{\sin x}{1+\cos x}\right]\)`,
19
+ String.raw`\(\displaystyle x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}\)`,
20
+ String.raw`\(\displaystyle \lim_{x\to 0}\frac{\sin x}{x} = 1\)`,
21
+ String.raw`\(\displaystyle \binom{n}{k} = \frac{n!}{k!(n-k)!}\)`,
22
+ String.raw`\(\displaystyle \begin{pmatrix} a & b \\ c & d \end{pmatrix}^{-1}\)`,
23
+ String.raw`\(\displaystyle \sqrt[3]{\frac{a^2+b^2}{c^2}}\)`,
24
+ ];
15
25
 
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>
26
+ const Question: React.FC<{ id: number }> = ({ id }) => {
27
+ const { openFeedback } = useFeedback();
28
+ const f1 = FORMULAS[id % FORMULAS.length];
29
+ const f2 = FORMULAS[(id + 3) % FORMULAS.length];
23
30
 
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>
31
+ return (
32
+ <div
33
+ data-question-id={id}
34
+ style={{
35
+ border: '1px solid #e5e7eb',
36
+ borderRadius: 8,
37
+ padding: 16,
38
+ margin: '12px 0',
39
+ background: '#fff',
40
+ }}
41
+ >
42
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
43
+ <strong>Câu {id}</strong>
44
+ <button onClick={() => openFeedback(id)}>Báo lỗi</button>
45
+ </div>
46
+ <p
47
+ dangerouslySetInnerHTML={{
48
+ __html: `Cho biểu thức ${f1}. Tính giá trị của ${f2} và chứng minh kết quả.`,
49
+ }}
50
+ />
51
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
52
+ {['A', 'B', 'C', 'D'].map((opt, i) => (
53
+ <label key={opt} style={{ display: 'flex', gap: 8 }}>
54
+ <input type="radio" name={`q-${id}`} />
55
+ <span
56
+ dangerouslySetInnerHTML={{
57
+ __html: `${opt}. ${FORMULAS[(id + i) % FORMULAS.length]}`,
58
+ }}
59
+ />
60
+ </label>
61
+ ))}
30
62
  </div>
31
63
  </div>
32
64
  );
33
65
  };
34
66
 
35
- function AppContent() {
36
- const [count, setCount] = useState(0);
37
-
67
+ // Mirror the host app's fixed full-screen watermark overlay (position:fixed; inset:0; z-index:10).
68
+ const Watermark: React.FC = () => {
69
+ const cols = 4;
70
+ const rows = 6;
71
+ const items = [];
72
+ for (let r = 0; r < rows; r++) {
73
+ for (let c = 0; c < cols; c++) {
74
+ items.push(
75
+ <div
76
+ key={`wm-${r}-${c}`}
77
+ style={{
78
+ position: 'absolute',
79
+ left: `${(c / cols) * 100 + 100 / cols / 2}%`,
80
+ top: `${(r / rows) * 100 + 100 / rows / 2}%`,
81
+ transform: 'translate(-50%, -50%) rotate(-45deg)',
82
+ fontSize: '20px',
83
+ fontWeight: 700,
84
+ color: 'rgba(0,0,0,0.06)',
85
+ whiteSpace: 'nowrap',
86
+ pointerEvents: 'none',
87
+ letterSpacing: '4px',
88
+ }}
89
+ >
90
+ HOCMAI.VN
91
+ </div>
92
+ );
93
+ }
94
+ }
38
95
  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>
96
+ <div style={{ position: 'fixed', inset: 0, zIndex: 10, overflow: 'hidden', pointerEvents: 'none' }} aria-hidden>
97
+ {items}
56
98
  </div>
57
- )
58
- }
99
+ );
100
+ };
59
101
 
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));
102
+ const COUNT = 60;
103
+
104
+ const App: React.FC = () => {
105
+ const handleSubmit = async (data: FeedbackData) => {
106
+ console.log('Feedback submitted (dev):', data);
64
107
  };
65
108
 
109
+ React.useEffect(() => {
110
+ // Typeset all the math, then flag readiness for the Playwright test.
111
+ const run = async () => {
112
+ if (window.MathJax?.startup?.promise) {
113
+ await window.MathJax.startup.promise;
114
+ }
115
+ if (window.MathJax?.typesetPromise) {
116
+ await window.MathJax.typesetPromise();
117
+ }
118
+ // Expose for the Playwright capture test.
119
+ (window as unknown as { __captureScreenshot?: typeof captureScreenshot }).__captureScreenshot =
120
+ captureScreenshot;
121
+ document.body.setAttribute('data-math-ready', 'true');
122
+ };
123
+ run();
124
+ }, []);
125
+
66
126
  return (
67
- <FeedbackProvider onSubmit={handleFeedbackSubmit}>
68
- <AppContent />
127
+ <FeedbackProvider
128
+ isCkEditor={true}
129
+ descriptionEditor={CkEditor}
130
+ onSubmit={handleSubmit}>
131
+ {/* Mirror host app: normal-flow root + fixed overlay watermark + fixed opaque header */}
132
+ <div style={{ width: '100vw', display: 'flex', flexDirection: 'column' }}>
133
+ <Watermark />
134
+ <div
135
+ style={{
136
+ height: 114,
137
+ width: '100%',
138
+ position: 'fixed',
139
+ top: 0,
140
+ left: 0,
141
+ zIndex: 20,
142
+ background: 'linear-gradient(90deg,#1e3a8a,#2563eb)',
143
+ color: '#fff',
144
+ display: 'flex',
145
+ alignItems: 'center',
146
+ justifyContent: 'center',
147
+ fontWeight: 700,
148
+ }}
149
+ >
150
+ ĐỀ THI THỬ — header cố định (fixed)
151
+ </div>
152
+ <div style={{ maxWidth: 760, margin: '0 auto', padding: 24, paddingTop: 140, fontFamily: 'system-ui, sans-serif' }}>
153
+ <h1>Đề thi thử (dev harness · {COUNT} câu nhiều công thức)</h1>
154
+ <p>Trang này tái hiện môi trường MathJax CHTML để kiểm thử chụp màn hình.</p>
155
+ {Array.from({ length: COUNT }, (_, i) => (
156
+ <Question key={i + 1} id={i + 1} />
157
+ ))}
158
+ </div>
159
+ </div>
69
160
  </FeedbackProvider>
70
- )
71
- }
72
-
73
- export default App
74
-
161
+ );
162
+ };
75
163
 
164
+ 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
+ }