Skip to content
Merged
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Svelte 5 + TypeScript + Vite. Public repository.
| Overview tab, next events, notifications | `lib/events.ts`, `lib/overview.ts` (dec. 023) |
| Hiding cards from the Overview and alerts | `visibleEvents` in `lib/overview.ts` (dec. 028) |
| Theme tokens | `src/app.css` (decision 009) |
| Hint, Note, empty state, timed Undo | `ui/`, `lib/cards.svelte.ts` (decision 029) |
| Link previews (Open Graph), preview image | `index.html`, `scripts/og-image.html` |
| CI, templates | `.github/` |
| Skills: `handoff`, `finish-pr`; agent: `scout` | `.claude/` |
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/010-resync.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 010: Resync: correcting tracked values by hand

2026-10-14, issue #4. Builds on decision 003 ("Resync"). Duration format amended by decision 022.
2026-10-14, issue #4. Builds on decision 003 ("Resync"). Duration format amended by decision 022; presets and the edit view by decision 029.

## Decision

Expand Down
62 changes: 62 additions & 0 deletions docs/decisions/029-shared-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 029: Shared card parts, always-editable cards and the Settings list

2026-10-15, issue #60. Builds on decisions 009, 011 and 026; amends 010.

## Decision

- **One component per repeated pattern** in `src/components/ui/`:
- `Hint`: muted help text in two sizes, normal (`--text-sm`) and `fine` (`--text-xs`, for
footnotes such as how an estimate is made).
- `Note`: a callout with a bar on its left and a bold title first. Tones:
- `warn`: needs a look ("Estimate.", "Payout cut.").
- `quiet`: a plain state ("Shut down.").
- `good`: a tip, with `actions` on their own line.
- `MarkOwned`: what a tab shows while nothing on it is owned, with the link to Settings. With
a property, the link opens Settings on that property (`#/settings/<id>`).
- `UndoStatus` with `TimedUndo` (`lib/cards.svelte.ts`): "Boost started. [Undo]" for 15
seconds, right where the press was. Used for the Acid Lab boost, the Timers cards and the
Overview's quick actions. The Undo puts back what the press replaced.
- `Button small`: for rows of several actions and the title bar.
- **Cards are always editable.** No Edit button and no separate edit view:
- Every value can be clicked to type it in.
- Bars can be dragged. The drag handle shows only while the pointer is over the card, or the
bar has focus. Touch screens always show it.
- The read-only extras stay, next to the editable value: units, "of $cap", value per goods.
- Decided after trying both styles side by side.
- **Fewer one-click presets** (amends 010): no Full on stock, safes or goods, and no Raided.
No Maxed on popularity either. Sold, Supplies bought, Collected and the popularity boosts stay.
Rarely used, and every value can be typed or dragged instead.
- **No card footer.** The settings gear is a small button at the right end of the title bar.
One card per tab has its settings open at a time.
- **Money Fronts heat:** each front's bar has its three actions right under it, as small
buttons in one row: +10% Laundering, +10% Faber, Legal job.
- **One card width:** `--card-width` (20rem) for every card grid.
- **Order by use:** `USE_ORDER` in `src/data/properties.ts` sorts Settings and gives each tab
its card order until the user moves the cards:
- Acid Lab, Bunker and Nightclub first.
- Then the MC businesses by money per hour at full upgrades, from `production.ts`: Cocaine,
Meth, Cash, Weed, Documents.
- Then the Money Fronts and the safes. The properties that only unlock something or add to a
safe come last, with the Mansion and the Bail Enforcement Office at the bottom.
- A test checks that the list names every property.
- **Bars grab at their ends:** the draggable area reaches half a handle past each end of the
bar, so a handle at 0% or 100% can be grabbed anywhere on it.
- **Settings is a list with a detail panel:**
- Every property is a row that keeps its height, with a gear in front of its name, a
one-line upgrade summary, and Owned (and Running) buttons in fixed-width slots.
- Pick a row to set its upgrades in a panel beside the list. The panel is centred on the
row, kept within the list, and has an arrow pointing at the row.
- Without room beside the list, the upgrades open under the picked row instead.
- A property that is not owned shows its upgrades greyed, so marking it owned changes
nothing in the layout.
- On wide screens the other settings are a narrow column on the right.

## Alternatives

