Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,33 @@ fonts, vectors and core masks at Vita's native 960x544 density. Physical
controls, left-analog input, and front-panel multi-touch snapshots are supported;
PSP builds retain their controller-only fallback.

The experimental Kindle Paperwhite 5 host uses a complete Kindle build source
tree: either a git checkout or an unpacked `@pocketjs/framework` npm tarball
followed by `bun install --frozen-lockfile` at its root. The published tarball
includes the Kindle docs, Hero/Paper Ink examples, assets, native host, and
device tools; the standalone zero-dependency `@pocketjs/cli` is not that source
tree.
It renders the 309×412 logical viewport at the panel's native 1236×1648
resolution, accepts multitouch through evdev, and uses an external FBInk
executable for bounded DU/A2 partial refresh followed by high-quality cleanup.
The complete jailbreak, USB SSH, build/deploy/reload, recovery, and real-device
verification workflow is in [docs/KINDLE.md](docs/KINDLE.md).

```sh
# One-time host and attached-device setup while /Volumes/Kindle is mounted:
bun run kindle:setup
bun run kindle:bootstrap --volume /Volumes/Kindle

# Safely eject the volume, keep the cable attached, start "PocketJS Dev -
# Start USB SSH" on the Kindle, then configure/check the new USB interface:
bun run kindle:usbnet --check
# If the doctor names an unconfigured enN:
# bun run kindle:usbnet --interface enN
bun run kindle probe
bun pocket build --target kindle-pw5 --manifest apps/hero/pocket.kindle.json --project-root .
bun run kindle dev --plan=.pocket/kindle-pw5/plan.json --skip-build --skip-native
```

## The Pocket Launcher (on-device app switching)

One PSP EBOOT or Vita VPK can embed every app admitted by that target plus a
Expand Down
145 changes: 116 additions & 29 deletions apps/hero/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Uses all three public primitives, class literals, a dynamic style object,
// focus + onPress, and a signal in text — the exact surface phase v1 supports.

import { createSignal, onMount, Show } from "solid-js";
import { createSignal, onCleanup, onMount, Show } from "solid-js";
import { Image, Text, View, type NodeMirror } from "@pocketjs/framework/components";
import { animate } from "@pocketjs/framework/animation";
import { enableTouchPress } from "@pocketjs/framework/input";
import { createSpriteAnimation } from "@pocketjs/framework/lifecycle";
import { frameworkName } from "@pocketjs/framework";
import { platform } from "@pocketjs/framework/platform";
import { frameworkName } from "@pocketjs/framework/solid";

