Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4a52c35
feat: scaffold @repo/pointer-content-renderer package
sterdsterd Apr 12, 2026
05fd686
feat: implement document, chat, and overview rendering modes
sterdsterd Apr 12, 2026
51ce4c4
feat: add native ContentWebView component and useContentBridge hook
sterdsterd Apr 12, 2026
9fbaf4f
fix: handle hardBreak node type as <br> in serializer
sterdsterd Apr 12, 2026
69544c1
feat: add tab navigation to overview mode, remove scrollToSection/vis…
sterdsterd Apr 12, 2026
89e3100
fix: overview scroll by removing position:fixed, chat auto-scroll onl…
sterdsterd Apr 12, 2026
6cd208a
fix: use sticky-to-bottom intent tracking for chat auto-scroll
sterdsterd Apr 12, 2026
a5f40c9
fix: show first pointing divider in chat, fix tab scroll tracking, sp…
sterdsterd Apr 12, 2026
a9a304a
refactor: move yes/no buttons inside bubble instead of separate conta…
sterdsterd Apr 12, 2026
2d5c43e
feat(pointer-content-renderer): enhance overview and chat modes
sterdsterd Apr 12, 2026
1b11ffc
fix(pointer-content-renderer): add `bridgeReady` message for WebView-RN
sterdsterd Apr 13, 2026
fb43a3a
fix(pointer-content-renderer): add abort signal handling and cleanup
sterdsterd Apr 13, 2026
1201fd4
build(native): support dynamic WebView HTML asset handling
sterdsterd Apr 13, 2026
4136a6e
fix(pointer-content-renderer): handle KaTeX load errors gracefully
sterdsterd Apr 13, 2026
96ed406
fix(pointer-content-renderer): correct document node content structure
sterdsterd Apr 13, 2026
c5253ba
feat(pointer-content-renderer): add bookmark icon states and toggle
sterdsterd Apr 13, 2026
f1335ec
refactor(pointer-content-renderer): centralize bookmark button state
sterdsterd Apr 13, 2026
dc65360
fix(pointer-content-renderer): remove redundant margin-bottom in
sterdsterd Apr 13, 2026
4f43171
fix(pointer-content-renderer): improve bookmark rollback logic to
sterdsterd Apr 13, 2026
5b2de1d
fix(pointer-content-renderer): skip malformed pointings in chat
sterdsterd Apr 13, 2026
ecf9a2f
fix(pointer-content-renderer): prevent answer misalignment in chat
sterdsterd Apr 13, 2026
a8ad355
fix(pointer-content-renderer): re-inject initMessage on change
sterdsterd Apr 13, 2026
da6281a
fix(renderer): add safePositiveInt for attribute validation
sterdsterd Apr 13, 2026
20745af
fix(pointer-content-renderer): add requestId to bookmark handling
sterdsterd Apr 13, 2026
265df3a
feat(pointer-content-renderer): add bookmark state management module
sterdsterd Apr 13, 2026
7fc4f1c
feat(pointer-content-renderer): add chat resume and answer event
sterdsterd Apr 13, 2026
454352c
fix(native): resolve React duplicate instance in monorepo
sterdsterd Apr 13, 2026
6f3d358
style(pointer-content-renderer): fix formatting inconsistencies
sterdsterd Apr 13, 2026
e6b67a6
style(native): remove unnecessary trailing commas in metro.config.js
sterdsterd Apr 13, 2026
5fa9339
style(pointer-content-renderer): correct CSS attribute selector quotes
sterdsterd Apr 13, 2026
f83bd77
feat(ci): update CI scripts to include pointer-content-renderer
sterdsterd Apr 13, 2026
73d9fb5
fix(pointer-content-renderer): update CSS selector for `.document-mode`
sterdsterd Apr 13, 2026
24a736d
fix(pointer-content-renderer): correct scroll behavior for instant mode
sterdsterd Apr 13, 2026
44894d1
fix(pointer-content-renderer): prevent memory leak in delay function
sterdsterd Apr 13, 2026
da7524b
fix(pointer-content-renderer): prevent duplicate message listeners
sterdsterd Apr 13, 2026
ddacf93
fix(pointer-content-renderer): update typecheck script for dual configs
sterdsterd Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/native/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@ app-example

