diff --git a/CLAUDE.md b/CLAUDE.md index 7515baa..952498d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,35 +6,35 @@ Svelte 5 + TypeScript + Vite. Public repository. ## Where things are -| Need | Look in | -| ---------------------------------------------- | ------------------------------------------------- | -| Why something is built the way it is | `docs/decisions/README.md` (index, then one file) | -| What to work on | the issue the maintainer names, only that issue | -| Continue an unfinished PR | `docs/handoff/.md` on the PR branch | -| Tabs and hash routing | `src/lib/tabs.ts`, `src/App.svelte` | -| Play/real clocks, `onPlayTime` for trackers | `src/lib/clock.ts`, `clock.svelte.ts` (dec. 008) | -| Game logic (pure TS, unit-tested) | `src/lib/` | -| Saved user state, migrations, export/import | `src/lib/state.ts`, `src/lib/store.svelte.ts` | -| UI components (thin) | `src/components/` | -| Base UI kit (panel, meter, button...), preview | `src/components/ui/`, `#/kit` in dev (dec. 009) | -| Resync values, editable bars (dec. 010, 018) | `ui/EditableValue`, `EditableBar`, `BusinessBars` | -| Game data with sources | `src/data/` (decision 004) | -| Owned properties and upgrades | `data/properties.ts`, `lib/properties.ts` (011) | -| Business production (stock, supplies, value) | `data/production.ts`, `lib/production.ts` (012) | -| Nightclub warehouse goods | `data/nightclub.ts`, `lib/nightclub.ts` (015) | -| Business card order (drag, keys) | `lib/order.ts`, `ui/Panel` `move` (decision 014) | -| Safes and nightclub popularity | `data/safes.ts`, `lib/safes.ts` (decision 016) | -| Acid Lab, production boost | `data/production.ts`, `lib/production.ts` (017) | -| Money Fronts safe and heat | `data/fronts.ts`, `lib/fronts.ts` (018) | -| Value bonus for owning a front | `ownedBonus` in `data/production.ts` (019) | -| Shut-down businesses (owned, not running) | `isRunning` in `lib/properties.ts` (020) | -| Bunker staff on manufacturing/both/research | `staffSplit` in `data/production.ts` (025) | -| Real-time timers: Lucky Wheel, Mansion boost | `data/timers.ts`, `lib/timers.ts` (022) | -| Overview tab, next events, notifications | `lib/events.ts`, `lib/overview.ts` (dec. 023) | -| Theme tokens | `src/app.css` (decision 009) | -| Link previews (Open Graph), preview image | `index.html`, `scripts/og-image.html` | -| CI, templates | `.github/` | -| Skills: `handoff`, `finish-pr`; agent: `scout` | `.claude/` | +| Need | Look in | +| ---------------------------------------------- | -------------------------------------------------- | +| Why something is built the way it is | `docs/decisions/README.md` (index, then one file) | +| What to work on | the issue the maintainer names, only that issue | +| Continue an unfinished PR | `docs/handoff/.md` on the PR branch | +| Tabs and hash routing | `src/lib/tabs.ts`, `src/App.svelte` | +| Play/real clocks, `onPlayTime` for trackers | `src/lib/clock.ts`, `clock.svelte.ts` (dec. 008) | +| Game logic (pure TS, unit-tested) | `src/lib/` | +| Saved user state, migrations, export/import | `src/lib/state.ts`, `src/lib/store.svelte.ts` | +| UI components (thin) | `src/components/` | +| Base UI kit (panel, meter, button...), preview | `src/components/ui/`, `#/kit` in dev (dec. 009) | +| Resync values, editable bars (dec. 010, 018) | `ui/EditableValue`, `EditableBar`, `BusinessBars` | +| Game data with sources | `src/data/` (decision 004) | +| Owned properties and upgrades | `data/properties.ts`, `lib/properties.ts` (011) | +| Business production (stock, supplies, value) | `data/production.ts`, `lib/production.ts` (012) | +| Nightclub warehouse goods | `data/nightclub.ts`, `lib/nightclub.ts` (015) | +| Card order (drag, keys) on every card tab | `lib/order.ts`, `ui/CardGrid` (decisions 014, 026) | +| Safes and nightclub popularity | `data/safes.ts`, `lib/safes.ts` (decision 016) | +| Acid Lab, production boost | `data/production.ts`, `lib/production.ts` (017) | +| Money Fronts safe and heat | `data/fronts.ts`, `lib/fronts.ts` (018) | +| Value bonus for owning a front | `ownedBonus` in `data/production.ts` (019) | +| Shut-down businesses (owned, not running) | `isRunning` in `lib/properties.ts` (020) | +| Bunker staff on manufacturing/both/research | `staffSplit` in `data/production.ts` (025) | +| Real-time timers: Lucky Wheel, Mansion boost | `data/timers.ts`, `lib/timers.ts` (022) | +| Overview tab, next events, notifications | `lib/events.ts`, `lib/overview.ts` (dec. 023) | +| Theme tokens | `src/app.css` (decision 009) | +| Link previews (Open Graph), preview image | `index.html`, `scripts/og-image.html` | +| CI, templates | `.github/` | +| Skills: `handoff`, `finish-pr`; agent: `scout` | `.claude/` | ## Commands diff --git a/docs/decisions/014-card-order.md b/docs/decisions/014-card-order.md index f23d878..c3ec610 100644 --- a/docs/decisions/014-card-order.md +++ b/docs/decisions/014-card-order.md @@ -1,6 +1,7 @@ # 014: Business cards in the user's order -2026-10-14, issue #30. Builds on decisions 006 and 009. +2026-10-14, issue #30. Builds on decisions 006 and 009. Extended to other tabs, with easier +grabbing and swapping, by decision 026. ## Decision diff --git a/docs/decisions/026-movable-cards-everywhere.md b/docs/decisions/026-movable-cards-everywhere.md new file mode 100644 index 0000000..751a646 --- /dev/null +++ b/docs/decisions/026-movable-cards-everywhere.md @@ -0,0 +1,27 @@ +# 026: Movable cards on every card tab, easier to swap + +2026-10-15, issue #51. Builds on decision 014. + +## Decision + +- **Shared grid.** The drag and keyboard moving of decision 014 lives in `ui/CardGrid`, which + renders each card through a snippet and hands it the title bar handlers (`Panel`'s `move`). + Businesses, Safes (Money Fronts and the property safes) and Timers use it. Settings stays + fixed (its panels are not cards to compare), and the Overview is lists, not cards. +- **Title bar only.** A card is picked up by its title bar, with mouse, pen and touch alike; + the rest of the card keeps its text selectable and its controls undisturbed (issue #51 + review: grabbing anywhere got in the way of both). It lifts only after 4 px of travel, so a + click on the title bar stays a click. +- **Swap by where the card is.** A swap is decided by the middle of the dragged card, not the + pointer: held by its title bar, a card hangs far below the pointer, so the pointer often sat + on a border while the card already covered its target. The middle must be 10% in from each + side of the other card (was 20%), which still leaves a band where neither swap applies, so + the cards do not flick back and forth. +- **Saved state** (version 13): `safeOrder` and `timerOrder`, lists of card ids like + `businessOrder`. Safe cards use property ids (the car wash for Money Fronts), timer cards + `lucky-wheel` and `mansion`, the same ids the Overview links scroll to. + +## Why + +Issue #51: only business cards moved, and they were hard to swap. +One grid component keeps the three tabs behaving alike; it came from three real cases. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 1790e7e..1735618 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -36,3 +36,4 @@ free number, add one line below. **Replacing one:** write a new decision, then m - [023](023-overview.md) Overview tab: trackers give a shared `NextEvent`, due first then soonest, supplies on their way counted in the forecast, links that scroll to the card, quick actions with undo on every ready item, popularity in view; alerts as pop-ups and/or notifications, set time ahead - [024](024-several-tabs.md) Several tabs: a tab takes the state another tab saves (`storage` event); only the tab that last started counting counts play time (`BroadcastChannel`) - [025](025-bunker-staff.md) Bunker staff on Manufacturing, Both or Research: a stock share (Both 0.5, Research 0) on the sourced rates, supplies used as fast on every option (est.), research progress not tracked +- [026](026-movable-cards-everywhere.md) Movable cards on Businesses, Safes and Timers (`ui/CardGrid`): picked up by the title bar; swap by the dragged card's middle, 10% in; orders saved per tab diff --git a/src/App.svelte b/src/App.svelte index edad11d..a26b409 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -112,6 +112,8 @@ onchange={setProperties} stored={store.state.safes} onresync={(id, safe) => (store.state.safes = { ...store.state.safes, [id]: safe })} + order={store.state.safeOrder} + onorder={(order) => (store.state.safeOrder = order)} /> {:else if active === 'timers'} (store.state.timers = timers)} + order={store.state.timerOrder} + onorder={(order) => (store.state.timerOrder = order)} /> {:else if active === 'settings'} diff --git a/src/components/Businesses.svelte b/src/components/Businesses.svelte index 2e6edc1..d405208 100644 --- a/src/components/Businesses.svelte +++ b/src/components/Businesses.svelte @@ -1,16 +1,11 @@ - - -{#if shown.length === 0} +{#if ownedList.length === 0}

