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 @@
|
|
|
1
|
+
import{BOX_TYPE as m}from"../puck/container-blocks.js";import{SLOT_SECTION_SPECS as h}from"../puck/slot-specs.js";import{eyebrowLookFor as a}from"./section-eyebrow-look.js";import{SECTION_HEAD_TYPE as n,SPLITTABLE_SECTIONS as r,canSplitHead as p}from"./section-head-placement.js";import{presetFor as s}from"./section-presets.js";function f(o,{nodeDefaults:i,newNodeId:S,sectionLabel:e,blockIsOffered:c}){const t=s(o);return t?t(i,S):p(o)&&c(m)&&c(n)?{type:m,props:{...i(m),id:S(m),direction:"column",name:`${e(o)} section`,content:[{type:n,props:{...i(n),id:S(n),...r[o]?.headStyle?{headStyle:r[o].headStyle}:{},...r[o]?.headRowGap?{rowGap:r[o].headRowGap}:{},...r[o]?.headDescriptionSize?{descriptionSize:r[o].headDescriptionSize}:{},...a(o)!==a(n)?{eyebrowLook:a(o)}:{}}},{type:o,props:{...i(o),id:S(o),headPlacement:"own-box"}}]}}:h[o]?.fixed===!0?{type:o,props:{...i(o),id:S(o),layout:"boxes"}}:null}export{f as newSectionNode};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseOption as o}from"../parse-option.js";const a="off",l="none",i=[{label:"None",value:"off"},{label:"1, 2, 3",value:"plain"},{label:"01, 02, 03",value:"padded"},{label:"N
|
|
1
|
+
import{parseOption as o}from"../parse-option.js";const a="off",l="none",i=[{label:"None",value:"off"},{label:"1, 2, 3",value:"plain"},{label:"01, 02, 03",value:"padded"},{label:"N°01, N°02",value:"numero"}],c=[{label:"None",value:"none"},{label:"Hairline",value:"hairline"}];function u(e){return o(i,a,e)}function x(e){return o(c,l,e)}const t="sf-card-index",n="sf-card-index",N="pointer-events-none absolute left-[18px] top-[18px] z-10 border border-line bg-bg/70 px-2 py-1 font-mono text-[length:var(--size-label,10px)] uppercase tracking-[0.16em] text-fg-dim",d={plain:`counter(${n})`,padded:`counter(${n}, decimal-leading-zero)`,numero:`"N°" counter(${n}, decimal-leading-zero)`};function f(e,r){return r==="off"?"":`.${e}{counter-reset:${n}}.${e} .${t}{counter-increment:${n}}.${e} .${t}::before{content:${d[r]}}`}function _(e){return e==="hairline"?"border border-line hover:border-accent":""}export{N as CARD_INDEX_BADGE_CLASS,t as CARD_INDEX_CLASS,i as CARD_INDEX_OPTIONS,c as CARD_OUTLINE_OPTIONS,a as DEFAULT_CARD_INDEX_FORMAT,l as DEFAULT_CARD_OUTLINE,f as cardIndexCss,_ as cardOutlineClass,u as parseCardIndexFormat,x as parseCardOutline};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseOption as t}from"../parse-option.js";const l="small",n="dot",s=[{label:"Small",value:"small"},{label:"Display",value:"display"}],o=[{label:"Dot
|
|
1
|
+
import{parseOption as t}from"../parse-option.js";const l="small",n="dot",s=[{label:"Small",value:"small"},{label:"Display",value:"display"}],o=[{label:"Dot ●",value:"dot"},{label:"Cross ✕",value:"cross"},{label:"Slash /",value:"slash"},{label:"Triangle ▼",value:"triangle"},{label:"None",value:"none"}],x={dot:"●",cross:"✕",slash:"/",triangle:"▼",none:""},r={small:"font-mono text-[length:var(--size-label,14px)] uppercase tracking-[0.25em] max-[800px]:text-[length:var(--size-label,12px)]",display:"font-display font-medium text-[length:var(--size-headingMedium,44px)] uppercase leading-none tracking-[0.005em] max-[800px]:text-[length:var(--size-headingMedium,28px)]"},i={small:{dot:"",cross:"text-[0.77em]",slash:"text-[0.77em]",triangle:"",none:""},display:{dot:"text-[0.3em]",cross:"text-[0.64em]",slash:"text-[0.64em]",triangle:"text-[0.3em]",none:""}};function c(e){return t(s,l,e)}function m(e){return t(o,n,e)}function u(e){return r[e]}function T(e,a){return i[e][a]}export{n as DEFAULT_TICKER_SEPARATOR,l as DEFAULT_TICKER_SIZE,x as SEPARATOR_GLYPH,o as TICKER_SEPARATOR_OPTIONS,s as TICKER_SIZE_OPTIONS,m as parseTickerSeparator,c as parseTickerSize,T as separatorScaleClass,u as tickerTypeClass};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export declare const ACCENT_TOKEN_EXTENSIONS: readonly string[];
|
|
3
|
+
export type KeptName = {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly pattern: RegExp;
|
|
6
|
+
readonly why: string;
|
|
7
|
+
};
|
|
8
|
+
export type AccentTokenNameOptions = {
|
|
9
|
+
readonly minFiles: number;
|
|
10
|
+
readonly exempt?: Readonly<Record<string, string>>;
|
|
11
|
+
readonly keptNames?: readonly KeptName[];
|
|
12
|
+
};
|
|
13
|
+
export type AccentTokenViolation = {
|
|
14
|
+
readonly file: string;
|
|
15
|
+
readonly line: number;
|
|
16
|
+
readonly text: string;
|
|
17
|
+
readonly kind: "custom-property" | "utility-class";
|
|
18
|
+
};
|
|
19
|
+
export declare function goldTokenUses(file: SourceFile): AccentTokenViolation[];
|
|
20
|
+
export declare function accentTokenName(files: readonly SourceFile[], options: AccentTokenNameOptions): RuleReport;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{ruleChecks as x}from"./rule.js";import{rootsLongestFirst as w}from"./support/accent-rename-bytes.js";const f=[".ts",".tsx",".css",".mjs",".js"],$=/--[a-z0-9-]*gold[a-z0-9-]*/gi,b=new RegExp(String.raw`(?<![a-z0-9-])`+String.raw`(?:[a-z0-9@_.\-\[\]\/]+:)*`+`(?:${w().join("|")})`+String.raw`-gold(?:-2)?`+String.raw`(?:\/(?:\d{1,3}|\[[^\]\s]*\]))?`+String.raw`(?![a-z0-9-])`,"gi");function p(n){const s=[];return n.text.split(`
|
|
2
|
+
`).forEach((o,h)=>{for(const[c,l]of[[$,"custom-property"],[b,"utility-class"]]){c.lastIndex=0;let i;for(;(i=c.exec(o))!==null;)s.push({file:n.path,line:h+1,text:i[0].trim(),kind:l})}}),s}const u=n=>p({path:"fixture.tsx",text:n});function k(n,s){const{check:o,report:h}=x("accent token name"),c=s.exempt??{};{const e=[["bare token"," --gold: var(--gold-base);"],["second stop"," --gold-2: var(--theme-accent-2);"],["dim variant"," --gold-dim: #9a8050;"],["theme bridge"," --color-gold: var(--gold);"],["var() reference"," outline: 2px solid var(--gold);"],["var() with a fallback",'className="text-[var(--gold,#c8a24c)]"'],["text utility",'<span className="text-gold">'],["bg utility",'<div className="bg-gold px-4">'],["directional border",'className="border-t-2 border-t-gold"'],["opacity modifier",'className="bg-gold/10"'],["variant prefix",'className="hover:text-gold"'],["second-stop utility",'className="bg-gold-2"'],["arbitrary opacity",'className="bg-gold/[0.08]"'],["variant + arbitrary opacity",'className="hover:bg-gold/[0.04]"'],["named group variant",'className="group-hover/kf:text-gold"'],["inside a regex literal","assert.match(html, /whitespace-nowrap text-gold/)"]],t=e.filter(([,r])=>u(r).length===0);o(`the scanner flags every shape the rename must remove (${e.length} shapes)`,t.length===0,`it walked past: ${t.map(([r,m])=>`${r} (${m})`).join("; ")}`);const a=[["the new token"," --accent: var(--accent-base);"],["the new utility",'<span className="text-accent">'],["PayPal's own API value",'style: { color: "gold", label: "paypal" }'],["a product finish name",'{ label: "Gloss Gold", value: "gloss-gold" }'],["the phrase renderer",'import { renderGold } from "./goldPhrases";'],["prose in a docblock"," * a gold-italic accent word, dark-on-gold"],["a longer word that starts the same",'className="text-golden-ratio"'],["a hyphen-joined word ending in gold","// see the dark-on-gold treatment"]],g=a.filter(([,r])=>u(r).length>0);o(`the scanner stays quiet on things that merely share the word (${a.length} shapes)`,g.length===0,`it flagged: ${g.map(([r,m])=>`${r} (${m})`).join("; ")}`)}const l=n.filter(e=>f.some(t=>e.path.endsWith(t)));o(`read the source (${l.length} ${f.join("/")} files, at least ${s.minFiles})`,l.length>=s.minFiles,"fewer means the walk lost a folder, and an absence over a missing folder is free");const i=l.filter(e=>!(e.path in c)).flatMap(e=>p(e)),d=new Map;for(const e of i)d.set(e.file,[...d.get(e.file)??[],e]);o(`no file names the colour gold — the colour is called "accent" (${l.length} files)`,i.length===0,`${i.length} reference(s) to the old colour name across ${d.size} file(s):
|
|
3
|
+
`+[...d.entries()].sort((e,t)=>t[1].length-e[1].length).map(([e,t])=>` ${e} (${t.length}): ${[...new Set(t.map(a=>a.text))].slice(0,6).join(", ")}`).join(`
|
|
4
|
+
`));for(const[e,t]of Object.entries(c)){const a=l.find(g=>g.path===e);o(`the exemption is still real: ${e} (${t})`,!!a&&p(a).length>0,"the file is gone or no longer names the old colour — remove the stale entry")}for(const e of s.keptNames??[]){const t=n.find(a=>a.path===e.path);o(`a deliberate exclusion is still here: ${e.path} keeps ${e.pattern}`,!!t&&t.text.search(e.pattern)!==-1,t?e.why:`${e.path} was not among the files read — ${e.why}`)}return h()}export{f as ACCENT_TOKEN_EXTENSIONS,k as accentTokenName,p as goldTokenUses};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
type Tokens = Readonly<Record<string, string>>;
|
|
3
|
+
export type ButtonCornersOptions = {
|
|
4
|
+
readonly stylesheet?: string;
|
|
5
|
+
readonly button?: string;
|
|
6
|
+
readonly rendered?: readonly (readonly [string, string])[];
|
|
7
|
+
readonly cornerRadiusTokens?: (v: number | null) => Tokens;
|
|
8
|
+
readonly radiusRange?: readonly [number, number];
|
|
9
|
+
readonly callers: readonly string[];
|
|
10
|
+
readonly callerFloor: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function buttonCorners(files: readonly SourceFile[], options: ButtonCornersOptions): RuleReport;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{ruleChecks as m}from"./rule.js";const l="--sf-button-radius",f="sf-button",p=i=>i.replace(/\/\*[\s\S]*?\*\//g,e=>" ".repeat(e.length)),b=i=>/^import\s+Btn\s+from\s+["'](?:@\/components\/Btn|\.{1,2}\/(?:[\w-]+\/)*Btn)["'];?$/m.test(i);function g(i){const e=[],n=/<Btn\b[^>]*className=(?:"([^"]*)"|\{`([^`]*)`\})/g;let u;for(;(u=n.exec(i.text))!==null;){const h=u[1]??u[2]??"";/\brounded-/.test(h)&&e.push(`${i.path}: ${h}`)}return e}function y(i,e){const{check:n,report:u}=m("button corners"),h=t=>i.find(s=>s.path===t);{const t={path:"fixture.tsx",text:['import Btn from "@/components/Btn";','<Btn href="/x" className="mt-4 rounded-lg">Go</Btn>',"<Btn className={`mt-2`}>Go</Btn>"].join(`
|
|
2
|
+
`)};n("§6/§7 the readers find a Btn import and a rounded-* className handed to it",b(t.text)&&g(t).length===1)}if(e.stylesheet!==void 0){const t=h(e.stylesheet);if(n(`§1 read the stylesheet ${e.stylesheet}`,!!t)){const s=/\.sf-button\s*\{([^}]*)\}/.exec(p(t.text));n(`§1 ${e.stylesheet} carries a \`.sf-button\` rule`,s!==null,"without it the class is decoration and the setting reaches no button"),s&&(n(`§1 the .sf-button rule reads \`var(${l}, 0)\``,/border-radius:\s*var\(--sf-button-radius,\s*0\)/.test(s[1]),`the property for the setting, and the \`0\` for every site that never chose one — got: ${s[1].trim()}`),n("§1 the .sf-button rule does not clip",!/overflow/.test(s[1]),`the trailing arrow translates 3px right on hover and would be shaved — got: ${s[1].trim()}`))}}if(e.button!==void 0){const t=h(e.button);n(`§2 read the shared button ${e.button}`,!!t)&&n(`§2 ${e.button} writes no \`rounded-*\` utility`,!/\brounded-(?!full\b)[a-z0-9[]/.test(t.text.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm,"")),"every one carries a built-in value, so an untouched site would jump from square to a radius the day it shipped");const s=e.rendered??[];n(`§2 the button was rendered (${s.length} shapes, at least 4)`,s.length>=4,"both variants as a link and as a button");for(const[r,a]of s)n(`§2 ${r}: the rendered button carries \`${f}\``,new RegExp(`class="${f}\\b`).test(a),`it is the element's only ear for Corners — got: ${a.slice(0,140)}`)}const o=e.cornerRadiusTokens;if(o){n("§3 a site that never chose a corner publishes NO button radius",!(l in o(null)),`that absence is what leaves it square, byte for byte as before — got: ${JSON.stringify(o(null))}`),n("§3 `0` is an ANSWER, not an absence: it publishes a flattened radius",o(0)[l]==="0",JSON.stringify(o(0)));for(const[d,c]of[[0,"0"],[9,"0.28125rem"],[16,"0.5rem"],[40,"1.25rem"]])n(`§3 ${d}px publishes ${l}: ${c}`,o(d)[l]===c,`a button is half the card, which is what inputs and panels beside it read — got: ${String(o(d)[l])}`);const[t,s]=e.radiusRange??[Number.NaN,Number.NaN];n(`§3 both ends of the track (${t}, ${s}) publish a button radius`,e.radiusRange!==void 0&&o(t)[l]!==void 0&&o(s)[l]!==void 0);const r=[null,0,9,40].map(d=>o(d)[l]??"0");n("§4 not-set and square both DRAW a square button",r[0]==="0"&&r[1]==="0",`they differ in what they publish, not in today's picture — got: ${JSON.stringify(r)}`),n("§4 9 and 40 draw two different NON-square buttons",r[2]!=="0"&&r[3]!=="0"&&r[2]!==r[3],`a setting that names buttons and moves none of them is this card's defect restored — got: ${JSON.stringify(r)}`);const a="--radius-full";for(const d of[0,9,16,40])n(`§5 ${d}px does not publish the fully-round token`,!(a in o(d)),"swatches, toggle knobs and status dots are a SHAPE, not a corner")}{const t=[];for(const s of e.callers){const r=h(s);r&&t.push(...g(r))}n(`§6 no caller hands \`Btn\` a \`rounded-*\` class (${e.callers.length} callers)`,t.length===0,`\`.sf-button\` is unlayered and would silently beat it; give the caller its own property instead — ${t.join(" | ")}`)}{const t=i.filter(a=>a.path.endsWith(".tsx")&&b(a.text)).map(a=>a.path).sort();n(`§7 the tree sweep found ${t.length} file(s) importing Btn (at least ${e.callerFloor})`,t.length>=e.callerFloor&&t.length>0,`too few to be real, so §7 is measuring nothing — ${JSON.stringify(t)}`);const s=t.filter(a=>!e.callers.includes(a));n("§7 the callers list names every file that imports `Btn`",s.length===0,`${JSON.stringify(s)} import(s) Btn and ${s.length===1?"is":"are"} not listed — a caller missing from it is a caller §6 never looks at. Add the path — the list is kept by hand on purpose, so a new caller is a visible line in a diff.`);const r=e.callers.filter(a=>!t.includes(a));n("§7 the callers list names no file that stopped importing `Btn`",r.length===0,`${JSON.stringify(r)} ${r.length===1?"is":"are"} listed and no longer import Btn — a dead row is a sweep over nothing. Drop the path, or say in a comment why the file is still worth sweeping.`)}return u()}export{y as buttonCorners};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export declare const CHROME_NOTE_CLASS = "sf-note";
|
|
3
|
+
export type EditorChromeNoteOptions = {
|
|
4
|
+
readonly stylesheet?: string;
|
|
5
|
+
readonly minFiles: number;
|
|
6
|
+
};
|
|
7
|
+
export interface CssRule {
|
|
8
|
+
readonly selector: string;
|
|
9
|
+
readonly body: string;
|
|
10
|
+
readonly line: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function leafRules(css: string): CssRule[];
|
|
13
|
+
export declare function declaration(body: string, prop: string): string | null;
|
|
14
|
+
export declare function noteClasses(text: string): Set<string>;
|
|
15
|
+
export declare function withoutComments(source: string): string;
|
|
16
|
+
export declare function editorChromeNote(files: readonly SourceFile[], options: EditorChromeNoteOptions): RuleReport;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{ruleChecks as z}from"./rule.js";const g="sf-note";function y(o){const i=[],t=[];let c="",r=1,n=0;for(;n<o.length;){const a=o[n];if(a==="/"&&o[n+1]==="*"){const e=o.indexOf("*/",n+2),s=e<0?o.length:e+2;for(const f of o.slice(n,s))f===`
|
|
2
|
+
`&&r++;n=s,c+=" ";continue}if(a==='"'||a==="'"){const e=a;for(n++;n<o.length&&o[n]!==e;)o[n]==="\\"?n++:o[n]===`
|
|
3
|
+
`&&r++,n++;n++;continue}if(a===`
|
|
4
|
+
`&&r++,a==="{"){t.length>0&&t[t.length-1].children++,t.push({selector:c.trim().replace(/\s+/g," "),line:r,bodyStart:n+1,children:0}),c="",n++;continue}if(a==="}"){const e=t.pop();e&&e.children===0&&i.push({selector:e.selector,body:o.slice(e.bodyStart,n),line:e.line}),c="",n++;continue}if(a===";"){c="",n++;continue}c+=a,n++}return i}function m(o,i){const t=o.match(new RegExp(`(?:^|[;{])\\s*${i}\\s*:([^;]*)`));return t?t[1].trim():null}function u(o){return new Set(Array.from(o.matchAll(/\bsf-[a-z0-9-]*?note\b/g),i=>i[0]))}function b(o){return o.replace(/\/\*[\s\S]*?\*\//g," ").replace(/(^|[^:])\/\/.*$/gm,"$1")}const p=(o,i)=>JSON.stringify(o)===JSON.stringify(i);function S(o,i){const{check:t,report:c}=z("editor chrome note");{const e=h=>["sf",h,"note"].join("-"),s=y(`.sf-note {
|
|
5
|
+
font-size: 13px;
|
|
6
|
+
color: red;
|
|
7
|
+
}`);t("the walker reads a rule's own declarations",s.length===1&&s[0].selector===".sf-note"&&s[0].line===1&&m(s[0].body,"font-size")==="13px"&&m(s[0].body,"padding")===null,JSON.stringify(s)),t("font-size is not confused with a longhand that ends in it",m(` font-size-adjust: 0.5;
|
|
8
|
+
font-size: 12px;
|
|
9
|
+
`,"font-size")==="12px"&&m(` font-size-adjust: 0.5;
|
|
10
|
+
`,"font-size")===null),t("a grouping block reports its children, not itself",p(y("@keyframes sf-x { from { opacity: 0; } to { opacity: 1; } }").map(h=>h.selector),["from","to"])),t("braces in comments and strings do not shift nesting",p(y('/* .a { x } */ .b { content: "}"; } .c { color: red; }').map(h=>h.selector),[".b",".c"])),t("modifiers and descendants report the base note class",p([...u(".sf-note--error")],["sf-note"])&&p([...u(".sf-note p + p")],["sf-note"])&&p([...u(`.${e("pages")}, .${e("sm")}`)],[e("pages"),e("sm")])&&u(".sf-notebook").size===0);const f=[`/* Two homes were rejected: \`.${e("pages")}\` belongs to the chrome. */`,`// …and .${e("sm")} is the sitemap's own.`,'const url = "https://x/y";','export const Note = () => <div className="sf-note">hi</div>;'].join(`
|
|
11
|
+
`);t("a class a file only TALKS about is not a class it renders, and a `//` in a URL is not a comment",p([...u(b(f))],["sf-note"])&&/https:\/\/x\/y/.test(b(f)))}let r=new Set([g]);if(i.stylesheet){const e=o.find(s=>s.path===i.stylesheet);if(t(`read the stylesheet ${i.stylesheet}`,!!e)&&e){const s=y(e.text);r=new Set;for(const l of s)for(const d of u(l.selector))r.add(d);t(`the stylesheet parses (${s.length} rules, more than 400)`,s.length>400,"the walker looks broken"),t(`the scan is not vacuous: it finds a note class (${r.size})`,r.size>0,"the selector scan looks broken"),t(`the chrome declares exactly ONE note class, .${g}`,p([...r].sort(),[g]),`it declares ${[...r].sort().map(l=>`.${l}`).join(", ")}. One quiet sentence, one class: a note that needs a different INSET takes it from the block it sits in (\`.sf-pages-list > .sf-note\`), never from a new class`);const f=new Map;for(const l of r){const d=s.find(x=>x.selector===`.${l}`),w=d?m(d.body,"font-size"):null;t(`.${l} has a type rule of its own`,!!d,"it is a modifier without a base"),t(`.${l} states its own font-size`,w!==null,"it inherits one — that is a size nobody chose"),f.set(l,w)}const h=new Set(f.values());t("every note class states the same size",h.size<=1,`the chrome's notes are set at ${h.size} sizes: ${[...f].map(([l,d])=>`.${l} ${d}`).join(", ")}`);const $=s.find(l=>l.selector===`.${g}`);t(`the note still reads as a note: .${g} is var(--sf-muted)`,!!$&&m($.body,"color")==="var(--sf-muted)",$?`color: ${m($.body,"color")}`:"the chrome no longer declares it")}}const n=o.filter(e=>/\.(tsx?|jsx?|mjs)$/.test(e.path));t(`read the source (${n.length} JS/TS files, at least ${i.minFiles})`,n.length>=i.minFiles,"fewer means the walk broke, and the check below is vacuous");const a=[];for(const e of n)for(const s of u(b(e.text)))r.has(s)||a.push(`${e.path} ${s}`);return t(`no source file names a note class the chrome does not declare (${[...r].map(e=>`.${e}`).join(", ")})`,a.length===0,`a \`.sf-*\` class is scoped under \`.sf-editor\` in the PARENT document, so a missing rule is not a compile error — the text simply renders at whatever it inherits:
|
|
12
|
+
${a.join(`
|
|
13
|
+
`)}`),c()}export{g as CHROME_NOTE_CLASS,m as declaration,S as editorChromeNote,y as leafRules,u as noteClasses,b as withoutComments};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{ruleChecks as c}from"./rule.js";const r="efficient-easel",i=`${r}/panel.css`,s=`${r}/menu.css`;function a(h){const n=h.filter(t=>t.path.endsWith(".tsx")&&t.text.includes(i));return{withPanel:n,missing:n.filter(t=>!t.text.includes(s)).map(t=>t.path)}}function p(h,n){const{check:t,report:l}=c("editor context menu");{const e=a([{path:"a.tsx",text:`import "${i}";
|
|
2
|
+
import "${s}";`},{path:"b.tsx",text:`import "${i}";`},{path:"c.tsx",text:`import "${s}";`}]);t("§8's reader finds the entries that import the panel sheet, and the one lacking the menu sheet",e.withPanel.length===2&&e.missing.join()==="b.tsx")}{const{withPanel:e,missing:o}=a(h);t(`(l) §8 every entry that imports the panel sheet imports the menu sheet too (${e.length} entries, at least ${n.minEntries})`,e.length>=n.minEntries&&e.length>0&&o.length===0,o.length?`these mount the editor chrome but not \`${s}\`, so the menu draws as an unstyled full-width div there:
|
|
3
|
+
`+o.join(`
|
|
4
|
+
`):`the walk found too few entries importing ${i} — it is not sweeping the editor at all, so this check is asking nothing.`)}return l()}export{p as editorContextMenu};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type EditorPuckDragOffOptions = {
|
|
3
|
+
readonly documentPermissions: (canEdit: boolean) => Readonly<Record<string, unknown>>;
|
|
4
|
+
readonly mustFind?: readonly string[];
|
|
5
|
+
readonly harnesses?: Readonly<Record<string, string>>;
|
|
6
|
+
readonly minFiles: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function editorPuckDragOff(files: readonly SourceFile[], options: EditorPuckDragOffOptions): RuleReport;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{ruleChecks as g}from"./rule.js";const $=/<(?:Puck|SiteEditorProvider|EditorStoreProvider|ScreenStoreProvider)\b[^<>]*\bdata=\{[^<>]*>/g,S=/\bpermissions=\{\s*(?:documentPermissions\(|permissions\s*\})/,P=/permissions=\{\s*permissions\s*\}/,k=["delete","drag","duplicate","edit","insert"],d=o=>[...o.text.matchAll($)].map(n=>({file:o.path,tag:n[0]}));function u(o,n){const s=[];return S.test(o.tag)?P.test(o.tag)&&!/documentPermissions\(/.test(n)&&s.push("passes a `permissions` it does not build from documentPermissions"):s.push("mounts the editor without the edit lock's permissions"),s}function w(o,n){const{check:s,report:f}=g("editor puck drag off"),m=n.harnesses??{};for(const e of[!0,!1]){const t=n.documentPermissions(e);s(`documentPermissions(${e}).drag is false`,t.drag===!1,`got ${String(t.drag)}`),s(`documentPermissions(${e}) names all five keys — Puck MERGES this prop, so a missing key keeps its old value`,Object.keys(t).sort().join(",")===k.join(","),`got ${Object.keys(t).sort().join(",")}`),s(`documentPermissions(${e}).edit is ${e}`,t.edit===e)}{const e="Puck",t="SiteEditorProvider",i=a=>d({path:"fixture.tsx",text:a}).map(p=>u(p,a).length);s("the scanner flags a mount with no permissions",JSON.stringify(i(`<${e} config={c} data={d} />`))==="[1]"),s("the scanner passes a mount that calls the helper",JSON.stringify(i(`<${t} data={d} permissions={documentPermissions(!readOnly)}>`))==="[0]"),s("a local `permissions` passes only when the file builds it from the helper",JSON.stringify(i(`<${e} data={d} permissions={permissions} />`))==="[1]"&&JSON.stringify(i(`const permissions = documentPermissions(true);
|
|
2
|
+
<${e} data={d} permissions={permissions} />`))==="[0]"),s("a tag with no `data=` is not a mount",i(`<${e}.Preview />`).length===0),s("the screen's store provider is a mount under either of its names",["Editor","Screen"].every(a=>JSON.stringify(i(`<${a}StoreProvider config={c} data={d} />`))==="[1]"))}const c=o.filter(e=>/\.tsx?$/.test(e.path));s(`read the source (${c.length} TS/TSX files, at least ${n.minFiles})`,c.length>=n.minFiles,"fewer means the walk broke, and every check below is vacuous");const r=[];for(const e of c)e.path in m||r.push(...d(e));for(const e of n.mustFind??[])s(`found the editor mount (data=…) in ${e}`,r.some(t=>t.file===e));for(const[e,t]of Object.entries(m)){const i=c.find(a=>a.path===e);s(`the harness exemption is still real: ${e} (${t})`,!!i&&d(i).length>0,"remove the stale entry")}const h=new Map(c.map(e=>[e.path,e.text])),l=r.flatMap(e=>u(e,h.get(e.file)??"").map(t=>`${e.file} ${t}`));return s(`every editor mount turns Puck's own canvas drag off (${r.length} mount(s)${r.length?`: ${[...new Set(r.map(e=>e.file))].join(", ")}`:""})`,l.length===0,`without the edit lock's permissions a read-only viewer's canvas would take edits the save then refuses:
|
|
3
|
+
${l.join(`
|
|
4
|
+
`)}`),f()}export{w as editorPuckDragOff};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type EfficientEaselVersionOptions = {
|
|
3
|
+
readonly packageJson: string;
|
|
4
|
+
readonly resolve: (specifier: string) => string;
|
|
5
|
+
readonly readText: (path: string) => string | undefined;
|
|
6
|
+
};
|
|
7
|
+
export declare function satisfiesCaret(version: string, range: string): boolean;
|
|
8
|
+
export declare function importedSubpaths(files: readonly SourceFile[]): string[];
|
|
9
|
+
export declare function efficientEaselVersion(files: readonly SourceFile[], options: EfficientEaselVersionOptions): RuleReport;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{ruleChecks as b}from"./rule.js";const p=n=>JSON.stringify(n),t="efficient-easel",u=`${t}/panel.css`;function m(n,r){const s=/^\^(\d+)\.(\d+)\.(\d+)$/.exec(r.trim());if(!s)throw new Error(`only \`^x.y.z\` pins are understood here, got "${r}"`);const a=/^(\d+)\.(\d+)\.(\d+)/.exec(n.trim());if(!a)throw new Error(`"${n}" is not a version this can read`);const[i,c,h]=s.slice(1).map(Number),[e,o,l]=a.slice(1).map(Number);return!(e!==i||i===0&&o!==c||o<c||o===c&&l<h)}const $=n=>{const r=Math.max(n.lastIndexOf("/"),n.lastIndexOf("\\"));return r<=0?n.slice(0,r+1)||n:n.slice(0,r)};function g(n,r){const s=n(u);let a=$(s);for(let i=0;i<12;i++){const c=r(`${a}/package.json`);if(c!==void 0)try{const e=JSON.parse(c);if(e.name===t){if(!e.version)throw new Error(`${t} resolved but its package.json carries no version`);return e.version}}catch(e){if(!(e instanceof SyntaxError))throw e}const h=$(a);if(h===a)break;a=h}throw new Error(`resolved ${u} to ${s} but found no ${t} package.json above it`)}function v(n){const r=new Set;for(const s of n)if(/\.(ts|tsx|mts|mjs|js|jsx)$/.test(s.path))for(const a of s.text.matchAll(/(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)["'](efficient-easel\/[A-Za-z0-9._/-]+)["']/g))r.add(a[1]);return[...r].sort()}const k=n=>n.code??"threw";function x(n,r){const{check:s,report:a}=b(`${t} version`);{const e=(f,w)=>{try{return m(f,w)}catch{return"threw"}};s("the caret reading is right about 0.x, where the minor is part of the lock",e("0.10.0","^0.10.1")===!1&&e("0.10.1","^0.10.1")===!0&&e("0.11.0","^0.11.0")===!0&&e("0.11.0","^0.10.1")===!1&&e("0.10.1","^0.11.0")===!1&&e("0.11.4","^0.11.0")===!0&&e("0.11.4","~0.11.0")==="threw","a stale patch is caught; 0.x carets cross no minor and go no backwards; patches above the pin pass; only ^x.y.z is read");const o={"/s/node_modules/.pnpm/efficient-easel@1.2.3/node_modules/efficient-easel/package.json":JSON.stringify({name:t,version:"1.2.3"}),"/s/node_modules/.pnpm/efficient-easel@1.2.3/node_modules/efficient-easel/dist/package.json":"{ not json"};let l="";try{l=g(()=>"/s/node_modules/.pnpm/efficient-easel@1.2.3/node_modules/efficient-easel/dist/css/panel.css",f=>o[f])}catch(f){l=f.message}s("the manifest walk climbs past an unreadable manifest to the package's own",l==="1.2.3",l);const d=v([{path:"fixture.ts",text:[`import { A } from ${p(t+"/menu")};`,`// see ${t}/edit-lock`,`const m = await import(${p(t+"/icons")});`,`const s = ${p(t+"/not-an-import")};`].join(`
|
|
2
|
+
`)}]);s("only import syntax counts as an import, never prose or a string",d.join()===`${t}/icons,${t}/menu`,JSON.stringify(d))}let i;try{const e=JSON.parse(r.packageJson);i=e.dependencies?.[t]??e.devDependencies?.[t]}catch(e){s("package.json reads as JSON",!1,e.message)}if(s(`package.json declares a dependency on ${t}`,i!==void 0)){let e;try{e=g(r.resolve,r.readText)}catch(o){s(`${t} resolves from the install`,!1,o.message)}if(e!==void 0){let o,l;try{o=m(e,i)}catch(d){o=!1,l=d.message}s(`the resolved ${t} satisfies the declared pin (declared ${i}, resolved ${e})`,o,l??`this tree resolves ${t} ${e}, which does not satisfy the declared ${i}. The install is stale — what you are looking at is not what this repository says it is. Refresh it with \`pnpm install\`, and inside Docker remember that a node_modules VOLUME outlives the container: it keeps whatever was installed into it until something installs again.`)}}const c=v(n);if(s(`found the ${t} imports (${c.length} subpaths)`,c.length>0,"the search is broken, not the code")){const e=[];for(const o of c)try{r.resolve(o)}catch(l){e.push(`${o} (${k(l)})`)}s(`every ${t} subpath this tree imports resolves from the install — ${c.join(", ")}`,e.length===0,`the installed package does not export: ${e.join(", ")}. The pin resolves and the lockfile is valid — the SUBPATH is missing, which means the pin points at a release published before it existed. Move the pin to the release that carries it.`)}let h=!1;try{r.resolve(`${t}/a-subpath-that-was-never-exported`)}catch{h=!0}return s(`the resolver refuses a ${t} subpath that does not exist`,h,"it resolved one — this check cannot fail"),a()}export{x as efficientEaselVersion,v as importedSubpaths,m as satisfiesCaret};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type EveryButtonOptions = {
|
|
3
|
+
readonly buttonFiles: readonly (readonly [string, number, number])[];
|
|
4
|
+
readonly exclude?: readonly string[];
|
|
5
|
+
readonly accentFillsThatStay: number;
|
|
6
|
+
readonly buttonFills: number;
|
|
7
|
+
readonly buttonSites: number;
|
|
8
|
+
readonly hoverFgKeepers: number;
|
|
9
|
+
readonly chips?: readonly string[];
|
|
10
|
+
readonly runtimeFallbacks?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function everyButtonFollowsTheButtonColour(files: readonly SourceFile[], options: EveryButtonOptions): RuleReport;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{ruleChecks as F}from"./rule.js";const h=/[-a-zA-Z0-9:/.[\]]*bg-accent[-a-zA-Z0-9:/.[\]]*/g,u=/[-a-zA-Z0-9:/.[\]]*bg-btn[-a-zA-Z0-9:/.[\]]*/g;function N(o){const t=o.trim();return t.startsWith("//")||t.startsWith("*")||t.startsWith("/*")}function f(o,t){const n=[];for(const r of o)r.text.split(`
|
|
2
|
+
`).forEach((l,i)=>{t.lastIndex=0,!(!t.test(l)||N(l))&&n.push({file:r.path,line:i+1,text:l})});return n}function g(o,t){const n=new Map;for(const r of o){const l=r.text.match(t)?.length??0;l&&n.set(r.file,(n.get(r.file)??0)+l)}return n}const d=o=>[...o.values()].reduce((t,n)=>t+n,0);function $(o){const t=/(?<![-:\w])text-bg\b/.test(o),n=/\btext-black\b/.test(o),r=/\bhover:text-bg\b/.test(o)&&/\bhover:bg-btn\b/.test(o);return t||n||r}const w=o=>/\bhover:bg-fg\b/.test(o)&&/\bhover:text-bg\b/.test(o);function B(o,t){const{check:n,report:r}=F("every button follows the button colour");{const e={path:"fixture.tsx",text:['<button className="bg-btn text-btn-ink hover:bg-btn/90" />','// <button className="bg-accent" /> — a comment is not paint','<span className={on ? "bg-accent text-bg" : "bg-fg/10"} />','<button className="bg-btn text-bg" />','<button className="bg-btn hover:bg-fg hover:text-bg" />'].join(`
|
|
3
|
+
`)},a=f([e],u);n("the readers count fills per occurrence and drop comment lines",d(g(a,u))===4&&d(g(f([e],h),h))===1),n("§4's reader flags page ink on a button and keeps the --fg hover's ink",a.filter(s=>$(s.text)).map(s=>s.line).join()==="4"&&a.filter(s=>w(s.text)).map(s=>s.line).join()==="5")}const l=o.filter(e=>/\.(tsx?|css)$/.test(e.path)),i=new Set(t.exclude??[]);for(const e of i)n(`the excluded file is still real: ${e}`,l.some(a=>a.path===e),"remove the stale entry");const b=l.filter(e=>!i.has(e.path));n(`read the .ts, .tsx and .css files (${b.length})`,b.length>0,"the population is empty — this rule is measuring nothing");const y=f(b,h),c=f(b,u),m=g(y,h),p=g(c,u);{const e=[];for(const[a,s,k]of t.buttonFiles){const v=p.get(a)??0,x=m.get(a)??0;v!==s&&e.push(`${a}: ${v} bg-btn, expected ${s}`),x!==k&&e.push(`${a}: ${x} bg-accent left, expected ${k}`)}n(`§1 every ruled button paints the button token, and its file's markers do not (${t.buttonFiles.length} files)`,e.length===0,`a ruled button still paints --accent, or a marker was swept along with it:
|
|
4
|
+
`+e.join(`
|
|
5
|
+
`))}{const e=d(m);n(`§2 ${t.accentFillsThatStay} bg-accent fills stay (found ${e})`,e===t.accentFillsThatStay,"either a marker was swept (too few) or new accent paint arrived (too many) — this count is derived from the tree, so a new call site has to come past it.");const a=d(p);n(`§2 ${t.buttonFills} bg-btn fills (found ${a})`,a===t.buttonFills)}for(const e of t.chips??[])n(`§3 ${e} still paints bg-accent`,(m.get(e)??0)>0,"a filter chip is not a button, and a sweep that repaints it has swept too far");{n(`§4 ${t.buttonSites} sites paint bg-btn (found ${c.length})`,c.length===t.buttonSites,"§4 cannot say anything about ink until the fills have actually moved");const e=c.filter(s=>$(s.text)).map(s=>`${s.file}:${s.line}`);n("§4 no button inks a bg-btn state with the page background or a literal",e.length===0,`a merchant's button colour moves the fill and leaves the ink behind:
|
|
6
|
+
`+e.join(`
|
|
7
|
+
`));const a=c.filter(s=>w(s.text)).length;n(`§4 ${t.hoverFgKeepers} buttons still pair hover:bg-fg with hover:text-bg (found ${a})`,a===t.hoverFgKeepers,"a sweep that moved those to --btn-ink has put the button's ink on a foreground-coloured fill")}if(t.runtimeFallbacks!==void 0){const e=o.find(a=>a.path===t.runtimeFallbacks);if(n(`§5 read ${t.runtimeFallbacks}`,!!e)){const a=[...e.text.matchAll(/const (btnBg|btnFg) = \w+ \|\| "([^"]+)";/g)].map(s=>[s[1],s[2]]);n(`§5 found the 2 button-colour fallbacks (found ${a.length})`,a.length===2,"if the shape changed, this rule stopped reading the thing it names"),n("§5 the UNSET button falls back to --btn and --btn-ink",JSON.stringify(Object.fromEntries(a))===JSON.stringify({btnBg:"var(--btn)",btnFg:"var(--btn-ink)"}),"it falls back to the accent or to a literal again. Unset is the majority case, and on hartes-metal that painted #C0392B under a hard-coded black — card 66309's defect on the most prominent button of the page. Only the fallback may move; a merchant's own colour short-circuits it.")}}return r()}export{B as everyButtonFollowsTheButtonColour};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as Theme from "../theme.js";
|
|
2
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
3
|
+
export type FlatThemeModule = Pick<typeof Theme, "ACCENT_DARKEN_STEPS" | "contrastRatio" | "darkerAccents" | "DEFAULT_THEMES" | "gradeContrast" | "normalizeThemeColors" | "READABILITY_AA" | "READABILITY_AA_LARGE" | "themeAccentStyle" | "themeColorStyle">;
|
|
4
|
+
export type FlatThemePaletteOptions = {
|
|
5
|
+
readonly minFiles: number;
|
|
6
|
+
readonly editorAccentCarriers: number;
|
|
7
|
+
readonly liveAccentCarrier?: string;
|
|
8
|
+
readonly theme?: FlatThemeModule;
|
|
9
|
+
};
|
|
10
|
+
export declare function flatThemePalette(files: readonly SourceFile[], options: FlatThemePaletteOptions): RuleReport;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{isRuleSource as y,ruleChecks as S}from"./rule.js";import{registryFiles as T,registryText as C}from"./support/registry-source.js";const k=/\.(ts|tsx|js|jsx|css|json)$/,x=["accentLight","backgroundLight","surfaceLight","lineLight","mutedLight","textLight"],_=["lightSurfaces"],R=["usesLightSurfaces","setLightSurfaces","lightPageBackground"],b=["accentForMode","isAccentLinked","linkAccent","unlinkAccent"],N=["--theme-fg-light","--theme-bg-light","--theme-surface-light","--theme-surface-2-light","--theme-line-light","--theme-muted-light","--theme-accent-light","--theme-accent-light-2"],v=[...x,"lightSurfaces"],I=["text","accent","background","surface","line","muted"];function u(c,a){const t=[];for(const d of c)d.text.split(`
|
|
2
|
+
`).forEach((l,f)=>{a.test(l)&&t.push(`${d.path}:${f+1} ${l.trim().slice(0,100)}`)});return t}const A=c=>new RegExp(`\\b${c}\\b`),$=c=>new RegExp(`${c}(?![\\w-])`);function F(c,a){const{check:t,report:d}=S("a theme is one flat palette");{const e=[{path:"fixture.ts",text:`const a = colors.textLight;
|
|
3
|
+
const b = contextLighting;
|
|
4
|
+
`},{path:"fixture.css",text:`.x { color: var(--theme-accent-light-2); }
|
|
5
|
+
.y { color: var(--theme-accent-light); }
|
|
6
|
+
`}],r=u(e,A("textLight"));t("the name reader flags `textLight`, and not `contextLighting`",r.length===1&&r[0].startsWith("fixture.ts:1"),JSON.stringify(r));const n=u(e,$("--theme-accent-light"));t("the token reader flags `--theme-accent-light`, and not `--theme-accent-light-2`",n.length===1&&n[0].startsWith("fixture.css:2"),JSON.stringify(n))}const l=c.filter(e=>k.test(e.path)&&!y(e.path));t(`read the shipped source (${l.length} files, at least ${a.minFiles})`,l.length>=a.minFiles,"every check below asserts an absence, and a read of nothing satisfies an absence");const f=(e,r,n)=>{const s=u(l,r);t(`${e} is gone`,s.length===0,`\`${e}\` still lives in ${s.length} place(s) — ${n} (66238):
|
|
7
|
+
${s.join(`
|
|
8
|
+
`)}`)};for(const e of x)f(e,A(e),"a theme is ONE flat palette");for(const e of[..._,...R,...b])f(e,A(e),"a theme is ONE flat palette");for(const e of N)f(e,$(e),"the keys and the tokens are ONE mechanism");if(a.theme){const e=a.theme;e.DEFAULT_THEMES.forEach((n,s)=>{const g=n,o=I.filter(h=>typeof g[h]!="string");t(`preset ${s} still states all six base colours`,o.length===0,`lost ${o.join(", ")} — 66238 removes a duplicated HALF, never a colour`);const i=v.filter(h=>h in g);t(`preset ${s} carries no key 66238 deleted`,i.length===0,`still carries ${i.join(", ")}`)}),t(`read the presets (${e.DEFAULT_THEMES.length})`,e.DEFAULT_THEMES.length>0);const r={selected:0,themes:[e.DEFAULT_THEMES[0]]};for(const[n,s]of[["themeAccentStyle",e.themeAccentStyle(r)],["themeColorStyle",e.themeColorStyle(r)]])t(`${n} does not set --accent or --accent-2 directly`,!("--accent"in s)&&!("--accent-2"in s));t("the grading bands sit exactly on the WCAG thresholds",e.gradeContrast(e.READABILITY_AA)==="good"&&e.gradeContrast(e.READABILITY_AA+.5)==="good"&&e.gradeContrast(e.READABILITY_AA-.01)==="large-only"&&e.gradeContrast(e.READABILITY_AA_LARGE)==="large-only"&&e.gradeContrast(e.READABILITY_AA_LARGE-.01)==="hard"&&e.gradeContrast(1)==="hard"),t("the contrast ratio is the real WCAG figure, and symmetric",e.contrastRatio("#000000","#ffffff")===21&&e.contrastRatio("#ffffff","#000000")===21&&e.contrastRatio("not a colour","#ffffff")===null);{const n="#c9a461",s=e.normalizeThemeColors({text:"#0a0a0a",accent:n,background:"#f5f1e8"}).background,g=e.darkerAccents(n),o=[];g.length!==e.ACCENT_DARKEN_STEPS.length&&o.push(`${g.length} suggestions for ${e.ACCENT_DARKEN_STEPS.length} steps`);let i=e.contrastRatio(n,s)??0;for(const h of g){/^#[0-9a-f]{6}$/.test(h)||o.push(`${h} is not a plain hex`);const m=e.contrastRatio(h,s)??0;m>i||o.push(`${h} (${m}) does not beat the previous ${i}`),i=m}e.gradeContrast(i)!=="good"&&o.push(`the deepest reads ${e.gradeContrast(i)}, not good`),t("the darker suggestions are the same hue, and actually read better",o.length===0,o.join("; "))}t("a non-hex suggestion list is empty rather than three swatches of black",e.darkerAccents("var(--brand)").length===0)}if(a.liveAccentCarrier){const e=c.find(r=>r.path===a.liveAccentCarrier);t(`read the live site's accent carrier, ${a.liveAccentCarrier}`,!!e),t("the live site's accent carrier wears sf-theme-accent",!!e&&/className="contents sf-theme-accent"/.test(e.text))}const E=T(c),p=C(c);t(`read this repository's share of the block registry (${E.length} files)`,E.length>0);const L=[...p.matchAll(/className="[^"]*sf-theme-accent/g)].length;return t(`the registry's editor accent carriers wear sf-theme-accent (found ${L}, expected exactly ${a.editorAccentCarriers})`,L===a.editorAccentCarriers),t("the editor preview emits --theme-accent via themeAccentStyle, not a hand-rolled --accent",!/\["--accent"\]:/.test(p)),d()}export{F as flatThemePalette};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type FullWidthMeansFullOptions = {
|
|
3
|
+
readonly converted: readonly string[];
|
|
4
|
+
readonly carrierFloor: number;
|
|
5
|
+
readonly widthHelper?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const SITE_CAP = "max-w-[var(--sf-site-width,1600px)]";
|
|
8
|
+
export declare function fullWidthMeansFull(files: readonly SourceFile[], options: FullWidthMeansFullOptions): RuleReport;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{ruleChecks as f}from"./rule.js";const o="max-w-[var(--sf-site-width,1600px)]",p=t=>(t.split("/").pop()??t).replace(/\.tsx$/,"");function a(t){return{asks:t.includes("sectionRunsFullWidth(widthProps)"),column:t.includes(`: "mx-auto ${o}`)||t.includes(`: "relative z-[2] mx-auto ${o}`)||t.includes(`: "mx-auto grid ${o}`)}}function w(t,l){const{check:s,report:c}=f("full width means full");{const e=a(`const full = sectionRunsFullWidth(widthProps);
|
|
2
|
+
<div className={full ? "w-full" : "mx-auto ${o} px-6"} />`),n=a(`<div className="mx-auto ${o} px-6" />`);s("the reader passes a conditional cap",e.asks&&e.column),s("…and fails an unconditional one",!n.asks&&!n.column)}const r=t.filter(e=>e.path.endsWith(".tsx")),i=r.filter(e=>e.text.includes(o));s(`…and it finds the files that carry one (${i.length} of ${r.length} .tsx files, at least ${l.carrierFloor})`,i.length>=l.carrierFloor&&i.length>0,`only ${i.length} files carry ${o}`);for(const e of l.converted){const n=t.find(u=>u.path===e);if(!s(`read ${e}`,!!n,"the converted section is gone — remove it from the list, or find where it went"))continue;const{asks:d,column:h}=a(n.text);s(`${p(e)} asks whether it runs full width`,d,"its content column is unconditional again — a merchant choosing Full width would get a capped section, which is the defect this card removed"),s("…and still draws the column when it does not",h,"the other arm lost the cap — every published page of this type would go edge to edge, which is NOT what this card does")}if(l.widthHelper!==void 0){const e=t.find(n=>n.path===l.widthHelper);s(`read the width helper ${l.widthHelper}`,!!e)&&s("sectionRunsFullWidth resolves absence to the content column, never to full",/resolveWidth\(props as WidthProps, "page"\)/.test(e.text),"absence resolving to `full` sends every published section of these types edge to edge on the day this ships")}return c()}export{o as SITE_CAP,w as fullWidthMeansFull};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type HeadingCeilingOptions = {
|
|
3
|
+
readonly notSwept?: Readonly<Record<string, string>>;
|
|
4
|
+
readonly reference?: string;
|
|
5
|
+
readonly minSites: number;
|
|
6
|
+
readonly minSwept: number;
|
|
7
|
+
readonly minCapped: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function enclosingLiteral(src: string, at: number): string | null;
|
|
10
|
+
export declare function headingCeilingNotes(files: readonly SourceFile[], options: HeadingCeilingOptions): string[];
|
|
11
|
+
export declare function headingCeilingSweep(files: readonly SourceFile[], options: HeadingCeilingOptions): RuleReport;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{HEADING_FACE_UTILITIES as _}from"../fonts.js";import{isRuleSource as k,ruleChecks as T}from"./rule.js";import{evalLength as S}from"./support/css-length.js";const j=["app/","components/","lib/"],v="40px",E=["1","1.4"],b=/(?:^|\s)((?:[a-z0-9@_.-]*\[[^\]]*\]:|[a-z0-9@_.-]+:)*)text-\[([^\]]*)\]/g;function L(t){const e=/max-\[(\d+)px\]:/.exec(t);return e?Number(e[1]):1600}function O(t,e){let s=-1,o="";for(let i=e;i>0;i--){const l=t[i-1];if((l==="`"||l==="'"||l==='"')&&t[i-2]!=="\\"){o=l,s=i;break}}if(s===-1)return null;for(let i=e;i<t.length;i++)if(t[i]===o&&t[i-1]!=="\\")return t.slice(s,i);return null}const A=/^(#|rgb|hsl|oklch|color|currentColor|inherit|transparent)/;function y(t){const e=[];for(const s of t){const o=s.text,i=new RegExp(_.join("|"),"g");let l;for(;l=i.exec(o);){const a=O(o,l.index);if(!a)continue;const h=[];b.lastIndex=0;let c;for(;c=b.exec(a);){const p=c[2].replace(/^length:/,"");A.test(p)||h.push({token:c[0].trim(),expr:p,vw:L(c[1])})}h.length!==0&&e.push({file:s.path,line:o.slice(0,l.index).split(`
|
|
2
|
+
`).length,className:a,steps:h})}}return e}function u(t,e){return S(t.expr,t.vw,e)}const C=t=>u(t,{"--scale-heading":"1"})>40;function m(t,e){return t[`${e.file}:${e.line}`]??Object.entries(t).find(([s])=>{const o=s.indexOf("#");return o>0&&s.slice(0,o)===e.file&&e.className.includes(s.slice(o+1))})?.[1]??t[e.file]}const I=t=>t.filter(e=>/\.tsx?$/.test(e.path)&&!k(e.path)&&j.some(s=>e.path.startsWith(s)));function D(t,e){const s=e.notSwept??{},o=y(I(t)).filter(l=>!m(s,l)).flatMap(l=>l.steps.filter(a=>!C(a)&&u(a,{"--sf-heading-max":v,"--scale-heading":"1.4"})>40).map(a=>`${l.file}:${l.line} · ${a.token}`)),i=[];if(o.length){i.push(` note — ${o.length} step(s) clear 40px at the default heading scale but not at 1.4; outside this card's census, listed so they are not a silent gap:`);for(const l of o)i.push(` ${l}`)}for(const[l,a]of Object.entries(s))i.push(` note — not swept: ${l} — ${a}`);return i}function H(t,e){const{check:s,report:o}=T("heading ceiling sweep"),i=e.notSwept??{},l=I(t),a=y(l);s(`read the source (${l.length} .ts/.tsx files)`,l.length>0,"found no source to sweep — this rule is not measuring anything"),s(`the scanner finds display headings at all (${a.length}, at least ${e.minSites})`,a.length>=e.minSites,`found ${a.length} heading-face class lists with a text-[…] size`);const h=a.filter(n=>!m(i,n)&&n.steps.some(C));for(const n of Object.keys(i)){const r=n.split(/[:#]/)[0];s(`notSwept["${n}"] still matches a real site`,a.some(f=>f.file===r&&m(i,f)===i[n]))}s(`the census finds the headings a 40px ceiling has to bring down (${h.length}, at least ${e.minSwept})`,h.length>=e.minSwept,`only ${h.length} class lists declare a size above 40px`);const c=[];for(const n of h)for(const r of n.steps)for(const f of E){const d=u(r,{"--sf-heading-max":v,"--scale-heading":f});d>40&&c.push(`${n.file}:${n.line} @scale ${f} → ${d}px · ${r.token}`)}s("no swept display heading renders above the site's 40px ceiling",c.length===0,`${c.length} step(s) ignore it:
|
|
3
|
+
${c.join(`
|
|
4
|
+
`)}`);const p=a.flatMap(n=>n.steps.filter(r=>r.expr.includes("--sf-heading-max")).map(r=>({site:n,step:r})));s(`the sweep actually capped headings (${p.length} steps read --sf-heading-max, at least ${e.minCapped})`,p.length>=e.minCapped);const g=[];for(const{site:n,step:r}of p){const f=/^min\((.*),\s*var\(--sf-heading-max,[^)]*\)\)$/.exec(r.expr)?.[1];if(f!==void 0)for(const d of E){const $=S(f,r.vw,{"--scale-heading":d}),w=u(r,{"--scale-heading":d});$!==w&&g.push(`${n.file}:${n.line} @scale ${d}: ${$}px → ${w}px`)}}if(s("a tenant that sets no ceiling renders exactly what it renders today",g.length===0,`${g.length} step(s) moved:
|
|
5
|
+
${g.join(`
|
|
6
|
+
`)}`),e.reference!==void 0){const n=t.find(r=>r.path===e.reference);s(`the reference component still spells the ceiling the one way (${e.reference})`,!!n&&/var\(--sf-heading-max,\s*9999px\)/.test(n.text),`${e.reference} no longer reads var(--sf-heading-max,9999px)`)}const N=/^var\(--sf-heading-max,/,x=p.filter(({step:n})=>!N.test(n.expr)).filter(({step:n})=>!/^min\(.*,var\(--sf-heading-max,9999px\)\)$/.test(n.expr)).map(({site:n,step:r})=>`${n.file}:${n.line} · ${r.token}`);return s("every capped step is spelt the reference's way",x.length===0,`${x.length} second spelling(s):
|
|
7
|
+
${x.join(`
|
|
8
|
+
`)}`),o()}export{O as enclosingLiteral,D as headingCeilingNotes,H as headingCeilingSweep};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type LabelHintsOptions = {
|
|
3
|
+
readonly minLabels: number;
|
|
4
|
+
readonly minJsxLabels: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function hintsIn(label: string): string[];
|
|
7
|
+
export declare function labelHints(files: readonly SourceFile[], options: LabelHintsOptions): RuleReport;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{ruleChecks as m}from"./rule.js";import{readJsxLabels as $,readLabels as k}from"./support/label-extraction.js";function W(t){const e=[];return t.includes("==")?e.push("shows raw `==` to the merchant"):t.includes("=")&&e.push("carries an `x = y` legend"),/[()]/.test(t)&&e.push("carries a bracket hint"),t.includes(" — ")&&e.push("carries a ` — ` hint tail"),e}function j(t){return t.includes("=")?t.includes("==")?["shows raw `==` to the reader"]:["carries an `x = y` legend"]:[]}function x(t){const e=k(t.text),a=[];for(const r of e){const l=W(r.label);l.length&&a.push({where:`${t.path}:${r.line}`,label:r.label,why:l})}return{read:e.length,offenders:a}}function g(t){const e=$(t.text),a=[];for(const{line:r,label:l}of e){const i=j(l);i.length&&a.push({where:`${t.path}:${r}`,label:l,why:i})}return{read:e.length,offenders:a}}const w=t=>`${t.length} label(s) are hints, not names — the manual does the teaching:
|
|
2
|
+
`+t.map(e=>` ${e.where} ${JSON.stringify(e.label)} — ${e.why.join(", ")}`).join(`
|
|
3
|
+
`);function L(t,e){const{check:a,report:r}=m("label hints");{const n=x({path:"lib/puck/fixture.tsx",text:['heading: plainText("Heading (new line = break)"),','accent: plainText("Accent ==word=="),','legend: text("Size = px"),','tail: textarea("Body — one paragraph"),','body: plainText("Body", { placeholder: "A ==word== in the theme colour" }),','layout: { type: "select", label: "Layout",',' options: [{ label: "Split (editorial aside)", value: "split" }] },','defaultProps: { heading: "Our ==new== range" },'].join(`
|
|
4
|
+
`)}),s=y=>n.offenders.filter(h=>h.where.endsWith(`:${y}`)).flatMap(h=>h.why);a("W1 flags a bracket hint (and the `x = y` inside it)",s(1).includes("carries a bracket hint")&&s(1).includes("carries an `x = y` legend")),a("W1 flags raw `==` in a label",s(2).includes("shows raw `==` to the merchant")),a("W1 flags an `x = y` legend",s(3).includes("carries an `x = y` legend")),a("W1 flags a ` — ` hint tail",s(4).includes("carries a ` — ` hint tail")),a("W1 passes a placeholder, a select option and a default prop, and reads the labels it should",n.offenders.length===4&&n.read===6,JSON.stringify(n));const o=g({path:"components/Fixture.tsx",text:['<Field label="Accent (==word==)" />','<Field label="Size = px" />','<Field label="Name (optional)" />','<button aria-label="a = b" />'].join(`
|
|
5
|
+
`)});a("W2 flags `==` and `x = y` in a JSX label, leaves a bracket alone, and reads no aria-label",o.offenders.length===2&&o.read===3&&o.offenders[0].why[0]==="shows raw `==` to the reader",JSON.stringify(o))}const l=t.filter(n=>n.path.startsWith("lib/puck/")&&/\.(ts|tsx)$/.test(n.path)),i=t.filter(n=>n.path.startsWith("components/")&&n.path.endsWith(".tsx")),f=l.map(x),u=i.map(g),d=f.reduce((n,s)=>n+s.read,0),c=u.reduce((n,s)=>n+s.read,0),p=f.flatMap(n=>n.offenders),b=u.flatMap(n=>n.offenders);return a(`read >= ${e.minLabels} field labels across ${l.length} files under lib/puck (found ${d})`,d>=e.minLabels,"the reader is broken, not the panel"),a(`read >= ${e.minJsxLabels} JSX label attributes across ${i.length} files under components (found ${c})`,c>=e.minJsxLabels,"the reader is broken, not the panel"),a(`W1 every field label under lib/puck is a plain name (${d} read)`,p.length===0,w(p)),a(`W2 no JSX label under components shows \`=\` (${c} read)`,b.length===0,w(b)),r()}export{W as hintsIn,L as labelHints};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export declare const NEVER_WORDS: readonly string[];
|
|
3
|
+
export type ParkedLabel = {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly why: string;
|
|
7
|
+
};
|
|
8
|
+
export type LabelVocabularyOptions = {
|
|
9
|
+
readonly floors: Readonly<Record<string, number>>;
|
|
10
|
+
readonly minRegistryLabels: number;
|
|
11
|
+
readonly parked: readonly ParkedLabel[];
|
|
12
|
+
};
|
|
13
|
+
export declare function vocabularyBreaches(label: string, item: boolean): string[];
|
|
14
|
+
export declare function labelVocabulary(files: readonly SourceFile[], options: LabelVocabularyOptions): RuleReport;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{failedChecks as $,ruleChecks as x}from"./rule.js";import{isPanelFile as v,readLabels as w,readPanelLabels as C}from"./support/label-extraction.js";import{registryFiles as T}from"./support/registry-source.js";const p=["kicker","tagline","headline","blurb","description","copy","paragraph","photo","picture","cta","kind","class","grayscale","greyscale","colour","align","align items","text alignment","centre","catalogue","behaviour","labelled","stylised","stack","container","gutter","margin","site width","max width","contained","boxed","header offset","clearance"],j=o=>o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),R=p.map(o=>({word:o,pattern:new RegExp(`\\b${j(o)}s?\\b`)}));function H(o){return o.split(/\(| — /)[0].trim()}function k(o,c){const t=H(o).toLowerCase(),a=[];for(const{word:d,pattern:l}of R)l.test(t)&&a.push(`N10.1 "${d}"`);/^title\b/.test(t)&&!c&&a.push("N10.2 Title at section level"),/^heading\b/.test(t)&&c&&a.push("N10.2 Heading at item level");const e=/\b(button|link)\s+(text|label|href|url)$/.exec(t);return e&&a.push(`N10.3 "${e[1]} ${e[2]}"`),t==="text"&&a.push("N10.4 bare Text"),a}function N(o,c,t){const a=T(o),e=o.filter(v),d=new Map,l=[],h=(n,s)=>{for(const u of n){const y=s(u.text);d.set(u.path,y.length);for(const m of y)l.push({...m,file:u.path,why:k(m.label,m.item)})}};h(a,w),h(e,C);for(const[n,s]of Object.entries(c.floors)){const u=d.get(n);t(`${n} yields >= ${s} labels (found ${u??"none: not read"})`,u!==void 0&&u>=s,u===void 0?"it has a floor but was not read — it dropped out of the panels and the registry":"the reader is broken, not the panel")}const r=a.reduce((n,s)=>n+(d.get(s.path)??0),0);t(`the block registry yields >= ${c.minRegistryLabels} labels across ${a.length} files (found ${r})`,r>=c.minRegistryLabels,"the reader is broken, not the panel");const g=l.filter(n=>n.why.length>0),f=n=>c.parked.some(s=>s.file===n.file&&s.label===n.label),i=g.filter(n=>!f(n)),b=l.filter(n=>n.item).length;t(`every field label obeys N10.1–N10.4 (${l.length} read across ${a.length+e.length} files: ${l.length-b} section-level, ${b} item-level)`,i.length===0,`${i.length} break doc/naming.md N10:
|
|
2
|
+
`+i.map(n=>` ${n.file}:${n.line} ${n.item?"[item] ":"[section]"} ${JSON.stringify(n.label)} — ${n.why.join(", ")}`).join(`
|
|
3
|
+
`));for(const n of c.parked)t(`PARKED ${n.file} ${JSON.stringify(n.label)} still breaks N10 — ${n.why}`,g.some(s=>s.file===n.file&&s.label===n.label),"it no longer breaks N10 (renamed, or its row deleted) — take it out of the list");return{read:l,live:i}}function L(o,c){const{check:t,report:a}=x("label vocabulary");t(`N10.1 forbids ${p.length} words, each written once, lower-case and trimmed: ${[...p].sort().join(", ")}`,p.length>0&&new Set(p).size===p.length&&p.every(e=>e===e.trim().toLowerCase()&&e.length>0),JSON.stringify(p));{const e=(l,h=!1)=>k(l,h),d=p.filter(l=>{const h=`N10.1 "${l}"`,r=l[0].toUpperCase()+l.slice(1);return!e(r).includes(h)||!e(`Card ${l}s`).includes(h)});t("N10.1 catches every word on the list, alone and as a qualified plural",d.length===0,d.join(", ")),t("N10.1 …but not inside a longer word",e("Copyright").length===0&&e("Classic layout").length===0&&e("Stacking order").length===0,JSON.stringify([e("Copyright"),e("Classic layout"),e("Stacking order")])),t("N10.2 a section's label may not start Title, an item's may not start Heading",e("Title").includes("N10.2 Title at section level")&&e("Heading",!0).includes("N10.2 Heading at item level")),t("N10.2 …and each is right at its own level",e("Heading").length===0&&e("Title",!0).length===0&&e("Card title").length===0&&e("Section heading",!0).length===0),t("N10.3 nothing follows `button` or `link`",e("Button text").includes('N10.3 "button text"')&&e("Submit button label").includes('N10.3 "button label"')&&e("Link href").includes('N10.3 "link href"')&&e("Archive link URL").includes('N10.3 "link url"')),t("N10.3 …and the pair itself passes",e("Button").length===0&&e("Button link").length===0&&e("Archive link").length===0),t("N10.4 a bare Text fails; Alt text is a name",e("Text").includes("N10.4 bare Text")&&e("Alt text").length===0),t("the name is cut at a hint tail, so a hint cannot corrupt the verdict",e("Body (blank line = new paragraph)").length===0&&e("Copy — the words under it").includes('N10.1 "copy"'))}{const e=[{path:"lib/puck/config.tsx",text:['heading: plainText("Heading"),','eyebrow: plainText("Kicker"),','title: plainText("Title"),','cards: { type: "array", label: "Cards",'," arrayFields: {",' heading: plainText("Heading"),',' title: plainText("Card title"),'," },","},",'layout: { type: "select", label: "Layout",',' options: [{ label: "Centre", value: "center" }] },'].join(`
|
|
4
|
+
`)},{path:"components/FixturePanel.tsx",text:['import { SettingsField } from "efficient-easel/panel";',"const items = [",' { label: "Copy", onClick: () => copy() },',"];","const rows = [",' { key: "photo", label: "Photo", group: "media" },',"];",'<SettingsField label="Colour" />'].join(`
|
|
5
|
+
`)},{path:"components/HandRolled.tsx",text:'<div className="sf-field"><Row label="Gutter" /></div>'},{path:"components/ShopperForm.tsx",text:'<Field label="Description" />'}],d=x("fixture"),{read:l,live:h}=N(e,{floors:{"components/FixturePanel.tsx":5,"components/HandRolled.tsx":1,"components/Moved.tsx":1},minRegistryLabels:8,parked:[{file:"components/FixturePanel.tsx",label:"Colour",why:"fixture: still breaks"},{file:"lib/puck/config.tsx",label:"Headline",why:"fixture: renamed long ago"}]},d.check),r=$(d.report()).map(i=>i.name),g=i=>r.some(b=>b.startsWith(i)),f=l.map(i=>i.label);t("the fixture reads the registry's fields and the panels' rows, and no option, action or shopper's form",f.join(" · ")==="Heading · Kicker · Title · Cards · Heading · Card title · Layout · Photo · Colour · Gutter",f.join(" · ")),t("…and the breaches are the ones it wrote, the parked one waived",h.map(i=>`${i.label}=${i.why.join("+")}`).join(" · ")==='Kicker=N10.1 "kicker" · Title=N10.2 Title at section level · Heading=N10.2 Heading at item level · Photo=N10.1 "photo" · Gutter=N10.1 "gutter"',JSON.stringify(h.map(i=>[i.label,i.why]))),t("a file under its floor goes red",g("components/FixturePanel.tsx yields")&&!g("components/HandRolled.tsx yields"),r.join(" | ")),t("a floored file that is not read goes red",g("components/Moved.tsx yields"),r.join(" | ")),t("a registry under its floor goes red",g("the block registry yields"),r.join(" | ")),t("a PARKED label that no longer breaks N10 goes red, and one that still does is waived",g('PARKED lib/puck/config.tsx "Headline"')&&!g('PARKED components/FixturePanel.tsx "Colour"'),r.join(" | ")),t("the verdict goes red on a live breach",g("every field label obeys"),r.join(" | ")),t("…and nothing else did",r.length===5,r.join(" | "))}return N(o,c,t),a()}export{p as NEVER_WORDS,L as labelVocabulary,k as vocabularyBreaches};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type * as TS from "typescript";
|
|
2
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
3
|
+
export type MirroredPair = {
|
|
4
|
+
readonly row: string;
|
|
5
|
+
readonly constant: string;
|
|
6
|
+
readonly declaredIn: string;
|
|
7
|
+
readonly drawnIn: string;
|
|
8
|
+
};
|
|
9
|
+
export type MirroredRowsOptions = {
|
|
10
|
+
readonly mirrored?: readonly MirroredPair[];
|
|
11
|
+
readonly sameWord?: readonly string[];
|
|
12
|
+
readonly floors: {
|
|
13
|
+
readonly pairs: number;
|
|
14
|
+
readonly labels: number;
|
|
15
|
+
readonly panelRows: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function glyphOf(text: string): string;
|
|
19
|
+
export declare function mirroredRowsAgree(ts: typeof TS, files: readonly SourceFile[], options: MirroredRowsOptions): RuleReport;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import{ruleChecks as k}from"./rule.js";import{blockSource as E,registryFiles as S}from"./support/registry-source.js";const x="lib/puck/controls.tsx",j=["slider","select","segmented","textList"];function y(c){return/icon=\{<([A-Z][A-Za-z0-9]*Icon)\s*\/>\}/.exec(c)?.[1]??/<([A-Z][A-Za-z0-9]*Icon)\s*\/>/.exec(c)?.[1]??/<FieldGlyph noun="([a-z]+)"/.exec(c)?.[1]??"none"}function I(c){const s=new Map;for(const l of c)l.text.split(`
|
|
2
|
+
`).forEach((t,p)=>{const a=t.trim();if(!(a.startsWith("//")||a.startsWith("*")||a.startsWith("/*")))for(const h of j){if(new RegExp(`^(export )?function ${h}\\(`).test(a))continue;const d=t.match(new RegExp(`(^|[^.\\w])${h}\\(\\s*"([^"]+)"`));d&&s.set(d[2],[...s.get(d[2])??[],{file:l,line:p+1,builder:h}])}});return s}function R(c){const s=[];for(const l of c)l.text.split(`
|
|
3
|
+
`).forEach((t,p)=>{const a=t.match(/<SettingsField\s+label="([^"]+)"/);a&&s.push({file:l.path,line:p+1,label:a[1].trim(),glyph:y(t)})});return s}function W(c,s,l){const{check:t,report:p}=k("mirrored rows agree"),a=l.mirrored??[],h=l.sameWord??[];{const e={path:"fixture.tsx",text:["export function select(label: string) {}",'const a = select("Product", <BoxIcon />, options);','// select("In a comment", <BoxIcon />)','const b = usePreviewParts().select("Not a row");','const c = <SettingsField label="Product" icon={<BoxIcon />}>{x}</SettingsField>;'].join(`
|
|
4
|
+
`)},o=I([e]),n=R([e]);t("the builder-label reader finds a call, and not its declaration, a comment or another object's method",[...o.keys()].join()==="Product"&&o.get("Product")?.[0].line===2,JSON.stringify([...o.keys()])),t("the row reader finds a hand-drawn row and the glyph it wears",n.length===1&&n[0].label==="Product"&&n[0].glyph==="BoxIcon",JSON.stringify(n))}const d=s.find(e=>e.path===x);for(const e of a){const o=d?new RegExp(`export const ${e.constant} = \\{[\\s\\S]*?\\} as const;`).exec(d.text):null;t(`${e.constant} is declared in ${x}`,o!==null),o&&(t("…and carries the label",/label:\s*"/.test(o[0]),o[0].slice(0,120)),t("…and carries the glyph",/icon:\s*</.test(o[0]),o[0].slice(0,120)));let n=null;try{n=E(c,s,e.declaredIn)}catch{n=null}t(`read the ${e.declaredIn} block`,n!==null,`no block "${e.declaredIn}" among the files handed in`);const r=s.find(i=>i.path===e.drawnIn)?.text??null;t(`read ${e.drawnIn}`,r!==null);for(const[i,$]of[[`the ${e.declaredIn} block`,n],[e.drawnIn,r]])$!==null&&(t(`${i} spreads ${e.constant}`,new RegExp(`${e.constant}\\.(label|icon)`).test($)),t(`…and no longer hard-codes "${e.row}"`,!new RegExp(`"${e.row}"`).test($),`${i} still contains the literal`))}t(`the mirrored pairs were checked (${a.length}, at least ${l.floors.pairs}; an empty sweep proves nothing)`,a.length>=l.floors.pairs);const f=I([...S(s),...d?[d]:[]]),w=s.filter(e=>/^components\/[^/]+\.tsx$/.test(e.path)),u=R(w),g=[],m=[];for(const e of u){if(!f.has(e.label))continue;if(h.includes(e.label)){m.push({at:`${e.file}:${e.line}`,label:e.label,glyph:e.glyph});continue}const o=(f.get(e.label)??[]).map(n=>`${n.builder} @ ${n.file.path}:${n.line}`).join(" · ");g.push(`${e.file}:${e.line} "${e.label}" — also ${o}`)}t(`the builder-label scan read something (${f.size} labels, at least ${l.floors.labels})`,f.size>=l.floors.labels),t(`the component scan read something (${u.length} hand-drawn rows in ${w.length} component files, at least ${l.floors.panelRows})`,u.length>=l.floors.panelRows),t("no NEW hard-coded mirror",g.length===0,`
|
|
5
|
+
${g.join(`
|
|
6
|
+
`)}`);const b=h.filter(e=>!f.has(e));t("no same-word entry has outlived its subject",b.length===0,b.join(" · "));for(const e of h){const o=new Set,n=[];for(const r of m.filter(i=>i.label===e))o.add(r.glyph),n.push(`${r.at} → ${r.glyph}`);for(const r of f.get(e)??[]){const i=y(r.file.text.split(`
|
|
7
|
+
`).slice(r.line-1,r.line+6).join(`
|
|
8
|
+
`));o.add(i),n.push(`${r.file.path}:${r.line} → ${i}`)}t(`"${e}" draws ONE picture wherever it is declared`,o.size===1,`${o.size} different:
|
|
9
|
+
${n.join(`
|
|
10
|
+
`)}`),t(`…and "${e}" was actually found in both places`,n.length>=2,`${n.length}`)}return p()}export{y as glyphOf,W as mirroredRowsAgree};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type * as TS from "typescript";
|
|
2
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
3
|
+
export type JsxCommentFinding = {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly line: number;
|
|
6
|
+
readonly text: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function jsxCommentLines(ts: typeof TS, file: SourceFile): JsxCommentFinding[];
|
|
9
|
+
export declare function noCommentInJsx(ts: typeof TS, files: readonly SourceFile[]): RuleReport;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{ruleChecks as g}from"./rule.js";function h(n,a){const s=n.createSourceFile(a.path,a.text,n.ScriptTarget.Latest,!0,n.ScriptKind.TSX),l=[],o=t=>{if(n.isJsxText(t)){const r=t.getFullText(s),e=t.getFullStart();r.split(`
|
|
2
|
+
`).forEach((m,c)=>{const i=m.trim();if(!(i.startsWith("//")||i.startsWith("/*")||i.startsWith("*/"))||i.startsWith("//")&&/^\/\/[A-Za-z0-9.-]+\.[A-Za-z]{2,}\//.test(i))return;const f=e+r.split(`
|
|
3
|
+
`).slice(0,c).join(`
|
|
4
|
+
`).length+(c>0?1:0),{line:x}=s.getLineAndCharacterOfPosition(f);l.push({file:a.path,line:x+1,text:i.length>90?`${i.slice(0,90)}…`:i})})}n.forEachChild(t,o)};return o(s),l}function d(n,a){const{check:s,report:l}=g("no comment in JSX"),o=h(n,{path:"fixture.tsx",text:["const a = ("," <div>"," // this line is printed on the page"," {/* this one is a comment */}",' <a href="x">//cdn.example.com/x.js</a>'," </div>",");"].join(`
|
|
5
|
+
`)});s("the reader flags a `//` line between tags, and neither a braced comment nor a protocol-relative URL",o.length===1&&o[0].line===3,JSON.stringify(o));const t=a.filter(e=>e.path.endsWith(".tsx"));s(`read the .tsx files (${t.length})`,t.length>0,"found no .tsx files to scan — this rule is not measuring anything");const r=t.flatMap(e=>h(n,e));return s(`no \`//\` or \`/* */\` comment line sits between JSX tags (${t.length} .tsx files)`,r.length===0,`${r.length} line(s) React renders as TEXT on the page — wrap them in {/* ... */}:
|
|
6
|
+
`+r.map(e=>` ${e.file}:${e.line} ${e.text}`).join(`
|
|
7
|
+
`)),l()}export{h as jsxCommentLines,d as noCommentInJsx};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export declare const CONTROLS_FILE = "lib/puck/controls.tsx";
|
|
3
|
+
export type NoGlyphRowsOptions = {
|
|
4
|
+
readonly expected: number;
|
|
5
|
+
readonly minChars: number;
|
|
6
|
+
readonly controls?: {
|
|
7
|
+
readonly buildersFloor: number;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type NoGlyphRow = {
|
|
11
|
+
readonly at: string;
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function noGlyphRowsIn(file: SourceFile): NoGlyphRow[];
|
|
15
|
+
export declare function noGlyphRows(files: readonly SourceFile[], options: NoGlyphRowsOptions): RuleReport;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{isRegistryPath as m,registryFiles as x}from"./support/registry-source.js";import{ruleChecks as y}from"./rule.js";const d="lib/puck/controls.tsx",u=/doc\/naming\.md|N10\.\d|owner|GG|SS seat|editor-shell|\d{4}-\d{2}-\d{2}/;function w(r){const e=x(r),o=r.filter(n=>n.path===d&&!m(n.path)),h=r.filter(n=>/^components\/[^/]+\.tsx$/.test(n.path));return[...e,...o,...h]}function f(r){const e=r.text,o=[],h=/noGlyph\(/g;let n;for(;n=h.exec(e);){const l=e.lastIndexOf(`
|
|
2
|
+
`,n.index)+1,s=e.slice(l,n.index);if(/^\s*(\/\/|\*|\/\*)/.test(s)||(s.match(/"/g)??[]).length%2===1||/\bfunction\s+$/.test(s))continue;let c=1,a=n.index+n[0].length;for(;a<e.length&&c>0;a++)e[a]==="("?c+=1:e[a]===")"&&(c-=1);const i=[...e.slice(n.index+n[0].length,a-1).matchAll(/"((?:[^"\\]|\\.)*)"/g)].map(g=>g[1]).join(""),p=e.slice(0,n.index).split(`
|
|
3
|
+
`).length;o.push({at:`${r.path}:${p}`,reason:i})}return o}function $(r,e){const{check:o,report:h}=y("no-glyph rows");{const t=f({path:"fixture.tsx",text:["/**"," * `noGlyph(reason)` — the docblock names it; that is not a use."," */","export function noGlyph(reason: string) {",' if (!reason) throw new Error("noGlyph() needs a reason");',"}",'const a = select("Process", noGlyph(',` "A facet's options grow out of the CATALOGUE — owner, " +`,' "2026-08-29.",',"));",'// noGlyph("in a comment") is not a use','const b = noGlyph("");'].join(`
|
|
4
|
+
`)});o("the reader finds the two calls, and neither the docblock, the declaration, the string nor the comment",t.length===2&&t[0].at==="fixture.tsx:7"&&t[1].at==="fixture.tsx:12",JSON.stringify(t)),o("…reads a concatenated reason whole, so it can be checked for a source",t[0]?.reason==="A facet's options grow out of the CATALOGUE — owner, 2026-08-29."&&u.test(t[0].reason),JSON.stringify(t[0])),o("…and reads a bare call as no reason",t[1]?.reason==="",JSON.stringify(t[1]))}const n=w(r),l=n.reduce((t,i)=>t+i.text.length,0);o(`the scan read the files it meant to (${n.length} files, ${l} characters, at least ${e.minChars})`,l>=e.minChars,"the registry, lib/puck/controls.tsx or components/ moved — a scan that reads nothing finds no exceptions");const s=n.flatMap(f),c=s.filter(t=>!t.reason.trim());o("every noGlyph() states a reason",c.length===0,c.map(t=>t.at).join(" · "));const a=s.filter(t=>t.reason.trim()&&!u.test(t.reason));if(o("…and every reason names a source that can be checked",a.length===0,a.map(t=>`${t.at} — "${t.reason.slice(0,60)}"`).join(" · ")),o(`the no-glyph rows still number ${e.expected} (found ${s.length}${s.length?`: ${s.map(t=>t.at).join(", ")}`:""})`,s.length===e.expected,"a ruling was added or lost — move the number, with the ruling, in the guard that counts it"),e.controls){const t=r.find(p=>p.path===d)?.text??"";o(`read ${d}`,t.length>0,"the file noGlyph and iconOf are written in is not among the files handed in"),o(`noGlyph is still exported from ${d}`,/export function noGlyph\(/.test(t)),o("…and a marker is unwrapped rather than drawn as an empty gutter",/export function iconOf\(/.test(t)&&/isNoGlyph\(icon\) \? undefined : icon/.test(t),"SettingsField draws its icon span only when given an icon");const i=(t.match(/icon=\{iconOf\(icon\)\}/g)??[]).length;o(`…and the shared builders pass their icon through it (${i}, at least ${e.controls.buildersFloor})`,i>=e.controls.buildersFloor,`${i} builders`)}return h()}export{d as CONTROLS_FILE,$ as noGlyphRows,f as noGlyphRowsIn};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type OneColourControlOptions = {
|
|
3
|
+
readonly sharedControl?: string;
|
|
4
|
+
readonly minFiles: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const stripComments: (src: string) => string;
|
|
7
|
+
export declare function oneColourControl(files: readonly SourceFile[], options: OneColourControlOptions): RuleReport;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{ruleChecks as $}from"./rule.js";const n=a=>a.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(^|[^:])\/\/.*$/gm,"$1"),l=new RegExp('type=\\{?"color"'),d=new RegExp("<ColourPicker\\b"),c=new RegExp('className="[^"]*sf-(ink|colorinput)');function k(a,r){const{check:t,report:p}=$("one colour control");{const e='<input type="color" value={v} />',m="<ColourPicker value={v} onChange={f} />";t("the reader sees a native colour input",l.test(n(e))),t("…and not one a comment talks about",!l.test(n(`/* ${e} */
|
|
2
|
+
// ${e}`))),t("the reader sees a ColourPicker",d.test(n(m))),t("…and the retired classes in markup",c.test('<span className="sf-ink-row" />')&&c.test('<div className="x sf-colorinput" />'))}const s=a.filter(e=>/\.tsx?$/.test(e.path));t(`read the source (${s.length} TS/TSX files, at least ${r.minFiles})`,s.length>=r.minFiles,"fewer means the walk broke, and every check below is vacuous");const i=s.map(e=>[e.path,n(e.text)]),h=i.filter(([,e])=>l.test(e)).map(([e])=>e),o=i.filter(([,e])=>d.test(e)).map(([e])=>e);t("§1 nothing draws a native colour input — its window is the operating system's",h.length===0,h.join(", ")),r.sharedControl?(t(`read the shared control ${r.sharedControl}`,s.some(e=>e.path===r.sharedControl)),t("§1 exactly one file draws efficient-easel's ColourPicker",o.length===1,o.length===0?"NONE — the reader is broken, and every check below is vacuous":`${o.length}: ${o.join(", ")}`),t(`§1 …and it is the shared control, ${r.sharedControl}`,o[0]===r.sharedControl,o.join(", "))):t("§1 no file here draws efficient-easel's ColourPicker — the builder's shared control is the one that does",o.length===0,`${o.length}: ${o.join(", ")}`);const u=s.filter(e=>c.test(e.text)).map(e=>e.path);return t("§9 nothing draws a `sf-ink-` or `sf-colorinput` class any more",u.length===0,u.join(", ")),p()}export{k as oneColourControl,n as stripComments};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as TS from "typescript";
|
|
2
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
3
|
+
export type OneDropdownOptions = {
|
|
4
|
+
readonly notEditor?: Readonly<Record<string, readonly [number, string]>>;
|
|
5
|
+
readonly ownedNow?: Readonly<Record<string, readonly [number, string]>>;
|
|
6
|
+
readonly until?: Readonly<Record<string, readonly [number, string, () => boolean]>>;
|
|
7
|
+
readonly fieldSelectFloor: number;
|
|
8
|
+
readonly minFiles: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function oneDropdown(ts: typeof TS, files: readonly SourceFile[], options: OneDropdownOptions): RuleReport;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{ruleChecks as x}from"./rule.js";function S(c,d){const i=c.createSourceFile(d.path,d.text,c.ScriptTarget.Latest,!0,c.ScriptKind.TSX),n=[];let g=0;const h=s=>i.getLineAndCharacterOfPosition(s.getStart(i)).line+1,u=s=>{if(c.isJsxOpeningElement(s)||c.isJsxSelfClosingElement(s)){const a=s.tagName.getText(i);a==="select"&&n.push({file:d.path,line:h(s),what:"R1 raw <select> — use efficient-easel's FieldSelect"}),(a==="FieldSelect"||/^FieldSelect</.test(a))&&g++}if(c.isCallExpression(s)){const a=s.expression.getText(i),r=s.arguments[0];(a==="h"||/(^|\.)createElement$/.test(a))&&r&&c.isStringLiteralLike(r)&&r.text==="select"&&n.push({file:d.path,line:h(s),what:`R2 ${a}("select") — use efficient-easel's FieldSelect`})}c.forEachChild(s,u)};return u(i),{selects:n,fieldSelect:g}}function v(c,d,i){const{check:n,report:g}=x("one dropdown"),h=i.notEditor??{},u=i.ownedNow??{},s=i.until??{};{const e=["const a = <select value={v} onChange={f}><option>x</option></select>;",'const b = h("select", { value: v });','const c = React.createElement("select", null);',"// a comment about <select> is not a use","const d = <FieldSelect value={v} options={o} onChange={f} />;","const e = <FieldSelect<string> value={v} options={o} onChange={f} />;",'const s = "<select>"; const t = <Select />;'].join(`
|
|
2
|
+
`),{selects:t,fieldSelect:f}=S(c,{path:"fixture.tsx",text:e}),l=t.map(o=>o.line);n("R1 flags a JSX <select>",t.some(o=>o.line===1&&o.what.startsWith("R1"))),n('R2 flags h("select")',t.some(o=>o.line===2&&o.what.startsWith("R2"))),n('R2 flags React.createElement("select")',t.some(o=>o.line===3&&o.what.startsWith("R2"))),n("a comment, a string and a <Select> component are not uses",!l.includes(4)&&!l.includes(7)),n("FieldSelect passes and is counted, generic form too",!l.includes(5)&&!l.includes(6)&&f===2)}const a=d.filter(e=>/\.tsx?$/.test(e.path)&&!e.path.endsWith(".d.ts")),r=new Map;let w=0;for(const e of a){const t=S(c,e);t.selects.length&&r.set(e.path,t.selects),w+=t.fieldSelect}const p=[];for(const[e,t]of r)e in h||e in u||e in s||p.push(...t);n("no raw <select> in editor code",p.length===0,`${p.length} hit(s):
|
|
3
|
+
`+p.map(e=>`${e.file}:${e.line} ${e.what}`).join(`
|
|
4
|
+
`));for(const[e,[t,f]]of Object.entries(h)){const l=r.get(e)?.length??0;n(`shopper select still real and still ${t}: ${e} (${f})`,l===t,`found ${l} — fix or remove the entry`)}for(const[e,[t,f]]of Object.entries(u)){const l=r.get(e)?.length??0;n(`left for another card, exactly ${t}: ${e} (${f})`,l===t,l>t?`found ${l} — a NEW raw <select> in a file that is being replaced; use FieldSelect`:`found ${l} — that card has landed; remove this entry`)}for(const[e,[t,f,l]]of Object.entries(s)){const o=r.get(e)?.length??0;n(`waiting, exactly ${t}: ${e} (until ${f})`,o===t,`found ${o} — swapped? remove this entry`);let m=!1;try{m=l()}catch($){n(`could read whether ${e}'s wait is over`,!1,$.message);continue}n(`${e} is still waiting (installed efficient-easel lacks it)`,!m,`the installed efficient-easel now has ${f} — swap ${e} to FieldSelect and remove its UNTIL entry`)}return n(`walked the editor source (${a.length} TS/TSX files, at least ${i.minFiles})`,a.length>=i.minFiles),n(`found >= ${i.fieldSelectFloor} <FieldSelect> uses (found ${w})`,w>=i.fieldSelectFloor,"the builder + the swapped rows; fewer means a row went back to a raw <select> or the reader broke"),g()}export{v as oneDropdown};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type OneEditableListPresence = {
|
|
3
|
+
readonly controls?: string;
|
|
4
|
+
readonly locationsPanel?: string;
|
|
5
|
+
readonly tickerPanel?: string;
|
|
6
|
+
};
|
|
7
|
+
export type OneEditableListOptions = {
|
|
8
|
+
readonly minFiles: number;
|
|
9
|
+
readonly minSheets: number;
|
|
10
|
+
readonly presence?: OneEditableListPresence;
|
|
11
|
+
readonly listModules?: {
|
|
12
|
+
readonly locationsTree: object;
|
|
13
|
+
readonly controls: object;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare function oneEditableList(files: readonly SourceFile[], options: OneEditableListOptions): RuleReport;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{ruleChecks as $}from"./rule.js";const y=s=>s.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(^|[^:"'`])\/\/[^\n]*/g,"$1").replace(/\{\/\*[\s\S]*?\*\/\}/g,""),v=[["Move up / Move down buttons",/aria-label=["'{`]*Move (up|down)/],['"+ Add …" buttons',/>\s*\+ Add [A-Za-z]|["'`]\+ Add [A-Za-z]/],["sf-textlist markup (the ticker's old list)",/["'` ]sf-textlist/],["TextListEditor",/\bTextListEditor\b/]];function M(s,h){const{check:U,report:w}=$("one editable list"),i=(t,d,o)=>{let p=!1,A=o;try{p=typeof d=="function"?d():d}catch(e){A=String(e.message??e)}return U(t,p,A)},x=`
|
|
2
|
+
<button type="button" style={iconBtn} aria-label="Move up" title="Move up" onClick={() => swap(1, 0)}>↑</button>
|
|
3
|
+
<button type="button" style={addBtn} onClick={addGroup}>+ Add group</button>
|
|
4
|
+
<div className="sf-textlist"><button className="sf-textlist-add">+ Add item</button></div>
|
|
5
|
+
export function TextListEditor() {}
|
|
6
|
+
`;for(const[t,d]of v)i(`control: the rule for ${t} catches it in old code`,d.test(y(x)));const a=s.filter(t=>t.path.endsWith(".tsx"));i(`read the editor's .tsx files (${a.length}, at least ${h.minFiles})`,a.length>=h.minFiles&&a.length>0,`only ${a.length}`);for(const[t,d]of v){const o=a.filter(p=>d.test(y(p.text))).map(p=>p.path);i(`no ${t} in the .tsx files`,o.length===0,o.join(", "))}const u=s.filter(t=>t.path.endsWith(".css"));i(`read the editor's stylesheets (${u.length}, at least ${h.minSheets})`,u.length>=h.minSheets);const r=u.filter(t=>/\.sf-textlist\b/.test(t.text.replace(/\/\*[\s\S]*?\*\//g,"")));i("no .sf-textlist rule left behind",r.length===0,r.map(t=>t.path).join(", "));const m=t=>{const d=s.find(o=>o.path===t);if(!d)throw new Error(`${t} was not among the files read`);return y(d.text)},c=h.presence??{};if(c.controls!==void 0){const t=c.controls;i(`${t} draws lines of copy with easel's ListField`,()=>/import \{[^}]*\bListField\b[^}]*\} from "efficient-easel\/fields"/.test(m(t)))}if(c.locationsPanel!==void 0){const t=c.locationsPanel;i(`the Locations panel (${t}) draws its groups and cities with ListField`,()=>(m(t).match(/<ListField\b/g)??[]).length===2)}if(c.tickerPanel!==void 0){const t=c.tickerPanel;i(`the ticker panel (${t}) draws Ticker items with the same TextListField the textList builder draws`,()=>/<TextListField\b/.test(m(t)))}return h.listModules&&E(i,h.listModules),w()}function E(s,h){const U=n=>()=>{throw new Error(`${n} does not exist`)},w=(n,l)=>n[l]??U(l),i=w(h.locationsTree,"writeGroupCities"),x=w(h.locationsTree,"writeGroupOrder"),a=w(h.controls,"aimAfterListChange"),u=(n,l)=>({name:n,group:l,groupName:""}),r=n=>n.map(l=>`${l.group}:${l.name}`).join(" "),m=(n,l,f)=>{const g=n.slice();return[g[l],g[f]]=[g[f],g[l]],g},c=(n,l,f,g)=>{const b=l.slice();return[b[f],b[f+g]]=[b[f+g],b[f]],b.flatMap(L=>n.filter(S=>(S.group??"")===L))},t=[u("Sydney","AU"),u("Austin","US"),u("Perth","AU"),u("Miami","US"),u("Hobart","AU")],d=[0,2,4],o=d.map(n=>t[n]);s("a city one step down saves what the old ↓ saved",()=>r(i(t,"AU",[o[1],o[0],o[2]]))===r(m(t,0,2))),s("a city one step up saves what the old ↑ saved",()=>r(i(t,"AU",[o[0],o[2],o[1]]))===r(m(t,4,2))),s("a city dragged from first to last leaves every other group's cards in their places",()=>{const n=i(t,"AU",[o[1],o[2],o[0]]);return n[1]===t[1]&&n[3]===t[3]&&n.length===t.length}),s("…and puts the group's own cards in the new order",()=>{const n=i(t,"AU",[o[1],o[2],o[0]]);return r(d.map(l=>n[l]))==="AU:Perth AU:Hobart AU:Sydney"});const p={...u("","AU")};s('"+ Add city" at the end saves what the old button saved (the new card last)',()=>r(i(t,"AU",[...o,p]))===r([...t,p])),s("a group one step down saves what the old ↓ saved",()=>r(x(t,["US","AU"],()=>[]))===r(c(t,["AU","US"],0,1)));const A=u("","Group 3");s('a new group gets its first city ("+ Add group" put it last)',()=>r(x(t,["AU","US","Group 3"],n=>n==="Group 3"?[A]:[]))===r([...c(t,["US","AU"],0,1),A]));const e=[{text:"a"},{text:"b"},{text:"c"}];s("typing in the aimed line keeps the aim",()=>a(e,[e[0],{text:"bb"},e[2]],1)===1),s("taking out a line BELOW the aimed one keeps the aim",()=>a(e,[e[0],e[1]],1)===1),s("taking out the aimed line lets go",()=>a(e,[e[0],e[2]],1)===null),s("taking out a line ABOVE the aimed one lets go (its box now holds other words)",()=>a(e,[e[1],e[2]],1)===null),s("a line added at the end keeps the aim",()=>a(e,[...e,{text:""}],1)===1),s("a line added above the aimed one lets go",()=>a(e,[e[0],{text:""},e[1],e[2]],1)===null),s("a move lets go (the caret going with the line aims again)",()=>a(e,[e[1],e[0],e[2]],1)===null),s("nothing aimed stays nothing aimed",()=>a(e,[e[1],e[0]],null)===null)}export{M as oneEditableList};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type OneEditorListOptions = {
|
|
3
|
+
readonly kept?: Readonly<Record<string, string>>;
|
|
4
|
+
readonly minFiles: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function drawsList(text: string): boolean;
|
|
7
|
+
export declare function oneEditorList(files: readonly SourceFile[], options: OneEditorListOptions): RuleReport;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{ruleChecks as d}from"./rule.js";function s(i){return i.includes('role="listbox"')||i.includes('type="checkbox"')&&/role="group"/.test(i)&&/set\w*Open\(/.test(i)}function u(i,o){const{check:t,report:c}=d("one editor list"),n=o.kept??{};t("a listbox is a list",s('<ul role="listbox">')),t("tick boxes in a group the file opens itself are a list",s(`const [o, setOpen] = useState(false);
|
|
2
|
+
<div role="group"><input type="checkbox" /></div>
|
|
3
|
+
setOpen(true);`)),t("a lone tick box is not a list",!s('<input type="checkbox" />')),t("FieldSelect is not a list of the file's own",!s("<FieldSelect options={o} multiple inline />"));const l=i.filter(e=>e.path.endsWith(".tsx"));t(`read the source (${l.length} .tsx files, at least ${o.minFiles})`,l.length>=o.minFiles,"fewer means the walk broke, and the census is vacuous");const r=l.filter(e=>s(e.text)).map(e=>e.path);for(const[e,p]of Object.entries(n))t(`the kept list is still real: ${e} (${p})`,r.includes(e),"it no longer draws a list — remove the stale entry");const a=r.filter(e=>!(e in n));return t("no second editor list: every file that draws a list of its own is a kept one",a.length===0,`a second editor list: ${a.join(", ")} — draw it with efficient-easel's FieldSelect`),c()}export{s as drawsList,u as oneEditorList};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RuleReport, type SourceFile } from "./rule.js";
|
|
2
|
+
export type OneRightClickOptions = {
|
|
3
|
+
readonly editor?: {
|
|
4
|
+
readonly stylesheets: readonly string[];
|
|
5
|
+
readonly locationsRows: string;
|
|
6
|
+
};
|
|
7
|
+
readonly minPanelFiles: number;
|
|
8
|
+
readonly registryLabelFloor: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function oneRightClick(files: readonly SourceFile[], options: OneRightClickOptions): RuleReport;
|