const SPINNER_FRAME_STEP = 3;
const SPINNER_FRAMES = [
Expand All @@ -30,61 +32,146 @@ function Stat(props: { label: string; value: string; cls: string }) {
}

export default function Hero() {
const isKindle = platform.target === "kindle-pw5";
const [count, setCount] = createSignal(0);
const spinnerSrc = createSpriteAnimation(SPINNER_FRAMES, { frameStep: SPINNER_FRAME_STEP });
const disableTouchPress = isKindle ? enableTouchPress() : undefined;
onCleanup(() => disableTouchPress?.());
const spinnerSrc = isKindle
? () => SPINNER_FRAMES[0]
: createSpriteAnimation(SPINNER_FRAMES, { frameStep: SPINNER_FRAME_STEP });
let underline: NodeMirror | undefined;
onMount(() => {
// Underline sweeps in once on mount — native tween, zero steady-state JS.
if (underline) animate(underline, "width", 210, { dur: 700, easing: "out", delay: 150 });
// Kindle keeps it static so the panel reaches its quiet cleanup window.
if (!isKindle && underline) {
animate(underline, "width", 210, { dur: 700, easing: "out", delay: 150 });
}
});
return (
<View
debugName="HeroScreen"
class="w-full h-full flex-col justify-between p-5 bg-gradient-to-b from-slate-50 to-slate-100"
class={isKindle
? "w-full h-full flex-col justify-between p-5 bg-white"
: "w-full h-full flex-col justify-between p-5 bg-gradient-to-b from-slate-50 to-slate-100"}
>
<View debugName="Header" class="flex-row flex-wrap items-center justify-between">
<View
debugName="Header"
class={isKindle
? "flex-col gap-4"
: "flex-row flex-wrap items-center justify-between"}
>
<View class="flex-row items-center gap-3">
<Image class="w-10 h-10 rounded-lg shadow" src="logo.png" />
<Image class="w-10 h-10 rounded-lg shadow" src="logo.png" />
<View class="flex-col">
<Text class="text-base text-slate-950 font-bold tracking-wide">PocketJS</Text>
<Text class="text-xs text-slate-500 tracking-wide">{frameworkName()} + RUST + SCEGU</Text>
<Text class="text-xs text-slate-500 tracking-wide">
{isKindle ? `${frameworkName()} + RUST + FBINK` : `${frameworkName()} + RUST + SCEGU`}
</Text>
</View>
</View>
<View class="flex-row gap-4">
<Stat label="FPS" value="60" cls="text-lg text-emerald-600 font-bold" />
<Stat label="NODES" value="42" cls="text-lg text-blue-600 font-bold" />
<Stat label="DRAWS" value="9" cls="text-lg text-amber-600 font-bold" />
<View class={isKindle ? "w-full flex-row justify-between" : "flex-row gap-4"}>
<Stat
label={isKindle ? "LOGIC" : "FPS"}
value="60"
cls={isKindle
? "text-lg text-slate-950 font-bold"
: "text-lg text-emerald-600 font-bold"}
/>
<Stat
label={isKindle ? "WAVE" : "NODES"}
value={isKindle ? "DU" : "42"}
cls={isKindle
? "text-lg text-slate-950 font-bold"
: "text-lg text-blue-600 font-bold"}
/>
<Stat
label="DRAWS"
value="9"
cls={isKindle
? "text-lg text-slate-950 font-bold"
: "text-lg text-amber-600 font-bold"}
/>
</View>
</View>

<View class="flex-col gap-2">
<Text class="text-xs text-blue-600 tracking-wide">ONE RUST CORE · ONE JSX APP</Text>
<View class="flex-row flex-wrap items-center justify-between">
<Text class="text-4xl text-slate-950 font-bold">JSX at 60 FPS.</Text>
<Image class="w-10 h-10" src={spinnerSrc()} />
</View>
<View class={isKindle ? "flex-col gap-4" : "flex-col gap-2"}>
<Text
class={isKindle
? "text-xs text-slate-950 tracking-wide"
: "text-xs text-blue-600 tracking-wide"}
>
ONE RUST CORE · ONE JSX APP
</Text>
<View
ref={underline}
class="h-1 w-0 rounded-full shadow bg-gradient-to-r from-blue-500 to-cyan-500"
style={{ translateX: count() * 2 }}
/>
<View debugName="Description" class="flex-row flex-wrap gap-1">
<Text class="text-sm text-slate-600">Flexbox, springs and baked type —</Text>
<Text class="text-sm text-slate-600">running on a 2005 handheld.</Text>
class={isKindle
? "flex-col gap-2"
: "flex-row flex-wrap items-center justify-between"}
>
<Text
class={isKindle
? "text-3xl text-slate-950 font-bold"
: "text-4xl text-slate-950 font-bold"}
>
{isKindle ? "JSX on e-ink." : "JSX at 60 FPS."}
</Text>
<Show
when={isKindle}
fallback={<Image class="w-10 h-10" src={spinnerSrc()} />}
>
<View class="self-start px-3 py-1 rounded-lg border-[1] border-slate-950 bg-white">
<Text class="text-xs text-slate-950 font-bold tracking-wide">E-INK NATIVE</Text>
</View>
</Show>
</View>
<Show
when={isKindle}
fallback={
<View
ref={underline}
class="h-1 w-0 rounded-full shadow bg-gradient-to-r from-blue-500 to-cyan-500"
style={{ translateX: count() * 2 }}
/>
}
>
<View class="w-full h-1 bg-slate-950" />
</Show>
<Show
when={isKindle}
fallback={
<View debugName="Description" class="flex-row flex-wrap gap-1">
<Text class="text-sm text-slate-600">Flexbox, springs and baked type —</Text>
<Text class="text-sm text-slate-600">running on a 2005 handheld.</Text>
</View>
}
>
<View debugName="Description" class="flex-col gap-1">
<Text class="text-sm text-slate-700">Flexbox + Solid signals.</Text>
<Text class="text-sm text-slate-700">Rendered through Rust + FBInk.</Text>
</View>
</Show>
</View>

<View class="flex-row flex-wrap items-center gap-4">
<View class={isKindle ? "flex-col gap-3" : "flex-row flex-wrap items-center gap-4"}>
<View
class="px-4 py-2 rounded-xl shadow-md bg-blue-600 border-blue-500 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150"
class={isKindle
? "w-full h-[52] flex-row items-center justify-center rounded-xl border-[2] border-slate-950 bg-slate-950 focus:bg-slate-800 active:bg-slate-600"
: "px-4 py-2 rounded-xl shadow-md bg-blue-600 border-blue-500 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150"}
focusable
onPress={() => setCount(count() + 1)}
>
<Text class="text-base text-white font-bold">Press Circle</Text>
<Text class="text-base text-white font-bold">
{isKindle ? "Tap to increment" : "Press Circle"}
</Text>
</View>
<Text class="text-sm text-slate-600">Count: {count()}</Text>
<Show when={count() > 3}>
<Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
<Text
class={isKindle
? "text-sm text-slate-950 font-bold"
: "text-sm text-emerald-600"}
>
{isKindle ? "Reactive on e-ink hardware." : "Reactive on real hardware."}
</Text>
</Show>
</View>
</View>
Expand Down
2 changes: 1 addition & 1 deletion apps/hero/main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @title PocketJS: Hero
import Hero from "./app.tsx";
import { mount } from "@pocketjs/framework";
import { mount } from "@pocketjs/framework/solid";

mount(() => <Hero />);
30 changes: 30 additions & 0 deletions apps/hero/pocket.kindle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://pocketjs.dev/schema/pocket-2.json",
"pocket": 2,
"id": "dev.pocket-stack.hero-kindle",
"name": "pocketjs-hero-kindle",
"title": "PocketJS: Hero for Kindle",
"version": "0.6.0",
"engine": {
"capabilities": {
"requires": [
"input.touch",
"text.glyphs.baked"
]
}
},
"app": {
"entry": "apps/hero/main.tsx",
"output": "hero-kindle-main",
"framework": "solid",
"viewport": {
"fixed": {
"logical": [
309,
412
],
"presentation": "native"
}
}
}
}
104 changes: 104 additions & 0 deletions apps/paper-ink/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { For, createSignal } from "solid-js";
import { Text, View } from "@pocketjs/framework/components";
import { onFrame } from "@pocketjs/framework/lifecycle";
import { touches } from "@pocketjs/framework/input";

interface InkPoint {
readonly id: number;
readonly x: number;
readonly y: number;
readonly shade: string;
readonly size: number;
}

const TRAIL_SHADES = ["#161616", "#303030", "#505050", "#747474", "#9a9a9a", "#bcbcbc"];
const TRAIL_SIZES = [14, 12, 10, 8, 7, 6];

export default function PaperInk() {
const [points, setPoints] = createSignal<readonly InkPoint[]>([]);
const [samples, setSamples] = createSignal(0);
let nextId = 1;
let previous = "";

onFrame(() => {
const contact = touches()[0];
if (!contact) {
previous = "";
return;
}
const key = `${contact.x}:${contact.y}`;
if (key === previous) return;
previous = key;
setSamples((value) => value + 1);
setPoints((current) => {
const head = {
id: nextId++,
x: contact.x,
y: contact.y,
shade: TRAIL_SHADES[0],
size: TRAIL_SIZES[0],
};
return [head, ...current.slice(0, TRAIL_SHADES.length - 1)].map((point, index) => ({
...point,
shade: TRAIL_SHADES[index],
size: TRAIL_SIZES[index],
}));
});
});

return (
<View
debugName="PaperInkLab"
class="relative w-full h-full overflow-hidden"
style={{ bgColor: "#f4f1e8" }}
>
<View class="absolute left-[18] top-[18] right-[18] flex-col gap-1">
<Text class="text-xl font-bold" style={{ textColor: "#161616" }}>
PAPER / INK
</Text>
<Text class="text-xs tracking-wide" style={{ textColor: "#66625b" }}>
TOUCH TO DRAW A PARTIAL-REFRESH TRACE
</Text>
<View class="mt-2 w-full h-[1]" style={{ bgColor: "#b8b2a7" }} />
</View>

<For each={[112, 152, 192, 232, 272, 312, 352]}>
{(top) => (
<View
class="absolute left-[18] right-[18] h-[1]"
style={{ insetT: top, bgColor: "#d8d2c7" }}
/>
)}
</For>

<For each={points()}>
{(point) => (
<View
debugName="InkPoint"
class="absolute"
style={{
insetL: point.x - point.size / 2,
insetT: point.y - point.size / 2,
width: point.size,
height: point.size,
borderRadius: point.size / 2,
bgColor: point.shade,
}}
/>
)}
</For>

<View
class="absolute left-[18] right-[18] bottom-[18] flex-row justify-between items-center px-3 py-2 border-[1]"
style={{ bgColor: "#ebe7dd", borderColor: "#b8b2a7" }}
>
<Text class="text-xs font-bold" style={{ textColor: "#2c2a27" }}>
AUTO PARTIAL
</Text>
<Text class="text-xs" style={{ textColor: "#66625b" }}>
SAMPLES {samples()}
</Text>
</View>
</View>
);
}
5 changes: 5 additions & 0 deletions apps/paper-ink/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @title PocketJS: Paper Ink
import { mount } from "@pocketjs/framework/solid";
import PaperInk from "./app.tsx";

mount(() => <PaperInk />);
Loading
Loading