cancado-ios4
v0.0.1
Published
An iOS 4 (2010) component kit for React — springboard, app chrome, navigation and the mobile-first primitives to build screens inside them
Maintainers
Readme
cancado-ios4
An iOS 4 (2010) component kit for React — a full device shell (lock screen, springboard, app chrome, navigation stack, multitasking tray) plus the mobile-first primitives to build screens that live inside it.
Skeuomorphic on purpose: glossy icons, pinstriped table backgrounds, the blue-grey navigation bar, slide-to-unlock.
pnpm add cancado-ios4Quick start
import { IosShell, ScrollView, TableView, TableSection, TableRow } from 'cancado-ios4';
import type { IosApp } from 'cancado-ios4';
const apps: IosApp[] = [
{
id: 'settings',
title: 'Settings',
icon: '⚙️',
iconBackground: 'linear-gradient(to bottom, #b8bec7 0%, #6b7480 100%)',
render: ({ push }) => (
<ScrollView grouped>
<TableView>
<TableSection header="General">
<TableRow
title="About"
onPress={() => push({ title: 'About', render: () => <p>iOS 4.3.5</p> })}
/>
</TableSection>
</TableView>
</ScrollView>
),
},
{ id: 'phone', title: 'Phone', icon: '📞', dock: true, render: () => null },
];
export function App() {
return <IosShell apps={apps} />;
}One registry drives everything: the springboard icon, the dock, the navigation
title and the screen content all come from the same IosApp.
Mobile-first
The shell fills whatever box it's given and defaults to the viewport — a real phone gets the real UI, not a picture of a phone. Everything else follows:
- Touch targets are 44pt minimum, the iOS floor
- Gestures are pointer-events based, so they work with touch, mouse and stylus
touch-action,-webkit-overflow-scrollingandoverscroll-behaviorare set so flicks scroll the right thing and never rubber-band the page behindenv(safe-area-inset-*)is respected when installed as a web app- Tap highlight and text-size inflation are disabled
Pass bezel to draw a phone frame instead — that's for showing the UI on a
desktop page, not for shipping to phones.
Architecture
The package is layered, and each layer is importable on its own.
| Layer | Path | What it is |
| --- | --- | --- |
| core/ | cancado-ios4/core | Pure TypeScript: the device reducer, springboard layout maths, the app-registry model. No React, no DOM, no CSS. |
| hooks/ | cancado-ios4/hooks | React bindings: useIosRuntime, usePointerDrag, useLongPress, useDismiss, useElementSize, useClock. |
| primitives/ | cancado-ios4/primitives | Buttons, table views, switches, segmented controls, fields, indicators. |
| components/ | cancado-ios4/IosShell, /Springboard, … | The shell pieces. Every one is presentational and controlled. |
State transitions live in core as a pure reducer, so device behaviour is
exercisable without rendering anything.
The runtime
useIosRuntime() is usable on its own if you want the behaviour without the
chrome:
import { useIosRuntime } from 'cancado-ios4/runtime';
const rt = useIosRuntime();
rt.launch('music', 'songs');
rt.push({ title: 'Now Playing', render: () => <Player /> });
rt.openSwitcher();Pass it to <IosShell runtime={rt} /> to drive the device from outside — a
router, a demo script, a test.
The reducer is exported too:
import { runtimeReducer, createRuntimeState } from 'cancado-ios4/core';
const state = runtimeReducer(createRuntimeState(), {
type: 'launch',
appId: 'settings',
});It models iOS 4 multitasking faithfully: launching an app doesn't destroy the previous one, going Home leaves apps running with their navigation stacks intact, and only the tray actually terminates an app.
Talking to the device from inside a screen
Every render receives a context:
render: ({ push, pop, selectTab, openApp, home, params }) => (
<TableRow
title="Wi-Fi"
onPress={() => push({ title: 'Wi-Fi', backTitle: 'Settings', render: WifiScreen })}
/>
)Anywhere deeper in the tree, useDevice() gives the same handles.
Components
| Component | Role |
| --- | --- |
| IosShell | The whole device — composes everything below |
| LockScreen | Wallpaper, clock, working slide-to-unlock |
| Springboard | Paged icon grid, page dots, dock, drag-to-rearrange |
| AppIcon | Rounded tile with baked-in gloss, badge, jiggle |
| AppScreen | An app's chrome: nav bar, content, toolbar, tab bar |
| NavigationBar / BarButton | The 44pt bar and its pointed back button |
| TabBar | The 49pt dark tab bar |
| StatusBar | Signal, carrier, live clock, battery |
| MultitaskingTray | Double-press Home — the recent-apps tray |
Gestures
Three gestures share the springboard without fighting:
- Swipe pages the grid — active only when not rearranging
- Long press enters rearrange mode — never captures the pointer and cancels on movement, so it coexists with the swipe
- Drag reorders icons — active only while rearranging
Paging commits on either distance (25% of the screen) or flick velocity, whichever lands first, and rubber-bands at the ends.
Theming
Every colour and metric is a CSS custom property (see src/styles/tokens.css):
:root {
--ios4-navbar-top: #c6a8b4;
--ios4-navbar-bottom: #996d7f;
--ios4-tint: #b8336a;
}Styles
Component subpaths inject their own CSS automatically. For everything at once:
import 'cancado-ios4/styles'; // all components
import 'cancado-ios4/tokens'; // just the variablesDevelopment
pnpm install
pnpm storybook # component gallery + a live device
pnpm typecheck
pnpm buildLicense
MIT
