diff --git a/CLAUDE.md b/CLAUDE.md index e2ba903..88a3a9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,7 @@ Each workspace has its own `CLAUDE.md` with its build, test and gotcha notes. Re ## Dependencies - Pin every dependency to an exact version (`.npmrc` sets `save-exact=true`). The only ranges allowed are the adapters' `peerDependencies` (`react`, `vue`). +- Dependencies shared by more than one workspace live in the named `catalogs` of `pnpm-workspace.yaml` (`core`, `react`, `tailwind`, `tooling`) and are referenced as `catalog:`. Bump them there, never in a `package.json`. A dependency used by a single workspace stays pinned in its own `package.json` until a second workspace needs it. - pnpm 12 settings (`overrides`, `allowBuilds`) go in `pnpm-workspace.yaml`, never in `package.json`. Installs reject versions published less than 24h ago (`minimumReleaseAge`), so pick an older exact version instead of adding exclusions. New packages with install scripts must be added to `allowBuilds`. - `packages/*` and `adapters/*` are published to npm as public packages and ship only `dist/` (`files`). `apps/*` and the root stay `private`. Workspace deps use `workspace:*`, which pnpm rewrites to exact versions on publish. -- Stay on TypeScript 6.x. TS 7.0 has no programmatic API, which breaks `dts-bundle-generator` (player-core) and `vue-tsc` (player-vue). +- TypeScript 7 (`catalog:core`) everywhere except `adapters/player-vue`, which stays on 6 (`catalog:vue-tooling`): TS 7 has no programmatic API, and `vue-tsc` and the Vue SFC loader need it. Tools that load TypeScript as a library don't work on 7, so check a new build/type tool against it before adding it. diff --git a/adapters/player-react/package.json b/adapters/player-react/package.json index cba4d77..005e8e1 100644 --- a/adapters/player-react/package.json +++ b/adapters/player-react/package.json @@ -59,10 +59,10 @@ ], "devDependencies": { "@rsbuild/plugin-react": "2.1.0", - "@rslib/core": "1.0.1", - "@size-limit/file": "14.0.0", - "@types/react": "19.3.0", - "size-limit": "14.0.0", - "typescript": "6.0.3" + "@rslib/core": "catalog:tooling", + "@size-limit/file": "catalog:tooling", + "@types/react": "catalog:react", + "size-limit": "catalog:tooling", + "typescript": "catalog:core" } } diff --git a/adapters/player-vue/CLAUDE.md b/adapters/player-vue/CLAUDE.md index 0a1e266..0caacee 100644 --- a/adapters/player-vue/CLAUDE.md +++ b/adapters/player-vue/CLAUDE.md @@ -5,3 +5,4 @@ - **The `vue-tsc` step in the `build` script is required.** Don't replace it with `dts: true` in `rslib.config.ts`: Rslib silently emits a generic `DefineComponent>` for `.vue` files. If the types look generic, inspect `dist/InteractiveDemo.vue.d.ts`, since a passing build proves nothing. - `--noEmit false` in that step overrides `tsconfig.json`'s `noEmit: true` (which `check-types` needs). - `vue` is a peer dependency (range), never a direct dependency. +- **Stays on TypeScript 6** (`catalog:vue-tooling`) while the rest of the repo is on 7: `vue-tsc` and the Vue loader (`@vue/compiler-sfc` resolving `defineProps` types) load TypeScript's JS API, which TS 7 removed. Move it to `catalog:core` once they support TS 7. diff --git a/adapters/player-vue/package.json b/adapters/player-vue/package.json index 4e0dd45..5964bee 100644 --- a/adapters/player-vue/package.json +++ b/adapters/player-vue/package.json @@ -59,11 +59,11 @@ ], "devDependencies": { "@rsbuild/plugin-vue": "2.0.1", - "@rslib/core": "1.0.1", - "@size-limit/file": "14.0.0", + "@rslib/core": "catalog:tooling", + "@size-limit/file": "catalog:tooling", "@vue/language-core": "3.3.11", - "size-limit": "14.0.0", - "typescript": "6.0.3", + "size-limit": "catalog:tooling", + "typescript": "catalog:vue-tooling", "vue": "3.5.43", "vue-tsc": "3.3.11" } diff --git a/apps/extension/entrypoints/editor/components/stage/HotspotLayer.tsx b/apps/extension/entrypoints/editor/components/stage/HotspotLayer.tsx index ad55285..a253619 100644 --- a/apps/extension/entrypoints/editor/components/stage/HotspotLayer.tsx +++ b/apps/extension/entrypoints/editor/components/stage/HotspotLayer.tsx @@ -168,7 +168,7 @@ export function HotspotLayer({ } as React.CSSProperties } > - {/* Same anatomy as the player's hotspot (Hotspot.svelte) */} + {/* Same anatomy as the player's hotspot (player-core ui/Hotspot/Hotspot.ts) */} @@ -221,7 +221,7 @@ function TooltipBubble({ const bg = hotspot.bgColor ?? defaultHotspotStyle.bgColor; const placement = layout?.placement; - // Box + tail as one outline — identical to the player's Tooltip.svelte. + // Box + tail as one outline — identical to the player's tooltip (player-core ui/Tooltip/Tooltip.ts). const d = layout ? bubblePath({ width: layout.width, diff --git a/apps/extension/entrypoints/editor/components/stage/player-visuals.css b/apps/extension/entrypoints/editor/components/stage/player-visuals.css index 2adee73..804c3f5 100644 --- a/apps/extension/entrypoints/editor/components/stage/player-visuals.css +++ b/apps/extension/entrypoints/editor/components/stage/player-visuals.css @@ -1,6 +1,6 @@ /* * The player's hotspot + tooltip look, mirrored for the editor stage so what you edit is what - * ships. Source of truth: packages/player-core/src/components/{Hotspot,Tooltip,PhotoLayer}.svelte + * ships. Source of truth: packages/player-core/src/ui/{Hotspot,Tooltip,PhotoLayer}/*.styles.ts * and packages/player-core/src/app.css (the --wd-* tokens). Keep the two in sync — these are * demo visuals, not editor UI, which is why they use literal colors instead of theme tokens. */ @@ -186,7 +186,7 @@ } } -/* the body: box + tail as one SVG outline (see HotspotLayer / player's Tooltip.svelte) */ +/* the body: box + tail as one SVG outline (see HotspotLayer / player-core ui/Tooltip/Tooltip.ts) */ .wd-body { position: absolute; inset: 0 auto auto 0; diff --git a/apps/extension/package.json b/apps/extension/package.json index 5a9f118..9673133 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -33,22 +33,22 @@ "idb": "8.0.3", "lucide-react": "1.47.0", "mediabunny": "1.59.0", - "react": "19.3.0", - "react-dom": "19.3.0", + "react": "catalog:react", + "react-dom": "catalog:react", "sonner": "2.0.8", "tailwind-merge": "3.7.0" }, "devDependencies": { - "@tailwindcss/vite": "4.3.3", + "@tailwindcss/vite": "catalog:tailwind", "@types/chrome": "0.3.0", - "@types/react": "19.3.0", - "@types/react-dom": "19.3.0", + "@types/react": "catalog:react", + "@types/react-dom": "catalog:react", "@wxt-dev/module-react": "1.2.2", - "tailwindcss": "4.3.3", + "tailwindcss": "catalog:tailwind", "tw-animate-css": "1.4.0", - "typescript": "6.0.3", - "vite": "8.3.0", - "vitest": "5.0.1", + "typescript": "catalog:core", + "vite": "catalog:tooling", + "vitest": "catalog:tooling", "wxt": "0.21.4" } } diff --git a/biome.json b/biome.json index 03bab34..269931a 100644 --- a/biome.json +++ b/biome.json @@ -83,7 +83,34 @@ }, "overrides": [ { - "includes": ["**/*.svelte", "**/*.vue"], + "includes": ["packages/player-core/**"], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "paths": { + "alien-signals": "Import signals from src/dom/signals.ts: it's the only module the build compiles alien-signals into dist/ through, so a direct import would ship as a dependency the package doesn't declare." + } + } + } + } + } + } + }, + { + "includes": ["packages/player-core/src/dom/signals.ts"], + "linter": { + "rules": { + "style": { + "noRestrictedImports": "off" + } + } + } + }, + { + "includes": ["**/*.vue"], "linter": { "rules": { "style": { diff --git a/package.json b/package.json index 92dfd81..6d29ae4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@changesets/cli": "3.0.3", "lefthook": "2.1.14", "turbo": "2.11.3", - "typescript": "6.0.3" + "typescript": "catalog:core" }, "packageManager": "pnpm@12.6.0", "engines": { diff --git a/packages/exporter/package.json b/packages/exporter/package.json index 83255ae..ef1c3ee 100644 --- a/packages/exporter/package.json +++ b/packages/exporter/package.json @@ -57,13 +57,13 @@ } ], "devDependencies": { - "@rslib/core": "1.0.1", - "@size-limit/file": "14.0.0", - "@vitest/browser": "5.0.1", - "@vitest/browser-playwright": "5.0.1", - "playwright": "1.63.0", - "size-limit": "14.0.0", - "typescript": "6.0.3", - "vitest": "5.0.1" + "@rslib/core": "catalog:tooling", + "@size-limit/file": "catalog:tooling", + "@vitest/browser": "catalog:tooling", + "@vitest/browser-playwright": "catalog:tooling", + "playwright": "catalog:tooling", + "size-limit": "catalog:tooling", + "typescript": "catalog:core", + "vitest": "catalog:tooling" } } diff --git a/packages/player-core/.size-limit.mjs b/packages/player-core/.size-limit.mjs new file mode 100644 index 0000000..05b299c --- /dev/null +++ b/packages/player-core/.size-limit.mjs @@ -0,0 +1,27 @@ +// What a host app actually ships from this package: bundled, minified and brotlied by +// esbuild, with the schema left out (the host already has it through the adapters). +const shared = { + path: "dist/index.js", + ignore: ["@rustrak/openshowcase-schema"], +}; + +export default [ + { + ...shared, + name: "Player (JS + CSS)", + import: "{ Player }", + limit: "35 KB", + }, + { + ...shared, + name: "Tooltip geometry only (the player must tree-shake away)", + import: "{ bubblePath, computeTooltipPlacement }", + limit: "2 KB", + // esbuild keeps the CSS of JS modules it tree-shook away (Rollup, webpack and Rspack + // drop it, per `sideEffects`), so only the JS is measured here. + modifyEsbuildConfig: (config) => ({ + ...config, + external: [...(config.external ?? []), "*.css"], + }), + }, +]; diff --git a/packages/player-core/CLAUDE.md b/packages/player-core/CLAUDE.md index 5bea660..0b6ba54 100644 --- a/packages/player-core/CLAUDE.md +++ b/packages/player-core/CLAUDE.md @@ -1,25 +1,31 @@ # @rustrak/openshowcase-player-core -Framework-agnostic playback engine used by both adapters and by the extension's editor preview. Written in Svelte 5 (runes) + Tailwind v4, compiled to plain JS. Consumers never see Svelte. +Framework-agnostic playback engine used by both adapters and by the extension's editor preview. Plain TypeScript + Tailwind v4, with no UI framework: components are functions built on a small in-house DOM layer (`src/dom/`) over `alien-signals`. The published package depends on nothing but the schema. Keep it that way: the player is embedded in other people's apps, so it must not ship a framework runtime or pull in dependencies. ## Build -- `pnpm build`: Vite (CSS inlined into JS, no `.css` output) + `dts-bundle-generator` for `dist/index.d.ts`. -- **Keep `exports["."].default` pointing at `./dist/index.js`** (only `types` points at `src`). Resolving raw source forces every consumer's bundler to compile `.svelte` files, and the extension's build breaks. -- `@rustrak/openshowcase-schema` stays `external` in `vite.config.ts`. Inlining it bundles a second copy of valibot. -- `pnpm size`: 35 KB brotli budget for `dist/index.js`. -- `pnpm test`: Vitest projects `unit` (Node, for `core/`) and `component` (covers `__test__/component` + `__test__/integration` in real Chromium via Playwright, not jsdom). +- `pnpm build`: Rslib in bundleless mode (`rslib.config.ts`): one readable, unminified ESM file per source module, so a host that imports only the tooltip geometry doesn't get the player. The host app's bundler minifies. Vite is only used by Vitest. +- **No side effects at import time.** `package.json` declares `"sideEffects": ["**/*.css"]`: `dist/app.css` (Tailwind) is the only side-effect import. Components inject their own CSS when they're created (`injectStyles`). Don't add module-level code that touches the DOM or globals. +- **alien-signals is compiled into `dist/`**, not a dependency: it's a `devDependency`, only `src/dom/signals.ts` imports it, and a second, bundled lib in `rslib.config.ts` builds that module (alien-signals included) into `dist/dom/signals.js`. Import signals from `dom/signals`, never from `alien-signals` directly (a Biome `noRestrictedImports` rule enforces it): a direct import would stay in `dist/` as an import of a package the published package doesn't declare. +- Declarations: `tsconfig.build.json` starts them at `src/index.ts`. +- **Keep `exports["."].default` pointing at `./dist/index.js`.** +- `pnpm size` (`.size-limit.mjs`): what a host ships, bundled and minified by esbuild. `Player` (JS + CSS) within 35 KB, and the tooltip geometry alone within 2 KB, which fails if the player stops tree-shaking away. +- `pnpm test`: Vitest projects `unit` (Node, for `core/`) and `component` (covers `__test__/component` + `__test__/integration` in real Chromium via Playwright, not jsdom). Run it with `--browser.headless`. ## Code structure - `src/core/*.ts`: pure orchestration and timing logic (state machines, clip watching, overlay choreography), unit-tested with fake timers and no DOM. -- `src/components/*.svelte`: markup + wiring only. Once a component's script logic grows past ~100 lines, move the decision-making into `core/` with tests. +- `src/dom/`: the DOM layer. `h()`/`svg()` create elements; a function prop or child stays bound to the signals it reads (one effect per binding, nothing re-renders). `show()` is `{#if}` (with an optional exit animation), `each()` is a keyed list, `mount()` owns a scope whose dispose stops every binding. Views created by `show()`/`each()` live in scopes detached from the effect that creates them: alien-signals disposes an effect's child effects on every re-run. +- `src/ui//`: one folder per component, `.ts` (the component) + `.styles.ts` (its CSS, when it has any), markup + wiring only. Props are `ViewProps

