From d1403a61addd6e6ae2321a856f985ebe887c5be4 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Thu, 9 Apr 2026 18:59:35 -0600 Subject: [PATCH 01/21] Proposed 1.2.2 --- Changelog.md | 27 +++++++ src/app/pages/[...slug]/index.tsx | 40 +++++------ src/app/pages/assets/fonts.tsx | 13 ++-- src/app/pages/assets/index.tsx | 4 +- src/app/pages/assets/logos.tsx | 4 +- src/app/pages/foundations/colors.tsx | 2 +- .../pages/foundations/icons/[name]/index.tsx | 10 +-- src/app/pages/foundations/icons/index.tsx | 2 +- src/app/pages/foundations/index.tsx | 2 +- src/app/pages/foundations/logo.tsx | 2 +- src/app/pages/foundations/typography.tsx | 10 --- src/app/pages/index.tsx | 5 +- .../system/component/[component]/index.tsx | 4 +- src/app/pages/system/index.tsx | 4 +- .../tokens/components/[component]/index.tsx | 59 ++++++--------- .../system/tokens/foundations/colors.tsx | 62 ++++++++-------- .../system/tokens/foundations/effects.tsx | 50 ++++++------- .../system/tokens/foundations/typography.tsx | 72 +++++++++---------- src/cli.ts | 2 +- 19 files changed, 185 insertions(+), 189 deletions(-) diff --git a/Changelog.md b/Changelog.md index d8fdb400..6c2b7191 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## \[1.2.2] - 2026-04-09 + +This patch release clears ESLint failures in the Next.js app pages and aligns +several code paths with the compiler target so production builds succeed without +enabling `--downlevelIteration`. + +### Lint and code quality + +* Resolved `no-else-return`, `no-unused-vars`, `camelcase`, `prefer-const`, + `eqeqeq`, `no-nested-ternary`, `no-lonely-if`, and `no-use-before-define` + across static pages (catch-all docs, assets, foundations, home, system, and + design-token foundation pages). +* Removed or refactored unused `getStaticProps` context parameters, props, and + locals; normalized naming (e.g. font machine keys); simplified icon detail + routing query handling. +* Reordered helper components (token color/effect/typography tables, component + token previews) ahead of page components where required for declaration order. +* Converted `getComponentPreviews` to a hoisted `function` declaration and + refactored the component token table for clearer control flow. + +### Build / TypeScript compatibility + +* Avoid iterating `Map` / `Set` / `Iterable` / `matchAll` results directly in + `for...of` where the compile target requires it: use `Array.from(...)` or + equivalent helpers in the config-diff registry, snapshot diff, CSF import + parsing, and component artifact sync. + ## \[1.2.1] - 2026-04-08 This is a minor release that fixes a bug in the new config diff calculator. diff --git a/src/app/pages/[...slug]/index.tsx b/src/app/pages/[...slug]/index.tsx index 3434f8e2..95967318 100644 --- a/src/app/pages/[...slug]/index.tsx +++ b/src/app/pages/[...slug]/index.tsx @@ -41,26 +41,7 @@ export const getStaticProps: GetStaticProps = (context) => { export default function DocCatchAllPage({ content, menu, metadata, current, config }: DocumentationProps) { const bodyRef = useRef(null); - if (content) { - return ( - -
- {metadata.title} -

{metadata.description}