{#if catalog.length === 0} No businesses are tracked yet. @@ -238,133 +106,86 @@ {/if}

{:else} -
- {#each shown as property (property.id)} + property.id} + label={(property) => property.label} + {order} + {onorder} + > + {#snippet card(property, move, moving)} {@const production = productions.find((p) => p.propertyId === property.id)} {@const warehouse = property.id === NIGHTCLUB_ID && goods.length > 0} -
-
- - {#if open === property.id} - - {:else} -

{summary(property)}

- {/if} - {#if production} - onresync(property.id, bars)} - bonus={valueBonus(production, owned, property)} - farEstimated={farValueEstimated(production, owned)} - boost={boosts[property.id]} - onboost={(boost) => onboost(property.id, boost)} - {mansionBoost} - /> +
+ + {#if open === property.id} + + {:else} +

{summary(property)}

+ {/if} + {#if production} + onresync(property.id, bars)} + bonus={valueBonus(production, owned, property)} + farEstimated={farValueEstimated(production, owned)} + boost={boosts[property.id]} + onboost={(boost) => onboost(property.id, boost)} + {mansionBoost} + /> + {/if} + {#if warehouse} + + {/if} + {#snippet footer()} + {#if production || warehouse} + {/if} - {#if warehouse} - - {/if} - {#snippet footer()} - {#if production || warehouse} - - {/if} - (open = open === property.id ? null : property.id)} - /> - {/snippet} -
-
+ (open = open === property.id ? null : property.id)} + /> + {/snippet} +
- {/each} -
-

{announcement}

+ {/snippet} +
{/if} diff --git a/src/components/ui/Panel.svelte b/src/components/ui/Panel.svelte index feadb39..0f1aba3 100644 --- a/src/components/ui/Panel.svelte +++ b/src/components/ui/Panel.svelte @@ -9,7 +9,7 @@ footer?: Snippet /** * Makes the title bar a handle to move the panel: these handlers go on it, it takes focus, - * and the parent does the moving (issue #30). + * and the parent does the moving (`CardGrid`, decision 026). */ move?: HTMLAttributes /** Highlights the panel while it is being moved. */ diff --git a/src/data/production.ts b/src/data/production.ts index ee7b0cc..640d5bc 100644 --- a/src/data/production.ts +++ b/src/data/production.ts @@ -110,7 +110,7 @@ const SUPPLIES_WITH_EQUIPMENT = 'Supply use as with both upgrades: only the equipment upgrade makes supplies last longer ' + '(gta.wiki, The Open Road, Upgrades).' const SUPPLIES_WITHOUT_EQUIPMENT = - 'Supplies last half as long as with equipment, as in the Bunker (10 instead of 20 units ' + + 'Supplies last half as long as with equipment as in the Bunker (10 instead of 20 units ' + 'per full supply bar).' const STAFF_ONLY = 'Speed and value as with equipment only, as in the Bunker, where either upgrade alone ' + diff --git a/src/lib/state.test.ts b/src/lib/state.test.ts index b9feb7a..3fb0fa7 100644 --- a/src/lib/state.test.ts +++ b/src/lib/state.test.ts @@ -11,6 +11,8 @@ import { // Version 12 defaults: alerts off, 10 minutes ahead, also while focused. const ALERTS = { browser: false, popup: false, leadMs: 600_000, unfocusedOnly: false } +// Version 13 defaults: safe and timer cards in their default order. +const ORDERS = { safeOrder: [], timerOrder: [] } describe('parseState', () => { it('round-trips the default state', () => { @@ -28,11 +30,13 @@ describe('parseState', () => { expect(result).toEqual({ ok: true, state: { - version: 12, + version: 13, clock: { trackOnOpen: false }, properties: {}, businesses: {}, businessOrder: [], + safeOrder: [], + timerOrder: [], nightclub: {}, safes: {}, boosts: {}, @@ -48,11 +52,13 @@ describe('parseState', () => { expect(result).toEqual({ ok: true, state: { - version: 12, + version: 13, clock: { trackOnOpen: true }, properties: {}, businesses: {}, businessOrder: [], + safeOrder: [], + timerOrder: [], nightclub: {}, safes: {}, boosts: {}, @@ -71,11 +77,13 @@ describe('parseState', () => { expect(result).toEqual({ ok: true, state: { - version: 12, + version: 13, clock: { trackOnOpen: false }, properties, businesses: {}, businessOrder: [], + safeOrder: [], + timerOrder: [], nightclub: {}, safes: {}, boosts: {}, @@ -95,11 +103,13 @@ describe('parseState', () => { expect(result).toEqual({ ok: true, state: { - version: 12, + version: 13, clock: { trackOnOpen: true }, properties, businesses, businessOrder: [], + safeOrder: [], + timerOrder: [], nightclub: {}, safes: {}, boosts: {}, @@ -123,7 +133,8 @@ describe('parseState', () => { ok: true, state: { ...sample, - version: 12, + version: 13, + ...ORDERS, nightclub: {}, safes: {}, boosts: {}, @@ -147,7 +158,8 @@ describe('parseState', () => { ok: true, state: { ...sample, - version: 12, + version: 13, + ...ORDERS, safes: {}, boosts: {}, timers: {}, @@ -169,7 +181,7 @@ describe('parseState', () => { } expect(parseState(JSON.stringify(sample))).toEqual({ ok: true, - state: { ...sample, version: 12, boosts: {}, timers: {}, alerts: ALERTS }, + state: { ...sample, version: 13, ...ORDERS, boosts: {}, timers: {}, alerts: ALERTS }, fromVersion: 7, }) }) @@ -187,7 +199,7 @@ describe('parseState', () => { } expect(parseState(JSON.stringify(sample))).toEqual({ ok: true, - state: { ...sample, version: 12, timers: {}, alerts: ALERTS }, + state: { ...sample, version: 13, ...ORDERS, timers: {}, alerts: ALERTS }, fromVersion: 8, }) }) @@ -205,7 +217,7 @@ describe('parseState', () => { } expect(parseState(JSON.stringify(sample))).toEqual({ ok: true, - state: { ...sample, version: 12, timers: {}, alerts: ALERTS }, + state: { ...sample, version: 13, ...ORDERS, timers: {}, alerts: ALERTS }, fromVersion: 9, }) }) @@ -223,7 +235,7 @@ describe('parseState', () => { } expect(parseState(JSON.stringify(sample))).toEqual({ ok: true, - state: { ...sample, version: 12, timers: {}, alerts: ALERTS }, + state: { ...sample, version: 13, ...ORDERS, timers: {}, alerts: ALERTS }, fromVersion: 10, }) }) @@ -242,11 +254,37 @@ describe('parseState', () => { } expect(parseState(JSON.stringify(sample))).toEqual({ ok: true, - state: { ...sample, version: 12, alerts: ALERTS }, + state: { ...sample, version: 13, ...ORDERS, alerts: ALERTS }, fromVersion: 11, }) }) + it('migrates a version 12 sample, with safe and timer cards in their default order', () => { + const sample = { + version: 12, + clock: { trackOnOpen: false }, + properties: { agency: { owned: true, upgrades: {} } }, + businesses: {}, + businessOrder: ['bunker'], + nightclub: {}, + safes: { agency: { money: 10_000, sinceMs: 0 } }, + boosts: {}, + timers: { luckyWheelSpunAt: 5_000 }, + alerts: ALERTS, + } + expect(parseState(JSON.stringify(sample))).toEqual({ + ok: true, + state: { ...sample, version: 13, ...ORDERS }, + fromVersion: 12, + }) + }) + + it('keeps the safe and timer card order', () => { + const order = { safeOrder: ['car-wash', 'agency'], timerOrder: ['mansion', 'lucky-wheel'] } + const text = JSON.stringify({ ...defaultState(), ...order }) + expect(parseState(text)).toMatchObject({ ok: true, state: order }) + }) + it('keeps the alert settings', () => { const alerts = { browser: true, popup: true, leadMs: 0, unfocusedOnly: true } const text = JSON.stringify({ ...defaultState(), alerts }) diff --git a/src/lib/state.ts b/src/lib/state.ts index 339f190..795b0d8 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -13,7 +13,7 @@ import { parseSafes, type SafesById } from './safes' import { parseTimers, type Timers } from './timers' /** Bump when the stored shape changes, and add a migration from the previous version. */ -export const CURRENT_VERSION = 12 +export const CURRENT_VERSION = 13 /** All user state. Trackers add their own sections here as they are built. */ export interface AppState { @@ -26,6 +26,10 @@ export interface AppState { businesses: BusinessBarsById /** Property ids in the order the user dragged the business cards into (issue #30). */ businessOrder: string[] + /** Card ids in the order the user dragged the safe cards into (issue #51). */ + safeOrder: string[] + /** Card ids in the order the user dragged the timer cards into (issue #51). */ + timerOrder: string[] /** Nightclub warehouse crates by goods id, as of the last play time counted (issue #7). */ nightclub: GoodsStock /** Money in each safe and nightclub popularity, as of the last play time counted (issue #8). */ @@ -60,6 +64,8 @@ export function defaultState(): AppState { properties: {}, businesses: {}, businessOrder: [], + safeOrder: [], + timerOrder: [], nightclub: {}, safes: {}, boosts: {}, @@ -99,6 +105,8 @@ export const MIGRATIONS: Readonly> = { 10: (data) => ({ ...data, timers: {} }), // Version 12 adds the alert settings (issue #15); alerts are off until opted in. 11: (data) => ({ ...data, alerts: defaultAlerts() }), + // Version 13 adds the order of the safe and timer cards (issue #51); default order until moved. + 12: (data) => ({ ...data, safeOrder: [], timerOrder: [] }), } export type ParseResult = @@ -175,6 +183,10 @@ function toState(data: StoredData): AppState | string { if (typeof businesses === 'string') return businesses const businessOrder = parseOrder(data.businessOrder) if (typeof businessOrder === 'string') return businessOrder + const safeOrder = parseOrder(data.safeOrder) + if (typeof safeOrder === 'string') return safeOrder + const timerOrder = parseOrder(data.timerOrder) + if (typeof timerOrder === 'string') return timerOrder const nightclub = parseGoodsStock(data.nightclub) if (typeof nightclub === 'string') return nightclub const safes = parseSafes(data.safes) @@ -191,6 +203,8 @@ function toState(data: StoredData): AppState | string { properties, businesses, businessOrder, + safeOrder, + timerOrder, nightclub, safes, boosts,