`: data as getters, `on*` callbacks as is. Once a component's logic grows past ~100 lines, move the decision-making into `core/` with tests. +- Signal writes apply at once. Group writes that must land together (a step render, the photo/video swap) in `batch()`. +- Tests: `renderView()` (`__test__/render-view.ts`) mounts a component with plain props, each one a signal that `rerender()` sets. ## Styling: the player lives in other people's pages - `app.css` imports only Tailwind's theme + utilities, **never full `tailwindcss`** (its preflight resets the host page). Required resets are scoped under `.openshowcase-player`. -- Host CSS is unlayered and beats Tailwind's layered utilities, so visuals that must survive any site (hotspot, tooltip, navbar, spotlight) are styled in scoped ` diff --git a/packages/player-core/src/components/Navbar.svelte b/packages/player-core/src/components/Navbar.svelte deleted file mode 100644 index 3723fd0..0000000 --- a/packages/player-core/src/components/Navbar.svelte +++ /dev/null @@ -1,209 +0,0 @@ - - -

- - diff --git a/packages/player-core/src/components/PhotoLayer.svelte b/packages/player-core/src/components/PhotoLayer.svelte deleted file mode 100644 index 8e6ac30..0000000 --- a/packages/player-core/src/components/PhotoLayer.svelte +++ /dev/null @@ -1,341 +0,0 @@ - - -{#if previous} - -{/if} - - - -{#each lights as light (light.id)} -
-{/each} - -{#if visible && hotspot} - (hovered = value)} - /> -{/if} - -{#each bursts as burst (burst.id)} - -{/each} - -{#if visible && tooltip} - (hovered = value)} - /> -{/if} - - diff --git a/packages/player-core/src/components/Player.svelte b/packages/player-core/src/components/Player.svelte deleted file mode 100644 index 9b54039..0000000 --- a/packages/player-core/src/components/Player.svelte +++ /dev/null @@ -1,538 +0,0 @@ - - - - -
-
- {#if demo.theme.wrapper === 'browser'} - goTo(0)} /> - {/if} - - -
-
- - diff --git a/packages/player-core/src/components/Tooltip.svelte b/packages/player-core/src/components/Tooltip.svelte deleted file mode 100644 index 3544630..0000000 --- a/packages/player-core/src/components/Tooltip.svelte +++ /dev/null @@ -1,253 +0,0 @@ - - -
onclick?.()} - onkeydown={(event) => (event.key === 'Enter' || event.key === ' ') && onclick?.()} - onmouseenter={() => onhoverchange?.(true)} - onmouseleave={() => onhoverchange?.(false)} - out:vanish ->{shown.text}
- - diff --git a/packages/player-core/src/components/VideoLayer.svelte b/packages/player-core/src/components/VideoLayer.svelte deleted file mode 100644 index dfd71f6..0000000 --- a/packages/player-core/src/components/VideoLayer.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/packages/player-core/src/components/types.ts b/packages/player-core/src/components/types.ts deleted file mode 100644 index 727c2f0..0000000 --- a/packages/player-core/src/components/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Shared prop/data shapes for components. Kept in a plain .ts module rather than exported - * from a .svelte file — TypeScript's ambient `*.svelte` module shim only recognizes the - * default export, not named exports from the component's `