@flood-md/agent 0.2.9 → 0.2.10
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 +26 -4
- package/README.md +35 -4
- package/SKILL.md +31 -0
- package/dist/catalog.json +7136 -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-aspect-ratio.md +36 -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-button-group.md +39 -0
- package/dist/materials/component-button.md +43 -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 +40 -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 +50 -0
- package/dist/materials/component-command-list.md +48 -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 +42 -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 +58 -0
- package/dist/materials/component-date-field.md +57 -0
- package/dist/materials/component-date-picker.md +66 -0
- package/dist/materials/component-date-range-field.md +62 -0
- package/dist/materials/component-date-range-picker.md +72 -0
- package/dist/materials/component-date-time-picker.md +60 -0
- package/dist/materials/component-date-time-range-picker.md +66 -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 +50 -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-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 +41 -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-meter.md +43 -0
- package/dist/materials/component-metric.md +39 -0
- package/dist/materials/component-multi-select.md +52 -0
- package/dist/materials/component-navigation-list.md +38 -0
- package/dist/materials/component-navigation-tree.md +43 -0
- package/dist/materials/component-number-input.md +47 -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 +47 -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 +51 -0
- package/dist/materials/component-select.md +45 -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 +43 -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 +39 -0
- package/dist/materials/component-toolbar.md +42 -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 +88 -0
- package/dist/materials/guide-agents.md +96 -0
- package/dist/materials/guide-faq.md +84 -0
- package/dist/materials/guide-foundations.md +232 -0
- package/dist/materials/guide-installation.md +79 -0
- package/dist/materials/guide-interface-content.md +76 -0
- package/dist/materials/guide-introduction.md +40 -0
- package/dist/materials/guide-layout.md +193 -0
- package/dist/materials/guide-privacy.md +23 -0
- package/dist/materials/guide-project-ui.md +127 -0
- package/dist/materials/guide-skills.md +108 -0
- package/dist/materials/guide-theming.md +73 -0
- package/dist/materials/recipe-command-palette.md +66 -0
- package/dist/materials/recipe-filter-bar.md +59 -0
- package/dist/materials/recipe-form-submission.md +39 -0
- package/dist/materials/recipe-grouped-form.md +66 -0
- package/dist/materials/recipe-select-all.md +31 -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.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.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.canvas.json +39 -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.overlay.json +39 -0
- package/dist/materials/token-sys.color.surface.raised.json +39 -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.duration-fast.json +34 -0
- package/dist/materials/token-sys.motion.duration-normal.json +34 -0
- package/dist/materials/token-sys.motion.duration-reduced.json +34 -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 +409 -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 +41 -1
- package/package.json +7 -1
- package/references/flood-composition/SKILL.md +30 -0
- package/references/flood-data-display/SKILL.md +32 -0
- package/references/flood-feedback/SKILL.md +24 -3
- package/references/flood-forms/SKILL.md +51 -2
- package/references/flood-foundations/SKILL.md +7 -0
- package/references/flood-hierarchy/SKILL.md +87 -0
- package/references/flood-interface-content/SKILL.md +142 -0
- package/references/flood-overlays/SKILL.md +7 -0
- package/references/flood-records/SKILL.md +108 -0
- package/references/flood-spacing/SKILL.md +16 -0
- package/references/flood-verification/SKILL.md +52 -0
- package/references/interface-content-examples.md +47 -0
- package/references/knowledge.md +49 -0
- package/references/record-decisions.md +25 -0
- package/references/rendered-review.md +97 -0
- package/references/routing.md +1 -0
- package/rules.json +152 -2
- package/workflows.json +146 -10
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFile, lstat, realpath, open } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { dirname, resolve, relative, isAbsolute, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const KINDS = new Set(['guide', 'workflow', 'reference', 'component', 'recipe', 'token']);
|
|
8
|
+
const ID = /^[a-z][a-z0-9-]*:[a-z0-9][a-z0-9.-]*$/;
|
|
9
|
+
const MAX_CATALOG_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const MAX_MATERIAL_BYTES = 1024 * 1024;
|
|
11
|
+
const PAGE_BYTES = 16 * 1024;
|
|
12
|
+
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
const fail = (code, message) => { const error = new Error(message); error.code = code; throw error; };
|
|
14
|
+
const inside = (root, path) => { const r = relative(root, path); return r === '' || (r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r)); };
|
|
15
|
+
const safePath = path => typeof path === 'string' && /^[a-zA-Z0-9._/-]+$/.test(path) && !path.startsWith('/') && !path.split('/').some(part => !part || part === '.' || part === '..');
|
|
16
|
+
const text = (value, limit) => typeof value === 'string' && value.trim().length > 0 && value.length <= limit;
|
|
17
|
+
|
|
18
|
+
/** Validate the data contract without loading material bodies or executing instructions. */
|
|
19
|
+
export function validateKnowledgeCatalog(catalog) {
|
|
20
|
+
if (catalog?.schemaVersion !== 1 || !/^\d+\.\d+\.\d+$/.test(catalog.version ?? '') || !Array.isArray(catalog.groups) || !catalog.groups.length || catalog.groups.length > 16 || !Array.isArray(catalog.materials) || !catalog.materials.length || !Array.isArray(catalog.capabilities) || catalog.capabilities.length > 32) fail('INVALID_CATALOG', 'Unsupported or incomplete Flood knowledge catalog.');
|
|
21
|
+
const groups = new Set(), ids = new Set(), files = new Set();
|
|
22
|
+
for (const group of catalog.groups) {
|
|
23
|
+
if (!/^[a-z][a-z0-9-]*$/.test(group.id ?? '') || groups.has(group.id) || !text(group.name, 128) || !text(group.description, 256)) fail('INVALID_CATALOG', 'Invalid or duplicate knowledge group.');
|
|
24
|
+
groups.add(group.id);
|
|
25
|
+
}
|
|
26
|
+
let previous = '';
|
|
27
|
+
for (const item of catalog.materials) {
|
|
28
|
+
if (!ID.test(item.id ?? '') || ids.has(item.id) || item.id <= previous || !KINDS.has(item.kind) || !item.id.startsWith(item.kind + ':') || !text(item.name, 128) || !text(item.description, 1024) || !safePath(item.file) || files.has(item.file) || !/^[a-f0-9]{64}$/.test(item.sha256 ?? '') || !Number.isSafeInteger(item.bytes) || item.bytes < 1 || item.bytes > MAX_MATERIAL_BYTES || !Array.isArray(item.groups) || !item.groups.length || item.groups.length > 8 || new Set(item.groups).size !== item.groups.length || item.groups.some(group => !groups.has(group)) || !Array.isArray(item.links) || !Array.isArray(item.requires) || item.status !== 'available') fail('INVALID_CATALOG', 'Invalid, unsorted or duplicate knowledge material.');
|
|
29
|
+
if (item.installedTarget !== undefined && (!safePath(item.installedTarget) || !(item.installedTarget.startsWith('flood/') || item.installedTarget.startsWith('.agents/skills/flood-ui/')))) fail('INVALID_CATALOG', 'Invalid owned guide target.');
|
|
30
|
+
if (item.requires.length > 16 || item.links.length > 256 || new Set(item.requires).size !== item.requires.length || new Set(item.links).size !== item.links.length) fail('INVALID_CATALOG', 'Knowledge relationships exceed their per-material budget or contain duplicates.');
|
|
31
|
+
ids.add(item.id); files.add(item.file); previous = item.id;
|
|
32
|
+
}
|
|
33
|
+
const byId = new Map(catalog.materials.map(item => [item.id, item]));
|
|
34
|
+
for (const item of catalog.materials) for (const id of [...item.links, ...item.requires]) if (!ids.has(id)) fail('INVALID_CATALOG', `Unresolved knowledge relationship: ${id}`);
|
|
35
|
+
const visiting = new Set(), depths = new Map();
|
|
36
|
+
const visit = id => {
|
|
37
|
+
if (visiting.has(id)) fail('INVALID_CATALOG', 'Knowledge prerequisite cycle.');
|
|
38
|
+
if (depths.has(id)) return depths.get(id);
|
|
39
|
+
if (visiting.size >= 32) fail('INVALID_CATALOG', 'Knowledge prerequisites exceed the supported depth.');
|
|
40
|
+
visiting.add(id);
|
|
41
|
+
let depth = 1;
|
|
42
|
+
for (const dependency of byId.get(id).requires) depth = Math.max(depth, 1 + visit(dependency));
|
|
43
|
+
visiting.delete(id);
|
|
44
|
+
if (depth > 32) fail('INVALID_CATALOG', 'Knowledge prerequisites exceed the supported depth.');
|
|
45
|
+
depths.set(id, depth); return depth;
|
|
46
|
+
};
|
|
47
|
+
for (const id of ids) visit(id);
|
|
48
|
+
const capabilities = new Set();
|
|
49
|
+
for (const item of catalog.capabilities) {
|
|
50
|
+
if (!/^[a-z][a-z0-9-]*$/.test(item.id ?? '') || capabilities.has(item.id) || !['available', 'not-provided'].includes(item.status) || !text(item.description, 256) || !Array.isArray(item.materials) || item.materials.length > 16 || item.materials.some(id => !ids.has(id)) || (item.status === 'available' && !item.materials.length) || (item.status === 'not-provided' && item.materials.length)) fail('INVALID_CATALOG', 'Invalid knowledge capability declaration.');
|
|
51
|
+
capabilities.add(item.id);
|
|
52
|
+
}
|
|
53
|
+
return catalog;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readRegular(root, path, maxBytes) {
|
|
57
|
+
if (!safePath(path)) fail('INVALID_PATH', 'Knowledge paths must come from the package catalog.');
|
|
58
|
+
const base = await realpath(root);
|
|
59
|
+
let current = base;
|
|
60
|
+
for (const part of path.split('/')) {
|
|
61
|
+
current = resolve(current, part);
|
|
62
|
+
const stat = await lstat(current).catch(() => fail('MISSING_MATERIAL', `Missing knowledge file: ${path}`));
|
|
63
|
+
if (stat.isSymbolicLink()) fail('INVALID_PATH', 'Refusing a linked knowledge file or directory.');
|
|
64
|
+
}
|
|
65
|
+
if (!inside(base, await realpath(current))) fail('INVALID_PATH', 'Knowledge file left its package root.');
|
|
66
|
+
const handle = await open(current, 'r');
|
|
67
|
+
try {
|
|
68
|
+
const stat = await handle.stat();
|
|
69
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > maxBytes) fail('INVALID_MATERIAL', 'Knowledge file is not a bounded regular file.');
|
|
70
|
+
const bytes = await handle.readFile();
|
|
71
|
+
if (bytes.length > maxBytes) fail('INVALID_MATERIAL', 'Knowledge file exceeds its read budget.');
|
|
72
|
+
return bytes;
|
|
73
|
+
} finally { await handle.close(); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const decode = bytes => { try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { fail('INVALID_MATERIAL', 'Knowledge must be valid UTF-8.'); } };
|
|
77
|
+
const summary = ({id, kind, name, description, groups, status, links, requires}) => ({id, kind, name, description, groups: [...groups], status, relatedCount: links.length, requires: [...requires]});
|
|
78
|
+
|
|
79
|
+
/** A provider-neutral, read-only reader. A fresh catalog is checked on every operation. */
|
|
80
|
+
export function createKnowledgeReader({root = DEFAULT_ROOT} = {}) {
|
|
81
|
+
async function load() {
|
|
82
|
+
const bytes = await readRegular(root, 'dist/catalog.json', MAX_CATALOG_BYTES);
|
|
83
|
+
let catalog;
|
|
84
|
+
try { catalog = JSON.parse(decode(bytes)); } catch (error) { if (error.code) throw error; fail('INVALID_CATALOG', 'Malformed Flood knowledge catalog.'); }
|
|
85
|
+
validateKnowledgeCatalog(catalog);
|
|
86
|
+
const pkg = JSON.parse(decode(await readRegular(root, 'package.json', 64 * 1024)));
|
|
87
|
+
if (pkg.name !== '@flood-md/agent' || pkg.version !== catalog.version) fail('VERSION_MISMATCH', 'Agent package and knowledge catalog versions differ.');
|
|
88
|
+
return {catalog, fingerprint: digest(bytes)};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
async info() {
|
|
92
|
+
const {catalog, fingerprint} = await load();
|
|
93
|
+
return {schemaVersion: 1, version: catalog.version, fingerprint, materialCount: catalog.materials.length,
|
|
94
|
+
groups: catalog.groups.map(group => ({...group, count: catalog.materials.filter(item => item.groups.includes(group.id)).length})),
|
|
95
|
+
kinds: [...KINDS].map(kind => ({kind, count: catalog.materials.filter(item => item.kind === kind).length})),
|
|
96
|
+
capabilities: structuredClone(catalog.capabilities)};
|
|
97
|
+
},
|
|
98
|
+
async list({kind, group, search = '', limit = 12, cursor} = {}) {
|
|
99
|
+
if ((kind !== undefined && !KINDS.has(kind)) || !Number.isSafeInteger(limit) || limit < 1 || limit > 50 || typeof search !== 'string' || search.length > 160) fail('INVALID_QUERY', 'Invalid catalog kind, limit or search.');
|
|
100
|
+
const {catalog, fingerprint} = await load();
|
|
101
|
+
if (group !== undefined && !catalog.groups.some(item => item.id === group)) fail('INVALID_QUERY', 'Unknown knowledge group.');
|
|
102
|
+
const query = {kind: kind ?? null, group: group ?? null, search: search.trim().toLowerCase()};
|
|
103
|
+
const queryHash = digest(JSON.stringify(query));
|
|
104
|
+
let offset = 0;
|
|
105
|
+
if (cursor !== undefined) {
|
|
106
|
+
let state;
|
|
107
|
+
try { if (typeof cursor !== 'string' || cursor.length > 1024 || !/^[A-Za-z0-9_-]+$/.test(cursor)) throw Error(); state = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')); } catch { fail('INVALID_CURSOR', 'Malformed knowledge cursor.'); }
|
|
108
|
+
if (state.fingerprint !== fingerprint || state.query !== queryHash || !Number.isSafeInteger(state.offset) || state.offset < 1) fail('STALE_CURSOR', 'Catalog or query changed; start a fresh catalog page.');
|
|
109
|
+
offset = state.offset;
|
|
110
|
+
}
|
|
111
|
+
const matches = catalog.materials.filter(item => (!kind || item.kind === kind) && (!group || item.groups.includes(group)) && (!query.search || `${item.id} ${item.name} ${item.description}`.toLowerCase().includes(query.search)));
|
|
112
|
+
if (offset > matches.length) fail('INVALID_CURSOR', 'Knowledge cursor is outside the result set.');
|
|
113
|
+
const page = [];
|
|
114
|
+
const response = () => {
|
|
115
|
+
const next = offset + page.length;
|
|
116
|
+
return {schemaVersion: 1, version: catalog.version, fingerprint, query, total: matches.length, items: page,
|
|
117
|
+
truncated: next < matches.length, nextCursor: next < matches.length ? Buffer.from(JSON.stringify({fingerprint, query: queryHash, offset: next})).toString('base64url') : null};
|
|
118
|
+
};
|
|
119
|
+
for (const item of matches.slice(offset, offset + limit)) {
|
|
120
|
+
page.push(summary(item));
|
|
121
|
+
if (Buffer.byteLength(JSON.stringify(response())) > PAGE_BYTES) { page.pop(); break; }
|
|
122
|
+
}
|
|
123
|
+
if (offset < matches.length && !page.length) fail('INVALID_CATALOG', 'A catalog entry exceeds the discovery budget.');
|
|
124
|
+
return response();
|
|
125
|
+
},
|
|
126
|
+
async read(id, {offset = 0, length = 10000, linksOffset = 0} = {}) {
|
|
127
|
+
if (!ID.test(id ?? '') || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 1 || length > 40000 || !Number.isSafeInteger(linksOffset) || linksOffset < 0) fail('INVALID_QUERY', 'Use a known material ID and a bounded character range.');
|
|
128
|
+
const {catalog, fingerprint} = await load();
|
|
129
|
+
const item = catalog.materials.find(item => item.id === id);
|
|
130
|
+
if (!item) fail('UNKNOWN_MATERIAL', `Unknown installed knowledge material: ${id}`);
|
|
131
|
+
if (linksOffset > item.links.length) fail('INVALID_QUERY', 'Link offset is outside the material.');
|
|
132
|
+
const bytes = await readRegular(root, item.file, MAX_MATERIAL_BYTES);
|
|
133
|
+
if (bytes.length !== item.bytes || digest(bytes) !== item.sha256) fail('INTEGRITY_MISMATCH', 'Knowledge content differs from the versioned catalog.');
|
|
134
|
+
const chars = Array.from(decode(bytes));
|
|
135
|
+
if (offset > chars.length) fail('INVALID_QUERY', 'Read offset is outside the material.');
|
|
136
|
+
const end = Math.min(chars.length, offset + length);
|
|
137
|
+
return {schemaVersion: 1, version: catalog.version, fingerprint, ...summary(item), source: 'package',
|
|
138
|
+
sha256: item.sha256, installedTarget: item.installedTarget ?? null, links: item.links.slice(linksOffset, linksOffset + 32),
|
|
139
|
+
linksOffset, nextLinksOffset: linksOffset + 32 < item.links.length ? linksOffset + 32 : null,
|
|
140
|
+
text: chars.slice(offset, end).join(''), offset, totalCharacters: chars.length,
|
|
141
|
+
truncated: end < chars.length, nextOffset: end < chars.length ? end : null};
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
package/lib/inspect.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface InspectionTarget {tag:string;id:string|null;className:string;label:string;}
|
|
2
|
+
export interface InspectionIssue {id:'icons.text-glyph'|'icons.unslotted-svg'|'table.mixed-row-centre'|'table.word-fragment'|'copy.action-echo'|'copy.unowned-support'|'forms.track-drift'|'forms.content-track-drift'|'layout.page-overflow';target:InspectionTarget;related?:InspectionTarget;message:string;offset?:number;word?:string;}
|
|
3
|
+
export interface RenderedUIInspection {
|
|
4
|
+
schemaVersion:1;issues:InspectionIssue[];copy:InspectionTarget[];
|
|
5
|
+
fields:(InspectionTarget&{x:number;y:number;width:number;height:number})[];
|
|
6
|
+
truncated:boolean;notChecked:string[];
|
|
7
|
+
}
|
|
8
|
+
/** Invoke inside a browser document, e.g. page.evaluate(inspectRenderedUI). No mutation or network calls. */
|
|
9
|
+
export declare function inspectRenderedUI(options?:{rootSelector?:string;maxElements?:number}):RenderedUIInspection;
|
|
10
|
+
export interface SourceUIIssue {id:'composition.native-dialog'|'composition.native-control'|'composition.layout-owner'|'composition.scaled-minimum';line:number;column:number;excerpt:string;owner:string;message:string;}
|
|
11
|
+
/** Text observations, not an AST or conformance check. No file access, execution or network. */
|
|
12
|
+
export declare function inspectSourceUI(source:string,options?:{format?:'jsx'|'css';maxFindings?:number}):{schemaVersion:1;issues:SourceUIIssue[];truncated:boolean;notChecked:string[]};
|
package/lib/inspect.mjs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** Optional text observations for host-owned JSX/CSS; no parsing or execution. */
|
|
2
|
+
export function inspectSourceUI(source,options={}) {
|
|
3
|
+
if(typeof source!=='string'||source.length>500000)throw new Error('Source must be a string of at most 500000 UTF-16 units');
|
|
4
|
+
const limit=options.maxFindings??50,format=options.format??'jsx';
|
|
5
|
+
if(!Number.isSafeInteger(limit)||limit<1||limit>500)throw new Error('maxFindings must be an integer from 1 to 500');
|
|
6
|
+
if(!['jsx','css'].includes(format))throw new Error('format must be jsx or css');
|
|
7
|
+
const issues=[];let truncated=false;
|
|
8
|
+
const record=(match,id,owner,message)=>{
|
|
9
|
+
if(issues.length>=limit){truncated=true;return;}
|
|
10
|
+
const offset=match.index+match[0].length-match[0].trimStart().length;
|
|
11
|
+
const prefix=source.slice(0,offset),line=prefix.split('\n').length,column=offset-prefix.lastIndexOf('\n');
|
|
12
|
+
issues.push({id,line,column,excerpt:match[0].trimStart().slice(0,180),owner,message});
|
|
13
|
+
};
|
|
14
|
+
if(format==='jsx'){
|
|
15
|
+
for(const match of source.matchAll(/\b(?:window|globalThis)\.(confirm|alert|prompt)\s*\(/g))record(match,'composition.native-dialog',match[1]==='confirm'?'AlertDialog':match[1]==='prompt'?'Dialog':'Alert','Native browser dialog candidate. Review the actual call and use the installed Flood owner unless native UI is explicitly required.');
|
|
16
|
+
for(const match of source.matchAll(/<(select|button)\b|<input\b[^>]*\btype\s*=\s*["'](date|time|checkbox|radio|range)["']/g)){
|
|
17
|
+
const role=match[1]||match[2],owner={select:'Select',button:'Button',date:'DatePicker',time:'TimeField',checkbox:'Checkbox',radio:'Radio',range:'Slider'}[role];
|
|
18
|
+
record(match,'composition.native-control',owner,'Raw control candidate. Review whether an existing Flood component owns this role; native form/table structure and deliberate documented adapters are separate cases.');
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
for(const match of source.matchAll(/([^{}]+)\{[^{}]*\bdisplay\s*:\s*(grid|flex)\b/g)){
|
|
22
|
+
if(match[1].trim().startsWith('.flood-'))continue;
|
|
23
|
+
record(match,'composition.layout-owner',match[2]==='grid'?'Grid':'Stack / Inline','Custom layout candidate. Shared form tracks and repeated layout roles should use Flood owners. A semantic host shell can legitimately own positioning; record the actual reason.');
|
|
24
|
+
}
|
|
25
|
+
for(const match of source.matchAll(/\bmin-(?:width|inline-size)\s*:\s*(\d*\.?\d+)rem\b/g)){
|
|
26
|
+
if(Number(match[1])>0)record(match,'composition.scaled-minimum','Container / local ScrollArea','Rem-based minimum width candidate. If applied to the whole host, it can force page overflow when text grows. Dense table minima can be legitimate inside a local scroll owner; inspect the actual selector and enlarged desktop result.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {schemaVersion:1,issues,truncated,notChecked:['Text matching is not parsing: comments, strings and examples can match; aliases, dynamic types and indirect calls can be missed. Review source context.','This function reads only the supplied text. It does not execute it, open files or inspect imports, component capability, rendered layout or editorial necessity.']};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read-only DOM observations. Pass this self-contained function to page.evaluate. */
|
|
33
|
+
export function inspectRenderedUI(options = {}) {
|
|
34
|
+
if (typeof document === 'undefined') throw new Error('inspectRenderedUI must run in the rendered browser document');
|
|
35
|
+
const root = options.rootSelector ? document.querySelector(options.rootSelector) : document;
|
|
36
|
+
if (!root) throw new Error('Flood inspection root was not found');
|
|
37
|
+
const limit = options.maxElements ?? 500;
|
|
38
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 2000) throw new Error('maxElements must be an integer from 1 to 2000');
|
|
39
|
+
const issues = [], copy = [], fields = [];
|
|
40
|
+
const visible = element => {
|
|
41
|
+
const style = getComputedStyle(element), box = element.getBoundingClientRect();
|
|
42
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && box.width > 0 && box.height > 0;
|
|
43
|
+
};
|
|
44
|
+
const label = element => (element.getAttribute('aria-label') || element.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 180);
|
|
45
|
+
const target = element => ({tag:element.tagName.toLowerCase(), id:element.id || null, className:element.getAttribute('class') || '', label:label(element)});
|
|
46
|
+
let truncated = false;
|
|
47
|
+
const addIssue = finding => { if (issues.length < limit) issues.push(finding); else truncated = true; };
|
|
48
|
+
const select = selector => {
|
|
49
|
+
const matches = [...root.querySelectorAll(selector)].filter(visible);
|
|
50
|
+
if(matches.length > limit) truncated = true;
|
|
51
|
+
return matches.slice(0, limit);
|
|
52
|
+
};
|
|
53
|
+
if(root===document&&document.documentElement.scrollWidth>innerWidth+1)addIssue({id:'layout.page-overflow',target:{tag:'html',id:document.documentElement.id||null,className:document.documentElement.getAttribute('class')||'',label:'Document'},offset:document.documentElement.scrollWidth-innerWidth,message:'The document extends beyond the viewport. Review the actual overflow owner at this text size; dense content can scroll locally. Hiding page overflow does not repair the layout.'});
|
|
54
|
+
for (const button of select('button.flood-button, button.flood-icon-button')) {
|
|
55
|
+
// Text is inspected outside real icon slots, SVGs and visually hidden names.
|
|
56
|
+
const walker = document.createTreeWalker(button, NodeFilter.SHOW_TEXT);
|
|
57
|
+
const parts = []; let node;
|
|
58
|
+
while ((node = walker.nextNode())) {
|
|
59
|
+
if (!node.parentElement?.closest('.flood-icon,svg,.flood-visually-hidden,[aria-hidden="true"]')) parts.push(node.textContent);
|
|
60
|
+
}
|
|
61
|
+
const text = parts.join('').trim();
|
|
62
|
+
if (/^[++×✕✖↗→←✓✔☰](?:\s|$)|(?:\s)[++×✕✖↗→←✓✔☰]$/u.test(text)) {
|
|
63
|
+
addIssue({id:'icons.text-glyph', target:target(button), message:'Possible action icon encoded as font text; review whether IconSlot with explicit artwork is required.'});
|
|
64
|
+
}
|
|
65
|
+
for (const svg of button.querySelectorAll('svg')) {
|
|
66
|
+
if (!svg.closest('.flood-icon,.flood-loading,.flood-spinner')) addIssue({id:'icons.unslotted-svg',target:target(button),message:'Action SVG has no Flood IconSlot owner; inspect sizing and accessible decoration.'});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const row of select('.flood-table tbody tr')) {
|
|
70
|
+
const controls = [...row.querySelectorAll('input:not([type="hidden"]),textarea,select')].filter(visible);
|
|
71
|
+
const actions = [...row.querySelectorAll('button,a.flood-link--button')].filter(visible);
|
|
72
|
+
if (!controls.length && !actions.length) continue;
|
|
73
|
+
const centres = controls.map(control=>{const r=control.getBoundingClientRect();return r.y+r.height/2;});
|
|
74
|
+
if (centres.length && Math.max(...centres)-Math.min(...centres)>3) continue;
|
|
75
|
+
const rowBox=row.getBoundingClientRect();
|
|
76
|
+
const expected = centres.length ? centres.reduce((a,b)=>a+b,0)/centres.length : rowBox.y+rowBox.height/2;
|
|
77
|
+
for (const cell of row.children) {
|
|
78
|
+
if(!cell.matches('td,th') || !visible(cell)) continue;
|
|
79
|
+
// Editable rows compare their passive content to input centres. Record/action
|
|
80
|
+
// rows compare every complete content block, including multiline identities.
|
|
81
|
+
if(centres.length && cell.querySelector('input,textarea,select')) continue;
|
|
82
|
+
const boxes=[];
|
|
83
|
+
const walker=document.createTreeWalker(cell,NodeFilter.SHOW_TEXT);let textNode;
|
|
84
|
+
while((textNode=walker.nextNode())) {
|
|
85
|
+
if(!textNode.textContent?.trim() || !textNode.parentElement || !visible(textNode.parentElement)
|
|
86
|
+
|| textNode.parentElement.closest('.flood-visually-hidden,[aria-hidden="true"],script,style')) continue;
|
|
87
|
+
const range=document.createRange();range.selectNodeContents(textNode);
|
|
88
|
+
boxes.push(...[...range.getClientRects()].filter(rect=>rect.width>0&&rect.height>0));
|
|
89
|
+
}
|
|
90
|
+
for(const control of [...cell.querySelectorAll('button,a.flood-link--button')].filter(visible)) boxes.push(control.getBoundingClientRect());
|
|
91
|
+
if(!boxes.length) continue;
|
|
92
|
+
const actual=(Math.min(...boxes.map(rect=>rect.top))+Math.max(...boxes.map(rect=>rect.bottom)))/2;
|
|
93
|
+
if(Math.abs(actual-expected)>3) addIssue({id:'table.mixed-row-centre',target:target(cell),offset:Math.round((actual-expected)*100)/100,message:centres.length
|
|
94
|
+
? 'Cell content is offset from neighbouring input centres. Review resolved geometry.'
|
|
95
|
+
: 'Cell content is offset from the centre of a record/action row. Compare multiline identity, values and actions; use deliberate top alignment only when justified.'});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const actionNames=select('button.flood-button,a.flood-link--button').map(button=>(button.textContent||'').trim().replace(/\s+/g,' ').toLowerCase()).filter(name=>name.length>=4);
|
|
99
|
+
let wordsChecked=0;
|
|
100
|
+
const segmenters=new Map();
|
|
101
|
+
const canInspectWords=typeof Intl.Segmenter==='function';
|
|
102
|
+
if(canInspectWords)for(const cell of select('.flood-table th,.flood-table td')){
|
|
103
|
+
if(cell.matches('[data-wrap="anywhere"]'))continue;
|
|
104
|
+
const locale=cell.closest('[lang]')?.getAttribute('lang')||document.documentElement.lang||undefined;
|
|
105
|
+
if(!segmenters.has(locale)){
|
|
106
|
+
try{segmenters.set(locale,new Intl.Segmenter(locale,{granularity:'word'}));}
|
|
107
|
+
catch{segmenters.set(locale,new Intl.Segmenter(undefined,{granularity:'word'}));}
|
|
108
|
+
}
|
|
109
|
+
const walker=document.createTreeWalker(cell,NodeFilter.SHOW_TEXT);let node;
|
|
110
|
+
while((node=walker.nextNode())){
|
|
111
|
+
if(!node.parentElement||!visible(node.parentElement)||node.parentElement.closest('code,pre,.flood-inline-code,.flood-visually-hidden,[aria-hidden="true"]'))continue;
|
|
112
|
+
for(const word of segmenters.get(locale).segment(node.textContent||'')){
|
|
113
|
+
if(!word.isWordLike||word.segment.length<4)continue;
|
|
114
|
+
if(wordsChecked>=limit){truncated=true;break;}wordsChecked++;
|
|
115
|
+
if(word.segment.length>200)continue;
|
|
116
|
+
const range=document.createRange();range.setStart(node,word.index);range.setEnd(node,word.index+word.segment.length);
|
|
117
|
+
const boxes=[...range.getClientRects()].filter(rect=>rect.width>0&&rect.height>0);
|
|
118
|
+
if(boxes.length>1&&Math.max(...boxes.map(rect=>rect.y))-Math.min(...boxes.map(rect=>rect.y))>2){
|
|
119
|
+
addIssue({id:'table.word-fragment',target:target(cell),word:word.segment,message:'A word is split across rendered lines in a table cell. Review intrinsic column width and wrapping; deliberate technical or localized line breaking can be an exception.'});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if(wordsChecked>=limit)break;
|
|
123
|
+
}
|
|
124
|
+
if(wordsChecked>=limit){truncated=true;break;}
|
|
125
|
+
}
|
|
126
|
+
const grids=select('form [class]').filter(element=>{
|
|
127
|
+
if(element.closest('table') || getComputedStyle(element).display!=='grid')return false;
|
|
128
|
+
return [...element.children].filter(visible).length>=2 && element.querySelectorAll('input,select,textarea,[role="combobox"]').length>=2;
|
|
129
|
+
}).map(element=>{
|
|
130
|
+
const style=getComputedStyle(element),columns=style.gridTemplateColumns.match(/[\d.]+px/g)?.map(parseFloat)??[];
|
|
131
|
+
return {element,columns,gap:parseFloat(style.columnGap)||0,rect:element.getBoundingClientRect()};
|
|
132
|
+
}).filter(grid=>grid.columns.length>=2);
|
|
133
|
+
const contentTracks=grid=>{
|
|
134
|
+
const owners='.flood-field,.flood-date-picker,.flood-date-field,.flood-time-field,.flood-date-range-field,.flood-date-time-picker';
|
|
135
|
+
const nodes=[...grid.element.querySelectorAll(owners)].filter(element=>visible(element)&&!element.closest('table')&&!element.parentElement?.closest(owners));
|
|
136
|
+
if(nodes.length>limit)truncated=true;
|
|
137
|
+
const tracks=[];
|
|
138
|
+
for(const element of nodes.slice(0,limit)){
|
|
139
|
+
const rect=element.getBoundingClientRect();
|
|
140
|
+
const track=tracks.find(item=>Math.abs(item.x-rect.x)<=1);
|
|
141
|
+
if(track){if(Math.abs(track.width-rect.width)>1)return null;}
|
|
142
|
+
else tracks.push({x:rect.x,width:rect.width});
|
|
143
|
+
}
|
|
144
|
+
if(tracks.length!==grid.columns.length)return null;
|
|
145
|
+
tracks.sort((a,b)=>a.x-b.x);
|
|
146
|
+
return {tracks,left:tracks[0].x,right:Math.max(...tracks.map(track=>track.x+track.width))};
|
|
147
|
+
};
|
|
148
|
+
for(const grid of grids)grid.content=contentTracks(grid);
|
|
149
|
+
for(let i=0;i<grids.length;i++)for(let j=i+1;j<grids.length;j++){
|
|
150
|
+
const a=grids[i],b=grids[j];
|
|
151
|
+
if(a.element.closest('form')!==b.element.closest('form'))continue;
|
|
152
|
+
if(a.element.contains(b.element)||b.element.contains(a.element))continue;
|
|
153
|
+
if(!(a.rect.bottom<=b.rect.top+1||b.rect.bottom<=a.rect.top+1))continue;
|
|
154
|
+
if(a.content&&b.content&&a.content.tracks.length===b.content.tracks.length&&Math.abs(a.content.left-b.content.left)<=1&&Math.abs(a.content.right-b.content.right)<=1){
|
|
155
|
+
const offset=Math.max(...a.content.tracks.map((track,index)=>Math.max(Math.abs(track.x-b.content.tracks[index].x),Math.abs(track.width-b.content.tracks[index].width))));
|
|
156
|
+
if(offset>1)addIssue({id:'forms.content-track-drift',target:target(b.element),related:target(a.element),offset:Math.round(offset*100)/100,message:'Stacked form groups share content edges but their actual field starts or widths differ. Review nested surface padding and shared track ownership; matching Grid props or outer boxes is insufficient.'});
|
|
157
|
+
}
|
|
158
|
+
if(Math.abs(a.rect.x-b.rect.x)>1||Math.abs(a.rect.width-b.rect.width)>1)continue;
|
|
159
|
+
const starts=grid=>grid.columns.map((_,index)=>grid.columns.slice(0,index).reduce((sum,width)=>sum+width,0)+index*grid.gap);
|
|
160
|
+
const left=starts(a),right=starts(b);
|
|
161
|
+
const drift=a.columns.length===b.columns.length?Math.max(...left.map((x,index)=>Math.abs(x-right[index]))):null;
|
|
162
|
+
if(drift===null||drift>1)addIssue({id:'forms.track-drift',target:target(b.element),related:target(a.element),...(drift===null?{}:{offset:Math.round(drift*100)/100}),message:'Stacked form grids share an outer frame but use different tracks. Review common gutters/group hierarchy or document why these contexts need different columns.'});
|
|
163
|
+
}
|
|
164
|
+
for(const element of select('h1,h2,h3,h4,h5,h6,.flood-type--helper,.flood-type--compact')) {
|
|
165
|
+
if(label(element)) {
|
|
166
|
+
copy.push(target(element));
|
|
167
|
+
if(!element.closest('button,a,label') && !element.matches('h1,h2,h3,h4,h5,h6')) {
|
|
168
|
+
if(!element.closest('.flood-field,.flood-fieldset,.flood-table,.flood-metric,.flood-status-indicator,.flood-file-meta,.flood-description-list,nav,article,dialog,[role="document"],[role="status"],[role="alert"],[role="alertdialog"],[role="dialog"]')) {
|
|
169
|
+
addIssue({id:'copy.unowned-support',target:target(element),message:'Supporting text sits outside a field, operation state, navigation or structured document. Identify the exact current decision it serves; remove implementation narration. Do not change typography or add a semantic wrapper merely to silence this observation.'});
|
|
170
|
+
}
|
|
171
|
+
const text=(element.textContent||'').trim().replace(/\s+/g,' ').toLowerCase();
|
|
172
|
+
if(actionNames.some(name=>text.includes(name))) addIssue({id:'copy.action-echo',target:target(element),message:'Supporting copy repeats a currently visible action label. Review whether it adds a necessary constraint or only narrates the control.'});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for(const element of select('.flood-field,fieldset,.flood-section')) {
|
|
177
|
+
const rect=element.getBoundingClientRect();
|
|
178
|
+
fields.push({...target(element),x:rect.x,y:rect.y,width:rect.width,height:rect.height});
|
|
179
|
+
}
|
|
180
|
+
return {schemaVersion:1, issues, copy, fields, truncated, notChecked:[
|
|
181
|
+
'Editorial necessity and accuracy require contextual review; copy inventory is not an editorial verdict.',
|
|
182
|
+
'Form grouping and column hierarchy require review of task relationships and visible geometry.',
|
|
183
|
+
'Only the currently rendered document state is inspected; portals outside a selected root, other pages, iframes and shadow roots are not traversed.',
|
|
184
|
+
'These observations do not certify accessibility, functional correctness or overall visual quality.',
|
|
185
|
+
...(!canInspectWords?['Word fragmentation was not inspected because Intl.Segmenter is unavailable.']:[])
|
|
186
|
+
]};
|
|
187
|
+
}
|
package/manifest.json
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"entry": "SKILL.md",
|
|
4
4
|
"files": [
|
|
5
|
+
{
|
|
6
|
+
"source": "dist/catalog.json",
|
|
7
|
+
"target": "flood/catalog.json",
|
|
8
|
+
"kind": "catalog"
|
|
9
|
+
},
|
|
5
10
|
{
|
|
6
11
|
"source": "DESIGN.md",
|
|
7
12
|
"target": "flood/DESIGN.md",
|
|
@@ -106,9 +111,44 @@
|
|
|
106
111
|
"source": "references/flood-verification/SKILL.md",
|
|
107
112
|
"target": ".agents/skills/flood-ui/references/flood-verification/SKILL.md",
|
|
108
113
|
"kind": "skill"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"source": "references/knowledge.md",
|
|
117
|
+
"target": ".agents/skills/flood-ui/references/knowledge.md",
|
|
118
|
+
"kind": "reference"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"source": "references/flood-interface-content/SKILL.md",
|
|
122
|
+
"target": ".agents/skills/flood-ui/references/flood-interface-content/SKILL.md",
|
|
123
|
+
"kind": "skill"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"source": "references/interface-content-examples.md",
|
|
127
|
+
"target": ".agents/skills/flood-ui/references/interface-content-examples.md",
|
|
128
|
+
"kind": "reference"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"source": "references/rendered-review.md",
|
|
132
|
+
"target": ".agents/skills/flood-ui/references/rendered-review.md",
|
|
133
|
+
"kind": "reference"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"source": "references/flood-hierarchy/SKILL.md",
|
|
137
|
+
"target": ".agents/skills/flood-ui/references/flood-hierarchy/SKILL.md",
|
|
138
|
+
"kind": "skill"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"source": "references/flood-records/SKILL.md",
|
|
142
|
+
"target": ".agents/skills/flood-ui/references/flood-records/SKILL.md",
|
|
143
|
+
"kind": "skill"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"source": "references/record-decisions.md",
|
|
147
|
+
"target": ".agents/skills/flood-ui/references/record-decisions.md",
|
|
148
|
+
"kind": "reference"
|
|
109
149
|
}
|
|
110
150
|
],
|
|
111
|
-
"version": "0.2.
|
|
151
|
+
"version": "0.2.10",
|
|
112
152
|
"installedEntry": ".agents/skills/flood-ui/SKILL.md",
|
|
113
153
|
"pathBase": "source is package-relative; target is relative to the host project root"
|
|
114
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flood-md/agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Versioned Flood design instructions for coding agents.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,12 +13,18 @@
|
|
|
13
13
|
"DESIGN.md",
|
|
14
14
|
"SKILL.md",
|
|
15
15
|
"references",
|
|
16
|
+
"lib",
|
|
17
|
+
"dist",
|
|
18
|
+
"knowledge.json",
|
|
16
19
|
"workflows.json",
|
|
17
20
|
"rules.json",
|
|
18
21
|
"manifest.json",
|
|
19
22
|
"README.md"
|
|
20
23
|
],
|
|
21
24
|
"exports": {
|
|
25
|
+
"./inspect": {"types":"./lib/inspect.d.ts","import":"./lib/inspect.mjs"},
|
|
26
|
+
".": {"types":"./lib/index.d.ts","import":"./lib/index.mjs"},
|
|
27
|
+
"./dist/*": "./dist/*",
|
|
22
28
|
"./DESIGN.md": "./DESIGN.md",
|
|
23
29
|
"./SKILL.md": "./SKILL.md",
|
|
24
30
|
"./manifest.json": "./manifest.json",
|
|
@@ -10,6 +10,11 @@ components. Preserve their anatomy and behavior when improving a composition.
|
|
|
10
10
|
Read the governing project instructions, matching declarations and relevant
|
|
11
11
|
component guide first. This skill grants no additional permissions.
|
|
12
12
|
|
|
13
|
+
When introducing visible interface text, apply the
|
|
14
|
+
[interface content skill](../flood-interface-content/SKILL.md) before filling
|
|
15
|
+
optional headings, descriptions or helper slots. Existing user-authored content
|
|
16
|
+
is not permission to rewrite it during an unrelated layout correction.
|
|
17
|
+
|
|
13
18
|
## Choose the smallest existing owner
|
|
14
19
|
|
|
15
20
|
Before changing JSX, identify the visible roles and their existing owners:
|
|
@@ -18,6 +23,31 @@ metadata, IconButton for an icon action, Dialog for modal behavior, Text/Heading
|
|
|
18
23
|
for typography, and the appropriate form, list or table primitive for semantics.
|
|
19
24
|
Check the actual installed exports and props; do not invent APIs from newer docs.
|
|
20
25
|
|
|
26
|
+
Search the installed catalog by the role before settling on a low-level building
|
|
27
|
+
block. Read the selected contract and compare a more specific owner when one
|
|
28
|
+
exists: Metric for a labelled statistic; DatePicker for ordinary date selection
|
|
29
|
+
with typing and a calendar, DateField for an explicitly entry-only interaction;
|
|
30
|
+
StatusIndicator for a named state. Input type="date"
|
|
31
|
+
invokes the browser's picker, not Flood's Calendar. Use it only for an explicit
|
|
32
|
+
native-picker requirement or a documented limitation of the installed controls.
|
|
33
|
+
Heading/Text remain valid for ordinary prose, not a default replacement for an
|
|
34
|
+
available statistic component.
|
|
35
|
+
|
|
36
|
+
Include layout and secondary interactions in that selection: Grid for repeated
|
|
37
|
+
form tracks, Stack/Inline for related layout, AlertDialog for a discard/delete
|
|
38
|
+
confirmation, and Alert/StatusIndicator for the appropriate feedback role.
|
|
39
|
+
window.confirm/alert/prompt are browser UI, not Flood components. A small scope
|
|
40
|
+
does not justify replacing an available owner with native UI or a parallel CSS
|
|
41
|
+
grid. Keep native form, table and semantic document/shell elements where they
|
|
42
|
+
supply structure; styling the host shell is different from reimplementing a
|
|
43
|
+
provided layout or control. Document a genuine missing capability before using
|
|
44
|
+
a project-owned adapter.
|
|
45
|
+
|
|
46
|
+
Record the selected owner and any rejected match before implementing the surface.
|
|
47
|
+
If discovery finds no suitable component, state the missing capability; do not
|
|
48
|
+
claim that the library has no owner without checking. Keep this decision record
|
|
49
|
+
in development notes, never as extra explanatory text in the product.
|
|
50
|
+
|
|
21
51
|
- Reuse the matching component with its supported props and tokens.
|
|
22
52
|
- If spacing or alignment is wrong, inspect both the primitive and the parent
|
|
23
53
|
composition. Fix the owner of the relationship. Keep the primitive intact.
|
|
@@ -9,6 +9,15 @@ Use the installed component contracts and
|
|
|
9
9
|
[composition workflow](../flood-composition/SKILL.md). Start with the content's
|
|
10
10
|
relationships, not a wrapper around every piece of text.
|
|
11
11
|
|
|
12
|
+
Keep visual/test fixtures separate from the host's working records unless sample
|
|
13
|
+
data is explicitly part of the task. Do not seed a new user's persisted list with
|
|
14
|
+
fictional business records just to fill a screen. Use isolated test state for
|
|
15
|
+
populated screenshots and exercise the real empty state. When samples are required, identify their provenance in the test brief/README.
|
|
16
|
+
A visible distinction is needed where it changes interpretation or action, such
|
|
17
|
+
as sample records mixed with real records. Do not automatically add a permanent
|
|
18
|
+
Demo badge to the shell of an explicitly requested isolated prototype; follow
|
|
19
|
+
the interface-content workflow for contextual placement.
|
|
20
|
+
|
|
12
21
|
- Table/DataTable preserve caption, headers, cell relationships and documented
|
|
13
22
|
sorting/selection behavior. A footer represents a summary and should be
|
|
14
23
|
visually distinguishable without relying on a heavy border everywhere.
|
|
@@ -23,6 +32,13 @@ relationships, not a wrapper around every piece of text.
|
|
|
23
32
|
- Preserve native image alt/fallback semantics and aspect constraints. Decorative
|
|
24
33
|
artwork should not duplicate an adjacent accessible identity.
|
|
25
34
|
|
|
35
|
+
## Choose the information, not all available slots
|
|
36
|
+
|
|
37
|
+
For record lists/details, apply the [records skill](../flood-records/SKILL.md)
|
|
38
|
+
before filling metadata and actions. Identity, supporting context, lifecycle state,
|
|
39
|
+
priority and timestamps have different purposes. Use Table/DataTable when users
|
|
40
|
+
need several named comparisons; a trailing Inline of values is not a column model.
|
|
41
|
+
|
|
26
42
|
## Exercise data and geometry
|
|
27
43
|
|
|
28
44
|
Test no records, one record, long/wrapping names, absent optional media/actions,
|
|
@@ -31,6 +47,22 @@ without inserting a second empty-state design or moving unrelated controls.
|
|
|
31
47
|
Align file title/metadata and status/progress by their intended row relationship;
|
|
32
48
|
do not hide a StatusIndicator dot or replace Progress with a custom track.
|
|
33
49
|
|
|
50
|
+
For an editable table, compare the centre of plain amounts and row actions with
|
|
51
|
+
the neighbouring input controls. Table centres record/action and editable rows
|
|
52
|
+
by default; opt into cellVerticalAlign="top" only for deliberate prose alignment.
|
|
53
|
+
Use the shared cellVerticalAlign contract rather than
|
|
54
|
+
moving text with transforms or overriding Input, NumberInput or Button internals.
|
|
55
|
+
Native style on an individual host-owned td/th can express an exceptional cell.
|
|
56
|
+
Do not depend on a low-specificity descendant CSS override. Check the rendered
|
|
57
|
+
centres; equal cell padding alone does not establish alignment.
|
|
58
|
+
|
|
59
|
+
Also inspect readable column widths with long neighbouring records. A centred
|
|
60
|
+
cell can still be wrong when a status or date is fragmented into single letters.
|
|
61
|
+
Table and StatusIndicator preserve word-based intrinsic width by default; keep
|
|
62
|
+
that recipe rather than applying global overflow-wrap:anywhere. Deliberate
|
|
63
|
+
technical identifier cells can opt into data-wrap="anywhere". Otherwise use
|
|
64
|
+
local scrolling or appropriate column budgets, not an empty no-overflow claim.
|
|
65
|
+
|
|
34
66
|
Use the documented local overflow owner, with a meaningful accessible name. Focus
|
|
35
67
|
the last action/cell and check that a rail or rounded corner does not cover it.
|
|
36
68
|
Compare narrow/wide, enlarged text and RTL. Locale-sensitive text in prerendered
|
|
@@ -10,6 +10,11 @@ visible role before choosing color: StatusIndicator names a state, Badge marks
|
|
|
10
10
|
metadata, Progress describes task progress, Meter describes a bounded measurement,
|
|
11
11
|
Alert gives contextual feedback and EmptyState explains missing content.
|
|
12
12
|
|
|
13
|
+
For several record attributes, use the [records skill](../flood-records/SKILL.md).
|
|
14
|
+
A category called Error is not a failed operation; priority is not lifecycle state.
|
|
15
|
+
Choose the semantic owner before its appearance, and keep essential triage data
|
|
16
|
+
visible even when reducing decorative emphasis.
|
|
17
|
+
|
|
13
18
|
## Keep one owner for each message
|
|
14
19
|
|
|
15
20
|
- Status text must distinguish pending, success, failure, cancellation and local
|
|
@@ -24,13 +29,29 @@ Alert gives contextual feedback and EmptyState explains missing content.
|
|
|
24
29
|
- Spinner retains a meaningful accessible label where standalone. Skeleton is
|
|
25
30
|
decoration, not another announced task. Preserve the existing reduced-motion
|
|
26
31
|
behavior without introducing a new animation system.
|
|
27
|
-
- EmptyState
|
|
28
|
-
|
|
32
|
+
- EmptyState identifies the actual condition. A title can be enough; add a
|
|
33
|
+
description only for new decision-relevant information, not an instruction
|
|
34
|
+
repeating a visible action. Its icon/action use existing primitives.
|
|
35
|
+
|
|
36
|
+
## Scope feedback to its operation
|
|
37
|
+
|
|
38
|
+
Associate a message with the operation and record that produced it. Preserve
|
|
39
|
+
entered data after refusal and keep a useful error while that operation remains
|
|
40
|
+
unresolved. A successful retry clears/replaces its failure. Explicitly discarding
|
|
41
|
+
the draft ends that draft's save error; it must not follow the user to a list or
|
|
42
|
+
another record. Ignore late results belonging to an abandoned operation.
|
|
43
|
+
|
|
44
|
+
Do not solve this with an automatic timer inside Alert or by clearing every
|
|
45
|
+
message on navigation. A collection-wide read failure can still block editing
|
|
46
|
+
after changing views and must remain until resolved. The host's state model owns
|
|
47
|
+
that distinction; the feedback primitive only presents the supplied state.
|
|
29
48
|
|
|
30
49
|
## Evidence
|
|
31
50
|
|
|
32
51
|
Trigger the operation and observe each applicable transition, including failure
|
|
33
|
-
and retry.
|
|
52
|
+
and retry. Also follow failure → explicit discard → list → reopen a saved record;
|
|
53
|
+
the abandoned draft's failure must be gone and the saved data unchanged. Distinguish
|
|
54
|
+
that from a still-unresolved collection-wide error. Check optional description/action/icon slots, long messages and text
|
|
34
55
|
enlargement. Keep content visible without stealing focus merely to announce it.
|
|
35
56
|
Reserve feedback space in a demo when needed for a stable sample, not universally
|
|
36
57
|
inside every production component. Report audible announcement behavior only
|