@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bnznamco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @structured-field/widget-editor
2
+
3
+ A Vue-powered JSON Schema form builder with first-class support for relation fields (ForeignKey, QuerySet) and autocomplete search. Ships as both Vue components and a Web Component custom element.
4
+
5
+ Built for the [django-structured-json-field](https://github.com/bnznamco/django-structured-field) admin widget, but usable standalone with any JSON Schema + REST search endpoint.
6
+
7
+ ## Features
8
+
9
+ - **JSON Schema driven** — renders forms from standard JSON Schema (`$ref`, `$defs`, `anyOf`, `oneOf`, `discriminator`)
10
+ - **Relation fields** — ForeignKey (single) and QuerySet (multi) with AJAX autocomplete from a search endpoint
11
+ - **Discriminated unions** — type selector that swaps sub-forms based on a discriminator property
12
+ - **Nullable fields** — togglable Add/Remove for optional nested objects
13
+ - **Array fields** — ordered list with add, remove, and reorder controls
14
+ - **Built with Vue 3** — internally uses Vue SFC components, exported as a Web Component custom element
15
+ - **Theme integration** — styles via CSS custom properties (ships with Django admin compatibility)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @structured-field/widget-editor
21
+ # or
22
+ pnpm add @structured-field/widget-editor
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Web Component (custom element)
28
+
29
+ Register the `<schema-form>` custom element, then use it in any HTML page or framework:
30
+
31
+ ```js
32
+ import { registerCustomElement } from '@structured-field/widget-editor';
33
+ import '@structured-field/widget-editor/css';
34
+
35
+ registerCustomElement(); // registers <schema-form>
36
+ ```
37
+
38
+ #### Programmatic initialization (recommended)
39
+
40
+ ```js
41
+ const el = document.createElement('schema-form');
42
+
43
+ el.schema = {
44
+ type: 'object',
45
+ properties: {
46
+ name: { type: 'string' },
47
+ age: { type: 'integer' },
48
+ },
49
+ };
50
+
51
+ el.initialData = { name: 'Ada', age: 36 };
52
+
53
+ el.addEventListener('change', (e) => {
54
+ console.log('Form value:', e.detail);
55
+ });
56
+
57
+ document.getElementById('my-form').appendChild(el);
58
+
59
+ // Get current value at any time
60
+ const value = el.getValue();
61
+ ```
62
+
63
+ #### Declarative HTML
64
+
65
+ ```html
66
+ <schema-form
67
+ schema='{"type":"object","properties":{"name":{"type":"string"}}}'
68
+ initial-data='{"name":"Ada"}'
69
+ ></schema-form>
70
+
71
+ <script>
72
+ document.querySelector('schema-form').addEventListener('change', (e) => {
73
+ console.log(e.detail);
74
+ });
75
+ </script>
76
+ ```
77
+
78
+ ### As a Vue component
79
+
80
+ Import `SchemaForm` directly for use inside Vue applications:
81
+
82
+ ```vue
83
+ <template>
84
+ <SchemaForm :schema="schema" :initial-data="data" @change="onFormChange" />
85
+ </template>
86
+
87
+ <script setup>
88
+ import { SchemaForm } from '@structured-field/widget-editor';
89
+ import '@structured-field/widget-editor/css';
90
+
91
+ const schema = { /* JSON Schema */ };
92
+ const data = { /* initial form data */ };
93
+
94
+ function onFormChange(value) {
95
+ console.log('Form value:', value);
96
+ }
97
+ </script>
98
+ ```
99
+
100
+ ### As an IIFE (script tag)
101
+
102
+ #### All-in-one bundle (recommended)
103
+
104
+ A single `<script>` tag that includes both JS and CSS — no separate stylesheet needed:
105
+
106
+ ```html
107
+ <!-- Latest version -->
108
+ <script src="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.iife.js"></script>
109
+
110
+ <!-- Pinned version -->
111
+ <script src="https://bnznamco.github.io/structured-widget-editor/v1.0.0/structured-widget-editor.iife.js"></script>
112
+ ```
113
+
114
+ #### Separate JS + CSS
115
+
116
+ ```html
117
+ <!-- Latest version -->
118
+ <link rel="stylesheet" href="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.css">
119
+ <script src="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.js"></script>
120
+
121
+ <!-- Pinned version -->
122
+ <link rel="stylesheet" href="https://bnznamco.github.io/structured-widget-editor/v1.0.0/structured-widget-editor.css">
123
+ <script src="https://bnznamco.github.io/structured-widget-editor/v1.0.0/structured-widget-editor.js"></script>
124
+ ```
125
+
126
+ #### Via jsDelivr (from npm)
127
+
128
+ ```html
129
+ <script src="https://cdn.jsdelivr.net/npm/@structured-field/widget-editor@latest/dist/structured-widget-editor.iife.js"></script>
130
+ <!-- or pinned -->
131
+ <script src="https://cdn.jsdelivr.net/npm/@structured-field/widget-editor@1.0.0/dist/structured-widget-editor.iife.js"></script>
132
+ ```
133
+
134
+ #### Usage
135
+
136
+ ```html
137
+ <script src="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.iife.js"></script>
138
+ <script>
139
+ StructuredWidgetEditor.registerCustomElement();
140
+
141
+ const el = document.createElement('schema-form');
142
+ el.schema = { /* JSON Schema */ };
143
+ el.initialData = { /* data */ };
144
+ el.addEventListener('change', (e) => console.log(e.detail));
145
+ document.getElementById('my-form').appendChild(el);
146
+ </script>
147
+ ```
148
+
149
+ ## Relation Fields
150
+
151
+ The editor supports a custom `type: "relation"` schema extension for ForeignKey and QuerySet fields:
152
+
153
+ ```json
154
+ {
155
+ "type": "relation",
156
+ "format": "select2",
157
+ "model": "app.ModelName",
158
+ "multiple": false,
159
+ "options": {
160
+ "select2": {
161
+ "placeholder": "Search...",
162
+ "allowClear": true,
163
+ "ajax": {
164
+ "url": "/api/search/app.ModelName/"
165
+ }
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ The search endpoint should return:
172
+
173
+ ```json
174
+ {
175
+ "items": [
176
+ { "id": 1, "name": "Item 1", "model": "app.modelname" },
177
+ { "id": 2, "name": "Item 2", "model": "app.modelname" }
178
+ ],
179
+ "more": false
180
+ }
181
+ ```
182
+
183
+ Query parameters sent: `_q` (search term), `page` (pagination).
184
+
185
+ ## Editors
186
+
187
+ | Schema type | Editor | Description |
188
+ |---|---|---|
189
+ | `string` | StringEditor | Text input or textarea |
190
+ | `integer` / `number` | NumberEditor | Number input with step |
191
+ | `boolean` | BooleanEditor | Checkbox |
192
+ | `enum` | SelectEditor | Dropdown select |
193
+ | `const` | HiddenEditor | Hidden (not rendered) |
194
+ | `object` | ObjectEditor | Nested fieldset |
195
+ | `array` | ArrayEditor | List with add/remove/reorder |
196
+ | `anyOf [T, null]` | NullableEditor | Togglable wrapper |
197
+ | `oneOf + discriminator` | UnionEditor | Type selector |
198
+ | `relation` | RelationEditor | Autocomplete with search |
199
+
200
+ ## Theming
201
+
202
+ All styles use CSS custom properties with sensible defaults. Override them to match your design system:
203
+
204
+ ```css
205
+ :root {
206
+ --body-bg: #fff;
207
+ --body-fg: #333;
208
+ --border-color: #ccc;
209
+ --primary: #79aec8;
210
+ --error-fg: #ba2121;
211
+ --darkened-bg: #f0f0f0;
212
+ --object-tools-bg: #888;
213
+ --object-tools-fg: #fff;
214
+ /* ... */
215
+ }
216
+ ```
217
+
218
+ ## Development
219
+
220
+ ```bash
221
+ pnpm install
222
+ pnpm run dev # watch mode
223
+ pnpm run build # production build
224
+ ```
225
+
226
+ ## License
227
+
228
+ MIT
@@ -0,0 +1 @@
1
+ .structured-field-editor{width:100%;border-radius:4px;font-size:.8125rem}.structured-field-editor *,.structured-field-editor *::before,.structured-field-editor *::after{box-sizing:border-box}.structured-field-editor .sf-label{display:block;font-weight:600;margin-bottom:4px;text-transform:capitalize;color:var(--body-quiet-color, #666);font-size:.8125rem}.structured-field-editor .sf-label.required::after{content:" *";color:var(--error-fg, #ba2121)}.structured-field-editor .sf-input{display:block;width:100%;padding:6px 8px;border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--body-bg, #fff);color:var(--body-fg, #333);font-size:.8125rem;font-family:inherit;transition:border-color .15s}.structured-field-editor .sf-input:focus{outline:none;border-color:var(--primary, #79aec8);box-shadow:0 0 0 2px rgba(121,174,200,.2)}.structured-field-editor .sf-textarea{resize:vertical;min-height:60px}.structured-field-editor .sf-select{appearance:auto;cursor:pointer}.structured-field-editor .sf-checkbox{margin-right:6px;accent-color:var(--primary, #79aec8)}.structured-field-editor .sf-checkbox-label{display:flex;align-items:center;font-weight:normal;cursor:pointer;padding:6px 0}.structured-field-editor .sf-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff);font-weight:400;font-size:.6875rem;text-transform:uppercase;letter-spacing:.5px;border-radius:15px;border:none;cursor:pointer;transition:background-color .15s}.structured-field-editor .sf-btn:hover{background-color:var(--object-tools-hover-bg, #666)}.structured-field-editor .sf-btn i{font-size:.6rem}.structured-field-editor .sf-btn-sm{padding:2px 8px;font-size:.625rem}.structured-field-editor .sf-btn-add{background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff)}.structured-field-editor .sf-btn-danger{background:var(--delete-button-bg, #ba2121);color:var(--delete-button-fg, #fff)}.structured-field-editor .sf-btn-danger:hover{background:#a41515}.structured-field-editor .sf-error{color:var(--error-fg, #ba2121);font-size:.75rem;margin-top:2px}.structured-field-editor .errors .sf-input{border-color:var(--error-fg, #ba2121)}.structured-field-editor .errorlist{margin:4px 0 0 0;padding:0;list-style:none;color:var(--error-fg, #ba2121);font-size:.75rem}.structured-field-editor .errorlist li{padding:2px 0}.form-row{overflow:visible !important}.structured-field-editor .sf-field{margin-bottom:12px}.structured-field-editor .sf-field-boolean{margin-bottom:8px}.structured-field-editor .sf-object:not(.sf-object-root){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-bottom:12px;background:var(--body-bg, #fff)}.structured-field-editor .sf-object-title{font-weight:600;font-size:.8125rem;color:var(--body-fg, #333);text-transform:capitalize;padding:0 4px}.structured-field-editor .sf-object-fields{display:flex;flex-direction:column}.structured-field-editor .sf-array{margin-bottom:12px}.structured-field-editor .sf-array-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.structured-field-editor .sf-array-header .sf-label{margin-bottom:0}.structured-field-editor .sf-array-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;background:var(--darkened-bg, #f0f0f0);border-radius:10px;font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-items{display:flex;flex-direction:column;gap:8px}.structured-field-editor .sf-array-item{border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--darkened-bg, #f8f8f8);overflow:hidden}.structured-field-editor .sf-array-item-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:var(--darkened-bg, #f0f0f0);border-bottom:1px solid var(--border-color, #ccc)}.structured-field-editor .sf-array-item-index{font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-item-actions{display:flex;gap:4px}.structured-field-editor .sf-array-item-body{padding:10px}.structured-field-editor .sf-array-item-body>.sf-object.sf-object-root,.structured-field-editor .sf-array-item-body>.sf-object:not(.sf-object-root){border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-nullable{margin-bottom:12px}.structured-field-editor .sf-nullable-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.structured-field-editor .sf-nullable-header .sf-label{margin-bottom:0}.structured-field-editor .sf-nullable-body{padding-left:0}.structured-field-editor .sf-nullable-body:not(:empty){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:4px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union{margin-bottom:12px}.structured-field-editor .sf-union-body{border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:8px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union-body>.sf-object{border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-relation{position:relative}.structured-field-editor .sf-relation-wrapper{display:flex;flex-direction:column;gap:6px}.structured-field-editor .sf-relation-selected{display:flex;flex-wrap:wrap;gap:6px}.structured-field-editor .sf-relation-selected:empty{display:none}.structured-field-editor .sf-relation-tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:var(--darkened-bg, #f0f0f0);border:1px solid var(--border-color, #ccc);border-radius:4px;font-size:.8125rem;color:var(--body-fg, #333);max-width:100%}.structured-field-editor .sf-relation-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.structured-field-editor .sf-relation-tag-remove{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;border-radius:50%;background:rgba(0,0,0,0);color:var(--body-quiet-color, #666);cursor:pointer;font-size:.6875rem;flex-shrink:0;transition:color .15s,background .15s}.structured-field-editor .sf-relation-tag-remove:hover{color:var(--error-fg, #ba2121);background:rgba(186,33,33,.1)}.structured-field-editor .sf-relation-search{position:relative}.structured-field-editor .sf-relation-input{padding-left:28px;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");background-repeat:no-repeat;background-position:8px center;background-size:14px}.structured-field-editor .sf-relation-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;max-height:240px;overflow-y:auto;background:var(--body-bg, #fff);border:1px solid var(--border-color, #ccc);border-top:none;border-radius:0 0 4px 4px;box-shadow:0 4px 12px rgba(0,0,0,.1)}.structured-field-editor .sf-relation-dropdown-item{padding:8px 12px;cursor:pointer;font-size:.8125rem;color:var(--body-fg, #333);transition:background .1s;border-bottom:1px solid var(--hairline-color, #eee)}.structured-field-editor .sf-relation-dropdown-item:last-child{border-bottom:none}.structured-field-editor .sf-relation-dropdown-item:hover,.structured-field-editor .sf-relation-dropdown-item.highlighted{background:var(--selected-bg, #e4e4e4)}.structured-field-editor .sf-relation-dropdown-empty{padding:12px;text-align:center;color:var(--body-quiet-color, #999);font-size:.8125rem;font-style:italic}.structured-field-editor .sf-relation-dropdown-more{padding:8px 12px;text-align:center;cursor:pointer;font-size:.75rem;color:var(--link-fg, #417690);font-weight:600;border-top:1px solid var(--hairline-color, #eee);transition:background .1s}.structured-field-editor .sf-relation-dropdown-more:hover{background:var(--darkened-bg, #f0f0f0)}