@structured-field/widget-editor 0.1.0

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.
@@ -0,0 +1,162 @@
1
+ .structured-field-editor {
2
+
3
+ // --- Field spacing ---
4
+ .sf-field {
5
+ margin-bottom: 12px;
6
+ }
7
+
8
+ .sf-field-boolean {
9
+ margin-bottom: 8px;
10
+ }
11
+
12
+ // --- Object editor ---
13
+ .sf-object {
14
+ &.sf-object-root {
15
+ // Root level: no border
16
+ }
17
+
18
+ &:not(.sf-object-root) {
19
+ border: 1px solid var(--border-color, #ccc);
20
+ border-radius: 4px;
21
+ padding: 12px;
22
+ margin-bottom: 12px;
23
+ background: var(--body-bg, #fff);
24
+ }
25
+ }
26
+
27
+ .sf-object-title {
28
+ font-weight: 600;
29
+ font-size: 0.8125rem;
30
+ color: var(--body-fg, #333);
31
+ text-transform: capitalize;
32
+ padding: 0 4px;
33
+ }
34
+
35
+ .sf-object-fields {
36
+ display: flex;
37
+ flex-direction: column;
38
+ }
39
+
40
+ // --- Array editor ---
41
+ .sf-array {
42
+ margin-bottom: 12px;
43
+ }
44
+
45
+ .sf-array-header {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 8px;
49
+ margin-bottom: 8px;
50
+
51
+ .sf-label {
52
+ margin-bottom: 0;
53
+ }
54
+ }
55
+
56
+ .sf-array-count {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ min-width: 20px;
61
+ height: 20px;
62
+ padding: 0 6px;
63
+ background: var(--darkened-bg, #f0f0f0);
64
+ border-radius: 10px;
65
+ font-size: 0.6875rem;
66
+ font-weight: 600;
67
+ color: var(--body-quiet-color, #666);
68
+ }
69
+
70
+ .sf-array-items {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: 8px;
74
+ }
75
+
76
+ .sf-array-item {
77
+ border: 1px solid var(--border-color, #ccc);
78
+ border-radius: 4px;
79
+ background: var(--darkened-bg, #f8f8f8);
80
+ overflow: hidden;
81
+ }
82
+
83
+ .sf-array-item-header {
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: space-between;
87
+ padding: 4px 8px;
88
+ background: var(--darkened-bg, #f0f0f0);
89
+ border-bottom: 1px solid var(--border-color, #ccc);
90
+ }
91
+
92
+ .sf-array-item-index {
93
+ font-size: 0.6875rem;
94
+ font-weight: 600;
95
+ color: var(--body-quiet-color, #666);
96
+ }
97
+
98
+ .sf-array-item-actions {
99
+ display: flex;
100
+ gap: 4px;
101
+ }
102
+
103
+ .sf-array-item-body {
104
+ padding: 10px;
105
+
106
+ > .sf-object.sf-object-root,
107
+ > .sf-object:not(.sf-object-root) {
108
+ border: none;
109
+ padding: 0;
110
+ margin: 0;
111
+ background: transparent;
112
+ }
113
+ }
114
+
115
+ // --- Nullable editor ---
116
+ .sf-nullable {
117
+ margin-bottom: 12px;
118
+ }
119
+
120
+ .sf-nullable-header {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 8px;
124
+ margin-bottom: 4px;
125
+
126
+ .sf-label {
127
+ margin-bottom: 0;
128
+ }
129
+ }
130
+
131
+ .sf-nullable-body {
132
+ padding-left: 0;
133
+
134
+ &:not(:empty) {
135
+ border: 1px solid var(--border-color, #ccc);
136
+ border-radius: 4px;
137
+ padding: 12px;
138
+ margin-top: 4px;
139
+ background: var(--body-bg, #fff);
140
+ }
141
+ }
142
+
143
+ // --- Union editor ---
144
+ .sf-union {
145
+ margin-bottom: 12px;
146
+ }
147
+
148
+ .sf-union-body {
149
+ border: 1px solid var(--border-color, #ccc);
150
+ border-radius: 4px;
151
+ padding: 12px;
152
+ margin-top: 8px;
153
+ background: var(--body-bg, #fff);
154
+
155
+ > .sf-object {
156
+ border: none;
157
+ padding: 0;
158
+ margin: 0;
159
+ background: transparent;
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,137 @@
1
+ .structured-field-editor {
2
+ width: 100%;
3
+ border-radius: 4px;
4
+ font-size: 0.8125rem;
5
+
6
+ *, *::before, *::after {
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ .sf-label {
11
+ display: block;
12
+ font-weight: 600;
13
+ margin-bottom: 4px;
14
+ text-transform: capitalize;
15
+ color: var(--body-quiet-color, #666);
16
+ font-size: 0.8125rem;
17
+
18
+ &.required::after {
19
+ content: " *";
20
+ color: var(--error-fg, #ba2121);
21
+ }
22
+ }
23
+
24
+ .sf-input {
25
+ display: block;
26
+ width: 100%;
27
+ padding: 6px 8px;
28
+ border: 1px solid var(--border-color, #ccc);
29
+ border-radius: 4px;
30
+ background: var(--body-bg, #fff);
31
+ color: var(--body-fg, #333);
32
+ font-size: 0.8125rem;
33
+ font-family: inherit;
34
+ transition: border-color 0.15s;
35
+
36
+ &:focus {
37
+ outline: none;
38
+ border-color: var(--primary, #79aec8);
39
+ box-shadow: 0 0 0 2px rgba(121, 174, 200, 0.2);
40
+ }
41
+ }
42
+
43
+ .sf-textarea {
44
+ resize: vertical;
45
+ min-height: 60px;
46
+ }
47
+
48
+ .sf-select {
49
+ appearance: auto;
50
+ cursor: pointer;
51
+ }
52
+
53
+ .sf-checkbox {
54
+ margin-right: 6px;
55
+ accent-color: var(--primary, #79aec8);
56
+ }
57
+
58
+ .sf-checkbox-label {
59
+ display: flex;
60
+ align-items: center;
61
+ font-weight: normal;
62
+ cursor: pointer;
63
+ padding: 6px 0;
64
+ }
65
+
66
+ .sf-btn {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ gap: 4px;
70
+ padding: 4px 12px;
71
+ background: var(--object-tools-bg, #888);
72
+ color: var(--object-tools-fg, #fff);
73
+ font-weight: 400;
74
+ font-size: 0.6875rem;
75
+ text-transform: uppercase;
76
+ letter-spacing: 0.5px;
77
+ border-radius: 15px;
78
+ border: none;
79
+ cursor: pointer;
80
+ transition: background-color 0.15s;
81
+
82
+ &:hover {
83
+ background-color: var(--object-tools-hover-bg, #666);
84
+ }
85
+
86
+ i {
87
+ font-size: 0.6rem;
88
+ }
89
+ }
90
+
91
+ .sf-btn-sm {
92
+ padding: 2px 8px;
93
+ font-size: 0.625rem;
94
+ }
95
+
96
+ .sf-btn-add {
97
+ background: var(--object-tools-bg, #888);
98
+ color: var(--object-tools-fg, #fff);
99
+ }
100
+
101
+ .sf-btn-danger {
102
+ background: var(--delete-button-bg, #ba2121);
103
+ color: var(--delete-button-fg, #fff);
104
+
105
+ &:hover {
106
+ background: #a41515;
107
+ }
108
+ }
109
+
110
+ .sf-error {
111
+ color: var(--error-fg, #ba2121);
112
+ font-size: 0.75rem;
113
+ margin-top: 2px;
114
+ }
115
+
116
+ .errors {
117
+ .sf-input {
118
+ border-color: var(--error-fg, #ba2121);
119
+ }
120
+ }
121
+
122
+ .errorlist {
123
+ margin: 4px 0 0 0;
124
+ padding: 0;
125
+ list-style: none;
126
+ color: var(--error-fg, #ba2121);
127
+ font-size: 0.75rem;
128
+
129
+ li {
130
+ padding: 2px 0;
131
+ }
132
+ }
133
+ }
134
+
135
+ .form-row {
136
+ overflow: visible !important;
137
+ }
@@ -0,0 +1,134 @@
1
+ .structured-field-editor {
2
+
3
+ // --- Relation field ---
4
+ .sf-relation {
5
+ position: relative;
6
+ }
7
+
8
+ .sf-relation-wrapper {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 6px;
12
+ }
13
+
14
+ .sf-relation-selected {
15
+ display: flex;
16
+ flex-wrap: wrap;
17
+ gap: 6px;
18
+
19
+ &:empty {
20
+ display: none;
21
+ }
22
+ }
23
+
24
+ .sf-relation-tag {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ gap: 6px;
28
+ padding: 4px 10px;
29
+ background: var(--darkened-bg, #f0f0f0);
30
+ border: 1px solid var(--border-color, #ccc);
31
+ border-radius: 4px;
32
+ font-size: 0.8125rem;
33
+ color: var(--body-fg, #333);
34
+ max-width: 100%;
35
+ }
36
+
37
+ .sf-relation-tag-text {
38
+ overflow: hidden;
39
+ text-overflow: ellipsis;
40
+ white-space: nowrap;
41
+ }
42
+
43
+ .sf-relation-tag-remove {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ width: 18px;
48
+ height: 18px;
49
+ padding: 0;
50
+ border: none;
51
+ border-radius: 50%;
52
+ background: transparent;
53
+ color: var(--body-quiet-color, #666);
54
+ cursor: pointer;
55
+ font-size: 0.6875rem;
56
+ flex-shrink: 0;
57
+ transition: color 0.15s, background 0.15s;
58
+
59
+ &:hover {
60
+ color: var(--error-fg, #ba2121);
61
+ background: rgba(186, 33, 33, 0.1);
62
+ }
63
+ }
64
+
65
+ // --- Search input ---
66
+ .sf-relation-search {
67
+ position: relative;
68
+ }
69
+
70
+ .sf-relation-input {
71
+ padding-left: 28px;
72
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
73
+ background-repeat: no-repeat;
74
+ background-position: 8px center;
75
+ background-size: 14px;
76
+ }
77
+
78
+ // --- Dropdown ---
79
+ .sf-relation-dropdown {
80
+ position: absolute;
81
+ top: 100%;
82
+ left: 0;
83
+ right: 0;
84
+ z-index: 1000;
85
+ max-height: 240px;
86
+ overflow-y: auto;
87
+ background: var(--body-bg, #fff);
88
+ border: 1px solid var(--border-color, #ccc);
89
+ border-top: none;
90
+ border-radius: 0 0 4px 4px;
91
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
92
+ }
93
+
94
+ .sf-relation-dropdown-item {
95
+ padding: 8px 12px;
96
+ cursor: pointer;
97
+ font-size: 0.8125rem;
98
+ color: var(--body-fg, #333);
99
+ transition: background 0.1s;
100
+ border-bottom: 1px solid var(--hairline-color, #eee);
101
+
102
+ &:last-child {
103
+ border-bottom: none;
104
+ }
105
+
106
+ &:hover,
107
+ &.highlighted {
108
+ background: var(--selected-bg, #e4e4e4);
109
+ }
110
+ }
111
+
112
+ .sf-relation-dropdown-empty {
113
+ padding: 12px;
114
+ text-align: center;
115
+ color: var(--body-quiet-color, #999);
116
+ font-size: 0.8125rem;
117
+ font-style: italic;
118
+ }
119
+
120
+ .sf-relation-dropdown-more {
121
+ padding: 8px 12px;
122
+ text-align: center;
123
+ cursor: pointer;
124
+ font-size: 0.75rem;
125
+ color: var(--link-fg, #417690);
126
+ font-weight: 600;
127
+ border-top: 1px solid var(--hairline-color, #eee);
128
+ transition: background 0.1s;
129
+
130
+ &:hover {
131
+ background: var(--darkened-bg, #f0f0f0);
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,3 @@
1
+ @import './components/form.scss';
2
+ @import './components/editors.scss';
3
+ @import './components/relation.scss';
package/src/utils.js ADDED
@@ -0,0 +1,38 @@
1
+ export function debounce(fn, delay) {
2
+ let timer;
3
+ return function (...args) {
4
+ clearTimeout(timer);
5
+ timer = setTimeout(() => fn.apply(this, args), delay);
6
+ };
7
+ }
8
+
9
+ export function deepClone(obj) {
10
+ if (obj === null || typeof obj !== 'object') return obj;
11
+ if (Array.isArray(obj)) return obj.map(deepClone);
12
+ const clone = {};
13
+ for (const key of Object.keys(obj)) {
14
+ clone[key] = deepClone(obj[key]);
15
+ }
16
+ return clone;
17
+ }
18
+
19
+ export function getDefaultForSchema(schema) {
20
+ if ('default' in schema) return deepClone(schema.default);
21
+ if (schema.type === 'object') {
22
+ const obj = {};
23
+ for (const [key, prop] of Object.entries(schema.properties || {})) {
24
+ if ('default' in prop) obj[key] = deepClone(prop.default);
25
+ else if (prop.type === 'string') obj[key] = '';
26
+ else if (prop.type === 'integer' || prop.type === 'number') obj[key] = 0;
27
+ else if (prop.type === 'boolean') obj[key] = false;
28
+ else if (prop.type === 'array') obj[key] = [];
29
+ }
30
+ return obj;
31
+ }
32
+ if (schema.type === 'array') return [];
33
+ if (schema.type === 'string') return '';
34
+ if (schema.type === 'integer' || schema.type === 'number') return 0;
35
+ if (schema.type === 'boolean') return false;
36
+ if (schema.type === 'relation') return schema.multiple ? [] : null;
37
+ return null;
38
+ }