- Edit and settings as two exclusive modes behind buttons: tried. It kept a footer and a
second view of every card.
- Global CSS classes instead of `Hint` and `Note`: shorter markup, but the tones and the title
would still be written out by hand in every card.
- Settings with collapsible rows, or grouped cards: either one still moves things around as
properties are marked owned or opened.
- One "+10%" button with a choice of mission: one more click for every mission logged.
3 changes: 2 additions & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ free number, add one line below. **Replacing one:** write a new decision, then m
- [007](007-state-store.md) State store: unreadable data blocks saving like a newer version; import overrides
- [008](008-clock-details.md) Clock details: session play time, opens paused (setting), throttled ticks counted, long gaps asked about
- [009](009-terminal-design.md) Terminal look: tokens in `app.css`, self-hosted Oswald (OFL), browser-style tabs, base components in `src/components/ui/`
- [010](010-resync.md) Resync: `EditableValue` for every tracked value, `BusinessBars` with Sold/Raided/Full and supply deliveries, undo keeps progress since, five-segment bars you can drag (duration format amended by 022)
- [010](010-resync.md) Resync: `EditableValue` for every tracked value, `BusinessBars` with Sold/Raided/Full and supply deliveries, undo keeps progress since, five-segment bars you can drag (duration format amended by 022; presets and edit view amended by 029)
- [011](011-owned-properties.md) Owned properties and upgrades: catalog in `src/data/properties.ts`, toggle/choice upgrades (count: 016), Settings panel and Businesses gear
- [012](012-business-production.md) Business production: rate per exact upgrade combination in `src/data/production.ts`, continuous on play time, unknown rates do not run
- [013](013-marked-estimates.md) Missing game values may be estimated by a stated rule from sourced ones, marked "(est.)" wherever shown
Expand All @@ -39,3 +39,4 @@ free number, add one line below. **Replacing one:** write a new decision, then m
- [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
- [027](027-weekly-contents.md) Weekly events: typed `src/data/weekly.ts` (no JSON, no fetch), checked by a test; Weekly tab only (no news bar); out of date: warning over the dimmed old week; contents: multipliers, Weekly Challenge, rotations, discounts, gifts, a less prominent "Also running" list; no GTA+, no daily map-bound items; explicit UTC start/end (Thursday 2:00 AM US Pacific); source gta.wiki weekly page; routine Thursday ~16:00 UTC
- [028](028-hide-from-overview.md) Hide from Overview: per card (event `focus`, timers too), Hide on each item with undo, Settings lists them with Show; still tracked and counted in the totals; showing again brings no alert
- [029](029-shared-ui.md) Shared card parts (`Hint`, `Note`, `MarkOwned`, timed `UndoStatus`, small buttons); cards always editable, drag handles on hover, no Full/Raided/Maxed; gear in the title bar, no footer; `--card-width`; default order by use (`USE_ORDER`); Settings as a fixed-row property list with the picked property's upgrades beside it
2 changes: 1 addition & 1 deletion src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
{:else if active === 'weekly'}
<Weekly week={WEEK} owned={store.state.properties} now={clocks.now} />
{:else if active === 'settings'}
<Settings />
<Settings select={focusFromHash(hash)} />
{:else}
<p class="placeholder">Not built yet.</p>
{/if}
Expand Down
10 changes: 10 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
--scanline-gap: 3px;
--bar-height: 0.5rem;
--max-width: 80rem;
/* Narrowest card column in a tab's card grid (`ui/CardGrid`). */
--card-width: 20rem;

color-scheme: dark;
font-family: var(--font-body);
Expand Down Expand Up @@ -104,6 +106,14 @@ a {
color: var(--accent-text);
}

/* Every checkbox in the theme's green, a little larger than the browser's. */
input[type='checkbox'] {
accent-color: var(--accent);
flex: none;
height: 1.125rem;
width: 1.125rem;
}

:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
Expand Down
103 changes: 35 additions & 68 deletions src/components/BusinessProduction.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
// Stock, supplies, value and what comes next for one business (issue #6). Collapsed it shows
// only that; `editing` adds the resync controls. A business with a production boost shows it
// and lets the user start it (issue #9), and the Mansion boost while it is on (issue #14).
// Stock, supplies, value and what comes next for one business (issue #6), always editable
// (issue #60). A business with a production boost shows it and lets the user start it
// (issue #9), and the Mansion boost while it is on (issue #14).
// Logic: lib/production.ts.
import { SUPPLY_PURCHASE } from '../data/business-events'
import type { Production, ProductionRate } from '../data/production'
Expand All @@ -19,11 +19,13 @@
} from '../lib/production'
import { formatValue, settleBars, type Bars } from '../lib/resync'
import { mansionBoostOn, type MansionBoost } from '../lib/timers'
import { TimedUndo } from '../lib/cards.svelte'
import BusinessBars from './ui/BusinessBars.svelte'
import Button from './ui/Button.svelte'
import Countdown from './ui/Countdown.svelte'
import Fact from './ui/Fact.svelte'
import Meter from './ui/Meter.svelte'
import Note from './ui/Note.svelte'
import UndoStatus from './ui/UndoStatus.svelte'

