Files
zeekay fb9cc40f69 Add the iOS lane: hanzo-templates had zero of them
The catalogue shipped 71 web templates and one EMPTY `mobile` repo, so
"build an app" had no starting point on the platform people actually
ship to. This is the iOS lane — Expo/React Native and Flutter for
cross-platform, SwiftUI for native.

The constraint that shaped every file here: an iOS archive needs macOS +
Xcode + a signing identity, and our arc fleet has none. hanzoai has 519
registered runners and every one reports `os: linux`; luxfi has 151, same
story; zooai has none. The only macOS builds in the org today
(hanzoai/world build-desktop.yml) run on GitHub-HOSTED macos-14, which is
exactly what the house CI rule says not to do.

So `hanzo.yml` declares only what a Linux arc runner can genuinely
execute — typecheck + web bundle for Expo, analyze + test + web for
Flutter, `swiftc -parse` for SwiftUI — and the macOS archive lives in a
separate `workflow_dispatch`-only `ios.yml`. A job pinned to a label no
runner advertises would sit queued forever if it fired on push; parking
it is honest, a permanently-yellow check is not. The README of every
template states the gap and the one command that closes it.

Same reasoning on the demos. Expo and Flutter both have a real web
target, so `<slug>.hanzo.app` serves the SAME source the simulator runs.
SwiftUI has no web target, so those two templates ship with no demo link
rather than a screenshot pretending to be one.

Upstreams are permissive only and recorded per template: expo/expo
templates (MIT), flutter/samples (BSD-3-Clause), XcodeGen (MIT).

Second measured gap, found while wiring this up: `hanzo-templates` is not in
arcd's `orgs:` list either (hanzoai, luxfi, zooai, parsdao, zenlm), so the
Linux jobs would queue with no runner too. Both gaps are one installer
re-run, no code change; both are spelled out in every README rather than
left to be discovered on the first red build.

Third gap, found by probing rather than assuming, and FIXED here: every
run in this org died as `startup_failure` with no logs and no annotation.
A bare `runs-on: [self-hosted, linux]` probe queued fine while the same
job importing hanzoai/ci failed identically at @v1 and at a pinned SHA —
so it was never the tag. The org defaulted GITHUB_TOKEN to `read`, and
the reusable declares `permissions: packages: write`; a caller cannot
exceed the org maximum, so GitHub kills the run before creating a job.
hanzo-templates now defaults to `write` like hanzoai/luxfi/zooai, and
runs reach `queued` — the honest state, waiting on gap 2.
2026-07-27 23:24:47 -07:00

40 lines
1.8 KiB
TypeScript

/** Catalog + cart. The catalog is seeded locally so the template runs with no
* backend; point `load()` at your own /v1/commerce/products and the screens
* do not change — they only depend on the Product shape. */
import { useSyncExternalStore } from 'react';
export type Product = { id: string; name: string; price: number; blurb: string; hue: number };
export const products: Product[] = [
{ id: 'aero', name: 'Aero Bottle', price: 34, hue: 262, blurb: 'Vacuum steel, 24h cold, 700ml.' },
{ id: 'field', name: 'Field Tote', price: 88, hue: 200, blurb: 'Waxed canvas, leather handles.' },
{ id: 'lume', name: 'Lume Lamp', price: 149, hue: 34, blurb: 'Warm dimmable LED, USB-C.' },
{ id: 'terra', name: 'Terra Mug', price: 22, hue: 14, blurb: 'Stoneware, reactive glaze, 350ml.' },
{ id: 'kite', name: 'Kite Shell', price: 210, hue: 150, blurb: '3-layer waterproof, taped seams.' },
{ id: 'nova', name: 'Nova Deck', price: 65, hue: 300, blurb: '52 cards, foil edges, linen finish.' },
];
export const byId = (id: string) => products.find((p) => p.id === id);
export const money = (cents: number) => `$${cents.toFixed(0)}`;
// Minimal observable store — id → qty. No state library for one Record.
let cart: Record<string, number> = {};
const subs = new Set<() => void>();
const snap = () => cart;
export const useCart = () =>
useSyncExternalStore((f) => (subs.add(f), () => subs.delete(f)), snap, snap);
export const add = (id: string, n = 1) => {
const next = { ...cart, [id]: (cart[id] ?? 0) + n };
if (next[id] <= 0) delete next[id];
cart = next;
subs.forEach((f) => f());
};
export const count = (c: Record<string, number>) =>
Object.values(c).reduce((a, b) => a + b, 0);
export const total = (c: Record<string, number>) =>
Object.entries(c).reduce((sum, [id, q]) => sum + (byId(id)?.price ?? 0) * q, 0);