diff --git a/documentation/src/components/Sights/LabelTable/LabelTable.tsx b/documentation/src/components/Sights/LabelTable/LabelTable.tsx index 48d0806a6..5cc463420 100644 --- a/documentation/src/components/Sights/LabelTable/LabelTable.tsx +++ b/documentation/src/components/Sights/LabelTable/LabelTable.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { labels } from '@monkvision/sights'; import clsx from 'clsx'; import styles from './LabelTable.module.css'; diff --git a/documentation/src/components/Sights/SightCard/SightCard.tsx b/documentation/src/components/Sights/SightCard/SightCard.tsx index fbd700f6b..1cf5c29f4 100644 --- a/documentation/src/components/Sights/SightCard/SightCard.tsx +++ b/documentation/src/components/Sights/SightCard/SightCard.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useMemo, useRef } from 'react'; +import { Fragment, useMemo, useRef } from 'react'; import { useColorMode } from '@docusaurus/theme-common'; import { labels, sights } from '@monkvision/sights'; import { Sight, VehicleDetails, VehicleModel } from '@monkvision/types'; @@ -25,6 +25,7 @@ const vehicleModelDisplayOverlays: Record< [VehicleModel.TESLAMY]: sights['teslamy-F9Nr3VtK'].overlay, [VehicleModel.TESLAMX]: sights['teslamx-aT9LpF7y'].overlay, [VehicleModel.TESLAMS]: sights['teslams-Km9XrLp5'].overlay, + [VehicleModel.TESLAC]: sights['teslac-Rx8SpL6m'].overlay, }; export interface SightCardProps { diff --git a/documentation/src/components/Sights/TopBar/TopBar.tsx b/documentation/src/components/Sights/TopBar/TopBar.tsx index eaa21317f..e534d2003 100644 --- a/documentation/src/components/Sights/TopBar/TopBar.tsx +++ b/documentation/src/components/Sights/TopBar/TopBar.tsx @@ -2,7 +2,7 @@ import { labels, sights, vehicles } from '@monkvision/sights'; import { SearchBar } from '@site/src/components'; import { SightCard } from '@site/src/components/Sights/SightCard'; import clsx from 'clsx'; -import React, { ReactElement, useRef } from 'react'; +import { ReactElement, useRef } from 'react'; import styles from './TopBar.module.css'; export interface ListItem { diff --git a/packages/common-ui-web/src/components/DynamicSVG/DynamicSVG.tsx b/packages/common-ui-web/src/components/DynamicSVG/DynamicSVG.tsx index 64747475e..46cf9fa38 100644 --- a/packages/common-ui-web/src/components/DynamicSVG/DynamicSVG.tsx +++ b/packages/common-ui-web/src/components/DynamicSVG/DynamicSVG.tsx @@ -1,5 +1,5 @@ import { SVGProps, useMemo } from 'react'; -import { DynamicSVGCustomizationFunctions, useXMLParser } from './hooks'; +import { DynamicSVGCustomizationFunctions, useSVGUniqueIds, useXMLParser } from './hooks'; import { SVGElement } from './SVGElement'; /** @@ -48,15 +48,16 @@ export interface DynamicSVGProps */ export function DynamicSVG({ svg, ...passThroughProps }: DynamicSVGProps) { const doc = useXMLParser(svg); + const uniqueDoc = useSVGUniqueIds(doc); const svgEl = useMemo(() => { - const element = doc.children[0] as SVGSVGElement; + const element = uniqueDoc.children[0] as SVGSVGElement; if (element.tagName !== 'svg') { throw new Error( `Invalid SVG string provided to the DynamicSVG component: expected tag as the first children of XML document but got <${element.tagName}>.`, ); } return element; - }, [doc]); + }, [uniqueDoc]); return ; } diff --git a/packages/common-ui-web/src/components/DynamicSVG/hooks/index.ts b/packages/common-ui-web/src/components/DynamicSVG/hooks/index.ts index 84bec8572..687bee50a 100644 --- a/packages/common-ui-web/src/components/DynamicSVG/hooks/index.ts +++ b/packages/common-ui-web/src/components/DynamicSVG/hooks/index.ts @@ -3,3 +3,4 @@ export * from './useCustomAttributes'; export * from './useInnerHTML'; export * from './useJSXTransformAttributes'; export * from './useXMLParser'; +export * from './useSVGUniqueIds'; diff --git a/packages/common-ui-web/src/components/DynamicSVG/hooks/useSVGUniqueIds.ts b/packages/common-ui-web/src/components/DynamicSVG/hooks/useSVGUniqueIds.ts new file mode 100644 index 000000000..c8fc498c8 --- /dev/null +++ b/packages/common-ui-web/src/components/DynamicSVG/hooks/useSVGUniqueIds.ts @@ -0,0 +1,96 @@ +import { useMemo } from 'react'; + +let idCounter = 0; + +/** + * Attributes that can contain URL references to SVG IDs + */ +const URL_ATTRIBUTES = [ + 'mask', + 'clip-path', + 'clipPath', + 'fill', + 'stroke', + 'filter', + 'marker-start', + 'marker-mid', + 'marker-end', + 'href', + 'xlink:href', +]; + +/** + * Makes all IDs in an SVG document unique by adding a prefix. + * This prevents ID collisions when multiple SVGs are rendered on the same page. + * + * @param doc - The parsed SVG document + * @param prefix - A unique prefix to add to all IDs + */ +function makeIdsUnique(doc: Document, prefix: string): void { + const idMap = new Map(); + + const elementsWithId = doc.querySelectorAll('[id]'); + elementsWithId.forEach((element) => { + const oldId = element.getAttribute('id'); + if (oldId) { + const newId = `${prefix}-${oldId}`; + idMap.set(oldId, newId); + element.setAttribute('id', newId); + } + }); + + const allElements = doc.querySelectorAll('*'); + allElements.forEach((element) => { + URL_ATTRIBUTES.forEach((attr) => { + const value = element.getAttribute(attr); + if (value) { + let newValue = value; + const urlPattern = /url\(#([^)]+)\)/g; + newValue = newValue.replace(urlPattern, (match, id) => { + const newId = idMap.get(id); + return newId ? `url(#${newId})` : match; + }); + + if (newValue.startsWith('#')) { + const id = newValue.substring(1); + const newId = idMap.get(id); + if (newId) { + newValue = `#${newId}`; + } + } + if (newValue !== value) { + element.setAttribute(attr, newValue); + } + } + }); + + const style = element.getAttribute('style'); + if (style) { + let newStyle = style; + const urlPattern = /url\(#([^)]+)\)/g; + newStyle = newStyle.replace(urlPattern, (match, id) => { + const newId = idMap.get(id); + return newId ? `url(#${newId})` : match; + }); + if (newStyle !== style) { + element.setAttribute('style', newStyle); + } + } + }); +} + +/** + * Hook that ensures all IDs within an SVG document are unique by adding a prefix. + * This prevents ID collisions when multiple SVGs with the same internal IDs are rendered on the same page. + * + * @param doc - The parsed SVG document + * @returns The same document with unique IDs + */ +export function useSVGUniqueIds(doc: Document): Document { + return useMemo(() => { + const uniquePrefix = `svg-${idCounter}`; + idCounter += 1; + makeIdsUnique(doc, uniquePrefix); + return doc; + }, [doc]); +} diff --git a/packages/common-ui-web/test/components/DynamicSVG/DynamicSVG.test.tsx b/packages/common-ui-web/test/components/DynamicSVG/DynamicSVG.test.tsx index 29d1aacdf..3fc5a2176 100644 --- a/packages/common-ui-web/test/components/DynamicSVG/DynamicSVG.test.tsx +++ b/packages/common-ui-web/test/components/DynamicSVG/DynamicSVG.test.tsx @@ -1,6 +1,6 @@ -import React from 'react'; jest.mock('../../../src/components/DynamicSVG/hooks', () => ({ useXMLParser: jest.fn(() => ({ children: [{ tagName: 'svg' }] })), + useSVGUniqueIds: jest.fn((doc) => doc), })); jest.mock('../../../src/components/DynamicSVG/SVGElement.tsx', () => ({ diff --git a/packages/common-ui-web/test/components/DynamicSVG/hooks/useSVGUniqueIds.test.ts b/packages/common-ui-web/test/components/DynamicSVG/hooks/useSVGUniqueIds.test.ts new file mode 100644 index 000000000..ac5711b53 --- /dev/null +++ b/packages/common-ui-web/test/components/DynamicSVG/hooks/useSVGUniqueIds.test.ts @@ -0,0 +1,106 @@ +import { renderHook } from '@testing-library/react'; +import { useSVGUniqueIds } from '../../../../src/components/DynamicSVG/hooks'; + +describe('useSVGUniqueIds hook', () => { + it('should make all IDs in an SVG unique', () => { + const svg = ` + + + + + + + + + + `; + const doc = new DOMParser().parseFromString(svg, 'text/xml'); + + const { result } = renderHook(() => useSVGUniqueIds(doc)); + const uniqueDoc = result.current; + + const clipPath = uniqueDoc.querySelector('clipPath'); + const mask = uniqueDoc.querySelector('mask'); + expect(clipPath?.getAttribute('id')).toMatch(/^svg-\d+-a$/); + expect(mask?.getAttribute('id')).toMatch(/^svg-\d+-b$/); + + const g = uniqueDoc.querySelector('g'); + const rect = uniqueDoc.querySelector('rect[mask]'); + expect(g?.getAttribute('clip-path')).toMatch(/^url\(#svg-\d+-a\)$/); + expect(rect?.getAttribute('mask')).toMatch(/^url\(#svg-\d+-b\)$/); + }); + + it('should handle multiple SVGs with same IDs independently', () => { + const svg1 = ''; + const svg2 = ''; + + const doc1 = new DOMParser().parseFromString(svg1, 'text/xml'); + const doc2 = new DOMParser().parseFromString(svg2, 'text/xml'); + + const { result: result1 } = renderHook(() => useSVGUniqueIds(doc1)); + const { result: result2 } = renderHook(() => useSVGUniqueIds(doc2)); + + const id1 = result1.current.querySelector('mask')?.getAttribute('id'); + const id2 = result2.current.querySelector('mask')?.getAttribute('id'); + + expect(id1).toBeTruthy(); + expect(id2).toBeTruthy(); + expect(id1).not.toBe(id2); + }); + + it('should handle href and xlink:href attributes', () => { + const svg = ` + + + + + + + + `; + const doc = new DOMParser().parseFromString(svg, 'text/xml'); + + const { result } = renderHook(() => useSVGUniqueIds(doc)); + const uniqueDoc = result.current; + + const gradient = uniqueDoc.querySelector('linearGradient'); + const gradientId = gradient?.getAttribute('id'); + + if (gradientId) { + expect(gradientId).toMatch(/^svg-\d+-grad1$/); + + const uses = uniqueDoc.querySelectorAll('use'); + uses.forEach((use) => { + const href = use.getAttribute('href') || use.getAttribute('xlink:href'); + if (href) { + expect(href).toBe(`#${gradientId}`); + } + }); + } else { + expect(uniqueDoc).toBeTruthy(); + } + }); + + it('should handle style attribute with url() references', () => { + const svg = ` + + + + + + + `; + const doc = new DOMParser().parseFromString(svg, 'text/xml'); + + const { result } = renderHook(() => useSVGUniqueIds(doc)); + const uniqueDoc = result.current; + + const filter = uniqueDoc.querySelector('filter'); + const filterId = filter?.getAttribute('id'); + expect(filterId).toMatch(/^svg-\d+-blur$/); + + const rect = uniqueDoc.querySelector('rect'); + const style = rect?.getAttribute('style'); + expect(style).toBe(`filter: url(#${filterId})`); + }); +}); diff --git a/packages/sights/research/data/teslac/overlays/teslac-Bv7TpK2r.svg b/packages/sights/research/data/teslac/overlays/teslac-Bv7TpK2r.svg new file mode 100644 index 000000000..c146e7083 --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Bv7TpK2r.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Cn5WmY9q.svg b/packages/sights/research/data/teslac/overlays/teslac-Cn5WmY9q.svg new file mode 100644 index 000000000..03f978dcc --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Cn5WmY9q.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Dm8RpX6z.svg b/packages/sights/research/data/teslac/overlays/teslac-Dm8RpX6z.svg new file mode 100644 index 000000000..5d13f08ed --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Dm8RpX6z.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Fk4LvT3n.svg b/packages/sights/research/data/teslac/overlays/teslac-Fk4LvT3n.svg new file mode 100644 index 000000000..d9200ac42 --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Fk4LvT3n.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Kp9NvR2w.svg b/packages/sights/research/data/teslac/overlays/teslac-Kp9NvR2w.svg new file mode 100644 index 000000000..aadf2e75a --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Kp9NvR2w.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Lm3XtY8p.svg b/packages/sights/research/data/teslac/overlays/teslac-Lm3XtY8p.svg new file mode 100644 index 000000000..cee8cd4d5 --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Lm3XtY8p.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Qr7WzK4n.svg b/packages/sights/research/data/teslac/overlays/teslac-Qr7WzK4n.svg new file mode 100644 index 000000000..24154bd5a --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Qr7WzK4n.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Rx8SpL6m.svg b/packages/sights/research/data/teslac/overlays/teslac-Rx8SpL6m.svg new file mode 100644 index 000000000..214798d5e --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Rx8SpL6m.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Tn2YpM9v.svg b/packages/sights/research/data/teslac/overlays/teslac-Tn2YpM9v.svg new file mode 100644 index 000000000..393d9b92c --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Tn2YpM9v.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Wp5RqL3x.svg b/packages/sights/research/data/teslac/overlays/teslac-Wp5RqL3x.svg new file mode 100644 index 000000000..6715a8e40 --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Wp5RqL3x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Xz6NmT4k.svg b/packages/sights/research/data/teslac/overlays/teslac-Xz6NmT4k.svg new file mode 100644 index 000000000..19a15100d --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Xz6NmT4k.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Yr9PvR7w.svg b/packages/sights/research/data/teslac/overlays/teslac-Yr9PvR7w.svg new file mode 100644 index 000000000..ca7ee52f7 --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Yr9PvR7w.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/overlays/teslac-Zq3LxN8m.svg b/packages/sights/research/data/teslac/overlays/teslac-Zq3LxN8m.svg new file mode 100644 index 000000000..2a9460622 --- /dev/null +++ b/packages/sights/research/data/teslac/overlays/teslac-Zq3LxN8m.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sights/research/data/teslac/teslac.json b/packages/sights/research/data/teslac/teslac.json new file mode 100644 index 000000000..56cbc8c1c --- /dev/null +++ b/packages/sights/research/data/teslac/teslac.json @@ -0,0 +1,171 @@ +{ + "teslac-Wp5RqL3x": { + "id": "teslac-Wp5RqL3x", + "category": "exterior", + "label": "front-low", + "overlay": "teslac-Wp5RqL3x.svg", + "vehicle": "teslac", + "tasks": ["damage_detection"], + "positioning": { + "position": 0, + "height": "low" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Wp5RqL3x.jpeg" + }, + "teslac-Lm3XtY8p": { + "id": "teslac-Lm3XtY8p", + "category": "exterior", + "label": "front-bumper-side-left", + "overlay": "teslac-Lm3XtY8p.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 25, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Lm3XtY8p.jpeg" + }, + "teslac-Rx8SpL6m": { + "id": "teslac-Rx8SpL6m", + "category": "exterior", + "label": "front-fender-left", + "overlay": "teslac-Rx8SpL6m.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 42, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Rx8SpL6m.jpeg" + }, + "teslac-Fk4LvT3n": { + "id": "teslac-Fk4LvT3n", + "category": "exterior", + "label": "front-roof-left", + "overlay": "teslac-Fk4LvT3n.svg", + "vehicle": "teslac", + "tasks": ["damage_detection"], + "positioning": { + "position": 61, + "height": "high" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Fk4LvT3n.jpeg" + }, + "teslac-Xz6NmT4k": { + "id": "teslac-Xz6NmT4k", + "category": "exterior", + "label": "lateral-full-left", + "overlay": "teslac-Xz6NmT4k.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 92, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Xz6NmT4k.jpeg" + }, + "teslac-Zq3LxN8m": { + "id": "teslac-Zq3LxN8m", + "category": "exterior", + "label": "rear-lateral-left", + "overlay": "teslac-Zq3LxN8m.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 130, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Zq3LxN8m.jpeg" + }, + "teslac-Cn5WmY9q": { + "id": "teslac-Cn5WmY9q", + "category": "exterior", + "label": "rear-left", + "overlay": "teslac-Cn5WmY9q.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 155, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Cn5WmY9q.jpeg" + }, + "teslac-Kp9NvR2w": { + "id": "teslac-Kp9NvR2w", + "category": "exterior", + "label": "rear-low", + "overlay": "teslac-Kp9NvR2w.svg", + "vehicle": "teslac", + "tasks": ["damage_detection"], + "positioning": { + "position": 180, + "height": "low" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Kp9NvR2w.jpeg" + }, + "teslac-Dm8RpX6z": { + "id": "teslac-Dm8RpX6z", + "category": "exterior", + "label": "rear-right", + "overlay": "teslac-Dm8RpX6z.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 205, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Dm8RpX6z.jpeg" + }, + "teslac-Bv7TpK2r": { + "id": "teslac-Bv7TpK2r", + "category": "exterior", + "label": "rear-lateral-right", + "overlay": "teslac-Bv7TpK2r.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 230, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Bv7TpK2r.jpeg" + }, + "teslac-Yr9PvR7w": { + "id": "teslac-Yr9PvR7w", + "category": "exterior", + "label": "lateral-full-right", + "overlay": "teslac-Yr9PvR7w.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 268, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Yr9PvR7w.jpeg" + }, + "teslac-Tn2YpM9v": { + "id": "teslac-Tn2YpM9v", + "category": "exterior", + "label": "front-fender-right", + "overlay": "teslac-Tn2YpM9v.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 317, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Tn2YpM9v.jpeg" + }, + "teslac-Qr7WzK4n": { + "id": "teslac-Qr7WzK4n", + "category": "exterior", + "label": "front-bumper-side-right", + "overlay": "teslac-Qr7WzK4n.svg", + "vehicle": "teslac", + "tasks": ["damage_detection", "wheel_analysis"], + "positioning": { + "position": 335, + "height": "mid" + }, + "referencePicture": "https://storage.googleapis.com/monk-sight-images/teslac-Qr7WzK4n.jpeg" + } +} diff --git a/packages/sights/research/data/teslac/teslac.schema.json b/packages/sights/research/data/teslac/teslac.schema.json new file mode 100644 index 000000000..29c61d39f --- /dev/null +++ b/packages/sights/research/data/teslac/teslac.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "teslac.schema", + "definitions": { + "Sight": { + "$ref": "sight.schema" + } + }, + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "teslac-[a-zA-Z\\d_-]+" + }, + "patternProperties": { + "": { + "allOf": [ + { + "$ref": "#/definitions/Sight" + } + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^teslac-[a-zA-Z\\d_-]+$" + }, + "mirror_sight": { + "type": "string", + "pattern": "^teslac-[a-zA-Z\\d_-]+$" + } + }, + "required": ["id"], + "unevaluatedProperties": false + } + }, + "unevaluatedProperties": false +} diff --git a/packages/sights/research/data/vehicles.json b/packages/sights/research/data/vehicles.json index 7a57cbaee..c507960c6 100644 --- a/packages/sights/research/data/vehicles.json +++ b/packages/sights/research/data/vehicles.json @@ -89,5 +89,11 @@ "make": "Tesla", "model": "Model X", "type": "cuv" + }, + "teslac": { + "id": "teslac", + "make": "Tesla", + "model": "Cybertruck", + "type": "pickup" } } diff --git a/packages/sights/research/schemas/subschemas/vehicle.schema.json b/packages/sights/research/schemas/subschemas/vehicle.schema.json index 5ca006ed3..0a0d89940 100644 --- a/packages/sights/research/schemas/subschemas/vehicle.schema.json +++ b/packages/sights/research/schemas/subschemas/vehicle.schema.json @@ -17,6 +17,7 @@ "teslam3", "teslamy", "teslams", - "teslamx" + "teslamx", + "teslac" ] } diff --git a/packages/sights/src/lib/data.ts b/packages/sights/src/lib/data.ts index 55bb934c8..b537fbd54 100644 --- a/packages/sights/src/lib/data.ts +++ b/packages/sights/src/lib/data.ts @@ -40,6 +40,8 @@ import teslamySightsJSON from './data/sights/teslamy.json'; import teslamxSightsJSON from './data/sights/teslamx.json'; // @ts-ignore import teslamsSightsJSON from './data/sights/teslams.json'; +// @ts-ignore +import teslacSightsJSON from './data/sights/teslac.json'; /** * Object map associating translation keys to sight labels translations. @@ -70,6 +72,7 @@ const sights = { ...teslamySightsJSON, ...teslamxSightsJSON, ...teslamsSightsJSON, + ...teslacSightsJSON, } as SightDictionary; /** diff --git a/packages/types/src/sights.ts b/packages/types/src/sights.ts index 974c39db0..9879ffcbd 100644 --- a/packages/types/src/sights.ts +++ b/packages/types/src/sights.ts @@ -141,6 +141,10 @@ export const VehicleModel = { * Tesla Model X (CUV) */ TESLAMX: 'teslamx', + /** + * Tesla Cybertruck (Pickup) + */ + TESLAC: 'teslac', } as const; /**