@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,61 @@
|
|
|
1
|
+
export function toString(path) {
|
|
2
|
+
return path.map(String).join('.')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function fromString(str) {
|
|
6
|
+
if (str === '') return []
|
|
7
|
+
return str.split('.').map((seg) => (/^\d+$/.test(seg) ? Number(seg) : seg))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getRuleAtPath(rules, path) {
|
|
11
|
+
let node = rules
|
|
12
|
+
for (const seg of path) {
|
|
13
|
+
if (node == null) return undefined
|
|
14
|
+
if (typeof node === 'string') {
|
|
15
|
+
// Scalar-array rules like 'li[]' contain leaf scalars at integer indices.
|
|
16
|
+
// Descend by stripping the trailing []; the leaf is treated as a plain
|
|
17
|
+
// scalar string for path queries.
|
|
18
|
+
if (node.endsWith('[]') && (typeof seg === 'number' || seg === '*')) {
|
|
19
|
+
node = node.slice(0, -2)
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(node)) {
|
|
25
|
+
if (typeof seg !== 'number' && seg !== '*') return undefined
|
|
26
|
+
node = node[1]
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (typeof node === 'object') {
|
|
30
|
+
if (typeof seg === 'number') return undefined
|
|
31
|
+
if (!(seg in node)) return undefined
|
|
32
|
+
node = node[seg]
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
return node
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getValueAtPath(data, path) {
|
|
41
|
+
let node = data
|
|
42
|
+
for (const seg of path) {
|
|
43
|
+
if (node == null) return undefined
|
|
44
|
+
node = node[seg]
|
|
45
|
+
}
|
|
46
|
+
return node
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function setAtPath(obj, path, value) {
|
|
50
|
+
if (path.length === 0) return value
|
|
51
|
+
const [k, ...rest] = path
|
|
52
|
+
if (typeof k === 'number') {
|
|
53
|
+
const next = Array.isArray(obj) ? [...obj] : []
|
|
54
|
+
next[k] = setAtPath(next[k], rest, value)
|
|
55
|
+
return next
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...(obj && typeof obj === 'object' ? obj : {}),
|
|
59
|
+
[k]: setAtPath((obj || {})[k], rest, value),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Where to put a floating thing next to a page element.
|
|
2
|
+
//
|
|
3
|
+
// COPIED, deliberately, from richclay/src/toolbar-float.js:1-59. Not imported:
|
|
4
|
+
// hypercms must not gain a hard richclay dependency to lay out a popover on a
|
|
5
|
+
// page that contains no rich text at all. The ladder is the same one richclay's
|
|
6
|
+
// floating toolbar has been using in production, and the copy is verbatim apart
|
|
7
|
+
// from the two notes below, so a fix on either side can be read across.
|
|
8
|
+
//
|
|
9
|
+
// Two differences from the original, both because a popover is not a toolbar:
|
|
10
|
+
//
|
|
11
|
+
// 1. A popover has ONE size. richclay measures a horizontal bar and a vertical
|
|
12
|
+
// rail because its toolbar reflows between them; ours does not. Callers pass
|
|
13
|
+
// the same box as both `bar` and `rail`, which collapses the side-rail rung
|
|
14
|
+
// of the ladder into a plain "put it beside the anchor" without changing a
|
|
15
|
+
// line of the math.
|
|
16
|
+
//
|
|
17
|
+
// 2. The `hidden` rung is a backstop here, not the visibility test. It compares
|
|
18
|
+
// the anchor to the viewport, which cannot see an anchor clipped by an
|
|
19
|
+
// overflow: hidden ancestor while still inside the viewport — the case that
|
|
20
|
+
// floats a control over whatever is visible in its place. The inline view
|
|
21
|
+
// decides visibility from an IntersectionObserver, which accounts for
|
|
22
|
+
// ancestor clipping by construction (measured, plan §3.1.2), and only asks
|
|
23
|
+
// for a placement once it knows the anchor is showing.
|
|
24
|
+
|
|
25
|
+
export const GAP = 16
|
|
26
|
+
export const GAP_LADDER = [16, 8, 0]
|
|
27
|
+
export const VIEWPORT_INSET = 8
|
|
28
|
+
export const PLACEMENT_SLACK = 4
|
|
29
|
+
|
|
30
|
+
// Pure placement ladder: above -> below -> beside -> pinned to viewport top.
|
|
31
|
+
// All coords are viewport-relative (the host is position: fixed). `current` gets
|
|
32
|
+
// PLACEMENT_SLACK of hysteresis so the mode doesn't flip-flop at exact
|
|
33
|
+
// boundaries while the page scrolls.
|
|
34
|
+
export function place({ anchor, bar, rail = bar, viewport, current = null }) {
|
|
35
|
+
const slack = (mode) => (current === mode ? PLACEMENT_SLACK : 0)
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
anchor.bottom <= 0 ||
|
|
39
|
+
anchor.top >= viewport.height ||
|
|
40
|
+
anchor.right <= 0 ||
|
|
41
|
+
anchor.left >= viewport.width
|
|
42
|
+
) {
|
|
43
|
+
return { mode: 'hidden', x: 0, y: 0 }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const clampX = (width) =>
|
|
47
|
+
Math.max(VIEWPORT_INSET, Math.min(anchor.left, viewport.width - width - VIEWPORT_INSET))
|
|
48
|
+
|
|
49
|
+
const aboveY = anchor.top - GAP - bar.height
|
|
50
|
+
if (aboveY >= VIEWPORT_INSET - slack('above')) {
|
|
51
|
+
return { mode: 'above', x: clampX(bar.width), y: aboveY }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const belowY = anchor.bottom + GAP
|
|
55
|
+
if (belowY + bar.height <= viewport.height - VIEWPORT_INSET + slack('below')) {
|
|
56
|
+
return { mode: 'below', x: clampX(bar.width), y: belowY }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rightSpace = viewport.width - anchor.right - VIEWPORT_INSET
|
|
60
|
+
const leftSpace = anchor.left - VIEWPORT_INSET
|
|
61
|
+
const sides =
|
|
62
|
+
rightSpace >= leftSpace
|
|
63
|
+
? [['rail-right', rightSpace], ['rail-left', leftSpace]]
|
|
64
|
+
: [['rail-left', leftSpace], ['rail-right', rightSpace]]
|
|
65
|
+
|
|
66
|
+
for (const [mode, space] of sides) {
|
|
67
|
+
for (const gap of GAP_LADDER) {
|
|
68
|
+
if (rail.width + gap > space + slack(mode)) continue
|
|
69
|
+
const x =
|
|
70
|
+
mode === 'rail-right'
|
|
71
|
+
? Math.min(anchor.right + gap, viewport.width - rail.width - VIEWPORT_INSET)
|
|
72
|
+
: Math.max(anchor.left - gap - rail.width, VIEWPORT_INSET)
|
|
73
|
+
const maxY = Math.min(anchor.bottom, viewport.height - VIEWPORT_INSET) - rail.height
|
|
74
|
+
const y = Math.max(VIEWPORT_INSET, Math.min(Math.max(anchor.top, VIEWPORT_INSET), maxY))
|
|
75
|
+
return { mode, x, y, gap }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { mode: 'pinned', x: clampX(bar.width), y: VIEWPORT_INSET }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A handle sits ON its anchor's top-right corner, overlapping it, so it reads as
|
|
83
|
+
// belonging to that element rather than floating near it. No ladder: a handle is
|
|
84
|
+
// small enough that it always fits, and moving it around the anchor would make
|
|
85
|
+
// the page feel like it was shuffling under the pointer.
|
|
86
|
+
//
|
|
87
|
+
// The one thing it does need is the viewport clamp. A card flush against the
|
|
88
|
+
// right edge would otherwise put its handle half off-screen, and a heading at
|
|
89
|
+
// the very top of the document would put it above the fold.
|
|
90
|
+
//
|
|
91
|
+
// `prefer` picks which of the two placements below the caller wants:
|
|
92
|
+
// 'auto' adapts to the anchor's size, 'corner' always takes the corner.
|
|
93
|
+
export function placeHandle({ anchor, handle, viewport, inset = 6, prefer = 'auto' }) {
|
|
94
|
+
// Overlapping only reads as "belongs to this" when there is something left to
|
|
95
|
+
// belong to. On a 108x18 link a 36x31 handle covered 28% of it, including the
|
|
96
|
+
// last word — measured in a real browser. So a small anchor gets its handle
|
|
97
|
+
// set BESIDE it instead: still adjacent, nothing hidden.
|
|
98
|
+
//
|
|
99
|
+
// That rule is about a handle floating near a link. A control that BELONGS to
|
|
100
|
+
// a list row wants the opposite and asks for 'corner': measured in Chrome, a
|
|
101
|
+
// 31px strip beside a 36px card row landed wholly outside the row, and beside
|
|
102
|
+
// a run of 60px-pitch pills the strips covered each other. Such a caller pins
|
|
103
|
+
// the control to the corner and decides for itself whether the row is big
|
|
104
|
+
// enough to carry one at all. The clamp below is shared either way.
|
|
105
|
+
const roomy = anchor.height >= handle.height * 1.5 && anchor.width >= handle.width * 2
|
|
106
|
+
if (prefer === 'corner' || roomy) {
|
|
107
|
+
const x = Math.max(
|
|
108
|
+
VIEWPORT_INSET,
|
|
109
|
+
Math.min(anchor.right - handle.width + inset, viewport.width - handle.width - VIEWPORT_INSET)
|
|
110
|
+
)
|
|
111
|
+
const y = Math.max(
|
|
112
|
+
VIEWPORT_INSET,
|
|
113
|
+
Math.min(anchor.top - inset, viewport.height - handle.height - VIEWPORT_INSET)
|
|
114
|
+
)
|
|
115
|
+
return { x, y, mode: 'over' }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Beside: to the right when it fits, otherwise to the left, vertically
|
|
119
|
+
// centred on the anchor. Both are clamped, so a handle never leaves the
|
|
120
|
+
// viewport even for an anchor flush against an edge.
|
|
121
|
+
const gap = 4
|
|
122
|
+
const fitsRight = anchor.right + gap + handle.width <= viewport.width - VIEWPORT_INSET
|
|
123
|
+
const rawX = fitsRight ? anchor.right + gap : anchor.left - gap - handle.width
|
|
124
|
+
// Only the left edge needs clamping. fitsRight is the test for "the right
|
|
125
|
+
// placement is inside the viewport", so the right branch is already in bounds
|
|
126
|
+
// by construction; the left branch can run off a narrow anchor's left side.
|
|
127
|
+
const x = Math.max(VIEWPORT_INSET, rawX)
|
|
128
|
+
const centred = anchor.top + anchor.height / 2 - handle.height / 2
|
|
129
|
+
const y = Math.max(VIEWPORT_INSET, Math.min(centred, viewport.height - handle.height - VIEWPORT_INSET))
|
|
130
|
+
return { x, y, mode: fitsRight ? 'beside-right' : 'beside-left' }
|
|
131
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The optional platform hypercms rides: the mutation hub, undo, the save-prepare
|
|
2
|
+
// hook, rich text, and the two upload helpers. Every one degrades to null, so the
|
|
3
|
+
// CMS still runs in a bare HTML file with no client at all.
|
|
4
|
+
//
|
|
5
|
+
// TWO clients provide this and they disagree on names. hyperclayjs owns
|
|
6
|
+
// window.hyperclay; clayjs owns window.clay and renamed things on the way past
|
|
7
|
+
// (onPrepareForSave -> addDocumentTransform, consent -> confirm, uploadFileBasic ->
|
|
8
|
+
// upload). So this is a capability table rather than a namespace pointer:
|
|
9
|
+
// `window.clay ?? window.hyperclay` would resolve to clay, find no
|
|
10
|
+
// onPrepareForSave, and silently stop stripping the CMS's own chrome out of every
|
|
11
|
+
// save.
|
|
12
|
+
//
|
|
13
|
+
// Reads happen per call, never cached. A client can install a capability after
|
|
14
|
+
// hypercms evaluates — that is the whole reason whenReady() below exists.
|
|
15
|
+
const CAPABILITIES = {
|
|
16
|
+
Mutation: (clay, hyperclay) => clay?.Mutation ?? hyperclay?.Mutation,
|
|
17
|
+
undo: (clay, hyperclay) => clay?.undo ?? hyperclay?.undo,
|
|
18
|
+
onPrepareForSave: (clay, hyperclay) => clay?.addDocumentTransform ?? hyperclay?.onPrepareForSave,
|
|
19
|
+
// Distinct from onPrepareForSave above, and the difference is the whole point:
|
|
20
|
+
// onPrepareForSave is save-only, so a hook there runs AFTER live sync has
|
|
21
|
+
// already broadcast the document. onSnapshot runs on every snapshot, save and
|
|
22
|
+
// sync alike, which is the only place a cleanup can sit and be seen by both.
|
|
23
|
+
onSnapshot: (clay, hyperclay) => clay?.onSnapshot ?? hyperclay?.onSnapshot,
|
|
24
|
+
consent: (clay, hyperclay) => clay?.confirm ?? hyperclay?.consent,
|
|
25
|
+
RichClay: (clay, hyperclay) => clay?.RichClay ?? hyperclay?.RichClay,
|
|
26
|
+
quickcrop: (clay, hyperclay) => clay?.quickcrop ?? hyperclay?.quickcrop,
|
|
27
|
+
upload: (clay, hyperclay) =>
|
|
28
|
+
clay?.upload ?? (hyperclay?.uploadFileBasic ? adaptLegacyUpload(hyperclay.uploadFileBasic) : null),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The one capability where the two clients differ in SHAPE and not just in name,
|
|
32
|
+
// so it is normalized here rather than at the call site. clay.upload answers one
|
|
33
|
+
// envelope for every outcome and never rejects; uploadFileBasic resolves on
|
|
34
|
+
// success and rejects on failure. A pick handler that had to know which client it
|
|
35
|
+
// was riding would grow two of every branch, and the branch that matters most is
|
|
36
|
+
// the one a rejection reads wrong: "this host does not store files" is not a
|
|
37
|
+
// failure, and a caller that catches it as one stops when it should have embedded.
|
|
38
|
+
//
|
|
39
|
+
// The legacy client has no cancellation, so `signal` is honored on the near side
|
|
40
|
+
// only: an upload already in flight runs to completion, and its result is
|
|
41
|
+
// discarded rather than written. That is the same outcome the caller's own
|
|
42
|
+
// ctx.closed check produces, one branch earlier.
|
|
43
|
+
const LEGACY_STATUS_CODES = {
|
|
44
|
+
402: 'payment-required',
|
|
45
|
+
413: 'too-large',
|
|
46
|
+
415: 'unsupported-type',
|
|
47
|
+
401: 'unauthorized',
|
|
48
|
+
403: 'forbidden',
|
|
49
|
+
404: 'not-found',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const aborted = () => ({ ok: false, msg: 'Upload cancelled', msgType: 'skipped', code: 'aborted', uploads: [] })
|
|
53
|
+
|
|
54
|
+
function adaptLegacyUpload(uploadFileBasic) {
|
|
55
|
+
return async function upload(file, { onProgress, signal } = {}) {
|
|
56
|
+
if (signal?.aborted) return aborted()
|
|
57
|
+
try {
|
|
58
|
+
// uploadFileBasic reports a bare percent; clay reports {loaded,total,percent}.
|
|
59
|
+
// Callers draw from `percent`, which is the only field both can supply.
|
|
60
|
+
const res = await uploadFileBasic(file, {
|
|
61
|
+
onProgress: (percent) => { onProgress?.({ loaded: null, total: null, percent }) },
|
|
62
|
+
})
|
|
63
|
+
if (signal?.aborted) return aborted()
|
|
64
|
+
const uploads = (res && res.uploads) || []
|
|
65
|
+
if (typeof uploads[0]?.url !== 'string') {
|
|
66
|
+
return { ok: false, msg: 'The host accepted the file but did not say where it put it', msgType: 'error', code: 'bad-response', uploads: [] }
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, msg: res.msg || 'Uploaded', msgType: res.msgType || 'success', code: res.code || null, uploads }
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (signal?.aborted) return aborted()
|
|
71
|
+
// The rejection carries the HTTP status and the raw body, so the host's own
|
|
72
|
+
// code survives the round trip and the caller branches on the reason rather
|
|
73
|
+
// than pattern-matching a message.
|
|
74
|
+
let body = {}
|
|
75
|
+
try { body = JSON.parse(err?.response || '{}') } catch { body = {} }
|
|
76
|
+
const code = body.code || LEGACY_STATUS_CODES[err?.status] || 'error'
|
|
77
|
+
return { ok: false, msg: (err && err.message) || 'Upload failed', msgType: 'error', code, uploads: [] }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Pass `win` when the caller already holds a realm (a doc.defaultView from a form
|
|
83
|
+
// mounted into another document); it defaults to the ambient one.
|
|
84
|
+
export function platform(name, win) {
|
|
85
|
+
const read = CAPABILITIES[name]
|
|
86
|
+
if (!read) throw new Error(`hypercms: unknown platform capability "${name}"`)
|
|
87
|
+
const scope = win || (typeof window !== 'undefined' ? window : null)
|
|
88
|
+
if (!scope) return null
|
|
89
|
+
return read(scope.clay, scope.hyperclay) || null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Both spellings of the same signal. Each client fires exactly one name: clayjs
|
|
93
|
+
// its own, hyperclayjs the legacy one. Listening for both is what lets one build
|
|
94
|
+
// of hypercms ride either client. The name-keyed guard below stays regardless, so
|
|
95
|
+
// a listener on both names still can't double-fire if a client ever dispatches
|
|
96
|
+
// the pair again.
|
|
97
|
+
export const MUTATION_READY = ['clay:mutation-ready', 'hyperclay:mutation-ready']
|
|
98
|
+
export const LIVESYNC_APPLIED = ['clay:sync-applied', 'hyperclay:livesync-applied']
|
|
99
|
+
|
|
100
|
+
// The readiness pair both clients publish once their public surface is
|
|
101
|
+
// assembled. clayjs fires clay:ready from its loader; hyperclayjs fires
|
|
102
|
+
// hyperclay:ready from exportToWindow. This is the handshake richclay uses for
|
|
103
|
+
// the same race, and it is the only reliable retry signal for a hook whose
|
|
104
|
+
// capability arrives later than hypercms does.
|
|
105
|
+
export const PLATFORM_READY = ['clay:ready', 'hyperclay:ready']
|
|
106
|
+
|
|
107
|
+
// Subscribe to every spelling of one signal, delivering the handler ONCE per
|
|
108
|
+
// occurrence. No client dispatches the pair today, but one that did would make a
|
|
109
|
+
// plain listener on each name fire the handler twice for one event: two form
|
|
110
|
+
// re-extracts, or two open() calls.
|
|
111
|
+
//
|
|
112
|
+
// The first name to arrive claims the tick, and only a DIFFERENT name is suppressed
|
|
113
|
+
// while it holds the claim. That distinction is what makes this exact rather than a
|
|
114
|
+
// heuristic: no client dispatches one name twice for one occurrence, so a repeat of
|
|
115
|
+
// the claiming name is always a second occurrence and always gets through — even
|
|
116
|
+
// when it lands in the same tick, which is precisely what a caller doing two
|
|
117
|
+
// synchronous applies looks like. Guarding on "have I run at all this tick" would
|
|
118
|
+
// swallow that.
|
|
119
|
+
//
|
|
120
|
+
// Returns an unsubscribe that removes every name.
|
|
121
|
+
export function onPlatformEvent(target, names, handler) {
|
|
122
|
+
let claimedBy = null
|
|
123
|
+
const wrapped = (event) => {
|
|
124
|
+
if (claimedBy !== null && claimedBy !== event.type) return
|
|
125
|
+
claimedBy = event.type
|
|
126
|
+
queueMicrotask(() => { claimedBy = null })
|
|
127
|
+
handler(event)
|
|
128
|
+
}
|
|
129
|
+
for (const name of names) target.addEventListener(name, wrapped)
|
|
130
|
+
return () => { for (const name of names) target.removeEventListener(name, wrapped) }
|
|
131
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { engine } from 'hyper-html-api'
|
|
2
|
+
import { buildForm } from './form-builder.js'
|
|
3
|
+
import { morphForm } from './morph.js'
|
|
4
|
+
import { injectDefaults, injectComponents } from './templates.js'
|
|
5
|
+
import { deriveFormRules } from './form-rules.js'
|
|
6
|
+
import { coerceBooleans, applyErrorState } from './events.js'
|
|
7
|
+
import { findUnresolved, applyUnresolvedState, stripReadOnly } from './unresolved.js'
|
|
8
|
+
import { enhanceFields } from './enhance.js'
|
|
9
|
+
import { platform } from './platform.js'
|
|
10
|
+
import { rowIdentitySeeder } from './row-identity.js'
|
|
11
|
+
|
|
12
|
+
const ENGINE_OPTS = { skip: '[data-hcms-shell]', templateAttr: 'cms-template' }
|
|
13
|
+
|
|
14
|
+
export function refreshForm(ctx, { ignoreActiveValue } = {}) {
|
|
15
|
+
// Re-resolve rules every refresh, source-aware: an object source returns the
|
|
16
|
+
// same literal (tagNode null); a token source re-resolves the tag so a
|
|
17
|
+
// livesync-replaced rules tag is picked up. Document-scoped via ctx.doc.
|
|
18
|
+
const found = engine.findRules(ctx.doc, ctx.rulesSource || 'cms')
|
|
19
|
+
if (found) {
|
|
20
|
+
// Re-apply the rule upgrade open() performed: the tag holds the author's
|
|
21
|
+
// bare rules, and using them raw would rebind upgraded fields back to
|
|
22
|
+
// textContent mid-session. Route it through the SAME seam open() used
|
|
23
|
+
// (session.js hands found.rules to view.prepareRules) rather than
|
|
24
|
+
// hardcoding one view's upgrade here — that undid a wider upgrade on the
|
|
25
|
+
// first refresh, silently flattening rich text back to plain.
|
|
26
|
+
ctx.pageRules = ctx.view.prepareRules(found.rules)
|
|
27
|
+
ctx.rulesTagNode = found.tagNode
|
|
28
|
+
}
|
|
29
|
+
// Re-derive formRules so schema changes (rule shape, template overrides) flow through.
|
|
30
|
+
injectDefaults(ctx.doc)
|
|
31
|
+
injectComponents(ctx.doc, ctx.pageRules)
|
|
32
|
+
ctx.formRules = deriveFormRules(ctx.pageRules, ctx.doc)
|
|
33
|
+
ctx.writeRules = stripReadOnly(ctx.pageRules)
|
|
34
|
+
// Before the extract below, for the same readable-failure reason as open().
|
|
35
|
+
ctx.unresolved = findUnresolved(ctx.pageRoot, ctx.pageRules)
|
|
36
|
+
|
|
37
|
+
// The morph below can rebuild a form row, which drops its binding to the page
|
|
38
|
+
// row it stood for. Read the pairing off this extract and re-establish it once
|
|
39
|
+
// the morph has settled, so the first operation after a refresh is not left
|
|
40
|
+
// guessing.
|
|
41
|
+
const seeder = rowIdentitySeeder()
|
|
42
|
+
const newData = coerceBooleans(
|
|
43
|
+
engine.extract(ctx.pageRoot, ctx.pageRules, { ...ENGINE_OPTS, ...seeder.hooks }),
|
|
44
|
+
ctx.pageRules
|
|
45
|
+
)
|
|
46
|
+
const fragment = buildForm({
|
|
47
|
+
pageRules: ctx.pageRules,
|
|
48
|
+
formRules: ctx.formRules,
|
|
49
|
+
data: newData,
|
|
50
|
+
doc: ctx.doc,
|
|
51
|
+
})
|
|
52
|
+
morphForm(ctx.formRoot, fragment, { ignoreActiveValue })
|
|
53
|
+
seeder.seed(ctx.formRoot)
|
|
54
|
+
// The morph can rebuild field nodes, dropping autosize heights and richclay
|
|
55
|
+
// instances (instances are per-element); re-enhance whatever is new.
|
|
56
|
+
enhanceFields(ctx.formRoot, ctx.doc, ctx.view?.enhanceFormRichText !== false)
|
|
57
|
+
// morphForm rebuilds the form structure; inline error slots get wiped. Re-apply
|
|
58
|
+
// the last error state so error messages survive across observer-driven refreshes
|
|
59
|
+
// (e.g., the rollback after a failed add).
|
|
60
|
+
applyErrorState(ctx)
|
|
61
|
+
applyUnresolvedState(ctx)
|
|
62
|
+
// Update fingerprint so subsequent commits against the new page state compare correctly.
|
|
63
|
+
if (ctx.updateFingerprint) ctx.updateFingerprint()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function installObserver({ debounce = 100, onRefresh }) {
|
|
67
|
+
const M = platform('Mutation')
|
|
68
|
+
if (!M || typeof M.onAnyChange !== 'function') {
|
|
69
|
+
throw new Error('hypercms: a mutation hub is required (clay.Mutation or hyperclay.Mutation). Load clayjs or hyperclayjs, or just the mutation utility, before initializing hypercms.')
|
|
70
|
+
}
|
|
71
|
+
let paused = 0
|
|
72
|
+
// The hub hands every subscriber the batch of changes it collected
|
|
73
|
+
// ({ type: 'add' | 'remove' | 'attribute', element, parent, attribute,
|
|
74
|
+
// oldValue, newValue }, see clayjs/src/lib/mutation.js). Discarding them was
|
|
75
|
+
// fine while the only view was the sidebar, which refreshes wholesale. A view
|
|
76
|
+
// that edits the page in place needs to know whether a batch is its own doing,
|
|
77
|
+
// so pass them on and let the view decide.
|
|
78
|
+
const unsub = M.onAnyChange({ debounce }, (changes) => {
|
|
79
|
+
if (paused > 0) return
|
|
80
|
+
onRefresh(changes)
|
|
81
|
+
})
|
|
82
|
+
return {
|
|
83
|
+
unsubscribe: typeof unsub === 'function' ? unsub : () => {},
|
|
84
|
+
pause() { paused++ },
|
|
85
|
+
resume() { paused = Math.max(0, paused - 1) },
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// hypercms's bridge to richclay: how it finds the class, and how it cleans up
|
|
2
|
+
// after the instances it creates.
|
|
3
|
+
//
|
|
4
|
+
// The cleanup exists because hypercms constructs `new RichClay(el, { hyperclay:
|
|
5
|
+
// false })`, and richclay installs its own save bridge only when that option is
|
|
6
|
+
// true (`richclay/src/richclay.js:135`). So on a page whose only richclay usage
|
|
7
|
+
// is hypercms's, NOTHING strips the editor's runtime state out of the document:
|
|
8
|
+
// a contenteditable heading reaches the file and stays editable forever.
|
|
9
|
+
// hypercms created those instances, so hypercms cleans them up.
|
|
10
|
+
|
|
11
|
+
import { platform } from './platform.js'
|
|
12
|
+
|
|
13
|
+
// Written on the live page element at bind time, removed from the snapshot
|
|
14
|
+
// clone. It is the only way to tell an element hypercms made rich from one the
|
|
15
|
+
// author marked up themselves: element identity does not survive cloneNode, so
|
|
16
|
+
// an in-memory Set cannot answer the question on the clone the hook receives.
|
|
17
|
+
//
|
|
18
|
+
// Its VALUE is whether the element held markup before richclay touched it:
|
|
19
|
+
// 'rich' or 'plain'. That has to be frozen here, because Squire wraps the
|
|
20
|
+
// content of a block it binds in a <div>, so from the first click the live DOM
|
|
21
|
+
// can no longer answer whether the author wrote markup in this element.
|
|
22
|
+
export const BOUND_ATTR = 'data-hcms-bound'
|
|
23
|
+
|
|
24
|
+
// The binding's own id, written on the live element and carried into the clone
|
|
25
|
+
// by cloneNode. It is how the save hook, holding a clone, finds the live binding
|
|
26
|
+
// that knows what the author's markup was before richclay rewrote it. Identity
|
|
27
|
+
// cannot cross cloneNode, and the markup is far too big to ride in an attribute.
|
|
28
|
+
export const BOUND_ID_ATTR = 'data-hcms-bound-id'
|
|
29
|
+
|
|
30
|
+
// id -> the live binding. Populated at bind, dropped at unbind, read only by the
|
|
31
|
+
// save hook. Holding the binding rather than a copy of its markup is deliberate:
|
|
32
|
+
// whether the restore should happen at all is a question about the binding's
|
|
33
|
+
// state right now, not about its state when it was created.
|
|
34
|
+
const boundById = new Map()
|
|
35
|
+
|
|
36
|
+
export function registerBinding(id, binding) {
|
|
37
|
+
boundById.set(id, binding)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function releaseBinding(id) {
|
|
41
|
+
boundById.delete(id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The author's markup for a clone element, or null when the clone must be left
|
|
45
|
+
// exactly as it stands. Null covers all three reasons: no live binding behind
|
|
46
|
+
// it, an editor the author owns, and any content this session has actually
|
|
47
|
+
// changed. See restorable() in inline.js, which asks the same question of the
|
|
48
|
+
// live element at teardown.
|
|
49
|
+
export function pristineMarkupFor(cloneEl) {
|
|
50
|
+
const id = cloneEl.getAttribute(BOUND_ID_ATTR)
|
|
51
|
+
if (!id) return null
|
|
52
|
+
const binding = boundById.get(id)
|
|
53
|
+
if (!binding || !binding.restorable()) return null
|
|
54
|
+
return binding.originalHTML
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Written beside the marker when hypercms's own construct call is what put
|
|
58
|
+
// data-richclay on the element. richclay 0.5.0 tracks the same provenance in
|
|
59
|
+
// data-richclay-runtime-marker and strips the attribute for us, but clayjs and
|
|
60
|
+
// hyperclayjs both vendor 0.4.0, which has no marker and leaves it in the file.
|
|
61
|
+
// data-richclay is richclay's mount selector, so left there it turns every
|
|
62
|
+
// heading anyone clicked into a permanent richclay region. hypercms knew the
|
|
63
|
+
// answer before it touched the element; it does not need to ask.
|
|
64
|
+
export const RC_OWNED_ATTR = 'data-hcms-owns-richclay'
|
|
65
|
+
|
|
66
|
+
export function resolveRichClay(win) {
|
|
67
|
+
return (
|
|
68
|
+
(win && win.richclay && win.richclay.RichClay) ||
|
|
69
|
+
platform('RichClay', win) ||
|
|
70
|
+
(win && typeof win.RichClay === 'function' ? win.RichClay : null)
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function markBound(el, heldMarkup, richClayIsOurs) {
|
|
75
|
+
if (!el || typeof el.setAttribute !== 'function') return
|
|
76
|
+
el.setAttribute(BOUND_ATTR, heldMarkup ? 'rich' : 'plain')
|
|
77
|
+
if (richClayIsOurs) el.setAttribute(RC_OWNED_ATTR, 'true')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ⚠ THE ORDER IS LOAD-BEARING. richclay's strip iterates
|
|
81
|
+
// "[data-richclay], [richclay], [editable], [clay-editable]". Remove
|
|
82
|
+
// data-richclay first and a bare <h1> stops matching that selector, the strip
|
|
83
|
+
// skips it, and every runtime attribute richclay wrote reaches the file. Strip
|
|
84
|
+
// FIRST, unmark second. Both orders were run and the bytes compared; the other
|
|
85
|
+
// one ships a permanently contenteditable heading.
|
|
86
|
+
export function cleanRichClayFromSnapshot(cloneDocEl, win) {
|
|
87
|
+
if (!cloneDocEl || typeof cloneDocEl.querySelectorAll !== 'function') return
|
|
88
|
+
const bound = cloneDocEl.querySelectorAll(`[${BOUND_ATTR}]`)
|
|
89
|
+
if (!bound.length) return
|
|
90
|
+
|
|
91
|
+
const RichClay = resolveRichClay(win)
|
|
92
|
+
if (RichClay && typeof RichClay.stripFromClone === 'function') {
|
|
93
|
+
try {
|
|
94
|
+
// A second pass over an already-stripped clone is string-identical, so
|
|
95
|
+
// this stays correct on a page that also uses richclay directly and has
|
|
96
|
+
// richclay's own bridge installed.
|
|
97
|
+
RichClay.stripFromClone(cloneDocEl)
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.warn('[hypercms] richclay strip failed; editor state may reach the save', err)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const el of bound) {
|
|
103
|
+
// Whether data-richclay comes off is decided by who put it there, not by
|
|
104
|
+
// which richclay is loaded. On 0.5.0 stripFromClone has already removed it
|
|
105
|
+
// and this is a no-op; on the 0.4.0 copies clayjs and hyperclayjs vendor,
|
|
106
|
+
// this is the only thing that removes it. An author's own opt-in never
|
|
107
|
+
// carries the marker, so it is never touched.
|
|
108
|
+
if (el.getAttribute(RC_OWNED_ATTR) === 'true') el.removeAttribute('data-richclay')
|
|
109
|
+
el.removeAttribute(RC_OWNED_ATTR)
|
|
110
|
+
el.removeAttribute(BOUND_ATTR)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A page element that carries the CMS marker but has no binding behind it: a
|
|
115
|
+
// clone the engine made of a bound row, or of a bound element it replaced while
|
|
116
|
+
// rolling back a failed apply. destroy() cannot reach it — destroy belongs to an
|
|
117
|
+
// instance and a clone has none — so it is left permanently editable with page
|
|
118
|
+
// undo switched off for it. The snapshot hook still cleans the saved file; this
|
|
119
|
+
// is the live page.
|
|
120
|
+
export function stripOrphanEditorState(el, win) {
|
|
121
|
+
if (!el || typeof el.removeAttribute !== 'function') return
|
|
122
|
+
const RichClay = resolveRichClay(win)
|
|
123
|
+
if (RichClay && typeof RichClay.stripElement === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
RichClay.stripElement(el)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.warn('[hypercms] richclay element strip failed; the clone stays editable', err)
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// An older richclay with no per-element strip. These two are what actually
|
|
131
|
+
// harm the live page; the rest is cosmetic and the snapshot hook removes it
|
|
132
|
+
// from the file either way.
|
|
133
|
+
el.removeAttribute('contenteditable')
|
|
134
|
+
el.removeAttribute('no-undo')
|
|
135
|
+
}
|
|
136
|
+
if (el.getAttribute(RC_OWNED_ATTR) === 'true') el.removeAttribute('data-richclay')
|
|
137
|
+
el.removeAttribute(RC_OWNED_ATTR)
|
|
138
|
+
el.removeAttribute(BOUND_ATTR)
|
|
139
|
+
el.removeAttribute(BOUND_ID_ATTR)
|
|
140
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row identity supplied to the engine, so two byte-identical rows can still be
|
|
3
|
+
* told apart on a remove or a reorder.
|
|
4
|
+
*
|
|
5
|
+
* The engine matches rows by content, which is exact for every operation the
|
|
6
|
+
* form emits except one: when two rows read identically, "remove row 0" and
|
|
7
|
+
* "remove row 1" produce the same array, and no score can separate them. The
|
|
8
|
+
* form can, because it holds a handle the engine never sees, its own row
|
|
9
|
+
* elements.
|
|
10
|
+
*
|
|
11
|
+
* Those elements are stable across the operations that matter. A keystroke
|
|
12
|
+
* never rebuilds the form (applyWithRollback pauses the mutation observer
|
|
13
|
+
* around apply), and add, remove and move mutate the form DOM in place rather
|
|
14
|
+
* than re-rendering it. By the time commit() runs the form is already in its
|
|
15
|
+
* new shape, so walking its rows in order gives the new item order with each
|
|
16
|
+
* row still carrying the page node it came from. The permutation falls out.
|
|
17
|
+
*
|
|
18
|
+
* refreshForm does re-render the form, through hyper-morph, when the page
|
|
19
|
+
* changes underneath us (livesync, undo, another session). Rows rebuilt there
|
|
20
|
+
* have no entry, identifyRows returns null for them, and the engine's matcher
|
|
21
|
+
* takes over. onRowsApplied re-establishes the pairing on the next apply. That
|
|
22
|
+
* is correct rather than merely tolerable: the form was just built from a fresh
|
|
23
|
+
* read of the page, so index correspondence is exact at that instant.
|
|
24
|
+
*/
|
|
25
|
+
const boundNode = new WeakMap()
|
|
26
|
+
|
|
27
|
+
// The engine's trace path as the form writes it in data-hcms-path.
|
|
28
|
+
const pathKey = (path) => path.map(String).join('.')
|
|
29
|
+
|
|
30
|
+
// Rule keys reach the selector verbatim, and a key holding a quote makes
|
|
31
|
+
// querySelector throw. Every other path-into-selector site in this package
|
|
32
|
+
// escapes the same way; this is that function, kept module-local like the rest.
|
|
33
|
+
function cssEscape(value) {
|
|
34
|
+
if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(value)
|
|
35
|
+
return String(value).replace(/[^a-zA-Z0-9_\-.*]/g, (c) => '\\' + c)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The form rows for one list, in document order, or null when the form has no
|
|
39
|
+
// such list. Keys look like "products" or "products.0.variants".
|
|
40
|
+
function formRowsAt(formRoot, key) {
|
|
41
|
+
if (!formRoot || !formRoot.querySelector) return null
|
|
42
|
+
const container = formRoot.querySelector(`[data-hcms-path="${cssEscape(key)}"]`)
|
|
43
|
+
const slot = container && container.querySelector('.hcms-array-items')
|
|
44
|
+
if (!slot) return null
|
|
45
|
+
// Direct children, filtered, rather than `:scope > ...`. jsdom's selector
|
|
46
|
+
// engine resolves `:scope >` against the document rather than the slot and
|
|
47
|
+
// returns descendants, so a page with nested lists reported the inner rows as
|
|
48
|
+
// rows of the outer list. The count then failed to match and identity silently
|
|
49
|
+
// fell back to content matching in exactly the shape this exists for.
|
|
50
|
+
return Array.from(slot.children).filter(
|
|
51
|
+
(el) => el.matches && el.matches('[data-hcms-card], [data-hcms-array-item]'),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function bind(formRoot, key, nodes) {
|
|
56
|
+
const rows = formRowsAt(formRoot, key)
|
|
57
|
+
if (!rows || rows.length !== nodes.length) return
|
|
58
|
+
rows.forEach((row, i) => {
|
|
59
|
+
if (nodes[i]) boundNode.set(row, nodes[i])
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Establish identity from the extract the form is built from, before any apply
|
|
65
|
+
* has happened.
|
|
66
|
+
*
|
|
67
|
+
* Without this the map is populated only by onRowsApplied, so the FIRST
|
|
68
|
+
* structural operation of a session has nothing to go on and falls back to
|
|
69
|
+
* content matching. Pass `hooks` to the top-level `engine.extract` that feeds
|
|
70
|
+
* buildForm, then call `seed(formRoot)` once the form rows exist. At that
|
|
71
|
+
* moment row i of the form is row i of the page by construction, because the
|
|
72
|
+
* form was built from exactly this read.
|
|
73
|
+
*
|
|
74
|
+
* Never pass these hooks to `apply`: listDiff re-extracts each existing row
|
|
75
|
+
* with a fresh path, which would report nested lists under a truncated one.
|
|
76
|
+
*/
|
|
77
|
+
export function rowIdentitySeeder() {
|
|
78
|
+
const readNodes = new Map()
|
|
79
|
+
return {
|
|
80
|
+
hooks: {
|
|
81
|
+
onRowsRead(path, nodes) {
|
|
82
|
+
readNodes.set(pathKey(path), nodes)
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
seed(formRoot) {
|
|
86
|
+
for (const [key, nodes] of readNodes) bind(formRoot, key, nodes)
|
|
87
|
+
readNodes.clear()
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function rowIdentityHooks(formRoot) {
|
|
93
|
+
return {
|
|
94
|
+
identifyRows(path, items) {
|
|
95
|
+
const rows = formRowsAt(formRoot, pathKey(path))
|
|
96
|
+
// A count mismatch means the form and the data disagree about this list,
|
|
97
|
+
// so the form is not a trustworthy witness for it. Say nothing and let
|
|
98
|
+
// the engine match.
|
|
99
|
+
if (!rows || rows.length !== items.length) return null
|
|
100
|
+
return rows.map((row) => boundNode.get(row) || null)
|
|
101
|
+
},
|
|
102
|
+
onRowsApplied(path, nodes) {
|
|
103
|
+
bind(formRoot, pathKey(path), nodes)
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Testing seam only: the WeakMap is module state and a test needs to observe it.
|
|
109
|
+
export function boundNodeFor(formRow) {
|
|
110
|
+
return boundNode.get(formRow) || null
|
|
111
|
+
}
|