pydantic-ui 0.1.5__tar.gz

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,54 @@
1
+ frontend/coverage/*
2
+ examples/basic/user_input/*
3
+ # Python
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+ *.so
8
+ .Python
9
+ build/
10
+ develop-eggs/
11
+ dist/
12
+ downloads/
13
+ eggs/
14
+ .eggs/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ ENV/
26
+ env/
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Node
35
+ node_modules/
36
+ frontend/dist/
37
+
38
+ # Build artifacts (but keep the static folder structure)
39
+ pydantic_ui/static/*
40
+ !pydantic_ui/static/.gitkeep
41
+
42
+ # OS
43
+ .DS_Store
44
+ Thumbs.db
45
+
46
+ # Test
47
+ .pytest_cache/
48
+ .coverage
49
+ htmlcov/
50
+
51
+ # Misc
52
+ *.log
53
+ .env
54
+ .env.local
@@ -0,0 +1,623 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydantic-ui
3
+ Version: 0.1.5
4
+ Summary: Dynamic UI for editing deeply nested Pydantic models
5
+ Project-URL: Homepage, https://github.com/idling-mind/pydantic-ui
6
+ Project-URL: Documentation, https://github.com/idling-mind/pydantic-ui#readme
7
+ Project-URL: Repository, https://github.com/idling-mind/pydantic-ui
8
+ Project-URL: Issues, https://github.com/idling-mind/pydantic-ui/issues
9
+ Author-email: Najeem Muhammed <najeem@gmail.com>
10
+ License-Expression: MIT
11
+ Keywords: editor,fastapi,form,pydantic,react,ui
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Framework :: Pydantic :: 2
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: fastapi>=0.100.0
26
+ Requires-Dist: pydantic>=2.0.0
27
+ Requires-Dist: uvicorn>=0.23.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: coverage>=7.4.0; extra == 'dev'
30
+ Requires-Dist: faker>=22.0.0; extra == 'dev'
31
+ Requires-Dist: httpx>=0.27.0; extra == 'dev'
32
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
35
+ Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
36
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
37
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # Pydantic UI
41
+
42
+ A dynamic, modern UI for editing deeply nested Pydantic models with FastAPI integration.
43
+
44
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
45
+ ![Python](https://img.shields.io/badge/python-3.10+-blue.svg)
46
+ ![FastAPI](https://img.shields.io/badge/fastapi-0.100+-green.svg)
47
+ ![Pydantic](https://img.shields.io/badge/pydantic-v2-green.svg)
48
+
49
+ ![Screenshot](./screenshot.png)
50
+
51
+ ## Features
52
+
53
+ - 🌳 **Tree Navigation**: Sidebar showing hierarchical structure of nested models
54
+ - 🎨 **Dynamic Renderers**: Auto-detect appropriate input components based on field types
55
+ - ⚙️ **Customizable**: Override default renderers with sliders, dropdowns, date pickers, etc.
56
+ - 🌓 **Theme Support**: Light and dark mode with system preference detection
57
+ - ✅ **Validation**: Real-time validation using Pydantic validators
58
+ - 📦 **Easy Integration**: Mount as a FastAPI router in your existing application
59
+ - 🔘 **Action Buttons**: Custom action buttons with Python callbacks
60
+ - 📡 **Real-time Updates**: Server-Sent Events (SSE) for live UI updates
61
+ - 💾 **Session Management**: Per-user session state with data persistence
62
+ - 📋 **Copy/Paste**: Clipboard support for tree nodes
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install pydantic-ui
68
+ ```
69
+
70
+ ## Quick Start
71
+
72
+ ```python
73
+ from fastapi import FastAPI
74
+ from pydantic import BaseModel, Field
75
+ from typing import Annotated
76
+ from pydantic_ui import create_pydantic_ui, FieldConfig, Renderer
77
+
78
+ # Define your Pydantic model
79
+ class Address(BaseModel):
80
+ street: str
81
+ city: str
82
+ zipcode: str
83
+
84
+ class Person(BaseModel):
85
+ name: str = Field(min_length=1, max_length=100)
86
+ age: Annotated[int, FieldConfig(
87
+ renderer=Renderer.SLIDER,
88
+ props={"min": 0, "max": 120}
89
+ )]
90
+ email: str
91
+ address: Address
92
+ tags: list[str] = []
93
+
94
+ # Create FastAPI app and mount pydantic-ui
95
+ app = FastAPI()
96
+
97
+ app.include_router(
98
+ create_pydantic_ui(Person, prefix="/editor"),
99
+ )
100
+
101
+ if __name__ == "__main__":
102
+ import uvicorn
103
+ uvicorn.run(app, host="0.0.0.0", port=8000)
104
+ ```
105
+
106
+ Then visit `http://localhost:8000/editor` to see the UI.
107
+
108
+ ## Public API
109
+
110
+ The package exports the following from `pydantic_ui`:
111
+
112
+ | Export | Description |
113
+ |--------|-------------|
114
+ | `create_pydantic_ui` | Factory function to create a FastAPI router for a Pydantic model |
115
+ | `UIConfig` | Global UI configuration class |
116
+ | `FieldConfig` | Per-field UI configuration class |
117
+ | `Renderer` | Enum of available field renderers |
118
+ | `ActionButton` | Configuration for custom action buttons |
119
+ | `PydanticUIController` | Controller for programmatic UI interaction |
120
+
121
+ ## UI Configuration
122
+
123
+ ### Global Configuration (`UIConfig`)
124
+
125
+ All available options for `UIConfig`:
126
+
127
+ ```python
128
+ from pydantic_ui import create_pydantic_ui, UIConfig
129
+
130
+ ui_config = UIConfig(
131
+ # Basic Settings
132
+ title="Data Editor", # Title shown in header (default: "Data Editor")
133
+ description="", # Description below title
134
+
135
+ # Logo/Branding
136
+ logo_text=None, # Short text for logo (e.g., "P", "UI").
137
+ # If not set, uses first letter of title
138
+ logo_url=None, # URL to logo image. Overrides logo_text if set
139
+
140
+ # Theme
141
+ theme="system", # "light", "dark", or "system" (default: "system")
142
+
143
+ # Form Behavior
144
+ read_only=False, # Make entire form read-only (default: False)
145
+ show_validation=True, # Show validation errors (default: True)
146
+ auto_save=False, # Auto-save changes (default: False)
147
+ auto_save_delay=1000, # Delay in ms before auto-saving (default: 1000)
148
+
149
+ # Tree Panel
150
+ collapsible_tree=True, # Allow tree nodes to collapse (default: True)
151
+ show_types=True, # Show type badges in tree (default: True)
152
+
153
+ # Footer
154
+ show_save_reset=False, # Show Save/Reset buttons in footer (default: False)
155
+ footer_text="Powered by Pydantic UI", # Footer text (empty string hides footer)
156
+ footer_url="https://github.com/idling-mind/pydantic-ui", # Footer link URL
157
+
158
+ # Layout
159
+ responsive_columns={ # Responsive column breakpoints
160
+ 640: 1, # 1 column up to 640px
161
+ 1000: 2, # 2 columns from 640-1000px
162
+ 1600: 3 # 3 columns above 1000px
163
+ },
164
+
165
+ # Custom Actions (see Action Buttons section)
166
+ actions=[], # List of ActionButton configurations
167
+ )
168
+
169
+ app.include_router(
170
+ create_pydantic_ui(
171
+ Person,
172
+ ui_config=ui_config,
173
+ prefix="/editor"
174
+ ),
175
+ )
176
+ ```
177
+
178
+ ### Per-Field Configuration (`FieldConfig`)
179
+
180
+ Use `Annotated` with `FieldConfig` to customize individual fields:
181
+
182
+ ```python
183
+ from typing import Annotated
184
+ from pydantic_ui import FieldConfig, Renderer
185
+
186
+ class Settings(BaseModel):
187
+ # Slider for numeric values
188
+ volume: Annotated[int, FieldConfig(
189
+ renderer=Renderer.SLIDER,
190
+ label="Volume Level", # Custom label (defaults to field name)
191
+ help_text="Adjust the volume", # Help text below field (alias: description)
192
+ placeholder="Enter value", # Placeholder text
193
+ props={"min": 0, "max": 100, "step": 5} # Renderer-specific props
194
+ )] = 50
195
+
196
+ # Dropdown for enum-like fields
197
+ theme: Annotated[str, FieldConfig(
198
+ renderer=Renderer.SELECT,
199
+ props={"options": ["light", "dark", "auto"]}
200
+ )] = "auto"
201
+
202
+ # Toggle instead of checkbox
203
+ notifications: Annotated[bool, FieldConfig(
204
+ renderer=Renderer.TOGGLE
205
+ )] = True
206
+
207
+ # Text area for long text
208
+ bio: Annotated[str, FieldConfig(
209
+ renderer=Renderer.TEXT_AREA,
210
+ props={"rows": 5, "placeholder": "Tell us about yourself..."}
211
+ )] = ""
212
+
213
+ # Hidden fields (not shown in UI)
214
+ internal_id: Annotated[str, FieldConfig(hidden=True)]
215
+
216
+ # Read-only fields (visible but not editable)
217
+ created_at: Annotated[str, FieldConfig(read_only=True)]
218
+ ```
219
+
220
+ ### Field Configs via Path (Alternative Method)
221
+
222
+ You can also configure fields by path without using `Annotated`:
223
+
224
+ ```python
225
+ field_configs = {
226
+ # Direct field path
227
+ "server.name": FieldConfig(
228
+ label="Application Name",
229
+ placeholder="Enter your app name",
230
+ ),
231
+
232
+ # Array item fields using [] syntax
233
+ "users.[].age": FieldConfig(
234
+ label="User Age",
235
+ renderer=Renderer.SLIDER,
236
+ props={"min": 0, "max": 120, "step": 1},
237
+ ),
238
+
239
+ # Nested paths
240
+ "database.password": FieldConfig(
241
+ label="Database Password",
242
+ props={"type": "password"},
243
+ ),
244
+ }
245
+
246
+ pydantic_ui_router = create_pydantic_ui(
247
+ model=AppConfig,
248
+ field_configs=field_configs,
249
+ prefix="/config",
250
+ )
251
+ ```
252
+
253
+ ### Available Renderers
254
+
255
+ | Renderer | Enum Value | Description | Props |
256
+ |----------|------------|-------------|-------|
257
+ | Auto | `Renderer.AUTO` | Auto-detect based on type | - |
258
+ | Text Input | `Renderer.TEXT_INPUT` | Single-line text input | `placeholder`, `maxLength` |
259
+ | Text Area | `Renderer.TEXT_AREA` | Multi-line text input | `rows`, `placeholder` |
260
+ | Number Input | `Renderer.NUMBER_INPUT` | Numeric input | `min`, `max`, `step` |
261
+ | Slider | `Renderer.SLIDER` | Range slider | `min`, `max`, `step`, `marks` |
262
+ | Checkbox | `Renderer.CHECKBOX` | Checkbox | - |
263
+ | Toggle | `Renderer.TOGGLE` | Toggle switch | - |
264
+ | Select | `Renderer.SELECT` | Dropdown select | `options` |
265
+ | Multi-Select | `Renderer.MULTI_SELECT` | Multi-select dropdown | `options` |
266
+ | Date Picker | `Renderer.DATE_PICKER` | Date picker | `format` |
267
+ | DateTime Picker | `Renderer.DATETIME_PICKER` | DateTime picker | `format` |
268
+ | Color Picker | `Renderer.COLOR_PICKER` | Color picker | - |
269
+ | File Upload | `Renderer.FILE_UPLOAD` | File upload | - |
270
+ | File Select | `Renderer.FILE_SELECT` | File selector | - |
271
+ | Password | `Renderer.PASSWORD` | Password input | - |
272
+ | Email | `Renderer.EMAIL` | Email input | - |
273
+ | URL | `Renderer.URL` | URL input | - |
274
+
275
+ ## Action Buttons
276
+
277
+ Add custom action buttons to the UI header that trigger Python callbacks:
278
+
279
+ ### Defining Action Buttons
280
+
281
+ ```python
282
+ from pydantic_ui import UIConfig, ActionButton
283
+
284
+ ui_config = UIConfig(
285
+ title="App Settings",
286
+ actions=[
287
+ ActionButton(
288
+ id="validate", # Unique identifier (required)
289
+ label="Validate", # Button label (required)
290
+ variant="secondary", # "default", "secondary", "outline",
291
+ # "ghost", "destructive"
292
+ icon="check-circle", # Lucide icon name (optional)
293
+ tooltip="Run validation", # Tooltip on hover (optional)
294
+ disabled=False, # Whether button is disabled
295
+ confirm=None, # Confirmation message before action
296
+ # If set, shows dialog before triggering
297
+ ),
298
+ ActionButton(
299
+ id="reset",
300
+ label="Reset All",
301
+ variant="destructive",
302
+ icon="refresh-cw",
303
+ confirm="Are you sure you want to reset all settings?"
304
+ ),
305
+ ActionButton(
306
+ id="save",
307
+ label="Save",
308
+ variant="default",
309
+ icon="save",
310
+ ),
311
+ ],
312
+ )
313
+ ```
314
+
315
+ ### Registering Action Handlers
316
+
317
+ ```python
318
+ from pydantic_ui import create_pydantic_ui, PydanticUIController
319
+
320
+ router = create_pydantic_ui(model=AppSettings, ui_config=ui_config, prefix="/settings")
321
+ app.include_router(router)
322
+
323
+ @router.action("validate")
324
+ async def handle_validate(data: dict, controller: PydanticUIController):
325
+ """Handler receives current data and a controller for UI interaction."""
326
+ errors = []
327
+
328
+ # Custom validation logic
329
+ if data.get("environment") == "production" and data.get("server", {}).get("debug"):
330
+ errors.append({
331
+ "path": "server.debug",
332
+ "message": "Debug mode should not be enabled in production"
333
+ })
334
+
335
+ if errors:
336
+ await controller.show_validation_errors(errors)
337
+ await controller.show_toast("Validation failed", "error")
338
+ else:
339
+ await controller.clear_validation_errors()
340
+ await controller.show_toast("All validations passed!", "success")
341
+
342
+ return {"valid": len(errors) == 0}
343
+
344
+ @router.action("save")
345
+ async def handle_save(data: dict, controller: PydanticUIController):
346
+ """Save handler with Pydantic validation."""
347
+ from pydantic import ValidationError
348
+
349
+ try:
350
+ validated = AppSettings.model_validate(data)
351
+ # Save to database, file, etc.
352
+ await controller.show_toast("Settings saved!", "success")
353
+ return {"saved": True}
354
+ except ValidationError as e:
355
+ await controller.show_toast(f"Validation error: {e}", "error")
356
+ return {"saved": False}
357
+ ```
358
+
359
+ ## Controller Methods (`PydanticUIController`)
360
+
361
+ The controller provides methods for programmatic UI interaction:
362
+
363
+ ### Validation Errors
364
+
365
+ ```python
366
+ # Show validation errors
367
+ await controller.show_validation_errors([
368
+ {"path": "users[0].age", "message": "Age must be positive"},
369
+ {"path": "name", "message": "Name is required"}
370
+ ])
371
+
372
+ # Clear all validation errors
373
+ await controller.clear_validation_errors()
374
+ ```
375
+
376
+ ### Toast Notifications
377
+
378
+ ```python
379
+ # Show toast notification
380
+ await controller.show_toast(
381
+ message="Operation completed!",
382
+ type="success", # "success", "error", "warning", "info"
383
+ duration=5000 # ms (0 for persistent)
384
+ )
385
+
386
+ # Broadcast toast to ALL connected sessions
387
+ await controller.broadcast_toast("Server restarting...", "warning")
388
+ ```
389
+
390
+ ### Data Updates
391
+
392
+ ```python
393
+ # Push new data to the UI
394
+ new_data = AppSettings(name="Updated", ...)
395
+ await controller.push_data(new_data) # Accepts BaseModel or dict
396
+
397
+ # Get current data from session
398
+ current_data = controller.get_current_data()
399
+
400
+ # Get validated model instance (returns None if invalid)
401
+ model_instance = controller.get_model_instance()
402
+
403
+ # Tell UI to refresh from server
404
+ await controller.refresh()
405
+
406
+ # Broadcast refresh to all sessions
407
+ await controller.broadcast_refresh()
408
+ ```
409
+
410
+ ### Confirmation Dialogs
411
+
412
+ ```python
413
+ # Request user confirmation (async - waits for response)
414
+ confirmed = await controller.request_confirmation(
415
+ message="Delete all users?",
416
+ title="Confirm Deletion", # Dialog title
417
+ confirm_text="Delete", # Confirm button text
418
+ cancel_text="Cancel", # Cancel button text
419
+ variant="destructive" # "default" or "destructive"
420
+ )
421
+
422
+ if confirmed:
423
+ # User clicked confirm
424
+ delete_all_users()
425
+ ```
426
+
427
+ ## Data Handlers
428
+
429
+ ### Custom Data Loading and Saving
430
+
431
+ Use decorators to set custom data loader/saver:
432
+
433
+ ```python
434
+ from pydantic_ui import create_pydantic_ui
435
+
436
+ router = create_pydantic_ui(Person, prefix="/editor")
437
+ app.include_router(router)
438
+
439
+ @router.data_loader
440
+ async def load_person() -> Person:
441
+ """Load data from your database or file."""
442
+ return await database.get_person(id=1)
443
+
444
+ @router.data_saver
445
+ async def save_person(data: Person) -> None:
446
+ """Save data to your database or file."""
447
+ await database.update_person(id=1, data=data)
448
+ ```
449
+
450
+ ### Initial Data via Parameter
451
+
452
+ ```python
453
+ initial_person = Person(
454
+ name="John Doe",
455
+ age=30,
456
+ email="john@example.com",
457
+ address=Address(street="123 Main St", city="NYC", zipcode="10001"),
458
+ )
459
+
460
+ app.include_router(
461
+ create_pydantic_ui(
462
+ Person,
463
+ initial_data=initial_person,
464
+ prefix="/editor"
465
+ ),
466
+ )
467
+ ```
468
+
469
+ ### Inline Data Loader/Saver
470
+
471
+ ```python
472
+ app.include_router(
473
+ create_pydantic_ui(
474
+ Person,
475
+ data_loader=lambda: load_from_db(),
476
+ data_saver=lambda data: save_to_db(data),
477
+ prefix="/editor"
478
+ ),
479
+ )
480
+ ```
481
+
482
+ ## Factory Function Reference
483
+
484
+ Complete signature for `create_pydantic_ui`:
485
+
486
+ ```python
487
+ def create_pydantic_ui(
488
+ model: type[BaseModel], # The Pydantic model class (required)
489
+ *,
490
+ ui_config: UIConfig | None = None, # Global UI configuration
491
+ field_configs: dict[str, FieldConfig] | None = None, # Per-field configs by path
492
+ initial_data: BaseModel | None = None, # Initial data to populate form
493
+ data_loader: Callable[[], BaseModel | dict] | None = None, # Data loader function
494
+ data_saver: Callable[[BaseModel], None] | None = None, # Data saver function
495
+ prefix: str = "", # URL prefix for router
496
+ ) -> APIRouter:
497
+ ...
498
+ ```
499
+
500
+ The returned router has additional attributes:
501
+
502
+ | Attribute | Description |
503
+ |-----------|-------------|
504
+ | `router.controller` | `PydanticUIController` instance |
505
+ | `@router.action(id)` | Decorator to register action handlers |
506
+ | `@router.data_loader` | Decorator to set custom data loader |
507
+ | `@router.data_saver` | Decorator to set custom data saver |
508
+
509
+ ## API Endpoints
510
+
511
+ When mounted at `/editor`, the following endpoints are available:
512
+
513
+ | Method | Endpoint | Description |
514
+ |--------|----------|-------------|
515
+ | GET | `/editor/` | Serve the React UI |
516
+ | GET | `/editor/api/schema` | Get the model schema |
517
+ | GET | `/editor/api/data` | Get current session data |
518
+ | POST | `/editor/api/data` | Update entire data (validates with Pydantic) |
519
+ | PATCH | `/editor/api/data` | Partial update (path + value) |
520
+ | POST | `/editor/api/validate` | Validate data without saving |
521
+ | GET | `/editor/api/config` | Get UI configuration |
522
+ | GET | `/editor/api/session` | Get or create session ID |
523
+ | GET | `/editor/api/events` | SSE endpoint for real-time events |
524
+ | GET | `/editor/api/events/poll` | Polling fallback for events |
525
+ | POST | `/editor/api/actions/{id}` | Trigger action handler |
526
+ | POST | `/editor/api/confirmation/{id}` | Handle confirmation response |
527
+
528
+ ## Supported Pydantic Types
529
+
530
+ The UI automatically handles these types:
531
+
532
+ | Type | Renderer |
533
+ |------|----------|
534
+ | `str` | Text Input |
535
+ | `int`, `float` | Number Input (Slider if min/max defined) |
536
+ | `bool` | Toggle |
537
+ | `datetime`, `date` | Date/DateTime Picker |
538
+ | `Enum`, `StrEnum` | Select Dropdown |
539
+ | `Literal["a", "b"]` | Select Dropdown |
540
+ | `list[T]` | Array Editor with add/remove |
541
+ | `dict[str, T]` | JSON Editor or Key-Value |
542
+ | `Optional[T]` / `T \| None` | Nullable field |
543
+ | Nested `BaseModel` | Nested object navigation |
544
+
545
+ ## Examples
546
+
547
+ See the [examples](./examples/) directory for complete examples:
548
+
549
+ - **[simple.py](./examples/basic/simple.py)** - Basic usage with field configs
550
+ - **[main.py](./examples/basic/main.py)** - Full configuration example
551
+ - **[callbacks.py](./examples/basic/callbacks.py)** - Action buttons, validation, toasts, confirmations
552
+
553
+ ### Running Examples
554
+
555
+ ```bash
556
+ # Run the simple example
557
+ cd examples/basic
558
+ python simple.py
559
+ # Visit http://localhost:8000/config
560
+
561
+ # Run the callbacks example
562
+ python callbacks.py
563
+ # Visit http://localhost:8000/settings
564
+ ```
565
+
566
+ ## Development
567
+
568
+ ### Setup
569
+
570
+ ```bash
571
+ # Clone the repository
572
+ git clone https://github.com/idling-mind/pydantic-ui.git
573
+ cd pydantic-ui
574
+
575
+ # Install dependencies
576
+ pip install -e ".[dev]"
577
+
578
+ # Install frontend dependencies
579
+ cd frontend
580
+ npm install
581
+ ```
582
+
583
+ ### Running Tests
584
+
585
+ ```bash
586
+ # Backend tests
587
+ pytest
588
+
589
+ # Frontend tests
590
+ cd frontend
591
+ npm test
592
+ ```
593
+
594
+ ### Building
595
+
596
+ ```bash
597
+ # Build frontend and copy to package
598
+ cd frontend
599
+ npm run build:package
600
+
601
+ # Or use the PowerShell script (Windows)
602
+ ./scripts/build-test.ps1
603
+
604
+ # Build Python package
605
+ python -m build
606
+ ```
607
+
608
+ ### Build Script Options
609
+
610
+ ```powershell
611
+ # Full build and run
612
+ ./scripts/build-test.ps1
613
+
614
+ # Skip frontend build (use existing)
615
+ ./scripts/build-test.ps1 -SkipBuild
616
+
617
+ # Run specific example on different port
618
+ ./scripts/build-test.ps1 -Example callbacks -Port 3000 -OpenBrowser
619
+ ```
620
+
621
+ ## License
622
+
623
+ MIT License - see [LICENSE](LICENSE) for details.