diff --git a/frontend/e2e/local/worldbuilding-backgrounds.spec.ts b/frontend/e2e/local/worldbuilding-backgrounds.spec.ts index 019dec865..98239ce7d 100644 --- a/frontend/e2e/local/worldbuilding-backgrounds.spec.ts +++ b/frontend/e2e/local/worldbuilding-backgrounds.spec.ts @@ -2,7 +2,8 @@ * Worldbuilding Custom Background Tests - Local Mode * * Exercises the per-element background theming (Appearance panel) end to end: - * - toggles start unchecked when no region is configured + * - the demo template ships each element with themed gradient backgrounds + * - disabling both regions clears them and unchecks the toggles * - enabling a region with a solid colour applies it to the sidenav * - enabling a region with a gradient applies it to the content area * - backgrounds persist across a reload @@ -148,10 +149,23 @@ test.describe('Worldbuilding Custom Backgrounds', () => { const sidenav = page.getByTestId('wb-sidenav'); const content = page.getByTestId('wb-content'); - await test.step('toggles start unchecked when unconfigured', async () => { + await test.step('demo template ships themed gradient backgrounds', async () => { + await expect(menuToggle).toHaveAttribute('aria-checked', 'true'); + await expect(contentToggle).toHaveAttribute('aria-checked', 'true'); + await expect(sidenav).toHaveClass(/has-custom-background/); + await expect(content).toHaveClass(/has-custom-background/); + await expect + .poll(() => contentBgImage(page)) + .toContain('linear-gradient'); + }); + + await test.step('disabling both regions clears the template backgrounds', async () => { + await menuToggle.click(); + await contentToggle.click(); await expect(menuToggle).toHaveAttribute('aria-checked', 'false'); await expect(contentToggle).toHaveAttribute('aria-checked', 'false'); await expect(sidenav).not.toHaveClass(/has-custom-background/); + await expect(content).not.toHaveClass(/has-custom-background/); }); await test.step('solid colour applies to the sidenav', async () => { diff --git a/frontend/e2e/screenshots/worldbuilding-background-screenshots.spec.ts b/frontend/e2e/screenshots/worldbuilding-background-screenshots.spec.ts index b44e29a67..0422bce7c 100644 --- a/frontend/e2e/screenshots/worldbuilding-background-screenshots.spec.ts +++ b/frontend/e2e/screenshots/worldbuilding-background-screenshots.spec.ts @@ -105,14 +105,23 @@ test.describe('Worldbuilding Editor Custom Background Screenshots', () => { } /** - * Enable a region via its slide toggle. Material slide toggles are clicked - * rather than checked/unchecked. + * Enable a region via its slide toggle, starting from a fresh default + * setting. The demo template ships every element with themed gradients, so + * a region that is already on is switched off first; re-enabling resets it + * to the default auto-adjusted solid colour the helpers below expect. + * Material slide toggles are clicked rather than checked/unchecked. */ async function enableRegion( page: Page, region: 'menu' | 'content' ): Promise { - await page.getByTestId(`appearance-${region}-toggle`).click(); + const toggle = page.getByTestId(`appearance-${region}-toggle`); + if ((await toggle.getAttribute('aria-checked')) === 'true') { + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-checked', 'false'); + } + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-checked', 'true'); } /** diff --git a/frontend/public/assets/project-templates/worldbuilding-demo/schemas.json b/frontend/public/assets/project-templates/worldbuilding-demo/schemas.json index 95361b35b..186d39139 100644 --- a/frontend/public/assets/project-templates/worldbuilding-demo/schemas.json +++ b/frontend/public/assets/project-templates/worldbuilding-demo/schemas.json @@ -334,7 +334,21 @@ ] } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3d6f5 0%, #e7c0ed 55%, #e3abd0 100%)", + "dark": "linear-gradient(160deg, #1c112c 0%, #39193e 55%, #522342 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7f4fb 0%, #f4eaf6 55%, #f1dfeb 100%)", + "dark": "linear-gradient(160deg, #130d1c 0%, #231325 55%, #2e1927 100%)" + } + } }, { "id": "location-v1", @@ -550,7 +564,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5e6 0%, #c0ede5 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #112c1f 0%, #193e37 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf7 0%, #eaf6f4 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d1c14 0%, #132522 55%, #192b2e 100%)" + } + } }, { "id": "wb-item-v1", @@ -754,7 +782,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #f5e8d6 0%, #ede0c0 55%, #e3dbab 100%)", + "dark": "linear-gradient(150deg, #2c2111 0%, #3e3419 55%, #524c23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #fbf8f4 0%, #f6f2ea 55%, #f1eedf 100%)", + "dark": "linear-gradient(150deg, #1c160d 0%, #252013 55%, #2e2c19 100%)" + } + } }, { "id": "species-v1", @@ -1017,7 +1059,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3f5d6 0%, #c0edc2 55%, #abe3c7 100%)", + "dark": "linear-gradient(160deg, #1c2c11 0%, #193e1b 55%, #23523b 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7fbf4 0%, #eaf6ea 55%, #dff1e8 100%)", + "dark": "linear-gradient(160deg, #131c0d 0%, #132514 55%, #192e24 100%)" + } + } }, { "id": "organization-v1", @@ -1288,7 +1344,21 @@ "order": 6 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e3f5 0%, #c0c5ed 55%, #b4abe3 100%)", + "dark": "linear-gradient(160deg, #111c2c 0%, #191e3e 55%, #2b2352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f7fb 0%, #eaebf6 55%, #e2dff1 100%)", + "dark": "linear-gradient(160deg, #0d131c 0%, #131525 55%, #1d192e 100%)" + } + } }, { "id": "deity-v1", @@ -1520,7 +1590,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5edd6 0%, #edd6c0 55%, #e3b9ab 100%)", + "dark": "linear-gradient(170deg, #2c2511 0%, #3e2b19 55%, #522f23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf9f4 0%, #f6f0ea 55%, #f1e4df 100%)", + "dark": "linear-gradient(170deg, #1c180d 0%, #251c13 55%, #2e1e19 100%)" + } + } }, { "id": "language-v1", @@ -1736,7 +1820,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f0f5 0%, #c0d6ed 55%, #abb4e3 100%)", + "dark": "linear-gradient(160deg, #11282c 0%, #192b3e 55%, #232b52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fafb 0%, #eaf0f6 55%, #dfe2f1 100%)", + "dark": "linear-gradient(160deg, #0d191c 0%, #131c25 55%, #191d2e 100%)" + } + } }, { "id": "material-v1", @@ -1943,7 +2041,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #f5e0d6 0%, #edd6c0 55%, #e3d0ab 100%)", + "dark": "linear-gradient(150deg, #2c1a11 0%, #3e2b19 55%, #524223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #fbf6f4 0%, #f6f0ea 55%, #f1ebdf 100%)", + "dark": "linear-gradient(150deg, #1c120d 0%, #251c13 55%, #2e2719 100%)" + } + } }, { "id": "technology-v1", @@ -2110,7 +2222,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6ebf5 0%, #c0e7ed 55%, #abe3de 100%)", + "dark": "linear-gradient(160deg, #11232c 0%, #19393e 55%, #23524e 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f8fb 0%, #eaf4f6 55%, #dff1ef 100%)", + "dark": "linear-gradient(160deg, #0d171c 0%, #132325 55%, #192e2d 100%)" + } + } }, { "id": "magic-system-v1", @@ -2313,7 +2439,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #ebd6f5 0%, #c7c0ed 55%, #abbee3 100%)", + "dark": "linear-gradient(160deg, #23112c 0%, #1f193e 55%, #233352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f8f4fb 0%, #eceaf6 55%, #dfe5f1 100%)", + "dark": "linear-gradient(160deg, #170d1c 0%, #161325 55%, #19202e 100%)" + } + } }, { "id": "spell-v1", @@ -2480,7 +2620,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6f5 0%, #dec0ed 55%, #beabe3 100%)", + "dark": "linear-gradient(160deg, #2c112c 0%, #31193e 55%, #332352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4fb 0%, #f2eaf6 55%, #e5dff1 100%)", + "dark": "linear-gradient(160deg, #1c0d1c 0%, #1f1325 55%, #20192e 100%)" + } + } }, { "id": "condition-v1", @@ -2672,7 +2826,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6db 0%, #edc1c0 55%, #e3b6ab 100%)", + "dark": "linear-gradient(160deg, #2c1115 0%, #3e1a19 55%, #522d23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4f5 0%, #f6eaea 55%, #f1e3df 100%)", + "dark": "linear-gradient(160deg, #1c0d10 0%, #251313 55%, #2e1d19 100%)" + } + } }, { "id": "profession-v1", @@ -2863,7 +3031,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e6d6 0%, #ede1c0 55%, #e3e3ab 100%)", + "dark": "linear-gradient(160deg, #2c1f11 0%, #3e3419 55%, #525223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf7f4 0%, #f6f3ea 55%, #f1f1df 100%)", + "dark": "linear-gradient(160deg, #1c140d 0%, #252113 55%, #2e2e19 100%)" + } + } }, { "id": "title-v1", @@ -3011,7 +3193,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5edd6 0%, #edc9c0 55%, #e3abbe 100%)", + "dark": "linear-gradient(170deg, #2c2511 0%, #3e2119 55%, #522333 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf9f4 0%, #f6ecea 55%, #f1dfe5 100%)", + "dark": "linear-gradient(170deg, #1c180d 0%, #251713 55%, #2e1920 100%)" + } + } }, { "id": "tradition-v1", @@ -3174,7 +3370,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5dbd6 0%, #edc0c3 55%, #e3abbe 100%)", + "dark": "linear-gradient(160deg, #2c1511 0%, #3e191c 55%, #522333 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf5f4 0%, #f6eaeb 55%, #f1dfe5 100%)", + "dark": "linear-gradient(160deg, #1c100d 0%, #251315 55%, #2e1920 100%)" + } + } }, { "id": "ethnicity-v1", @@ -3363,7 +3573,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e3d6 0%, #edcbc0 55%, #e3b0ab 100%)", + "dark": "linear-gradient(160deg, #2c1c11 0%, #3e2219 55%, #522723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf7f4 0%, #f6edea 55%, #f1e1df 100%)", + "dark": "linear-gradient(160deg, #1c130d 0%, #251813 55%, #2e1b19 100%)" + } + } }, { "id": "conflict-v1", @@ -3576,7 +3800,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5d6d6 0%, #edc9c0 55%, #e3c2ab 100%)", + "dark": "linear-gradient(170deg, #2c1111 0%, #3e2119 55%, #523723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf4f4 0%, #f6ecea 55%, #f1e7df 100%)", + "dark": "linear-gradient(170deg, #1c0d0d 0%, #251713 55%, #2e2219 100%)" + } + } }, { "id": "myth-v1", @@ -3741,7 +3979,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #dbd6f5 0%, #d6c0ed 55%, #daabe3 100%)", + "dark": "linear-gradient(160deg, #15112c 0%, #2b193e 55%, #4a2352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5f4fb 0%, #f0eaf6 55%, #eedff1 100%)", + "dark": "linear-gradient(160deg, #100d1c 0%, #1c1325 55%, #2b192e 100%)" + } + } }, { "id": "vehicle-v1", @@ -3911,7 +4163,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e5f5 0%, #c0deed 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #111f2c 0%, #19313e 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f7fb 0%, #eaf2f6 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d141c 0%, #131f25 55%, #192b2e 100%)" + } + } }, { "id": "event-v1", @@ -4072,7 +4338,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5f2d6 0%, #ede0c0 55%, #e3c7ab 100%)", + "dark": "linear-gradient(160deg, #2c2a11 0%, #3e3319 55%, #523b23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbfaf4 0%, #f6f2ea 55%, #f1e8df 100%)", + "dark": "linear-gradient(160deg, #1c1a0d 0%, #252013 55%, #2e2419 100%)" + } + } }, { "id": "lore-v1", @@ -4240,7 +4520,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5ebd6 0%, #edd8c0 55%, #e3c2ab 100%)", + "dark": "linear-gradient(160deg, #2c2311 0%, #3e2d19 55%, #523723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf8f4 0%, #f6f0ea 55%, #f1e7df 100%)", + "dark": "linear-gradient(160deg, #1c170d 0%, #251d13 55%, #2e2219 100%)" + } + } }, { "id": "building-v1", @@ -4446,7 +4740,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5ded6 0%, #edd1c0 55%, #e3c9ab 100%)", + "dark": "linear-gradient(160deg, #2c1811 0%, #3e2719 55%, #523c23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf6f4 0%, #f6eeea 55%, #f1e9df 100%)", + "dark": "linear-gradient(160deg, #1c110d 0%, #251a13 55%, #2e2419 100%)" + } + } }, { "id": "settlement-v1", @@ -4696,7 +5004,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e8d6 0%, #ebedc0 55%, #c7e3ab 100%)", + "dark": "linear-gradient(160deg, #2c2111 0%, #3c3e19 55%, #3b5223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf8f4 0%, #f5f6ea 55%, #e8f1df 100%)", + "dark": "linear-gradient(160deg, #1c160d 0%, #242513 55%, #242e19 100%)" + } + } }, { "id": "geographic-feature-v1", @@ -4860,7 +5182,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5db 0%, #c0edd6 55%, #abe3da 100%)", + "dark": "linear-gradient(160deg, #112c15 0%, #193e2b 55%, #23524a 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf5 0%, #eaf6f0 55%, #dff1ee 100%)", + "dark": "linear-gradient(160deg, #0d1c10 0%, #13251c 55%, #192e2b 100%)" + } + } }, { "id": "region-v1", @@ -5054,7 +5390,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5ed 0%, #c0edd1 55%, #abe3ab 100%)", + "dark": "linear-gradient(160deg, #112c25 0%, #193e27 55%, #235223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf9 0%, #eaf6ee 55%, #dff1df 100%)", + "dark": "linear-gradient(160deg, #0d1c18 0%, #13251a 55%, #192e19 100%)" + } + } }, { "id": "country-v1", @@ -5345,7 +5695,21 @@ "order": 5 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6dbf5 0%, #c0d2ed 55%, #abd0e3 100%)", + "dark": "linear-gradient(160deg, #11152c 0%, #19283e 55%, #234252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f5fb 0%, #eaeff6 55%, #dfebf1 100%)", + "dark": "linear-gradient(160deg, #0d101c 0%, #131b25 55%, #19272e 100%)" + } + } }, { "id": "landmark-v1", @@ -5496,7 +5860,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5edd6 0%, #c0edc2 55%, #abd0e3 100%)", + "dark": "linear-gradient(160deg, #2c2511 0%, #193e1b 55%, #234252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf9f4 0%, #eaf6ea 55%, #dfebf1 100%)", + "dark": "linear-gradient(160deg, #1c180d 0%, #132514 55%, #19272e 100%)" + } + } }, { "id": "planet-v1", @@ -5665,7 +6043,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e0f5 0%, #c7c0ed 55%, #d0abe3 100%)", + "dark": "linear-gradient(160deg, #111a2c 0%, #1f193e 55%, #422352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f6fb 0%, #eceaf6 55%, #ebdff1 100%)", + "dark": "linear-gradient(160deg, #0d121c 0%, #161325 55%, #27192e 100%)" + } + } }, { "id": "plane-v1", @@ -5864,6 +6256,20 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6f5 0%, #c0c0ed 55%, #abe3e3 100%)", + "dark": "linear-gradient(160deg, #2c112c 0%, #19193e 55%, #235252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4fb 0%, #eaeaf6 55%, #dff1f1 100%)", + "dark": "linear-gradient(160deg, #1c0d1c 0%, #131325 55%, #192e2e 100%)" + } + } } ] diff --git a/frontend/public/assets/project-templates/worldbuilding-demo/worldbuilding.json b/frontend/public/assets/project-templates/worldbuilding-demo/worldbuilding.json index 0cf6134b0..11529327d 100644 --- a/frontend/public/assets/project-templates/worldbuilding-demo/worldbuilding.json +++ b/frontend/public/assets/project-templates/worldbuilding-demo/worldbuilding.json @@ -72,6 +72,20 @@ "Bottomless satchel", "Moonstone pendant (family heirloom)" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3d6f5 0%, #e7c0ed 55%, #e3abd0 100%)", + "dark": "linear-gradient(160deg, #1c112c 0%, #39193e 55%, #522342 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7f4fb 0%, #f4eaf6 55%, #f1dfeb 100%)", + "dark": "linear-gradient(160deg, #130d1c 0%, #231325 55%, #2e1927 100%)" + } } }, { @@ -146,6 +160,20 @@ "Hunting knife", "Healer's kit" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3d6f5 0%, #e7c0ed 55%, #e3abd0 100%)", + "dark": "linear-gradient(160deg, #1c112c 0%, #39193e 55%, #522342 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7f4fb 0%, #f4eaf6 55%, #f1dfeb 100%)", + "dark": "linear-gradient(160deg, #130d1c 0%, #231325 55%, #2e1927 100%)" + } } }, { @@ -221,6 +249,20 @@ "Master's tools", "The gold locket (contains something important)" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3d6f5 0%, #e7c0ed 55%, #e3abd0 100%)", + "dark": "linear-gradient(160deg, #1c112c 0%, #39193e 55%, #522342 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7f4fb 0%, #f4eaf6 55%, #f1dfeb 100%)", + "dark": "linear-gradient(160deg, #130d1c 0%, #231325 55%, #2e1927 100%)" + } } }, { @@ -263,6 +305,20 @@ "The Thornwood Accord (peace treaty with forest folk)" ], "history.background": "Founded by miners seeking silver, the town outlasted its mines through adaptability. When the silver ran out, the people turned to trade and crafts. The Thornwood Accord, negotiated 200 years ago, established peaceful relations with the forest's denizens and opened profitable trade routes." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5e6 0%, #c0ede5 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #112c1f 0%, #193e37 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf7 0%, #eaf6f4 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d1c14 0%, #132522 55%, #192b2e 100%)" + } } }, { @@ -305,6 +361,20 @@ "The Night of Walking Trees (70 years ago)" ], "history.background": "The forest is a remnant of the great primordial woods that once covered the continent. It survived the Sundering through the sacrifice of its most powerful guardians. It's said the Library of Moonveil is hidden somewhere in its depths." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5e6 0%, #c0ede5 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #112c1f 0%, #193e37 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf7 0%, #eaf6f4 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d1c14 0%, #132522 55%, #192b2e 100%)" + } } }, { @@ -347,6 +417,20 @@ "The Great Reformation (100 years ago)" ], "history.background": "Founded by the legendary Archmage Aldric the Grey, Cloudspire was originally a mountaintop fortress. When war threatened, Aldric raised the entire complex into the sky to ensure knowledge would survive. It has floated ever since, accessible only by magic or the rare visiting airship." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5e6 0%, #c0ede5 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #112c1f 0%, #193e37 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf7 0%, #eaf6f4 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d1c14 0%, #132522 55%, #192b2e 100%)" + } } }, { @@ -375,6 +459,20 @@ "Thornwood fox (common ancestor)" ], "relations.notes": "The moonshadow fox is considered a good omen by the Woodfolk." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3f5d6 0%, #c0edc2 55%, #abe3c7 100%)", + "dark": "linear-gradient(160deg, #1c2c11 0%, #193e1b 55%, #23523b 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7fbf4 0%, #eaf6ea 55%, #dff1e8 100%)", + "dark": "linear-gradient(160deg, #131c0d 0%, #132514 55%, #192e24 100%)" + } } }, { @@ -404,6 +502,20 @@ "Elder Thornfolk" ], "relations.notes": "Theron Blackwood has negotiated peace between the Thornfolk and Silverhollow." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3f5d6 0%, #c0edc2 55%, #abe3c7 100%)", + "dark": "linear-gradient(160deg, #1c2c11 0%, #193e1b 55%, #23523b 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7fbf4 0%, #eaf6ea 55%, #dff1e8 100%)", + "dark": "linear-gradient(160deg, #131c0d 0%, #132514 55%, #192e24 100%)" + } } }, { @@ -457,6 +569,20 @@ "The founding of Ironhold", "The sale of weapons during the Border Wars" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e3f5 0%, #c0c5ed 55%, #b4abe3 100%)", + "dark": "linear-gradient(160deg, #111c2c 0%, #191e3e 55%, #2b2352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f7fb 0%, #eaebf6 55%, #e2dff1 100%)", + "dark": "linear-gradient(160deg, #0d131c 0%, #131525 55%, #1d192e 100%)" + } } }, { @@ -505,6 +631,20 @@ "The founding of the Moon Temple", "Refugee sheltering during the Thornwood War" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e3f5 0%, #c0c5ed 55%, #b4abe3 100%)", + "dark": "linear-gradient(160deg, #111c2c 0%, #191e3e 55%, #2b2352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f7fb 0%, #eaebf6 55%, #e2dff1 100%)", + "dark": "linear-gradient(160deg, #0d131c 0%, #131525 55%, #1d192e 100%)" + } } }, { @@ -555,6 +695,20 @@ "history.origin": "Born from the first moonrise after the Sundering, when the shattered sky healed into the silver disc.", "history.myths": "It is said she once traded her memories for a lost child's life, and now keeps the world's forgotten stories instead of her own.", "history.notes": "Elara Nightwhisper believes the Library of Moonveil is guarded by one of Lunara's avatars." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5edd6 0%, #edd6c0 55%, #e3b9ab 100%)", + "dark": "linear-gradient(170deg, #2c2511 0%, #3e2b19 55%, #522f23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf9f4 0%, #f6f0ea 55%, #f1e4df 100%)", + "dark": "linear-gradient(170deg, #1c180d 0%, #251c13 55%, #2e1e19 100%)" + } } }, { @@ -596,6 +750,20 @@ "The Great Reformation of Cloudspire" ], "history.notes": "Elara's thesis on pre-Sundering weaving techniques is still the definitive text." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #ebd6f5 0%, #c7c0ed 55%, #abbee3 100%)", + "dark": "linear-gradient(160deg, #23112c 0%, #1f193e 55%, #233352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f8f4fb 0%, #eceaf6 55%, #dfe5f1 100%)", + "dark": "linear-gradient(160deg, #170d1c 0%, #161325 55%, #19202e 100%)" + } } }, { @@ -613,6 +781,20 @@ "summary": "Creates a globe of gentle moonlight and whispers a calming word to one creature, granting clarity.", "effect.description": "The caster's hand glows with soft silver light, illuminating a 30-foot radius. If used on a willing creature, they feel their fears quiet and gain a clearer mind for the duration.", "effect.higherLevels": "At higher circles, the light reveals hidden writing and dispels minor illusions." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6f5 0%, #dec0ed 55%, #beabe3 100%)", + "dark": "linear-gradient(160deg, #2c112c 0%, #31193e 55%, #332352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4fb 0%, #f2eaf6 55%, #e5dff1 100%)", + "dark": "linear-gradient(160deg, #1c0d1c 0%, #1f1325 55%, #20192e 100%)" + } } }, { @@ -638,6 +820,20 @@ "significance.importance": "The single most important repository of pre-Sundering knowledge. Its location would reshape politics, magic, and religion.", "significance.location": "Unknown — rumoured to be hidden somewhere in Thornwood Forest, guarded by a white doe.", "significance.notes": "The Church of the Silver Moon quietly funds Elara's search." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5ebd6 0%, #edd8c0 55%, #e3c2ab 100%)", + "dark": "linear-gradient(160deg, #2c2311 0%, #3e2d19 55%, #523723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf8f4 0%, #f6f0ea 55%, #f1e7df 100%)", + "dark": "linear-gradient(160deg, #1c170d 0%, #251d13 55%, #2e2219 100%)" + } } }, { @@ -665,6 +861,20 @@ "The Woodfolk version features the Thornfolk Elder instead of a human hunter" ], "significance.truth": "Historians believe a real treaty was signed near the Heart Grove; the magical details are likely embellishment." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #dbd6f5 0%, #d6c0ed 55%, #daabe3 100%)", + "dark": "linear-gradient(160deg, #15112c 0%, #2b193e 55%, #4a2352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5f4fb 0%, #f0eaf6 55%, #eedff1 100%)", + "dark": "linear-gradient(160deg, #100d1c 0%, #1c1325 55%, #2b192e 100%)" + } } }, { @@ -692,6 +902,20 @@ ], "significance.notes": "Cloudspire Academy floats above the highest peak, accessible by magic or airship.", "significanceSummary": "Source of moonsilver and home to Ironhold and the dwarf clans" + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5db 0%, #c0edd6 55%, #abe3da 100%)", + "dark": "linear-gradient(160deg, #112c15 0%, #193e2b 55%, #23524a 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf5 0%, #eaf6f0 55%, #dff1ee 100%)", + "dark": "linear-gradient(160deg, #0d1c10 0%, #13251c 55%, #192e2b 100%)" + } } }, { @@ -729,6 +953,20 @@ "The founding of Silverhollow", "The Great Fire of 312" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5ed 0%, #c0edd1 55%, #abe3ab 100%)", + "dark": "linear-gradient(160deg, #112c25 0%, #193e27 55%, #235223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf9 0%, #eaf6ee 55%, #dff1df 100%)", + "dark": "linear-gradient(160deg, #0d1c18 0%, #13251a 55%, #192e19 100%)" + } } }, { @@ -778,6 +1016,20 @@ "The Great Reformation of Cloudspire" ], "history.notes": "The Moonveil Accord, signed 200 years ago, remains the cornerstone of Eldorian foreign policy." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6dbf5 0%, #c0d2ed 55%, #abd0e3 100%)", + "dark": "linear-gradient(160deg, #11152c 0%, #19283e 55%, #234252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f5fb 0%, #eaeff6 55%, #dfebf1 100%)", + "dark": "linear-gradient(160deg, #0d101c 0%, #131b25 55%, #19272e 100%)" + } } }, { @@ -817,6 +1069,20 @@ "The secession of Mira Stonehart" ], "history.notes": "Mira was born here and is still wanted by the Council for leaving without leave." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e8d6 0%, #ebedc0 55%, #c7e3ab 100%)", + "dark": "linear-gradient(160deg, #2c2111 0%, #3c3e19 55%, #3b5223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf8f4 0%, #f5f6ea 55%, #e8f1df 100%)", + "dark": "linear-gradient(160deg, #1c160d 0%, #242513 55%, #242e19 100%)" + } } }, { @@ -846,6 +1112,20 @@ "history.origin": "The Woodfolk descend from the pre-Sundering peoples who sheltered inside the forest during the catastrophe.", "history.relations": "Allies of the Thornfolk; wary but increasingly friendly with Silverhollow after the Thornwood Accord.", "history.notes": "Grandmother Moss, the forest witch who trained Theron, is the eldest known Woodfolk elder." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e3d6 0%, #edcbc0 55%, #e3b0ab 100%)", + "dark": "linear-gradient(160deg, #2c1c11 0%, #3e2219 55%, #522723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf7f4 0%, #f6edea 55%, #f1e1df 100%)", + "dark": "linear-gradient(160deg, #1c130d 0%, #251813 55%, #2e1b19 100%)" + } } }, { @@ -871,6 +1151,20 @@ ], "history.notes": "Silverhollow's festival is the largest; its lanterns reflect in the Silver Fountain.", "significanceSummary": "Honours Lunara and the memory of the Accord; marks the harvest's end." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5dbd6 0%, #edc0c3 55%, #e3abbe 100%)", + "dark": "linear-gradient(160deg, #2c1511 0%, #3e191c 55%, #522333 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf5f4 0%, #f6eaeb 55%, #f1dfe5 100%)", + "dark": "linear-gradient(160deg, #1c100d 0%, #251315 55%, #2e1920 100%)" + } } }, { @@ -901,6 +1195,20 @@ "\"The long disagreement\" — euphemism for war" ], "vocabulary.naming": "Names are two syllables: a nature-word and a relation-word (e.g., Ash-mir, \"quiet autumn\")." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f0f5 0%, #c0d6ed 55%, #abb4e3 100%)", + "dark": "linear-gradient(160deg, #11282c 0%, #192b3e 55%, #232b52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fafb 0%, #eaf0f6 55%, #dfe2f1 100%)", + "dark": "linear-gradient(160deg, #0d191c 0%, #131c25 55%, #191d2e 100%)" + } } }, { @@ -940,6 +1248,20 @@ ], "aftermath.treaties": "The Moonveil Accord (1219 M.R.) — signed at Cloudspire Academy, brokered by the Archmage Council.", "aftermath.legacy": "Theron Blackwood deserted during this war; the scars of it still shape border politics." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5d6d6 0%, #edc9c0 55%, #e3c2ab 100%)", + "dark": "linear-gradient(170deg, #2c1111 0%, #3e2119 55%, #523723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf4f4 0%, #f6ecea 55%, #f1e7df 100%)", + "dark": "linear-gradient(170deg, #1c0d0d 0%, #251713 55%, #2e2219 100%)" + } } }, { @@ -971,6 +1293,20 @@ ], "significance.impact": "Ended the deadliest war of the century and created the institutions that still keep the peace.", "significance.remembered": "Commemorated by the Festival of the Silver Moon and the annual Ranger Oath." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5f2d6 0%, #ede0c0 55%, #e3c7ab 100%)", + "dark": "linear-gradient(160deg, #2c2a11 0%, #3e3319 55%, #523b23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbfaf4 0%, #f6f2ea 55%, #f1e8df 100%)", + "dark": "linear-gradient(160deg, #1c1a0d 0%, #252013 55%, #2e2419 100%)" + } } }, { @@ -998,6 +1334,20 @@ "The Moonblade's scabbard, the Silver Sheath" ], "variants.notes": "A copy hangs in the Moon Temple of Silverhollow; the true blade remains with the Crown." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #f5e8d6 0%, #ede0c0 55%, #e3dbab 100%)", + "dark": "linear-gradient(150deg, #2c2111 0%, #3e3419 55%, #524c23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #fbf8f4 0%, #f6f2ea 55%, #f1eedf 100%)", + "dark": "linear-gradient(150deg, #1c160d 0%, #252013 55%, #2e2c19 100%)" + } } }, { @@ -1028,6 +1378,20 @@ "The abandoned Dwarf Road" ], "sources.extraction": "Mined in narrow veins; the extraction is dangerous and slow, which keeps the price high." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #f5e0d6 0%, #edd6c0 55%, #e3d0ab 100%)", + "dark": "linear-gradient(150deg, #2c1a11 0%, #3e2b19 55%, #524223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #fbf6f4 0%, #f6f0ea 55%, #f1ebdf 100%)", + "dark": "linear-gradient(150deg, #1c120d 0%, #251c13 55%, #2e2719 100%)" + } } }, { @@ -1067,6 +1431,20 @@ "First flight across the Moonfall Pass", "The Starwing's record voyage to the Sea of Glass" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6ebf5 0%, #c0e7ed 55%, #abe3de 100%)", + "dark": "linear-gradient(160deg, #11232c 0%, #19393e 55%, #23524e 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f8fb 0%, #eaf4f6 55%, #dff1ef 100%)", + "dark": "linear-gradient(160deg, #0d171c 0%, #132325 55%, #192e2d 100%)" + } } }, { @@ -1102,6 +1480,20 @@ "history.notableCases": [ "The Poet of Moonfall Pass, who wrote his best work in the second phase" ] + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6db 0%, #edc1c0 55%, #e3b6ab 100%)", + "dark": "linear-gradient(160deg, #2c1115 0%, #3e1a19 55%, #522d23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4f5 0%, #f6eaea 55%, #f1e3df 100%)", + "dark": "linear-gradient(160deg, #1c0d10 0%, #251313 55%, #2e1d19 100%)" + } } }, { @@ -1142,6 +1534,20 @@ "Theron Blackwood" ], "notables.notes": "Theron is the de facto warden of Thornwood, though he holds no official rank." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e6d6 0%, #ede1c0 55%, #e3e3ab 100%)", + "dark": "linear-gradient(160deg, #2c1f11 0%, #3e3419 55%, #525223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf7f4 0%, #f6f3ea 55%, #f1f1df 100%)", + "dark": "linear-gradient(160deg, #1c140d 0%, #252113 55%, #2e2e19 100%)" + } } }, { @@ -1172,6 +1578,20 @@ "Theron Blackwood (current)" ], "history.notes": "Theron was chosen after saving the Heart Grove from a logging expedition — a deed the forest remembered." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5edd6 0%, #edc9c0 55%, #e3abbe 100%)", + "dark": "linear-gradient(170deg, #2c2511 0%, #3e2119 55%, #522333 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf9f4 0%, #f6ecea 55%, #f1dfe5 100%)", + "dark": "linear-gradient(170deg, #1c180d 0%, #251713 55%, #2e1920 100%)" + } } }, { @@ -1195,6 +1615,20 @@ "The Moonfall Pass rescue (1196)" ], "history.notes": "The Starwing is the last Cloud Runner captained by a Cloudspire graduate rather than a licensed merchant." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e5f5 0%, #c0deed 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #111f2c 0%, #19313e 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f7fb 0%, #eaf2f6 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d141c 0%, #131f25 55%, #192b2e 100%)" + } } }, { @@ -1218,6 +1652,20 @@ ], "astronomy.notes": "Some scholars believe the Shadow Moon is not a moon at all, but a wound in the sky from the Sundering.", "inhabitantsOverview": "Humans, dwarves, elves, Thornfolk, and countless other peoples" + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e0f5 0%, #c7c0ed 55%, #d0abe3 100%)", + "dark": "linear-gradient(160deg, #111a2c 0%, #1f193e 55%, #422352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f6fb 0%, #eceaf6 55%, #ebdff1 100%)", + "dark": "linear-gradient(160deg, #0d121c 0%, #161325 55%, #27192e 100%)" + } } }, { @@ -1251,6 +1699,20 @@ "portals.rules": "Gates open at the solstices. Entering requires an invitation; leaving requires a promise kept.", "portals.notes": "The Library of Moonveil is sometimes said to be not in the material world at all, but in a forgotten wing of the Court.", "inhabitantsOverview": "Fey, Thornfolk dreamers, and the court of the Summer King" + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6f5 0%, #c0c0ed 55%, #abe3e3 100%)", + "dark": "linear-gradient(160deg, #2c112c 0%, #19193e 55%, #235252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4fb 0%, #eaeaf6 55%, #dff1f1 100%)", + "dark": "linear-gradient(160deg, #1c0d1c 0%, #131325 55%, #192e2e 100%)" + } } }, { @@ -1268,6 +1730,20 @@ "occupation": "Elven Archivist (deceased)", "summary": "An elven archivist of the enclave near Thornwood Forest who fell in love with a human merchant. Her careful records of lunar magic are said to be preserved somewhere in the Library of Moonveil.", "notes": "Deceased - passed illness while Elara was young. Left behind her archivist's seal, which Elara still carries." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3d6f5 0%, #e7c0ed 55%, #e3abd0 100%)", + "dark": "linear-gradient(160deg, #1c112c 0%, #39193e 55%, #522342 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7f4fb 0%, #f4eaf6 55%, #f1dfeb 100%)", + "dark": "linear-gradient(160deg, #130d1c 0%, #231325 55%, #2e1927 100%)" + } } }, { @@ -1285,6 +1761,20 @@ "occupation": "Merchant (deceased)", "summary": "A human merchant whose trade caravans crossed the Silver Vales. He passed his restless curiosity to his daughter Elara before dying of old age.", "notes": "Deceased - died of old age while Elara was still studying at Cloudspire Academy." + }, + "appearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3d6f5 0%, #e7c0ed 55%, #e3abd0 100%)", + "dark": "linear-gradient(160deg, #1c112c 0%, #39193e 55%, #522342 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7f4fb 0%, #f4eaf6 55%, #f1dfeb 100%)", + "dark": "linear-gradient(160deg, #130d1c 0%, #231325 55%, #2e1927 100%)" + } } } ] diff --git a/frontend/public/assets/project-templates/worldbuilding-empty/schemas.json b/frontend/public/assets/project-templates/worldbuilding-empty/schemas.json index 95361b35b..186d39139 100644 --- a/frontend/public/assets/project-templates/worldbuilding-empty/schemas.json +++ b/frontend/public/assets/project-templates/worldbuilding-empty/schemas.json @@ -334,7 +334,21 @@ ] } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3d6f5 0%, #e7c0ed 55%, #e3abd0 100%)", + "dark": "linear-gradient(160deg, #1c112c 0%, #39193e 55%, #522342 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7f4fb 0%, #f4eaf6 55%, #f1dfeb 100%)", + "dark": "linear-gradient(160deg, #130d1c 0%, #231325 55%, #2e1927 100%)" + } + } }, { "id": "location-v1", @@ -550,7 +564,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5e6 0%, #c0ede5 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #112c1f 0%, #193e37 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf7 0%, #eaf6f4 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d1c14 0%, #132522 55%, #192b2e 100%)" + } + } }, { "id": "wb-item-v1", @@ -754,7 +782,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #f5e8d6 0%, #ede0c0 55%, #e3dbab 100%)", + "dark": "linear-gradient(150deg, #2c2111 0%, #3e3419 55%, #524c23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #fbf8f4 0%, #f6f2ea 55%, #f1eedf 100%)", + "dark": "linear-gradient(150deg, #1c160d 0%, #252013 55%, #2e2c19 100%)" + } + } }, { "id": "species-v1", @@ -1017,7 +1059,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #e3f5d6 0%, #c0edc2 55%, #abe3c7 100%)", + "dark": "linear-gradient(160deg, #1c2c11 0%, #193e1b 55%, #23523b 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f7fbf4 0%, #eaf6ea 55%, #dff1e8 100%)", + "dark": "linear-gradient(160deg, #131c0d 0%, #132514 55%, #192e24 100%)" + } + } }, { "id": "organization-v1", @@ -1288,7 +1344,21 @@ "order": 6 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e3f5 0%, #c0c5ed 55%, #b4abe3 100%)", + "dark": "linear-gradient(160deg, #111c2c 0%, #191e3e 55%, #2b2352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f7fb 0%, #eaebf6 55%, #e2dff1 100%)", + "dark": "linear-gradient(160deg, #0d131c 0%, #131525 55%, #1d192e 100%)" + } + } }, { "id": "deity-v1", @@ -1520,7 +1590,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5edd6 0%, #edd6c0 55%, #e3b9ab 100%)", + "dark": "linear-gradient(170deg, #2c2511 0%, #3e2b19 55%, #522f23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf9f4 0%, #f6f0ea 55%, #f1e4df 100%)", + "dark": "linear-gradient(170deg, #1c180d 0%, #251c13 55%, #2e1e19 100%)" + } + } }, { "id": "language-v1", @@ -1736,7 +1820,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f0f5 0%, #c0d6ed 55%, #abb4e3 100%)", + "dark": "linear-gradient(160deg, #11282c 0%, #192b3e 55%, #232b52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fafb 0%, #eaf0f6 55%, #dfe2f1 100%)", + "dark": "linear-gradient(160deg, #0d191c 0%, #131c25 55%, #191d2e 100%)" + } + } }, { "id": "material-v1", @@ -1943,7 +2041,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #f5e0d6 0%, #edd6c0 55%, #e3d0ab 100%)", + "dark": "linear-gradient(150deg, #2c1a11 0%, #3e2b19 55%, #524223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(150deg, #fbf6f4 0%, #f6f0ea 55%, #f1ebdf 100%)", + "dark": "linear-gradient(150deg, #1c120d 0%, #251c13 55%, #2e2719 100%)" + } + } }, { "id": "technology-v1", @@ -2110,7 +2222,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6ebf5 0%, #c0e7ed 55%, #abe3de 100%)", + "dark": "linear-gradient(160deg, #11232c 0%, #19393e 55%, #23524e 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f8fb 0%, #eaf4f6 55%, #dff1ef 100%)", + "dark": "linear-gradient(160deg, #0d171c 0%, #132325 55%, #192e2d 100%)" + } + } }, { "id": "magic-system-v1", @@ -2313,7 +2439,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #ebd6f5 0%, #c7c0ed 55%, #abbee3 100%)", + "dark": "linear-gradient(160deg, #23112c 0%, #1f193e 55%, #233352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f8f4fb 0%, #eceaf6 55%, #dfe5f1 100%)", + "dark": "linear-gradient(160deg, #170d1c 0%, #161325 55%, #19202e 100%)" + } + } }, { "id": "spell-v1", @@ -2480,7 +2620,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6f5 0%, #dec0ed 55%, #beabe3 100%)", + "dark": "linear-gradient(160deg, #2c112c 0%, #31193e 55%, #332352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4fb 0%, #f2eaf6 55%, #e5dff1 100%)", + "dark": "linear-gradient(160deg, #1c0d1c 0%, #1f1325 55%, #20192e 100%)" + } + } }, { "id": "condition-v1", @@ -2672,7 +2826,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6db 0%, #edc1c0 55%, #e3b6ab 100%)", + "dark": "linear-gradient(160deg, #2c1115 0%, #3e1a19 55%, #522d23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4f5 0%, #f6eaea 55%, #f1e3df 100%)", + "dark": "linear-gradient(160deg, #1c0d10 0%, #251313 55%, #2e1d19 100%)" + } + } }, { "id": "profession-v1", @@ -2863,7 +3031,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e6d6 0%, #ede1c0 55%, #e3e3ab 100%)", + "dark": "linear-gradient(160deg, #2c1f11 0%, #3e3419 55%, #525223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf7f4 0%, #f6f3ea 55%, #f1f1df 100%)", + "dark": "linear-gradient(160deg, #1c140d 0%, #252113 55%, #2e2e19 100%)" + } + } }, { "id": "title-v1", @@ -3011,7 +3193,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5edd6 0%, #edc9c0 55%, #e3abbe 100%)", + "dark": "linear-gradient(170deg, #2c2511 0%, #3e2119 55%, #522333 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf9f4 0%, #f6ecea 55%, #f1dfe5 100%)", + "dark": "linear-gradient(170deg, #1c180d 0%, #251713 55%, #2e1920 100%)" + } + } }, { "id": "tradition-v1", @@ -3174,7 +3370,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5dbd6 0%, #edc0c3 55%, #e3abbe 100%)", + "dark": "linear-gradient(160deg, #2c1511 0%, #3e191c 55%, #522333 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf5f4 0%, #f6eaeb 55%, #f1dfe5 100%)", + "dark": "linear-gradient(160deg, #1c100d 0%, #251315 55%, #2e1920 100%)" + } + } }, { "id": "ethnicity-v1", @@ -3363,7 +3573,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e3d6 0%, #edcbc0 55%, #e3b0ab 100%)", + "dark": "linear-gradient(160deg, #2c1c11 0%, #3e2219 55%, #522723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf7f4 0%, #f6edea 55%, #f1e1df 100%)", + "dark": "linear-gradient(160deg, #1c130d 0%, #251813 55%, #2e1b19 100%)" + } + } }, { "id": "conflict-v1", @@ -3576,7 +3800,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #f5d6d6 0%, #edc9c0 55%, #e3c2ab 100%)", + "dark": "linear-gradient(170deg, #2c1111 0%, #3e2119 55%, #523723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(170deg, #fbf4f4 0%, #f6ecea 55%, #f1e7df 100%)", + "dark": "linear-gradient(170deg, #1c0d0d 0%, #251713 55%, #2e2219 100%)" + } + } }, { "id": "myth-v1", @@ -3741,7 +3979,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #dbd6f5 0%, #d6c0ed 55%, #daabe3 100%)", + "dark": "linear-gradient(160deg, #15112c 0%, #2b193e 55%, #4a2352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5f4fb 0%, #f0eaf6 55%, #eedff1 100%)", + "dark": "linear-gradient(160deg, #100d1c 0%, #1c1325 55%, #2b192e 100%)" + } + } }, { "id": "vehicle-v1", @@ -3911,7 +4163,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e5f5 0%, #c0deed 55%, #abdae3 100%)", + "dark": "linear-gradient(160deg, #111f2c 0%, #19313e 55%, #234a52 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f7fb 0%, #eaf2f6 55%, #dfeef1 100%)", + "dark": "linear-gradient(160deg, #0d141c 0%, #131f25 55%, #192b2e 100%)" + } + } }, { "id": "event-v1", @@ -4072,7 +4338,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5f2d6 0%, #ede0c0 55%, #e3c7ab 100%)", + "dark": "linear-gradient(160deg, #2c2a11 0%, #3e3319 55%, #523b23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbfaf4 0%, #f6f2ea 55%, #f1e8df 100%)", + "dark": "linear-gradient(160deg, #1c1a0d 0%, #252013 55%, #2e2419 100%)" + } + } }, { "id": "lore-v1", @@ -4240,7 +4520,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5ebd6 0%, #edd8c0 55%, #e3c2ab 100%)", + "dark": "linear-gradient(160deg, #2c2311 0%, #3e2d19 55%, #523723 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf8f4 0%, #f6f0ea 55%, #f1e7df 100%)", + "dark": "linear-gradient(160deg, #1c170d 0%, #251d13 55%, #2e2219 100%)" + } + } }, { "id": "building-v1", @@ -4446,7 +4740,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5ded6 0%, #edd1c0 55%, #e3c9ab 100%)", + "dark": "linear-gradient(160deg, #2c1811 0%, #3e2719 55%, #523c23 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf6f4 0%, #f6eeea 55%, #f1e9df 100%)", + "dark": "linear-gradient(160deg, #1c110d 0%, #251a13 55%, #2e2419 100%)" + } + } }, { "id": "settlement-v1", @@ -4696,7 +5004,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5e8d6 0%, #ebedc0 55%, #c7e3ab 100%)", + "dark": "linear-gradient(160deg, #2c2111 0%, #3c3e19 55%, #3b5223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf8f4 0%, #f5f6ea 55%, #e8f1df 100%)", + "dark": "linear-gradient(160deg, #1c160d 0%, #242513 55%, #242e19 100%)" + } + } }, { "id": "geographic-feature-v1", @@ -4860,7 +5182,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5db 0%, #c0edd6 55%, #abe3da 100%)", + "dark": "linear-gradient(160deg, #112c15 0%, #193e2b 55%, #23524a 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf5 0%, #eaf6f0 55%, #dff1ee 100%)", + "dark": "linear-gradient(160deg, #0d1c10 0%, #13251c 55%, #192e2b 100%)" + } + } }, { "id": "region-v1", @@ -5054,7 +5390,21 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6f5ed 0%, #c0edd1 55%, #abe3ab 100%)", + "dark": "linear-gradient(160deg, #112c25 0%, #193e27 55%, #235223 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4fbf9 0%, #eaf6ee 55%, #dff1df 100%)", + "dark": "linear-gradient(160deg, #0d1c18 0%, #13251a 55%, #192e19 100%)" + } + } }, { "id": "country-v1", @@ -5345,7 +5695,21 @@ "order": 5 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6dbf5 0%, #c0d2ed 55%, #abd0e3 100%)", + "dark": "linear-gradient(160deg, #11152c 0%, #19283e 55%, #234252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f5fb 0%, #eaeff6 55%, #dfebf1 100%)", + "dark": "linear-gradient(160deg, #0d101c 0%, #131b25 55%, #19272e 100%)" + } + } }, { "id": "landmark-v1", @@ -5496,7 +5860,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5edd6 0%, #c0edc2 55%, #abd0e3 100%)", + "dark": "linear-gradient(160deg, #2c2511 0%, #193e1b 55%, #234252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf9f4 0%, #eaf6ea 55%, #dfebf1 100%)", + "dark": "linear-gradient(160deg, #1c180d 0%, #132514 55%, #19272e 100%)" + } + } }, { "id": "planet-v1", @@ -5665,7 +6043,21 @@ "order": 3 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #d6e0f5 0%, #c7c0ed 55%, #d0abe3 100%)", + "dark": "linear-gradient(160deg, #111a2c 0%, #1f193e 55%, #422352 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f4f6fb 0%, #eceaf6 55%, #ebdff1 100%)", + "dark": "linear-gradient(160deg, #0d121c 0%, #161325 55%, #27192e 100%)" + } + } }, { "id": "plane-v1", @@ -5864,6 +6256,20 @@ "order": 4 } ], - "defaultValues": {} + "defaultValues": {}, + "defaultAppearance": { + "menu": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #f5d6f5 0%, #c0c0ed 55%, #abe3e3 100%)", + "dark": "linear-gradient(160deg, #2c112c 0%, #19193e 55%, #235252 100%)" + }, + "content": { + "type": "gradient", + "mode": "manual", + "light": "linear-gradient(160deg, #fbf4fb 0%, #eaeaf6 55%, #dff1f1 100%)", + "dark": "linear-gradient(160deg, #1c0d1c 0%, #131325 55%, #192e2e 100%)" + } + } } ] diff --git a/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.spec.ts b/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.spec.ts index 04bee14cc..97023bb4a 100644 --- a/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.spec.ts +++ b/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.spec.ts @@ -154,6 +154,9 @@ describe('AppearancePanelComponent', () => { component['onAppearanceEdited']({ menu: { type: 'color', mode: 'auto', value: '#123456' }, }); + // Disabling a region: the editor emits the appearance without the region, + // then reports the region key as deleted. + component['onAppearanceEdited']({}); component['onDeletes']({ menu: true }); await vi.waitFor(() => { @@ -166,6 +169,57 @@ describe('AppearancePanelComponent', () => { }); }); + it('should drop a region deletion marker when the region is re-enabled before saving', async () => { + fixture.detectChanges(); + component['onAppearanceEdited']({}); + component['onDeletes']({ menu: true }); + // Re-enable and configure the region inside the debounce window. + component['onAppearanceEdited']({ + menu: { type: 'color', mode: 'auto', value: '#abcdef' }, + }); + + await vi.waitFor(() => { + const calls = worldbuildingService.saveIdentityData.mock.calls; + const lastCall = calls[calls.length - 1]; + const payload = lastCall[1] as { appearance: Record }; + expect(payload.appearance['menu']).toEqual({ + type: 'color', + mode: 'auto', + value: '#abcdef', + }); + }); + }); + + it('should drop a slot deletion marker only once the slot has a value again', async () => { + fixture.detectChanges(); + component['onDeletes']({ 'menu.light': true }); + // Editing the region without refilling the slot keeps the slot delete. + component['onAppearanceEdited']({ + menu: { type: 'color', mode: 'manual', dark: '#000000' }, + }); + // Refilling the slot supersedes it. + component['onAppearanceEdited']({ + menu: { + type: 'color', + mode: 'manual', + dark: '#000000', + light: '#ffffff', + }, + }); + + await vi.waitFor(() => { + const calls = worldbuildingService.saveIdentityData.mock.calls; + const lastCall = calls[calls.length - 1]; + const payload = lastCall[1] as { appearance: Record }; + expect(payload.appearance['menu']).toEqual({ + type: 'color', + mode: 'manual', + dark: '#000000', + light: '#ffffff', + }); + }); + }); + it('should write a queued save to the element it was created for', async () => { fixture.detectChanges(); // Edit element 1, then switch to element 2 before the debounce fires. @@ -199,6 +253,7 @@ describe('AppearancePanelComponent', () => { component['onAppearanceEdited']({ menu: { type: 'color', mode: 'auto', value: '#123456' }, }); + component['onAppearanceEdited']({}); component['onDeletes']({ menu: true }); // First save failed; a subsequent save must still send APPEARANCE_DELETE. @@ -216,6 +271,44 @@ describe('AppearancePanelComponent', () => { }); }); + it('should not restore a deletion marker for a region re-enabled while a failing save was in flight', async () => { + let rejectSave: (err: Error) => void = () => {}; + worldbuildingService.saveIdentityData.mockImplementationOnce( + () => + new Promise((_, reject) => { + rejectSave = reject; + }) + ); + fixture.detectChanges(); + + // Disable the menu region; the debounced save folds APPEARANCE_DELETE. + component['onAppearanceEdited']({}); + component['onDeletes']({ menu: true }); + await vi.waitFor(() => { + expect(worldbuildingService.saveIdentityData).toHaveBeenCalledTimes(1); + }); + + // Re-enable and configure the region while that save is still pending, + // then let the save fail. + component['onAppearanceEdited']({ + menu: { type: 'color', mode: 'auto', value: '#abcdef' }, + }); + rejectSave(new Error('boom')); + + await vi.waitFor(() => { + const calls = worldbuildingService.saveIdentityData.mock.calls; + expect(calls.length).toBeGreaterThanOrEqual(2); + const payload = calls[calls.length - 1][1] as { + appearance: Record; + }; + expect(payload.appearance['menu']).toEqual({ + type: 'color', + mode: 'auto', + value: '#abcdef', + }); + }); + }); + describe('observe', () => { it('should apply remote appearance when it arrives', async () => { const remoteAppearance: ElementAppearance = { diff --git a/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.ts b/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.ts index 00a51c3ba..10bf38937 100644 --- a/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.ts +++ b/frontend/src/app/components/worldbuilding/appearance-panel/appearance-panel.component.ts @@ -163,9 +163,30 @@ export class AppearancePanelComponent implements OnDestroy { this.hasLocalEdit = true; this.editGeneration++; this.appearance.set(appearance); + this.dropSupersededDeletes(appearance); this.queueSave(); } + /** + * Forget deletion markers for regions/slots that the edit has re-populated. + * Otherwise disabling a region and re-enabling it within the debounce window + * (or before the first save completes) folds a stale APPEARANCE_DELETE over + * the new setting, wiping it on persist while the live preview still shows it. + */ + private dropSupersededDeletes(appearance: ElementAppearance): void { + for (const key of Object.keys(this.pendingDeletes)) { + const [region, slot] = key.split('.') as [AppearanceRegion, string?]; + const setting = appearance[region]; + if (!setting) continue; + const slotValue = slot + ? (setting as unknown as Record)[slot] + : undefined; + if (!slot || (slotValue !== undefined && slotValue !== '')) { + delete this.pendingDeletes[key]; + } + } + } + /** Record keys that were explicitly cleared so the backend removes them. */ protected onDeletes(keys: Record): void { for (const key of Object.keys(keys)) { @@ -314,11 +335,14 @@ export class AppearancePanelComponent implements OnDestroy { } catch { // Persistence failed: restore the deletion markers so a later save still // sends APPEARANCE_DELETE for the removed regions/slots. Same element - // guard as above. + // guard as above. Markers for a region/slot the user re-populated while + // this save was in flight are not restored, or the next save would wipe + // the freshly configured setting. if (snapshot.elementId === this.elementId()) { for (const key of Object.keys(snapshot.pendingDeletes)) { this.pendingDeletes[key] = true; } + this.dropSupersededDeletes(this.appearance()); } } finally { if (this.lastSnapshot === snapshot) { diff --git a/frontend/src/app/models/project-archive.ts b/frontend/src/app/models/project-archive.ts index 89bdd5384..57b299e79 100644 --- a/frontend/src/app/models/project-archive.ts +++ b/frontend/src/app/models/project-archive.ts @@ -1,3 +1,4 @@ +import { type ElementAppearance } from '@models/element-appearance'; import { type ElementRelationship, type RelationshipTypeDefinition, @@ -141,6 +142,13 @@ export interface ArchiveWorldbuildingData { schemaId: string; /** Flattened data from Y.Map */ data: Record; + /** + * Per-element background appearance (menu / content regions), stored in + * the element's identity map rather than its data map. Kept separate from + * `data` because schemas such as Character already use an `appearance` + * field group there. + */ + appearance?: ElementAppearance; } /** diff --git a/frontend/src/app/services/project/document-import.service.spec.ts b/frontend/src/app/services/project/document-import.service.spec.ts index 28827a9a7..0743a7175 100644 --- a/frontend/src/app/services/project/document-import.service.spec.ts +++ b/frontend/src/app/services/project/document-import.service.spec.ts @@ -159,6 +159,31 @@ describe('DocumentImportService', () => { expect(logger.debug).toHaveBeenCalled(); }); + it('should write a per-element appearance into the identity map', async () => { + const wb = { + elementId: 'wb-elem-appearance', + schemaId: 'character-v1', + data: { name: 'Themed Character' }, + appearance: { + menu: { + type: 'gradient' as const, + mode: 'manual' as const, + light: 'linear-gradient(160deg, #ffffff 0%, #eeeeee 100%)', + dark: 'linear-gradient(160deg, #000000 0%, #111111 100%)', + }, + }, + }; + + // A successful await proves the nested Y.Map conversion did not throw; + // the success-path log is only emitted after the transaction commits. + await service.writeWorldbuildingData(wb, 'testuser', 'project-slug'); + + expect(logger.debug).toHaveBeenCalledWith( + 'DocumentImport', + expect.stringContaining('wb-elem-appearance') + ); + }); + it('should write dot-notation keys as nested Y.Map structures', async () => { const wb = { elementId: 'wb-elem-dot', diff --git a/frontend/src/app/services/project/document-import.service.ts b/frontend/src/app/services/project/document-import.service.ts index 6300ebd01..be05c3b16 100644 --- a/frontend/src/app/services/project/document-import.service.ts +++ b/frontend/src/app/services/project/document-import.service.ts @@ -127,6 +127,14 @@ export class DocumentImportService { if (wb.data['image']) { identityMap.set('image', wb.data['image']); } + if (wb.appearance && typeof wb.appearance === 'object') { + identityMap.set( + 'appearance', + this.toNestedYMap( + wb.appearance as unknown as Record + ) + ); + } }); // Wait for IndexedDB persistence to sync the changes @@ -176,6 +184,24 @@ export class DocumentImportService { // Skip nested objects — created from dot-notation keys } + /** + * Convert a plain nested object into nested Y.Maps so it matches the shape + * `WorldbuildingService.readAppearance` expects for the identity map. + * `undefined`/`null` leaves are skipped. + */ + private toNestedYMap(value: Record): Y.Map { + const map = new Y.Map(); + for (const [key, entry] of Object.entries(value)) { + if (entry === undefined || entry === null) continue; + if (typeof entry === 'object' && !Array.isArray(entry)) { + map.set(key, this.toNestedYMap(entry as Record)); + } else { + map.set(key, entry); + } + } + return map; + } + /** * Wait for the IndexedDB provider to be synced. * @param provider - The IndexedDB provider diff --git a/frontend/src/app/services/project/project-export.service.spec.ts b/frontend/src/app/services/project/project-export.service.spec.ts index e6a72b955..f37389f43 100644 --- a/frontend/src/app/services/project/project-export.service.spec.ts +++ b/frontend/src/app/services/project/project-export.service.spec.ts @@ -727,6 +727,45 @@ describe('ProjectExportService', () => { ); }); + it('should export the identity appearance alongside the element data', async () => { + const appearance = { + content: { + type: 'gradient' as const, + mode: 'manual' as const, + light: 'linear-gradient(160deg, #ffffff 0%, #eeeeee 100%)', + dark: 'linear-gradient(160deg, #000000 0%, #111111 100%)', + }, + }; + worldbuildingService.getWorldbuildingData.mockResolvedValue({ + name: 'Hero', + }); + worldbuildingService.getIdentityData.mockResolvedValue({ appearance }); + + await service.exportProject(); + + const worldbuilding = + await readJsonFromZip< + Array<{ data: Record; appearance?: unknown }> + >('worldbuilding.json'); + expect(worldbuilding[0].appearance).toEqual(appearance); + expect(worldbuilding[0].data).not.toHaveProperty('appearance'); + }); + + it('should omit the appearance key when the element has none', async () => { + worldbuildingService.getWorldbuildingData.mockResolvedValue({ + name: 'Hero', + }); + worldbuildingService.getIdentityData.mockResolvedValue({}); + + await service.exportProject(); + + const worldbuilding = + await readJsonFromZip>>( + 'worldbuilding.json' + ); + expect(worldbuilding[0]).not.toHaveProperty('appearance'); + }); + it('should not overwrite existing description from worldbuilding data', async () => { const wbData = { name: 'Hero', description: 'Original description' }; worldbuildingService.getWorldbuildingData.mockResolvedValue(wbData); diff --git a/frontend/src/app/services/project/project-export.service.ts b/frontend/src/app/services/project/project-export.service.ts index d5fbc0de5..772c12d08 100644 --- a/frontend/src/app/services/project/project-export.service.ts +++ b/frontend/src/app/services/project/project-export.service.ts @@ -1,5 +1,6 @@ import { inject, Injectable, signal } from '@angular/core'; import { type Element, ElementType, ImagesService } from '@inkweld/index'; +import { type ElementAppearance } from '@models/element-appearance'; import { type ElementRelationship } from '@models/element-ref.model'; import JSZip from '@progress/jszip-esm'; import { firstValueFrom } from 'rxjs'; @@ -434,31 +435,18 @@ export class ProjectExportService { const schemaId = (data['schemaId'] as string) || elem.schemaId || ''; const flatData = this.flattenYjsData(data); - // Merge identity fields (image, description) which are stored in a separate Yjs map - try { - const identity = await this.worldbuildingService.getIdentityData( - elem.id, - username, - slug - ); - if (identity.image) { - flatData['image'] = identity.image; - } - if (identity.description && !flatData['description']) { - flatData['description'] = identity.description; - } - } catch (identityErr) { - this.logger.warn( - 'ProjectExport', - `Failed to get identity data for ${elem.id}`, - identityErr - ); - } + const appearance = await this.mergeIdentityData( + elem.id, + username, + slug, + flatData + ); worldbuilding.push({ elementId: elem.id, schemaId, data: flatData, + ...(appearance ? { appearance } : {}), }); } } catch (err) { @@ -473,6 +461,41 @@ export class ProjectExportService { return worldbuilding; } + /** + * Merge the identity fields (image, description) stored in an element's + * separate identity Yjs map into its flattened data, and return the + * element's appearance (kept alongside the data in the archive). Identity + * read failures are logged and leave the data untouched. + */ + private async mergeIdentityData( + elementId: string, + username: string, + slug: string, + flatData: Record + ): Promise { + try { + const identity = await this.worldbuildingService.getIdentityData( + elementId, + username, + slug + ); + if (identity.image) { + flatData['image'] = identity.image; + } + if (identity.description && !flatData['description']) { + flatData['description'] = identity.description; + } + return identity.appearance; + } catch (identityErr) { + this.logger.warn( + 'ProjectExport', + `Failed to get identity data for ${elementId}`, + identityErr + ); + return undefined; + } + } + /** * Flatten Yjs data to plain JSON. * Converts Y.Array to arrays and Y.Map to objects. diff --git a/frontend/src/app/services/project/project-template-catalogues.spec.ts b/frontend/src/app/services/project/project-template-catalogues.spec.ts index 5114eca94..85efe06c1 100644 --- a/frontend/src/app/services/project/project-template-catalogues.spec.ts +++ b/frontend/src/app/services/project/project-template-catalogues.spec.ts @@ -3,8 +3,10 @@ import { describe, expect, it } from 'vitest'; import demoRelationshipTypes from '../../../../public/assets/project-templates/worldbuilding-demo/relationship-types.json'; import demoSchemas from '../../../../public/assets/project-templates/worldbuilding-demo/schemas.json'; +import demoWorldbuilding from '../../../../public/assets/project-templates/worldbuilding-demo/worldbuilding.json'; import emptyRelationshipTypes from '../../../../public/assets/project-templates/worldbuilding-empty/relationship-types.json'; import emptySchemas from '../../../../public/assets/project-templates/worldbuilding-empty/schemas.json'; +import type { ArchiveWorldbuildingData } from '../../models/project-archive'; import type { ElementTypeSchema } from '../../models/schema-types'; const emptySchemaList = emptySchemas as unknown as ElementTypeSchema[]; @@ -13,6 +15,10 @@ const emptyRelationshipList = emptyRelationshipTypes as unknown as RelationshipTypeDefinition[]; const demoRelationshipList = demoRelationshipTypes as unknown as RelationshipTypeDefinition[]; +const demoWorldbuildingList = + demoWorldbuilding as unknown as ArchiveWorldbuildingData[]; + +const LINEAR_GRADIENT = /^linear-gradient\(\d+deg(, #[0-9a-f]{6} \d+%){2,}\)$/; describe('worldbuilding template catalogues', () => { it('should keep the demo and empty schema catalogues identical', () => { @@ -42,4 +48,33 @@ describe('worldbuilding template catalogues', () => { } } }); + + it('should give every built-in schema a themed gradient default appearance', () => { + for (const schema of emptySchemaList) { + for (const region of ['menu', 'content'] as const) { + const setting = schema.defaultAppearance?.[region]; + expect(setting, `${schema.id} ${region}`).toBeDefined(); + expect(setting?.type).toBe('gradient'); + expect(setting?.mode).toBe('manual'); + expect(setting?.light, `${schema.id} ${region} light`).toMatch( + LINEAR_GRADIENT + ); + expect(setting?.dark, `${schema.id} ${region} dark`).toMatch( + LINEAR_GRADIENT + ); + } + } + }); + + it("should give every demo element its schema's default appearance", () => { + const byId = new Map(demoSchemaList.map(schema => [schema.id, schema])); + expect(demoWorldbuildingList.length).toBeGreaterThan(0); + for (const element of demoWorldbuildingList) { + const schema = byId.get(element.schemaId); + expect(schema, `${element.elementId} schema`).toBeDefined(); + expect(element.appearance, element.elementId).toEqual( + schema?.defaultAppearance + ); + } + }); });