Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
aaef1da
Refine Where card and calendar styling
kyleve Aug 2, 2026
2956d07
Match region previews to location cards
kyleve Aug 2, 2026
cff9ce2
Expand the region color palette
kyleve Aug 2, 2026
e59d41f
Round calendar month cards
kyleve Aug 2, 2026
f52b9d6
Use a color-neutral card sheen
kyleve Aug 2, 2026
df8fdf7
Repeat region silhouettes across cards
kyleve Aug 2, 2026
3d51fc5
Let Liquid Glass own card borders
kyleve Aug 2, 2026
f0107e6
Cache simplified region artwork paths
kyleve Aug 2, 2026
65c9993
Repeat region silhouettes as card microprint
kyleve Aug 2, 2026
6648e8d
Preserve thin geography in small region paths
kyleve Aug 2, 2026
bde1b9c
Use micro paths for card security borders
kyleve Aug 2, 2026
a319b0a
Remove stroke from geographic entry stamps
kyleve Aug 2, 2026
92d7210
Lighten card security print in dark mode
kyleve Aug 2, 2026
1846847
Use the full palette for fallback region colors
kyleve Aug 2, 2026
4517f1e
Drive region artwork from render styles
kyleve Aug 2, 2026
32c85f3
Move entry stamp drawing into card styles
kyleve Aug 2, 2026
7454e5a
Add a microprint border preview
kyleve Aug 2, 2026
5c0f6df
Refresh the developer overlay region map snapshot
kyleve Aug 2, 2026
8615d70
Keep dark card security print legible on touch
kyleve Aug 2, 2026
0fd11ec
Blend dark card security print by luminosity
kyleve Aug 2, 2026
860b406
Add card designer configuration groundwork
kyleve Aug 2, 2026
66ca085
Add debug card designer studio
kyleve Aug 2, 2026
6f29511
Match card designer page to preview appearance
kyleve Aug 2, 2026
7fda341
Add copyable card designer diffs
kyleve Aug 2, 2026
7afa099
Recover corrupt card designer drafts
kyleve Aug 2, 2026
77adc02
Reload card artwork when designer state changes
kyleve Aug 2, 2026
585ebaa
Localize the card designer studio
kyleve Aug 2, 2026
288843c
Add previews for card designer controls
kyleve Aug 2, 2026
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
14 changes: 9 additions & 5 deletions Where/RegionKit/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@ This file complements the root [`AGENTS.md`](../../AGENTS.md) and the feature
checks its regions in order and the first polygon match wins (regions are
mutually exclusive at our resolution). (Day-count ranking lives in `WhereCore`'s
`Region+Ordering`, not here.)
- **Attribution is per-region, on demand.** `RegionAttributor(for:)` loads only
- **Geometry access is per-region, on demand.** `RegionAttributor(for:)` loads only
the passed regions' `regions/<id>.geojson` files, so the app parses only the
tracked set — never the whole US at launch. `.all` loads the whole catalog
(dev viewer/tests); `.shared` the default four. It's UI-free: `BoundingBox` /
`LongitudeSpan` expose the min/max math, but MapKit conversion lives in the UI
layer. `RegionAttributing` lets `WhereCore` supply a live, swappable attributor.
tracked set, while `RegionGeometryCatalog.outlines(for: Region)` caches only
the drawable region requested by UI artwork — never load the whole US for one
card. `RegionGeometrySimplifier` vends stateless, projection-aware geometry
reduction; rendering fidelity and render-artifact caches belong to consumers.
`.all` loads the whole catalog (dev viewer/tests); `.shared` the default four.
It's UI-free: `BoundingBox` / `LongitudeSpan` expose the min/max math, but
drawing and MapKit conversion live in the UI layer. `RegionAttributing` lets
`WhereCore` supply a live, swappable attributor.
- **Bundled geometry is credited in code, not only in prose.** `RegionDataSource`
states each boundary set's origin, license, and fidelity, and derives its
coverage from the catalog — the US sources by the `us-` id prefix the generator
Expand Down
8 changes: 5 additions & 3 deletions Where/RegionKit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ into it for lookup. RegionKit depends only on
only those regions' files; `.all` covers the whole catalog and `.shared` the
default four. `RegionAttributing` is the protocol the app's live, swappable
attributor also conforms to.
- **`RegionGeometryCatalog`** — read-only drawable `RegionOutline`s for the
developer region-map viewer (`.attribution` for a given attributor vs `.source`
for the whole catalog).
- **`RegionGeometryCatalog`** — read-only drawable `RegionOutline`s: a cached,
per-region path for UI artwork, plus the developer region-map viewer's
`.attribution` view of a given attributor and `.source` view of the whole
catalog. `RegionGeometrySimplifier` can derive reduced geometry at a
consumer-chosen normalized tolerance without imposing UI sizes on RegionKit.
- **`RegionDataSource`** — where the bundled geometry came from: the boundary
set's name, its links, its `License`, its `Fidelity` (`.authoritative` vs the
`.approximate` hand-drawn outlines), and the regions it covers.
Expand Down
41 changes: 35 additions & 6 deletions Where/RegionKit/Sources/Logging/RegionGeometryCatalogLog.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,64 @@
import PeriscopeCore

