Compare commits

...

18 Commits

Author SHA1 Message Date
Maurice 1be997f7d8 test(studio): the pattern picker test counts the tiles against BOOK_PATTERNS 2026-10-24 11:36:30 +02:00
Maurice 5a8ff16afa test(studio): the shape swap test finds the ellipse by its translated name 2026-10-24 11:28:13 +02:00
Maurice 4435dc32b8 refactor(studio): gradient stops built by a plain function; the letters' fill in a helper 2026-10-24 11:07:16 +02:00
Maurice f5a33e0c1e refactor(studio): a paint's SVG parts in a module of their own; the pattern letters a component of their own 2026-10-24 11:07:16 +02:00
Maurice a8326b6f5a chore(client): the shape name entry at the end of the dynamic key list, beside the sticker one 2026-10-24 11:07:16 +02:00
Maurice 1e999d968a fix(studio): the new stickers drawn with the path commands the catalogue takes; the text fill test without Array.at 2026-10-24 11:07:16 +02:00
Maurice d0f74c9f02 feat(studio): add_book_pages, set_book_cover and design_book's cover take the decorated templates too 2026-10-24 11:07:16 +02:00
Maurice f14f1e41b9 fix(studio): bold text measured at each family's own bold width, so a bold condensed heading no longer overruns its frame 2026-10-24 11:07:16 +02:00
Maurice 9138e722bb feat(studio): twelve new stickers: Parthenon, Milan Cathedral, canal houses, gondola, Lisbon tram, Celtic harp, tiger, monkey, camel and three doodles 2026-10-24 11:07:16 +02:00
Maurice 9695e269e3 fix(studio): shapes named in every language, the align target kept across selections, up and down in a tab grid 2026-10-24 11:07:16 +02:00
Maurice 82bdf62c7b feat(studio): linen and paper grain, two quiet textures among the patterns 2026-10-24 11:07:15 +02:00
Maurice 8893e3afe0 feat(studio): letters filled with a gradient or a pattern 2026-10-24 11:07:15 +02:00
Maurice cd1e7d8fa8 refactor(pdf): call the markdown row directly 2026-10-24 10:39:36 +02:00
Maurice 059e72df69 refactor(pdf): the markdown rows and styles in a module of their own; the shared stop's markdown test beside its component 2026-10-24 10:39:36 +02:00
Maurice dc519b3ea7 fix(journey): a journey's created_at is an epoch number 2026-10-24 10:39:36 +02:00
Maurice ad4074f107 fix: journey year helper takes what a journey row carries; PDF markdown test without casts 2026-10-24 10:39:36 +02:00
Maurice 0534d7b3a7 fix: test strings, logical sides in the PDF markdown styles, one export per markdown module 2026-10-24 10:39:36 +02:00
Maurice a0554c2346 fix: four reported bugs that dev still had
- Dashboard: a browser that names no time zone (Chrome on Windows for a zone
  it cannot map) crashed the world clocks and the whole dashboard. One helper
  now reads the browser zone and every reader picks its fallback; a stored
  zone the browser does not know is left out instead of throwing (#2694).
- Phone: a leg of a multi-leg train or flight showed only its route, and its
  sheet showed the first leg's seat. The day plan subtitle is one helper for
  desktop and phone now, so a leg names its own train or flight, platform and
  seat, translated instead of the hardcoded "Gl."/"Sitz"; the sheet opened
  from a leg shows that leg's number, seat, platform and times (#2701). The
  PDF export translates platform and seat too.
- Journey list: the year badge named the year a journey was written, not the
  years of its trips; a trip over New Year shows both (#2685).
- A place's description and notes are markdown in the planner but printed as
  raw syntax in the PDF export and on a shared trip. Both render it now through
  the app's sanitized markdown pipeline, raw HTML stays text (#2692).

Closes #2694
Closes #2701
Closes #2685
Closes #2692
2026-10-24 10:39:35 +02:00
158 changed files with 5757 additions and 287 deletions
+7
View File
@@ -146,6 +146,13 @@ export const DYNAMIC_ALLOWED = [
'id is a sticker id of the library in @trek/shared/stickers; stickerLibrary.test.ts resolves the key of every ' +
'sticker the library holds against en',
},
{
file: 'components/Studio/shapes.ts',
template: 'journey.studio.shapeName.${shape}',
because:
'shapeName takes a BookShapeId; shared/src/book/book-shape-catalogue.spec.ts holds an en name for every ' +
'shape of BOOK_SHAPE_GROUPS',
},
];
const KEY_RE = /^[a-z][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_:-]+)+$/;
+1 -1
View File
@@ -12,7 +12,7 @@
"src/components/PDF/TripPDF.tsx": 1082,
"src/components/Planner/DayDetailPanel.test.tsx": 2333,
"src/components/Planner/DayPlanSidebar.test.tsx": 5349,
"src/components/Planner/DayPlanSidebar.tsx": 2953,
"src/components/Planner/DayPlanSidebar.tsx": 2936,
"src/components/Planner/PlaceFormModal.test.tsx": 2836,
"src/components/Planner/PlaceFormModal.tsx": 1157,
"src/components/Planner/PlaceInspector.tsx": 1290,
@@ -1,4 +1,4 @@
// FE-COMP-TZHOOK-001 to -007: the world clocks both dashboards share.
// FE-COMP-TZHOOK-001 to -009: the world clocks both dashboards share.
import { act, renderHook, waitFor } from '@testing-library/react';
import { useSettingsStore } from '../../store/settingsStore';
@@ -97,4 +97,27 @@ describe('useDashboardTimezones', () => {
expect(localStorage.getItem('trek_dashboard_tz')).toBeNull();
expect(updateSetting).not.toHaveBeenCalled();
});
// #2694: Chrome on Windows names no zone for a system zone it cannot map.
it('FE-COMP-TZHOOK-008: a browser that names no zone still gets the default clocks', () => {
const original = Intl.DateTimeFormat.prototype.resolvedOptions;
const spy = vi.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockImplementation(function (
this: Intl.DateTimeFormat
) {
return { ...original.call(this), timeZone: undefined as unknown as string };
});
try {
const { result } = renderHook(() => useDashboardTimezones('en'));
expect(result.current.zones).toEqual(['Europe/London', 'Asia/Tokyo']);
expect(() => result.current.zones.map((tz) => result.current.timeIn(tz))).not.toThrow();
} finally {
spy.mockRestore();
}
});
it('FE-COMP-TZHOOK-009: a stored zone this browser does not know is left out instead of crashing the clocks', () => {
seedSettings(['Europe/Berlin', 'Mars/Olympus_Mons', '']);
const { result } = renderHook(() => useDashboardTimezones('en'));
expect(result.current.zones).toEqual(['Europe/Berlin']);
});
});
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useSettingsStore } from '../../store/settingsStore';
import { browserTimeZone, isValidTimeZone } from '../../utils/localDate';
const DEFAULT_ZONES = ['Europe/London', 'Asia/Tokyo'];
@@ -38,13 +39,15 @@ export function shortZone(tz: string): string {
* clocks tick every 30 seconds, `adding` toggles the zone picker.
*/
export function useDashboardTimezones(locale: string) {
const home = Intl.DateTimeFormat().resolvedOptions().timeZone;
const home = browserTimeZone();
const [now, setNow] = useState(() => new Date());
const isLoaded = useSettingsStore((s) => s.isLoaded);
const updateSetting = useSettingsStore((s) => s.updateSetting);
const stored = useSettingsStore((s) => s.settings.dashboard_timezones);
// Unset (never chosen) falls back to home + defaults; an explicit list is honoured.
const zones = stored ?? [home, ...DEFAULT_ZONES];
// A browser that names no zone (#2694) or a zone this browser does not know
// would throw while rendering a clock, so such entries are left out.
const zones = (stored ?? [home, ...DEFAULT_ZONES]).filter(isValidTimeZone);
const setZones = (next: string[]) => {
updateSetting('dashboard_timezones', next).catch(() => {});
};
+15 -2
View File
@@ -397,6 +397,19 @@ describe('downloadTripPDF', () => {
expect(iframe!.srcdoc).toContain('€')
})
// #2692: a place's description and notes print as the planner shows them, not as raw markdown.
it('FE-COMP-TRIPPDF-016c: renders markdown in a place description, sanitized', async () => {
const description = ['**Colosseum** tickets', '', '| Gate | Time |', '| --- | --- |', '| A | 9:00 |', '', '<script>alert(1)</script>'].join('\n')
const place = { ...placeWithDetails, description }
const md = { ...richArgs, places: [place], assignments: { '10': [{ ...assignmentForDay, place }] } as typeof richArgs.assignments }
await downloadTripPDF(md)
const html = getIframe()?.srcdoc ?? ''
expect(html).toContain('<strong>Colosseum</strong>')
expect(html).toContain('<td>9:00</td>')
expect(html).not.toContain('**Colosseum**')
expect(html).not.toContain('<script>alert(1)</script>')
})
it('FE-COMP-TRIPPDF-016b: formats price chip and totals in the trip currency, not EUR', async () => {
const usdArgs = {
...richArgs,
@@ -796,7 +809,7 @@ describe('downloadTripPDF remaining branches', () => {
}]))
const html = srcdoc()
expect(html).toContain('ICE 599 · Gl. 7 · Seat 21A · FRA → BER')
expect(html).toContain('ICE 599 · reservations.meta.platform 7 · reservations.meta.seat 21A · FRA → BER')
})
it('FE-W5PDF-006: a multi-leg train renders one line per leg', async () => {
@@ -811,7 +824,7 @@ describe('downloadTripPDF remaining branches', () => {
}]))
const html = srcdoc()
expect(html).toContain('IC 1 · Gl. 3 · BER → MUC')
expect(html).toContain('IC 1 · reservations.meta.platform 3 · BER → MUC')
expect(html).toContain('EC 2 · MUC → ZRH')
})
+11 -8
View File
@@ -24,6 +24,7 @@ import { fetchExchangeRates } from '../../hooks/useExchangeRates'
import { getFlightLegs, getTrainLegs, usesStationRoute } from '../../utils/flightLegs'
import { isServiceStopType } from '../Roadtrip/roadtripModel'
import { onlyMyPlan } from './pdfScope'
import { createPdfMarkdown, PDF_MARKDOWN_CSS } from './pdfMarkdown'
/**
* Every day starts a new page by default. On a trip of short days that prints
@@ -231,6 +232,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
let hasTransportNotes = false
const loc = _locale || undefined
const tr = _t || (k => k)
const markdown = createPdfMarkdown()
// The store read is the fallback, not the source: a caller that forgets the
// prop still prints the reader's own format instead of silently reverting.
const is12h = (_timeFormat || useSettingsStore.getState().settings.time_format || '24h') === '12h'
@@ -439,8 +441,8 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
const source = (usesStationRoute(r.type) ? getTrainLegs(r) : getFlightLegs(r))[l.index]
subtitleLines = [[
l.airline, l.flight_number, l.train_number,
l.platform ? `Gl. ${l.platform}` : '',
l.seat ? `Seat ${l.seat}` : '',
l.platform ? `${tr('reservations.meta.platform')} ${l.platform}` : '',
l.seat ? `${tr('reservations.meta.seat')} ${l.seat}` : '',
(l.from || l.to) ? [l.from, l.to].filter(Boolean).join(' → ') : '',
source?.confirmation_number,
].filter(Boolean).join(' · ')].filter(Boolean)
@@ -471,7 +473,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
// Multi-leg: one line per leg so every train number + segment route shows,
// with the segment's own booking code where it has one (#1943).
subtitleLines = legs.map(l =>
[l.train_number, l.platform ? `Gl. ${l.platform}` : '',
[l.train_number, l.platform ? `${tr('reservations.meta.platform')} ${l.platform}` : '',
(l.from || l.to) ? [l.from, l.to].filter(Boolean).join(' → ') : '',
l.confirmation_number]
.filter(Boolean).join(' · '))
@@ -479,7 +481,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
} else {
const stops = (r.endpoints || []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0)).map(e => e.code || e.name)
const route = stops.length >= 2 ? stops.join(' → ') : ''
subtitle = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Seat ${meta.seat}` : '', route].filter(Boolean).join(' · ')
subtitle = [meta.train_number, meta.platform ? `${tr('reservations.meta.platform')} ${meta.platform}` : '', meta.seat ? `${tr('reservations.meta.seat')} ${meta.seat}` : '', route].filter(Boolean).join(' · ')
}
}
else if (r.type === 'car') {
@@ -587,10 +589,10 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
</div>
${place.address ? `<div class="info-row">${svgPin}<span class="info-text">${escHtml(place.address)}</span></div>` : ''}
${(place.lat != null && place.lng != null) ? `<div class="info-row"><span class="info-spacer"></span><span class="info-text muted">${Number(place.lat).toFixed(5)}, ${Number(place.lng).toFixed(5)}</span></div>` : ''}
${place.description ? `<div class="info-row"><span class="info-spacer"></span><span class="info-text muted italic">${escHtml(place.description)}</span></div>` : ''}
${place.description ? markdown.row(place.description) : ''}
${chips ? `<div class="chips">${chips}</div>` : ''}
${place.notes ? `<div class="info-row"><span class="info-spacer"></span><span class="info-text muted italic">${escHtml(place.notes)}</span></div>` : ''}
${item.data.notes ? `<div class="info-row"><span class="info-spacer"></span><span class="info-text muted italic">${escHtml(item.data.notes)}</span></div>` : ''}
${place.notes ? markdown.row(place.notes) : ''}
${item.data.notes ? markdown.row(item.data.notes) : ''}
</div>
</div>`
}).join('')
@@ -836,7 +838,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
.info-spacer { width: 13px; flex-shrink: 0; }
.info-text { font-size: 9px; color: #64748b; line-height: 1.5; }
.info-text.muted { color: #94a3b8; }
.info-text.italic { font-style: italic; }
${PDF_MARKDOWN_CSS}
.chips { display: flex; flex-wrap: wrap; gap: 4px; padding-left: 21px; margin-top: 4px; }
.chip { display: inline-flex; align-items: center; gap: 3px; font-size: 8px; font-weight: 600; background: #f1f5f9; color: #374151; border-radius: 99px; padding: 2px 7px; white-space: nowrap; }
@@ -930,6 +932,7 @@ ${tripMapHtml}
${daysHtml}
${pluginSectionsHtml}
</body></html>`
markdown.dispose()
// Open in modal with srcdoc iframe (no URL loading = no X-Frame-Options issue)
const overlay = document.createElement('div')
+30
View File
@@ -0,0 +1,30 @@
import { createMarkdownHtml } from '../shared/markdownHtml';
/**
* A place's description and notes are markdown in the app, so the export
* renders them the same way instead of printing the raw syntax (#2692): one
* info row per text, the HTML from the app's own sanitized pipeline.
*/
export function createPdfMarkdown(): { row: (text: string) => string; dispose: () => void } {
const markdown = createMarkdownHtml();
return {
row: (text) =>
`<div class="info-row"><span class="info-spacer"></span><div class="info-text muted md">${markdown.html(text)}</div></div>`,
dispose: markdown.dispose,
};
}
/** The markdown's own spacing, lists, tables and links at the export's small size. */
export const PDF_MARKDOWN_CSS = `
.info-text.md { min-width: 0; overflow-wrap: anywhere; }
.md > :first-child { margin-top: 0; }
.md > :last-child { margin-bottom: 0; }
.md p, .md ul, .md ol, .md table, .md blockquote, .md pre { margin: 0 0 4px; }
.md ul, .md ol { padding-inline-start: 14px; }
.md h1, .md h2, .md h3, .md h4, .md h5, .md h6 { font-size: 1.1em; font-weight: 700; margin: 4px 0 2px; }
.md table { border-collapse: collapse; }
.md th, .md td { border: 1px solid #e2e8f0; padding: 2px 5px; text-align: start; }
.md code { font-family: ui-monospace, monospace; font-size: 8.5px; }
.md a { color: inherit; text-decoration: underline; }
.md blockquote { border-inline-start: 2px solid #e2e8f0; padding-inline-start: 6px; }
`;
@@ -43,6 +43,7 @@ import './dayPlanRows.css'
import { TypeTile } from './bookings/bookingParts'
import { typeInfo } from './bookings/bookingsModel'
import { transportSubtitle } from './transportDetailModel'
import { dayHeadStays } from './dayHeadStays'
import { getAccommodationAnchors, getDayBookendHotels, shouldDrawMorningLeg, shouldDrawEveningLeg, type CarrierEdge } from '../../utils/dayOrder'
import {
@@ -2166,24 +2167,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
let meta: any = {}
try { meta = typeof res.metadata === 'string' ? JSON.parse(res.metadata || '{}') : (res.metadata || {}) } catch { meta = {} }
// Subtitle aus Metadaten zusammensetzen
let subtitle = ''
if (res.__leg) {
// One leg of a multi-leg flight/train — show this segment's own detail.
const parts = res.type === 'train'
? [res.__leg.train_number, res.__leg.platform ? `${t('reservations.meta.platform')} ${res.__leg.platform}` : '', res.__leg.seat ? `${t('reservations.meta.seat')} ${res.__leg.seat}` : ''].filter(Boolean)
: [res.__leg.airline, res.__leg.flight_number].filter(Boolean)
if (res.__leg.from || res.__leg.to)
parts.push([res.__leg.from, res.__leg.to].filter(Boolean).join(' → '))
subtitle = parts.join(', ')
} else if (res.type === 'flight') {
const parts = [meta.airline, meta.flight_number].filter(Boolean)
if (meta.departure_airport || meta.arrival_airport)
parts.push([meta.departure_airport, meta.arrival_airport].filter(Boolean).join(' → '))
subtitle = parts.join(', ')
} else if (res.type === 'train') {
subtitle = [meta.train_number, meta.platform ? `${t('reservations.meta.platform')} ${meta.platform}` : '', meta.seat ? `${t('reservations.meta.seat')} ${meta.seat}` : ''].filter(Boolean).join(', ')
}
// A leg of a multi-leg flight or train shows its own detail (#2701).
const subtitle = transportSubtitle(res, t) ?? ''
// A transit journey (#1065) renders its itinerary inline —
// line badges in their colors instead of a plain subtitle,
@@ -23,3 +23,65 @@ export function bookingDayLabel(date: string, locale: string): string {
export function segmentCodeLabel(leg: { from?: string | null; to?: string | null }, t: Translate): string {
return [leg.from, leg.to].filter(Boolean).join(' → ') || t('reservations.confirmationCode');
}
/** The per-leg detail the day plan's leg rows carry (utils/dayMerge's `__leg`). */
export interface TransportLegDetail {
index: number;
from?: string | null;
to?: string | null;
airline?: string | null;
flight_number?: string | null;
train_number?: string | null;
platform?: string | null;
seat?: string | null;
}
/**
* The subtitle of a transport row in the day plan, desktop and phone alike. A
* leg of a multi-leg train or flight names its own train or flight, platform
* and seat (#2701); a whole booking reads them off its metadata. Null for the
* other transport types, which each view labels its own way.
*/
export function transportSubtitle(
res: { type: string; metadata?: unknown; __leg?: TransportLegDetail | null },
t: Translate
): string | null {
const meta = parseMeta(res.metadata);
const leg = res.__leg;
const trainParts = (d: { train_number?: unknown; platform?: unknown; seat?: unknown }) => [
str(d.train_number),
str(d.platform) ? `${t('reservations.meta.platform')} ${str(d.platform)}` : '',
str(d.seat) ? `${t('reservations.meta.seat')} ${str(d.seat)}` : '',
];
if (leg) {
const parts = res.type === 'train' ? trainParts(leg) : [str(leg.airline), str(leg.flight_number)];
if (leg.from || leg.to) parts.push([leg.from, leg.to].filter(Boolean).join(' → '));
return parts.filter(Boolean).join(', ');
}
if (res.type === 'flight') {
const parts = [str(meta.airline), str(meta.flight_number)];
if (meta.departure_airport || meta.arrival_airport) {
parts.push([str(meta.departure_airport), str(meta.arrival_airport)].filter(Boolean).join(' → '));
}
return parts.filter(Boolean).join(', ');
}
if (res.type === 'train') return trainParts(meta).filter(Boolean).join(', ');
return null;
}
function str(value: unknown): string {
return typeof value === 'string' || typeof value === 'number' ? String(value) : '';
}
function parseMeta(metadata: unknown): Record<string, unknown> {
let meta = metadata;
// An earlier bug could double-encode the metadata; reading it twice heals that.
for (let i = 0; i < 2 && typeof meta === 'string'; i++) {
try {
meta = JSON.parse(meta || '{}');
} catch {
meta = {};
}
}
return meta && typeof meta === 'object' ? (meta as Record<string, unknown>) : {};
}
@@ -11,6 +11,7 @@ import ConfirmDialog from '../shared/ConfirmDialog'
import { pluginsApi } from '../../api/client'
import { addListener, removeListener } from '../../api/websocket'
import { useIsPhone } from '../../mobile/useIsPhone'
import { browserTimeZone } from '../../utils/localDate'
// The design-token contract handed to plugins (#4 richer context): non-secret CSS
// values, resolved for the CURRENT theme, so a plugin can match TREK exactly (and
@@ -379,7 +380,7 @@ export default function PluginFrame({ pluginId, tripId = null, placeId = null, d
timeFormat: settings.time_format,
distanceUnit: settings.distance_unit,
temperatureUnit: settings.temperature_unit,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timezone: browserTimeZone() ?? 'UTC',
blurBookingCodes: Boolean(settings.blur_booking_codes),
},
tokens: readThemeTokens(),
@@ -20,7 +20,6 @@ import {
Ungroup,
type LucideIcon,
} from 'lucide-react';
import { useState } from 'react';
import { useStudioStore } from '../../store/studioStore';
import '../../styles/studio-panels.css';
@@ -118,7 +117,8 @@ export function StudioArrangeSection({
const copyStyle = useStudioStore((s) => s.copyStyle);
const pasteStyle = useStudioStore((s) => s.pasteStyle);
const keys = useStudioShortcut();
const [chosenTo, setChosenTo] = useState<BookAlignTo | null>(null);
const chosenTo = useStudioStore((s) => s.alignTo);
const setChosenTo = useStudioStore((s) => s.setAlignTo);
if (!spread || !selection.length) return null;
const sel = spread.elements.filter((e) => selection.includes(e.id));
@@ -2,7 +2,7 @@ import { useId, useState } from 'react'
import { Frame, Minus, Quote, Shapes, Spline, Sparkles, Square, Sticker, Type, type LucideIcon } from 'lucide-react'
import type { BookElement, BookPageSetup, BookShapeId } from '@trek/shared'
import { useStudioStore } from '../../store/studioStore'
import { FRAME_SHAPES, HOLED_SHAPES, SHAPE_GROUPS } from './shapes'
import { FRAME_SHAPES, HOLED_SHAPES, SHAPE_GROUPS, shapeName } from './shapes'
import {
BOOK_GRIDS as GRIDS, MAX_SPREAD_ELEMENTS, bookElementId, bookShapeSize, bookGridElements as gridElements,
defaultBookGridBox as defaultGridBox,
@@ -280,7 +280,8 @@ function ShapesTab({ addShape, t }: { addShape: Adders['addShape']; t: (k: strin
key={shape}
className="st-shape-btn is-glyph"
onClick={() => addShape(shape)}
aria-label={shape}
title={shapeName(shape, t)}
aria-label={shapeName(shape, t)}
>
<ShapeGlyph shape={shape} />
</button>
@@ -319,7 +320,8 @@ function FramesTab({ adders, t }: { adders: Adders; t: (k: string) => string })
key={shape}
className="st-shape-btn is-glyph"
onClick={() => adders.addFrame(shape === 'rect' ? null : shape)}
aria-label={shape}
title={shapeName(shape, t)}
aria-label={shapeName(shape, t)}
>
<ShapeGlyph shape={shape} photo />
</button>
@@ -64,12 +64,19 @@ export function StudioPaintPicker({
channel,
t,
noneLabel,
kinds = KINDS,
}: {
value: BookPaint | null;
channel: PaintChannel;
t: StudioT;
/** What "no paint" is called here: no fill on a shape, the paper on a page. */
noneLabel: string;
/**
* The kinds offered as tabs; all four by default. With one, the row of tabs
* is left out: the letters of a text pick gradient or pattern beside their
* other fills and only need that kind's controls here.
*/
kinds?: readonly PaintKind[];
}) {
const kind = kindOf(value);
// The last paint of each kind shown here, so a kind visited again comes back as it was left.
@@ -96,28 +103,30 @@ export function StudioPaintPicker({
return (
<div className="st-paint">
<div className="st-paint-tabs" role="tablist" aria-label={t('journey.studio.paint.kind')}>
{KINDS.map((k) => (
<button
type="button"
key={k}
role="tab"
aria-selected={k === kind}
className={`st-paint-tab ${k === kind ? 'is-on' : ''}`}
onClick={() => choose(k)}
>
<span
className={`st-paint-tab-chip ${k === 'none' ? 'is-none' : ''} ${k === 'pattern' ? 'is-pattern' : ''}`}
// A pattern's chip keeps its strokes (studio-paint.css) over the ground colour.
style={k === 'pattern' ? { backgroundColor: tabPreview(k) } : { background: tabPreview(k) }}
aria-hidden
{kinds.length > 1 && (
<div className="st-paint-tabs" role="tablist" aria-label={t('journey.studio.paint.kind')}>
{kinds.map((k) => (
<button
type="button"
key={k}
role="tab"
aria-selected={k === kind}
className={`st-paint-tab ${k === kind ? 'is-on' : ''}`}
onClick={() => choose(k)}
>
{k === 'none' && <Ban size={11} strokeWidth={2.2} />}
</span>
<span className="st-paint-tab-label">{k === 'none' ? noneLabel : kindLabel(k, t)}</span>
</button>
))}
</div>
<span
className={`st-paint-tab-chip ${k === 'none' ? 'is-none' : ''} ${k === 'pattern' ? 'is-pattern' : ''}`}
// A pattern's chip keeps its strokes (studio-paint.css) over the ground colour.
style={k === 'pattern' ? { backgroundColor: tabPreview(k) } : { background: tabPreview(k) }}
aria-hidden
>
{k === 'none' && <Ban size={11} strokeWidth={2.2} />}
</span>
<span className="st-paint-tab-label">{k === 'none' ? noneLabel : kindLabel(k, t)}</span>
</button>
))}
</div>
)}
{value?.type === 'solid' && (
<Swatches
@@ -6,7 +6,7 @@ import { fireEvent, render, screen } from '../../../tests/helpers/render';
import { StudioTabPanel, StudioTabs } from './StudioTabs';
/**
* FE-STUDIO-TABS-001 to FE-STUDIO-TABS-004: the tab rows inside Studio's
* FE-STUDIO-TABS-001 to FE-STUDIO-TABS-005: the tab rows inside Studio's
* panels, as a screen reader and a keyboard meet them, and as a long word
* meets them.
*/
@@ -69,6 +69,20 @@ describe('StudioTabs', () => {
expect(three?.tabIndex).toBe(0);
});
it('FE-STUDIO-TABS-005: in a grid of two lines, up and down move to the tab above or below', () => {
render(<Row />);
const [one, two, three] = screen.getAllByRole('tab');
one?.focus();
fireEvent.keyDown(one as HTMLElement, { key: 'ArrowDown' });
expect(document.activeElement).toBe(three);
fireEvent.keyDown(three as HTMLElement, { key: 'ArrowUp' });
expect(document.activeElement).toBe(one);
two?.focus();
// Nothing below the second tab: the focus stays.
fireEvent.keyDown(two as HTMLElement, { key: 'ArrowDown' });
expect(document.activeElement).toBe(two);
});
it('FE-STUDIO-TABS-003: keeps a long word in a cell that breaks it, and the count as plain figures', () => {
const { container } = render(<Row />);
const label = screen.getByText('Klistermärken');
+11 -3
View File
@@ -15,7 +15,8 @@ import { rovingTarget, runsRtl } from './studioRoving';
* label's own colour, quieter than the word, never a pill inside the tab.
*
* The keyboard is the rail's (studioRoving.ts): the arrows move along the row,
* Enter or Space opens the tab.
* Enter or Space opens the tab. In a grid of more than one line, up and down
* move to the tab above or below.
*/
interface StudioTab<T extends string> {
@@ -45,8 +46,15 @@ export function StudioTabs<T extends string>({
columns?: number;
}) {
const buttons = useRef<(HTMLButtonElement | null)[]>([]);
const across = Math.max(1, Math.min(columns, tabs.length));
const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
const next = rovingTarget(e.key, index, tabs.length, { vertical: false, rtl: runsRtl(e.currentTarget) });
const below = e.key === 'ArrowDown' ? index + across : e.key === 'ArrowUp' ? index - across : null;
const next =
below !== null && across < tabs.length
? below >= 0 && below < tabs.length
? below
: null
: rovingTarget(e.key, index, tabs.length, { vertical: false, rtl: runsRtl(e.currentTarget) });
if (next === null) return;
e.preventDefault();
buttons.current[next]?.focus();
@@ -56,7 +64,7 @@ export function StudioTabs<T extends string>({
className={`st-subtabs ${tabs.some((tab) => tab.icon) ? 'has-icons' : ''}`}
role="tablist"
aria-label={label}
style={{ gridTemplateColumns: `repeat(${Math.max(1, Math.min(columns, tabs.length))}, minmax(0, 1fr))` }}
style={{ gridTemplateColumns: `repeat(${across}, minmax(0, 1fr))` }}
>
{tabs.map((tab, i) => {
const on = tab.id === value;
@@ -189,6 +189,32 @@ describe('the fill of the letters', () => {
expect(screen.getByText('journey.studio.fx.focalHint')).toBeTruthy();
});
it('turns the letters into a gradient or a pattern from their own colour, edited as a shape is', () => {
const plain = mount(text({ shadow: { dx: 1, dy: 1, color: '#111111' }, color: '#0f766e' }));
fireEvent.click(screen.getByRole('radio', { name: 'journey.studio.paint.gradient' }));
const gradient = plain.set.mock.lastCall?.[0]?.fillPaint;
expect(gradient?.type).toBe('linear');
expect(gradient?.stops[0]?.color).toBe('#0f766e');
fireEvent.click(screen.getByRole('radio', { name: 'journey.studio.paint.pattern' }));
expect(plain.set.mock.lastCall?.[0]?.fillPaint?.type).toBe('pattern');
plain.unmount();
const patterned = {
type: 'pattern' as const,
pattern: 'dots' as const,
fg: '#d62828',
bg: '#ffffff',
scale: 1,
angle: 0,
};
mount(text({ fillPaint: patterned }));
expect(screen.getByRole('radio', { name: 'journey.studio.paint.pattern' }).getAttribute('aria-checked')).toBe(
'true'
);
// Only the pattern's own controls: the kind is picked in the row above, so the paint's tabs stay away.
expect(screen.queryByRole('tablist')).toBeNull();
});
it('says so, and offers no photo fill, when the journey has no photos', () => {
mount(text({ shadow: { dx: 1, dy: 1, color: '#111111' } }), []);
expect(screen.getByText('journey.studio.fx.noPhotos')).toBeTruthy();
@@ -8,13 +8,16 @@ import {
} from 'lucide-react';
import type { BookElement, BookSpread, BookTextElement } from '@trek/shared';
import { bookTextIsDrawn } from '@trek/shared';
import { bookTextIsDrawn, bookTextPaintFill } from '@trek/shared';
import '../../styles/studio-text.css';
import { photoSrc } from './bookRender';
import { FocalPad } from './inspector/FocalPad';
import { textEffectsInUse, textPlacementInUse } from './inspector/inspectorModel';
import { paintAs } from './paint/paintEdit';
import type { PaintChannel } from './paint/usePaintChannel';
import { Choice, Line, NumField, Section, Switch } from './StudioControls';
import { StudioPaintPicker } from './StudioPaintPicker';
import { Swatches } from './StudioSwatches';
import { defaultBand, defaultOutline, defaultShadow, firstFillPhoto, isHollow } from './textEffects';
@@ -275,10 +278,13 @@ function CurveControls({ el, set, t }: Pick<Props, 'el' | 'set' | 't'>) {
);
}
type FillKind = 'colour' | 'hollow' | 'photo';
type FillKind = 'colour' | 'hollow' | 'photo' | 'gradient' | 'pattern';
function fillKindOf(el: BookTextElement): FillKind {
if (el.fillPaint?.type === 'photo') return 'photo';
const type = el.fillPaint?.type;
if (type === 'photo') return 'photo';
if (type === 'linear' || type === 'radial') return 'gradient';
if (type === 'pattern') return 'pattern';
return isHollow(el) ? 'hollow' : 'colour';
}
@@ -294,9 +300,18 @@ function FillControls({ el, set, live, photos, spread, t }: Props) {
outline: el.outline ?? { ...defaultOutline(el), color: el.color },
} as Patch);
}
if (next === 'gradient' || next === 'pattern') {
// From the text's own colour, as a shape's paint turns from its solid fill.
return set({ fillPaint: paintAs(next, { type: 'solid', color: el.color }) } as Patch);
}
const photoId = firstFillPhoto(spread, photos);
if (photoId != null) set({ fillPaint: { type: 'photo', photoId, focalX: 0.5, focalY: 0.5, zoom: 1 } } as Patch);
};
const letterPaint = bookTextPaintFill(el);
const channel: PaintChannel = {
live: (paint) => live({ fillPaint: paint } as Patch),
commit: (paint) => set({ fillPaint: paint } as Patch),
};
return (
<div className="st-fx-group">
<Line label={t('journey.studio.fx.letters')}>
@@ -307,11 +322,22 @@ function FillControls({ el, set, live, photos, spread, t }: Props) {
{ value: 'colour' as const, label: t('journey.studio.colour') },
{ value: 'hollow' as const, label: t('journey.studio.fx.hollow') },
{ value: 'photo' as const, label: t('journey.studio.kind.photo'), disabled: !photos.length },
{ value: 'gradient' as const, label: t('journey.studio.paint.gradient') },
{ value: 'pattern' as const, label: t('journey.studio.paint.pattern') },
]}
onPick={pick}
/>
</Line>
{!photos.length && <p className="st-hint st-fx-hint">{t('journey.studio.fx.noPhotos')}</p>}
{letterPaint && (kind === 'gradient' || kind === 'pattern') && (
<StudioPaintPicker
value={letterPaint}
channel={channel}
t={t}
noneLabel={t('journey.studio.colour')}
kinds={[kind]}
/>
)}
{photo && (
<div className="st-fx-body">
<div className="st-fx-photos" role="radiogroup" aria-label={t('journey.studio.fx.pickPhoto')}>
@@ -108,6 +108,40 @@ describe('a text drawn as SVG', () => {
expect(Array.from(clip?.querySelectorAll('tspan') ?? []).map((t) => t.textContent)).toEqual(['LIS', 'BON']);
});
it('fills the letters with a gradient through a gradient of their own frame', () => {
const el = text({
fillPaint: {
type: 'linear',
angle: 90,
stops: [
{ at: 0, color: '#ff6600' },
{ at: 1, color: '#ffcc00' },
],
},
});
const { container } = render(<TextEffectsView el={el} frameStyle={frameStyle} big={false} />);
const gradient = container.querySelector('linearGradient');
expect(gradient?.getAttribute('gradientUnits')).toBe('userSpaceOnUse');
expect(Array.from(gradient?.querySelectorAll('stop') ?? []).map((s) => s.getAttribute('stop-color'))).toEqual([
'#ff6600',
'#ffcc00',
]);
const letters = container.querySelector('svg > text');
expect(letters?.getAttribute('fill')).toBe(`url(#${gradient?.getAttribute('id')})`);
});
it('fills the letters with a pattern, its ground and its marks clipped to them', () => {
const el = text({
fillPaint: { type: 'pattern', pattern: 'stripes', fg: '#d62828', bg: '#ffffff', scale: 1, angle: 0 },
});
const { container } = render(<TextEffectsView el={el} frameStyle={frameStyle} big={false} />);
const clip = container.getElementsByTagName('clipPath')[0];
const group = container.querySelector('g[clip-path]');
expect(group?.getAttribute('clip-path')).toBe(`url(#${clip?.getAttribute('id')})`);
expect(group?.querySelector('rect')?.getAttribute('fill')).toBe('#ffffff');
expect(group?.querySelectorAll('path[fill="#d62828"]').length).toBeGreaterThan(0);
});
it('gives every copy of a page its own ids, so a page drawn twice never clips to the other copy', () => {
const el = text({ fillPaint: { type: 'photo', photoId: 12 }, curve: { type: 'circle' } });
const { container } = render(
@@ -2,10 +2,11 @@ import type { CSSProperties, ReactNode } from 'react';
import { useId } from 'react';
import type { BookTextElement } from '@trek/shared';
import { bookDrawnLines, bookTextCurvePath, bookTextIsDrawn } from '@trek/shared';
import { bookDrawnLines, bookTextCurvePath, bookTextIsDrawn, bookTextPaintFill } from '@trek/shared';
import { fontStack } from './bookFonts';
import { photoSrc } from './bookRender';
import { paintParts, type PaintParts } from './paint/paintParts';
import {
caseStyle,
curveAnchor,
@@ -145,15 +146,46 @@ interface GlyphPaint {
strokeWidth?: number;
}
/** A gradient or pattern in the letters, laid out over the frame as a shape's paint is over its box. */
function lettersPaint(el: BookTextElement, id: string): PaintParts | null {
const paint = bookTextPaintFill(el);
return paint ? paintParts(paint, { x: 0, y: 0, w: el.frame.w, h: el.frame.h }, id) : null;
}
/** What the letters are filled with: the paint's fill, else their own colour. */
function lettersFill(el: BookTextElement, paint: PaintParts | null): string {
if (paint) return paint.fill;
return letterFill(el) ?? el.color;
}
/** A pattern's ground and its marks, clipped to the letters. */
function PatternLetters({ parts, w, h, clipId }: { parts: PaintParts; w: number; h: number; clipId: string }) {
const pattern = parts.pattern;
if (!pattern) return null;
return (
<g clipPath={`url(#${clipId})`}>
{parts.fill !== 'none' && <rect width={w} height={h} fill={parts.fill} />}
<g transform={pattern.transform}>
{pattern.layers.map((layer, i) => (
<path key={i} d={layer.d} fill={pattern.ink} fillOpacity={layer.opacity < 1 ? layer.opacity : undefined} />
))}
</g>
</g>
);
}
function DrawnText({ el, frameStyle, big }: { el: BookTextElement; frameStyle: CSSProperties; big: boolean }) {
const reactId = useId();
const pathId = svgId('tp-', reactId);
const clipId = svgId('tc-', reactId);
const paintId = svgId('tf-', reactId);
const { w, h } = el.frame;
const fontMm = ptToMm(el.size);
const lines = bookDrawnLines(el);
const curve = el.curve ? bookTextCurvePath(el.curve, w, h, fontMm) : null;
const photo = el.fillPaint?.type === 'photo' ? el.fillPaint : null;
const letterPaint = lettersPaint(el, paintId);
const pattern = letterPaint ? letterPaint.pattern : null;
const hollow = isHollow(el);
const { outline, shadow } = el;
@@ -211,8 +243,10 @@ function DrawnText({ el, frameStyle, big }: { el: BookTextElement; frameStyle: C
if (photo) {
const cover = photoCover(w, h, photo);
layers.push(<image key="photo" href={photoSrc(photo.photoId, big)} {...cover} clipPath={`url(#${clipId})`} />);
} else if (pattern && letterPaint) {
layers.push(<PatternLetters key="pattern" parts={letterPaint} w={w} h={h} clipId={clipId} />);
} else {
layers.push(glyphs({ fill: letterFill(el) ?? el.color }, 'fill'));
layers.push(glyphs({ fill: lettersFill(el, letterPaint) }, 'fill'));
}
return (
@@ -227,7 +261,8 @@ function DrawnText({ el, frameStyle, big }: { el: BookTextElement; frameStyle: C
>
<defs>
{curve && <path id={pathId} d={curve.d} fill="none" />}
{photo && (
{letterPaint?.defs}
{(photo || pattern) && (
<clipPath id={clipId} clipPathUnits="userSpaceOnUse">
{glyphs(null, 'clip')}
</clipPath>
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
import { useStudioStore } from '../../../store/studioStore';
import { PhotoCropExtras, PhotoLookSections } from '../photo/StudioPhotoLook';
import { FRAME_SHAPES } from '../shapes';
import { FRAME_SHAPES, shapeName } from '../shapes';
import { Choice, Line, NumField, Section } from '../StudioControls';
import { EdgeSection } from '../StudioEdgeControls';
import { ShapeGlyph } from '../StudioElementsPanel';
@@ -178,7 +178,8 @@ export function StudioPhotoInspector({
key={sh}
className={`st-mini-shape ${el.mask === sh ? 'is-on' : ''}`}
onClick={() => set({ mask: sh })}
aria-label={sh}
title={shapeName(sh, t)}
aria-label={shapeName(sh, t)}
aria-pressed={el.mask === sh}
>
<ShapeGlyph shape={sh} />
@@ -1,7 +1,7 @@
import type { BookElement, BookShapeElement, BookShapeId, BookTheme } from '@trek/shared';
import type { ReactNode } from 'react';
import { SHAPE_GROUPS } from '../shapes';
import { SHAPE_GROUPS, shapeName } from '../shapes';
import { NumField, Section } from '../StudioControls';
import { EdgeSection } from '../StudioEdgeControls';
import { ShapeGlyph } from '../StudioElementsPanel';
@@ -41,7 +41,8 @@ export function StudioShapeInspector({
key={sh}
className={`st-mini-shape ${el.shape === sh ? 'is-on' : ''}`}
onClick={() => set({ shape: sh })}
aria-label={sh}
title={shapeName(sh, t)}
aria-label={shapeName(sh, t)}
aria-pressed={el.shape === sh}
>
<ShapeGlyph shape={sh} />
@@ -0,0 +1,80 @@
import { sortedStops, stopAlpha, type BookPaint, type BookPaintStop } from '@trek/shared';
import type { ReactNode } from 'react';
import { linearLine, r2 } from './paintGeometry';
import { patternGeometry, type PatternBox } from './patterns';
/**
* A paint as SVG parts over a box (paintRender.tsx explains why the ids come
* from useId): kept apart from the components that draw them, so a shape and
* the letters of a text share it without the component file exporting a helper.
*/
/** A gradient's stops as SVG elements, in order. */
function stopElements(stops: readonly BookPaintStop[]): ReactNode {
// Two stops may share an offset (a hard edge), so the index keys them.
return sortedStops(stops).map((s, i) => (
<stop key={i} offset={r2(s.at)} stopColor={s.color} stopOpacity={stopAlpha(s) < 1 ? stopAlpha(s) : undefined} />
));
}
/** What a painted outline needs: the defs to put in the SVG, and how to fill the outline. */
export interface PaintParts {
defs: ReactNode;
fill: string;
fillOpacity?: number;
/** A pattern's geometry, to draw clipped to the outline over its fill. */
pattern: { clipId: string; layers: { d: string; opacity: number }[]; transform?: string; ink: string } | null;
}
/**
* What drawing a paint over a box takes: the gradient definition and the fill
* to point at it, or a pattern's geometry to draw clipped to the outline over
* its ground. A shape's outline and the letters of a text both draw from it.
*/
export function paintParts(paint: BookPaint, box: PatternBox, id: string): PaintParts {
switch (paint.type) {
case 'solid':
return {
defs: null,
fill: paint.color,
fillOpacity: stopAlpha(paint) < 1 ? stopAlpha(paint) : undefined,
pattern: null,
};
case 'linear': {
const line = linearLine(paint.angle, box);
return {
defs: (
<linearGradient id={id} gradientUnits="userSpaceOnUse" {...line}>
{stopElements(paint.stops)}
</linearGradient>
),
fill: `url(#${id})`,
pattern: null,
};
}
case 'radial':
return {
defs: (
<radialGradient
id={id}
gradientUnits="userSpaceOnUse"
cx={r2(box.x + paint.cx * box.w)}
cy={r2(box.y + paint.cy * box.h)}
r={r2((paint.r * Math.hypot(box.w, box.h)) / 2)}
>
{stopElements(paint.stops)}
</radialGradient>
),
fill: `url(#${id})`,
pattern: null,
};
case 'pattern': {
const geo = patternGeometry(paint, box);
return {
defs: null,
fill: paint.bg ?? 'none',
pattern: { clipId: `${id}-clip`, layers: geo.layers, transform: geo.transform, ink: paint.fg },
};
}
}
}
@@ -1,7 +1,8 @@
import { sortedStops, stopAlpha, type BookPaint, type BookPaintStop } from '@trek/shared';
import { useMemo, type ReactNode, type SVGProps } from 'react';
import { linearLine, r2 } from './paintGeometry';
import { patternGeometry, type PatternBox } from './patterns';
import type { BookPaint } from '@trek/shared';
import { useMemo, type SVGProps } from 'react';
import { r2 } from './paintGeometry';
import { paintParts } from './paintParts';
import type { PatternBox } from './patterns';
import { usePaintId } from './usePaintId';
/**
@@ -25,79 +26,6 @@ import { usePaintId } from './usePaintId';
* element's own id is kept in front only to make the markup readable.
*/
function Stops({ stops }: { stops: readonly BookPaintStop[] }) {
return (
<>
{sortedStops(stops).map((s, i) => (
<stop
// Two stops may share an offset (a hard edge), so the index keys them.
key={i}
offset={r2(s.at)}
stopColor={s.color}
stopOpacity={stopAlpha(s) < 1 ? stopAlpha(s) : undefined}
/>
))}
</>
);
}
/** What a painted outline needs: the defs to put in the SVG, and how to fill the outline. */
interface PaintParts {
defs: ReactNode;
fill: string;
fillOpacity?: number;
/** A pattern's geometry, to draw clipped to the outline over its fill. */
pattern: { clipId: string; layers: { d: string; opacity: number }[]; transform?: string; ink: string } | null;
}
function paintParts(paint: BookPaint, box: PatternBox, id: string): PaintParts {
switch (paint.type) {
case 'solid':
return {
defs: null,
fill: paint.color,
fillOpacity: stopAlpha(paint) < 1 ? stopAlpha(paint) : undefined,
pattern: null,
};
case 'linear': {
const line = linearLine(paint.angle, box);
return {
defs: (
<linearGradient id={id} gradientUnits="userSpaceOnUse" {...line}>
<Stops stops={paint.stops} />
</linearGradient>
),
fill: `url(#${id})`,
pattern: null,
};
}
case 'radial':
return {
defs: (
<radialGradient
id={id}
gradientUnits="userSpaceOnUse"
cx={r2(box.x + paint.cx * box.w)}
cy={r2(box.y + paint.cy * box.h)}
r={r2((paint.r * Math.hypot(box.w, box.h)) / 2)}
>
<Stops stops={paint.stops} />
</radialGradient>
),
fill: `url(#${id})`,
pattern: null,
};
case 'pattern': {
const geo = patternGeometry(paint, box);
return {
defs: null,
fill: paint.bg ?? 'none',
pattern: { clipId: `${id}-clip`, layers: geo.layers, transform: geo.transform, ink: paint.fg },
};
}
}
}
type OutlineProps = Omit<SVGProps<SVGPathElement>, 'fill' | 'd'>;
/**
+41 -8
View File
@@ -46,6 +46,11 @@ interface PatternGeometry {
const r2 = (v: number) => Math.round(v * 100) / 100 || 0;
const f = (v: number) => String(r2(v));
/** Threads per cell each way in linen: fine enough to read as cloth, few enough for a whole page under the cap. */
const LINEN_THREADS = 6;
/** Specks per cell in grain. */
const GRAIN_SPECKS = 3;
/**
* How many primitives a repeat needs over a region: `area` primitives per
* cell squared, `lin` per cell along the width and the height. Read before
@@ -66,6 +71,8 @@ const DENSITY: Record<BookPattern, { area: number; lin: number }> = {
scales: { area: 2, lin: 0 },
sunburst: { area: 0, lin: 0 },
confetti: { area: 1.2, lin: 0 },
linen: { area: 0, lin: 2 * LINEN_THREADS },
grain: { area: GRAIN_SPECKS, lin: 0 },
};
/** A primitive count estimate for a repeat over a region at a cell size. */
@@ -269,6 +276,24 @@ const DRAW: Record<Exclude<BookPattern, 'sunburst'>, Draw> = {
]);
}
},
// Threads of uneven weight both ways, on two half-clear layers: where they cross the cloth reads denser.
linen: (pen, g, s, alt) => {
const rand = mulberry32(Math.round(s * 1000) + 31);
const pitch = s / LINEN_THREADS;
for (const y of steps(g.y0, g.y1, pitch)) pen.rect(g.x0, y, g.x1 - g.x0, pitch * (0.2 + rand() * 0.35));
for (const x of steps(g.x0, g.x1, pitch)) alt.rect(x, g.y0, pitch * (0.2 + rand() * 0.35), g.y1 - g.y0);
},
// Specks of a few sizes, the larger ones fainter, scattered the same way on every render.
grain: (pen, g, s, alt) => {
const rand = mulberry32(Math.round(s * 1000) + 43);
const n = Math.round((((g.x1 - g.x0) * (g.y1 - g.y0)) / (s * s)) * GRAIN_SPECKS);
for (let i = 0; i < n && !pen.full && !alt.full; i++) {
const x = g.x0 + rand() * (g.x1 - g.x0);
const y = g.y0 + rand() * (g.y1 - g.y0);
if (rand() < 0.6) pen.circle(x, y, s * (0.012 + rand() * 0.012));
else alt.circle(x, y, s * (0.025 + rand() * 0.02));
}
},
};
/** A seeded generator, so a confetti page draws the same scatter on every render and in print. */
@@ -307,6 +332,13 @@ function sunburst(pen: Pen, box: PatternBox, cell: number, angle: number) {
}
}
/** The repeats drawn on two layers, and how clear each layer is: textures stay quiet over their ground. */
const LAYER_OPACITY: Partial<Record<BookPattern, readonly [number, number]>> = {
gingham: [0.5, 0.5],
linen: [0.32, 0.32],
grain: [0.5, 0.28],
};
/**
* A repeat over a box. The geometry covers the box however it is turned: the
* region drawn is the square around the box's circumscribed circle, and the
@@ -338,17 +370,18 @@ export function patternGeometry(
return { layers: [{ d: pen.d, opacity: 1 }], count: pen.count, cell };
}
// The second layer only ever holds gingham's cross bands; the cell fitted above keeps both under the cap together.
// The second layer holds gingham's and linen's cross bands and grain's larger specks;
// the cell fitted above keeps both under the cap together.
const alt = new Pen(cap);
DRAW[paint.pattern](pen, region, cell, alt);
const count = pen.count + alt.count;
const layers =
paint.pattern === 'gingham'
? [
{ d: pen.d, opacity: 0.5 },
{ d: alt.d, opacity: 0.5 },
]
: [{ d: pen.d, opacity: 1 }];
const two = LAYER_OPACITY[paint.pattern];
const layers = two
? [
{ d: pen.d, opacity: two[0] },
{ d: alt.d, opacity: two[1] },
]
: [{ d: pen.d, opacity: 1 }];
return {
layers: layers.filter((l) => l.d),
count,
+5
View File
@@ -330,3 +330,8 @@ export const HOLED_SHAPES = new Set<BookShapeId>(['gear', 'postmark', 'label-tab
*/
export const SHAPE_GROUPS = BOOK_SHAPE_GROUPS
export const FRAME_SHAPES = BOOK_FRAME_SHAPES
/** A shape's name in the reader's language, for its tooltip and a screen reader. */
export function shapeName(shape: BookShapeId, t: (k: string) => string): string {
return t(`journey.studio.shapeName.${shape}`)
}
+1 -1
View File
@@ -28,7 +28,7 @@ export function withAlpha(hex: string, alpha = 1): string {
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${Math.max(0, alpha)})`;
}
/** The colour the letters are filled with, or null when a photograph fills them. */
/** The colour the letters are filled with, or null when a photograph, a gradient or a pattern fills them. */
export function letterFill(el: BookTextElement): string | null {
const fill = el.fillPaint;
if (!fill) return el.color;
@@ -0,0 +1,37 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { createMarkdownHtml } from './markdownHtml';
import { SafeMarkdown } from './SafeMarkdown';
describe('SafeMarkdown (#2692)', () => {
it('FE-COMP-SAFEMD-001: renders GitHub markdown with kept line breaks and safe links', () => {
render(<SafeMarkdown>{'# Title\nfirst\nsecond\n\n[site](https://example.com)'}</SafeMarkdown>);
expect(screen.getByRole('heading', { name: 'Title' })).toBeInTheDocument();
expect(document.querySelector('br')).not.toBeNull();
const link = screen.getByRole('link', { name: 'site' });
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer nofollow');
});
it('FE-COMP-SAFEMD-002: shows raw HTML as text and never as an element', () => {
render(<SafeMarkdown>{'Room <TBD> <img src=x onerror=alert(1)>'}</SafeMarkdown>);
expect(document.querySelector('img')).toBeNull();
expect(screen.getByText(/Room <TBD>/)).toBeInTheDocument();
});
});
describe('createMarkdownHtml (#2692)', () => {
it('FE-COMP-SAFEMD-003: returns the sanitized HTML, the same for the same text', () => {
const markdown = createMarkdownHtml();
try {
const html = markdown.html('**bold** and <script>alert(1)</script>');
expect(html).toContain('<strong>bold</strong>');
expect(html).not.toContain('<script>');
expect(markdown.html('**bold** and <script>alert(1)</script>')).toBe(html);
expect(markdown.html('- a\n- b')).toContain('<li>a</li>');
} finally {
markdown.dispose();
}
});
});
@@ -0,0 +1,22 @@
import Markdown from 'react-markdown';
import remarkBreaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import { sanitizedMarkdownComponents, sanitizedMarkdownPlugins } from './markdownSanitize';
/**
* Text a user wrote in markdown (a place's description, its notes), rendered
* the way the planner shows it: GitHub flavour, line breaks kept, raw HTML
* shown as text, the rest sanitized, links in a new tab without an opener.
* Safe for pages strangers see, like a shared trip (#2692).
*/
export function SafeMarkdown({ children }: { children: string }) {
return (
<Markdown
remarkPlugins={[remarkGfm, remarkBreaks]}
rehypePlugins={sanitizedMarkdownPlugins}
components={sanitizedMarkdownComponents}
>
{children}
</Markdown>
);
}
@@ -0,0 +1,27 @@
import { flushSync } from 'react-dom';
import { createRoot } from 'react-dom/client';
import { SafeMarkdown } from './SafeMarkdown';
/**
* Turns markdown into the HTML {@link SafeMarkdown} renders, for output built as
* a string such as the PDF export (#2692). It renders through the same
* pipeline into a detached node, so the sanitizer is the one the app uses and
* no second markdown library is bundled. Call it outside a render and dispose
* of it when done; the same text is rendered once.
*/
export function createMarkdownHtml(): { html: (text: string) => string; dispose: () => void } {
const host = document.createElement('div');
const root = createRoot(host);
const done = new Map<string, string>();
return {
html: (text) => {
const known = done.get(text);
if (known !== undefined) return known;
flushSync(() => root.render(<SafeMarkdown>{text}</SafeMarkdown>));
done.set(text, host.innerHTML);
return host.innerHTML;
},
dispose: () => root.unmount(),
};
}
@@ -224,7 +224,7 @@ export default function MPlanTimeline({ planner, shell }: MPlanTimelineProps) {
drag={dragFor(row)}
onOpen={() => {
if (editing) tl.editTransport(row.res)
else shell.openSheet('transport', { reservationId: row.res.id })
else shell.openSheet('transport', { reservationId: row.res.id, legIndex: row.res.__leg?.index })
}}
/>
{dayScheduleFor('reservation', row.res.id)}
@@ -265,7 +265,7 @@ export function TransportRow({ res, dayId, chrome, reorder, drag, onOpen }: {
const time = start
? `${fmtTime(start, chrome)}${phase === 'single' && end ? ` – ${fmtTime(end, chrome)}` : ''}`
: ''
const sub = transportSubtitle(res)
const sub = transportSubtitle(res, chrome.t)
return (
<div
@@ -5,6 +5,7 @@ import {
import { getDayBookendHotels, isDayInAccommodationRange } from '../../../../utils/dayOrder'
import type { MergedItem } from '../../../../utils/dayMerge'
import type { TransitLegDisplay } from '../../../../components/Planner/transitDisplay'
import { transportSubtitle as dayPlanTransportSubtitle, type TransportLegDetail } from '../../../../components/Planner/transportDetailModel'
import { projectDayItinerary } from '../../../../components/Map/dayTourProjection'
import type { Accommodation, Assignment, Day, DayNote, Place, Reservation, RouteSegment, TranslationFn } from '../../../../types'
@@ -16,7 +17,7 @@ import type { Accommodation, Assignment, Day, DayNote, Place, Reservation, Route
/** A day-timeline transport row can be a synthetic per-leg expansion of a multi-leg booking. */
export type TransportEntry = Reservation & {
__leg?: { index: number; total: number; from: string | null; to: string | null }
__leg?: TransportLegDetail & { total: number }
}
export interface TransitMeta {
@@ -48,26 +49,8 @@ export function getTransitMeta(res: Reservation): TransitMeta | null {
}
/** Same subtitle language as the desktop day plan: airline/number, train/platform/seat, leg detail. */
export function transportSubtitle(res: TransportEntry): string {
const meta = parseReservationMeta(res) as Record<string, string | undefined>
if (res.__leg) {
const parts = [res.__leg.from, res.__leg.to].filter(Boolean).length
? [[res.__leg.from, res.__leg.to].filter(Boolean).join(' → ')]
: []
return parts.join(' · ')
}
if (res.type === 'flight') {
const parts = [meta.airline, meta.flight_number].filter(Boolean) as string[]
if (meta.departure_airport || meta.arrival_airport) {
parts.push([meta.departure_airport, meta.arrival_airport].filter(Boolean).join(' → '))
}
return parts.join(' · ')
}
if (res.type === 'train') {
return [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Sitz ${meta.seat}` : '']
.filter(Boolean).join(' · ')
}
return res.location || ''
export function transportSubtitle(res: TransportEntry, t: TranslationFn): string {
return dayPlanTransportSubtitle(res, t) ?? (res.location || '')
}
const sameCoord = (a: [number, number], b: [number, number]): boolean =>
@@ -6,7 +6,7 @@ import { useTranslation } from '../../../../i18n'
import { useSettingsStore } from '../../../../store/settingsStore'
import { RES_ICONS } from '../../../../components/Planner/DayPlanSidebar.constants'
import { splitReservationDateTime } from '../../../../utils/formatters'
import { getFlightLegs, getTrainLegs, usesStationRoute } from '../../../../utils/flightLegs'
import { getFlightLegs, getTrainLegs, usesStationRoute, type FlightLeg, type TrainLeg } from '../../../../utils/flightLegs'
import { openFile } from '../../../../utils/fileDownload'
import { runsOnDay } from '../../../../utils/reservationRoutes'
import { filesFor } from '../../../../utils/reservationFiles'
@@ -19,6 +19,8 @@ import { Eyebrow, INNER_CLS, StatBox, TileHeader, displayTime } from './MTripShe
interface TransportSheetPayload {
reservationId?: number
/** The leg of a multi-leg train or flight the day plan row stood for (#2701). */
legIndex?: number
}
interface TransitLeg {
@@ -118,24 +120,38 @@ export default function MTransportSheet({ planner, shell }: MTripSheetsProps) {
const to = (res.endpoints || []).find(e => e.role === 'to')
const { date, time: startTime } = splitReservationDateTime(res.reservation_time)
const { time: endTime } = splitReservationDateTime(res.reservation_end_time)
const depTime = from?.local_time || startTime
const arrTime = to?.local_time || endTime
const subParts: string[] = []
if (meta.airline) subParts.push(meta.airline)
if (meta.flight_number) subParts.push(meta.flight_number)
if (meta.train_number) subParts.push(meta.train_number)
if (from?.name && to?.name) subParts.push(`${from.name} → ${to.name}`)
else if (date) subParts.push(bookingDayLabel(date, locale))
const seat = meta.seat
const platform = meta.platform
const transitLegs: TransitLeg[] = Array.isArray(meta.transit?.legs) ? meta.transit.legs : []
const bookingDepTime = from?.local_time || startTime
const bookingArrTime = to?.local_time || endTime
// Per-segment booking codes (#1943), only on a real stopover booking: the
// single-leg fallback would just echo the booking's own code shown below.
const routeLegs = res.type === 'flight' ? getFlightLegs(res) : usesStationRoute(res.type) ? getTrainLegs(res) : []
const isFlight = res.type === 'flight'
const routeLegs: (FlightLeg | TrainLeg)[] = isFlight ? getFlightLegs(res) : usesStationRoute(res.type) ? getTrainLegs(res) : []
const legCodes = routeLegs.length > 1 ? routeLegs.filter(l => l.confirmation_number) : []
// Opened from one leg's row, the sheet shows that leg's own number, seat,
// platform and times rather than the first leg's (#2701).
const leg = routeLegs.length > 1 && payload.legIndex != null ? routeLegs[payload.legIndex] ?? null : null
const legDetail = (leg ?? {}) as Partial<FlightLeg & TrainLeg>
const depTime = leg ? leg.dep_time || '' : bookingDepTime
const arrTime = leg ? leg.arr_time || '' : bookingArrTime
const depLabel = (leg ? leg.from : from?.name) || t('reservations.time')
const arrLabel = (leg ? leg.to : to?.name) || t('reservations.time')
const subParts: string[] = []
const airline = leg ? legDetail.airline : meta.airline
const flightNumber = leg ? legDetail.flight_number : meta.flight_number
const trainNumber = leg ? legDetail.train_number : meta.train_number
if (airline) subParts.push(airline)
if (flightNumber) subParts.push(flightNumber)
if (trainNumber) subParts.push(trainNumber)
if (leg && (leg.from || leg.to)) subParts.push([leg.from, leg.to].filter(Boolean).join(' → '))
else if (from?.name && to?.name) subParts.push(`${from.name} → ${to.name}`)
else if (date) subParts.push(bookingDayLabel(date, locale))
// A flight's legs carry no seat or platform of their own, so those stay the booking's.
const seat = leg && !isFlight ? legDetail.seat : meta.seat
const platform = leg && !isFlight ? legDetail.platform : meta.platform
const transitLegs: TransitLeg[] = Array.isArray(meta.transit?.legs) ? meta.transit.legs : []
const resFiles = filesFor(res, planner.files || [])
@@ -186,10 +202,10 @@ export default function MTransportSheet({ planner, shell }: MTripSheetsProps) {
{(depTime || arrTime || seat || platform) && (
<div className="mt-[14px] flex gap-[6px]">
{depTime && (
<StatBox value={displayTime(depTime, locale, timeFormat)} label={from?.name || t('reservations.time')} />
<StatBox value={displayTime(depTime, locale, timeFormat)} label={depLabel} />
)}
{arrTime && (
<StatBox value={displayTime(arrTime, locale, timeFormat)} label={to?.name || t('reservations.time')} />
<StatBox value={displayTime(arrTime, locale, timeFormat)} label={arrLabel} />
)}
{platform && <StatBox value={platform} label={t('reservations.meta.platform')} />}
{seat && <StatBox value={seat} label={t('reservations.meta.seat')} />}
+2 -1
View File
@@ -9,6 +9,7 @@ import {
import type { Journey } from '../store/journeyStore'
import { computeJourneyLifecycle } from '../utils/journeyLifecycle'
import { useJourney } from './journey/useJourney'
import { journeyYearLabel } from './journey/journeyModel'
import HelpAnchor from '../components/Help/HelpAnchor'
import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../components/shared/DialogShell'
import { EditorField, INPUT } from '../components/shared/dialogParts'
@@ -438,7 +439,7 @@ function JourneyCard({ journey, onClick }: { journey: Journey & { entry_count?:
<span className="absolute top-3.5 start-3.5 z-[2] inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-white text-[11px] font-semibold"
style={{ background: 'rgba(255,255,255,0.18)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', border: '1px solid rgba(255,255,255,0.26)' }}>
<Calendar size={12} strokeWidth={2.2} />
{new Date(j.created_at).getFullYear()}
{journeyYearLabel(j)}
</span>
{lifecycle !== 'live' && (
<span className="absolute top-3.5 end-3.5 z-[2] px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-wide text-white"
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { journeyYearLabel } from './journeyModel';
describe('journeyYearLabel (#2685)', () => {
it('FE-PAGE-JOURNEY-YEAR-001: names the year of the trips, not the year the journey was written', () => {
expect(
journeyYearLabel({ trip_date_min: '2024-09-12', trip_date_max: '2024-09-20', created_at: '2026-03-01 10:00:00' })
).toBe('2024');
});
it('FE-PAGE-JOURNEY-YEAR-002: a trip over New Year names both years', () => {
expect(
journeyYearLabel({ trip_date_min: '2024-12-28', trip_date_max: '2025-01-04', created_at: '2026-03-01 10:00:00' })
).toBe('2024/2025');
});
it('FE-PAGE-JOURNEY-YEAR-003: one known date is enough, and without any it falls back to when it was written', () => {
expect(
journeyYearLabel({ trip_date_min: null, trip_date_max: '2023-05-02', created_at: '2026-03-01 10:00:00' })
).toBe('2023');
expect(journeyYearLabel({ trip_date_min: null, trip_date_max: null, created_at: '2026-03-01 10:00:00' })).toBe(
'2026'
);
});
});
+13
View File
@@ -0,0 +1,13 @@
/** The year a journey card names: the years its trips cover, not the year it was written (#2685). */
export function journeyYearLabel(journey: {
trip_date_min?: string | null;
trip_date_max?: string | null;
created_at?: string | number | null;
}): string {
// The dates are calendar dates, so the year is read from the text, not through a Date in some zone.
const from = journey.trip_date_min?.slice(0, 4);
const to = journey.trip_date_max?.slice(0, 4);
if (from && to && to !== from) return `${from}/${to}`;
if (from || to) return (from || to) as string;
return journey.created_at ? String(new Date(journey.created_at).getFullYear()) : '';
}
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '../../../tests/helpers/render';
import { SharedPlaceDetails } from './SharedPlaceDetails';
/** FE-PAGE-SHARED-PLACE-001: a stop's description and notes on a shared trip, read as the planner shows them (#2692). */
describe('SharedPlaceDetails', () => {
it('FE-PAGE-SHARED-PLACE-001: renders markdown in the description and notes, and keeps raw HTML as text', () => {
render(
<SharedPlaceDetails
place={{
name: 'Market',
address: 'Market Square 1',
description: [
'## Opening',
'',
'| Day | Hours |',
'| --- | --- |',
'| Sat | 8 to 14 |',
'',
'<img src=x onerror=alert(1)>',
].join('\n'),
notes: ['- fruit', '- bread'].join('\n'),
}}
assignmentNotes="**Bring** cash"
/>
);
expect(screen.getByRole('heading', { name: 'Opening' })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: '8 to 14' })).toBeInTheDocument();
expect(screen.getByText('Bring').tagName).toBe('STRONG');
expect(screen.getByText('fruit').tagName).toBe('LI');
expect(document.querySelector('img[src="x"]')).toBeNull();
expect(screen.queryByText('## Opening')).toBeNull();
});
});
@@ -3,6 +3,7 @@ import { useTranslation } from '../../i18n'
import { fs } from '../../components/shared/DialogShell'
import { Tooltip } from '../../components/shared/Tooltip'
import { WhiteBadge } from '../../components/Planner/planParts'
import { SafeMarkdown } from '../../components/shared/SafeMarkdown'
import { getGoogleMapsUrlForPlace } from '../../components/Planner/placeGoogleMaps'
import { formatDurationMinutes, isHttpUrl, plannedStayMinutes } from './sharedTripModel'
@@ -61,7 +62,7 @@ export function SharedPlaceDetails({ place, assignmentNotes }: { place: SharedPl
</div>
)}
{place.description && (
<div className="whitespace-pre-wrap text-content-secondary [overflow-wrap:anywhere]" style={fs(12, 'body')}>{place.description}</div>
<div className="collab-note-md-full text-content-secondary [overflow-wrap:anywhere]" style={fs(12, 'body')}><SafeMarkdown>{place.description}</SafeMarkdown></div>
)}
{dayNote && <NoteLine label={t('places.assignmentNotes')} text={dayNote} strong />}
{placeNote && <NoteLine label={t('places.formNotes')} text={placeNote} />}
@@ -98,7 +99,7 @@ function NoteLine({ label, text, strong = false }: { label: string; text: string
<Tooltip label={label} placement="top">
<div className={`flex items-start gap-1.5 rounded-lg bg-surface-secondary px-2 py-1.5 ${strong ? 'text-content-secondary' : 'text-content-muted'}`} style={fs(11.5, 'body')}>
<FileText size={11} strokeWidth={2} className="mt-0.5 flex-none text-content-faint" aria-label={label} />
<span className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]">{text}</span>
<div className="collab-note-md-full min-w-0 [overflow-wrap:anywhere]"><SafeMarkdown>{text}</SafeMarkdown></div>
</div>
</Tooltip>
)
+2 -1
View File
@@ -21,6 +21,7 @@ import { aiConnectionsRepo } from '../repo/aiConnectionsRepo';
import { assistantRepo } from '../repo/assistantRepo';
import { OnlineOnlyError } from '../repo/onlineOnly';
import type { Assignment } from '../types';
import { browserTimeZone } from '../utils/localDate';
import { randomId } from '../utils/randomId';
import type { RouteProfileFocus } from '../utils/routeGeometry';
import { assistantScopeOf, assistantTargetOf, type AssistantChatKey } from './assistantChatKey';
@@ -331,7 +332,7 @@ function errorCodeOf(err: unknown): AssistantErrorCode {
function clientContext() {
return {
locale: useSettingsStore.getState().settings.language || 'en',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
timeZone: browserTimeZone() ?? 'UTC',
};
}
+2 -1
View File
@@ -18,6 +18,7 @@ import { forgetServerLanguage } from './settingsStore'
import { forgetResumeRoute } from '../utils/resumeRoute'
import { markSignedOut, clearSignedOut } from '../utils/signedOut'
import { forgetPushDeviceOnLogout, resyncPushSubscription } from '../push/webPush'
import { browserTimeZone } from '../utils/localDate'
/** How long a logout waits for the queued changes to go out. */
const LOGOUT_FLUSH_MS = 5000
@@ -154,7 +155,7 @@ export const useAuthStore = create<AuthState>()(
hasMapsKey: false,
hasAmapKey: false,
placesProvider: 'auto',
serverTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
serverTimezone: browserTimeZone() ?? '',
appRequireMfa: false,
tripRemindersEnabled: false,
placesPhotosEnabled: true,
+10 -1
View File
@@ -1,5 +1,5 @@
import { create } from 'zustand'
import type { BookDocument, BookElement, BookFrame, BookLayout, BookSpread } from '@trek/shared'
import type { BookAlignTo, BookDocument, BookElement, BookFrame, BookLayout, BookSpread } from '@trek/shared'
import {
MAX_BOOK_LAYOUTS, MAX_SPREAD_ELEMENTS, applySavedBookLayout as applySavedLayout,
bookLayoutFromSpread as layoutFromSpread, canEditSpread as isMovableSpread, fitBookTexts, innerSpreadBounds as innerBounds,
@@ -43,6 +43,13 @@ interface StudioState extends StudioLayerActions {
refusedFull: number
/** Notes that an addition was refused because the page is full. */
refuseFull: () => void
/**
* What the arrange section lines things up with, as last picked: kept for the
* session, so a new selection or another kind of element does not reset it.
* Null until somebody picks; the section then chooses by how much is selected.
*/
alignTo: BookAlignTo | null
setAlignTo: (to: BookAlignTo) => void
/**
* The page a chat card points at, outlined in the page rail (journey-studio.md
* 4.2). Not part of the book or its history; opening a book clears it.
@@ -157,6 +164,8 @@ export const useStudioStore = create<StudioState>((set, get) => ({
clipboard: null,
refusedFull: 0,
refuseFull: () => set(st => ({ refusedFull: st.refusedFull + 1 })),
alignTo: null,
setAlignTo: to => set({ alignTo: to }),
hintSpread: null,
readOnly: false,
+24 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { isLocalToday, localIsoDate, localMinutes } from './localDate'
import { browserTimeZone, isLocalToday, isValidTimeZone, localIsoDate, localMinutes } from './localDate'
/**
* "Today" must be the user's local calendar date. `toISOString()` is the UTC
@@ -66,3 +66,26 @@ describe('isLocalToday', () => {
expect(isLocalToday(undefined)).toBe(false)
})
})
describe('browserTimeZone and isValidTimeZone (#2694)', () => {
it('names the browser zone, and nothing when the browser names none', () => {
expect(browserTimeZone()).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone)
const original = Intl.DateTimeFormat.prototype.resolvedOptions
const spy = vi.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockImplementation(function (this: Intl.DateTimeFormat) {
return { ...original.call(this), timeZone: undefined as unknown as string }
})
try {
expect(browserTimeZone()).toBeUndefined()
} finally {
spy.mockRestore()
}
})
it('accepts the zones Intl knows and refuses the rest', () => {
expect(isValidTimeZone('Europe/Berlin')).toBe(true)
expect(isValidTimeZone('UTC')).toBe(true)
expect(isValidTimeZone('Mars/Olympus_Mons')).toBe(false)
expect(isValidTimeZone('')).toBe(false)
expect(isValidTimeZone(undefined)).toBe(false)
})
})
+21
View File
@@ -27,3 +27,24 @@ export function localMinutes(d: Date = new Date()): number {
export function isLocalToday(date: string | null | undefined): boolean {
return !!date && date.slice(0, 10) === localIsoDate()
}
/**
* The browser's IANA time zone, or undefined when it cannot name one. Chrome
* on Windows answers undefined for a system zone it cannot map, such as
* Alberta's permanent UTC-6 (#2694), so every reader picks its own fallback.
*/
export function browserTimeZone(): string | undefined {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone as string | undefined
return typeof tz === 'string' && tz ? tz : undefined
}
/** Whether `Intl` accepts the zone; formatting with one it rejects throws a RangeError. */
export function isValidTimeZone(tz: unknown): tz is string {
if (typeof tz !== 'string' || !tz) return false
try {
new Intl.DateTimeFormat(undefined, { timeZone: tz })
return true
} catch {
return false
}
}
@@ -193,7 +193,7 @@ describe('TransportRow', () => {
expect(screen.getByText('LH 714 to Tokyo')).toBeInTheDocument()
expect(container.textContent).toContain('08:15 – 18:40')
expect(screen.getByText('Lufthansa · LH714 · MUC → HND')).toBeInTheDocument()
expect(screen.getByText('Lufthansa, LH714, MUC → HND')).toBeInTheDocument()
})
it('FE-MOB-PLROW-014: a multi-day span shows only the day-relevant time', () => {
@@ -234,7 +234,7 @@ describe('TransportRow', () => {
} as unknown as TransportEntry
render(<TransportRow {...base} res={train} />)
expect(screen.getByText('ICE 599 · Gl. 7 · Sitz 21A')).toBeInTheDocument()
expect(screen.getByText('ICE 599, reservations.meta.platform 7, reservations.meta.seat 21A')).toBeInTheDocument()
})
it('FE-MOB-PLROW-018: an untimed booking of an unknown type still renders its title', () => {
@@ -7,7 +7,7 @@ import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
import { resetAllStores, seedStore } from '../../../helpers/store'
import { fireEvent, render, screen } from '../../../helpers/render'
// FE-MOB-TRSH-001 to FE-MOB-TRSH-033
// FE-MOB-TRSH-001 to FE-MOB-TRSH-035
vi.mock('../../../../src/utils/fileDownload', () => ({ openFile: vi.fn() }))
@@ -442,4 +442,34 @@ describe('MTransportSheet', () => {
expect(planner.handleSelectDay).toHaveBeenCalledWith(3, false)
})
})
// #2701: a leg row opens the sheet on that leg, not on the booking's first one.
describe('opened from one leg of a multi-leg train', () => {
const TRAIN = {
...FLIGHT, id: 8, type: 'train', title: 'Takayama to Kyoto', confirmation_number: null,
metadata: JSON.stringify({
train_number: 'Hida 6', seat: '10-4C',
legs: [
{ from: 'Takayama', to: 'Nagoya', train_number: 'Hida 6', seat: '10-4C', platform: '1', dep_time: '09:40', arr_time: '12:10' },
{ from: 'Nagoya', to: 'Kyoto', train_number: 'Nozomi 67', seat: '4-5A', platform: '14', dep_time: '12:35', arr_time: '13:10' },
],
}),
} as unknown as Reservation
it("FE-MOB-TRSH-034: shows the second leg's own train, seat, platform and times", () => {
render(<MTransportSheet planner={makePlanner({ reservations: [TRAIN] })} shell={makeShell({ sheet: { id: 'transport', payload: { reservationId: 8, legIndex: 1 } } })} />)
expect(screen.getByText(/Nozomi 67/)).toBeInTheDocument()
expect(screen.getByText('4-5A')).toBeInTheDocument()
expect(screen.getByText('14')).toBeInTheDocument()
expect(screen.getByText('12:35')).toBeInTheDocument()
expect(screen.getByText('13:10')).toBeInTheDocument()
expect(screen.queryByText('10-4C')).not.toBeInTheDocument()
})
it('FE-MOB-TRSH-035: without a leg the sheet still shows the booking as a whole', () => {
render(<MTransportSheet planner={makePlanner({ reservations: [TRAIN] })} shell={makeShell({ sheet: { id: 'transport', payload: { reservationId: 8 } } })} />)
expect(screen.getByText('10-4C')).toBeInTheDocument()
expect(screen.queryByText('4-5A')).not.toBeInTheDocument()
})
})
})
@@ -128,14 +128,17 @@ describe('planTimelineModel — metadata parsing', () => {
})
describe('planTimelineModel — transportSubtitle', () => {
// The labels stay keys, so the assertions read what the subtitle puts where.
const tr: TranslationFn = (key) => key
it('FE-MOB-PTLM-006: renders a synthetic leg as from → to', () => {
const res = { ...buildReservation({ type: 'flight' }), __leg: { index: 0, total: 2, from: 'FRA', to: 'IST' } } as TransportEntry
expect(transportSubtitle(res)).toBe('FRA → IST')
expect(transportSubtitle(res, tr)).toBe('FRA → IST')
})
it('FE-MOB-PTLM-007: renders a leg without endpoints as an empty subtitle', () => {
const res = { ...buildReservation({ type: 'flight' }), __leg: { index: 1, total: 2, from: null, to: null } } as TransportEntry
expect(transportSubtitle(res)).toBe('')
expect(transportSubtitle(res, tr)).toBe('')
})
it('FE-MOB-PTLM-008: joins airline, flight number and airports for a flight', () => {
@@ -143,25 +146,42 @@ describe('planTimelineModel — transportSubtitle', () => {
type: 'flight',
metadata: JSON.stringify({ airline: 'LH', flight_number: 'LH123', departure_airport: 'FRA', arrival_airport: 'HND' }),
})
expect(transportSubtitle(res)).toBe('LH · LH123 · FRA → HND')
expect(transportSubtitle(res, tr)).toBe('LH, LH123, FRA → HND')
})
it('FE-MOB-PTLM-009: omits the airport pair when neither airport is known', () => {
const res = buildReservation({ type: 'flight', metadata: JSON.stringify({ airline: 'LH', flight_number: 'LH123' }) })
expect(transportSubtitle(res)).toBe('LH · LH123')
expect(transportSubtitle(res, tr)).toBe('LH, LH123')
})
it('FE-MOB-PTLM-010: labels platform and seat for a train', () => {
const full = buildReservation({
type: 'train', metadata: JSON.stringify({ train_number: 'ICE 599', platform: '7', seat: '32A' }),
})
expect(transportSubtitle(full)).toBe('ICE 599 · Gl. 7 · Sitz 32A')
expect(transportSubtitle(buildReservation({ type: 'train', metadata: '{"train_number":"ICE 599"}' }))).toBe('ICE 599')
expect(transportSubtitle(full, tr)).toBe('ICE 599, reservations.meta.platform 7, reservations.meta.seat 32A')
expect(transportSubtitle(buildReservation({ type: 'train', metadata: '{"train_number":"ICE 599"}' }), tr)).toBe('ICE 599')
})
// #2701: a leg names its own train, platform and seat, not the first leg's.
it('FE-MOB-PTLM-011b: a train leg carries its own number, platform and seat', () => {
const res = {
...buildReservation({ type: 'train', metadata: JSON.stringify({ train_number: 'Hida 6', seat: '10-4C' }) }),
__leg: { index: 1, total: 2, from: 'Nagoya', to: 'Kyoto', train_number: 'Nozomi 67', platform: '14', seat: '4-5A' },
} as TransportEntry
expect(transportSubtitle(res, tr)).toBe('Nozomi 67, reservations.meta.platform 14, reservations.meta.seat 4-5A, Nagoya → Kyoto')
})
it('FE-MOB-PTLM-011c: a flight leg carries its own airline and flight number', () => {
const res = {
...buildReservation({ type: 'flight' }),
__leg: { index: 1, total: 2, from: 'IST', to: 'HND', airline: 'TK', flight_number: 'TK52' },
} as TransportEntry
expect(transportSubtitle(res, tr)).toBe('TK, TK52, IST → HND')
})
it('FE-MOB-PTLM-011: falls back to the location for any other type', () => {
expect(transportSubtitle(buildReservation({ type: 'bus', location: 'Central Station' }))).toBe('Central Station')
expect(transportSubtitle(buildReservation({ type: 'bus', location: null }))).toBe('')
expect(transportSubtitle(buildReservation({ type: 'bus', location: 'Central Station' }), tr)).toBe('Central Station')
expect(transportSubtitle(buildReservation({ type: 'bus', location: null }), tr)).toBe('')
})
})
+1 -1
View File
@@ -487,7 +487,7 @@ describe('a shape', () => {
load([shape({ shape: 'rect', fill: '#c2410c' })])
const { container } = draw()
fireEvent.click(container.querySelector('.st-mini-shapes [aria-label="ellipse"]')!)
fireEvent.click(container.querySelector('.st-mini-shapes [aria-label="journey.studio.shapeName.ellipse"]')!)
expect(el()).toMatchObject({ shape: 'ellipse', fill: '#c2410c', frame: FRAME })
})
@@ -1,4 +1,4 @@
import type { BookPaint } from '@trek/shared';
import { BOOK_PATTERNS, type BookPaint } from '@trek/shared';
import { useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { StudioPaintPicker } from '../../../src/components/Studio/StudioPaintPicker';
@@ -172,7 +172,7 @@ describe('a pattern', () => {
it('draws a tile for each repeat, turns stripes to the diagonal, and lets the ground go', () => {
const channel = spy();
const { container } = render(<Harness start={start} channel={channel} />);
expect(container.querySelectorAll('.st-pattern-tile')).toHaveLength(12);
expect(container.querySelectorAll('.st-pattern-tile')).toHaveLength(BOOK_PATTERNS.length);
expect(need(container.querySelector('.st-pattern-tile[aria-checked="true"]')).getAttribute('aria-label')).toBe(
'journey.studio.pattern.dots'
);
@@ -182,7 +182,7 @@ describe('a pattern', () => {
fireEvent.click(need(container.querySelector('[role="switch"]')));
expect(last(channel.commit)).toMatchObject({ bg: null });
expect(container.querySelectorAll('.st-pattern-tile.is-clear')).toHaveLength(12);
expect(container.querySelectorAll('.st-pattern-tile.is-clear')).toHaveLength(BOOK_PATTERNS.length);
fireEvent.click(need(container.querySelector('[role="switch"]')));
expect(last(channel.commit)).toMatchObject({ bg: '#ffffff' });
});
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { useStudioStore } from '../../../src/store/studioStore';
/** FE-STUDIO-ALIGNTO-001: what the arrange section lines things up with stays as picked across selections. */
describe('the align target', () => {
it('FE-STUDIO-ALIGNTO-001: is kept when the selection changes', () => {
useStudioStore.setState({ alignTo: null, selection: [] });
useStudioStore.getState().setAlignTo('safe');
useStudioStore.getState().select(['a']);
useStudioStore.getState().select(['b', 'c']);
expect(useStudioStore.getState().alignTo).toBe('safe');
});
});
+2 -2
View File
@@ -160,7 +160,7 @@ export class JourneyBookMcp {
@Tool({
name: 'add_book_pages',
description: `Add 1 to ${BOOK_OP_LIMITS.pagesPerCall} empty double spreads after afterSpreadId, always kept between the first and the last page, laid out on template when one is named and tied to the journey entry entryId when given. ${WRITE_ANSWER}`,
description: `Add 1 to ${BOOK_OP_LIMITS.pagesPerCall} empty double spreads after afterSpreadId, always kept between the first and the last page, laid out on template when one is named (a built-in or decorated double-spread layout from list_book_layouts) and tied to the journey entry entryId when given. ${WRITE_ANSWER}`,
inputSchema: { journeyId: idSchema, ...addBookPagesArgs },
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
when: journeyAddonOn,
@@ -208,7 +208,7 @@ export class JourneyBookMcp {
@Tool({
name: 'set_book_cover',
description: `The cover of a journey's book: its layout (a single page template), its photo (from the journey's gallery, into a free frame, else a full bleed one behind everything), its title and its subtitle. A title or subtitle bound to the journey's own stays bound unless other words are given. With returnPrevious the answer also holds the cover as it was, under previous_spreads, and a digest of the cover as the write left it under page_digests (for restore_book_page expectedPages). ${WRITE_ANSWER}`,
description: `The cover of a journey's book: its layout (a built-in or decorated single-page template from list_book_layouts), its photo (from the journey's gallery, into a free frame, else a full bleed one behind everything), its title and its subtitle. A title or subtitle bound to the journey's own stays bound unless other words are given. With returnPrevious the answer also holds the cover as it was, under previous_spreads, and a digest of the cover as the write left it under page_digests (for restore_book_page expectedPages). ${WRITE_ANSWER}`,
inputSchema: { journeyId: idSchema, ...setBookCoverArgs },
annotations: TOOL_ANNOTATIONS_WRITE,
when: journeyAddonOn,
+2 -2
View File
@@ -1,7 +1,7 @@
import {
BOOK_FINDING_CODES,
BOOK_OP_LIMITS,
bookCoverTemplateIdSchema,
bookAnyTemplateIdSchema,
bookPageOpSchema,
bookSpreadIdSchema,
} from '../../book/book-ops.schema';
@@ -63,7 +63,7 @@ export type BookCardLayout = z.infer<typeof bookCardLayoutSchema>;
/** `set_book_cover`'s arguments, null where the call leaves a part of the cover as it is. */
export const bookCardCoverSchema = z.object({
template: bookCoverTemplateIdSchema.nullable(),
template: bookAnyTemplateIdSchema.nullable(),
photoId: idSchema.nullable(),
title: z.string().max(MAX_BOOK_TITLE).nullable(),
subtitle: z.string().max(MAX_BOOK_TITLE).nullable(),
+7 -2
View File
@@ -76,7 +76,12 @@ export function photoDpi(el: BookPhotoElement, info: BookPhotoInfo | undefined):
}
/** Average letter width per em at weight 400, per family: the catalogue's measure (book-fonts.data.ts). */
const letterEm = (font: string) => bookFontFace(font)?.letterEm ?? 0.5;
/** A family's average letter width per em at a weight: its measured bold from 600 on, else a guess a little wider. */
function letterEm(font: string, weight: number): number {
const face = bookFontFace(font);
const regular = face?.letterEm ?? 0.5;
return weight >= 600 ? (face?.boldEm ?? regular * 1.06) : regular;
}
/**
* The headroom on letter width the design engine sizes a text frame with. A
@@ -124,7 +129,7 @@ export function bookLetterMm(el: BookTextElement, headroom = 1): number {
const sizeMm = (orDefault(el.size, 11) * 25.4) / 72;
const tracking = orDefault(el.tracking, 0);
const caps = bookFontFace(el.font)?.caps ? 1 : (CASE_WIDTH[el.transform ?? 'none'] ?? 1);
return sizeMm * (letterEm(el.font) * (el.weight >= 600 ? 1.06 : 1) * caps * headroom + tracking);
return sizeMm * (letterEm(el.font, el.weight) * caps * headroom + tracking);
}
/**
+20
View File
@@ -58,6 +58,11 @@ export interface BookFontFace {
* the face's own advance widths, for the text measure in book-findings.ts.
*/
letterEm: number;
/**
* The same at weight 700, measured the same way, for a family that ships a
* real bold: a condensed bold sets up to a sixth wider than its regular.
*/
boldEm?: number;
/** Set in capitals whatever is typed. */
caps?: true;
}
@@ -81,6 +86,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.507,
boldEm: 0.519,
},
inter: {
name: 'Inter',
@@ -91,6 +97,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.478,
boldEm: 0.491,
},
montserrat: {
name: 'Montserrat',
@@ -101,6 +108,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.507,
boldEm: 0.536,
},
// ── serif ──
serif: {
@@ -112,6 +120,7 @@ export const BOOK_FONT_FACES = {
weights: MID,
italics: MID,
letterEm: 0.469,
boldEm: 0.481,
},
garamond: {
name: 'EB Garamond',
@@ -122,6 +131,7 @@ export const BOOK_FONT_FACES = {
weights: [400, 500, 600, 700, 800],
italics: [400, 500, 600, 700, 800],
letterEm: 0.383,
boldEm: 0.424,
},
// ── elegant display serifs ──
playfair: {
@@ -133,6 +143,7 @@ export const BOOK_FONT_FACES = {
weights: [400, 500, 600, 700, 800, 900],
italics: [400, 500, 600, 700, 800, 900],
letterEm: 0.454,
boldEm: 0.462,
},
fraunces: {
name: 'Fraunces',
@@ -143,6 +154,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: ALL,
letterEm: 0.467,
boldEm: 0.497,
},
cormorant: {
name: 'Cormorant Garamond',
@@ -153,6 +165,7 @@ export const BOOK_FONT_FACES = {
weights: [300, 400, 500, 600, 700],
italics: [300, 400, 500, 600, 700],
letterEm: 0.393,
boldEm: 0.398,
},
dmserif: {
name: 'DM Serif Display',
@@ -173,6 +186,7 @@ export const BOOK_FONT_FACES = {
weights: [400, 500, 600, 700, 800, 900],
italics: [400, 500, 600, 700, 800, 900],
letterEm: 0.463,
boldEm: 0.488,
},
// ── condensed and poster ──
bebas: {
@@ -205,6 +219,7 @@ export const BOOK_FONT_FACES = {
weights: [200, 300, 400, 500, 600, 700],
italics: [],
letterEm: 0.365,
boldEm: 0.412,
},
league: {
name: 'League Gothic',
@@ -225,6 +240,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.304,
boldEm: 0.355,
},
archivo: {
name: 'Archivo Black',
@@ -246,6 +262,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.492,
boldEm: 0.5,
},
shrikhand: {
name: 'Shrikhand',
@@ -276,6 +293,7 @@ export const BOOK_FONT_FACES = {
weights: [200, 300, 400, 500, 600, 700, 800],
italics: [],
letterEm: 0.471,
boldEm: 0.49,
},
abril: {
name: 'Abril Fatface',
@@ -297,6 +315,7 @@ export const BOOK_FONT_FACES = {
weights: MID,
italics: [],
letterEm: 0.344,
boldEm: 0.349,
},
dancing: {
name: 'Dancing Script',
@@ -307,6 +326,7 @@ export const BOOK_FONT_FACES = {
weights: MID,
italics: [],
letterEm: 0.363,
boldEm: 0.372,
},
pacifico: {
name: 'Pacifico',
+6
View File
@@ -71,6 +71,12 @@ describe('the catalogue', () => {
for (const w of face.italics) expect(face.weights, `${id} italic ${w}`).toContain(w);
expect(face.letterEm, id).toBeGreaterThan(0.2);
expect(face.letterEm, id).toBeLessThan(0.7);
// A measured bold only where the family ships one, and never narrower than its regular.
const bold = (face as { boldEm?: number }).boldEm;
if (bold !== undefined) {
expect(face.weights as readonly number[], id).toContain(700);
expect(bold, id).toBeGreaterThanOrEqual(face.letterEm);
}
expect(face.roles.length, id).toBeGreaterThan(0);
}
});
+4 -13
View File
@@ -7,7 +7,6 @@ import {
bookAddOpInSchema,
} from './book-element-ops.schema';
import { BOOK_GROUP_OP_SCHEMAS } from './book-group-ops.schema';
import { BOOK_COVER_TEMPLATES, BOOK_TEMPLATES } from './book-layouts';
import {
BOOK_BINDING_HINT,
bookFontIdSchema,
@@ -76,16 +75,6 @@ export const BOOK_OP_LIMITS = {
findings: 40,
} as const;
const idsOf = (list: readonly { id: string }[]) => list.map((t) => t.id) as [string, ...string[]];
/** The spread layouts, by id. */
export const bookTemplateIdSchema = z
.enum(idsOf(BOOK_TEMPLATES))
.describe('A built-in double page layout by id; list_book_layouts says what each one holds.');
/** The single-page (cover and back) layouts, by id. */
export const bookCoverTemplateIdSchema = z
.enum(idsOf(BOOK_COVER_TEMPLATES))
.describe('A built-in cover or back layout by id; list_book_layouts says what each one holds.');
/**
* A layout of either set, built-in or decorated: which one fits is the page's
* question, answered when the operation runs. A string checked against the
@@ -278,7 +267,8 @@ export const addBookPagesArgs = {
/** The new pages go after this one, kept inside the covers. */
afterSpreadId: bookSpreadIdSchema,
count: z.number().int().min(1).max(BOOK_OP_LIMITS.pagesPerCall).optional(),
template: bookTemplateIdSchema.optional(),
/** A double-spread layout, built-in or decorated; a single-page one is refused when the page is laid. */
template: bookAnyTemplateIdSchema.optional(),
entryId: idSchema.optional(),
};
export const moveBookPageArgs = {
@@ -296,7 +286,8 @@ export const deleteBookPagesArgs = {
expectedVersion,
};
export const setBookCoverArgs = {
template: bookCoverTemplateIdSchema.optional(),
/** A single-page layout, built-in or decorated; a double-spread one is refused when the cover is laid. */
template: bookAnyTemplateIdSchema.optional(),
photoId: idSchema.optional(),
title: z.string().max(MAX_BOOK_TITLE).optional(),
subtitle: z.string().max(MAX_BOOK_TITLE).optional(),
+20
View File
@@ -108,6 +108,19 @@ describe('add, move and delete', () => {
expect(doc.spreads[3]?.elements.length).toBeGreaterThan(0);
});
it('BOOKPG-029: lays added pages out on a decorated template, and refuses a cover template for a double spread', () => {
const doc = ok(
addBookPages(book(1), { afterSpreadId: 'sp-in1', count: 2, template: 'hero-script-word' }, { newId }),
);
expect(doc.spreads[3]?.elements.length).toBeGreaterThan(0);
expect(doc.spreads[4]?.elements.length).toBe(doc.spreads[3]?.elements.length);
const wrong = addBookPages(book(1), { afterSpreadId: 'sp-in1', template: 'poster-pop-sunburst' }, { newId });
expect(refused(wrong)).toBe('layout_role');
expect(refused(addBookPages(book(1), { afterSpreadId: 'sp-in1', template: 'no-such' }, { newId }))).toBe(
'invalid_op',
);
});
it('BOOKPG-020: ties an added page only to an entry the journey has, when the journey is known', () => {
const journey = { title: 'Iceland', entries: [{ id: 7 }] };
const added = ok(addBookPages(book(1), { afterSpreadId: 'sp-in1', entryId: 7 }, { newId, journey }));
@@ -336,6 +349,13 @@ describe('the cover', () => {
expect(title).toBeDefined();
});
it('BOOKPG-030: lays the cover out on a decorated cover template, and refuses a double-spread one', () => {
const doc = ok(setBookCover(book(1), { template: 'poster-pop-sunburst', title: 'Iceland 2026' }, { newId }));
expect(coverOf(doc).elements.length).toBeGreaterThan(1);
expect(textsOf(doc).some((t) => t.text === 'Iceland 2026')).toBe(true);
expect(refused(setBookCover(book(1), { template: 'hero-script-word' }, { newId }))).toBe('layout_role');
});
it('BOOKPG-016: keeps a bound title bound when the words are what the journey says, and the subtitle apart', () => {
const base = book(1);
const bound = bookTextElementSchema.parse({
+15 -9
View File
@@ -1,5 +1,5 @@
import { bookElementId } from './book-ids';
import { applyBookTemplate, classifyBookTexts, findBookTemplate } from './book-layouts';
import { classifyBookTexts } from './book-layouts';
import { applyBookOps } from './book-ops';
import {
caught,
@@ -62,17 +62,23 @@ export function addBookPages(doc: BookDocument, args: unknown, ctx: BookOpsConte
}
const { first, limit } = innerSpreadBounds(doc.spreads);
const at = Math.min(Math.max(after + 1, first), limit);
const tpl = template === undefined ? undefined : findBookTemplate(template);
const added = Array.from({ length: count }, () => {
const empty = { ...blankSpread('inner', ctx.newId ?? bookElementId), entryId: entryId ?? null };
return checkedSpread(tpl ? applyBookTemplate(empty, tpl, doc.page) : empty, null);
});
const spreads = [...doc.spreads.slice(0, at), ...added, ...doc.spreads.slice(at)];
const added = Array.from({ length: count }, () =>
checkedSpread({ ...blankSpread('inner', ctx.newId ?? bookElementId), entryId: entryId ?? null }, null),
);
let next: BookDocument = { ...doc, spreads: [...doc.spreads.slice(0, at), ...added, ...doc.spreads.slice(at)] };
// Laid out as edit_book_page lays a page, so a decorated template takes the book's theme and words too.
if (template !== undefined) {
for (const spread of added) {
const r = applyBookOps(next, { spreadId: spread.id, ops: [{ op: 'layout', template }] }, ctx);
if (r.ok === false) throw failOp(r.error.code, r.error.message, null, r.error.elementId);
next = r.doc;
}
}
return {
ok: true,
doc: { ...doc, spreads },
doc: next,
spreadIds: added.map((s) => s.id),
created: createdIds(doc.spreads, spreads),
created: createdIds(doc.spreads, next.spreads),
dropped: [],
};
});
+26 -17
View File
@@ -42,6 +42,8 @@ export const BOOK_PATTERNS = [
'scales',
'sunburst',
'confetti',
'linen',
'grain',
] as const;
export type BookPattern = (typeof BOOK_PATTERNS)[number];
@@ -68,32 +70,39 @@ type ObjectFactory = typeof z.object | typeof z.strictObject;
function paintSchemas(obj: ObjectFactory, pattern: z.ZodType<BookPattern, string>) {
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 linear = obj({ type: z.literal('linear'), angle, stops });
const radial = obj({
type: z.literal('radial'),
cx: z.number().min(0).max(1),
cy: z.number().min(0).max(1),
r: z.number().min(RADIAL_RADIUS.min).max(RADIAL_RADIUS.max),
stops,
});
const patterned = obj({
type: z.literal('pattern'),
pattern,
fg: hex,
bg: hex.nullable(),
scale: z.number().min(PATTERN_SCALE.min).max(PATTERN_SCALE.max),
angle,
});
const paint = z.discriminatedUnion('type', [
obj({ type: z.literal('solid'), color: hex, alpha: alpha.optional() }),
obj({ type: z.literal('linear'), angle, stops }),
obj({
type: z.literal('radial'),
cx: z.number().min(0).max(1),
cy: z.number().min(0).max(1),
r: z.number().min(RADIAL_RADIUS.min).max(RADIAL_RADIUS.max),
stops,
}),
obj({
type: z.literal('pattern'),
pattern,
fg: hex,
bg: hex.nullable(),
scale: z.number().min(PATTERN_SCALE.min).max(PATTERN_SCALE.max),
angle,
}),
linear,
radial,
patterned,
]);
return { stop, paint };
return { stop, paint, linear, radial, patterned };
}
const docSchemas = paintSchemas(z.object, z.string().pipe(z.enum(BOOK_PATTERNS)));
export const bookPaintStopSchema = docSchemas.stop;
export const bookPaintSchema = docSchemas.paint;
/** The paints that are more than one colour, as the letters of a text take them too (book-text-effects.schema.ts). */
export const bookLinearPaintSchema = docSchemas.linear;
export const bookRadialPaintSchema = docSchemas.radial;
export const bookPatternPaintSchema = docSchemas.patterned;
export type BookPaint = z.infer<typeof bookPaintSchema>;
export type BookPaintStop = z.infer<typeof bookPaintStopSchema>;
export type BookGradientPaint = Extract<BookPaint, { type: 'linear' | 'radial' }>;
+1 -1
View File
@@ -71,7 +71,7 @@ describe('the paint contract', () => {
for (const p of [{ type: 'solid', color: '#abcdef', alpha: 0.4 }, linear, radial, stripes]) {
expect(bookPaintSchema.parse(p)).toEqual(p);
}
expect(BOOK_PATTERNS).toHaveLength(12);
expect(BOOK_PATTERNS).toHaveLength(14);
for (const pattern of BOOK_PATTERNS) expect(bookPaintSchema.safeParse({ ...stripes, pattern }).success).toBe(true);
});
+3 -1
View File
@@ -43,8 +43,10 @@ import type { BookDocument } from './book.schema';
* - 9: text effects and fitting (`transform`, `vAlign`, `outline`, `shadow`, `highlight`,
* `curve`, `fillPaint`, `fit`, `fitMax`, book-text-effects.schema.ts), text up to 400 pt and
* tracking up to 1.5 em.
* - 10: letters filled with a gradient or a pattern (`fillPaint` of type `linear`, `radial`,
* `pattern`, book-text-effects.schema.ts).
*/
export const BOOK_SCHEMA_REV = 9;
export const BOOK_SCHEMA_REV = 10;
/** The `code` a refused save carries in its body when the stored book is newer than the writer. */
export const BOOK_SCHEMA_NEWER_CODE = 'book_schema_newer';
@@ -0,0 +1,17 @@
import en from '../i18n/en';
import { BOOK_FRAME_SHAPES, BOOK_SHAPE_GROUPS } from './book-shape-catalogue';
import { describe, expect, it } from 'vitest';
/**
* Every shape the Studio offers has a name in en, which the client's
* `shapeName` reads for the tooltip and the screen reader of the shape's
* button (client/scripts/i18n-keys.mjs allows the key built from the id).
*/
describe('the shape catalogue', () => {
it('BKSHAPE-001: names every shape of the shelves and every frame shape in en', () => {
const ids = new Set([...BOOK_SHAPE_GROUPS.flatMap((g) => g.shapes), ...BOOK_FRAME_SHAPES]);
const strings = en as Record<string, string>;
for (const id of ids) expect(strings[`journey.studio.shapeName.${id}`], id).toBeTruthy();
});
});
+9
View File
@@ -10,10 +10,19 @@ const n = (v: number) => `${Math.round(v * 10) / 10}`;
const CASES: Record<string, string> = { upper: 'capitals', lower: 'lower case', smallcaps: 'small caps' };
function stopWords(stops: readonly { color: string }[]): string {
return `from ${stops[0]?.color ?? ''} to ${stops[stops.length - 1]?.color ?? ''}`;
}
function fillWords(fill: NonNullable<BookTextEffects['fillPaint']>): string {
if (fill.type === 'photo') {
return `letters filled with photo ${fill.photoId} (focal ${fill.focalX.toFixed(2)}/${fill.focalY.toFixed(2)}, zoom ${n(fill.zoom)})`;
}
if (fill.type === 'linear') return `letters in a linear gradient at ${n(fill.angle)} deg, ${stopWords(fill.stops)}`;
if (fill.type === 'radial') return `letters in a radial gradient, ${stopWords(fill.stops)}`;
if (fill.type === 'pattern') {
return `letters patterned ${fill.pattern} in ${fill.fg} on ${fill.bg ?? 'clear'} at scale ${n(fill.scale)}`;
}
return fill.alpha === 0 ? 'hollow letters (outline only)' : `fill ${fill.color} at alpha ${n(fill.alpha)}`;
}
@@ -1,4 +1,6 @@
import { bookTextIsDrawn } from './book-text-effects.schema';
import { bookTextEffectWords } from './book-text-describe';
import { bookTextIsDrawn, bookTextPaintFill } from './book-text-effects.schema';
import { bookTextHasOwnGround } from './book-text-findings';
import { bookTextElementSchema } from './book.schema';
import { describe, expect, it } from 'vitest';
@@ -75,3 +77,36 @@ describe('the effect fields of a text', () => {
expect(bookTextIsDrawn(parse({ fillPaint: { type: 'photo', photoId: 3 } }))).toBe(true);
});
});
describe('letters filled with a gradient or a pattern', () => {
const gradient = {
type: 'linear',
angle: 90,
stops: [
{ at: 0, color: '#ff6600' },
{ at: 1, color: '#ffcc00' },
],
};
const gingham = { type: 'pattern', pattern: 'gingham', fg: '#d62828', bg: '#ffffff', scale: 1, angle: 0 };
it('BKFX-005: reads a gradient and a pattern in the shape a shape takes them, and draws them as SVG', () => {
const lin = parse({ fillPaint: gradient });
const pat = parse({ fillPaint: gingham });
expect(bookTextPaintFill(lin)).toEqual(gradient);
expect(bookTextPaintFill(pat)).toEqual(gingham);
expect(bookTextIsDrawn(lin)).toBe(true);
expect(bookTextIsDrawn(pat)).toBe(true);
expect(bookTextPaintFill(parse({ fillPaint: { type: 'photo', photoId: 3 } }))).toBeNull();
expect(() => parse({ fillPaint: { ...gingham, pattern: 'plaid' } })).toThrow();
});
it('BKFX-006: names the paint for a model that cannot see the page, and judges no contrast against it', () => {
expect(bookTextEffectWords({ ...parse({ fillPaint: gradient }), size: 40 })).toContain(
'linear gradient at 90 deg, from #ff6600 to #ffcc00',
);
expect(bookTextEffectWords({ ...parse({ fillPaint: gingham }), size: 40 })).toContain(
'patterned gingham in #d62828 on #ffffff',
);
expect(bookTextHasOwnGround(parse({ fillPaint: gingham }))).toBe(true);
});
});
+29 -9
View File
@@ -1,3 +1,10 @@
import {
bookLinearPaintSchema,
bookPatternPaintSchema,
bookRadialPaintSchema,
type BookPaint,
} from './book-paint.schema';
import { z } from 'zod';
/**
@@ -27,10 +34,11 @@ import { z } from 'zod';
*
* ── The fill and the paint system ──────────────────────────────────────────
*
* `fillPaint` takes a solid colour with its own alpha or a photograph today.
* Its solid variant has the shape the book's paint type gives a solid fill, so
* gradients and patterns join this union as further members, without a
* reader of today's fields changing.
* `fillPaint` takes a solid colour with its own alpha, a photograph, or one
* of the book's paints of more than one colour: a linear or radial gradient or
* a pattern, in exactly the shape a shape or a page takes them, so one picker
* and one renderer serve all three. A gradient or pattern fill is drawn as SVG
* like a photo fill, the paint clipped to the letters.
*/
const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, 'expected #rrggbb');
@@ -84,7 +92,10 @@ export const bookTextCurveSchema = z.object({
});
export type BookTextCurve = z.infer<typeof bookTextCurveSchema>;
/** What fills the letters in place of `color`: a colour with its own alpha (0 leaves an outline only), or a photograph. */
/**
* What fills the letters in place of `color`: a colour with its own alpha (0
* leaves an outline only), a photograph, or a gradient or pattern.
*/
export const bookTextFillSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('solid'), color: hex, alpha: alpha.default(1) }),
z.object({
@@ -95,9 +106,18 @@ export const bookTextFillSchema = z.discriminatedUnion('type', [
focalY: z.number().min(0).max(1).default(0.5),
zoom: z.number().min(1).max(4).default(1),
}),
bookLinearPaintSchema,
bookRadialPaintSchema,
bookPatternPaintSchema,
]);
export type BookTextFill = z.infer<typeof bookTextFillSchema>;
/** The letters' gradient or pattern, as the paint a shape would take, or null for any other fill. */
export function bookTextPaintFill(el: { fillPaint?: BookTextFill | null }): BookPaint | null {
const fill = el.fillPaint;
return fill && (fill.type === 'linear' || fill.type === 'radial' || fill.type === 'pattern') ? fill : null;
}
/** `shrink` sets the text at the largest size up to `fitMax` that holds the frame, solved on edit and stored in `size`. */
export const BOOK_TEXT_FITS = ['none', 'shrink'] as const;
@@ -133,10 +153,10 @@ export interface BookTextEffects {
/**
* Whether a text is drawn as SVG rather than as a box of HTML type: on a
* curve, or filled with a photograph. Such a text is set line by line as
* written, never wrapped, which is what word art wants and what the checks
* measure it by.
* curve, or filled with a photograph, a gradient or a pattern. Such a text is
* set line by line as written, never wrapped, which is what word art wants and
* what the checks measure it by.
*/
export function bookTextIsDrawn(el: BookTextEffects): boolean {
return !!el.curve || el.fillPaint?.type === 'photo';
return !!el.curve || el.fillPaint?.type === 'photo' || !!bookTextPaintFill(el);
}
+2 -1
View File
@@ -30,7 +30,8 @@ const MIN_FILL_PX_PER_MM = 200 / 25.4;
*/
export function bookTextHasOwnGround(el: BookTextElement): boolean {
if (el.highlight) return true;
if (el.fillPaint && (el.fillPaint.type === 'photo' || el.fillPaint.alpha < 0.5)) return true;
// A photo, a gradient or a pattern in the letters is no one colour the page could be judged against.
if (el.fillPaint && (el.fillPaint.type !== 'solid' || el.fillPaint.alpha < 0.5)) return true;
return !!el.outline && el.outline.width >= ptToMm(el.size) * 0.04;
}
+9 -1
View File
@@ -1,4 +1,4 @@
import { textHeightNeeded } from './book-findings';
import { bookLetterMm, textHeightNeeded } from './book-findings';
import { blankBook } from './book-pages';
import {
bookCasedText,
@@ -138,4 +138,12 @@ describe('fitting a page', () => {
expect((next.spreads[0]?.elements[0] as BookTextElement).size).toBeLessThan(60);
expect(next.spreads[1]).toBe(doc.spreads[1]);
});
it('BKFIT-011: a bold condensed face is measured at its own width, not the regular widened by a guess', () => {
const oswald = (weight: number) => bookLetterMm(text({ font: 'oswald', weight, size: 20 }));
// Oswald's 700 sets about 13% wider than its 400; a flat 6% would let a bold heading overrun its frame.
expect(oswald(700) / oswald(400)).toBeGreaterThan(1.1);
const vibes = (weight: number) => bookLetterMm(text({ font: 'vibes', weight, size: 20 }));
expect(vibes(700) / vibes(400)).toBeCloseTo(1.06, 6);
});
});
+3 -1
View File
@@ -23,7 +23,9 @@ export const TEXT_EFFECT_PROPS = {
shadow: f.shadow.describe('Hard, never blurred; dx, dy in mm.'),
highlight: f.highlight.describe('A marker band behind each line; pads in mm.'),
curve: f.curve.describe('One line on an arc (bend > 0 arches up) or round a circle (start: degrees, 0 top).'),
fillPaint: f.fillPaint.describe('Replaces color: solid (alpha 0 is hollow, with an outline) or a gallery photo.'),
fillPaint: f.fillPaint.describe(
'Replaces color: solid (alpha 0 is hollow, with an outline), a gallery photo, or a linear/radial gradient or pattern shaped as on a shape.',
),
fit: f.fit.describe('shrink: the largest size up to size that holds the frame.'),
};
@@ -138,4 +138,6 @@ export const BOOK_PATTERN_LINES: Readonly<Record<BookPattern, string>> = {
scales: 'Overlapping half circles, fish scales or roof tiles.',
sunburst: 'Rays from a centre, a sunburst.',
confetti: 'Scattered confetti.',
linen: 'Fine woven threads, the texture of a cloth-bound book, best in a colour close to its ground.',
grain: 'Fine scattered specks, the grain of paper or kraft card, best in a colour close to its ground.',
};
@@ -356,6 +356,17 @@ describe('design_book', () => {
expect(JSON.stringify(again.doc)).toBe(JSON.stringify(banded.doc));
});
it('BOOKDSN-032: lays the cover out on a decorated cover template, and leaves a double-spread one out with a note', () => {
const poster = designBook(null, input('rich'), { style: 'minimal', cover: { template: 'poster-pop-sunburst' } });
const cover = poster.doc.spreads[0] as BookSpread;
expect(cover.elements.some((e) => e.kind === 'photo')).toBe(true);
expect(cover.elements.some((e) => e.kind === 'text' && e.binding?.source === 'journey.title')).toBe(true);
const again = designBook(null, input('rich'), { style: 'minimal', cover: { template: 'poster-pop-sunburst' } });
expect(JSON.stringify(again.doc)).toBe(JSON.stringify(poster.doc));
const spread = designBook(null, input('rich'), { style: 'minimal', cover: { template: 'hero-script-word' } });
expect(spread.notes.some((n) => n.includes('hero-script-word'))).toBe(true);
});
it('BOOKDSN-014: stays within the spread cap beside many kept pages, the closing pages kept', () => {
const stored = storedBook();
const lockedPage = stored.spreads[2] as BookSpread;
@@ -1,6 +1,6 @@
import { idSchema } from '../../common/primitives.schema';
import { bookFontIdSchema, bookMetricSchema, bookPagePresetSchema } from '../book-op-enums';
import { bookCoverTemplateIdSchema } from '../book-ops.schema';
import { bookAnyTemplateIdSchema } from '../book-ops.schema';
import { MAX_BOOK_TITLE } from '../book.schema';
import { bookThemeCatalogue } from '../theme/book-theme-describe';
import { BOOK_THEME_IDS } from '../theme/book-theme.schema';
@@ -84,7 +84,7 @@ export const bookDesignDirectivesSchema = z.strictObject({
.optional(),
cover: z
.strictObject({
template: bookCoverTemplateIdSchema.optional(),
template: bookAnyTemplateIdSchema.optional(),
archetype: bookCoverArchetypeSchema.optional(),
lockup: bookCoverLockupSchema.optional(),
title: z.string().max(MAX_BOOK_TITLE).optional(),
+18 -2
View File
@@ -6,6 +6,8 @@ import { formatBookDate } from '../book-text';
import { bookTravelCountries, bookTravelMap, bookTravelStats } from '../book-travel';
import type { BookElement, BookMetric, BookSpread, BookTextElement } from '../book.schema';
import type { JourneyStats } from '../journey-stats.schema';
import { applyDecoratedTemplate } from '../templates/template-apply';
import { findDecoratedTemplate } from '../templates/template-catalogue';
import {
bleedPage,
frameOf,
@@ -90,8 +92,22 @@ export function composeCover(
*/
function withCoverTemplate(c: ComposeCtx, cover: BookSpread): BookSpread {
const wanted = c.directives.cover?.template;
const tpl = wanted ? BOOK_COVER_TEMPLATES.find((t) => t.id === wanted) : undefined;
if (!tpl) return cover;
if (!wanted) return cover;
const decorated = findDecoratedTemplate(wanted);
if (decorated) {
if (decorated.kind !== 'single') {
c.notes.push(`${wanted} is a double-spread template, so cover.template is left out`);
return cover;
}
// A designed page keeps its own faces and colours, in the book's theme, and takes the cover's photo and words.
const opts = { theme: c.look.theme, travel: c.travel, locale: c.locale, newId: c.newId };
return { ...applyDecoratedTemplate(cover, decorated, c.page, opts), parked: [] };
}
const tpl = BOOK_COVER_TEMPLATES.find((t) => t.id === wanted);
if (!tpl) {
c.notes.push(`${wanted} is a double-spread layout, so cover.template is left out`);
return cover;
}
const laid = applyBookTemplate(cover, tpl, c.page);
const elements = laid.elements.map((e): BookElement => {
if (e.kind === 'text') return { ...e, id: c.newId('t'), color: c.look.ink };
+246
View File
@@ -614,4 +614,250 @@ export const ASIA_STICKERS: readonly BookStickerDef[] = [
],
license: 'in-house',
},
{
id: 'tiger',
theme: 'asia',
tags: ['tiger', 'bengal tiger', 'india', 'jungle', 'safari', 'wildlife', 'animal', 'big cat'],
descr:
'A friendly Bengal tiger walking left with its face turned to the viewer, orange fur with black stripes, white cheeks and belly and a curled striped tail.',
box: [110, 80],
slots: [
{ name: 'fur', color: '#f08a24' },
{ name: 'belly', color: '#fbf6ee' },
{ name: 'stripes', color: '#262229' },
{ name: 'nose', color: '#e8828a' },
],
layers: [
{
p: 'shade',
d: 'M18 75.5C18 76.94 36.8 78.1 60 78.1C83.2 78.1 102 76.94 102 75.5C102 74.06 83.2 72.9 60 72.9C36.8 72.9 18 74.06 18 75.5Z',
o: 0.12,
},
{
p: 0,
d: 'M46 46L48 70C48 73 49.5 74.5 52.5 74.5L57.5 74.5C59.5 74.5 59.5 71.5 57.5 70.5L56 46ZM80 44L86 70C86.6 73 88 74.5 91 74.5L95.5 74.5C97.5 74.5 97.5 71.5 95.5 70.5L91 44Z',
},
{
p: 'shade',
d: 'M46 46L48 70C48 73 49.5 74.5 52.5 74.5L57.5 74.5C59.5 74.5 59.5 71.5 57.5 70.5L56 46ZM80 44L86 70C86.6 73 88 74.5 91 74.5L95.5 74.5C97.5 74.5 97.5 71.5 95.5 70.5L91 44Z',
o: 0.28,
},
{ p: 0, d: 'M88 37C99 34 101 22 99 14C97.8 9 101 5.5 105 7', w: 4.4 },
{
p: 2,
d: 'M96.9 30.4L97.3 29.7L97.6 29.1L98 28.4M99.3 24.1L99.4 23.4L99.5 22.7L99.6 21.9M99.6 17.6L99.5 16.9L99.4 16.2L99.3 15.5M100.1 8.1L100.8 7.4L101.7 6.9L102.7 6.7L103.8 6.7L105 7',
w: 4.4,
},
{
p: 0,
d: 'M32 43C32 51.01 45.43 57.5 62 57.5C78.57 57.5 92 51.01 92 43C92 34.99 78.57 28.5 62 28.5C45.43 28.5 32 34.99 32 43Z',
},
{ p: 0, d: 'M34 36C38 31 45 30 50 31.5L50 52L35 52Z' },
{
p: 0,
d: 'M72 36C84 31 95 38 93 50C92.5 53 91 56 89 58L91.5 70C92 73 90.5 74.5 88 74.5L82 74.5C79.5 74.5 79.5 71.5 82 70.5L80.5 58C74 56 69 46 72 36Z',
},
{ p: 0, d: 'M33 42L30.5 70C30.3 73 31.8 74.5 34.8 74.5L41.3 74.5C43.3 74.5 43.3 71.5 41.3 70.5L44.5 44Z' },
{
p: 1,
d: 'M40 52.3L43 53.6L46 54.7L49 55.5L52 56.1L55 56.5L58 56.8L61 56.9L64 56.9L67 56.7L70 56.4L73 55.9L76 55.2L79 54.3L82 53.2L82 53.2L79 53.4L76 53.4L73 53.3L70 53.1L67 52.9L64 52.8L61 52.7L58 52.7L55 52.7L52 52.8L49 52.8L46 52.8L43 52.7L40 52.3Z',
},
{ p: 'light', d: 'M48 33.2C56 31 66 30.8 74 32', w: 1.4, o: 0.3 },
{
p: 2,
d: 'M43.6 32.3L46.4 31.4C45.4 34.53 44.27 37.77 43 41.1C43.2 37.77 43.4 34.83 43.6 32.3ZM49.1 30.7L52.9 30C51.7 34.67 50.53 39.5 49.4 44.5C49.47 39.5 49.37 34.9 49.1 30.7ZM55.2 29.7L57.8 29.4C57.2 31.93 56.43 34.7 55.5 37.7C55.37 34.7 55.27 32.03 55.2 29.7ZM60 29.3L64 29.3C63.13 34.1 62.33 39.17 61.6 44.5C61.27 39.17 60.73 34.1 60 29.3ZM66.1 29.4L68.9 29.7C68.7 32.37 68.37 35.37 67.9 38.7C67.3 35.37 66.7 32.27 66.1 29.4ZM71.1 30L74.9 30.7C74.57 34.57 74.27 38.83 74 43.5C73.2 38.83 72.23 34.33 71.1 30ZM77.2 31.3L79.8 32.1C80 34.3 80.03 36.9 79.9 39.9C78.97 36.9 78.07 34.03 77.2 31.3ZM82.4 33.2L85.6 34.8C85.8 37 85.93 39.77 86 43.1C84.87 39.77 83.67 36.47 82.4 33.2ZM87.9 36.5L90.1 38.7C90.5 39.37 90.67 40.7 90.6 42.7C89.6 40.7 88.7 38.63 87.9 36.5ZM47 54.6C48.6 51.6 50.4 49.6 53 48.6C51.6 51 51 53 51 55.6ZM58 56.6C59.4 53 61.4 50.6 64.4 49.4C62.8 52.2 62.2 54.6 62.2 57.4ZM69 56.4C70.2 53.2 72 51.2 74.6 50C73.2 52.4 72.8 54.4 72.8 56.8ZM86 40.5C82 43 80 47 80.5 51.5C82 47.5 84.5 44 88.2 42ZM90.4 46C86.8 48.4 85 51.6 85 55C86.8 52 88.6 50 91.6 49ZM32.4 56L37.4 56.6L32.3 58.4ZM31.8 62L37 62.6L31.7 64.4ZM43.6 52L39.6 53L43.5 54.6ZM80.9 62L85.5 62.4L81.1 64.2ZM81.3 66.4L85.4 66.8L81.4 68.4ZM90.3 61L86.6 62L90.6 63.4Z',
},
{ p: 'shade', d: 'M33.6 50C44 57.6 70 59 86.6 52.4C80 56.2 74 57.4 62 57.5C50 57.5 40 55 33.6 50Z', o: 0.12 },
{ p: 'shade', d: 'M35.3 72.6L35.3 74M38.3 72.6L38.3 74M84 72.6L84 74M87 72.6L87 74', w: 0.7, o: 0.35 },
{
p: 0,
d: 'M7.4 23.5C7.4 26.59 9.91 29.1 13 29.1C16.09 29.1 18.6 26.59 18.6 23.5C18.6 20.41 16.09 17.9 13 17.9C9.91 17.9 7.4 20.41 7.4 23.5ZM25.4 23.5C25.4 26.59 27.91 29.1 31 29.1C34.09 29.1 36.6 26.59 36.6 23.5C36.6 20.41 34.09 17.9 31 17.9C27.91 17.9 25.4 20.41 25.4 23.5Z',
},
{
p: 2,
d: 'M9.6 23.5C9.6 24.71 10.25 25.84 11.3 26.44C12.35 27.05 13.65 27.05 14.7 26.44C15.75 25.84 16.4 24.71 16.4 23.5C16.4 22.29 15.75 21.16 14.7 20.56C13.65 19.95 12.35 19.95 11.3 20.56C10.25 21.16 9.6 22.29 9.6 23.5ZM27.6 23.5C27.6 24.71 28.25 25.84 29.3 26.44C30.35 27.05 31.65 27.05 32.7 26.44C33.75 25.84 34.4 24.71 34.4 23.5C34.4 22.29 33.75 21.16 32.7 20.56C31.65 19.95 30.35 19.95 29.3 20.56C28.25 21.16 27.6 22.29 27.6 23.5Z',
},
{
p: 1,
d: 'M11.4 24C11.4 25.1 12.3 26 13.4 26C14.5 26 15.4 25.1 15.4 24C15.4 22.9 14.5 22 13.4 22C12.3 22 11.4 22.9 11.4 24ZM28.6 24C28.6 25.1 29.5 26 30.6 26C31.7 26 32.6 25.1 32.6 24C32.6 22.9 31.7 22 30.6 22C29.5 22 28.6 22.9 28.6 24Z',
},
{
p: 0,
d: 'M22 20C30.5 20 36 25.5 36 33C36 35 37.5 37 39 38.4C36.6 38.6 35.6 39.4 35.4 40.6C37 41.8 37.2 43.4 36.4 44.6C34 49 28.6 51.4 22 51.4C15.4 51.4 10 49 7.6 44.6C6.8 43.4 7 41.8 8.6 40.6C8.4 39.4 7.4 38.6 5 38.4C6.5 37 8 35 8 33C8 25.5 13.5 20 22 20Z',
},
{
p: 1,
d: 'M22 38C25.4 34.8 31.6 36.2 32 41C32.4 46.6 27.8 50.6 22 50.6C16.2 50.6 11.6 46.6 12 41C12.4 36.2 18.6 34.8 22 38Z',
},
{
p: 1,
d: 'M12 30.2C12 30.99 12.69 31.71 13.8 32.11C14.91 32.5 16.29 32.5 17.4 32.11C18.51 31.71 19.2 30.99 19.2 30.2C19.2 29.41 18.51 28.69 17.4 28.29C16.29 27.9 14.91 27.9 13.8 28.29C12.69 28.69 12 29.41 12 30.2ZM24.8 30.2C24.8 30.99 25.49 31.71 26.6 32.11C27.71 32.5 29.09 32.5 30.2 32.11C31.31 31.71 32 30.99 32 30.2C32 29.41 31.31 28.69 30.2 28.29C29.09 27.9 27.71 27.9 26.6 28.29C25.49 28.69 24.8 29.41 24.8 30.2Z',
},
{
p: 2,
d: 'M19.2 21.5L22 28L24.8 21.5C22.93 21.17 21.07 21.17 19.2 21.5ZM15.4 22.6L18.4 26.4L18.6 21.6ZM28.6 22.6L25.6 26.4L25.4 21.6ZM8.2 31.2L13.6 33.6L8.3 34.4ZM35.8 31.2L30.4 33.6L35.7 34.4ZM8.6 37.4L12.6 38.6L9 39.8ZM35.4 37.4L31.4 38.6L35 39.8Z',
},
{
p: 'ink',
d: 'M14.5 33C14.5 34.05 15.35 34.9 16.4 34.9C17.45 34.9 18.3 34.05 18.3 33C18.3 31.95 17.45 31.1 16.4 31.1C15.35 31.1 14.5 31.95 14.5 33ZM25.7 33C25.7 34.05 26.55 34.9 27.6 34.9C28.65 34.9 29.5 34.05 29.5 33C29.5 31.95 28.65 31.1 27.6 31.1C26.55 31.1 25.7 31.95 25.7 33Z',
},
{
p: 'paper',
d: 'M16.3 32.3C16.3 32.55 16.43 32.78 16.65 32.91C16.87 33.03 17.13 33.03 17.35 32.91C17.57 32.78 17.7 32.55 17.7 32.3C17.7 32.05 17.57 31.82 17.35 31.69C17.13 31.57 16.87 31.57 16.65 31.69C16.43 31.82 16.3 32.05 16.3 32.3ZM27.5 32.3C27.5 32.55 27.63 32.78 27.85 32.91C28.07 33.03 28.33 33.03 28.55 32.91C28.77 32.78 28.9 32.55 28.9 32.3C28.9 32.05 28.77 31.82 28.55 31.69C28.33 31.57 28.07 31.57 27.85 31.69C27.63 31.82 27.5 32.05 27.5 32.3Z',
},
{ p: 3, d: 'M18.6 39.6C18.6 38.4 25.4 38.4 25.4 39.6C25.4 41 23.4 42.6 22 42.6C20.6 42.6 18.6 41 18.6 39.6Z' },
{ p: 'ink', d: 'M22 42.6L22 44.4M22 44.4C21 46 18.6 46 18 44.6M22 44.4C23 46 25.4 46 26 44.6', w: 0.8 },
{
p: 'shade',
d: 'M15.1 44.5C15.1 44.72 15.28 44.9 15.5 44.9C15.72 44.9 15.9 44.72 15.9 44.5C15.9 44.28 15.72 44.1 15.5 44.1C15.28 44.1 15.1 44.28 15.1 44.5ZM13.6 43C13.6 43.22 13.78 43.4 14 43.4C14.22 43.4 14.4 43.22 14.4 43C14.4 42.78 14.22 42.6 14 42.6C13.78 42.6 13.6 42.78 13.6 43ZM28.1 44.5C28.1 44.64 28.18 44.77 28.3 44.85C28.42 44.92 28.58 44.92 28.7 44.85C28.82 44.77 28.9 44.64 28.9 44.5C28.9 44.36 28.82 44.23 28.7 44.15C28.58 44.08 28.42 44.08 28.3 44.15C28.18 44.23 28.1 44.36 28.1 44.5ZM29.6 43C29.6 43.14 29.68 43.27 29.8 43.35C29.92 43.42 30.08 43.42 30.2 43.35C30.32 43.27 30.4 43.14 30.4 43C30.4 42.86 30.32 42.73 30.2 42.65C30.08 42.58 29.92 42.58 29.8 42.65C29.68 42.73 29.6 42.86 29.6 43Z',
o: 0.35,
},
],
license: 'in-house',
},
{
id: 'monkey',
theme: 'asia',
tags: ['monkey', 'macaque', 'india', 'temple', 'jungle', 'wildlife', 'animal', 'banana'],
descr:
'A cheerful brown macaque sitting with its knees up, holding a yellow banana in both hands, its long tail curling out in a spiral beside it.',
box: [100, 105],
slots: [
{ name: 'fur', color: '#9c6b45' },
{ name: 'belly', color: '#d9b58c' },
{ name: 'skin', color: '#f2c4a8' },
{ name: 'banana', color: '#f5cf45' },
],
layers: [
{
p: 'shade',
d: 'M18 98.6C18 99.46 24.86 100.25 36 100.68C47.14 101.11 60.86 101.11 72 100.68C83.14 100.25 90 99.46 90 98.6C90 97.74 83.14 96.95 72 96.52C60.86 96.09 47.14 96.09 36 96.52C24.86 96.95 18 97.74 18 98.6Z',
o: 0.12,
},
{ p: 0, d: 'M60 92C76 98 92 94 93 78C94 66 84 60 77 64C71 68 73 77 80 76', w: 4.4 },
{ p: 'shade', d: 'M60 92C76 98 92 94 93 78C94 66 84 60 77 64C71 68 73 77 80 76', w: 4.4, o: 0.12 },
{ p: 0, d: 'M32 94C25 84 26 64 36 55C42 50 54 50 60 55C70 64 71 84 64 94C56 97 40 97 32 94Z' },
{
p: 'shade',
d: 'M60 55C70 64 71 84 64 94C61 95 58 95.6 56 96C62 86 63 68 56 54C57.6 54 59 54.4 60 55Z',
o: 0.14,
},
{
p: 1,
d: 'M38 75C38 82.46 42.48 88.5 48 88.5C53.52 88.5 58 82.46 58 75C58 67.54 53.52 61.5 48 61.5C42.48 61.5 38 67.54 38 75Z',
},
{
p: 0,
d: 'M25.5 84C25.5 89.25 29.75 93.5 35 93.5C40.25 93.5 44.5 89.25 44.5 84C44.5 78.75 40.25 74.5 35 74.5C29.75 74.5 25.5 78.75 25.5 84Z',
},
{
p: 0,
d: 'M51.5 84C51.5 89.25 55.75 93.5 61 93.5C66.25 93.5 70.5 89.25 70.5 84C70.5 78.75 66.25 74.5 61 74.5C55.75 74.5 51.5 78.75 51.5 84Z',
},
{
p: 'shade',
d: 'M27 88C29 93 33 94 37 93.4C41 93 44 89.6 44.4 86C42 90 37 91.6 33 91C30 90.6 28 89.4 27 88Z',
o: 0.16,
},
{
p: 'shade',
d: 'M52 88C54 93 58 94 62 93.4C66 93 69 89.6 70.4 86C68 90 63 91.6 59 91C56 90.6 54 89.4 52 88Z',
o: 0.16,
},
{
p: 2,
d: 'M24 95C24 96.29 25.33 97.47 27.5 98.12C29.67 98.76 32.33 98.76 34.5 98.12C36.67 97.47 38 96.29 38 95C38 93.71 36.67 92.53 34.5 91.88C32.33 91.24 29.67 91.24 27.5 91.88C25.33 92.53 24 93.71 24 95ZM58 95C58 96.29 59.33 97.47 61.5 98.12C63.67 98.76 66.33 98.76 68.5 98.12C70.67 97.47 72 96.29 72 95C72 93.71 70.67 92.53 68.5 91.88C66.33 91.24 63.67 91.24 61.5 91.88C59.33 92.53 58 93.71 58 95Z',
},
{ p: 'shade', d: 'M26.5 94.6L26.5 97M29.5 95L29.5 97.6M62.5 95L62.5 97.6M65.5 94.6L65.5 97', w: 0.7, o: 0.3 },
{
p: 'shade',
d: 'M27.4 80C29 76.4 32 75 35 75C38.4 75 41.6 77 43.2 80.4M52.8 80.4C54.4 77 57.6 75 61 75C64 75 67 76.4 68.6 80',
w: 1,
o: 0.2,
},
{
p: 'shade',
d: 'M39.4 60.4C33.4 65.4 34.6 72.6 42.6 71.6M56.6 60.4C62.6 65.4 61.4 72.6 53.4 71.6',
w: 8.6,
o: 0.22,
},
{ p: 0, d: 'M39.4 60.4C33.4 65.4 34.6 72.6 42.6 71.6M56.6 60.4C62.6 65.4 61.4 72.6 53.4 71.6', w: 7 },
{ p: 3, d: 'M35.4 63.4C39.4 74.6 56.4 76.4 62.4 62.4C58.4 67.6 42.4 69 35.4 63.4Z' },
{ p: 'shade', d: 'M37.2 67.6C42 73.6 53.6 74.8 59.6 68.4C54.6 75.8 41.6 74.6 37.2 67.6Z', o: 0.2 },
{ p: '#6b4a2b', d: 'M62 62.9L63.6 60.4', w: 1.5 },
{
p: '#6b4a2b',
d: 'M35 63.9C35 64.22 35.17 64.52 35.45 64.68C35.73 64.84 36.07 64.84 36.35 64.68C36.63 64.52 36.8 64.22 36.8 63.9C36.8 63.58 36.63 63.28 36.35 63.12C36.07 62.96 35.73 62.96 35.45 63.12C35.17 63.28 35 63.58 35 63.9Z',
},
{
p: 2,
d: 'M39.4 71C39.4 72.99 41.01 74.6 43 74.6C44.99 74.6 46.6 72.99 46.6 71C46.6 69.01 44.99 67.4 43 67.4C41.01 67.4 39.4 69.01 39.4 71Z',
},
{
p: 2,
d: 'M49.4 71C49.4 72.99 51.01 74.6 53 74.6C54.99 74.6 56.6 72.99 56.6 71C56.6 69.01 54.99 67.4 53 67.4C51.01 67.4 49.4 69.01 49.4 71Z',
},
{ p: 'shade', d: 'M41 69.4L44.6 69.8M41 71.8L44.6 72M51.4 69.8L55 69.4M51.4 72L55 71.8', w: 0.6, o: 0.3 },
{
p: 0,
d: 'M23.7 37C23.7 39.43 25 41.67 27.1 42.89C29.2 44.1 31.8 44.1 33.9 42.89C36 41.67 37.3 39.43 37.3 37C37.3 34.57 36 32.33 33.9 31.11C31.8 29.9 29.2 29.9 27.1 31.11C25 32.33 23.7 34.57 23.7 37ZM58.7 37C58.7 39.43 60 41.67 62.1 42.89C64.2 44.1 66.8 44.1 68.9 42.89C71 41.67 72.3 39.43 72.3 37C72.3 34.57 71 32.33 68.9 31.11C66.8 29.9 64.2 29.9 62.1 31.11C60 32.33 58.7 34.57 58.7 37Z',
},
{
p: 2,
d: 'M26.6 37.2C26.6 38.7 27.4 40.09 28.7 40.84C30 41.59 31.6 41.59 32.9 40.84C34.2 40.09 35 38.7 35 37.2C35 35.7 34.2 34.31 32.9 33.56C31.6 32.81 30 32.81 28.7 33.56C27.4 34.31 26.6 35.7 26.6 37.2ZM61 37.2C61 39.52 62.88 41.4 65.2 41.4C67.52 41.4 69.4 39.52 69.4 37.2C69.4 34.88 67.52 33 65.2 33C62.88 33 61 34.88 61 37.2Z',
},
{
p: 'shade',
d: 'M29.4 37.6C29.4 38.7 30.3 39.6 31.4 39.6C32.5 39.6 33.4 38.7 33.4 37.6C33.4 36.5 32.5 35.6 31.4 35.6C30.3 35.6 29.4 36.5 29.4 37.6ZM62.6 37.6C62.6 38.31 62.98 38.97 63.6 39.33C64.22 39.69 64.98 39.69 65.6 39.33C66.22 38.97 66.6 38.31 66.6 37.6C66.6 36.89 66.22 36.23 65.6 35.87C64.98 35.51 64.22 35.51 63.6 35.87C62.98 36.23 62.6 36.89 62.6 37.6Z',
o: 0.15,
},
{
p: 0,
d: 'M42.6 20.6C43.4 17 45.8 15.4 47 13.6C47.6 15.6 48 16.6 49.6 16.4C50.6 14.8 52.6 14 54.4 14.6C53 16.4 54 18.6 54.4 20.6Z',
},
{
p: 0,
d: 'M31 36.5C31 45.89 38.61 53.5 48 53.5C57.39 53.5 65 45.89 65 36.5C65 27.11 57.39 19.5 48 19.5C38.61 19.5 31 27.11 31 36.5Z',
},
{ p: 'light', d: 'M37 26.6C39.4 23.6 42.8 22 46 21.6', w: 1.6, o: 0.3 },
{
p: 'shade',
d: 'M60.6 25C67.6 33 65.6 47.6 54.4 52.2C50.6 53.8 46 53.8 42 52.6C52 52.6 62 44 60.6 25Z',
o: 0.12,
},
{
p: 2,
d: 'M35 35C35 37.57 36.37 39.95 38.6 41.24C40.83 42.52 43.57 42.52 45.8 41.24C48.03 39.95 49.4 37.57 49.4 35C49.4 32.43 48.03 30.05 45.8 28.76C43.57 27.48 40.83 27.48 38.6 28.76C36.37 30.05 35 32.43 35 35Z',
},
{
p: 2,
d: 'M46.6 35C46.6 37.57 47.97 39.95 50.2 41.24C52.43 42.52 55.17 42.52 57.4 41.24C59.63 39.95 61 37.57 61 35C61 32.43 59.63 30.05 57.4 28.76C55.17 27.48 52.43 27.48 50.2 28.76C47.97 30.05 46.6 32.43 46.6 35Z',
},
{
p: 2,
d: 'M38 44.2C38 48.51 42.48 52 48 52C53.52 52 58 48.51 58 44.2C58 39.89 53.52 36.4 48 36.4C42.48 36.4 38 39.89 38 44.2Z',
},
{
p: '#e98f8f',
d: 'M36.8 43.6C36.8 44.48 37.96 45.2 39.4 45.2C40.84 45.2 42 44.48 42 43.6C42 42.72 40.84 42 39.4 42C37.96 42 36.8 42.72 36.8 43.6ZM54 43.6C54 44.48 55.16 45.2 56.6 45.2C58.04 45.2 59.2 44.48 59.2 43.6C59.2 42.72 58.04 42 56.6 42C55.16 42 54 42.72 54 43.6Z',
o: 0.45,
},
{
p: 'ink',
d: 'M40.3 35.6C40.3 36.87 41.33 37.9 42.6 37.9C43.87 37.9 44.9 36.87 44.9 35.6C44.9 34.33 43.87 33.3 42.6 33.3C41.33 33.3 40.3 34.33 40.3 35.6ZM51.1 35.6C51.1 36.87 52.13 37.9 53.4 37.9C54.67 37.9 55.7 36.87 55.7 35.6C55.7 34.33 54.67 33.3 53.4 33.3C52.13 33.3 51.1 34.33 51.1 35.6Z',
},
{
p: 'paper',
d: 'M42.5 34.8C42.5 35.24 42.86 35.6 43.3 35.6C43.74 35.6 44.1 35.24 44.1 34.8C44.1 34.36 43.74 34 43.3 34C42.86 34 42.5 34.36 42.5 34.8ZM53.3 34.8C53.3 35.24 53.66 35.6 54.1 35.6C54.54 35.6 54.9 35.24 54.9 34.8C54.9 34.36 54.54 34 54.1 34C53.66 34 53.3 34.36 53.3 34.8Z',
},
{ p: 0, d: 'M38.8 30.2C40.8 28.6 43.6 28.4 45.6 29.6M50.4 29.6C52.4 28.4 55.2 28.6 57.2 30.2', w: 1.1 },
{
p: 'ink',
d: 'M45.8 42.2C45.8 42.81 46.16 43.3 46.6 43.3C47.04 43.3 47.4 42.81 47.4 42.2C47.4 41.59 47.04 41.1 46.6 41.1C46.16 41.1 45.8 41.59 45.8 42.2ZM48.6 42.2C48.6 42.81 48.96 43.3 49.4 43.3C49.84 43.3 50.2 42.81 50.2 42.2C50.2 41.59 49.84 41.1 49.4 41.1C48.96 41.1 48.6 41.59 48.6 42.2Z',
o: 0.8,
},
{ p: 'ink', d: 'M43.4 46.4C46 49.4 50 49.4 52.6 46.4', w: 0.9 },
],
license: 'in-house',
},
];
@@ -208,4 +208,185 @@ export const DOODLES_STICKERS: readonly BookStickerDef[] = [
],
license: 'in-house',
},
{
id: 'doodlePalm',
theme: 'doodles',
tags: ['palm', 'palm tree', 'doodle', 'hand drawn', 'beach', 'tropical', 'island', 'sketch'],
descr:
'A hand-drawn palm tree doodle with a curved ringed trunk, four notched green fronds, three coconuts and a small sandy island line.',
box: [100, 108],
slots: [
{ name: 'leaf', color: '#2a9d5c' },
{ name: 'trunk', color: '#8a5a2b' },
],
layers: [
{
p: 1,
w: 2.4,
d: 'M13.8 101C15.5 100.5 20.1 99 23.8 98C27.6 97.1 32 96 36.4 95.4C40.7 94.8 45.4 94.6 50 94.6C54.6 94.5 59.7 94.6 64 95.2C68.3 95.8 71.7 97.2 75.7 98.2C79.7 99.2 85.8 100.7 87.9 101.2',
},
{
p: 1,
w: 2.2,
d: 'M20 104.6C22.3 104.4 29.4 103.7 33.9 103.8C38.4 103.8 44.8 104.5 46.9 104.7M56 104.4C58.3 104.4 65.8 104 70 104C74.1 104.1 79.1 104.7 81 104.9',
},
{
p: 1,
w: 2.4,
d: 'M41.8 97C41.6 95.7 41.2 92.1 41 89.6C40.7 87.1 40.3 84.5 40.4 82.1C40.5 79.7 41.1 77.4 41.4 75.2C41.6 72.9 41.5 70.7 42 68.5C42.5 66.3 43.5 64.1 44.2 62C44.9 59.9 45.5 57.8 46.3 55.9C47 54 47.8 52.5 48.7 50.8C49.6 49 50.8 46.9 51.7 45.2C52.7 43.6 53.3 42.2 54.4 40.7C55.4 39.2 57.4 36.9 58 36.1',
},
{
p: 1,
w: 2.4,
d: 'M52.2 95C52 93.9 51.1 90.4 50.8 88.2C50.5 86 50.4 83.8 50.3 81.8C50.3 79.7 50.5 78 50.6 76.1C50.7 74.2 50.6 72.2 50.8 70.3C51 68.3 51.2 66.3 51.7 64.5C52.2 62.6 53.1 60.7 53.8 59C54.4 57.2 54.9 55.5 55.6 53.8C56.3 52.1 57.1 50.4 57.9 48.8C58.8 47.3 59.7 46 60.5 44.5C61.4 43.1 62.6 40.7 63 40',
},
{
p: 1,
w: 1.8,
d: 'M41.3 89.4C42 89 44.5 87.5 45.5 87.4C46.6 87.2 47.3 88.5 47.7 88.7M41.1 82.1C41.8 81.9 44.4 80.7 45.4 80.7C46.4 80.7 46.8 81.8 47.1 82.1M41.5 75.2C42.2 75 44.9 74.2 45.9 74.3C46.8 74.4 47 75.5 47.3 75.7M42.7 68.6C43.4 68.5 45.9 67.7 46.8 67.8C47.7 68 47.9 69.3 48.1 69.5M44.3 62.2C44.9 62.1 47.5 61.6 48.3 61.8C49.2 62 49.2 63.3 49.4 63.7M46.5 56.2C47.1 56.3 49.3 56.1 50.1 56.4C50.9 56.7 51 57.7 51.1 58M49 50.7C49.6 50.7 52 50.6 52.7 50.9C53.5 51.2 53.1 52.4 53.2 52.7M51.7 45.6C52.3 45.6 54.7 45.4 55.4 45.8C56.1 46.1 55.7 47.4 55.7 47.7M55 40.7C55.5 40.8 57.4 40.6 58 41C58.6 41.4 58.3 42.7 58.4 43',
},
{
p: 0,
w: 2.3,
d: 'M57.9 33.1C58.2 32.4 57.7 31.2 56.7 30.2C55.7 29.2 53.8 27.9 52.2 27C50.6 26.1 48.7 25.3 47 24.8C45.2 24.3 43.3 24.2 41.5 24.2C39.7 24.2 37.7 24.3 35.9 24.7C34.2 25.2 32.6 26 31 26.9C29.4 27.8 27.9 28.8 26.6 30.1C25.2 31.4 24.1 32.9 23 34.5C21.9 36.1 20.8 37.8 20.1 39.6C19.3 41.5 17.9 45 18.5 45.5C19.2 46 21.9 43.1 23.8 42.7C25.8 42.2 29 43.2 30.4 42.7C31.8 42.1 31.2 39.5 32.4 39.3C33.7 39.2 36.6 42.2 37.8 41.9C38.9 41.6 38.5 37.9 39.3 37.7C40.2 37.4 41.6 40.9 42.8 40.6C43.9 40.3 45 36.5 46.1 36C47.2 35.5 48.1 37.8 49.6 37.6C51.1 37.4 53.7 35.4 55 34.6C56.4 33.9 57.7 33.9 57.9 33.1Z',
},
{
p: 0,
w: 1.9,
d: 'M58.4 33.4C57.3 33 54.1 31.6 51.9 31C49.8 30.4 47.6 30.1 45.5 30C43.4 29.9 41.5 30.1 39.5 30.5C37.5 30.8 35.4 31.4 33.6 32.2C31.8 32.9 30.2 33.7 28.5 34.9C26.8 36.2 24.3 38.8 23.4 39.5',
},
{
p: 0,
w: 2.3,
d: 'M59.9 32.4C60.5 32.1 60.7 31.6 60.8 30.6C60.8 29.6 60.4 27.9 60.1 26.7C59.7 25.5 59.2 24.4 58.8 23.4C58.4 22.5 58.1 21.7 57.7 20.8C57.3 19.9 56.8 18.9 56.3 18.2C55.7 17.4 54.9 16.8 54.3 16.2C53.6 15.7 53.1 15.4 52.4 14.9C51.7 14.5 50.9 13.8 50.1 13.6C49.4 13.4 48.6 13.6 47.9 13.6C47.2 13.6 46.2 13.2 45.8 13.7C45.4 14.2 45.6 15.8 45.6 16.6C45.6 17.4 45.7 18 45.7 18.7C45.8 19.4 45.7 19.9 45.8 20.6C46 21.2 46.5 22 46.7 22.7C47 23.3 47.1 23.9 47.6 24.5C48 25.1 48.6 25.8 49.3 26.4C50 27 50.8 27.4 51.6 28.1C52.5 28.7 53.2 29.7 54.2 30.4C55.1 31.1 56.3 32 57.3 32.4C58.3 32.7 59.3 32.7 59.9 32.4Z',
},
{
p: 0,
w: 1.9,
d: 'M60 32.8C59.7 32.1 58.6 30.1 57.8 28.8C57 27.5 56 26.3 55.3 25.1C54.5 23.9 54.1 22.8 53.4 21.8C52.7 20.8 51.8 20 51.1 19.1C50.4 18.3 49.7 17.4 49.1 16.7C48.5 16.1 47.7 15.5 47.4 15.2',
},
{
p: 0,
w: 2.3,
d: 'M62.1 32.5C61.6 32.2 61.5 31.5 61.4 30.6C61.4 29.6 61.6 28.1 61.8 26.9C62 25.7 62.3 24.5 62.7 23.5C63 22.4 63.5 21.5 63.9 20.6C64.3 19.8 64.6 19 65.1 18.2C65.6 17.4 66.1 16.5 66.7 15.9C67.4 15.3 68.3 14.9 69.1 14.5C69.8 14.1 70.5 13.8 71.2 13.6C71.8 13.3 72.3 12.9 73 12.8C73.8 12.8 75 12.7 75.5 13.2C76 13.7 75.7 15.2 75.8 16C75.8 16.8 75.9 17.5 75.8 18.1C75.8 18.8 75.6 19.4 75.5 20C75.4 20.7 75.4 21.3 75.1 21.9C74.8 22.6 74.2 23.5 73.8 24.2C73.3 24.9 72.9 25.5 72.3 26.1C71.8 26.7 71 27.2 70.2 27.9C69.5 28.5 68.8 29.2 67.9 29.9C66.9 30.6 65.6 31.8 64.7 32.2C63.7 32.6 62.7 32.8 62.1 32.5Z',
},
{
p: 0,
w: 1.9,
d: 'M61.8 32.9C62.2 32.1 63.4 29.8 64.2 28.4C64.9 27.1 65.6 26 66.3 24.9C67 23.7 67.7 22.4 68.4 21.4C69 20.4 69.5 19.4 70.1 18.6C70.7 17.9 71.3 17.3 71.9 16.6C72.6 16 73.5 15 73.8 14.6',
},
{
p: 0,
w: 2.3,
d: 'M63.6 33.2C63.3 32.6 63.8 31.8 64.6 30.9C65.4 30.1 67 28.9 68.5 28.2C69.9 27.5 71.7 27 73.2 26.7C74.8 26.3 76.3 25.9 77.8 26C79.4 26.1 81 26.8 82.4 27.4C83.9 28 85.5 28.7 86.8 29.6C88.1 30.5 89.3 31.5 90.4 32.7C91.5 34 92.3 35.7 93.2 37.3C94 38.8 94.9 40.3 95.5 42.1C96.1 43.8 97.3 47.4 96.8 47.7C96.3 48.1 94 45 92.2 44.3C90.5 43.6 87.6 44.1 86.3 43.4C85 42.6 85.4 40.1 84.3 39.8C83.2 39.5 80.6 41.9 79.7 41.5C78.7 41.1 79.4 37.8 78.7 37.6C78 37.3 76.4 40.2 75.6 39.9C74.7 39.5 74.4 36 73.6 35.5C72.7 35.1 71.7 37.3 70.4 37.1C69.1 37 67.1 35.2 66 34.6C64.8 33.9 63.8 33.8 63.6 33.2Z',
},
{
p: 0,
w: 1.9,
d: 'M63.1 33.4C64.1 33.1 66.9 32 68.8 31.5C70.6 31.1 72.5 30.6 74.2 30.6C75.9 30.6 77.4 31.2 79 31.6C80.6 32.1 82.2 32.5 83.7 33.4C85.3 34.3 86.7 35.6 88.2 37C89.7 38.3 91.9 40.8 92.6 41.5',
},
{
p: 1,
w: 2,
d: 'M59.8 38.7C59.9 39 60.2 40.2 59.9 40.8C59.7 41.4 59 42.1 58.4 42.4C57.8 42.7 56.9 42.7 56.2 42.5C55.6 42.2 54.9 41.6 54.6 41C54.3 40.4 54.2 39.5 54.4 38.9C54.6 38.2 55.3 37.5 55.9 37.2C56.5 36.9 57.5 36.8 58.1 37C58.8 37.2 59.4 37.8 59.8 38.4C60.1 39.1 60 40.3 60.1 40.7M67 39.1C67 39.5 67.2 40.6 67 41.2C66.7 41.8 66.1 42.5 65.5 42.8C64.9 43 64 43.2 63.4 43C62.7 42.7 62 42 61.7 41.4C61.4 40.8 61.4 39.9 61.6 39.3C61.9 38.7 62.5 38 63.1 37.7C63.7 37.3 64.7 37.2 65.3 37.4C66 37.6 66.7 38.2 67 38.8C67.3 39.4 67.1 40.7 67.1 41M63.6 43.3C63.5 43.6 63.7 44.8 63.4 45.4C63.1 46.1 62.6 46.7 62 46.9C61.4 47.2 60.4 47.4 59.8 47.2C59.1 47 58.4 46.3 58.1 45.7C57.8 45.1 57.8 44.1 58 43.5C58.3 42.8 58.9 42.2 59.5 41.9C60.1 41.6 61 41.4 61.7 41.6C62.4 41.8 63.1 42.4 63.4 43C63.7 43.6 63.5 44.9 63.5 45.2',
},
],
license: 'in-house',
},
{
id: 'doodleWaves',
theme: 'doodles',
tags: ['waves', 'sea', 'ocean', 'doodle', 'hand drawn', 'water', 'beach', 'surf'],
descr:
'Three stacked hand-drawn wave lines in blue pen, each crest leaning forward and rolling over into a small spiral curl.',
box: [100, 86],
slots: [
{ name: 'deep', color: '#2b86d1' },
{ name: 'surf', color: '#4fc0e8' },
],
layers: [
{
p: 0,
w: 2.6,
d: 'M4.9 29C5.6 28.8 7.8 28.7 9.1 28.1C10.5 27.6 11.7 26.7 13 25.7C14.3 24.7 15.7 23.1 16.9 22C18.1 21 19 20 20.2 19.3C21.4 18.7 22.9 18.2 24.1 18.2C25.2 18.3 26.6 18.9 27.3 19.5C28 20.2 28.3 21.4 28.3 22.3C28.3 23.2 27.9 24.4 27.3 24.9C26.8 25.5 25.7 25.7 25 25.5C24.4 25.3 23.6 24.4 23.4 23.7C23.2 23.1 23.5 22 24 21.5C24.5 21 25.9 21.1 26.3 21M32.9 29C33.6 28.9 35.7 28.8 37.1 28.2C38.4 27.7 39.8 26.8 41.1 25.7C42.5 24.6 43.8 22.9 45 21.8C46.1 20.6 47 19.4 48.1 18.7C49.2 18 50.5 17.6 51.6 17.8C52.8 17.9 54.3 18.8 55.1 19.5C55.9 20.2 56.3 21.1 56.3 21.9C56.3 22.8 55.8 24.2 55.2 24.8C54.6 25.3 53.4 25.6 52.8 25.3C52.1 25.1 51.5 24.1 51.3 23.4C51.2 22.8 51.6 21.7 52 21.3C52.5 20.8 53.9 20.8 54.3 20.7M60.8 29C61.4 28.9 63.3 28.8 64.7 28.2C66.1 27.6 67.7 26.7 69.1 25.6C70.4 24.6 71.6 22.9 72.8 21.8C74 20.7 74.9 19.8 76.1 19.2C77.2 18.6 78.5 18.3 79.6 18.4C80.8 18.5 82.2 19.1 83 19.7C83.7 20.3 84.2 21.3 84.2 22.1C84.2 22.9 83.6 24 83 24.5C82.5 25.1 81.6 25.5 80.9 25.4C80.2 25.2 79.2 24.3 79 23.6C78.8 22.9 79.2 21.7 79.7 21.2C80.2 20.8 81.7 20.9 82.1 20.9',
},
{
p: 0,
w: 2.4,
d: 'M28.2 21.8C28.3 22.3 28.5 24.1 28.8 25C29.2 25.9 29.8 26.6 30.4 27.2C31 27.9 31.6 28.6 32.3 28.9C33.1 29.2 34.5 29.1 34.9 29.1M56.2 21.8C56.3 22.3 56.3 23.8 56.7 24.8C57.1 25.7 57.8 26.7 58.4 27.4C59 28.1 59.6 28.6 60.4 28.9C61.1 29.1 62.4 29 62.8 29M84.1 21.8C84.2 22.3 84.3 24 84.6 24.9C84.9 25.8 85.4 26.8 86.1 27.3C86.8 27.9 88.2 28.3 88.7 28.5',
},
{
p: 1,
w: 2.6,
d: 'M10.1 54.9C10.8 54.8 12.9 54.7 14.2 54.2C15.6 53.6 16.9 52.7 18.1 51.7C19.4 50.6 20.8 48.9 22 47.8C23.1 46.7 24 45.6 25.1 44.9C26.3 44.3 27.5 43.8 28.7 43.8C29.8 43.9 31.1 44.5 32 45.2C32.8 45.9 33.4 47 33.5 47.9C33.6 48.8 33.1 50.1 32.5 50.6C31.9 51.1 30.6 51.3 29.9 51.2C29.1 51 28.4 50.3 28.2 49.7C28.1 49 28.5 47.7 29 47.2C29.5 46.7 31 46.8 31.4 46.7M38 55.1C38.6 54.9 40.5 54.8 41.9 54.3C43.2 53.8 44.6 53.1 45.9 52C47.2 50.9 48.6 48.9 49.8 47.7C51 46.6 51.8 45.7 53 45.1C54.2 44.5 55.8 44 57 44C58.1 44.1 59.2 44.8 60 45.5C60.8 46.2 61.5 47.4 61.5 48.3C61.6 49.2 61 50.2 60.4 50.7C59.8 51.3 58.7 51.6 58 51.5C57.3 51.3 56.2 50.4 56.1 49.7C55.9 49 56.4 47.6 56.9 47.1C57.4 46.6 58.8 46.8 59.2 46.8M65.8 55.1C66.5 55 68.5 54.9 69.9 54.4C71.3 53.8 72.8 53 74.1 51.9C75.4 50.8 76.6 48.9 77.8 47.7C79 46.6 79.9 45.5 81.1 44.9C82.2 44.3 83.4 43.9 84.6 44C85.7 44 87.3 44.6 88.1 45.3C88.8 46 89 47.3 89 48.2C89.1 49.1 88.9 49.9 88.4 50.4C87.9 51 86.6 51.6 85.9 51.5C85.1 51.3 84.3 50.2 84.1 49.5C83.9 48.8 84.2 47.8 84.7 47.3C85.2 46.9 86.7 46.8 87.1 46.7',
},
{
p: 1,
w: 2.4,
d: 'M33.3 47.8C33.4 48.3 33.6 49.9 33.9 50.9C34.3 51.8 34.8 52.9 35.4 53.5C36 54.1 36.8 54.5 37.6 54.7C38.3 55 39.5 55 39.9 55.1M61.2 47.8C61.3 48.3 61.5 50.1 61.8 51C62.1 51.9 62.6 52.6 63.2 53.2C63.8 53.8 64.6 54.3 65.3 54.6C66.1 54.9 67.4 55 67.8 55.1M89 47.7C89.1 48.3 89.2 50.1 89.6 51C90 51.9 90.7 52.8 91.3 53.3C92 53.9 93.2 54.3 93.6 54.5',
},
{
p: 0,
w: 2.6,
d: 'M3 81C3.6 80.9 5.5 80.8 6.9 80.3C8.3 79.8 9.9 78.9 11.2 77.8C12.5 76.8 13.6 75 14.8 73.9C16 72.8 17.1 71.7 18.3 71.2C19.4 70.6 20.5 70.4 21.7 70.5C22.8 70.6 24.4 71 25.1 71.6C25.9 72.2 26.2 73.3 26.3 74.2C26.4 75.1 26.2 76.5 25.6 77C25 77.5 23.6 77.5 22.9 77.3C22.2 77.1 21.6 76.2 21.4 75.6C21.3 75 21.4 74 21.9 73.6C22.3 73.2 23.9 73.1 24.3 73M30.9 81C31.6 80.9 33.7 80.6 35.1 80.1C36.4 79.5 37.7 78.5 39 77.5C40.3 76.4 41.4 74.9 42.6 73.7C43.8 72.6 44.9 71.2 46.1 70.5C47.3 69.9 48.7 69.8 49.9 69.9C51.1 70 52.4 70.6 53.2 71.2C53.9 71.9 54.4 73.1 54.5 74C54.6 74.9 54.1 75.9 53.6 76.4C53 76.9 51.8 77.4 51.1 77.2C50.4 77 49.5 75.9 49.3 75.2C49.1 74.5 49.5 73.5 50 73C50.5 72.6 51.8 72.6 52.2 72.5M58.8 81C59.5 80.8 61.4 80.6 62.8 80C64.2 79.4 65.7 78.6 67 77.4C68.3 76.3 69.4 74.4 70.6 73.2C71.7 72.1 72.7 71 73.8 70.4C75 69.8 76.5 69.4 77.7 69.5C78.8 69.6 80.3 70.1 81 70.8C81.8 71.5 82.1 72.7 82.1 73.6C82.2 74.6 81.9 75.9 81.3 76.5C80.7 77.1 79.3 77.4 78.6 77.2C77.9 77.1 77.2 76.1 77.1 75.3C76.9 74.6 77.1 73.4 77.6 72.9C78.1 72.4 79.7 72.5 80.1 72.4',
},
{
p: 0,
w: 2.4,
d: 'M26.3 73.8C26.4 74.4 26.6 76 27 76.9C27.3 77.8 27.8 78.6 28.3 79.2C28.9 79.9 29.6 80.5 30.3 80.8C31.1 81.1 32.5 81 33 81.1M54.2 73.8C54.3 74.3 54.5 76.1 54.9 77C55.2 77.9 55.6 78.7 56.2 79.3C56.8 79.9 57.7 80.3 58.4 80.6C59.2 80.9 60.4 81 60.8 81.1M82.1 73.8C82.2 74.3 82.5 76.1 82.8 77C83.1 78 83.5 78.7 84.1 79.2C84.7 79.8 86.2 80.3 86.6 80.5',
},
],
license: 'in-house',
},
{
id: 'doodleSunRays',
theme: 'doodles',
tags: ['sun', 'sunrise', 'sunset', 'doodle', 'hand drawn', 'summer', 'rays', 'riviera'],
descr:
'A hand-drawn half sun rising over a wobbly horizon line, filled yellow with orange outline, long and short rays and sea dashes below.',
box: [100, 82],
slots: [
{ name: 'sun', color: '#ffc93c' },
{ name: 'ray', color: '#f0772b' },
],
layers: [
{
p: 0,
d: 'M28.9 64.4C29.2 63.2 29.6 59.5 30.4 57.3C31.2 55 32.4 52.8 33.9 50.9C35.5 49 37.5 47.3 39.6 46.1C41.7 44.8 44.1 43.7 46.5 43.2C48.9 42.8 51.5 42.8 53.8 43.2C56.1 43.7 58.4 44.6 60.4 45.8C62.5 47.1 64.5 48.9 66.1 50.8C67.7 52.7 69 54.8 69.9 57C70.7 59.3 70.8 63.2 71 64.5L70.5 65.4L29.5 65.4Z',
},
{
p: 1,
w: 2.6,
d: 'M28.9 64.3C29.1 63.1 29.1 59.1 29.9 56.9C30.7 54.6 32.3 52.6 33.9 50.8C35.5 49 37.6 47.3 39.6 46.1C41.6 44.9 43.7 44 46 43.5C48.3 43 51.1 42.7 53.6 43.2C56 43.6 58.7 44.9 60.8 46.2C62.9 47.4 64.5 48.9 66 50.7C67.6 52.4 69.3 54.5 70.1 56.8C71 59.1 70.9 63.3 71 64.6',
},
{
p: 'light',
o: 0.6,
w: 2,
d: 'M38.6 56.7C38.8 56.5 39.5 55.6 40 55.1C40.5 54.6 41 54.1 41.6 53.6C42.2 53.1 42.9 52.5 43.5 52.2C44.1 51.8 44.6 51.7 45.3 51.4C45.9 51.2 47.1 50.9 47.5 50.7',
},
{
p: 1,
w: 2.6,
d: 'M76.3 60.7C77.5 60.4 81.3 59.4 83.8 59.1C86.3 58.8 89.9 58.9 91.1 58.8M71.8 48.9C72.7 48.1 75.4 45.6 77.5 44.2C79.6 42.7 83.3 41 84.5 40.3M62.3 40.5C62.8 39.3 64.2 35.7 65.3 33.5C66.3 31.3 68.1 28.3 68.6 27.2M50 37.5C49.9 36.3 49.5 32.6 49.5 30.1C49.5 27.6 50.1 23.8 50.2 22.5M37.7 40.5C37.1 39.4 34.9 36.1 33.8 33.9C32.7 31.7 31.6 28.3 31.2 27.2M28.2 48.9C27.2 48.2 23.9 46.3 21.9 44.9C19.8 43.5 16.8 41.1 15.8 40.3M23.7 60.7C22.5 60.6 18.9 60.5 16.4 60.2C14 59.9 10.1 59 8.8 58.8',
},
{
p: 1,
w: 2.2,
d: 'M74.7 54.5C75.4 54.2 77.6 53.2 78.9 52.7C80.3 52.1 82.3 51.5 83 51.3M67.5 44.1C68 43.5 69.2 41.4 70.2 40.3C71.3 39.2 73.3 37.9 73.9 37.4M56.3 38.3C56.4 37.5 56.6 35.4 57.1 34C57.5 32.5 58.8 30.3 59.1 29.5M43.7 38.3C43.4 37.6 42.4 35.6 42.1 34.1C41.9 32.7 42.2 30.3 42.2 29.5M32.5 44.1C31.9 43.6 30.1 42.4 29.2 41.3C28.3 40.2 27.3 38 26.9 37.4M25.3 54.5C24.6 54.3 22.5 53.7 21.1 53.2C19.6 52.6 17.5 51.6 16.7 51.3',
},
{
p: 1,
w: 2.6,
d: 'M7 65.5C9.5 65.3 16.5 64.4 21.7 64.4C26.9 64.4 33.4 65.5 38.1 65.5C42.8 65.5 45.7 64.6 50 64.5C54.3 64.4 58.7 64.9 63.7 65C68.6 65 74.9 64.8 79.8 64.9C84.7 65 90.8 65.3 92.9 65.3',
},
{
p: 1,
w: 2.2,
d: 'M24 71.6C25.5 71.5 29.9 71.2 32.9 71.2C35.9 71.2 40.5 71.5 42 71.5M55 71.3C56.7 71.3 61.7 70.9 65 71C68.3 71 73.3 71.4 75 71.5',
},
{ p: 1, w: 2, d: 'M38 77C40 76.9 46.1 76.7 49.9 76.7C53.7 76.7 59.1 77 61 77' },
],
license: 'in-house',
},
];
@@ -843,4 +843,423 @@ export const LANDMARKS_STICKERS: readonly BookStickerDef[] = [
],
license: 'in-house',
},
{
id: 'parthenon',
theme: 'landmarks',
tags: ['parthenon', 'acropolis', 'athens', 'greece', 'temple', 'ancient', 'columns', 'landmark'],
descr:
'The Parthenon on the Acropolis in Athens, a row of fluted Doric columns in warm marble under a low pediment, standing on stepped base and rock with olive shrubs.',
box: [110, 84],
slots: [
{ name: 'marble', color: '#efdfbf' },
{ name: 'rock', color: '#b99a74' },
{ name: 'detail', color: '#a98a62' },
{ name: 'olive', color: '#7f8f4e' },
],
layers: [
{ p: 'shade', d: 'M8 82C39.33 83.6 70.67 83.6 102 82C70.67 81.07 39.33 81.07 8 82Z', o: 0.1 },
{
p: 1,
d: 'M3 81C2.4 77 4 73.5 7 71.5C9 69 11.5 67.5 15 66.8L40 65.4L72 65.4L95 66.8C98.5 67.5 101 69 103 71.5C106 73.5 107.6 77 107 81C90 82.6 20 82.6 3 81Z',
},
{
p: 'shade',
d: 'M3 81C2.4 77 4 73.5 7 71.5C9 69 11.5 67.5 15 66.8L17.5 69.5C15 72 16.5 76 21 81.6C12 81.6 6 81.4 3 81ZM107 81C107.6 77 106 73.5 103 71.5C101 69 98.5 67.5 95 66.8L92.5 69.5C95 72 93.5 76 89 81.6C98 81.6 104 81.4 107 81Z',
o: 0.2,
},
{
p: 'shade',
d: 'M4.2 77.6C25 79.6 85 79.6 105.8 77.6C106.6 78.8 107 79.8 107 81C90 82.6 20 82.6 3 81C3 79.8 3.4 78.6 4.2 77.6Z',
o: 0.14,
},
{
p: 2,
d: 'M26 72.6C32.67 71.67 39.33 71.73 46 72.8M58 75.4C66 74.47 74.67 74.6 84 75.8M31 78.4C37 77.73 43.33 77.8 50 78.6M66 79.4C70 79 74 79.07 78 79.6M88 72C90 71.6 91.83 71.6 93.5 72',
o: 0.7,
w: 0.9,
},
{ p: 'light', d: 'M12 69C24 67.6 36.67 66.9 50 66.9M62 66.9C75.33 66.9 87.33 67.6 98 69', o: 0.35, w: 0.9 },
{
p: 3,
d: 'M5.1 72.7C3.4 69.9 6.2 67.2 8.4 67.9C9.5 65.5 13.4 66.1 13.5 68.8C15.7 69.2 15.7 72.7 13.9 72.7ZM96.5 72.5C95 70 97.5 67.5 99.5 68.2C100.5 66 104 66.5 104.1 69C106.1 69.4 106.1 72.5 104.5 72.5ZM13.5 69.3C12.4 67.5 14.3 65.6 15.8 66.1C16.5 64.5 19.1 64.8 19.2 66.7C20.7 67 20.7 69.3 19.5 69.3ZM90.9 69.4C89.9 67.8 91.6 66.1 92.9 66.6C93.5 65.2 95.8 65.5 95.8 67.1C97.1 67.4 97.1 69.4 96.1 69.4Z',
},
{
p: 'shade',
d: 'M5.5 72.3C7.5 73.4 11.5 73.4 13.6 72.3L13.9 72.6C11.5 73.9 7.5 73.9 5.2 72.6ZM96.8 72.2C98.6 73.2 102.4 73.2 104.2 72.2L104.5 72.5C102.4 73.7 98.6 73.7 96.5 72.5Z',
o: 0.3,
},
{ p: 0, d: 'M12 61.5L98 61.5L98 66.5L12 66.5Z' },
{ p: 0, d: 'M14 58L96 58L96 63L14 63Z' },
{ p: 0, d: 'M16 54.5L94 54.5L94 59.5L16 59.5Z' },
{
p: 'shade',
d: 'M12 65L98 65L98 66.5L12 66.5ZM14 61.5L96 61.5L96 62.7L14 62.7ZM16 58L94 58L94 59.2L16 59.2Z',
o: 0.2,
},
{
p: 'light',
d: 'M12 61.6L98 61.6L98 62.2L12 62.2ZM14 58.1L96 58.1L96 58.7L14 58.7ZM16 54.6L94 54.6L94 55.2L16 55.2Z',
o: 0.5,
},
{ p: 0, d: 'M19 26L91 26L91 56L19 56Z' },
{ p: 'shade', d: 'M19 26L91 26L91 56L19 56Z', o: 0.38 },
{
p: 0,
d: 'M19.1 28.5L24.3 28.5L24.9 55.5L18.5 55.5ZM28.6 28.5L33.8 28.5L34.4 55.5L28 55.5ZM38.1 28.5L43.3 28.5L43.9 55.5L37.5 55.5ZM47.6 28.5L52.8 28.5L53.4 55.5L47 55.5ZM57.2 28.5L62.4 28.5L63 55.5L56.6 55.5ZM66.7 28.5L71.9 28.5L72.5 55.5L66.1 55.5ZM76.2 28.5L81.4 28.5L82 55.5L75.6 55.5ZM85.7 28.5L90.9 28.5L91.5 55.5L85.1 55.5Z',
},
{
p: 'shade',
d: 'M22.5 28.5L24.3 28.5L24.9 55.5L22.6 55.5ZM32 28.5L33.8 28.5L34.4 55.5L32.1 55.5ZM41.5 28.5L43.3 28.5L43.9 55.5L41.6 55.5ZM51 28.5L52.8 28.5L53.4 55.5L51.1 55.5ZM60.5 28.5L62.4 28.5L63 55.5L60.7 55.5ZM70 28.5L71.9 28.5L72.5 55.5L70.2 55.5ZM79.6 28.5L81.4 28.5L82 55.5L79.7 55.5ZM89.1 28.5L90.9 28.5L91.5 55.5L89.2 55.5Z',
o: 0.16,
},
{
p: 'shade',
d: 'M20.6 30.5L20.6 54M21.7 30.5L21.7 54M30.1 30.5L30.1 54M31.2 30.5L31.2 54M39.6 30.5L39.6 54M40.7 30.5L40.7 54M49.2 30.5L49.2 54M50.2 30.5L50.2 54M58.7 30.5L58.7 54M59.8 30.5L59.8 54M68.2 30.5L68.2 54M69.3 30.5L69.3 54M77.7 30.5L77.7 54M78.8 30.5L78.8 54M87.2 30.5L87.2 54M88.3 30.5L88.3 54',
o: 0.14,
w: 0.45,
},
{
p: 0,
d: 'M17.9 26.3L25.5 26.3L25.5 28.2C25.1 29.27 24.67 29.87 24.2 30L19.2 30C18.73 29.87 18.3 29.27 17.9 28.2ZM27.4 26.3L35 26.3L35 28.2C34.6 29.27 34.17 29.87 33.7 30L28.7 30C28.23 29.87 27.8 29.27 27.4 28.2ZM36.9 26.3L44.5 26.3L44.5 28.2C44.1 29.27 43.67 29.87 43.2 30L38.2 30C37.73 29.87 37.3 29.27 36.9 28.2ZM46.4 26.3L54 26.3L54 28.2C53.6 29.27 53.17 29.87 52.7 30L47.7 30C47.23 29.87 46.8 29.27 46.4 28.2ZM56 26.3L63.6 26.3L63.6 28.2C63.2 29.27 62.77 29.87 62.3 30L57.3 30C56.83 29.87 56.4 29.27 56 28.2ZM65.5 26.3L73.1 26.3L73.1 28.2C72.7 29.27 72.27 29.87 71.8 30L66.8 30C66.33 29.87 65.9 29.27 65.5 28.2ZM75 26.3L82.6 26.3L82.6 28.2C82.2 29.27 81.77 29.87 81.3 30L76.3 30C75.83 29.87 75.4 29.27 75 28.2ZM84.5 26.3L92.1 26.3L92.1 28.2C91.7 29.27 91.27 29.87 90.8 30L85.8 30C85.33 29.87 84.9 29.27 84.5 28.2Z',
},
{
p: 'shade',
d: 'M17.9 28.2L25.5 28.2L25.5 29L17.9 29ZM27.4 28.2L35 28.2L35 29L27.4 29ZM36.9 28.2L44.5 28.2L44.5 29L36.9 29ZM46.4 28.2L54 28.2L54 29L46.4 29ZM56 28.2L63.6 28.2L63.6 29L56 29ZM65.5 28.2L73.1 28.2L73.1 29L65.5 29ZM75 28.2L82.6 28.2L82.6 29L75 29ZM84.5 28.2L92.1 28.2L92.1 29L84.5 29Z',
o: 0.14,
},
{ p: 0, d: 'M15 21L95 21L95 27L15 27Z' },
{ p: 'shade', d: 'M15 25.4L95 25.4L95 27L15 27Z', o: 0.18 },
{ p: 0, d: 'M15.5 16.5L94.5 16.5L94.5 21.7L15.5 21.7Z' },
{
p: 2,
d: 'M15.3 17.3L17.1 17.3L17.1 21.1L15.3 21.1ZM20.1 17.3L21.9 17.3L21.9 21.1L20.1 21.1ZM24.9 17.3L26.7 17.3L26.7 21.1L24.9 21.1ZM29.7 17.3L31.5 17.3L31.5 21.1L29.7 21.1ZM34.6 17.3L36.4 17.3L36.4 21.1L34.6 21.1ZM39.4 17.3L41.2 17.3L41.2 21.1L39.4 21.1ZM44.2 17.3L46 17.3L46 21.1L44.2 21.1ZM49 17.3L50.8 17.3L50.8 21.1L49 21.1ZM53.8 17.3L55.6 17.3L55.6 21.1L53.8 21.1ZM58.6 17.3L60.4 17.3L60.4 21.1L58.6 21.1ZM63.4 17.3L65.2 17.3L65.2 21.1L63.4 21.1ZM68.2 17.3L70 17.3L70 21.1L68.2 21.1ZM73.1 17.3L74.9 17.3L74.9 21.1L73.1 21.1ZM77.9 17.3L79.7 17.3L79.7 21.1L77.9 21.1ZM82.7 17.3L84.5 17.3L84.5 21.1L82.7 21.1ZM87.5 17.3L89.3 17.3L89.3 21.1L87.5 21.1ZM92.3 17.3L94.1 17.3L94.1 21.1L92.3 21.1Z',
o: 0.55,
},
{ p: 'light', d: 'M15 21L95 21L95 21.6L15 21.6Z', o: 0.55 },
{ p: 0, d: 'M13 14.4L97 14.4L97 17.2L13 17.2Z' },
{ p: 'shade', d: 'M13 16.4L97 16.4L97 17.2L13 17.2Z', o: 0.2 },
{ p: 0, d: 'M13.5 15L55 4L83 11.4L85 12.6L88 12.4L90 13.6L96.5 15Z' },
{ p: 'shade', d: 'M19.5 14.6L55 6.2L80.5 12.3L83.2 14.6Z', o: 0.2 },
{
p: 2,
d: 'M40 14.6L40 11.4M44.5 14.6L44.5 10.4M49 14.6L49 9.4M53.5 14.6L53.5 8.6M58 14.6L58 8.8M62.5 14.6L62.5 9.8M67 14.6L67 10.9M36 14.6C37 13.4 37.87 13.07 38.6 13.6M71 14.6C71.8 13.27 72.73 12.87 73.8 13.4',
o: 0.45,
w: 0.9,
},
{ p: 'light', d: 'M16 14.7L55 5.3L81.5 11.6', o: 0.6, w: 0.6 },
{ p: 'light', d: 'M13 14.4L97 14.4L97 15L13 15Z', o: 0.5 },
],
license: 'in-house',
},
{
id: 'milanDuomo',
theme: 'landmarks',
tags: ['milan', 'duomo', 'cathedral', 'italy', 'gothic', 'lombardy', 'church', 'landmark'],
descr:
'The pale pink marble Gothic facade of Milan Cathedral, bristling with slender spires and statues, five portals, and the golden Madonnina atop the central spire.',
box: [100, 104],
slots: [
{ name: 'marble', color: '#f6ebe4' },
{ name: 'trim', color: '#e3c9bd' },
{ name: 'openings', color: '#5c4a4a' },
{ name: 'gold', color: '#e4b23a' },
],
layers: [
{ p: 0, d: 'M47 46L47.6 26L48.6 16L49.4 11L50.6 11L51.4 16L52.4 26L53 46Z' },
{ p: 'shade', d: 'M50 11.5L50.6 11L51.4 16L52.4 26L53 46L50 46Z', o: 0.14 },
{ p: 1, d: 'M48 34.5L52 34.5M48.4 26.5L51.6 26.5M48.9 19.5L51.1 19.5', w: 0.6, o: 0.9 },
{
p: 3,
d: 'M48.8 4.6C48.8 3.94 49.34 3.4 50 3.4C50.66 3.4 51.2 3.94 51.2 4.6C51.2 5.26 50.66 5.8 50 5.8C49.34 5.8 48.8 5.26 48.8 4.6ZM48.6 6.2C49.53 5.53 50.47 5.53 51.4 6.2L51.9 11.6L48.1 11.6ZM48.7 6.6L46.8 5.4L46.6 5.9L48.6 7.6ZM51.3 6.6L53.2 5.4L53.4 5.9L51.4 7.6Z',
},
{ p: 0, d: 'M9 96L9 50L23 46L23 44L37 40L50 30.5L63 40L77 44L77 46L91 50L91 96Z' },
{ p: 1, d: 'M9 50L23 46M23 44L37 40L50 30.5L63 40L77 44M77 46L91 50', w: 1.3 },
{
p: 0,
d: 'M8.2 38L11.2 38L11.2 96L8.2 96ZM22.2 34L25.2 34L25.2 96L22.2 96ZM36.2 28L39.8 28L39.8 96L36.2 96ZM60.6 28L64.2 28L64.2 96L60.6 96ZM74.6 34L77.6 34L77.6 96L74.6 96ZM88.6 38L91.6 38L91.6 96L88.6 96ZM8.4 39L8.9 33.5L9.7 30L10.5 33.5L11 39ZM22.4 35L22.9 29.5L23.7 26L24.5 29.5L25 35ZM36.4 29L36.9 23.5L38 20L39.1 23.5L39.6 29ZM60.8 29L61.3 23.5L62.4 20L63.5 23.5L64 29ZM74.8 35L75.3 29.5L76.1 26L76.9 29.5L77.4 35ZM88.8 39L89.3 33.5L90.1 30L90.9 33.5L91.4 39Z',
},
{
p: 0,
d: 'M9 27.8C8.88 27.47 8.99 27.09 9.27 26.88C9.55 26.67 9.95 26.67 10.23 26.88C10.51 27.09 10.62 27.47 10.5 27.8C10.62 28.13 10.51 28.51 10.23 28.72C9.95 28.93 9.55 28.93 9.27 28.72C8.99 28.51 8.88 28.13 9 27.8ZM9.2 28.1L10.2 28.1L10.2 30.5L9.2 30.5ZM23 23.8C22.88 23.47 22.99 23.09 23.27 22.88C23.55 22.67 23.95 22.67 24.23 22.88C24.51 23.09 24.62 23.47 24.5 23.8C24.62 24.13 24.51 24.51 24.23 24.72C23.95 24.93 23.55 24.93 23.27 24.72C22.99 24.51 22.88 24.13 23 23.8ZM23.2 24.1L24.2 24.1L24.2 26.5L23.2 26.5ZM37.3 17.8C37.18 17.47 37.29 17.09 37.57 16.88C37.85 16.67 38.25 16.67 38.53 16.88C38.81 17.09 38.92 17.47 38.8 17.8C38.92 18.13 38.81 18.51 38.53 18.72C38.25 18.93 37.85 18.93 37.57 18.72C37.29 18.51 37.18 18.13 37.3 17.8ZM37.5 18.1L38.5 18.1L38.5 20.5L37.5 20.5ZM61.7 17.8C61.58 17.47 61.69 17.09 61.97 16.88C62.25 16.67 62.65 16.67 62.93 16.88C63.21 17.09 63.32 17.47 63.2 17.8C63.32 18.13 63.21 18.51 62.93 18.72C62.65 18.93 62.25 18.93 61.97 18.72C61.69 18.51 61.58 18.13 61.7 17.8ZM61.9 18.1L62.9 18.1L62.9 20.5L61.9 20.5ZM75.4 23.8C75.28 23.47 75.39 23.09 75.67 22.88C75.95 22.67 76.35 22.67 76.63 22.88C76.91 23.09 77.02 23.47 76.9 23.8C77.02 24.13 76.91 24.51 76.63 24.72C76.35 24.93 75.95 24.93 75.67 24.72C75.39 24.51 75.28 24.13 75.4 23.8ZM75.6 24.1L76.6 24.1L76.6 26.5L75.6 26.5ZM89.4 27.8C89.28 27.47 89.39 27.09 89.67 26.88C89.95 26.67 90.35 26.67 90.63 26.88C90.91 27.09 91.02 27.47 90.9 27.8C91.02 28.13 90.91 28.51 90.63 28.72C90.35 28.93 89.95 28.93 89.67 28.72C89.39 28.51 89.28 28.13 89.4 27.8ZM89.6 28.1L90.6 28.1L90.6 30.5L89.6 30.5Z',
},
{
p: 'shade',
d: 'M9.8 38L11.2 38L11.2 96L9.8 96ZM23.8 34L25.2 34L25.2 96L23.8 96ZM38.1 28L39.8 28L39.8 96L38.1 96ZM62.5 28L64.2 28L64.2 96L62.5 96ZM76.2 34L77.6 34L77.6 96L76.2 96ZM90.2 38L91.6 38L91.6 96L90.2 96Z',
o: 0.1,
},
{ p: 1, d: 'M8.4 36L11 36M22.4 32L25 32M36.4 26L39.6 26M60.8 26L64 26M74.8 32L77.4 32M88.8 36L91.4 36', w: 0.6 },
{
p: 0,
d: 'M13.2 49.6L13.3 45.8L14 42.3L14.7 45.8L14.8 49.6ZM17.7 48.5L17.8 44.7L18.5 41.2L19.2 44.7L19.3 48.5ZM27.2 44.3L27.3 40.5L28 37L28.7 40.5L28.8 44.3ZM31.7 43L31.8 39.2L32.5 35.7L33.2 39.2L33.3 43ZM41.2 38.3L41.3 34.5L42 31L42.7 34.5L42.8 38.3ZM44.7 35.8L44.8 32L45.5 28.5L46.2 32L46.3 35.8ZM53.7 35.8L53.8 32L54.5 28.5L55.2 32L55.3 35.8ZM57.2 38.3L57.3 34.5L58 31L58.7 34.5L58.8 38.3ZM66.7 43L66.8 39.2L67.5 35.7L68.2 39.2L68.3 43ZM71.2 44.3L71.3 40.5L72 37L72.7 40.5L72.8 44.3ZM80.7 48.5L80.8 44.7L81.5 41.2L82.2 44.7L82.3 48.5ZM85.2 49.6L85.3 45.8L86 42.3L86.7 45.8L86.8 49.6Z',
},
{ p: 1, d: 'M9 66L91 66L91 67.2L9 67.2ZM9 80L91 80L91 81.1L9 81.1Z', o: 0.9 },
{ p: 1, d: 'M42.6 61L50 39.6L57.4 61ZM26.4 62.4L30.7 45L35 62.4ZM65 62.4L69.3 45L73.6 62.4Z', o: 0.75 },
{ p: 0, d: 'M44.2 61L50 43.2L55.8 61ZM27.8 62.4L30.7 49L33.6 62.4ZM66.4 62.4L69.3 49L72.2 62.4Z' },
{ p: 2, d: 'M45 59L45 50.5C45 46.5 46.67 44 50 43C53.33 44 55 46.5 55 50.5L55 59Z' },
{ p: 'light', d: 'M50 44.5L50 58.5M46 51L54 51', o: 0.35, w: 0.5 },
{ p: 2, d: 'M28.2 61L28.2 52.8C28.2 50.8 29.03 49.53 30.7 49C32.37 49.53 33.2 50.8 33.2 52.8L33.2 61Z' },
{ p: 2, d: 'M66.8 61L66.8 52.8C66.8 50.8 67.63 49.53 69.3 49C70.97 49.53 71.8 50.8 71.8 52.8L71.8 61Z' },
{ p: 2, d: 'M14 62.6L14 56.2C14 54.47 14.7 53.4 16.1 53C17.5 53.4 18.2 54.47 18.2 56.2L18.2 62.6Z' },
{ p: 2, d: 'M81.8 62.6L81.8 56.2C81.8 54.47 82.5 53.4 83.9 53C85.3 53.4 86 54.47 86 56.2L86 62.6Z' },
{ p: 2, d: 'M14.2 76.8L14.2 71.5C14.2 70.45 15.05 69.6 16.1 69.6C17.15 69.6 18 70.45 18 71.5L18 76.8Z', o: 0.85 },
{
p: 2,
d: 'M28.6 76.8L28.6 71.7C28.6 70.54 29.54 69.6 30.7 69.6C31.86 69.6 32.8 70.54 32.8 71.7L32.8 76.8Z',
o: 0.85,
},
{
p: 2,
d: 'M67.2 76.8L67.2 71.7C67.2 70.54 68.14 69.6 69.3 69.6C70.46 69.6 71.4 70.54 71.4 71.7L71.4 76.8Z',
o: 0.85,
},
{ p: 2, d: 'M82 76.8L82 71.5C82 70.45 82.85 69.6 83.9 69.6C84.95 69.6 85.8 70.45 85.8 71.5L85.8 76.8Z', o: 0.85 },
{ p: 2, d: 'M41.5 76.8L41.5 70.6C41.5 69.72 42.22 69 43.1 69C43.98 69 44.7 69.72 44.7 70.6L44.7 76.8Z', o: 0.85 },
{ p: 2, d: 'M55.3 76.8L55.3 70.6C55.3 69.72 56.02 69 56.9 69C57.78 69 58.5 69.72 58.5 70.6L58.5 76.8Z', o: 0.85 },
{ p: 1, d: 'M11.8 82.5L20.2 82.5L20.2 96L11.8 96ZM11.6 82.6L16 79.5L20.4 82.6Z' },
{ p: 2, d: 'M13.2 96L13.2 86.8C13.2 85.25 14.45 84 16 84C17.55 84 18.8 85.25 18.8 86.8L18.8 96Z' },
{ p: 1, d: 'M26.2 82.5L35.2 82.5L35.2 96L26.2 96ZM26 82.6L30.7 79.5L35.4 82.6Z' },
{ p: 2, d: 'M27.6 96L27.6 87.1C27.6 85.39 28.99 84 30.7 84C32.41 84 33.8 85.39 33.8 87.1L33.8 96Z' },
{ p: 1, d: 'M43.2 80.5L56.8 80.5L56.8 96L43.2 96ZM43 80.6L50 77.5L57 80.6Z' },
{ p: 2, d: 'M44.6 96L44.6 87.4C44.6 84.42 47.02 82 50 82C52.98 82 55.4 84.42 55.4 87.4L55.4 96Z' },
{ p: 1, d: 'M64.8 82.5L73.8 82.5L73.8 96L64.8 96ZM64.6 82.6L69.3 79.5L74 82.6Z' },
{ p: 2, d: 'M66.2 96L66.2 87.1C66.2 85.39 67.59 84 69.3 84C71.01 84 72.4 85.39 72.4 87.1L72.4 96Z' },
{ p: 1, d: 'M79.8 82.5L88.2 82.5L88.2 96L79.8 96ZM79.6 82.6L84 79.5L88.4 82.6Z' },
{ p: 2, d: 'M81.2 96L81.2 86.8C81.2 85.25 82.45 84 84 84C85.55 84 86.8 85.25 86.8 86.8L86.8 96Z' },
{ p: 'light', d: 'M50 85.4L50 96', o: 0.3, w: 0.5 },
{ p: 1, d: 'M4 95.4L96 95.4L96 98.6L4 98.6Z' },
{ p: 1, d: 'M1.5 98L98.5 98L98.5 101.4L1.5 101.4Z' },
{ p: 'shade', d: 'M4 97.6L96 97.6L96 98.6L4 98.6ZM1.5 100.4L98.5 100.4L98.5 101.4L1.5 101.4Z', o: 0.15 },
{ p: 'light', d: 'M4 95.5L96 95.5L96 96.1L4 96.1ZM1.5 98.1L98.5 98.1L98.5 98.7L1.5 98.7Z', o: 0.5 },
],
license: 'in-house',
},
{
id: 'canalHouses',
theme: 'landmarks',
tags: ['amsterdam', 'canal houses', 'netherlands', 'dutch', 'gables', 'canal', 'grachten', 'city'],
descr:
'A row of five narrow Amsterdam canal houses with bell, neck and step gables, tall white framed windows and hoisting beams above a strip of canal.',
box: [110, 100],
slots: [
{ name: 'brick', color: '#a3543a' },
{ name: 'slate', color: '#3f5560' },
{ name: 'ochre', color: '#c99555' },
{ name: 'canal', color: '#4d8392' },
],
layers: [
{ p: 3, d: 'M0 87L110 87L110 100L0 100Z' },
{ p: '#8f8a84', d: 'M0 84.5L110 84.5L110 88.5L0 88.5Z' },
{ p: 'shade', d: 'M0 87.3L110 87.3L110 88.5L0 88.5Z', o: 0.25 },
{ p: 'light', d: 'M0 84.6L110 84.6L110 85.2L0 85.2Z', o: 0.5 },
{
p: 'light',
d: 'M6 92.5L20 92.5M30 95.5L40 95.5M52 92.5L70 92.5M80 96L96 96M14 98L26 98M88 91.5L104 91.5',
o: 0.45,
w: 0.9,
},
{ p: 0, d: 'M4 86.5L4 32C8.8 32 11 29 11 22C13.67 18.33 16.33 18.33 19 22C19 29 21.2 32 26 32L26 86.5Z' },
{ p: 'shade', d: 'M24.4 32L26 32L26 86.5L24.4 86.5Z', o: 0.14 },
{ p: 'paper', d: 'M4.8 32.6C8.8 32 11 29 11 22C13.67 18.33 16.33 18.33 19 22C19 29 21.2 32 26 32', w: 0.9 },
{ p: 'paper', d: 'M4.3 32.6L25.7 32.6L25.7 33.9L4.3 33.9Z' },
{ p: '#3a2f2a', d: 'M14.4 22.6L15.6 22.6L15.6 24.2L14.4 24.2Z' },
{ p: '#3a2f2a', d: 'M15 24L15 26.2', w: 0.35 },
{ p: 'paper', d: 'M13.2 26.2L16.8 26.2L16.8 30.6L13.2 30.6Z' },
{ p: '#2c3338', d: 'M13.9 26.9L16.1 26.9L16.1 29.9L13.9 29.9Z' },
{
p: 'paper',
d: 'M6.1 36L10.7 36L10.7 44.6L6.1 44.6ZM12.7 36L17.3 36L17.3 44.6L12.7 44.6ZM19.4 36L24 36L24 44.6L19.4 44.6ZM6.1 47.5L10.7 47.5L10.7 55.3L6.1 55.3ZM12.7 47.5L17.3 47.5L17.3 55.3L12.7 55.3ZM19.4 47.5L24 47.5L24 55.3L19.4 55.3ZM6.1 59L10.7 59L10.7 66.8L6.1 66.8ZM12.7 59L17.3 59L17.3 66.8L12.7 66.8ZM19.4 59L24 59L24 66.8L19.4 66.8ZM5.6 44.3L11.2 44.3L11.2 45.3L5.6 45.3ZM12.2 44.3L17.8 44.3L17.8 45.3L12.2 45.3ZM18.9 44.3L24.5 44.3L24.5 45.3L18.9 45.3ZM5.6 55L11.2 55L11.2 56L5.6 56ZM12.2 55L17.8 55L17.8 56L12.2 56ZM18.9 55L24.5 55L24.5 56L18.9 56ZM5.6 66.5L11.2 66.5L11.2 67.5L5.6 67.5ZM12.2 66.5L17.8 66.5L17.8 67.5L12.2 67.5ZM18.9 66.5L24.5 66.5L24.5 67.5L18.9 67.5Z',
},
{
p: '#2c3338',
d: 'M6.7 36.6L10.1 36.6L10.1 44L6.7 44ZM13.3 36.6L16.7 36.6L16.7 44L13.3 44ZM20 36.6L23.4 36.6L23.4 44L20 44ZM6.7 48.1L10.1 48.1L10.1 54.7L6.7 54.7ZM13.3 48.1L16.7 48.1L16.7 54.7L13.3 54.7ZM20 48.1L23.4 48.1L23.4 54.7L20 54.7ZM6.7 59.6L10.1 59.6L10.1 66.2L6.7 66.2ZM13.3 59.6L16.7 59.6L16.7 66.2L13.3 66.2ZM20 59.6L23.4 59.6L23.4 66.2L20 66.2Z',
},
{
p: 'paper',
d: 'M8.4 36.6L8.4 44M6.7 39.9L10.1 39.9M15 36.6L15 44M13.3 39.9L16.7 39.9M21.7 36.6L21.7 44M20 39.9L23.4 39.9M8.4 48.1L8.4 54.7M6.7 51L10.1 51M15 48.1L15 54.7M13.3 51L16.7 51M21.7 48.1L21.7 54.7M20 51L23.4 51M8.4 59.6L8.4 66.2M6.7 62.5L10.1 62.5M15 59.6L15 66.2M13.3 62.5L16.7 62.5M21.7 59.6L21.7 66.2M20 62.5L23.4 62.5',
w: 0.45,
},
{
p: 'light',
d: 'M6.7 36.6L10.1 36.6L10.1 44L6.7 44ZM13.3 36.6L16.7 36.6L16.7 44L13.3 44ZM20 36.6L23.4 36.6L23.4 44L20 44ZM6.7 48.1L10.1 48.1L10.1 54.7L6.7 54.7ZM13.3 48.1L16.7 48.1L16.7 54.7L13.3 54.7ZM20 48.1L23.4 48.1L23.4 54.7L20 54.7ZM6.7 59.6L10.1 59.6L10.1 66.2L6.7 66.2ZM13.3 59.6L16.7 59.6L16.7 66.2L13.3 66.2ZM20 59.6L23.4 59.6L23.4 66.2L20 66.2Z',
o: 0.12,
},
{
p: 'paper',
d: 'M5.5 85.6L5.5 75.6C5.5 73.83 6.93 72.4 8.7 72.4C10.47 72.4 11.9 73.83 11.9 75.6L11.9 85.6ZM14 73L24 73L24 82L14 82Z',
},
{ p: '#2f4a3f', d: 'M6.1 85.6L6.1 75.6C6.1 74.16 7.26 73 8.7 73C10.14 73 11.3 74.16 11.3 75.6L11.3 85.6Z' },
{ p: '#2c3338', d: 'M14.6 73.6L23.4 73.6L23.4 81.4L14.6 81.4Z' },
{ p: 'paper', d: 'M19 73.6L19 81.4M14.6 77L23.3 77', w: 0.45 },
{ p: 'light', d: 'M8.7 76.5L8.7 85.4', o: 0.25, w: 0.4 },
{ p: '#8f8a84', d: 'M4.7 85L12.7 85L12.7 87.2L4.7 87.2Z' },
{
p: '#e7dac0',
d: 'M24 86.5L24 26L26.9 26L26.9 22.5L29.7 22.5L29.7 19L32.1 19L32.1 12L35.9 12L35.9 19L38.3 19L38.3 22.5L41.1 22.5L41.1 26L44 26L44 86.5Z',
},
{ p: 'shade', d: 'M42.4 26L44 26L44 86.5L42.4 86.5Z', o: 0.14 },
{ p: 'paper', d: 'M24.3 26.6L43.7 26.6L43.7 27.9L24.3 27.9Z' },
{ p: '#3a2f2a', d: 'M33.4 16.6L34.6 16.6L34.6 18.2L33.4 18.2Z' },
{ p: '#3a2f2a', d: 'M34 18L34 20.2', w: 0.35 },
{ p: 'paper', d: 'M32.2 20.2L35.8 20.2L35.8 24.6L32.2 24.6Z' },
{ p: '#2c3338', d: 'M32.9 20.9L35.1 20.9L35.1 23.9L32.9 23.9Z' },
{
p: 'paper',
d: 'M27.1 30L32.5 30L32.5 38.6L27.1 38.6ZM35.5 30L40.9 30L40.9 38.6L35.5 38.6ZM27.1 41.5L32.5 41.5L32.5 49.3L27.1 49.3ZM35.5 41.5L40.9 41.5L40.9 49.3L35.5 49.3ZM27.1 53L32.5 53L32.5 60.8L27.1 60.8ZM35.5 53L40.9 53L40.9 60.8L35.5 60.8ZM26.6 38.3L33 38.3L33 39.3L26.6 39.3ZM35 38.3L41.4 38.3L41.4 39.3L35 39.3ZM26.6 49L33 49L33 50L26.6 50ZM35 49L41.4 49L41.4 50L35 50ZM26.6 60.5L33 60.5L33 61.5L26.6 61.5ZM35 60.5L41.4 60.5L41.4 61.5L35 61.5Z',
},
{
p: '#2c3338',
d: 'M27.7 30.6L31.9 30.6L31.9 38L27.7 38ZM36.1 30.6L40.3 30.6L40.3 38L36.1 38ZM27.7 42.1L31.9 42.1L31.9 48.7L27.7 48.7ZM36.1 42.1L40.3 42.1L40.3 48.7L36.1 48.7ZM27.7 53.6L31.9 53.6L31.9 60.2L27.7 60.2ZM36.1 53.6L40.3 53.6L40.3 60.2L36.1 60.2Z',
},
{
p: 'paper',
d: 'M29.8 30.6L29.8 38M27.7 33.9L31.9 33.9M38.2 30.6L38.2 38M36.1 33.9L40.3 33.9M29.8 42.1L29.8 48.7M27.7 45L31.9 45M38.2 42.1L38.2 48.7M36.1 45L40.3 45M29.8 53.6L29.8 60.2M27.7 56.5L31.9 56.5M38.2 53.6L38.2 60.2M36.1 56.5L40.3 56.5',
w: 0.45,
},
{
p: 'light',
d: 'M27.7 30.6L31.9 30.6L31.9 38L27.7 38ZM36.1 30.6L40.3 30.6L40.3 38L36.1 38ZM27.7 42.1L31.9 42.1L31.9 48.7L27.7 48.7ZM36.1 42.1L40.3 42.1L40.3 48.7L36.1 48.7ZM27.7 53.6L31.9 53.6L31.9 60.2L27.7 60.2ZM36.1 53.6L40.3 53.6L40.3 60.2L36.1 60.2Z',
o: 0.12,
},
{
p: 'paper',
d: 'M35.1 85.6L35.1 75.6C35.1 73.83 36.53 72.4 38.3 72.4C40.07 72.4 41.5 73.83 41.5 75.6L41.5 85.6ZM27.1 73L33.1 73L33.1 82L27.1 82Z',
},
{ p: '#2f4a3f', d: 'M35.7 85.6L35.7 75.6C35.7 74.16 36.86 73 38.3 73C39.74 73 40.9 74.16 40.9 75.6L40.9 85.6Z' },
{ p: '#2c3338', d: 'M27.7 73.6L32.5 73.6L32.5 81.4L27.7 81.4Z' },
{ p: 'paper', d: 'M30.1 73.6L30.1 81.4M27.7 77L32.5 77', w: 0.45 },
{ p: 'light', d: 'M38.3 76.5L38.3 85.4', o: 0.25, w: 0.4 },
{ p: '#8f8a84', d: 'M34.3 85L42.3 85L42.3 87.2L34.3 87.2Z' },
{
p: 1,
d: 'M42 86.5L42 34C46.2 34 48.3 32 48.3 28L48.3 23L52.5 19L56.7 23L56.7 28C56.7 32 58.8 34 63 34L63 86.5Z',
},
{ p: 'shade', d: 'M61.4 34L63 34L63 86.5L61.4 86.5Z', o: 0.14 },
{
p: 'paper',
d: 'M42.8 34.6C46.47 34.2 48.3 32 48.3 28L48.3 23L52.5 19L56.7 23L56.7 28C56.7 32 58.8 34 63 34',
w: 0.9,
},
{ p: 'paper', d: 'M42.3 34.6L62.7 34.6L62.7 35.9L42.3 35.9Z' },
{ p: '#3a2f2a', d: 'M51.9 24.6L53.1 24.6L53.1 26.2L51.9 26.2Z' },
{ p: '#3a2f2a', d: 'M52.5 26L52.5 28.2', w: 0.35 },
{ p: 'paper', d: 'M50.7 28.2L54.3 28.2L54.3 32.6L50.7 32.6Z' },
{ p: '#2c3338', d: 'M51.4 28.9L53.6 28.9L53.6 31.9L51.4 31.9Z' },
{
p: 'paper',
d: 'M45.4 38L50.8 38L50.8 46.6L45.4 46.6ZM54.2 38L59.6 38L59.6 46.6L54.2 46.6ZM45.4 49.5L50.8 49.5L50.8 57.3L45.4 57.3ZM54.2 49.5L59.6 49.5L59.6 57.3L54.2 57.3ZM45.4 61L50.8 61L50.8 68.8L45.4 68.8ZM54.2 61L59.6 61L59.6 68.8L54.2 68.8ZM44.9 46.3L51.3 46.3L51.3 47.3L44.9 47.3ZM53.7 46.3L60.1 46.3L60.1 47.3L53.7 47.3ZM44.9 57L51.3 57L51.3 58L44.9 58ZM53.7 57L60.1 57L60.1 58L53.7 58ZM44.9 68.5L51.3 68.5L51.3 69.5L44.9 69.5ZM53.7 68.5L60.1 68.5L60.1 69.5L53.7 69.5Z',
},
{
p: '#2c3338',
d: 'M46 38.6L50.2 38.6L50.2 46L46 46ZM54.8 38.6L59 38.6L59 46L54.8 46ZM46 50.1L50.2 50.1L50.2 56.7L46 56.7ZM54.8 50.1L59 50.1L59 56.7L54.8 56.7ZM46 61.6L50.2 61.6L50.2 68.2L46 68.2ZM54.8 61.6L59 61.6L59 68.2L54.8 68.2Z',
},
{
p: 'paper',
d: 'M48.1 38.6L48.1 46M46 41.9L50.2 41.9M56.9 38.6L56.9 46M54.8 41.9L59 41.9M48.1 50.1L48.1 56.7M46 53L50.2 53M56.9 50.1L56.9 56.7M54.8 53L59 53M48.1 61.6L48.1 68.2M46 64.5L50.2 64.5M56.9 61.6L56.9 68.2M54.8 64.5L59 64.5',
w: 0.45,
},
{
p: 'light',
d: 'M46 38.6L50.2 38.6L50.2 46L46 46ZM54.8 38.6L59 38.6L59 46L54.8 46ZM46 50.1L50.2 50.1L50.2 56.7L46 56.7ZM54.8 50.1L59 50.1L59 56.7L54.8 56.7ZM46 61.6L50.2 61.6L50.2 68.2L46 68.2ZM54.8 61.6L59 61.6L59 68.2L54.8 68.2Z',
o: 0.12,
},
{
p: 'paper',
d: 'M44.8 85.6L44.8 75.6C44.8 73.83 46.23 72.4 48 72.4C49.77 72.4 51.2 73.83 51.2 75.6L51.2 85.6ZM53.6 73L59.6 73L59.6 82L53.6 82Z',
},
{ p: '#7a2e2a', d: 'M45.4 85.6L45.4 75.6C45.4 74.16 46.56 73 48 73C49.44 73 50.6 74.16 50.6 75.6L50.6 85.6Z' },
{ p: '#2c3338', d: 'M54.2 73.6L59 73.6L59 81.4L54.2 81.4Z' },
{ p: 'paper', d: 'M56.6 73.6L56.6 81.4M54.2 77L59 77', w: 0.45 },
{ p: 'light', d: 'M48 76.5L48 85.4', o: 0.25, w: 0.4 },
{ p: '#8f8a84', d: 'M44 85L52 85L52 87.2L44 87.2Z' },
{
p: 2,
d: 'M61 86.5L61 28L64.3 28L64.3 24.5L67.6 24.5L67.6 21L70.4 21L70.4 14L74.6 14L74.6 21L77.4 21L77.4 24.5L80.7 24.5L80.7 28L84 28L84 86.5Z',
},
{ p: 'shade', d: 'M82.4 28L84 28L84 86.5L82.4 86.5Z', o: 0.14 },
{ p: 'paper', d: 'M61.3 28.6L83.7 28.6L83.7 29.9L61.3 29.9Z' },
{ p: '#3a2f2a', d: 'M71.9 18.6L73.1 18.6L73.1 20.2L71.9 20.2Z' },
{ p: '#3a2f2a', d: 'M72.5 20L72.5 22.2', w: 0.35 },
{ p: 'paper', d: 'M70.7 22.2L74.3 22.2L74.3 26.6L70.7 26.6Z' },
{ p: '#2c3338', d: 'M71.4 22.9L73.6 22.9L73.6 25.9L71.4 25.9Z' },
{
p: 'paper',
d: 'M63.3 32L67.9 32L67.9 40.6L63.3 40.6ZM70.2 32L74.8 32L74.8 40.6L70.2 40.6ZM77.1 32L81.7 32L81.7 40.6L77.1 40.6ZM63.3 43.5L67.9 43.5L67.9 51.3L63.3 51.3ZM70.2 43.5L74.8 43.5L74.8 51.3L70.2 51.3ZM77.1 43.5L81.7 43.5L81.7 51.3L77.1 51.3ZM63.3 55L67.9 55L67.9 62.8L63.3 62.8ZM70.2 55L74.8 55L74.8 62.8L70.2 62.8ZM77.1 55L81.7 55L81.7 62.8L77.1 62.8ZM62.8 40.3L68.4 40.3L68.4 41.3L62.8 41.3ZM69.7 40.3L75.3 40.3L75.3 41.3L69.7 41.3ZM76.6 40.3L82.2 40.3L82.2 41.3L76.6 41.3ZM62.8 51L68.4 51L68.4 52L62.8 52ZM69.7 51L75.3 51L75.3 52L69.7 52ZM76.6 51L82.2 51L82.2 52L76.6 52ZM62.8 62.5L68.4 62.5L68.4 63.5L62.8 63.5ZM69.7 62.5L75.3 62.5L75.3 63.5L69.7 63.5ZM76.6 62.5L82.2 62.5L82.2 63.5L76.6 63.5Z',
},
{
p: '#2c3338',
d: 'M63.9 32.6L67.3 32.6L67.3 40L63.9 40ZM70.8 32.6L74.2 32.6L74.2 40L70.8 40ZM77.7 32.6L81.1 32.6L81.1 40L77.7 40ZM63.9 44.1L67.3 44.1L67.3 50.7L63.9 50.7ZM70.8 44.1L74.2 44.1L74.2 50.7L70.8 50.7ZM77.7 44.1L81.1 44.1L81.1 50.7L77.7 50.7ZM63.9 55.6L67.3 55.6L67.3 62.2L63.9 62.2ZM70.8 55.6L74.2 55.6L74.2 62.2L70.8 62.2ZM77.7 55.6L81.1 55.6L81.1 62.2L77.7 62.2Z',
},
{
p: 'paper',
d: 'M65.6 32.6L65.6 40M63.9 35.9L67.3 35.9M72.5 32.6L72.5 40M70.8 35.9L74.2 35.9M79.4 32.6L79.4 40M77.7 35.9L81.1 35.9M65.6 44.1L65.6 50.7M63.9 47L67.3 47M72.5 44.1L72.5 50.7M70.8 47L74.2 47M79.4 44.1L79.4 50.7M77.7 47L81.1 47M65.6 55.6L65.6 62.2M63.9 58.5L67.3 58.5M72.5 55.6L72.5 62.2M70.8 58.5L74.2 58.5M79.4 55.6L79.4 62.2M77.7 58.5L81.1 58.5',
w: 0.45,
},
{
p: 'light',
d: 'M63.9 32.6L67.3 32.6L67.3 40L63.9 40ZM70.8 32.6L74.2 32.6L74.2 40L70.8 40ZM77.7 32.6L81.1 32.6L81.1 40L77.7 40ZM63.9 44.1L67.3 44.1L67.3 50.7L63.9 50.7ZM70.8 44.1L74.2 44.1L74.2 50.7L70.8 50.7ZM77.7 44.1L81.1 44.1L81.1 50.7L77.7 50.7ZM63.9 55.6L67.3 55.6L67.3 62.2L63.9 62.2ZM70.8 55.6L74.2 55.6L74.2 62.2L70.8 62.2ZM77.7 55.6L81.1 55.6L81.1 62.2L77.7 62.2Z',
o: 0.12,
},
{
p: 'paper',
d: 'M75.9 85.6L75.9 75.6C75.9 73.83 77.33 72.4 79.1 72.4C80.87 72.4 82.3 73.83 82.3 75.6L82.3 85.6ZM63.3 73L73.3 73L73.3 82L63.3 82Z',
},
{ p: '#2f4a3f', d: 'M76.5 85.6L76.5 75.6C76.5 74.16 77.66 73 79.1 73C80.54 73 81.7 74.16 81.7 75.6L81.7 85.6Z' },
{ p: '#2c3338', d: 'M63.9 73.6L72.7 73.6L72.7 81.4L63.9 81.4Z' },
{ p: 'paper', d: 'M68.3 73.6L68.3 81.4M63.9 77L72.7 77', w: 0.45 },
{ p: 'light', d: 'M79.1 76.5L79.1 85.4', o: 0.25, w: 0.4 },
{ p: '#8f8a84', d: 'M75.1 85L83.1 85L83.1 87.2L75.1 87.2Z' },
{
p: '#7f3f2c',
d: 'M82 86.5L82 38C87.3 38 89.7 35 89.7 28C92.57 24.33 95.43 24.33 98.3 28C98.3 35 100.7 38 106 38L106 86.5Z',
},
{ p: 'shade', d: 'M104.4 38L106 38L106 86.5L104.4 86.5Z', o: 0.14 },
{
p: 'paper',
d: 'M82.8 38.6C87.3 38 89.7 35 89.7 28C92.57 24.33 95.43 24.33 98.3 28C98.3 35 100.7 38 106 38',
w: 0.9,
},
{ p: 'paper', d: 'M82.3 38.6L105.7 38.6L105.7 39.9L82.3 39.9Z' },
{ p: '#3a2f2a', d: 'M93.4 28.6L94.6 28.6L94.6 30.2L93.4 30.2Z' },
{ p: '#3a2f2a', d: 'M94 30L94 32.2', w: 0.35 },
{ p: 'paper', d: 'M92.2 32.2L95.8 32.2L95.8 36.6L92.2 36.6Z' },
{ p: '#2c3338', d: 'M92.9 32.9L95.1 32.9L95.1 35.9L92.9 35.9Z' },
{
p: 'paper',
d: 'M84.6 42L89.1 42L89.1 50.6L84.6 50.6ZM91.7 42L96.3 42L96.3 50.6L91.7 50.6ZM98.9 42L103.5 42L103.5 50.6L98.9 50.6ZM84.6 53.5L89.1 53.5L89.1 61.3L84.6 61.3ZM91.7 53.5L96.3 53.5L96.3 61.3L91.7 61.3ZM98.9 53.5L103.5 53.5L103.5 61.3L98.9 61.3ZM84.1 50.3L89.6 50.3L89.6 51.3L84.1 51.3ZM91.2 50.3L96.8 50.3L96.8 51.3L91.2 51.3ZM98.4 50.3L104 50.3L104 51.3L98.4 51.3ZM84.1 61L89.6 61L89.6 62L84.1 62ZM91.2 61L96.8 61L96.8 62L91.2 62ZM98.4 61L104 61L104 62L98.4 62Z',
},
{
p: '#2c3338',
d: 'M85.1 42.6L88.6 42.6L88.6 50L85.1 50ZM92.3 42.6L95.7 42.6L95.7 50L92.3 50ZM99.4 42.6L102.9 42.6L102.9 50L99.4 50ZM85.1 54.1L88.6 54.1L88.6 60.7L85.1 60.7ZM92.3 54.1L95.7 54.1L95.7 60.7L92.3 60.7ZM99.4 54.1L102.9 54.1L102.9 60.7L99.4 60.7Z',
},
{
p: 'paper',
d: 'M86.9 42.6L86.9 50M85.1 45.9L88.6 45.9M94 42.6L94 50M92.3 45.9L95.7 45.9M101.1 42.6L101.1 50M99.4 45.9L102.9 45.9M86.9 54.1L86.9 60.7M85.1 57L88.6 57M94 54.1L94 60.7M92.3 57L95.7 57M101.1 54.1L101.1 60.7M99.4 57L102.9 57',
w: 0.45,
},
{
p: 'light',
d: 'M85.1 42.6L88.6 42.6L88.6 50L85.1 50ZM92.3 42.6L95.7 42.6L95.7 50L92.3 50ZM99.4 42.6L102.9 42.6L102.9 50L99.4 50ZM85.1 54.1L88.6 54.1L88.6 60.7L85.1 60.7ZM92.3 54.1L95.7 54.1L95.7 60.7L92.3 60.7ZM99.4 54.1L102.9 54.1L102.9 60.7L99.4 60.7Z',
o: 0.12,
},
{
p: 'paper',
d: 'M84 85.6L84 75.6C84 73.83 85.43 72.4 87.2 72.4C88.97 72.4 90.4 73.83 90.4 75.6L90.4 85.6ZM93.5 73L103.5 73L103.5 82L93.5 82Z',
},
{ p: '#2f4a3f', d: 'M84.6 85.6L84.6 75.6C84.6 74.16 85.76 73 87.2 73C88.64 73 89.8 74.16 89.8 75.6L89.8 85.6Z' },
{ p: '#2c3338', d: 'M94.1 73.6L102.9 73.6L102.9 81.4L94.1 81.4Z' },
{ p: 'paper', d: 'M98.5 73.6L98.5 81.4M94.1 77L102.9 77', w: 0.45 },
{ p: 'light', d: 'M87.1 76.5L87.1 85.4', o: 0.25, w: 0.4 },
{ p: '#8f8a84', d: 'M83.1 85L91.1 85L91.1 87.2L83.1 87.2Z' },
{ p: 'shade', d: 'M58 96.6C66.67 97.4 75.33 97.4 84 96.6', o: 0.18, w: 1.2 },
{ p: '#2f3b3f', d: 'M56.5 91.6L86.5 91.6C86 94.6 83.6 96.4 80 96.4L62.4 96.4C59 96.4 57 94.6 56.5 91.6Z' },
{ p: 2, d: 'M56.5 91.6L86.5 91.6L86.5 93L56.5 93Z' },
{ p: 'paper', d: 'M66 88.6L78 88.6L78 92.2L66 92.2Z' },
{
p: '#2c3338',
d: 'M67 89.4L69.8 89.4L69.8 91.2L67 91.2ZM70.6 89.4L73.4 89.4L73.4 91.2L70.6 91.2ZM74.2 89.4L77 89.4L77 91.2L74.2 91.2Z',
},
{ p: 'light', d: 'M60 94.4L83 94.4', o: 0.25, w: 0.5 },
],
license: 'in-house',
},
];
@@ -900,4 +900,331 @@ export const MEDITERRANEAN_STICKERS: readonly BookStickerDef[] = [
],
license: 'in-house',
},
{
id: 'gondola',
theme: 'mediterranean',
tags: ['gondola', 'venice', 'italy', 'gondolier', 'canal', 'boat', 'lagoon', 'romantic'],
descr:
'A black Venetian gondola with a silver ferro on its bow and a red seat, rowed by a gondolier in a striped shirt and straw boater on green blue water.',
box: [110, 90],
slots: [
{ name: 'hull', color: '#24242c' },
{ name: 'water', color: '#2c9c94' },
{ name: 'stripes', color: '#22407e' },
{ name: 'straw', color: '#e6c271' },
],
layers: [
{
p: 1,
d: 'M3 76C10 73.6 17 73.6 24 76C31 78.4 38 78.4 45 76C52 73.6 59 73.6 66 76C73 78.4 80 78.4 87 76C94 73.6 101 73.6 107 76L107 84C107 86.2 105.2 88 103 88L7 88C4.8 88 3 86.2 3 84Z',
},
{ p: 'shade', d: 'M16 79.5C30 81.5 80 81.5 94 79.5C90 83 22 83 16 79.5Z', o: 0.25 },
{ p: 'light', d: 'M10 84L22 84M30 85.5L44 85.5M68 85L82 85M90 83.5L99 83.5', w: 1.1, o: 0.45 },
{
p: 0,
d: 'M4.6 44.5C6 60 15 70 30 71.5L80 71.5C92 70.5 99 61 100.5 45L103.5 45.5C103.5 63 96 78.5 78 79.5L32 79.5C14 79 4 66 2 45C2 43.4 4.4 43 4.6 44.5Z',
},
{
p: 'shade',
d: 'M7 66C14 75 22 79 32 79.5L78 79.5C90 79 97 73 100.6 64C96 72 88 75.5 78 76L32 76C20 75.6 12 72 7 66Z',
o: 0.3,
},
{ p: '#e2b04a', d: 'M8 59C14 66 21 70 31 71.5M80 71.5C90 70.6 96 64 99.3 55', w: 0.9 },
{ p: 0, d: 'M40 72.5L40 66C40 64.5 41 63.5 42.5 63.5L44 63.5L44 72.5Z' },
{
p: '#b3283a',
d: 'M44.7 64.6L56.3 64.6C56.88 64.6 57.44 64.83 57.86 65.24C58.27 65.66 58.5 66.22 58.5 66.8L58.5 70C58.5 70.58 58.27 71.14 57.86 71.56C57.44 71.97 56.88 72.2 56.3 72.2L44.7 72.2C44.12 72.2 43.56 71.97 43.14 71.56C42.73 71.14 42.5 70.58 42.5 70L42.5 66.8C42.5 66.22 42.73 65.66 43.14 65.24C43.56 64.83 44.12 64.6 44.7 64.6Z',
},
{ p: 'light', d: 'M44.8 66.6L56.2 66.6', w: 1, o: 0.4 },
{ p: 0, d: 'M57 72.5L57 64.5C57 63.5 57.6 62.8 58.6 62.8C59.8 62.8 60.6 63.6 60.6 64.8L60.6 72.5Z' },
{
p: '#e2b04a',
d: 'M57.5 62.6C57.5 63.32 58.08 63.9 58.8 63.9C59.52 63.9 60.1 63.32 60.1 62.6C60.1 61.88 59.52 61.3 58.8 61.3C58.08 61.3 57.5 61.88 57.5 62.6Z',
},
{ p: '#e2b04a', d: 'M68 74.4C70 72.6 72.6 72.6 74.6 74.4C72.6 76.2 70 76.2 68 74.4Z' },
{ p: '#e2b04a', d: 'M3.4 47.6L5.2 47.4', w: 1.2 },
{
p: '#cfd5dc',
d: 'M97 52.5L97.6 34C97.9 30 100.8 28 103.8 29.4L105.6 30.6L102.4 31.6L102.4 34.2L105.6 34.2L105.6 36.2L102.4 36.2L102.4 38.6L105.6 38.6L105.6 40.6L102.4 40.6L102.4 43L105.6 43L105.6 45L102.4 45L102.4 47.4L105.6 47.4L105.6 49.4L102.2 49.4L101.8 52.5Z',
},
{ p: 'shade', d: 'M100.2 31L102.4 31.6L102.4 49.4L102.2 49.4L101.8 52.5L100 52.5Z', o: 0.18 },
{ p: 'light', d: 'M98.9 36L98.6 50', w: 0.8, o: 0.6 },
{ p: '#b98048', d: 'M30.5 36L66 82', w: 2 },
{
p: '#b98048',
d: 'M60.6 74.4C62.4 73.4 64.6 74 66.6 76.4L71 82.4C71.8 83.8 70.6 85.2 69 84.6C66.6 83.6 64.2 81.2 62.2 78.8Z',
},
{ p: 1, d: 'M58 80.6C62 79.6 66 79.6 70 80.6C74 81.6 78 81.6 81 80.6L81 86L58 86Z', o: 0.75 },
{ p: 'light', d: 'M60 81.6C64 80.8 68 80.8 72 81.8', w: 0.9, o: 0.6 },
{ p: 0, d: 'M19.5 49.5L29.2 49.5L29 60L28.4 70.5L25.4 70.5L24.6 57L23.6 70.5L20.6 70.5L19.6 59Z' },
{ p: 'light', d: 'M24.4 51.5L24.5 56.5', w: 0.6, o: 0.25 },
{ p: 0, d: 'M19.6 69.2L24.4 69.2C25 69.2 25.2 70 24.8 70.6L24.2 72.8L19.4 72.8Z' },
{ p: 0, d: 'M25 69.2L30 69.2C30.8 69.2 31 70.2 30.4 70.8L29.8 72.8L25 72.8Z' },
{ p: '#fbf8f0', d: 'M18.6 36.4C18.6 33.6 20.2 32 23 32L25.4 32C28 32 29.6 33.6 29.6 36.4L29.8 51L18.4 51Z' },
{
p: 2,
d: 'M18.5 35.4L29.8 35.4L29.8 36.9L18.5 36.9ZM18.5 38.5L29.8 38.5L29.8 40L18.5 40ZM18.5 41.6L29.8 41.6L29.8 43.1L18.5 43.1ZM18.5 44.7L29.8 44.7L29.8 46.2L18.5 46.2ZM18.5 47.8L29.8 47.8L29.8 49.3L18.5 49.3Z',
},
{ p: 'shade', d: 'M18.6 36.4C18.6 33.6 20.2 32 23 32L22 32C21 34 21 42 21.6 51L18.4 51Z', o: 0.12 },
{ p: 0, d: 'M18.3 49.6L29.9 49.6L29.9 51.4L18.3 51.4Z' },
{ p: '#efbf98', d: 'M22.6 28.8L25.6 28.8L25.6 33L22.6 33Z' },
{
p: '#efbf98',
d: 'M20.1 26.2C20.1 28.46 21.94 30.3 24.2 30.3C26.46 30.3 28.3 28.46 28.3 26.2C28.3 23.94 26.46 22.1 24.2 22.1C21.94 22.1 20.1 23.94 20.1 26.2Z',
},
{ p: '#efbf98', d: 'M28 25.6L29.3 27.3L28 27.9Z' },
{ p: '#4a3022', d: 'M20.2 25.6C20 23.6 20.6 22.6 21.6 22.2L22 26.8C21.2 27.6 20.4 27 20.2 25.6Z' },
{
p: 'ink',
d: 'M26 25.6C26 25.81 26.11 26.01 26.3 26.12C26.49 26.23 26.71 26.23 26.9 26.12C27.09 26.01 27.2 25.81 27.2 25.6C27.2 25.39 27.09 25.19 26.9 25.08C26.71 24.97 26.49 24.97 26.3 25.08C26.11 25.19 26 25.39 26 25.6Z',
},
{
p: '#d9826b',
d: 'M25.5 27.8C25.5 28.05 25.63 28.28 25.85 28.41C26.07 28.53 26.33 28.53 26.55 28.41C26.77 28.28 26.9 28.05 26.9 27.8C26.9 27.55 26.77 27.32 26.55 27.19C26.33 27.07 26.07 27.07 25.85 27.19C25.63 27.32 25.5 27.55 25.5 27.8Z',
o: 0.5,
},
{ p: 3, d: 'M19.8 22.8L20.2 18.4C20.3 17.4 21 16.8 22 16.8L26.6 16.8C27.6 16.8 28.3 17.4 28.4 18.4L28.8 22.8Z' },
{ p: 2, d: 'M20 20.2L28.6 20.2L28.8 22L19.8 22Z' },
{
p: 3,
d: 'M15.9 22.9C15.9 23.78 19.66 24.5 24.3 24.5C28.94 24.5 32.7 23.78 32.7 22.9C32.7 22.02 28.94 21.3 24.3 21.3C19.66 21.3 15.9 22.02 15.9 22.9Z',
},
{
p: 'shade',
d: 'M16.4 23.2C18 24.2 30.6 24.2 32.2 23.2C31 24.3 27.5 24.5 24.3 24.5C21 24.5 17.6 24.3 16.4 23.2Z',
o: 0.25,
},
{ p: 'light', d: 'M21.4 18.2L27.2 18.2', w: 0.7, o: 0.5 },
{ p: '#e3ae86', d: 'M21 36.4C21.6 41 27 43.6 35 43', w: 2.4 },
{ p: '#fbf8f0', d: 'M27.4 34.4L30 39.4', w: 3 },
{ p: 2, d: 'M27.2 35.6L29.6 34.6M28.6 38.2L30.8 37.2', w: 1 },
{ p: '#efbf98', d: 'M30.2 40.2L33.4 39.4', w: 2.4 },
{
p: '#efbf98',
d: 'M31.8 39.6C31.8 40.17 32.1 40.7 32.6 40.99C33.1 41.27 33.7 41.27 34.2 40.99C34.7 40.7 35 40.17 35 39.6C35 39.03 34.7 38.5 34.2 38.21C33.7 37.93 33.1 37.93 32.6 38.21C32.1 38.5 31.8 39.03 31.8 39.6Z',
},
{
p: '#e3ae86',
d: 'M33.8 42.6C33.8 43.48 34.52 44.2 35.4 44.2C36.28 44.2 37 43.48 37 42.6C37 41.72 36.28 41 35.4 41C34.52 41 33.8 41.72 33.8 42.6Z',
},
],
license: 'in-house',
},
{
id: 'lisbonTram',
theme: 'mediterranean',
tags: ['tram', 'lisbon', 'portugal', 'streetcar', 'yellow', 'alfama', 'transport', 'trolley'],
descr:
'The yellow Lisbon tram 28 in side view, a rounded body with a cream window band and framed windows, its trolley pole reaching the overhead cable.',
box: [110, 90],
slots: [
{ name: 'body', color: '#f6c31c' },
{ name: 'cream', color: '#f6efdd' },
{ name: 'frame', color: '#3d4048' },
{ name: 'glass', color: '#8fb7c8' },
],
layers: [
{ p: 'ink', d: 'M2 6.5L108 6.5', w: 0.8 },
{ p: 2, d: 'M38 22L72 7.2', w: 1.5 },
{ p: 2, d: 'M70 6.2L74.6 5.6L75 8L71.6 8.8Z' },
{ p: 2, d: 'M34 25L34 21.6C34 20.6 34.8 20 35.8 20L41 20C42 20 42.6 20.8 42.4 21.8L42 25Z' },
{ p: 'ink', d: 'M4 87.2L106 87.2', w: 2 },
{ p: 2, d: 'M20 70L90 70L90 79C90 80.2 89.2 81 88 81L22 81C20.8 81 20 80.2 20 79Z' },
{
p: 2,
d: 'M23.6 81.4C23.6 83.69 24.82 85.8 26.8 86.94C28.78 88.09 31.22 88.09 33.2 86.94C35.18 85.8 36.4 83.69 36.4 81.4C36.4 79.11 35.18 77 33.2 75.86C31.22 74.71 28.78 74.71 26.8 75.86C24.82 77 23.6 79.11 23.6 81.4ZM73.6 81.4C73.6 84.93 76.47 87.8 80 87.8C83.53 87.8 86.4 84.93 86.4 81.4C86.4 77.87 83.53 75 80 75C76.47 75 73.6 77.87 73.6 81.4Z',
},
{
p: 'light',
d: 'M27.6 81.4C27.6 82.26 28.06 83.05 28.8 83.48C29.54 83.91 30.46 83.91 31.2 83.48C31.94 83.05 32.4 82.26 32.4 81.4C32.4 80.54 31.94 79.75 31.2 79.32C30.46 78.89 29.54 78.89 28.8 79.32C28.06 79.75 27.6 80.54 27.6 81.4ZM77.6 81.4C77.6 82.73 78.67 83.8 80 83.8C81.33 83.8 82.4 82.73 82.4 81.4C82.4 80.07 81.33 79 80 79C78.67 79 77.6 80.07 77.6 81.4Z',
o: 0.4,
},
{ p: 'shade', d: 'M42 77L68 77L68 80.6L42 80.6Z', o: 0.3 },
{ p: 1, d: 'M16 32L16 27.5C16 24.6 18 23 21 23L89 23C92 23 94 24.6 94 27.5L94 32Z' },
{ p: 'shade', d: 'M16 29.5L94 29.5L94 32L16 32Z', o: 0.15 },
{ p: 'shade', d: 'M28 23.2L72 23.2L72 25.4L28 25.4Z', o: 0.12 },
{
p: 0,
d: 'M11 41C11 34 15 30 22 30L88 30C95 30 99 34 99 41L99 70.5C99 72.6 97.6 74 95.5 74L14.5 74C12.4 74 11 72.6 11 70.5Z',
},
{ p: 1, d: 'M11 41C11 34 15 30 22 30L88 30C95 30 99 34 99 41L99 54L11 54Z' },
{
p: 3,
d: 'M21 50.5L21 38.6C21 37 22 36 23.4 36L27.8 36C29.2 36 30.2 37 30.2 38.6L30.2 50.5ZM32.6 50.5L32.6 38.6C32.6 37 33.6 36 35 36L39.4 36C40.8 36 41.8 37 41.8 38.6L41.8 50.5ZM44.2 50.5L44.2 38.6C44.2 37 45.2 36 46.6 36L51 36C52.4 36 53.4 37 53.4 38.6L53.4 50.5ZM55.8 50.5L55.8 38.6C55.8 37 56.8 36 58.2 36L62.6 36C64 36 65 37 65 38.6L65 50.5ZM67.4 50.5L67.4 38.6C67.4 37 68.4 36 69.8 36L74.2 36C75.6 36 76.6 37 76.6 38.6L76.6 50.5ZM79 50.5L79 38.6C79 37 80 36 81.4 36L85.8 36C87.2 36 88.2 37 88.2 38.6L88.2 50.5ZM91 50.5L91 36L93.5 36C95.8 36.2 97 38 97 40.6L97 50.5ZM19 50.5L19 36L16.5 36C14.2 36.2 13 38 13 40.6L13 50.5Z',
},
{
p: 'light',
d: 'M23 47L27.5 38.8M34.6 47L39.1 38.8M46.2 47L50.7 38.8M57.8 47L62.3 38.8M69.4 47L73.9 38.8M81 47L85.5 38.8',
w: 1.2,
o: 0.45,
},
{ p: 'shade', d: 'M13 49L97 49L97 50.5L13 50.5Z', o: 0.12 },
{
p: 'shade',
d: 'M92 30.4C95.8 31.6 99 35 99 41L99 70.5C99 72.6 97.6 74 95.5 74L93 74C94.6 60 94.4 42 92 30.4Z',
o: 0.1,
},
{
p: 1,
d: 'M21 43.4L30.2 43.4M32.6 43.4L41.8 43.4M44.2 43.4L53.4 43.4M55.8 43.4L65 43.4M67.4 43.4L76.6 43.4M79 43.4L88.2 43.4',
w: 0.9,
},
{ p: 2, d: 'M11.6 54.2L98.4 54.2', w: 1.3 },
{ p: 2, d: 'M11.6 67.2L98.4 67.2', w: 0.9 },
{
p: 'shade',
d: 'M22.2 57.4L29 57.4C29.32 57.4 29.62 57.53 29.85 57.75C30.07 57.98 30.2 58.28 30.2 58.6L30.2 63.2C30.2 63.52 30.07 63.82 29.85 64.05C29.62 64.27 29.32 64.4 29 64.4L22.2 64.4C21.88 64.4 21.58 64.27 21.35 64.05C21.13 63.82 21 63.52 21 63.2L21 58.6C21 58.28 21.13 57.98 21.35 57.75C21.58 57.53 21.88 57.4 22.2 57.4ZM33.8 57.4L40.6 57.4C41.26 57.4 41.8 57.94 41.8 58.6L41.8 63.2C41.8 63.86 41.26 64.4 40.6 64.4L33.8 64.4C33.14 64.4 32.6 63.86 32.6 63.2L32.6 58.6C32.6 57.94 33.14 57.4 33.8 57.4ZM45.4 57.4L52.2 57.4C52.86 57.4 53.4 57.94 53.4 58.6L53.4 63.2C53.4 63.86 52.86 64.4 52.2 64.4L45.4 64.4C44.74 64.4 44.2 63.86 44.2 63.2L44.2 58.6C44.2 57.94 44.74 57.4 45.4 57.4ZM57 57.4L63.8 57.4C64.12 57.4 64.42 57.53 64.65 57.75C64.87 57.98 65 58.28 65 58.6L65 63.2C65 63.52 64.87 63.82 64.65 64.05C64.42 64.27 64.12 64.4 63.8 64.4L57 64.4C56.68 64.4 56.38 64.27 56.15 64.05C55.93 63.82 55.8 63.52 55.8 63.2L55.8 58.6C55.8 58.28 55.93 57.98 56.15 57.75C56.38 57.53 56.68 57.4 57 57.4ZM68.6 57.4L75.4 57.4C76.06 57.4 76.6 57.94 76.6 58.6L76.6 63.2C76.6 63.86 76.06 64.4 75.4 64.4L68.6 64.4C67.94 64.4 67.4 63.86 67.4 63.2L67.4 58.6C67.4 57.94 67.94 57.4 68.6 57.4ZM80.2 57.4L87 57.4C87.32 57.4 87.62 57.53 87.85 57.75C88.07 57.98 88.2 58.28 88.2 58.6L88.2 63.2C88.2 63.52 88.07 63.82 87.85 64.05C87.62 64.27 87.32 64.4 87 64.4L80.2 64.4C79.88 64.4 79.58 64.27 79.35 64.05C79.13 63.82 79 63.52 79 63.2L79 58.6C79 58.28 79.13 57.98 79.35 57.75C79.58 57.53 79.88 57.4 80.2 57.4Z',
o: 0.1,
},
{ p: 2, d: 'M91.2 55.6L96.4 55.6L96.4 72.6L91.2 72.6Z', o: 0.55 },
{ p: 2, d: 'M13.6 55.6L18.8 55.6L18.8 72.6L13.6 72.6Z', o: 0.55 },
{ p: 'shade', d: 'M11 68L99 68L99 70.5C99 72.6 97.6 74 95.5 74L14.5 74C12.4 74 11 72.6 11 70.5Z', o: 0.18 },
{ p: 'light', d: 'M20 32.4L90 32.4', w: 1, o: 0.6 },
{
p: '#fff4c2',
d: 'M96 60.5C96 61.38 96.72 62.1 97.6 62.1C98.48 62.1 99.2 61.38 99.2 60.5C99.2 59.62 98.48 58.9 97.6 58.9C96.72 58.9 96 59.62 96 60.5Z',
},
{ p: 2, d: 'M89.6 72.6L98.2 72.6L98.2 76.4L89.6 76.4ZM11.8 72.6L20.4 72.6L20.4 76.4L11.8 76.4Z' },
{ p: 1, d: 'M90.2 56.4L90.2 71M19.8 56.4L19.8 71', w: 0.8 },
{
p: 2,
d: 'M53 20.4L57 20.4C57.55 20.4 58 20.85 58 21.4L58 22.8C58 23.35 57.55 23.8 57 23.8L53 23.8C52.45 23.8 52 23.35 52 22.8L52 21.4C52 20.85 52.45 20.4 53 20.4Z',
},
{
p: '#fff4c2',
d: 'M54.1 21.6C54.1 21.92 54.27 22.22 54.55 22.38C54.83 22.54 55.17 22.54 55.45 22.38C55.73 22.22 55.9 21.92 55.9 21.6C55.9 21.28 55.73 20.98 55.45 20.82C55.17 20.66 54.83 20.66 54.55 20.82C54.27 20.98 54.1 21.28 54.1 21.6Z',
},
{
p: 2,
d: 'M79.2 24.6L89.8 24.6C90.12 24.6 90.42 24.73 90.65 24.95C90.87 25.18 91 25.48 91 25.8L91 30C91 30.32 90.87 30.62 90.65 30.85C90.42 31.07 90.12 31.2 89.8 31.2L79.2 31.2C78.88 31.2 78.58 31.07 78.35 30.85C78.13 30.62 78 30.32 78 30L78 25.8C78 25.48 78.13 25.18 78.35 24.95C78.58 24.73 78.88 24.6 79.2 24.6Z',
},
{
p: '#fbf8f0',
d: 'M80.6 26.6C80.8 25.8 83.6 25.6 83.6 27C83.6 28.2 80.6 28.6 80.6 29.8L83.8 29.8M86.8 27.8C85.4 27.8 85.4 25.9 86.8 25.9C88.2 25.9 88.2 27.8 86.8 27.8C85.2 27.8 85.2 29.9 86.8 29.9C88.4 29.9 88.4 27.8 86.8 27.8',
w: 0.75,
},
{
p: 2,
d: 'M98.4 68.6L100.4 68.6C100.95 68.6 101.4 69.05 101.4 69.6L101.4 70.8C101.4 71.35 100.95 71.8 100.4 71.8L98.4 71.8C97.85 71.8 97.4 71.35 97.4 70.8L97.4 69.6C97.4 69.05 97.85 68.6 98.4 68.6ZM9.6 68.6L11.6 68.6C12.15 68.6 12.6 69.05 12.6 69.6L12.6 70.8C12.6 71.35 12.15 71.8 11.6 71.8L9.6 71.8C9.05 71.8 8.6 71.35 8.6 70.8L8.6 69.6C8.6 69.05 9.05 68.6 9.6 68.6Z',
},
],
license: 'in-house',
},
{
id: 'camel',
theme: 'mediterranean',
tags: ['camel', 'dromedary', 'morocco', 'desert', 'sahara', 'road trip', 'animal', 'caravan'],
descr:
'A dromedary camel standing in profile facing left, its single hump draped in a red saddle blanket with gold trim, teal diamonds and tassels, wearing a red halter.',
box: [106, 90],
slots: [
{ name: 'fur', color: '#d6a467' },
{ name: 'blanket', color: '#c8233a' },
{ name: 'trim', color: '#f0b740' },
{ name: 'pattern', color: '#1f8a8a' },
],
layers: [
{
p: 'shade',
d: 'M26 86.2C26 87.42 43.01 88.4 64 88.4C84.99 88.4 102 87.42 102 86.2C102 84.98 84.99 84 64 84C43.01 84 26 84.98 26 86.2Z',
o: 0.12,
},
{
p: 0,
d: 'M53.8 52L54.6 71C53.4 73.6 54 75.6 54.8 77L54.6 84L59.6 84L59.2 77C60 75.6 60.6 73.6 59.4 71L60.2 52ZM53.2 84.2C53.2 84.88 54.04 85.51 55.4 85.85C56.76 86.18 58.44 86.18 59.8 85.85C61.16 85.51 62 84.88 62 84.2C62 83.52 61.16 82.89 59.8 82.55C58.44 82.22 56.76 82.22 55.4 82.55C54.04 82.89 53.2 83.52 53.2 84.2ZM76.8 52L77.6 71C76.4 73.6 77 75.6 77.8 77L77.6 84L82.6 84L82.2 77C83 75.6 83.6 73.6 82.4 71L83.2 52ZM76.2 84.2C76.2 84.88 77.04 85.51 78.4 85.85C79.76 86.18 81.44 86.18 82.8 85.85C84.16 85.51 85 84.88 85 84.2C85 83.52 84.16 82.89 82.8 82.55C81.44 82.22 79.76 82.22 78.4 82.55C77.04 82.89 76.2 83.52 76.2 84.2Z',
},
{
p: 'shade',
d: 'M53.8 52L54.6 71C53.4 73.6 54 75.6 54.8 77L54.6 84L59.6 84L59.2 77C60 75.6 60.6 73.6 59.4 71L60.2 52ZM53.2 84.2C53.2 84.88 54.04 85.51 55.4 85.85C56.76 86.18 58.44 86.18 59.8 85.85C61.16 85.51 62 84.88 62 84.2C62 83.52 61.16 82.89 59.8 82.55C58.44 82.22 56.76 82.22 55.4 82.55C54.04 82.89 53.2 83.52 53.2 84.2ZM76.8 52L77.6 71C76.4 73.6 77 75.6 77.8 77L77.6 84L82.6 84L82.2 77C83 75.6 83.6 73.6 82.4 71L83.2 52ZM76.2 84.2C76.2 84.88 77.04 85.51 78.4 85.85C79.76 86.18 81.44 86.18 82.8 85.85C84.16 85.51 85 84.88 85 84.2C85 83.52 84.16 82.89 82.8 82.55C81.44 82.22 79.76 82.22 78.4 82.55C77.04 82.89 76.2 83.52 76.2 84.2Z',
o: 0.3,
},
{ p: 0, d: 'M98 44C101.6 50 102 56 100 62', w: 1.8 },
{ p: 0, d: 'M100 60.6C98.6 63.6 99.4 67 100.6 68C101.6 66.4 102.6 63.4 101.6 60.4Z' },
{ p: 'shade', d: 'M100 60.6C98.6 63.6 99.4 67 100.6 68C101.6 66.4 102.6 63.4 101.6 60.4Z', o: 0.25 },
{
p: 0,
d: 'M36 46C38 36 48 32 56 31C62 15 80 14 88 28C96 31 101 39 99 49C97 56 91 59 83 59L46 59C39 59 35 54 36 46Z',
},
{
p: 0,
d: 'M43.8 52L44.6 71C43.4 73.6 44 75.6 44.8 77L44.6 84L49.6 84L49.2 77C50 75.6 50.6 73.6 49.4 71L50.2 52ZM85.8 52L86.6 71C85.4 73.6 86 75.6 86.8 77L86.6 84L91.6 84L91.2 77C92 75.6 92.6 73.6 91.4 71L92.2 52Z',
},
{
p: 0,
d: 'M43.2 84.2C43.2 84.88 44.04 85.51 45.4 85.85C46.76 86.18 48.44 86.18 49.8 85.85C51.16 85.51 52 84.88 52 84.2C52 83.52 51.16 82.89 49.8 82.55C48.44 82.22 46.76 82.22 45.4 82.55C44.04 82.89 43.2 83.52 43.2 84.2ZM85.2 84.2C85.2 84.88 86.04 85.51 87.4 85.85C88.76 86.18 90.44 86.18 91.8 85.85C93.16 85.51 94 84.88 94 84.2C94 83.52 93.16 82.89 91.8 82.55C90.44 82.22 88.76 82.22 87.4 82.55C86.04 82.89 85.2 83.52 85.2 84.2Z',
},
{
p: 'shade',
d: 'M44.6 72C45.4 73.2 47.4 73.6 49.4 72.4M86.6 72C87.4 73.2 89.4 73.6 91.4 72.4',
w: 0.8,
o: 0.35,
},
{ p: 'shade', d: 'M43.6 84.6L50.6 84.6M85.6 84.6L92.6 84.6', w: 0.8, o: 0.3 },
{
p: 'shade',
d: 'M37 50C40 56 44 58.4 50 58.4L84 58.4C90 58.4 95 57 97.6 53C95 57.4 90 59 83 59L46 59C41.4 59 38 55.4 37 50Z',
o: 0.25,
},
{
p: 0,
d: 'M48 53C38 57 27 54 23.4 44C21 37.6 20 31 15.4 24.6L26 19.4C27.6 27 29.6 35 33.6 39.4C37.6 43.4 42 42 45 37Z',
},
{ p: 'shade', d: 'M24 45C27.6 52.6 36 56 45.6 53.8C38 57.4 27.4 54.6 24 45Z', o: 0.2 },
{
p: 0,
d: 'M22.7 12.6C22.7 14.15 23.55 15.4 24.6 15.4C25.65 15.4 26.5 14.15 26.5 12.6C26.5 11.05 25.65 9.8 24.6 9.8C23.55 9.8 22.7 11.05 22.7 12.6Z',
},
{
p: 0,
d: 'M4.6 20.6C4.6 15.6 9 12.6 15 12.4C20.4 12.2 25.6 14 27.6 18C29 21.4 27.4 25.4 23 26.4L13.4 27.6C8 28 4.6 25.4 4.6 20.6Z',
},
{ p: 'shade', d: 'M5.2 23C6 25.4 8.4 27 12 26.6L17 26C12.6 27.8 6.6 27.8 5.2 23Z', o: 0.25 },
{ p: 'shade', d: 'M6.4 20.4C7.8 21.2 9.6 21.2 11 20.4', w: 0.8, o: 0.4 },
{ p: 'ink', d: 'M7.2 17.2C7.8 16.6 8.6 16.6 9 17.2', w: 0.8 },
{ p: 1, d: 'M11.8 13.4C11 17.6 11 22.6 12.4 27.4M11.6 21.4L27.2 20.6', w: 1.4 },
{
p: 2,
d: 'M10.4 21.4C10.4 22.06 10.94 22.6 11.6 22.6C12.26 22.6 12.8 22.06 12.8 21.4C12.8 20.74 12.26 20.2 11.6 20.2C10.94 20.2 10.4 20.74 10.4 21.4Z',
},
{ p: 2, d: 'M12.4 27.4L12.6 31', w: 0.9 },
{
p: 1,
d: 'M11.5 32C11.5 32.57 11.71 33.1 12.05 33.39C12.39 33.67 12.81 33.67 13.15 33.39C13.49 33.1 13.7 32.57 13.7 32C13.7 31.43 13.49 30.9 13.15 30.61C12.81 30.33 12.39 30.33 12.05 30.61C11.71 30.9 11.5 31.43 11.5 32Z',
},
{
p: 'ink',
d: 'M18.2 17.2C18.2 18.08 18.83 18.8 19.6 18.8C20.37 18.8 21 18.08 21 17.2C21 16.32 20.37 15.6 19.6 15.6C18.83 15.6 18.2 16.32 18.2 17.2Z',
},
{
p: 'paper',
d: 'M19.6 16.6C19.6 16.88 19.82 17.1 20.1 17.1C20.38 17.1 20.6 16.88 20.6 16.6C20.6 16.32 20.38 16.1 20.1 16.1C19.82 16.1 19.6 16.32 19.6 16.6Z',
},
{ p: 0, d: 'M17.4 15.6C18.6 14.6 20.8 14.6 22 15.8', w: 1 },
{ p: 'ink', d: 'M17.6 15.4L17 14.4M19.2 14.8L19 13.8M20.8 14.9L21.2 13.9', w: 0.5 },
{
p: 1,
d: 'M55.4 30.6C61.6 14.2 80.4 13.2 88.6 27.6C90.2 34.6 91.4 42 92.2 50.4L89.4 53.8L86.4 50.6L83.4 53.8L80.4 50.6L77.4 53.8L74.4 50.6L71.4 53.8L68.4 50.6L65.4 53.8L62.4 50.6L59.4 53.8L56.4 50.6L53.4 53.4C53.4 45 54 37.4 55.4 30.6Z',
},
{ p: 'light', d: 'M60.6 28.4C63.4 23.6 67.6 21.2 72.6 20.8', w: 1.4, o: 0.3 },
{ p: 2, d: 'M55.2 46.6L91.2 46.6M56.4 33.6C64 30.4 80 29.8 89.4 32.4', w: 1.5 },
{
p: 3,
d: 'M59 37L61.2 39.8L59 42.6L56.8 39.8ZM64 37L66.2 39.8L64 42.6L61.8 39.8ZM69 37L71.2 39.8L69 42.6L66.8 39.8ZM74 37L76.2 39.8L74 42.6L71.8 39.8ZM79 37L81.2 39.8L79 42.6L76.8 39.8ZM84 37L86.2 39.8L84 42.6L81.8 39.8Z',
},
{
p: 2,
d: 'M60.8 39.8C60.8 40.19 61.11 40.5 61.5 40.5C61.89 40.5 62.2 40.19 62.2 39.8C62.2 39.41 61.89 39.1 61.5 39.1C61.11 39.1 60.8 39.41 60.8 39.8ZM65.8 39.8C65.8 40.19 66.11 40.5 66.5 40.5C66.89 40.5 67.2 40.19 67.2 39.8C67.2 39.41 66.89 39.1 66.5 39.1C66.11 39.1 65.8 39.41 65.8 39.8ZM70.8 39.8C70.8 40.19 71.11 40.5 71.5 40.5C71.89 40.5 72.2 40.19 72.2 39.8C72.2 39.41 71.89 39.1 71.5 39.1C71.11 39.1 70.8 39.41 70.8 39.8ZM75.8 39.8C75.8 40.19 76.11 40.5 76.5 40.5C76.89 40.5 77.2 40.19 77.2 39.8C77.2 39.41 76.89 39.1 76.5 39.1C76.11 39.1 75.8 39.41 75.8 39.8ZM80.8 39.8C80.8 40.19 81.11 40.5 81.5 40.5C81.89 40.5 82.2 40.19 82.2 39.8C82.2 39.41 81.89 39.1 81.5 39.1C81.11 39.1 80.8 39.41 80.8 39.8Z',
},
{
p: 2,
d: 'M53.4 53.4C52.2 54.8 52.2 56.6 53.4 57.2C54.6 56.6 54.6 54.8 53.4 53.4ZM59.4 53.4C58.2 54.8 58.2 56.6 59.4 57.2C60.6 56.6 60.6 54.8 59.4 53.4ZM65.4 53.4C64.2 54.8 64.2 56.6 65.4 57.2C66.6 56.6 66.6 54.8 65.4 53.4ZM71.4 53.4C70.2 54.8 70.2 56.6 71.4 57.2C72.6 56.6 72.6 54.8 71.4 53.4ZM77.4 53.4C76.2 54.8 76.2 56.6 77.4 57.2C78.6 56.6 78.6 54.8 77.4 53.4ZM83.4 53.4C82.2 54.8 82.2 56.6 83.4 57.2C84.6 56.6 84.6 54.8 83.4 53.4ZM89.4 53.4C88.2 54.8 88.2 56.6 89.4 57.2C90.6 56.6 90.6 54.8 89.4 53.4Z',
},
{
p: 'shade',
d: 'M55 48.4C68 49.6 80 49.6 91.8 48.4L92.2 50.4L89.4 53.8L86.4 50.6L83.4 53.8L80.4 50.6L77.4 53.8L74.4 50.6L71.4 53.8L68.4 50.6L65.4 53.8L62.4 50.6L59.4 53.8L56.4 50.6L53.4 53.4C53.4 51.6 53.6 50 55 48.4Z',
o: 0.18,
},
],
license: 'in-house',
},
];
@@ -476,4 +476,82 @@ export const NORDIC_STICKERS: readonly BookStickerDef[] = [
],
license: 'in-house',
},
{
id: 'celticHarp',
theme: 'nordic',
tags: ['harp', 'celtic', 'ireland', 'irish', 'music', 'instrument', 'folk', 'dublin'],
descr:
'An Irish Celtic harp in golden brown wood with a curved carved forepillar, an arched neck with brass tuning pins and pale strings over the soundbox.',
box: [90, 110],
slots: [
{ name: 'wood', color: '#b8783a' },
{ name: 'darkwood', color: '#6e4220' },
{ name: 'strings', color: '#f4e7c4' },
{ name: 'brass', color: '#e8b84a' },
],
layers: [
{
p: 2,
d: 'M29 12.9L29 92.2M32.3 14.8L32.3 87M35.6 16.4L35.6 81.8M38.9 17.8L38.9 76.7M42.2 18.9L42.2 71.5M45.5 19.5L45.5 66.3M48.8 19.7L48.8 61.2M52.1 19.3L52.1 56M55.4 18.4L55.4 50.8M58.7 16.8L58.7 45.7M62 15.1L62 40.5M65.3 13.8L65.3 35.3M68.6 12.9L68.6 30.2M71.9 12.5L71.9 25',
w: 0.75,
},
{
p: 0,
d: 'M24 98L77 15C79 13 82 13 84 15L85.4 18C86 19.6 85.8 21 85 22.4L37 103C36 104.6 34.6 105.2 33 105L27 104.2C24.4 103.8 23 101 24 98Z',
},
{
p: 'shade',
d: 'M31 99.6L80.6 18.4C82 16.6 84 16.6 85 18.4C85.8 19.8 85.8 21 85 22.4L37 103C36 104.6 34.6 105.2 33 105L31 104.8C29.6 103.4 29.8 101.6 31 99.6Z',
o: 0.25,
},
{ p: 1, d: 'M27.2 97.2L78.6 16.6', w: 1.1 },
{
p: 1,
d: 'M44.6 74.4C45.9 72.4 48 72.8 47.6 74.6C47.2 76.4 45 77.2 44.6 74.4ZM58.2 52.6C59.5 50.6 61.6 51 61.2 52.8C60.8 54.6 58.6 55.4 58.2 52.6ZM69 35.2C70.1 33.6 71.8 34 71.5 35.4C71.2 36.8 69.3 37.4 69 35.2Z',
},
{ p: 3, d: 'M31.4 101.4L36.8 102.6M76.6 20.4L83.8 21.8', w: 1.3 },
{
p: 1,
d: 'M18 101.4C18 99.8 19.2 99 20.8 99L36 99C37.6 99 38.6 100.2 38.6 101.6L38.6 105.4C38.6 106.6 37.8 107.4 36.6 107.4L19.6 107.4C18.6 107.4 18 106.6 18 105.6Z',
},
{ p: 0, d: 'M7 12C1.5 40 5.5 74 20.4 103L31.2 101.6C18.4 75 14 44 16.8 16Z' },
{ p: 'shade', d: 'M12.6 14.5C10.4 44 14.6 75 26.6 102.2L31.2 101.6C18.4 75 14 44 16.8 16Z', o: 0.25 },
{
p: 3,
d: 'M7.2 34C7.2 31.1 12 36.9 12 34C12 31.1 7.2 36.9 7.2 34ZM7.4 48C7.4 45.1 12.2 50.9 12.2 48C12.2 45.1 7.4 50.9 7.4 48ZM9 62C9 59.1 13.8 64.9 13.8 62C13.8 59.1 9 64.9 9 62ZM12 76C12 73.1 16.8 78.9 16.8 76C16.8 73.1 12 78.9 12 76ZM16.2 89C16.2 86.1 21 91.9 21 89C21 86.1 16.2 91.9 16.2 89Z',
w: 0.9,
},
{
p: 1,
d: 'M5.6 28.8L13.8 28.4M5.8 39.4L14 39M5.8 42.8L14 42.4M6 53.4L14.2 53M7.4 56.8L15.6 56.4M7.6 67.4L15.8 67M10.4 70.8L18.6 70.4M10.6 81.4L18.8 81M14.6 83.8L22.8 83.4M14.8 94.4L23 94',
w: 0.9,
o: 0.85,
},
{
p: 0,
d: 'M7 11C10 3 20 2 27 6C37 12 47 18 57 12C66 6 78 4 86 12L82 18C76 12 66 14 58 19C47 25 36 19 26 13C20 10 15 11 14 16.5Z',
},
{
p: 'shade',
d: 'M14 16.5C15 11 20 10 26 13C36 19 47 25 58 19C66 14 76 12 82 18L83.6 15.6C76 10 66 11 58 16C47 22 36 16.4 26 10.4C20 7 15.6 9 14 16.5Z',
o: 0.22,
},
{ p: 'light', d: 'M11 8.6C15 4.6 21 4.4 27 7.6C37 13.4 47 18.6 57 13C65 8.4 74 6.6 80.6 9.2', w: 0.9, o: 0.45 },
{
p: 3,
d: 'M28.1 11C28.1 11.32 28.27 11.62 28.55 11.78C28.83 11.94 29.17 11.94 29.45 11.78C29.73 11.62 29.9 11.32 29.9 11C29.9 10.68 29.73 10.38 29.45 10.22C29.17 10.06 28.83 10.06 28.55 10.22C28.27 10.38 28.1 10.68 28.1 11ZM31.4 12.8C31.4 13.3 31.8 13.7 32.3 13.7C32.8 13.7 33.2 13.3 33.2 12.8C33.2 12.3 32.8 11.9 32.3 11.9C31.8 11.9 31.4 12.3 31.4 12.8ZM34.7 14.5C34.7 14.82 34.87 15.12 35.15 15.28C35.43 15.44 35.77 15.44 36.05 15.28C36.33 15.12 36.5 14.82 36.5 14.5C36.5 14.18 36.33 13.88 36.05 13.72C35.77 13.56 35.43 13.56 35.15 13.72C34.87 13.88 34.7 14.18 34.7 14.5ZM38 16C38 16.32 38.17 16.62 38.45 16.78C38.73 16.94 39.07 16.94 39.35 16.78C39.63 16.62 39.8 16.32 39.8 16C39.8 15.68 39.63 15.38 39.35 15.22C39.07 15.06 38.73 15.06 38.45 15.22C38.17 15.38 38 15.68 38 16ZM41.3 17.1C41.3 17.6 41.7 18 42.2 18C42.7 18 43.1 17.6 43.1 17.1C43.1 16.6 42.7 16.2 42.2 16.2C41.7 16.2 41.3 16.6 41.3 17.1ZM44.6 17.8C44.6 18.12 44.77 18.42 45.05 18.58C45.33 18.74 45.67 18.74 45.95 18.58C46.23 18.42 46.4 18.12 46.4 17.8C46.4 17.48 46.23 17.18 45.95 17.02C45.67 16.86 45.33 16.86 45.05 17.02C44.77 17.18 44.6 17.48 44.6 17.8ZM47.9 18C47.9 18.5 48.3 18.9 48.8 18.9C49.3 18.9 49.7 18.5 49.7 18C49.7 17.5 49.3 17.1 48.8 17.1C48.3 17.1 47.9 17.5 47.9 18ZM51.2 17.6C51.2 17.92 51.37 18.22 51.65 18.38C51.93 18.54 52.27 18.54 52.55 18.38C52.83 18.22 53 17.92 53 17.6C53 17.28 52.83 16.98 52.55 16.82C52.27 16.66 51.93 16.66 51.65 16.82C51.37 16.98 51.2 17.28 51.2 17.6ZM54.5 16.5C54.5 16.82 54.67 17.12 54.95 17.28C55.23 17.44 55.57 17.44 55.85 17.28C56.13 17.12 56.3 16.82 56.3 16.5C56.3 16.18 56.13 15.88 55.85 15.72C55.57 15.56 55.23 15.56 54.95 15.72C54.67 15.88 54.5 16.18 54.5 16.5ZM57.8 14.8C57.8 15.3 58.2 15.7 58.7 15.7C59.2 15.7 59.6 15.3 59.6 14.8C59.6 14.3 59.2 13.9 58.7 13.9C58.2 13.9 57.8 14.3 57.8 14.8ZM61.1 13.1C61.1 13.42 61.27 13.72 61.55 13.88C61.83 14.04 62.17 14.04 62.45 13.88C62.73 13.72 62.9 13.42 62.9 13.1C62.9 12.78 62.73 12.48 62.45 12.32C62.17 12.16 61.83 12.16 61.55 12.32C61.27 12.48 61.1 12.78 61.1 13.1ZM64.4 11.8C64.4 12.12 64.57 12.42 64.85 12.58C65.13 12.74 65.47 12.74 65.75 12.58C66.03 12.42 66.2 12.12 66.2 11.8C66.2 11.48 66.03 11.18 65.75 11.02C65.47 10.86 65.13 10.86 64.85 11.02C64.57 11.18 64.4 11.48 64.4 11.8ZM67.7 10.9C67.7 11.22 67.87 11.52 68.15 11.68C68.43 11.84 68.77 11.84 69.05 11.68C69.33 11.52 69.5 11.22 69.5 10.9C69.5 10.58 69.33 10.28 69.05 10.12C68.77 9.96 68.43 9.96 68.15 10.12C67.87 10.28 67.7 10.58 67.7 10.9ZM71 10.5C71 10.82 71.17 11.12 71.45 11.28C71.73 11.44 72.07 11.44 72.35 11.28C72.63 11.12 72.8 10.82 72.8 10.5C72.8 10.18 72.63 9.88 72.35 9.72C72.07 9.56 71.73 9.56 71.45 9.72C71.17 9.88 71 10.18 71 10.5Z',
},
{
p: 0,
d: 'M4 11.4C4 14.38 6.42 16.8 9.4 16.8C12.38 16.8 14.8 14.38 14.8 11.4C14.8 8.42 12.38 6 9.4 6C6.42 6 4 8.42 4 11.4Z',
},
{
p: 3,
d: 'M9.4 11.4C9.4 10.2 11 10.2 11 11.6C11 13.4 8 13.6 7.6 11.4C7.2 8.6 11.4 7.8 12.6 10.4C13.6 12.8 12 15.6 9.4 15.6',
w: 1,
},
{ p: 3, d: 'M14.6 16.2L18.4 17.2', w: 1.3 },
],
license: 'in-house',
},
];
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'حراشف',
'journey.studio.pattern.sunburst': 'أشعة الشمس',
'journey.studio.pattern.confetti': 'قصاصات ملونة',
'journey.studio.pattern.linen': 'كتان',
'journey.studio.pattern.grain': 'حبيبات الورق',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'انقر مرتين للإزالة',
'assistant.card.book.finding.hairline': 'خط أرق من أن يُطبع',
'assistant.card.book.finding.edge_unused': 'حافة لا يقبلها هذا الشكل',
'journey.studio.shapeName.rect': 'مستطيل',
'journey.studio.shapeName.squircle': 'مربع مستدير',
'journey.studio.shapeName.ellipse': 'شكل بيضاوي',
'journey.studio.shapeName.capsule': 'كبسولة',
'journey.studio.shapeName.triangle': 'مثلث',
'journey.studio.shapeName.triangle-down': 'مثلث لأسفل',
'journey.studio.shapeName.diamond': 'معين',
'journey.studio.shapeName.parallelogram': 'متوازي أضلاع',
'journey.studio.shapeName.trapezoid': 'شبه منحرف',
'journey.studio.shapeName.arch': 'قوس',
'journey.studio.shapeName.half-circle': 'نصف دائرة',
'journey.studio.shapeName.quarter-circle': 'ربع دائرة',
'journey.studio.shapeName.pentagon': 'خماسي',
'journey.studio.shapeName.hexagon': 'سداسي',
'journey.studio.shapeName.hexagon-flat': 'سداسي مسطح',
'journey.studio.shapeName.heptagon': 'سباعي',
'journey.studio.shapeName.octagon': 'ثماني',
'journey.studio.shapeName.star-4': 'نجمة رباعية',
'journey.studio.shapeName.star-5': 'نجمة خماسية',
'journey.studio.shapeName.star-6': 'نجمة سداسية',
'journey.studio.shapeName.star-8': 'نجمة ثمانية',
'journey.studio.shapeName.star-12': 'نجمة اثنا عشرية',
'journey.studio.shapeName.burst': 'انفجار',
'journey.studio.shapeName.seal': 'ختم',
'journey.studio.shapeName.sparkle': 'بريق',
'journey.studio.shapeName.star-rounded': 'نجمة مستديرة',
'journey.studio.shapeName.star-twinkle': 'نجمة متلألئة',
'journey.studio.shapeName.arrow-right': 'سهم لليمين',
'journey.studio.shapeName.arrow-left': 'سهم لليسار',
'journey.studio.shapeName.arrow-up': 'سهم لأعلى',
'journey.studio.shapeName.arrow-down': 'سهم لأسفل',
'journey.studio.shapeName.arrow-both': 'سهم مزدوج',
'journey.studio.shapeName.chevron-right': 'شيفرون لليمين',
'journey.studio.shapeName.chevron-left': 'شيفرون لليسار',
'journey.studio.shapeName.arrow-bent': 'سهم منحني الزاوية',
'journey.studio.shapeName.arrow-curve': 'سهم منحنٍ',
'journey.studio.shapeName.arrow-circle': 'سهم دائري',
'journey.studio.shapeName.arrow-notched': 'سهم مفرّغ',
'journey.studio.shapeName.arrow-thin': 'سهم رفيع',
'journey.studio.shapeName.bubble-round': 'فقاعة حوار مستديرة',
'journey.studio.shapeName.bubble-square': 'فقاعة حوار مربعة',
'journey.studio.shapeName.bubble-oval': 'فقاعة حوار بيضاوية',
'journey.studio.shapeName.bubble-think': 'فقاعة تفكير',
'journey.studio.shapeName.bubble-shout': 'فقاعة صراخ',
'journey.studio.shapeName.bubble-side': 'فقاعة حوار جانبية',
'journey.studio.shapeName.bubble-capsule': 'فقاعة حوار كبسولة',
'journey.studio.shapeName.pin': 'دبوس خريطة',
'journey.studio.shapeName.compass': 'بوصلة',
'journey.studio.shapeName.mountain': 'جبل',
'journey.studio.shapeName.sun': 'شمس',
'journey.studio.shapeName.moon': 'قمر',
'journey.studio.shapeName.cloud': 'سحابة',
'journey.studio.shapeName.cloud-puffy': 'سحابة منتفخة',
'journey.studio.shapeName.drop': 'قطرة',
'journey.studio.shapeName.wave': 'موجة',
'journey.studio.shapeName.leaf': 'ورقة',
'journey.studio.shapeName.shield': 'درع',
'journey.studio.shapeName.ticket': 'تذكرة',
'journey.studio.shapeName.heart': 'قلب',
'journey.studio.shapeName.flower-5': 'زهرة بخمس بتلات',
'journey.studio.shapeName.flower-6': 'زهرة بست بتلات',
'journey.studio.shapeName.cross': 'صليب',
'journey.studio.shapeName.plus': 'علامة زائد',
'journey.studio.shapeName.gear': 'ترس',
'journey.studio.shapeName.blob-1': 'شكل عضوي 1',
'journey.studio.shapeName.blob-2': 'شكل عضوي 2',
'journey.studio.shapeName.blob-3': 'شكل عضوي 3',
'journey.studio.shapeName.blob-4': 'شكل عضوي 4',
'journey.studio.shapeName.heart-round': 'قلب مستدير',
'journey.studio.shapeName.heart-pair': 'قلبان',
'journey.studio.shapeName.flower-8': 'زهرة بثماني بتلات',
'journey.studio.shapeName.banner-ribbon': 'لافتة شريط',
'journey.studio.shapeName.banner-pennant': 'راية مثلثة',
'journey.studio.shapeName.banner-bookmark': 'فاصل كتاب',
'journey.studio.shapeName.banner-flag': 'لافتة علم',
'journey.studio.shapeName.ribbon-ends': 'شريط بأطراف مطوية',
'journey.studio.shapeName.ribbon-arc': 'شريط مقوس',
'journey.studio.shapeName.wavy-band': 'شريط متموج',
'journey.studio.shapeName.torn-strip': 'شريط ممزق',
'journey.studio.shapeName.arch-tall': 'قوس عالٍ',
'journey.studio.shapeName.arch-pointed': 'قوس مدبب',
'journey.studio.shapeName.arch-double': 'قوس مزدوج',
'journey.studio.shapeName.arch-half': 'نصف قوس',
'journey.studio.shapeName.arch-rainbow': 'قوس قزح',
'journey.studio.shapeName.leaf-palm': 'سعفة نخيل',
'journey.studio.shapeName.leaf-oak': 'ورقة بلوط',
'journey.studio.shapeName.leaf-long': 'ورقة طويلة',
'journey.studio.shapeName.leaf-monstera': 'ورقة مونستيرا',
'journey.studio.shapeName.coral-1': 'مرجان 1',
'journey.studio.shapeName.coral-2': 'مرجان 2',
'journey.studio.shapeName.seaweed': 'أعشاب بحرية',
'journey.studio.shapeName.star-organic': 'نجمة عضوية',
'journey.studio.shapeName.sun-cutout': 'شمس مفرّغة',
'journey.studio.shapeName.organic-1': 'شكل عضوي 1',
'journey.studio.shapeName.organic-2': 'شكل عضوي 2',
'journey.studio.shapeName.organic-3': 'شكل عضوي 3',
'journey.studio.shapeName.stamp-rect': 'ختم مستطيل',
'journey.studio.shapeName.stamp-circle': 'ختم دائري',
'journey.studio.shapeName.postmark': 'ختم بريدي',
'journey.studio.shapeName.rosette': 'وردة شرفية',
'journey.studio.shapeName.scallop-circle': 'دائرة مسننة',
'journey.studio.shapeName.badge-shield': 'شارة درع',
'journey.studio.shapeName.label-tab': 'لسان ملصق',
'journey.studio.shapeName.ticket-stub': 'قسيمة تذكرة',
'journey.studio.shapeName.sunburst-12': 'أشعة شمس 12',
'journey.studio.shapeName.sunburst-24': 'أشعة شمس 24',
'journey.studio.shapeName.frame-notched': 'إطار مشطوف الزوايا',
'journey.studio.shapeName.frame-chamfer': 'إطار مشطّب',
'journey.studio.shapeName.frame-cushion': 'إطار وسادي',
'journey.studio.shapeName.flourish-scroll': 'زخرفة لفافة',
'journey.studio.shapeName.laurel': 'إكليل غار',
'journey.studio.shapeName.fleuron': 'زخرفة نباتية',
'journey.studio.shapeName.corner-flourish': 'زخرفة زاوية',
};
export default studioShapes;
+12
View File
@@ -174,6 +174,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'إكليل غار',
'journey.studio.sticker.cornerFlourish': 'زاوية مزخرفة',
'journey.studio.sticker.cornerScroll': 'زاوية بلفائف',
'journey.studio.sticker.parthenon': 'البارثينون',
'journey.studio.sticker.milanDuomo': 'كاتدرائية ميلانو',
'journey.studio.sticker.canalHouses': 'بيوت القنوات',
'journey.studio.sticker.gondola': 'جندول',
'journey.studio.sticker.lisbonTram': 'ترام لشبونة',
'journey.studio.sticker.celticHarp': 'قيثارة سلتية',
'journey.studio.sticker.tiger': 'نمر',
'journey.studio.sticker.monkey': 'قرد',
'journey.studio.sticker.camel': 'جمل',
'journey.studio.sticker.doodlePalm': 'نخلة مرسومة باليد',
'journey.studio.sticker.doodleWaves': 'أمواج مرسومة باليد',
'journey.studio.sticker.doodleSunRays': 'شمس مشرقة مرسومة باليد',
};
export default studioStickers;
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'Pulcuqlar',
'journey.studio.pattern.sunburst': 'Şüalar',
'journey.studio.pattern.confetti': 'Konfeti',
'journey.studio.pattern.linen': 'Kətan',
'journey.studio.pattern.grain': 'Kağız dənəsi',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'Silmək üçün iki dəfə kliklə',
'assistant.card.book.finding.hairline': 'Çap üçün çox nazik xətt',
'assistant.card.book.finding.edge_unused': 'Bu formanın qəbul etmədiyi kənar',
'journey.studio.shapeName.rect': 'Düzbucaqlı',
'journey.studio.shapeName.squircle': 'Yuvarlaqlaşdırılmış kvadrat',
'journey.studio.shapeName.ellipse': 'Ellips',
'journey.studio.shapeName.capsule': 'Kapsul',
'journey.studio.shapeName.triangle': 'Üçbucaq',
'journey.studio.shapeName.triangle-down': 'Aşağı baxan üçbucaq',
'journey.studio.shapeName.diamond': 'Romb',
'journey.studio.shapeName.parallelogram': 'Paraleloqram',
'journey.studio.shapeName.trapezoid': 'Trapesiya',
'journey.studio.shapeName.arch': 'Tağ',
'journey.studio.shapeName.half-circle': 'Yarımdairə',
'journey.studio.shapeName.quarter-circle': 'Dairənin dörddə biri',
'journey.studio.shapeName.pentagon': 'Beşbucaq',
'journey.studio.shapeName.hexagon': 'Altıbucaq',
'journey.studio.shapeName.hexagon-flat': 'Düz altıbucaq',
'journey.studio.shapeName.heptagon': 'Yeddibucaq',
'journey.studio.shapeName.octagon': 'Səkkizbucaq',
'journey.studio.shapeName.star-4': 'Dördguşəli ulduz',
'journey.studio.shapeName.star-5': 'Beşguşəli ulduz',
'journey.studio.shapeName.star-6': 'Altıguşəli ulduz',
'journey.studio.shapeName.star-8': 'Səkkizguşəli ulduz',
'journey.studio.shapeName.star-12': 'On ikiguşəli ulduz',
'journey.studio.shapeName.burst': 'Partlayış',
'journey.studio.shapeName.seal': 'Möhür',
'journey.studio.shapeName.sparkle': 'Parıltı',
'journey.studio.shapeName.star-rounded': 'Yuvarlaq ulduz',
'journey.studio.shapeName.star-twinkle': 'Közərən ulduz',
'journey.studio.shapeName.arrow-right': 'Sağ ox',
'journey.studio.shapeName.arrow-left': 'Sol ox',
'journey.studio.shapeName.arrow-up': 'Yuxarı ox',
'journey.studio.shapeName.arrow-down': 'Aşağı ox',
'journey.studio.shapeName.arrow-both': 'İkitərəfli ox',
'journey.studio.shapeName.chevron-right': 'Sağ şevron',
'journey.studio.shapeName.chevron-left': 'Sol şevron',
'journey.studio.shapeName.arrow-bent': 'Əyilmiş ox',
'journey.studio.shapeName.arrow-curve': 'Əyri ox',
'journey.studio.shapeName.arrow-circle': 'Dairəvi ox',
'journey.studio.shapeName.arrow-notched': 'Çentikli ox',
'journey.studio.shapeName.arrow-thin': 'Nazik ox',
'journey.studio.shapeName.bubble-round': 'Yuvarlaq danışıq balonu',
'journey.studio.shapeName.bubble-square': 'Kvadrat danışıq balonu',
'journey.studio.shapeName.bubble-oval': 'Oval danışıq balonu',
'journey.studio.shapeName.bubble-think': 'Düşüncə balonu',
'journey.studio.shapeName.bubble-shout': 'Qışqırıq balonu',
'journey.studio.shapeName.bubble-side': 'Yandan danışıq balonu',
'journey.studio.shapeName.bubble-capsule': 'Kapsul danışıq balonu',
'journey.studio.shapeName.pin': 'Xəritə nişanı',
'journey.studio.shapeName.compass': 'Kompas',
'journey.studio.shapeName.mountain': 'Dağ',
'journey.studio.shapeName.sun': 'Günəş',
'journey.studio.shapeName.moon': 'Ay',
'journey.studio.shapeName.cloud': 'Bulud',
'journey.studio.shapeName.cloud-puffy': 'Qabarıq bulud',
'journey.studio.shapeName.drop': 'Damla',
'journey.studio.shapeName.wave': 'Dalğa',
'journey.studio.shapeName.leaf': 'Yarpaq',
'journey.studio.shapeName.shield': 'Qalxan',
'journey.studio.shapeName.ticket': 'Bilet',
'journey.studio.shapeName.heart': 'Ürək',
'journey.studio.shapeName.flower-5': 'Beşləçəkli çiçək',
'journey.studio.shapeName.flower-6': 'Altıləçəkli çiçək',
'journey.studio.shapeName.cross': 'Xaç',
'journey.studio.shapeName.plus': 'Plus', // same-as-en
'journey.studio.shapeName.gear': 'Dişli',
'journey.studio.shapeName.blob-1': 'Ləkə 1',
'journey.studio.shapeName.blob-2': 'Ləkə 2',
'journey.studio.shapeName.blob-3': 'Ləkə 3',
'journey.studio.shapeName.blob-4': 'Ləkə 4',
'journey.studio.shapeName.heart-round': 'Yuvarlaq ürək',
'journey.studio.shapeName.heart-pair': 'İki ürək',
'journey.studio.shapeName.flower-8': 'Səkkizləçəkli çiçək',
'journey.studio.shapeName.banner-ribbon': 'Lent bannerı',
'journey.studio.shapeName.banner-pennant': 'Vımpel',
'journey.studio.shapeName.banner-bookmark': 'Əlfəcin',
'journey.studio.shapeName.banner-flag': 'Bayraq bannerı',
'journey.studio.shapeName.ribbon-ends': 'Qatlanmış uclu lent',
'journey.studio.shapeName.ribbon-arc': 'Qövslü lent',
'journey.studio.shapeName.wavy-band': 'Dalğalı zolaq',
'journey.studio.shapeName.torn-strip': 'Cırıq zolaq',
'journey.studio.shapeName.arch-tall': 'hündür tağ',
'journey.studio.shapeName.arch-pointed': 'Sivri tağ',
'journey.studio.shapeName.arch-double': 'İkili tağ',
'journey.studio.shapeName.arch-half': 'Yarım tağ',
'journey.studio.shapeName.arch-rainbow': 'Göy qurşağı tağı',
'journey.studio.shapeName.leaf-palm': 'Palma yarpağı',
'journey.studio.shapeName.leaf-oak': 'Palıd yarpağı',
'journey.studio.shapeName.leaf-long': 'Uzun yarpaq',
'journey.studio.shapeName.leaf-monstera': 'Monstera yarpağı',
'journey.studio.shapeName.coral-1': 'Mərcan 1',
'journey.studio.shapeName.coral-2': 'Mərcan 2',
'journey.studio.shapeName.seaweed': 'Dəniz yosunu',
'journey.studio.shapeName.star-organic': 'Orqanik ulduz',
'journey.studio.shapeName.sun-cutout': 'Kəsmə günəş',
'journey.studio.shapeName.organic-1': 'Orqanik forma 1',
'journey.studio.shapeName.organic-2': 'Orqanik forma 2',
'journey.studio.shapeName.organic-3': 'Orqanik forma 3',
'journey.studio.shapeName.stamp-rect': 'Düzbucaqlı ştamp',
'journey.studio.shapeName.stamp-circle': 'Yuvarlaq ştamp',
'journey.studio.shapeName.postmark': 'Poçt ştampı',
'journey.studio.shapeName.rosette': 'Rozet',
'journey.studio.shapeName.scallop-circle': 'Dişli kənarlı dairə',
'journey.studio.shapeName.badge-shield': 'Qalxan nişanı',
'journey.studio.shapeName.label-tab': 'Etiket tabı',
'journey.studio.shapeName.ticket-stub': 'Bilet kəsiyi',
'journey.studio.shapeName.sunburst-12': 'Günəş şüaları, 12 şüa',
'journey.studio.shapeName.sunburst-24': 'Günəş şüaları, 24 şüa',
'journey.studio.shapeName.frame-notched': 'Çentikli çərçivə',
'journey.studio.shapeName.frame-chamfer': 'Kəsik küncülü çərçivə',
'journey.studio.shapeName.frame-cushion': 'Yastıq çərçivə',
'journey.studio.shapeName.flourish-scroll': 'Qıvrım bəzək',
'journey.studio.shapeName.laurel': 'Dəfnə çələngi',
'journey.studio.shapeName.fleuron': 'Yarpaq bəzəyi',
'journey.studio.shapeName.corner-flourish': 'Künc bəzəyi',
};
export default studioShapes;
+12
View File
@@ -175,6 +175,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'Dəfnə çələngi',
'journey.studio.sticker.cornerFlourish': 'Naxışlı künc',
'journey.studio.sticker.cornerScroll': 'Burulmuş künc',
'journey.studio.sticker.parthenon': 'Parfenon',
'journey.studio.sticker.milanDuomo': 'Milan Katedralı',
'journey.studio.sticker.canalHouses': 'Kanal evləri',
'journey.studio.sticker.gondola': 'Qondol',
'journey.studio.sticker.lisbonTram': 'Lissabon tramvayı',
'journey.studio.sticker.celticHarp': 'Kelt arfası',
'journey.studio.sticker.tiger': 'Pələng',
'journey.studio.sticker.monkey': 'Meymun',
'journey.studio.sticker.camel': 'Dəvə',
'journey.studio.sticker.doodlePalm': 'Əl ilə çəkilmiş palma',
'journey.studio.sticker.doodleWaves': 'Əl ilə çəkilmiş dalğalar',
'journey.studio.sticker.doodleSunRays': 'Əl ilə çəkilmiş doğan günəş',
};
export default studioStickers;
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'Escamas',
'journey.studio.pattern.sunburst': 'Raios de sol',
'journey.studio.pattern.confetti': 'Confete',
'journey.studio.pattern.linen': 'Linho',
'journey.studio.pattern.grain': 'Textura de papel',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'Clique duplo para remover',
'assistant.card.book.finding.hairline': 'Uma linha fina demais para imprimir',
'assistant.card.book.finding.edge_unused': 'Uma borda que esta forma não aceita',
'journey.studio.shapeName.rect': 'Retângulo',
'journey.studio.shapeName.squircle': 'Quadrado arredondado',
'journey.studio.shapeName.ellipse': 'Elipse',
'journey.studio.shapeName.capsule': 'Cápsula',
'journey.studio.shapeName.triangle': 'Triângulo',
'journey.studio.shapeName.triangle-down': 'Triângulo para baixo',
'journey.studio.shapeName.diamond': 'Losango',
'journey.studio.shapeName.parallelogram': 'Paralelogramo',
'journey.studio.shapeName.trapezoid': 'Trapézio',
'journey.studio.shapeName.arch': 'Arco',
'journey.studio.shapeName.half-circle': 'Semicírculo',
'journey.studio.shapeName.quarter-circle': 'Quarto de círculo',
'journey.studio.shapeName.pentagon': 'Pentágono',
'journey.studio.shapeName.hexagon': 'Hexágono',
'journey.studio.shapeName.hexagon-flat': 'Hexágono achatado',
'journey.studio.shapeName.heptagon': 'Heptágono',
'journey.studio.shapeName.octagon': 'Octógono',
'journey.studio.shapeName.star-4': 'Estrela de quatro pontas',
'journey.studio.shapeName.star-5': 'Estrela de cinco pontas',
'journey.studio.shapeName.star-6': 'Estrela de seis pontas',
'journey.studio.shapeName.star-8': 'Estrela de oito pontas',
'journey.studio.shapeName.star-12': 'Estrela de doze pontas',
'journey.studio.shapeName.burst': 'Explosão',
'journey.studio.shapeName.seal': 'Selo',
'journey.studio.shapeName.sparkle': 'Brilho',
'journey.studio.shapeName.star-rounded': 'Estrela arredondada',
'journey.studio.shapeName.star-twinkle': 'Estrela cintilante',
'journey.studio.shapeName.arrow-right': 'Seta para a direita',
'journey.studio.shapeName.arrow-left': 'Seta para a esquerda',
'journey.studio.shapeName.arrow-up': 'Seta para cima',
'journey.studio.shapeName.arrow-down': 'Seta para baixo',
'journey.studio.shapeName.arrow-both': 'Seta dupla',
'journey.studio.shapeName.chevron-right': 'Divisa para a direita',
'journey.studio.shapeName.chevron-left': 'Divisa para a esquerda',
'journey.studio.shapeName.arrow-bent': 'Seta dobrada',
'journey.studio.shapeName.arrow-curve': 'Seta curva',
'journey.studio.shapeName.arrow-circle': 'Seta circular',
'journey.studio.shapeName.arrow-notched': 'Seta entalhada',
'journey.studio.shapeName.arrow-thin': 'Seta fina',
'journey.studio.shapeName.bubble-round': 'Balão de fala redondo',
'journey.studio.shapeName.bubble-square': 'Balão de fala quadrado',
'journey.studio.shapeName.bubble-oval': 'Balão de fala oval',
'journey.studio.shapeName.bubble-think': 'Balão de pensamento',
'journey.studio.shapeName.bubble-shout': 'Balão de grito',
'journey.studio.shapeName.bubble-side': 'Balão de fala lateral',
'journey.studio.shapeName.bubble-capsule': 'Balão de fala em cápsula',
'journey.studio.shapeName.pin': 'Marcador de mapa',
'journey.studio.shapeName.compass': 'Bússola',
'journey.studio.shapeName.mountain': 'Montanha',
'journey.studio.shapeName.sun': 'Sol',
'journey.studio.shapeName.moon': 'Lua',
'journey.studio.shapeName.cloud': 'Nuvem',
'journey.studio.shapeName.cloud-puffy': 'Nuvem fofa',
'journey.studio.shapeName.drop': 'Gota',
'journey.studio.shapeName.wave': 'Onda',
'journey.studio.shapeName.leaf': 'Folha',
'journey.studio.shapeName.shield': 'Escudo',
'journey.studio.shapeName.ticket': 'Ingresso',
'journey.studio.shapeName.heart': 'Coração',
'journey.studio.shapeName.flower-5': 'Flor de cinco pétalas',
'journey.studio.shapeName.flower-6': 'Flor de seis pétalas',
'journey.studio.shapeName.cross': 'Cruz',
'journey.studio.shapeName.plus': 'Mais',
'journey.studio.shapeName.gear': 'Engrenagem',
'journey.studio.shapeName.blob-1': 'Blob 1', // same-as-en
'journey.studio.shapeName.blob-2': 'Blob 2', // same-as-en
'journey.studio.shapeName.blob-3': 'Blob 3', // same-as-en
'journey.studio.shapeName.blob-4': 'Blob 4', // same-as-en
'journey.studio.shapeName.heart-round': 'Coração redondo',
'journey.studio.shapeName.heart-pair': 'Dois corações',
'journey.studio.shapeName.flower-8': 'Flor de oito pétalas',
'journey.studio.shapeName.banner-ribbon': 'Faixa de fita',
'journey.studio.shapeName.banner-pennant': 'Flâmula',
'journey.studio.shapeName.banner-bookmark': 'Marcador de página',
'journey.studio.shapeName.banner-flag': 'Faixa de bandeira',
'journey.studio.shapeName.ribbon-ends': 'Fita com pontas dobradas',
'journey.studio.shapeName.ribbon-arc': 'Fita em arco',
'journey.studio.shapeName.wavy-band': 'Faixa ondulada',
'journey.studio.shapeName.torn-strip': 'Tira rasgada',
'journey.studio.shapeName.arch-tall': 'Arco alto',
'journey.studio.shapeName.arch-pointed': 'Arco pontiagudo',
'journey.studio.shapeName.arch-double': 'Arco duplo',
'journey.studio.shapeName.arch-half': 'Meio arco',
'journey.studio.shapeName.arch-rainbow': 'Arco-íris',
'journey.studio.shapeName.leaf-palm': 'Folha de palmeira',
'journey.studio.shapeName.leaf-oak': 'Folha de carvalho',
'journey.studio.shapeName.leaf-long': 'Folha longa',
'journey.studio.shapeName.leaf-monstera': 'Folha de monstera',
'journey.studio.shapeName.coral-1': 'Coral 1', // same-as-en
'journey.studio.shapeName.coral-2': 'Coral 2', // same-as-en
'journey.studio.shapeName.seaweed': 'Alga',
'journey.studio.shapeName.star-organic': 'Estrela orgânica',
'journey.studio.shapeName.sun-cutout': 'Sol recortado',
'journey.studio.shapeName.organic-1': 'Forma orgânica 1',
'journey.studio.shapeName.organic-2': 'Forma orgânica 2',
'journey.studio.shapeName.organic-3': 'Forma orgânica 3',
'journey.studio.shapeName.stamp-rect': 'Carimbo retangular',
'journey.studio.shapeName.stamp-circle': 'Carimbo redondo',
'journey.studio.shapeName.postmark': 'Carimbo postal',
'journey.studio.shapeName.rosette': 'Roseta',
'journey.studio.shapeName.scallop-circle': 'Círculo recortado',
'journey.studio.shapeName.badge-shield': 'Emblema de escudo',
'journey.studio.shapeName.label-tab': 'Aba de etiqueta',
'journey.studio.shapeName.ticket-stub': 'Canhoto de ingresso',
'journey.studio.shapeName.sunburst-12': 'Raios de sol, 12 raios',
'journey.studio.shapeName.sunburst-24': 'Raios de sol, 24 raios',
'journey.studio.shapeName.frame-notched': 'Moldura entalhada',
'journey.studio.shapeName.frame-chamfer': 'Moldura chanfrada',
'journey.studio.shapeName.frame-cushion': 'Moldura almofadada',
'journey.studio.shapeName.flourish-scroll': 'Ornamento em voluta',
'journey.studio.shapeName.laurel': 'Louros',
'journey.studio.shapeName.fleuron': 'Florão',
'journey.studio.shapeName.corner-flourish': 'Ornamento de canto',
};
export default studioShapes;
+12
View File
@@ -175,6 +175,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'Coroa de louros',
'journey.studio.sticker.cornerFlourish': 'Canto com floreio',
'journey.studio.sticker.cornerScroll': 'Canto com volutas',
'journey.studio.sticker.parthenon': 'Partenon',
'journey.studio.sticker.milanDuomo': 'Catedral de Milão',
'journey.studio.sticker.canalHouses': 'Casas do canal',
'journey.studio.sticker.gondola': 'Gôndola',
'journey.studio.sticker.lisbonTram': 'Bonde de Lisboa',
'journey.studio.sticker.celticHarp': 'Harpa celta',
'journey.studio.sticker.tiger': 'Tigre',
'journey.studio.sticker.monkey': 'Macaco',
'journey.studio.sticker.camel': 'Camelo',
'journey.studio.sticker.doodlePalm': 'Palmeira desenhada',
'journey.studio.sticker.doodleWaves': 'Ondas desenhadas',
'journey.studio.sticker.doodleSunRays': 'Sol nascente desenhado',
};
export default studioStickers;
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'Escates',
'journey.studio.pattern.sunburst': 'Raigs',
'journey.studio.pattern.confetti': 'Confeti',
'journey.studio.pattern.linen': 'Lli',
'journey.studio.pattern.grain': 'Gra de paper',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'Doble clic per treure',
'assistant.card.book.finding.hairline': 'Una línia massa fina per imprimir',
'assistant.card.book.finding.edge_unused': 'Una vora que aquesta forma no admet',
'journey.studio.shapeName.rect': 'Rectangle', // same-as-en
'journey.studio.shapeName.squircle': 'Quadrat arrodonit',
'journey.studio.shapeName.ellipse': 'El·lipse',
'journey.studio.shapeName.capsule': 'Càpsula',
'journey.studio.shapeName.triangle': 'Triangle', // same-as-en
'journey.studio.shapeName.triangle-down': 'Triangle cap avall',
'journey.studio.shapeName.diamond': 'Rombe',
'journey.studio.shapeName.parallelogram': 'Paral·lelogram',
'journey.studio.shapeName.trapezoid': 'Trapezi',
'journey.studio.shapeName.arch': 'Arc',
'journey.studio.shapeName.half-circle': 'Mig cercle',
'journey.studio.shapeName.quarter-circle': 'Quart de cercle',
'journey.studio.shapeName.pentagon': 'Pentàgon',
'journey.studio.shapeName.hexagon': 'Hexàgon',
'journey.studio.shapeName.hexagon-flat': 'Hexàgon pla',
'journey.studio.shapeName.heptagon': 'Heptàgon',
'journey.studio.shapeName.octagon': 'Octàgon',
'journey.studio.shapeName.star-4': 'Estrella de quatre puntes',
'journey.studio.shapeName.star-5': 'Estrella de cinc puntes',
'journey.studio.shapeName.star-6': 'Estrella de sis puntes',
'journey.studio.shapeName.star-8': 'Estrella de vuit puntes',
'journey.studio.shapeName.star-12': 'Estrella de dotze puntes',
'journey.studio.shapeName.burst': 'Explosió',
'journey.studio.shapeName.seal': 'Segell',
'journey.studio.shapeName.sparkle': 'Espurna',
'journey.studio.shapeName.star-rounded': 'Estrella arrodonida',
'journey.studio.shapeName.star-twinkle': 'Estrella brillant',
'journey.studio.shapeName.arrow-right': 'Fletxa cap a la dreta',
'journey.studio.shapeName.arrow-left': "Fletxa cap a l'esquerra",
'journey.studio.shapeName.arrow-up': 'Fletxa cap amunt',
'journey.studio.shapeName.arrow-down': 'Fletxa cap avall',
'journey.studio.shapeName.arrow-both': 'Fletxa doble',
'journey.studio.shapeName.chevron-right': 'Xevró cap a la dreta',
'journey.studio.shapeName.chevron-left': "Xevró cap a l'esquerra",
'journey.studio.shapeName.arrow-bent': 'Fletxa doblegada',
'journey.studio.shapeName.arrow-curve': 'Fletxa corba',
'journey.studio.shapeName.arrow-circle': 'Fletxa circular',
'journey.studio.shapeName.arrow-notched': 'Fletxa amb osca',
'journey.studio.shapeName.arrow-thin': 'Fletxa fina',
'journey.studio.shapeName.bubble-round': 'Globus de diàleg rodó',
'journey.studio.shapeName.bubble-square': 'Globus de diàleg quadrat',
'journey.studio.shapeName.bubble-oval': 'Globus de diàleg oval',
'journey.studio.shapeName.bubble-think': 'Globus de pensament',
'journey.studio.shapeName.bubble-shout': 'Globus de crit',
'journey.studio.shapeName.bubble-side': 'Globus de diàleg lateral',
'journey.studio.shapeName.bubble-capsule': 'Globus de diàleg en càpsula',
'journey.studio.shapeName.pin': 'Xinxeta de mapa',
'journey.studio.shapeName.compass': 'Brúixola',
'journey.studio.shapeName.mountain': 'Muntanya',
'journey.studio.shapeName.sun': 'Sol',
'journey.studio.shapeName.moon': 'Lluna',
'journey.studio.shapeName.cloud': 'Núvol',
'journey.studio.shapeName.cloud-puffy': 'Núvol esponjós',
'journey.studio.shapeName.drop': 'Gota',
'journey.studio.shapeName.wave': 'Ona',
'journey.studio.shapeName.leaf': 'Fulla',
'journey.studio.shapeName.shield': 'Escut',
'journey.studio.shapeName.ticket': 'Entrada',
'journey.studio.shapeName.heart': 'Cor',
'journey.studio.shapeName.flower-5': 'Flor de cinc pètals',
'journey.studio.shapeName.flower-6': 'Flor de sis pètals',
'journey.studio.shapeName.cross': 'Creu',
'journey.studio.shapeName.plus': 'Més',
'journey.studio.shapeName.gear': 'Engranatge',
'journey.studio.shapeName.blob-1': 'Taca 1',
'journey.studio.shapeName.blob-2': 'Taca 2',
'journey.studio.shapeName.blob-3': 'Taca 3',
'journey.studio.shapeName.blob-4': 'Taca 4',
'journey.studio.shapeName.heart-round': 'Cor rodó',
'journey.studio.shapeName.heart-pair': 'Dos cors',
'journey.studio.shapeName.flower-8': 'Flor de vuit pètals',
'journey.studio.shapeName.banner-ribbon': 'Banderola de cinta',
'journey.studio.shapeName.banner-pennant': 'Banderí',
'journey.studio.shapeName.banner-bookmark': 'Punt de llibre',
'journey.studio.shapeName.banner-flag': 'Banderola de bandera',
'journey.studio.shapeName.ribbon-ends': 'Cinta amb extrems plegats',
'journey.studio.shapeName.ribbon-arc': 'Cinta arquejada',
'journey.studio.shapeName.wavy-band': 'Banda ondulada',
'journey.studio.shapeName.torn-strip': 'Tira estripada',
'journey.studio.shapeName.arch-tall': 'Arc alt',
'journey.studio.shapeName.arch-pointed': 'Arc apuntat',
'journey.studio.shapeName.arch-double': 'Arc doble',
'journey.studio.shapeName.arch-half': 'Mig arc',
'journey.studio.shapeName.arch-rainbow': 'Arc de Sant Martí',
'journey.studio.shapeName.leaf-palm': 'Fulla de palmera',
'journey.studio.shapeName.leaf-oak': 'Fulla de roure',
'journey.studio.shapeName.leaf-long': 'Fulla llarga',
'journey.studio.shapeName.leaf-monstera': 'Fulla de monstera',
'journey.studio.shapeName.coral-1': 'Corall 1',
'journey.studio.shapeName.coral-2': 'Corall 2',
'journey.studio.shapeName.seaweed': 'Alga',
'journey.studio.shapeName.star-organic': 'Estrella orgànica',
'journey.studio.shapeName.sun-cutout': 'Sol retallat',
'journey.studio.shapeName.organic-1': 'Forma orgànica 1',
'journey.studio.shapeName.organic-2': 'Forma orgànica 2',
'journey.studio.shapeName.organic-3': 'Forma orgànica 3',
'journey.studio.shapeName.stamp-rect': 'Segell rectangular',
'journey.studio.shapeName.stamp-circle': 'Segell rodó',
'journey.studio.shapeName.postmark': 'Mata-segells',
'journey.studio.shapeName.rosette': 'Roseta',
'journey.studio.shapeName.scallop-circle': 'Cercle festonejat',
'journey.studio.shapeName.badge-shield': "Insígnia d'escut",
'journey.studio.shapeName.label-tab': "Pestanya d'etiqueta",
'journey.studio.shapeName.ticket-stub': "Taló d'entrada",
'journey.studio.shapeName.sunburst-12': 'Raigs de sol, 12 raigs',
'journey.studio.shapeName.sunburst-24': 'Raigs de sol, 24 raigs',
'journey.studio.shapeName.frame-notched': 'Marc amb osques',
'journey.studio.shapeName.frame-chamfer': 'Marc xamfranat',
'journey.studio.shapeName.frame-cushion': 'Marc coixí',
'journey.studio.shapeName.flourish-scroll': 'Floritura en volutes',
'journey.studio.shapeName.laurel': 'Llorer',
'journey.studio.shapeName.fleuron': 'Floró',
'journey.studio.shapeName.corner-flourish': 'Floritura de cantonada',
};
export default studioShapes;
+12
View File
@@ -175,6 +175,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'Corona de llorer',
'journey.studio.sticker.cornerFlourish': 'Cantonada amb floritura',
'journey.studio.sticker.cornerScroll': 'Cantonada amb volutes',
'journey.studio.sticker.parthenon': 'Partenó',
'journey.studio.sticker.milanDuomo': 'Catedral de Milà',
'journey.studio.sticker.canalHouses': 'Cases del canal',
'journey.studio.sticker.gondola': 'Góndola',
'journey.studio.sticker.lisbonTram': 'Tramvia de Lisboa',
'journey.studio.sticker.celticHarp': 'Arpa cèltica',
'journey.studio.sticker.tiger': 'Tigre',
'journey.studio.sticker.monkey': 'Mona',
'journey.studio.sticker.camel': 'Camell',
'journey.studio.sticker.doodlePalm': 'Palmera dibuixada',
'journey.studio.sticker.doodleWaves': 'Onades dibuixades',
'journey.studio.sticker.doodleSunRays': 'Sol naixent dibuixat',
};
export default studioStickers;
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'Šupiny',
'journey.studio.pattern.sunburst': 'Paprsky',
'journey.studio.pattern.confetti': 'Konfety',
'journey.studio.pattern.linen': 'Plátno',
'journey.studio.pattern.grain': 'Zrno papíru',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'Dvojklikem odeberete',
'assistant.card.book.finding.hairline': 'Čára příliš tenká pro tisk',
'assistant.card.book.finding.edge_unused': 'Okraj, který tento tvar nepřijme',
'journey.studio.shapeName.rect': 'Obdélník',
'journey.studio.shapeName.squircle': 'Zaoblený čtverec',
'journey.studio.shapeName.ellipse': 'Elipsa',
'journey.studio.shapeName.capsule': 'Kapsle',
'journey.studio.shapeName.triangle': 'Trojúhelník',
'journey.studio.shapeName.triangle-down': 'Trojúhelník dolů',
'journey.studio.shapeName.diamond': 'Kosočtverec',
'journey.studio.shapeName.parallelogram': 'Rovnoběžník',
'journey.studio.shapeName.trapezoid': 'Lichoběžník',
'journey.studio.shapeName.arch': 'Oblouk',
'journey.studio.shapeName.half-circle': 'Půlkruh',
'journey.studio.shapeName.quarter-circle': 'Čtvrtkruh',
'journey.studio.shapeName.pentagon': 'Pětiúhelník',
'journey.studio.shapeName.hexagon': 'Šestiúhelník',
'journey.studio.shapeName.hexagon-flat': 'Plochý šestiúhelník',
'journey.studio.shapeName.heptagon': 'Sedmiúhelník',
'journey.studio.shapeName.octagon': 'Osmiúhelník',
'journey.studio.shapeName.star-4': 'Čtyřcípá hvězda',
'journey.studio.shapeName.star-5': 'Pěticípá hvězda',
'journey.studio.shapeName.star-6': 'Šestícípá hvězda',
'journey.studio.shapeName.star-8': 'Osmicípá hvězda',
'journey.studio.shapeName.star-12': 'Dvanáctícípá hvězda',
'journey.studio.shapeName.burst': 'Výbuch',
'journey.studio.shapeName.seal': 'Pečeť',
'journey.studio.shapeName.sparkle': 'Jiskra',
'journey.studio.shapeName.star-rounded': 'Zaoblená hvězda',
'journey.studio.shapeName.star-twinkle': 'Třpytivá hvězda',
'journey.studio.shapeName.arrow-right': 'Šipka doprava',
'journey.studio.shapeName.arrow-left': 'Šipka doleva',
'journey.studio.shapeName.arrow-up': 'Šipka nahoru',
'journey.studio.shapeName.arrow-down': 'Šipka dolů',
'journey.studio.shapeName.arrow-both': 'Dvojitá šipka',
'journey.studio.shapeName.chevron-right': 'Chevron doprava',
'journey.studio.shapeName.chevron-left': 'Chevron doleva',
'journey.studio.shapeName.arrow-bent': 'Zahnutá šipka',
'journey.studio.shapeName.arrow-curve': 'Obloukovitá šipka',
'journey.studio.shapeName.arrow-circle': 'Kruhová šipka',
'journey.studio.shapeName.arrow-notched': 'Šipka s výřezem',
'journey.studio.shapeName.arrow-thin': 'Tenká šipka',
'journey.studio.shapeName.bubble-round': 'Kulatá bublina',
'journey.studio.shapeName.bubble-square': 'Hranatá bublina',
'journey.studio.shapeName.bubble-oval': 'Oválná bublina',
'journey.studio.shapeName.bubble-think': 'Myšlenková bublina',
'journey.studio.shapeName.bubble-shout': 'Bublina s výkřikem',
'journey.studio.shapeName.bubble-side': 'Boční bublina',
'journey.studio.shapeName.bubble-capsule': 'Kapslová bublina',
'journey.studio.shapeName.pin': 'Špendlík mapy',
'journey.studio.shapeName.compass': 'Kompas',
'journey.studio.shapeName.mountain': 'Hora',
'journey.studio.shapeName.sun': 'Slunce',
'journey.studio.shapeName.moon': 'Měsíc',
'journey.studio.shapeName.cloud': 'Mrak',
'journey.studio.shapeName.cloud-puffy': 'Nadýchaný mrak',
'journey.studio.shapeName.drop': 'Kapka',
'journey.studio.shapeName.wave': 'Vlna',
'journey.studio.shapeName.leaf': 'List',
'journey.studio.shapeName.shield': 'Štít',
'journey.studio.shapeName.ticket': 'Vstupenka',
'journey.studio.shapeName.heart': 'Srdce',
'journey.studio.shapeName.flower-5': 'Pětilistý květ',
'journey.studio.shapeName.flower-6': 'Šestilistý květ',
'journey.studio.shapeName.cross': 'Kříž',
'journey.studio.shapeName.plus': 'Plus', // same-as-en
'journey.studio.shapeName.gear': 'Ozubené kolo',
'journey.studio.shapeName.blob-1': 'Skvrna 1',
'journey.studio.shapeName.blob-2': 'Skvrna 2',
'journey.studio.shapeName.blob-3': 'Skvrna 3',
'journey.studio.shapeName.blob-4': 'Skvrna 4',
'journey.studio.shapeName.heart-round': 'Kulaté srdce',
'journey.studio.shapeName.heart-pair': 'Dvě srdce',
'journey.studio.shapeName.flower-8': 'Osmilistý květ',
'journey.studio.shapeName.banner-ribbon': 'Stuhový banner',
'journey.studio.shapeName.banner-pennant': 'Praporek',
'journey.studio.shapeName.banner-bookmark': 'Záložka',
'journey.studio.shapeName.banner-flag': 'Vlajkový banner',
'journey.studio.shapeName.ribbon-ends': 'Stuha se zahnutými konci',
'journey.studio.shapeName.ribbon-arc': 'Obloukovitá stuha',
'journey.studio.shapeName.wavy-band': 'Vlnitý pás',
'journey.studio.shapeName.torn-strip': 'Roztrhaný proužek',
'journey.studio.shapeName.arch-tall': 'Vysoký oblouk',
'journey.studio.shapeName.arch-pointed': 'Lomený oblouk',
'journey.studio.shapeName.arch-double': 'Dvojitý oblouk',
'journey.studio.shapeName.arch-half': 'Půloblouk',
'journey.studio.shapeName.arch-rainbow': 'Duhový oblouk',
'journey.studio.shapeName.leaf-palm': 'Palmový list',
'journey.studio.shapeName.leaf-oak': 'Dubový list',
'journey.studio.shapeName.leaf-long': 'Dlouhý list',
'journey.studio.shapeName.leaf-monstera': 'List monstery',
'journey.studio.shapeName.coral-1': 'Korál 1',
'journey.studio.shapeName.coral-2': 'Korál 2',
'journey.studio.shapeName.seaweed': 'Řasa',
'journey.studio.shapeName.star-organic': 'Organická hvězda',
'journey.studio.shapeName.sun-cutout': 'Vystřižené slunce',
'journey.studio.shapeName.organic-1': 'Organický tvar 1',
'journey.studio.shapeName.organic-2': 'Organický tvar 2',
'journey.studio.shapeName.organic-3': 'Organický tvar 3',
'journey.studio.shapeName.stamp-rect': 'Hranaté razítko',
'journey.studio.shapeName.stamp-circle': 'Kulaté razítko',
'journey.studio.shapeName.postmark': 'Poštovní razítko',
'journey.studio.shapeName.rosette': 'Rozeta',
'journey.studio.shapeName.scallop-circle': 'Vroubkovaný kruh',
'journey.studio.shapeName.badge-shield': 'Štítový odznak',
'journey.studio.shapeName.label-tab': 'Štítek se záložkou',
'journey.studio.shapeName.ticket-stub': 'Útržek vstupenky',
'journey.studio.shapeName.sunburst-12': 'Paprsky slunce, 12',
'journey.studio.shapeName.sunburst-24': 'Paprsky slunce, 24',
'journey.studio.shapeName.frame-notched': 'Rám s výřezy',
'journey.studio.shapeName.frame-chamfer': 'Rám se zkosenými rohy',
'journey.studio.shapeName.frame-cushion': 'Polštářový rám',
'journey.studio.shapeName.flourish-scroll': 'Ozdobný svitek',
'journey.studio.shapeName.laurel': 'Vavřín',
'journey.studio.shapeName.fleuron': 'Fleuron', // same-as-en
'journey.studio.shapeName.corner-flourish': 'Rohová ozdoba',
};
export default studioShapes;
+12
View File
@@ -174,6 +174,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'Vavřínový věnec',
'journey.studio.sticker.cornerFlourish': 'Ozdobný roh',
'journey.studio.sticker.cornerScroll': 'Roh s volutami',
'journey.studio.sticker.parthenon': 'Parthenón',
'journey.studio.sticker.milanDuomo': 'Milánský dóm',
'journey.studio.sticker.canalHouses': 'Domy u kanálu',
'journey.studio.sticker.gondola': 'Gondola', // same-as-en
'journey.studio.sticker.lisbonTram': 'Lisabonská tramvaj',
'journey.studio.sticker.celticHarp': 'Keltská harfa',
'journey.studio.sticker.tiger': 'Tygr',
'journey.studio.sticker.monkey': 'Opice',
'journey.studio.sticker.camel': 'Velbloud',
'journey.studio.sticker.doodlePalm': 'Kreslená palma',
'journey.studio.sticker.doodleWaves': 'Kreslené vlny',
'journey.studio.sticker.doodleSunRays': 'Kreslené vycházející slunce',
};
export default studioStickers;
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'Schuppen',
'journey.studio.pattern.sunburst': 'Strahlen',
'journey.studio.pattern.confetti': 'Konfetti',
'journey.studio.pattern.linen': 'Leinen',
'journey.studio.pattern.grain': 'Papierkorn',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'Doppelklick zum Entfernen',
'assistant.card.book.finding.hairline': 'Eine Linie, zu dünn für den Druck',
'assistant.card.book.finding.edge_unused': 'Ein Rand, den diese Form nicht annimmt',
'journey.studio.shapeName.rect': 'Rechteck',
'journey.studio.shapeName.squircle': 'Abgerundetes Quadrat',
'journey.studio.shapeName.ellipse': 'Ellipse', // same-as-en
'journey.studio.shapeName.capsule': 'Kapsel',
'journey.studio.shapeName.triangle': 'Dreieck',
'journey.studio.shapeName.triangle-down': 'Dreieck nach unten',
'journey.studio.shapeName.diamond': 'Raute',
'journey.studio.shapeName.parallelogram': 'Parallelogramm',
'journey.studio.shapeName.trapezoid': 'Trapez',
'journey.studio.shapeName.arch': 'Bogen',
'journey.studio.shapeName.half-circle': 'Halbkreis',
'journey.studio.shapeName.quarter-circle': 'Viertelkreis',
'journey.studio.shapeName.pentagon': 'Fünfeck',
'journey.studio.shapeName.hexagon': 'Sechseck',
'journey.studio.shapeName.hexagon-flat': 'Flaches Sechseck',
'journey.studio.shapeName.heptagon': 'Siebeneck',
'journey.studio.shapeName.octagon': 'Achteck',
'journey.studio.shapeName.star-4': 'Vierzackiger Stern',
'journey.studio.shapeName.star-5': 'Fünfzackiger Stern',
'journey.studio.shapeName.star-6': 'Sechszackiger Stern',
'journey.studio.shapeName.star-8': 'Achtzackiger Stern',
'journey.studio.shapeName.star-12': 'Zwölfzackiger Stern',
'journey.studio.shapeName.burst': 'Explosion',
'journey.studio.shapeName.seal': 'Siegel',
'journey.studio.shapeName.sparkle': 'Funkeln',
'journey.studio.shapeName.star-rounded': 'Runder Stern',
'journey.studio.shapeName.star-twinkle': 'Funkelnder Stern',
'journey.studio.shapeName.arrow-right': 'Pfeil nach rechts',
'journey.studio.shapeName.arrow-left': 'Pfeil nach links',
'journey.studio.shapeName.arrow-up': 'Pfeil nach oben',
'journey.studio.shapeName.arrow-down': 'Pfeil nach unten',
'journey.studio.shapeName.arrow-both': 'Doppelpfeil',
'journey.studio.shapeName.chevron-right': 'Winkel nach rechts',
'journey.studio.shapeName.chevron-left': 'Winkel nach links',
'journey.studio.shapeName.arrow-bent': 'Geknickter Pfeil',
'journey.studio.shapeName.arrow-curve': 'Geschwungener Pfeil',
'journey.studio.shapeName.arrow-circle': 'Kreispfeil',
'journey.studio.shapeName.arrow-notched': 'Gekerbter Pfeil',
'journey.studio.shapeName.arrow-thin': 'Dünner Pfeil',
'journey.studio.shapeName.bubble-round': 'Runde Sprechblase',
'journey.studio.shapeName.bubble-square': 'Eckige Sprechblase',
'journey.studio.shapeName.bubble-oval': 'Ovale Sprechblase',
'journey.studio.shapeName.bubble-think': 'Gedankenblase',
'journey.studio.shapeName.bubble-shout': 'Schreiblase',
'journey.studio.shapeName.bubble-side': 'Seitliche Sprechblase',
'journey.studio.shapeName.bubble-capsule': 'Kapsel-Sprechblase',
'journey.studio.shapeName.pin': 'Kartenmarkierung',
'journey.studio.shapeName.compass': 'Kompass',
'journey.studio.shapeName.mountain': 'Berg',
'journey.studio.shapeName.sun': 'Sonne',
'journey.studio.shapeName.moon': 'Mond',
'journey.studio.shapeName.cloud': 'Wolke',
'journey.studio.shapeName.cloud-puffy': 'Bauschige Wolke',
'journey.studio.shapeName.drop': 'Tropfen',
'journey.studio.shapeName.wave': 'Welle',
'journey.studio.shapeName.leaf': 'Blatt',
'journey.studio.shapeName.shield': 'Schild',
'journey.studio.shapeName.ticket': 'Ticket', // same-as-en
'journey.studio.shapeName.heart': 'Herz',
'journey.studio.shapeName.flower-5': 'Fünfblättrige Blume',
'journey.studio.shapeName.flower-6': 'Sechsblättrige Blume',
'journey.studio.shapeName.cross': 'Kreuz',
'journey.studio.shapeName.plus': 'Plus', // same-as-en
'journey.studio.shapeName.gear': 'Zahnrad',
'journey.studio.shapeName.blob-1': 'Blob 1', // same-as-en
'journey.studio.shapeName.blob-2': 'Blob 2', // same-as-en
'journey.studio.shapeName.blob-3': 'Blob 3', // same-as-en
'journey.studio.shapeName.blob-4': 'Blob 4', // same-as-en
'journey.studio.shapeName.heart-round': 'Rundes Herz',
'journey.studio.shapeName.heart-pair': 'Zwei Herzen',
'journey.studio.shapeName.flower-8': 'Achtblättrige Blume',
'journey.studio.shapeName.banner-ribbon': 'Bandbanner',
'journey.studio.shapeName.banner-pennant': 'Wimpel',
'journey.studio.shapeName.banner-bookmark': 'Lesezeichen',
'journey.studio.shapeName.banner-flag': 'Fahnenbanner',
'journey.studio.shapeName.ribbon-ends': 'Band mit gefalteten Enden',
'journey.studio.shapeName.ribbon-arc': 'Gebogenes Band',
'journey.studio.shapeName.wavy-band': 'Welliges Band',
'journey.studio.shapeName.torn-strip': 'Abgerissener Streifen',
'journey.studio.shapeName.arch-tall': 'Hoher Bogen',
'journey.studio.shapeName.arch-pointed': 'Spitzbogen',
'journey.studio.shapeName.arch-double': 'Doppelbogen',
'journey.studio.shapeName.arch-half': 'Halber Bogen',
'journey.studio.shapeName.arch-rainbow': 'Regenbogen',
'journey.studio.shapeName.leaf-palm': 'Palmenblatt',
'journey.studio.shapeName.leaf-oak': 'Eichenblatt',
'journey.studio.shapeName.leaf-long': 'Langes Blatt',
'journey.studio.shapeName.leaf-monstera': 'Monstera-Blatt',
'journey.studio.shapeName.coral-1': 'Koralle 1',
'journey.studio.shapeName.coral-2': 'Koralle 2',
'journey.studio.shapeName.seaweed': 'Seetang',
'journey.studio.shapeName.star-organic': 'Organischer Stern',
'journey.studio.shapeName.sun-cutout': 'Ausgeschnittene Sonne',
'journey.studio.shapeName.organic-1': 'Organische Form 1',
'journey.studio.shapeName.organic-2': 'Organische Form 2',
'journey.studio.shapeName.organic-3': 'Organische Form 3',
'journey.studio.shapeName.stamp-rect': 'Eckiger Stempel',
'journey.studio.shapeName.stamp-circle': 'Runder Stempel',
'journey.studio.shapeName.postmark': 'Poststempel',
'journey.studio.shapeName.rosette': 'Rosette', // same-as-en
'journey.studio.shapeName.scallop-circle': 'Wellenkreis',
'journey.studio.shapeName.badge-shield': 'Wappen-Abzeichen',
'journey.studio.shapeName.label-tab': 'Etikett',
'journey.studio.shapeName.ticket-stub': 'Ticketabschnitt',
'journey.studio.shapeName.sunburst-12': 'Sonnenstrahlen, 12 Strahlen',
'journey.studio.shapeName.sunburst-24': 'Sonnenstrahlen, 24 Strahlen',
'journey.studio.shapeName.frame-notched': 'Gekerbter Rahmen',
'journey.studio.shapeName.frame-chamfer': 'Rahmen mit Fase',
'journey.studio.shapeName.frame-cushion': 'Kissenrahmen',
'journey.studio.shapeName.flourish-scroll': 'Schnörkel',
'journey.studio.shapeName.laurel': 'Lorbeer',
'journey.studio.shapeName.fleuron': 'Zierblatt',
'journey.studio.shapeName.corner-flourish': 'Eckschnörkel',
};
export default studioShapes;
+12
View File
@@ -175,6 +175,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'Lorbeerkranz',
'journey.studio.sticker.cornerFlourish': 'Eckschnörkel',
'journey.studio.sticker.cornerScroll': 'Eckornament',
'journey.studio.sticker.parthenon': 'Parthenon', // same-as-en
'journey.studio.sticker.milanDuomo': 'Mailänder Dom',
'journey.studio.sticker.canalHouses': 'Grachtenhäuser',
'journey.studio.sticker.gondola': 'Gondel',
'journey.studio.sticker.lisbonTram': 'Straßenbahn Lissabon',
'journey.studio.sticker.celticHarp': 'Keltische Harfe',
'journey.studio.sticker.tiger': 'Tiger', // same-as-en
'journey.studio.sticker.monkey': 'Affe',
'journey.studio.sticker.camel': 'Kamel',
'journey.studio.sticker.doodlePalm': 'Palme, gekritzelt',
'journey.studio.sticker.doodleWaves': 'Wellen, gekritzelt',
'journey.studio.sticker.doodleSunRays': 'Sonnenaufgang, gekritzelt',
};
export default studioStickers;
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'Scales',
'journey.studio.pattern.sunburst': 'Sunburst',
'journey.studio.pattern.confetti': 'Confetti',
'journey.studio.pattern.linen': 'Linen',
'journey.studio.pattern.grain': 'Paper grain',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'Double-click to remove',
'assistant.card.book.finding.hairline': 'A line too thin to print',
'assistant.card.book.finding.edge_unused': 'An edge this shape does not take',
'journey.studio.shapeName.rect': 'Rectangle',
'journey.studio.shapeName.squircle': 'Rounded square',
'journey.studio.shapeName.ellipse': 'Ellipse',
'journey.studio.shapeName.capsule': 'Capsule',
'journey.studio.shapeName.triangle': 'Triangle',
'journey.studio.shapeName.triangle-down': 'Triangle pointing down',
'journey.studio.shapeName.diamond': 'Diamond',
'journey.studio.shapeName.parallelogram': 'Parallelogram',
'journey.studio.shapeName.trapezoid': 'Trapezoid',
'journey.studio.shapeName.arch': 'Arch',
'journey.studio.shapeName.half-circle': 'Half circle',
'journey.studio.shapeName.quarter-circle': 'Quarter circle',
'journey.studio.shapeName.pentagon': 'Pentagon',
'journey.studio.shapeName.hexagon': 'Hexagon',
'journey.studio.shapeName.hexagon-flat': 'Flat hexagon',
'journey.studio.shapeName.heptagon': 'Heptagon',
'journey.studio.shapeName.octagon': 'Octagon',
'journey.studio.shapeName.star-4': 'Four-pointed star',
'journey.studio.shapeName.star-5': 'Five-pointed star',
'journey.studio.shapeName.star-6': 'Six-pointed star',
'journey.studio.shapeName.star-8': 'Eight-pointed star',
'journey.studio.shapeName.star-12': 'Twelve-pointed star',
'journey.studio.shapeName.burst': 'Burst',
'journey.studio.shapeName.seal': 'Seal',
'journey.studio.shapeName.sparkle': 'Sparkle',
'journey.studio.shapeName.star-rounded': 'Rounded star',
'journey.studio.shapeName.star-twinkle': 'Twinkling star',
'journey.studio.shapeName.arrow-right': 'Arrow right',
'journey.studio.shapeName.arrow-left': 'Arrow left',
'journey.studio.shapeName.arrow-up': 'Arrow up',
'journey.studio.shapeName.arrow-down': 'Arrow down',
'journey.studio.shapeName.arrow-both': 'Double arrow',
'journey.studio.shapeName.chevron-right': 'Chevron right',
'journey.studio.shapeName.chevron-left': 'Chevron left',
'journey.studio.shapeName.arrow-bent': 'Bent arrow',
'journey.studio.shapeName.arrow-curve': 'Curved arrow',
'journey.studio.shapeName.arrow-circle': 'Circular arrow',
'journey.studio.shapeName.arrow-notched': 'Notched arrow',
'journey.studio.shapeName.arrow-thin': 'Thin arrow',
'journey.studio.shapeName.bubble-round': 'Round speech bubble',
'journey.studio.shapeName.bubble-square': 'Square speech bubble',
'journey.studio.shapeName.bubble-oval': 'Oval speech bubble',
'journey.studio.shapeName.bubble-think': 'Thought bubble',
'journey.studio.shapeName.bubble-shout': 'Shout bubble',
'journey.studio.shapeName.bubble-side': 'Speech bubble from the side',
'journey.studio.shapeName.bubble-capsule': 'Capsule speech bubble',
'journey.studio.shapeName.pin': 'Map pin',
'journey.studio.shapeName.compass': 'Compass',
'journey.studio.shapeName.mountain': 'Mountain',
'journey.studio.shapeName.sun': 'Sun',
'journey.studio.shapeName.moon': 'Moon',
'journey.studio.shapeName.cloud': 'Cloud',
'journey.studio.shapeName.cloud-puffy': 'Puffy cloud',
'journey.studio.shapeName.drop': 'Drop',
'journey.studio.shapeName.wave': 'Wave',
'journey.studio.shapeName.leaf': 'Leaf',
'journey.studio.shapeName.shield': 'Shield',
'journey.studio.shapeName.ticket': 'Ticket',
'journey.studio.shapeName.heart': 'Heart',
'journey.studio.shapeName.flower-5': 'Five-petal flower',
'journey.studio.shapeName.flower-6': 'Six-petal flower',
'journey.studio.shapeName.cross': 'Cross',
'journey.studio.shapeName.plus': 'Plus',
'journey.studio.shapeName.gear': 'Gear',
'journey.studio.shapeName.blob-1': 'Blob 1',
'journey.studio.shapeName.blob-2': 'Blob 2',
'journey.studio.shapeName.blob-3': 'Blob 3',
'journey.studio.shapeName.blob-4': 'Blob 4',
'journey.studio.shapeName.heart-round': 'Round heart',
'journey.studio.shapeName.heart-pair': 'Two hearts',
'journey.studio.shapeName.flower-8': 'Eight-petal flower',
'journey.studio.shapeName.banner-ribbon': 'Ribbon banner',
'journey.studio.shapeName.banner-pennant': 'Pennant',
'journey.studio.shapeName.banner-bookmark': 'Bookmark',
'journey.studio.shapeName.banner-flag': 'Flag banner',
'journey.studio.shapeName.ribbon-ends': 'Ribbon with folded ends',
'journey.studio.shapeName.ribbon-arc': 'Arched ribbon',
'journey.studio.shapeName.wavy-band': 'Wavy band',
'journey.studio.shapeName.torn-strip': 'Torn strip',
'journey.studio.shapeName.arch-tall': 'Tall arch',
'journey.studio.shapeName.arch-pointed': 'Pointed arch',
'journey.studio.shapeName.arch-double': 'Double arch',
'journey.studio.shapeName.arch-half': 'Half arch',
'journey.studio.shapeName.arch-rainbow': 'Rainbow arch',
'journey.studio.shapeName.leaf-palm': 'Palm leaf',
'journey.studio.shapeName.leaf-oak': 'Oak leaf',
'journey.studio.shapeName.leaf-long': 'Long leaf',
'journey.studio.shapeName.leaf-monstera': 'Monstera leaf',
'journey.studio.shapeName.coral-1': 'Coral 1',
'journey.studio.shapeName.coral-2': 'Coral 2',
'journey.studio.shapeName.seaweed': 'Seaweed',
'journey.studio.shapeName.star-organic': 'Organic star',
'journey.studio.shapeName.sun-cutout': 'Cut-out sun',
'journey.studio.shapeName.organic-1': 'Organic shape 1',
'journey.studio.shapeName.organic-2': 'Organic shape 2',
'journey.studio.shapeName.organic-3': 'Organic shape 3',
'journey.studio.shapeName.stamp-rect': 'Rectangular stamp',
'journey.studio.shapeName.stamp-circle': 'Round stamp',
'journey.studio.shapeName.postmark': 'Postmark',
'journey.studio.shapeName.rosette': 'Rosette',
'journey.studio.shapeName.scallop-circle': 'Scalloped circle',
'journey.studio.shapeName.badge-shield': 'Shield badge',
'journey.studio.shapeName.label-tab': 'Label tab',
'journey.studio.shapeName.ticket-stub': 'Ticket stub',
'journey.studio.shapeName.sunburst-12': 'Sunburst, 12 rays',
'journey.studio.shapeName.sunburst-24': 'Sunburst, 24 rays',
'journey.studio.shapeName.frame-notched': 'Notched frame',
'journey.studio.shapeName.frame-chamfer': 'Chamfered frame',
'journey.studio.shapeName.frame-cushion': 'Cushion frame',
'journey.studio.shapeName.flourish-scroll': 'Scroll flourish',
'journey.studio.shapeName.laurel': 'Laurel',
'journey.studio.shapeName.fleuron': 'Fleuron',
'journey.studio.shapeName.corner-flourish': 'Corner flourish',
};
export default studioShapes;
+12
View File
@@ -175,6 +175,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'Laurel wreath',
'journey.studio.sticker.cornerFlourish': 'Corner flourish',
'journey.studio.sticker.cornerScroll': 'Corner scroll',
'journey.studio.sticker.parthenon': 'Parthenon',
'journey.studio.sticker.milanDuomo': 'Milan Cathedral',
'journey.studio.sticker.canalHouses': 'Canal houses',
'journey.studio.sticker.gondola': 'Gondola',
'journey.studio.sticker.lisbonTram': 'Lisbon tram',
'journey.studio.sticker.celticHarp': 'Celtic harp',
'journey.studio.sticker.tiger': 'Tiger',
'journey.studio.sticker.monkey': 'Monkey',
'journey.studio.sticker.camel': 'Camel',
'journey.studio.sticker.doodlePalm': 'Palm doodle',
'journey.studio.sticker.doodleWaves': 'Wave doodle',
'journey.studio.sticker.doodleSunRays': 'Rising sun doodle',
};
export default studioStickers;
+2
View File
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
'journey.studio.pattern.scales': 'Escamas',
'journey.studio.pattern.sunburst': 'Rayos',
'journey.studio.pattern.confetti': 'Confeti',
'journey.studio.pattern.linen': 'Lino',
'journey.studio.pattern.grain': 'Grano de papel',
};
export default studioPaint;
+113
View File
@@ -66,5 +66,118 @@ const studioShapes: TranslationStrings = {
'journey.studio.removePoint': 'Doble clic para quitar',
'assistant.card.book.finding.hairline': 'Una línea demasiado fina para imprimir',
'assistant.card.book.finding.edge_unused': 'Un borde que esta forma no admite',
'journey.studio.shapeName.rect': 'Rectángulo',
'journey.studio.shapeName.squircle': 'Cuadrado redondeado',
'journey.studio.shapeName.ellipse': 'Elipse',
'journey.studio.shapeName.capsule': 'Cápsula',
'journey.studio.shapeName.triangle': 'Triángulo',
'journey.studio.shapeName.triangle-down': 'Triángulo hacia abajo',
'journey.studio.shapeName.diamond': 'Rombo',
'journey.studio.shapeName.parallelogram': 'Paralelogramo',
'journey.studio.shapeName.trapezoid': 'Trapecio',
'journey.studio.shapeName.arch': 'Arco',
'journey.studio.shapeName.half-circle': 'Semicírculo',
'journey.studio.shapeName.quarter-circle': 'Cuarto de círculo',
'journey.studio.shapeName.pentagon': 'Pentágono',
'journey.studio.shapeName.hexagon': 'Hexágono',
'journey.studio.shapeName.hexagon-flat': 'Hexágono plano',
'journey.studio.shapeName.heptagon': 'Heptágono',
'journey.studio.shapeName.octagon': 'Octógono',
'journey.studio.shapeName.star-4': 'Estrella de cuatro puntas',
'journey.studio.shapeName.star-5': 'Estrella de cinco puntas',
'journey.studio.shapeName.star-6': 'Estrella de seis puntas',
'journey.studio.shapeName.star-8': 'Estrella de ocho puntas',
'journey.studio.shapeName.star-12': 'Estrella de doce puntas',
'journey.studio.shapeName.burst': 'Explosión',
'journey.studio.shapeName.seal': 'Sello',
'journey.studio.shapeName.sparkle': 'Destello',
'journey.studio.shapeName.star-rounded': 'Estrella redondeada',
'journey.studio.shapeName.star-twinkle': 'Estrella centelleante',
'journey.studio.shapeName.arrow-right': 'Flecha a la derecha',
'journey.studio.shapeName.arrow-left': 'Flecha a la izquierda',
'journey.studio.shapeName.arrow-up': 'Flecha hacia arriba',
'journey.studio.shapeName.arrow-down': 'Flecha hacia abajo',
'journey.studio.shapeName.arrow-both': 'Flecha doble',
'journey.studio.shapeName.chevron-right': 'Chevron a la derecha',
'journey.studio.shapeName.chevron-left': 'Chevron a la izquierda',
'journey.studio.shapeName.arrow-bent': 'Flecha acodada',
'journey.studio.shapeName.arrow-curve': 'Flecha curva',
'journey.studio.shapeName.arrow-circle': 'Flecha circular',
'journey.studio.shapeName.arrow-notched': 'Flecha con muesca',
'journey.studio.shapeName.arrow-thin': 'Flecha fina',
'journey.studio.shapeName.bubble-round': 'Bocadillo redondo',
'journey.studio.shapeName.bubble-square': 'Bocadillo cuadrado',
'journey.studio.shapeName.bubble-oval': 'Bocadillo ovalado',
'journey.studio.shapeName.bubble-think': 'Bocadillo de pensamiento',
'journey.studio.shapeName.bubble-shout': 'Bocadillo de grito',
'journey.studio.shapeName.bubble-side': 'Bocadillo lateral',
'journey.studio.shapeName.bubble-capsule': 'Bocadillo en cápsula',
'journey.studio.shapeName.pin': 'Marcador de mapa',
'journey.studio.shapeName.compass': 'Brújula',
'journey.studio.shapeName.mountain': 'Montaña',
'journey.studio.shapeName.sun': 'Sol',
'journey.studio.shapeName.moon': 'Luna',
'journey.studio.shapeName.cloud': 'Nube',
'journey.studio.shapeName.cloud-puffy': 'Nube esponjosa',
'journey.studio.shapeName.drop': 'Gota',
'journey.studio.shapeName.wave': 'Ola',
'journey.studio.shapeName.leaf': 'Hoja',
'journey.studio.shapeName.shield': 'Escudo',
'journey.studio.shapeName.ticket': 'Entrada',
'journey.studio.shapeName.heart': 'Corazón',
'journey.studio.shapeName.flower-5': 'Flor de cinco pétalos',
'journey.studio.shapeName.flower-6': 'Flor de seis pétalos',
'journey.studio.shapeName.cross': 'Cruz',
'journey.studio.shapeName.plus': 'Más',
'journey.studio.shapeName.gear': 'Engranaje',
'journey.studio.shapeName.blob-1': 'Mancha 1',
'journey.studio.shapeName.blob-2': 'Mancha 2',
'journey.studio.shapeName.blob-3': 'Mancha 3',
'journey.studio.shapeName.blob-4': 'Mancha 4',
'journey.studio.shapeName.heart-round': 'Corazón redondo',
'journey.studio.shapeName.heart-pair': 'Dos corazones',
'journey.studio.shapeName.flower-8': 'Flor de ocho pétalos',
'journey.studio.shapeName.banner-ribbon': 'Banner de cinta',
'journey.studio.shapeName.banner-pennant': 'Banderín',
'journey.studio.shapeName.banner-bookmark': 'Marcapáginas',
'journey.studio.shapeName.banner-flag': 'Banner de bandera',
'journey.studio.shapeName.ribbon-ends': 'Cinta con extremos doblados',
'journey.studio.shapeName.ribbon-arc': 'Cinta en arco',
'journey.studio.shapeName.wavy-band': 'Banda ondulada',
'journey.studio.shapeName.torn-strip': 'Tira rasgada',
'journey.studio.shapeName.arch-tall': 'Arco alto',
'journey.studio.shapeName.arch-pointed': 'Arco apuntado',
'journey.studio.shapeName.arch-double': 'Arco doble',
'journey.studio.shapeName.arch-half': 'Medio arco',
'journey.studio.shapeName.arch-rainbow': 'Arco iris',
'journey.studio.shapeName.leaf-palm': 'Hoja de palma',
'journey.studio.shapeName.leaf-oak': 'Hoja de roble',
'journey.studio.shapeName.leaf-long': 'Hoja alargada',
'journey.studio.shapeName.leaf-monstera': 'Hoja de monstera',
'journey.studio.shapeName.coral-1': 'Coral 1', // same-as-en
'journey.studio.shapeName.coral-2': 'Coral 2', // same-as-en
'journey.studio.shapeName.seaweed': 'Alga',
'journey.studio.shapeName.star-organic': 'Estrella orgánica',
'journey.studio.shapeName.sun-cutout': 'Sol recortado',
'journey.studio.shapeName.organic-1': 'Forma orgánica 1',
'journey.studio.shapeName.organic-2': 'Forma orgánica 2',
'journey.studio.shapeName.organic-3': 'Forma orgánica 3',
'journey.studio.shapeName.stamp-rect': 'Sello rectangular',
'journey.studio.shapeName.stamp-circle': 'Sello redondo',
'journey.studio.shapeName.postmark': 'Matasellos',
'journey.studio.shapeName.rosette': 'Roseta',
'journey.studio.shapeName.scallop-circle': 'Círculo festoneado',
'journey.studio.shapeName.badge-shield': 'Insignia de escudo',
'journey.studio.shapeName.label-tab': 'Etiqueta con pestaña',
'journey.studio.shapeName.ticket-stub': 'Resguardo de entrada',
'journey.studio.shapeName.sunburst-12': 'Rayos de sol, 12 rayos',
'journey.studio.shapeName.sunburst-24': 'Rayos de sol, 24 rayos',
'journey.studio.shapeName.frame-notched': 'Marco con muescas',
'journey.studio.shapeName.frame-chamfer': 'Marco biselado',
'journey.studio.shapeName.frame-cushion': 'Marco acolchado',
'journey.studio.shapeName.flourish-scroll': 'Adorno en voluta',
'journey.studio.shapeName.laurel': 'Laurel', // same-as-en
'journey.studio.shapeName.fleuron': 'Flor tipográfica',
'journey.studio.shapeName.corner-flourish': 'Adorno de esquina',
};
export default studioShapes;
+12
View File
@@ -175,6 +175,18 @@ const studioStickers: TranslationStrings = {
'journey.studio.sticker.laurelWreath': 'Corona de laurel',
'journey.studio.sticker.cornerFlourish': 'Esquina con floritura',
'journey.studio.sticker.cornerScroll': 'Esquina con volutas',
'journey.studio.sticker.parthenon': 'Partenón',
'journey.studio.sticker.milanDuomo': 'Catedral de Milán',
'journey.studio.sticker.canalHouses': 'Casas del canal',
'journey.studio.sticker.gondola': 'Góndola',
'journey.studio.sticker.lisbonTram': 'Tranvía de Lisboa',
'journey.studio.sticker.celticHarp': 'Arpa celta',
'journey.studio.sticker.tiger': 'Tigre',
'journey.studio.sticker.monkey': 'Mono',
'journey.studio.sticker.camel': 'Camello',
'journey.studio.sticker.doodlePalm': 'Palmera dibujada',
'journey.studio.sticker.doodleWaves': 'Olas dibujadas',
'journey.studio.sticker.doodleSunRays': 'Sol naciente dibujado',
};
export default studioStickers;

Some files were not shown because too many files have changed in this diff Show More