diff --git a/.claude/skills/make-space/SKILL.md b/.claude/skills/make-space/SKILL.md new file mode 100644 index 0000000..56de680 --- /dev/null +++ b/.claude/skills/make-space/SKILL.md @@ -0,0 +1,95 @@ +--- +name: make-space +description: "Project-specific knowledge for the `make_space` Supernote plugin — THIS repo (sn_make_space). OneNote-style 'insert extra writing space' via tap → native lasso, in both Above and Below directions. Trigger whenever working in this repo, or when the user mentions make_space, Make Space, Fai Spazio, 'insert writing space', or this plugin by name/pluginID (lome8csqs3xu38pv). This skill covers only facts specific to this codebase — locked decisions, current architecture, known incidents, release workflow. For generic Supernote SDK/API/pattern knowledge (API signatures, coordinate systems, permission gotchas, build/deploy mechanics), use the companion `supernote-plugin-dev` skill; the two are designed to be used together without duplicating each other." +--- + +# make_space — Project Skill + +This repository **is** the `make_space` plugin, not a generic playground. Goal: insert blank +writing space anywhere on a NOTE page (inspired by OneNote's *"Insert extra writing space"*). + +**Always pair this with the `supernote-plugin-dev` skill** — that one has the generic SDK +reference (API signatures, patterns, gotchas, build/deploy mechanics); this one has only what's +true about *this* codebase. Read this skill first for project facts, then the generic skill for +anything it doesn't repeat. + +## Current state (v0.6.0) + +- **Two toolbar buttons** (NOTE only, `showType:1`): id `100` "Make Space Below" (original v1 + direction), id `101` "Make Space Above" (added in v0.6.0). Both open the same `App.tsx` overlay; + direction is determined via the **Pending Button ID pattern** (generic skill's + `references/patterns.md` Pattern 5) — `index.js` exports `checkPendingDirection()` for the first + open, `App.tsx`'s mount effect also registers its own `PluginManager.registerButtonListener` to + catch every subsequent press live (PluginHost reuses the App instance across opens, see + `references/make-space.md` §4 — a one-time mount effect can't otherwise see later presses). +- `computeLassoRect(tapY, viewH, pageW, pageH, direction)` in `src/makeSpace.ts` — pure, fully + unit-tested (25 tests), `direction: 'above' | 'below'` is a required parameter. +- Three hand-drawn icons (`assets/icon.png` = bidirectional glyph for the app itself, + `assets/icon-below.png`, `assets/icon-above.png`) replaced the unused template puzzle piece. +- `PluginConfig.json` `name` = `"Make Space"` (the human-facing title shown in Settings → Apps → + Plugins) — **`pluginKey`/`pluginID` (`sn_make_space` / `lome8csqs3xu38pv`) must never change**, + they identify the plugin to the host for anyone who already installed it. +- i18n: **en + it only**. Hint/intro copy is direction-aware: + `hint.tapToInsertSpaceBelow`/`...Above`, `intro.bodyBelow`/`...Above`. + +## Confirmed flow (the move is done by the native lasso, NOT by the plugin) + +1. User taps **Make Space Below** (id 100) or **Make Space Above** (id 101) — sets `direction`. +2. UI = fullscreen **transparent overlay with a thick grey border** (the visual cue "do something"). +3. User taps a Y point → `computeLassoRect(tapY, viewH, pageW, pageH, direction)` builds a pixel + rect covering the requested side of the line → `PluginCommAPI.lassoElements(rect)` selects it + (this call ALSO shows the native selection box) → `PluginManager.closePluginView()`. +4. User **drags the native lasso selection by hand** to open/close space. Native move + native undo. + +**Why no auto-move**: the SDK has **no "move selection" API** (`resizeLassoRect` = proportional +scale only). A true one-gesture auto-move would require rewriting every stroke's EMR sample points +via `ElementDataAccessor.setRange` + coord conversion — out of scope for now, see +`references/make-space.md` §10. + +## Locked decisions + +Target **NOTE only**; overflow past page bottom **ignored**; layer toggle (all↔current) **pending +SDK verification**; i18n = **en + it** only (extendable); transparent overlay is primary, +`generateNotePng` background image is the documented (unused so far) fallback if transparency isn't +honored on some future firmware. + +## Known incidents (full postmortems in `references/make-space.md`) + +- **§12 — `getPageSize` permission-gate incident**: the 0.1.65 SDK bump broke the entire cut flow + on firmware Chauvet `3.29.43_beta` — `PluginFileAPI.getPageSize` got silently `FILE:READ`-gated, + so the flow bailed out before `lassoElements` was ever called. Fixed by switching to + `PluginCommAPI.getPageDisplaySize()`. See also the generic skill's gotcha #38. +- **§8 — stale-tag release incident**: `v0.5.3` was first tagged from a local `main` that was one + PR behind `origin/main` (`git checkout main` doesn't fetch) — shipped a release missing its own + fix, with every quality gate still green. Always `git pull origin main` immediately before + tagging; sanity-check `git log --oneline -1` against `origin/main` post-fetch. +- **`MyStyle/Plugins/` reinstall trap**: reinstalling via Settings → Apps → Plugins reads the + host's own managed copy at `MyStyle/Plugins/sn_make_space.snplg`, not whatever was just + `adb push`ed to `MyStyle/` root. See the generic skill's gotcha #39. + +## Process rules (from the user) + +Serious JSDoc comments (explain *why*, not *what*); pure logic in `src/makeSpace.ts` +(`computeLassoRect`) so it's unit-testable without a device; quality gates +(typecheck/eslint/prettier/jest) before every commit; **each unit on its own branch + PR**; CI on +push/PR; release on tag `v*` (see the stale-tag incident above before cutting one). Full original +step-by-step plan: `/home/gorlix/.claude/plans/valuterei-che-quando-si-compiled-eagle.md`; the +"Make Space Above" feature plan: `/home/gorlix/.claude/plans/functional-knitting-lantern.md`. + +## InkHub distribution readiness + +No submission spec exists yet (Supernote's own docs say only "in the future" as of firmware +`3.29.43_beta`) — see `references/make-space.md` §11 for the full tracking note. `v0.6.0` is the +first release with the no-regret prep done: `LICENSE`, presentable `name`/`desc`, real icons. +Current outreach channels while InkHub doesn't exist: the +[`fharper/awesome-supernote`](https://github.com/fharper/awesome-supernote) community catalogue PR +and the `r/Supernote_dev` announcement post — both need manual edits per release, not automated. + +## Where to look next + +| Task | Read | +|------|------| +| Full implementation recipe, all the "why"s, file layout, quality gates, incident postmortems | **`references/make-space.md`** (this skill) | +| Any SDK API signature, type, or version-gated fact | `supernote-plugin-dev` skill (generic) | +| Common recipes (lasso ops, pending-button, permissions) | `supernote-plugin-dev` skill's `references/patterns.md` | +| Build/deploy/debug mechanics not specific to this repo | `supernote-plugin-dev` skill's `references/setup-and-build.md` | diff --git a/.claude/skills/make-space/references/make-space.md b/.claude/skills/make-space/references/make-space.md new file mode 100644 index 0000000..ec53a47 --- /dev/null +++ b/.claude/skills/make-space/references/make-space.md @@ -0,0 +1,343 @@ +# `make_space` — Implementation Reference + +Task-specific recipe for THIS repo. For generic API signatures use the `supernote-docs` MCP first, +then the `supernote-plugin-dev` skill's `api-quick-ref.md` / `types.md`. Full execution plans: +`/home/gorlix/.claude/plans/valuterei-che-quando-si-compiled-eagle.md` (v1/v2 core), +`/home/gorlix/.claude/plans/functional-knitting-lantern.md` ("Make Space Above"). + +--- + +## 1. What the plugin does + +OneNote-style "insert extra writing space". User taps a horizontal Y line; everything **above or +below** it (depending on which toolbar button opened the plugin) is selected as a **native lasso**; +the user then drags that selection by hand to open blank space. The plugin only **creates the +selection** — the move and undo are native NOTE behavior. + +Flow: + +``` +button(id 100 "Below" or id 101 "Above", NOTE, showType 1) → App mounts/reused + direction = which button was pressed (Pending Button ID pattern, see §4a) + load current page's pixel size (PluginCommAPI.getPageDisplaySize) + render transparent fullscreen View + thick grey border (visual cue) + user taps Y → computeLassoRect(tapY, viewH, pageW, pageH, direction) + PluginCommAPI.lassoElements(rect) (rect in PIXELS; this ALSO shows the box) + PluginManager.closePluginView() (hand control back to NOTE) + → user drags selection natively + + ⚠️ Do NOT call setLassoBoxState(0) here — see §3. +``` + +## 2. Coordinates — everything is PIXELS here + +No EMR conversion needed. The tap is in screen pixels; `lassoElements` wants a pixel `Rect`. +Map the tap's Y (DP within the view) to page pixels by **proportion**, which sidesteps pixel-density +and view-scaling issues: + +```ts +// src/makeSpace.ts — pure, no SDK, unit-testable +export type Rect = {left: number; top: number; right: number; bottom: number}; +export type CutDirection = 'above' | 'below'; + +/** + * Map a tap Y (in DP, relative to the fullscreen plugin view) to the lasso rect + * covering everything ABOVE or BELOW that line, in page pixel coordinates. + * + * Proportional mapping (tapY/viewH) avoids density/scale mismatches between the + * RN view (DP) and the note page (px). The cut position is clamped to [0, pageH] + * so an out-of-bounds tap can never produce an invalid rect. + * + * @param tapY tap Y in DP, from PressEvent.nativeEvent.locationY + * @param viewH measured height of the plugin view in DP (onLayout) + * @param pageW page width in pixels (PluginCommAPI.getPageDisplaySize) + * @param pageH page height in pixels + * @param direction which side of the cut line to select + * @returns lasso rect in page pixels, full width, on the requested side of the cut + */ +export function computeLassoRect( + tapY: number, viewH: number, pageW: number, pageH: number, + direction: CutDirection, +): Rect { + const ratio = viewH > 0 ? tapY / viewH : 0; + const rawCut = Math.round(ratio * pageH); + const cutY = Math.max(0, Math.min(rawCut, pageH)); + return direction === 'below' + ? {left: 0, top: cutY, right: pageW, bottom: pageH} + : {left: 0, top: 0, right: pageW, bottom: cutY}; +} +``` + +Keep this logic out of `App.tsx` so Jest can test it without a device. + +## 3. The lasso recipe (the core) + +```ts +const res = await PluginCommAPI.lassoElements(rect); // APIResponse +// lassoElements ALREADY creates AND shows the selection box (auto-fit to the +// content, like a hand-drawn lasso). That's all we need — just hand control back. +await PluginManager.closePluginView(); +``` + +**⚠️ Do NOT call `setLassoBoxState(0)` after `lassoElements` (verified on-device, generic-skill +gotcha #34-equivalent, discovered here first).** `lassoElements` already shows the box, so the call +is redundant — and worse, it puts the firmware into the native **transfer/paste mode** +(`TransferModeView`, `isPenFloatingOnTheScreen`). If the user has anything in the Supernote +clipboard, the next tap-to-deselect **pastes that clipboard content (scaled up)** instead of +deselecting. A native hand-drawn lasso never calls `setLassoBoxState` and never pastes; that one +call was the only difference. Leave `setLassoBoxState` for flows that genuinely need to hide/remove +the box (states 1/2/3) — not for "show". + +**Critical caveat — strokes must be FULLY inside the rect.** `lassoElements` uses +`findTrailsContourInBox`: any stroke with a contour point outside the rect is **not** selected. For +"insert space between two lines" you tap in the gap, so the lines on the selected side are fully +inside → fine. A stroke that *crosses* the cut line will be missed, on either side. Do **not** add +padding to compensate — it would grab content from the wrong side. Accept this limit; document it +for the user. + +Other notes: +- Always check `APIResponse.success` before `.result` (see the generic skill's "API Response + Pattern"). +- `lassoElements` is layer-scoped to the current layer in practice → the "all layers" toggle may + not be achievable via SDK. Verify on-device before promising it; fall back to current-layer. +- Do NOT call `deleteElements` before/around the lasso (triggers a ~300ms page reload that starves + the binder pool — see the generic skill's `pen-emr.md` Pattern 14). Not needed here anyway. + +## 4. UI — transparent frame (+ fallback) + +Primary: root `View` `backgroundColor:'transparent'`, `borderWidth: 8`, `borderColor:'#9e9e9e'`. +A `Pressable` over `StyleSheet.absoluteFill` captures the tap; measure height with `onLayout`. + +**Transparency verified on-device (Nomad / A5X2): the overlay IS transparent** — the note shows +through the grey frame. Fallback (untested, kept documented in case a future firmware renders the +view opaque): render the page to an image and tap on that, via `PluginFileAPI.generateNotePng`. + +The tap-Y math and the lasso call are identical regardless of which button/direction opened the +plugin. + +### 4a. Two buttons, one App instance — the Pending Button ID pattern + +`index.js` registers both toolbar buttons (id 100 "Below", id 101 "Above"). Since PluginHost opens +the same `App.tsx` overlay for either, the plugin needs to know *which one* was pressed for the +current open: + +```js +// index.js — module level, survives across opens (see §4b) +let pendingDirection = null; +PluginManager.registerButtonListener({ + onButtonPress(event) { + pendingDirection = event.id === 101 ? 'above' : 'below'; + }, +}); +export const checkPendingDirection = () => { + const d = pendingDirection; + pendingDirection = null; + return d; +}; +``` + +```tsx +// App.tsx +const [direction, setDirection] = useState( + () => checkPendingDirection() ?? 'below', +); + +useEffect(() => { + // ...existing mount logic... + const sub = PluginManager.registerButtonListener({ + onButtonPress: event => setDirection(event.id === 101 ? 'above' : 'below'), + }); + return () => sub.remove(); +}, []); +``` + +`checkPendingDirection()` on mount only covers the **very first** open (before `App.tsx` existed to +register its own listener). Every subsequent press is caught live by the effect's own listener, +which stays subscribed for the component's whole (reused) lifetime — see §4b for why a one-time +effect still works for this. + +### ⚠️ PluginHost REUSES the App instance across open/close (verified on-device) + +When the plugin view is closed (`closePluginView`) and reopened, PluginHost does **not** remount the +React tree — there is **one** `App mounted`, then every later open only re-renders. Confirmed by +logcat: tap → close → reopen → tap shows no `App unmounted`/`App mounted` between them. + +Consequences: +- `useRef` / module-level state **persists** across opens. A guard ref (e.g. `busy` to debounce the + tap) that you set on tap **must be reset in `finally`**, or the second open is frozen with the + guard stuck `true` (this was the real "second open stuck on the grey frame" bug). +- `useEffect(() => …, [])` runs **once**, not per open. Do not rely on it to refresh per-open state + (page context, direction from a *new* button press are exceptions — see below). Fetch the page + context (`getPageDisplaySize`) **fresh inside the tap handler**, so it is always current even if + the user switched note/page between opens. +- A live **subscription** (event listener) set up in that one-time effect is NOT the same problem — + it stays registered and keeps firing on every later event, which is exactly what §4a's + `registerButtonListener` in the mount effect relies on to track direction across reopens. + +## 5. i18n (en + it) + +`i18next` + `react-i18next` only (**no `react-native-localize`** — pure JS, avoids the PluginHost +`reactPackages` native-linking issue). Initial language from RN core +`NativeModules.I18nManager.localeIdentifier`; runtime changes via `registerLangListener`. Init in +`src/i18n/index.ts`, imported once from `index.js`. Locales `src/i18n/locales/{en_US,it_IT}.json`. +All UI strings via `t('key')`. Button names = serialized JSON so they follow device language: + +```js +name: JSON.stringify({en: 'Make Space Below', it: 'Fai Spazio Sotto'}) +name: JSON.stringify({en: 'Make Space Above', it: 'Fai Spazio Sopra'}) +``` + +Hint/intro copy is **direction-aware**: `hint.tapToInsertSpaceBelow`/`...Above`, +`intro.bodyBelow`/`...Above` — `App.tsx` picks the key by `direction` when calling `t(...)`. No +i18next interpolation is used anywhere in this codebase; direction-dependent strings are explicit +key pairs, not a templated clause, to keep this file's style consistent. + +`registerLangListener` callback uses `onMsg`, and `msg.lang` uses underscores (`it_IT`) → convert +with `.replace('_','-')` before `i18n.changeLanguage` (generic skill gotcha #13). Structure is +extendable: add a locale = new JSON + one entry in `resources`. + +## 6. Files & layout + +``` +index.js 2 toolbar buttons (100=below, 101=above), pending-direction export, init, import ./src/i18n +App.tsx transparent frame + tap capture + lasso + close (uses computeLassoRect, t()) +src/makeSpace.ts pure computeLassoRect(..., direction) +src/sdk.ts typed facade over sn-plugin-lib (getPageDisplaySize, lassoElements, closePluginView) +src/i18n/index.ts i18next init + registerLangListener +src/i18n/locales/en_US.json +src/i18n/locales/it_IT.json +assets/icon.png app icon (bidirectional glyph) +assets/icon-below.png button 100 icon +assets/icon-above.png button 101 icon +__tests__/makeSpace.test.ts Jest tests for computeLassoRect (above + below, 14 cases) +__mocks__/sn-plugin-lib.js manual Jest mock — keep in sync with actual sdk.ts usage +jest.config.js mock sn-plugin-lib NativeModules +.github/workflows/ci.yml typecheck + lint + format + test on push/PR +.github/workflows/release.yml build .snplg + GitHub release on tag v* (fetches the real annotated tag object first — see §8) +``` + +## 7. Quality gates + +Scripts: `typecheck` (`tsc --noEmit`), `lint` (`eslint .`), `format` (`prettier --check .`), +`test` (`jest`), `test:ci` (`jest --ci --coverage`). Run order locally: typecheck → lint → format +→ test. `computeLassoRect` tests cover both directions: mid-view, top-tap, past-bottom clamp, +negative clamp, view≠page scaling, and that `left=0`/`right=pageW` always hold (plus the +direction-specific opposite edge: `bottom=pageH` for 'below', `top=0` for 'above'). Mock +`sn-plugin-lib` in Jest (`__mocks__/sn-plugin-lib.js`) so `App.tsx` imports don't pull +NativeModules — keep this mock's API surface in sync with what `src/sdk.ts` actually calls; a +stale mock throws `TypeError: ... is not a function` the moment a test finally exercises the gap +(happened once already — `getPageDisplaySize` was missing from the mock for a while, latent since +no test imported `App.tsx`/`sdk.ts` directly). + +## 8. Git / CI / Release workflow + +- **No commits on `main`.** Each unit on its own branch → PR, squash-merged. +- Conventional Commits; footer `Co-Authored-By: Claude Sonnet 5 `. +- Remote: GitHub repo `gorlix/sn_make_space`. +- CI `ci.yml`: Node 20, `npm ci`, typecheck + lint + format + test:ci. No Android (fast). +- Release `release.yml`: trigger on tag `v*`. Build `.snplg` via `buildPlugin.sh` (needs Android + toolchain + JDK 17), use the **annotated tag message** as release notes, attach the `.snplg`. + Create release tags with `git tag -a vX.Y.Z -m "..."` + `git push origin vX.Y.Z`. +- **`actions/checkout` resolves a tag-push trigger straight to the commit SHA, losing the + annotated tag object** — the workflow force-fetches the real tag ref right after checkout + (`git fetch --force origin "refs/tags/${GITHUB_REF_NAME}:refs/tags/${GITHUB_REF_NAME}"`) before + extracting release notes. Without this step the published notes silently become the tagged + commit's message instead of the tag annotation (happened for real on `v0.5.2`, fixed in + `v0.5.3`+). +- **⚠️ Always `git pull origin main` (not just `git checkout main`) immediately before tagging.** + `git checkout main` only reports "up to date with origin/main" against the last-fetched local + remote-tracking ref — it does **not** implicitly fetch. After merging a PR from a *different* + local branch (the normal flow here), local `main` can silently be behind `origin/main` by one or + more just-merged PRs. Tagging that stale HEAD ships a release **missing the very fix it claims to + contain**, with no error at any step — `typecheck`/`lint`/`test:ci` all pass because they run + against the (older, still-valid) code that got tagged. This happened for real: `v0.5.3` was + first tagged one PR short of the fix it was supposed to ship, had to be deleted + (`gh release delete vX.Y.Z --yes --cleanup-tag` + `git tag -d` locally) and retagged after an + explicit `git pull`. Before pushing any release tag, sanity-check with + `git log --oneline -1` against `git log --oneline origin/main -1` (post-fetch) — they must match. + +## 9. Build / deploy / debug (on-device) + +```bash +./buildPlugin.sh # → build/outputs/sn_make_space.snplg +adb push build/outputs/sn_make_space.snplg /storage/emulated/0/MyStyle/Plugins/sn_make_space.snplg +# ⚠️ push to MyStyle/Plugins/ (overwrite in place) when iterating on an already-installed +# build — the host's "reinstall" reads that managed copy, NOT MyStyle/ root. See the generic +# skill's gotcha #39. +# install/reinstall via Settings → Apps → Plugins +adb logcat -c; ; adb logcat -d -s ReactNativeJS:V +adb logcat -P "" # if NativeModules look null (chatty filter) +``` + +Confirm `build/generated/PluginConfig.json` has `pluginKey === "sn_make_space"` (must equal the +first arg of `AppRegistry.registerComponent`) and `pluginID === "lome8csqs3xu38pv"` (never +changes). See the generic skill's gotcha #33 about `reactPackages`. + +The startup log line `[make_space] v (code ) starting` in `index.js` is +deliberately **not** gated behind `__DEV__` (everything else is, and release builds are always +`--dev false`) — always check it in `adb logcat` first when debugging a sideload, to rule out +testing against a stale build before anything else (see the generic skill's gotcha #40 and the +`MyStyle/Plugins/` trap above — this line is exactly what caught that trap). + +## 10. Out of scope (advanced, later) + +True one-gesture auto-move: read `getElements`, shift each element's position (strokes: every EMR +point via `ElementDataAccessor.setRange` + `maxY`; geometry/textbox/picture/title: pixel fields), +then `modifyElements` + `saveCurrentNote` + `reloadFile`. Also page-overflow handling +(`insertNotePage`) and multi-layer shifting. + +## 11. InkHub distribution readiness (tracking, no spec yet) + +Firmware Chauvet `3.29.43_beta` (2026-08-26, Manta/Nomad) added a "Plugins" resource type to +InkHub, Supernote's in-device community sharing hub (existing for notes/templates/stickers). As of +that beta **no submission process, review criteria, or manifest requirements are published** — +Supernote's own docs/changelog say only "in the future". Current real-world plugin distribution is +still community-run: sideload via `MyStyle/` + the firmware's plugin manager (puzzle-piece sidebar +icon, requires the beta), catalogued informally on GitHub +([`fharper/awesome-supernote`](https://github.com/fharper/awesome-supernote)) and +`r/Supernote_dev`. Don't build InkHub-specific submission code against guesses. + +**What's genuinely no-regret prep, done as of v0.6.0:** +- `LICENSE` (MIT) — any future listing (InkHub or community catalogue) expects clear terms. +- `PluginConfig.json` metadata (`name`, `desc`, `iconPath`, `versionName`, `homepage`) reads like a + store listing — keep it accurate on every release, it's the likely source InkHub would pull from. + `name` was the raw `pluginKey` string (`sn_make_space`) until the v0.6.0 presentable-name pass — + it's independent of `pluginKey`/`pluginID` (those stay untouched, changing them would confuse the + host's registry for anyone who already installed the plugin) and is purely the human-facing title + shown in Settings → Apps → Plugins and the plugin detail screen. Distinct icons per button + (`assets/icon-below.png`/`icon-above.png`) plus a redesigned `assets/icon.png` app icon replaced + the generic puzzle-piece template default. +- Permission hygiene: this plugin declares no `uses-permissions` in `PluginConfig.json` — every SDK + call it makes (`getPageDisplaySize`, `lassoElements`, `setLassoBoxState`) operates on the + currently-open file via context, not a `filePath` argument. It briefly did need one by accident: + see §12, a real incident where an unrelated call silently tripped the gate. The 0.1.65 + `hasPermission`/`requestPermission` gate is the mechanism most likely reused for any future + InkHub review — if a feature ever reads/writes an arbitrary path, declare the permission before + submitting anywhere. + +**What to watch (re-check before doing more):** re-query the `supernote-docs` MCP and +`https://support.supernote.com/changelog-for-the-beta-versions-of-manta-and-nomad` periodically for +"InkHub" + "plugin" — that's where the real spec will land first. + +## 12. Incident: `getPageSize` silently broke the cut flow under 0.1.65 (2026-09-01) + +Right after the 0.1.65 SDK bump, the plugin stopped selecting anything: `lassoElements` was never +even reached. Root cause, found via `adb logcat`: `PluginFileAPI.getPageSize(filePath, page)` — +used since v1 to convert the tap into a pixel rect — got `FILE:READ`-gated by firmware Chauvet +`3.29.43_beta`. The API didn't throw; it just resolved as unavailable, so `loadContext()` returned +null and `runCut` bailed out silently before calling the lasso at all. See the generic skill's +gotcha #38 for the full log signature (`PluginSec: DENY reason=sdcard_no_read`) and gotcha #40 for +why the JS logging didn't show anything either (release builds are always `--dev false`, silencing +every `__DEV__`-gated `log()` call — the fix included one deliberately ungated startup log line for +this reason, see §9). + +**Fix**: swap `PluginFileAPI.getPageSize(filePath, page)` for `PluginCommAPI.getPageDisplaySize()` +— no `filePath` argument, current-page context like `lassoElements`, not gated. This also removed +the now-pointless `getCurrentFilePath`/`getCurrentPageNum` calls that existed only to build the +`getPageSize` arguments. + +**A second, unrelated trap surfaced during the same debugging session**: `adb push`-ing a rebuilt +`.snplg` to `MyStyle/` and tapping "reinstall" in Settings → Apps → Plugins re-ran the *old* build +— the host reinstalls from its own managed copy at `MyStyle/Plugins/.snplg`, not from +`MyStyle/` root. Looked exactly like "the fix did nothing" until caught by comparing file +size/mtime on-device. See §9 and the generic skill's gotcha #39. diff --git a/.claude/skills/supernote-plugin-dev/SKILL.md b/.claude/skills/supernote-plugin-dev/SKILL.md index 4620cd4..1791bfe 100644 --- a/.claude/skills/supernote-plugin-dev/SKILL.md +++ b/.claude/skills/supernote-plugin-dev/SKILL.md @@ -1,73 +1,57 @@ --- name: supernote-plugin-dev -description: "Build, debug, and extend Supernote e-ink device plugins using the sn-plugin-lib SDK (React Native + Android). This repo IS the `make_space` plugin (OneNote-style 'insert writing space' via tap-Y → native lasso). Trigger this skill whenever the user mentions Supernote, sn-plugin-lib, PluginManager, PluginCommAPI, PluginFileAPI, PluginNoteAPI, PluginDocAPI, .snplg files, e-ink plugin development, the make_space plugin, lasso/insert-space, or wants to create/modify a plugin for Supernote NOTE or DOC apps. Also trigger when the user discusses EMR coordinates, lasso operations on e-ink devices, or any React Native plugin targeting the Supernote PluginHost runtime. Even if the user just says 'plugin for my notebook' or 'extend my note-taking app' in the context of Supernote hardware, use this skill." +description: "Build, debug, and extend Supernote e-ink device plugins using the sn-plugin-lib SDK (React Native + Android). Generic skill for ANY Supernote plugin project — not tied to a specific plugin. Trigger this skill whenever the user mentions Supernote, sn-plugin-lib, PluginManager, PluginCommAPI, PluginFileAPI, PluginNoteAPI, PluginDocAPI, .snplg files, e-ink plugin development, PluginHost, lasso operations, EMR coordinates, or wants to create/modify/debug a plugin for the Supernote NOTE or DOC apps. Even if the user just says 'plugin for my notebook' or 'extend my note-taking app' in the context of Supernote hardware, use this skill. If the current repo also has a project-specific Supernote skill (e.g. named after the plugin itself), read that one FIRST for this-repo facts, then use this skill for the generic SDK/workflow knowledge it doesn't repeat." --- # Supernote Plugin Development Skill You are an expert Supernote plugin developer. Supernote plugins extend the NOTE (handwriting notebook) and DOC (document reader) apps on Supernote e-ink devices. Plugins run inside a **PluginHost** process that provides a React Native runtime, and communicate with NOTE/DOC via AIDL + SDK interfaces. +This skill is deliberately **project-agnostic** — it applies to any Supernote plugin. If the repo you're working in has its own project-specific skill (check `.claude/*/SKILL.md` for one named after the plugin), read that first for this-repo facts (pluginID, architecture, known pitfalls specific to that codebase); come back here for generic SDK/workflow/gotcha knowledge. + ## 📦 SDK version -This project pins `sn-plugin-lib` `^0.1.65` (bumped 2026-09-01 from `^0.1.19`; installed -`node_modules` had drifted to `0.1.43`). `0.1.65` is a superset for everything this project uses — -`typecheck`/`lint`/`test:ci` all pass unchanged, no source changes were required for the bump -itself. Notable SDK changes between `0.1.19` and `0.1.65` that affect the reference docs below: +Check the plugin's own `package.json` for the pinned `sn-plugin-lib` version before relying on any +version-gated fact below — don't assume `0.1.65` is what a given repo uses. Notable SDK changes +between `0.1.19` and `0.1.65` that affect the reference docs below (verify against your repo's +actual pinned version and the MCP before trusting these): `PluginManager.addPluginLifeListener` → `registerPluginLifeListener` (breaking, different callback shape), `PluginDocAPI.generateDocImage` → `generateCurrentDocImage` (renamed + new `type` param), `PointUtils.getNotePageSize` removed (no replacement), new `hasPermission`/`requestPermission` -runtime permission gate, and new current-file page-element CRUD -(`modifyPageElements`/`insertPageElements`/`deletePageElements`/`batchUpdatePageElements` on -`PluginCommAPI`). None of these are used by this repo's current code. +runtime permission gate (see Pattern 17 in `references/patterns.md`), and new current-file +page-element CRUD (`modifyPageElements`/`insertPageElements`/`deletePageElements`/ +`batchUpdatePageElements` on `PluginCommAPI`). + +**react-native is pinned to `0.79.2` for every plugin, hard, by the host device itself** — not just +a per-project convention. Supernote's own docs state: *"The plugin framework uses React Native +0.79.2. Your plugin project must use the same version; otherwise it may fail to run or be +incompatible with the host."* Do not let a plugin's `react-native` drift from `0.79.2`, and treat +any request to bump it as blocked-by-design until Supernote itself updates the on-device host +runtime — re-check the `supernote-docs` MCP periodically rather than assuming this is stale. + +**A React `react-native-renderer` version mismatch crashes silently on-device, not in `npm test`.** +`react-native@0.79.2` bundles its own `react-native-renderer` built against `react@19.0.0` exactly. +npm's peer range on `react-native` (`^19.0.0`) is loose enough to let `npm install` pull a newer +`react` (e.g. `19.2.x`) without any warning, but at runtime React throws `Incompatible React +versions` followed by `TypeError: Cannot read property 'default' of undefined` before the first +render — the UI never appears and there's no error visible to the user. If `react-native` stays +pinned at `0.79.2`, keep `react`/`react-test-renderer`/`@types/react` pinned at exactly `19.0.0` +too, regardless of what `npm outdated` suggests. Verify any `react` bump by watching `adb logcat` +on a real device (or emulator) before trusting `npm test` alone. ## ⚠️ Authoritative source: the `supernote-docs` MCP -A live documentation MCP (`supernote-docs`) is installed and **MUST be used** for any API/signature -question. It has two tools: `search_supernote` (semantic) and `query_docs_filesystem_supernote` -(`rg`/`cat`/`tree` over the `.mdx` docs). The live docs are the **authoritative, up-to-date** -source. The local `references/*.md` files are a useful supplement but **may lag** — when they -disagree with the MCP, trust the MCP. Query the MCP before writing any SDK code. - -## 🎯 This Project: `make_space` (v2) — READ FIRST - -This repository is **one specific plugin**, not a generic playground. Goal: insert blank writing -space mid-page (inspired by OneNote *"Insert extra writing space"*). - -**Confirmed v2 flow** (the move is done by the native lasso, NOT by the plugin): -1. Sidebar/toolbar button (**NOTE only**, `id 100`, `showType:1`) opens the plugin UI. -2. UI = fullscreen **transparent overlay with a thick grey border** (the visual cue "do something"). -3. User taps a Y point → plugin builds a pixel rect `{left:0, top:cutY, right:pageW, bottom:pageH}` - → `PluginCommAPI.lassoElements(rect)` selects everything below the line → - `setLassoBoxState(0)` shows the box → `PluginManager.closePluginView()`. -4. User **drags the native lasso selection by hand** to open space. Native move + native undo. - -**Why no auto-move:** the SDK has **no "move selection" API** (`resizeLassoRect` = proportional -scale only). A true one-gesture auto-move would require rewriting every stroke's EMR sample points -via `ElementDataAccessor.setRange` + coord conversion — deferred to "v2 advanced". - -**API subset actually used** (look these up in the MCP, not from memory): -`PluginManager.init / registerButton / closePluginView`, `PluginCommAPI.getPageDisplaySize / -lassoElements / setLassoBoxState` (+ `generateNotePng` for the transparency fallback). -**Not** `PluginFileAPI.getPageSize` — that got `FILE:READ`-gated under firmware Chauvet -`3.29.43_beta` and silently broke the cut flow; see `references/make-space.md` §12 and gotcha #38. - -**Locked decisions:** target **NOTE only**; overflow past page bottom **ignored in v1**; layer -toggle (all↔current) **pending SDK verification**; i18n = **en + it** only (extendable); -transparent overlay is primary, `generateNotePng` background image is the fallback if transparency -isn't honored on-device. - -**Most relevant gotchas for THIS task:** #3 (pixel vs EMR — here everything is **pixel**, no -conversion), #6 (lasso needs an active selection — we create it), plus the `lassoElements` -"strokes must be **fully inside** the rect" rule (a stroke crossing the cut line is missed — see -`references/make-space.md`). - -**Process rules (from the user):** serious JSDoc comments (explain *why*, not *what*); pure logic -in `src/makeSpace.ts` (`computeLassoRect`) so it's unit-testable without a device; quality gates -(typecheck/eslint/prettier/jest); **each unit on its own branch + PR**; CI on push/PR; release on -tag `v*`. Full step-by-step plan lives at -`/home/gorlix/.claude/plans/valuterei-che-quando-si-compiled-eagle.md`. - -→ **For the implementation recipe, read `references/make-space.md` first.** +A live documentation MCP (`supernote-docs`) should be configured for any serious Supernote plugin +work and **MUST be used** for any API/signature question — if it isn't configured in the current +project yet, add it: `claude mcp add --transport http --scope project supernote-docs +https://docs.supernote.com/mcp` (then approve it in an interactive session; it can't self-approve +in a non-interactive one). It has three tools: `search_supernote` (semantic search), +`query_docs_filesystem_supernote` (`rg`/`cat`/`tree` over the `.mdx` docs), and `submit_feedback` +(report a doc bug back to Supernote). The live docs are the **authoritative, up-to-date** source. +The local `references/*.md` files in this skill are a useful supplement but **may lag** — when they +disagree with the MCP, trust the MCP. Query the MCP before writing any SDK code, and especially +before assuming any version-pin, permission, or compatibility fact carried over from a previous +project or an older revision of these reference files. ## Before You Start @@ -76,19 +60,17 @@ against the `supernote-docs` MCP, which is authoritative): | Task | Read first | |------|-----------| -| **`make_space` implementation (THIS project)** | **`references/make-space.md`** ⭐ | | Any API call or type question | MCP `supernote-docs` → then `references/api-quick-ref.md` | | New project / environment / build / deploy / debug | `references/setup-and-build.md` | -| Common recipes (lasso ops, coordinate conversion, pending button, etc.) | `references/patterns.md` | +| Common recipes (lasso ops, coordinate conversion, pending button, permissions, etc.) | `references/patterns.md` | | Type definitions (Element, Stroke, Geometry, TextBox, etc.) | `references/types.md` | -| i18n, multi-language buttons (used here: en + it) | `references/i18n.md` | -| Floating window overlay (transparency fallback context) | `references/floating-window.md` | -| Pen lasso, EMR pen disable, scoped pen lock (v2-advanced only) | `references/pen-emr.md` | -| SQLite local storage in plugins (not used in v1) | `references/sqlite.md` | +| i18n, multi-language buttons | `references/i18n.md` | +| Floating window overlay | `references/floating-window.md` | +| Pen lasso, EMR pen disable, scoped pen lock | `references/pen-emr.md` | +| SQLite local storage in plugins | `references/sqlite.md` | -For `make_space` work, `references/make-space.md` is the entry point. The reference files contain -**authoritative API signatures and constraints** — do not rely on memory alone; the live MCP wins -on any conflict. +The reference files contain **authoritative API signatures and constraints** gathered from real +plugin builds — do not rely on memory alone; the live MCP wins on any conflict. ## Architecture (30-second overview) @@ -161,6 +143,18 @@ Always check `success` before reading `result`. ### PluginConfig.json - `pluginKey` MUST match the first argument of `AppRegistry.registerComponent(...)`. Mismatch = plugin won't load. - `pluginID` is auto-generated on first build. Never change it after distribution — it identifies the plugin. +- `name` is what shows in Settings → Apps → Plugins. It is **not** auto-derived from the in-app + title string — a plugin can easily ship with a friendly in-app title while the install list still + shows the raw npm-style package slug (e.g. `sn-my-plugin`) because `name` was never edited from + its scaffold default. Keep it in sync with whatever the plugin actually calls itself. +- `iconPath` (relative path to project root, e.g. `assets/icon.png`) is **not set by the scaffold + script** — it must be added manually, same as `author`. Without it, the plugin ships with no + custom icon in the install list (silently — no build warning). `buildPlugin.sh` copies the file + and rewrites `iconPath` to `/` in the packaged config; check the generated + `build/outputs/*.snplg`'s `PluginConfig.json` to confirm it actually landed. +- `uses-permissions` (string array, e.g. `["plugin.permission.INTERNET"]`) must list every + permission the plugin will ever call `hasPermission`/`requestPermission` for — see Pattern 17 in + `references/patterns.md`. Not set by the scaffold either; add manually. ## Build, Deploy & Debug @@ -226,10 +220,8 @@ What do you need to do? │ 4. cancelRecognize() to abort a long-running recognition if needed │ └─ Extract hardcoded strings / add multi-language support (i18n) - → THIS project ships en + it only. See references/make-space.md §5 and i18n.md - Pattern 7 (JSON button name) + Pattern 10 (registerLangListener). - Locale files: src/i18n/locales/{en_US,it_IT}.json - → Generic extract-translate-convert workflow (other locales): patterns.md Pattern 12 + → references/i18n.md Pattern 7 (JSON button name) + Pattern 10 (registerLangListener) + → Generic extract-translate-convert workflow for new locales: patterns.md Pattern 12 ``` ## Common Gotchas @@ -271,9 +263,10 @@ What do you need to do? 35. **`PluginManager.addPluginLifeListener` no longer exists (removed in 0.1.65)**: If you (or a stale example) write `PluginManager.addPluginLifeListener({onStart, onStop})`, it will be `undefined` at runtime — TypeScript will also reject it since the type was removed. Use `registerPluginLifeListener({onMsg(msg)})` instead; `msg.state` carries the lifecycle value, there's no more separate start/stop callbacks. See Pattern 9 in `references/patterns.md`. 36. **`PluginDocAPI.generateDocImage` was renamed to `generateCurrentDocImage` (0.1.65)**: The old signature took an explicit `docPath`; the new one always targets whatever document is currently open in the host and adds a required `type` param (0=default, 1=with text-selection styling). There is no way to render an arbitrary closed document's page anymore via this API. 37. **`PointUtils.getNotePageSize` was removed (0.1.65), no replacement in `PointUtils`**: Get page size from `PluginFileAPI.getPageSize(filePath, page)` (any file) or `PluginCommAPI.getPageDisplaySize()` (current file only, needed for the new page-element CRUD APIs — using `getPageSize` there can misalign coordinates). -38. **`PluginFileAPI.getPageSize(filePath, page)` got silently `FILE:READ`-gated under firmware Chauvet `3.29.43_beta` (confirmed on-device, sn-plugin-lib 0.1.65)**: no `PluginConfig.json` change is documented as required, and the call used to just work — but on this firmware `checkAPIAvailable` now logs `PluginSec: DENY reason=sdcard_no_read path=...` for any plugin that hasn't declared `FILE:READ`, and the API resolves as if the page size were unavailable (no thrown error, just an unusable result — easy to misdiagnose as a lasso/geometry bug instead of a permission one). **make_space was broken by exactly this**: `getPageSize` failing meant `loadContext()` returned null and `lassoElements` was never even called, so the symptom looked like "the lasso selects nothing" with no hint that a permission check was involved. Fix used here: switch to `PluginCommAPI.getPageDisplaySize()` (current-page context, like `lassoElements`/`setLassoBoxState`) instead of declaring the permission — it isn't gated and needs no `filePath`. Only fall back to declaring `plugin.permission.FILE:READ` (Pattern 17) if you genuinely need an arbitrary file's page size, not the currently open one. When any previously-working `PluginFileAPI` call starts silently no-opping after a firmware/SDK update, grep `adb logcat` for `PluginSec.*DENY` before assuming the plugin's own logic broke. +38. **`PluginFileAPI.getPageSize(filePath, page)` got silently `FILE:READ`-gated under firmware Chauvet `3.29.43_beta` (confirmed on-device, sn-plugin-lib 0.1.65)**: no `PluginConfig.json` change is documented as required, and the call used to just work — but on this firmware `checkAPIAvailable` now logs `PluginSec: DENY reason=sdcard_no_read path=...` for any plugin that hasn't declared `FILE:READ`, and the API resolves as if the page size were unavailable (no thrown error, just an unusable result — easy to misdiagnose as a lasso/geometry bug instead of a permission one). One plugin was broken by exactly this: `getPageSize` failing meant a lasso-setup helper returned null and the lasso API was never even called, so the symptom looked like "the lasso selects nothing" with no hint that a permission check was involved. Prefer `PluginCommAPI.getPageDisplaySize()` (current-page context, like `lassoElements`/`setLassoBoxState`) over `getPageSize` when you only need the currently-open file's page size — it isn't gated and needs no `filePath`. Only fall back to declaring `plugin.permission.FILE:READ` (Pattern 17) if you genuinely need an arbitrary file's page size. When any previously-working `PluginFileAPI` call starts silently no-opping after a firmware/SDK update, grep `adb logcat` for `PluginSec.*DENY` before assuming the plugin's own logic broke. The same class of bug bit `INTERNET` too (see the SDK version note above and Pattern 17) — any permission-gated API can regress this way after a firmware update, not just file access. 39. **Reinstalling from Settings → Apps → Plugins does NOT read the file you just `adb push`ed to `MyStyle/`**: the host keeps its own managed copy at `MyStyle/Plugins/.snplg` and the in-UI "reinstall/update" action installs from *that* copy, not from whatever you dropped in `MyStyle/` root. Confirmed on-device: pushing a rebuilt `.snplg` to `MyStyle/` and tapping reinstall silently reran the **old** build (same `versionName`/`versionCode` as before) — looked exactly like "the fix didn't do anything." Push directly to `MyStyle/Plugins/.snplg` (overwrite in place) when iterating on a fix, or use "Add Plugin" to browse to the new file explicitly rather than reinstalling the existing entry. -40. **Every `console.log` gated behind `__DEV__` is silent in a real install — `buildPlugin.sh` always bundles with `--dev false`**, including ad-hoc local test builds, not just CI releases. There is no separate "debug build" path in this project's build script. Consequence: extensive `__DEV__`-gated logging (this project has plenty, by design — see the `log()` helper in `index.js`/`App.tsx`) is completely invisible via `adb logcat` on any sideloaded build, which can make a real bug look like "nothing happened" when actually the JS ran fine but you can't see it. Keep one deliberately **ungated** `console.log` at startup (e.g. `${TAG} v${versionName} (code ${versionCode}) starting`, reading from `PluginConfig.json`) so `adb logcat -s ReactNativeJS:V` at least confirms which build is actually running before debugging further — this also catches gotcha #39 (stale reinstall) immediately instead of after a confusing detour. +40. **Every `console.log` gated behind `__DEV__` is silent in a real install — `buildPlugin.sh` always bundles with `--dev false`**, including ad-hoc local test builds, not just CI releases. There is typically no separate "debug build" path in a plugin's build script. Consequence: extensive `__DEV__`-gated logging is completely invisible via `adb logcat` on any sideloaded build, which can make a real bug look like "nothing happened" when actually the JS ran fine but you can't see it. Keep one deliberately **ungated** `console.log`/native-log line at startup (e.g. `${TAG} v${versionName} (code ${versionCode}) starting`, reading from `PluginConfig.json`) so `adb logcat` at least confirms which build is actually running before debugging further — this also catches gotcha #39 (stale reinstall) immediately instead of after a confusing detour. Prefer a custom logcat tag over the generic `ReactNativeJS` tag if the plugin has native code too (route both JS and native logs through one tag) — it makes `adb logcat -v time -s ` show the full picture in one stream instead of juggling multiple filters. +41. **Testing a plugin's own TCP/network sockets doesn't need the device on the same network as your dev machine**: `adb forward tcp: tcp:` tunnels a local TCP port straight to a port the plugin opened on the device over the existing USB/ADB connection, regardless of WiFi. Use it to `curl`/`nc`/raw-socket-probe a plugin's listener directly (`adb forward tcp:18888 tcp:8888` then `curl http://127.0.0.1:18888/`) instead of asking the user to find and share the device's WiFi IP. Combine with `adb logcat -v time -s ` (see gotcha #40) to correlate what you see over the forwarded connection with what the plugin's own logs say happened — this is how a "the tunnel opens but relays fail" bug (native socket blocked by an undeclared permission, not a code bug) gets diagnosed instead of guessed at. ## When Helping the User @@ -281,5 +274,6 @@ What do you need to do? - **For API questions**: Look up the exact signature in `references/api-quick-ref.md`. Provide working code with proper error handling. - **For debugging**: Check the gotchas list first. Common issues: missing init, wrong coordinates, missing lasso context, wrong layer. - **For complex features**: Combine patterns from `references/patterns.md`. Show the full flow including error handling and resource cleanup. -- **For i18n / localization requests** ("extract strings", "multi-language", "i18n"): THIS project ships **en + it** only (`src/i18n/locales/{en_US,it_IT}.json`) — see `references/make-space.md` §5 and `references/i18n.md` Pattern 7 (JSON button name) + Pattern 10 (`registerLangListener`). For adding *new* locales from scratch, the generic extract-translate-convert workflow is Pattern 12 in `references/patterns.md`. -- **Always**: Include TypeScript types, proper `APIResponse` checking, and `recycle()` calls where applicable. +- **For i18n / localization requests** ("extract strings", "multi-language", "i18n"): see `references/i18n.md` Pattern 7 (JSON button name) + Pattern 10 (`registerLangListener`). For adding new locales from scratch, the generic extract-translate-convert workflow is Pattern 12 in `references/patterns.md`. Check the repo's own locale files/config for which languages it already ships before assuming. +- **For network/socket features** (a plugin that opens its own TCP/HTTP listener or makes outbound requests): remember `INTERNET` is a runtime-gated permission like the file ones — declare it in `uses-permissions` and call `requestPermission` before the first socket, or every connection will fail with an on-device-only `SocketException` that's invisible unless you're watching `adb logcat` (see the SDK version note above, Pattern 17, and gotcha #41 for how to verify it actually works end-to-end). +- **Always**: Include TypeScript types, proper `APIResponse` checking, and `recycle()` calls where applicable. Verify any non-trivial change on a real device via `adb logcat` before calling it done — `npm test`/`tsc` passing does not mean the plugin works on-device (see the react-native-renderer version-mismatch note above for a concrete example of a change that passed all local checks and still crashed at runtime). diff --git a/.claude/skills/supernote-plugin-dev/references/api-quick-ref.md b/.claude/skills/supernote-plugin-dev/references/api-quick-ref.md index 38f3315..22c186c 100644 --- a/.claude/skills/supernote-plugin-dev/references/api-quick-ref.md +++ b/.claude/skills/supernote-plugin-dev/references/api-quick-ref.md @@ -2,9 +2,9 @@ All imports from `sn-plugin-lib`. All async methods return `APIResponse` unless noted. -Signatures verified against `sn-plugin-lib@0.1.65` (this project pins `^0.1.65`, up from -`^0.1.19`). Anything tagged "Added in 0.1.43" or "0.1.65" documents an SDK version floor; always -re-check the `supernote-docs` MCP for the current signature before writing code. +Signatures verified against `sn-plugin-lib@0.1.65`. Anything tagged "Added in 0.1.43" or "0.1.65" +documents an SDK version floor; always re-check the `supernote-docs` MCP for the current signature +before writing code, and check your own `package.json` for the pinned `sn-plugin-lib` version. --- diff --git a/.claude/skills/supernote-plugin-dev/references/floating-window.md b/.claude/skills/supernote-plugin-dev/references/floating-window.md index a854252..31e7e50 100644 --- a/.claude/skills/supernote-plugin-dev/references/floating-window.md +++ b/.claude/skills/supernote-plugin-dev/references/floating-window.md @@ -1,11 +1,5 @@ # Floating Window & Screen Adaptation Patterns -> **make_space:** NOT used in v1. Background context only for the transparency question (the plugin -> view itself is the overlay; if transparency fails, fall back to `generateNotePng`, not a floating -> window). See [make-space.md](make-space.md) §4. - ---- - ## Pattern 6: Native System Floating Window (TYPE_APPLICATION_OVERLAY) **Use case**: Persistent status bubble that survives `closePluginView()` — e.g., background processing indicator. diff --git a/.claude/skills/supernote-plugin-dev/references/i18n.md b/.claude/skills/supernote-plugin-dev/references/i18n.md index d5f710e..f68d610 100644 --- a/.claude/skills/supernote-plugin-dev/references/i18n.md +++ b/.claude/skills/supernote-plugin-dev/references/i18n.md @@ -1,10 +1,5 @@ # Internationalization (i18n) Patterns -> **make_space:** ships **en + it** only (extendable). Use Pattern 7 (JSON-serialized button name) -> and Pattern 10 (`registerLangListener`, underscore→dash). See [make-space.md](make-space.md) §5. - ---- - ## Pattern 7: Multi-language Button Names The `name` field in `registerButton` is not a plain string — it's an embedded JSON string that displays the matching name based on device language. diff --git a/.claude/skills/supernote-plugin-dev/references/make-space.md b/.claude/skills/supernote-plugin-dev/references/make-space.md deleted file mode 100644 index 08de228..0000000 --- a/.claude/skills/supernote-plugin-dev/references/make-space.md +++ /dev/null @@ -1,268 +0,0 @@ -# `make_space` — Implementation Reference (v2) - -Task-specific recipe for THIS repo. For generic API signatures use the `supernote-docs` MCP first, -then `api-quick-ref.md` / `types.md`. Full execution plan: -`/home/gorlix/.claude/plans/valuterei-che-quando-si-compiled-eagle.md`. - ---- - -## 1. What the plugin does - -OneNote-style "insert extra writing space". User taps a horizontal Y line; everything below is -selected as a **native lasso**; the user then drags that selection by hand to open blank space. -The plugin only **creates the selection** — the move and undo are native NOTE behavior. - -Flow: - -``` -button(id 100, NOTE, showType 1) → App mounts - load current page's pixel size (PluginCommAPI.getPageDisplaySize) - render transparent fullscreen View + thick grey border (visual cue) - user taps Y → computeLassoRect(tapY, viewH, pageW, pageH) - PluginCommAPI.lassoElements(rect) (rect in PIXELS; this ALSO shows the box) - PluginManager.closePluginView() (hand control back to NOTE) - → user drags selection natively - - ⚠️ Do NOT call setLassoBoxState(0) here — see §3. -``` - -## 2. Coordinates — everything is PIXELS here - -No EMR conversion needed. The tap is in screen pixels; `lassoElements` wants a pixel `Rect`. -Map the tap's Y (DP within the view) to page pixels by **proportion**, which sidesteps pixel-density -and view-scaling issues: - -```ts -// src/makeSpace.ts — pure, no SDK, unit-testable -export type Rect = {left: number; top: number; right: number; bottom: number}; - -/** - * Map a tap Y (in DP, relative to the fullscreen plugin view) to the lasso rect - * covering everything BELOW that line, in page pixel coordinates. - * - * Proportional mapping (tapY/viewH) avoids density/scale mismatches between the - * RN view (DP) and the note page (px). `top` is clamped to [0, pageH] so an - * out-of-bounds tap can never produce an invalid rect. - * - * @param tapY tap Y in DP, from PressEvent.nativeEvent.locationY - * @param viewH measured height of the plugin view in DP (onLayout) - * @param pageW page width in pixels (PluginCommAPI.getPageDisplaySize) - * @param pageH page height in pixels - * @returns lasso rect in page pixels: {left:0, top, right:pageW, bottom:pageH} - */ -export function computeLassoRect( - tapY: number, viewH: number, pageW: number, pageH: number, -): Rect { - const top = Math.round((tapY / viewH) * pageH); - return {left: 0, top: Math.max(0, Math.min(top, pageH)), right: pageW, bottom: pageH}; -} -``` - -Keep this logic out of `App.tsx` so Jest can test it without a device. - -## 3. The lasso recipe (the core) - -```ts -const res = await PluginCommAPI.lassoElements(rect); // APIResponse -// lassoElements ALREADY creates AND shows the selection box (auto-fit to the -// content, like a hand-drawn lasso). That's all we need — just hand control back. -await PluginManager.closePluginView(); -``` - -**⚠️ Do NOT call `setLassoBoxState(0)` after `lassoElements` (verified on-device, #34).** -`lassoElements` already shows the box, so the call is redundant — and worse, it puts -the firmware into the native **transfer/paste mode** (`TransferModeView`, -`isPenFloatingOnTheScreen`). If the user has anything in the Supernote clipboard, the -next tap-to-deselect **pastes that clipboard content (scaled up)** instead of -deselecting. A native hand-drawn lasso never calls `setLassoBoxState` and never -pastes; that one call was the only difference. Leave `setLassoBoxState` for flows that -genuinely need to hide/remove the box (states 1/2/3) — not for "show". - -**Critical caveat — strokes must be FULLY inside the rect.** `lassoElements` uses -`findTrailsContourInBox`: any stroke with a contour point outside the rect is **not** selected. For -"insert space between two lines" you tap in the gap, so the lines below are fully inside → fine. A -stroke that *crosses* the cut line will be missed. Do **not** add top padding to compensate — it -would grab the line above. Accept this limit in v1; document it for the user. - -Other notes: -- Always check `APIResponse.success` before `.result` (see SKILL.md "API Response Pattern"). -- `lassoElements` is layer-scoped to the current layer in practice → the "all layers" toggle may - not be achievable via SDK. Verify on-device before promising it; fall back to current-layer. -- Do NOT call `deleteElements` before/around the lasso (triggers a ~300ms page reload that starves - the binder pool — see `pen-emr.md` Pattern 14). Not needed here anyway. - -## 4. UI — transparent frame (+ fallback) - -Primary: root `View` `backgroundColor:'transparent'`, `borderWidth: 8`, `borderColor:'#9e9e9e'`. -A `Pressable` over `StyleSheet.absoluteFill` captures the tap; measure height with `onLayout`. - -**Transparency is unconfirmed on PluginHost.** Test first. If the view renders opaque (the note -isn't visible underneath, so the user can't see where to cut), use the fallback: render the page to -an image and tap on that. - -```ts -// fallback: snapshot the current page as a white-background PNG, show as background -await PluginFileAPI.generateNotePng({ - NOTEPath: filePath, page: pageNum, times: 1, - pngPath: pluginDir + '/preview.png', type: 1, // 1 = white bg -}); -``` - -The tap-Y math and the lasso call are identical in both UI variants. - -**Transparency verified on-device (A5X2 / SN100): the overlay IS transparent** — the note shows -through the grey frame, no `generateNotePng` fallback needed. Keep the fallback documented for other -firmware, but the primary path works. - -### ⚠️ PluginHost REUSES the App instance across open/close (verified on-device) - -When the plugin view is closed (`closePluginView`) and reopened, PluginHost does **not** remount the -React tree — there is **one** `App mounted`, then every later open only re-renders. Confirmed by -logcat: tap → close → reopen → tap shows no `App unmounted`/`App mounted` between them. - -Consequences: -- `useRef` / module-level state **persists** across opens. A guard ref (e.g. `busy` to debounce the - tap) that you set on tap **must be reset in `finally`**, or the second open is frozen with the - guard stuck `true` (this was the real "second open stuck on the grey frame" bug). -- `useEffect(() => …, [])` runs **once**, not per open. Do not rely on it to refresh per-open state. - Fetch the page context (`getCurrentFilePath/PageNum/PageSize`) **fresh inside the tap handler**, so - it is always current even if the user switched note/page between opens. - -## 5. i18n (en + it) - -`i18next` + `react-i18next` only (**no `react-native-localize`** — pure JS, avoids the PluginHost -`reactPackages` native-linking issue). Initial language from RN core -`NativeModules.I18nManager.localeIdentifier`; runtime changes via `registerLangListener`. Init in -`src/i18n/index.ts`, imported once from `index.js`. Locales `src/i18n/locales/{en_US,it_IT}.json`. -All UI strings via `t('key')`. Button name = serialized JSON so it follows device language: - -```js -name: JSON.stringify({en: 'Make Space', it: 'Fai Spazio'}) -``` - -`registerLangListener` callback uses `onMsg`, and `msg.lang` uses underscores (`it_IT`) → convert -with `.replace('_','-')` before `i18n.changeLanguage` (SKILL.md gotcha #13). Structure is -extendable: add a locale = new JSON + one entry in `resources`. - -## 6. Files & layout - -``` -index.js single toolbar button (NOTE, id 100) + init + import ./src/i18n -App.tsx transparent frame + tap capture + lasso + close (uses computeLassoRect, t()) -src/makeSpace.ts pure computeLassoRect -src/i18n/index.ts i18next init + registerLangListener -src/i18n/locales/en_US.json -src/i18n/locales/it_IT.json -__tests__/makeSpace.test.ts Jest tests for computeLassoRect -jest.config.js mock sn-plugin-lib NativeModules -.github/workflows/ci.yml typecheck + lint + format + test on push/PR -.github/workflows/release.yml build .snplg + GitHub release on tag v* -``` - -## 7. Quality gates - -Scripts: `typecheck` (`tsc --noEmit`), `lint` (`eslint .`), `format` (`prettier --check .`), -`test` (`jest`), `test:ci` (`jest --ci --coverage`). Run order locally: typecheck → lint → format -→ test. `computeLassoRect` tests: mid-view, top (tapY=0 → full page), past-bottom (clamp), -view≠page scaling, and that `left=0`/`right=pageW`/`bottom=pageH` always hold. Mock -`sn-plugin-lib` in Jest so `App.tsx` imports don't pull NativeModules. - -## 8. Git / CI / Release workflow - -- **No commits on `main`.** Each unit on its own branch → PR. Branches: `chore/tooling-scripts`, - `feat/make-space-core`, `feat/i18n`, `feat/make-space-ui`, `ci/pipeline`, `ci/release`. -- Conventional Commits; footer `Co-Authored-By: Claude Opus 4.8 `. -- Remote: **private** GitHub repo (`gh repo create gorlix/sn_make_space --private`). The repo has - no remote yet; add a proper RN `.gitignore` and push `main` first. -- CI `ci.yml`: Node 20, `npm ci`, typecheck + lint + format + test:ci. No Android (fast). -- Release `release.yml`: trigger on tag `v*`. Build `.snplg` via `buildPlugin.sh` (needs Android - toolchain + JDK 17), use the **annotated tag message** as release notes, attach the `.snplg`. - Create release tags with `git tag -a vX.Y.Z -m "..."` + `git push origin vX.Y.Z`. Verify - `buildPlugin.sh` is non-interactive in CI (it generates `pluginID` on first build). -- **⚠️ Always `git pull origin main` (not just `git checkout main`) immediately before tagging.** - `git checkout main` only reports "up to date with origin/main" against the last-fetched local - remote-tracking ref — it does **not** implicitly fetch. After merging a PR from a *different* - local branch (the normal flow here), local `main` can silently be behind `origin/main` by one or - more just-merged PRs. Tagging that stale HEAD ships a release **missing the very fix it claims to - contain**, with no error at any step — `typecheck`/`lint`/`test:ci` all pass because they run - against the (older, still-valid) code that got tagged. This happened for real: `v0.5.3` was - first tagged one PR short of the fix it was supposed to ship, had to be deleted - (`gh release delete vX.Y.Z --yes --cleanup-tag` + `git tag -d` locally) and retagged after an - explicit `git pull`. Before pushing any release tag, sanity-check with - `git log --oneline -1` against `git log --oneline origin/main -1` (post-fetch) — they must match. - -## 9. Build / deploy / debug (on-device) - -```bash -./buildPlugin.sh # → build/outputs/*.snplg -adb push build/outputs/*.snplg /storage/emulated/0/MyStyle/ -# install via Settings → Apps → Plugins -adb logcat -c; ; adb logcat -d -s ReactNativeJS:V -adb logcat -P "" # if NativeModules look null (chatty filter) -``` - -Confirm `build/generated/PluginConfig.json` has `pluginKey === "sn_make_space"` (must equal the -first arg of `AppRegistry.registerComponent`). See SKILL.md gotcha #33 about `reactPackages`. - -## 10. Out of scope (v2-advanced, later) - -True one-gesture auto-move: read `getElements`, shift each element's position (strokes: every EMR -point via `ElementDataAccessor.setRange` + `maxY`; geometry/textbox/picture/title: pixel fields), -then `modifyElements` + `saveCurrentNote` + `reloadFile`. Also page-overflow handling -(`insertNotePage`) and multi-layer shifting. Not in v1. - -## 11. InkHub distribution readiness (tracking, no spec yet) - -Firmware Chauvet `3.29.43_beta` (2026-08-26, Manta/Nomad) added a "Plugins" resource type to -InkHub, Supernote's in-device community sharing hub (existing for notes/templates/stickers). As of -that beta **no submission process, review criteria, or manifest requirements are published** — -Supernote's own docs/changelog say only "in the future". Current real-world plugin distribution is -still community-run: sideload via `MyStyle/` + the firmware's plugin manager (puzzle-piece sidebar -icon, requires the beta), catalogued informally on GitHub -([`fharper/awesome-supernote`](https://github.com/fharper/awesome-supernote)) and -`r/Supernote_dev`. Don't build InkHub-specific submission code against guesses. - -**What's genuinely no-regret prep, done now:** -- Repo needs a `LICENSE` — any future listing (InkHub or community catalogue) expects clear terms. -- `PluginConfig.json` metadata (`name`, `desc`, `iconPath`, `versionName`, `homepage`) already - reads like a store listing — keep it accurate on every release, it's the likely source InkHub - would pull from. `name` was the raw `pluginKey` string (`sn_make_space`) until the presentable- - name pass — it's independent of `pluginKey`/`pluginID` (those stay untouched, changing them would - confuse the host's registry for anyone who already installed the plugin) and is purely the - human-facing title shown in Settings → Apps → Plugins and the plugin detail screen. Distinct - icons per button (`assets/icon-below.png`/`icon-above.png`) plus a redesigned `assets/icon.png` - app icon replaced the generic puzzle-piece template default. -- Permission hygiene: this plugin declares no `uses-permissions` in `PluginConfig.json` — every SDK - call it makes (`getPageDisplaySize`, `lassoElements`, `setLassoBoxState`) operates on the - currently-open file via context, not a `filePath` argument. It briefly did need one by accident: - see §12, a real incident where an unrelated call silently tripped the gate. The 0.1.65 - `hasPermission`/`requestPermission` gate (see `api-quick-ref.md` §1 "Permissions") is the - mechanism most likely reused for any future InkHub review — if a feature ever reads/writes an - arbitrary path, declare the permission before submitting anywhere. - -**What to watch (re-check before doing more):** re-query the `supernote-docs` MCP and -`https://support.supernote.com/changelog-for-the-beta-versions-of-manta-and-nomad` periodically for -"InkHub" + "plugin" — that's where the real spec will land first. - -## 12. Incident: `getPageSize` silently broke the cut flow under 0.1.65 (2026-09-01) - -Right after the 0.1.65 bump (§11), the plugin stopped selecting anything: `lassoElements` was never -even reached. Root cause, found via `adb logcat`: `PluginFileAPI.getPageSize(filePath, page)` — -used since v1 to convert the tap into a pixel rect — got `FILE:READ`-gated by firmware Chauvet -`3.29.43_beta`. The API didn't throw; it just resolved as unavailable, so `loadContext()` returned -null and `runCut` bailed out silently before calling the lasso at all. See SKILL.md gotcha #38 for -the full log signature (`PluginSec: DENY reason=sdcard_no_read`) and gotcha #40 for why the JS -logging didn't show anything either (release builds are always `--dev false`, silencing every -`__DEV__`-gated `log()` call — the fix included one deliberately ungated startup log line for this -reason). - -**Fix**: swap `PluginFileAPI.getPageSize(filePath, page)` for `PluginCommAPI.getPageDisplaySize()` -— no `filePath` argument, current-page context like `lassoElements`, not gated. This also removed -the now-pointless `getCurrentFilePath`/`getCurrentPageNum` calls that existed only to build the -`getPageSize` arguments (see `src/sdk.ts`, `App.tsx`). - -**A second, unrelated trap surfaced during the same debugging session**: `adb push`-ing a rebuilt -`.snplg` to `MyStyle/` and tapping "reinstall" in Settings → Apps → Plugins re-ran the *old* build -— the host reinstalls from its own managed copy at `MyStyle/Plugins/.snplg`, not from -`MyStyle/` root. Looked exactly like "the fix did nothing" until caught by comparing file -size/mtime on-device. See SKILL.md gotcha #39. diff --git a/.claude/skills/supernote-plugin-dev/references/patterns.md b/.claude/skills/supernote-plugin-dev/references/patterns.md index 19fa7a6..35e4293 100644 --- a/.claude/skills/supernote-plugin-dev/references/patterns.md +++ b/.claude/skills/supernote-plugin-dev/references/patterns.md @@ -1,9 +1,5 @@ # Common Supernote Plugin Patterns -> **make_space:** relevant here = lasso ops + coordinate handling (though this plugin stays in -> pixels and needs no EMR conversion). Pen-lasso (Pattern 14) and floating-window (Pattern 6) are -> NOT used in v1. Recipe: [make-space.md](make-space.md). - Reusable code patterns for the most frequent plugin development tasks. --- @@ -282,7 +278,7 @@ useEffect(() => { const lifeSub = PluginManager.registerPluginLifeListener({ onMsg: msg => { // msg.state is the lifecycle state value; confirm the mapping via the MCP - // for the states this project cares about before branching on it. + // for the states your plugin cares about before branching on it. // Treat "panel closed" as the reset trigger: setCurrentScreen(null); setSelectedItems([]); @@ -410,11 +406,16 @@ event as long as it happened within the past second, so a late listener still ca `FILE:READ`/`FILE:WRITE`/`FILE:DELETE`/`INTERNET` are gated at runtime for anything outside the plugin's own private data directory (`Document`, `EXPORT`, `INBOX`, `MyStyle`, `Note`, -`SCREENSHOT`, external SD/OTG). Declare the permission in `PluginConfig.json`'s `uses-permissions` -first — calling `hasPermission`/`requestPermission` for an undeclared permission throws (error -`1500`). This project's own operations (`lassoElements`, `setLassoBoxState`) stay inside the -currently-open file via `PluginCommAPI` and don't need this; it matters only if a feature starts -reading/writing arbitrary paths (e.g. sticker export, cross-note operations). +`SCREENSHOT`, external SD/OTG) — **and `INTERNET` is gated for every outbound socket, full stop, +with no default-accessible exception** (confirmed on-device: a plugin opening a plain `Socket` +from native Java/Kotlin code, not just from JS, is blocked identically). Declare the permission in +`PluginConfig.json`'s `uses-permissions` first — calling `hasPermission`/`requestPermission` for an +undeclared permission throws (error `1500`), and skipping `requestPermission` entirely still gets +silently blocked at the socket layer with `SocketException: ... has no NETWORK permission` (visible +only in `adb logcat`, not surfaced to JS as a catchable error unless your native code forwards it). +Operations that stay inside the currently-open file via `PluginCommAPI` (`lassoElements`, +`setLassoBoxState`, etc.) don't need any of this; it matters only once a feature reads/writes +arbitrary paths or opens a network socket. ```ts import { PluginManager } from 'sn-plugin-lib'; @@ -431,9 +432,27 @@ async function ensureWriteAccess(): Promise { } ``` +Same pattern for `INTERNET` — check/request it once before opening the plugin's first socket (e.g. +right before starting a listener or making an outbound connection), not at mount time, so the +dialog appears in context: + +```ts +const granted = await PluginManager.hasPermission('plugin.permission.INTERNET'); +if (granted !== 1) { + const result = await PluginManager.requestPermission( + 'plugin.permission.INTERNET', + 'Explain in one line why the plugin needs network access.', + ); + if (result === 0) throw new Error('PERMISSION_DENIED'); // or your own sentinel +} +``` + **Gotchas**: - `"Allow this time only"` (`1`) is revoked when the plugin exits — re-check with `hasPermission` on next launch, don't cache the result across sessions. - `-1` (dialog dismissed without a choice) is treated as deny, but the dialog reappears on the next `requestPermission` call — don't loop-retry immediately, surface a UI affordance instead. - `desc` only customizes the "already denied" dialog copy; it's ignored for every other state. +- Forgetting to add the permission to `PluginConfig.json`'s `uses-permissions` array is the single + most common cause of "start error / this permission has not been declared" — check the manifest + before debugging the JS/native call chain. diff --git a/.claude/skills/supernote-plugin-dev/references/pen-emr.md b/.claude/skills/supernote-plugin-dev/references/pen-emr.md index 717265f..5fc0d9c 100644 --- a/.claude/skills/supernote-plugin-dev/references/pen-emr.md +++ b/.claude/skills/supernote-plugin-dev/references/pen-emr.md @@ -1,10 +1,5 @@ # Pen Lasso & EMR Pen Disable Patterns -> **make_space:** NOT used in v1. The user taps with a finger and draws nothing inside the plugin, -> so there is no EMR leak to guard. Relevant only if v2-advanced adds in-plugin pen gestures. - ---- - ## Pattern 14: Pen Lasso — Convert a Drawn Stroke into a Rectangular Lasso **Use case**: User presses a "lasso mode" button, draws a freehand stroke to define a region, and the plugin converts that stroke into a native rectangular lasso selection. diff --git a/.claude/skills/supernote-plugin-dev/references/setup-and-build.md b/.claude/skills/supernote-plugin-dev/references/setup-and-build.md index ec47950..70a2ffa 100644 --- a/.claude/skills/supernote-plugin-dev/references/setup-and-build.md +++ b/.claude/skills/supernote-plugin-dev/references/setup-and-build.md @@ -1,10 +1,5 @@ # Setup, Build & Debug Guide -> **make_space:** scaffold already exists — skip project init. Use this for `buildPlugin.sh`, -> adb deploy, logcat debug, and as context for the release CI. See [make-space.md](make-space.md) §9. - ---- - ## §1 Environment Requirements | Dependency | Minimum Version | Notes | diff --git a/.claude/skills/supernote-plugin-dev/references/sqlite.md b/.claude/skills/supernote-plugin-dev/references/sqlite.md index 17efe75..5e4e90c 100644 --- a/.claude/skills/supernote-plugin-dev/references/sqlite.md +++ b/.claude/skills/supernote-plugin-dev/references/sqlite.md @@ -1,9 +1,5 @@ # SQLite Local Storage Pattern -> **make_space:** NOT used — the plugin stores no local data in v1. - ---- - ## Pattern 11: SQLite Local Storage in a Plugin Plugins can use SQLite for persistent data storage via `react-native-sqlite-storage`. The database path must be prefixed with `plugins//` to stay within the plugin's sandboxed directory. diff --git a/.claude/skills/supernote-plugin-dev/references/types.md b/.claude/skills/supernote-plugin-dev/references/types.md index bb3da05..feccc78 100644 --- a/.claude/skills/supernote-plugin-dev/references/types.md +++ b/.claude/skills/supernote-plugin-dev/references/types.md @@ -1,9 +1,5 @@ # Supernote Plugin SDK Type Definitions -> **make_space:** relevant types are `Rect` (pixel — for `lassoElements`), `Size` (for -> `getPageSize`), and `APIResponse`. `Element`/`Stroke` field-level position shifting matters only -> for the deferred v2-advanced auto-move. See [make-space.md](make-space.md). - All types from `sn-plugin-lib`. --- diff --git a/App.tsx b/App.tsx index b27bcec..24b8d43 100644 --- a/App.tsx +++ b/App.tsx @@ -9,7 +9,7 @@ * can drag the selection to make space. * * The move and its undo are native NOTE behavior — this plugin only builds the - * selection. See .claude/skills/supernote-plugin-dev/references/make-space.md. + * selection. See .claude/skills/make-space/references/make-space.md. * * @format */