efficient-web-builder 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +2 -1
- package/dist/components/AddBlockRow.d.ts +1 -7
- package/dist/components/AddBlockRow.js +1 -1
- package/dist/components/AddChildRow.d.ts +1 -6
- package/dist/components/AddChildRow.js +1 -1
- package/dist/components/AddSidePanel.d.ts +2 -39
- package/dist/components/AddSidePanel.js +1 -1
- package/dist/components/BlockGapField.js +1 -1
- package/dist/components/BlogPostSource.js +1 -1
- package/dist/components/Breadcrumb.js +1 -1
- package/dist/components/Btn.js +1 -1
- package/dist/components/CardShadowField.js +1 -1
- package/dist/components/CategoryBar.js +1 -1
- package/dist/components/ChartSection.js +1 -1
- package/dist/components/ChatSettingsPanel.js +1 -1
- package/dist/components/ClearColoursDialog.js +1 -1
- package/dist/components/ContactLocations.js +5 -5
- package/dist/components/CornersField.d.ts +0 -1
- package/dist/components/CornersField.js +1 -1
- package/dist/components/DocBody.js +1 -1
- package/dist/components/DuplicateToPageMenu.js +1 -1
- package/dist/components/EditorScreenMount.d.ts +1 -0
- package/dist/components/EditorScreenMount.js +1 -0
- package/dist/components/EditorStoreProvider.d.ts +1 -0
- package/dist/components/EditorStoreProvider.js +1 -0
- package/dist/components/FeaturedCover.js +1 -1
- package/dist/components/FieldGlyph.js +1 -1
- package/dist/components/Footprint.js +1 -1
- package/dist/components/HeadingAudit.js +1 -1
- package/dist/components/HeroImageField.js +1 -1
- package/dist/components/HeroSection.d.ts +2 -6
- package/dist/components/HeroSection.js +4 -4
- package/dist/components/HoverEffectField.js +1 -1
- package/dist/components/ImageBlock.js +1 -1
- package/dist/components/ImageFocusRow.js +1 -1
- package/dist/components/KeepHint.d.ts +5 -0
- package/dist/components/KeepHint.js +1 -0
- package/dist/components/LocationsGroupedField.js +1 -1
- package/dist/components/LongRead.js +1 -1
- package/dist/components/MediaSidePanel.d.ts +1 -6
- package/dist/components/MediaSidePanel.js +1 -1
- package/dist/components/MediaUploadField.js +1 -1
- package/dist/components/MockupSection.js +1 -1
- package/dist/components/MultiSelectField.d.ts +4 -1
- package/dist/components/MultiSelectField.js +1 -1
- package/dist/components/NodeContextMenu.d.ts +2 -96
- package/dist/components/NodeContextMenu.js +1 -1
- package/dist/components/PageEditGuardNotices.d.ts +1 -1
- package/dist/components/PageEditGuardNotices.js +1 -1
- package/dist/components/PagesPanel.js +1 -1
- package/dist/components/PanelToggleIcon.d.ts +1 -3
- package/dist/components/PanelToggleIcon.js +1 -1
- package/dist/components/PartResetDot.js +1 -1
- package/dist/components/PartSettingsPanel.d.ts +5 -0
- package/dist/components/PartSettingsPanel.js +1 -1
- package/dist/components/PhotographyField.js +1 -1
- package/dist/components/PostCard.js +1 -1
- package/dist/components/PreviewDeviceContext.d.ts +2 -4
- package/dist/components/PreviewDeviceContext.js +1 -1
- package/dist/components/PreviewPartsContext.d.ts +1 -3
- package/dist/components/PreviewPartsContext.js +1 -1
- package/dist/components/ProcessSteps.js +1 -1
- package/dist/components/RMark.js +1 -1
- package/dist/components/Reel.js +1 -1
- package/dist/components/SearchSelectField.d.ts +3 -1
- package/dist/components/SearchSelectField.js +1 -1
- package/dist/components/SectionEditor.d.ts +2 -5
- package/dist/components/SectionEditor.js +1 -1
- package/dist/components/SectionEditorShell.d.ts +1 -1
- package/dist/components/SectionEditorShell.js +1 -1
- package/dist/components/SectionHeader.js +1 -1
- package/dist/components/SectionThemePicker.d.ts +3 -1
- package/dist/components/SectionThemePicker.js +1 -1
- package/dist/components/SectionThumbnail.js +1 -1
- package/dist/components/ShadowColourField.js +1 -1
- package/dist/components/SidePanelHead.d.ts +1 -23
- package/dist/components/SidePanelHead.js +1 -1
- package/dist/components/SideSpaceField.d.ts +0 -1
- package/dist/components/SideSpaceField.js +1 -1
- package/dist/components/SiteEditorProvider.d.ts +3 -10
- package/dist/components/SiteEditorProvider.js +1 -1
- package/dist/components/SiteWidthField.d.ts +0 -1
- package/dist/components/SiteWidthField.js +1 -1
- package/dist/components/SlotChildActionsContext.d.ts +2 -13
- package/dist/components/SlotChildActionsContext.js +1 -1
- package/dist/components/SlotChildRows.js +1 -1
- package/dist/components/SocialLinksPanel.js +1 -1
- package/dist/components/SourceStatus.js +1 -1
- package/dist/components/Split.js +1 -1
- package/dist/components/StandaloneSectionEditor.d.ts +1 -0
- package/dist/components/StandaloneSectionEditor.js +1 -1
- package/dist/components/StorefrontDocumentContext.d.ts +2 -67
- package/dist/components/StorefrontDocumentContext.js +1 -1
- package/dist/components/StorefrontEditor.d.ts +1 -0
- package/dist/components/StorefrontEditor.js +1 -1
- package/dist/components/StorefrontEditorChrome.d.ts +1 -1
- package/dist/components/StorefrontEditorChrome.js +1 -1
- package/dist/components/StorefrontLinkField.d.ts +9 -0
- package/dist/components/StorefrontLinkField.js +1 -1
- package/dist/components/StylesPanel.js +1 -1
- package/dist/components/Testimonials.js +1 -1
- package/dist/components/TextStylesField.js +1 -1
- package/dist/components/ThemeCard.js +1 -1
- package/dist/components/ThemeColourField.js +1 -1
- package/dist/components/ThemeField.js +1 -1
- package/dist/components/TickerSettingsPanel.js +1 -1
- package/dist/components/Timeline.js +1 -1
- package/dist/components/TimelineTrack.js +1 -1
- package/dist/components/TitleWithButtons.js +1 -1
- package/dist/components/UiIcon.d.ts +2 -10
- package/dist/components/UiIcon.js +1 -1
- package/dist/components/WebsiteScreenParts.d.ts +2 -0
- package/dist/components/WebsiteScreenParts.js +1 -0
- package/dist/components/WebsiteSettingsPanel.js +1 -1
- package/dist/components/WidthFollowField.js +1 -1
- package/dist/components/goldPhrases.js +1 -1
- package/dist/components/panel-widths.d.ts +2 -12
- package/dist/components/panel-widths.js +1 -1
- package/dist/components/storefront-editor.css +147 -1195
- package/dist/components/useFooterFocus.d.ts +2 -0
- package/dist/components/useFooterFocus.js +1 -0
- package/dist/components/useLocationsTree.d.ts +6 -0
- package/dist/components/useLocationsTree.js +1 -0
- package/dist/components/usePartBridge.d.ts +1 -9
- package/dist/components/usePartBridge.js +1 -1
- package/dist/components/usePinnedChromeSeed.d.ts +2 -0
- package/dist/components/usePinnedChromeSeed.js +1 -0
- package/dist/components/useSocialSettings.js +1 -1
- package/dist/components/useStorefrontBlogPosts.js +1 -1
- package/dist/lib/client-only.d.ts +1 -0
- package/dist/lib/client-only.js +1 -0
- package/dist/lib/content/contact.js +3 -3
- package/dist/lib/content/subscribe.js +1 -1
- package/dist/lib/content/view-tiers.d.ts +2 -6
- package/dist/lib/content/view-tiers.js +1 -1
- package/dist/lib/editor-admin-bridge.d.ts +2 -29
- package/dist/lib/editor-admin-bridge.js +1 -1
- package/dist/lib/editor-backend-error.d.ts +2 -0
- package/dist/lib/editor-backend-error.js +1 -0
- package/dist/lib/editor-blocks.d.ts +2 -2
- package/dist/lib/editor-chrome.d.ts +2 -52
- package/dist/lib/editor-chrome.js +1 -1
- package/dist/lib/editor-document-adapter.d.ts +3 -51
- package/dist/lib/editor-document-adapter.js +1 -1
- package/dist/lib/editor-document-addressing.d.ts +2 -0
- package/dist/lib/editor-document-addressing.js +1 -0
- package/dist/lib/editor-document-contract.d.ts +1 -0
- package/dist/lib/editor-document-contract.js +0 -0
- package/dist/lib/editor-document-hooks.d.ts +1 -15
- package/dist/lib/editor-document-switch.d.ts +2 -0
- package/dist/lib/editor-document-switch.js +1 -0
- package/dist/lib/editor-failure.d.ts +1 -4
- package/dist/lib/editor-failure.js +1 -1
- package/dist/lib/editor-host-inbound.d.ts +1 -1
- package/dist/lib/editor-host-inbound.js +1 -1
- package/dist/lib/editor-host.d.ts +2 -135
- package/dist/lib/editor-host.js +1 -1
- package/dist/lib/editor-key-scope.d.ts +1 -3
- package/dist/lib/editor-key-scope.js +1 -1
- package/dist/lib/editor-load-error.d.ts +1 -4
- package/dist/lib/editor-load-error.js +1 -1
- package/dist/lib/editor-media-panel.d.ts +2 -23
- package/dist/lib/editor-media-panel.js +1 -1
- package/dist/lib/editor-media-picker.d.ts +2 -12
- package/dist/lib/editor-media-picker.js +1 -1
- package/dist/lib/editor-page-lock.d.ts +1 -1
- package/dist/lib/editor-page-lock.js +1 -1
- package/dist/lib/editor-screen.d.ts +2 -0
- package/dist/lib/editor-screen.js +1 -0
- package/dist/lib/editor-section-templates.d.ts +1 -2
- package/dist/lib/editor-section-templates.js +1 -1
- package/dist/lib/editor-session-context.d.ts +1 -7
- package/dist/lib/editor-session-context.js +1 -1
- package/dist/lib/editor-session.d.ts +2 -45
- package/dist/lib/editor-session.js +1 -1
- package/dist/lib/editor-site-slug.js +1 -1
- package/dist/lib/editor-theme.d.ts +1 -1
- package/dist/lib/editor-theme.js +1 -1
- package/dist/lib/hero-arrows.d.ts +1 -1
- package/dist/lib/links/link-filters.d.ts +1 -0
- package/dist/lib/links/link-filters.js +0 -0
- package/dist/lib/links/link-picker-setup.d.ts +29 -0
- package/dist/lib/links/link-picker-setup.js +1 -0
- package/dist/lib/puck/ColourControl.d.ts +4 -1
- package/dist/lib/puck/ColourControl.js +1 -1
- package/dist/lib/puck/MarkupToolbar.js +1 -1
- package/dist/lib/puck/add-palette.d.ts +3 -9
- package/dist/lib/puck/add-palette.js +1 -1
- package/dist/lib/puck/add-target-types.d.ts +1 -0
- package/dist/lib/puck/add-target-types.js +0 -0
- package/dist/lib/puck/add-target.d.ts +2 -9
- package/dist/lib/puck/block-choice.d.ts +2 -0
- package/dist/lib/puck/block-choice.js +1 -0
- package/dist/lib/puck/block-offer.js +1 -1
- package/dist/lib/puck/block-types-in.d.ts +1 -1
- package/dist/lib/puck/block-types-in.js +1 -1
- package/dist/lib/puck/blocks/general/CategoryBar/fields.js +1 -1
- package/dist/lib/puck/blocks/general/Chart/fields.js +1 -1
- package/dist/lib/puck/blocks/general/FAQ/render.js +1 -1
- package/dist/lib/puck/blocks/general/FeaturedCover/render.js +1 -1
- package/dist/lib/puck/blocks/general/Footprint/fields.js +1 -1
- package/dist/lib/puck/blocks/general/IntroBleed/render.js +1 -1
- package/dist/lib/puck/blocks/general/LogoBlock/fields.js +1 -1
- package/dist/lib/puck/blocks/general/LongRead/render.js +1 -1
- package/dist/lib/puck/blocks/general/Masthead/render.js +1 -1
- package/dist/lib/puck/blocks/general/Mockup/fields.js +1 -1
- package/dist/lib/puck/blocks/general/OrderSummary/render.js +1 -1
- package/dist/lib/puck/blocks/general/ParagraphsA/render.js +1 -1
- package/dist/lib/puck/blocks/general/PostCard/render.js +1 -1
- package/dist/lib/puck/blocks/general/ProcessSteps/render.js +1 -1
- package/dist/lib/puck/blocks/general/Series/render.js +1 -1
- package/dist/lib/puck/blocks/general/Split/render.js +1 -1
- package/dist/lib/puck/blocks/general/Testimonials/fields.js +1 -1
- package/dist/lib/puck/blocks/general/Testimonials/render.js +1 -1
- package/dist/lib/puck/blocks/general/TimelineTrack/render.js +1 -1
- package/dist/lib/puck/blocks/general/index.d.ts +2 -2
- package/dist/lib/puck/blocks/placeholders.js +1 -1
- package/dist/lib/puck/content-tree.d.ts +2 -43
- package/dist/lib/puck/content-tree.js +1 -1
- package/dist/lib/puck/controls.d.ts +11 -22
- package/dist/lib/puck/controls.js +1 -1
- package/dist/lib/puck/device-fields.d.ts +2 -24
- package/dist/lib/puck/device-fields.js +1 -1
- package/dist/lib/puck/field-buckets.d.ts +2 -0
- package/dist/lib/puck/field-buckets.js +1 -0
- package/dist/lib/puck/field-groups.d.ts +3 -3
- package/dist/lib/puck/field-groups.js +1 -1
- package/dist/lib/puck/field-markers.d.ts +2 -16
- package/dist/lib/puck/field-markers.js +1 -1
- package/dist/lib/puck/footer-block-row-name.d.ts +4 -0
- package/dist/lib/puck/footer-block-row-name.js +1 -0
- package/dist/lib/puck/footer-blocks.d.ts +2 -10
- package/dist/lib/puck/footer-blocks.js +1 -1
- package/dist/lib/puck/geocode-resolve.js +1 -1
- package/dist/lib/puck/hidable.d.ts +2 -9
- package/dist/lib/puck/hidable.js +1 -1
- package/dist/lib/puck/locations-tree.js +2 -2
- package/dist/lib/puck/neutralization-contracts.js +1 -1
- package/dist/lib/puck/palette-sections.d.ts +2 -0
- package/dist/lib/puck/palette-sections.js +1 -0
- package/dist/lib/puck/panel-fields.d.ts +2 -12
- package/dist/lib/puck/panel-fields.js +1 -1
- package/dist/lib/puck/panel-no-control-mark.d.ts +2 -7
- package/dist/lib/puck/panel-no-control-mark.js +1 -1
- package/dist/lib/puck/part-id.d.ts +1 -0
- package/dist/lib/puck/part-id.js +1 -0
- package/dist/lib/puck/part-rows.js +1 -1
- package/dist/lib/puck/pinned-seed-gate.d.ts +2 -12
- package/dist/lib/puck/pinned-seed-gate.js +1 -1
- package/dist/lib/puck/preview-click.d.ts +1 -2
- package/dist/lib/puck/preview-click.js +1 -1
- package/dist/lib/puck/preview-dom.d.ts +1 -6
- package/dist/lib/puck/preview-dom.js +1 -1
- package/dist/lib/puck/preview-events.d.ts +2 -0
- package/dist/lib/puck/preview-events.js +1 -0
- package/dist/lib/puck/preview-selection-context.d.ts +2 -19
- package/dist/lib/puck/preview-selection-context.js +1 -1
- package/dist/lib/puck/section-catalog.js +1 -1
- package/dist/lib/puck/section-descriptions.js +1 -1
- package/dist/lib/puck/section-doc.d.ts +1 -5
- package/dist/lib/puck/section-doc.js +1 -1
- package/dist/lib/puck/section-groups.d.ts +1 -1
- package/dist/lib/puck/slot-child-name.d.ts +2 -8
- package/dist/lib/puck/slot-child-name.js +1 -1
- package/dist/lib/puck/slot-heal.d.ts +1 -2
- package/dist/lib/puck/slot-heal.js +1 -1
- package/dist/lib/puck/slot-section-spec.d.ts +1 -0
- package/dist/lib/puck/slot-section-spec.js +0 -0
- package/dist/lib/puck/slot-specs.d.ts +2 -13
- package/dist/lib/puck/style-clipboard.d.ts +1 -19
- package/dist/lib/puck/style-clipboard.js +1 -1
- package/dist/lib/puck/text-control-kinds.d.ts +2 -13
- package/dist/lib/puck/text-control-kinds.js +1 -1
- package/dist/lib/puck/text-parts.d.ts +2 -5
- package/dist/lib/puck/text-parts.js +1 -1
- package/dist/lib/puck/tree-reveal.d.ts +1 -11
- package/dist/lib/puck/tree-reveal.js +1 -1
- package/dist/lib/puck/use-section-ops.d.ts +2 -25
- package/dist/lib/puck/use-section-ops.js +1 -1
- package/dist/lib/reel.d.ts +1 -1
- package/dist/lib/section-template-types.d.ts +1 -21
- package/dist/lib/sections/divider-style.js +1 -1
- package/dist/lib/sections/new-section-node.d.ts +2 -0
- package/dist/lib/sections/new-section-node.js +1 -0
- package/dist/lib/sections/product-card-style.js +1 -1
- package/dist/lib/sections/ticker-style.js +1 -1
- package/dist/lib/source-rules/accent-token-name.d.ts +20 -0
- package/dist/lib/source-rules/accent-token-name.js +4 -0
- package/dist/lib/source-rules/button-corners.d.ts +13 -0
- package/dist/lib/source-rules/button-corners.js +2 -0
- package/dist/lib/source-rules/editor-chrome-note.d.ts +16 -0
- package/dist/lib/source-rules/editor-chrome-note.js +13 -0
- package/dist/lib/source-rules/editor-context-menu.d.ts +5 -0
- package/dist/lib/source-rules/editor-context-menu.js +4 -0
- package/dist/lib/source-rules/editor-puck-drag-off.d.ts +8 -0
- package/dist/lib/source-rules/editor-puck-drag-off.js +4 -0
- package/dist/lib/source-rules/efficient-easel-version.d.ts +9 -0
- package/dist/lib/source-rules/efficient-easel-version.js +2 -0
- package/dist/lib/source-rules/every-button-follows-the-button-colour.d.ts +12 -0
- package/dist/lib/source-rules/every-button-follows-the-button-colour.js +7 -0
- package/dist/lib/source-rules/flat-theme-palette.d.ts +10 -0
- package/dist/lib/source-rules/flat-theme-palette.js +8 -0
- package/dist/lib/source-rules/full-width-means-full.d.ts +8 -0
- package/dist/lib/source-rules/full-width-means-full.js +2 -0
- package/dist/lib/source-rules/heading-ceiling-sweep.d.ts +11 -0
- package/dist/lib/source-rules/heading-ceiling-sweep.js +8 -0
- package/dist/lib/source-rules/label-hints.d.ts +7 -0
- package/dist/lib/source-rules/label-hints.js +5 -0
- package/dist/lib/source-rules/label-vocabulary.d.ts +14 -0
- package/dist/lib/source-rules/label-vocabulary.js +5 -0
- package/dist/lib/source-rules/mirrored-rows-agree.d.ts +19 -0
- package/dist/lib/source-rules/mirrored-rows-agree.js +10 -0
- package/dist/lib/source-rules/no-comment-in-jsx.d.ts +9 -0
- package/dist/lib/source-rules/no-comment-in-jsx.js +7 -0
- package/dist/lib/source-rules/no-glyph-rows.d.ts +15 -0
- package/dist/lib/source-rules/no-glyph-rows.js +4 -0
- package/dist/lib/source-rules/one-colour-control.d.ts +7 -0
- package/dist/lib/source-rules/one-colour-control.js +2 -0
- package/dist/lib/source-rules/one-dropdown.d.ts +10 -0
- package/dist/lib/source-rules/one-dropdown.js +4 -0
- package/dist/lib/source-rules/one-editable-list.d.ts +16 -0
- package/dist/lib/source-rules/one-editable-list.js +6 -0
- package/dist/lib/source-rules/one-editor-list.d.ts +7 -0
- package/dist/lib/source-rules/one-editor-list.js +3 -0
- package/dist/lib/source-rules/one-right-click.d.ts +10 -0
- package/dist/lib/source-rules/one-right-click.js +2 -0
- package/dist/lib/source-rules/one-switch.d.ts +9 -0
- package/dist/lib/source-rules/one-switch.js +7 -0
- package/dist/lib/source-rules/one-up-level-link.d.ts +16 -0
- package/dist/lib/source-rules/one-up-level-link.js +18 -0
- package/dist/lib/source-rules/panel-colour-tokens.d.ts +48 -0
- package/dist/lib/source-rules/panel-colour-tokens.js +20 -0
- package/dist/lib/source-rules/panel-no-divider-lines.d.ts +14 -0
- package/dist/lib/source-rules/panel-no-divider-lines.js +10 -0
- package/dist/lib/source-rules/panel-one-arrow.d.ts +21 -0
- package/dist/lib/source-rules/panel-one-arrow.js +1 -0
- package/dist/lib/source-rules/panel-one-hover.d.ts +8 -0
- package/dist/lib/source-rules/panel-one-hover.js +2 -0
- package/dist/lib/source-rules/panel-rows-use-the-shared-row.d.ts +23 -0
- package/dist/lib/source-rules/panel-rows-use-the-shared-row.js +22 -0
- package/dist/lib/source-rules/part-slider-fills-row.d.ts +11 -0
- package/dist/lib/source-rules/part-slider-fills-row.js +29 -0
- package/dist/lib/source-rules/part-style-naming.d.ts +10 -0
- package/dist/lib/source-rules/part-style-naming.js +2 -0
- package/dist/lib/source-rules/puck-document-surfaces.d.ts +16 -0
- package/dist/lib/source-rules/puck-document-surfaces.js +8 -0
- package/dist/lib/source-rules/reduced-motion.d.ts +26 -0
- package/dist/lib/source-rules/reduced-motion.js +9 -0
- package/dist/lib/source-rules/route-content-width.d.ts +22 -0
- package/dist/lib/source-rules/route-content-width.js +5 -0
- package/dist/lib/source-rules/rule.d.ts +21 -0
- package/dist/lib/source-rules/rule.js +1 -0
- package/dist/lib/source-rules/section-theme-preview.d.ts +9 -0
- package/dist/lib/source-rules/section-theme-preview.js +8 -0
- package/dist/lib/source-rules/site-gutter.d.ts +6 -0
- package/dist/lib/source-rules/site-gutter.js +4 -0
- package/dist/lib/source-rules/site-square-by-default.d.ts +17 -0
- package/dist/lib/source-rules/site-square-by-default.js +3 -0
- package/dist/lib/source-rules/site-width.d.ts +11 -0
- package/dist/lib/source-rules/site-width.js +7 -0
- package/dist/lib/source-rules/size-token-paints.d.ts +8 -0
- package/dist/lib/source-rules/size-token-paints.js +11 -0
- package/dist/lib/source-rules/social-icons-import.d.ts +6 -0
- package/dist/lib/source-rules/social-icons-import.js +6 -0
- package/dist/lib/source-rules/source-status-is-shared.d.ts +7 -0
- package/dist/lib/source-rules/source-status-is-shared.js +1 -0
- package/dist/lib/source-rules/style-face-matches-size.d.ts +10 -0
- package/dist/lib/source-rules/style-face-matches-size.js +4 -0
- package/dist/lib/source-rules/support/accent-rename-bytes.d.ts +5 -0
- package/dist/lib/source-rules/support/accent-rename-bytes.js +1 -0
- package/dist/lib/source-rules/support/css-box-model.d.ts +5 -0
- package/dist/lib/source-rules/support/css-box-model.js +1 -0
- package/dist/lib/source-rules/support/css-length.d.ts +2 -0
- package/dist/lib/source-rules/support/css-length.js +1 -0
- package/dist/lib/source-rules/support/css-rules.d.ts +12 -0
- package/dist/lib/source-rules/support/css-rules.js +1 -0
- package/dist/lib/source-rules/support/css-source.d.ts +2 -0
- package/dist/lib/source-rules/support/css-source.js +3 -0
- package/dist/lib/source-rules/support/label-extraction.d.ts +14 -0
- package/dist/lib/source-rules/support/label-extraction.js +5 -0
- package/dist/lib/source-rules/support/panel-select-cascade.d.ts +72 -0
- package/dist/lib/source-rules/support/panel-select-cascade.js +2 -0
- package/dist/lib/source-rules/support/part-slider-call-sites.d.ts +9 -0
- package/dist/lib/source-rules/support/part-slider-call-sites.js +17 -0
- package/dist/lib/source-rules/support/registry-source.d.ts +14 -0
- package/dist/lib/source-rules/support/registry-source.js +3 -0
- package/dist/lib/source-rules/support/ts-source.d.ts +2 -0
- package/dist/lib/source-rules/support/ts-source.js +3 -0
- package/dist/lib/source-rules/theme-mode-controls-gone.d.ts +10 -0
- package/dist/lib/source-rules/theme-mode-controls-gone.js +5 -0
- package/dist/lib/source-rules/upload-intake-guard.d.ts +11 -0
- package/dist/lib/source-rules/upload-intake-guard.js +6 -0
- package/dist/lib/source-rules/word-styles.d.ts +8 -0
- package/dist/lib/source-rules/word-styles.js +4 -0
- package/dist/lib/theme-row-persist.js +1 -1
- package/dist/lib/theme-save-message.js +1 -1
- package/dist/lib/web-backend.d.ts +4 -59
- package/dist/lib/web-backend.js +1 -1
- package/dist/lib/website-document-addressing.d.ts +2 -0
- package/dist/lib/website-document-addressing.js +1 -0
- package/dist/lib/website-host-inbound.d.ts +1 -0
- package/dist/lib/website-host-inbound.js +1 -0
- package/dist/lib/website-session.d.ts +5 -0
- package/dist/lib/website-session.js +1 -0
- package/package.json +18 -8
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{ruleChecks as y}from"./rule.js";import{isPanelFile as b,readJsxLabels as m,readLabels as f,readPanelLabels as u,readRowLabels as w}from"./support/label-extraction.js";import{registryFiles as L,registryText as $}from"./support/registry-source.js";const F=/label:\s*"(?:[^"\\]|\\.)*"\s*,\s*onClick:/g;function p(e){const s=new Set;return[...f(e),...w(e),...m(e)].filter(t=>{const l=`${t.line}:${t.label}`;return s.has(l)?!1:(s.add(l),!0)})}function g(e){return{lost:p(e).length-u(e).length,actions:(e.match(F)??[]).length}}function k(e,s,t){const l=o=>s.find(a=>a.path===o)?.text,i=[...t.stylesheets,t.locationsRows],r=i.filter(o=>l(o)===void 0);e(`read the files drawn round the screen (${i.length-r.length} of ${i.length})`,r.length===0&&t.stylesheets.length>0,`not among the files handed in: ${r.join(", ")||"no stylesheet named"} — every clause below is vacuous without them`);const n=l(t.locationsRows)??"",c=t.stylesheets.filter(o=>(l(o)??"").includes("sf-paste-hint"));e("§1d no stylesheet of the editor's draws a second toast",c.length===0,`sf-paste-hint in ${c.join(", ")}`),e("§3 the Locations rows are where this reads them",/selectLocationsFocus\(si, \{ level: "group"/.test(n)&&/selectLocationsFocus\(si, \{ level: "city"/.test(n)),e("§3 a Locations group row is not addressed as a node",!/selectLocationsFocus\(si, \{ level: "group"[\s\S]{0,200}data-node-id/.test(n)&&!/data-node-id/.test(n)),e("§3 a Locations city row is not addressed as a node",!/selectLocationsFocus\(si, \{ level: "city"[\s\S]{0,200}data-node-id/.test(n))}function P(e,s){const{check:t,report:l}=y("one right click");s.editor&&k(t,e,s.editor);{const a=["const items = [",' { label: "Copy", onClick: () => copy() },',"];","const rows = [",' { key: "heading", label: "Heading", group: "text" },',"];"].join(`
|
|
2
|
+
`),d=u(a).map(h=>h.label);t("§4 the action rule drops an action's label and keeps a field's",d.length===1&&d[0]==="Heading"&&g(a).lost===1&&g(a).actions===1,JSON.stringify({kept:d,...g(a)}))}const i=e.filter(b);t(`§4 read the panel files (${i.length}, at least ${s.minPanelFiles})`,i.length>=s.minPanelFiles,"fewer means a panel stopped drawing a row or the predicate broke");const r=[];for(const a of i){const{lost:d,actions:h}=g(a.text);d>h&&r.push(`${a.path} (${d}, ${h} of them actions)`)}t("§4 no panel file loses a label to the action rule beyond its actions",r.length===0,r.join(", ")+" — a real panel dropping out of the vocabulary guard is the failure this rule must not cause");const n=L(e),c=$(e),o=f(c).length;return t(`§4 read the block registry (${n.length} files, ${o} labels)`,n.length>0,"no registry file among the files handed in"),t("§4 the inspector's own labels are read by a path the rule cannot reach",n.length>0&&(p(c).length===u(c).length||o>=s.registryLabelFloor),`the registry goes through readLabels, not readPanelLabels (${o} read, floor ${s.registryLabelFloor})`),l()}export{P as oneRightClick};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type * as TS from "typescript";
|
|
2
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
3
|
+
export type OneSwitchOptions = {
|
|
4
|
+
readonly notEditor?: Readonly<Record<string, string>>;
|
|
5
|
+
readonly settingsSwitchFloor: number;
|
|
6
|
+
readonly minFiles: number;
|
|
7
|
+
readonly stylesheets?: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function oneSwitch(ts: typeof TS, files: readonly SourceFile[], options: OneSwitchOptions): RuleReport;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{ruleChecks as x}from"./rule.js";function S(i,o){const n=i.createSourceFile(o.path,o.text,i.ScriptTarget.Latest,!0,i.ScriptKind.TSX),s=[],f=[],a=t=>n.getLineAndCharacterOfPosition(t.getStart(n)).line+1,r=t=>{if(i.isJsxOpeningElement(t)||i.isJsxSelfClosingElement(t)){let h=!1,g="";for(const e of t.attributes.properties){if(!i.isJsxAttribute(e))continue;const c=e.name.getText(n),m=e.initializer,w=m?m.getText(n):"";c==="role"&&/["'`]switch["'`]/.test(w)&&(h=!0),c==="className"&&(g=w)}s.push({name:t.tagName.getText(n),line:a(t),roleSwitch:h,classText:g})}if(i.isPropertyAssignment(t)){const h=t.name.getText(n).replace(/["']/g,""),g=t.initializer.getText(n);h==="role"&&/^["'`]switch["'`]$/.test(g)&&s.push({name:"(createElement)",line:a(t),roleSwitch:!0,classText:""}),h==="className"&&f.push({line:a(t),text:g})}i.forEachChild(t,r)};return r(n),{tags:s,classStrings:f}}function p(i,o,n){const{tags:s,classStrings:f}=S(i,o),a=[];let r=0;for(const t of s)t.name==="SettingsSwitch"&&r++,t.name==="ToggleSwitch"&&a.push({file:o.path,line:t.line,what:"R1 <ToggleSwitch> — use easel's SettingsSwitch"}),t.roleSwitch&&!(o.path in n)&&a.push({file:o.path,line:t.line,what:`R2 hand-rolled role="switch" on <${t.name}> — use easel's SettingsSwitch`}),/\bsf-toggle\b/.test(t.classText)&&a.push({file:o.path,line:t.line,what:"R3 sf-toggle class in className"});for(const t of f)/\bsf-toggle\b/.test(t.text)&&a.push({file:o.path,line:t.line,what:"R3 sf-toggle class in className"});return{bad:a,settingsSwitch:r}}function d(i){const o=[];return i.text.replace(/\/\*[\s\S]*?\*\//g,s=>s.replace(/[^\n]/g," ")).split(`
|
|
2
|
+
`).forEach((s,f)=>{/\.sf-toggle\b/.test(s)&&o.push({file:i.path,line:f+1,what:"R3 .sf-toggle selector"})}),o}function b(i,o,n){const{check:s,report:f}=x("one switch"),a=n.notEditor??{};{const e=["const a = <ToggleSwitch value onChange={f} />;",'const b = <button role="switch" aria-checked={x} />;','const c = <span className={`sf-toggle ${on ? "is-on" : ""}`} />;','const d = h("button", { role: "switch", className: "sf-toggle" });',"// a comment about <ToggleSwitch> is not a use",'const ok = <SettingsSwitch checked onChange={f} ariaLabel="X" />;'].join(`
|
|
3
|
+
`),{bad:c,settingsSwitch:m}=p(i,{path:"fixture.tsx",text:e},{}),w=c.map(l=>l.line);s("R1 flags <ToggleSwitch>",c.some(l=>l.line===1&&l.what.startsWith("R1"))),s('R2 flags <button role="switch">',c.some(l=>l.line===2&&l.what.startsWith("R2"))),s("R3 flags an sf-toggle className",c.some(l=>l.line===3&&l.what.startsWith("R3"))),s("R2 + R3 flag the createElement form",w.filter(l=>l===4).length===2),s("a comment is not a use",!w.includes(5)),s("SettingsSwitch passes and is counted",!w.includes(6)&&m===1);const u=d({path:"fixture.css",text:`/* .sf-toggle in a comment */
|
|
4
|
+
.sf-toggle.is-on { }
|
|
5
|
+
.es-switch { }`});s("R3 flags a .sf-toggle selector, not a comment",u.length===1&&u[0].line===2)}const r=o.filter(e=>/\.tsx?$/.test(e.path)&&!e.path.endsWith(".d.ts")),t=o.filter(e=>e.path.endsWith(".css")),h=[];let g=0;for(const e of r){const c=p(i,e,a);h.push(...c.bad),g+=c.settingsSwitch}for(const e of t)h.push(...d(e));s('no local switch: no <ToggleSwitch>, no hand-rolled role="switch", no sf-toggle',h.length===0,`${h.length} hit(s):
|
|
6
|
+
`+h.map(e=>`${e.file}:${e.line} ${e.what}`).join(`
|
|
7
|
+
`)),s(`read the source (${r.length} TS/TSX files, at least ${n.minFiles})`,r.length>=n.minFiles);for(const e of n.stylesheets??[])s(`read the stylesheet ${e}`,t.some(c=>c.path===e));s(`found >= ${n.settingsSwitchFloor} <SettingsSwitch> uses (found ${g})`,g>=n.settingsSwitchFloor,"fewer means a row went back to a local switch or the reader broke");for(const[e,c]of Object.entries(a)){const m=o.find(u=>u.path===e),w=!!m&&S(i,m).tags.some(u=>u.roleSwitch);s(`the exception is still real: ${e} (${c})`,w,"remove the stale entry")}return f()}export{b as oneSwitch};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type OneUpLevelLinkOptions = {
|
|
3
|
+
readonly waiting?: Readonly<Record<string, {
|
|
4
|
+
readonly count: number;
|
|
5
|
+
readonly why: string;
|
|
6
|
+
}>>;
|
|
7
|
+
readonly upLevelUses?: readonly {
|
|
8
|
+
readonly file: string;
|
|
9
|
+
readonly words: readonly RegExp[];
|
|
10
|
+
readonly what: string;
|
|
11
|
+
}[];
|
|
12
|
+
readonly minEditorFiles: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function backControls(source: string): number[];
|
|
15
|
+
export declare const isEditorCode: (source: string) => boolean;
|
|
16
|
+
export declare function oneUpLevelLink(files: readonly SourceFile[], options: OneUpLevelLinkOptions): RuleReport;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import{ruleChecks as w}from"./rule.js";const k=/className=["'][^"']*?(?<=["'\s-])back(?=["'\s-])/,v=/^\s*(?:<span[^>]*>\s*)?(?:\{\s*)?['"`]?\s*(?:[←‹↰⟵]|Back\b|All of\b)/,C=/<[A-Z]\w*(?:Back|Chevron|ArrowLeft)\w*\b/;function b(l){const a=l.replace(/=>/g,"=:"),e=[];for(const i of a.matchAll(/<button\b[^>]*(?<!\/)>([\s\S]*?)<\/button>/g)){const c=i[1],h=i[0].slice(0,i[0].length-c.length-9);(v.test(c)||k.test(h)||k.test(c)||C.test(c))&&e.push(a.slice(0,i.index).split(`
|
|
2
|
+
`).length)}return e}const y=l=>/from\s+["']efficient-easel\/(?!render["']|markup["']|data["'])[\w-]+["']/.test(l);function $(l,a){const{check:e,report:i}=w("one up-level link"),c=a.waiting??{},h=[["Locations link",`<button type="button" style={linkBtn} onClick={() => setLocationsFocus({ level: "groups" })}>
|
|
3
|
+
← Locations
|
|
4
|
+
</button>`],["Locations link, a name",'<button type="button" style={linkBtn} onClick={onBack}>← {String(card.name ?? "") || "City"}</button>'],["part panel back",`<button type="button" className="sf-part-back" onClick={onBack}>
|
|
5
|
+
<BackChevron />
|
|
6
|
+
<span>Back to the page</span>
|
|
7
|
+
</button>`],["easel's old list back",`<button type="button" className="es-list-back" onClick={() => onOpen(null)}>
|
|
8
|
+
<span className="es-list-back-chev" aria-hidden="true" />
|
|
9
|
+
{label}
|
|
10
|
+
</button>`],["a chevron as text",`<button type="button" onClick={() => setOpen(null)}>
|
|
11
|
+
<span aria-hidden>‹</span>
|
|
12
|
+
<span>All panels</span>
|
|
13
|
+
</button>`]];for(const[t,n]of h)e(`§1 the scanner sees a retired shape: ${t}`,b(n).length===1);const g=[["a select's chevron",`<button className="es-fin es-fsel-button" onClick={() => toggle()}>
|
|
14
|
+
<span className="es-fsel-chev" aria-hidden="true" />
|
|
15
|
+
</button>`],["a reorder arrow",'<button type="button" onClick={() => move(-1)}>↑</button>'],["a background class",'<button className="sf-background-chip">Fill</button>'],["a self-closing button before a real one",`<button className="sf-swatch" onClick={() => pick(t)} />
|
|
16
|
+
<span className="sf-go-back">x</span>
|
|
17
|
+
<button>Done</button>`]];for(const[t,n]of g)e(`§1 the scanner leaves alone: ${t}`,b(n).length===0);const p=l.filter(t=>/\.(tsx|jsx)$/.test(t.path)&&y(t.text));e(`§2 the scan reached the editor code (${p.length} files, at least ${a.minEditorFiles})`,p.length>=a.minEditorFiles,"fewer means the walk or the editor-code test broke, and the check below is vacuous");const u=[],f={};for(const t of p){const n=b(t.text);if(t.path in c)f[t.path]=n.length;else for(const r of n)u.push(`${t.path}:${r}`)}e("§2 no hand-built back control in editor code — use InspectorUpLevel from efficient-easel/chrome",u.length===0,u.join(`
|
|
18
|
+
`));for(const[t,{count:n,why:r}]of Object.entries(c)){const o=f[t];e(`§3 ${t} still has exactly ${n} waiting (${o??"file not scanned"})`,o===n,`${r}. If they moved, delete this waiting entry.`)}for(const t of a.upLevelUses??[]){const n=l.find(s=>s.path===t.file);if(!e(`§4 read ${t.file}`,!!n)||!n)continue;const r=n.text.replace(/=>/g,"=:"),o=[...r.matchAll(/<InspectorUpLevel\b([^>]*?)\/>/g)].map(s=>s[1]);e(`§4 ${t.file} draws InspectorUpLevel ${t.words.length} times (${o.length})`,o.length===t.words.length),e("§4 …each inline: it sits in the panel body, under the inspector head",o.length>0&&o.every(s=>/\binline\b/.test(s)));const d=o.map(s=>s.match(/name=(\{.*?\}|"[^"]*")\s+onSelect=/)?.[1]??s);e(`§4 …with the words it had: ${t.what}`,t.words.every((s,m)=>s.test(d[m]??"")),d.join(" | ")),e("§4 …and no linkBtn back is left",!/style=\{linkBtn\}\s+onClick=\{(?:onBack|\(\) =: setLocationsFocus)/.test(r))}return i()}export{b as backControls,y as isEditorCode,$ as oneUpLevelLink};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type * as TS from "typescript";
|
|
2
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
3
|
+
export type PanelRuling = {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly kind: "KEEP";
|
|
6
|
+
readonly count: number;
|
|
7
|
+
readonly why: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly file: string;
|
|
10
|
+
readonly kind: "UNTIL";
|
|
11
|
+
readonly count: number;
|
|
12
|
+
readonly until: string;
|
|
13
|
+
};
|
|
14
|
+
export type PanelColourTokensOptions = {
|
|
15
|
+
readonly editorRoot: (path: string) => boolean;
|
|
16
|
+
readonly publishedRoot: (path: string) => boolean;
|
|
17
|
+
readonly panelFile: (path: string) => boolean;
|
|
18
|
+
readonly allow: readonly PanelRuling[];
|
|
19
|
+
readonly anchors?: readonly string[];
|
|
20
|
+
readonly drawnByAField?: readonly string[];
|
|
21
|
+
readonly opacitySites?: readonly string[];
|
|
22
|
+
readonly shopperFacing?: readonly string[];
|
|
23
|
+
readonly floors: {
|
|
24
|
+
readonly editorRoots: number;
|
|
25
|
+
readonly publishedRoots: number;
|
|
26
|
+
readonly population: number;
|
|
27
|
+
readonly customFields: number;
|
|
28
|
+
readonly bytes: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export declare function blankVarFallbacks(input: string): string;
|
|
32
|
+
export type LiteralKind = "palette" | "bw" | "colour-value" | "size-value";
|
|
33
|
+
export declare function classifyClassToken(token: string): LiteralKind | null;
|
|
34
|
+
export type PanelLiteral = {
|
|
35
|
+
readonly file: string;
|
|
36
|
+
readonly line: number;
|
|
37
|
+
readonly kind: LiteralKind;
|
|
38
|
+
readonly text: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function scanSource(ts: typeof TS, fileLabel: string, source: string): PanelLiteral[];
|
|
41
|
+
export declare function panelPopulation(ts: typeof TS, files: readonly SourceFile[], options: Pick<PanelColourTokensOptions, "editorRoot" | "publishedRoot" | "panelFile">): {
|
|
42
|
+
source: SourceFile[];
|
|
43
|
+
editorRoots: string[];
|
|
44
|
+
publishedRoots: string[];
|
|
45
|
+
customFields: string[];
|
|
46
|
+
population: string[];
|
|
47
|
+
};
|
|
48
|
+
export declare function panelColourTokens(ts: typeof TS, files: readonly SourceFile[], options: PanelColourTokensOptions): RuleReport;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import{ruleChecks as F}from"./rule.js";const I=new Set("slate gray zinc neutral stone red orange amber yellow lime green emerald teal cyan sky blue indigo violet purple fuchsia pink rose".split(" ")),S=new Set("text bg border ring fill stroke divide outline shadow from via to placeholder accent caret decoration".split(" ")),E=new Set("text w h p px py pt pb pl pr m mx my mt mb ml mr gap top bottom left right inset basis size leading tracking rounded border min-w max-w min-h max-h gap-x gap-y space-x space-y translate-x translate-y".split(" "));function R(t){let a=t;for(let s=0;s<20;s++){const n=a.replace(/var\(\s*--[A-Za-z0-9-]+\s*,((?:[^()]|\([^()]*\))*)\)/g,(d,u)=>`var(--x,${"·".repeat(u.length)})`);if(n===a)break;a=n}return a}function N(t){const a=t.lastIndexOf("]"),s=t.indexOf("/",a+1);if(s===-1)return t;const n=t.slice(s+1);return/^(?:\d{1,3}|\[[^\]]*\])$/.test(n)?t.slice(0,s):t}function P(t){let a=t;for(let s=0;s<12;s++){const n=/^(?:[a-z0-9-]+|[a-z-]+-\[[^\]]*\]|\[[^\]]*\]):(.*)$/.exec(a);if(!n)break;a=n[1]}return N(a.replace(/^!/,"").replace(/^-/,""))}function O(t){const a=P(t),s=/^([a-z-]+)-\[([^\]]+)\]$/.exec(a);if(s){const[,u,o]=s;if(o.includes("·")||/^var\(/.test(o)||/^url\(|^none$|^inherit$|^auto$/.test(o))return null;const p=/^#/.test(o)||/^(?:rgba?|hsla?|oklch|color-mix)\(/.test(o)||/^(?:white|black|transparent|currentColor)$/.test(o),f=/^-?[\d.]+(?:px|rem|em|%|vw|vh|ch|ex|pt|fr)?$|calc\(|^clamp\(/.test(o);return p&&(S.has(u)||E.has(u))?"colour-value":f&&E.has(u)?"size-value":S.has(u)&&!f?"colour-value":E.has(u)?"size-value":null}const n=/^([a-z-]+)-([a-z]+)-(\d{2,3})$/.exec(a);if(n&&S.has(n[1])&&I.has(n[2]))return"palette";const d=/^([a-z-]+)-(white|black)$/.exec(a);return d&&S.has(d[1])?"bw":null}const C=/#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/g,B=/\b(?:rgba?|hsla?|oklch|color-mix)\(/g;function A(t,a,s){const n=t.createSourceFile(a,s,t.ScriptTarget.Latest,!0,t.ScriptKind.TSX),d=[],u=o=>{let p=null;if((t.isStringLiteral(o)||t.isNoSubstitutionTemplateLiteral(o)||t.isTemplateHead(o)||t.isTemplateMiddle(o)||t.isTemplateTail(o))&&(p=o.text),p&&p.length){const f=n.getLineAndCharacterOfPosition(o.getStart(n)).line+1;let r=R(p);for(const c of r.split(/[\s;{}]+/)){if(!c)continue;const w=O(c);w&&(d.push({file:a,line:f,kind:w,text:c}),c.includes("[")&&(r=r.split(c).join("·".repeat(c.length))))}for(const c of[C,B]){c.lastIndex=0;let w;for(;w=c.exec(r);)d.push({file:a,line:f,kind:"colour-value",text:w[0]})}}t.forEachChild(o,u)};return t.forEachChild(n,u),d}function U(t,a,s){let n;if(a.startsWith("@/"))n=a.slice(2);else if(a.startsWith("./")||a.startsWith("../")){const d=s.split("/").slice(0,-1);for(const u of a.split("/"))u===".."?d.pop():u!=="."&&d.push(u);n=d.join("/")}else return null;for(const d of[`${n}.ts`,`${n}.tsx`,`${n}/index.ts`,`${n}/index.tsx`,n])if(t.has(d)&&/\.tsx?$/.test(d)&&!d.endsWith(".d.ts"))return d;return null}function j(t,a){const s=new Set(a.map(o=>o.path)),n=new Map,d=new Set;for(const o of a){const p=t.createSourceFile(o.path,o.text,t.ScriptTarget.Latest,!0,t.ScriptKind.TSX),f=new Map,r=[],c=(i,m)=>{const x=U(s,i,o.path);if(x&&(r.push(x),!!m&&(m.name&&f.set(m.name.text,x),m.namedBindings&&t.isNamedImports(m.namedBindings))))for(const g of m.namedBindings.elements)f.set(g.name.text,x)},w=i=>{if(t.isImportDeclaration(i)&&t.isStringLiteral(i.moduleSpecifier)&&c(i.moduleSpecifier.text,i.importClause),t.isExportDeclaration(i)&&i.moduleSpecifier&&t.isStringLiteral(i.moduleSpecifier)&&c(i.moduleSpecifier.text),t.isCallExpression(i)&&i.expression.kind===t.SyntaxKind.ImportKeyword){const m=i.arguments[0];m&&t.isStringLiteral(m)&&c(m.text)}if(t.isObjectLiteralExpression(i)){const m=i.properties.some(g=>t.isPropertyAssignment(g)&&t.isIdentifier(g.name)&&g.name.text==="type"&&t.isStringLiteral(g.initializer)&&g.initializer.text==="custom"),x=i.properties.find(g=>t.isPropertyAssignment(g)&&t.isIdentifier(g.name)&&g.name.text==="render");if(m&&x){const g=e=>{(t.isJsxOpeningElement(e)||t.isJsxSelfClosingElement(e))&&t.isIdentifier(e.tagName)&&f.has(e.tagName.text)&&d.add(f.get(e.tagName.text)),t.forEachChild(e,g)};g(x.initializer)}}t.forEachChild(i,w)};t.forEachChild(p,w),n.set(o.path,r)}return{customFieldTargets:d,reachableFrom:o=>{const p=new Set,f=[...o];for(;f.length;){const r=f.pop();if(!p.has(r)){p.add(r);for(const c of n.get(r)??[])f.push(c)}}return p}}}const L=["bg-[","url(a","/b.png)]"].join("");function K(t,a,s){const n=a.filter(i=>/\.tsx?$/.test(i.path)&&!i.path.endsWith(".d.ts")),d=j(t,n),u=n.filter(i=>s.editorRoot(i.path)).map(i=>i.path),o=n.filter(i=>s.publishedRoot(i.path)).map(i=>i.path),p=d.reachableFrom(u),f=d.reachableFrom(o),r=[...p].filter(i=>s.panelFile(i)&&!f.has(i)),c=[...d.customFieldTargets].filter(s.panelFile),w=[...new Set([...r,...c])].sort();return{source:n,editorRoots:u,publishedRoots:o,customFields:c,population:w}}function M(t,a,s){const{check:n,report:d}=F("panel colour tokens");{const e=["// a stale slot — #319 and 13.6:1","/** Theme 1 accent #2e2e2e + black ink */",'const a = "text-[13px] py-[7px]";','const b = "text-[var(--sf-warning,#b45309)]";','const c = "border-[var(--sf-line, rgba(128,128,128,0.3))]";','const d = "text-zinc-400 hover:bg-amber-100 md:text-white";','const e = "z-[200] grid-cols-[1fr_auto]";','const f = { color: "#d85a30", boxShadow: "0 1px 2px rgba(20,20,22,0.10)" };','const g = "text-[#eee] max-w-[170px]";','const h = "bg-black/30 text-zinc-400/50 border-white/10";','const j = "w-2 bg-white/45 hover:bg-white/70";',`const i = "${L} w-1/2";`].join(`
|
|
2
|
+
`),l=A(t,"sample.tsx",e).map(b=>`${b.kind}:${b.text}`).sort(),h=["bw:bg-black/30","bw:bg-white/45","bw:border-white/10","bw:hover:bg-white/70","bw:md:text-white","colour-value:#d85a30","colour-value:rgba(","colour-value:text-[#eee]","palette:hover:bg-amber-100","palette:text-zinc-400","palette:text-zinc-400/50","size-value:max-w-[170px]","size-value:py-[7px]","size-value:text-[13px]"];n("the scanner is CALIBRATED — it reads code, not comments, and not tokens",JSON.stringify(l)===JSON.stringify(h),`the scanner has drifted. It must (1) never read a comment — the card number #319 and the contrast table are not colours; (2) read var(--tok, #fallback) as the TOKEN it is, at any nesting; (3) decide colour-vs-size from the VALUE, so text-[13px] is a size and text-[#eee] is a colour; (4) ignore z-[…] and grid-cols-[…], which are neither; (5) see THROUGH an opacity modifier — bg-black/30 is still a hard-coded black — without mistaking the slash inside ${L} or the one in w-1/2 for one. Got: ${JSON.stringify(l)}`)}const{source:u,editorRoots:o,publishedRoots:p,customFields:f,population:r}=K(t,a,s),{floors:c}=s;n(`the editor entries were found (${o.length}, at least ${c.editorRoots})`,o.length>=c.editorRoots&&o.length>0,"the walk read no editor entry, so every rule below would be an assertion over an empty list and would pass for the wrong reason"),n(`the published entries were found (${p.length}, at least ${c.publishedRoots})`,p.length>=c.publishedRoots&&p.length>0,"without them every component would look editor-only and the population would balloon"),n(`the panel population is ${r.length} files (at least ${c.population})`,r.length>=c.population&&r.length>0,"a population that small is not a clean repository, it is a scan that read nowhere — something in module resolution broke"),n(`Puck custom fields draw ${f.length} panel files (at least ${c.customFields})`,f.length>=c.customFields,"rule B is what covers every settings field a registry reaches; if it stops matching, the field components leave the population in silence");for(const e of s.anchors??[])n(`${e} is panel chrome, and in the population`,r.includes(e),"it is missing — the walk has stopped reaching the editor");for(const e of s.drawnByAField??[])n(`${e} is drawn by a Puck custom field, and in the population`,r.includes(e),'it belongs to the panel, but its NAME does not say "panel" — rule B has stopped working');for(const e of s.opacitySites??[])n(`${e} carries a colour behind an opacity modifier, and stays in the population`,r.includes(e),"it is one of the sites this rule was once blind to; if it has dropped out, the rule is green while ignoring a defect it was written to find");for(const e of s.shopperFacing??[])n(`${e} paints a shopper's page, and is not in the population`,!r.includes(e),"it is not this rule's business, and taking it would make this a storefront gate wearing a panel gate's name");const w=new Map(u.map(e=>[e.path,e.text])),i=new Map;for(const e of r){const l=A(t,e,w.get(e)??"");l.length&&i.set(e,l)}const m=[...i.values()].reduce((e,l)=>e+l.length,0),x=new Map(s.allow.map(e=>[e.file,e])),g=r.reduce((e,l)=>e+(w.get(l)??"").length,0);n(`the scan read ${g} bytes of panel source (at least ${c.bytes})`,g>=c.bytes&&g>0,'that is not the editor. Every "no literals found" below would be meaningless.'),n("the scanner found literals while the allow list still lists files",m>0||s.allow.length===0,"the scanner found nothing anywhere while the allow list still lists files. A scan that reads nothing looks exactly like a clean codebase.");{const e=[];for(const[l,h]of[...i].sort()){if(x.has(l))continue;const b=h.slice(0,6).map($=>` ${l}:${$.line} ${$.text}`);e.push(` ${l} — ${h.length} literal${h.length===1?"":"s"}
|
|
3
|
+
`+b.join(`
|
|
4
|
+
`)+(h.length>b.length?`
|
|
5
|
+
…and ${h.length-b.length} more`:""))}n("NO panel file outside the list carries a colour or size literal",e.length===0,`Hard-coded colour/size literals in editor PANEL components.
|
|
6
|
+
|
|
7
|
+
The panel's colours and sizes come from tokens — the --sf-* custom properties the editor's sheets define (efficient-easel/screen.css, and the builder's components/storefront-editor.css) and the .es-* classes efficient-easel draws. A literal here is a private copy of one step of that scale: it does not move when the scale moves, and nothing says it is wrong until somebody looks at the panel beside the campaign designer.
|
|
8
|
+
|
|
9
|
+
Spell it as a token. If it genuinely cannot be one, add a line to the allow list with KEEP (a design reason) or UNTIL (what has to land), and its exact count.
|
|
10
|
+
|
|
11
|
+
`+e.join(`
|
|
12
|
+
|
|
13
|
+
`))}{const e=[];for(const l of s.allow){const h=i.get(l.file);if(!r.includes(l.file)){e.push(` ${l.file} is on the list but is no longer a panel component (the editor does not reach it, and no Puck custom field draws it). Delete the line.`);continue}if(!h){e.push(` ${l.file} is CLEAN now — 0 literals, the line still allows ${l.count}. Delete the line: that is what "done" looks like.`);continue}if(h.length>l.count){const b=h.slice(l.count).map($=>`${$.text} @${$.line}`);e.push(` ${l.file} has ${h.length} literals, the line allows ${l.count}. A NEW literal went into a file we already tolerate — this is exactly how the count creeps back. Spell it as a token; do not raise the number. (e.g. ${b.slice(0,3).join(", ")})`)}h.length<l.count&&e.push(` ${l.file} is down to ${h.length} literals from ${l.count}. Good — now finish it: either clear the rest and delete the line, or lower the count in this same PR. A line that allows more than the file spends goes on excusing a debt that has been paid.`)}n(`every allow line still describes its file — counts pinned BOTH ways (${s.allow.length} lines)`,e.length===0,`the allow list is out of date:
|
|
14
|
+
|
|
15
|
+
${e.join(`
|
|
16
|
+
|
|
17
|
+
`)}`)}{const e=[],l=new Set;for(const h of s.allow)l.has(h.file)&&e.push(` ${h.file} is listed twice`),l.add(h.file),h.count<1&&e.push(` ${h.file} pins count ${h.count} — a line allowing nothing is a deleted line`),h.kind==="KEEP"&&h.why.trim().length<20&&e.push(` ${h.file} is a KEEP with no design behind it. A KEEP says why the literal is RIGHT; if you cannot say that, it is an UNTIL.`),h.kind==="UNTIL"&&h.until.trim().length<20&&e.push(` ${h.file} is an UNTIL with no expiry. A reason explains why a line is here today; an expiry is what GETS IT OUT. Say what has to land.`);n("every allow line carries an EXPIRY, not just a reason",e.length===0,`allow lines are malformed:
|
|
18
|
+
|
|
19
|
+
${e.join(`
|
|
20
|
+
`)}`)}{const e=i.size,l=r.length-e,h=r.length?Math.round(l/r.length*100):0,b=s.allow.filter(y=>y.kind==="KEEP").length,$=s.allow.filter(y=>y.kind==="UNTIL").length,z=[...i.values()].flat().filter(y=>y.kind!=="size-value").length;n(`PROGRESS — panel token coverage: ${l}/${r.length} files clean (${h}%) · ${m} literals left in ${e} file${e===1?"":"s"} (${z} colour + ${m-z} size) · allow: ${b} KEEP + ${$} UNTIL`,r.length>0);for(const[y,T]of[...i].sort((k,v)=>v[1].length-k[1].length)){const k=T.filter(v=>v.kind!=="size-value").length;n(`PROGRESS ${String(k).padStart(3)} colour ${String(T.length-k).padStart(3)} size ${y}`,!0)}}return d()}export{R as blankVarFallbacks,O as classifyClassToken,M as panelColourTokens,K as panelPopulation,A as scanSource};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type PanelNoDividerLinesOptions = {
|
|
3
|
+
readonly panelSheets?: readonly string[];
|
|
4
|
+
readonly allowed?: Readonly<Record<string, string>>;
|
|
5
|
+
readonly pendingElsewhere?: Readonly<Record<string, string>>;
|
|
6
|
+
readonly fullBleed?: readonly string[];
|
|
7
|
+
readonly allowedTsx?: readonly {
|
|
8
|
+
readonly file: string;
|
|
9
|
+
readonly util: string;
|
|
10
|
+
readonly why: string;
|
|
11
|
+
}[];
|
|
12
|
+
readonly minFiles: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function panelNoDividerLines(files: readonly SourceFile[], options: PanelNoDividerLinesOptions): RuleReport;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import{ruleChecks as E}from"./rule.js";function v(r){const s=r.split(`
|
|
2
|
+
`),n=[];return s.forEach((g,a)=>{if(!/border-(top|bottom|left|right)[a-z-]*\s*:[^;]*\bsolid\b/.test(g))return;let $=0,c="?";for(let i=a-1;i>=0;i-=1)if($+=(s[i].match(/\}/g)??[]).length-(s[i].match(/\{/g)??[]).length,$<0){c=s[i].split("{")[0].trim();let h=i-1;for(;h>=0&&s[h].trim().endsWith(",");)c=`${s[h].trim()} ${c}`,h-=1;break}n.push({line:a+1,selector:c,decl:g.trim()})}),n}const y=r=>r.split(/[\s,+>~]/)[0].trim(),j=(r,s)=>s in r||y(s)in r,O=/\bzinc-|--sf-line\b|--sf-divider\b|--sf-input-border\b|--sf-chip-bg\b|--es-line\b|--es-divider\b|--sf-text-secondary\b/,k=[["utility",/(^|["'`\s])border-(t|b|l|r)($|["'`\s])/],["style object",/border(Top|Bottom|Left|Right)\s*:/],["css in a string",/border-(top|bottom|left|right)\s*:/]];function S(r){return/radius/i.test(r)||!O.test(r)?null:k.find(([,s])=>s.test(r))?.[0]??null}function N(r,s){const{check:n,report:g}=E("panel divider lines"),a=s.allowed??{},$=s.pendingElsewhere??{},c=s.allowedTsx??[];{const e=u=>["border",u].join("-"),t=[".a,",".b + .c {",` ${e("top")}: 1px solid red;`,"}",`.d { ${e("radius")}: 4px; }`].join(`
|
|
3
|
+
`),l=v(t);n("the census finds a directional solid border, with its whole multi-line selector",l.length===1&&l[0].selector===".a, .b + .c"&&y(l[0].selector)===".a",JSON.stringify(l));const d=["zinc","200"].join("-"),f=[["a utility",`<div className="${e("t")} border-${d}" />`,"utility"],["a style object",`style={{ ${["border","Top"].join("")}: "1px solid var(--sf-line)" }}`,"style object"],["CSS in a string",`const s = "${e("left")}: 1px solid var(--es-divider)";`,"css in a string"],["a border in the storefront's own ink",`<div className="${e("t")} border-line" />`,null],["a radius",`<div className="${e("t")} border-${d} rounded" style={{ borderRadius: 4 }} />`,null]];for(const[u,o,p]of f)n(`the sweep reads ${u} as ${p??"no hairline"}`,S(o)===p,o)}const i=[];for(const e of s.panelSheets??[]){const t=r.find(o=>o.path===e)?.text??"",l=v(t);n(`${e} was read and parsed`,t.length>1e4&&l.length>0,`${t.length} bytes, ${l.length} directional border(s)`),i.push(...l.map(o=>({...o,sheet:e})));const f=(t.replace(/\/\*[\s\S]*?\*\//g,"").match(/\[class\*="_PuckFields[^"]*"\][^{]*\{/g)??[]).length;n(`${e} reaches for no Puck field wrapper`,f===0,`${f} rule(s) still select \`[class*="_PuckFields…"]\` — the rows are \`.es-insp-row\` now`);const u=t.replace(/,\s*\n\s*/g,", ");for(const o of s.fullBleed??[]){if(!(o in a))continue;const p=u.indexOf(`
|
|
4
|
+
${o} {`);if(!n(`the allowed full-bleed rule is still in ${e}`,p>=0,o))continue;const x=u.slice(p,u.indexOf("}",p));for(const w of["left","right"])n(`the full-bleed rule cancels the panel's ${w} padding, so it reaches the edge`,new RegExp(`margin-${w}:\\s*calc\\(var\\(--sf-insp-pad-x\\)\\s*\\*\\s*-1\\)`).test(x),"inset, this is the line the owner rejected — full bleed is the exemption"),n(`…and puts that ${w} padding back inside, so the words stay on the rows' edge`,new RegExp(`padding-${w}:\\s*var\\(--sf-insp-pad-x\\)`).test(x))}}for(const e of s.fullBleed??[])n(`the full-bleed selector is an ALLOWED entry: ${e}`,e in a);if(s.panelSheets?.length){const e=i.filter(t=>!j(a,t.selector)&&!j($,t.selector));n(`no unlisted directional border survives in a panel stylesheet (${i.length} censused)`,e.length===0,`${e.length} neither removed, allowed, nor owed to another card — delete it and let the rhythm tokens separate, or add it to the allow-list with a reason if it is a control edge:
|
|
5
|
+
`+e.map(t=>`${t.sheet}:${t.line} ${t.selector} — ${t.decl}`).join(`
|
|
6
|
+
`))}const h=e=>i.some(t=>t.selector===e||y(t.selector)===e);for(const[e,t]of Object.entries(a))n(`ALLOWED entry ${e} still names a real rule (${t.slice(0,48)}…)`,h(e),"the rule is gone: delete this entry with it");for(const[e,t]of Object.entries($))n(`PENDING entry ${e} is still owed — ${t}`,h(e),"that card has landed and the rule is gone: delete this entry");const b=r.filter(e=>/\.tsx?$/.test(e.path));n(`the repository's own TS/TSX files were swept (${b.length}, at least ${s.minFiles})`,b.length>=s.minFiles);const m=[];for(const e of b)e.text.split(`
|
|
7
|
+
`).forEach((t,l)=>{const d=S(t);d&&(c.some(f=>f.file===e.path&&t.includes(f.util))||m.push(`${e.path}:${l+1} (${d}) ${t.trim().slice(0,80)}`))});n("no editor panel file draws an unlisted directional border",m.length===0,`${m.length} found:
|
|
8
|
+
${m.join(`
|
|
9
|
+
`)}`);for(const e of c){const t=b.find(l=>l.path===e.file)?.text??"";n(`ALLOWED_TSX entry ${e.file} ${e.util} still names a real line (${e.why.slice(0,40)}…)`,t.split(`
|
|
10
|
+
`).some(l=>k.some(([,d])=>d.test(l))&&l.includes(e.util)),"the line is gone: delete this entry")}return g()}export{N as panelNoDividerLines};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as TS from "typescript";
|
|
2
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
3
|
+
import { type SelectHelper } from "./support/panel-select-cascade.js";
|
|
4
|
+
export type PanelCascadeOptions = {
|
|
5
|
+
readonly panelSheet: string | SourceFile;
|
|
6
|
+
readonly selectHelper: SelectHelper;
|
|
7
|
+
readonly populationFloor: number;
|
|
8
|
+
readonly houseDrawnFloor: number;
|
|
9
|
+
};
|
|
10
|
+
export type PanelOneArrowOptions = PanelCascadeOptions & {
|
|
11
|
+
readonly editorEntries?: readonly string[];
|
|
12
|
+
readonly loadedSheets?: Readonly<Record<string, string | null>>;
|
|
13
|
+
};
|
|
14
|
+
export declare const namesSelectElement: (selector: string) => boolean;
|
|
15
|
+
export declare function panelSheetOf(files: readonly SourceFile[], panelSheet: string | SourceFile): {
|
|
16
|
+
sheet: SourceFile | undefined;
|
|
17
|
+
ours: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare const censusFiles: (files: readonly SourceFile[]) => SourceFile[];
|
|
20
|
+
export declare function editorPackageSheets(ts: typeof TS, files: readonly SourceFile[], entries: readonly string[], sheetPath: string): string[];
|
|
21
|
+
export declare function panelOneArrow(ts: typeof TS, files: readonly SourceFile[], options: PanelOneArrowOptions): RuleReport;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isRuleSource as U,ruleChecks as G}from"./rule.js";import{readRules as K}from"./support/css-rules.js";import{stripComments as V}from"./support/css-source.js";import{applicable as q,census as J,deriveShapes as L,derefVars as Q,importsOf as B,readVars as X,resolve as O,splitDecls as W}from"./support/panel-select-cascade.js";const S="--sf-select-arrow",R=["background-image","background-size","background-repeat","background-position"],I=l=>/(^|[\s,>+~(])select(?![\w-])/.test(l);function Y(l,a){return typeof a!="string"?{sheet:a,ours:!1}:{sheet:l.find(o=>o.path===a),ours:!0}}const Z=l=>l.filter(a=>/\.(tsx?|mts)$/.test(a.path)&&!U(a.path)),P=(l,a)=>l===`@/${a}`||l.endsWith(`/${a}`)||l===`./${a.split("/").pop()}`;function oe(l,a,o,n){const x=new Set;for(const j of o){const k=a.find(r=>r.path===j);if(k)for(const r of B(l,k))r.endsWith(".css")&&!P(r,n)&&!r.startsWith("@/")&&!r.startsWith(".")&&x.add(r)}return[...x].sort()}function ae(l,a,o){const{check:n,report:x}=G("panel one arrow"),{sheet:j,ours:k}=Y(a,o.panelSheet),r=j?.path??(typeof o.panelSheet=="string"?o.panelSheet:o.panelSheet.path),{population:g,helperDeclared:N}=J(l,Z(a),o.selectHelper),T=g.filter(e=>e.engineDrawn),p=g.filter(e=>!e.engineDrawn);a.some(e=>e.path===o.selectHelper.file)&&n(`${o.selectHelper.file} declares the house \`select\` helper`,N,"the census has no helper to resolve (B) against"),n(`the panel draws house-drawn selects (${p.length}, at least ${o.houseDrawnFloor})`,p.length>=o.houseDrawnFloor&&(p.length>0||o.houseDrawnFloor===0),`${p.length} found: ${p.map(e=>`${e.file}:${e.line}`).join(", ")||"(none)"} — with none, this card's rows do not exist and §3 passes empty`);const v=V(j?.text??""),$=X(v);n(`${r} was read`,v.length>1e4,`${v.length} bytes`);const D=K(v),i=L(g,v),F=i.reduce((e,t)=>e+t.rows,0);n(`every house-drawn select is in exactly one shape, and no engine-drawn field is in any (${i.map(e=>e.name).join(", ")})`,F===p.length,`shapes hold ${F} row(s), the census found ${p.length} house-drawn`);const u=new Map,E=new Map,y=[],M=[];for(const e of i){const t=q(D,e,r,0,"rest"),s=q(D,e,r,0,"hover");y.push(...t.unreadable,...s.unreadable),M.push(...s.undecided);const m={appearance:O(t.applied,"appearance",$).value};for(const h of R)m[h]=O(t.applied,h,$).value;m["@from"]=O(t.applied,"background-image",$).from,u.set(e.name,m);const c={};for(const h of R)c[h]=O(s.applied,h,$).value;E.set(e.name,c)}const f=(e,t)=>e.get(t.name);n("every rule that can reach a panel select was understood",y.length===0,`could not parse: ${[...new Set(y)].slice(0,4).join(" · ")}${y.length>4?` (+${y.length-4} more)`:""} — a rule this model skips is a rule it lies about`);for(const e of i){const t=f(u,e);n(`${e.name}: the browser draws no arrow of its own (appearance: none)`,t.appearance==="none",`appearance resolves to \`${t.appearance}\` on ${e.rows} row(s) — the platform arrow is still painted, so an added image would sit beside it`)}for(const e of i){const t=f(u,e);n(`${e.name}: an arrow IS drawn`,/url\(/.test(t["background-image"]),`background-image resolves to \`${t["background-image"]}\` on ${e.rows} row(s) — a row with no arrow reads as a stylesheet that failed to load, and taking the arrow away undoes the owner's 2026-09-01 ruling`)}const A=f(u,i[0]);for(const e of["appearance",...R]){const t=i.filter(s=>f(u,s)[e]!==A[e]);n(`every shape resolves the same \`${e}\``,t.length===0,`${i[0].name} (${i[0].rows} row(s)) says \`${A[e].slice(0,44)}\`, but `+t.map(s=>`${s.name} (${s.rows} row(s)) says \`${f(u,s)[e].slice(0,44)}\``).join("; "))}const C=e=>e["background-repeat"]!=="no-repeat"||e["background-size"]==="auto";for(const e of i){const t=f(u,e);n(`${e.name}: the arrow is never tiled (66397)`,!C(t),`repeat=${t["background-repeat"]} size=${t["background-size"]} — a full-size tile repeats ~2.5x across a 248px row and draws a bar of triangles`)}for(const e of i){const t=f(E,e);n(`${e.name}: the arrow is never tiled under the pointer either (66397)`,/url\(/.test(t["background-image"])&&!C(t),`hovered image=${t["background-image"].slice(0,40)} repeat=${t["background-repeat"]} size=${t["background-size"]} — this is the dark bar with three triangles, in the state it was filed in`)}const H=M.filter(e=>W(e.decls).some(t=>t.prop==="background"||R.includes(t.prop)));if(n("no rule this model cannot judge decides a hovered arrow property",H.length===0,`${[...new Set(H.map(e=>`@media ${e.media} { ${e.selector} }`))].join(" · ")} — an unjudged media query holding one of these properties makes (e) a guess`),k){const e=D.flatMap(t=>t.selector.split(",").map(s=>s.trim()).filter(s=>I(s)).filter(()=>W(t.decls).some(s=>s.prop==="background")).map(s=>t.media?`${t.media} { ${s} }`:s));n("no rule that names the `select` element uses the `background` shorthand",e.length===0,`${e.join(" · ")} — a shorthand resets background-size/-repeat/-position/-image; set \`background-color\` instead (card 66397)`)}if(o.editorEntries!==void 0){const e=/^@measured\/puck(\/|$)/,t=new Set;for(const c of o.editorEntries){const h=a.find(d=>d.path===c),w=h?B(l,h):[];n(`${c} loads ${r}`,w.some(d=>P(d,r)),h?"§3 resolves the panel stylesheet's cascade, and this editor does not load it":"the entry is not among the files");const b=w.filter(d=>e.test(d));n(`${c} imports nothing from Puck's package`,b.length===0,`imports ${b.join(", ")} — Puck was removed in blueprint 6.3, and §3 no longer models its stylesheet`);for(const d of w)d.endsWith(".css")&&!P(d,r)&&t.add(d)}const s=[],m=[];for(const c of t){const w=(c.startsWith("@/")?a.find(b=>b.path===c.slice(2))?.text:void 0)??o.loadedSheets?.[c]??null;if(w===null){m.push(c);continue}for(const b of K(V(w)))for(const d of b.selector.split(","))I(d)&&s.push(`${c}: ${d.trim()}`)}n(`every other stylesheet the editors load was read (${t.size})`,m.length===0,`could not resolve ${m.join(", ")} — a sheet this rule cannot open is a cascade it cannot rule out`),n("no other stylesheet the editors load has a rule naming the `select` element",s.length===0,`${s.slice(0,4).join(" · ")}${s.length>4?` (+${s.length-4} more)`:""} — §3 resolves ${r} alone, so this rule is invisible to every answer above`)}const z=Q($.get(S)??"",$);if(k){const e=D.flatMap(t=>W(t.decls)).filter(t=>t.prop===S);n(`${r} declares \`${S}\` exactly once, as an image`,e.length===1&&/^url\(/.test(z),`${e.length} declaration(s), value \`${z.slice(0,60)||"(none)"}\` — the one arrow needs one definition`)}for(const e of i){const t=f(u,e)["background-image"];n(`${e.name}: the arrow it draws is \`var(${S})\`, glyph for glyph`,z!==""&&t===z,`resolves \`${t.slice(0,60)}\` — a redrawn arrow is the SS seat's call, made once in \`${S}\`, not in a rule of its own`)}const _=p.flatMap(e=>e.classes.filter(t=>/^(appearance-|bg-(none|\[|gradient|linear|radial|conic))/.test(t)).map(t=>`${e.file}:${e.line} \`${t}\``));return n("no panel select decides its arrow with a utility class",_.length===0,`${_.join(", ")} — §3 reads only ${r}`),n(`the population came from the program, not from a number typed here (${g.length}, at least ${o.populationFloor})`,g.length>=o.populationFloor&&g.length>0,`${g.length} dropdowns found (${p.length} house-drawn, ${T.length} engine-drawn) — a census that collapses to a handful means the walk stopped reading`),x()}export{Z as censusFiles,oe as editorPackageSheets,I as namesSelectElement,ae as panelOneArrow,Y as panelSheetOf};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type * as TS from "typescript";
|
|
2
|
+
import { type PanelCascadeOptions } from "./panel-one-arrow.js";
|
|
3
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
4
|
+
export type PanelOneHoverOptions = PanelCascadeOptions & {
|
|
5
|
+
readonly chrome?: boolean;
|
|
6
|
+
readonly screenSheet?: SourceFile;
|
|
7
|
+
};
|
|
8
|
+
export declare function panelOneHover(ts: typeof TS, files: readonly SourceFile[], options: PanelOneHoverOptions): RuleReport;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{censusFiles as Q,panelSheetOf as X}from"./panel-one-arrow.js";import{ruleChecks as Y}from"./rule.js";import{readRules as I}from"./support/css-rules.js";import{stripComments as K}from"./support/css-source.js";import{applicable as P,census as ee,chainInMarkup as te,compound as q,deriveShapes as oe,readVars as ne,resolve as A,splitCombinators as B,splitDecls as F}from"./support/panel-select-cascade.js";const y=["background-color","background-image","border-color"],se=["background-image","border-color"],j="--sf-hover",C="--sf-select-arrow",m=p=>{const u=/fill='([^']*)'/.exec(p)?.[1];return u?`url(<svg … fill='${u}'>)`:p.length>46?`${p.slice(0,46)}…`:p};function ie(p,u,r){const{check:s,report:Z}=Y("panel one hover"),{sheet:N,ours:E}=X(u,r.panelSheet),k=N?.path??(typeof r.panelSheet=="string"?r.panelSheet:r.panelSheet.path),{population:f,helperDeclared:G}=ee(p,Q(u),r.selectHelper),J=f.filter(e=>e.engineDrawn),$=f.filter(e=>!e.engineDrawn);u.some(e=>e.path===r.selectHelper.file)&&s(`${r.selectHelper.file} declares the house \`select\` helper`,G,"the census has no helper to resolve against"),s(`the panel draws house-drawn selects (${$.length}, at least ${r.houseDrawnFloor})`,$.length>=r.houseDrawnFloor&&($.length>0||r.houseDrawnFloor===0),`${$.length} found — with none, this card's rows do not exist`),s(`the population came from the program, not from a number typed here (${f.length}, at least ${r.populationFloor})`,f.length>=r.populationFloor&&f.length>0,`${f.length} dropdowns found (${$.length} house-drawn, ${J.length} engine-drawn) — a census that collapses to a handful means the walk stopped reading`);const a=r.screenSheet,S=K(a?.text??""),w=K(N?.text??""),R=a?`${S}
|
|
2
|
+
${w}`:w,v=ne(R);s(`${k} was read`,w.length>1e4,`${w.length} bytes`),a&&s(`${a.path} was read`,S.length>1e4,`${S.length} bytes`);const D=I(S),L=I(w),c=oe(f,R),b=(e,t)=>{const n=P(D,e,a?.path??"",0,t),o=P(L,e,k,D.length,t);return{applied:[...n.applied,...o.applied],unreadable:[...n.unreadable,...o.unreadable],undecided:[...n.undecided,...o.undecided]}},T=(e,t)=>Object.fromEntries(t.map(n=>[n,A(e.applied,n,v).value])),x=new Map,g=new Map,H=[],V=[];for(const e of c){const t=b(e,"rest"),n=b(e,"hover");H.push(...t.unreadable,...n.unreadable),V.push(...n.undecided),x.set(e.name,T(t,y)),g.set(e.name,{...T(n,y),"@from":A(n.applied,"background-color",v).from})}const d=(e,t)=>e.get(t.name);s("every rule that can reach a panel select was understood",H.length===0,`could not parse: ${[...new Set(H)].slice(0,4).join(" · ")} — a rule this model skips is a rule it lies about`);const U=V.filter(e=>F(e.decls).some(t=>y.includes(t.prop)||t.prop==="background"||t.prop==="border"));s("no rule this model cannot judge decides a hovered value",U.length===0,`${U.map(e=>`@media ${e.media} { ${e.selector} }`).join(" · ")} — an unjudged media query holding one of these properties makes every answer above a guess`);const M=c[0];for(const e of y){const t=d(g,M)[e],n=c.filter(o=>d(g,o)[e]!==t);s(`every shape resolves the same hovered \`${e}\``,n.length===0,`${M.name} (${M.rows} row(s)) says \`${m(t)}\`, but `+n.map(o=>`${o.name} (${o.rows} row(s)) says \`${m(d(g,o)[e])}\``).join("; "))}for(const e of c){const t=d(x,e),n=d(g,e);s(`${e.name}: the ground actually changes under the pointer`,t["background-color"]!==n["background-color"],`rest and hover both resolve \`${m(n["background-color"])}\` on ${e.rows} row(s) — a row that does not answer the pointer is the other half of this card's defect`)}const O=v.get(j);(E||a)&&s(`the ${a?"editor's sheets declare":"panel declares"} \`${j}\``,O!==void 0,`the token the ruling names is not in ${a?`${a.path} or ${k}`:"the panel stylesheet"}`);for(const e of c){const t=d(g,e);s(`${e.name}: the hovered ground is the panel's own \`var(${j})\``,O!==void 0&&t["background-color"]===O,`resolves to \`${m(t["background-color"])}\`, and \`${j}\` is \`${O??"(not declared)"}\` — option D is the panel's own hover ground, picked by Lewis on 2026-09-02, and a near miss is not it`)}for(const e of c){const t=d(x,e),n=d(g,e);for(const o of se)s(`${e.name}: \`${o}\` is unchanged under the pointer`,t[o]===n[o],`rest \`${m(t[o])}\` -> hover \`${m(n[o])}\` on ${e.rows} row(s) — Lewis chose D (ground only) over B (border jump) and C (darker glyph) from pictures; moving this is re-deciding it`)}if(E){const e=l=>l.replace(/\s+/g," ").replace(/^url\(([\s\S]*)\)$/,"$1").replace(/^["']|["']$/g,"").trim(),t=v.get(C),n=e(t??"");s(`the panel declares \`${C}\` as an image`,t!==void 0&&/^url\(/.test(t.trim())&&n!=="",`\`${C}\` is \`${m(t??"(not declared)")}\` — the one arrow needs one definition`);const o=n===""?-1:R.split(n).length-1;s(`the arrow glyph is written ONCE in ${k}${a?` and ${a.path}`:""}`,o===1,`found ${o} — pasting the arrow into a second rule is exactly what card 66399 removed. Read \`var(${C})\` instead`);const i=[...D,...L].filter(l=>/select/.test(l.selector)&&F(l.decls).some(_=>/url\(\s*["']?data:/.test(_.value)));s("no select rule holds the arrow inline; every one reads the token",i.length===0,`${i.map(l=>l.selector.trim()).join(" · ")} still writes a data URI`)}const W=b(c[0],"hover").applied.filter(e=>/:hover/.test(e.selector)),h=W[0];if(E){s("the panel declares a select hover rule at all",W.length>0,"no `…select:hover` rule in the panel stylesheet reaches a house row — they do nothing under the pointer, which is this card's defect");const e=h?F(h.decls):[],t=h?B(h.selector).slice(0,-1).flatMap(o=>q(o)?.classes??[]):[];s("the hover rule uses no `!important`",h!==void 0&&!/!\s*important/i.test(h.decls),"an `!important` here beats every other state the panel paints, including ones this card was never given");const n=e.map(o=>o.prop).filter(o=>!y.includes(o));s("the hover rule declares only the three properties the ruling names",h!==void 0&&n.length===0,`also declares ${n.join(", ")} — the ruling is a ground colour, plus the arrow and border restated at rest. Anything else is a state this card was not given`),s("the hover rule is confined to the inspector body",t.includes("sf-insp-body"),`its ancestor chain is \`${t.join(" ")}\` — without \`.sf-insp-body\` it reaches every select in the editor, not the panel's`)}for(const e of c){const t=A(b(e,"rest").applied,"background-color",v).from;s(`${e.name}: the resting ground is NOT decided by the hover rule`,h===void 0||!t.includes(h.selector),`rest resolves from \`${t}\` — the pointer state has leaked into the row at rest`)}if(r.chrome){const e=new Set;for(const o of c)for(const i of["rest","hover"])for(const l of b(o,i).applied)for(const _ of B(l.selector).slice(0,-1))for(const z of q(_)?.classes??[])z.startsWith("@")||e.add(z);s(`the rules that resolved lean on wrapper classes (${[...e].map(o=>`.${o}`).join(" ")||"none"})`,e.size>0);const t=te(p,u,[...e]),n=new Set(D.flatMap(o=>[...o.selector.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map(i=>i[1])));for(const o of e){const i=(t.get(o)??[]).length>0,l=!i&&n.has(o);s(`\`.${o}\` is still written above a panel select in the editor's markup${l?` — the screen's (${a?.path} styles it)`:""}`,i||l,`no component writes \`${o}\` as a className any more${a?`, and ${a.path} styles no \`.${o}\` either`:""} — the rules leaning on it still parse, still look right, and match none of the ${$.length} house-drawn rows`)}}return Z()}export{ie as panelOneHover};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export declare const CONTROLS_FILE = "lib/puck/controls.tsx";
|
|
3
|
+
export type PanelRowException = {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly why: string;
|
|
7
|
+
};
|
|
8
|
+
export type PanelRowsOptions = {
|
|
9
|
+
readonly writesBuilders: boolean;
|
|
10
|
+
readonly floors: {
|
|
11
|
+
readonly toggle: number;
|
|
12
|
+
readonly textList: number;
|
|
13
|
+
readonly segmented: number;
|
|
14
|
+
readonly select: number;
|
|
15
|
+
readonly slider: number;
|
|
16
|
+
readonly handDrawn: number;
|
|
17
|
+
};
|
|
18
|
+
readonly exceptions?: readonly PanelRowException[];
|
|
19
|
+
readonly rulings: number;
|
|
20
|
+
};
|
|
21
|
+
export declare function secondArg(src: string, from: number): string;
|
|
22
|
+
export declare function callParen(line: string, builder: string): number;
|
|
23
|
+
export declare function panelRowsUseTheSharedRow(files: readonly SourceFile[], options: PanelRowsOptions): RuleReport;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import{ruleChecks as v}from"./rule.js";import{isRegistryPath as F,registryFiles as D}from"./support/registry-source.js";const L="lib/puck/controls.tsx",I={},S={},E=["boxShareField","edgePad","placementField","CoordsField","textList","segmented","select","slider"],P=/sf-field-label|<Label>/,N=/<[A-Z][A-Za-z0-9]*Icon\s*\/>/,B=s=>s.startsWith("//")||s.startsWith("*")||s.startsWith("/*");function A(s,o){const e=s.indexOf("(",o);if(e===-1)return"";let g=0;const a=[];let f=e+1;for(let d=e;d<s.length;d+=1){const r=s[d];if(r==="("||r==="["||r==="{")g+=1;else if(r===")"||r==="]"||r==="}"){if(g-=1,g===0){a.push(s.slice(f,d));break}}else if(r===","&&g===1&&(a.push(s.slice(f,d)),f=d+1,a.length===2))break}return a[1]??""}function W(s,o){const e=new RegExp(`(?:^|[^.\\w])${o}\\s*(?:<[^()]*?>)?\\s*\\(|\\bcontrols\\.${o}\\s*(?:<[^()]*?>)?\\s*\\(`).exec(s);return e?s.indexOf("(",e.index):s.indexOf("(")}function _(s,o){const e=s.slice(0,o);return e.includes("//")?!1:(e.match(/"/g)??[]).length%2===0&&(e.match(/`/g)??[]).length%2===0&&(e.match(/'/g)??[]).length%2===0}const T=(s,o)=>new RegExp(`import \\{[^}]*\\b${o}\\b[^}]*\\} from "@/lib/puck/controls"`).test(s)||/import \* as controls from "@\/lib\/puck\/controls"/.test(s);function G(s,o,e){const g=new RegExp(`(^|[^.\\w])${o}\\s*(<[^()]*?>)?\\s*\\(|\\bcontrols\\.${o}\\s*(<[^()]*?>)?\\s*\\(`,"g"),a=new RegExp(`^(export )?function ${o}(<[^>]*>)?\\(`),f=[];let d=0;for(const r of s){const p=r.text.split(`
|
|
2
|
+
`),x=p.join(`
|
|
3
|
+
`);p.forEach((b,h)=>{const m=b.trim();if(B(m)||a.test(m))return;g.lastIndex=0;const $=g.exec(b);if(!$||!_(b,$.index))return;d+=1;const t=A(x,p.slice(0,h).join(`
|
|
4
|
+
`).length+W(b,o));N.test(t)||/<FieldGlyph\b/.test(t)||e.some(n=>n.test(t))||f.push(`${r.path}:${h+1} ${m.slice(0,80)}`)})}return{bare:f,total:d}}function U(s){const o={};for(const e of s){const g=e.text.split(`
|
|
5
|
+
`);g.forEach((a,f)=>{if(!/className="sf-field"/.test(a))return;const d=g.slice(f,f+9).join(`
|
|
6
|
+
`);if(!P.test(d))return;const r=d.match(/sf-field-label[^>]*>([^<{][^<]*)</),p=d.match(/sf-field-label[^>]*>\{([^}]*)\}/),x=d.match(/<Label>([^<{][^<]*)</),b=d.match(/<Label>\{([^}]*)\}/),h=$=>$.trim()==="label"?"{label}":"{...}",m=r?r[1].trim():p?h(p[1]):x?x[1].trim():b?h(b[1]):"{...}";(o[e.path]??=[]).push(m)})}return o}function M(s){const o=[];let e=0;for(const g of s){const a=g.text.split(`
|
|
7
|
+
`);a.forEach((f,d)=>{if(!/<SettingsField\b/.test(f))return;const r=[];let p=!1;for(let b=d;b<Math.min(d+40,a.length)&&!p;b+=1){let h=a[b];if(h=h.replace(/\/\*[\s\S]*?\*\//g," "),/\/\*/.test(h)&&(h=h.slice(0,h.indexOf("/*"))),/^\s*\*/.test(a[b]))continue;const m=h.indexOf("//");m!==-1&&(h=h.slice(0,m)),r.push(h),h.includes(">")&&(p=!0)}const x=r.join(`
|
|
8
|
+
`);/\blabel=/.test(x)&&(e+=1,!/\bicon=/.test(x)&&o.push(`${g.path}:${d+1} ${f.trim().slice(0,80)}`))})}return{seen:e,bare:o}}function K(s,o){const{check:e,report:g}=v("panel rows use the shared row"),a=o.exceptions??[],{floors:f}=o;{const t={path:"fixture.tsx",text:['import { slider, textList } from "@/lib/puck/controls";','const a = { textList: () => controls.textList("Items", <ListIcon />, x) };','const b = slider("Thickness", 0, 10);','const c = slider("Gap", <GapIcon />, 0, 10);','// slider("In a comment", 0, 10) is not a row'].join(`
|
|
9
|
+
`)},n=G([t],"textList",[]),c=G([t],"slider",[]);e("the reader reads a namespaced call's own arguments, not the arrow's",n.total===1&&n.bare.length===0,JSON.stringify(n)),e("the reader flags a bare row whose neighbour has a glyph, and skips a comment",c.total===2&&c.bare.length===1&&c.bare[0].startsWith("fixture.tsx:3"),JSON.stringify(c))}const r=new Map(s.map(t=>[t.path,t])).get(L);e(o.writesBuilders?`read ${L}, where the builders are declared`:`${L} is not among this repository's files (it calls the builders, it does not declare them)`,!!r===o.writesBuilders);const p=D(s);e(`read this repository's share of the block registry (${p.length} files)`,p.length>0);const x=s.filter(t=>/^components\/[^/]+\.tsx$/.test(t.path)),b=s.filter(t=>/^scripts\/[^/]+\.tsx$/.test(t.path)),h=[...p,...r?[r]:[],...x],m=t=>t.path===L;{const t=h.filter(l=>m(l)||/import \{[^}]*\btoggle\b[^}]*\} from "@\/lib\/puck\/controls"/.test(l.text)),n=t.filter(l=>m(l)||F(l.path));e("only the known files use the toggle builder",t.length===n.length&&(!o.writesBuilders||n.some(m))&&n.some(l=>F(l.path)),t.map(l=>l.path).join(" · "));const c=[];let u=0;for(const l of n)l.text.split(`
|
|
10
|
+
`).forEach((w,i)=>{const y=w.trim();B(y)||/classList\.toggle/.test(w)||/^export function toggle\(/.test(y)||/(^|[^.\w])toggle\(/.test(w)&&(u+=1,!(N.test(w)||/<FieldGlyph\b/.test(w))&&c.push(`${l.path}:${i+1} ${y.slice(0,80)}`))});e("every toggle() row carries a glyph",c.length===0,`
|
|
11
|
+
${c.join(`
|
|
12
|
+
`)}`),e(`…and there are rows to check (${u} toggle() call sites, at least ${f.toggle})`,u>=f.toggle,"the scan is reading nothing")}const $=[{builder:"textList",inControls:!1,accept:[/TICKER_ITEMS_ROW\.icon/],name:"every textList() row carries a glyph"},{builder:"segmented",inControls:!0,accept:[/\bnoGlyph\(/],name:"every segmented() row carries a glyph (or a ruled noGlyph)"},{builder:"select",inControls:!0,accept:[/\btextKindGlyph\(/,/\bnoGlyph\(/],name:"every select() row carries a glyph (or a ruled noGlyph)"},{builder:"slider",inControls:!0,accept:[/\bnoGlyph\(/],name:"every slider() row carries a glyph"}];for(const t of $){const n=[...p,...t.inControls&&r?[r]:[],...b].filter(l=>t.inControls&&m(l)||F(l.path)||T(l.text,t.builder)),{bare:c,total:u}=G(n,t.builder,t.accept);e(t.name,c.length===0,`
|
|
13
|
+
${c.join(`
|
|
14
|
+
`)}`),e(`…and the ${t.builder} scan actually read rows (${u} call sites in ${n.length} files, at least ${f[t.builder]})`,u>=f[t.builder])}{const t=U(h);for(const i of a)e(`the exception still names a real row — ${i.label}`,(t[i.file]??[]).includes(i.label),`${i.file} no longer draws it`);const n={};for(const[i,y]of Object.entries(t)){const k=y.filter(O=>!a.some(j=>j.file===i&&j.label===O));k.length&&(n[i]=k)}const c=i=>i.slice().sort(),u=[];for(const[i,y]of Object.entries(n)){const k=I[i];if(!k){u.push(`${i} — hand-rolls ${y.length} row(s) and is not in PENDING`);continue}const O=c(y),j=c(k);(O.length!==j.length||O.some((C,R)=>C!==j[R]))&&u.push(`${i} — found [${O.join(" · ")}] but PENDING says [${j.join(" · ")}]`)}e("no NEW hand-rolled settings row",u.length===0,`
|
|
15
|
+
${u.join(`
|
|
16
|
+
`)}`);const l=Object.keys(I).filter(i=>!n[i]);e("no PENDING entry has outlived its subject",l.length===0,l.join(" · "));const w=Object.values(n).reduce((i,y)=>i+y.length,0);e("the hand-rolled rows are GONE — the count is zero",w===0,`${w}`),e("…and no file is left hand-rolling one",Object.keys(I).length===0,Object.keys(I).join(" · ")),e(`…and the rulings are exactly ${o.rulings} (a new one is argued in review, not added)`,a.length===o.rulings&&a.every(i=>i.why.trim().length>40),`${a.length}`),e("…and BlogSource is not among them, because its row is gone",!a.some(i=>i.file.endsWith("BlogSource.tsx")))}{const t=Object.values(S).reduce((n,c)=>n+c,0);if(e("every builder is finished — nothing left to count",t===0,`${t}`),e("…and the emptiness is because FOUR builders are done, not because the list was cleared",E.length===8&&["textList","segmented","select","slider"].every(n=>E.includes(n)),E.join(" · ")),e("…and a finished builder has left the list",E.every(n=>!(n in S)),E.join(" · ")),r){const n=Object.keys(S).filter(c=>!new RegExp(`(export )?function ${c}\\(`).test(r.text));e(`…and every builder it names is really declared in ${L}`,n.length===0,n.join(" · "))}}if(r){const t=r.text,n=t.indexOf("export function backgroundField("),c=n===-1?"":t.slice(n),u=c.slice(0,c.indexOf(`
|
|
17
|
+
}
|
|
18
|
+
`));e("backgroundField is declared",n!==-1),e("backgroundField renders SettingsField",/<SettingsField/.test(u)),e("…and hand-rolls no row of its own",u.length>0&&!/className="sf-field"/.test(u)),e("…and passes `BackgroundIcon`, not merely some glyph",/icon=\{<BackgroundIcon \/>\}/.test(u));const l=t.indexOf("export function cardImageBackgroundField("),w=l===-1?"":t.slice(l);e("…and cardImageBackgroundField still passes `GroundIcon`",/icon=\{<GroundIcon \/>\}/.test(w.slice(0,w.indexOf(`
|
|
19
|
+
}
|
|
20
|
+
`))))}{const{seen:t,bare:n}=M(x);e(`the hand-rolled row scan read something (${t} hand-drawn SettingsField rows across ${x.length} files, at least ${f.handDrawn})`,t>=f.handDrawn),e("no hand-rolled SettingsField row goes without a glyph",n.length===0,`
|
|
21
|
+
${n.join(`
|
|
22
|
+
`)}`)}return g()}export{L as CONTROLS_FILE,W as callParen,K as panelRowsUseTheSharedRow,A as secondArg};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type PartSliderFillsRowOptions = {
|
|
3
|
+
readonly partPanel?: string;
|
|
4
|
+
readonly sheet?: string;
|
|
5
|
+
readonly shellSheet?: SourceFile;
|
|
6
|
+
readonly hosts: readonly string[];
|
|
7
|
+
readonly renderedHosts?: Readonly<Record<string, string>>;
|
|
8
|
+
readonly minFiles: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function reachesSlider(selector: string): boolean;
|
|
11
|
+
export declare function partSliderFillsRow(files: readonly SourceFile[], options: PartSliderFillsRowOptions): RuleReport;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import{DEFAULT_PANEL_W as k,MAX_PANEL_W as N,MIN_PANEL_W as y}from"../../components/panel-widths.js";import{isRuleSource as A,ruleChecks as F}from"./rule.js";import{parseRules as w}from"./support/css-box-model.js";import{readSliderCallSites as E,sliderFields as v}from"./support/part-slider-call-sites.js";const m=[["sf-editor","es-editor"],["sf-panel","sf-panel--right"],["sf-insp-body","sf-scroll"],["sf-part-panel"],["es-group"],["es-group-body"],["es-field","sf-part-field"],["es-field-ctl"]],_=["sf-slider","has-notch"],S=129;function $(r){const e=r.trim();if(!e.startsWith("."))return null;const s=e.split(".").filter(Boolean);return s.every(o=>/^[a-zA-Z0-9_-]+$/.test(o))?s:null}function c(r){if(/[+~:]/.test(r))return!1;const e=r.trim().split(/\s*(>)\s*|\s+/).filter(Boolean);if(!e.length)return!1;const s=$(e[e.length-1]);if(!s||!s.every(a=>_.includes(a)))return!1;let o=e.length-2,d=m.length-1,i=!1;for(;o>=0;){const a=e[o];if(a===">"){i=!0,o-=1;continue}const f=$(a);if(!f)return!1;let t=-1;for(let n=d;n>=0;n-=1){if(f.every(l=>m[n].includes(l))){t=n;break}if(i)break}if(t===-1)return!1;d=t-1,i=!1,o-=1}return!0}function W(r){const e=r.trim();if(e==="none")return 0;if(e==="auto"||e==="initial")return e==="auto"?1:0;const s=Number.parseFloat(e);return Number.isFinite(s)?s:0}function P(r){const e={grow:0,width:"auto",from:[]};for(const s of r)for(const o of w(s.css)){if(!o.selector.split(",").some(i=>c(i)))continue;let d=!1;for(const[i,a]of o.declarations)i==="flex"?(e.grow=W(a),d=!0):i==="flex-grow"?(e.grow=Number.parseFloat(a)||0,d=!0):i==="width"&&(e.width=a.trim(),d=!0);d&&e.from.push(`${s.name} ${o.selector}`)}return e}function x(r,e){return r.grow>=1||/^100%$/.test(r.width)?e:S}function j(r){return r.filter(e=>{const s=e.text.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/[^\n]*/g,"");return/SettingsField/.test(s)&&/<Slider[\s/>]/.test(s)}).map(e=>e.path).sort()}function I(r,e){const{check:s,report:o}=F("part slider fills row"),d=new Map(r.map(t=>[t.path,t]));if(s("the walk reaches the slot's own slider, and not the notch, a hover or another panel's row",c(".es-field-ctl > .sf-slider")&&c(".sf-insp-body .sf-slider")&&!c(".sf-slider.has-notch::before")&&!c(".es-field-ctl > .sf-slider:hover")&&!c(".sf-field .sf-slider")&&!c(".es-field-ctl > div > .sf-slider")),e.partPanel!==void 0){const t=d.get(e.partPanel);let n=null,l="";try{n=t?E(t.text):null}catch(u){l=u instanceof Error?u.message:String(u)}s(`read the part panel ${e.partPanel}`,!!t&&n!==null,l||"the file is not among those handed in"),s("§1 the part panel draws no slider since plan S2",n!==null&&n.length===0,`${e.partPanel} draws ${n?.length??"?"} slider(s) again — fields ${v(n??[]).join(", ")}. Since plan S2 its numbers are the text box's cells; if a slider is meant to come back, it comes back into the hosts and this section moves with it.`)}if(e.shellSheet!==void 0){const t=w(e.shellSheet.text).filter(n=>n.selector.trim()===".es-field-ctl");s(`§2 ${e.shellSheet.path} declares the control slot \`.es-field-ctl\``,t.length>0,"that is the slot `SettingsField` stacks a control into, and this rule is about how an item sizes inside it"),s("§2 the shell's control slot is still a flex row",t.length>0&&t[t.length-1].declarations.get("display")==="flex","`.es-field-ctl` is no longer `display: flex` in the installed shell. How a flex ITEM sizes is then not the question, and this rule would be answering the old one. Re-read the shell before trusting a verdict from here.")}if(e.sheet!==void 0){const t=d.get(e.sheet);s(`read the stylesheet ${e.sheet}`,!!t),s("§3 the installed shell stylesheet was handed in beside it",e.shellSheet!==void 0);const n=[...t?[{name:t.path,css:t.text}]:[],...e.shellSheet?[{name:e.shellSheet.path,css:e.shellSheet.text}]:[]],l=P(n),u=[{panel:y,content:192,what:"MIN_PANEL_W"},{panel:k.right,content:280,what:"the default"},{panel:N,content:520,what:"MAX_PANEL_W"}],b=u.map(h=>{const p=x(l,h.content),g=h.content-p;return` panel ${String(h.panel).padStart(3)} (${h.what.padEnd(11)}) field content ${String(h.content).padStart(3)} track ${String(p).padStart(3)} `+(g>0?`SHORT BY ${g} — ${Math.round(p/h.content*100)}% of the row`:"fills")}).join(`
|
|
2
|
+
`);for(const h of u)s(`§3 the stacked slider fills its row at ${h.what} (field content ${h.content}px)`,!!t&&x(l,h.content)===h.content,`the shell's stacked sliders do not fill their row.
|
|
3
|
+
|
|
4
|
+
${e.hosts.length} host(s) — ${e.hosts.join(", ")} — draw the shared \`Slider\` inside
|
|
5
|
+
\`SettingsField\`, whose control slot \`.es-field-ctl\` is \`display: flex\`. Nothing in ${e.sheet} makes \`.sf-slider\` take
|
|
6
|
+
that row, so it is \`flex: 0 1 auto\` and collapses to the UA's own ${S}px range input — the
|
|
7
|
+
same width whatever the panel is dragged to:
|
|
8
|
+
|
|
9
|
+
${b}
|
|
10
|
+
|
|
11
|
+
Resolved on \`.sf-slider\` in that slot: flex-grow ${l.grow}, width ${l.width}`+(l.from.length?`
|
|
12
|
+
from: ${l.from.join(`
|
|
13
|
+
`)}`:`
|
|
14
|
+
from: (no rule reaches it)`)+`
|
|
15
|
+
|
|
16
|
+
The shell asks for this in its own words (src/panel/SettingsField.tsx):
|
|
17
|
+
"The row is stacked rather than side-by-side so the control gets the panel's full
|
|
18
|
+
width — a 308px panel cannot afford a label column AND a usable slider."`)}const i=r.filter(t=>/\.tsx?$/.test(t.path)&&!A(t.path)&&(t.path.startsWith("components/")||t.path.startsWith("lib/")));s(`read the source (${i.length} .ts/.tsx files under components/ and lib/, at least ${e.minFiles})`,i.length>=e.minFiles);const a=j(i),f=[...e.hosts].sort();s(`§4 the files that draw a \`Slider\` inside a \`SettingsField\` are the ${f.length} known host(s)`,a.join(`
|
|
19
|
+
`)===f.join(`
|
|
20
|
+
`),`the set has changed.
|
|
21
|
+
|
|
22
|
+
known: ${f.join(", ")||"(none)"}
|
|
23
|
+
found: ${a.join(", ")||"(none)"}
|
|
24
|
+
|
|
25
|
+
A NEW one is a row \`.es-field-ctl > .sf-slider\` now fills and nothing here watches — add it to the hosts
|
|
26
|
+
(and render it for the direct-child check). One REMOVED is a guard pointing at nothing.`);for(const[t,n]of Object.entries(e.renderedHosts??{})){const l=/class="es-field-ctl"><([a-z]+) class="([^"]*)"/.exec(n);s(`§4 ${t}: the rendered row has a \`.es-field-ctl\` slot with an element child`,l!==null,"has it stopped going through `SettingsField`?"),l&&s(`§4 ${t}: the slot's first child is the \`.sf-slider\` itself`,l[2].split(/\s+/).includes("sf-slider"),`the control slot's first child is <${l[1]} class="${l[2]}">. Something wraps the slider, so
|
|
27
|
+
\`.es-field-ctl > .sf-slider\` no longer reaches it and that row is back to the UA's 129px — the wrapper is the
|
|
28
|
+
flex item now. Widening the selector to a descendant would NOT fix it; it would paint \`flex: 1\` on an element
|
|
29
|
+
whose parent is the wrapper and leave the measurement unchanged. Card 66247.`)}return o()}export{I as partSliderFillsRow,c as reachesSlider};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type PartStyleNamingOptions = {
|
|
3
|
+
readonly roleSheet?: string;
|
|
4
|
+
readonly panel?: string;
|
|
5
|
+
readonly bridge?: string;
|
|
6
|
+
readonly minSites: number;
|
|
7
|
+
readonly minWithScale: number;
|
|
8
|
+
readonly minWithoutScale: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function partStyleNaming(files: readonly SourceFile[], options: PartStyleNamingOptions): RuleReport;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{fontRoleOfClassName as y,FONT_ROLE_LABELS as x}from"../fonts.js";import{isRuleSource as w,ruleChecks as E}from"./rule.js";import{enclosingLiteral as N}from"./heading-ceiling-sweep.js";import{stripComments as b}from"./support/ts-source.js";import{typeScaleGroupOfClassName as u}from"../theme.js";const R=["app/","components/","lib/"],p={"font-display":"heading","font-heading2":"heading2","font-heading3":"heading3","font-body":"body","font-serif":"accent","font-mono":"label"},k={heading:"--font-role-heading",heading2:"--font-role-heading2",heading3:"--font-role-heading3",body:"--font-role-body",accent:"--font-role-accent",label:"--font-role-label"},$=[];function L(m){const n=[],t=new Set;for(const d of m){const c=b(d.text),l=/\bfont-(?:display|body|serif|mono)\b/g;let r;for(;r=l.exec(c);){const i=N(c,r.index);if(!i)continue;const o=c.slice(0,r.index).split(`
|
|
2
|
+
`).length,h=`${d.path}:${o}:${i}`;if(t.has(h))continue;t.add(h);const g=i.split(/\s+/).filter(a=>!a.slice(0,a.lastIndexOf(":")+1).includes("&")).map(a=>a.slice(a.lastIndexOf(":")+1)),f=Object.keys(p).filter(a=>g.includes(a));f.length!==0&&n.push({file:d.path,line:o,className:i,utilities:f})}}return n}function I(m,n){const{check:t,report:d}=E("part style naming"),c=m.filter(e=>/\.tsx?$/.test(e.path)&&!w(e.path)&&R.some(s=>e.path.startsWith(s))),l=L(c),r=new Map(m.map(e=>[e.path,e]));t(`read the source (${c.length} .ts/.tsx files)`,c.length>0,"found no source to sweep — this rule is not measuring anything"),t(`the scanner finds face-tagged class lists at all (${l.length}, at least ${n.minSites})`,l.length>=n.minSites);const i=l.filter(e=>u(e.className)!==null),o=l.filter(e=>u(e.className)===null);t(`…both taggings are represented, so neither branch below is untested (${i.length} with a scale token, at least ${n.minWithScale})`,i.length>=n.minWithScale&&i.length>0),t(`…and the face-without-a-size-knob population is real, not an artefact (${o.length}, at least ${n.minWithoutScale})`,o.length>=n.minWithoutScale&&o.length>0,`${o.length} class lists carry a face utility and no matching scale token`);const h=l.filter(e=>y(e.className)!==p[e.utilities[0]]);if(t("every class list in the census reads back as its own utility's role",h.length===0,h.slice(0,5).map(e=>`${e.file}:${e.line} has ${e.utilities[0]} but reads ${y(e.className)}`).join("; ")),n.roleSheet!==void 0){const e=r.get(n.roleSheet),s=Object.values(k).filter(O=>!e||!e.text.includes(O));t(`…and every role named here is a custom property ${n.roleSheet} defines`,!!e&&s.length===0,e?s.join(", "):"the sheet was not read")}const g=Object.values(p).filter(e=>!(e in x));t("every face utility names a role the panel offers",g.length===0,g.join(", "));const f=Object.keys(x).filter(e=>!Object.values(p).includes(e));t("…and the roles no utility can name are exactly the ones decided to have none",f.join()===$.join(),`no utility reaches: [${f}] — expected [${$}]`);const a=o.filter(e=>u(e.className)!==null);t("no class list without a scale token claims a size group",a.length===0,a.slice(0,5).map(e=>`${e.file}:${e.line}`).join("; ")),t("the reported case: a `font-display text-[22px]` heading is Headings, size group none",y("font-display text-[22px] font-medium")==="heading"&&u("font-display text-[22px] font-medium")===null),t("…and genuinely body-tagged copy still reads as the body size group",u("text-[var(--size-body,16px)]")==="body");const S=l.filter(e=>e.utilities.length>1);if(t("every class list names at most one face",S.length===0,S.slice(0,5).map(e=>`${e.file}:${e.line} — ${e.utilities.join(" + ")}`).join("; ")),n.panel!==void 0){const e=r.get(n.panel),s=e?b(e.text):"";t(`read the part panel ${n.panel}`,!!e),t("the box's name comes from the FACE, not from a size group",s.includes("FONT_ROLE_LABELS[face.role]")&&!s.includes("TYPE_GROUP_LABELS[")),t("…a face the page does not draw as its declared role is called Custom",/return\s*"Custom"/.test(s)&&s.includes('kind: "custom"')),t("…and a part no size knob reaches says so under the text box",/sizeGroup === null && \(\s*<p className="es-field-hint sf-hint-keep">Fixed size/.test(s))}if(n.bridge!==void 0){const e=r.get(n.bridge),s=e?b(e.text):"";t(`read the preview's part bridge ${n.bridge}`,!!e),t("the preview publishes BOTH tags, each walked up to its own nearest declarer",s.includes("fontRoleOfClassName")&&s.includes("typeScaleGroupOfClassName")&&s.includes("el.parentElement")),t("…and the element it reads from is one that declares EITHER tag",/partStyleSource\([\s\S]{0,200}fontRoleOfClassName[\s\S]{0,120}typeScaleGroupOfClassName/.test(s))}return d()}export{I as partStyleNaming};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type PuckDocumentMount = {
|
|
3
|
+
readonly migratedBy: string | null;
|
|
4
|
+
readonly why: string;
|
|
5
|
+
};
|
|
6
|
+
export type PuckDocumentSurfacesOptions = {
|
|
7
|
+
readonly mounts: Readonly<Record<string, PuckDocumentMount>>;
|
|
8
|
+
readonly migrator?: string;
|
|
9
|
+
readonly handedMigrator?: {
|
|
10
|
+
readonly rules: string;
|
|
11
|
+
readonly entry: string;
|
|
12
|
+
};
|
|
13
|
+
readonly storedFloor: number;
|
|
14
|
+
readonly minFiles: number;
|
|
15
|
+
};
|
|
16
|
+
export declare function puckDocumentSurfaces(files: readonly SourceFile[], options: PuckDocumentSurfacesOptions): RuleReport;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{ruleChecks as P}from"./rule.js";const o=/<(Render|Puck|SiteEditorProvider|EditorStoreProvider|ScreenStoreProvider)\b[^<>]*\bdata=\{/,m=/migrateRenamedProps\s*\(/,g=/export function migrateRenamedProps\b/;function S(l,r){const{check:t,report:p}=P("puck document surfaces"),a=e=>l.find(s=>s.path===e)?.text;{const e=n=>`<${n}`;t("a mount is found",o.test(`${e("Render")} config={c} data={doc} />`)),t("…across lines",o.test(`${e("SiteEditorProvider")}
|
|
2
|
+
config={c}
|
|
3
|
+
data={initial}
|
|
4
|
+
>`)),t("…and the screen's store provider is one under either of its names",["Editor","Screen"].every(n=>o.test(`${e(`${n}StoreProvider`)} config={c} data={doc}>`))),t("…and a tag named in prose is not one",!o.test(`the ${e("Puck")}> mounts, then its data={x} is read`)),t("…nor the composition API",!o.test(`${e("Puck.Preview")} />`));const s="migrateRenamedProps";t("a call is found",m.test(`const d = ${s}(stored);`)),t("…and a mention is not one",!m.test(`// runs ${s} first`)),t("the declaration is found, generic as it is",g.test(`export function ${s}<T>(data: T): T {`))}const d=l.filter(e=>/\.tsx?$/.test(e.path));t(`the source scan reached the app (${d.length} TS/TSX files, at least ${r.minFiles})`,d.length>=r.minFiles);const c=d.filter(e=>o.test(e.text)).map(e=>e.path).sort(),$=Object.keys(r.mounts).sort(),u=c.filter(e=>!(e in r.mounts)),f=$.filter(e=>!c.includes(e));t(`every mount is declared (${c.length} found) — a NEW one must say whether its document comes from stored props (migrate it) or is a mock (say so)`,u.length===0,u.map(e=>`UNDECLARED: ${e}`).join(`
|
|
5
|
+
`)),t("every declared mount is a real mount — a stale row would shelter a surface that later stops migrating",f.length===0,f.map(e=>`NOT FOUND: ${e}`).join(`
|
|
6
|
+
`));const i=Object.entries(r.mounts).filter(([,e])=>e.migratedBy!==null),h=i.filter(([,e])=>!m.test(a(e.migratedBy)??""));if(t(`SURFACES=${i.length} — every mount fed from STORED props runs migrateRenamedProps, so a renamed prop key cannot silently reset a live section`,h.length===0,h.map(([e,s])=>`${e}
|
|
7
|
+
→ ${s.migratedBy} never calls migrateRenamedProps(`).join(`
|
|
8
|
+
`)),r.handedMigrator){const{rules:e,entry:s}=r.handedMigrator,n=a(e)??"";t(`${e} carries the REAL migrateRenamedProps, not a stand-in`,/import \{ migrateRenamedProps \} from "@\/lib\/puck\/renamed-props";/.test(n)&&/^\s*migrateRenamedProps,$/m.test(n)),t(`…${s} passes those rules to the screen's mount`,/<(?:EditorScreenMount|ScreenMount)\b[^>]*addressing=\{WEBSITE_ADDRESSING\}/.test(a(s)??""))}return t(`at least ${r.storedFloor} mount(s) fed from stored props (found ${i.length})`,i.length>=r.storedFloor,"if this drops, a surface was reclassified as a mock rather than fixed"),r.migrator&&t(`${r.migrator} still exports migrateRenamedProps — the function §2 hunts calls of is live`,g.test(a(r.migrator)??"")),p()}export{S as puckDocumentSurfaces};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type ReducedMotionOptions = {
|
|
3
|
+
readonly globalSheet?: string;
|
|
4
|
+
readonly stylesheets?: readonly string[];
|
|
5
|
+
readonly editorOnly?: Readonly<Record<string, string>>;
|
|
6
|
+
readonly askingSites?: Readonly<Record<string, number>>;
|
|
7
|
+
readonly hook?: string;
|
|
8
|
+
readonly minScripts: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const MOVING_PROPERTIES: string[];
|
|
11
|
+
export declare const REQUIRED_FADES: string[];
|
|
12
|
+
export declare const HARD_CODED_SMOOTH: RegExp;
|
|
13
|
+
export interface ReduceBlock {
|
|
14
|
+
body: string;
|
|
15
|
+
selector: string;
|
|
16
|
+
decls: string;
|
|
17
|
+
unlayered: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function readReduceBlock(css: string): ReduceBlock | null;
|
|
20
|
+
export declare function valueOf(decls: string, property: string): string | null;
|
|
21
|
+
export declare function hardCodedSmoothScrolls(file: SourceFile): {
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
text: string;
|
|
25
|
+
}[];
|
|
26
|
+
export declare function reducedMotion(files: readonly SourceFile[], options: ReducedMotionOptions): RuleReport;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{isRuleSource as C,ruleChecks as I}from"./rule.js";import{stripCommentsAndStrings as O}from"./support/css-source.js";import{stripComments as S}from"./support/ts-source.js";const R=["all","transform","translate","rotate","scale","top","right","bottom","left","inset","margin","padding","width","height","max-height","max-width","min-height","min-width","gap","flex","flex-basis","grid-template-columns","grid-template-rows","background-position","object-position","offset-distance"],D=["opacity","color","background-color","border-color"],x=/\bbehaviou?r\s*:\s*["'`]smooth["'`]/,N=/\b(transition|animation)[a-z-]*\s*:[^;]*!important/i;function $(n){const o=n.search(/@media[^{]*prefers-reduced-motion\s*:\s*reduce/);if(o<0)return null;let e=0;const d=[];let r="";for(let c=0;c<o;c++){const t=n[c];if(t==="{"){const i=/^@layer\b/.test(r.trim());d.push(i),i&&e++,r=""}else t==="}"?(d.pop()&&e--,r=""):t===";"?r="":r+=t}const m=n.indexOf("{",o);if(m<0)return null;let u=0,g=-1;for(let c=m;c<n.length;c++)if(n[c]==="{")u++;else if(n[c]==="}"&&--u===0){g=c;break}if(g<0)return null;const s=n.slice(m+1,g),y=s.indexOf("{"),b=s.lastIndexOf("}");return y<0||b<0?null:{body:s,selector:s.slice(0,y).trim().replace(/\s+/g," "),decls:s.slice(y+1,b),unlayered:e===0}}function p(n,o){const e=new RegExp(`(?:^|;)\\s*${o}\\s*:([^;]*)`,"i"),d=n.match(e);return d?d[1].replace(/!important/i,"").trim().replace(/\s+/g," "):null}function j(n){const o=S(n.text),e=[];for(const[d,r]of o.split(`
|
|
2
|
+
`).entries())x.test(r)&&e.push({file:n.path,line:d+1,text:r.trim()});return e}function B(n,o){const e=O(n.text),d=o?e.replace(o,""):e,r=[];for(const[m,u]of d.split(`
|
|
3
|
+
`).entries())N.test(u)&&r.push(`${n.path}:${m+1} ${u.trim()}`);return r}function L(n,o){const{check:e,report:d}=I("reduced motion"),r=o.editorOnly??{},m=new Map(n.map(t=>[t.path,t]));{e("a rule that exists only in a comment is NOT counted",$(O(`/* the site should have a @media (prefers-reduced-motion: reduce) rule */
|
|
4
|
+
a { color: red; }`))===null);const i="@media (prefers-reduced-motion: reduce) { * { animation-duration: 1ms !important; } }";e("the layer check fails on a layered copy and passes on an unlayered one",$(i)?.unlayered===!0&&$(`@layer base { ${i} }`)?.unlayered===!1&&$(`@layer base { a { color: red; } } ${i}`)?.unlayered===!0),e("valueOf reads a declaration and drops !important",p("animation-duration: 1ms !important;","animation-duration")==="1ms"&&p("transition-property: opacity, color !important;","transition-property")==="opacity, color"&&p("animation-duration: 1ms;","animation")===null&&p("scroll-behavior: auto;","animation-delay")===null);const a=f=>(f??"").split(",").map(v=>v.trim()),h=a(p("transition-property: opacity, transform !important;","transition-property")),w=a(p("transition-property: opacity, color !important;","transition-property"));e("the moving-property check fails on a copy that lets transform through, and passes a fade-only list",h.some(f=>R.includes(f))&&!w.some(f=>R.includes(f))),e("the !important scan flags a motion declaration and not one in a comment",B({path:"fixture.css",text:`/* transition: x !important */
|
|
5
|
+
a { transition: opacity 1s !important; }`},null).length===1),e("the smooth-scroll regex bites, both ways",x.test('el.scrollIntoView({ behavior: "smooth" })')&&x.test("window.scrollTo({ top: 0, behavior: 'smooth' })")&&!x.test("el.scrollIntoView({ behavior: scrollBehavior() })")&&!x.test('window.scrollTo({ top: 0, behavior: "auto" })')),e("a commented example is not a breach",j({path:"fixture.ts",text:`// behavior: "smooth" in a comment
|
|
6
|
+
const a = 1;`}).length===0)}const u=n.filter(t=>t.path.endsWith(".css")),g=n.filter(t=>/\.tsx?$/.test(t.path)&&!t.path.endsWith(".d.ts")&&!C(t.path));let s=null;if(o.globalSheet!==void 0){const t=m.get(o.globalSheet),i=t?O(t.text):"";s=$(i);const a=o.globalSheet;e(`${a} was actually read`,i.length>1e4,`expected the real stylesheet, got ${i.length} chars`),e(`${a} has a real prefers-reduced-motion rule, not just prose about one`,s!==null&&s.decls.trim().length>0,"no `@media (prefers-reduced-motion: reduce)` block once comments are stripped, or it is empty. Before this rule landed the sheet's ONLY match for that string was a comment, which is the state this asserts is over."),e("the rule is UNLAYERED, so Tailwind's utilities cannot outrank it",s?.unlayered===!0,"the reduced-motion rule sits inside an @layer. Tailwind's utilities live in a later layer, so every `transition-*` class would beat it and the site would move for everyone, with nothing to show for it.");const h=s?.selector??"";e("the rule applies to every element and both generated boxes",/\*/.test(h)&&/::before/.test(h)&&/::after/.test(h),`selector \`${h}\` — it must start from \`*\` (a narrower one covers only what somebody remembered) and cover \`::before\` and \`::after\`: a generated box animates like any other`);const w=(s?.decls??"").split(";").map(l=>l.trim()).filter(l=>l.length>0),f=w.filter(l=>!/!important\s*$/i.test(l)).map(l=>l.split(":")[0].trim());e("every declaration carries !important",w.length>=4&&f.length===0,`${w.length} declaration(s), without !important: ${f.join(", ")||"(none)"}. Being unlayered beats Tailwind, but not the unlayered class rules in the same sheet and not an inline style.`);const v=p(s?.decls??"","transition-property"),E=(v??"").split(",").map(l=>l.trim().toLowerCase()),T=E.filter(l=>R.includes(l));e("the transition allow-list lets nothing move",v!==null&&T.length===0,v===null?"the rule must set `transition-property` — that is how movement is sorted from fades":`movement is back on the allow-list: ${T.join(", ")}. Anything on that list still animates for a visitor who asked for no motion.`);const M=D.filter(l=>!E.includes(l));e("the transition allow-list still lets fades through",M.length===0,`\`${M.join("`, `")}\` must stay transitionable. A fade carries no movement, helps people follow what changed, and harms nobody — dropping it is the crude rule this one was written instead of.`);const k=p(s?.decls??"","animation-duration");e("animations are collapsed to a short NON-ZERO time",k!==null&&/^(0?\.\d+|[1-9]\d*)m?s$/.test(k),`animation-duration is \`${k}\`; the rule must cap it (CSS cannot sort a keyframe list by what it moves), and exactly 0s stops \`animationend\` firing in some engines and hangs any component waiting on it.`),e("a loop that still repeats is still a loop: animation-iteration-count is 1",p(s?.decls??"","animation-iteration-count")==="1"),e("animations are not collapsed with `animation: none`, which can hide content",p(s?.decls??"","animation")===null,"an element whose static styles are invisible, made visible by a `forwards` fill, would never appear. Run the animation to its end instead."),e("a scroll the visitor did not ask for is movement too: scroll-behavior is auto",p(s?.decls??"","scroll-behavior")==="auto","the sheet sets `scroll-behavior: smooth` on html inside @layer base; this must turn it off."),e("the escape hatch exists and is spelled the one way",/\[data-motion-keep\]/.test(h),"the exemption attribute must appear in the selector, so one grep finds every exception.")}for(const t of o.stylesheets??[])e(`read the stylesheet ${t}`,m.has(t)&&t.endsWith(".css"));const y=u.flatMap(t=>B(t,t.path===o.globalSheet&&s?s.body:null));e(`nothing in the stylesheets can outrank the rule (${u.length} sheet(s))`,u.length>0&&y.length===0,u.length===0?"read no stylesheet — this check is not measuring anything":`!important motion declaration(s) outside the reduced-motion rule:
|
|
7
|
+
`+y.join(`
|
|
8
|
+
`)),e(`read the scripts (${g.length} .ts/.tsx files, at least ${o.minScripts})`,g.length>=o.minScripts);const b=g.flatMap(j),c=b.filter(t=>!(t.file in r));e("no shopper-facing code hard-codes a smooth scroll",c.length===0,'an explicit `behavior: "smooth"` beats the stylesheet, so this scroll animates for a visitor who asked for no motion:\n '+c.map(t=>`${t.file}:${t.line} ${t.text}`).join(`
|
|
9
|
+
`)+"\n Call `scrollBehavior()` from components/useReducedMotion.ts instead. It still scrolls — it just arrives instead of travelling.");for(const[t,i]of Object.entries(r)){const a=m.get(t);e(`the editor waiver is still real: ${t} (${i})`,!!a&&/editor/i.test(a.text.slice(0,400))&&b.some(h=>h.file===t),a?"its own opening no longer says it is the editor's, or it no longer holds the literal — the waiver covers nothing":"the file is gone — remove the stale entry")}for(const[t,i]of Object.entries(o.askingSites??{})){const a=m.get(t),h=a?(S(a.text).match(/behaviou?r:\s*scrollBehavior\(\)/g)??[]).length:-1;e(`${t} still asks scrollBehavior() ${i} time(s)`,h===i,a?`found ${h}. A shopper-facing scroll stopped asking — if a site was deliberately removed, change the number and say which.`:"the file is gone")}if(o.hook!==void 0){const t=m.get(o.hook),i=t?S(t.text):"";e(`${o.hook} exports scrollBehavior()`,/export function scrollBehavior\(\)/.test(i)),e(`${o.hook} names the media query once and shares it`,/export const REDUCED_MOTION_QUERY/.test(i),"the media query must be named once and shared, not typed out per reader");const a=(i.match(/prefers-reduced-motion/g)??[]).length;e(`the query string appears exactly once in ${o.hook}`,a===1,`it appears ${a} times. Two spellings of the same question can disagree; there must be exactly one.`)}return d()}export{x as HARD_CODED_SMOOTH,R as MOVING_PROPERTIES,D as REQUIRED_FADES,j as hardCodedSmoothScrolls,$ as readReduceBlock,L as reducedMotion,p as valueOf};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type NamedCap = {
|
|
3
|
+
readonly file: string;
|
|
4
|
+
readonly cls: string;
|
|
5
|
+
readonly why: string;
|
|
6
|
+
};
|
|
7
|
+
export type MovedColumn = {
|
|
8
|
+
readonly file: string;
|
|
9
|
+
readonly from: number;
|
|
10
|
+
readonly moved: number;
|
|
11
|
+
readonly caps: number;
|
|
12
|
+
readonly what: string;
|
|
13
|
+
readonly cap?: "site" | "chrome";
|
|
14
|
+
};
|
|
15
|
+
export type RouteContentWidthOptions = {
|
|
16
|
+
readonly measures?: readonly NamedCap[];
|
|
17
|
+
readonly ownedElsewhere?: readonly NamedCap[];
|
|
18
|
+
readonly moved?: readonly MovedColumn[];
|
|
19
|
+
readonly movedColumns?: number;
|
|
20
|
+
readonly minFiles: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function routeContentWidth(files: readonly SourceFile[], options: RouteContentWidthOptions): RuleReport;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{CHROME_WIDTH_CAP_CLASS as v,PAGE_WIDTH as o,SITE_WIDTH_CAP_CLASS as u,SITE_WIDTH_VAR as $}from"../content/page-width.js";import{ruleChecks as C}from"./rule.js";const i=/max-w-\[1\d{3}px\]/,f=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),E=s=>/^\s*(\/\/|\*|\/\*)/.test(s);function x(s){const n=[];return s.text.split(`
|
|
2
|
+
`).forEach((t,h)=>{if(!E(t))for(const r of t.matchAll(new RegExp(i,"g")))n.push({file:s.path,line:h+1,cls:r[0]})}),n}const b=s=>s.cap==="chrome"?v:u;function S(s,n){const{check:t,report:h}=C("route content width"),r=n.measures??[],g=[...r,...n.ownedElsewhere??[]],m=n.moved??[];t("a site's width starts at 1000px",o.min===1e3,`PAGE_WIDTH.min is ${o.min}`),t("the band holds the narrowest site width",i.test(`max-w-[${o.min}px]`)),t("the band holds the built-in site width",i.test(`max-w-[${o.default}px]`)),t("a cap below the band is never looked at",!i.test("max-w-[999px]")&&!i.test("max-w-[680px]")),t("the replacement class is not itself a literal in the band",!i.test(u));{const e=x({path:"fixture.tsx",text:["// a doc line naming max-w-[1100px] is not a use",'<div className="mx-auto max-w-[1100px]" />'].join(`
|
|
3
|
+
`)});t("the reader finds a cap in code and skips a comment line",e.length===1&&e[0].line===2,JSON.stringify(e))}const c=s.filter(e=>/\.tsx?$/.test(e.path));t(`read the source (${c.length} TS/TSX files, at least ${n.minFiles})`,c.length>=n.minFiles,"fewer means the walk broke, and the census below is vacuous");const d=c.flatMap(x).filter(e=>!g.some(a=>a.file===e.file&&a.cls===e.cls));t("no route layout types its own content column (no literal cap in the 1000–1999px band)",d.length===0,`${d.length} cap(s) in the band are a hand-typed content column. Each should read ${u} — or, if it is a reading measure, be named as one with the reason it is one:
|
|
4
|
+
`+d.map(e=>`${e.file}:${e.line} ${e.cls}`).join(`
|
|
5
|
+
`));const p=new Map(c.map(e=>[e.path,e.text]));for(const e of r)t(`the measure is still there: ${e.file} ${e.cls}`,new RegExp(f(e.cls)).test(p.get(e.file)??""),"delete that measure rather than leaving a hole the next hard-coded column can sit in"),t(`…and it carries its reason: ${e.file} ${e.cls}`,e.why.trim().length>40,"a measure needs its reason");for(const e of m){const a=p.get(e.file);if(!t(`read the moved column's file ${e.file} (${e.what})`,a!==void 0,"the file is gone — delete its row"))continue;const l=b(e);t(`${e.file} (${e.what}) declares a cap that reads ${$}`,l.includes($),l);const w=[...a.matchAll(new RegExp(f(l),"g"))].length;t(`${e.file} (${e.what}) carries ${e.caps} ${e.cap==="chrome"?"chrome":"site"} width cap(s)`,w===e.caps,`found ${w}`),t(`${e.file} (${e.what}) no longer types its old ${e.from}px column`,!new RegExp(f(`max-w-[${e.from}px]`)).test(a)),t(`${e.file} (${e.what}) gets wider at the default width, never narrower`,o.default>e.from,`it would NARROW by ${e.from-o.default}px`)}if(n.movedColumns!==void 0){const e=m.reduce((a,l)=>a+l.moved,0);t(`the moved list and the census agree: ${n.movedColumns} column(s) moved across ${m.length} file(s)`,e===n.movedColumns,`the rows add up to ${e} — a column moved without a row stops the report of what a merchant will SEE change matching the code`)}return h()}export{S as routeContentWidth};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SourceFile = {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly text: string;
|
|
4
|
+
};
|
|
5
|
+
export type RuleCheck = {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly ok: boolean;
|
|
8
|
+
readonly detail?: string;
|
|
9
|
+
};
|
|
10
|
+
export type RuleReport = {
|
|
11
|
+
readonly rule: string;
|
|
12
|
+
readonly checks: readonly RuleCheck[];
|
|
13
|
+
};
|
|
14
|
+
export declare const RULES_DIR = "lib/source-rules/";
|
|
15
|
+
export declare function isRuleSource(path: string): boolean;
|
|
16
|
+
export declare function ruleChecks(rule: string): {
|
|
17
|
+
check(name: string, ok: boolean, detail?: string): boolean;
|
|
18
|
+
report(): RuleReport;
|
|
19
|
+
};
|
|
20
|
+
export declare function failedChecks(report: RuleReport): readonly RuleCheck[];
|
|
21
|
+
export declare function reportLines(report: RuleReport): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const u="lib/source-rules/";function c(r){return r.startsWith(u)}function s(r){const e=[];return{check(n,t,o){return e.push(t||o===void 0?{name:n,ok:t}:{name:n,ok:t,detail:o}),t},report(){return e.length===0&&e.push({name:`${r} checked nothing`,ok:!1}),{rule:r,checks:e}}}}function i(r){return r.checks.filter(e=>!e.ok)}function h(r){return r.checks.map(e=>e.ok?` ok ${e.name}`:` FAIL ${e.name}${e.detail?` — ${e.detail}`:""}`)}export{u as RULES_DIR,i as failedChecks,c as isRuleSource,h as reportLines,s as ruleChecks};
|