Skip to content

Commit 0224d1a

Browse files
authored
Merge pull request #189 from Convertiv/feature/components-page-builder
Component page customization support
2 parents 402c54f + 04eb5fc commit 0224d1a

File tree

32 files changed

+978
-397
lines changed

32 files changed

+978
-397
lines changed

dist/app.js

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
493493
return {
494494
js: builder_1.ComponentSegment.JavaScript,
495495
scss: builder_1.ComponentSegment.Style,
496+
template: builder_1.ComponentSegment.Previews,
496497
templates: builder_1.ComponentSegment.Previews,
497498
}[type];
498499
};
@@ -514,12 +515,8 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
514515
case 'unlink':
515516
if (!debounce) {
516517
debounce = true;
517-
let segmentToUpdate = undefined;
518-
const matchingPath = runtimeComponentPathsToWatch.get(file);
519-
if (matchingPath) {
520-
const entryType = runtimeComponentPathsToWatch.get(matchingPath);
521-
segmentToUpdate = entryTypeToSegment(entryType);
522-
}
518+
const entryType = runtimeComponentPathsToWatch.get(file);
519+
const segmentToUpdate = entryType ? entryTypeToSegment(entryType) : undefined;
523520
const componentDir = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(file)));
524521
yield (0, builder_1.default)(handoff, componentDir, segmentToUpdate);
525522
debounce = false;
@@ -564,7 +561,7 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
564561
if (fs_extra_1.default.existsSync(normalizedComponentEntryPath)) {
565562
const entryType = runtimeComponentEntryType;
566563
if (fs_extra_1.default.statSync(normalizedComponentEntryPath).isFile()) {
567-
result.set(path_1.default.dirname(normalizedComponentEntryPath), entryType);
564+
result.set(path_1.default.resolve(normalizedComponentEntryPath), entryType);
568565
}
569566
else {
570567
result.set(normalizedComponentEntryPath, entryType);
@@ -575,21 +572,6 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
575572
}
576573
return result;
577574
};
578-
/*
579-
if (fs.existsSync(path.resolve(handoff.workingPath, 'handoff.config.json'))) {
580-
chokidar.watch(path.resolve(handoff.workingPath, 'handoff.config.json'), { ignoreInitial: true }).on('all', async (event, file) => {
581-
console.log(chalk.yellow('handoff.config.json changed. Please restart server to see changes...'));
582-
if (!debounce) {
583-
debounce = true;
584-
handoff.reload();
585-
watchRuntimeComponents(getRuntimeComponentsPathsToWatch());
586-
watchRuntimeConfiguration();
587-
await processComponents(handoff, undefined, sharedStyles, documentationObject.components);
588-
debounce = false;
589-
}
590-
});
591-
}
592-
*/
593575
watchRuntimeComponents(getRuntimeComponentsPathsToWatch());
594576
watchRuntimeConfiguration();
595577
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)) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
/**
3+
* Card interface for the Cards component
4+
*/
5+
export interface Card {
6+
/** Title of the card */
7+
title: string;
8+
/** Content of the card (supports multi-line with \n separators) */
9+
content: string;
10+
/** (Optional) Visual type of the card affecting icon and colors */
11+
type?: 'positive' | 'negative';
12+
}
13+
/**
14+
* Props for the Cards component
15+
*/
16+
export interface CardsProps {
17+
/** Array of cards to display */
18+
cards: Card[];
19+
/** Maximum number of cards per row (1-2, default: 2) */
20+
maxCardsPerRow?: 1 | 2;
21+
/** Additional CSS classes */
22+
className?: string;
23+
}
24+
declare const Cards: React.FC<CardsProps>;
25+
export default Cards;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
const react_1 = __importDefault(require("react"));
7+
const CardIcon = ({ type }) => {
8+
switch (type) {
9+
case 'positive':
10+
return (<svg className="mt-[5px] h-3 w-3 flex-shrink-0 text-emerald-600" strokeWidth={3} fill="none" viewBox="0 0 24 24" stroke="currentColor">
11+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7"/>
12+
</svg>);
13+
case 'negative':
14+
return (<svg className="mt-[5px] h-3 w-3 flex-shrink-0 text-gray-400" strokeWidth={3} fill="none" viewBox="0 0 24 24" stroke="currentColor">
15+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12"/>
16+
</svg>);
17+
default:
18+
return <></>;
19+
}
20+
};
21+
const CardItem = ({ card }) => {
22+
// Split content by newlines and render each line as a separate item
23+
const lines = card.content.split('\n').filter((line) => line.trim());
24+
return (<ul className="space-y-3">
25+
{lines.map((line, index) => (<li key={index} className="flex items-start gap-3">
26+
<CardIcon type={card.type}/>
27+
<p className="text-sm">{line}</p>
28+
</li>))}
29+
</ul>);
30+
};
31+
const Cards = ({ cards, maxCardsPerRow = 2, className = '' }) => {
32+
if (!cards || cards.length === 0) {
33+
return null;
34+
}
35+
// Calculate grid columns based on maxCardsPerRow (always full width, max 2 per row)
36+
const getGridCols = () => {
37+
if (maxCardsPerRow === 1)
38+
return 'grid-cols-1';
39+
if (maxCardsPerRow === 2)
40+
return 'grid-cols-1 sm:grid-cols-2';
41+
if (maxCardsPerRow === 3)
42+
return 'grid-cols-1 sm:grid-cols-2'; // Cap at 2 per row
43+
return 'grid-cols-1 sm:grid-cols-2'; // Default to max 2 per row
44+
};
45+
return (<div className={`flex flex-col gap-2 pb-7 ${className}`}>
46+
<div className={`grid gap-6 ${getGridCols()}`}>
47+
{cards.map((card, index) => (<div key={`card-${index}`} className={`relative rounded-lg border p-8 ${card.type === 'positive'
48+
? 'bg-gray-50 text-gray-600 dark:bg-gray-800'
49+
: card.type === 'negative'
50+
? 'bg-gray-50 text-gray-600 dark:bg-gray-800'
51+
: 'bg-gray-50 text-gray-600 dark:bg-gray-800'}`}>
52+
<h2 className={`mb-3 font-normal ${card.type === 'positive' ? 'text-gray-700' : card.type === 'negative' ? 'text-gray-900' : 'text-gray-800'}`}>
53+
{card.title}
54+
</h2>
55+
<div className={`${card.type === 'positive' ? 'text-emerald-800' : card.type === 'negative' ? 'text-red-800' : 'text-blue-800'}`}>
56+
<CardItem card={card}/>
57+
</div>
58+
</div>))}
59+
</div>
60+
</div>);
61+
};
62+
exports.default = Cards;

