@developer_tribe/react-builder 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AttributesEditor.d.ts +3 -1
- package/dist/RenderPage.d.ts +2 -1
- package/dist/android.svg +43 -0
- package/dist/apple.svg +16 -0
- package/dist/attributes-editor/Field.d.ts +4 -2
- package/dist/attributes-editor/SizeField.d.ts +9 -0
- package/dist/attributes-editor/SpecialCategorySection.d.ts +2 -1
- package/dist/build-components/BackgroundImage/BackgroundImage.d.ts +5 -0
- package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +45 -0
- package/dist/build-components/Button/ButtonProps.generated.d.ts +8 -0
- package/dist/build-components/Carousel/CarouselProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +8 -0
- package/dist/build-components/Image/ImageProps.generated.d.ts +8 -0
- package/dist/build-components/Onboard/OnboardProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +8 -1
- package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +9 -3
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +9 -1
- package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +8 -1
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +8 -0
- package/dist/build-components/Text/TextProps.generated.d.ts +8 -0
- package/dist/build-components/View/ViewProps.generated.d.ts +8 -0
- package/dist/build-components/index.d.ts +2 -1
- package/dist/build-components/patterns.generated.d.ts +1612 -46
- package/dist/components/AttributesEditorPanel.d.ts +3 -4
- package/dist/components/Builder.d.ts +2 -1
- package/dist/components/BuilderButton.d.ts +9 -0
- package/dist/components/JsonTextEditor.d.ts +9 -0
- package/dist/index.cjs.js +5 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +5 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/modals/ColorModal.d.ts +3 -1
- package/dist/pages/ProjectPage.d.ts +3 -3
- package/dist/pages/tabs/BuilderPanel.d.ts +8 -0
- package/dist/pages/tabs/SideTool.d.ts +8 -0
- package/dist/store.d.ts +9 -1
- package/dist/styles.css +1 -1
- package/dist/types/Project.d.ts +11 -0
- package/dist/utils/analyseNode.d.ts +1 -0
- package/dist/utils/extractImageStyle.d.ts +2 -1
- package/dist/utils/extractTextStyle.d.ts +8 -1
- package/dist/utils/extractViewStyle.d.ts +7 -1
- package/dist/utils/parseColor.d.ts +7 -0
- package/dist/utils/selection.d.ts +7 -0
- package/dist/utils/useMergedStyle.d.ts +2 -0
- package/package.json +2 -5
- package/src/.DS_Store +0 -0
- package/src/AttributesEditor.tsx +83 -16
- package/src/RenderPage.tsx +86 -4
- package/src/attributes-editor/Field.tsx +60 -165
- package/src/attributes-editor/SizeField.tsx +184 -0
- package/src/attributes-editor/SpecialCategorySection.tsx +12 -4
- package/src/build-components/BackgroundImage/BackgroundImage.tsx +77 -0
- package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +61 -0
- package/src/build-components/BackgroundImage/pattern.json +45 -0
- package/src/build-components/Button/Button.tsx +29 -4
- package/src/build-components/Button/ButtonProps.generated.ts +8 -0
- package/src/build-components/Carousel/Carousel.tsx +25 -3
- package/src/build-components/Carousel/CarouselProps.generated.ts +8 -0
- package/src/build-components/CarouselButtons/CarouselButtons.tsx +19 -4
- package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +8 -0
- package/src/build-components/CarouselDots/CarouselDots.tsx +13 -4
- package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +8 -0
- package/src/build-components/CarouselItem/CarouselItem.tsx +20 -4
- package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +8 -0
- package/src/build-components/CarouselProvider/CarouselProvider.tsx +14 -3
- package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +8 -0
- package/src/build-components/Image/Image.tsx +27 -9
- package/src/build-components/Image/ImageProps.generated.ts +8 -0
- package/src/build-components/Image/pattern.json +1 -9
- package/src/build-components/Onboard/Onboard.tsx +2 -2
- package/src/build-components/Onboard/OnboardProps.generated.ts +8 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +11 -7
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +8 -1
- package/src/build-components/OnboardButtons/OnboardButtons.tsx +17 -5
- package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +8 -0
- package/src/build-components/OnboardDot/OnboardDot.tsx +68 -39
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +9 -3
- package/src/build-components/OnboardDot/pattern.json +3 -19
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +37 -14
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +8 -0
- package/src/build-components/OnboardImage/OnboardImage.tsx +28 -6
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +9 -1
- package/src/build-components/OnboardItem/OnboardItem.tsx +15 -14
- package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +8 -0
- package/src/build-components/OnboardProvider/OnboardProvider.tsx +35 -20
- package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +8 -1
- package/src/build-components/OnboardProvider/pattern.json +0 -8
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +8 -0
- package/src/build-components/OnboardSubtitle/pattern.json +1 -1
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +8 -0
- package/src/build-components/OnboardTitle/pattern.json +1 -1
- package/src/build-components/RenderNode.generated.tsx +3 -0
- package/src/build-components/Text/Text.tsx +28 -10
- package/src/build-components/Text/TextProps.generated.ts +8 -0
- package/src/build-components/View/View.tsx +25 -3
- package/src/build-components/View/ViewProps.generated.ts +8 -0
- package/src/build-components/View/pattern.json +67 -1
- package/src/build-components/index.ts +5 -0
- package/src/build-components/patterns.generated.ts +1620 -46
- package/src/components/AttributesEditorPanel.tsx +13 -6
- package/src/components/Builder.tsx +200 -56
- package/src/components/BuilderButton.tsx +127 -0
- package/src/components/DeviceNavigationBar.tsx +0 -1
- package/src/components/EditorHeader.tsx +11 -1
- package/src/components/JsonTextEditor.tsx +185 -0
- package/src/index.ts +2 -2
- package/src/mockOS/components/MockOSRouter.tsx +17 -3
- package/src/mockOS/context/MockOSContext.tsx +0 -5
- package/src/mockOS/managers/mockPermissionManager.ts +0 -4
- package/src/mockOS/managers/navigationManager.ts +1 -6
- package/src/modals/ColorModal.tsx +306 -71
- package/src/modals/LocalicationModal.tsx +4 -5
- package/src/modals/Modal.tsx +8 -1
- package/src/pages/ProjectPage.tsx +299 -55
- package/src/pages/tabs/{BuilderTab.tsx → BuilderPanel.tsx} +13 -9
- package/src/pages/tabs/SideTool.tsx +260 -0
- package/src/size-matters/index.ts +6 -0
- package/src/store.ts +18 -1
- package/src/styles/base/_global.scss +163 -7
- package/src/styles/components/_attributes-editor.scss +12 -0
- package/src/styles/components/_editor-shell.scss +25 -0
- package/src/styles/foundation/_sizes.scss +1 -1
- package/src/styles/layout/_builder.scss +66 -10
- package/src/styles/modals/_color-modal.scss +59 -1
- package/src/styles/utilities/_carousel.scss +9 -8
- package/src/types/Project.ts +14 -0
- package/src/utils/analyseNode.ts +98 -0
- package/src/utils/extractImageStyle.ts +3 -6
- package/src/utils/extractTextStyle.ts +19 -82
- package/src/utils/extractViewStyle.ts +41 -12
- package/src/utils/parseColor.ts +43 -0
- package/src/utils/selection.ts +24 -0
- package/src/utils/useMergedStyle.ts +16 -0
- package/dist/pages/tabs/BuilderTab.d.ts +0 -9
- package/dist/pages/tabs/DebugTab.d.ts +0 -7
- package/dist/pages/tabs/PreviewTab.d.ts +0 -3
- package/src/pages/tabs/DebugTab.tsx +0 -64
- package/src/pages/tabs/PreviewTab.tsx +0 -206
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import type { ViewPropsGenerated } from '../build-components/View/ViewProps.generated';
|
|
3
|
-
import { ColorModal } from '../modals/ColorModal';
|
|
3
|
+
import { ColorModal, resolveProjectColorValue } from '../modals/ColorModal';
|
|
4
|
+
import type { ProjectColors } from '../types/Project';
|
|
4
5
|
import {
|
|
5
6
|
getArrayItemType,
|
|
6
7
|
getTypeSchema,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
} from '../utils/patterns';
|
|
9
10
|
import { Checkbox } from '../components/Checkbox';
|
|
10
11
|
import { LayoutPreviewPicker } from './LayoutPreviewPicker';
|
|
12
|
+
import { SizeField, type PreferredScale } from './SizeField';
|
|
11
13
|
import { LayoutContext, LayoutFieldName, isBooleanFieldType } from './types';
|
|
12
14
|
|
|
13
15
|
export type FieldProps = {
|
|
@@ -16,11 +18,11 @@ export type FieldProps = {
|
|
|
16
18
|
value: any;
|
|
17
19
|
onChange: (v: any) => void;
|
|
18
20
|
componentType?: string;
|
|
19
|
-
projectColors?:
|
|
21
|
+
projectColors?: ProjectColors;
|
|
20
22
|
layoutContext?: LayoutContext;
|
|
21
23
|
viewAttributes?: Partial<ViewPropsGenerated['attributes']>;
|
|
22
24
|
label?: React.ReactNode;
|
|
23
|
-
preferredScale?:
|
|
25
|
+
preferredScale?: PreferredScale;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
const layoutFieldNames: LayoutFieldName[] = [
|
|
@@ -250,10 +252,54 @@ export function Field({
|
|
|
250
252
|
);
|
|
251
253
|
})}
|
|
252
254
|
</div>
|
|
253
|
-
<div
|
|
255
|
+
<div
|
|
256
|
+
style={{
|
|
257
|
+
marginTop: 8,
|
|
258
|
+
display: 'flex',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
justifyContent: 'space-between',
|
|
261
|
+
gap: 8,
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={(event) => {
|
|
268
|
+
event.stopPropagation();
|
|
269
|
+
if (idx <= 0) return;
|
|
270
|
+
const next = [...arr];
|
|
271
|
+
const tmp = next[idx - 1];
|
|
272
|
+
next[idx - 1] = next[idx];
|
|
273
|
+
next[idx] = tmp;
|
|
274
|
+
onChange(next);
|
|
275
|
+
}}
|
|
276
|
+
disabled={idx <= 0}
|
|
277
|
+
aria-label="Move up"
|
|
278
|
+
>
|
|
279
|
+
↑
|
|
280
|
+
</button>
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
onClick={(event) => {
|
|
284
|
+
event.stopPropagation();
|
|
285
|
+
if (idx >= arr.length - 1) return;
|
|
286
|
+
const next = [...arr];
|
|
287
|
+
const tmp = next[idx + 1];
|
|
288
|
+
next[idx + 1] = next[idx];
|
|
289
|
+
next[idx] = tmp;
|
|
290
|
+
onChange(next);
|
|
291
|
+
}}
|
|
292
|
+
disabled={idx >= arr.length - 1}
|
|
293
|
+
aria-label="Move down"
|
|
294
|
+
>
|
|
295
|
+
↓
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
254
299
|
<button
|
|
255
300
|
type="button"
|
|
256
|
-
onClick={() => {
|
|
301
|
+
onClick={(event) => {
|
|
302
|
+
event.stopPropagation();
|
|
257
303
|
const next = arr.filter((_, i) => i !== idx);
|
|
258
304
|
onChange(next.length ? next : undefined);
|
|
259
305
|
}}
|
|
@@ -441,16 +487,22 @@ export function Field({
|
|
|
441
487
|
type ColorPickerButtonProps = {
|
|
442
488
|
value?: string;
|
|
443
489
|
onChange: (color?: string) => void;
|
|
444
|
-
projectColors?:
|
|
490
|
+
projectColors?: ProjectColors;
|
|
445
491
|
};
|
|
446
492
|
|
|
447
493
|
function ColorPickerButton({
|
|
448
494
|
value,
|
|
449
495
|
onChange,
|
|
450
|
-
projectColors
|
|
496
|
+
projectColors,
|
|
451
497
|
}: ColorPickerButtonProps) {
|
|
452
498
|
const [isOpen, setIsOpen] = useState(false);
|
|
453
499
|
const normalizedValue = typeof value === 'string' ? value : undefined;
|
|
500
|
+
const previewColor = useMemo(
|
|
501
|
+
() =>
|
|
502
|
+
resolveProjectColorValue(normalizedValue, projectColors) ??
|
|
503
|
+
normalizedValue,
|
|
504
|
+
[normalizedValue, projectColors],
|
|
505
|
+
);
|
|
454
506
|
|
|
455
507
|
return (
|
|
456
508
|
<>
|
|
@@ -477,7 +529,7 @@ function ColorPickerButton({
|
|
|
477
529
|
height: 32,
|
|
478
530
|
borderRadius: 6,
|
|
479
531
|
border: '1px solid rgba(0,0,0,0.1)',
|
|
480
|
-
background:
|
|
532
|
+
background: previewColor ?? 'transparent',
|
|
481
533
|
}}
|
|
482
534
|
/>
|
|
483
535
|
<span style={{ flex: 1, textAlign: 'left', fontWeight: 500 }}>
|
|
@@ -503,160 +555,3 @@ function ColorPickerButton({
|
|
|
503
555
|
</>
|
|
504
556
|
);
|
|
505
557
|
}
|
|
506
|
-
|
|
507
|
-
type SizeUnit = '' | 'vs' | 's' | 'f' | '%';
|
|
508
|
-
|
|
509
|
-
function parseSizeValue(value: unknown): { amount: string; unit: SizeUnit } {
|
|
510
|
-
const empty = { amount: '', unit: '' as SizeUnit };
|
|
511
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
512
|
-
return { amount: String(value), unit: '' };
|
|
513
|
-
}
|
|
514
|
-
if (typeof value !== 'string') return empty;
|
|
515
|
-
const trimmed = value.trim();
|
|
516
|
-
if (!trimmed) return empty;
|
|
517
|
-
if (trimmed.endsWith('%')) {
|
|
518
|
-
return { amount: trimmed.slice(0, -1), unit: '%' };
|
|
519
|
-
}
|
|
520
|
-
const lower = trimmed.toLowerCase();
|
|
521
|
-
if (lower.endsWith('@vs'))
|
|
522
|
-
return { amount: trimmed.slice(0, -3), unit: 'vs' };
|
|
523
|
-
if (lower.endsWith('vs')) return { amount: trimmed.slice(0, -2), unit: 'vs' };
|
|
524
|
-
if (lower.endsWith('@fs')) return { amount: trimmed.slice(0, -3), unit: 'f' };
|
|
525
|
-
if (lower.endsWith('@f')) return { amount: trimmed.slice(0, -2), unit: 'f' };
|
|
526
|
-
if (lower.endsWith('fs')) return { amount: trimmed.slice(0, -2), unit: 'f' };
|
|
527
|
-
if (lower.endsWith('f')) return { amount: trimmed.slice(0, -1), unit: 'f' };
|
|
528
|
-
if (lower.endsWith('@s')) return { amount: trimmed.slice(0, -2), unit: 's' };
|
|
529
|
-
if (lower.endsWith('s')) return { amount: trimmed.slice(0, -1), unit: 's' };
|
|
530
|
-
if (lower.endsWith('px')) return { amount: trimmed.slice(0, -2), unit: '' };
|
|
531
|
-
return { amount: trimmed, unit: '' };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function composeSizeValue(amount: string, unit: SizeUnit): string | number {
|
|
535
|
-
const trimmed = amount.trim();
|
|
536
|
-
if (unit === '%') {
|
|
537
|
-
return `${trimmed}%`;
|
|
538
|
-
}
|
|
539
|
-
if (unit === '') {
|
|
540
|
-
const numeric = Number(trimmed);
|
|
541
|
-
return Number.isFinite(numeric) ? numeric : trimmed;
|
|
542
|
-
}
|
|
543
|
-
if (unit === 'f') {
|
|
544
|
-
return `${trimmed}@fs`;
|
|
545
|
-
}
|
|
546
|
-
return `${trimmed}@${unit}`;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
type SizeFieldProps = {
|
|
550
|
-
value: unknown;
|
|
551
|
-
onChange: (val: unknown) => void;
|
|
552
|
-
preferredScale?: string;
|
|
553
|
-
fieldName: string;
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
function normalizePreferredScale(
|
|
557
|
-
preferredScale: string | undefined,
|
|
558
|
-
fieldName: string,
|
|
559
|
-
): SizeUnit {
|
|
560
|
-
const fallbackName = fieldName.trim().toLowerCase();
|
|
561
|
-
const fallback: SizeUnit =
|
|
562
|
-
fallbackName.includes('height') ||
|
|
563
|
-
fallbackName.includes('top') ||
|
|
564
|
-
fallbackName.includes('vertical')
|
|
565
|
-
? 'vs'
|
|
566
|
-
: 's';
|
|
567
|
-
if (typeof preferredScale !== 'string') return fallback;
|
|
568
|
-
const normalized = preferredScale.trim().toLowerCase();
|
|
569
|
-
if (
|
|
570
|
-
normalized === 'vs' ||
|
|
571
|
-
normalized === 's' ||
|
|
572
|
-
normalized === 'f' ||
|
|
573
|
-
normalized === '%'
|
|
574
|
-
) {
|
|
575
|
-
return normalized as SizeUnit;
|
|
576
|
-
}
|
|
577
|
-
return fallback;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function SizeField({
|
|
581
|
-
value,
|
|
582
|
-
onChange,
|
|
583
|
-
preferredScale,
|
|
584
|
-
fieldName,
|
|
585
|
-
}: SizeFieldProps) {
|
|
586
|
-
const parsed = useMemo(() => parseSizeValue(value), [value]);
|
|
587
|
-
const normalizedPreferred = useMemo(
|
|
588
|
-
() => normalizePreferredScale(preferredScale, fieldName),
|
|
589
|
-
[preferredScale, fieldName],
|
|
590
|
-
);
|
|
591
|
-
const [amount, setAmount] = useState(parsed.amount);
|
|
592
|
-
const [unit, setUnit] = useState<SizeUnit>(
|
|
593
|
-
parsed.unit || normalizedPreferred,
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
useEffect(() => {
|
|
597
|
-
setAmount(parsed.amount);
|
|
598
|
-
setUnit(parsed.unit || normalizedPreferred);
|
|
599
|
-
}, [parsed.amount, parsed.unit, normalizedPreferred]);
|
|
600
|
-
|
|
601
|
-
const emitValue = useCallback(
|
|
602
|
-
(nextAmount: string, nextUnit: SizeUnit) => {
|
|
603
|
-
const trimmed = nextAmount.trim();
|
|
604
|
-
if (!trimmed) {
|
|
605
|
-
onChange(undefined);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
onChange(composeSizeValue(trimmed, nextUnit));
|
|
609
|
-
},
|
|
610
|
-
[onChange],
|
|
611
|
-
);
|
|
612
|
-
|
|
613
|
-
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
614
|
-
const nextAmount = e.target.value;
|
|
615
|
-
setAmount(nextAmount);
|
|
616
|
-
if (!nextAmount.trim()) {
|
|
617
|
-
onChange(undefined);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
emitValue(nextAmount, unit);
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
const handleUnitChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
624
|
-
const nextUnit = e.target.value as SizeUnit;
|
|
625
|
-
setUnit(nextUnit);
|
|
626
|
-
if (!amount.trim()) return;
|
|
627
|
-
emitValue(amount, nextUnit);
|
|
628
|
-
};
|
|
629
|
-
|
|
630
|
-
const unitPriority: SizeUnit[] = ['vs', 's', 'f', '%'];
|
|
631
|
-
const orderedUnits = [
|
|
632
|
-
normalizedPreferred,
|
|
633
|
-
...unitPriority.filter((unit) => unit !== normalizedPreferred),
|
|
634
|
-
];
|
|
635
|
-
const unitOptions: Array<{ value: SizeUnit; label: string }> =
|
|
636
|
-
orderedUnits.map((unit) => ({
|
|
637
|
-
value: unit,
|
|
638
|
-
label: unit === normalizedPreferred ? `${unit}` : unit,
|
|
639
|
-
}));
|
|
640
|
-
|
|
641
|
-
return (
|
|
642
|
-
<div className="attributes-editor__size-field">
|
|
643
|
-
<input
|
|
644
|
-
type="number"
|
|
645
|
-
className="input attributes-editor__size-field-input"
|
|
646
|
-
value={amount}
|
|
647
|
-
onChange={handleAmountChange}
|
|
648
|
-
/>
|
|
649
|
-
<select
|
|
650
|
-
className="input attributes-editor__size-field-select"
|
|
651
|
-
value={unit}
|
|
652
|
-
onChange={handleUnitChange}
|
|
653
|
-
>
|
|
654
|
-
{unitOptions.map((opt) => (
|
|
655
|
-
<option key={opt.value} value={opt.value}>
|
|
656
|
-
{opt.label}
|
|
657
|
-
</option>
|
|
658
|
-
))}
|
|
659
|
-
</select>
|
|
660
|
-
</div>
|
|
661
|
-
);
|
|
662
|
-
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type SizeUnit = '' | 'vs' | 's' | 'f' | '%';
|
|
4
|
+
type SizeSelectUnit = SizeUnit | 'auto';
|
|
5
|
+
|
|
6
|
+
export type PreferredScale = 'vs' | 's' | 'f' | '%';
|
|
7
|
+
|
|
8
|
+
export function toPreferredScale(value: unknown): PreferredScale | undefined {
|
|
9
|
+
if (value === 'vs' || value === 's' || value === 'f' || value === '%') {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value !== 'string') return undefined;
|
|
13
|
+
const normalized = value.trim().toLowerCase();
|
|
14
|
+
return normalized === 'vs' ||
|
|
15
|
+
normalized === 's' ||
|
|
16
|
+
normalized === 'f' ||
|
|
17
|
+
normalized === '%'
|
|
18
|
+
? (normalized as PreferredScale)
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SizeFieldProps = {
|
|
23
|
+
value: unknown;
|
|
24
|
+
onChange: (val: unknown) => void;
|
|
25
|
+
preferredScale?: PreferredScale;
|
|
26
|
+
fieldName: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function parseSizeValue(value: unknown): { amount: string; unit: SizeUnit } {
|
|
30
|
+
const empty = { amount: '', unit: '' as SizeUnit };
|
|
31
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
32
|
+
return { amount: String(value), unit: '' };
|
|
33
|
+
}
|
|
34
|
+
if (typeof value !== 'string') return empty;
|
|
35
|
+
const trimmed = value.trim();
|
|
36
|
+
if (!trimmed) return empty;
|
|
37
|
+
if (trimmed.endsWith('%')) {
|
|
38
|
+
return { amount: trimmed.slice(0, -1), unit: '%' };
|
|
39
|
+
}
|
|
40
|
+
const lower = trimmed.toLowerCase();
|
|
41
|
+
if (lower.endsWith('@vs'))
|
|
42
|
+
return { amount: trimmed.slice(0, -3), unit: 'vs' };
|
|
43
|
+
if (lower.endsWith('vs')) return { amount: trimmed.slice(0, -2), unit: 'vs' };
|
|
44
|
+
if (lower.endsWith('@fs')) return { amount: trimmed.slice(0, -3), unit: 'f' };
|
|
45
|
+
if (lower.endsWith('@f')) return { amount: trimmed.slice(0, -2), unit: 'f' };
|
|
46
|
+
if (lower.endsWith('fs')) return { amount: trimmed.slice(0, -2), unit: 'f' };
|
|
47
|
+
if (lower.endsWith('f')) return { amount: trimmed.slice(0, -1), unit: 'f' };
|
|
48
|
+
if (lower.endsWith('@s')) return { amount: trimmed.slice(0, -2), unit: 's' };
|
|
49
|
+
if (lower.endsWith('s')) return { amount: trimmed.slice(0, -1), unit: 's' };
|
|
50
|
+
if (lower.endsWith('px')) return { amount: trimmed.slice(0, -2), unit: '' };
|
|
51
|
+
return { amount: trimmed, unit: '' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function composeSizeValue(amount: string, unit: SizeUnit): string | number {
|
|
55
|
+
const trimmed = amount.trim();
|
|
56
|
+
if (unit === '%') {
|
|
57
|
+
return `${trimmed}%`;
|
|
58
|
+
}
|
|
59
|
+
if (unit === '') {
|
|
60
|
+
const numeric = Number(trimmed);
|
|
61
|
+
return Number.isFinite(numeric) ? numeric : trimmed;
|
|
62
|
+
}
|
|
63
|
+
if (unit === 'f') {
|
|
64
|
+
return `${trimmed}@fs`;
|
|
65
|
+
}
|
|
66
|
+
return `${trimmed}@${unit}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizePreferredScale(
|
|
70
|
+
preferredScale: PreferredScale | undefined,
|
|
71
|
+
fieldName: string,
|
|
72
|
+
): SizeUnit {
|
|
73
|
+
const fallbackName = fieldName.trim().toLowerCase();
|
|
74
|
+
const fallback: SizeUnit =
|
|
75
|
+
fallbackName.includes('height') ||
|
|
76
|
+
fallbackName.includes('top') ||
|
|
77
|
+
fallbackName.includes('vertical')
|
|
78
|
+
? 'vs'
|
|
79
|
+
: 's';
|
|
80
|
+
return preferredScale ?? fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function SizeField({
|
|
84
|
+
value,
|
|
85
|
+
onChange,
|
|
86
|
+
preferredScale,
|
|
87
|
+
fieldName,
|
|
88
|
+
}: SizeFieldProps) {
|
|
89
|
+
const parsed = useMemo(() => parseSizeValue(value), [value]);
|
|
90
|
+
const normalizedPreferred = useMemo(
|
|
91
|
+
() => normalizePreferredScale(preferredScale, fieldName),
|
|
92
|
+
[preferredScale, fieldName],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const [amount, setAmount] = useState(parsed.amount);
|
|
96
|
+
const [unit, setUnit] = useState<SizeSelectUnit>(() => {
|
|
97
|
+
// Default to "auto" when the stored unit is already the preferred one (or empty)
|
|
98
|
+
// so that "auto" means "use preferredScale" without changing behavior.
|
|
99
|
+
return parsed.unit && parsed.unit !== normalizedPreferred
|
|
100
|
+
? parsed.unit
|
|
101
|
+
: 'auto';
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setAmount(parsed.amount);
|
|
106
|
+
|
|
107
|
+
// Keep "auto" selected whenever value is preferred (or unscaled).
|
|
108
|
+
const nextUnit: SizeSelectUnit =
|
|
109
|
+
parsed.unit && parsed.unit !== normalizedPreferred ? parsed.unit : 'auto';
|
|
110
|
+
setUnit(nextUnit);
|
|
111
|
+
}, [parsed.amount, parsed.unit, normalizedPreferred]);
|
|
112
|
+
|
|
113
|
+
const resolveUnit = useCallback(
|
|
114
|
+
(nextUnit: SizeSelectUnit): SizeUnit =>
|
|
115
|
+
nextUnit === 'auto' ? normalizedPreferred : nextUnit,
|
|
116
|
+
[normalizedPreferred],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const emitValue = useCallback(
|
|
120
|
+
(nextAmount: string, nextUnit: SizeSelectUnit) => {
|
|
121
|
+
const trimmed = nextAmount.trim();
|
|
122
|
+
if (!trimmed) {
|
|
123
|
+
onChange(undefined);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// If "auto" is selected, persist the preferredScale.
|
|
127
|
+
onChange(composeSizeValue(trimmed, resolveUnit(nextUnit)));
|
|
128
|
+
},
|
|
129
|
+
[onChange, resolveUnit],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
133
|
+
const nextAmount = e.target.value;
|
|
134
|
+
setAmount(nextAmount);
|
|
135
|
+
if (!nextAmount.trim()) {
|
|
136
|
+
onChange(undefined);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
emitValue(nextAmount, unit);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleUnitChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
143
|
+
const nextUnit = e.target.value as SizeSelectUnit;
|
|
144
|
+
setUnit(nextUnit);
|
|
145
|
+
if (!amount.trim()) return;
|
|
146
|
+
emitValue(amount, nextUnit);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const unitPriority: SizeUnit[] = ['vs', 's', 'f', '%'];
|
|
150
|
+
const orderedUnits = [
|
|
151
|
+
normalizedPreferred,
|
|
152
|
+
...unitPriority.filter((u) => u !== normalizedPreferred),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const unitOptions: Array<{ value: SizeSelectUnit; label: string }> = [
|
|
156
|
+
{ value: 'auto', label: 'auto' },
|
|
157
|
+
...orderedUnits.map((u) => ({
|
|
158
|
+
value: u,
|
|
159
|
+
label: u === normalizedPreferred ? `${u} (preferred)` : u,
|
|
160
|
+
})),
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className="attributes-editor__size-field">
|
|
165
|
+
<input
|
|
166
|
+
type="number"
|
|
167
|
+
className="input attributes-editor__size-field-input"
|
|
168
|
+
value={amount}
|
|
169
|
+
onChange={handleAmountChange}
|
|
170
|
+
/>
|
|
171
|
+
<select
|
|
172
|
+
className="input attributes-editor__size-field-select"
|
|
173
|
+
value={unit}
|
|
174
|
+
onChange={handleUnitChange}
|
|
175
|
+
>
|
|
176
|
+
{unitOptions.map((opt) => (
|
|
177
|
+
<option key={opt.value} value={opt.value}>
|
|
178
|
+
{opt.label}
|
|
179
|
+
</option>
|
|
180
|
+
))}
|
|
181
|
+
</select>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { NodeDefaultAttribute } from '../types/Node';
|
|
3
|
+
import type { ProjectColors } from '../types/Project';
|
|
3
4
|
import { Field } from './Field';
|
|
4
5
|
import { FieldInfoTooltip } from './FieldInfoTooltip';
|
|
6
|
+
import { toPreferredScale } from './SizeField';
|
|
5
7
|
import {
|
|
6
8
|
AttributeMetaMap,
|
|
7
9
|
LayoutContext,
|
|
@@ -16,7 +18,7 @@ type SpecialCategorySectionProps = {
|
|
|
16
18
|
attributes: NodeDefaultAttribute;
|
|
17
19
|
onAttributeChange: (name: string, value: unknown) => void;
|
|
18
20
|
componentType?: string;
|
|
19
|
-
projectColors?:
|
|
21
|
+
projectColors?: ProjectColors;
|
|
20
22
|
layoutContext?: LayoutContext;
|
|
21
23
|
viewAttributes?: NodeDefaultAttribute;
|
|
22
24
|
meta?: {
|
|
@@ -91,7 +93,9 @@ export function SpecialCategorySection({
|
|
|
91
93
|
{entries.map(({ name, type }) => {
|
|
92
94
|
const label = attributeMeta?.[name]?.label ?? name;
|
|
93
95
|
const description = attributeMeta?.[name]?.description;
|
|
94
|
-
const preferredScale =
|
|
96
|
+
const preferredScale = toPreferredScale(
|
|
97
|
+
attributeMeta?.[name]?.preferedScale,
|
|
98
|
+
);
|
|
95
99
|
const currentValue = (attributes as Record<string, unknown>)[name];
|
|
96
100
|
const isBoolean = isBooleanFieldType(type);
|
|
97
101
|
const fieldSlot = detectFieldSlot(name);
|
|
@@ -168,7 +172,9 @@ export function SpecialCategorySection({
|
|
|
168
172
|
{boxEntries.map(({ name, type }) => {
|
|
169
173
|
const label = attributeMeta?.[name]?.label ?? name;
|
|
170
174
|
const description = attributeMeta?.[name]?.description;
|
|
171
|
-
const preferredScale =
|
|
175
|
+
const preferredScale = toPreferredScale(
|
|
176
|
+
attributeMeta?.[name]?.preferedScale,
|
|
177
|
+
);
|
|
172
178
|
const currentValue = (attributes as Record<string, unknown>)[name];
|
|
173
179
|
const isBoolean = isBooleanFieldType(type);
|
|
174
180
|
const fieldSlot = detectFieldSlot(name);
|
|
@@ -211,7 +217,9 @@ export function SpecialCategorySection({
|
|
|
211
217
|
{baseEntries.map(({ name, type }) => {
|
|
212
218
|
const label = attributeMeta?.[name]?.label ?? name;
|
|
213
219
|
const description = attributeMeta?.[name]?.description;
|
|
214
|
-
const preferredScale =
|
|
220
|
+
const preferredScale = toPreferredScale(
|
|
221
|
+
attributeMeta?.[name]?.preferedScale,
|
|
222
|
+
);
|
|
215
223
|
const currentValue = (attributes as Record<string, unknown>)[name];
|
|
216
224
|
const isBoolean = isBooleanFieldType(type);
|
|
217
225
|
const wrapperClassNames = [
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, { useId, useMemo } from 'react';
|
|
2
|
+
import type { BackgroundImageComponentProps } from './BackgroundImageProps.generated';
|
|
3
|
+
import useNode from '../useNode';
|
|
4
|
+
import { useRenderStore } from '../../store';
|
|
5
|
+
import { extractViewStyle } from '../../utils/extractViewStyle';
|
|
6
|
+
import { useLogRender } from '../../utils/useLogRender';
|
|
7
|
+
import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
|
|
8
|
+
import { useMergedStyle } from '../../utils/useMergedStyle';
|
|
9
|
+
|
|
10
|
+
function BackgroundImage({ node }: BackgroundImageComponentProps) {
|
|
11
|
+
useLogRender('BackgroundImage');
|
|
12
|
+
node = useNode(node);
|
|
13
|
+
const generatedId = useId();
|
|
14
|
+
const attributeName =
|
|
15
|
+
(node as any)?.sourceType ?? node.type ?? 'background-image';
|
|
16
|
+
const attributeKey = node.key ?? generatedId;
|
|
17
|
+
|
|
18
|
+
const { previewMode, current, appConfig, projectColors } = useRenderStore(
|
|
19
|
+
(s) => ({
|
|
20
|
+
previewMode: s.previewMode,
|
|
21
|
+
current: s.current,
|
|
22
|
+
appConfig: s.appConfig,
|
|
23
|
+
projectColors: s.projectColors,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const baseStyle = useMemo(
|
|
28
|
+
() => extractViewStyle(node, { appConfig, projectColors }),
|
|
29
|
+
[node, appConfig, projectColors],
|
|
30
|
+
);
|
|
31
|
+
const backgroundStyle = useMemo(() => {
|
|
32
|
+
const attrs = node.attributes;
|
|
33
|
+
const style: React.CSSProperties = {
|
|
34
|
+
backgroundRepeat: 'no-repeat',
|
|
35
|
+
backgroundPosition: 'center',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (attrs?.src) {
|
|
39
|
+
style.backgroundImage = `url(${attrs.src})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (attrs?.resizeMode) {
|
|
43
|
+
case 'cover':
|
|
44
|
+
style.backgroundSize = 'cover';
|
|
45
|
+
break;
|
|
46
|
+
case 'contain':
|
|
47
|
+
style.backgroundSize = 'contain';
|
|
48
|
+
break;
|
|
49
|
+
case 'stretch':
|
|
50
|
+
style.backgroundSize = '100% 100%';
|
|
51
|
+
break;
|
|
52
|
+
case 'center':
|
|
53
|
+
style.backgroundSize = 'auto';
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
style.backgroundSize = 'cover';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return style;
|
|
60
|
+
}, [node]);
|
|
61
|
+
const isSelected = isNodeSelected({ previewMode, current, node });
|
|
62
|
+
const mergedStyle = useMergedStyle(baseStyle, backgroundStyle);
|
|
63
|
+
const style = useMergedStyle(
|
|
64
|
+
mergedStyle,
|
|
65
|
+
isSelected ? SELECTED_OUTLINE_STYLE : undefined,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
attribute-name={attributeName}
|
|
71
|
+
attribute-key={attributeKey}
|
|
72
|
+
style={style}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default React.memo(BackgroundImage);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* AUTO-GENERATED FILE - DO NOT EDIT */
|
|
2
|
+
|
|
3
|
+
import type { NodeData } from '../../types/Node';
|
|
4
|
+
|
|
5
|
+
export type FlexDirectionOptionType = 'row' | 'column';
|
|
6
|
+
export type AlignItemsOptionType =
|
|
7
|
+
| 'flex-start'
|
|
8
|
+
| 'center'
|
|
9
|
+
| 'flex-end'
|
|
10
|
+
| 'stretch'
|
|
11
|
+
| 'baseline';
|
|
12
|
+
export type JustifyContentOptionType =
|
|
13
|
+
| 'flex-start'
|
|
14
|
+
| 'center'
|
|
15
|
+
| 'flex-end'
|
|
16
|
+
| 'space-between'
|
|
17
|
+
| 'space-around'
|
|
18
|
+
| 'space-evenly';
|
|
19
|
+
export type PositionOptionType = 'relative' | 'absolute';
|
|
20
|
+
export type ResizeModeOptionType = 'cover' | 'contain' | 'stretch' | 'center';
|
|
21
|
+
|
|
22
|
+
export interface BackgroundImagePropsGenerated {
|
|
23
|
+
child: string;
|
|
24
|
+
attributes: {
|
|
25
|
+
scrollable?: boolean;
|
|
26
|
+
flexDirection?: FlexDirectionOptionType;
|
|
27
|
+
alignItems?: AlignItemsOptionType;
|
|
28
|
+
justifyContent?: JustifyContentOptionType;
|
|
29
|
+
gap?: string;
|
|
30
|
+
padding?: string;
|
|
31
|
+
paddingHorizontal?: string;
|
|
32
|
+
paddingVertical?: string;
|
|
33
|
+
paddingTop?: string;
|
|
34
|
+
paddingBottom?: string;
|
|
35
|
+
paddingLeft?: string;
|
|
36
|
+
paddingRight?: string;
|
|
37
|
+
margin?: string;
|
|
38
|
+
marginVertical?: string;
|
|
39
|
+
marginTop?: string;
|
|
40
|
+
marginBottom?: string;
|
|
41
|
+
marginLeft?: string;
|
|
42
|
+
marginRight?: string;
|
|
43
|
+
backgroundColor?: string;
|
|
44
|
+
borderRadius?: string;
|
|
45
|
+
width?: string;
|
|
46
|
+
height?: string;
|
|
47
|
+
flex?: number;
|
|
48
|
+
position?: PositionOptionType;
|
|
49
|
+
top?: string;
|
|
50
|
+
bottom?: string;
|
|
51
|
+
left?: string;
|
|
52
|
+
right?: string;
|
|
53
|
+
zIndex?: number;
|
|
54
|
+
src?: string;
|
|
55
|
+
resizeMode?: ResizeModeOptionType;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BackgroundImageComponentProps {
|
|
60
|
+
node: NodeData<BackgroundImagePropsGenerated['attributes']>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"allowUnknownAttributes": false,
|
|
4
|
+
"pattern": {
|
|
5
|
+
"type": "background-image",
|
|
6
|
+
"children": "never",
|
|
7
|
+
"extends": "View",
|
|
8
|
+
"attributes": {
|
|
9
|
+
"src": "string",
|
|
10
|
+
"resizeMode": ["cover", "contain", "stretch", "center"]
|
|
11
|
+
},
|
|
12
|
+
"defaults": {
|
|
13
|
+
"resizeMode": "cover",
|
|
14
|
+
"width": "100%",
|
|
15
|
+
"height": "100%",
|
|
16
|
+
"position": "fixed",
|
|
17
|
+
"top": 0,
|
|
18
|
+
"left": 0,
|
|
19
|
+
"right": 0,
|
|
20
|
+
"bottom": 0,
|
|
21
|
+
"zIndex": 0
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"meta": {
|
|
25
|
+
"desiredParent": ["all", "background"],
|
|
26
|
+
"label": "Background Image",
|
|
27
|
+
"description": "Background image.",
|
|
28
|
+
"attributes": {
|
|
29
|
+
"src": {
|
|
30
|
+
"label": "Src",
|
|
31
|
+
"description": "Image source URL.",
|
|
32
|
+
"category": "other",
|
|
33
|
+
"specialCategory": null,
|
|
34
|
+
"sort": 1
|
|
35
|
+
},
|
|
36
|
+
"resizeMode": {
|
|
37
|
+
"label": "Resize Mode",
|
|
38
|
+
"description": "How the image fits its container.",
|
|
39
|
+
"category": "style",
|
|
40
|
+
"specialCategory": null,
|
|
41
|
+
"sort": 4
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|