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 @@ -32,6 +32,7 @@ Svelte 5 + TypeScript + Vite. Public repository.
| Real-time timers: Lucky Wheel, Mansion boost | `data/timers.ts`, `lib/timers.ts` (022) |
| Weekly events and the Weekly tab | `data/weekly.ts`, `lib/weekly.ts` (decision 027) |
| 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) |
| Link previews (Open Graph), preview image | `index.html`, `scripts/og-image.html` |
| CI, templates | `.github/` |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ emptying.
for your upgrades, LSOS uses a best estimate and labels those numbers "(est.)".
- **Overview for a second screen.** One page lists what is ready now and what comes next across
every tracker, soonest first, with the money waiting. Mark things done right there (sold,
collected, spun), and get optional alerts as pop-ups or browser notifications.
collected, spun), hide what you don't care about, and get optional alerts as pop-ups or
browser notifications.
- **Resync anytime.** When an estimate drifts from what the game shows, click the value and type
the real one.
- **Weekly events** (bonuses, discounts, rotations) come from a data file in this repo that's updated
Expand Down
23 changes: 23 additions & 0 deletions docs/decisions/028-hide-from-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 028: Hiding cards from the Overview and the alerts

2026-10-15, issue #62. Builds on decision 023.

## Decision

- **Per card, not per item.** What is hidden is an event's `focus`, the card it links to:
`car-wash` hides the Money Fronts safe and its heat together, `nightclub` its safe and its
goods. The Lucky Wheel (`lucky-wheel`) and the Mansion boost (`mansion`) hide the same way,
though they belong to no business card. Saved as `overviewHidden` (state version 14).
- **Where:** a small Hide control on each Ready now and Coming up item, with the same Undo as the
quick actions; Settings lists what is hidden ("Hidden from Overview"), each with Show. Not on
the cards' gear menus: the timers have none, and one place to undo is enough.
- **Only the Overview and the alerts leave it out.** The card stays owned and tracked on its
own tab, and its money still counts in the summary strip: the totals say what you have.
- **No alert on showing again.** Hidden items still count as seen by the alerts, so an item
already near or due when it is shown again does not pop up; one that comes near later does.

## Alternatives

- Per item (`heat:car-wash` apart from `safe:car-wash`): finer, but a longer list to manage,
and no case asked for it yet.
- Leaving hidden cards out of the totals: the strip would then understate the money waiting.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ free number, add one line below. **Replacing one:** write a new decision, then m
- [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
- [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
11 changes: 10 additions & 1 deletion src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@
import { SAFES } from './data/safes'
import { WEEK } from './data/weekly'
import { clocks } from './lib/clock.svelte'
import { dismissPopup, overview, quick, undoQuick } from './lib/overview.svelte'
import {
dismissPopup,
hideFromOverview,
overview,
quick,
showOnOverview,
undoQuick,
} from './lib/overview.svelte'
import './lib/production.svelte'
import './lib/safes.svelte'
import { setProperties, store } from './lib/store.svelte'
Expand Down Expand Up @@ -82,6 +89,8 @@
paused={clocks.play.paused}
onquick={quick}
onundo={undoQuick}
onhide={hideFromOverview}
onshow={showOnOverview}
/>
{:else if active === 'businesses'}
<Businesses
Expand Down
134 changes: 105 additions & 29 deletions src/components/Overview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// The Overview (issue #15): made to stay open on a second screen and be read at a glance.
// Money waiting and nightclub popularity on top, then what to do now, then everything coming
// up, soonest first. Each item links to its card, which scrolls into view and glows. Timers
// that need nothing else (a spin, a boost) are marked done right here, with an undo.
// that need nothing else (a spin, a boost) are marked done right here, with an undo. A card
// the user does not care about can be hidden from here (issue #62); Settings shows it again.
// Logic: lib/overview.ts, lib/events.ts.
import { formatDuration } from '../lib/clock'
import { isDue, type NextEvent } from '../lib/events'
import type { Holdings, Popularity, QuickResult } from '../lib/overview'
import { PROPERTIES } from '../data/properties'
import { hiddenLabel, type Holdings, type Popularity, type QuickResult } from '../lib/overview'
import { formatValue } from '../lib/resync'
import { hashForTab } from '../lib/tabs'
import Button from './ui/Button.svelte'
Expand All @@ -25,13 +27,17 @@
/** Marks an event done (its `quick`); returns what undoes it, if done. */
onquick: (event: NextEvent, choice?: string) => QuickResult | null
onundo: (eventId: string, undo: QuickResult['undo']) => void
/** Leaves a card's items (event `focus`) out of the Overview and the alerts. */
onhide: (focus: string) => void
/** Shows a hidden card's items again. */
onshow: (focus: string) => void
}

let { events, holdings, popularity, paused, onquick, onundo }: Props = $props()
let { events, holdings, popularity, paused, onquick, onundo, onhide, onshow }: Props = $props()

// The last quick action, offered for undo for a while (a UI timer, not game time).
// The last quick action or hide, offered for undo for a while (a UI timer, not game time).
const UNDO_MS = 15_000
let undo = $state<{ id: string; text: string; change: QuickResult['undo'] } | null>(null)
let undo = $state<{ text: string; run: () => void } | null>(null)
let undoTimer: ReturnType<typeof setTimeout> | undefined
// The business picked for each event with choices (the Mansion boost).
let picked = $state<Record<string, string>>({})
Expand All @@ -41,17 +47,28 @@
const choice = choices && (picked[event.id] ?? choices[0]?.id)
const result = onquick(event, choice)
if (!result) return
undo = { id: event.id, text: result.done, change: result.undo }
offerUndo(result.done, () => onundo(event.id, result.undo))
}