dist/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ declare class Handoff {
109109
private readJsonFile;
110110
}
111111
export declare const initIntegrationObject: (handoff: Handoff) => [integrationObject: IntegrationObject, configs: string[]];
112-
export type { ComponentListObject as Component } from './transformers/preview/types';
112+
export type { ComponentObject as Component } from './transformers/preview/types';
113113
export type { Config } from './types/config';
114114
export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core';
115115
export default Handoff;

dist/transformers/preview/component/api.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export declare const getAPIPath: (handoff: Handoff) => string;
77
* @param componentData
88
*/
99
declare const writeComponentSummaryAPI: (handoff: Handoff, componentData: ComponentListObject[]) => Promise<void>;
10-
export declare const writeComponentApi: (id: string, component: TransformComponentTokensResult, version: string, handoff: Handoff, isPartialUpdate?: boolean) => Promise<void>;
10+
export declare const writeComponentApi: (id: string, component: TransformComponentTokensResult, version: string, handoff: Handoff, preserveKeys?: string[]) => Promise<void>;
1111
export declare const writeComponentMetadataApi: (id: string, summary: ComponentListObject, handoff: Handoff) => Promise<void>;
1212
/**
1313
* Update the main component summary API with the new component data

dist/transformers/preview/component/api.js

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
1515
exports.updateComponentSummaryApi = exports.writeComponentMetadataApi = exports.writeComponentApi = exports.getAPIPath = void 0;
1616
const fs_extra_1 = __importDefault(require("fs-extra"));
1717
const path_1 = __importDefault(require("path"));
18-
function updateObject(target, source) {
19-
return Object.entries(source).reduce((acc, [key, value]) => {
20-
if (value !== undefined && value !== null && value !== '') {
21-
acc[key] = value;
18+
/**
19+
* Merges values from a source object into a target object, returning a new object.
20+
* For each key present in either object:
21+
* - If the key is listed in preserveKeys and the source value is undefined, null, or an empty string,
22+
* the target's value is preserved.
23+
* - Otherwise, the value from the source is used (even if undefined, null, or empty string).
24+
* This is useful for partial updates where some properties should not be overwritten unless explicitly set.
25+
*
26+
* @param target - The original object to merge into
27+
* @param source - The object containing new values
28+
* @param preserveKeys - Keys for which the target's value should be preserved if the source value is undefined, null, or empty string
29+
* @returns A new object with merged values
30+
*/
31+
function updateObject(target, source, preserveKeys = []) {
32+
// Collect all unique keys from both target and source
33+
const allKeys = Array.from(new Set([...Object.keys(target), ...Object.keys(source)]));
34+
return allKeys.reduce((acc, key) => {
35+
const sourceValue = source[key];
36+
const targetValue = target[key];
37+
// Preserve existing values for specified keys when source value is undefined
38+
if (preserveKeys.includes(key) && (sourceValue === undefined || sourceValue === null || sourceValue === '')) {
39+
acc[key] = targetValue;
40+
}
41+
else {
42+
acc[key] = sourceValue;
2243
}
2344
return acc;
2445
}, Object.assign({}, target));
@@ -42,22 +63,23 @@ const writeComponentSummaryAPI = (handoff, componentData) => __awaiter(void 0, v
4263
componentData.sort((a, b) => a.title.localeCompare(b.title));
4364
yield fs_extra_1.default.writeFile(path_1.default.resolve((0, exports.getAPIPath)(handoff), 'components.json'), JSON.stringify(componentData, null, 2));
4465
});
45-
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) {
66+
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 = []) {
4667
const outputDirPath = path_1.default.resolve((0, exports.getAPIPath)(handoff), 'component', id);
47-
if (isPartialUpdate) {
48-
const outputFilePath = path_1.default.resolve(outputDirPath, `${version}.json`);
49-
if (fs_extra_1.default.existsSync(outputFilePath)) {
50-
const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
51-
if (existingJson) {
52-
try {
53-
const existingData = JSON.parse(existingJson);
54-
const mergedData = updateObject(existingData, component);
55-
yield fs_extra_1.default.writeFile(path_1.default.resolve(outputDirPath, `${version}.json`), JSON.stringify(mergedData, null, 2));
56-
return;
57-
}
58-
catch (_) {
59-
// Unable to parse existing file
60-
}
68+
const outputFilePath = path_1.default.resolve(outputDirPath, `${version}.json`);
69+
if (fs_extra_1.default.existsSync(outputFilePath)) {
70+
const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
71+
if (existingJson) {
72+
try {
73+
const existingData = JSON.parse(existingJson);
74+
// Special case: always allow page to be cleared when undefined
75+
// This handles the case where page slices are removed
76+
const finalPreserveKeys = component.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys;
77+
const mergedData = updateObject(existingData, component, finalPreserveKeys);
78+
yield fs_extra_1.default.writeFile(path_1.default.resolve(outputDirPath, `${version}.json`), JSON.stringify(mergedData, null, 2));
79+
return;
80+
}
81+
catch (_) {
82+
// Unable to parse existing file
6183
}
6284
}
6385
}

dist/transformers/preview/component/builder.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,32 @@ var ComponentSegment;
7070
ComponentSegment["Previews"] = "previews";
7171
ComponentSegment["Validation"] = "validation";
7272
})(ComponentSegment || (exports.ComponentSegment = ComponentSegment = {}));
73+
/**
74+
* Determines which keys should be preserved based on the segment being processed.
75+
* When processing a specific segment, we want to preserve data from other segments
76+
* to avoid overwriting them with undefined values.
77+
*/
78+
function getPreserveKeysForSegment(segmentToProcess) {
79+
if (!segmentToProcess) {
80+
return []; // No preservation needed for full updates
81+
}
82+
switch (segmentToProcess) {
83+
case ComponentSegment.JavaScript:
84+
// When processing JavaScript segment, preserve CSS and previews data
85+
return ['css', 'sass', 'sharedStyles', 'previews', 'validations'];
86+
case ComponentSegment.Style:
87+
// When processing Style segment, preserve JavaScript and previews data
88+
return ['js', 'jsCompiled', 'previews', 'validations'];
89+
case ComponentSegment.Previews:
90+
// When processing Previews segment, preserve JavaScript and CSS data
91+
return ['js', 'jsCompiled', 'css', 'sass', 'sharedStyles', 'validations'];
92+
case ComponentSegment.Validation:
93+
// When processing Validation segment, preserve all other data
94+
return ['js', 'jsCompiled', 'css', 'sass', 'sharedStyles', 'previews'];
95+
default:
96+
return [];
97+
}
98+
}
7399
/**
74100
* Process components and generate their code, styles, and previews
75101
* @param handoff - The Handoff instance containing configuration and state
@@ -84,6 +110,10 @@ function processComponents(handoff, id, segmentToProcess) {
84110
const components = (yield handoff.getDocumentationObject()).components;
85111
const sharedStyles = yield handoff.getSharedStyles();
86112
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 : {};
113+
// Determine which keys to preserve based on the segment being processed
114+
// This ensures that when processing only specific segments (e.g., JavaScript only),
115+
// we don't overwrite data from other segments (e.g., CSS, previews) with undefined values
116+
const preserveKeys = getPreserveKeysForSegment(segmentToProcess);
87117
for (const runtimeComponentId of Object.keys(runtimeComponents)) {
88118
if (!!id && runtimeComponentId !== id) {
89119
continue;
@@ -110,13 +140,13 @@ function processComponents(handoff, id, segmentToProcess) {
110140
data.validations = validationResults;
111141
}
112142
data.sharedStyles = sharedStyles;
113-
yield (0, api_1.writeComponentApi)(runtimeComponentId, data, version, handoff, true);
143+
yield (0, api_1.writeComponentApi)(runtimeComponentId, data, version, handoff, preserveKeys);
114144
if (version === latest) {
115145
latestVersion = data;
116146
}
117147
})));
118148
if (latestVersion) {
119-
yield (0, api_1.writeComponentApi)(runtimeComponentId, latestVersion, 'latest', handoff, true);
149+
yield (0, api_1.writeComponentApi)(runtimeComponentId, latestVersion, 'latest', handoff, preserveKeys);
120150
const summary = buildComponentSummary(runtimeComponentId, latestVersion, versions);
121151
yield (0, api_1.writeComponentMetadataApi)(runtimeComponentId, summary, handoff);
122152
result.push(summary);

dist/transformers/preview/component/css.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff:
4040
};
4141
};
4242
validations?: Record<string, import("../../../types").ValidationResult>;
43+
page?: import("../types").ComponentPageDefinition;
4344
}>;
4445
/**
4546
* Build the main CSS file using Vite

dist/transformers/preview/component/json.d.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

dist/transformers/preview/component/json.js

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)