@flood-md/agent 0.2.9 → 0.2.11
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/DESIGN.md +33 -8
- package/README.md +35 -4
- package/SKILL.md +42 -0
- package/dist/catalog.json +8716 -0
- package/dist/materials/component-accordion.md +42 -0
- package/dist/materials/component-alert-dialog.md +46 -0
- package/dist/materials/component-alert.md +41 -0
- package/dist/materials/component-app-shell.md +47 -0
- package/dist/materials/component-app-top-bar.md +36 -0
- package/dist/materials/component-aspect-ratio.md +36 -0
- package/dist/materials/component-autocomplete.md +54 -0
- package/dist/materials/component-avatar-group.md +39 -0
- package/dist/materials/component-avatar.md +40 -0
- package/dist/materials/component-badge.md +37 -0
- package/dist/materials/component-blockquote.md +35 -0
- package/dist/materials/component-breadcrumb.md +36 -0
- package/dist/materials/component-bulk-action-bar.md +52 -0
- package/dist/materials/component-button-group.md +39 -0
- package/dist/materials/component-button.md +44 -0
- package/dist/materials/component-calendar.md +59 -0
- package/dist/materials/component-card.md +46 -0
- package/dist/materials/component-center.md +39 -0
- package/dist/materials/component-character-count.md +42 -0
- package/dist/materials/component-check-card.md +39 -0
- package/dist/materials/component-checkbox-group.md +48 -0
- package/dist/materials/component-checkbox.md +37 -0
- package/dist/materials/component-code-snippet.md +37 -0
- package/dist/materials/component-collapsible.md +41 -0
- package/dist/materials/component-collection-toolbar.md +68 -0
- package/dist/materials/component-collection-view-switcher.md +49 -0
- package/dist/materials/component-color-picker.md +49 -0
- package/dist/materials/component-color-swatch.md +35 -0
- package/dist/materials/component-combobox.md +60 -0
- package/dist/materials/component-command-list.md +48 -0
- package/dist/materials/component-command-palette.md +53 -0
- package/dist/materials/component-container.md +36 -0
- package/dist/materials/component-context-menu.md +43 -0
- package/dist/materials/component-copy-button.md +43 -0
- package/dist/materials/component-data-list-item.md +40 -0
- package/dist/materials/component-data-list.md +38 -0
- package/dist/materials/component-data-table.md +70 -0
- package/dist/materials/component-date-field.md +57 -0
- package/dist/materials/component-date-picker.md +67 -0
- package/dist/materials/component-date-range-field.md +62 -0
- package/dist/materials/component-date-range-picker.md +73 -0
- package/dist/materials/component-date-time-picker.md +62 -0
- package/dist/materials/component-date-time-range-picker.md +70 -0
- package/dist/materials/component-description-list.md +42 -0
- package/dist/materials/component-dialog.md +45 -0
- package/dist/materials/component-divider.md +37 -0
- package/dist/materials/component-drawer.md +51 -0
- package/dist/materials/component-dropzone.md +45 -0
- package/dist/materials/component-editable.md +57 -0
- package/dist/materials/component-empty-state.md +41 -0
- package/dist/materials/component-error-summary.md +40 -0
- package/dist/materials/component-field.md +46 -0
- package/dist/materials/component-fieldset.md +46 -0
- package/dist/materials/component-figure.md +36 -0
- package/dist/materials/component-file-input.md +45 -0
- package/dist/materials/component-file-item.md +48 -0
- package/dist/materials/component-file-list.md +39 -0
- package/dist/materials/component-file-meta.md +36 -0
- package/dist/materials/component-filter-condition.md +53 -0
- package/dist/materials/component-filter-group.md +55 -0
- package/dist/materials/component-flood-root.md +33 -0
- package/dist/materials/component-grid.md +38 -0
- package/dist/materials/component-heading.md +38 -0
- package/dist/materials/component-hover-card.md +48 -0
- package/dist/materials/component-icon-button.md +43 -0
- package/dist/materials/component-icon-slot.md +37 -0
- package/dist/materials/component-image-preview.md +50 -0
- package/dist/materials/component-image.md +37 -0
- package/dist/materials/component-inline-code.md +36 -0
- package/dist/materials/component-inline.md +39 -0
- package/dist/materials/component-input-group.md +43 -0
- package/dist/materials/component-input.md +39 -0
- package/dist/materials/component-kbd.md +34 -0
- package/dist/materials/component-label.md +37 -0
- package/dist/materials/component-link.md +35 -0
- package/dist/materials/component-list-box.md +57 -0
- package/dist/materials/component-list.md +40 -0
- package/dist/materials/component-menu.md +54 -0
- package/dist/materials/component-menubar.md +41 -0
- package/dist/materials/component-meter.md +43 -0
- package/dist/materials/component-metric.md +39 -0
- package/dist/materials/component-multi-select.md +62 -0
- package/dist/materials/component-navigation-group.md +51 -0
- package/dist/materials/component-navigation-item.md +59 -0
- package/dist/materials/component-navigation-list.md +38 -0
- package/dist/materials/component-navigation-tree.md +51 -0
- package/dist/materials/component-number-input.md +47 -0
- package/dist/materials/component-overflow-actions.md +47 -0
- package/dist/materials/component-page-header.md +40 -0
- package/dist/materials/component-pagination.md +44 -0
- package/dist/materials/component-password-input.md +43 -0
- package/dist/materials/component-pin-input.md +45 -0
- package/dist/materials/component-popover.md +48 -0
- package/dist/materials/component-progress.md +42 -0
- package/dist/materials/component-radio-card.md +43 -0
- package/dist/materials/component-radio-group.md +49 -0
- package/dist/materials/component-radio.md +39 -0
- package/dist/materials/component-range-calendar.md +58 -0
- package/dist/materials/component-rating.md +46 -0
- package/dist/materials/component-resizable-panels.md +48 -0
- package/dist/materials/component-scroll-area.md +42 -0
- package/dist/materials/component-search-input.md +40 -0
- package/dist/materials/component-section.md +42 -0
- package/dist/materials/component-segmented-control.md +53 -0
- package/dist/materials/component-select.md +45 -0
- package/dist/materials/component-sidebar.md +64 -0
- package/dist/materials/component-skeleton.md +42 -0
- package/dist/materials/component-skip-link.md +37 -0
- package/dist/materials/component-slider.md +51 -0
- package/dist/materials/component-spinner.md +37 -0
- package/dist/materials/component-split.md +40 -0
- package/dist/materials/component-stack.md +37 -0
- package/dist/materials/component-status-indicator.md +38 -0
- package/dist/materials/component-steps.md +40 -0
- package/dist/materials/component-surface.md +37 -0
- package/dist/materials/component-switch.md +35 -0
- package/dist/materials/component-table.md +46 -0
- package/dist/materials/component-tabs.md +45 -0
- package/dist/materials/component-tag-input.md +46 -0
- package/dist/materials/component-tag.md +40 -0
- package/dist/materials/component-text.md +39 -0
- package/dist/materials/component-textarea.md +38 -0
- package/dist/materials/component-time-field.md +56 -0
- package/dist/materials/component-time-picker.md +60 -0
- package/dist/materials/component-timeline.md +38 -0
- package/dist/materials/component-toast-region.md +42 -0
- package/dist/materials/component-toggle-group.md +44 -0
- package/dist/materials/component-toggle.md +40 -0
- package/dist/materials/component-toolbar.md +46 -0
- package/dist/materials/component-tooltip.md +40 -0
- package/dist/materials/component-tree-select.md +56 -0
- package/dist/materials/component-tree.md +52 -0
- package/dist/materials/component-upload-item.md +52 -0
- package/dist/materials/component-upload-list.md +40 -0
- package/dist/materials/component-visually-hidden.md +37 -0
- package/dist/materials/guide-agent-knowledge.md +95 -0
- package/dist/materials/guide-agents.md +103 -0
- package/dist/materials/guide-faq.md +84 -0
- package/dist/materials/guide-foundations.md +275 -0
- package/dist/materials/guide-installation.md +79 -0
- package/dist/materials/guide-interface-content.md +76 -0
- package/dist/materials/guide-introduction.md +42 -0
- package/dist/materials/guide-layout.md +222 -0
- package/dist/materials/guide-motion.md +69 -0
- package/dist/materials/guide-privacy.md +23 -0
- package/dist/materials/guide-project-ui.md +127 -0
- package/dist/materials/guide-skills.md +117 -0
- package/dist/materials/guide-theming.md +73 -0
- package/dist/materials/recipe-collection-views.md +648 -0
- package/dist/materials/recipe-command-palette.md +45 -0
- package/dist/materials/recipe-filter-bar.md +68 -0
- package/dist/materials/recipe-form-submission.md +39 -0
- package/dist/materials/recipe-grouped-form.md +71 -0
- package/dist/materials/recipe-navigation-workspace.md +222 -0
- package/dist/materials/recipe-record-management.md +296 -0
- package/dist/materials/recipe-record-workspace.md +207 -0
- package/dist/materials/recipe-select-all.md +31 -0
- package/dist/materials/recipe-settings-workspace.md +232 -0
- package/dist/materials/recipe-split-button.md +26 -0
- package/dist/materials/recipe-table-bulk-actions.md +88 -0
- package/dist/materials/recipe-workspace-review.md +134 -0
- package/dist/materials/token-sys.color.accent.content.json +41 -0
- package/dist/materials/token-sys.color.action.danger.content.json +42 -0
- package/dist/materials/token-sys.color.action.ghost.content.json +41 -0
- package/dist/materials/token-sys.color.action.ghost.disabled-content.json +41 -0
- package/dist/materials/token-sys.color.action.ghost.disabled.json +41 -0
- package/dist/materials/token-sys.color.action.ghost.hover.json +41 -0
- package/dist/materials/token-sys.color.action.ghost.pressed.json +41 -0
- package/dist/materials/token-sys.color.action.ghost.rest.json +41 -0
- package/dist/materials/token-sys.color.action.on-surface.content.json +39 -0
- package/dist/materials/token-sys.color.action.on-surface.disabled-content.json +39 -0
- package/dist/materials/token-sys.color.action.on-surface.disabled.json +39 -0
- package/dist/materials/token-sys.color.action.on-surface.hover.json +39 -0
- package/dist/materials/token-sys.color.action.on-surface.pressed.json +39 -0
- package/dist/materials/token-sys.color.action.on-surface.rest.json +39 -0
- package/dist/materials/token-sys.color.action.primary.content.json +39 -0
- package/dist/materials/token-sys.color.action.primary.disabled-content.json +39 -0
- package/dist/materials/token-sys.color.action.primary.disabled.json +39 -0
- package/dist/materials/token-sys.color.action.primary.hover.json +39 -0
- package/dist/materials/token-sys.color.action.primary.pressed.json +39 -0
- package/dist/materials/token-sys.color.action.primary.rest.json +39 -0
- package/dist/materials/token-sys.color.action.subtle.content.json +39 -0
- package/dist/materials/token-sys.color.action.subtle.disabled-content.json +39 -0
- package/dist/materials/token-sys.color.action.subtle.disabled.json +39 -0
- package/dist/materials/token-sys.color.action.subtle.hover.json +39 -0
- package/dist/materials/token-sys.color.action.subtle.pressed.json +39 -0
- package/dist/materials/token-sys.color.action.subtle.rest.json +39 -0
- package/dist/materials/token-sys.color.border.decorative.json +42 -0
- package/dist/materials/token-sys.color.border.default.json +39 -0
- package/dist/materials/token-sys.color.border.floating.json +39 -0
- package/dist/materials/token-sys.color.border.strong.json +39 -0
- package/dist/materials/token-sys.color.content.default.json +41 -0
- package/dist/materials/token-sys.color.content.helper.json +39 -0
- package/dist/materials/token-sys.color.content.subtle.json +43 -0
- package/dist/materials/token-sys.color.context.base.action-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.base.action-hover.json +41 -0
- package/dist/materials/token-sys.color.context.base.action-pressed.json +41 -0
- package/dist/materials/token-sys.color.context.base.action-rest.json +41 -0
- package/dist/materials/token-sys.color.context.base.avatar-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.border-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.base.divider-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-border.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-readonly.json +41 -0
- package/dist/materials/token-sys.color.context.base.indicator-track.json +41 -0
- package/dist/materials/token-sys.color.context.base.loading-placeholder.json +41 -0
- package/dist/materials/token-sys.color.context.base.muted-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-hover.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-pressed.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-rest.json +41 -0
- package/dist/materials/token-sys.color.context.base.selection-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.supporting-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-hover.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-pressed.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-rest.json +41 -0
- package/dist/materials/token-sys.color.context.filled.avatar-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.border-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.filled.divider-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-border.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-readonly.json +41 -0
- package/dist/materials/token-sys.color.context.filled.indicator-track.json +41 -0
- package/dist/materials/token-sys.color.context.filled.loading-placeholder.json +41 -0
- package/dist/materials/token-sys.color.context.filled.muted-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.selection-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.supporting-background.json +41 -0
- package/dist/materials/token-sys.color.divider.decorative.json +41 -0
- package/dist/materials/token-sys.color.feedback.error.json +39 -0
- package/dist/materials/token-sys.color.feedback.info.json +39 -0
- package/dist/materials/token-sys.color.feedback.success.json +39 -0
- package/dist/materials/token-sys.color.feedback.warning.json +39 -0
- package/dist/materials/token-sys.color.field.background.json +39 -0
- package/dist/materials/token-sys.color.field.border.json +39 -0
- package/dist/materials/token-sys.color.field.boundary.json +39 -0
- package/dist/materials/token-sys.color.field.content.json +39 -0
- package/dist/materials/token-sys.color.field.disabled-content.json +39 -0
- package/dist/materials/token-sys.color.field.disabled.json +39 -0
- package/dist/materials/token-sys.color.field.placeholder.json +39 -0
- package/dist/materials/token-sys.color.field.readonly.json +41 -0
- package/dist/materials/token-sys.color.focus.ring.json +39 -0
- package/dist/materials/token-sys.color.indicator.accent.json +39 -0
- package/dist/materials/token-sys.color.indicator.fill.json +39 -0
- package/dist/materials/token-sys.color.indicator.track.json +39 -0
- package/dist/materials/token-sys.color.loading.placeholder.json +42 -0
- package/dist/materials/token-sys.color.scrollbar.thumb-hover.json +39 -0
- package/dist/materials/token-sys.color.scrollbar.thumb.json +39 -0
- package/dist/materials/token-sys.color.selection.background.json +39 -0
- package/dist/materials/token-sys.color.selection.content.json +39 -0
- package/dist/materials/token-sys.color.selection.floating.json +39 -0
- package/dist/materials/token-sys.color.shadow.floating.json +39 -0
- package/dist/materials/token-sys.color.surface.avatar.json +41 -0
- package/dist/materials/token-sys.color.surface.canvas.json +39 -0
- package/dist/materials/token-sys.color.surface.current.json +41 -0
- package/dist/materials/token-sys.color.surface.default.json +39 -0
- package/dist/materials/token-sys.color.surface.floating-translucent.json +39 -0
- package/dist/materials/token-sys.color.surface.floating.json +39 -0
- package/dist/materials/token-sys.color.surface.muted.json +41 -0
- package/dist/materials/token-sys.color.surface.overlay.json +39 -0
- package/dist/materials/token-sys.color.surface.raised.json +39 -0
- package/dist/materials/token-sys.color.surface.supporting.json +41 -0
- package/dist/materials/token-sys.control.2xl.gap.json +34 -0
- package/dist/materials/token-sys.control.2xl.height.json +34 -0
- package/dist/materials/token-sys.control.2xl.icon-size.json +34 -0
- package/dist/materials/token-sys.control.2xl.line-height.json +31 -0
- package/dist/materials/token-sys.control.2xl.padding-inline.json +34 -0
- package/dist/materials/token-sys.control.2xl.text-size.json +34 -0
- package/dist/materials/token-sys.control.2xs.gap.json +34 -0
- package/dist/materials/token-sys.control.2xs.height.json +34 -0
- package/dist/materials/token-sys.control.2xs.icon-size.json +34 -0
- package/dist/materials/token-sys.control.2xs.line-height.json +31 -0
- package/dist/materials/token-sys.control.2xs.padding-inline.json +34 -0
- package/dist/materials/token-sys.control.2xs.text-size.json +34 -0
- package/dist/materials/token-sys.control.lg.gap.json +34 -0
- package/dist/materials/token-sys.control.lg.height.json +34 -0
- package/dist/materials/token-sys.control.lg.icon-size.json +34 -0
- package/dist/materials/token-sys.control.lg.line-height.json +31 -0
- package/dist/materials/token-sys.control.lg.padding-inline.json +34 -0
- package/dist/materials/token-sys.control.lg.text-size.json +34 -0
- package/dist/materials/token-sys.control.md.gap.json +34 -0
- package/dist/materials/token-sys.control.md.height.json +34 -0
- package/dist/materials/token-sys.control.md.icon-size.json +34 -0
- package/dist/materials/token-sys.control.md.line-height.json +31 -0
- package/dist/materials/token-sys.control.md.padding-inline.json +34 -0
- package/dist/materials/token-sys.control.md.text-size.json +34 -0
- package/dist/materials/token-sys.control.sm.gap.json +34 -0
- package/dist/materials/token-sys.control.sm.height.json +34 -0
- package/dist/materials/token-sys.control.sm.icon-size.json +34 -0
- package/dist/materials/token-sys.control.sm.line-height.json +31 -0
- package/dist/materials/token-sys.control.sm.padding-inline.json +34 -0
- package/dist/materials/token-sys.control.sm.text-size.json +34 -0
- package/dist/materials/token-sys.control.xl.gap.json +34 -0
- package/dist/materials/token-sys.control.xl.height.json +34 -0
- package/dist/materials/token-sys.control.xl.icon-size.json +34 -0
- package/dist/materials/token-sys.control.xl.line-height.json +31 -0
- package/dist/materials/token-sys.control.xl.padding-inline.json +34 -0
- package/dist/materials/token-sys.control.xl.text-size.json +34 -0
- package/dist/materials/token-sys.control.xs.gap.json +34 -0
- package/dist/materials/token-sys.control.xs.height.json +34 -0
- package/dist/materials/token-sys.control.xs.icon-size.json +34 -0
- package/dist/materials/token-sys.control.xs.line-height.json +31 -0
- package/dist/materials/token-sys.control.xs.padding-inline.json +34 -0
- package/dist/materials/token-sys.control.xs.text-size.json +34 -0
- package/dist/materials/token-sys.effect.floating.blur.json +34 -0
- package/dist/materials/token-sys.effect.floating.saturation.json +31 -0
- package/dist/materials/token-sys.effect.floating.shadow-blur.json +34 -0
- package/dist/materials/token-sys.effect.floating.shadow-offset.json +34 -0
- package/dist/materials/token-sys.focus.offset.json +34 -0
- package/dist/materials/token-sys.focus.width.json +34 -0
- package/dist/materials/token-sys.font.mono.json +36 -0
- package/dist/materials/token-sys.font.sans.json +35 -0
- package/dist/materials/token-sys.layer.base.json +31 -0
- package/dist/materials/token-sys.layer.modal.json +31 -0
- package/dist/materials/token-sys.layer.popover.json +31 -0
- package/dist/materials/token-sys.layer.sticky.json +31 -0
- package/dist/materials/token-sys.layer.toast.json +31 -0
- package/dist/materials/token-sys.motion.distance-panel.json +36 -0
- package/dist/materials/token-sys.motion.distance-small.json +36 -0
- package/dist/materials/token-sys.motion.duration-enter.json +36 -0
- package/dist/materials/token-sys.motion.duration-exit.json +36 -0
- package/dist/materials/token-sys.motion.duration-fast.json +34 -0
- package/dist/materials/token-sys.motion.duration-layer.json +36 -0
- package/dist/materials/token-sys.motion.duration-loop.json +36 -0
- package/dist/materials/token-sys.motion.duration-normal.json +34 -0
- package/dist/materials/token-sys.motion.duration-press.json +36 -0
- package/dist/materials/token-sys.motion.duration-reduced.json +34 -0
- package/dist/materials/token-sys.motion.easing-enter.json +38 -0
- package/dist/materials/token-sys.motion.easing-exit.json +38 -0
- package/dist/materials/token-sys.motion.easing.json +36 -0
- package/dist/materials/token-sys.radius.action.json +34 -0
- package/dist/materials/token-sys.radius.block.json +34 -0
- package/dist/materials/token-sys.radius.field.json +34 -0
- package/dist/materials/token-sys.radius.indicator.json +34 -0
- package/dist/materials/token-sys.radius.surface.json +34 -0
- package/dist/materials/token-sys.scrollbar.inset.json +34 -0
- package/dist/materials/token-sys.scrollbar.min-thumb.json +34 -0
- package/dist/materials/token-sys.scrollbar.size.json +34 -0
- package/dist/materials/token-sys.size.choice.json +34 -0
- package/dist/materials/token-sys.size.container.lg.json +37 -0
- package/dist/materials/token-sys.size.container.md.json +37 -0
- package/dist/materials/token-sys.size.container.sm.json +37 -0
- package/dist/materials/token-sys.size.grid.min-item.json +37 -0
- package/dist/materials/token-sys.size.indicator.lg.json +34 -0
- package/dist/materials/token-sys.size.indicator.md.json +34 -0
- package/dist/materials/token-sys.size.indicator.sm.json +34 -0
- package/dist/materials/token-sys.size.keycap.lg.json +34 -0
- package/dist/materials/token-sys.size.keycap.md.json +34 -0
- package/dist/materials/token-sys.size.keycap.sm.json +34 -0
- package/dist/materials/token-sys.size.switch-height.json +34 -0
- package/dist/materials/token-sys.size.switch-thumb.json +34 -0
- package/dist/materials/token-sys.size.switch-width.json +34 -0
- package/dist/materials/token-sys.size.touch.json +34 -0
- package/dist/materials/token-sys.space.0.json +34 -0
- package/dist/materials/token-sys.space.1.json +34 -0
- package/dist/materials/token-sys.space.12.json +34 -0
- package/dist/materials/token-sys.space.2.json +34 -0
- package/dist/materials/token-sys.space.3.json +34 -0
- package/dist/materials/token-sys.space.4.json +34 -0
- package/dist/materials/token-sys.space.6.json +34 -0
- package/dist/materials/token-sys.space.8.json +34 -0
- package/dist/materials/token-sys.stroke.width.json +34 -0
- package/dist/materials/token-sys.type.body.line-height.json +31 -0
- package/dist/materials/token-sys.type.body.size.json +34 -0
- package/dist/materials/token-sys.type.body.tracking.json +34 -0
- package/dist/materials/token-sys.type.body.weight.json +31 -0
- package/dist/materials/token-sys.type.compact.line-height.json +31 -0
- package/dist/materials/token-sys.type.compact.size.json +34 -0
- package/dist/materials/token-sys.type.compact.tracking.json +34 -0
- package/dist/materials/token-sys.type.compact.weight.json +31 -0
- package/dist/materials/token-sys.type.control.line-height.json +31 -0
- package/dist/materials/token-sys.type.control.size.json +34 -0
- package/dist/materials/token-sys.type.control.tracking.json +34 -0
- package/dist/materials/token-sys.type.control.weight.json +31 -0
- package/dist/materials/token-sys.type.dialog-title.line-height.json +33 -0
- package/dist/materials/token-sys.type.dialog-title.size.json +36 -0
- package/dist/materials/token-sys.type.dialog-title.tracking.json +36 -0
- package/dist/materials/token-sys.type.dialog-title.weight.json +33 -0
- package/dist/materials/token-sys.type.display.line-height.json +31 -0
- package/dist/materials/token-sys.type.display.size.json +34 -0
- package/dist/materials/token-sys.type.display.tracking.json +34 -0
- package/dist/materials/token-sys.type.display.weight.json +31 -0
- package/dist/materials/token-sys.type.field-label.line-height.json +33 -0
- package/dist/materials/token-sys.type.field-label.size.json +36 -0
- package/dist/materials/token-sys.type.field-label.tracking.json +36 -0
- package/dist/materials/token-sys.type.field-label.weight.json +33 -0
- package/dist/materials/token-sys.type.group-title.line-height.json +33 -0
- package/dist/materials/token-sys.type.group-title.size.json +36 -0
- package/dist/materials/token-sys.type.group-title.tracking.json +36 -0
- package/dist/materials/token-sys.type.group-title.weight.json +33 -0
- package/dist/materials/token-sys.type.helper.line-height.json +31 -0
- package/dist/materials/token-sys.type.helper.size.json +34 -0
- package/dist/materials/token-sys.type.helper.tracking.json +34 -0
- package/dist/materials/token-sys.type.helper.weight.json +31 -0
- package/dist/materials/token-sys.type.label.line-height.json +31 -0
- package/dist/materials/token-sys.type.label.size.json +34 -0
- package/dist/materials/token-sys.type.label.tracking.json +34 -0
- package/dist/materials/token-sys.type.label.weight.json +31 -0
- package/dist/materials/token-sys.type.large.line-height.json +31 -0
- package/dist/materials/token-sys.type.large.size.json +34 -0
- package/dist/materials/token-sys.type.large.tracking.json +34 -0
- package/dist/materials/token-sys.type.large.weight.json +31 -0
- package/dist/materials/token-sys.type.lead.line-height.json +31 -0
- package/dist/materials/token-sys.type.lead.size.json +34 -0
- package/dist/materials/token-sys.type.lead.tracking.json +34 -0
- package/dist/materials/token-sys.type.lead.weight.json +31 -0
- package/dist/materials/token-sys.type.page.line-height.json +31 -0
- package/dist/materials/token-sys.type.page.size.json +34 -0
- package/dist/materials/token-sys.type.page.tracking.json +34 -0
- package/dist/materials/token-sys.type.page.weight.json +31 -0
- package/dist/materials/token-sys.type.section.line-height.json +31 -0
- package/dist/materials/token-sys.type.section.size.json +34 -0
- package/dist/materials/token-sys.type.section.tracking.json +34 -0
- package/dist/materials/token-sys.type.section.weight.json +31 -0
- package/knowledge.json +522 -0
- package/lib/index.d.ts +33 -0
- package/lib/index.mjs +144 -0
- package/lib/inspect.d.ts +12 -0
- package/lib/inspect.mjs +187 -0
- package/manifest.json +51 -1
- package/package.json +13 -1
- package/references/component-families.md +146 -0
- package/references/composition.md +4 -4
- package/references/customization.md +14 -0
- package/references/flood-composition/SKILL.md +34 -0
- package/references/flood-data-display/SKILL.md +149 -0
- package/references/flood-feedback/SKILL.md +49 -3
- package/references/flood-forms/SKILL.md +82 -2
- package/references/flood-foundations/SKILL.md +43 -0
- package/references/flood-hierarchy/SKILL.md +103 -0
- package/references/flood-interface-content/SKILL.md +163 -0
- package/references/flood-motion/SKILL.md +44 -0
- package/references/flood-navigation/SKILL.md +195 -0
- package/references/flood-overlays/SKILL.md +42 -0
- package/references/flood-records/SKILL.md +148 -0
- package/references/flood-spacing/SKILL.md +54 -0
- package/references/flood-verification/SKILL.md +72 -0
- package/references/interface-content-examples.md +51 -0
- package/references/knowledge.md +49 -0
- package/references/record-decisions.md +25 -0
- package/references/rendered-review.md +102 -0
- package/references/routing.md +1 -0
- package/references/spacing.md +30 -0
- package/rules.json +242 -6
- package/workflows.json +171 -10
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
# Collection filters and saved views
|
|
2
|
+
|
|
3
|
+
[Open Projects and Files](/playground/collections).
|
|
4
|
+
|
|
5
|
+
One set of controls serves a DataTable and a FileList. It demonstrates search,
|
|
6
|
+
quick filters, staged conditions, applied tags, saved definitions and sorting from existing
|
|
7
|
+
Flood primitives. CollectionViewControls is project-owned recipe code, not an
|
|
8
|
+
export from @flood-md/react. The installed catalog contains these same typed
|
|
9
|
+
sources. Keep model and controls together; adapt datasets and shell to the host.
|
|
10
|
+
|
|
11
|
+
## Query ownership
|
|
12
|
+
|
|
13
|
+
The wide layout has two control rows. View selection and its actions share the
|
|
14
|
+
first row. Search and filter/reset actions share the second row. At most two
|
|
15
|
+
secondary filter properties appear inline. With three or more properties, one
|
|
16
|
+
Filters panel contains the choices, including currently inactive properties.
|
|
17
|
+
Applied conditions remain visible with property names and remove actions.
|
|
18
|
+
Groups wrap as space becomes constrained. Test both empty filters and longer
|
|
19
|
+
selected values before adopting the host layout.
|
|
20
|
+
|
|
21
|
+
Filters anchors to the inline end of the control row. A reserved Clear filters
|
|
22
|
+
slot precedes it, so applying or clearing conditions does not shift the trigger.
|
|
23
|
+
The inactive reset is visually hidden, disabled and absent from accessibility
|
|
24
|
+
navigation. Applied chips stay in the search area and can wrap independently.
|
|
25
|
+
|
|
26
|
+
A query contains search, a flat all/any group, enum conditions and sort. Search
|
|
27
|
+
combines with the group using AND. Values within one condition mean any matching
|
|
28
|
+
value; is-not excludes all of them. Missing record values do not match is and do
|
|
29
|
+
match is-not. The host defines properties, values and record meaning.
|
|
30
|
+
|
|
31
|
+
For inline simple filters, a named Menu applies one value per property immediately.
|
|
32
|
+
The adjacent Tag shows the selected value and gets its property context from
|
|
33
|
+
the menu beside it. Choose All or remove the Tag to clear that property. The Tag returns
|
|
34
|
+
focus to its menu when removed. Projects and Files use the same controls with
|
|
35
|
+
their own properties. Search and sort retain their current values.
|
|
36
|
+
|
|
37
|
+
The collapsed panel stages its labelled choices until Apply filters. Cancel
|
|
38
|
+
discards the changes. Advanced conditions switches the same panel to the full
|
|
39
|
+
condition editor when exclusions, multiple values or Any logic are needed.
|
|
40
|
+
|
|
41
|
+
Use the Filters editor for exclusion, multiple values per property or Any across
|
|
42
|
+
properties. It has a separate draft. Apply validates and commits, while Cancel or
|
|
43
|
+
outside dismissal discards it. Advanced conditions are summarized by removable
|
|
44
|
+
tags with their All/Any logic visible when relevant. Clear filters removes search
|
|
45
|
+
and conditions, retaining sort. Unknown properties, values, operators, sort
|
|
46
|
+
columns and schema versions fail explicitly rather than silently broadening a view.
|
|
47
|
+
|
|
48
|
+
An applied query change outside the editor closes its obsolete draft. Applying
|
|
49
|
+
conditions never restores an old search/sort value from when the editor opened.
|
|
50
|
+
|
|
51
|
+
DataTable receives the full locally filtered collection. The example resets page
|
|
52
|
+
and selection on query/preset/sort changes. Saving or renaming a definition does
|
|
53
|
+
not change the query or selection. FileList has its own host sort. Remote requests,
|
|
54
|
+
permissions and server-wide selection remain host responsibilities.
|
|
55
|
+
|
|
56
|
+
Table selection serves real status and archive commands for the chosen projects.
|
|
57
|
+
Hosts without a selection operation should omit selectable rather than display
|
|
58
|
+
checkboxes with no resulting action.
|
|
59
|
+
|
|
60
|
+
## Selected records
|
|
61
|
+
|
|
62
|
+
Selecting projects reveals one shared action bar floating at the viewport bottom. It shows the
|
|
63
|
+
selected count, changes status, confirms archiving and clears the
|
|
64
|
+
selection. Selection spans the filtered client collection across its pages.
|
|
65
|
+
Changing the query resets it. Archiving captures IDs and names before opening
|
|
66
|
+
the confirmation. Cancel keeps the selection and records intact.
|
|
67
|
+
|
|
68
|
+
The host owns the update and accepted records. This example changes its in-memory
|
|
69
|
+
records after the optional onUpdateStatus callback accepts the whole operation.
|
|
70
|
+
Reload returns to the declared fixtures. A rejection keeps accepted records and
|
|
71
|
+
selection intact. Retry uses the failed operation's captured IDs and status.
|
|
72
|
+
Dismiss or a deliberate query/selection change abandons its feedback.
|
|
73
|
+
|
|
74
|
+
Only one update can run at a time. Pending actions are disabled. If the user
|
|
75
|
+
changes the query or selection while an update is pending, its result still
|
|
76
|
+
belongs to the captured IDs. Success removes those IDs from current selection,
|
|
77
|
+
preserving newly selected records, and returns pagination to the first page.
|
|
78
|
+
The host callback must accept/reject atomically. A service allowing partial
|
|
79
|
+
success needs a per-record result and reconciliation contract.
|
|
80
|
+
|
|
81
|
+
Clear selection returns focus to the collection. A successful update moves focus
|
|
82
|
+
there if the disappearing action bar held it. Confirmation renders outside the
|
|
83
|
+
roving Toolbar so the dialog retains its own focus model.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useState } from 'react';
|
|
87
|
+
import { AlertDialog, BulkActionBar, VisuallyHidden } from '@flood-md/react';
|
|
88
|
+
|
|
89
|
+
export type ProjectStatus = 'active' | 'paused' | 'archived';
|
|
90
|
+
export interface SelectedProject { id: string; name: string }
|
|
91
|
+
export interface ProjectStatusUpdate { ids: string[]; status: ProjectStatus }
|
|
92
|
+
|
|
93
|
+
export function ProjectSelectionActions({ projects, pending, onUpdate, onClear }: {
|
|
94
|
+
projects: readonly SelectedProject[];
|
|
95
|
+
pending: boolean;
|
|
96
|
+
onUpdate: (update: ProjectStatusUpdate) => void;
|
|
97
|
+
onClear: () => void;
|
|
98
|
+
}) {
|
|
99
|
+
const [archive, setArchive] = useState<SelectedProject[] | null>(null);
|
|
100
|
+
|
|
101
|
+
return <>
|
|
102
|
+
<BulkActionBar label="Selected projects" selectionCount={projects.length} pending={pending}
|
|
103
|
+
items={[
|
|
104
|
+
{ id: 'archive', label: 'Archive', priority: 10 },
|
|
105
|
+
{ id: 'paused', label: 'Pause', priority: 5 },
|
|
106
|
+
{ id: 'active', label: 'Set active' },
|
|
107
|
+
]}
|
|
108
|
+
onAction={action => {
|
|
109
|
+
if (action === 'archive') setArchive(projects.map(project => ({ ...project })));
|
|
110
|
+
else onUpdate({ ids: projects.map(project => project.id), status: action as ProjectStatus });
|
|
111
|
+
}}
|
|
112
|
+
onClear={onClear} />
|
|
113
|
+
<VisuallyHidden role="status">{pending ? 'Updating…' : ''}</VisuallyHidden>
|
|
114
|
+
{archive && <AlertDialog
|
|
115
|
+
title={`Archive ${archive.length} ${archive.length === 1 ? 'project' : 'projects'}?`}
|
|
116
|
+
description={`${archive.map(project => project.name).join(', ')} will move to Archived.`}
|
|
117
|
+
confirmLabel="Archive projects"
|
|
118
|
+
cancelLabel="Keep projects"
|
|
119
|
+
open
|
|
120
|
+
onOpenChange={open => { if (!open) setArchive(null); }}
|
|
121
|
+
onConfirm={() => {
|
|
122
|
+
onUpdate({ ids: archive.map(project => project.id), status: 'archived' });
|
|
123
|
+
}}
|
|
124
|
+
trigger={() => null}
|
|
125
|
+
/>}
|
|
126
|
+
</>;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The panel uses the existing floating-surface tokens and a viewport-safe inset.
|
|
131
|
+
It leaves normal document flow and stays centered when the document scrolls.
|
|
132
|
+
BulkActionBar keeps a single pill-shaped row with at most two visible commands
|
|
133
|
+
by default. Lower-priority commands move into More as space shrinks; the host
|
|
134
|
+
does not add a second row of commands. ResizeObserver measures its actual height, and the
|
|
135
|
+
collection reserves enough bottom space for the final rows and pagination to
|
|
136
|
+
scroll above the panel. Keep this measured clearance with the host styles.
|
|
137
|
+
|
|
138
|
+
```css
|
|
139
|
+
.collection-examples { padding-block:var(--flood-sys-space-8); min-inline-size:0; }
|
|
140
|
+
.collection-view-controls { min-inline-size:0; }
|
|
141
|
+
.flood-root .collection-filter-editor { inline-size:min(42rem,calc(100vw - 2rem)); }
|
|
142
|
+
|
|
143
|
+
.collection-selection-panel {
|
|
144
|
+
position: fixed;
|
|
145
|
+
inset-inline: 0;
|
|
146
|
+
inset-block-end: max(var(--flood-sys-space-4), env(safe-area-inset-bottom));
|
|
147
|
+
margin-inline: auto;
|
|
148
|
+
inline-size: min(26rem, calc(100% - 2 * var(--flood-sys-space-4)));
|
|
149
|
+
max-inline-size: calc(100% - 2 * var(--flood-sys-space-4));
|
|
150
|
+
z-index: var(--flood-sys-layer-sticky);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.collection-projects[data-selected="true"] {
|
|
154
|
+
padding-block-end: calc(var(--collection-selection-height, 0px) + 2 * var(--flood-sys-space-4) + env(safe-area-inset-bottom));
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Working state and saved definitions
|
|
159
|
+
|
|
160
|
+
Built-in presets are read-only. A saved view stores the query and supported sort,
|
|
161
|
+
not record IDs or cached results. Dirty views offer Update and Reset. Save as new
|
|
162
|
+
captures the current query; Rename changes only its name, even while filters are
|
|
163
|
+
dirty. Deleting a definition leaves collection records unchanged.
|
|
164
|
+
|
|
165
|
+
Working changes stay transient until saved. Reload starts at the default preset;
|
|
166
|
+
saved definitions remain selectable. Choosing another view replaces the working
|
|
167
|
+
query. URL sharing, remembered active views, nested groups, date/number operators,
|
|
168
|
+
collaboration and cross-tab synchronization are not implemented here.
|
|
169
|
+
|
|
170
|
+
Names are trimmed, at most 80 UTF-16 code units and unique after NFC/case
|
|
171
|
+
normalization, including preset names. Equality ignores condition UI IDs, condition
|
|
172
|
+
order and selected-value order; it is not general boolean equivalence.
|
|
173
|
+
|
|
174
|
+
## Persistence and focus
|
|
175
|
+
|
|
176
|
+
Two keys keep project/file definitions separate. Read occurs after mount to retain
|
|
177
|
+
consistent initial server/client markup. Mount a separate keyed instance when
|
|
178
|
+
changing collection schema. Browser storage is an example adapter, not production
|
|
179
|
+
durability. A missing key means no custom definitions; malformed/refused reads
|
|
180
|
+
preserve the available raw snapshot and block writes to that store. Independently
|
|
181
|
+
loaded records remain usable. Retry/export/explicit replacement address the actual
|
|
182
|
+
preferences failure; exported bytes do not guarantee automatic restoration.
|
|
183
|
+
|
|
184
|
+
Refused writes retain accepted definitions and entered names. Cancel ends a name
|
|
185
|
+
draft's error; Reset ends a failed update of the discarded query. Read failures
|
|
186
|
+
have a separate lifetime. A real remote adapter needs pending/result ownership,
|
|
187
|
+
reconciliation and migrations rather than this synchronous example.
|
|
188
|
+
|
|
189
|
+
Existing overlay owners retain nested behaviour. Naming/deletion actions focus
|
|
190
|
+
the persistent view selector before opening a modal so it remains a useful return
|
|
191
|
+
target when an action disappears. Escape closes the value picker before the outer
|
|
192
|
+
condition editor; closing the editor returns to Filters.
|
|
193
|
+
|
|
194
|
+
## Reused owners
|
|
195
|
+
|
|
196
|
+
FilterGroup composes FilterCondition rows for the flat advanced draft. It owns
|
|
197
|
+
row addition/removal, duplicate-property choice policy, the all/any switch and
|
|
198
|
+
accepted focus transitions. FilterCondition keeps equal control sizing and
|
|
199
|
+
container-based reflow. Changing a property clears incompatible values only in
|
|
200
|
+
the proposed draft. The collection form retains Apply/Cancel and query validation,
|
|
201
|
+
so creating an incomplete row never changes accepted results.
|
|
202
|
+
|
|
203
|
+
CollectionToolbar owns the shared query layout: search, accepted summaries,
|
|
204
|
+
reserved Clear filters and end-aligned Filters. Its fieldset uses normal Tab
|
|
205
|
+
navigation because it includes editable text. Keep saved-view controls outside
|
|
206
|
+
this row. Supply the staged editor through filterContent; opening it copies the
|
|
207
|
+
accepted query, Apply accepts it and Cancel abandons it. Reset and removal return
|
|
208
|
+
focus to the persistent Filters trigger (search when no panel exists). The host
|
|
209
|
+
defines reset scope and supplies canClear for any state held by quickFilters.
|
|
210
|
+
At most two task-important quick properties remain inline; additional properties
|
|
211
|
+
belong inside Filters. Summaries outside the panel name the property and preserve
|
|
212
|
+
any-match meaning. Container width, rather than viewport size, governs reflow.
|
|
213
|
+
|
|
214
|
+
CollectionViewSwitcher owns the separate accepted-view choice and adaptive
|
|
215
|
+
commands. It reuses Field/Select and Button/OverflowActions with the same size.
|
|
216
|
+
The recipe supplies applicable actions and retains all persistence, name-only
|
|
217
|
+
rename, dirty query reset, confirmation and failure recovery. selectRef preserves
|
|
218
|
+
the existing naming/deletion dialog return target; saving a definition does not
|
|
219
|
+
reset result selection.
|
|
220
|
+
|
|
221
|
+
SearchInput, Select, Menu, MultiSelect and SegmentedControl own input/choice behaviour;
|
|
222
|
+
Popover contains the condition draft and Tag removes applied conditions. Button
|
|
223
|
+
and IconButton perform actions. Several view commands use OverflowActions; a
|
|
224
|
+
single command uses Button directly. Field/Fieldset, Grid/Stack/Inline, Dialog,
|
|
225
|
+
AlertDialog and Alert retain their own roles. DataTable/StatusIndicator and
|
|
226
|
+
FileList/FileItem/FileMeta demonstrate two result structures. No owner is inserted
|
|
227
|
+
merely to increase its usage count, and no new package atom is needed for layout.
|
|
228
|
+
|
|
229
|
+
For a few mutually exclusive views of the same result table, use SegmentedControl
|
|
230
|
+
with a controlled value. An option's `count` can show workload, such as pending
|
|
231
|
+
items. Leave it unset when it repeats a total already visible beside the results.
|
|
232
|
+
Use Tabs when each choice opens its own associated content panel.
|
|
233
|
+
|
|
234
|
+
## Model
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import type {ComboboxOption,DataTableSort} from '@flood-md/react';
|
|
238
|
+
|
|
239
|
+
export interface FilterProperty {id:string;label:string;options:readonly ComboboxOption[];}
|
|
240
|
+
export interface CollectionSchema {properties:readonly FilterProperty[];sortColumns:readonly string[];}
|
|
241
|
+
export interface FilterCondition {id:string;property:string;operator:'is'|'is-not';values:string[];}
|
|
242
|
+
export interface CollectionQuery {search:string;match:'all'|'any';conditions:FilterCondition[];sort:DataTableSort|null;}
|
|
243
|
+
export interface CollectionView {id:string;name:string;query:CollectionQuery;}
|
|
244
|
+
export type ViewStorage=Pick<Storage,'getItem'|'setItem'>;
|
|
245
|
+
export type ViewRead={state:'ready';views:CollectionView[]}|{state:'error';views:[];raw:string|null};
|
|
246
|
+
const object=(value:unknown):value is Record<string,unknown>=>Boolean(value)&&typeof value==='object'&&!Array.isArray(value);
|
|
247
|
+
export const emptyCollectionQuery=():CollectionQuery=>({search:'',match:'all',conditions:[],sort:null});
|
|
248
|
+
export const copyCollectionQuery=(query:CollectionQuery):CollectionQuery=>({...query,conditions:query.conditions.map(condition=>({...condition,values:[...condition.values]})),sort:query.sort?{...query.sort}:null});
|
|
249
|
+
|
|
250
|
+
/** Validate persisted definitions instead of silently dropping unknown predicates. */
|
|
251
|
+
export function parseCollectionQuery(value:unknown,schema:CollectionSchema):CollectionQuery {
|
|
252
|
+
if(!object(value)||typeof value.search!=='string'||(value.match!=='all'&&value.match!=='any')||!Array.isArray(value.conditions))throw Error('Unsupported view query');
|
|
253
|
+
const ids=new Set<string>(),properties=new Set<string>();
|
|
254
|
+
const conditions=value.conditions.map((condition:unknown):FilterCondition=>{
|
|
255
|
+
if(!object(condition)||typeof condition.id!=='string'||!condition.id.trim()||ids.has(condition.id)||typeof condition.property!=='string'||properties.has(condition.property)||(condition.operator!=='is'&&condition.operator!=='is-not')||!Array.isArray(condition.values)||!condition.values.length)throw Error('Unsupported view condition');
|
|
256
|
+
const property=schema.properties.find(property=>property.id===condition.property);
|
|
257
|
+
if(!property||new Set(condition.values).size!==condition.values.length||condition.values.some(value=>typeof value!=='string'||!property.options.some(option=>option.value===value)))throw Error('Unknown view values');
|
|
258
|
+
ids.add(condition.id);properties.add(condition.property);
|
|
259
|
+
return {id:condition.id,property:condition.property,operator:condition.operator as FilterCondition['operator'],values:[...condition.values] as string[]};
|
|
260
|
+
});
|
|
261
|
+
let sort:DataTableSort|null=null;
|
|
262
|
+
if(value.sort!==null){if(!object(value.sort)||typeof value.sort.column!=='string'||!schema.sortColumns.includes(value.sort.column)||(value.sort.direction!=='ascending'&&value.sort.direction!=='descending'))throw Error('Unsupported view sort');sort={column:value.sort.column,direction:value.sort.direction};}
|
|
263
|
+
return {search:value.search,match:value.match as CollectionQuery['match'],conditions,sort};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Row IDs and condition UI IDs do not define a saved query's meaning. */
|
|
267
|
+
export function collectionQueryKey(query:CollectionQuery){return JSON.stringify({search:query.search.trim(),match:query.conditions.length>1?query.match:'all',conditions:query.conditions.map(({property,operator,values})=>({property,operator,values:[...values].sort()})).sort((a,b)=>a.property.localeCompare(b.property)),sort:query.sort});}
|
|
268
|
+
|
|
269
|
+
export function matchesCollectionQuery(query:CollectionQuery,searchText:string,read:(property:string)=>string|readonly string[]|null){
|
|
270
|
+
if(!searchText.toLocaleLowerCase().includes(query.search.trim().toLocaleLowerCase()))return false;
|
|
271
|
+
const tests=query.conditions.map(condition=>{const raw=read(condition.property),values=raw===null?[]:typeof raw==='string'?[raw]:raw;const includes=condition.values.some(value=>values.includes(value));return condition.operator==='is'?includes:!includes;});
|
|
272
|
+
return !tests.length||(query.match==='all'?tests.every(Boolean):tests.some(Boolean));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function readCollectionViews(key:string,schema:CollectionSchema,storage:ViewStorage,reservedNames:readonly string[]=[]):ViewRead {
|
|
276
|
+
let raw:string|null=null;
|
|
277
|
+
try{
|
|
278
|
+
raw=storage.getItem(key);if(raw===null)return {state:'ready',views:[]};
|
|
279
|
+
const value:unknown=JSON.parse(raw);
|
|
280
|
+
if(!object(value)||value.version!==1||!Array.isArray(value.views))throw Error('Unsupported saved views');
|
|
281
|
+
const ids=new Set<string>(),names=new Set(reservedNames.map(name=>name.normalize('NFC').toLowerCase()));
|
|
282
|
+
const views=value.views.map((view:unknown):CollectionView=>{
|
|
283
|
+
if(!object(view)||typeof view.id!=='string'||!view.id.startsWith('saved-')||ids.has(view.id)||typeof view.name!=='string'||!view.name.trim()||view.name.length>80||names.has(view.name.trim().normalize('NFC').toLowerCase()))throw Error('Unsupported saved view');
|
|
284
|
+
ids.add(view.id);names.add(view.name.trim().normalize('NFC').toLowerCase());
|
|
285
|
+
return {id:view.id,name:view.name.trim(),query:parseCollectionQuery(view.query,schema)};
|
|
286
|
+
});
|
|
287
|
+
return {state:'ready',views};
|
|
288
|
+
}catch{return {state:'error',views:[],raw};}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Shared controls
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
"use client";
|
|
296
|
+
import {useEffect,useRef,useState,type FormEvent} from 'react';
|
|
297
|
+
import {Alert,AlertDialog,Button,CollectionToolbar,CollectionViewSwitcher,Dialog,Field,FilterGroup,IconSlot,Inline,Input,Menu,Select,Stack,Tag,type OverflowAction} from '@flood-md/react';
|
|
298
|
+
import {collectionQueryKey,copyCollectionQuery,parseCollectionQuery,readCollectionViews,type CollectionQuery,type CollectionSchema,type CollectionView,type FilterProperty,type ViewRead,type ViewStorage} from './collection-views-model';
|
|
299
|
+
|
|
300
|
+
export function AppliedFilterControl({ property, value, onChange }: {
|
|
301
|
+
property: FilterProperty;
|
|
302
|
+
value: string;
|
|
303
|
+
onChange: (value: string) => void;
|
|
304
|
+
}) {
|
|
305
|
+
const trigger = useRef<HTMLButtonElement | null>(null);
|
|
306
|
+
const { id, label, options } = property;
|
|
307
|
+
const selected = options.find(option => option.value === value);
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<Inline gap="2" align="center">
|
|
311
|
+
<Menu
|
|
312
|
+
label={label}
|
|
313
|
+
trigger={({ ref, ...props }) => (
|
|
314
|
+
<Button {...props} ref={node => { trigger.current = node; ref(node); }} variant="subtle" size="sm">
|
|
315
|
+
{label} <IconSlot size="xs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg></IconSlot>
|
|
316
|
+
</Button>
|
|
317
|
+
)}
|
|
318
|
+
items={[{ value: '', label: 'All', disabled: false }, ...options].map(option => ({
|
|
319
|
+
id: option.value ? `option-${option.value}` : 'all',
|
|
320
|
+
label: option.label,
|
|
321
|
+
type: 'radio',
|
|
322
|
+
group: id,
|
|
323
|
+
disabled: option.disabled,
|
|
324
|
+
checked: value === option.value,
|
|
325
|
+
closeOnSelect: true,
|
|
326
|
+
onCheckedChange: () => onChange(option.value),
|
|
327
|
+
}))}
|
|
328
|
+
/>
|
|
329
|
+
{selected && (
|
|
330
|
+
<Tag size="md" removeLabel={`Remove ${label.toLowerCase()} filter: ${selected.label}`} onRemove={() => {
|
|
331
|
+
trigger.current?.focus({ preventScroll: true });
|
|
332
|
+
onChange('');
|
|
333
|
+
}}>
|
|
334
|
+
{selected.label}
|
|
335
|
+
</Tag>
|
|
336
|
+
)}
|
|
337
|
+
</Inline>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface CollectionViewControlsProps {
|
|
342
|
+
label:string;schema:CollectionSchema;presets:readonly CollectionView[];
|
|
343
|
+
value:CollectionQuery;onValueChange:(query:CollectionQuery)=>void;storageKey:string;storage?:ViewStorage;
|
|
344
|
+
}
|
|
345
|
+
type NameDraft={kind:'create'|'rename';id?:string;name:string;snapshot:CollectionQuery;error?:string;invalid?:string};
|
|
346
|
+
const inlineFilterLimit = 2;
|
|
347
|
+
|
|
348
|
+
/** Host recipe: shared behaviour, not a second Flood component implementation. */
|
|
349
|
+
export function CollectionViewControls({label,schema,presets,value,onValueChange,storageKey,storage}:CollectionViewControlsProps){
|
|
350
|
+
const [saved,setSaved]=useState<ViewRead|{state:'loading';views:[]}>({state:'loading',views:[]}),[selectedId,setSelectedId]=useState(presets[0].id);
|
|
351
|
+
const [editing,setEditing]=useState(false),[draft,setDraft]=useState(()=>copyCollectionQuery(value)),[draftError,setDraftError]=useState('');
|
|
352
|
+
const [advancedEditor, setAdvancedEditor] = useState(false);
|
|
353
|
+
const editorPanel = useRef<HTMLDivElement>(null);
|
|
354
|
+
const [nameDraft,setNameDraft]=useState<NameDraft|null>(null),[confirmation,setConfirmation]=useState<{kind:'delete';id:string;name:string}|{kind:'replace'}|null>(null);
|
|
355
|
+
const [operationError,setOperationError]=useState<{kind:'update'|'delete'|'replace'|'export';message:string}|null>(null),[readAttempt,setReadAttempt]=useState(0);
|
|
356
|
+
const nameInput=useRef<HTMLInputElement>(null),viewInput=useRef<HTMLSelectElement>(null);
|
|
357
|
+
const schemaKey=JSON.stringify({properties:schema.properties.map(property=>[property.id,property.options.map(option=>option.value)]),sort:schema.sortColumns});
|
|
358
|
+
const appliedKey=collectionQueryKey(value);
|
|
359
|
+
const compactConditions = (value.match === 'all' || value.conditions.length < 2) &&
|
|
360
|
+
value.conditions.every(condition => condition.operator === 'is' && condition.values.length === 1);
|
|
361
|
+
const collapseFilters = schema.properties.length > inlineFilterLimit;
|
|
362
|
+
const canClearFilters = value.conditions.length > 0 || Boolean(value.search);
|
|
363
|
+
const simpleEditor = collapseFilters && compactConditions && !advancedEditor;
|
|
364
|
+
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (advancedEditor) editorPanel.current?.focus({ preventScroll: true });
|
|
367
|
+
}, [advancedEditor]);
|
|
368
|
+
|
|
369
|
+
function changeSimpleDraft(property: string, option: string) {
|
|
370
|
+
setDraft(current => {
|
|
371
|
+
const existing = current.conditions.find(condition => condition.property === property);
|
|
372
|
+
const conditions = current.conditions.filter(condition => condition.property !== property);
|
|
373
|
+
if (option) conditions.push({ id: existing?.id ?? crypto.randomUUID(), property, operator: 'is', values: [option] });
|
|
374
|
+
return { ...current, match: 'all', conditions };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function changeQuickFilter(property: string, option: string) {
|
|
379
|
+
const existing = value.conditions.find(condition => condition.property === property);
|
|
380
|
+
const conditions = value.conditions.filter(condition => condition.property !== property);
|
|
381
|
+
if (option) conditions.push({ id: existing?.id ?? crypto.randomUUID(), property, operator: 'is', values: [option] });
|
|
382
|
+
onValueChange({ ...value, match: 'all', conditions });
|
|
383
|
+
}
|
|
384
|
+
useEffect(()=>setEditing(false),[appliedKey]);
|
|
385
|
+
useEffect(()=>{try{setSaved(readCollectionViews(storageKey,schema,storage??window.localStorage,presets.map(preset=>preset.name)));}catch{setSaved({state:'error',views:[],raw:null});}},[storageKey,storage,schemaKey,readAttempt]);
|
|
386
|
+
const views=[...presets,...saved.views],selected=views.find(view=>view.id===selectedId)??presets[0];
|
|
387
|
+
const dirty=collectionQueryKey(value)!==collectionQueryKey(selected.query),custom=saved.views.find(view=>view.id===selected.id);
|
|
388
|
+
function persist(views:CollectionView[]){
|
|
389
|
+
if(saved.state!=='ready')return false;
|
|
390
|
+
try{(storage??window.localStorage).setItem(storageKey,JSON.stringify({version:1,views}));setSaved({state:'ready',views});setOperationError(null);return true;}catch{return false;}
|
|
391
|
+
}
|
|
392
|
+
const requestEditor=(next:boolean)=>{if(next){setDraft(copyCollectionQuery(value));setDraftError('');setAdvancedEditor(false);}setEditing(next);};
|
|
393
|
+
function apply(event:FormEvent<HTMLFormElement>){event.preventDefault();try{onValueChange(parseCollectionQuery({...value,match:draft.match,conditions:draft.conditions},schema));setEditing(false);}catch{setDraftError('Choose a value for each condition.');}}
|
|
394
|
+
function saveName(event:FormEvent<HTMLFormElement>){
|
|
395
|
+
event.preventDefault();if(!nameDraft)return;const name=nameDraft.name.trim();
|
|
396
|
+
if(!name){setNameDraft({...nameDraft,invalid:'Enter a view name.'});return;}
|
|
397
|
+
if(views.some(view=>view.id!==nameDraft.id&&view.name.normalize('NFC').toLowerCase()===name.normalize('NFC').toLowerCase())){setNameDraft({...nameDraft,invalid:'A view with this name already exists.'});return;}
|
|
398
|
+
const id=nameDraft.kind==='create'?`saved-${crypto.randomUUID()}`:nameDraft.id!;
|
|
399
|
+
const next=nameDraft.kind==='create'?[...saved.views,{id,name,query:copyCollectionQuery(nameDraft.snapshot)}]:saved.views.map(view=>view.id===id?{...view,name}:view);
|
|
400
|
+
if(persist(next)){setNameDraft(null);setSelectedId(id);}else setNameDraft({...nameDraft,error:'The view could not be saved. Try again.'});
|
|
401
|
+
}
|
|
402
|
+
function action(id:string){
|
|
403
|
+
if(id==='reset'){onValueChange(copyCollectionQuery(selected.query));setOperationError(error=>error?.kind==='update'?null:error);return;}
|
|
404
|
+
if(id==='create'||id==='rename'||id==='delete')viewInput.current?.focus({preventScroll:true});
|
|
405
|
+
if(id==='create')setNameDraft({kind:'create',name:'',snapshot:copyCollectionQuery(value)});
|
|
406
|
+
else if(id==='rename'&&custom)setNameDraft({kind:'rename',id:custom.id,name:custom.name,snapshot:copyCollectionQuery(custom.query)});
|
|
407
|
+
else if(id==='update'&&custom){if(!persist(saved.views.map(view=>view.id===custom.id?{...view,query:copyCollectionQuery(value)}:view)))setOperationError({kind:'update',message:'The view could not be updated. Try again.'});}
|
|
408
|
+
else if(id==='delete'&&custom)setConfirmation({kind:'delete',id:custom.id,name:custom.name});
|
|
409
|
+
}
|
|
410
|
+
function confirm(){
|
|
411
|
+
const pending=confirmation;setConfirmation(null);if(!pending)return;
|
|
412
|
+
if(pending.kind==='delete'){
|
|
413
|
+
if(persist(saved.views.filter(view=>view.id!==pending.id))){setSelectedId(presets[0].id);onValueChange(copyCollectionQuery(presets[0].query));}
|
|
414
|
+
else setOperationError({kind:'delete',message:'The view could not be deleted. Try again.'});
|
|
415
|
+
}else{
|
|
416
|
+
try{(storage??window.localStorage).setItem(storageKey,JSON.stringify({version:1,views:[]}));setSaved({state:'ready',views:[]});setSelectedId(presets[0].id);setOperationError(null);}
|
|
417
|
+
catch{setOperationError({kind:'replace',message:'Saved views could not be replaced.'});}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function exportSnapshot(){
|
|
421
|
+
if(saved.state!=='error'||saved.raw===null)return;
|
|
422
|
+
try{const url=URL.createObjectURL(new Blob([saved.raw],{type:'application/json'})),link=document.createElement('a');link.href=url;link.download='saved-views-recovery.json';link.click();setTimeout(()=>URL.revokeObjectURL(url),0);}catch{setOperationError({kind:'export',message:'The snapshot could not be exported.'});}
|
|
423
|
+
}
|
|
424
|
+
const actions:OverflowAction[]=[
|
|
425
|
+
...(custom&&dirty?[{id:'update',label:'Update view',priority:20,disabled:saved.state!=='ready'}]:[]),
|
|
426
|
+
{id:'create',label:'Save as new view',priority:10,disabled:saved.state!=='ready'},
|
|
427
|
+
...(dirty?[{id:'reset',label:'Reset view'}]:[]),
|
|
428
|
+
...(custom?[{id:'rename',label:'Rename',disabled:saved.state!=='ready'},{id:'delete',label:'Delete view',tone:'danger' as const,disabled:saved.state!=='ready'}]:[]),
|
|
429
|
+
];
|
|
430
|
+
return <Stack gap="3" className="collection-view-controls" aria-label={`${label} collection controls`}>
|
|
431
|
+
<CollectionViewSwitcher label={`${label} view`} selectRef={viewInput} controlSize="sm"
|
|
432
|
+
views={views.map(view=>({id:view.id,name:view.name}))} value={selected.id}
|
|
433
|
+
onValueChange={id=>{const next=views.find(view=>view.id===id);if(next){setSelectedId(next.id);setOperationError(null);onValueChange(copyCollectionQuery(next.query));}}}
|
|
434
|
+
actions={actions} actionsLabel={`${label} view actions`} moreLabel="View actions" onAction={action}/>
|
|
435
|
+
{saved.state==='error'&&<Stack gap="3"><Alert tone="error">Saved views could not be read.</Alert><Inline gap="3"><Button size="sm" variant="subtle" onClick={()=>setReadAttempt(attempt=>attempt+1)}>Retry saved views</Button>{saved.raw!==null&&<Button size="sm" variant="subtle" onClick={exportSnapshot}>Export snapshot</Button>}<Button size="sm" variant="subtle" tone="danger" onClick={()=>{viewInput.current?.focus({preventScroll:true});setConfirmation({kind:'replace'});}}>Replace saved views</Button></Inline></Stack>}
|
|
436
|
+
{operationError&&<Alert tone="error" announcement="polite"><Inline gap="3">{operationError.message}<Button size="sm" variant="ghost" onClick={()=>setOperationError(null)}>Dismiss</Button></Inline></Alert>}
|
|
437
|
+
<CollectionToolbar label={label+' collection toolbar'} searchValue={value.search}
|
|
438
|
+
onSearchValueChange={search=>onValueChange({...value,search})}
|
|
439
|
+
searchLabel={'Search '+label.toLowerCase()} searchPlaceholder={'Search '+label.toLowerCase()+'…'} clearSearchLabel={'Clear '+label.toLowerCase()+' search'}
|
|
440
|
+
appliedFiltersLabel={label+' applied filters'}
|
|
441
|
+
quickFilters={compactConditions&&!collapseFilters?schema.properties.map(property=><AppliedFilterControl key={property.id}
|
|
442
|
+
property={property} value={value.conditions.find(condition=>condition.property===property.id)?.values[0]??''}
|
|
443
|
+
onChange={option=>changeQuickFilter(property.id,option)} />):undefined}
|
|
444
|
+
appliedFilters={compactConditions&&!collapseFilters?[]:value.conditions.map(condition=>{
|
|
445
|
+
const property=schema.properties.find(property=>property.id===condition.property)!;
|
|
446
|
+
const labels=condition.values.map(id=>property.options.find(option=>option.value===id)!.label);
|
|
447
|
+
return {id:condition.id,label:property.label+(compactConditions?': ':condition.operator==='is'?' is ':' excludes ')+labels.join(condition.operator==='is'?' or ':', '),
|
|
448
|
+
removeLabel:'Remove '+property.label.toLowerCase()+' filter'+(compactConditions?': '+labels[0]:'')};
|
|
449
|
+
})}
|
|
450
|
+
summaryPrefix={!compactConditions&&value.conditions.length>1?(value.match==='all'?'All:':'Any:'):undefined}
|
|
451
|
+
onRemoveFilter={id=>onValueChange({...value,conditions:value.conditions.filter(condition=>condition.id!==id)})}
|
|
452
|
+
onClear={()=>onValueChange({...value,search:'',conditions:[]})} canClear={canClearFilters}
|
|
453
|
+
filterTitle={label+' filters'} filtersOpen={editing} onFiltersOpenChange={requestEditor}
|
|
454
|
+
filterPanelRef={editorPanel} filterPanelClassName={simpleEditor?undefined:'collection-filter-editor'}
|
|
455
|
+
filterContent={
|
|
456
|
+
<form onSubmit={apply}><Stack gap="4">
|
|
457
|
+
{simpleEditor ? <>
|
|
458
|
+
{schema.properties.map(property => (
|
|
459
|
+
<Field key={property.id} label={property.label}>{props => (
|
|
460
|
+
<Select {...props} controlSize="sm"
|
|
461
|
+
value={draft.conditions.find(condition => condition.property === property.id)?.values[0] ?? ''}
|
|
462
|
+
onChange={event => changeSimpleDraft(property.id, event.currentTarget.value)}>
|
|
463
|
+
<option value="">All</option>
|
|
464
|
+
{property.options.map(option => <option key={option.value} value={option.value} disabled={option.disabled}>{option.label}</option>)}
|
|
465
|
+
</Select>
|
|
466
|
+
)}</Field>
|
|
467
|
+
))}
|
|
468
|
+
<Button variant="subtle" size="sm" onClick={() => setAdvancedEditor(true)}>Advanced conditions</Button>
|
|
469
|
+
</> : <>
|
|
470
|
+
<FilterGroup label={`${label} conditions`} properties={schema.properties}
|
|
471
|
+
value={{match:draft.match,conditions:draft.conditions}}
|
|
472
|
+
onValueChange={next=>{setDraft(current=>({...current,...next}));setDraftError('');}} />
|
|
473
|
+
</>}
|
|
474
|
+
{draftError&&<Alert tone="error" announcement="polite">{draftError}</Alert>}
|
|
475
|
+
<Inline gap="3"><Button type="submit">Apply filters</Button><Button variant="subtle" onClick={()=>setEditing(false)}>Cancel</Button></Inline>
|
|
476
|
+
</Stack></form>
|
|
477
|
+
} />
|
|
478
|
+
{nameDraft&&<Dialog title={nameDraft.kind==='create'?'Save view':'Rename view'} open onOpenChange={next=>{if(!next)setNameDraft(null);}} trigger={()=>null} initialFocusRef={nameInput}>
|
|
479
|
+
<form onSubmit={saveName}><Stack gap="6"><Field label="View name" required error={nameDraft.invalid}>{props=><Input {...props} ref={nameInput} maxLength={80} value={nameDraft.name} onChange={event=>setNameDraft({...nameDraft,name:event.target.value,invalid:undefined})}/>}</Field>{nameDraft.error&&<Alert tone="error" announcement="polite">{nameDraft.error}</Alert>}<Inline gap="3"><Button type="submit">{nameDraft.kind==='create'?'Save view':'Rename view'}</Button><Button variant="subtle" onClick={()=>setNameDraft(null)}>Cancel</Button></Inline></Stack></form>
|
|
480
|
+
</Dialog>}
|
|
481
|
+
{confirmation&&<AlertDialog open onOpenChange={next=>{if(!next)setConfirmation(null);}} trigger={()=>null} title={confirmation.kind==='delete'?`Delete “${confirmation.name}”?`:'Replace saved views?'} description={confirmation.kind==='delete'?'The saved view will be removed. Collection records are unchanged.':'Unreadable saved views will be replaced with an empty set. Export the snapshot first if you need it.'} confirmLabel={confirmation.kind==='delete'?'Delete view':'Replace saved views'} cancelLabel="Keep views" onConfirm={confirm}/>}
|
|
482
|
+
</Stack>;
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Two consumers
|
|
487
|
+
|
|
488
|
+
Projects keeps DataTable mounted when an accepted filter finds no rows. Its
|
|
489
|
+
emptyContent composes EmptyState and Reset filters; the host clears search and
|
|
490
|
+
conditions, retains sort, resets page/selection and returns focus to search.
|
|
491
|
+
This is distinct from a read failure or loading state, which remain DataTable's
|
|
492
|
+
Alert/retry and Spinner owners. Files uses EmptyState outside its native FileList.
|
|
493
|
+
|
|
494
|
+
The last export is the website-only shell. Hosts need the model, controls and their
|
|
495
|
+
chosen consumer with actual data and their own chrome.
|
|
496
|
+
|
|
497
|
+
```tsx
|
|
498
|
+
"use client";
|
|
499
|
+
import {useEffect,useRef,useState} from 'react';
|
|
500
|
+
import {Alert,Button,DataTable,EmptyState,Field,FileItem,FileList,FloodRoot,Heading,Inline,Select,Stack,StatusIndicator,Tabs} from '@flood-md/react';
|
|
501
|
+
import {SiteHeader} from './site-header';
|
|
502
|
+
import {SiteFooter} from './site-footer';
|
|
503
|
+
import {CollectionViewControls} from './recipes/collection-views';
|
|
504
|
+
import {emptyCollectionQuery,matchesCollectionQuery,type CollectionQuery,type CollectionSchema,type CollectionView,type ViewStorage} from './recipes/collection-views-model';
|
|
505
|
+
import './collections-playground.css';
|
|
506
|
+
import {ProjectSelectionActions,type ProjectStatusUpdate} from './recipes/project-selection-actions';
|
|
507
|
+
|
|
508
|
+
const owners=[{value:'maya',label:'Maya'},{value:'noah',label:'Noah'},{value:'sam',label:'Sam'}];
|
|
509
|
+
const projectSchema:CollectionSchema={properties:[
|
|
510
|
+
{id:'status',label:'Status',options:[{value:'active',label:'Active'},{value:'paused',label:'Paused'},{value:'archived',label:'Archived'}]},
|
|
511
|
+
{id:'owner',label:'Owner',options:owners},
|
|
512
|
+
{id:'priority',label:'Priority',options:[{value:'urgent',label:'Urgent'},{value:'normal',label:'Normal'}]},
|
|
513
|
+
],sortColumns:['name','owner','status','priority']};
|
|
514
|
+
const projects=[
|
|
515
|
+
{id:'design',name:'Design system',status:'active',owner:'maya',priority:'urgent'},
|
|
516
|
+
{id:'checkout',name:'Checkout',status:'active',owner:'noah',priority:'urgent'},
|
|
517
|
+
{id:'help',name:'Help center',status:'paused',owner:'sam',priority:'normal'},
|
|
518
|
+
{id:'portal',name:'Customer portal',status:'active',owner:'maya',priority:'normal'},
|
|
519
|
+
{id:'analytics',name:'Analytics',status:'archived',owner:'noah',priority:'normal'},
|
|
520
|
+
{id:'onboarding',name:'Onboarding',status:'paused',owner:'maya',priority:'urgent'},
|
|
521
|
+
{id:'docs',name:'Documentation',status:'active',owner:'sam',priority:'normal'},
|
|
522
|
+
{id:'billing',name:'Billing',status:'archived',owner:'sam',priority:'normal'},
|
|
523
|
+
];
|
|
524
|
+
const projectPresets:CollectionView[]=[{id:'all',name:'All projects',query:emptyCollectionQuery()},{id:'active',name:'Active projects',query:{...emptyCollectionQuery(),conditions:[{id:'preset-active',property:'status',operator:'is',values:['active']}]}}];
|
|
525
|
+
|
|
526
|
+
export function ProjectCollection({storage,onUpdateStatus}: {
|
|
527
|
+
storage?:ViewStorage;
|
|
528
|
+
onUpdateStatus?:(update:ProjectStatusUpdate)=>void|Promise<void>;
|
|
529
|
+
}){
|
|
530
|
+
const [records, setRecords] = useState(projects);
|
|
531
|
+
const [query, setQuery] = useState(emptyCollectionQuery);
|
|
532
|
+
const [page, setPage] = useState(1);
|
|
533
|
+
const [selection, setSelection] = useState<readonly string[]>([]);
|
|
534
|
+
const [pending, setPending] = useState(false);
|
|
535
|
+
const [feedback, setFeedback] = useState<
|
|
536
|
+
{ kind: 'success'; message: string } |
|
|
537
|
+
{ kind: 'error'; update: ProjectStatusUpdate; message: string } | null
|
|
538
|
+
>(null);
|
|
539
|
+
const inFlight = useRef(false);
|
|
540
|
+
const collection = useRef<HTMLElement>(null);
|
|
541
|
+
const selectionPanel = useRef<HTMLDivElement>(null);
|
|
542
|
+
const change = (query: CollectionQuery) => {
|
|
543
|
+
setQuery(query);
|
|
544
|
+
setPage(1);
|
|
545
|
+
setSelection([]);
|
|
546
|
+
setFeedback(null);
|
|
547
|
+
};
|
|
548
|
+
const rows=records.filter(project=>matchesCollectionQuery(query,`${project.name} ${owners.find(owner=>owner.value===project.owner)!.label}`,property=>project[property as 'status'|'owner'|'priority']));
|
|
549
|
+
const selectedProjects=rows.filter(project=>selection.includes(project.id));
|
|
550
|
+
const hasSelection = selectedProjects.length > 0;
|
|
551
|
+
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
const panel = selectionPanel.current;
|
|
554
|
+
const host = collection.current;
|
|
555
|
+
if (!hasSelection || !panel || !host) return;
|
|
556
|
+
const measure = () => host.style.setProperty('--collection-selection-height', `${panel.getBoundingClientRect().height}px`);
|
|
557
|
+
measure();
|
|
558
|
+
const observer = new ResizeObserver(measure);
|
|
559
|
+
observer.observe(panel);
|
|
560
|
+
return () => {
|
|
561
|
+
observer.disconnect();
|
|
562
|
+
host.style.removeProperty('--collection-selection-height');
|
|
563
|
+
};
|
|
564
|
+
}, [hasSelection]);
|
|
565
|
+
|
|
566
|
+
async function updateStatus(proposal: ProjectStatusUpdate) {
|
|
567
|
+
if (inFlight.current) return;
|
|
568
|
+
const update = { ids: [...new Set(proposal.ids)], status: proposal.status };
|
|
569
|
+
if (!update.ids.length) return;
|
|
570
|
+
inFlight.current = true;
|
|
571
|
+
setPending(true);
|
|
572
|
+
setFeedback(null);
|
|
573
|
+
try {
|
|
574
|
+
await onUpdateStatus?.({ ids: [...update.ids], status: update.status });
|
|
575
|
+
const ids = new Set(update.ids);
|
|
576
|
+
setRecords(current => current.map(project => ids.has(project.id) ? { ...project, status: update.status } : project));
|
|
577
|
+
if (selectionPanel.current?.contains(collection.current?.ownerDocument.activeElement ?? null)) {
|
|
578
|
+
collection.current?.focus({ preventScroll: true });
|
|
579
|
+
}
|
|
580
|
+
setSelection(current => current.filter(id => !ids.has(id)));
|
|
581
|
+
setPage(1);
|
|
582
|
+
const result = update.status === 'active' ? 'set active' : update.status;
|
|
583
|
+
setFeedback({ kind: 'success', message: `${ids.size} ${ids.size === 1 ? 'project' : 'projects'} ${result}.` });
|
|
584
|
+
} catch {
|
|
585
|
+
setFeedback({ kind: 'error', update, message: `Couldn’t update ${update.ids.length} ${update.ids.length === 1 ? 'project' : 'projects'}.` });
|
|
586
|
+
} finally {
|
|
587
|
+
inFlight.current = false;
|
|
588
|
+
setPending(false);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return <section ref={collection} tabIndex={-1} className="collection-projects" data-selected={hasSelection || undefined} aria-label="Projects collection"><Stack gap="6">
|
|
593
|
+
<CollectionViewControls label="Projects" schema={projectSchema} presets={projectPresets} value={query} onValueChange={change} storageKey="flood-project-views-v1" storage={storage}/>
|
|
594
|
+
{hasSelection&&<div ref={selectionPanel} className="collection-selection-panel"><ProjectSelectionActions projects={selectedProjects} pending={pending} onUpdate={update=>{void updateStatus(update);}} onClear={()=>{
|
|
595
|
+
collection.current?.focus({preventScroll:true});setSelection([]);setFeedback(null);
|
|
596
|
+
}}/></div>}
|
|
597
|
+
{feedback&&<Alert tone={feedback.kind==='error'?'error':'success'} announcement="polite"><Inline gap="3">
|
|
598
|
+
{feedback.message}
|
|
599
|
+
{feedback.kind==='error'&&<Button size="sm" variant="subtle" disabled={pending} onClick={()=>{
|
|
600
|
+
collection.current?.focus({preventScroll:true});void updateStatus(feedback.update);
|
|
601
|
+
}}>Retry update</Button>}
|
|
602
|
+
<Button size="sm" variant="ghost" disabled={pending} onClick={()=>{
|
|
603
|
+
collection.current?.focus({preventScroll:true});setFeedback(null);
|
|
604
|
+
}}>Dismiss</Button>
|
|
605
|
+
</Inline></Alert>}
|
|
606
|
+
<DataTable caption="Projects" hideCaption minWidth="36rem" selectable pageSize={5} page={page} onPageChange={setPage} selectedIds={selection} onSelectionChange={ids=>{setSelection(ids);setFeedback(null);}} sort={query.sort} onSortChange={sort=>change({...query,sort})}
|
|
607
|
+
emptyContent={<EmptyState title="No matching projects" actions={<Button variant="subtle" onClick={()=>{change({...query,search:'',conditions:[]});collection.current?.querySelector<HTMLInputElement>('.flood-collection-toolbar input')?.focus({preventScroll:true});}}>Reset filters</Button>}/>}
|
|
608
|
+
columns={[{id:'name',label:'Project',sortable:true},{id:'owner',label:'Owner',sortable:true},{id:'status',label:'Status',sortable:true},{id:'priority',label:'Priority',sortable:true}]}
|
|
609
|
+
rows={rows.map(project=>({id:project.id,label:project.name,cells:{name:project.name,owner:owners.find(owner=>owner.value===project.owner)!.label,status:<StatusIndicator label={projectSchema.properties[0].options.find(option=>option.value===project.status)!.label} tone={project.status==='active'?'success':project.status==='paused'?'warning':'neutral'}/>,priority:project.priority==='urgent'?'Urgent':'Normal'},sortValues:{name:project.name,owner:project.owner,status:project.status,priority:project.priority==='urgent'?1:0}}))}/>
|
|
610
|
+
</Stack></section>;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const teams=[{value:'design',label:'Design team'},{value:'product',label:'Product team'},{value:'finance',label:'Finance team'}];
|
|
614
|
+
const fileSchema:CollectionSchema={properties:[
|
|
615
|
+
{id:'kind',label:'Format',options:[{value:'pdf',label:'PDF'},{value:'fig',label:'Figma'},{value:'png',label:'PNG'},{value:'csv',label:'CSV'}]},
|
|
616
|
+
{id:'team',label:'Team',options:teams},
|
|
617
|
+
],sortColumns:['name','size']};
|
|
618
|
+
const files=[
|
|
619
|
+
{id:'guidelines',name:'Brand guidelines.pdf',kind:'pdf',team:'design',size:2400000},
|
|
620
|
+
{id:'library',name:'Component library.fig',kind:'fig',team:'design',size:18400000},
|
|
621
|
+
{id:'research',name:'Research summary.pdf',kind:'pdf',team:'product',size:780000},
|
|
622
|
+
{id:'forecast',name:'Revenue forecast.csv',kind:'csv',team:'finance',size:28000},
|
|
623
|
+
{id:'cover',name:'Launch cover.png',kind:'png',team:'design',size:4100000},
|
|
624
|
+
{id:'feedback',name:'Customer feedback.csv',kind:'csv',team:'product',size:92000},
|
|
625
|
+
];
|
|
626
|
+
const fileDefault=():CollectionQuery=>({...emptyCollectionQuery(),sort:{column:'name',direction:'ascending'}});
|
|
627
|
+
const filePresets:CollectionView[]=[{id:'all',name:'All files',query:fileDefault()},{id:'design',name:'Design files',query:{...fileDefault(),conditions:[{id:'preset-design',property:'team',operator:'is',values:['design']}]}}];
|
|
628
|
+
export function FileCollection({storage}: {storage?:ViewStorage}){
|
|
629
|
+
const [query,setQuery]=useState(fileDefault);
|
|
630
|
+
const sort=query.sort??{column:'name',direction:'ascending'};
|
|
631
|
+
const rows=files.filter(file=>matchesCollectionQuery(query,file.name,property=>file[property as 'kind'|'team'])).sort((a,b)=>(sort.column==='size'?a.size-b.size:a.name.localeCompare(b.name))*(sort.direction==='ascending'?1:-1));
|
|
632
|
+
return <section aria-label="Files collection"><Stack gap="6">
|
|
633
|
+
<CollectionViewControls label="Files" schema={fileSchema} presets={filePresets} value={query} onValueChange={setQuery} storageKey="flood-file-views-v1" storage={storage}/>
|
|
634
|
+
<div style={{maxWidth:'16rem'}}><Field label="Sort files" hideLabel>{props=><Select {...props} value={`${sort.column}:${sort.direction}`} onChange={event=>{const[column,direction]=event.target.value.split(':');setQuery({...query,sort:{column,direction:direction as 'ascending'|'descending'}});}}><option value="name:ascending">Name A–Z</option><option value="name:descending">Name Z–A</option><option value="size:ascending">Size small–large</option><option value="size:descending">Size large–small</option></Select>}</Field></div>
|
|
635
|
+
{rows.length?<FileList label="Files">{rows.map(file=><FileItem key={file.id} name={file.name} size={file.size} format={file.kind.toUpperCase()} description={teams.find(team=>team.value===file.team)!.label}/>)}</FileList>:<EmptyState title="No matching files" actions={<Button variant="subtle" onClick={()=>setQuery({...query,search:'',conditions:[]})}>Reset filters</Button>}/>}
|
|
636
|
+
</Stack></section>;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export default function CollectionsPlayground(){return <FloodRoot className="site-root"><div className="site-shell"><SiteHeader path="/playground/collections"/><main id="main-content" className="collection-examples"><Stack gap="4"><Heading level={1} size="page">Collections</Heading><Tabs label="Collection examples" variant="line" items={[{value:'projects',label:'Projects',content:<ProjectCollection/>},{value:'files',label:'Files',content:<FileCollection/>}]}/></Stack></main><SiteFooter/></div></FloodRoot>;}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Verification
|
|
643
|
+
|
|
644
|
+
Exercise all/any, exclusion, incomplete drafts, Cancel/Escape, removal and no-results.
|
|
645
|
+
Verify page/selection changes; save, rename while dirty, update, reset, delete and
|
|
646
|
+
reload. Refuse reads/writes and preserve bytes/names. Check both consumers, nested
|
|
647
|
+
focus, narrow/enlarged geometry and final scroll actions. These checks do not
|
|
648
|
+
establish full UX, performance or assistive-technology certification.
|