@dotcms/uve 1.2.1 → 1.2.2

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/README.md CHANGED
@@ -30,6 +30,20 @@ With `@dotcms/uve`, framework SDKs are able to:
30
30
  - [`updateNavigation()`](#updatenavigationpathname)
31
31
  - [`reorderMenu()`](#reordermenuconfig)
32
32
  - [`sendMessageToUVE()`](#sendmessagetouvemessage)
33
+ - [Style Editor](#style-editor)
34
+ - [What is the Style Editor?](#what-is-the-style-editor)
35
+ - [Quick Start](#quick-start)
36
+ - [`defineStyleEditorSchema()`](#definestyleeditorschemaform)
37
+ - [Field Types](#field-types)
38
+ - [`styleEditorField.input()`](#styleeditorfieldinputconfig)
39
+ - [`styleEditorField.dropdown()`](#styleeditorfielddropdownconfig)
40
+ - [`styleEditorField.radio()`](#styleditorfieldradioconfig)
41
+ - [`styleEditorField.checkboxGroup()`](#styleeditorfieldcheckboxgroupconfig)
42
+ - [`registerStyleEditorSchemas()`](#registerstyleeditorschemasschemas)
43
+ - [`useStyleEditorSchemas()` (React Hook)](#usestyleeditorschemasschemas-react-hook)
44
+ - [Accessing Style Values](#accessing-style-values)
45
+ - [Best Practices](#best-practices)
46
+ - [Complete Example](#complete-example)
33
47
  - [Troubleshooting](#troubleshooting)
34
48
  - [Common Issues & Solutions](#common-issues--solutions)
35
49
  - [Debugging Tips](#debugging-tips)
@@ -473,6 +487,1135 @@ sendMessageToUVE({
473
487
  | `CLIENT_READY` | --- |
474
488
  | `EDIT_CONTENTLET` | `DotCMSBasicContentlet` |
475
489
 
490
+ ## Style Editor
491
+
492
+ ### What is the Style Editor?
493
+
494
+ The Style Editor is a powerful feature that enables content authors and developers to define dynamic, real-time editable properties for contentlets within the Universal Visual Editor (UVE). This allows for live customization of component appearance, layout, typography, colors, and any other configurable aspects without requiring code changes or page reloads.
495
+
496
+ **Key Benefits:**
497
+
498
+ - **Real-Time Visual Editing**: Modify component styles and see changes instantly in the editor
499
+ - **Content-Specific Customization**: Different content types can have unique style schemas
500
+ - **Developer-Controlled**: Developers define which properties are editable and how they're presented
501
+ - **Flexible Configuration**: Support for text inputs, dropdowns, radio buttons, and checkbox groups
502
+ - **Type-Safe**: Full TypeScript support with type inference for option values
503
+
504
+ **Use Cases:**
505
+
506
+ - Adjust typography (font size, family, weight)
507
+ - Configure layouts (grid columns, alignment, spacing)
508
+ - Customize colors and themes
509
+ - Toggle component features (borders, shadows, decorations)
510
+ - Control responsive behavior
511
+ - Modify animation settings
512
+
513
+ ### Quick Start
514
+
515
+ **1. Install the required packages:**
516
+
517
+ ```bash
518
+ npm install @dotcms/uve@latest
519
+ npm install @dotcms/types@latest --save-dev
520
+ ```
521
+
522
+ **2. Define a style editor schema:**
523
+
524
+ ```typescript
525
+ import { defineStyleEditorSchema, styleEditorField } from '@dotcms/uve';
526
+
527
+ const mySchema = defineStyleEditorSchema({
528
+ contentType: 'BlogPost',
529
+ sections: [
530
+ {
531
+ title: 'Typography',
532
+ fields: [
533
+ styleEditorField.dropdown({
534
+ id: 'font-size',
535
+ label: 'Font Size',
536
+ options: [
537
+ { label: 'Small (14px)', value: '14px' },
538
+ { label: 'Medium (16px)', value: '16px' },
539
+ { label: 'Large (18px)', value: '18px' }
540
+ ]
541
+ })
542
+ ]
543
+ }
544
+ ]
545
+ });
546
+ ```
547
+
548
+ **3. Register the schema:**
549
+
550
+ **Using React:**
551
+
552
+ ```typescript
553
+ import { useStyleEditorSchemas } from '@dotcms/react';
554
+
555
+ function MyComponent() {
556
+ useStyleEditorSchemas([mySchema]);
557
+
558
+ return <div>Your component content</div>;
559
+ }
560
+ ```
561
+
562
+ **Using vanilla JavaScript:**
563
+
564
+ ```typescript
565
+ import { registerStyleEditorSchemas } from '@dotcms/uve';
566
+
567
+ registerStyleEditorSchemas([mySchema]);
568
+ ```
569
+
570
+ ### `defineStyleEditorSchema(form)`
571
+
572
+ `defineStyleEditorSchema` creates a normalized style editor schema that UVE can process. It validates your form definition and converts it into the format expected by the Universal Visual Editor.
573
+
574
+ | Input | Type | Required | Description |
575
+ | ------ | ------------------ | -------- | ------------------------------------------------------ |
576
+ | `form` | `StyleEditorForm` | ✅ | The form definition with content type, sections, and fields |
577
+
578
+ **Returns:** `StyleEditorFormSchema` - A normalized schema ready for registration with UVE
579
+
580
+ #### StyleEditorForm Structure
581
+
582
+ ```typescript
583
+ interface StyleEditorForm {
584
+ contentType: string; // The content type identifier
585
+ sections: StyleEditorSection[]; // Array of form sections
586
+ }
587
+
588
+ interface StyleEditorSection {
589
+ title: string; // Section heading displayed in the editor
590
+ fields: StyleEditorField[]; // Array of field definitions
591
+ }
592
+ ```
593
+
594
+ #### Usage
595
+
596
+ ```typescript
597
+ import { defineStyleEditorSchema, styleEditorField } from '@dotcms/uve';
598
+
599
+ const schema = defineStyleEditorSchema({
600
+ contentType: 'Activity',
601
+ sections: [
602
+ {
603
+ title: 'Typography',
604
+ fields: [
605
+ styleEditorField.input({
606
+ id: 'heading-size',
607
+ label: 'Heading Size',
608
+ inputType: 'number',
609
+ placeholder: '24'
610
+ }),
611
+ styleEditorField.dropdown({
612
+ id: 'font-family',
613
+ label: 'Font Family',
614
+ options: ['Arial', 'Helvetica', 'Georgia']
615
+ })
616
+ ]
617
+ },
618
+ {
619
+ title: 'Layout',
620
+ fields: [
621
+ styleEditorField.radio({
622
+ id: 'alignment',
623
+ label: 'Text Alignment',
624
+ options: ['Left', 'Center', 'Right']
625
+ })
626
+ ]
627
+ }
628
+ ]
629
+ });
630
+ ```
631
+
632
+ **⚠️ Important Notes:**
633
+
634
+ - Each field must have a unique `id` within the schema
635
+ - The `contentType` must match the content type in your dotCMS instance
636
+ - Schemas are only processed when UVE is in EDIT mode
637
+
638
+ ### Field Types
639
+
640
+ The Style Editor supports four field types, each designed for specific use cases. Use the `styleEditorField` factory functions to create type-safe field definitions.
641
+
642
+ #### `styleEditorField.input(config)`
643
+
644
+ Creates a text or number input field for free-form entry.
645
+
646
+ **Configuration:**
647
+
648
+ ```typescript
649
+ interface StyleEditorInputFieldConfig {
650
+ id: string; // Unique identifier
651
+ label: string; // Display label
652
+ inputType: StyleEditorFieldInputType; // Input type
653
+ placeholder?: string; // Optional placeholder text
654
+ }
655
+ ```
656
+
657
+ **Use Cases:**
658
+
659
+ - Custom values (e.g., font sizes, margins, colors)
660
+ - Numeric settings (e.g., animation duration, opacity)
661
+ - Text values (e.g., CSS class names, custom IDs)
662
+
663
+ **Examples:**
664
+
665
+ ```typescript
666
+ // Number input for pixel values
667
+ styleEditorField.input({
668
+ id: 'padding-top',
669
+ label: 'Top Padding (px)',
670
+ inputType: 'number',
671
+ placeholder: '16'
672
+ });
673
+
674
+ // Text input for custom CSS
675
+ styleEditorField.input({
676
+ id: 'custom-class',
677
+ label: 'Custom CSS Class',
678
+ inputType: 'text',
679
+ placeholder: 'my-custom-style'
680
+ });
681
+
682
+ // Number input with decimal values
683
+ styleEditorField.input({
684
+ id: 'opacity',
685
+ label: 'Opacity',
686
+ inputType: 'number',
687
+ placeholder: '1.0'
688
+ });
689
+ ```
690
+
691
+ #### `styleEditorField.dropdown(config)`
692
+
693
+ Creates a dropdown (select) field with predefined options. Users can select one value from the list.
694
+
695
+ **Configuration:**
696
+
697
+ ```typescript
698
+ interface StyleEditorDropdownField {
699
+ id: string; // Unique identifier
700
+ label: string; // Display label
701
+ options: StyleEditorOption[]; // Array of options
702
+ }
703
+
704
+ type StyleEditorOption = { label: string; value: string };
705
+ ```
706
+
707
+ **Use Cases:**
708
+
709
+ - Predefined sizes (e.g., small, medium, large)
710
+ - Font families or style presets
711
+ - Color themes
712
+ - Any single-choice selection from a list
713
+
714
+ **Examples:**
715
+
716
+ ```typescript
717
+ // Font size options
718
+ const FONT_SIZES = [
719
+ { label: 'Extra Small (12px)', value: '12px' },
720
+ { label: 'Small (14px)', value: '14px' },
721
+ { label: 'Medium (16px)', value: '16px' },
722
+ { label: 'Large (18px)', value: '18px' },
723
+ { label: 'Extra Large (24px)', value: '24px' }
724
+ ];
725
+
726
+ styleEditorField.dropdown({
727
+ id: 'font-size',
728
+ label: 'Font Size',
729
+ options: FONT_SIZES
730
+ });
731
+
732
+ // Theme selection
733
+ styleEditorField.dropdown({
734
+ id: 'theme',
735
+ label: 'Color Theme',
736
+ options: [
737
+ { label: 'Light Theme', value: 'light' },
738
+ { label: 'Dark Theme', value: 'dark' },
739
+ { label: 'High Contrast', value: 'high-contrast' }
740
+ ]
741
+ });
742
+ ```
743
+
744
+
745
+ #### `styleEditorField.radio(config)`
746
+
747
+ Creates a radio button group for single-choice selection. Optionally supports images for visual selection.
748
+
749
+ **Configuration:**
750
+
751
+ ```typescript
752
+ interface StyleEditorRadioField {
753
+ id: string; // Unique identifier
754
+ label: string; // Display label
755
+ options: StyleEditorRadioOption[]; // Array of options
756
+ columns?: 1 | 2; // Layout: 1 or 2 columns (default: 1)
757
+ }
758
+
759
+ type StyleEditorRadioOption = {
760
+ label: string;
761
+ value: string;
762
+ imageURL?: string; // Optional preview image
763
+ };
764
+ ```
765
+
766
+ **Use Cases:**
767
+
768
+ - Layout selection with visual previews
769
+ - Alignment options (left, center, right)
770
+ - Style variants with images
771
+ - Any single-choice where visual feedback is helpful
772
+
773
+ **Examples:**
774
+
775
+ ```typescript
776
+ // Simple text options
777
+ styleEditorField.radio({
778
+ id: 'text-align',
779
+ label: 'Text Alignment',
780
+ options: [
781
+ { label: 'Left', value: 'left' },
782
+ { label: 'Center', value: 'center' },
783
+ { label: 'Right', value: 'right' },
784
+ { label: 'Justify', value: 'justify' }
785
+ ]
786
+ });
787
+
788
+ // Two-column layout with images
789
+ const LAYOUT_OPTIONS = [
790
+ {
791
+ label: 'Left Sidebar',
792
+ value: 'left',
793
+ imageURL: 'https://example.com/layouts/left-sidebar.png'
794
+ },
795
+ {
796
+ label: 'Right Sidebar',
797
+ value: 'right',
798
+ imageURL: 'https://example.com/layouts/right-sidebar.png'
799
+ },
800
+ {
801
+ label: 'Full Width',
802
+ value: 'full',
803
+ imageURL: 'https://example.com/layouts/full-width.png'
804
+ },
805
+ {
806
+ label: 'Split View',
807
+ value: 'split',
808
+ imageURL: 'https://example.com/layouts/split-view.png'
809
+ }
810
+ ];
811
+
812
+ styleEditorField.radio({
813
+ id: 'page-layout',
814
+ label: 'Page Layout',
815
+ columns: 2, // Display in 2-column grid
816
+ options: LAYOUT_OPTIONS
817
+ });
818
+
819
+ // Font weight selection
820
+ styleEditorField.radio({
821
+ id: 'font-weight',
822
+ label: 'Font Weight',
823
+ options: [
824
+ { label: 'Normal', value: '400' },
825
+ { label: 'Medium', value: '500' },
826
+ { label: 'Semi-Bold', value: '600' },
827
+ { label: 'Bold', value: '700' }
828
+ ]
829
+ });
830
+ ```
831
+
832
+ **💡 Image Guidelines:**
833
+
834
+ - Use clear, recognizable preview images
835
+ - Recommended size: 200x150px or similar aspect ratio
836
+ - Use consistent image dimensions within a radio group
837
+ - Images should clearly differentiate between options
838
+
839
+ #### `styleEditorField.checkboxGroup(config)`
840
+
841
+ Creates a group of checkboxes for multi-selection. Each checkbox returns a boolean value (checked/unchecked).
842
+
843
+ **Configuration:**
844
+
845
+ ```typescript
846
+ interface StyleEditorCheckboxGroupField {
847
+ id: string; // Unique identifier for the group
848
+ label: string; // Display label for the group
849
+ options: StyleEditorCheckboxOption[]; // Array of checkbox options
850
+ }
851
+
852
+ interface StyleEditorCheckboxOption {
853
+ label: string; // Display text for the checkbox
854
+ key: string; // Unique identifier (NOT 'value')
855
+ }
856
+ ```
857
+
858
+ **⚠️ Important:** Checkbox options use `key` instead of `value` because the actual value is boolean (true/false).
859
+
860
+ **Use Cases:**
861
+
862
+ - Text decorations (bold, italic, underline)
863
+ - Feature toggles (enable shadows, borders, animations)
864
+ - Multiple style attributes
865
+ - Any multi-select boolean options
866
+
867
+ **Examples:**
868
+
869
+ ```typescript
870
+ // Typography settings
871
+ styleEditorField.checkboxGroup({
872
+ id: 'text-style',
873
+ label: 'Text Style',
874
+ options: [
875
+ { label: 'Bold', key: 'bold' },
876
+ { label: 'Italic', key: 'italic' },
877
+ { label: 'Underline', key: 'underline' },
878
+ { label: 'Strikethrough', key: 'strikethrough' }
879
+ ]
880
+ });
881
+
882
+ // Component features
883
+ styleEditorField.checkboxGroup({
884
+ id: 'component-features',
885
+ label: 'Component Features',
886
+ options: [
887
+ { label: 'Show Shadow', key: 'shadow' },
888
+ { label: 'Show Border', key: 'border' },
889
+ { label: 'Enable Animation', key: 'animate' },
890
+ { label: 'Rounded Corners', key: 'rounded' }
891
+ ]
892
+ });
893
+
894
+ // Responsive behavior
895
+ styleEditorField.checkboxGroup({
896
+ id: 'responsive',
897
+ label: 'Responsive Options',
898
+ options: [
899
+ { label: 'Hide on Mobile', key: 'hide-mobile' },
900
+ { label: 'Hide on Tablet', key: 'hide-tablet' },
901
+ { label: 'Full Width on Mobile', key: 'full-width-mobile' }
902
+ ]
903
+ });
904
+ ```
905
+
906
+ **Return Value Structure:**
907
+
908
+ ```typescript
909
+ // Example return value when checkboxes are checked
910
+ {
911
+ "bold": true,
912
+ "italic": false,
913
+ "underline": true,
914
+ "strikethrough": false
915
+ }
916
+ ```
917
+
918
+ ### `registerStyleEditorSchemas(schemas)`
919
+
920
+ `registerStyleEditorSchemas` registers one or more style editor schemas with UVE. This function should be called during your component initialization to make the schemas available in the editor.
921
+
922
+ | Input | Type | Required | Description |
923
+ | --------- | --------------------------- | -------- | ------------------------------------------------ |
924
+ | `schemas` | `StyleEditorFormSchema[]` | ✅ | Array of normalized schemas from `defineStyleEditorSchema` |
925
+
926
+ **Returns:** `void`
927
+
928
+ **Behavior:**
929
+
930
+ - Only registers schemas when UVE is in **EDIT** mode
931
+ - Silently returns if UVE is not in EDIT mode
932
+ - Validates that each schema has a `contentType` property
933
+ - Logs a warning and skips schemas without `contentType`
934
+ - Sends validated schemas to UVE via internal messaging
935
+
936
+ #### Usage
937
+
938
+ ```typescript
939
+ import { defineStyleEditorSchema, styleEditorField, registerStyleEditorSchemas } from '@dotcms/uve';
940
+
941
+ // Create schemas
942
+ const blogSchema = defineStyleEditorSchema({
943
+ contentType: 'BlogPost',
944
+ sections: [
945
+ {
946
+ title: 'Typography',
947
+ fields: [
948
+ styleEditorField.dropdown({
949
+ id: 'font-size',
950
+ label: 'Font Size',
951
+ options: ['14px', '16px', '18px']
952
+ })
953
+ ]
954
+ }
955
+ ]
956
+ });
957
+
958
+ const activitySchema = defineStyleEditorSchema({
959
+ contentType: 'Activity',
960
+ sections: [
961
+ {
962
+ title: 'Layout',
963
+ fields: [
964
+ styleEditorField.radio({
965
+ id: 'layout',
966
+ label: 'Layout',
967
+ options: ['Left', 'Right', 'Center']
968
+ })
969
+ ]
970
+ }
971
+ ]
972
+ });
973
+
974
+ // Register multiple schemas at once
975
+ registerStyleEditorSchemas([blogSchema, activitySchema]);
976
+ ```
977
+
978
+ **⚠️ Important Notes:**
979
+
980
+ - Call this function after UVE initialization (`initUVE`)
981
+ - Schemas are only processed in EDIT mode
982
+ - Missing `contentType` will cause the schema to be skipped
983
+ - You can register multiple schemas for different content types
984
+
985
+ ### `useStyleEditorSchemas(schemas)` (React Hook)
986
+
987
+ **Available in:** `@dotcms/react` package
988
+
989
+ `useStyleEditorSchemas` is a React hook that simplifies schema registration by automatically handling the component lifecycle. It registers schemas when the component mounts and re-registers if the schemas array reference changes.
990
+
991
+ | Input | Type | Required | Description |
992
+ | -------- | --------------------------- | -------- | ------------------------------- |
993
+ | `schemas` | `StyleEditorFormSchema[]` | ✅ | Array of normalized form schemas |
994
+
995
+ **Returns:** `void`
996
+
997
+ **Behavior:**
998
+
999
+ - Registers schemas on component mount
1000
+ - Re-registers when the `schemas` array reference changes
1001
+ - Internally calls `registerStyleEditorSchemas()`
1002
+ - Safe to call in multiple components
1003
+
1004
+ #### Usage
1005
+
1006
+ ```typescript
1007
+ import { useStyleEditorSchemas } from '@dotcms/react';
1008
+ import { defineStyleEditorSchema, styleEditorField } from '@dotcms/uve';
1009
+
1010
+ function BlogPostEditor() {
1011
+ // Define schemas
1012
+ const schemas = [
1013
+ defineStyleEditorSchema({
1014
+ contentType: 'BlogPost',
1015
+ sections: [
1016
+ {
1017
+ title: 'Typography',
1018
+ fields: [
1019
+ styleEditorField.dropdown({
1020
+ id: 'font-size',
1021
+ label: 'Font Size',
1022
+ options: [
1023
+ { label: '14px', value: '14px' },
1024
+ { label: '16px', value: '16px' },
1025
+ { label: '18px', value: '18px' },
1026
+ { label: '24px', value: '24px' }
1027
+ ]
1028
+ }),
1029
+ styleEditorField.radio({
1030
+ id: 'font-weight',
1031
+ label: 'Font Weight',
1032
+ options: [
1033
+ { label: 'Normal', value: 'normal' },
1034
+ { label: 'Bold', value: 'bold' }
1035
+ ]
1036
+ })
1037
+ ]
1038
+ }
1039
+ ]
1040
+ })
1041
+ ];
1042
+
1043
+ // Register schemas automatically
1044
+ useStyleEditorSchemas(schemas);
1045
+
1046
+ return (
1047
+ <div>
1048
+ <h1>Blog Post Editor</h1>
1049
+ {/* Your component content */}
1050
+ </div>
1051
+ );
1052
+ }
1053
+ ```
1054
+
1055
+ **💡 Performance Tip:** For better performance in components that re-render frequently, you can optionally use `useMemo` to prevent re-creating the schema on every render:
1056
+
1057
+ ```typescript
1058
+ import { useMemo } from 'react';
1059
+
1060
+ function BlogPostEditor() {
1061
+ const schemas = useMemo(
1062
+ () => [
1063
+ defineStyleEditorSchema({
1064
+ /* schema definition */
1065
+ })
1066
+ ],
1067
+ [] // Empty deps = create once
1068
+ );
1069
+
1070
+ useStyleEditorSchemas(schemas);
1071
+ return <div>Content</div>;
1072
+ }
1073
+ ```
1074
+
1075
+ ### Accessing Style Values
1076
+
1077
+ Style Editor values are managed internally by UVE and passed to your components through the `styleProperties` attribute. This attribute is available in your contentlet component props.
1078
+
1079
+ #### In React Components
1080
+
1081
+ When rendering contentlets, style properties are accessed through the `styleProperties` prop:
1082
+
1083
+ ```typescript
1084
+ import { DotCMSContentlet } from '@dotcms/types';
1085
+
1086
+ interface ActivityProps {
1087
+ contentlet: DotCMSContentlet;
1088
+ styleProperties?: Record<string, any>;
1089
+ }
1090
+
1091
+ function Activity(props: ActivityProps) {
1092
+ const { title, description, styleProperties } = props; // Contentlet information
1093
+
1094
+ // Access style values using dot notation or bracket notation
1095
+ const fontSize = styleProperties?.['font-size'];
1096
+ const textAlign = styleProperties?.text;
1097
+ const layout = styleProperties?.layout;
1098
+
1099
+ return (
1100
+ <div style={{ fontSize, textAlign }}>
1101
+ <h1>{title}</h1>
1102
+ <p>{description}</p>
1103
+ </div>
1104
+ );
1105
+ }
1106
+ ```
1107
+
1108
+ #### Value Types by Field Type
1109
+
1110
+ **Input Field:**
1111
+
1112
+ ```typescript
1113
+ // Returns: string (text) or number (number input)
1114
+ const fontSize: string = '16px';
1115
+ const padding: number = 24;
1116
+ ```
1117
+
1118
+ **Dropdown Field:**
1119
+
1120
+ ```typescript
1121
+ // Returns: string (the selected value)
1122
+ const theme: string = 'light';
1123
+ const fontFamily: string = 'Arial';
1124
+ ```
1125
+
1126
+ **Radio Field:**
1127
+
1128
+ ```typescript
1129
+ // Returns: string (the selected value)
1130
+ const layout: string = 'left';
1131
+ const alignment: string = 'center';
1132
+ ```
1133
+
1134
+ **Checkbox Group:**
1135
+
1136
+ ```typescript
1137
+ // Returns: Record<string, boolean> (object with key-value pairs)
1138
+ const textStyles: Record<string, boolean> = {
1139
+ bold: true,
1140
+ italic: false,
1141
+ underline: true,
1142
+ strikethrough: false
1143
+ };
1144
+
1145
+ // Access individual values
1146
+ if (textStyles.bold) {
1147
+ // Apply bold styling
1148
+ }
1149
+ ```
1150
+
1151
+ #### Applying Style Values
1152
+
1153
+ Use the style values to conditionally render styles, classes, or component variants:
1154
+
1155
+ ```typescript
1156
+ function BlogPost(props) {
1157
+ const { title, body, styleProperties } = props;
1158
+
1159
+ // Example: Apply dynamic font size
1160
+ const fontSize = styleProperties?.['font-size'] || '16px';
1161
+
1162
+ // Example: Apply layout classes
1163
+ const layout = styleProperties?.layout || 'default';
1164
+ const layoutClass = `layout-${layout}`;
1165
+
1166
+ // Example: Apply checkbox group values
1167
+ const textStyles = styleProperties?.['text-style'] || {};
1168
+ const textStyleClasses = [
1169
+ textStyles.bold ? 'font-bold' : '',
1170
+ textStyles.italic ? 'font-italic' : '',
1171
+ textStyles.underline ? 'text-underline' : ''
1172
+ ]
1173
+ .filter(Boolean)
1174
+ .join(' ');
1175
+
1176
+ return (
1177
+ <div className={`${layoutClass} ${textStyleClasses}`} style={{ fontSize }}>
1178
+ <h1>{title}</h1>
1179
+ <p>{body}</p>
1180
+ </div>
1181
+ );
1182
+ }
1183
+ ```
1184
+
1185
+ **💡 Note:** The `styleProperties` prop is automatically passed to your contentlet components by the framework SDK when UVE is active and style schemas are registered.
1186
+
1187
+ ### Best Practices
1188
+
1189
+ #### 1. Use Meaningful IDs and Labels
1190
+
1191
+ ```typescript
1192
+ // ✅ Good: Clear, descriptive IDs and labels
1193
+ styleEditorField.dropdown({
1194
+ id: 'heading-font-size',
1195
+ label: 'Heading Font Size',
1196
+ options: [
1197
+ { label: 'Small (18px)', value: '18px' },
1198
+ { label: 'Medium (24px)', value: '24px' },
1199
+ { label: 'Large (32px)', value: '32px' }
1200
+ ]
1201
+ });
1202
+
1203
+ // ❌ Bad: Vague IDs and labels
1204
+ styleEditorField.dropdown({
1205
+ id: 'size',
1206
+ label: 'Size',
1207
+ options: ['18px', '24px', '32px']
1208
+ });
1209
+ ```
1210
+
1211
+ #### 2. Group Related Fields in Sections
1212
+
1213
+ ```typescript
1214
+ // ✅ Good: Logical grouping by functionality
1215
+ defineStyleEditorSchema({
1216
+ contentType: 'BlogPost',
1217
+ sections: [
1218
+ {
1219
+ title: 'Typography',
1220
+ fields: [
1221
+ /* font-related fields */
1222
+ ]
1223
+ },
1224
+ {
1225
+ title: 'Layout',
1226
+ fields: [
1227
+ /* layout-related fields */
1228
+ ]
1229
+ },
1230
+ {
1231
+ title: 'Colors',
1232
+ fields: [
1233
+ /* color-related fields */
1234
+ ]
1235
+ }
1236
+ ]
1237
+ });
1238
+
1239
+ // ❌ Bad: All fields in one section
1240
+ defineStyleEditorSchema({
1241
+ contentType: 'BlogPost',
1242
+ sections: [
1243
+ {
1244
+ title: 'Settings',
1245
+ fields: [
1246
+ /* all fields mixed together */
1247
+ ]
1248
+ }
1249
+ ]
1250
+ });
1251
+ ```
1252
+
1253
+ #### 3. Provide Clear Option Labels
1254
+
1255
+ ```typescript
1256
+ // ✅ Good: Descriptive labels with context
1257
+ styleEditorField.dropdown({
1258
+ id: 'font-size',
1259
+ label: 'Font Size',
1260
+ options: [
1261
+ { label: 'Extra Small (12px)', value: '12px' },
1262
+ { label: 'Small (14px)', value: '14px' },
1263
+ { label: 'Medium (16px)', value: '16px' },
1264
+ { label: 'Large (18px)', value: '18px' }
1265
+ ]
1266
+ });
1267
+
1268
+ // ❌ Bad: Unclear labels
1269
+ styleEditorField.dropdown({
1270
+ id: 'font-size',
1271
+ label: 'Font Size',
1272
+ options: ['XS', 'S', 'M', 'L']
1273
+ });
1274
+ ```
1275
+
1276
+ #### 4. Use Appropriate Field Types
1277
+
1278
+ ```typescript
1279
+ // ✅ Good: Radio with images for visual layouts
1280
+ styleEditorField.radio({
1281
+ id: 'page-layout',
1282
+ label: 'Layout',
1283
+ columns: 2,
1284
+ options: [
1285
+ { label: 'Left', value: 'left', imageURL: '...' },
1286
+ { label: 'Right', value: 'right', imageURL: '...' }
1287
+ ]
1288
+ });
1289
+
1290
+ // ✅ Good: Dropdown for text-only options
1291
+ styleEditorField.dropdown({
1292
+ id: 'font-family',
1293
+ label: 'Font',
1294
+ options: ['Arial', 'Georgia', 'Verdana']
1295
+ });
1296
+
1297
+ // ✅ Good: Checkbox group for boolean flags
1298
+ styleEditorField.checkboxGroup({
1299
+ id: 'text-decorations',
1300
+ label: 'Text Decorations',
1301
+ options: [
1302
+ { label: 'Bold', key: 'bold' },
1303
+ { label: 'Italic', key: 'italic' }
1304
+ ]
1305
+ });
1306
+ ```
1307
+
1308
+ #### 5. Validate Content Type Matching
1309
+
1310
+ ```typescript
1311
+ // ✅ Good: Content type matches your dotCMS content type
1312
+ defineStyleEditorSchema({
1313
+ contentType: 'BlogPost', // Matches content type in dotCMS
1314
+ sections: [
1315
+ /* ... */
1316
+ ]
1317
+ });
1318
+
1319
+ // ❌ Bad: Typo or mismatch
1320
+ defineStyleEditorSchema({
1321
+ contentType: 'blog-post', // Won't match 'BlogPost' in dotCMS
1322
+ sections: [
1323
+ /* ... */
1324
+ ]
1325
+ });
1326
+ ```
1327
+
1328
+ #### 6. Provide Sensible Defaults
1329
+
1330
+ When using style properties, always provide fallback defaults:
1331
+
1332
+ ```typescript
1333
+ // ✅ Good: Fallback values prevent errors
1334
+ const fontSize = styleProperties?.['font-size'] || '16px';
1335
+ const layout = styleProperties?.layout || 'default';
1336
+ const textStyles = styleProperties?.['text-style'] || {};
1337
+
1338
+ // ❌ Bad: No fallbacks (could cause errors)
1339
+ const fontSize = styleProperties?.['font-size'];
1340
+ const layout = styleProperties?.layout;
1341
+ ```
1342
+
1343
+ ### Complete Example
1344
+
1345
+ Here's a comprehensive example demonstrating all Style Editor features:
1346
+
1347
+ ```typescript
1348
+ import { useStyleEditorSchemas } from '@dotcms/react';
1349
+ import { defineStyleEditorSchema, styleEditorField } from '@dotcms/uve';
1350
+
1351
+ export function BlogPostStyleEditor() {
1352
+ // Define option constants
1353
+ const FONT_SIZES = [
1354
+ { label: 'Extra Small (12px)', value: '12px' },
1355
+ { label: 'Small (14px)', value: '14px' },
1356
+ { label: 'Medium (16px)', value: '16px' },
1357
+ { label: 'Large (18px)', value: '18px' },
1358
+ { label: 'Extra Large (24px)', value: '24px' },
1359
+ { label: 'Huge (32px)', value: '32px' }
1360
+ ];
1361
+
1362
+ const FONT_FAMILIES = [
1363
+ { label: 'Arial', value: 'arial' },
1364
+ { label: 'Georgia', value: 'georgia' },
1365
+ { label: 'Helvetica', value: 'helvetica' },
1366
+ { label: 'Times New Roman', value: 'times' },
1367
+ { label: 'Verdana', value: 'verdana' },
1368
+ { label: 'Courier New', value: 'courier' }
1369
+ ];
1370
+
1371
+ const LAYOUT_OPTIONS = [
1372
+ {
1373
+ label: 'Left Sidebar',
1374
+ value: 'sidebar-left',
1375
+ imageURL: 'https://example.com/layouts/sidebar-left.png'
1376
+ },
1377
+ {
1378
+ label: 'Right Sidebar',
1379
+ value: 'sidebar-right',
1380
+ imageURL: 'https://example.com/layouts/sidebar-right.png'
1381
+ },
1382
+ {
1383
+ label: 'Full Width',
1384
+ value: 'full-width',
1385
+ imageURL: 'https://example.com/layouts/full-width.png'
1386
+ },
1387
+ {
1388
+ label: 'Centered',
1389
+ value: 'centered',
1390
+ imageURL: 'https://example.com/layouts/centered.png'
1391
+ }
1392
+ ];
1393
+
1394
+ const COLOR_THEMES = [
1395
+ { label: 'Light Theme', value: 'light' },
1396
+ { label: 'Dark Theme', value: 'dark' },
1397
+ { label: 'High Contrast', value: 'high-contrast' },
1398
+ { label: 'Sepia', value: 'sepia' }
1399
+ ];
1400
+
1401
+ // Define schema (optionally use useMemo to prevent re-creation on every render)
1402
+ const schemas = [
1403
+ defineStyleEditorSchema({
1404
+ contentType: 'BlogPost',
1405
+ sections: [
1406
+ {
1407
+ title: 'Typography',
1408
+ fields: [
1409
+ styleEditorField.dropdown({
1410
+ id: 'heading-font-size',
1411
+ label: 'Heading Font Size',
1412
+ options: FONT_SIZES
1413
+ }),
1414
+ styleEditorField.dropdown({
1415
+ id: 'body-font-size',
1416
+ label: 'Body Font Size',
1417
+ options: FONT_SIZES.slice(0, 4) // Only smaller sizes
1418
+ }),
1419
+ styleEditorField.dropdown({
1420
+ id: 'font-family',
1421
+ label: 'Font Family',
1422
+ options: FONT_FAMILIES
1423
+ }),
1424
+ styleEditorField.input({
1425
+ id: 'line-height',
1426
+ label: 'Line Height',
1427
+ inputType: 'number',
1428
+ placeholder: '1.5'
1429
+ }),
1430
+ styleEditorField.checkboxGroup({
1431
+ id: 'text-style',
1432
+ label: 'Text Style',
1433
+ options: [
1434
+ { label: 'Bold Headings', key: 'bold-headings' },
1435
+ { label: 'Italic Quotes', key: 'italic-quotes' },
1436
+ { label: 'Underline Links', key: 'underline-links' }
1437
+ ]
1438
+ })
1439
+ ]
1440
+ },
1441
+ {
1442
+ title: 'Layout',
1443
+ fields: [
1444
+ styleEditorField.radio({
1445
+ id: 'page-layout',
1446
+ label: 'Page Layout',
1447
+ columns: 2,
1448
+ options: LAYOUT_OPTIONS
1449
+ }),
1450
+ styleEditorField.radio({
1451
+ id: 'content-width',
1452
+ label: 'Content Width',
1453
+ options: [
1454
+ { label: 'Narrow (800px)', value: '800px' },
1455
+ { label: 'Medium (1000px)', value: '1000px' },
1456
+ { label: 'Wide (1200px)', value: '1200px' },
1457
+ { label: 'Extra Wide (1400px)', value: '1400px' }
1458
+ ]
1459
+ }),
1460
+ styleEditorField.input({
1461
+ id: 'section-spacing',
1462
+ label: 'Section Spacing (px)',
1463
+ inputType: 'number',
1464
+ placeholder: '40'
1465
+ })
1466
+ ]
1467
+ },
1468
+ {
1469
+ title: 'Colors & Theme',
1470
+ fields: [
1471
+ styleEditorField.dropdown({
1472
+ id: 'color-theme',
1473
+ label: 'Color Theme',
1474
+ options: COLOR_THEMES
1475
+ }),
1476
+ styleEditorField.input({
1477
+ id: 'primary-color',
1478
+ label: 'Primary Color',
1479
+ inputType: 'text',
1480
+ placeholder: '#007bff'
1481
+ }),
1482
+ styleEditorField.input({
1483
+ id: 'secondary-color',
1484
+ label: 'Secondary Color',
1485
+ inputType: 'text',
1486
+ placeholder: '#6c757d'
1487
+ }),
1488
+ styleEditorField.input({
1489
+ id: 'background-color',
1490
+ label: 'Background Color',
1491
+ inputType: 'text',
1492
+ placeholder: '#ffffff'
1493
+ })
1494
+ ]
1495
+ },
1496
+ {
1497
+ title: 'Component Features',
1498
+ fields: [
1499
+ styleEditorField.checkboxGroup({
1500
+ id: 'features',
1501
+ label: 'Enable Features',
1502
+ options: [
1503
+ { label: 'Drop Shadow', key: 'shadow' },
1504
+ { label: 'Border', key: 'border' },
1505
+ { label: 'Rounded Corners', key: 'rounded' },
1506
+ { label: 'Smooth Animations', key: 'animate' },
1507
+ { label: 'Hover Effects', key: 'hover-effects' }
1508
+ ]
1509
+ }),
1510
+ styleEditorField.checkboxGroup({
1511
+ id: 'responsive',
1512
+ label: 'Responsive Options',
1513
+ options: [
1514
+ { label: 'Hide on Mobile', key: 'hide-mobile' },
1515
+ { label: 'Stack on Tablet', key: 'stack-tablet' },
1516
+ {
1517
+ label: 'Full Width on Mobile',
1518
+ key: 'full-width-mobile'
1519
+ }
1520
+ ]
1521
+ })
1522
+ ]
1523
+ }
1524
+ ]
1525
+ })
1526
+ ];
1527
+
1528
+ // Register schemas with UVE
1529
+ useStyleEditorSchemas(schemas);
1530
+
1531
+ return (
1532
+ <div>
1533
+ <h1>Blog Post Style Editor</h1>
1534
+ <p>Style editor schema is registered and available in UVE edit mode.</p>
1535
+ </div>
1536
+ );
1537
+ }
1538
+
1539
+ // Example: Using style properties in a component
1540
+ export function BlogPostRenderer(props) {
1541
+ const { title, body, styleProperties } = props;
1542
+
1543
+ // Extract style values with defaults
1544
+ const headingSize = styleProperties?.['heading-font-size'] || '24px';
1545
+ const bodySize = styleProperties?.['body-font-size'] || '16px';
1546
+ const fontFamily = styleProperties?.['font-family'] || 'arial';
1547
+ const lineHeight = styleProperties?.['line-height'] || '1.5';
1548
+ const layout = styleProperties?.['page-layout'] || 'full-width';
1549
+ const contentWidth = styleProperties?.['content-width'] || '1000px';
1550
+ const sectionSpacing = styleProperties?.['section-spacing'] || 40;
1551
+ const theme = styleProperties?.['color-theme'] || 'light';
1552
+ const primaryColor = styleProperties?.['primary-color'] || '#007bff';
1553
+ const backgroundColor = styleProperties?.['background-color'] || '#ffffff';
1554
+
1555
+ // Extract checkbox group values
1556
+ const textStyle = styleProperties?.['text-style'] || {};
1557
+ const features = styleProperties?.features || {};
1558
+ const responsive = styleProperties?.responsive || {};
1559
+
1560
+ // Build CSS classes based on values
1561
+ const containerClasses = [
1562
+ `layout-${layout}`,
1563
+ `theme-${theme}`,
1564
+ features.shadow ? 'has-shadow' : '',
1565
+ features.border ? 'has-border' : '',
1566
+ features.rounded ? 'has-rounded' : '',
1567
+ features.animate ? 'has-animations' : '',
1568
+ responsive['hide-mobile'] ? 'hide-mobile' : '',
1569
+ responsive['stack-tablet'] ? 'stack-tablet' : ''
1570
+ ]
1571
+ .filter(Boolean)
1572
+ .join(' ');
1573
+
1574
+ return (
1575
+ <div
1576
+ className={containerClasses}
1577
+ style={{
1578
+ fontFamily,
1579
+ lineHeight,
1580
+ backgroundColor,
1581
+ maxWidth: contentWidth,
1582
+ paddingTop: `${sectionSpacing}px`,
1583
+ paddingBottom: `${sectionSpacing}px`
1584
+ }}
1585
+ >
1586
+ <h1
1587
+ style={{
1588
+ fontSize: headingSize,
1589
+ fontWeight: textStyle['bold-headings'] ? 'bold' : 'normal',
1590
+ color: primaryColor
1591
+ }}
1592
+ >
1593
+ {title}
1594
+ </h1>
1595
+
1596
+ <div
1597
+ style={{
1598
+ fontSize: bodySize
1599
+ }}
1600
+ >
1601
+ {body}
1602
+ </div>
1603
+ </div>
1604
+ );
1605
+ }
1606
+ ```
1607
+
1608
+ **This example demonstrates:**
1609
+
1610
+ - ✅ Organized option constants
1611
+ - ✅ Logical section grouping (Typography, Layout, Colors, Features)
1612
+ - ✅ All four field types (input, dropdown, radio, checkboxGroup)
1613
+ - ✅ Visual layout selection with images
1614
+ - ✅ Checkbox groups for boolean flags
1615
+ - ✅ Clear, descriptive labels
1616
+ - ✅ Safe value extraction with defaults using `styleProperties`
1617
+ - ✅ Dynamic styling based on style values
1618
+
476
1619
  ## Troubleshooting
477
1620
 
478
1621
  ### Common Issues & Solutions