From c6b3f609b7e742c92e18a77fe98c58c48be5d24c Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 23 Oct 2026 21:58:43 +0200 Subject: [PATCH] fix: dev green after the Studio, place picture and tour merges The ids of long catalogues (fonts, shapes, patterns) read as a checked string in the book tools' schemas, so restore_book_page and the page edit stay under their ceilings. The lightbox takes its reading direction from the language. Baselines follow the removed avatar upload and the lower counts, the doubled NEWER constants and three unused imports go, the locale indexes are sorted, and the place gallery, picture, sheet, inspector weight and paddle tests follow the merged behaviour. --- client/scripts/eslint-baseline.json | 16 ++++++------- client/scripts/offline-baseline.json | 1 - client/scripts/rtl-baseline.json | 1 - client/scripts/strict-baseline.json | 3 +-- client/scripts/theme-baseline.json | 1 - .../components/Journey/PhotoLightbox.test.tsx | 4 +++- .../src/components/Journey/PhotoLightbox.tsx | 7 +++--- .../Planner/PlaceDetailsColumn.test.tsx | 9 ++++--- .../src/components/Planner/PlaceFormModal.tsx | 1 - .../Planner/PlacePhotoGallery.test.tsx | 6 ++++- .../Planner/usePlaceGallery.test.ts | 6 ++++- client/src/components/Studio/StudioCanvas.tsx | 2 +- .../src/components/Studio/StudioSidebar.tsx | 2 +- .../unit/mobile/trip/MPlaceSheet.test.tsx | 5 ++++ client/tests/unit/studio/inspector.test.tsx | 7 +++++- server/tests/e2e/journey-book.e2e.test.ts | 3 --- .../unit/nest/journey-book.service.test.ts | 3 --- server/tests/unit/nest/tour-plan.mcp.test.ts | 7 ++++-- shared/src/book/book-op-enums.ts | 3 ++- shared/src/book/book-paint.schema.ts | 14 +++++++---- shared/src/book/book.schema.ts | 24 +++++++++++++++---- shared/src/i18n/ar/index.ts | 2 +- shared/src/i18n/az/index.ts | 2 +- shared/src/i18n/br/index.ts | 2 +- shared/src/i18n/ca/index.ts | 2 +- shared/src/i18n/cs/index.ts | 2 +- shared/src/i18n/de/index.ts | 2 +- shared/src/i18n/en/index.ts | 2 +- shared/src/i18n/es/index.ts | 2 +- shared/src/i18n/et/index.ts | 2 +- shared/src/i18n/fr/index.ts | 2 +- shared/src/i18n/gr/index.ts | 2 +- shared/src/i18n/hu/index.ts | 2 +- shared/src/i18n/id/index.ts | 2 +- shared/src/i18n/it/index.ts | 2 +- shared/src/i18n/ja/index.ts | 2 +- shared/src/i18n/ko/index.ts | 2 +- shared/src/i18n/nl/index.ts | 2 +- shared/src/i18n/pl/index.ts | 2 +- shared/src/i18n/ru/index.ts | 2 +- shared/src/i18n/sk/index.ts | 2 +- shared/src/i18n/sv/index.ts | 2 +- shared/src/i18n/th/index.ts | 2 +- shared/src/i18n/tr/index.ts | 2 +- shared/src/i18n/uk/index.ts | 2 +- shared/src/i18n/vi/index.ts | 2 +- shared/src/i18n/zh-TW/index.ts | 2 +- shared/src/i18n/zh/index.ts | 2 +- 48 files changed, 108 insertions(+), 71 deletions(-) diff --git a/client/scripts/eslint-baseline.json b/client/scripts/eslint-baseline.json index d1308f2c7..eebb85bf8 100644 --- a/client/scripts/eslint-baseline.json +++ b/client/scripts/eslint-baseline.json @@ -1,18 +1,18 @@ { "src": { - "@typescript-eslint/no-non-null-assertion": 404, - "@typescript-eslint/no-misused-promises": 854, + "@typescript-eslint/no-non-null-assertion": 403, + "@typescript-eslint/no-misused-promises": 850, "no-console": 88, "complexity": 226, "@typescript-eslint/no-explicit-any": 146, - "trek/no-swallowed-catch": 131, + "trek/no-swallowed-catch": 130, "max-params": 33, "trek/store-without-selector": 67, - "react-hooks/refs": 557, - "react-hooks/set-state-in-effect": 195, + "react-hooks/refs": 556, + "react-hooks/set-state-in-effect": 194, "@typescript-eslint/no-floating-promises": 165, "react-hooks/exhaustive-deps": 116, - "@typescript-eslint/no-unused-vars": 126, + "@typescript-eslint/no-unused-vars": 125, "react-hooks/immutability": 28, "max-lines-per-function": 132, "react-hooks/static-components": 78, @@ -20,7 +20,7 @@ "no-useless-assignment": 7, "no-empty": 19, "trek/no-window-globals": 41, - "react-refresh/only-export-components": 46, + "react-refresh/only-export-components": 45, "unused-disable-directive": 6, "max-depth": 21, "react-hooks/void-use-memo": 1, @@ -34,7 +34,7 @@ "react-hooks/exhaustive-deps": 46 }, "tests": { - "@typescript-eslint/no-explicit-any": 491, + "@typescript-eslint/no-explicit-any": 490, "@typescript-eslint/no-non-null-assertion": 1932, "@typescript-eslint/no-misused-promises": 22, "@typescript-eslint/no-unused-vars": 51, diff --git a/client/scripts/offline-baseline.json b/client/scripts/offline-baseline.json index efdcea437..2b219d8f7 100644 --- a/client/scripts/offline-baseline.json +++ b/client/scripts/offline-baseline.json @@ -116,7 +116,6 @@ "src/components/Vacay/useVacayCalendarLogic.ts", "src/components/Vacay/useVacaySettings.ts", "src/components/Vacay/useVacayUserPicker.ts", - "src/components/shared/PlaceAvatarUpload.tsx", "src/hooks/useAirtrailConnection.ts", "src/hooks/useDawarichConnection.ts", "src/hooks/useDawarichSuggestions.ts", diff --git a/client/scripts/rtl-baseline.json b/client/scripts/rtl-baseline.json index 5225aeb69..41c1eace1 100644 --- a/client/scripts/rtl-baseline.json +++ b/client/scripts/rtl-baseline.json @@ -73,7 +73,6 @@ "components/shared/DetailShell.tsx": 1, "components/shared/ElevationProfile.tsx": 2, "components/shared/NavigationMenu.tsx": 2, - "components/shared/PlaceAvatarUpload.tsx": 3, "components/shared/PublicLanguagePicker.tsx": 1, "components/shared/SlidingTabs.tsx": 3, "components/shared/StarRating.tsx": 3, diff --git a/client/scripts/strict-baseline.json b/client/scripts/strict-baseline.json index dc58b3d0f..c315f8a98 100644 --- a/client/scripts/strict-baseline.json +++ b/client/scripts/strict-baseline.json @@ -51,7 +51,7 @@ "src/components/Planner/DayPlanSidebarNoteModal.test.tsx": 1, "src/components/Planner/PlaceFormModal.test.tsx": 3, "src/components/Planner/PlaceFormModal.tsx": 1, - "src/components/Planner/PlaceInspector.tsx": 17, + "src/components/Planner/PlaceInspector.tsx": 16, "src/components/Planner/PlacesBulkCategoryModal.test.tsx": 1, "src/components/Planner/PlacesSidebar.tsx": 1, "src/components/Planner/PlacesSidebarMobileDayPicker.tsx": 8, @@ -97,7 +97,6 @@ "src/components/shared/CustomSelect.test.tsx": 1, "src/components/shared/PlaceAvatar.test.tsx": 2, "src/components/shared/PlaceAvatar.tsx": 1, - "src/components/shared/PlaceAvatarUpload.test.tsx": 1, "src/hooks/useDawarichSuggestions.test.ts": 1, "src/mobile/screens/admin/MAdminAddonManager.tsx": 2, "src/mobile/screens/admin/MAdminPluginsPanel.tsx": 6, diff --git a/client/scripts/theme-baseline.json b/client/scripts/theme-baseline.json index 525e60739..a3ac566ae 100644 --- a/client/scripts/theme-baseline.json +++ b/client/scripts/theme-baseline.json @@ -132,7 +132,6 @@ "components/shared/Modal.tsx": 3, "components/shared/NavigationMenu.tsx": 1, "components/shared/NoteColorPicker.tsx": 2, - "components/shared/PlaceAvatarUpload.tsx": 3, "components/shared/PublicLanguagePicker.tsx": 3, "components/shared/Skeleton.tsx": 2, "components/shared/Spinner.tsx": 2, diff --git a/client/src/components/Journey/PhotoLightbox.test.tsx b/client/src/components/Journey/PhotoLightbox.test.tsx index 87fab26dd..89d6ee1a7 100644 --- a/client/src/components/Journey/PhotoLightbox.test.tsx +++ b/client/src/components/Journey/PhotoLightbox.test.tsx @@ -17,6 +17,7 @@ vi.mock('../../api/websocket', () => ({ import { render, screen, fireEvent } from '../../../tests/helpers/render'; import { resetAllStores } from '../../../tests/helpers/store'; +import { useSettingsStore } from '../../store/settingsStore'; import PhotoLightbox from './PhotoLightbox'; const samplePhotos = [ @@ -277,7 +278,8 @@ describe('PhotoLightbox', () => { describe('right to left', () => { beforeEach(() => { - document.documentElement.dir = 'rtl'; + // The direction follows the language, as in the app, where the provider also sets . + useSettingsStore.setState((state) => ({ settings: { ...state.settings, language: 'ar' } })); }); afterEach(() => { document.documentElement.dir = ''; diff --git a/client/src/components/Journey/PhotoLightbox.tsx b/client/src/components/Journey/PhotoLightbox.tsx index 31c46f5d0..96c4e4b2a 100644 --- a/client/src/components/Journey/PhotoLightbox.tsx +++ b/client/src/components/Journey/PhotoLightbox.tsx @@ -3,7 +3,7 @@ import { ChevronLeft, ChevronRight, X, ZoomIn, ZoomOut } from 'lucide-react' import VideoPlayer from './VideoPlayerLazy' import { usePhotoZoom } from './usePhotoZoom' import { Tooltip } from '../shared/Tooltip' -import { useTranslation } from '../../i18n' +import { isRtlLanguage, useTranslation } from '../../i18n' interface LightboxPhoto { id: string @@ -36,9 +36,10 @@ export default function PhotoLightbox({ photos, startIndex = 0, onClose, label } const [idx, setIdx] = useState(startIndex) const touchStart = useRef<{ x: number; y: number } | null>(null) const dialogRef = useRef(null) - const { t } = useTranslation() + const { t, language } = useTranslation() // Right to left, the next photo lies to the left: the arrow keys and swipes follow the reading direction. - const rtl = typeof document !== 'undefined' && document.documentElement.dir === 'rtl' + // Taken from the language, not read off , so it stays put across re-renders. + const rtl = isRtlLanguage(language) // Focus moves into the dialog when it opens and back to what opened it when it closes. useEffect(() => { diff --git a/client/src/components/Planner/PlaceDetailsColumn.test.tsx b/client/src/components/Planner/PlaceDetailsColumn.test.tsx index e51f88972..7e67d7ffb 100644 --- a/client/src/components/Planner/PlaceDetailsColumn.test.tsx +++ b/client/src/components/Planner/PlaceDetailsColumn.test.tsx @@ -613,16 +613,19 @@ describe('PlaceDetailsColumn — picture layout', () => { withPhotos(3) const { onPickImage } = renderColumn() - // Nothing is picked yet, and the line under the grid says so. - await screen.findByText('places.image.none') - expect(screen.queryByText('Author 0')).not.toBeInTheDocument() + // Nothing is picked and there is no own picture to remove, so 'No picture' + // stays away and the first photo is credited. + await screen.findByText('Author 0') + expect(screen.queryByText('places.image.none')).not.toBeInTheDocument() const tiles = screen.getAllByRole('button', { name: /places.details.pickImage/ }) fireEvent.mouseEnter(tiles[2]) expect(screen.getByText('Author 2')).toBeInTheDocument() + expect(screen.queryByText('Author 0')).not.toBeInTheDocument() expect(onPickImage).not.toHaveBeenCalled() fireEvent.mouseLeave(tiles[2]) expect(screen.queryByText('Author 2')).not.toBeInTheDocument() + expect(screen.getByText('Author 0')).toBeInTheDocument() }) it('FE-PDC-037: credits the picture in play, licence included', async () => { diff --git a/client/src/components/Planner/PlaceFormModal.tsx b/client/src/components/Planner/PlaceFormModal.tsx index 77408547e..c0ee829d2 100644 --- a/client/src/components/Planner/PlaceFormModal.tsx +++ b/client/src/components/Planner/PlaceFormModal.tsx @@ -43,7 +43,6 @@ import { } from '../shared/DialogShell' import { EditorField, GRID_2, INPUT, LABEL, PANEL, PillSelect, TEXTAREA } from '../shared/dialogParts' import { SoftPill, tintOf } from './planParts' -import { WHITE_BUTTON } from './placeDialogParts' import { TimeSection } from './PlaceFormTimeSection' // The submit payload mirrors the form, but lat/lng are parsed to numbers and diff --git a/client/src/components/Planner/PlacePhotoGallery.test.tsx b/client/src/components/Planner/PlacePhotoGallery.test.tsx index be96791fa..652de8740 100644 --- a/client/src/components/Planner/PlacePhotoGallery.test.tsx +++ b/client/src/components/Planner/PlacePhotoGallery.test.tsx @@ -11,7 +11,11 @@ import PlacePhotoGallery from './PlacePhotoGallery'; import { __clearEnrichmentCacheForTests } from './placeEnrichment'; const placeEnrichment = vi.fn(); -vi.mock('../../api/client', () => ({ mapsApi: { placeEnrichment: (...a: unknown[]) => placeEnrichment(...a) } })); +// Partial: the store registry behind resetAllStores needs the real default client. +vi.mock('../../api/client', async (importOriginal) => ({ + ...(await importOriginal()), + mapsApi: { placeEnrichment: (...a: unknown[]) => placeEnrichment(...a) }, +})); vi.mock('../Journey/VideoPlayerLazy', () => ({ default: () => null })); const PLACE = buildPlace({ id: 3, name: 'Tower', lat: 48.85, lng: 2.29, image_url: '/uploads/places/tower.jpg' }); diff --git a/client/src/components/Planner/usePlaceGallery.test.ts b/client/src/components/Planner/usePlaceGallery.test.ts index 76c75ac10..ef466f9d8 100644 --- a/client/src/components/Planner/usePlaceGallery.test.ts +++ b/client/src/components/Planner/usePlaceGallery.test.ts @@ -13,7 +13,11 @@ import { __clearEnrichmentCacheForTests, readCachedEnrichment, storeEnrichment } import { usePlaceGallery } from './usePlaceGallery'; const placeEnrichment = vi.fn(); -vi.mock('../../api/client', () => ({ mapsApi: { placeEnrichment: (...a: unknown[]) => placeEnrichment(...a) } })); +// Partial: the store registry behind resetAllStores needs the real default client. +vi.mock('../../api/client', async (importOriginal) => ({ + ...(await importOriginal()), + mapsApi: { placeEnrichment: (...a: unknown[]) => placeEnrichment(...a) }, +})); // One toast for every render, the way the real hook memoises it. const toast = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() })); vi.mock('../shared/Toast', () => ({ useToast: () => toast })); diff --git a/client/src/components/Studio/StudioCanvas.tsx b/client/src/components/Studio/StudioCanvas.tsx index e38d2f724..29b864e72 100644 --- a/client/src/components/Studio/StudioCanvas.tsx +++ b/client/src/components/Studio/StudioCanvas.tsx @@ -6,7 +6,7 @@ import { PeerCursors } from './PeerCursors' import type { PeerCursor } from './useBookPresence' import { bookElementId as elementId } from '@trek/shared' import { fontStack, nearestWeight } from './bookFonts' -import { useSpreadInteraction, type HandleId } from './useSpreadInteraction' +import { useSpreadInteraction } from './useSpreadInteraction' import { useStudioKeys } from './useStudioKeys' import { selectClass, selectionShape } from './studioGestures' import { MarqueeRect, ResizeHandles, SelectionBox } from './StudioSelectionChrome' diff --git a/client/src/components/Studio/StudioSidebar.tsx b/client/src/components/Studio/StudioSidebar.tsx index 7ac6de83e..8354ea2e2 100644 --- a/client/src/components/Studio/StudioSidebar.tsx +++ b/client/src/components/Studio/StudioSidebar.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { - Camera, Check, ChevronDown, ImagePlus, ChevronUp, Compass, Copy, FileDown, Files, ImageIcon, Layers, LayoutTemplate, + Camera, ChevronDown, ImagePlus, ChevronUp, Compass, Copy, FileDown, Files, ImageIcon, Layers, LayoutTemplate, Plus, Search, Shapes, Sticker, Trash2, Upload, X, } from 'lucide-react' import type { BookElement, BookPageSetup, JourneyStats } from '@trek/shared' diff --git a/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx b/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx index 972e65788..0be0614db 100644 --- a/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx +++ b/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx @@ -7,6 +7,7 @@ import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/ import { useAddonStore } from '../../../../src/store/addonStore' import { useSaveToCollectionStore } from '../../../../src/store/saveToCollectionStore' import { useTripStore } from '../../../../src/store/tripStore' +import { __clearEnrichmentCacheForTests } from '../../../../src/components/Planner/placeEnrichment' import { openFile } from '../../../../src/utils/fileDownload' import type { Assignment, Category, Day, Place, TripFile, TripMember } from '../../../../src/types' import { resetAllStores, seedStore } from '../../../helpers/store' @@ -143,6 +144,10 @@ describe('MPlaceSheet', () => { resetAllStores() seedStore(useAddonStore, { addons: [{ id: 'collections', name: 'Collections', type: 'feature', icon: '', enabled: true }] }) vi.spyOn(window, 'open').mockImplementation(() => null) + // The enrichment cache outlives a test: an earlier case's photos for the same place would open the gallery. + sessionStorage.clear() + __clearEnrichmentCacheForTests() + galleryToast.info.mockClear() }) afterEach(() => { diff --git a/client/tests/unit/studio/inspector.test.tsx b/client/tests/unit/studio/inspector.test.tsx index aeaa72f49..7539177e6 100644 --- a/client/tests/unit/studio/inspector.test.tsx +++ b/client/tests/unit/studio/inspector.test.tsx @@ -7,6 +7,7 @@ import { import { fireEvent, render } from '../../helpers/render' import { StudioInspector } from '../../../src/components/Studio/StudioInspector' import { useStudioStore } from '../../../src/store/studioStore' +import { BOOK_FONTS } from '../../../src/components/Studio/bookFonts' import type { JourneySource } from '../../../src/components/Studio/StudioSidebar' /** @@ -213,10 +214,14 @@ describe('a text element', () => { expect(el()).toMatchObject({ text: 'Bergen harbour', overridden: true }) }) + /* The weight is a slider over the weights the family ships, not a row of chips. */ it('sets the weight', () => { load([text()]) const { container } = draw() - pick(container, '600') + const slider = container.querySelector('input[type="range"][aria-label="journey.studio.weight"]') as HTMLInputElement + const at = BOOK_FONTS.sans.weights.indexOf(600) + expect(at).toBeGreaterThan(-1) + fireEvent.change(slider, { target: { value: String(at) } }) expect(el()).toMatchObject({ weight: 600 }) }) diff --git a/server/tests/e2e/journey-book.e2e.test.ts b/server/tests/e2e/journey-book.e2e.test.ts index 7beb59ec3..21e8e925a 100644 --- a/server/tests/e2e/journey-book.e2e.test.ts +++ b/server/tests/e2e/journey-book.e2e.test.ts @@ -129,9 +129,6 @@ async function journeyWithBook() { return { owner, editor, viewer, journey, inner, everyone: [owner.id, editor.id, viewer.id].sort((a, b) => a - b) }; } -/** A revision one past what this build reads. */ -const NEWER = BOOK_SCHEMA_REV + 1; - describe('Studio book over MCP and REST', () => { it('JBE2E-001: an MCP write is what GET reads next, and a viewer is refused in the words a tool answers', async () => { const { editor, viewer, journey, inner } = await journeyWithBook(); diff --git a/server/tests/unit/nest/journey-book.service.test.ts b/server/tests/unit/nest/journey-book.service.test.ts index 31eee1c5e..0fa5d3f1d 100644 --- a/server/tests/unit/nest/journey-book.service.test.ts +++ b/server/tests/unit/nest/journey-book.service.test.ts @@ -35,9 +35,6 @@ import { BOOK_SCHEMA_REV, normalizeBookDocument } from '@trek/shared'; import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest'; -/** A format revision newer than this build reads. */ -const NEWER = BOOK_SCHEMA_REV + 1; - vi.mock('../../../src/db/database', async () => { const { createSnapshotTestDb } = await import('../../helpers/db-mock'); const db = createSnapshotTestDb(); diff --git a/server/tests/unit/nest/tour-plan.mcp.test.ts b/server/tests/unit/nest/tour-plan.mcp.test.ts index 7f0bcda26..77a21152f 100644 --- a/server/tests/unit/nest/tour-plan.mcp.test.ts +++ b/server/tests/unit/nest/tour-plan.mcp.test.ts @@ -427,10 +427,13 @@ describe('TourPlanMcp: create_paddle_tour', () => { expect(paddleRoute).toHaveBeenCalledWith( expect.objectContaining({ boat: 'canoe', + // The router is asked with the bare coordinates: the names of the points stay with the tour. legs: [ - { from: PADDLE_POINTS[0], to: PADDLE_POINTS[1] }, - { from: PADDLE_POINTS[1], to: PADDLE_POINTS[2] }, + { from: { lat: 48.1, lng: 11.6 }, to: { lat: 48.11, lng: 11.6 } }, + { from: { lat: 48.11, lng: 11.6 }, to: { lat: 48.12, lng: 11.6 } }, ], + lake_style: 'direct', + lock_policy: 'auto', }), ); expect(created.route).toEqual({ planned: true }); diff --git a/shared/src/book/book-op-enums.ts b/shared/src/book/book-op-enums.ts index 40f1c650b..984e1a400 100644 --- a/shared/src/book/book-op-enums.ts +++ b/shared/src/book/book-op-enums.ts @@ -6,6 +6,7 @@ import { BOOK_METRICS, BOOK_SHAPES, bookBadgeElementSchema, + bookCatalogueIdSchema, bookCountriesElementSchema, bookListElementSchema, bookMapElementSchema, @@ -47,7 +48,7 @@ export const bookFontIdSchema = z .enum(BOOK_FONTS_IDS) .describe(`Typeface by role; weights a family lacks snap to its nearest. ${bookFontIndex()}`); /** The typeface of a travel element, where the full line above would repeat ten times. */ -export const bookTypesetFontSchema = z.enum(BOOK_FONTS_IDS).describe('Typeface id, as for text.'); +export const bookTypesetFontSchema = bookCatalogueIdSchema(BOOK_FONTS_IDS).describe('Typeface id, as for text.'); /** How heavy type is set, 100 thin to 900 black. */ export const bookWeightSchema = bookTextElementSchema.shape.weight .unwrap() diff --git a/shared/src/book/book-paint.schema.ts b/shared/src/book/book-paint.schema.ts index fa98f9f1d..04f48bd8c 100644 --- a/shared/src/book/book-paint.schema.ts +++ b/shared/src/book/book-paint.schema.ts @@ -58,8 +58,14 @@ type ObjectFactory = typeof z.object | typeof z.strictObject; * The union, built once for the document (plain objects, which drop a field * they do not know, as the rest of the document does) and once for the * operations (strict, as every operation is), so the two cannot drift. + * + * The document takes the pattern as a string held to BOOK_PATTERNS on parse: + * a page carries paint on its background and on every shape, twice over in the + * restore tool's schema (elements and parked), and the ids spelled out each + * time are what a model handing a page back never needs. The operations, + * where a model picks a pattern, keep the enum. */ -function paintSchemas(obj: ObjectFactory) { +function paintSchemas(obj: ObjectFactory, pattern: z.ZodType) { const stop = obj({ at: z.number().min(0).max(1), color: hex, alpha: alpha.optional() }); const stops = z.array(stop).min(2).max(MAX_PAINT_STOPS); const paint = z.discriminatedUnion('type', [ @@ -74,7 +80,7 @@ function paintSchemas(obj: ObjectFactory) { }), obj({ type: z.literal('pattern'), - pattern: z.enum(BOOK_PATTERNS), + pattern, fg: hex, bg: hex.nullable(), scale: z.number().min(PATTERN_SCALE.min).max(PATTERN_SCALE.max), @@ -84,7 +90,7 @@ function paintSchemas(obj: ObjectFactory) { return { stop, paint }; } -const docSchemas = paintSchemas(z.object); +const docSchemas = paintSchemas(z.object, z.string().pipe(z.enum(BOOK_PATTERNS))); export const bookPaintStopSchema = docSchemas.stop; export const bookPaintSchema = docSchemas.paint; @@ -97,7 +103,7 @@ export type BookPatternPaint = Extract; * Paint as the page operations take it. Described once, here, because the * tool schema is all a model reads of it and it appears in three places. */ -export const bookPaintInSchema = paintSchemas(z.strictObject).paint.describe( +export const bookPaintInSchema = paintSchemas(z.strictObject, z.enum(BOOK_PATTERNS)).paint.describe( 'solid (alpha 0..1), linear (angle: 0 towards the top, 90 towards the right) or radial (cx, cy 0..1 of the box, r of half its diagonal) with 2 to 6 stops at 0..1, or a pattern: fg on bg (null shows through), scale 1 is an 8 mm cell.', ); diff --git a/shared/src/book/book.schema.ts b/shared/src/book/book.schema.ts index 1d92dc4f9..1b94ee0c9 100644 --- a/shared/src/book/book.schema.ts +++ b/shared/src/book/book.schema.ts @@ -160,6 +160,20 @@ export const BOOK_SHAPES = [ ] as const; export type BookShapeId = (typeof BOOK_SHAPES)[number]; +/** + * A field that takes one id out of a long catalogue: a typeface, a shape. + * + * A tool's JSON schema shows it as a plain string, and the parse holds it to + * the catalogue, refusing an id it lacks with the ids it has. Spelled out as an + * enum it would be repeated wherever the field appears, on every element kind + * of a page, because the tool schemas carry no `$ref` to share it, and that + * repetition is what held the book tools over their size ceiling + * (MCP-SCHEMA-003 and 004 on the server). Where a model has to choose an id, + * the described enums of book-op-enums.ts still list them. + */ +export const bookCatalogueIdSchema = (ids: T) => + z.string().pipe(z.enum(ids)); + /** * The families a book may be set in, and the weights type is set at. The * catalogue is book-fonts.data.ts: which face each id names, its weights, its @@ -230,7 +244,7 @@ export const bookPhotoElementSchema = z.object({ * makes a frame usable: you place a wide star across a spread and it is wide. * A shape you cannot resize freely is an ornament, not a frame. */ - mask: z.enum(BOOK_SHAPES).nullable().default(null), + mask: bookCatalogueIdSchema(BOOK_SHAPES).nullable().default(null), /** * Decoration *around* the picture — a Polaroid's thick chin, a print's white * border, the shadow of a photo lying on a page. @@ -245,7 +259,7 @@ export const bookTextElementSchema = z.object({ ...elementBase, kind: z.literal('text'), text: z.string().max(MAX_TEXT_LENGTH).default(''), - font: z.enum(BOOK_FONTS_IDS).default('sans'), + font: bookCatalogueIdSchema(BOOK_FONTS_IDS).default('sans'), /** Points. */ size: z.number().min(4).max(200).default(11), weight: bookWeightSchema.default(400), @@ -307,7 +321,7 @@ export const bookTextElementSchema = z.object({ export const bookShapeElementSchema = z.object({ ...elementBase, kind: z.literal('shape'), - shape: z.enum(BOOK_SHAPES).default('rect'), + shape: bookCatalogueIdSchema(BOOK_SHAPES).default('rect'), fill: hex.nullable().default('#111827'), /** * Fade the fill out towards one edge. A cover almost always needs this: a @@ -358,7 +372,7 @@ export const bookShapeElementSchema = z.object({ /** What the travel elements share with text: they are typeset, not drawn. */ const typeset = { - font: z.enum(BOOK_FONTS_IDS).default('sans'), + font: bookCatalogueIdSchema(BOOK_FONTS_IDS).default('sans'), color: hex.default('#1a1a1a'), /** * The one colour that carries emphasis — the figure in a stat, the route on @@ -930,7 +944,7 @@ export const bookPageNumbersSchema = z.object({ */ autoColor: z.boolean().default(true), color: hex.default('#8a8578'), - font: z.enum(BOOK_FONTS_IDS).default('sans'), + font: bookCatalogueIdSchema(BOOK_FONTS_IDS).default('sans'), }); export type BookPageNumbers = z.infer; diff --git a/shared/src/i18n/ar/index.ts b/shared/src/i18n/ar/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/ar/index.ts +++ b/shared/src/i18n/ar/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/az/index.ts b/shared/src/i18n/az/index.ts index bb742de44..30bac2897 100644 --- a/shared/src/i18n/az/index.ts +++ b/shared/src/i18n/az/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/br/index.ts b/shared/src/i18n/br/index.ts index fbba619c0..b4feb8f61 100644 --- a/shared/src/i18n/br/index.ts +++ b/shared/src/i18n/br/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/ca/index.ts b/shared/src/i18n/ca/index.ts index b6be9878f..06929b141 100644 --- a/shared/src/i18n/ca/index.ts +++ b/shared/src/i18n/ca/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/cs/index.ts b/shared/src/i18n/cs/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/cs/index.ts +++ b/shared/src/i18n/cs/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/de/index.ts b/shared/src/i18n/de/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/de/index.ts +++ b/shared/src/i18n/de/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/en/index.ts b/shared/src/i18n/en/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/en/index.ts +++ b/shared/src/i18n/en/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/es/index.ts b/shared/src/i18n/es/index.ts index 75e4b80a3..6f0df92aa 100644 --- a/shared/src/i18n/es/index.ts +++ b/shared/src/i18n/es/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/et/index.ts b/shared/src/i18n/et/index.ts index bb742de44..30bac2897 100644 --- a/shared/src/i18n/et/index.ts +++ b/shared/src/i18n/et/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/fr/index.ts b/shared/src/i18n/fr/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/fr/index.ts +++ b/shared/src/i18n/fr/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/gr/index.ts b/shared/src/i18n/gr/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/gr/index.ts +++ b/shared/src/i18n/gr/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/hu/index.ts b/shared/src/i18n/hu/index.ts index fbba619c0..b4feb8f61 100644 --- a/shared/src/i18n/hu/index.ts +++ b/shared/src/i18n/hu/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/id/index.ts b/shared/src/i18n/id/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/id/index.ts +++ b/shared/src/i18n/id/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/it/index.ts b/shared/src/i18n/it/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/it/index.ts +++ b/shared/src/i18n/it/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/ja/index.ts b/shared/src/i18n/ja/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/ja/index.ts +++ b/shared/src/i18n/ja/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/ko/index.ts b/shared/src/i18n/ko/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/ko/index.ts +++ b/shared/src/i18n/ko/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/nl/index.ts b/shared/src/i18n/nl/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/nl/index.ts +++ b/shared/src/i18n/nl/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/pl/index.ts b/shared/src/i18n/pl/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/pl/index.ts +++ b/shared/src/i18n/pl/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/ru/index.ts b/shared/src/i18n/ru/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/ru/index.ts +++ b/shared/src/i18n/ru/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/sk/index.ts b/shared/src/i18n/sk/index.ts index bb742de44..30bac2897 100644 --- a/shared/src/i18n/sk/index.ts +++ b/shared/src/i18n/sk/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/sv/index.ts b/shared/src/i18n/sv/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/sv/index.ts +++ b/shared/src/i18n/sv/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/th/index.ts b/shared/src/i18n/th/index.ts index bb742de44..30bac2897 100644 --- a/shared/src/i18n/th/index.ts +++ b/shared/src/i18n/th/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/tr/index.ts b/shared/src/i18n/tr/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/tr/index.ts +++ b/shared/src/i18n/tr/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/uk/index.ts b/shared/src/i18n/uk/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/uk/index.ts +++ b/shared/src/i18n/uk/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/vi/index.ts b/shared/src/i18n/vi/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/vi/index.ts +++ b/shared/src/i18n/vi/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/zh-TW/index.ts b/shared/src/i18n/zh-TW/index.ts index e48c00ab3..e08aa93b4 100644 --- a/shared/src/i18n/zh-TW/index.ts +++ b/shared/src/i18n/zh-TW/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection'; diff --git a/shared/src/i18n/zh/index.ts b/shared/src/i18n/zh/index.ts index f5373ba81..de8e36c54 100644 --- a/shared/src/i18n/zh/index.ts +++ b/shared/src/i18n/zh/index.ts @@ -59,9 +59,9 @@ import share from './share'; import shared from './shared'; import storage from './storage'; import studioFonts from './studioFonts'; +import studioLayers from './studioLayers'; import studioPaint from './studioPaint'; import studioStickers from './studioStickers'; -import studioLayers from './studioLayers'; import system_notice from './system_notice'; import todo from './todo'; import tourCollection from './tourCollection';