interface Props {
production: Production
Expand All @@ -36,16 +38,14 @@
bars: Bars
/** Wall-clock time, for supply deliveries. */
now: number
/** Show the controls to correct the bars and record sales, raids and supplies. */
editing: boolean
onresync: (bars: Bars) => void
/** What bought upgrades add to the value, e.g. 0.05 for a named product. */
bonus?: number
/** The far value adds an owned-property bonus to the far bonus by estimate (decision 019). */
farEstimated?: boolean
/** The last production boost, if the business has one (kept after it ends). */
boost?: Boost
/** Called with the new boost, or undefined to take back one started by mistake. */
/** Called with the boost just started, or the one before it to take a press back. */
onboost?: (boost: Boost | undefined) => void
/** The last Mansion boost, on any business (issue #14). */
mansionBoost?: MansionBoost
Expand All @@ -58,7 +58,6 @@
shutDown = false,
bars,
now,
editing,
onresync,
bonus = 0,
farEstimated = false,
Expand Down Expand Up @@ -94,7 +93,6 @@
})
: null,
)
const percent = (value: number) => formatValue(value, 'percent')
// Numbers resting on estimated rates say so (decision 013). The value also counts units made
// at an estimated rate.
// No source gives the speed of both boosts at once: LSOS multiplies them (est.).
Expand All @@ -104,27 +102,24 @@
return { ...parts, production: parts.production || stacked }
})
const est = (label: string, isEstimate: boolean) => (isEstimate ? `${label} (est.)` : label)

// A press by mistake can be taken back for a while, right here (issue #60).
const undo = new TimedUndo()
function startBoostNow() {
const before = boost
onboost(startBoost(production.boost!, now))
undo.offer('boost', 'Boost started', () => onboost(before))
}
</script>

{#if editing}
<BusinessBars {bars} {now} {onresync} raids={production.raids !== false} />
{:else}
<Meter
label={est('Stock', estimated.production)}
value={current.stock}
max={100}
valueText="{units} / {production.maxUnits} {production.unitName} · {percent(current.stock)}"
/>
<Meter
label={est('Supplies', estimated.production)}
value={current.supplies ?? 0}
max={100}
kind="supplies"
/>
{#if current.suppliesDueAt !== undefined}
<Countdown label="Supplies arrive in" remainingMs={current.suppliesDueAt - now} />
{/if}
{/if}
<BusinessBars
{bars}
{now}
{onresync}
stockLabel={est('Stock', estimated.production)}
suppliesLabel={est('Supplies', estimated.production)}
stockDetail="{units} / {production.maxUnits} {production.unitName}"
/>

{#if value}
<!-- Far sales pay more, so that is the headline; nearby is the alternative. -->
Expand All @@ -144,16 +139,11 @@
{#if production.boost && running?.boostedMinutesPerUnit !== undefined && !shutDown}
<!-- Play time while it runs; the next one is a real-time wait (daily reset). -->
{#if boosting}
<Fact label="Boost on">
{Math.ceil(boost!.unitsLeft)}
{production.unitName} left
<Button onclick={() => onboost(undefined)} title="Take back a boost started by mistake"
>Undo</Button
>
</Fact>
<Fact label="Boost on">{Math.ceil(boost!.unitsLeft)} {production.unitName} left</Fact>
<UndoStatus {undo} />
{:else if now >= nextBoost}
<Fact label="Boost ready">
<Button onclick={() => onboost(startBoost(production.boost!, now))}>Boost started</Button>
<Button onclick={startBoostNow}>Boost started</Button>
</Fact>
{/if}
{#if !boosting && now < nextBoost}
Expand All @@ -169,9 +159,9 @@
{/if}

{#if shutDown}
<p class="note quiet" role="note">
<strong>Shut down.</strong> No stock is made and no supplies are used until you start it again.
</p>
<Note tone="quiet" title="Shut down."
>No stock is made and no supplies are used until you start it again.</Note
>
{:else if ahead}
<!-- Only what happens first: the other one cannot happen before it. Play time. -->
{#if ahead.fullInMs !== null}
Expand All @@ -189,50 +179,27 @@
/>
{/if}
{#if rate?.estimate}
<p class="note" role="note">
<strong>Estimate.</strong> No reliable data for these upgrades. {rate.estimate.how}
</p>
<Note title="Estimate.">No reliable data for these upgrades. {rate.estimate.how}</Note>
{/if}
{#if stacked}
<p class="note" role="note">
<strong>Estimate.</strong> No source gives the speed with both boosts on: LSOS takes the
boosted speed ×{MANSION_BOOST.speed}.
</p>
<Note title="Estimate."
>No source gives the speed with both boosts on: LSOS takes the boosted speed ×{MANSION_BOOST.speed}.</Note
>
{/if}
{:else}
<p class="note" role="note">
<strong>Not tracked.</strong>
<Note title="Not tracked.">
{#if rate}
There is no reliable data on how fast this business uses supplies with these upgrades.
{:else}
There is no reliable data for this combination of upgrades.
{/if}
Stock and supplies stay where you set them.
</p>
</Note>
{/if}

<style>
.alt {
color: var(--muted);
font-size: var(--text-sm);
}

.note {
border-left: 3px solid var(--warn);
font-size: var(--text-sm);
margin: 0;
padding-left: var(--space-2);
}

.note.quiet {
border-left-color: var(--muted);
}

.note strong {
color: var(--warn);
}

.note.quiet strong {
color: inherit;
}
</style>
Loading
Loading