diff --git a/dist/app.js b/dist/app.js index 8e39e27b..21e6d717 100644 --- a/dist/app.js +++ b/dist/app.js @@ -493,6 +493,7 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () { return { js: builder_1.ComponentSegment.JavaScript, scss: builder_1.ComponentSegment.Style, + template: builder_1.ComponentSegment.Previews, templates: builder_1.ComponentSegment.Previews, }[type]; }; @@ -514,12 +515,8 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () { case 'unlink': if (!debounce) { debounce = true; - let segmentToUpdate = undefined; - const matchingPath = runtimeComponentPathsToWatch.get(file); - if (matchingPath) { - const entryType = runtimeComponentPathsToWatch.get(matchingPath); - segmentToUpdate = entryTypeToSegment(entryType); - } + const entryType = runtimeComponentPathsToWatch.get(file); + const segmentToUpdate = entryType ? entryTypeToSegment(entryType) : undefined; const componentDir = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(file))); yield (0, builder_1.default)(handoff, componentDir, segmentToUpdate); debounce = false; @@ -564,7 +561,7 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () { if (fs_extra_1.default.existsSync(normalizedComponentEntryPath)) { const entryType = runtimeComponentEntryType; if (fs_extra_1.default.statSync(normalizedComponentEntryPath).isFile()) { - result.set(path_1.default.dirname(normalizedComponentEntryPath), entryType); + result.set(path_1.default.resolve(normalizedComponentEntryPath), entryType); } else { result.set(normalizedComponentEntryPath, entryType); @@ -575,21 +572,6 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () { } return result; }; - /* - if (fs.existsSync(path.resolve(handoff.workingPath, 'handoff.config.json'))) { - chokidar.watch(path.resolve(handoff.workingPath, 'handoff.config.json'), { ignoreInitial: true }).on('all', async (event, file) => { - console.log(chalk.yellow('handoff.config.json changed. Please restart server to see changes...')); - if (!debounce) { - debounce = true; - handoff.reload(); - watchRuntimeComponents(getRuntimeComponentsPathsToWatch()); - watchRuntimeConfiguration(); - await processComponents(handoff, undefined, sharedStyles, documentationObject.components); - debounce = false; - } - }); - } - */ watchRuntimeComponents(getRuntimeComponentsPathsToWatch()); watchRuntimeConfiguration(); if (((_f = (_e = handoff.integrationObject) === null || _e === void 0 ? void 0 : _e.entries) === null || _f === void 0 ? void 0 : _f.integration) && fs_extra_1.default.existsSync((_h = (_g = handoff.integrationObject) === null || _g === void 0 ? void 0 : _g.entries) === null || _h === void 0 ? void 0 : _h.integration)) { diff --git a/dist/app/components/Component/Cards.d.ts b/dist/app/components/Component/Cards.d.ts new file mode 100644 index 00000000..476ae0ee --- /dev/null +++ b/dist/app/components/Component/Cards.d.ts @@ -0,0 +1,25 @@ +import React from 'react'; +/** + * Card interface for the Cards component + */ +export interface Card { + /** Title of the card */ + title: string; + /** Content of the card (supports multi-line with \n separators) */ + content: string; + /** (Optional) Visual type of the card affecting icon and colors */ + type?: 'positive' | 'negative'; +} +/** + * Props for the Cards component + */ +export interface CardsProps { + /** Array of cards to display */ + cards: Card[]; + /** Maximum number of cards per row (1-2, default: 2) */ + maxCardsPerRow?: 1 | 2; + /** Additional CSS classes */ + className?: string; +} +declare const Cards: React.FC; +export default Cards; diff --git a/dist/app/components/Component/Cards.jsx b/dist/app/components/Component/Cards.jsx new file mode 100644 index 00000000..e93d1a36 --- /dev/null +++ b/dist/app/components/Component/Cards.jsx @@ -0,0 +1,62 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const react_1 = __importDefault(require("react")); +const CardIcon = ({ type }) => { + switch (type) { + case 'positive': + return ( + + ); + case 'negative': + return ( + + ); + default: + return <>; + } +}; +const CardItem = ({ card }) => { + // Split content by newlines and render each line as a separate item + const lines = card.content.split('\n').filter((line) => line.trim()); + return (); +}; +const Cards = ({ cards, maxCardsPerRow = 2, className = '' }) => { + if (!cards || cards.length === 0) { + return null; + } + // Calculate grid columns based on maxCardsPerRow (always full width, max 2 per row) + const getGridCols = () => { + if (maxCardsPerRow === 1) + return 'grid-cols-1'; + if (maxCardsPerRow === 2) + return 'grid-cols-1 sm:grid-cols-2'; + if (maxCardsPerRow === 3) + return 'grid-cols-1 sm:grid-cols-2'; // Cap at 2 per row + return 'grid-cols-1 sm:grid-cols-2'; // Default to max 2 per row + }; + return (
+
+ {cards.map((card, index) => (
+

+ {card.title} +

+
+ +
+
))} +
+
); +}; +exports.default = Cards; diff --git a/dist/index.d.ts b/dist/index.d.ts index 4785831e..0f279d9c 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -109,7 +109,7 @@ declare class Handoff { private readJsonFile; } export declare const initIntegrationObject: (handoff: Handoff) => [integrationObject: IntegrationObject, configs: string[]]; -export type { ComponentListObject as Component } from './transformers/preview/types'; +export type { ComponentObject as Component } from './transformers/preview/types'; export type { Config } from './types/config'; export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core'; export default Handoff; diff --git a/dist/transformers/preview/component/api.d.ts b/dist/transformers/preview/component/api.d.ts index 5badbed7..edfe1f96 100644 --- a/dist/transformers/preview/component/api.d.ts +++ b/dist/transformers/preview/component/api.d.ts @@ -7,7 +7,7 @@ export declare const getAPIPath: (handoff: Handoff) => string; * @param componentData */ declare const writeComponentSummaryAPI: (handoff: Handoff, componentData: ComponentListObject[]) => Promise; -export declare const writeComponentApi: (id: string, component: TransformComponentTokensResult, version: string, handoff: Handoff, isPartialUpdate?: boolean) => Promise; +export declare const writeComponentApi: (id: string, component: TransformComponentTokensResult, version: string, handoff: Handoff, preserveKeys?: string[]) => Promise; export declare const writeComponentMetadataApi: (id: string, summary: ComponentListObject, handoff: Handoff) => Promise; /** * Update the main component summary API with the new component data diff --git a/dist/transformers/preview/component/api.js b/dist/transformers/preview/component/api.js index c2a976b6..d4f50207 100644 --- a/dist/transformers/preview/component/api.js +++ b/dist/transformers/preview/component/api.js @@ -15,10 +15,31 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.updateComponentSummaryApi = exports.writeComponentMetadataApi = exports.writeComponentApi = exports.getAPIPath = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); -function updateObject(target, source) { - return Object.entries(source).reduce((acc, [key, value]) => { - if (value !== undefined && value !== null && value !== '') { - acc[key] = value; +/** + * Merges values from a source object into a target object, returning a new object. + * For each key present in either object: + * - If the key is listed in preserveKeys and the source value is undefined, null, or an empty string, + * the target's value is preserved. + * - Otherwise, the value from the source is used (even if undefined, null, or empty string). + * This is useful for partial updates where some properties should not be overwritten unless explicitly set. + * + * @param target - The original object to merge into + * @param source - The object containing new values + * @param preserveKeys - Keys for which the target's value should be preserved if the source value is undefined, null, or empty string + * @returns A new object with merged values + */ +function updateObject(target, source, preserveKeys = []) { + // Collect all unique keys from both target and source + const allKeys = Array.from(new Set([...Object.keys(target), ...Object.keys(source)])); + return allKeys.reduce((acc, key) => { + const sourceValue = source[key]; + const targetValue = target[key]; + // Preserve existing values for specified keys when source value is undefined + if (preserveKeys.includes(key) && (sourceValue === undefined || sourceValue === null || sourceValue === '')) { + acc[key] = targetValue; + } + else { + acc[key] = sourceValue; } return acc; }, Object.assign({}, target)); @@ -42,22 +63,23 @@ const writeComponentSummaryAPI = (handoff, componentData) => __awaiter(void 0, v componentData.sort((a, b) => a.title.localeCompare(b.title)); yield fs_extra_1.default.writeFile(path_1.default.resolve((0, exports.getAPIPath)(handoff), 'components.json'), JSON.stringify(componentData, null, 2)); }); -const writeComponentApi = (id_1, component_1, version_1, handoff_1, ...args_1) => __awaiter(void 0, [id_1, component_1, version_1, handoff_1, ...args_1], void 0, function* (id, component, version, handoff, isPartialUpdate = false) { +const writeComponentApi = (id_1, component_1, version_1, handoff_1, ...args_1) => __awaiter(void 0, [id_1, component_1, version_1, handoff_1, ...args_1], void 0, function* (id, component, version, handoff, preserveKeys = []) { const outputDirPath = path_1.default.resolve((0, exports.getAPIPath)(handoff), 'component', id); - if (isPartialUpdate) { - const outputFilePath = path_1.default.resolve(outputDirPath, `${version}.json`); - if (fs_extra_1.default.existsSync(outputFilePath)) { - const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8'); - if (existingJson) { - try { - const existingData = JSON.parse(existingJson); - const mergedData = updateObject(existingData, component); - yield fs_extra_1.default.writeFile(path_1.default.resolve(outputDirPath, `${version}.json`), JSON.stringify(mergedData, null, 2)); - return; - } - catch (_) { - // Unable to parse existing file - } + const outputFilePath = path_1.default.resolve(outputDirPath, `${version}.json`); + if (fs_extra_1.default.existsSync(outputFilePath)) { + const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8'); + if (existingJson) { + try { + const existingData = JSON.parse(existingJson); + // Special case: always allow page to be cleared when undefined + // This handles the case where page slices are removed + const finalPreserveKeys = component.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys; + const mergedData = updateObject(existingData, component, finalPreserveKeys); + yield fs_extra_1.default.writeFile(path_1.default.resolve(outputDirPath, `${version}.json`), JSON.stringify(mergedData, null, 2)); + return; + } + catch (_) { + // Unable to parse existing file } } } diff --git a/dist/transformers/preview/component/builder.js b/dist/transformers/preview/component/builder.js index a6c9aa06..cc18eeb7 100644 --- a/dist/transformers/preview/component/builder.js +++ b/dist/transformers/preview/component/builder.js @@ -70,6 +70,32 @@ var ComponentSegment; ComponentSegment["Previews"] = "previews"; ComponentSegment["Validation"] = "validation"; })(ComponentSegment || (exports.ComponentSegment = ComponentSegment = {})); +/** + * Determines which keys should be preserved based on the segment being processed. + * When processing a specific segment, we want to preserve data from other segments + * to avoid overwriting them with undefined values. + */ +function getPreserveKeysForSegment(segmentToProcess) { + if (!segmentToProcess) { + return []; // No preservation needed for full updates + } + switch (segmentToProcess) { + case ComponentSegment.JavaScript: + // When processing JavaScript segment, preserve CSS and previews data + return ['css', 'sass', 'sharedStyles', 'previews', 'validations']; + case ComponentSegment.Style: + // When processing Style segment, preserve JavaScript and previews data + return ['js', 'jsCompiled', 'previews', 'validations']; + case ComponentSegment.Previews: + // When processing Previews segment, preserve JavaScript and CSS data + return ['js', 'jsCompiled', 'css', 'sass', 'sharedStyles', 'validations']; + case ComponentSegment.Validation: + // When processing Validation segment, preserve all other data + return ['js', 'jsCompiled', 'css', 'sass', 'sharedStyles', 'previews']; + default: + return []; + } +} /** * Process components and generate their code, styles, and previews * @param handoff - The Handoff instance containing configuration and state @@ -84,6 +110,10 @@ function processComponents(handoff, id, segmentToProcess) { const components = (yield handoff.getDocumentationObject()).components; const sharedStyles = yield handoff.getSharedStyles(); const runtimeComponents = (_c = (_b = (_a = handoff.integrationObject) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) !== null && _c !== void 0 ? _c : {}; + // Determine which keys to preserve based on the segment being processed + // This ensures that when processing only specific segments (e.g., JavaScript only), + // we don't overwrite data from other segments (e.g., CSS, previews) with undefined values + const preserveKeys = getPreserveKeysForSegment(segmentToProcess); for (const runtimeComponentId of Object.keys(runtimeComponents)) { if (!!id && runtimeComponentId !== id) { continue; @@ -110,13 +140,13 @@ function processComponents(handoff, id, segmentToProcess) { data.validations = validationResults; } data.sharedStyles = sharedStyles; - yield (0, api_1.writeComponentApi)(runtimeComponentId, data, version, handoff, true); + yield (0, api_1.writeComponentApi)(runtimeComponentId, data, version, handoff, preserveKeys); if (version === latest) { latestVersion = data; } }))); if (latestVersion) { - yield (0, api_1.writeComponentApi)(runtimeComponentId, latestVersion, 'latest', handoff, true); + yield (0, api_1.writeComponentApi)(runtimeComponentId, latestVersion, 'latest', handoff, preserveKeys); const summary = buildComponentSummary(runtimeComponentId, latestVersion, versions); yield (0, api_1.writeComponentMetadataApi)(runtimeComponentId, summary, handoff); result.push(summary); diff --git a/dist/transformers/preview/component/css.d.ts b/dist/transformers/preview/component/css.d.ts index 3ad70728..8aab81a9 100644 --- a/dist/transformers/preview/component/css.d.ts +++ b/dist/transformers/preview/component/css.d.ts @@ -40,6 +40,7 @@ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff: }; }; validations?: Record; + page?: import("../types").ComponentPageDefinition; }>; /** * Build the main CSS file using Vite diff --git a/dist/transformers/preview/component/json.d.ts b/dist/transformers/preview/component/json.d.ts deleted file mode 100644 index 43f2e0a7..00000000 --- a/dist/transformers/preview/component/json.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { TransformComponentTokensResult } from '../types'; -declare const parseComponentJson: (id: string, location: string, data: TransformComponentTokensResult) => Promise; -export default parseComponentJson; diff --git a/dist/transformers/preview/component/json.js b/dist/transformers/preview/component/json.js deleted file mode 100644 index da3d79e8..00000000 --- a/dist/transformers/preview/component/json.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const chalk_1 = __importDefault(require("chalk")); -const fs_extra_1 = __importDefault(require("fs-extra")); -const path_1 = __importDefault(require("path")); -const types_1 = require("../types"); -const parseComponentJson = (id, location, data) => __awaiter(void 0, void 0, void 0, function* () { - // Is there a JSON file with the same name? - const jsonFile = id + '.json'; - const jsonPath = path_1.default.resolve(location, jsonFile); - let parsed = {}; - if (fs_extra_1.default.existsSync(jsonPath)) { - const json = yield fs_extra_1.default.readFile(jsonPath, 'utf8'); - if (json) { - try { - parsed = JSON.parse(json); - // The JSON file defines each of the fields - if (parsed) { - data.title = parsed.title; - data.image = parsed.image; - data.should_do = parsed.should_do || []; - data.should_not_do = parsed.should_not_do || []; - data.type = parsed.type || types_1.ComponentType.Element; - data.group = parsed.group || 'default'; - data.tags = parsed.tags || []; - data.categories = parsed.categories || []; - data.figma = parsed.figma || ''; - data.description = parsed.description; - data.properties = parsed.properties; - data.previews = parsed.previews; - data.options = parsed.options; - } - } - catch (e) { - console.log(chalk_1.default.red(`Error parsing JSON for ${id}`)); - console.log(e); - } - } - } - else { - console.log(chalk_1.default.red(`No JSON file found for ${id}`)); - } - return data; -}); -exports.default = parseComponentJson; diff --git a/dist/transformers/preview/types.d.ts b/dist/transformers/preview/types.d.ts index a7c4095a..d6c37536 100644 --- a/dist/transformers/preview/types.d.ts +++ b/dist/transformers/preview/types.d.ts @@ -1,3 +1,4 @@ +import { Card } from '../../app/components/Component/Cards'; import { ValidationResult } from '../../types'; import { Filter } from '../../utils/filter'; import { SlotMetadata } from './component'; @@ -7,49 +8,97 @@ export declare enum ComponentType { Navigation = "navigation", Utility = "utility" } -export type ComponentListObject = { - id?: string; - version: string; - image: string; - title: string; - type: string; - group: string; - categories: string[]; - tags: string[]; - description: string; - figma: string; - properties: { - [key: string]: SlotMetadata; +export type PageSliceType = 'BEST_PRACTICES' | 'COMPONENT_DISPLAY' | 'VALIDATION_RESULTS' | 'PROPERTIES' | 'TEXT' | 'CARDS'; +export interface BasePageSlice { + type: PageSliceType; +} +export interface BestPracticesPageSlice extends BasePageSlice { + type: 'BEST_PRACTICES'; +} +export interface ComponentDisplayPageSlice extends BasePageSlice { + type: 'COMPONENT_DISPLAY'; + showPreview?: boolean; + showCodeHighlight?: boolean; + defaultHeight?: string; + filterBy?: Filter; +} +export interface ValidationResultsPageSlice extends BasePageSlice { + type: 'VALIDATION_RESULTS'; +} +export interface PropertiesPageSlice extends BasePageSlice { + type: 'PROPERTIES'; +} +export interface TextPageSlice extends BasePageSlice { + type: 'TEXT'; + /** Optional title text (always rendered as H3) */ + title?: string; + /** Optional HTML content to render */ + content?: string; +} +export interface CardsPageSlice extends BasePageSlice { + type: 'CARDS'; + /** Array of cards to display */ + cards: Card[]; + /** Maximum number of cards per row (default: 2, max: 2, always full width) */ + maxCardsPerRow?: 1 | 2; +} +/** + * Discriminated union type for all page slices. + * Provides type safety by ensuring each slice type has its specific settings. + * TypeScript will narrow the type based on the 'type' discriminator property. + */ +export type PageSlice = BestPracticesPageSlice | ComponentDisplayPageSlice | ValidationResultsPageSlice | PropertiesPageSlice | TextPageSlice | CardsPageSlice; +export type ComponentPageDefinition = { + slices: PageSlice[]; + options?: Record; +}; +export type ComponentOptions = { + preview?: { + groupBy?: string; + filterBy?: Filter; }; - versions: string[]; - previews: { - [key: string]: OptionalPreviewRender; + transformer: { + cssRootClass?: string; + tokenNameSegments?: string[]; + defaults: { + [variantProperty: string]: string; + }; + replace: { + [variantProperty: string]: { + [source: string]: string; + }; + }; }; - paths: string[]; +}; +export type ComponentObject = { + title: string; + description: string; + image: string; + group: string; + type: string; entries?: { js?: string; scss?: string; templates?: string; }; - options?: { - preview?: { - groupBy?: string; - filterBy?: Filter; - }; - transformer: { - cssRootClass?: string; - tokenNameSegments?: string[]; - defaults: { - [variantProperty: string]: string; - }; - replace: { - [variantProperty: string]: { - [source: string]: string; - }; - }; - }; + properties: { + [key: string]: SlotMetadata; + }; + previews: { + [key: string]: OptionalPreviewRender; }; + categories?: string[]; + tags?: string[]; + figma?: string; + page?: ComponentPageDefinition; + options?: ComponentOptions; }; +export type ComponentListObject = { + id: string; + version: string; + versions: string[]; + paths: string[]; +} & ComponentObject; export type TransformComponentTokensResult = { id: string; source?: 'figma' | 'custom'; @@ -89,11 +138,8 @@ export type TransformComponentTokensResult = { groupBy?: string; }; }; - /** - * Validation results for the component - * Each key represents a validation type and the value contains detailed validation results - */ validations?: Record; + page?: ComponentPageDefinition; } | null; export type OptionalPreviewRender = { title: string; diff --git a/dist/types.d.ts b/dist/types.d.ts index 80e3f237..c6311065 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -1,5 +1,6 @@ import { Types as CoreTypes } from 'handoff-core'; import { SlotMetadata } from './transformers/preview/component'; +import { ComponentPageDefinition } from './transformers/preview/types'; import { type Filter } from './utils/filter'; export interface ValidationResult { /** @@ -70,6 +71,7 @@ export interface PreviewObject { * Each key represents a validation type and the value contains detailed validation results */ validations?: Record; + page?: ComponentPageDefinition; } export type PreviewJson = { components: { diff --git a/dist/utils/filter.js b/dist/utils/filter.js index 6b800dbd..e22591fb 100644 --- a/dist/utils/filter.js +++ b/dist/utils/filter.js @@ -12,31 +12,31 @@ function evaluateFilter(obj, filter) { var _a, _b, _c; // Handle array of field filters (implicit AND) if (Array.isArray(filter)) { - const results = filter.map(f => evaluateFieldFilter(obj, f)); + const results = filter.map((f) => evaluateFieldFilter(obj, f)); // If any filter doesn't match, return false - if (!results.every(r => r.matches)) { + if (!results.every((r) => r.matches)) { return { matches: false }; } // If any filter has an order, use the first one (since we're using AND) - const order = (_a = results.find(r => r.order !== undefined)) === null || _a === void 0 ? void 0 : _a.order; + const order = (_a = results.find((r) => r.order !== undefined)) === null || _a === void 0 ? void 0 : _a.order; return { matches: true, order }; } // Handle logical filter if ('and' in filter) { - const results = filter.and.map(f => evaluateFilter(obj, f)); - if (!results.every(r => r.matches)) { + const results = filter.and.map((f) => evaluateFilter(obj, f)); + if (!results.every((r) => r.matches)) { return { matches: false }; } - const order = (_b = results.find(r => r.order !== undefined)) === null || _b === void 0 ? void 0 : _b.order; + const order = (_b = results.find((r) => r.order !== undefined)) === null || _b === void 0 ? void 0 : _b.order; return { matches: true, order }; } if ('or' in filter) { - const results = filter.or.map(f => evaluateFilter(obj, f)); - if (!results.some(r => r.matches)) { + const results = filter.or.map((f) => evaluateFilter(obj, f)); + if (!results.some((r) => r.matches)) { return { matches: false }; } // For OR, we use the first matching order - const order = (_c = results.find(r => r.matches && r.order !== undefined)) === null || _c === void 0 ? void 0 : _c.order; + const order = (_c = results.find((r) => r.matches && r.order !== undefined)) === null || _c === void 0 ? void 0 : _c.order; return { matches: true, order }; } if ('not' in filter) { @@ -54,13 +54,22 @@ function evaluateFilter(obj, filter) { function evaluateFieldFilter(obj, filter) { const { field, op, value } = filter; const actual = obj[field]; + if (op === 'neq') { + console.log('EVAL', filter, actual, actual !== value); + } switch (op) { - case 'eq': return { matches: actual === value }; - case 'neq': return { matches: actual !== value }; - case 'gt': return { matches: actual > value }; - case 'gte': return { matches: actual >= value }; - case 'lt': return { matches: actual < value }; - case 'lte': return { matches: actual <= value }; + case 'eq': + return { matches: actual === value }; + case 'neq': + return { matches: actual !== value }; + case 'gt': + return { matches: actual > value }; + case 'gte': + return { matches: actual >= value }; + case 'lt': + return { matches: actual < value }; + case 'lte': + return { matches: actual <= value }; case 'contains': if (typeof actual === 'string') return { matches: actual.includes(value) }; @@ -72,8 +81,10 @@ function evaluateFieldFilter(obj, filter) { return { matches: false }; const index = value.indexOf(actual); return { matches: index !== -1, order: index }; - case 'nin': return { matches: Array.isArray(value) && !value.includes(actual) }; - default: return { matches: false }; + case 'nin': + return { matches: Array.isArray(value) && !value.includes(actual) }; + default: + return { matches: false }; } } /** @@ -83,9 +94,9 @@ function evaluateFieldFilter(obj, filter) { * @returns Filtered and sorted array of objects */ function filterAndSort(items, filter) { - const results = items.map(item => ({ + const results = items.map((item) => ({ item, - result: evaluateFilter(item, filter) + result: evaluateFilter(item, filter), })); return results .filter(({ result }) => result.matches) diff --git a/src/app.ts b/src/app.ts index 768406e1..0549a09b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -509,6 +509,7 @@ export const watchApp = async (handoff: Handoff): Promise => { return { js: ComponentSegment.JavaScript, scss: ComponentSegment.Style, + template: ComponentSegment.Previews, templates: ComponentSegment.Previews, }[type]; }; @@ -534,13 +535,8 @@ export const watchApp = async (handoff: Handoff): Promise => { case 'unlink': if (!debounce) { debounce = true; - let segmentToUpdate: ComponentSegment = undefined; - const matchingPath = runtimeComponentPathsToWatch.get(file); - - if (matchingPath) { - const entryType = runtimeComponentPathsToWatch.get(matchingPath); - segmentToUpdate = entryTypeToSegment(entryType); - } + const entryType = runtimeComponentPathsToWatch.get(file); + const segmentToUpdate: ComponentSegment = entryType ? entryTypeToSegment(entryType) : undefined; const componentDir = path.basename(path.dirname(path.dirname(file))); await processComponents(handoff, componentDir, segmentToUpdate); @@ -589,7 +585,7 @@ export const watchApp = async (handoff: Handoff): Promise => { if (fs.existsSync(normalizedComponentEntryPath)) { const entryType = runtimeComponentEntryType as keyof ComponentListObject['entries']; if (fs.statSync(normalizedComponentEntryPath).isFile()) { - result.set(path.dirname(normalizedComponentEntryPath), entryType); + result.set(path.resolve(normalizedComponentEntryPath), entryType); } else { result.set(normalizedComponentEntryPath, entryType); } @@ -601,22 +597,6 @@ export const watchApp = async (handoff: Handoff): Promise => { return result; }; - /* - if (fs.existsSync(path.resolve(handoff.workingPath, 'handoff.config.json'))) { - chokidar.watch(path.resolve(handoff.workingPath, 'handoff.config.json'), { ignoreInitial: true }).on('all', async (event, file) => { - console.log(chalk.yellow('handoff.config.json changed. Please restart server to see changes...')); - if (!debounce) { - debounce = true; - handoff.reload(); - watchRuntimeComponents(getRuntimeComponentsPathsToWatch()); - watchRuntimeConfiguration(); - await processComponents(handoff, undefined, sharedStyles, documentationObject.components); - debounce = false; - } - }); - } - */ - watchRuntimeComponents(getRuntimeComponentsPathsToWatch()); watchRuntimeConfiguration(); diff --git a/src/app/components/Component/BestPracticesCard.tsx b/src/app/components/Component/BestPracticesCard.tsx index 29a32b0d..84ecd434 100644 --- a/src/app/components/Component/BestPracticesCard.tsx +++ b/src/app/components/Component/BestPracticesCard.tsx @@ -1,43 +1,35 @@ import { PreviewObject } from '@handoff/types'; -import { Check, X } from 'lucide-react'; import React from 'react'; - -const PracticeLine: React.FC<{ rule: string; yes: boolean }> = ({ rule, yes }) => ( -
  • - {yes ? ( - - ) : ( - - )} -

    {rule}

    -
  • -); +import Cards, { Card } from './Cards'; const BestPracticesCard: React.FC<{ component: PreviewObject }> = ({ component }) => { + const cards: Card[] = []; + + // Add best practices card if available + if (component.should_do && component.should_do.length > 0) { + cards.push({ + title: 'Best Practices', + content: component.should_do.join('\n'), + type: 'positive' + }); + } + + // Add common mistakes card if available + if (component.should_not_do && component.should_not_do.length > 0) { + cards.push({ + title: 'Common Mistakes', + content: component.should_not_do.join('\n'), + type: 'negative' + }); + } + + if (cards.length === 0) { + return null; + } + return ( -
    -
    - {component.should_do && component.should_do.length > 0 && ( -
    -

    Best Practices

    -
      - {component.should_do.map((rule, index) => ( - - ))} -
    -
    - )} - {component.should_not_do && component.should_not_do.length > 0 && ( -
    -

    Common Mistakes

    -
      - {component.should_not_do.map((rule, index) => ( - - ))} -
    -
    - )} -
    +
    +
    ); }; diff --git a/src/app/components/Component/Cards.tsx b/src/app/components/Component/Cards.tsx new file mode 100644 index 00000000..818333d6 --- /dev/null +++ b/src/app/components/Component/Cards.tsx @@ -0,0 +1,114 @@ +import React from 'react'; + +/** + * Card interface for the Cards component + */ +export interface Card { + /** Title of the card */ + title: string; + /** Content of the card (supports multi-line with \n separators) */ + content: string; + /** (Optional) Visual type of the card affecting icon and colors */ + type?: 'positive' | 'negative'; +} + +/** + * Props for the Cards component + */ +export interface CardsProps { + /** Array of cards to display */ + cards: Card[]; + /** Maximum number of cards per row (1-2, default: 2) */ + maxCardsPerRow?: 1 | 2; + /** Additional CSS classes */ + className?: string; +} + +const CardIcon: React.FC<{ type: Card['type'] }> = ({ type }) => { + switch (type) { + case 'positive': + return ( + + + + ); + case 'negative': + return ( + + + + ); + default: + return <>; + } +}; + +const CardItem: React.FC<{ card: Card }> = ({ card }) => { + // Split content by newlines and render each line as a separate item + const lines = card.content.split('\n').filter((line) => line.trim()); + + return ( +
      + {lines.map((line, index) => ( +
    • + +

      {line}

      +
    • + ))} +
    + ); +}; + +const Cards: React.FC = ({ cards, maxCardsPerRow = 2, className = '' }) => { + if (!cards || cards.length === 0) { + return null; + } + + // Calculate grid columns based on maxCardsPerRow (always full width, max 2 per row) + const getGridCols = () => { + if (maxCardsPerRow === 1) return 'grid-cols-1'; + if (maxCardsPerRow === 2) return 'grid-cols-1 sm:grid-cols-2'; + if (maxCardsPerRow === 3) return 'grid-cols-1 sm:grid-cols-2'; // Cap at 2 per row + return 'grid-cols-1 sm:grid-cols-2'; // Default to max 2 per row + }; + + return ( +
    +
    + {cards.map((card, index) => ( +
    +

    + {card.title} +

    +
    + +
    +
    + ))} +
    +
    + ); +}; + +export default Cards; diff --git a/src/app/components/Component/PageSliceResolver.tsx b/src/app/components/Component/PageSliceResolver.tsx new file mode 100644 index 00000000..016314d2 --- /dev/null +++ b/src/app/components/Component/PageSliceResolver.tsx @@ -0,0 +1,74 @@ +import { PageSlice } from '@handoff/transformers/preview/types'; +import { PreviewObject } from '@handoff/types'; +import React from 'react'; +import { BestPracticesSlice, CardsSlice, ComponentDisplaySlice, PropertiesSlice, TextSlice, ValidationResultsSlice } from './slices'; + +export interface PageSliceResolverProps { + slice: PageSlice; + preview: PreviewObject; + title: string; + height?: string; + currentValues?: Record; + onValuesChange?: (values: Record) => void; + bestPracticesCard?: boolean; + codeHighlight?: boolean; + properties?: boolean; + validations?: boolean; +} + +export const PageSliceResolver: React.FC = ({ + slice, + preview, + title, + height, + currentValues, + onValuesChange, + bestPracticesCard = true, + codeHighlight = true, + properties = true, + validations = true, +}) => { + switch (slice.type) { + case 'BEST_PRACTICES': { + return ; + } + + case 'COMPONENT_DISPLAY': { + return ( + + ); + } + + case 'VALIDATION_RESULTS': { + return ; + } + + case 'PROPERTIES': { + return ; + } + + case 'TEXT': { + return ; + } + + case 'CARDS': { + return ; + } + + default: + return null; + } +}; + +export default PageSliceResolver; diff --git a/src/app/components/Component/Preview.tsx b/src/app/components/Component/Preview.tsx index 564e6408..39328e06 100644 --- a/src/app/components/Component/Preview.tsx +++ b/src/app/components/Component/Preview.tsx @@ -1,4 +1,5 @@ import { SlotMetadata } from '@handoff/transformers/preview/component'; +import { PageSlice } from '@handoff/transformers/preview/types'; import { PreviewObject } from '@handoff/types'; import { Types as CoreTypes } from 'handoff-core'; import { startCase } from 'lodash'; @@ -18,8 +19,6 @@ import React, { useCallback, useContext, useEffect } from 'react'; import { HotReloadContext } from '../context/HotReloadProvider'; import { usePreviewContext } from '../context/PreviewContext'; import RulesSheet from '../Foundations/RulesSheet'; -import { CodeHighlight } from '../Markdown/CodeHighlight'; -import HeadersType from '../Typography/Headers'; import { Badge } from '../ui/badge'; import { Button } from '../ui/button'; import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; @@ -27,8 +26,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '. import { Separator } from '../ui/separator'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; -import { ValidationResults } from '../Validation/ValidationResults'; -import BestPracticesCard from './BestPracticesCard'; +import PageSliceResolver from './PageSliceResolver'; + +const getDefaultSlices = (): PageSlice[] => [ + { type: 'BEST_PRACTICES' }, + { type: 'COMPONENT_DISPLAY' }, + { type: 'VALIDATION_RESULTS' }, + { type: 'PROPERTIES' }, +]; export type ComponentPreview = { component: CoreTypes.IComponentInstance; @@ -56,6 +61,32 @@ export const ComponentDisplay: React.FC<{ const [inspect, setInspect] = React.useState(false); const [scale, setScale] = React.useState(0.8); + // Generate variants from component previews if they differ from context + const localVariants = React.useMemo(() => { + if (!component?.previews) return null; + + // Check if component previews are different from context previews + const componentPreviewKeys = Object.keys(component.previews).sort(); + const contextPreviewKeys = context.preview ? Object.keys(context.preview.previews).sort() : []; + + if (JSON.stringify(componentPreviewKeys) !== JSON.stringify(contextPreviewKeys)) { + // Generate variants from component previews + const variantMap: Record> = {}; + Object.values(component.previews).forEach((preview: any) => { + Object.entries(preview.values).forEach(([key, value]) => { + if (!variantMap[key]) { + variantMap[key] = new Set(); + } + variantMap[key].add(String(value)); + }); + }); + + return Object.fromEntries(Object.entries(variantMap).map(([key, values]) => [key, Array.from(values)])); + } + + return context.variants; + }, [component?.previews, context.preview, context.variants]); + const onLoad = useCallback(() => { if (defaultHeight) { setHeight(defaultHeight); @@ -125,14 +156,14 @@ export const ComponentDisplay: React.FC<{ <>
    - {context.variants ? ( + {localVariants ? ( <> - {Object.keys(context.variants).length > 0 && ( + {Object.keys(localVariants).length > 0 && ( <>

    {title ?? 'Variant'}

    - {Object.keys(context.variants) - .filter((variantProperty) => context.variants[variantProperty].length > 1) + {Object.keys(localVariants) + .filter((variantProperty) => localVariants[variantProperty].length > 1) .map((variantProperty) => (