# Firebase Google Services
google-services.json
GoogleService-Info.plist
GoogleService-Info.plist

# Generated WebView bundle (see sync-webview-html)
assets/webview/content.html
36 changes: 36 additions & 0 deletions apps/native/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ config.resolver.nodeModulesPaths = [
path.resolve(monorepoRoot, 'node_modules'),
];

// Force a single copy of React/RN so workspace packages (e.g. @repo/pointer-content-renderer)
// resolve the same instance as the app. Prevents "Invalid hook call" in monorepo.
const DEDUPE_MODULES = {
react: path.resolve(projectRoot, 'node_modules/react'),
'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
'react-native-webview': path.resolve(projectRoot, 'node_modules/react-native-webview'),
};
config.resolver.extraNodeModules = {
...(config.resolver.extraNodeModules ?? {}),
...DEDUPE_MODULES,
};

// WebView 용 HTML을 asset으로 다루기 위해 등록
config.resolver.assetExts = [...config.resolver.assetExts, 'html'];

// blockList에서 /dist\/.*/ 제거!
// node_modules 안의 dist 폴더는 필요하므로, 프로젝트의 dist만 차단
config.resolver.blockList = [
Expand All @@ -40,6 +55,27 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
};
}

// Force-dedupe React/RN regardless of which workspace package requests them.
// Prevents multiple React instances when packages resolve from different
// node_modules roots (monorepo + pnpm symlinks).
if (DEDUPE_MODULES[moduleName]) {
return context.resolveRequest(
{ ...context, originModulePath: path.join(projectRoot, 'package.json') },
moduleName,
platform
);
}
// Handle sub-path imports like 'react/jsx-runtime' by redirecting the base.
for (const baseName of Object.keys(DEDUPE_MODULES)) {
if (moduleName.startsWith(`${baseName}/`)) {
return context.resolveRequest(
{ ...context, originModulePath: path.join(projectRoot, 'package.json') },
moduleName,
platform
);
}
}

if (originalResolveRequest) {
return originalResolveRequest(context, moduleName, platform);
}
Expand Down
7 changes: 7 additions & 0 deletions apps/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@
"version": "1.0.0",
"scripts": {
"start": "expo start",
"prestart": "pnpm sync-webview-html",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo run:android",
"preandroid": "pnpm sync-webview-html",
"ios": "expo run:ios",
"preios": "pnpm sync-webview-html",
"web": "expo start --web --port 3000",
"preweb": "pnpm sync-webview-html",
"lint": "expo lint",
"typecheck": "tsc --noEmit",
"sync-webview-html": "pnpm --filter @repo/pointer-content-renderer build && mkdir -p ./assets/webview && cp ../../packages/pointer-content-renderer/dist/index.html ./assets/webview/content.html",
"eas-build-post-install": "pnpm sync-webview-html",
"openapi": "pnpm dlx openapi-typescript https://dev.api.math-pointer.com/v3/api-docs --output ./src/types/api/schema.d.ts && prettier --write ./src/types/api/schema.d.ts"
},
"dependencies": {
"@repo/pointer-content-renderer": "workspace:*",
"@expo/ngrok": "^4.1.3",
"@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "^5.2.7",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"format": "prettier --write \"**/*.{js,ts,tsx,jsx,mjs,hbs,json,css,md}\"",
"format:check": "prettier --check \"**/*.{js,ts,tsx,jsx,mjs,hbs,json,css,md}\"",
"svgr:admin": "pnpm dlx @svgr/cli apps/admin/public/svg --out-dir apps/admin/src/assets/svg --no-prettier && prettier --write \"apps/admin/src/assets/svg/**/*.{ts,tsx}\"",
"ci:lint": "turbo lint --filter=admin --filter=@repo/pointer-editor-v2",
"ci:typecheck": "turbo run typecheck --filter=admin --filter=@repo/pointer-editor-v2",
"ci:build": "turbo build --filter=admin --filter=@repo/pointer-editor-v2"
"ci:lint": "turbo lint --filter=admin --filter=@repo/pointer-editor-v2 --filter=@repo/pointer-content-renderer",
"ci:typecheck": "turbo run typecheck",
"ci:build": "turbo build"
Comment on lines +22 to +23
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ci:typecheck and ci:build were changed from filtered runs to turbo run typecheck / turbo build for the whole monorepo. This can significantly increase CI time and may run tasks for packages that weren’t previously validated in CI. If the intent is only to include @repo/pointer-content-renderer, consider keeping filters consistent (or document why full-monorepo CI is required).

Suggested change
"ci:typecheck": "turbo run typecheck",
"ci:build": "turbo build"
"ci:typecheck": "turbo run typecheck --filter=admin --filter=@repo/pointer-editor-v2 --filter=@repo/pointer-content-renderer",
"ci:build": "turbo build --filter=admin --filter=@repo/pointer-editor-v2 --filter=@repo/pointer-content-renderer"

Copilot uses AI. Check for mistakes.
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.23.0",
Expand Down
24 changes: 24 additions & 0 deletions packages/pointer-content-renderer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
135 changes: 135 additions & 0 deletions packages/pointer-content-renderer/dev/dev-panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { isRNWebView } from '../src/web/bridge';
import type { RNToWebViewMessage } from '../src/types';

import {
mockDocumentContent,
mockChatScenario,
mockOverviewSections,
mockAllLeftSections,
mockAllRightSections,
mockChatResumeAfterQuestion,
mockChatResumeAfterFirstPointing,
mockChatResumeMidSecond,
mockChatResumeAllComplete,
} from './mock-data';

function sendMockMessage(msg: RNToWebViewMessage): void {
window.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(msg) }));
}

