@panphora/bevel 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 +16 -0
- package/README.md +226 -0
- package/bevel-fonts.css +16 -0
- package/bevel.css +3075 -0
- package/bevel.js +2126 -0
- package/dist/react/components/activity-feed.d.ts +89 -0
- package/dist/react/components/activity-feed.js +54 -0
- package/dist/react/components/alert-dialog.d.ts +14 -0
- package/dist/react/components/alert-dialog.js +19 -0
- package/dist/react/components/app-shell.d.ts +98 -0
- package/dist/react/components/app-shell.js +86 -0
- package/dist/react/components/app-toolbar.d.ts +19 -0
- package/dist/react/components/app-toolbar.js +22 -0
- package/dist/react/components/avatar-group.d.ts +67 -0
- package/dist/react/components/avatar-group.js +49 -0
- package/dist/react/components/badge-and-pill.d.ts +34 -0
- package/dist/react/components/badge-and-pill.js +12 -0
- package/dist/react/components/breadcrumbs.d.ts +56 -0
- package/dist/react/components/breadcrumbs.js +44 -0
- package/dist/react/components/bulk-bar.d.ts +22 -0
- package/dist/react/components/bulk-bar.js +59 -0
- package/dist/react/components/button.d.ts +15 -0
- package/dist/react/components/button.js +20 -0
- package/dist/react/components/callout-and-hint.d.ts +17 -0
- package/dist/react/components/callout-and-hint.js +8 -0
- package/dist/react/components/checkbox.d.ts +5 -0
- package/dist/react/components/checkbox.js +13 -0
- package/dist/react/components/chip.d.ts +149 -0
- package/dist/react/components/chip.js +29 -0
- package/dist/react/components/code-block.d.ts +50 -0
- package/dist/react/components/code-block.js +77 -0
- package/dist/react/components/code-editor.d.ts +122 -0
- package/dist/react/components/code-editor.js +695 -0
- package/dist/react/components/column-manager.d.ts +28 -0
- package/dist/react/components/column-manager.js +85 -0
- package/dist/react/components/combobox.d.ts +32 -0
- package/dist/react/components/combobox.js +37 -0
- package/dist/react/components/data-grid.d.ts +108 -0
- package/dist/react/components/data-grid.js +159 -0
- package/dist/react/components/data-table.d.ts +2 -0
- package/dist/react/components/data-table.js +6 -0
- package/dist/react/components/date.d.ts +23 -0
- package/dist/react/components/date.js +11 -0
- package/dist/react/components/details.d.ts +13 -0
- package/dist/react/components/details.js +20 -0
- package/dist/react/components/dialog.d.ts +74 -0
- package/dist/react/components/dialog.js +31 -0
- package/dist/react/components/diff-view.d.ts +73 -0
- package/dist/react/components/diff-view.js +68 -0
- package/dist/react/components/documents.d.ts +70 -0
- package/dist/react/components/documents.js +10 -0
- package/dist/react/components/drawer.d.ts +45 -0
- package/dist/react/components/drawer.js +35 -0
- package/dist/react/components/editable-grid-cells.d.ts +32 -0
- package/dist/react/components/editable-grid-cells.js +116 -0
- package/dist/react/components/eyebrow-kicker-lede-pull-quote.d.ts +39 -0
- package/dist/react/components/eyebrow-kicker-lede-pull-quote.js +12 -0
- package/dist/react/components/field.d.ts +32 -0
- package/dist/react/components/field.js +27 -0
- package/dist/react/components/file-tree.d.ts +75 -0
- package/dist/react/components/file-tree.js +247 -0
- package/dist/react/components/file.d.ts +43 -0
- package/dist/react/components/file.js +73 -0
- package/dist/react/components/files-table.d.ts +174 -0
- package/dist/react/components/files-table.js +237 -0
- package/dist/react/components/filter-builder.d.ts +37 -0
- package/dist/react/components/filter-builder.js +109 -0
- package/dist/react/components/form-error-summary.d.ts +16 -0
- package/dist/react/components/form-error-summary.js +49 -0
- package/dist/react/components/icon-button.d.ts +8 -0
- package/dist/react/components/icon-button.js +14 -0
- package/dist/react/components/icon.d.ts +7 -0
- package/dist/react/components/icon.js +15 -0
- package/dist/react/components/image.d.ts +72 -0
- package/dist/react/components/image.js +24 -0
- package/dist/react/components/inline-edit.d.ts +70 -0
- package/dist/react/components/inline-edit.js +149 -0
- package/dist/react/components/input.d.ts +29 -0
- package/dist/react/components/input.js +20 -0
- package/dist/react/components/kanban-board.d.ts +140 -0
- package/dist/react/components/kanban-board.js +374 -0
- package/dist/react/components/layout.d.ts +51 -0
- package/dist/react/components/layout.js +28 -0
- package/dist/react/components/list.d.ts +28 -0
- package/dist/react/components/list.js +6 -0
- package/dist/react/components/masthead-and-verdict.d.ts +60 -0
- package/dist/react/components/masthead-and-verdict.js +8 -0
- package/dist/react/components/menu.d.ts +26 -0
- package/dist/react/components/menu.js +31 -0
- package/dist/react/components/multi-select-picker.d.ts +49 -0
- package/dist/react/components/multi-select-picker.js +20 -0
- package/dist/react/components/note.d.ts +33 -0
- package/dist/react/components/note.js +8 -0
- package/dist/react/components/number-field.d.ts +16 -0
- package/dist/react/components/number-field.js +17 -0
- package/dist/react/components/pagination.d.ts +45 -0
- package/dist/react/components/pagination.js +87 -0
- package/dist/react/components/persistent-action-banner.d.ts +71 -0
- package/dist/react/components/persistent-action-banner.js +39 -0
- package/dist/react/components/popconfirm.d.ts +42 -0
- package/dist/react/components/popconfirm.js +59 -0
- package/dist/react/components/popover.d.ts +16 -0
- package/dist/react/components/popover.js +21 -0
- package/dist/react/components/progress-and-gauge.d.ts +44 -0
- package/dist/react/components/progress-and-gauge.js +19 -0
- package/dist/react/components/radio.d.ts +7 -0
- package/dist/react/components/radio.js +13 -0
- package/dist/react/components/rich-text-toolbar.d.ts +26 -0
- package/dist/react/components/rich-text-toolbar.js +64 -0
- package/dist/react/components/rows-and-card.d.ts +88 -0
- package/dist/react/components/rows-and-card.js +10 -0
- package/dist/react/components/save-status-indicator.d.ts +11 -0
- package/dist/react/components/save-status-indicator.js +33 -0
- package/dist/react/components/segmented-control.d.ts +21 -0
- package/dist/react/components/segmented-control.js +15 -0
- package/dist/react/components/select.d.ts +29 -0
- package/dist/react/components/select.js +29 -0
- package/dist/react/components/selectable-cards.d.ts +30 -0
- package/dist/react/components/selectable-cards.js +29 -0
- package/dist/react/components/settings-rows.d.ts +51 -0
- package/dist/react/components/settings-rows.js +8 -0
- package/dist/react/components/sidebar-nav.d.ts +77 -0
- package/dist/react/components/sidebar-nav.js +41 -0
- package/dist/react/components/skeleton-and-empty.d.ts +51 -0
- package/dist/react/components/skeleton-and-empty.js +14 -0
- package/dist/react/components/slider.d.ts +19 -0
- package/dist/react/components/slider.js +17 -0
- package/dist/react/components/small-charts.d.ts +60 -0
- package/dist/react/components/small-charts.js +76 -0
- package/dist/react/components/sortable.d.ts +30 -0
- package/dist/react/components/sortable.js +85 -0
- package/dist/react/components/split-button.d.ts +38 -0
- package/dist/react/components/split-button.js +43 -0
- package/dist/react/components/split-panes.d.ts +35 -0
- package/dist/react/components/split-panes.js +102 -0
- package/dist/react/components/stat-tiles.d.ts +54 -0
- package/dist/react/components/stat-tiles.js +8 -0
- package/dist/react/components/steps.d.ts +11 -0
- package/dist/react/components/steps.js +13 -0
- package/dist/react/components/switch.d.ts +6 -0
- package/dist/react/components/switch.js +13 -0
- package/dist/react/components/table.d.ts +61 -0
- package/dist/react/components/table.js +6 -0
- package/dist/react/components/tabs.d.ts +7 -0
- package/dist/react/components/tabs.js +13 -0
- package/dist/react/components/tags.d.ts +33 -0
- package/dist/react/components/tags.js +83 -0
- package/dist/react/components/textarea.d.ts +10 -0
- package/dist/react/components/textarea.js +6 -0
- package/dist/react/components/toast.d.ts +35 -0
- package/dist/react/components/toast.js +90 -0
- package/dist/react/components/tooltip-and-key.d.ts +20 -0
- package/dist/react/components/tooltip-and-key.js +20 -0
- package/dist/react/components/tree-view.d.ts +34 -0
- package/dist/react/components/tree-view.js +118 -0
- package/dist/react/components/upload-dropzone.d.ts +92 -0
- package/dist/react/components/upload-dropzone.js +116 -0
- package/dist/react/components/wizard-shell.d.ts +78 -0
- package/dist/react/components/wizard-shell.js +176 -0
- package/dist/react/fonts.css.d.ts +1 -0
- package/dist/react/index.d.ts +76 -0
- package/dist/react/index.js +77 -0
- package/dist/react/integrations/quickcrop.d.ts +58 -0
- package/dist/react/integrations/quickcrop.js +137 -0
- package/dist/react/integrations/richclay.d.ts +43 -0
- package/dist/react/integrations/richclay.js +129 -0
- package/dist/react/internal/classes.d.ts +3 -0
- package/dist/react/internal/classes.js +10 -0
- package/dist/react/internal/close-mark.d.ts +1 -0
- package/dist/react/internal/close-mark.js +10 -0
- package/dist/react/internal/controllable-state.d.ts +9 -0
- package/dist/react/internal/controllable-state.js +22 -0
- package/dist/react/internal/drop-effect.d.ts +28 -0
- package/dist/react/internal/drop-effect.js +8 -0
- package/dist/react/internal/form-reset.d.ts +2 -0
- package/dist/react/internal/form-reset.js +22 -0
- package/dist/react/internal/format.d.ts +1 -0
- package/dist/react/internal/format.js +14 -0
- package/dist/react/internal/link.d.ts +2 -0
- package/dist/react/internal/link.js +8 -0
- package/dist/react/internal/part.d.ts +20 -0
- package/dist/react/internal/part.js +47 -0
- package/dist/react/internal/pointer-session.d.ts +8 -0
- package/dist/react/internal/pointer-session.js +60 -0
- package/dist/react/internal/refs.d.ts +2 -0
- package/dist/react/internal/refs.js +22 -0
- package/dist/react/internal/rich-text.d.ts +28 -0
- package/dist/react/internal/rich-text.js +4 -0
- package/dist/react/internal/table-context.d.ts +39 -0
- package/dist/react/internal/table-context.js +12 -0
- package/dist/react/internal/use-object-url.d.ts +1 -0
- package/dist/react/internal/use-object-url.js +15 -0
- package/dist/react/layout.css +274 -0
- package/dist/react/layout.css.d.ts +1 -0
- package/dist/react/models/filter.d.ts +31 -0
- package/dist/react/models/filter.js +79 -0
- package/dist/react/models/kanban.d.ts +51 -0
- package/dist/react/models/kanban.js +35 -0
- package/dist/react/models/reorder.d.ts +9 -0
- package/dist/react/models/reorder.js +23 -0
- package/dist/react/models/table.d.ts +85 -0
- package/dist/react/models/table.js +144 -0
- package/dist/react/models/tree.d.ts +16 -0
- package/dist/react/models/tree.js +26 -0
- package/dist/react/provider.d.ts +18 -0
- package/dist/react/provider.js +31 -0
- package/dist/react/recipes/context-hypercms.d.ts +17 -0
- package/dist/react/recipes/context-hypercms.js +71 -0
- package/dist/react/recipes/context-quickcrop.d.ts +8 -0
- package/dist/react/recipes/context-quickcrop.js +49 -0
- package/dist/react/theme.css +8004 -0
- package/dist/react/theme.css.d.ts +1 -0
- package/examples/README.md +34 -0
- package/examples/components/activity-feed.html +28 -0
- package/examples/components/app-shell.html +12 -0
- package/examples/components/app-toolbar.html +38 -0
- package/examples/components/avatar-group.html +27 -0
- package/examples/components/breadcrumbs.html +26 -0
- package/examples/components/bulk-bar.html +22 -0
- package/examples/components/code-block.html +12 -0
- package/examples/components/code-editor.html +25 -0
- package/examples/components/column-manager.html +9 -0
- package/examples/components/combobox.html +13 -0
- package/examples/components/context-hypercms.css +41 -0
- package/examples/components/context-hypercms.html +40 -0
- package/examples/components/context-hypercms.js +62 -0
- package/examples/components/context-quickcrop.html +137 -0
- package/examples/components/diff-view.html +9 -0
- package/examples/components/drawer.html +11 -0
- package/examples/components/editable-grid-cells.html +9 -0
- package/examples/components/file-browser.html +53 -0
- package/examples/components/file-tree.html +34 -0
- package/examples/components/files-table.html +80 -0
- package/examples/components/filter-builder.html +11 -0
- package/examples/components/form-error-summary.html +11 -0
- package/examples/components/index.html +51 -0
- package/examples/components/inline-edit.html +42 -0
- package/examples/components/kanban-board.html +42 -0
- package/examples/components/manifest.json +3279 -0
- package/examples/components/modal.html +11 -0
- package/examples/components/multi-select-picker.html +13 -0
- package/examples/components/pagination.html +32 -0
- package/examples/components/persistent-action-banner.html +23 -0
- package/examples/components/popconfirm.html +13 -0
- package/examples/components/popover-menu.html +13 -0
- package/examples/components/reference/baseline/DECISIONS.md +85 -0
- package/examples/components/reference/baseline/README.md +58 -0
- package/examples/components/reference/baseline/bevel-fonts.css +16 -0
- package/examples/components/reference/baseline/bevel.css +1339 -0
- package/examples/components/reference/baseline/bevel.js +496 -0
- package/examples/components/reference/baseline/fonts/LICENSE-ibm-plex-mono.txt +93 -0
- package/examples/components/reference/baseline/fonts/LICENSE-ibm-plex-sans.txt +93 -0
- package/examples/components/reference/baseline/fonts/LICENSE-newsreader.txt +93 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-400-italic.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/examples/components/reference/baseline/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/examples/components/reference/fable/IMPROVEMENTS.md +106 -0
- package/examples/components/reference/fable/PARENT-REPAIRS.md +17 -0
- package/examples/components/reference/fable/__pycache__/build_repairs.cpython-311.pyc +0 -0
- package/examples/components/reference/fable/__pycache__/kanban.cpython-311.pyc +0 -0
- package/examples/components/reference/fable/as-authored/IMPROVEMENTS-completed.md +106 -0
- package/examples/components/reference/fable/as-authored/base-astra.css +17 -0
- package/examples/components/reference/fable/as-authored/base-parent.css +416 -0
- package/examples/components/reference/fable/as-authored/build.py +858 -0
- package/examples/components/reference/fable/as-authored/fable.css +823 -0
- package/examples/components/reference/fable/as-authored/kanban-completed.css +235 -0
- package/examples/components/reference/fable/as-authored/kanban-completed.html +53 -0
- package/examples/components/reference/fable/as-authored/kanban-completed.py +212 -0
- package/examples/components/reference/fable/as-authored/kanban.py +207 -0
- package/examples/components/reference/fable/base-astra.css +17 -0
- package/examples/components/reference/fable/base-parent.css +416 -0
- package/examples/components/reference/fable/build.py +858 -0
- package/examples/components/reference/fable/build_repairs.py +25 -0
- package/examples/components/reference/fable/components/activity-feed.html +3 -0
- package/examples/components/reference/fable/components/app-toolbar.html +3 -0
- package/examples/components/reference/fable/components/avatar-group.html +4 -0
- package/examples/components/reference/fable/components/breadcrumbs.html +3 -0
- package/examples/components/reference/fable/components/code-block.html +9 -0
- package/examples/components/reference/fable/components/column-manager.html +3 -0
- package/examples/components/reference/fable/components/combobox.html +6 -0
- package/examples/components/reference/fable/components/context-hypercms.html +3 -0
- package/examples/components/reference/fable/components/context-quickcrop.html +4 -0
- package/examples/components/reference/fable/components/diff-view.html +13 -0
- package/examples/components/reference/fable/components/drawer.html +3 -0
- package/examples/components/reference/fable/components/editable-grid-cells.html +3 -0
- package/examples/components/reference/fable/components/filter-builder.html +3 -0
- package/examples/components/reference/fable/components/form-error-summary.html +4 -0
- package/examples/components/reference/fable/components/inline-edit.html +3 -0
- package/examples/components/reference/fable/components/kanban-board.html +53 -0
- package/examples/components/reference/fable/components/modal.html +3 -0
- package/examples/components/reference/fable/components/multi-select-picker.html +5 -0
- package/examples/components/reference/fable/components/pagination.html +3 -0
- package/examples/components/reference/fable/components/persistent-action-banner.html +4 -0
- package/examples/components/reference/fable/components/popconfirm.html +5 -0
- package/examples/components/reference/fable/components/popover-menu.html +3 -0
- package/examples/components/reference/fable/components/rich-text-toolbar.html +3 -0
- package/examples/components/reference/fable/components/save-status-indicator.html +2 -0
- package/examples/components/reference/fable/components/selectable-cards.html +3 -0
- package/examples/components/reference/fable/components/settings-rows.html +3 -0
- package/examples/components/reference/fable/components/sidebar-nav.html +4 -0
- package/examples/components/reference/fable/components/small-charts.html +5 -0
- package/examples/components/reference/fable/components/split-button.html +4 -0
- package/examples/components/reference/fable/components/split-panes.html +8 -0
- package/examples/components/reference/fable/components/stat-tiles.html +3 -0
- package/examples/components/reference/fable/components/toast-stack.html +5 -0
- package/examples/components/reference/fable/components/tree-view.html +4 -0
- package/examples/components/reference/fable/components/upload-dropzone.html +6 -0
- package/examples/components/reference/fable/components/wizard-shell.html +3 -0
- package/examples/components/reference/fable/coverage.json +414 -0
- package/examples/components/reference/fable/fable.css +823 -0
- package/examples/components/reference/fable/index.html +5 -0
- package/examples/components/reference/fable/kanban.css +237 -0
- package/examples/components/reference/fable/kanban.py +212 -0
- package/examples/components/reference/fable/parent-repairs.css +9 -0
- package/examples/components/reference/fixtures/data.json +102 -0
- package/examples/components/reference/fixtures/host.js +47 -0
- package/examples/components/reference/fixtures/static-host.js +39 -0
- package/examples/components/reference/fixtures/workspace.jpg +0 -0
- package/examples/components/reference/parent/CHANGES.md +179 -0
- package/examples/components/reference/parent/__pycache__/kanban.cpython-311.pyc +0 -0
- package/examples/components/reference/parent/base-astra.css +17 -0
- package/examples/components/reference/parent/base-parent.css +416 -0
- package/examples/components/reference/parent/build.py +148 -0
- package/examples/components/reference/parent/components/activity-feed.html +2 -0
- package/examples/components/reference/parent/components/app-toolbar.html +2 -0
- package/examples/components/reference/parent/components/avatar-group.html +4 -0
- package/examples/components/reference/parent/components/breadcrumbs.html +2 -0
- package/examples/components/reference/parent/components/code-block.html +8 -0
- package/examples/components/reference/parent/components/column-manager.html +2 -0
- package/examples/components/reference/parent/components/combobox.html +6 -0
- package/examples/components/reference/parent/components/context-hypercms.html +2 -0
- package/examples/components/reference/parent/components/context-quickcrop.html +4 -0
- package/examples/components/reference/parent/components/diff-view.html +4 -0
- package/examples/components/reference/parent/components/drawer.html +2 -0
- package/examples/components/reference/parent/components/editable-grid-cells.html +2 -0
- package/examples/components/reference/parent/components/filter-builder.html +2 -0
- package/examples/components/reference/parent/components/form-error-summary.html +2 -0
- package/examples/components/reference/parent/components/inline-edit.html +2 -0
- package/examples/components/reference/parent/components/kanban-board.html +84 -0
- package/examples/components/reference/parent/components/modal.html +2 -0
- package/examples/components/reference/parent/components/multi-select-picker.html +5 -0
- package/examples/components/reference/parent/components/pagination.html +2 -0
- package/examples/components/reference/parent/components/persistent-action-banner.html +4 -0
- package/examples/components/reference/parent/components/popconfirm.html +5 -0
- package/examples/components/reference/parent/components/popover-menu.html +2 -0
- package/examples/components/reference/parent/components/rich-text-toolbar.html +2 -0
- package/examples/components/reference/parent/components/save-status-indicator.html +2 -0
- package/examples/components/reference/parent/components/selectable-cards.html +2 -0
- package/examples/components/reference/parent/components/settings-rows.html +2 -0
- package/examples/components/reference/parent/components/sidebar-nav.html +4 -0
- package/examples/components/reference/parent/components/small-charts.html +2 -0
- package/examples/components/reference/parent/components/split-button.html +5 -0
- package/examples/components/reference/parent/components/split-panes.html +8 -0
- package/examples/components/reference/parent/components/stat-tiles.html +2 -0
- package/examples/components/reference/parent/components/toast-stack.html +5 -0
- package/examples/components/reference/parent/components/tree-view.html +4 -0
- package/examples/components/reference/parent/components/upload-dropzone.html +6 -0
- package/examples/components/reference/parent/components/wizard-shell.html +2 -0
- package/examples/components/reference/parent/coverage.json +414 -0
- package/examples/components/reference/parent/index.html +1 -0
- package/examples/components/reference/parent/kanban.css +114 -0
- package/examples/components/reference/parent/kanban.py +55 -0
- package/examples/components/reference/parent/refinement.css +53 -0
- package/examples/components/reference/selected/build.py +70 -0
- package/examples/components/reference/selected/components/activity-feed.html +3 -0
- package/examples/components/reference/selected/components/app-toolbar.html +3 -0
- package/examples/components/reference/selected/components/avatar-group.html +4 -0
- package/examples/components/reference/selected/components/breadcrumbs.html +3 -0
- package/examples/components/reference/selected/components/code-block.html +9 -0
- package/examples/components/reference/selected/components/column-manager.html +3 -0
- package/examples/components/reference/selected/components/combobox.html +6 -0
- package/examples/components/reference/selected/components/context-hypercms.html +3 -0
- package/examples/components/reference/selected/components/context-quickcrop.html +4 -0
- package/examples/components/reference/selected/components/diff-view.html +13 -0
- package/examples/components/reference/selected/components/drawer.html +3 -0
- package/examples/components/reference/selected/components/editable-grid-cells.html +3 -0
- package/examples/components/reference/selected/components/filter-builder.html +3 -0
- package/examples/components/reference/selected/components/form-error-summary.html +4 -0
- package/examples/components/reference/selected/components/inline-edit.html +3 -0
- package/examples/components/reference/selected/components/kanban-board.html +53 -0
- package/examples/components/reference/selected/components/modal.html +2 -0
- package/examples/components/reference/selected/components/multi-select-picker.html +5 -0
- package/examples/components/reference/selected/components/pagination.html +3 -0
- package/examples/components/reference/selected/components/persistent-action-banner.html +4 -0
- package/examples/components/reference/selected/components/popconfirm.html +5 -0
- package/examples/components/reference/selected/components/popover-menu.html +3 -0
- package/examples/components/reference/selected/components/rich-text-toolbar.html +3 -0
- package/examples/components/reference/selected/components/save-status-indicator.html +2 -0
- package/examples/components/reference/selected/components/selectable-cards.html +3 -0
- package/examples/components/reference/selected/components/settings-rows.html +3 -0
- package/examples/components/reference/selected/components/sidebar-nav.html +4 -0
- package/examples/components/reference/selected/components/small-charts.html +5 -0
- package/examples/components/reference/selected/components/split-button.html +4 -0
- package/examples/components/reference/selected/components/split-panes.html +8 -0
- package/examples/components/reference/selected/components/stat-tiles.html +3 -0
- package/examples/components/reference/selected/components/toast-stack.html +5 -0
- package/examples/components/reference/selected/components/tree-view.html +4 -0
- package/examples/components/reference/selected/components/upload-dropzone.html +6 -0
- package/examples/components/reference/selected/components/wizard-shell.html +3 -0
- package/examples/components/reference/selected/corrections.css +11 -0
- package/examples/components/reference/selected/coverage.json +413 -0
- package/examples/components/reference/selected/gallery-build.mjs +49 -0
- package/examples/components/reference/selected/gallery-spec.json +12 -0
- package/examples/components/reference/selected/index.html +1 -0
- package/examples/components/reference/selected/kanban-comparison.json +6 -0
- package/examples/components/reference/selected/manifest.json +476 -0
- package/examples/components/reference/sha256.json +142 -0
- package/examples/components/reference/sources/diagonal-close.svg +1 -0
- package/examples/components/reference/sources/hyperclay/public-assets/css/hyperclay-app-source.css +614 -0
- package/examples/components/reference/sources/hyperclay/public-assets/js/app.js +94 -0
- package/examples/components/reference/sources/hyperclayjs/src/ui/theModal.js +897 -0
- package/examples/components/reference/sources/hypercms/scripts/build-theme.js +203 -0
- package/examples/components/reference/sources/hypercms/src/anchor.js +24 -0
- package/examples/components/reference/sources/hypercms/src/apply-loop.js +167 -0
- package/examples/components/reference/sources/hypercms/src/diagnostics.js +44 -0
- package/examples/components/reference/sources/hypercms/src/enhance.js +173 -0
- package/examples/components/reference/sources/hypercms/src/events.js +955 -0
- package/examples/components/reference/sources/hypercms/src/form-builder.js +518 -0
- package/examples/components/reference/sources/hypercms/src/form-rules.js +208 -0
- package/examples/components/reference/sources/hypercms/src/hooks.js +100 -0
- package/examples/components/reference/sources/hypercms/src/hypercms-bundle.js +10 -0
- package/examples/components/reference/sources/hypercms/src/hypercms.js +347 -0
- package/examples/components/reference/sources/hypercms/src/morph.js +10 -0
- package/examples/components/reference/sources/hypercms/src/path.js +61 -0
- package/examples/components/reference/sources/hypercms/src/place.js +131 -0
- package/examples/components/reference/sources/hypercms/src/platform.js +131 -0
- package/examples/components/reference/sources/hypercms/src/refresh.js +87 -0
- package/examples/components/reference/sources/hypercms/src/richclay-bridge.js +140 -0
- package/examples/components/reference/sources/hypercms/src/row-identity.js +111 -0
- package/examples/components/reference/sources/hypercms/src/scaffold.js +10 -0
- package/examples/components/reference/sources/hypercms/src/session.js +303 -0
- package/examples/components/reference/sources/hypercms/src/shell.js +248 -0
- package/examples/components/reference/sources/hypercms/src/styles.css +298 -0
- package/examples/components/reference/sources/hypercms/src/targets.js +144 -0
- package/examples/components/reference/sources/hypercms/src/templates.js +656 -0
- package/examples/components/reference/sources/hypercms/src/theme/pixel-quiet.overrides.css +761 -0
- package/examples/components/reference/sources/hypercms/src/theme.generated.css +1772 -0
- package/examples/components/reference/sources/hypercms/src/toggle.js +666 -0
- package/examples/components/reference/sources/hypercms/src/unresolved.js +211 -0
- package/examples/components/reference/sources/hypercms/src/vendor/mirk.vendor.js +212 -0
- package/examples/components/reference/sources/hypercms/src/views/inline-layer.js +421 -0
- package/examples/components/reference/sources/hypercms/src/views/inline.js +1072 -0
- package/examples/components/reference/sources/hypercms/src/views/sidebar.js +97 -0
- package/examples/components/reference/sources/modal-reference.html +301 -0
- package/examples/components/reference/sources/quickcrop/quickcrop.css +124 -0
- package/examples/components/reference/sources/quickcrop/quickcrop.js +400 -0
- package/examples/components/reference/sources/richclay/richclay.css +416 -0
- package/examples/components/reference/sources/richclay/richclay.js +2962 -0
- package/examples/components/reference/sources/richclay/src/a11y.js +67 -0
- package/examples/components/reference/sources/richclay/src/browser-global.js +7 -0
- package/examples/components/reference/sources/richclay/src/buttons.js +254 -0
- package/examples/components/reference/sources/richclay/src/hyperclay.js +470 -0
- package/examples/components/reference/sources/richclay/src/normalize.js +380 -0
- package/examples/components/reference/sources/richclay/src/richclay.js +1356 -0
- package/examples/components/reference/sources/richclay/src/sanitize.js +189 -0
- package/examples/components/reference/sources/richclay/src/styles.js +23 -0
- package/examples/components/reference/sources/richclay/src/toolbar-float.js +175 -0
- package/examples/components/reference/sources/richclay/src/toolbar.js +394 -0
- package/examples/components/reference/sources/richclay/src/vendor-bundle.js +15 -0
- package/examples/components/reference/sources/richclay/vendor/DOMPURIFY-LICENSE +202 -0
- package/examples/components/reference/sources/richclay/vendor/DOMPURIFY-LICENSE-MPL +373 -0
- package/examples/components/reference/sources/richclay/vendor/SQUIRE-LICENSE +21 -0
- package/examples/components/reference/sources/richclay/vendor/purify.es.mjs +2249 -0
- package/examples/components/reference/sources/richclay/vendor/purify.min.js +3 -0
- package/examples/components/reference/sources/richclay/vendor/squire.js +22 -0
- package/examples/components/reference/sources/richclay/vendor/squire.mjs +22 -0
- package/examples/components/reference/sources/richclay/vendor/squire.provenance.json +19 -0
- package/examples/components/rich-text-toolbar.html +43 -0
- package/examples/components/save-status-indicator.html +11 -0
- package/examples/components/selectable-cards.html +9 -0
- package/examples/components/settings-rows.html +10 -0
- package/examples/components/sidebar-nav.html +9 -0
- package/examples/components/small-charts.html +21 -0
- package/examples/components/split-button.html +13 -0
- package/examples/components/split-panes-editor.html +6 -0
- package/examples/components/split-panes.html +9 -0
- package/examples/components/stat-tiles.html +23 -0
- package/examples/components/toast-stack.html +21 -0
- package/examples/components/tree-view.html +9 -0
- package/examples/components/upload-dropzone.html +88 -0
- package/examples/components/wizard-shell.html +50 -0
- package/examples/datastar/index.html +38 -0
- package/examples/datastar/server.mjs +31 -0
- package/examples/fixtures/field-guide.pdf +14 -0
- package/examples/fixtures/workspace.jpg +0 -0
- package/examples/setup-peers.mjs +35 -0
- package/fonts/LICENSE-ibm-plex-mono.txt +93 -0
- package/fonts/LICENSE-ibm-plex-sans.txt +93 -0
- package/fonts/LICENSE-newsreader.txt +93 -0
- package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-400-italic.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/icons/LICENSE +21 -0
- package/icons/house.json +71 -0
- package/icons/icons.d.ts +17 -0
- package/icons/icons.js +648 -0
- package/icons/icons.svg +126 -0
- package/icons/marks/build-tray.source.mjs +45 -0
- package/icons/marks/hyperclay-local.svg +11 -0
- package/icons/phosphor.json +53 -0
- package/icons/round.json +8 -0
- package/integrations/datastar.js +202 -0
- package/integrations/quickcrop.css +26 -0
- package/integrations/quickcrop.js +56 -0
- package/integrations/richclay.css +100 -0
- package/integrations/richclay.js +131 -0
- package/package.json +630 -0
- package/react/README.md +244 -0
- package/src/README.md +83 -0
- package/vendor/datastar/datastar-1.0.3.js +10 -0
- package/vendor/datastar/provenance.json +6 -0
- package/vendor/floating-ui/LICENSE-core +20 -0
- package/vendor/floating-ui/LICENSE-dom +20 -0
- package/vendor/floating-ui/floating-ui.core.umd.min.js +1 -0
- package/vendor/floating-ui/floating-ui.dom.umd.min.js +1 -0
- package/vendor/floating-ui/provenance.json +17 -0
- package/vendor/richclay/LICENSE +16 -0
- package/vendor/richclay/THIRD-PARTY-NOTICES.md +45 -0
- package/vendor/richclay/provenance.json +17 -0
- package/vendor/richclay/richclay.css +404 -0
- package/vendor/richclay/richclay.js +2940 -0
- package/vendor/richclay/vendor/DOMPURIFY-LICENSE +202 -0
- package/vendor/richclay/vendor/DOMPURIFY-LICENSE-MPL +373 -0
- package/vendor/richclay/vendor/SQUIRE-LICENSE +21 -0
- package/vendor/richclay/vendor/purify.min.js +3 -0
- package/vendor/richclay/vendor/squire.js +22 -0
- package/vendor/richclay/vendor/squire.provenance.json +30 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { engine } from 'hyper-html-api'
|
|
2
|
+
import { applyWithRollback } from './apply-loop.js'
|
|
3
|
+
import { fromString as pathFromString, getRuleAtPath } from './path.js'
|
|
4
|
+
import { buildItem, fileNameFromUrl, radioGroupName } from './form-builder.js'
|
|
5
|
+
import { scaffold } from './scaffold.js'
|
|
6
|
+
import { autosizeTextarea, enhanceFields } from './enhance.js'
|
|
7
|
+
import { platform } from './platform.js'
|
|
8
|
+
|
|
9
|
+
const BOUND = new WeakSet()
|
|
10
|
+
|
|
11
|
+
// Wraps a structural apply in the pause-before / commit-on-success pattern.
|
|
12
|
+
// Pauses the undo recorder, runs fn, then either commits the captured
|
|
13
|
+
// records (if the apply succeeded) or discards them (if it failed). Either
|
|
14
|
+
// way, the undo stack only ever contains successful applies — never an
|
|
15
|
+
// apply+rollback no-op pair.
|
|
16
|
+
//
|
|
17
|
+
// No-op pass-through when window.hyperclay.undo isn't loaded.
|
|
18
|
+
export function commitWithUndo(label, fn) {
|
|
19
|
+
const u = platform('undo')
|
|
20
|
+
if (!u) return fn()
|
|
21
|
+
u.pause()
|
|
22
|
+
try {
|
|
23
|
+
const result = fn()
|
|
24
|
+
if (result && result.ok) {
|
|
25
|
+
u.commitCaptured(label)
|
|
26
|
+
} else {
|
|
27
|
+
u.discardCaptured()
|
|
28
|
+
}
|
|
29
|
+
return result
|
|
30
|
+
} finally {
|
|
31
|
+
u.resume()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Runs fn with the undo recorder paused and its captured records discarded, so
|
|
36
|
+
// editor-chrome-only DOM mutations (e.g. the body class toggles the shell adds
|
|
37
|
+
// on mount/unmount) never enter the undo stack. No-op pass-through when undo
|
|
38
|
+
// isn't loaded.
|
|
39
|
+
export function suppressUndo(fn) {
|
|
40
|
+
const u = platform('undo')
|
|
41
|
+
if (!u) return fn()
|
|
42
|
+
u.pause()
|
|
43
|
+
try {
|
|
44
|
+
return fn()
|
|
45
|
+
} finally {
|
|
46
|
+
u.discardCaptured()
|
|
47
|
+
u.resume()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function bindEvents(ctx) {
|
|
52
|
+
const { formRoot } = ctx
|
|
53
|
+
if (!formRoot || BOUND.has(formRoot)) return
|
|
54
|
+
BOUND.add(formRoot)
|
|
55
|
+
|
|
56
|
+
const onInput = (e) => {
|
|
57
|
+
const target = e.target
|
|
58
|
+
if (!target || !target.closest) return
|
|
59
|
+
if (!target.closest('[data-hcms-form-root]')) return
|
|
60
|
+
if (!target.matches('input, textarea, select, [contenteditable][data-hcms-field]')) return
|
|
61
|
+
if (target.tagName === 'TEXTAREA') autosizeTextarea(target)
|
|
62
|
+
// The upload picker fires `input` too, but its .value is the OS fake path
|
|
63
|
+
// (C:\fakepath\…); it's handled on `change` via onUploadChange. The wrapper
|
|
64
|
+
// carries data-hcms-field, so without this guard the closest() below would
|
|
65
|
+
// route the fake path into onScalarChange.
|
|
66
|
+
if (target.matches('input[type="file"]')) return
|
|
67
|
+
if (!target.closest('[data-hcms-field]') && !target.hasAttribute?.('data-hcms-field')) return
|
|
68
|
+
onScalarChange(target, ctx)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const onChange = (e) => {
|
|
72
|
+
const target = e.target
|
|
73
|
+
if (!target || !target.closest) return
|
|
74
|
+
if (!target.closest('[data-hcms-form-root]')) return
|
|
75
|
+
if (target.matches('input[type="file"][data-hcms-upload]')) {
|
|
76
|
+
onUploadChange(target, ctx)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (target.matches('input[type="checkbox"], input[type="radio"], select')) {
|
|
80
|
+
onScalarChange(target, ctx)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const onClick = (e) => {
|
|
85
|
+
const target = e.target
|
|
86
|
+
if (!target || !target.closest) return
|
|
87
|
+
const actionEl = target.closest('[data-hcms-action]')
|
|
88
|
+
if (!actionEl) return
|
|
89
|
+
const action = actionEl.getAttribute('data-hcms-action')
|
|
90
|
+
// add/remove/move must live inside the form root; close inside the shell.
|
|
91
|
+
// Ignore stray data-hcms-action attributes elsewhere on the page.
|
|
92
|
+
if (action === 'add' || action === 'remove' || action === 'move-up' || action === 'move-down' || action === 'clear-upload') {
|
|
93
|
+
if (!actionEl.closest('[data-hcms-form-root]')) return
|
|
94
|
+
} else if (action === 'close') {
|
|
95
|
+
if (!actionEl.closest('[data-hcms-shell]')) return
|
|
96
|
+
}
|
|
97
|
+
if (action === 'add') {
|
|
98
|
+
const arrayEl = actionEl.closest('[data-hcms-path]')
|
|
99
|
+
if (!arrayEl) return
|
|
100
|
+
const arrayPath = arrayEl.getAttribute('data-hcms-path')
|
|
101
|
+
onAdd(arrayPath, ctx)
|
|
102
|
+
} else if (action === 'remove') {
|
|
103
|
+
const itemEl = actionEl.closest('[data-hcms-card], [data-hcms-array-item]')
|
|
104
|
+
if (!itemEl) return
|
|
105
|
+
requestRemove(itemEl, ctx)
|
|
106
|
+
} else if (action === 'move-up' || action === 'move-down') {
|
|
107
|
+
const itemEl = actionEl.closest('[data-hcms-card], [data-hcms-array-item]')
|
|
108
|
+
if (!itemEl) return
|
|
109
|
+
onMove(itemEl, action === 'move-up' ? -1 : 1, ctx)
|
|
110
|
+
} else if (action === 'clear-upload') {
|
|
111
|
+
onClearUpload(actionEl, ctx)
|
|
112
|
+
} else if (action === 'close') {
|
|
113
|
+
ctx.onCloseRequested?.()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const doc = formRoot.ownerDocument
|
|
118
|
+
doc.addEventListener('input', onInput, true)
|
|
119
|
+
doc.addEventListener('change', onChange, true)
|
|
120
|
+
doc.addEventListener('click', onClick, true)
|
|
121
|
+
|
|
122
|
+
ctx.detachEvents = () => {
|
|
123
|
+
doc.removeEventListener('input', onInput, true)
|
|
124
|
+
doc.removeEventListener('change', onChange, true)
|
|
125
|
+
doc.removeEventListener('click', onClick, true)
|
|
126
|
+
BOUND.delete(formRoot)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Element PROPERTIES a form field projects to that fire NO MutationRecord, so
|
|
131
|
+
// hyper-undo's observer can't see them (an input's live `value`, a checkbox's
|
|
132
|
+
// `checked`). Text projections ARE observed (characterData) and need nothing
|
|
133
|
+
// here. Kept narrow on purpose: the broader engine DOM_PROPERTIES_WRITE_SET
|
|
134
|
+
// includes observable writes (innerHTML/outerHTML) we must NOT double-record.
|
|
135
|
+
const UNOBSERVED_FIELD_PROPS = new Set(['value', 'checked'])
|
|
136
|
+
|
|
137
|
+
// Resolve the page element + property a scalar field projects to, but ONLY for
|
|
138
|
+
// the unobserved-property case above. `pathStr` is the field's FULL path (the
|
|
139
|
+
// scalar field element carries data-hcms-path = full path, e.g. "name" or
|
|
140
|
+
// "products.0.name"). Returns { el, prop, oldValue } captured from the live page
|
|
141
|
+
// BEFORE the apply, or null when the projection is text, targets an array item
|
|
142
|
+
// (the engine's ctx isn't pageRoot then), or can't be resolved.
|
|
143
|
+
function resolveUnobservedProjection(ctx, pathStr) {
|
|
144
|
+
if (!pathStr) return null
|
|
145
|
+
const fullPath = pathFromString(pathStr)
|
|
146
|
+
// Array-item fields project under a per-item engine ctx, not pageRoot. Resolving
|
|
147
|
+
// pageRoot.querySelector(selector) would grab the wrong item, so skip — the edit
|
|
148
|
+
// simply keeps today's no-undo behavior rather than recording a wrong revert.
|
|
149
|
+
if (fullPath.some((seg) => typeof seg === 'number' || seg === '*')) return null
|
|
150
|
+
const rule = getRuleAtPath(ctx.pageRules, fullPath)
|
|
151
|
+
if (typeof rule !== 'string') return null
|
|
152
|
+
const at = engine.ruleAttrIndex(rule)
|
|
153
|
+
if (at === -1) return null // text projection — observed
|
|
154
|
+
const prop = rule.slice(at + 1)
|
|
155
|
+
if (!UNOBSERVED_FIELD_PROPS.has(prop)) return null
|
|
156
|
+
const selector = rule.slice(0, at)
|
|
157
|
+
const el = selector ? ctx.pageRoot.querySelector(selector) : ctx.pageRoot
|
|
158
|
+
if (!el) return null
|
|
159
|
+
return { el, prop, oldValue: el[prop] }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function onScalarChange(target, ctx) {
|
|
163
|
+
const fieldEl = target.closest('[data-hcms-field]') || target
|
|
164
|
+
const pathStr = fieldEl.closest('[data-hcms-path]')?.getAttribute('data-hcms-path') || ''
|
|
165
|
+
// Capture the projected page value BEFORE commit() overwrites it. Property
|
|
166
|
+
// writes (@value/@checked) leave no MutationRecord, so without recording them
|
|
167
|
+
// explicitly they produce no undo step (text projections are observed).
|
|
168
|
+
const proj = resolveUnobservedProjection(ctx, pathStr)
|
|
169
|
+
commit(extractFormData(ctx), { path: pathStr, structural: false }, ctx)
|
|
170
|
+
if (proj) {
|
|
171
|
+
const u = platform('undo')
|
|
172
|
+
if (u && typeof u.recordValue === 'function') {
|
|
173
|
+
u.recordValue(proj.el, { prop: proj.prop, oldValue: proj.oldValue, newValue: proj.el[proj.prop] })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Crop adapter seam: ALL crop coupling lives here, between the file pick and
|
|
179
|
+
// the upload. A field opts in via data-hcms-crop on the page element (copied
|
|
180
|
+
// onto the built @image field by the form-builder); absent attr or absent
|
|
181
|
+
// quickcrop (standalone page) uploads raw. Returns { file, dataURL? }, or null
|
|
182
|
+
// to abort (crop cancelled / failed) — the caller resets the picker and skips
|
|
183
|
+
// the commit. quickcrop resolves null on cancel (it does NOT reject), so the
|
|
184
|
+
// cancel branch is the null check, not the catch.
|
|
185
|
+
const CROP_OUTPUT = { type: 'image/webp', quality: 0.85, maxWidth: 2048, maxHeight: 2048 }
|
|
186
|
+
|
|
187
|
+
async function maybeCrop(file, fieldEl) {
|
|
188
|
+
const attr = fieldEl && fieldEl.getAttribute ? fieldEl.getAttribute('data-hcms-crop') : null
|
|
189
|
+
if (attr == null) return { file }
|
|
190
|
+
const crop = platform('quickcrop')
|
|
191
|
+
if (typeof crop !== 'function') return { file }
|
|
192
|
+
try {
|
|
193
|
+
// QuickCrop's `auto` modal adapter looks only at window.themodal. clayjs
|
|
194
|
+
// attaches its dialog as clay.modal and deliberately sets no global, so a
|
|
195
|
+
// clayjs page would silently get QuickCrop's own plain dialog. Naming it here
|
|
196
|
+
// keeps QuickCrop free of client knowledge and clayjs free of the global.
|
|
197
|
+
const modal = (typeof window !== 'undefined' && (window.clay?.modal ?? window.themodal)) || 'auto'
|
|
198
|
+
const r = await crop(file, { aspect: parseCropAspect(attr), modal, ...CROP_OUTPUT })
|
|
199
|
+
if (r === null) return null
|
|
200
|
+
return { file: blobToFile(r.blob, file.name), dataURL: r.dataURL }
|
|
201
|
+
} catch (err) {
|
|
202
|
+
showFieldUploadError(fieldEl, (err && err.message) || 'Crop failed')
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// "1:1" → 1, "16:9" → 16/9, "free" / "" / unparseable → null (freeform crop).
|
|
208
|
+
// Exported for tests.
|
|
209
|
+
export function parseCropAspect(value) {
|
|
210
|
+
const v = String(value ?? '').trim().toLowerCase()
|
|
211
|
+
if (v === '' || v === 'free') return null
|
|
212
|
+
const m = v.match(/^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/)
|
|
213
|
+
if (!m) return null
|
|
214
|
+
const w = parseFloat(m[1])
|
|
215
|
+
const h = parseFloat(m[2])
|
|
216
|
+
if (!w || !h) return null
|
|
217
|
+
return w / h
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// quickcrop returns a nameless Blob; the legacy uploader rejects raw Blobs and a
|
|
221
|
+
// host keys content-type off the filename extension, so wrap it as a File
|
|
222
|
+
// whose extension matches the encoded mime. Exported for tests.
|
|
223
|
+
export function blobToFile(blob, originalName) {
|
|
224
|
+
const ext = blob.type === 'image/webp' ? '.webp' : blob.type === 'image/jpeg' ? '.jpg' : '.png'
|
|
225
|
+
const base = String(originalName || 'image').replace(/\.[^.]+$/, '')
|
|
226
|
+
try {
|
|
227
|
+
return new File([blob], base + ext, { type: blob.type })
|
|
228
|
+
} catch {
|
|
229
|
+
return blob
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// A file was picked in an @file/@image widget. Optionally crop, ask the host to
|
|
234
|
+
// store the file, write the resulting URL into the bound img.src / a.href leaf,
|
|
235
|
+
// then commit once. The leaf attribute change is an observed page mutation, so
|
|
236
|
+
// undo gets a clean step with no commitWithUndo. Async; failures surface inline
|
|
237
|
+
// and reset the picker.
|
|
238
|
+
//
|
|
239
|
+
// The upload either stores the file or it does not, and those are not the same
|
|
240
|
+
// as success and failure. Three outcomes, deliberately spelled out:
|
|
241
|
+
//
|
|
242
|
+
// stored write the served URL. The page carries a reference.
|
|
243
|
+
// cannot store here embed the bytes. A document open from a plain file server
|
|
244
|
+
// or a desktop app has nowhere to put a file, and embedding
|
|
245
|
+
// is the RIGHT answer there, not a consolation prize.
|
|
246
|
+
// failed write nothing. Embedding a file the host refused for size
|
|
247
|
+
// or type is the disease this capability exists to cure: it
|
|
248
|
+
// is how a 2 MB photo ends up in the document, in every
|
|
249
|
+
// future save, and in every stored version.
|
|
250
|
+
const EMBED_CODES = new Set(['unsupported', 'payment-required'])
|
|
251
|
+
|
|
252
|
+
export async function onUploadChange(inputEl, ctx) {
|
|
253
|
+
const file = inputEl.files && inputEl.files[0]
|
|
254
|
+
if (!file) return
|
|
255
|
+
const fieldEl = inputEl.closest('[data-hcms-path]')
|
|
256
|
+
if (!fieldEl) return
|
|
257
|
+
const pathStr = fieldEl.getAttribute('data-hcms-path') || ''
|
|
258
|
+
|
|
259
|
+
// A fresh pick is a fresh attempt: clear any stale inline error (e.g. a
|
|
260
|
+
// crop failure from a previous attempt) before running the new flow.
|
|
261
|
+
showFieldUploadError(fieldEl, null)
|
|
262
|
+
|
|
263
|
+
const cropped = await maybeCrop(file, fieldEl)
|
|
264
|
+
if (!cropped || ctx.closed) { resetPicker(inputEl); return }
|
|
265
|
+
const fileLike = cropped.file
|
|
266
|
+
const previewDataUrl = cropped.dataURL || null
|
|
267
|
+
|
|
268
|
+
const upload = platform('upload')
|
|
269
|
+
if (typeof upload !== 'function') {
|
|
270
|
+
// No client at all: a bare HTML file with the CMS dropped in. Same outcome as
|
|
271
|
+
// a host that answers `unsupported`, and silent for the same reason.
|
|
272
|
+
return finishUpload(inputEl, fieldEl, ctx, pathStr, await embedUrl(fileLike, previewDataUrl, fieldEl), fileLike)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// The crop, shown immediately on the thumb's own chrome and NOT on the bound
|
|
276
|
+
// leaf. Writing a two-megabyte data URL to the leaf makes it the field's value,
|
|
277
|
+
// and any commit() landing while the upload is in flight would project it
|
|
278
|
+
// straight into the live page.
|
|
279
|
+
showFieldUploadPreview(fieldEl, previewDataUrl)
|
|
280
|
+
setUploadProgress(fieldEl, 0)
|
|
281
|
+
|
|
282
|
+
const controller = beginUpload(ctx)
|
|
283
|
+
let res
|
|
284
|
+
try {
|
|
285
|
+
res = await upload(fileLike, {
|
|
286
|
+
signal: controller?.signal,
|
|
287
|
+
onProgress: ({ percent }) => setUploadProgress(fieldEl, percent),
|
|
288
|
+
})
|
|
289
|
+
} finally {
|
|
290
|
+
endUpload(ctx, controller)
|
|
291
|
+
clearUploadProgress(fieldEl)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// The editor may have been closed while the upload was in flight; bail before
|
|
295
|
+
// writing the (now detached) form leaf or committing to the live page.
|
|
296
|
+
if (ctx.closed) { resetPicker(inputEl); return }
|
|
297
|
+
if (res.code === 'aborted') { resetPicker(inputEl); return }
|
|
298
|
+
|
|
299
|
+
if (res.ok) {
|
|
300
|
+
return finishUpload(inputEl, fieldEl, ctx, pathStr, res.uploads[0].url, fileLike)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!EMBED_CODES.has(res.code)) {
|
|
304
|
+
showFieldUploadError(fieldEl, res.msg || 'Upload failed')
|
|
305
|
+
ctx.dispatch?.('hcms:error', { error: new Error(res.msg || 'Upload failed'), code: res.code, path: pathStr })
|
|
306
|
+
resetPicker(inputEl)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Embedded, and only `payment-required` says so. A person editing a file on
|
|
311
|
+
// their own desktop does not need to be told their file is a file; a person on
|
|
312
|
+
// a host that WOULD store it, for money, is being told why it did not.
|
|
313
|
+
const note = res.code === 'payment-required'
|
|
314
|
+
? 'This file is stored in the page. Add a paid plan to upload files.'
|
|
315
|
+
: null
|
|
316
|
+
return finishUpload(inputEl, fieldEl, ctx, pathStr, await embedUrl(fileLike, previewDataUrl, fieldEl), fileLike, note)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Write the URL into the leaf and commit once. `url` empty (no FileReader, an
|
|
320
|
+
// unreadable file) means there is nothing to write, and writing '' would clear a
|
|
321
|
+
// picture the person already had.
|
|
322
|
+
//
|
|
323
|
+
// The note goes on AFTER the commit, never before: a successful commit calls
|
|
324
|
+
// setError(ctx, null), which clears every inline slot in the form. Writing the
|
|
325
|
+
// note first leaves the person with an embedded file and no explanation of why.
|
|
326
|
+
function finishUpload(inputEl, fieldEl, ctx, pathStr, url, fileLike, note = null) {
|
|
327
|
+
if (ctx.closed) { resetPicker(inputEl); return }
|
|
328
|
+
showFieldUploadPreview(fieldEl, null)
|
|
329
|
+
showFieldUploadError(fieldEl, null)
|
|
330
|
+
if (!url) { resetPicker(inputEl); return }
|
|
331
|
+
writeUploadLeaf(fieldEl, url, fileLike.name)
|
|
332
|
+
commit(extractFormData(ctx), { path: pathStr, structural: false }, ctx)
|
|
333
|
+
if (note) showFieldUploadError(fieldEl, note, 'info')
|
|
334
|
+
resetPicker(inputEl)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// The crop when there was one, otherwise the file's own bytes. Always a data:
|
|
338
|
+
// URL and never blob:, so both paths degrade identically: a blob: URL is dead the
|
|
339
|
+
// moment the page reloads, so the no-crop path used to commit a picture that
|
|
340
|
+
// looked right until the person came back to it.
|
|
341
|
+
async function embedUrl(fileLike, previewDataUrl, fieldEl) {
|
|
342
|
+
return previewDataUrl || (await fileToDataUrl(fileLike, fieldEl))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The FileReader comes from the field's own realm, never from globalThis: a form
|
|
346
|
+
// mounted into another document reads a File belonging to THAT window, and a
|
|
347
|
+
// reader from a different realm rejects it.
|
|
348
|
+
function fileToDataUrl(file, fieldEl) {
|
|
349
|
+
const Reader = fieldEl?.ownerDocument?.defaultView?.FileReader || globalThis.FileReader
|
|
350
|
+
if (!Reader) return Promise.resolve('')
|
|
351
|
+
return new Promise((resolve) => {
|
|
352
|
+
const reader = new Reader()
|
|
353
|
+
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '')
|
|
354
|
+
reader.onerror = () => resolve('')
|
|
355
|
+
try { reader.readAsDataURL(file) } catch { resolve('') }
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// In-flight uploads hang off the ctx, so close() tears them down the same way it
|
|
360
|
+
// tears down the observer and the undo subscription. Without this the request
|
|
361
|
+
// keeps running after the editor is gone: harmless to the page, because every
|
|
362
|
+
// write is behind a ctx.closed check, but it holds the connection and the bytes.
|
|
363
|
+
function beginUpload(ctx) {
|
|
364
|
+
if (typeof AbortController !== 'function') return null
|
|
365
|
+
const controller = new AbortController()
|
|
366
|
+
;(ctx.uploads || (ctx.uploads = new Set())).add(controller)
|
|
367
|
+
return controller
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function endUpload(ctx, controller) {
|
|
371
|
+
if (controller) ctx.uploads?.delete(controller)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The × on an upload widget. Clears the bound leaf to empty and commits, so the
|
|
375
|
+
// :has()-driven chrome falls back to the empty state. Removes immediately (no
|
|
376
|
+
// consent gate) per the approved design; distinct from the "remove" action,
|
|
377
|
+
// which deletes a whole array item.
|
|
378
|
+
export function onClearUpload(actionEl, ctx) {
|
|
379
|
+
const fieldEl = actionEl.closest('[data-hcms-path]')
|
|
380
|
+
if (!fieldEl) return
|
|
381
|
+
const pathStr = fieldEl.getAttribute('data-hcms-path') || ''
|
|
382
|
+
writeUploadLeaf(fieldEl, '', '')
|
|
383
|
+
const input = fieldEl.querySelector('input[type="file"][data-hcms-upload]')
|
|
384
|
+
if (input) resetPicker(input)
|
|
385
|
+
showFieldUploadError(fieldEl, null)
|
|
386
|
+
commit(extractFormData(ctx), { path: pathStr, structural: false }, ctx)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function findUploadLeaf(fieldEl) {
|
|
390
|
+
return fieldEl.querySelector
|
|
391
|
+
? fieldEl.querySelector('img[data-hcms-field], a[data-hcms-field]')
|
|
392
|
+
: null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Write a URL into the widget's bound leaf: an <img>'s src, or an <a>'s href
|
|
396
|
+
// plus its visible filename text. Empty url clears to the empty state.
|
|
397
|
+
function writeUploadLeaf(fieldEl, url, fileName) {
|
|
398
|
+
const leaf = findUploadLeaf(fieldEl)
|
|
399
|
+
if (!leaf) return
|
|
400
|
+
const tag = (leaf.tagName || '').toUpperCase()
|
|
401
|
+
if (tag === 'IMG') {
|
|
402
|
+
leaf.src = url || ''
|
|
403
|
+
} else if (tag === 'A') {
|
|
404
|
+
leaf.href = url || ''
|
|
405
|
+
leaf.textContent = url ? (fileName || fileNameFromUrl(url)) : ''
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function resetPicker(inputEl) {
|
|
410
|
+
// Clear the selection so re-picking the same file fires `change` again.
|
|
411
|
+
try { inputEl.value = '' } catch { /* some inputs reject programmatic clear */ }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// The at-upload-time picture, painted on the frame rather than assigned to the
|
|
415
|
+
// leaf, because the leaf IS the field's value. CSS reveals the thumb while
|
|
416
|
+
// [data-hcms-uploading] is set, so the preview shows even on a field that is
|
|
417
|
+
// still empty.
|
|
418
|
+
function showFieldUploadPreview(fieldEl, dataUrl) {
|
|
419
|
+
const frame = fieldEl.querySelector ? fieldEl.querySelector('.mirk-image__frame') : null
|
|
420
|
+
if (!frame) return
|
|
421
|
+
if (dataUrl) frame.style.backgroundImage = `url("${dataUrl.replace(/"/g, '%22')}")`
|
|
422
|
+
else frame.style.removeProperty('background-image')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Progress where the person is looking. The percent drives a CSS custom property
|
|
426
|
+
// rather than any markup of its own, so the templates keep their exact shape and
|
|
427
|
+
// a half-finished upload cannot leave an orphan node behind in the form.
|
|
428
|
+
function setUploadProgress(fieldEl, percent) {
|
|
429
|
+
const pct = Math.max(0, Math.min(100, Number(percent) || 0))
|
|
430
|
+
fieldEl.setAttribute('data-hcms-uploading', '')
|
|
431
|
+
fieldEl.style?.setProperty?.('--hcms-upload-progress', `${pct}%`)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function clearUploadProgress(fieldEl) {
|
|
435
|
+
fieldEl.removeAttribute('data-hcms-uploading')
|
|
436
|
+
fieldEl.style?.removeProperty?.('--hcms-upload-progress')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// `kind` distinguishes a refusal from a note: 'info' is the file that WAS stored,
|
|
440
|
+
// in the page, and the slot says why rather than shouting about a failure.
|
|
441
|
+
function showFieldUploadError(fieldEl, message, kind = 'error') {
|
|
442
|
+
const slot = fieldEl.querySelector ? fieldEl.querySelector(':scope > .hcms-error') : null
|
|
443
|
+
if (!slot) return
|
|
444
|
+
slot.classList.toggle('hcms-error--info', !!message && kind === 'info')
|
|
445
|
+
if (message) {
|
|
446
|
+
slot.textContent = message
|
|
447
|
+
slot.hidden = false
|
|
448
|
+
} else {
|
|
449
|
+
slot.textContent = ''
|
|
450
|
+
slot.hidden = true
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function onAdd(arrayPath, ctx) {
|
|
455
|
+
const { formRoot, pageRules } = ctx
|
|
456
|
+
const arrayEl = formRoot.querySelector(`[data-hcms-path="${cssEscape(arrayPath)}"]`)
|
|
457
|
+
if (!arrayEl) throw new Error(`hypercms: no element at path "${arrayPath}"`)
|
|
458
|
+
const slot = arrayEl.querySelector('.hcms-array-items')
|
|
459
|
+
if (!slot) throw new Error(`hypercms: array container missing .hcms-array-items at "${arrayPath}"`)
|
|
460
|
+
|
|
461
|
+
const pathArr = pathFromString(arrayPath)
|
|
462
|
+
const ruleAtPath = ruleAt(pageRules, pathArr)
|
|
463
|
+
const isObjectArray = Array.isArray(ruleAtPath)
|
|
464
|
+
const isScalarArray = typeof ruleAtPath === 'string' && ruleAtPath.endsWith('[]')
|
|
465
|
+
if (!isObjectArray && !isScalarArray) throw new Error(`hypercms: path "${arrayPath}" is not an array`)
|
|
466
|
+
|
|
467
|
+
const maxItems = readIntAttr(arrayEl, 'data-hcms-max-items')
|
|
468
|
+
const existingItems = slot.querySelectorAll(':scope > [data-hcms-card], :scope > [data-hcms-array-item]')
|
|
469
|
+
if (arrayEl.hasAttribute('data-hcms-no-add')) return
|
|
470
|
+
if (maxItems != null && existingItems.length >= maxItems) return
|
|
471
|
+
const nextIndex = existingItems.length
|
|
472
|
+
|
|
473
|
+
const itemShape = isObjectArray ? ruleAtPath[1] : ruleAtPath.replace(/\[\]$/, '')
|
|
474
|
+
const itemData = scaffold(isObjectArray ? itemShape : 'string')
|
|
475
|
+
const itemNode = buildItem({
|
|
476
|
+
shape: isObjectArray ? 'object-array-item' : 'scalar-array-item',
|
|
477
|
+
itemShape,
|
|
478
|
+
pathArr: [...pathArr, nextIndex],
|
|
479
|
+
data: itemData,
|
|
480
|
+
doc: ctx.doc,
|
|
481
|
+
// Component arrays (chips) record their item-template key at build time so
|
|
482
|
+
// added items keep the same chrome.
|
|
483
|
+
itemKey: arrayEl.getAttribute('data-hcms-item-tpl') || null,
|
|
484
|
+
pageRules,
|
|
485
|
+
})
|
|
486
|
+
slot.appendChild(itemNode)
|
|
487
|
+
enhanceFields(itemNode, ctx.doc, ctx.view?.enhanceFormRichText !== false)
|
|
488
|
+
updateArrayButtonsVisibility(arrayEl)
|
|
489
|
+
return commitWithUndo(`Add ${arrayPath}`, () =>
|
|
490
|
+
commit(extractFormData(ctx), { path: arrayPath, structural: true }, ctx)
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function onMove(itemEl, direction, ctx) {
|
|
495
|
+
const arrayEl = itemEl.closest('[data-hcms-shape="object-array"], [data-hcms-shape="scalar-array"]')
|
|
496
|
+
if (!arrayEl) return
|
|
497
|
+
if (arrayEl.hasAttribute('data-hcms-no-reorder')) return
|
|
498
|
+
const slot = arrayEl.querySelector('.hcms-array-items')
|
|
499
|
+
if (!slot) return
|
|
500
|
+
const siblings = Array.from(slot.querySelectorAll(':scope > [data-hcms-card], :scope > [data-hcms-array-item]'))
|
|
501
|
+
const idx = siblings.indexOf(itemEl)
|
|
502
|
+
if (idx < 0) return
|
|
503
|
+
const targetIdx = idx + direction
|
|
504
|
+
if (targetIdx < 0 || targetIdx >= siblings.length) return
|
|
505
|
+
const moveAction = itemEl.querySelector(`[data-hcms-action="${direction < 0 ? 'move-up' : 'move-down'}"]`)
|
|
506
|
+
if (direction < 0) {
|
|
507
|
+
slot.insertBefore(itemEl, siblings[targetIdx])
|
|
508
|
+
} else {
|
|
509
|
+
slot.insertBefore(itemEl, siblings[targetIdx].nextSibling)
|
|
510
|
+
}
|
|
511
|
+
restampSiblingPaths(slot)
|
|
512
|
+
updateArrayButtonsVisibility(arrayEl)
|
|
513
|
+
// Restore focus to the moved button so keyboard users can continue pressing
|
|
514
|
+
// the same button (now landed in its new position).
|
|
515
|
+
if (moveAction && typeof moveAction.focus === 'function') {
|
|
516
|
+
const newAction = itemEl.querySelector(`[data-hcms-action="${direction < 0 ? 'move-up' : 'move-down'}"]`)
|
|
517
|
+
newAction?.focus?.()
|
|
518
|
+
}
|
|
519
|
+
return commitWithUndo(`Reorder ${arrayEl.getAttribute('data-hcms-path') || ''}`, () =>
|
|
520
|
+
commit(extractFormData(ctx), { path: arrayEl.getAttribute('data-hcms-path') || '', structural: true }, ctx)
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const DEFAULT_CONFIRM_REMOVE = 'Delete this item?'
|
|
525
|
+
|
|
526
|
+
// Resolve whether removing an item from `arrayEl` should confirm first, and with
|
|
527
|
+
// what prompt. Most specific wins: a per-array `data-hcms-confirm-remove`
|
|
528
|
+
// attribute (a string overrides the prompt; "off"/"false"/"no"/"0" disables that
|
|
529
|
+
// one array), then the global `confirmRemove` open() option (a string overrides
|
|
530
|
+
// the prompt, true forces every list, false disables everything), then the
|
|
531
|
+
// built-in default. The default confirms card removals (object-array items hold
|
|
532
|
+
// several fields) and leaves scalar-array chips instant. Returns the prompt to
|
|
533
|
+
// confirm with, or null to remove without asking.
|
|
534
|
+
function resolveRemoveConfirm(arrayEl, ctx) {
|
|
535
|
+
const attr = arrayEl && arrayEl.getAttribute('data-hcms-confirm-remove')
|
|
536
|
+
if (attr != null) {
|
|
537
|
+
if (/^(off|false|no|0)$/i.test(attr.trim())) return null
|
|
538
|
+
return attr || DEFAULT_CONFIRM_REMOVE
|
|
539
|
+
}
|
|
540
|
+
const global = ctx && ctx.confirmRemove
|
|
541
|
+
if (global === false) return null
|
|
542
|
+
if (typeof global === 'string') return global || DEFAULT_CONFIRM_REMOVE
|
|
543
|
+
if (global === true) return DEFAULT_CONFIRM_REMOVE
|
|
544
|
+
return arrayEl && arrayEl.getAttribute('data-hcms-shape') === 'object-array'
|
|
545
|
+
? DEFAULT_CONFIRM_REMOVE
|
|
546
|
+
: null
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Built-in delete confirmation: every list-item removal routes through here so
|
|
550
|
+
// the policy applies uniformly. Confirmation uses hyperclay's consent() (the
|
|
551
|
+
// styled modal) when the host page provides it, falls back to native confirm(),
|
|
552
|
+
// and proceeds when neither exists (non-browser). Programmatic api.removeItem()
|
|
553
|
+
// bypasses this by calling onRemove directly, the way undo treats deliberate
|
|
554
|
+
// actions.
|
|
555
|
+
export function requestRemove(itemEl, ctx) {
|
|
556
|
+
const arrayEl = itemEl.closest('[data-hcms-shape="object-array"], [data-hcms-shape="scalar-array"]')
|
|
557
|
+
const message = resolveRemoveConfirm(arrayEl, ctx)
|
|
558
|
+
if (message == null) return onRemove(itemEl, ctx)
|
|
559
|
+
const consent = platform('consent') || (typeof window !== 'undefined' && window.consent)
|
|
560
|
+
if (typeof consent === 'function') {
|
|
561
|
+
Promise.resolve(consent(message)).then(() => {
|
|
562
|
+
// The editor can be closed, or switched to another view, while the
|
|
563
|
+
// confirm is still on screen. ctx is dead by then, but itemEl still
|
|
564
|
+
// points into the detached form and commit would apply that stale form
|
|
565
|
+
// over the live page: the card goes, and every other field reverts to
|
|
566
|
+
// whatever it held when the dialog opened.
|
|
567
|
+
if (ctx.closed) return
|
|
568
|
+
onRemove(itemEl, ctx)
|
|
569
|
+
}, () => {})
|
|
570
|
+
} else if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
|
|
571
|
+
if (window.confirm(message)) onRemove(itemEl, ctx)
|
|
572
|
+
} else {
|
|
573
|
+
onRemove(itemEl, ctx)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function onRemove(itemEl, ctx) {
|
|
578
|
+
const path = itemEl.getAttribute('data-hcms-path') || ''
|
|
579
|
+
const parent = itemEl.parentElement
|
|
580
|
+
const arrayEl = itemEl.closest('[data-hcms-shape="object-array"], [data-hcms-shape="scalar-array"]')
|
|
581
|
+
if (arrayEl?.hasAttribute('data-hcms-no-remove')) return
|
|
582
|
+
if (arrayEl) {
|
|
583
|
+
const minItems = readIntAttr(arrayEl, 'data-hcms-min-items')
|
|
584
|
+
const slot = arrayEl.querySelector('.hcms-array-items')
|
|
585
|
+
const count = slot
|
|
586
|
+
? slot.querySelectorAll(':scope > [data-hcms-card], :scope > [data-hcms-array-item]').length
|
|
587
|
+
: 0
|
|
588
|
+
if (minItems != null && count <= minItems) return
|
|
589
|
+
}
|
|
590
|
+
itemEl.remove()
|
|
591
|
+
// After removal, re-stamp sibling paths to keep indices contiguous.
|
|
592
|
+
if (parent) restampSiblingPaths(parent)
|
|
593
|
+
if (arrayEl) updateArrayButtonsVisibility(arrayEl)
|
|
594
|
+
return commitWithUndo(`Remove ${path}`, () =>
|
|
595
|
+
commit(extractFormData(ctx), { path, structural: true }, ctx)
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function commit(newData, info, ctx) {
|
|
600
|
+
// The last line of defence for any deferred path that reaches a torn-down
|
|
601
|
+
// session: a resolved confirmation, an upload that finished after close, a
|
|
602
|
+
// queued handler. ctx.pageRoot is still the live page.
|
|
603
|
+
//
|
|
604
|
+
// ok:false is deliberate and load-bearing — commitWithUndo records an undo
|
|
605
|
+
// entry on a truthy ok, so returning ok:true here would put a commit that
|
|
606
|
+
// never happened onto the undo stack. Returning before the error branch below
|
|
607
|
+
// is equally deliberate: setError would write into a detached shell.
|
|
608
|
+
if (ctx.closed) return { ok: false, skipped: true, closed: true }
|
|
609
|
+
const fingerprint = stableStringify(newData)
|
|
610
|
+
// Only a plain edit can be skipped on an unchanged fingerprint. A structural
|
|
611
|
+
// operation is never a no-op even when the data is identical: reordering two
|
|
612
|
+
// rows whose ruled fields read the same produces the same JSON, and the page
|
|
613
|
+
// nodes still have to move. Skipping it here meant the form supplied identity
|
|
614
|
+
// the engine never got to use.
|
|
615
|
+
if (!info.structural && fingerprint === ctx.lastFingerprint) {
|
|
616
|
+
return { ok: true, skipped: true }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// undefined is a legitimate value here: a rules tree that is nothing but a
|
|
620
|
+
// read-only projection strips to it, and engine.apply writes nothing when the
|
|
621
|
+
// rules are undefined. Only a ctx that never had the field at all — the
|
|
622
|
+
// hand-built stubs in older tests — falls back to the unstripped rules.
|
|
623
|
+
const writeRules = Object.hasOwn(ctx, 'writeRules') ? ctx.writeRules : ctx.pageRules
|
|
624
|
+
|
|
625
|
+
const result = applyWithRollback(ctx.pageRoot, writeRules, newData, {
|
|
626
|
+
observerHandle: ctx.observerHandle,
|
|
627
|
+
shellRoot: ctx.shellRoot,
|
|
628
|
+
structural: !!info.structural,
|
|
629
|
+
structuralPath: info.path || null,
|
|
630
|
+
formRoot: ctx.formRoot,
|
|
631
|
+
})
|
|
632
|
+
if (result.ok) {
|
|
633
|
+
ctx.lastFingerprint = fingerprint
|
|
634
|
+
ctx.lastData = newData
|
|
635
|
+
setError(ctx, null)
|
|
636
|
+
ctx.dispatch?.('hcms:change', { data: newData, path: info.path, structural: !!info.structural })
|
|
637
|
+
ctx.onChange?.(newData, info)
|
|
638
|
+
} else {
|
|
639
|
+
setError(ctx, formatError(result.error, info.path))
|
|
640
|
+
ctx.dispatch?.('hcms:error', { error: result.error, attemptedData: newData })
|
|
641
|
+
ctx.onError?.(result.error)
|
|
642
|
+
}
|
|
643
|
+
return result
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Resolve one form leaf by path, and write a value into it. Shared by
|
|
647
|
+
// cms.api.setValue and by the inline view's text commit, which reads the page
|
|
648
|
+
// element richclay just edited and mirrors it into the form before committing.
|
|
649
|
+
export function findLeafField(formRoot, path) {
|
|
650
|
+
const esc = cssEscape(path)
|
|
651
|
+
// Prefer a tag-qualified leaf (input/textarea/select/img/a) — since v0.3
|
|
652
|
+
// stamps data-hcms-field on the wrapping container too, a bare attribute
|
|
653
|
+
// match would resolve to the container and lose the value write.
|
|
654
|
+
const leafSel =
|
|
655
|
+
`[data-hcms-path="${esc}"] input[data-hcms-field], ` +
|
|
656
|
+
`[data-hcms-path="${esc}"] textarea[data-hcms-field], ` +
|
|
657
|
+
`[data-hcms-path="${esc}"] select[data-hcms-field], ` +
|
|
658
|
+
`[data-hcms-path="${esc}"] img[data-hcms-field], ` +
|
|
659
|
+
`[data-hcms-path="${esc}"] a[data-hcms-field], ` +
|
|
660
|
+
`[data-hcms-path="${esc}"] [contenteditable][data-hcms-field], ` +
|
|
661
|
+
// Leaf is the path-stamped element itself (inline-stamped fields):
|
|
662
|
+
`input[data-hcms-path="${esc}"][data-hcms-field], ` +
|
|
663
|
+
`textarea[data-hcms-path="${esc}"][data-hcms-field], ` +
|
|
664
|
+
`select[data-hcms-path="${esc}"][data-hcms-field], ` +
|
|
665
|
+
`img[data-hcms-path="${esc}"][data-hcms-field], ` +
|
|
666
|
+
`a[data-hcms-path="${esc}"][data-hcms-field], ` +
|
|
667
|
+
`[contenteditable][data-hcms-path="${esc}"][data-hcms-field]`
|
|
668
|
+
return formRoot.querySelector(leafSel)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function writeFieldValue(el, value, formRoot, path) {
|
|
672
|
+
const tag = (el.tagName || '').toUpperCase()
|
|
673
|
+
const type = (el.getAttribute('type') || '').toLowerCase()
|
|
674
|
+
if (tag === 'INPUT' && type === 'checkbox') {
|
|
675
|
+
el.checked = value === true || value === 'true'
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
if (tag === 'INPUT' && type === 'radio') {
|
|
679
|
+
// Radios sharing the same path act as a group. Toggle the matching option.
|
|
680
|
+
const esc = cssEscape(path)
|
|
681
|
+
const group = formRoot.querySelectorAll(
|
|
682
|
+
`[data-hcms-path="${esc}"][data-hcms-field][type="radio"], [data-hcms-path="${esc}"] [data-hcms-field][type="radio"]`
|
|
683
|
+
)
|
|
684
|
+
if (group.length) {
|
|
685
|
+
group.forEach((r) => { r.checked = String(r.value) === String(value ?? '') })
|
|
686
|
+
} else {
|
|
687
|
+
el.checked = String(el.value) === String(value ?? '')
|
|
688
|
+
}
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
if (tag === 'IMG') {
|
|
692
|
+
el.src = value == null ? '' : String(value)
|
|
693
|
+
return
|
|
694
|
+
}
|
|
695
|
+
if (tag === 'A') {
|
|
696
|
+
el.href = value == null ? '' : String(value)
|
|
697
|
+
return
|
|
698
|
+
}
|
|
699
|
+
if (el.hasAttribute && el.hasAttribute('contenteditable')) {
|
|
700
|
+
el.innerHTML = value == null ? '' : String(value)
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
if ('value' in el) {
|
|
704
|
+
el.value = value == null ? '' : String(value)
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
el.textContent = value == null ? '' : String(value)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function extractFormData(ctx) {
|
|
711
|
+
const raw = engine.extract(ctx.formRoot, ctx.formRules)
|
|
712
|
+
return coerceBooleans(raw, ctx.formRules)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Walks either pageRules OR formRules (both use the same `endsWith('@checked')`
|
|
716
|
+
// semantics) and coerces leaf strings "true" / "false" to booleans. Engine
|
|
717
|
+
// extract stringifies @checked properties; the form layer wants real booleans
|
|
718
|
+
// so writeValue's Boolean(value) doesn't flip "false" → true on hydration.
|
|
719
|
+
export function coerceBooleans(data, rules) {
|
|
720
|
+
if (rules == null || data == null) return data
|
|
721
|
+
if (typeof rules === 'string') {
|
|
722
|
+
if (rules.endsWith('@checked')) {
|
|
723
|
+
return data === true || data === 'true'
|
|
724
|
+
}
|
|
725
|
+
return data
|
|
726
|
+
}
|
|
727
|
+
if (Array.isArray(rules)) {
|
|
728
|
+
if (!Array.isArray(data)) return data
|
|
729
|
+
const [, itemShape] = rules
|
|
730
|
+
return data.map((item) => coerceBooleans(item, itemShape))
|
|
731
|
+
}
|
|
732
|
+
if (typeof rules === 'object') {
|
|
733
|
+
if (typeof data !== 'object' || Array.isArray(data)) return data
|
|
734
|
+
const out = {}
|
|
735
|
+
for (const [k, child] of Object.entries(rules)) {
|
|
736
|
+
out[k] = coerceBooleans(data[k], child)
|
|
737
|
+
}
|
|
738
|
+
return out
|
|
739
|
+
}
|
|
740
|
+
return data
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export { stableStringify }
|
|
744
|
+
|
|
745
|
+
// Errors render inline when we can match a path to a slot in the form (a `.hcms-error`
|
|
746
|
+
// direct child of the path's container — added by the default scalar / array templates).
|
|
747
|
+
// A single commit can produce multiple mismatches (ShapeMismatch carries an array); each one
|
|
748
|
+
// tries to land at its own path. Custom templates that omit the slot, and path-less errors,
|
|
749
|
+
// fall back to the global banner (joined when there's more than one).
|
|
750
|
+
function setError(ctx, errors) {
|
|
751
|
+
ctx.lastErrors = errors && errors.length ? errors : null
|
|
752
|
+
applyErrorState(ctx)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Re-apply ctx.lastErrors to the DOM. Exported so refreshForm can call this
|
|
756
|
+
// after morphForm rebuilds the form — the morph wipes inline error slots
|
|
757
|
+
// (they're inside the form), so we restore them post-morph.
|
|
758
|
+
export function applyErrorState(ctx) {
|
|
759
|
+
clearInlineErrors(ctx)
|
|
760
|
+
if (ctx.errorEl) {
|
|
761
|
+
ctx.errorEl.textContent = ''
|
|
762
|
+
ctx.errorEl.hidden = true
|
|
763
|
+
}
|
|
764
|
+
if (!ctx.lastErrors) return
|
|
765
|
+
|
|
766
|
+
const unplaced = []
|
|
767
|
+
for (const { message, path } of ctx.lastErrors) {
|
|
768
|
+
if (path != null && path !== '') {
|
|
769
|
+
const target = findInlineErrorSlot(ctx.formRoot, path)
|
|
770
|
+
if (target) {
|
|
771
|
+
// Two errors at the same path concatenate into one slot, separated by a newline.
|
|
772
|
+
target.textContent = target.textContent ? `${target.textContent}\n${message}` : message
|
|
773
|
+
target.hidden = false
|
|
774
|
+
continue
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
unplaced.push(message)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (unplaced.length && ctx.errorEl) {
|
|
781
|
+
ctx.errorEl.textContent = unplaced.join('\n')
|
|
782
|
+
ctx.errorEl.hidden = false
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function clearInlineErrors(ctx) {
|
|
787
|
+
if (!ctx.formRoot) return
|
|
788
|
+
for (const el of ctx.formRoot.querySelectorAll('.hcms-error')) {
|
|
789
|
+
el.textContent = ''
|
|
790
|
+
el.hidden = true
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function findInlineErrorSlot(formRoot, path) {
|
|
795
|
+
if (!formRoot) return null
|
|
796
|
+
// Walk up the path: try the exact match first, then trim the last segment, etc.
|
|
797
|
+
// Lets deep-path errors land at the nearest ancestor that has a slot — useful when
|
|
798
|
+
// engine paths go deeper than the form's stamped structure (e.g., migration data).
|
|
799
|
+
const segs = path.split('.')
|
|
800
|
+
while (segs.length > 0) {
|
|
801
|
+
const candidate = segs.join('.')
|
|
802
|
+
const esc = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(candidate) : candidate.replace(/[^a-zA-Z0-9_\-.*]/g, (c) => '\\' + c)
|
|
803
|
+
const container = formRoot.querySelector(`[data-hcms-path="${esc}"]`)
|
|
804
|
+
if (container) {
|
|
805
|
+
for (const child of container.children) {
|
|
806
|
+
if (child.classList && child.classList.contains('hcms-error')) return child
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
segs.pop()
|
|
810
|
+
}
|
|
811
|
+
return null
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function formatError(err, fallbackPath) {
|
|
815
|
+
if (!err) return [{ message: 'unknown error', path: fallbackPath }]
|
|
816
|
+
if (err.name === 'EmptyListInsert') {
|
|
817
|
+
return [{ message: 'Add a seed item in HTML first.', path: fallbackPath }]
|
|
818
|
+
}
|
|
819
|
+
if (err.name === 'ShapeMismatch' && Array.isArray(err.mismatches) && err.mismatches.length) {
|
|
820
|
+
return err.mismatches.map((m) => ({
|
|
821
|
+
message: `Shape mismatch: expected ${m.expected}, got ${m.got}`,
|
|
822
|
+
path: m.path,
|
|
823
|
+
}))
|
|
824
|
+
}
|
|
825
|
+
return [{ message: err.message || String(err), path: fallbackPath }]
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function ruleAt(rules, pathArr) {
|
|
829
|
+
let node = rules
|
|
830
|
+
for (const seg of pathArr) {
|
|
831
|
+
if (node == null) return undefined
|
|
832
|
+
if (typeof node === 'string') return undefined
|
|
833
|
+
if (Array.isArray(node)) {
|
|
834
|
+
if (typeof seg !== 'number' && seg !== '*') return undefined
|
|
835
|
+
node = node[1]
|
|
836
|
+
continue
|
|
837
|
+
}
|
|
838
|
+
if (typeof node === 'object') {
|
|
839
|
+
if (typeof seg === 'number') return undefined
|
|
840
|
+
if (!(seg in node)) return undefined
|
|
841
|
+
node = node[seg]
|
|
842
|
+
continue
|
|
843
|
+
}
|
|
844
|
+
return undefined
|
|
845
|
+
}
|
|
846
|
+
return node
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function readIntAttr(el, name) {
|
|
850
|
+
if (!el || !el.hasAttribute(name)) return null
|
|
851
|
+
const n = parseInt(el.getAttribute(name), 10)
|
|
852
|
+
return Number.isFinite(n) ? n : null
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export function updateArrayButtonsVisibility(arrayEl) {
|
|
856
|
+
if (!arrayEl) return
|
|
857
|
+
const slot = arrayEl.querySelector('.hcms-array-items')
|
|
858
|
+
if (!slot) return
|
|
859
|
+
const items = Array.from(slot.querySelectorAll(':scope > [data-hcms-card], :scope > [data-hcms-array-item]'))
|
|
860
|
+
const count = items.length
|
|
861
|
+
const max = readIntAttr(arrayEl, 'data-hcms-max-items')
|
|
862
|
+
const min = readIntAttr(arrayEl, 'data-hcms-min-items')
|
|
863
|
+
const noAdd = arrayEl.hasAttribute('data-hcms-no-add')
|
|
864
|
+
const noRemove = arrayEl.hasAttribute('data-hcms-no-remove')
|
|
865
|
+
const noReorder = arrayEl.hasAttribute('data-hcms-no-reorder')
|
|
866
|
+
const addBtn = arrayEl.querySelector(':scope > .hcms-add, :scope > * > .hcms-add, :scope > [data-hcms-action="add"]')
|
|
867
|
+
if (addBtn) addBtn.hidden = noAdd || (max != null && count >= max)
|
|
868
|
+
items.forEach((item, i) => {
|
|
869
|
+
const rm = item.querySelector('[data-hcms-action="remove"]')
|
|
870
|
+
if (rm) rm.hidden = noRemove || (min != null && count <= min)
|
|
871
|
+
const up = item.querySelector('[data-hcms-action="move-up"]')
|
|
872
|
+
if (up) up.hidden = noReorder || i === 0
|
|
873
|
+
const dn = item.querySelector('[data-hcms-action="move-down"]')
|
|
874
|
+
if (dn) dn.hidden = noReorder || i === count - 1
|
|
875
|
+
})
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export function restampAllSiblings(formRoot) {
|
|
879
|
+
if (!formRoot || !formRoot.querySelectorAll) return
|
|
880
|
+
formRoot.querySelectorAll('.hcms-array-items').forEach((slot) => restampSiblingPaths(slot))
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function restampSiblingPaths(parent) {
|
|
884
|
+
// Radio names are rewritten one item at a time below, so during a move or
|
|
885
|
+
// drag a just-renamed radio can transiently share a name with a
|
|
886
|
+
// not-yet-renamed sibling — and the browser enforces single-selection per
|
|
887
|
+
// group AT THAT INSTANT, permanently unchecking one of them (null extract →
|
|
888
|
+
// the whole-form commit clobbers that field's page value). Snapshot every
|
|
889
|
+
// radio's checked state and re-assert it once all names are final (and
|
|
890
|
+
// unique per field) again.
|
|
891
|
+
const radios = parent.querySelectorAll
|
|
892
|
+
? Array.from(parent.querySelectorAll('input[type="radio"][data-hcms-field]'), (r) => [r, r.checked])
|
|
893
|
+
: []
|
|
894
|
+
let i = 0
|
|
895
|
+
for (const child of parent.children) {
|
|
896
|
+
if (!child.matches?.('[data-hcms-card], [data-hcms-array-item]')) continue
|
|
897
|
+
const path = child.getAttribute('data-hcms-path')
|
|
898
|
+
if (!path) continue
|
|
899
|
+
const segs = path.split('.')
|
|
900
|
+
segs[segs.length - 1] = String(i)
|
|
901
|
+
const newPath = segs.join('.')
|
|
902
|
+
if (newPath !== path) restampSubtree(child, path, newPath)
|
|
903
|
+
i++
|
|
904
|
+
}
|
|
905
|
+
for (const [r, wasChecked] of radios) {
|
|
906
|
+
if (r.checked !== wasChecked) r.checked = wasChecked
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function restampSubtree(root, oldPrefix, newPrefix) {
|
|
911
|
+
const all = root.querySelectorAll('[data-hcms-path]')
|
|
912
|
+
root.setAttribute('data-hcms-path', newPrefix)
|
|
913
|
+
for (const el of all) {
|
|
914
|
+
const p = el.getAttribute('data-hcms-path')
|
|
915
|
+
if (p === oldPrefix) {
|
|
916
|
+
el.setAttribute('data-hcms-path', newPrefix)
|
|
917
|
+
} else if (p && p.startsWith(oldPrefix + '.')) {
|
|
918
|
+
el.setAttribute('data-hcms-path', newPrefix + p.slice(oldPrefix.length))
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
syncRadioGroupNames(root)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Radio group names derive from the field path at build time (populateOptions),
|
|
925
|
+
// so a restamp that moves paths must recompute them. Otherwise a removed-then-
|
|
926
|
+
// re-added sibling gets a fresh name that collides with this item's stale one:
|
|
927
|
+
// the two fields share a browser radio group, checking one unchecks the other,
|
|
928
|
+
// and the whole-form commit clobbers the unchecked field's page value. Only
|
|
929
|
+
// names the builder minted ('hcms-' prefix) are touched — radios in inline or
|
|
930
|
+
// custom templates keep whatever name the author gave them.
|
|
931
|
+
function syncRadioGroupNames(root) {
|
|
932
|
+
for (const input of root.querySelectorAll('input[type="radio"][data-hcms-field]')) {
|
|
933
|
+
if (!input.name || !input.name.startsWith('hcms-')) continue
|
|
934
|
+
const host = input.closest('[data-hcms-path]')
|
|
935
|
+
if (host) input.name = radioGroupName(host.getAttribute('data-hcms-path'))
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function stableStringify(value) {
|
|
940
|
+
return JSON.stringify(value, (_, v) => {
|
|
941
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
942
|
+
// Use a null-prototype container so `__proto__` keys survive sorting and
|
|
943
|
+
// serialize as data rather than mutating object prototype.
|
|
944
|
+
const sorted = Object.create(null)
|
|
945
|
+
for (const k of Object.keys(v).sort()) sorted[k] = v[k]
|
|
946
|
+
return sorted
|
|
947
|
+
}
|
|
948
|
+
return v
|
|
949
|
+
})
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export function cssEscape(value) {
|
|
953
|
+
if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(value)
|
|
954
|
+
return String(value).replace(/[^a-zA-Z0-9_\-.*]/g, (c) => '\\' + c)
|
|
955
|
+
}
|