-
-
-
-
- - {content} - -
-
- -
-
- ); - } else { + if (!content) { return (
@@ -85,4 +66,23 @@ export default function DocCatchAllPage({ content, menu, metadata, current, conf
); } + + return ( + +
+ {metadata.title} +

{metadata.description}

+
+
+
+
+ + {content} + +
+
+ +
+
+ ); } diff --git a/src/app/pages/assets/fonts.tsx b/src/app/pages/assets/fonts.tsx index 6f17de0c..1be4caee 100644 --- a/src/app/pages/assets/fonts.tsx +++ b/src/app/pages/assets/fonts.tsx @@ -21,7 +21,7 @@ import { fetchDocPageMarkdown, FontDocumentationProps, getClientRuntimeConfig, g * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { const fonts = fs.readdirSync( path.resolve(process.env.HANDOFF_MODULE_PATH ?? '', '.handoff', `${process.env.HANDOFF_PROJECT_ID}`, 'public', 'fonts') ); @@ -45,16 +45,15 @@ export const getStaticProps: GetStaticProps = async (context) => { }; }; -const FontsPage = ({ content, menu, metadata, current, customFonts, design, config }: FontDocumentationProps) => { +const FontsPage = ({ content, menu, metadata, customFonts, design, config }: FontDocumentationProps) => { const fontFamilies: string[] = uniq(design.typography.map((type) => type.values.fontFamily)); const fontLinks: string[] = fontFamilies.map((fontFamily) => { - const machine_name = fontFamily.replace(/\s/g, ''); - const custom = customFonts.find((font) => font === machine_name); + const machineName = fontFamily.replace(/\s/g, ''); + const custom = customFonts.find((font) => font === machineName); if (custom) { - return `/fonts/${machine_name}.zip`; - } else { - return `https://fonts.google.com/specimen/${fontFamily}`; + return `/fonts/${machineName}.zip`; } + return `https://fonts.google.com/specimen/${fontFamily}`; }); return (
diff --git a/src/app/pages/assets/index.tsx b/src/app/pages/assets/index.tsx index 7d1d08e0..ae8a684c 100644 --- a/src/app/pages/assets/index.tsx +++ b/src/app/pages/assets/index.tsx @@ -18,7 +18,7 @@ import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { return { props: { ...fetchDocPageMarkdown('docs/', 'assets', `/assets`).props, @@ -27,7 +27,7 @@ export const getStaticProps: GetStaticProps = async (context) => { }; }; -const AssetsPage = ({ content, menu, metadata, current, config }: DocumentationProps) => { +const AssetsPage = ({ content, menu, metadata, config }: DocumentationProps) => { return (
diff --git a/src/app/pages/assets/logos.tsx b/src/app/pages/assets/logos.tsx index 4ee58770..e25e4ae2 100644 --- a/src/app/pages/assets/logos.tsx +++ b/src/app/pages/assets/logos.tsx @@ -17,7 +17,7 @@ import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { return { props: { ...fetchDocPageMarkdown('docs/assets/', 'logos', `/assets`).props, @@ -26,7 +26,7 @@ export const getStaticProps: GetStaticProps = async (context) => { }; }; -const AssetsLogosPage = ({ content, menu, metadata, current, config }: DocumentationProps) => { +const AssetsLogosPage = ({ content, menu, metadata, config }: DocumentationProps) => { return (
diff --git a/src/app/pages/foundations/colors.tsx b/src/app/pages/foundations/colors.tsx index da9bf5d7..683ba29a 100644 --- a/src/app/pages/foundations/colors.tsx +++ b/src/app/pages/foundations/colors.tsx @@ -22,7 +22,7 @@ import { getTokens } from '../../components/util'; * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { return { props: { ...util.fetchFoundationDocPageMarkdown('docs/foundations/', 'colors', `/foundations`).props, diff --git a/src/app/pages/foundations/icons/[name]/index.tsx b/src/app/pages/foundations/icons/[name]/index.tsx index f3455b0e..805631b8 100644 --- a/src/app/pages/foundations/icons/[name]/index.tsx +++ b/src/app/pages/foundations/icons/[name]/index.tsx @@ -58,7 +58,7 @@ export async function getStaticPaths() { * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = (context) => { +export const getStaticProps: GetStaticProps = () => { return { props: { ...fetchDocPageMarkdown('docs/foundations/', 'icons', `/foundations`).props, @@ -68,10 +68,12 @@ export const getStaticProps: GetStaticProps = (context) => { }; }; -export default function SingleIcon({ content, menu, metadata, current, config, assets }: AssetDocumentationProps) { +export default function SingleIcon({ menu, metadata, current, config, assets }: AssetDocumentationProps) { const router = useRouter(); - let { name } = router.query; - const icon = assets?.icons.find((icon) => icon.icon === name); + const nameParam = router.query.name; + const name = typeof nameParam === 'string' ? nameParam : Array.isArray(nameParam) ? nameParam[0] : undefined; + const icon = assets?.icons.find((i) => i.icon === name); + const copySvg = React.useCallback( (event) => { event.preventDefault(); diff --git a/src/app/pages/foundations/icons/index.tsx b/src/app/pages/foundations/icons/index.tsx index c3270178..3a749bfc 100644 --- a/src/app/pages/foundations/icons/index.tsx +++ b/src/app/pages/foundations/icons/index.tsx @@ -52,7 +52,7 @@ export const DisplayIcon: React.FC<{ icon: CoreTypes.IAssetObject }> = ({ icon } * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { return { props: { ...fetchDocPageMarkdown('docs/foundations/', 'icons', `/foundations`).props, diff --git a/src/app/pages/foundations/index.tsx b/src/app/pages/foundations/index.tsx index d4fcbb1c..2bfcd8c3 100644 --- a/src/app/pages/foundations/index.tsx +++ b/src/app/pages/foundations/index.tsx @@ -17,7 +17,7 @@ import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { return { props: { ...fetchDocPageMarkdown('docs/', 'foundations', `/foundations`).props, diff --git a/src/app/pages/foundations/logo.tsx b/src/app/pages/foundations/logo.tsx index 6d619c63..c7a378b4 100644 --- a/src/app/pages/foundations/logo.tsx +++ b/src/app/pages/foundations/logo.tsx @@ -36,7 +36,7 @@ import { buttonVariants } from '../../components/ui/button'; * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { return { props: { ...fetchDocPageMarkdown('docs/foundations/', 'logo', `/foundations`).props, diff --git a/src/app/pages/foundations/typography.tsx b/src/app/pages/foundations/typography.tsx index dfc29722..611557bc 100644 --- a/src/app/pages/foundations/typography.tsx +++ b/src/app/pages/foundations/typography.tsx @@ -62,16 +62,6 @@ const Typography = ({ }; }, {} as FontFamily); - const type_copy = config?.app?.type_copy ?? 'Almost before we knew it, we had left the ground.'; - - const typographyCategories = typography.reduce((acc, type) => { - if (type.name.includes('/')) { - const category = type.name.split('/')[0].trim(); - acc.add(category); - } - return acc; - }, new Set()); - return (
diff --git a/src/app/pages/index.tsx b/src/app/pages/index.tsx index d0db0211..ed0aabd0 100644 --- a/src/app/pages/index.tsx +++ b/src/app/pages/index.tsx @@ -2,7 +2,6 @@ import { ArrowRight, Component, Hexagon, Layers, Shapes } from 'lucide-react'; import { GetStaticProps } from 'next'; import Image from 'next/image'; import Link from 'next/link'; -import { useRouter } from 'next/router'; import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; @@ -21,7 +20,7 @@ import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { return { props: { ...fetchDocPageMarkdown('docs/', 'index', `/`).props, @@ -31,8 +30,6 @@ export const getStaticProps: GetStaticProps = async (context) => { }; const Home = ({ content, menu, metadata, config, current }: DocumentationProps) => { - const router = useRouter(); - return (
diff --git a/src/app/pages/system/component/[component]/index.tsx b/src/app/pages/system/component/[component]/index.tsx index 3a801938..978ecb08 100644 --- a/src/app/pages/system/component/[component]/index.tsx +++ b/src/app/pages/system/component/[component]/index.tsx @@ -110,7 +110,7 @@ export const getStaticProps = async (context) => { }; function filterPreviews(previews: Record, filter: Filter): Record { - return Object.fromEntries(Object.entries(previews).filter(([_, preview]) => evaluateFilter(preview.values, filter))); + return Object.fromEntries(Object.entries(previews).filter(([, preview]) => evaluateFilter(preview.values, filter))); } const GenericComponentPage = ({ menu, metadata, current, id, config, componentHotReloadIsAvailable, previousComponent, nextComponent }) => { @@ -123,7 +123,7 @@ const GenericComponentPage = ({ menu, metadata, current, id, config, componentHo const componentRoute = (componentId: string) => `${normalizedBasePath}/system/component/${componentId}`; const fetchComponents = async () => { - let data = await fetch(`${normalizedBasePath}/api/component/${id}.json`).then((res) => res.json()); + const data = await fetch(`${normalizedBasePath}/api/component/${id}.json`).then((res) => res.json()); setComponent(data as PreviewObject); }; diff --git a/src/app/pages/system/index.tsx b/src/app/pages/system/index.tsx index 0a2b1650..41df768c 100644 --- a/src/app/pages/system/index.tsx +++ b/src/app/pages/system/index.tsx @@ -30,7 +30,7 @@ type ComponentPageDocumentationProps = DocumentationProps; * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async () => { // Read current slug const config = getClientRuntimeConfig(); return { @@ -49,7 +49,7 @@ const ComponentsPage = ({ content, menu, metadata, current, config }: ComponentP // Fetch components from api const [components, setComponents] = useState(undefined); const fetchComponents = async () => { - let data = await fetch(`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/components.json`).then((res) => res.json()); + const data = await fetch(`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/components.json`).then((res) => res.json()); setComponents(data as PreviewObject[]); }; useEffect(() => { diff --git a/src/app/pages/system/tokens/components/[component]/index.tsx b/src/app/pages/system/tokens/components/[component]/index.tsx index 6b3c4ab8..74924465 100644 --- a/src/app/pages/system/tokens/components/[component]/index.tsx +++ b/src/app/pages/system/tokens/components/[component]/index.tsx @@ -67,7 +67,6 @@ const GenericComponentPage = ({ menu, metadata, current, - id, config, component, options, @@ -101,11 +100,11 @@ const GenericComponentPage = ({ ); }; -export const getComponentPreviews = ( +export function getComponentPreviews( tab: 'overview' | 'tokens', component: CoreTypes.IFileComponentObject, options: ComponentDocumentationOptions -) => { +): ComponentPreview[] { const instances = component.instances; const view = (options?.views ?? {})[tab] ?? {}; const viewFilters = view.condition ?? {}; @@ -147,13 +146,10 @@ export const getComponentPreviews = ( // Filter value object keys do not contain the value of the respective component property // Since component should not be displayed we return null value return null; - } else { - // Filter value object keys do contain the value of the respective component property - // We will store the property value of the filter value object for later use - overrides = filterValue[variantProps.get(filterProp)]; - // Use as a default possibly distinctive name - name ??= variantProps.get(filterProp); } + // Filter value object keys do contain the value of the respective component property + overrides = filterValue[variantProps.get(filterProp)]; + name ??= variantProps.get(filterProp); } } else if (typeof filterValue === 'string' && variantProps.get(filterProp) !== filterValue) { return null; @@ -175,7 +171,7 @@ export const getComponentPreviews = ( } return tabComponents; -}; +} function multiPropSort(properties: string[], array: ComponentPreview[]) { return array.sort((l, r) => { @@ -199,14 +195,10 @@ function multiPropSort(properties: string[], array: ComponentPreview[]) { }); } -const IsColorValue = (value: string) => { - return value.match(/^#[0-9A-F]{6}$/i) || value.match(/linear-gradient\(.*?\)|rgba\(.*?\)/); -}; - const NormalizeValue = (value: string): string => { if (!Number.isNaN(Number(value))) { const numericValue = Number(value); - if (numericValue % 1 != 0) { + if (numericValue % 1 !== 0) { return round(numericValue, 2).toFixed(2); } } @@ -220,7 +212,6 @@ export interface ComponentDesignTokensProps { previewObjectOptions?: CoreTypes.IHandoffConfigurationComponentOptions; componentInstances: CoreTypes.IComponentInstance[]; overrides?: { [variantProp: string]: string[] }; - children?: React.ReactNode; renderPreviews: boolean; useReferences: boolean; } @@ -228,18 +219,17 @@ export interface ComponentDesignTokensProps { interface DataTableRow extends Map { } interface DataTable extends Map { } -export const ComponentDesignTokens: React.FC = ({ +export function ComponentDesignTokens({ title, componentInstances, previewObject, previewObjectOptions, overrides, - children, - renderPreviews, + renderPreviews: _renderPreviews, useReferences, -}) => { +}: ComponentDesignTokensProps) { const previewObjectVariantPropsMap = new Map(previewObject.variantProperties); - const [showReference, setShowReference] = React.useState(useReferences); + const [showReference] = React.useState(useReferences); const headings: Set = new Set(); const dataTable = new Map() as DataTable; @@ -310,8 +300,6 @@ export const ComponentDesignTokens: React.FC = ({ return <>; } - const layoutLeftColWidth = renderPreviews ? (numberOfColumns >= 7 ? 11 : 4 + numberOfColumns) : 12; - const layoutRightColWidth = 12 - layoutLeftColWidth; const headingsTotal = Array.from(headings).length + 1; return ( @@ -341,26 +329,19 @@ export const ComponentDesignTokens: React.FC = ({ {Array.from(propertiesMap) .sort(([lProp], [rProp]) => lProp.localeCompare(rProp)) - .map(([prop, cells], i) => ( + .map(([, cells], i) => ( - {/*

{prop}

*/} {cells[0][0]}
- {cells.map(([_, tokenValue, tokenReference], i) => ( - - {/* {IsColorValue(tokenValue) && ( -
- )} */} - {/* */} - {!showReference ? ( - NormalizeValue(tokenValue) - ) : tokenReference ? ( - <>{tokenReferenceFormat(tokenReference, 'css')} - ) : ( - NormalizeValue(tokenValue) - )} + {cells.map(([, tokenValue, tokenReference], cellIdx) => ( + + {(() => { + if (!showReference) return NormalizeValue(tokenValue); + if (tokenReference) return <>{tokenReferenceFormat(tokenReference, 'css')} ; + return NormalizeValue(tokenValue); + })()} ))}
@@ -371,6 +352,6 @@ export const ComponentDesignTokens: React.FC = ({ ); -}; +} export default GenericComponentPage; diff --git a/src/app/pages/system/tokens/foundations/colors.tsx b/src/app/pages/system/tokens/foundations/colors.tsx index f3b6a7b1..c3002587 100644 --- a/src/app/pages/system/tokens/foundations/colors.tsx +++ b/src/app/pages/system/tokens/foundations/colors.tsx @@ -19,8 +19,7 @@ import { fetchDocPageMarkdown, FoundationDocumentationProps, getClientRuntimeCon * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { - // Read current slug +export const getStaticProps: GetStaticProps = async () => { const config = getClientRuntimeConfig(); return { ...{ @@ -32,6 +31,36 @@ export const getStaticProps: GetStaticProps = async (context) => { }, }; }; + +const ColorGroupTable = ({ colors }: { group: string; colors: CoreTypes.IColorObject[] }) => { + return ( + + + + Name + Value + + + + {colors.map((color) => ( + + {color.reference} + +
+ + {color.value} +
+
+
+ ))} +
+
+ ); +}; + /** * Define the components page * @param param0 @@ -94,33 +123,4 @@ const ComponentsPage = ({ content, menu, metadata, current, config, design }: Fo ); }; -const ColorGroupTable = ({ group, colors }: { group: string; colors: CoreTypes.IColorObject[] }) => { - return ( - - - - Name - Value - - - - {colors.map((color) => ( - - {color.reference} - -
- - {color.value} -
-
-
- ))} -
-
- ); -}; - export default ComponentsPage; diff --git a/src/app/pages/system/tokens/foundations/effects.tsx b/src/app/pages/system/tokens/foundations/effects.tsx index 127c4821..b2949feb 100644 --- a/src/app/pages/system/tokens/foundations/effects.tsx +++ b/src/app/pages/system/tokens/foundations/effects.tsx @@ -19,8 +19,7 @@ import { fetchDocPageMarkdown, FoundationDocumentationProps, getClientRuntimeCon * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { - // Read current slug +export const getStaticProps: GetStaticProps = async () => { const config = getClientRuntimeConfig(); return { ...{ @@ -32,6 +31,30 @@ export const getStaticProps: GetStaticProps = async (context) => { }, }; }; + +const EffectsTable = ({ effects }: { group: string; effects: CoreTypes.IEffectObject[] }) => { + return ( + + + + Reference + Effects + + + + {effects.map((effect) => ( + + {effect.reference.replace(/-+/g, '-')} + + {effect.effects.map((effect) => effect.value).join(', ') || 'none'} + + + ))} + +
+ ); +}; + /** * Define the components page * @param param0 @@ -87,27 +110,4 @@ const ComponentsPage = ({ content, menu, metadata, current, config, design }: Fo ); }; -const EffectsTable = ({ group, effects }: { group: string; effects: CoreTypes.IEffectObject[] }) => { - return ( - - - - Reference - Effects - - - - {effects.map((effect) => ( - - {effect.reference.replace(/-+/g, '-')} - - {effect.effects.map((effect) => effect.value).join(', ') || 'none'} - - - ))} - -
- ); -}; - export default ComponentsPage; diff --git a/src/app/pages/system/tokens/foundations/typography.tsx b/src/app/pages/system/tokens/foundations/typography.tsx index 4d79e287..254add6a 100644 --- a/src/app/pages/system/tokens/foundations/typography.tsx +++ b/src/app/pages/system/tokens/foundations/typography.tsx @@ -20,8 +20,7 @@ import { fetchDocPageMarkdown, FoundationDocumentationProps, getClientRuntimeCon * @param context GetStaticProps * @returns */ -export const getStaticProps: GetStaticProps = async (context) => { - // Read current slug +export const getStaticProps: GetStaticProps = async () => { const config = getClientRuntimeConfig(); return { ...{ @@ -33,40 +32,6 @@ export const getStaticProps: GetStaticProps = async (context) => { }, }; }; -/** - * Define the components page - * @param param0 - * @returns - */ -const ComponentsPage = ({ content, menu, metadata, current, config, design }: FoundationDocumentationProps) => { - const typography = design.typography.slice().sort((a, b) => { - const l = (config?.app?.type_sort ?? []).indexOf(a.name) >>> 0; - const r = (config?.app?.type_sort ?? []).indexOf(b.name) >>> 0; - return l !== r ? l - r : a.name.localeCompare(b.name); - }); - - return ( - -
- {metadata.title} -

{metadata.description}

-
-
-
-
- - {content} - -
- -
- ({ [`${type.name}-typography`]: `${upperFirst(type.name)}` }))])]} - /> -
-
- ); -}; const FontsTable = ({ types }: { types: CoreTypes.ITypographyObject[] }) => { return ( @@ -114,6 +79,41 @@ const FontsTable = ({ types }: { types: CoreTypes.ITypographyObject[] }) => { ); }; +/** + * Define the components page + * @param param0 + * @returns + */ +const ComponentsPage = ({ content, menu, metadata, current, config, design }: FoundationDocumentationProps) => { + const typography = design.typography.slice().sort((a, b) => { + const l = (config?.app?.type_sort ?? []).indexOf(a.name) >>> 0; + const r = (config?.app?.type_sort ?? []).indexOf(b.name) >>> 0; + return l !== r ? l - r : a.name.localeCompare(b.name); + }); + + return ( + +
+ {metadata.title} +

{metadata.description}

+
+
+
+
+ + {content} + +
+ +
+ ({ [`${type.name}-typography`]: `${upperFirst(type.name)}` }))])]} + /> +
+
+ ); +}; + export const pluckStyle = (type: CoreTypes.ITypographyObject) => { return { fontFamily: type.values.fontFamily, diff --git a/src/cli.ts b/src/cli.ts index 553f21b3..48d397b8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,7 +19,7 @@ class HandoffCliError extends Error { * Show the help message */ const showVersion = () => { - return 'Handoff App - 1.2.1'; + return 'Handoff App - 1.2.2'; }; /** From 6eb00364d65901f95e80a5913a6e236717e27ba1 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Thu, 9 Apr 2026 19:11:24 -0600 Subject: [PATCH 02/21] 1.2.2-0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 155eb35c..783b3da5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.1", + "version": "1.2.2-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.1", + "version": "1.2.2-0", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index 676351a8..8c8d2d21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.1", + "version": "1.2.2-0", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From 4f103f337a7f192ad0f3ab51869a2c8a0da536b9 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Thu, 9 Apr 2026 20:01:52 -0600 Subject: [PATCH 03/21] A couple of additional eslint improvemnts --- src/app/pages/foundations/icons/[name]/index.tsx | 3 ++- .../pages/system/tokens/components/[component]/index.tsx | 9 --------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/app/pages/foundations/icons/[name]/index.tsx b/src/app/pages/foundations/icons/[name]/index.tsx index 805631b8..16874157 100644 --- a/src/app/pages/foundations/icons/[name]/index.tsx +++ b/src/app/pages/foundations/icons/[name]/index.tsx @@ -71,7 +71,8 @@ export const getStaticProps: GetStaticProps = () => { export default function SingleIcon({ menu, metadata, current, config, assets }: AssetDocumentationProps) { const router = useRouter(); const nameParam = router.query.name; - const name = typeof nameParam === 'string' ? nameParam : Array.isArray(nameParam) ? nameParam[0] : undefined; + let name = undefined; + if (typeof nameParam === 'string') { name = nameParam } else if (Array.isArray(nameParam)) { name = nameParam[0] } else { name = undefined } const icon = assets?.icons.find((i) => i.icon === name); const copySvg = React.useCallback( diff --git a/src/app/pages/system/tokens/components/[component]/index.tsx b/src/app/pages/system/tokens/components/[component]/index.tsx index 74924465..87aa2779 100644 --- a/src/app/pages/system/tokens/components/[component]/index.tsx +++ b/src/app/pages/system/tokens/components/[component]/index.tsx @@ -225,7 +225,6 @@ export function ComponentDesignTokens({ previewObject, previewObjectOptions, overrides, - renderPreviews: _renderPreviews, useReferences, }: ComponentDesignTokensProps) { const previewObjectVariantPropsMap = new Map(previewObject.variantProperties); @@ -233,8 +232,6 @@ export function ComponentDesignTokens({ const headings: Set = new Set(); const dataTable = new Map() as DataTable; - let numberOfColumns = 0; - if (overrides) { const overrideVariantProps = Object.keys(overrides) ?? []; const masterOverride = overrideVariantProps[0]; @@ -268,9 +265,6 @@ export function ComponentDesignTokens({ .push([token.name, token.value, token ?? undefined]); }); - // Increase columns count - numberOfColumns++; - // Append heading to the list of headings headings.add(componentVariantPropsMap.get(masterOverride)); }); @@ -289,9 +283,6 @@ export function ComponentDesignTokens({ .push([token.name, token.value, token ?? undefined]); }); - // Increase columns count - numberOfColumns++; - // Append heading to the list of headings headings.add('Value'); } From 92e8f9db797cce9157ee29915fd3753fb32b257e Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Thu, 9 Apr 2026 21:15:43 -0600 Subject: [PATCH 04/21] 1.2.2-1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 783b3da5..f6a7f45b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.2-0", + "version": "1.2.2-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.2-0", + "version": "1.2.2-1", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index 8c8d2d21..d32f6f71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.2-0", + "version": "1.2.2-1", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From 615dc32a6da0a1f6098aed1ec577f1e6d46c85da Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Fri, 10 Apr 2026 19:34:49 -0600 Subject: [PATCH 05/21] Fixing app bug --- src/app/pages/_app.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/pages/_app.tsx b/src/app/pages/_app.tsx index aceb4f42..41dbdf03 100644 --- a/src/app/pages/_app.tsx +++ b/src/app/pages/_app.tsx @@ -1,6 +1,4 @@ import type { AppProps } from 'next/app'; - -import '../css/index.css'; import '../css/theme.css'; function MyApp({ Component, pageProps }: AppProps) { From 02a2f4c13597383a2ebf4271eab86535850d0921 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Fri, 10 Apr 2026 19:35:28 -0600 Subject: [PATCH 06/21] 1.2.2-2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6a7f45b..f02e3d71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.2-1", + "version": "1.2.2-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.2-1", + "version": "1.2.2-2", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index d32f6f71..53cbbdb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.2-1", + "version": "1.2.2-2", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From 80230356de16af3c41caecd9b02916f3715206f6 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Fri, 10 Apr 2026 20:43:30 -0600 Subject: [PATCH 07/21] Removing unneed theme declarations --- src/app/pages/_app.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/pages/_app.tsx b/src/app/pages/_app.tsx index 41dbdf03..39b5372e 100644 --- a/src/app/pages/_app.tsx +++ b/src/app/pages/_app.tsx @@ -1,5 +1,4 @@ import type { AppProps } from 'next/app'; -import '../css/theme.css'; function MyApp({ Component, pageProps }: AppProps) { return ( From 8b76fc548879ba6a1090a717b7a718bf89881675 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Fri, 10 Apr 2026 20:44:01 -0600 Subject: [PATCH 08/21] 1.2.2-3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f02e3d71..60313bc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.2-2", + "version": "1.2.2-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.2-2", + "version": "1.2.2-3", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index 53cbbdb0..131d9c7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.2-2", + "version": "1.2.2-3", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From 8773b25bdf01710b1c2ce80aafeddd0e7e7aeba0 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Mon, 13 Apr 2026 06:24:39 -0600 Subject: [PATCH 09/21] Fixing css typing --- src/app-builder/build.ts | 19 ++++++++++++++++--- src/app/css.d.ts | 1 + src/app/pages/_app.tsx | 3 +++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/app/css.d.ts diff --git a/src/app-builder/build.ts b/src/app-builder/build.ts index 43929368..964185cf 100644 --- a/src/app-builder/build.ts +++ b/src/app-builder/build.ts @@ -4,13 +4,23 @@ import path from 'path'; import Handoff from '..'; import { buildComponents } from '../pipeline/components'; import { buildPatterns } from '../pipeline/patterns'; +import processComponents from '../transformers/preview/component/builder'; import { buildMainCss } from '../transformers/preview/component/css'; import { buildMainJS } from '../transformers/preview/component/javascript'; -import processComponents from '../transformers/preview/component/builder'; import { Logger } from '../utils/logger'; import { generateTokensApi, persistClientConfig } from './client-config'; import { getAppPath, syncPublicFiles } from './paths'; -import { WatcherState, getRuntimeComponentsPathsToWatch, watchAppSource, watchComponentDirectories, watchGlobalEntries, watchPages, watchPublicDirectory, watchRuntimeComponents, watchRuntimeConfiguration } from './watchers'; +import { + WatcherState, + getRuntimeComponentsPathsToWatch, + watchAppSource, + watchComponentDirectories, + watchGlobalEntries, + watchPages, + watchPublicDirectory, + watchRuntimeComponents, + watchRuntimeConfiguration, +} from './watchers'; import { createWebSocketServer } from './websocket'; const escapeForSingleQuotedJsString = (value: string): string => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); @@ -46,10 +56,13 @@ const initializeProjectApp = async (handoff: Handoff): Promise => { // Copy custom theme CSS if it exists in the user's project const customThemePath = path.resolve(handoff.workingPath, 'theme.css'); + const destPath = path.resolve(appPath, 'css', 'theme.css'); if (fs.existsSync(customThemePath)) { - const destPath = path.resolve(appPath, 'css', 'theme.css'); await fs.copy(customThemePath, destPath, { overwrite: true }); Logger.success(`Custom theme.css loaded`); + } else { + // create a empty theme.css file + await fs.writeFile(destPath, ''); } // Prepare project app configuration using stable placeholder replacement. diff --git a/src/app/css.d.ts b/src/app/css.d.ts new file mode 100644 index 00000000..a839446c --- /dev/null +++ b/src/app/css.d.ts @@ -0,0 +1 @@ +declare module '*.css' {} diff --git a/src/app/pages/_app.tsx b/src/app/pages/_app.tsx index 39b5372e..aceb4f42 100644 --- a/src/app/pages/_app.tsx +++ b/src/app/pages/_app.tsx @@ -1,5 +1,8 @@ import type { AppProps } from 'next/app'; +import '../css/index.css'; +import '../css/theme.css'; + function MyApp({ Component, pageProps }: AppProps) { return ( <> From 1fac3b87c99b563c5fc5781aa4581b758c90916e Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Mon, 13 Apr 2026 06:28:20 -0600 Subject: [PATCH 10/21] fixing security problem in vite --- package-lock.json | 90 +++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60313bc9..1d33bcbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1884,9 +1884,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz", - "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz", + "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1899,9 +1899,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz", - "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz", + "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==", "cpu": [ "arm64" ], @@ -1915,9 +1915,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz", - "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz", + "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==", "cpu": [ "x64" ], @@ -1931,9 +1931,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz", - "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz", + "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==", "cpu": [ "arm64" ], @@ -1947,9 +1947,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz", - "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz", + "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==", "cpu": [ "arm64" ], @@ -1963,9 +1963,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz", - "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz", + "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==", "cpu": [ "x64" ], @@ -1979,9 +1979,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz", - "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz", + "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==", "cpu": [ "x64" ], @@ -1995,9 +1995,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz", - "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz", + "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==", "cpu": [ "arm64" ], @@ -2011,9 +2011,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz", - "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz", + "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==", "cpu": [ "x64" ], @@ -10988,9 +10988,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.defaults": { @@ -12242,12 +12242,12 @@ } }, "node_modules/next": { - "version": "15.5.14", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz", - "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==", + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz", + "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.14", + "@next/env": "15.5.15", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -12260,14 +12260,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.14", - "@next/swc-darwin-x64": "15.5.14", - "@next/swc-linux-arm64-gnu": "15.5.14", - "@next/swc-linux-arm64-musl": "15.5.14", - "@next/swc-linux-x64-gnu": "15.5.14", - "@next/swc-linux-x64-musl": "15.5.14", - "@next/swc-win32-arm64-msvc": "15.5.14", - "@next/swc-win32-x64-msvc": "15.5.14", + "@next/swc-darwin-arm64": "15.5.15", + "@next/swc-darwin-x64": "15.5.15", + "@next/swc-linux-arm64-gnu": "15.5.15", + "@next/swc-linux-arm64-musl": "15.5.15", + "@next/swc-linux-x64-gnu": "15.5.15", + "@next/swc-linux-x64-musl": "15.5.15", + "@next/swc-win32-arm64-msvc": "15.5.15", + "@next/swc-win32-x64-msvc": "15.5.15", "sharp": "^0.34.3" }, "peerDependencies": { @@ -16186,9 +16186,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", From 35b410fe107292deb4852bd9484e3fcac6b9c24c Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Mon, 13 Apr 2026 06:29:27 -0600 Subject: [PATCH 11/21] 1.2.2-4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d33bcbd..e0b914fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.2-3", + "version": "1.2.2-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.2-3", + "version": "1.2.2-4", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index 131d9c7f..1d7787a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.2-3", + "version": "1.2.2-4", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From 804f405a9686b4501b7cbe84b5cbabd389066ce0 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Thu, 16 Apr 2026 10:44:02 -0600 Subject: [PATCH 12/21] Increasing flexibility of slot defaults --- src/transformers/preview/component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transformers/preview/component.ts b/src/transformers/preview/component.ts index 1f8bdf8f..9f854f3a 100644 --- a/src/transformers/preview/component.ts +++ b/src/transformers/preview/component.ts @@ -2,10 +2,10 @@ import type { DocAnnotation, TypeNode } from 'handoff-docgen'; import path from 'path'; import Handoff from '../../index'; import { getAPIPath } from './component/api'; -import writeComponentSummaryAPI from './component/summary'; import processComponents from './component/builder'; import { buildMainCss } from './component/css'; import { buildMainJS } from './component/javascript'; +import writeComponentSummaryAPI from './component/summary'; export interface ComponentMetadata { title: string; @@ -33,7 +33,7 @@ export interface SlotMetadata { name: string; description: string; generic: string; - default?: string; + default?: string | number | boolean | object | any[] | null; type: SlotType; // used if type is array items?: { From a6983a5995acecc8bd7d0fc5addd7c4756827ebc Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Thu, 16 Apr 2026 10:49:13 -0600 Subject: [PATCH 13/21] Updating changelog --- Changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog.md b/Changelog.md index 6c2b7191..20a6e6bf 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,6 +12,11 @@ This patch release clears ESLint failures in the Next.js app pages and aligns several code paths with the compiler target so production builds succeed without enabling `--downlevelIteration`. +### Change + +* Altered the avaliable types to the SlotMetadata so the default can handle + a wide variaty of types - `default?: string | number | boolean | object | any[] | null;` + ### Lint and code quality * Resolved `no-else-return`, `no-unused-vars`, `camelcase`, `prefer-const`, From ef735f593dd6ddfae671558bf1bfdff0cd70c0fc Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Thu, 16 Apr 2026 11:05:19 -0600 Subject: [PATCH 14/21] 1.2.2-5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0b914fe..9b166da8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.2-4", + "version": "1.2.2-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.2-4", + "version": "1.2.2-5", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index 1d7787a0..c965006e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.2-4", + "version": "1.2.2-5", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From 96b62e24fdc983190bef5918e7816aa8945b5890 Mon Sep 17 00:00:00 2001 From: Domagoj Gojak Date: Fri, 17 Apr 2026 16:57:16 +0200 Subject: [PATCH 15/21] Improve pattern discovery and build reliability in watch mode --- src/app-builder/build.ts | 3 + src/app-builder/watchers.ts | 165 +++++++++++++++--- src/app-builder/watchers/utils.ts | 2 + src/app/components/NotFound.tsx | 24 +++ src/app/pages/[...slug]/index.tsx | 18 +- .../pages/system/pattern/[pattern]/index.tsx | 33 +++- src/cache/build-cache.ts | 17 ++ src/config/runtime.ts | 7 +- 8 files changed, 225 insertions(+), 44 deletions(-) create mode 100644 src/app/components/NotFound.tsx diff --git a/src/app-builder/build.ts b/src/app-builder/build.ts index 964185cf..be596329 100644 --- a/src/app-builder/build.ts +++ b/src/app-builder/build.ts @@ -17,6 +17,7 @@ import { watchComponentDirectories, watchGlobalEntries, watchPages, + watchPatternDirectories, watchPublicDirectory, watchRuntimeComponents, watchRuntimeConfiguration, @@ -224,12 +225,14 @@ export const watchApp = async (handoff: Handoff): Promise => { runtimeComponentsWatcher: null, runtimeConfigurationWatcher: null, componentDirectoriesWatcher: null, + patternDirectoriesWatcher: null, }; watchPublicDirectory(handoff, wss, state, chokidarConfig); watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff)); watchRuntimeConfiguration(handoff, state); watchComponentDirectories(handoff, state, chokidarConfig); + watchPatternDirectories(handoff, state, chokidarConfig); watchGlobalEntries(handoff, state, chokidarConfig); watchPages(handoff, chokidarConfig); }; diff --git a/src/app-builder/watchers.ts b/src/app-builder/watchers.ts index 987af7f5..f5fe6338 100644 --- a/src/app-builder/watchers.ts +++ b/src/app-builder/watchers.ts @@ -2,6 +2,7 @@ import chokidar from 'chokidar'; import fs from 'fs-extra'; import path from 'path'; import Handoff from '..'; +import { buildPatterns } from '../pipeline/patterns'; import processComponents, { ComponentSegment } from '../transformers/preview/component/builder'; import { buildMainCss } from '../transformers/preview/component/css'; import { buildMainJS } from '../transformers/preview/component/javascript'; @@ -273,22 +274,49 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState) }; /** - * Watches the parent component directories from config.entries.components - * for new components being added. When a new config file (e.g. button.json) - * appears in a new subdirectory, reloads the runtime config and restarts - * the component/configuration watchers so the new component is picked up. + * Shared factory for watching a parent directory for newly created entity + * subdirectories (components or patterns). Handles the common scaffolding so + * that callers only describe what differs for their entity kind: + * + * - which config-entry paths to watch + * - how to read the current known ids after a reload + * - which WatcherState slot to use + * - a log label for the entity kind + * - the rebuild work to run once the runtime config is fresh */ -export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => { - if (state.componentDirectoriesWatcher) { - state.componentDirectoriesWatcher.close(); +const watchEntityDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions, options: { + /** Config paths to watch, e.g. handoff.config.entries?.components */ + getConfigPaths: (handoff: Handoff) => string[]; + /** Current known entity ids from runtime config, called again after reload to refresh the set */ + getKnownIds: (handoff: Handoff) => string[]; + /** Read the active FSWatcher from state */ + getWatcher: (state: WatcherState) => chokidar.FSWatcher | null; + /** Write the active FSWatcher back into state */ + setWatcher: (state: WatcherState, watcher: chokidar.FSWatcher) => void; + /** Prefix for the scheduleHandler key, e.g. 'newComponent' or 'newPattern' */ + scheduleKeyPrefix: string; + /** Human-readable label used in log messages, e.g. 'component' or 'pattern' */ + entityLabel: string; + /** + * Entity-specific rebuild work, called after reload + persistClientConfig + + * watcher re-registration. Receives the handoff instance (with fresh runtime + * config) and the directory name of the new entity. + */ + onDetected: (handoff: Handoff, dirName: string) => Promise; +}) => { + const { getConfigPaths, getKnownIds, getWatcher, setWatcher, scheduleKeyPrefix, entityLabel, onDetected } = options; + + const existingWatcher = getWatcher(state); + if (existingWatcher) { + existingWatcher.close(); } - const componentPaths = handoff.config.entries?.components ?? []; - if (componentPaths.length === 0) return; + const configPaths = getConfigPaths(handoff); + if (configPaths.length === 0) return; const dirsToWatch: string[] = []; - for (const componentPath of componentPaths) { - const resolved = path.resolve(handoff.workingPath, componentPath); + for (const configPath of configPaths) { + const resolved = path.resolve(handoff.workingPath, configPath); if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { dirsToWatch.push(resolved); } @@ -296,40 +324,127 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, if (dirsToWatch.length === 0) return; - const knownComponents = new Set(Object.keys(handoff.runtimeConfig?.entries?.components ?? {})); + // Snapshot known ids at watch startup so we only react to genuinely new + // entities, not to file changes inside already-known directories. + const knownIds = new Set(getKnownIds(handoff)); - state.componentDirectoriesWatcher = chokidar.watch(dirsToWatch, { + const watcher = chokidar.watch(dirsToWatch, { ...chokidarConfig, depth: 1, }); + setWatcher(state, watcher); - state.componentDirectoriesWatcher.on('add', async (file) => { + watcher.on('add', async (file) => { const basename = path.basename(file); const dirName = path.basename(path.dirname(file)); - const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs'); - const isNewComponent = isConfigFile && basename.startsWith(dirName) && !knownComponents.has(dirName); + // Only react to the primary declaration file for the directory (e.g. + // button.json, button.js, or button.handoff.ts inside a button/ subdir). + // The basename.startsWith(dirName) guard below ensures .ts here only + // ever matches files named after their parent directory, not arbitrary + // TypeScript source files. + const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs') || basename.endsWith('.ts'); + const isNewEntity = isConfigFile && basename.startsWith(dirName) && !knownIds.has(dirName); - if (!isNewComponent) return; + if (!isNewEntity) return; - await scheduleHandler(state, `newComponent:${dirName}`, async () => { + await scheduleHandler(state, `${scheduleKeyPrefix}:${dirName}`, async () => { try { - Logger.warn(`New component detected: ${dirName}. Reloading configuration...`); + Logger.warn(`New ${entityLabel} detected: ${dirName}. Reloading configuration...`); + + // Snapshot ids before reload so we can tell whether the new entity + // was successfully registered afterwards. + const idsBefore = new Set(getKnownIds(handoff)); + handoff.reload(); - knownComponents.add(dirName); + knownIds.add(dirName); - for (const id of Object.keys(handoff.runtimeConfig?.entries?.components ?? {})) { - knownComponents.add(id); + // Refresh from the post-reload runtime config so any ids that were + // discovered alongside the new entity are also marked as known. + const idsAfter = getKnownIds(handoff); + for (const id of idsAfter) { + knownIds.add(id); } await persistClientConfig(handoff); + + // Re-register file watchers so the new config file and any runtime + // files it introduces are covered going forward. This must happen even + // when the file failed to parse so that watchRuntimeConfiguration can + // trigger a retry the moment the file is saved with valid content. watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff)); watchRuntimeConfiguration(handoff, state); - await processComponents(handoff, dirName); - await runAllFinalizers(handoff, { patternRebuildComponentIds: [dirName] }); + + // If no new ids appeared after reload the file was empty or invalid. + // Skip the build — watchRuntimeConfiguration will fire when it is saved. + const hasNewIds = idsAfter.some((id) => !idsBefore.has(id)); + if (!hasNewIds) { + Logger.warn(`${entityLabel} "${dirName}" config is empty or incomplete — build will run automatically once the file is saved.`); + return; + } + + await onDetected(handoff, dirName); } catch (e) { - Logger.error('Error processing new component:', e); + Logger.error(`Error processing new ${entityLabel}:`, e); } }); }); }; + +/** + * Watches the parent component directories from config.entries.components for + * new components being added. When a new config file appears in a new + * subdirectory, reloads the runtime config, restarts the component and + * configuration watchers, then builds the new component and any patterns that + * reference it. + */ +export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => { + watchEntityDirectories(handoff, state, chokidarConfig, { + getConfigPaths: (h) => h.config.entries?.components ?? [], + getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.components ?? {}), + getWatcher: (s) => s.componentDirectoriesWatcher, + setWatcher: (s, w) => { s.componentDirectoriesWatcher = w; }, + scheduleKeyPrefix: 'newComponent', + entityLabel: 'component', + onDetected: async (handoff, dirName) => { + await processComponents(handoff, dirName); + await runAllFinalizers(handoff, { patternRebuildComponentIds: [dirName] }); + }, + }); +}; + +/** + * Watches the parent pattern directories from config.entries.patterns for new + * patterns being added. The rebuild sequence intentionally differs from + * watchComponentDirectories because the dependency arrow is reversed: a new + * pattern references existing components, so we rebuild only the preview + * segment of those components (to produce the __pattern_* HTML files that + * injectPatternPreviews registered) before composing the pattern itself. + */ +export const watchPatternDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => { + watchEntityDirectories(handoff, state, chokidarConfig, { + getConfigPaths: (h) => h.config.entries?.patterns ?? [], + getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.patterns ?? {}), + getWatcher: (s) => s.patternDirectoriesWatcher, + setWatcher: (s, w) => { s.patternDirectoriesWatcher = w; }, + scheduleKeyPrefix: 'newPattern', + entityLabel: 'pattern', + onDetected: async (handoff, dirName) => { + // Rebuild only the previews segment for each component the new pattern + // references. Their JS/CSS/structure are unchanged — only the new + // synthetic preview HTML files need to be generated so that buildPatterns + // can assemble the pattern from them. + const newPattern = handoff.runtimeConfig?.entries?.patterns?.[dirName]; + if (newPattern?.components?.length) { + for (const ref of newPattern.components) { + await processComponents(handoff, ref.id, ComponentSegment.Previews); + } + } + + // Build only the new pattern directly rather than going through + // runAllFinalizers, which would rebuild all patterns unnecessarily. + const newPatternId = newPattern?.id ?? dirName; + await buildPatterns(handoff, { onlyPatternIds: new Set([newPatternId]) }); + }, + }); +}; diff --git a/src/app-builder/watchers/utils.ts b/src/app-builder/watchers/utils.ts index 7d21590e..5bc69976 100644 --- a/src/app-builder/watchers/utils.ts +++ b/src/app-builder/watchers/utils.ts @@ -7,6 +7,8 @@ export interface WatcherState { runtimeComponentsWatcher: chokidar.FSWatcher | null; runtimeConfigurationWatcher: chokidar.FSWatcher | null; componentDirectoriesWatcher: chokidar.FSWatcher | null; + /** Watches entries.patterns parent directories for newly created pattern subdirectories. */ + patternDirectoriesWatcher: chokidar.FSWatcher | null; } /** diff --git a/src/app/components/NotFound.tsx b/src/app/components/NotFound.tsx new file mode 100644 index 00000000..81f3e160 --- /dev/null +++ b/src/app/components/NotFound.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface NotFoundProps { + title?: string; + description?: React.ReactNode; + linkHref?: string; + linkLabel?: string; +} + +const NotFound: React.FC = () => { + return ( +
+
404
+

Oops! Page not found.

+

+ This page doesn’t exist or has been moved. +
+ Please check the URL or return to the homepage. +

+
+ ); +}; + +export default NotFound; diff --git a/src/app/pages/[...slug]/index.tsx b/src/app/pages/[...slug]/index.tsx index 95967318..30c56ad8 100644 --- a/src/app/pages/[...slug]/index.tsx +++ b/src/app/pages/[...slug]/index.tsx @@ -1,7 +1,7 @@ import { GetStaticProps } from 'next'; import Head from 'next/head'; -import Link from 'next/link'; import { useRef } from 'react'; +import NotFound from '../../components/NotFound'; import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; @@ -48,21 +48,7 @@ export default function DocCatchAllPage({ content, menu, metadata, current, conf 404 - Page Not Found -
-
404
-

Oops! Page not found.

-

- Sorry, the page you are looking for does not exist or has been moved. -
- Please check the URL or return to the homepage. -

- - Go Home - -
+
); } diff --git a/src/app/pages/system/pattern/[pattern]/index.tsx b/src/app/pages/system/pattern/[pattern]/index.tsx index faa2f3e6..d2c4d95c 100644 --- a/src/app/pages/system/pattern/[pattern]/index.tsx +++ b/src/app/pages/system/pattern/[pattern]/index.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import Layout from '../../../../components/Layout/Main'; +import NotFound from '../../../../components/NotFound'; import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents'; import PrevNextNav from '../../../../components/Navigation/PrevNextNav'; import HeadersType from '../../../../components/Typography/Headers'; @@ -130,6 +131,7 @@ export const getStaticProps = async (context: { params: IParams }) => { const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nextPattern }: PatternPageProps) => { const [pattern, setPattern] = useState(undefined); const [fetchError, setFetchError] = useState(undefined); + const [patternNotFound, setPatternNotFound] = useState(false); const [iframeHeight, setIframeHeight] = useState('400px'); const iframeRef = React.useRef(null); @@ -145,13 +147,20 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex const controller = new AbortController(); setPattern(undefined); setFetchError(undefined); + setPatternNotFound(false); fetch(`${normalizedBasePath}/api/pattern/${id}.json`, { signal: controller.signal }) .then((res) => { + if (res.status === 404) { + setPatternNotFound(true); + return null; + } if (!res.ok) throw new Error(`Failed to load pattern: ${res.status} ${res.statusText}`); return res.json(); }) - .then((data) => setPattern(data as PatternListObject)) + .then((data) => { + if (data) setPattern(data as PatternListObject); + }) .catch((err) => { if (err.name !== 'AbortError') { setFetchError(err instanceof Error ? err.message : 'Failed to load pattern data.'); @@ -167,7 +176,27 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex } }, []); - if (fetchError) return

{fetchError}

; + if (patternNotFound) { + return ( + +
+ +
+
+ ); + } + + if (fetchError) { + return ( + +
+

Something went wrong

+

{fetchError}

+
+
+ ); + } + if (!pattern) return

Loading...

; return ( diff --git a/src/cache/build-cache.ts b/src/cache/build-cache.ts index 0b80af06..9ec0fc0c 100644 --- a/src/cache/build-cache.ts +++ b/src/cache/build-cache.ts @@ -226,6 +226,23 @@ export function getComponentFilePaths(handoff: Handoff, componentId: string): { files.push(matchingConfigPath); } + // Include every pattern config file whose declaration references this component. + // When a pattern is added or modified, injectPatternPreviews injects new + // __pattern_* synthetic preview keys onto the referenced component, changing + // what processComponents must render. Without tracking these files here, the + // cache would incorrectly consider the component unchanged and skip the preview + // rebuild, leaving buildPatterns unable to find the required HTML fragments. + const runtimePatterns = handoff.runtimeConfig?.entries?.patterns ?? {}; + for (const configPath of configPaths) { + const entry = handoff.getConfigFileEntry(configPath); + if (entry?.kind !== 'pattern') continue; + const pattern = runtimePatterns[entry.entityId]; + if (!pattern) continue; + if (pattern.components?.some((ref) => ref.id === componentId)) { + files.push(configPath); + } + } + return { files, templateDir }; } diff --git a/src/config/runtime.ts b/src/config/runtime.ts index b9371891..46798c11 100644 --- a/src/config/runtime.ts +++ b/src/config/runtime.ts @@ -284,7 +284,12 @@ export const initRuntimeConfig = (handoff: HandoffContext): [runtimeConfig: Runt fallbackId: patternBaseName, }); } catch (err) { - Logger.error(`Failed to read or parse pattern config: ${resolvedPatternConfigPath}`, err); + // Treat as a warning rather than an error: the file may be empty or + // mid-edit. It is already tracked in configFiles above, so + // watchRuntimeConfiguration will pick it up once the file is saved + // with valid content. + Logger.warn(`Pattern config skipped (incomplete or invalid) — will retry on next save: ${resolvedPatternConfigPath}`); + Logger.debug(`Pattern parse detail:`, err); continue; } From 8cff9cdf6bbf2671a49c17a3a16c26b38062a96c Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Sat, 18 Apr 2026 13:20:52 -0600 Subject: [PATCH 16/21] Fixing a bug with selects on system list page, where component group was empty --- src/app/components/Component/ComponentLists.tsx | 2 +- src/app/components/Component/Preview.tsx | 4 +++- src/app/components/context/PreviewContext.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/components/Component/ComponentLists.tsx b/src/app/components/Component/ComponentLists.tsx index 03427658..70f8b3ba 100644 --- a/src/app/components/Component/ComponentLists.tsx +++ b/src/app/components/Component/ComponentLists.tsx @@ -42,7 +42,7 @@ export const ComponentList = ({ const [groupedList, setGroupedList] = React.useState>({}); const categories = React.useMemo(() => { - const categories = new Set(components.map((component) => component.group).flat()); + const categories = new Set(components.map((component) => component.group).flat().filter((g): g is string => !!g)); return Array.from(categories); }, [components]); diff --git a/src/app/components/Component/Preview.tsx b/src/app/components/Component/Preview.tsx index 00a1a1cf..1cda77f1 100644 --- a/src/app/components/Component/Preview.tsx +++ b/src/app/components/Component/Preview.tsx @@ -78,10 +78,12 @@ export const ComponentDisplay: React.FC<{ const variantMap: Record> = {}; Object.values(component.previews).forEach((preview: any) => { Object.entries(preview.values).forEach(([key, value]) => { + const strValue = String(value); + if (!strValue) return; if (!variantMap[key]) { variantMap[key] = new Set(); } - variantMap[key].add(String(value)); + variantMap[key].add(strValue); }); }); diff --git a/src/app/components/context/PreviewContext.tsx b/src/app/components/context/PreviewContext.tsx index 5819bcbf..1e99d1cd 100644 --- a/src/app/components/context/PreviewContext.tsx +++ b/src/app/components/context/PreviewContext.tsx @@ -33,13 +33,13 @@ function groupVariantProperties(items: Record[]): Record [key, Array.from(set)]) - // .filter((i) => i[1].length > 1) ); } From 6a6cd5f502fe10deb694ce5edd2a97d613bf4ae4 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Sat, 18 Apr 2026 13:22:38 -0600 Subject: [PATCH 17/21] 1.2.2-6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b166da8..d3a77d98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.2-5", + "version": "1.2.2-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.2-5", + "version": "1.2.2-6", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index c965006e..e60c142e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.2-5", + "version": "1.2.2-6", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From d56374d248a1c2d112166b943de45ea8776ec116 Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Sat, 18 Apr 2026 15:32:24 -0600 Subject: [PATCH 18/21] filter empty categories from select item --- .../components/Component/ComponentLists.tsx | 204 +++++++++--------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/src/app/components/Component/ComponentLists.tsx b/src/app/components/Component/ComponentLists.tsx index 70f8b3ba..163c6eb3 100644 --- a/src/app/components/Component/ComponentLists.tsx +++ b/src/app/components/Component/ComponentLists.tsx @@ -76,114 +76,114 @@ export const ComponentList = ({ return (
-
-
-
- - setSearch(e.currentTarget.value)} - aria-label="Search components" - style={{ minWidth: 200 }} - - /> -
+
+
+
+ + setSearch(e.currentTarget.value)} + aria-label="Search components" + style={{ minWidth: 200 }} + + /> +
-
-
-
- - -
- - - - - - - Group by category - -
- storeLayout(value)}> - - - - - - - - Grid layout - - - - - - - - - - Column layout - - - +
+
+ + +
+ + + + + + + Group by category + + +
+ storeLayout(value)}> + + + + + + + + Grid layout + + + + + + + + + + Column layout + + + +
-
- {groupBy ? ( - <> - {Object.keys(groupedList).map((group) => { - return ( -
- {group} -
- {groupedList[group].map((component) => { - return ; - })} + {groupBy ? ( + <> + {Object.keys(groupedList).map((group) => { + return ( +
+ {group} +
+ {groupedList[group].map((component) => { + return ; + })} +
-
- ); + ); + })} + + ) : ( +
{list.map((component) => { + return ; })} - - ) : ( -
{list.map((component) => { - return ; - })} -
- )} +
+ )}
); From b41222abced72b0f41b416dc17443a12962f354c Mon Sep 17 00:00:00 2001 From: Brad Mering Date: Sun, 19 Apr 2026 16:34:25 -0600 Subject: [PATCH 19/21] 1.2.2-7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3a77d98..20d7c847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "handoff-app", - "version": "1.2.2-6", + "version": "1.2.2-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "handoff-app", - "version": "1.2.2-6", + "version": "1.2.2-7", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index e60c142e..5877450f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "handoff-app", - "version": "1.2.2-6", + "version": "1.2.2-7", "description": "Automated documentation toolchain for building client side documentation from figma", "author ": { "name": "Convertiv", From e47c28fce25d0e15acc6bda16697d908dc02d1b4 Mon Sep 17 00:00:00 2001 From: Domagoj Gojak Date: Sun, 17 May 2026 19:53:11 +0200 Subject: [PATCH 20/21] Fix pattern watcher rebuild edge cases by using discovered entity IDs after runtime config reloads instead of assuming folder names match IDs, and by rebuilding referenced component previews when pattern configs become valid after an incomplete save. --- src/app-builder/watchers.ts | 43 ++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/app-builder/watchers.ts b/src/app-builder/watchers.ts index f5fe6338..4b8e702e 100644 --- a/src/app-builder/watchers.ts +++ b/src/app-builder/watchers.ts @@ -210,6 +210,15 @@ export const watchRuntimeComponents = ( } }; +const rebuildPatternComponentPreviews = async (handoff: Handoff, patternId: string) => { + const pattern = handoff.runtimeConfig?.entries?.patterns?.[patternId]; + if (!pattern?.components?.length) return; + + for (const ref of pattern.components) { + await processComponents(handoff, ref.id, ComponentSegment.Previews); + } +}; + /** * Watches the runtime configuration for changes. * @@ -257,6 +266,8 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState) await processComponents(handoff); } else if (handle) { await handle.apply(handoff, entryAfter?.entityId); + } else if (entryAfter?.kind === 'pattern') { + await rebuildPatternComponentPreviews(handoff, entryAfter.entityId); } else { const effectiveId = entryAfter?.entityId ?? path.basename(path.dirname(changedFile)); await processComponents(handoff, effectiveId); @@ -300,9 +311,10 @@ const watchEntityDirectories = (handoff: Handoff, state: WatcherState, chokidarC /** * Entity-specific rebuild work, called after reload + persistClientConfig + * watcher re-registration. Receives the handoff instance (with fresh runtime - * config) and the directory name of the new entity. + * config), the triggering directory name, and actual entity ids discovered + * after reload. */ - onDetected: (handoff: Handoff, dirName: string) => Promise; + onDetected: (handoff: Handoff, context: { dirName: string; addedIds: string[] }) => Promise; }) => { const { getConfigPaths, getKnownIds, getWatcher, setWatcher, scheduleKeyPrefix, entityLabel, onDetected } = options; @@ -357,11 +369,11 @@ const watchEntityDirectories = (handoff: Handoff, state: WatcherState, chokidarC const idsBefore = new Set(getKnownIds(handoff)); handoff.reload(); - knownIds.add(dirName); // Refresh from the post-reload runtime config so any ids that were // discovered alongside the new entity are also marked as known. const idsAfter = getKnownIds(handoff); + const addedIds = idsAfter.filter((id) => !idsBefore.has(id)); for (const id of idsAfter) { knownIds.add(id); } @@ -377,13 +389,12 @@ const watchEntityDirectories = (handoff: Handoff, state: WatcherState, chokidarC // If no new ids appeared after reload the file was empty or invalid. // Skip the build — watchRuntimeConfiguration will fire when it is saved. - const hasNewIds = idsAfter.some((id) => !idsBefore.has(id)); - if (!hasNewIds) { + if (addedIds.length === 0) { Logger.warn(`${entityLabel} "${dirName}" config is empty or incomplete — build will run automatically once the file is saved.`); return; } - await onDetected(handoff, dirName); + await onDetected(handoff, { dirName, addedIds }); } catch (e) { Logger.error(`Error processing new ${entityLabel}:`, e); } @@ -406,9 +417,11 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, setWatcher: (s, w) => { s.componentDirectoriesWatcher = w; }, scheduleKeyPrefix: 'newComponent', entityLabel: 'component', - onDetected: async (handoff, dirName) => { - await processComponents(handoff, dirName); - await runAllFinalizers(handoff, { patternRebuildComponentIds: [dirName] }); + onDetected: async (handoff, { addedIds }) => { + for (const componentId of addedIds) { + await processComponents(handoff, componentId); + await runAllFinalizers(handoff, { patternRebuildComponentIds: [componentId] }); + } }, }); }; @@ -429,22 +442,18 @@ export const watchPatternDirectories = (handoff: Handoff, state: WatcherState, c setWatcher: (s, w) => { s.patternDirectoriesWatcher = w; }, scheduleKeyPrefix: 'newPattern', entityLabel: 'pattern', - onDetected: async (handoff, dirName) => { + onDetected: async (handoff, { addedIds }) => { // Rebuild only the previews segment for each component the new pattern // references. Their JS/CSS/structure are unchanged — only the new // synthetic preview HTML files need to be generated so that buildPatterns // can assemble the pattern from them. - const newPattern = handoff.runtimeConfig?.entries?.patterns?.[dirName]; - if (newPattern?.components?.length) { - for (const ref of newPattern.components) { - await processComponents(handoff, ref.id, ComponentSegment.Previews); - } + for (const patternId of addedIds) { + await rebuildPatternComponentPreviews(handoff, patternId); } // Build only the new pattern directly rather than going through // runAllFinalizers, which would rebuild all patterns unnecessarily. - const newPatternId = newPattern?.id ?? dirName; - await buildPatterns(handoff, { onlyPatternIds: new Set([newPatternId]) }); + await buildPatterns(handoff, { onlyPatternIds: new Set(addedIds) }); }, }); }; From fc962bbd643beaa09c64f3c74e82b11eaace76c8 Mon Sep 17 00:00:00 2001 From: Domagoj Gojak Date: Sun, 17 May 2026 20:03:29 +0200 Subject: [PATCH 21/21] chore: update changelog for 1.2.2 --- Changelog.md | 52 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/Changelog.md b/Changelog.md index 20a6e6bf..027e77db 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,19 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## \[1.2.2] - 2026-04-09 +## \[1.2.2] - TBD -This patch release clears ESLint failures in the Next.js app pages and aligns -several code paths with the compiler target so production builds succeed without -enabling `--downlevelIteration`. +This patch release improves watch-mode reliability for patterns, broadens slot +default typing, clears downstream lint/build compatibility issues, and fixes a +few release-branch UI and dependency issues. -### Change +### Added -* Altered the avaliable types to the SlotMetadata so the default can handle - a wide variaty of types - `default?: string | number | boolean | object | any[] | null;` +* Added `watchPatternDirectories` so newly created pattern subdirectories are + detected during `start` without restarting the dev server. +* Added a shared `NotFound` component and reused it for stale pattern URLs and + missing markdown pages. -### Lint and code quality +### Changed +* Broadened `SlotMetadata.default` so slot defaults can be strings, numbers, + booleans, objects, arrays, or `null`. +* Refactored component and pattern directory watching through a shared + `watchEntityDirectories` helper. +* Downgraded incomplete or mid-edit pattern declarations from hard parse errors + to warnings, allowing watch mode to retry automatically after the file is + saved with valid content. +* Removed unneeded theme declarations. + +### Fixed + +* Fixed watch-mode rebuilds for newly added patterns by rebuilding referenced + component previews before composing the new pattern. +* Fixed pattern watcher edge cases where declaration IDs differ from directory + names, and where an initially incomplete pattern config later becomes valid. +* Fixed `.handoff.ts` declaration files not being detected by the new-entity + config-file gate. +* Extended component build-cache inputs to include pattern config files that + reference the component, preventing stale preview output after pattern changes. +* Fixed select/list handling when a component group or category value is empty. +* Fixed CSS typing issues and app build issues found during the release branch. * Resolved `no-else-return`, `no-unused-vars`, `camelcase`, `prefer-const`, `eqeqeq`, `no-nested-ternary`, `no-lonely-if`, and `no-use-before-define` across static pages (catch-all docs, assets, foundations, home, system, and @@ -26,17 +49,16 @@ enabling `--downlevelIteration`. * Removed or refactored unused `getStaticProps` context parameters, props, and locals; normalized naming (e.g. font machine keys); simplified icon detail routing query handling. -* Reordered helper components (token color/effect/typography tables, component - token previews) ahead of page components where required for declaration order. +* Reordered helper components ahead of page components where required for + declaration order. * Converted `getComponentPreviews` to a hoisted `function` declaration and refactored the component token table for clearer control flow. +* Avoided iterating `Map`, `Set`, `Iterable`, and `matchAll` results directly in + `for...of` where the compile target requires `--downlevelIteration`. -### Build / TypeScript compatibility +### Security -* Avoid iterating `Map` / `Set` / `Iterable` / `matchAll` results directly in - `for...of` where the compile target requires it: use `Array.from(...)` or - equivalent helpers in the config-diff registry, snapshot diff, CSF import - parsing, and component artifact sync. +* Updated Vite to address a security issue. ## \[1.2.1] - 2026-04-08