fix(settings): declare ui.card_overlays for native clients (#530)

The Apple and Android clients are moving onto the canonical
ui.card_overlays key (silo-apple#118, silo-android#158), so the
manifest's advisory platforms metadata now lists them alongside web.
Revision bumped to 4; Go/TS bindings and the conformance fixture
regenerated. Client-repo bindings will pick the bump up on their next
`make settings-bindings` run — nothing else in the manifest changed.

Also stops the web client from erasing the two schema-legal ribbon ids
it does not render yet: buildItems() rebuilt `items` from the web
registry alone, so a stored `imdb_top_250` / `rt_certified_fresh`
config (authorable from Android's native editor, and valid per
card-overlays.json) vanished from both `items` and `order` on the next
web save. Those ids now pass through parse/serialize untouched, on the
same defaults the native registries use, while remaining invisible to
web rendering and settings.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Quick
2026-08-02 09:54:42 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent 7aee29be7b
commit 97669d52d4
6 changed files with 30 additions and 12 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"fixture_version": 1,
"manifest_revision": 3,
"manifest_revision": 4,
"description": "Cross-platform conformance cases for settings resolution. Every case runs against the shipped manifest in this directory: definitions are referenced by key, never restated, so an expectation can only be satisfied by resolving the real contract. Each platform's resolver (Go in internal/settingsresolve, TypeScript in web/src/lib/settingsResolve.ts, Kotlin and Swift in the client repos) runs every case through a hand-written runner; a runner must fail on any fixture field it does not know, because schema drift in the fixture itself is drift. A case's constraint_bindings attach a constraint to a copy of a real definition so constraint semantics stay testable even while no shipped definition carries that constraint kind. In expected entries, constrained:true requires stored_value and constraint_kind to be present, and stored_value may be null to mean the authored value was JSON null.",
"cases": [
{
+2 -2
View File
@@ -1,6 +1,6 @@
{
"api_version": 1,
"revision": 3,
"revision": 4,
"option_sets": {
"playback_audio_languages": {
"type": "language_tag",
@@ -801,7 +801,7 @@
"nullable": true
},
"default_value": null,
"platforms": ["web"],
"platforms": ["web", "ios", "tvos", "macos", "android", "android_tv"],
"category": "appearance",
"label": "Poster badges",
"description": "Which badges appear on poster cards, and where.",
+1 -1
View File
@@ -9,7 +9,7 @@
package settingskeys
// Revision is the manifest revision these bindings were generated from.
const Revision = 3
const Revision = 4
// Setting keys, one constant per definition.
const (
+23 -5
View File
@@ -13,6 +13,22 @@ export function buildDefaultPrefs(): CardOverlayPrefs {
return { version: 2, preset: "classic", order: [], items: buildItems(undefined) };
}
// Ids that are legal in the contract schema (card-overlays.json) but have no
// registry entry yet because no API field backs them. The web client neither
// renders nor edits these, but their stored config must survive a round-trip:
// the native clients' settings UIs can author them, and dropping them here
// would erase another client's preference on the next web save. Their bases
// mirror the native registries' defaults (ribbons: top-right, disabled).
const PASSTHROUGH_IDS = ["imdb_top_250", "rt_certified_fresh"] as const satisfies readonly OverlayId[];
const PASSTHROUGH_BASE: OverlayItemConfig = { enabled: false, position: "top-right" };
function isKnownOverlayId(v: unknown): v is OverlayId {
return (
typeof v === "string" &&
(OVERLAY_MAP.has(v as OverlayId) || (PASSTHROUGH_IDS as readonly string[]).includes(v))
);
}
function isValidPosition(v: unknown): v is OverlayPosition {
return typeof v === "string" && (OVERLAY_POSITIONS as readonly string[]).includes(v);
}
@@ -61,6 +77,12 @@ function buildItems(
? applyItemPatch(base, entry as Record<string, unknown>)
: base;
}
for (const id of PASSTHROUGH_IDS) {
const entry = source?.[id];
if (entry && typeof entry === "object") {
items[id] = applyItemPatch(PASSTHROUGH_BASE, entry as Record<string, unknown>);
}
}
return items;
}
@@ -75,11 +97,7 @@ function parseV2(parsed: Record<string, unknown>): CardOverlayPrefs {
return {
version: 2,
preset: isValidPreset(parsed.preset) ? parsed.preset : "classic",
order: Array.isArray(parsed.order)
? (parsed.order as unknown[]).filter(
(id): id is OverlayId => typeof id === "string" && OVERLAY_MAP.has(id as OverlayId),
)
: [],
order: Array.isArray(parsed.order) ? (parsed.order as unknown[]).filter(isKnownOverlayId) : [],
items: buildItems(sourceItems),
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"fixture_version": 1,
"manifest_revision": 3,
"manifest_revision": 4,
"description": "Cross-platform conformance cases for settings resolution. Every case runs against the shipped manifest in this directory: definitions are referenced by key, never restated, so an expectation can only be satisfied by resolving the real contract. Each platform's resolver (Go in internal/settingsresolve, TypeScript in web/src/lib/settingsResolve.ts, Kotlin and Swift in the client repos) runs every case through a hand-written runner; a runner must fail on any fixture field it does not know, because schema drift in the fixture itself is drift. A case's constraint_bindings attach a constraint to a copy of a real definition so constraint semantics stay testable even while no shipped definition carries that constraint kind. In expected entries, constrained:true requires stored_value and constraint_kind to be present, and stored_value may be null to mean the authored value was JSON null.",
"cases": [
{
+2 -2
View File
@@ -8,7 +8,7 @@
* is a manifest change plus a regeneration, never a hand-written key.
*/
export const SETTINGS_REVISION = 3;
export const SETTINGS_REVISION = 4;
export interface SettingSuggestedOption {
value: string;
@@ -934,7 +934,7 @@ export const SETTING_DEFINITIONS: Record<SettingKey, SettingDefinition> = {
label: "Poster badges",
description: "Which badges appear on poster cards, and where.",
category: "appearance",
platforms: ["web"],
platforms: ["web", "ios", "tvos", "macos", "android", "android_tv"],
},
"ui.custom_css": {
key: "ui.custom_css",