function hide(event: NextEvent) {
onhide(event.focus)
offerUndo(`Hid ${card(event)} from the Overview`, () => onshow(event.focus))
}

function offerUndo(text: string, run: () => void) {
undo = { text, run }
clearTimeout(undoTimer)
undoTimer = setTimeout(() => (undo = null), UNDO_MS)
}

function takeBack() {
if (undo) onundo(undo.id, undo.change)
undo?.run()
undo = null
}

const link = (e: NextEvent) => hashForTab(e.tab, e.focus)
// Hide works on the whole card, e.g. the Money Fronts items hide with the Hands On Car Wash.
const card = (e: NextEvent) => hiddenLabel(e.focus, PROPERTIES)
const cut = $derived(popularity !== null && popularity.payout < popularity.best)

const due = $derived(events.filter(isDue))
Expand Down Expand Up @@ -126,26 +143,35 @@
{#if event.value}<span class="money">{money(event.value)}</span>{/if}
</span>
</a>
{#if event.quick}
<span class="quick">
{#if event.quick.choices}
<span class="pick">
<Select
id="pick-{event.id}"
value={picked[event.id] ?? event.quick.choices[0]?.id}
options={event.quick.choices}
onchange={(id) => (picked = { ...picked, [event.id]: id })}
/>
</span>
{/if}
<Button
variant="primary"
onclick={() => press(event)}
aria-label="{event.source}: {event.quick.label}"
title="Press once you have done it in game">{event.quick.label}</Button
>
</span>
{/if}
<span class="side">
{#if event.quick}
<span class="quick">
{#if event.quick.choices}
<span class="pick">
<Select
id="pick-{event.id}"
value={picked[event.id] ?? event.quick.choices[0]?.id}
options={event.quick.choices}
onchange={(id) => (picked = { ...picked, [event.id]: id })}
/>
</span>
{/if}
<Button
variant="primary"
onclick={() => press(event)}
aria-label="{event.source}: {event.quick.label}"
title="Press once you have done it in game">{event.quick.label}</Button
>
</span>
{/if}
<button
type="button"
class="hide"
aria-label="Hide {card(event)} from the Overview"
title="Hide {card(event)} here and in alerts. Show it again in Settings."
onclick={() => hide(event)}>Hide</button
>
</span>
</li>
{/each}
</ul>
Expand All @@ -164,7 +190,7 @@
{:else}
<ul class="next">
{#each coming as event (event.id)}
<li>
<li class="item">
<a
class="row {event.tone}"
class:frozen={paused && event.clock === 'play'}
Expand Down Expand Up @@ -195,6 +221,13 @@
</span>
<span class="value">{event.value ? money(event.value) : ''}</span>
</a>
<button
type="button"
class="hide"
aria-label="Hide {card(event)} from the Overview"
title="Hide {card(event)} here and in alerts. Show it again in Settings."
onclick={() => hide(event)}>Hide</button
>
</li>
{/each}
</ul>
Expand Down Expand Up @@ -340,6 +373,16 @@
padding: var(--space-2) var(--space-1) var(--space-2) var(--space-3);
}

/* The quick action with Hide below it, so Hide takes no width from the name. */
.side {
align-items: flex-end;
display: flex;
flex: none;
flex-direction: column;
gap: 2px;
padding: var(--space-1) 0;
}

.quick {
align-items: center;
display: flex;
Expand All @@ -366,6 +409,11 @@
width: 12rem;
}

/* Ready now has room for a second line: long names wrap instead of being cut. */
.card .source {
white-space: normal;
}

.undo {
align-items: center;
color: var(--fg);
Expand Down Expand Up @@ -455,10 +503,17 @@
break-inside: avoid;
}

.row {
.item {
align-items: center;
border-bottom: 1px solid var(--line);
display: flex;
}

.row {
align-items: center;
display: grid;
flex: 1;
min-width: 0;
gap: var(--space-3);
grid-template-columns: 5.5rem minmax(0, 1fr) 4.5rem 6.5rem;
padding: var(--space-1) var(--space-2);
Expand All @@ -468,6 +523,27 @@
background: var(--panel-raised);
}

/* Hide (issue #62): small and quiet, so it is not pressed instead of the item. */
.hide {
background: none;
border: 1px solid transparent;
border-radius: var(--radius);
color: var(--muted);
cursor: pointer;
flex: none;
font-family: var(--font-display);
font-size: var(--text-xs);
letter-spacing: var(--tracking);
padding: var(--space-1) var(--space-2);
text-transform: uppercase;
}

.hide:hover,
.hide:focus-visible {
border-color: var(--line);
color: var(--fg);
}

.row .name {
align-items: baseline;
flex-direction: row;
Expand Down
46 changes: 45 additions & 1 deletion src/components/Settings.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { PROPERTIES } from '../data/properties'
import { hiddenLabel } from '../lib/overview'
import { exportFileName } from '../lib/state'
import { notificationsSupported, setBrowserAlerts } from '../lib/overview.svelte'
import { notificationsSupported, setBrowserAlerts, showOnOverview } from '../lib/overview.svelte'
import { exportText, importText, setProperties, store } from '../lib/store.svelte'
import PropertyList from './PropertyList.svelte'
import Button from './ui/Button.svelte'
Expand All @@ -25,6 +26,12 @@
}))
const alerts = $derived(store.state.alerts)
const off = $derived(!alerts.browser && !alerts.popup)
// Cards hidden from the Overview and the alerts (issue #62), by name.
const hidden = $derived(
store.state.overviewHidden
.map((focus) => ({ focus, label: hiddenLabel(focus, PROPERTIES) }))
.sort((a, b) => a.label.localeCompare(b.label)),
)

function exportBackup() {
const blob = new Blob([exportText()], { type: 'application/json' })
Expand Down Expand Up @@ -115,6 +122,27 @@
<p class="hint">Alerts come only while LSOS is open in a tab.</p>
</Panel>

<Panel title="Hidden from Overview">
<p class="hint">
Left out of the Overview and the alerts, but still tracked on their own tabs and counted in
the money waiting. Hide one with Hide on its Overview item.
</p>
{#if hidden.length === 0}
<p>Nothing is hidden.</p>
{:else}
<ul class="hidden">
{#each hidden as item (item.focus)}
<li>
<span>{item.label}</span>
<Button onclick={() => showOnOverview(item.focus)} aria-label="Show {item.label}"
>Show</Button
>
</li>
{/each}
</ul>
{/if}
</Panel>

<Panel title="Backup">
<p>
Your data lives only in this browser. Export it to keep a copy or to move to another device.
Expand Down Expand Up @@ -194,4 +222,20 @@
.error {
color: var(--danger);
}

.hidden {
display: flex;
flex-direction: column;
gap: var(--space-2);
list-style: none;
margin: 0;
padding: 0;
}

.hidden li {
align-items: center;
display: flex;
gap: var(--space-3);
justify-content: space-between;
}
</style>
Loading
Loading