From baecb21a9e5364e0b9191b7822175e758456245c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 16:00:25 +0000 Subject: [PATCH 1/2] ci: pin toolchain with mise and fix pnpm setup failure CI was failing at the "Setup pnpm" step on every run: Error: No pnpm version is specified. pnpm/action-setup@v4 requires the pnpm version to come from either its `version` input or package.json's `packageManager` field, and neither was present. Fixes and improvements: - Add mise.toml pinning exact LTS versions (node 24.18.0, pnpm 10.34.4) as the single source of truth for local dev and CI. - Add `packageManager` and `engines.node` to package.json. - Drive CI tool versions from mise.toml via jdx/mise-action; apply the same setup to the release workflow, which had the identical bug. - Cache the pnpm store and cancel superseded runs (concurrency). - Add a Prettier format check (`format:check` script) to CI, with a .prettierignore so generated files (lockfile, dist) are skipped. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018JWDajbftveEqqZWcrFH5n --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++------ .github/workflows/release.yml | 17 +++++++++++------ .prettierignore | 5 +++++ mise.toml | 5 +++++ package.json | 7 ++++++- 5 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 .prettierignore create mode 100644 mise.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82e9687..8e10169 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,11 @@ on: pull_request: branches: [main] +# Cancel superseded runs on the same ref to save CI minutes. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -13,18 +18,30 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 + # Installs Node and pnpm at the exact versions pinned in mise.toml. + - name: Setup toolchain (mise) + uses: jdx/mise-action@v2 + with: + cache: true + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV" - - name: Setup Node - uses: actions/setup-node@v4 + - name: Cache pnpm store + uses: actions/cache@v4 with: - node-version: 24 - cache: pnpm + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Format check + run: pnpm format:check + - name: Typecheck run: pnpm typecheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1023f2a..338823a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,13 +17,18 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 + # Installs Node and pnpm at the exact versions pinned in mise.toml. + - name: Setup toolchain (mise) + uses: jdx/mise-action@v2 with: - node-version: 24 - cache: pnpm - registry-url: https://registry.npmjs.org + cache: true + + # Configure npm auth for changesets publish. + - name: Configure npm registry + run: | + echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: pnpm install --frozen-lockfile diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f33ebba --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# Generated / vendored — never reformat. +dist +node_modules +pnpm-lock.yaml +CHANGELOG.md diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..2052c6f --- /dev/null +++ b/mise.toml @@ -0,0 +1,5 @@ +# Pinned toolchain — single source of truth for local dev and CI. +# Exact versions keep every environment byte-for-byte reproducible. +[tools] +node = "24.18.0" # latest Node.js 24 LTS (Krypton) +pnpm = "10.34.4" # latest pnpm 10.x (lockfileVersion 9.0) diff --git a/package.json b/package.json index efba171..33e15dd 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,10 @@ "video-player" ], "type": "module", + "packageManager": "pnpm@10.34.4", + "engines": { + "node": ">=24" + }, "sideEffects": [ "**/*.css", "./dist/mux.js" @@ -46,7 +50,8 @@ "test:watch": "vitest", "typecheck": "tsc --noEmit", "lint": "eslint .", - "format": "prettier --write ." + "format": "prettier --write .", + "format:check": "prettier --check ." }, "peerDependencies": { "react": ">=19", From a384d08c74191a5d2a30bceeb119214eb7275082 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 16:00:29 +0000 Subject: [PATCH 2/2] style: normalize formatting with prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the repo's Prettier config (semi: false) across the codebase so the new CI format check passes. Purely cosmetic — no behavioral changes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018JWDajbftveEqqZWcrFH5n --- README.md | 42 +- demo/file-provider.ts | 4 +- .../plans/2026-06-25-kino-video-player.md | 675 ++++++++++++++---- .../2026-06-25-kino-video-player-design.md | 4 +- src/core/fake-provider.ts | 40 +- src/core/store.test.tsx | 19 +- src/core/store.tsx | 10 +- src/index.ts | 7 +- src/mux/urls.test.ts | 8 +- src/mux/urls.ts | 2 +- src/ui/buttons.tsx | 10 +- src/ui/control-bar.test.tsx | 12 +- src/ui/idle-overlay.test.tsx | 4 +- src/ui/menus.test.tsx | 6 +- src/ui/menus.tsx | 8 +- src/ui/player.test.tsx | 2 +- src/ui/rolling-time.tsx | 2 +- src/ui/scrubber.test.tsx | 6 +- src/ui/settings-sheet.test.tsx | 8 +- src/ui/tooltip.tsx | 5 +- src/util/storyboard.test.ts | 7 +- src/util/storyboard.ts | 5 +- 22 files changed, 678 insertions(+), 208 deletions(-) diff --git a/README.md b/README.md index bf63aba..0e14320 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,18 @@ The quickest knob is the `accentColor` prop, which drives the scrubber fill, act For deeper control, every visual is driven by CSS custom properties on the `.kino` root. Override them in your own stylesheet, or pass a `theme` object of property/value pairs to set them inline. -| Custom property | Default | Role | -| --- | --- | --- | -| `--kino-accent` | `oklch(50.8% 0.118 165.612)` | Accent color (progress, active items, ranges) | -| `--kino-radius` | `12px` | Corner radius of glass surfaces | -| `--kino-surface` | `color-mix(in oklab, black 55%, transparent)` | Glass surface fill | -| `--kino-surface-strong` | `color-mix(in oklab, black 70%, transparent)` | Stronger surface (idle play button) | -| `--kino-border` | `color-mix(in oklab, white 14%, transparent)` | Hairline borders | -| `--kino-text` | `oklch(98% 0 0)` | Primary text and icons | -| `--kino-text-dim` | `color-mix(in oklab, white 65%, transparent)` | Secondary text (timecode) | -| `--kino-blur` | `18px` | Backdrop blur radius | -| `--kino-shadow` | `0 8px 40px rgba(0, 0, 0, 0.45)` | Surface drop shadow | -| `--kino-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | Shared transition easing | +| Custom property | Default | Role | +| ----------------------- | --------------------------------------------- | --------------------------------------------- | +| `--kino-accent` | `oklch(50.8% 0.118 165.612)` | Accent color (progress, active items, ranges) | +| `--kino-radius` | `12px` | Corner radius of glass surfaces | +| `--kino-surface` | `color-mix(in oklab, black 55%, transparent)` | Glass surface fill | +| `--kino-surface-strong` | `color-mix(in oklab, black 70%, transparent)` | Stronger surface (idle play button) | +| `--kino-border` | `color-mix(in oklab, white 14%, transparent)` | Hairline borders | +| `--kino-text` | `oklch(98% 0 0)` | Primary text and icons | +| `--kino-text-dim` | `color-mix(in oklab, white 65%, transparent)` | Secondary text (timecode) | +| `--kino-blur` | `18px` | Backdrop blur radius | +| `--kino-shadow` | `0 8px 40px rgba(0, 0, 0, 0.45)` | Surface drop shadow | +| `--kino-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | Shared transition easing | ```css .kino { @@ -70,15 +70,15 @@ For deeper control, every visual is driven by CSS custom properties on the `.kin The player is keyboard-first. Shortcuts are ignored while a text input, textarea, select, or contenteditable element is focused, and modifier-key combinations (Ctrl/Cmd/Alt) are passed through. -| Key | Action | -| --- | --- | -| `Space` / `K` | Play / pause | -| `<` / `>` | Decrease / increase playback rate (0.25 step) | -| `M` | Toggle mute | -| `C` | Toggle captions | -| `S` | Open the speed menu | -| `F` | Toggle fullscreen | -| `0`-`9` | Seek to 0%-90% of the duration | +| Key | Action | +| ------------- | --------------------------------------------- | +| `Space` / `K` | Play / pause | +| `<` / `>` | Decrease / increase playback rate (0.25 step) | +| `M` | Toggle mute | +| `C` | Toggle captions | +| `S` | Open the speed menu | +| `F` | Toggle fullscreen | +| `0`-`9` | Seek to 0%-90% of the duration | ## Capability gating diff --git a/demo/file-provider.ts b/demo/file-provider.ts index 39ab0ed..66dc29e 100644 --- a/demo/file-provider.ts +++ b/demo/file-provider.ts @@ -16,7 +16,9 @@ export function createFileProvider(src: string, poster?: string): Provider { capabilities: { canSetQuality: false, hasStoryboard: false, - canPiP: typeof document !== "undefined" && "pictureInPictureEnabled" in document, + canPiP: + typeof document !== "undefined" && + "pictureInPictureEnabled" in document, canFullscreen: true, canSetRate: true, hasTextTracks: false, diff --git a/docs/superpowers/plans/2026-06-25-kino-video-player.md b/docs/superpowers/plans/2026-06-25-kino-video-player.md index 560944e..0f39bff 100644 --- a/docs/superpowers/plans/2026-06-25-kino-video-player.md +++ b/docs/superpowers/plans/2026-06-25-kino-video-player.md @@ -25,15 +25,15 @@ These types are defined in Task 2 (`src/core/types.ts`) and referenced verbatim ```ts export type QualityLevel = { - id: string // rendition id from the engine - height: number // e.g. 1080 - bitrate: number // bits/sec - selected: boolean // currently the pinned manual selection + id: string // rendition id from the engine + height: number // e.g. 1080 + bitrate: number // bits/sec + selected: boolean // currently the pinned manual selection } export type TextTrackInfo = { id: string - kind: string // "subtitles" | "captions" | ... + kind: string // "subtitles" | "captions" | ... label: string lang: string mode: "showing" | "hidden" | "disabled" @@ -43,7 +43,7 @@ export type Capabilities = { canSetQuality: boolean hasStoryboard: boolean canPiP: boolean - canFullscreen: boolean // custom-chrome fullscreen (false on iPhone) + canFullscreen: boolean // custom-chrome fullscreen (false on iPhone) canSetRate: boolean hasTextTracks: boolean } @@ -54,11 +54,11 @@ export type MediaState = { paused: boolean currentTime: number duration: number - buffered: Array<[number, number]> // [start, end] seconds + buffered: Array<[number, number]> // [start, end] seconds rate: number - volume: number // 0..1 + volume: number // 0..1 muted: boolean - readyState: number // HTMLMediaElement.readyState + readyState: number // HTMLMediaElement.readyState seeking: boolean ended: boolean error: MediaError | null @@ -138,11 +138,13 @@ kino/ (repo root /Users/karn/code/karnstack/kin ### Task 1: Repo scaffold, tooling, and theme stylesheet **Files:** + - Create: `package.json`, `tsconfig.json`, `tsup.config.ts`, `vitest.config.ts`, `eslint.config.js`, `.prettierrc`, `.gitignore`, `LICENSE`, `README.md` - Create: `src/index.ts`, `src/mux.ts`, `src/styles/kino.css` - Create: `src/util/format-time.ts`, `src/util/format-time.test.ts` **Interfaces:** + - Produces: a buildable/testable package; `formatTime(seconds: number): string`. - [ ] **Step 1: Create the repo and pnpm package** @@ -290,9 +292,16 @@ import "@testing-library/jest-dom/vitest" background: black; overflow: hidden; } -.kino * { box-sizing: border-box; } +.kino * { + box-sizing: border-box; +} .kino mux-video, -.kino video { position: absolute; inset: 0; width: 100%; height: 100%; } +.kino video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} .kino-glass { background: var(--kino-surface); backdrop-filter: blur(var(--kino-blur)); @@ -302,7 +311,11 @@ import "@testing-library/jest-dom/vitest" box-shadow: var(--kino-shadow); } @media (prefers-reduced-motion: reduce) { - .kino *, .kino *::before, .kino *::after { transition-duration: 0.01ms !important; } + .kino *, + .kino *::before, + .kino *::after { + transition-duration: 0.01ms !important; + } } ``` @@ -363,10 +376,12 @@ git commit -m "chore: scaffold @karnstack/kino package, tooling, theme css" ### Task 2: Core types and the player store **Files:** + - Create: `src/core/types.ts` (the Shared Type Contract verbatim) - Create: `src/core/fake-provider.ts`, `src/core/store.tsx`, `src/core/store.test.tsx` **Interfaces:** + - Consumes: nothing. - Produces: - `PlayerContext` (React context holding a `Provider`) @@ -382,17 +397,35 @@ git commit -m "chore: scaffold @karnstack/kino package, tooling, theme css" import type { MediaState, Provider, PlayerActions } from "./types" const DEFAULT_CAPS = { - canSetQuality: true, hasStoryboard: false, canPiP: true, - canFullscreen: true, canSetRate: true, hasTextTracks: false, + canSetQuality: true, + hasStoryboard: false, + canPiP: true, + canFullscreen: true, + canSetRate: true, + hasTextTracks: false, } export function defaultState(): MediaState { return { - paused: true, currentTime: 0, duration: 0, buffered: [], - rate: 1, volume: 1, muted: false, readyState: 0, seeking: false, - ended: false, error: null, qualities: [], activeQualityId: "auto", - textTracks: [], activeTextTrackId: null, fullscreen: false, pip: false, - storyboard: null, capabilities: { ...DEFAULT_CAPS }, + paused: true, + currentTime: 0, + duration: 0, + buffered: [], + rate: 1, + volume: 1, + muted: false, + readyState: 0, + seeking: false, + ended: false, + error: null, + qualities: [], + activeQualityId: "auto", + textTracks: [], + activeTextTrackId: null, + fullscreen: false, + pip: false, + storyboard: null, + capabilities: { ...DEFAULT_CAPS }, } } @@ -421,7 +454,10 @@ export function createFakeProvider(initial?: Partial) { const provider: Provider = { mount: () => {}, getState: () => state, - subscribe: (l) => { listeners.add(l); return () => listeners.delete(l) }, + subscribe: (l) => { + listeners.add(l) + return () => listeners.delete(l) + }, actions, destroy: () => listeners.clear(), } @@ -452,7 +488,7 @@ test("useMediaSelector re-renders only when the selected slice changes", () => { render( - + , ) const before = renders act(() => provider.set({ currentTime: 5 })) // unrelated slice @@ -469,8 +505,9 @@ test("usePlayer exposes actions that drive state", () => { } render( - + +