/// Structured events for the developer region-map viewer's geometry load. A
/// failed load is degraded-but-handled (the viewer shows an error state), so it
/// logs at `.warning`. Public because the viewer lives in WhereUI, above
/// RegionKit, and emits through ``RegionLog/geometryCatalog``.
/// Structured events for drawable geometry loads. A failed developer-viewer
/// load is degraded-but-handled and logs at `.warning`; a missing production
/// artwork resource is a bundled-data invariant and logs at `.fault`. Public
/// because the UI consumers live above RegionKit and emit through
/// ``RegionLog/geometryCatalog``.
public enum RegionGeometryCatalogLog: LogEvent {
/// Names the catalog's timed span.
/// `Sendable` is spelled out because this is a `public` nested type — unlike
/// the internal `SpanName`s elsewhere, it gets no inferred conformance, and
/// `LogEvent.SpanName` requires one.
public enum SpanName: Hashable, Sendable {
public enum SpanName: Hashable, Sendable, CustomStringConvertible {
/// The `.source` build: decoding *every* catalog region's GeoJSON at full
/// authored fidelity, which is far heavier than attribution's tracked
/// subset. Runs once per process behind the cache actor, so this span is
/// what the viewer's first toggle to source actually costs.
case buildSourceOutlines
/// The first request for one region's drawable outlines. Later requests
/// reuse the per-region cache.
case loadRegionOutlines(Region)

public var description: String {
switch self {
case .buildSourceOutlines: "buildSourceOutlines"
case let .loadRegionOutlines(region):
"loadRegionOutlines(\(region.rawValue))"
}
}
}

/// Loading the outlines for a `RegionGeometryKind` failed.
case loadFailed(kind: String, description: String)
/// Loading the bundled outlines used by region-specific artwork failed.
/// Bundled geometry is a programmer-owned invariant, so this is a fault.
case regionLoadFailed(region: Region, description: String)

public static let eventName = "RegionGeometryCatalog"

public var level: LogLevel {
.warning
switch self {
case .loadFailed: .warning
case .regionLoadFailed: .fault
}
}

public var message: String {
switch self {
case let .loadFailed(kind, description):
"Region map viewer failed to load \(kind) geometry: \(description)"
case let .regionLoadFailed(region, description):
"Failed to load drawable outlines for \(region.rawValue): \(description)"
}
}

public var externalID: String? {
switch self {
case .loadFailed:
nil
case let .regionLoadFailed(region, _):
region.regionURL.absoluteString
}
}
}
4 changes: 2 additions & 2 deletions Where/RegionKit/Sources/Logging/RegionLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public enum RegionLog {
/// `RegionCatalog` — the bundled `regions.json` manifest load.
static let catalog = root(RegionCatalogLog.self)

/// `RegionGeometryCatalog` — the developer region-map viewer's geometry
/// load. Public because the viewer lives in WhereUI, above RegionKit.
/// `RegionGeometryCatalog` — drawable geometry for region artwork and the
/// developer map viewer. Public because both consumers live above RegionKit.
public static let geometryCatalog = root(RegionGeometryCatalogLog.self)
}
83 changes: 70 additions & 13 deletions Where/RegionKit/Sources/RegionGeometryCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ public struct RegionOutline: Identifiable, Sendable, Hashable {
}
}

/// Failure decoding bundled region geometry. Surfaced (never swallowed)
/// so the viewer can show a real error state instead of an empty map.
/// Failure decoding bundled region geometry. Surfaced (never swallowed) so
/// developer tools can show a real error state and production artwork can log
/// a broken bundled-resource invariant instead of silently drawing nothing.
public enum RegionGeometryError: Error {
case missingResource(String)
case emptyResource(String)
}