function createDevPanel(): void {
if (isRNWebView()) return;

const panel = document.createElement('div');
panel.style.cssText = 'position:fixed;bottom:8px;right:8px;z-index:9999;display:flex;gap:6px;';

const modes = [
{
label: '문제(document)',
action: () =>
sendMockMessage({
type: 'init',
mode: 'document',
content: mockDocumentContent,
backgroundColor: '#ffffff',
}),
},
{
label: '포인팅(chat)',
action: () =>
sendMockMessage({
type: 'init',
mode: 'chat',
scenario: mockChatScenario,
}),
},
{
label: 'chat resume: Q1 answered',
action: () =>
sendMockMessage({
type: 'init',
mode: 'chat',
scenario: mockChatScenario,
userAnswers: mockChatResumeAfterQuestion,
}),
},
{
label: 'chat resume: P1 done',
action: () =>
sendMockMessage({
type: 'init',
mode: 'chat',
scenario: mockChatScenario,
userAnswers: mockChatResumeAfterFirstPointing,
}),
},
{
label: 'chat resume: mid P2',
action: () =>
sendMockMessage({
type: 'init',
mode: 'chat',
scenario: mockChatScenario,
userAnswers: mockChatResumeMidSecond,
}),
},
{
label: 'chat resume: all done',
action: () =>
sendMockMessage({
type: 'init',
mode: 'chat',
scenario: mockChatScenario,
userAnswers: mockChatResumeAllComplete,
}),
},
{
label: '학습 마무리(overview)',
action: () =>
sendMockMessage({
type: 'init',
mode: 'overview',
variant: 'summary',
sections: mockOverviewSections,
}),
},
{
label: '포인팅 전체보기 L(overview)',
action: () =>
sendMockMessage({
type: 'init',
mode: 'overview',
variant: 'pointing',
sections: mockAllLeftSections,
}),
},
{
label: '포인팅 전체보기 R(overview)',
action: () =>
sendMockMessage({
type: 'init',
mode: 'overview',
variant: 'pointing',
sections: mockAllRightSections,
}),
},
] as const;

for (const mode of modes) {
const btn = document.createElement('button');
btn.textContent = mode.label;
btn.style.cssText =
'padding:6px 12px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;';
btn.addEventListener('click', () => {
const content = document.getElementById('content');
if (content) content.innerHTML = '';
document.body.className = '';
mode.action();
});
panel.appendChild(btn);
}

document.body.appendChild(panel);
}

createDevPanel();
Loading
Loading