extension RegionGeometryError: LocalizedError {
Expand All @@ -62,15 +64,38 @@ extension RegionGeometryError: LocalizedError {
switch self {
case let .missingResource(resource):
"Missing bundled region geometry resource “\(resource).geojson”."
case let .emptyResource(resource):
"Bundled region geometry resource “\(resource).geojson” contains no drawable outlines."
}
}
}

/// Read-only catalog of region boundary geometry for the developer
/// region-map viewer. The single public entry point is
/// ``outlines(for:)``; UI never touches `RegionAttributor`'s internal
/// Read-only catalog of region boundary geometry for region artwork and the
/// developer region-map viewer. UI never touches `RegionAttributor`'s internal
/// polygons or the `GeoJSON` decoder directly.
public enum RegionGeometryCatalog {
/// Drawable outlines for one region, cached after the first request.
///
/// This is the lightweight path for region-specific UI artwork: it decodes
/// only `region` rather than the full source catalog. `.other` returns an
/// empty array because it intentionally has no geometry. Missing, corrupt,
/// or empty bundled geometry logs a fault and asserts in debug; release
/// builds safely omit the decorative outline.
public static func outlines(for region: Region) async -> [RegionOutline] {
guard region != .other else { return [] }
do {
return try await RegionCache.shared.outlines(for: region)
} catch {
RegionLog.geometryCatalog(attachments: [.error(error, name: "geometry-error")]) {
.regionLoadFailed(region: region, description: error.localizedDescription)
}
assertionFailure(
"Failed to load drawable outlines for \(region.rawValue): \(error.localizedDescription)",
)
return []
}
}

/// Drawable outlines for `kind`.
///
/// - `.attribution` reflects exactly what `attributor` loaded (the tracked
Expand All @@ -79,14 +104,10 @@ public enum RegionGeometryCatalog {
/// - `.source` decodes every available region from the catalog and ignores
/// `attributor`.
///
/// The file read + JSON decode runs **off the main thread**:
/// `RegionGeometryCatalog` is a plain (non-`@MainActor`) type and
/// this method is `nonisolated`, so `await`-ing it from a
/// `@MainActor` view hops to the cooperative pool (and, for
/// `.source`, the cache actor) to decode, then returns the
/// `Sendable` result back to the main actor. Throws
/// `RegionGeometryError` / a `DecodingError` rather than absorbing a
/// missing or malformed bundle into an empty list.
/// `.source` decoding runs on its cache actor. `.attribution` maps the
/// caller-provided attributor on the caller's actor because it is already
/// resolved in memory. Throws `RegionGeometryError` / a `DecodingError`
/// rather than absorbing a missing or malformed bundle into an empty list.
public static func outlines(
for kind: RegionGeometryKind,
attributor: RegionAttributor,
Expand Down Expand Up @@ -147,6 +168,27 @@ public enum RegionGeometryCatalog {
return try GeoJSON.namedPolygons(at: url)
}

/// Decode one region for card/overlay artwork without loading unrelated
/// catalog entries.
private static func buildRegionOutlines(for region: Region) throws -> [RegionOutline] {
try RegionLog.geometryCatalog.measure(.loadRegionOutlines(region), budget: .seconds(1)) {
var builder = OutlineBuilder()
for feature in try namedPolygons(for: region) {
for polygon in feature.polygons {
builder.add(
title: region.localizedName,
region: region,
coordinates: polygon.vertices,
)
}
}
guard !builder.outlines.isEmpty else {
throw RegionGeometryError.emptyResource(region.rawValue)
}
return builder.outlines
}
}

/// Caches the heavy `.source` decode (parsing every per-region file) so
/// toggling back to source after the first load is instant. An `actor` both
/// serializes the one-time build and runs it off the main thread.
Expand All @@ -161,6 +203,21 @@ public enum RegionGeometryCatalog {
return built
}
}

/// Per-region cache for UI artwork. Actor isolation serializes simultaneous
/// requests for the same first-use decode without introducing a global
/// mutable registry in the UI layer.
private actor RegionCache {
static let shared = RegionCache()
private var cached: [Region: [RegionOutline]] = [:]

func outlines(for region: Region) throws -> [RegionOutline] {
if let cached = cached[region] { return cached }
let built = try RegionGeometryCatalog.buildRegionOutlines(for: region)
cached[region] = built
return built
}
}
}

/// Accumulates `RegionOutline`s while assigning each a unique, stable
Expand Down
Loading
Loading