efficient-web-builder 2.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.
Files changed (134) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/components/BlogPostSource.js +1 -1
  3. package/dist/components/Breadcrumb.js +1 -1
  4. package/dist/components/Btn.js +1 -1
  5. package/dist/components/CardShadowField.js +1 -1
  6. package/dist/components/CategoryBar.js +1 -1
  7. package/dist/components/ChartSection.js +1 -1
  8. package/dist/components/ChatSettingsPanel.js +1 -1
  9. package/dist/components/ClearColoursDialog.js +1 -1
  10. package/dist/components/ContactLocations.js +5 -5
  11. package/dist/components/CornersField.js +1 -1
  12. package/dist/components/DocBody.js +1 -1
  13. package/dist/components/DuplicateToPageMenu.js +1 -1
  14. package/dist/components/FeaturedCover.js +1 -1
  15. package/dist/components/FieldGlyph.js +1 -1
  16. package/dist/components/Footprint.js +1 -1
  17. package/dist/components/HeadingAudit.js +1 -1
  18. package/dist/components/HeroSection.js +1 -1
  19. package/dist/components/HoverEffectField.js +1 -1
  20. package/dist/components/ImageBlock.js +1 -1
  21. package/dist/components/ImageFocusRow.js +1 -1
  22. package/dist/components/LocationsGroupedField.js +1 -1
  23. package/dist/components/LongRead.js +1 -1
  24. package/dist/components/MediaUploadField.js +1 -1
  25. package/dist/components/MockupSection.js +1 -1
  26. package/dist/components/PagesPanel.js +1 -1
  27. package/dist/components/PartResetDot.js +1 -1
  28. package/dist/components/PartSettingsPanel.js +1 -1
  29. package/dist/components/PostCard.js +1 -1
  30. package/dist/components/ProcessSteps.js +1 -1
  31. package/dist/components/RMark.js +1 -1
  32. package/dist/components/Reel.js +1 -1
  33. package/dist/components/SectionEditor.js +1 -1
  34. package/dist/components/SectionHeader.js +1 -1
  35. package/dist/components/SectionThumbnail.js +1 -1
  36. package/dist/components/SlotChildRows.js +1 -1
  37. package/dist/components/SourceStatus.js +1 -1
  38. package/dist/components/Split.js +1 -1
  39. package/dist/components/StandaloneSectionEditor.js +1 -1
  40. package/dist/components/StylesPanel.js +1 -1
  41. package/dist/components/Testimonials.js +1 -1
  42. package/dist/components/TextStylesField.js +1 -1
  43. package/dist/components/ThemeCard.js +1 -1
  44. package/dist/components/ThemeColourField.js +1 -1
  45. package/dist/components/ThemeField.js +1 -1
  46. package/dist/components/TickerSettingsPanel.js +1 -1
  47. package/dist/components/Timeline.js +1 -1
  48. package/dist/components/TimelineTrack.js +1 -1
  49. package/dist/components/TitleWithButtons.js +1 -1
  50. package/dist/components/WebsiteScreenParts.js +1 -1
  51. package/dist/components/WebsiteSettingsPanel.js +1 -1
  52. package/dist/components/goldPhrases.js +1 -1
  53. package/dist/components/storefront-editor.css +25 -212
  54. package/dist/lib/content/contact.js +3 -3
  55. package/dist/lib/content/subscribe.js +1 -1
  56. package/dist/lib/puck/MarkupToolbar.js +1 -1
  57. package/dist/lib/puck/blocks/general/CategoryBar/fields.js +1 -1
  58. package/dist/lib/puck/blocks/general/Chart/fields.js +1 -1
  59. package/dist/lib/puck/blocks/general/FAQ/render.js +1 -1
  60. package/dist/lib/puck/blocks/general/FeaturedCover/render.js +1 -1
  61. package/dist/lib/puck/blocks/general/Footprint/fields.js +1 -1
  62. package/dist/lib/puck/blocks/general/IntroBleed/render.js +1 -1
  63. package/dist/lib/puck/blocks/general/LogoBlock/fields.js +1 -1
  64. package/dist/lib/puck/blocks/general/LongRead/render.js +1 -1
  65. package/dist/lib/puck/blocks/general/Masthead/render.js +1 -1
  66. package/dist/lib/puck/blocks/general/Mockup/fields.js +1 -1
  67. package/dist/lib/puck/blocks/general/OrderSummary/render.js +1 -1
  68. package/dist/lib/puck/blocks/general/ParagraphsA/render.js +1 -1
  69. package/dist/lib/puck/blocks/general/PostCard/render.js +1 -1
  70. package/dist/lib/puck/blocks/general/ProcessSteps/render.js +1 -1
  71. package/dist/lib/puck/blocks/general/Series/render.js +1 -1
  72. package/dist/lib/puck/blocks/general/Split/render.js +1 -1
  73. package/dist/lib/puck/blocks/general/Testimonials/fields.js +1 -1
  74. package/dist/lib/puck/blocks/general/Testimonials/render.js +1 -1
  75. package/dist/lib/puck/blocks/general/TimelineTrack/render.js +1 -1
  76. package/dist/lib/puck/blocks/placeholders.js +1 -1
  77. package/dist/lib/puck/controls.js +1 -1
  78. package/dist/lib/puck/field-groups.js +1 -1
  79. package/dist/lib/puck/footer-block-row-name.js +1 -1
  80. package/dist/lib/puck/geocode-resolve.js +1 -1
  81. package/dist/lib/puck/locations-tree.js +2 -2
  82. package/dist/lib/puck/neutralization-contracts.js +1 -1
  83. package/dist/lib/puck/part-rows.js +1 -1
  84. package/dist/lib/puck/section-catalog.js +1 -1
  85. package/dist/lib/puck/section-descriptions.js +1 -1
  86. package/dist/lib/sections/divider-style.js +1 -1
  87. package/dist/lib/sections/product-card-style.js +1 -1
  88. package/dist/lib/sections/ticker-style.js +1 -1
  89. package/dist/lib/source-rules/accent-token-name.js +2 -2
  90. package/dist/lib/source-rules/button-corners.js +1 -1
  91. package/dist/lib/source-rules/editor-chrome-note.js +2 -2
  92. package/dist/lib/source-rules/editor-context-menu.js +2 -2
  93. package/dist/lib/source-rules/editor-puck-drag-off.js +2 -2
  94. package/dist/lib/source-rules/efficient-easel-version.js +1 -1
  95. package/dist/lib/source-rules/every-button-follows-the-button-colour.js +4 -4
  96. package/dist/lib/source-rules/flat-theme-palette.js +2 -2
  97. package/dist/lib/source-rules/full-width-means-full.js +1 -1
  98. package/dist/lib/source-rules/heading-ceiling-sweep.js +3 -3
  99. package/dist/lib/source-rules/label-hints.js +4 -4
  100. package/dist/lib/source-rules/label-vocabulary.js +4 -4
  101. package/dist/lib/source-rules/mirrored-rows-agree.js +4 -4
  102. package/dist/lib/source-rules/no-comment-in-jsx.js +2 -2
  103. package/dist/lib/source-rules/no-glyph-rows.js +2 -2
  104. package/dist/lib/source-rules/one-colour-control.js +2 -2
  105. package/dist/lib/source-rules/one-dropdown.js +2 -2
  106. package/dist/lib/source-rules/one-editable-list.js +3 -3
  107. package/dist/lib/source-rules/one-editor-list.js +1 -1
  108. package/dist/lib/source-rules/one-right-click.js +2 -2
  109. package/dist/lib/source-rules/one-switch.js +1 -1
  110. package/dist/lib/source-rules/one-up-level-link.js +7 -7
  111. package/dist/lib/source-rules/panel-colour-tokens.js +7 -7
  112. package/dist/lib/source-rules/panel-no-divider-lines.js +5 -5
  113. package/dist/lib/source-rules/panel-one-arrow.js +1 -1
  114. package/dist/lib/source-rules/panel-one-hover.js +2 -2
  115. package/dist/lib/source-rules/panel-rows-use-the-shared-row.js +5 -5
  116. package/dist/lib/source-rules/part-slider-fills-row.js +8 -8
  117. package/dist/lib/source-rules/part-style-naming.js +1 -1
  118. package/dist/lib/source-rules/puck-document-surfaces.js +6 -6
  119. package/dist/lib/source-rules/reduced-motion.js +2 -2
  120. package/dist/lib/source-rules/route-content-width.js +2 -2
  121. package/dist/lib/source-rules/rule.js +1 -1
  122. package/dist/lib/source-rules/section-theme-preview.js +2 -2
  123. package/dist/lib/source-rules/site-square-by-default.js +1 -1
  124. package/dist/lib/source-rules/site-width.js +2 -2
  125. package/dist/lib/source-rules/size-token-paints.js +2 -2
  126. package/dist/lib/source-rules/social-icons-import.js +1 -1
  127. package/dist/lib/source-rules/source-status-is-shared.js +1 -1
  128. package/dist/lib/source-rules/style-face-matches-size.js +3 -3
  129. package/dist/lib/source-rules/support/part-slider-call-sites.js +4 -4
  130. package/dist/lib/source-rules/support/registry-source.js +1 -1
  131. package/dist/lib/source-rules/theme-mode-controls-gone.js +3 -3
  132. package/dist/lib/source-rules/upload-intake-guard.js +2 -2
  133. package/dist/lib/source-rules/word-styles.js +2 -2
  134. package/package.json +5 -5
@@ -1 +1 @@
1
- const p=["general","commerce","service"],o=[{key:"openers",title:"Hero & intros",defaultExpanded:!0,templateable:!0,sections:[{type:"Hero",label:"Hero Slideshow",set:"general"},{type:"HeroSingle",label:"Hero",set:"general"},{type:"IntroBleed",label:"Scroll-Over Text",set:"general"}]},{key:"shop",title:"Shop & galleries",templateable:!0,sections:[{type:"Collections",label:"Collections",set:"commerce"},{type:"BrandStrip",label:"Brand Strip",set:"commerce"},{type:"AlloyWheelsListing",label:"List",set:"commerce"},{type:"CatalogFilterBar",label:"Catalog Filter Bar",set:"commerce"},{type:"ProductGrid",label:"Product Grid",set:"commerce"},{type:"FitmentFinder",label:"Fitment Form",set:"commerce"},{type:"Gallery",label:"Tabbed Gallery",set:"commerce"},{type:"GalleryB",label:"Customer Photo Grid",set:"commerce"},{type:"SpecStrip",label:"Spec Strip",set:"general"}]},{key:"product",title:"Product page",templateable:!0,sections:[{type:"ProductHero",label:"Product Hero",set:"commerce"},{type:"SpecTable",label:"Spec Table",set:"commerce"},{type:"ProcessSteps",label:"Image & Text Columns",set:"general"},{type:"DetailRows",label:"Feature Rows",set:"commerce"},{type:"FitmentTable",label:"Fitment Table",set:"commerce"},{type:"TyreSizes",label:"Tyre Sizes",set:"commerce"},{type:"TyreFitment",label:"Tyre Fitment Form",set:"commerce"},{type:"EuTyreLabel",label:"EU Tyre Label",set:"commerce"},{type:"RelatedProducts",label:"Related Products",set:"commerce"},{type:"BundleOffer",label:"Bundle Products",set:"commerce"}]},{key:"checkout",title:"Checkout & ordering",templateable:!0,sections:[{type:"OrderSummary",label:"Order Table",set:"general"},{type:"DeliveryOptions",label:"Delivery Options",set:"commerce"},{type:"PaymentBadges",label:"Payment Methods",set:"general"},{type:"CheckoutCTA",label:"Ready-to-Order Banner",set:"commerce"}]},{key:"about",title:"About & brand story",templateable:!0,sections:[{type:"Manifesto",label:"Brand Text",set:"general"},{type:"Timeline",label:"Story Timeline",set:"general"},{type:"TimelineTrack",label:"Milestone Timeline",set:"general"},{type:"Footprint",label:"Locations Map",set:"general"}]},{key:"content",title:"Text & content",templateable:!0,sections:[{type:"Prose",label:"Editorial Text",set:"general"},{type:"ParagraphsA",label:"Lead Text",set:"general"},{type:"Split",label:"Image & Text Split",set:"general"},{type:"Cards",label:"Cards",set:"general"},{type:"KeyFeatures",label:"Key Features",set:"general"}]},{key:"proof",title:"Stats & social proof",templateable:!0,sections:[{type:"Stats",label:"Stats",set:"general"},{type:"StatsParagraph",label:"Stats with Text",set:"general"},{type:"Chart",label:"Chart",set:"general"},{type:"Mockup",label:"Mockup Card",set:"general"},{type:"Testimonials",label:"Testimonials",set:"general"},{type:"Reviews",label:"Reviews",set:"service"},{type:"Logos",label:"Logos",set:"general"}]},{key:"promo",title:"Promotions & sign-ups",templateable:!0,sections:[{type:"Banner",label:"Banner",set:"general"},{type:"TitleWithButtons",label:"Call-to-Action Heading",set:"general"},{type:"Ticker",label:"Scrolling Bar",set:"general"},{type:"Subscribe",label:"Signup Form",set:"service"}]},{key:"blog",title:"Blog & journal",templateable:!0,sections:[{type:"Masthead",label:"Issue Header",set:"general"},{type:"FeaturedCover",label:"Feature Banner",set:"general"},{type:"LongRead",label:"Article Text",set:"general"},{type:"Series",label:"Recurring Columns",set:"general"},{type:"Events",label:"Latest Posts",set:"service"}]},{key:"navigation",title:"Navigation & wayfinding",templateable:!0,sections:[{type:"Breadcrumb",label:"Breadcrumb",set:"general"},{type:"CategoryBar",label:"Category Bar",set:"general"},{type:"DocTabs",label:"Document Tabs",set:"general"}]},{key:"legal",title:"Legal & documents",templateable:!0,sections:[{type:"DocMasthead",label:"Document Header",set:"general"},{type:"DocBody",label:"Document Text",set:"general"},{type:"DocMetaCards",label:"Document Detail Cards",set:"general"}]},{key:"contact",title:"Contact, FAQ & forms",templateable:!0,sections:[{type:"FAQ",label:"Q&A",set:"general"},{type:"ContactRouting",label:"Contact Form",set:"service"},{type:"ContactLocations",label:"Contact / Locations",set:"general"},{type:"SubmitBuild",label:"Photo Submission Form",set:"service"},{type:"ReviewForm",label:"Review Form",set:"service"},{type:"Form",label:"Form",set:"service"},{type:"AppointmentBooking",label:"Booking Form",set:"service"}]},{key:"structure",title:"Layout, structure & embeds",templateable:!1,sections:[{type:"SectionHeader",label:"Section Header",set:"general"},{type:"Flex",label:"Box",set:"general"},{type:"Spacer",label:"Spacer",set:"general"},{type:"Divider",label:"Divider",set:"general"},{type:"CustomHTML",label:"Custom HTML",set:"general"}]}];function h(){return Object.fromEntries(o.map(e=>[e.key,{title:e.title,components:e.sections.map(t=>t.type),...e.defaultExpanded?{defaultExpanded:!0}:{}}]))}const l={Grid:'Lewis, 2026-09-14: Box and Grid duplicate each other and it feels confusing. 2026-09-15 he chose the order: give Box uneven widths FIRST, then merge Grid into Box. Shown three ways to pick the widths (mockup "Uneven Box Widths"), he chose A \u2014 shape pictures on the Box\'s `Block widths` row, the same "a row is a shape, pick a picture of it" rule he set for the photo wall on 2026-09-07.\n\nWHAT REPLACES IT: a Box set to One row, with `Block widths` at Equal or a shape (Big left, Big right, Bigger left, Bigger right) and its own `Columns on phone`. Measured on PRODUCTION 2026-09-16 ~09:28 AEST (database `efficient-system`, all 10 tenant schemas, 128 storefront pages, both `layout` and `published_layout` \u2014 256 documents): 2 Grid nodes, and they are ONE Grid counted twice \u2014 the 9DIEM homepage on 9diem.com (`xtreme_mudder` schema), once as draft and once as published. It is top-level, holds three `ImageBlock`s at `columns` 3 \u2014 equal thirds \u2014 and NO block sets `Positions`. A Box draws that with `Block widths` at Equal.\n\n\u26A0\uFE0F THE COUNT MOVES; THE RULE MUST NOT DEPEND ON IT. This entry has already carried three different readings. A local-clone snapshot said no Grid used `Positions`. A PRODUCTION reading on 2026-09-15 ~13:15 AEST found 6 nodes on two sites, 4 of them spanned \u2014 hartesmetal.com.au homepage and about, 4 columns, spans 1 + 3. That one was TRUE when it was taken and stopped being true the same day: those two pages were rebuilt on Box (`Block widths: Bigger right`), and on 2026-09-16 they hold zero Grid nodes and are built from `Flex`. \u26D4 So the rule is NOT "there are still N spanned Grids". NEVER delete the Grid component or its `gridSpan` handling, whatever a census says \u2014 Grid still renders, still edits, still drags, and a merchant may rebuild a page either way on any day. Re-run the census on production before anyone PROPOSES deleting either, and read any number written here as the reading of its own day, not as the state of the world.\n\nWHAT A MERCHANT LOSES: a new Grid, and with it a fixed "N per row" that wraps by itself past three across, and the per-block `Positions` number. Box wraps with `Wrap`, but by content width, not by a fixed count.\n\nTHE TYPE KEY STAYS AND SO DOES THE COMPONENT. Every page holding a Grid still renders through it and can still be edited, dragged and added to; no document is rewritten and NO MIGRATION RUNS \u2014 the owner redoes live sections himself.\n\n\u26A0\uFE0F AND IT STAYS DROPPABLE WHERE IT LIVES. The drop rule in lib/puck/config.tsx read the nestable containers off the catalogue\'s `structure` row, so leaving the catalogue would have made every open slot \u2014 a Box included \u2014 refuse a Grid already stored inside one. `scripts/test-retired-grid-stays-editable.tsx` \xA73 pins it.',CompatibleTyres:'Lewis, 2026-09-09: "Do not bother counting. Delete Compatible Tyres and use the new one. The new one needs complete settings \u2014 it must have every setting our cards have." Shown three ways to finish it, he chose C: retire from the menu now, and take styling the card text as its own card.\n\nWHAT REPLACES IT: `RelatedProducts` with its `Show` dropdown set to "Tyres that fit this product". That block is the GENERIC relationship section from card 1128 \u2014 it knows nothing about wheels or tyres, it asks the registry for a relationship by name. The two have rendered through the SAME renderer (`DynamicRelatedProducts`) since that card; this one is the pre-registry one-off that was never removed.\n\n\u26A0\uFE0F THE SWAP WAS NOT SAFE UNTIL #1305, AND SAYING SO IS THE POINT. The first old-versus-new table put in front of the owner was WRONG. `DynamicRelatedProducts` returns EARLY on a tyre answer and draws this component, so the seven card settings, the head split and the Width control all stopped at that call \u2014 nine settings, measured NO EFFECT, on precisely the relationship that replaces this block. #1283 gave the new block the card settings, #1299 the head split and its own Width, #1305 carried all of it onto the tyre branch. Only then was the claim true.\n\nWHAT WAS MEASURED BEFORE RETIRING, four ways, because the premise decides the card:\n \u2022 settable text kinds (SECTION_TEXT_KINDS): both blocks ABSENT \u2014 the map holds only TimelineTrack and Footprint, both AUTHORED lists\n \u2022 read-only text regions: neither `components/CompatibleTyres` nor `components/TyreCard` stamps one\n \u2022 panel rows through resolveFields: this block 31, `RelatedProducts` 42. This block\'s only style-ish key is `_sStyle`, the bucket HEADER, not a control\n \u2022 head text (num/title/description) is authored on both, so `_partStyles` reaches it identically\nSo on the CARD runs the two are zero against zero \u2014 neither can style them \u2014 and everywhere else the new block is ahead. Styling those runs needs a section-level prop family and a paint path for DERIVED text, which no section has yet: a part is anchored by matching an AUTHORED prop\'s string against the DOM, and `lib/puck/source-bound-parts` keeps hydrated copy unreachable on purpose so a merchant cannot type into a copy of a product name and lose it at the next sync. That is its own card, and `lib/sections/text-regions` is the read-only half that already ships.\n\nTHE TYPE KEY STAYS AND SO DOES THE COMPONENT, the same contract Subscribe, Split and SubscribeBlogs have. Every page holding a `CompatibleTyres` still renders through it, no published document is rewritten, and NO MIGRATION RUNS \u2014 the owner\'s standing ruling of 2026-09-05: he redoes live sections himself and deletes the old ones when he does. He also said explicitly not to count them first.\n\n\u26A0\uFE0F AND THIS COMPONENT IS LOAD-BEARING FOR ITS OWN REPLACEMENT: `DynamicRelatedProducts` draws it for EVERY tyre answer. Deleting it would blank the block that replaces it, not just the pages still holding this one.',SubscribeBlogs:"card 66396 \u2014 folded into `Subscribe`, which now chooses its rail's content with a `Type` control (the newsletter's own issues, or live blog posts). The two shared 21 of their fields and rendered THE SAME BYTES from equivalent rows, so offering both asked a merchant to choose between two names for one section. The type key stays because it is stored on published pages, and the component stays because those pages still render through it."},m=["Grid"],y=Array.from(new Set([...o.flatMap(e=>e.sections.map(t=>t.type)),...Object.keys(l)])),u=Object.fromEntries(o.flatMap(e=>e.sections.map(t=>[t.type,t.label]))),i={Heading:"general",Text:"general",Eyebrow:"general",ImageBlock:"general",LogoBlock:"general",ButtonBlock:"general",PostCard:"general",HeroSlide:"general",HeroGroup:"general",Integrations:"general",Grid:"general",CollectionCard:"commerce",GalleryItem:"commerce",ProductRef:"commerce",CompatibleTyres:"commerce",Header:"commerce",SearchBlock:"commerce",CartBlock:"commerce",CountrySwitchBlock:"commerce",SubmitBox:"service",BookingBox:"service",SubscribeBlogs:"service",Footer:"service",FooterLogo:"service",FooterMenu:"service",FooterDivider:"service",FooterGroup:"service",FooterText:"service",FooterEmailSignup:"service",FooterSocial:"service",MenuBlock:"service",EmailFormBlock:"service",SocialLogosBlock:"service",LegalLineBlock:"service",SiteHeader:"service",SiteFooter:"service"},a={...i,...Object.fromEntries(o.flatMap(e=>e.sections.map(t=>[t.type,t.set])))};function b(e){return Object.prototype.hasOwnProperty.call(a,e)?a[e]:void 0}function r(e){return Object.keys(a).filter(t=>a[t]===e).sort()}const g=r("general"),T=r("commerce"),S=r("service"),c="Flex",s="Box",E=`${s}es`;function d(e){return e==="section"?"Blank section":s}function w(e,t,n){return e===c?d(t):n}export{y as ALL_SECTION_TYPES,p as BLOCK_SETS,s as BOX_LABEL,E as BOX_LABEL_PLURAL,T as COMMERCE_BLOCKS,g as GENERAL_BLOCKS,c as LEVEL_NAMED_TYPE,m as RETIRED_NESTABLE_SECTIONS,l as RETIRED_SECTIONS,o as SECTION_CATALOG,u as SECTION_LABELS,S as SERVICE_BLOCKS,i as UNLISTED_BLOCK_SETS,b as blockSetOf,r as blocksInSet,h as buildPuckCategories,w as labelAtLevel,d as labelForFlex};
1
+ const p=["general","commerce","service"],o=[{key:"openers",title:"Hero & intros",defaultExpanded:!0,templateable:!0,sections:[{type:"Hero",label:"Hero Slideshow",set:"general"},{type:"HeroSingle",label:"Hero",set:"general"},{type:"IntroBleed",label:"Scroll-Over Text",set:"general"}]},{key:"shop",title:"Shop & galleries",templateable:!0,sections:[{type:"Collections",label:"Collections",set:"commerce"},{type:"BrandStrip",label:"Brand Strip",set:"commerce"},{type:"AlloyWheelsListing",label:"List",set:"commerce"},{type:"CatalogFilterBar",label:"Catalog Filter Bar",set:"commerce"},{type:"ProductGrid",label:"Product Grid",set:"commerce"},{type:"FitmentFinder",label:"Fitment Form",set:"commerce"},{type:"Gallery",label:"Tabbed Gallery",set:"commerce"},{type:"GalleryB",label:"Customer Photo Grid",set:"commerce"},{type:"SpecStrip",label:"Spec Strip",set:"general"}]},{key:"product",title:"Product page",templateable:!0,sections:[{type:"ProductHero",label:"Product Hero",set:"commerce"},{type:"SpecTable",label:"Spec Table",set:"commerce"},{type:"ProcessSteps",label:"Image & Text Columns",set:"general"},{type:"DetailRows",label:"Feature Rows",set:"commerce"},{type:"FitmentTable",label:"Fitment Table",set:"commerce"},{type:"TyreSizes",label:"Tyre Sizes",set:"commerce"},{type:"TyreFitment",label:"Tyre Fitment Form",set:"commerce"},{type:"EuTyreLabel",label:"EU Tyre Label",set:"commerce"},{type:"RelatedProducts",label:"Related Products",set:"commerce"},{type:"BundleOffer",label:"Bundle Products",set:"commerce"}]},{key:"checkout",title:"Checkout & ordering",templateable:!0,sections:[{type:"OrderSummary",label:"Order Table",set:"general"},{type:"DeliveryOptions",label:"Delivery Options",set:"commerce"},{type:"PaymentBadges",label:"Payment Methods",set:"general"},{type:"CheckoutCTA",label:"Ready-to-Order Banner",set:"commerce"}]},{key:"about",title:"About & brand story",templateable:!0,sections:[{type:"Manifesto",label:"Brand Text",set:"general"},{type:"Timeline",label:"Story Timeline",set:"general"},{type:"TimelineTrack",label:"Milestone Timeline",set:"general"},{type:"Footprint",label:"Locations Map",set:"general"}]},{key:"content",title:"Text & content",templateable:!0,sections:[{type:"Prose",label:"Editorial Text",set:"general"},{type:"ParagraphsA",label:"Lead Text",set:"general"},{type:"Split",label:"Image & Text Split",set:"general"},{type:"Cards",label:"Cards",set:"general"},{type:"KeyFeatures",label:"Key Features",set:"general"}]},{key:"proof",title:"Stats & social proof",templateable:!0,sections:[{type:"Stats",label:"Stats",set:"general"},{type:"StatsParagraph",label:"Stats with Text",set:"general"},{type:"Chart",label:"Chart",set:"general"},{type:"Mockup",label:"Mockup Card",set:"general"},{type:"Testimonials",label:"Testimonials",set:"general"},{type:"Reviews",label:"Reviews",set:"service"},{type:"Logos",label:"Logos",set:"general"}]},{key:"promo",title:"Promotions & sign-ups",templateable:!0,sections:[{type:"Banner",label:"Banner",set:"general"},{type:"TitleWithButtons",label:"Call-to-Action Heading",set:"general"},{type:"Ticker",label:"Scrolling Bar",set:"general"},{type:"Subscribe",label:"Signup Form",set:"service"}]},{key:"blog",title:"Blog & journal",templateable:!0,sections:[{type:"Masthead",label:"Issue Header",set:"general"},{type:"FeaturedCover",label:"Feature Banner",set:"general"},{type:"LongRead",label:"Article Text",set:"general"},{type:"Series",label:"Recurring Columns",set:"general"},{type:"Events",label:"Latest Posts",set:"service"}]},{key:"navigation",title:"Navigation & wayfinding",templateable:!0,sections:[{type:"Breadcrumb",label:"Breadcrumb",set:"general"},{type:"CategoryBar",label:"Category Bar",set:"general"},{type:"DocTabs",label:"Document Tabs",set:"general"}]},{key:"legal",title:"Legal & documents",templateable:!0,sections:[{type:"DocMasthead",label:"Document Header",set:"general"},{type:"DocBody",label:"Document Text",set:"general"},{type:"DocMetaCards",label:"Document Detail Cards",set:"general"}]},{key:"contact",title:"Contact, FAQ & forms",templateable:!0,sections:[{type:"FAQ",label:"Q&A",set:"general"},{type:"ContactRouting",label:"Contact Form",set:"service"},{type:"ContactLocations",label:"Contact / Locations",set:"general"},{type:"SubmitBuild",label:"Photo Submission Form",set:"service"},{type:"ReviewForm",label:"Review Form",set:"service"},{type:"Form",label:"Form",set:"service"},{type:"AppointmentBooking",label:"Booking Form",set:"service"}]},{key:"structure",title:"Layout, structure & embeds",templateable:!1,sections:[{type:"SectionHeader",label:"Section Header",set:"general"},{type:"Flex",label:"Box",set:"general"},{type:"Spacer",label:"Spacer",set:"general"},{type:"Divider",label:"Divider",set:"general"},{type:"CustomHTML",label:"Custom HTML",set:"general"}]}];function h(){return Object.fromEntries(o.map(e=>[e.key,{title:e.title,components:e.sections.map(t=>t.type),...e.defaultExpanded?{defaultExpanded:!0}:{}}]))}const l={Grid:'Lewis, 2026-09-14: Box and Grid duplicate each other and it feels confusing. 2026-09-15 he chose the order: give Box uneven widths FIRST, then merge Grid into Box. Shown three ways to pick the widths (mockup "Uneven Box Widths"), he chose A — shape pictures on the Box\'s `Block widths` row, the same "a row is a shape, pick a picture of it" rule he set for the photo wall on 2026-09-07.\n\nWHAT REPLACES IT: a Box set to One row, with `Block widths` at Equal or a shape (Big left, Big right, Bigger left, Bigger right) and its own `Columns on phone`. Measured on PRODUCTION 2026-09-16 ~09:28 AEST (database `efficient-system`, all 10 tenant schemas, 128 storefront pages, both `layout` and `published_layout` — 256 documents): 2 Grid nodes, and they are ONE Grid counted twice — the 9DIEM homepage on 9diem.com (`xtreme_mudder` schema), once as draft and once as published. It is top-level, holds three `ImageBlock`s at `columns` 3 — equal thirds — and NO block sets `Positions`. A Box draws that with `Block widths` at Equal.\n\n⚠️ THE COUNT MOVES; THE RULE MUST NOT DEPEND ON IT. This entry has already carried three different readings. A local-clone snapshot said no Grid used `Positions`. A PRODUCTION reading on 2026-09-15 ~13:15 AEST found 6 nodes on two sites, 4 of them spanned — hartesmetal.com.au homepage and about, 4 columns, spans 1 + 3. That one was TRUE when it was taken and stopped being true the same day: those two pages were rebuilt on Box (`Block widths: Bigger right`), and on 2026-09-16 they hold zero Grid nodes and are built from `Flex`. ⛔ So the rule is NOT "there are still N spanned Grids". NEVER delete the Grid component or its `gridSpan` handling, whatever a census says — Grid still renders, still edits, still drags, and a merchant may rebuild a page either way on any day. Re-run the census on production before anyone PROPOSES deleting either, and read any number written here as the reading of its own day, not as the state of the world.\n\nWHAT A MERCHANT LOSES: a new Grid, and with it a fixed "N per row" that wraps by itself past three across, and the per-block `Positions` number. Box wraps with `Wrap`, but by content width, not by a fixed count.\n\nTHE TYPE KEY STAYS AND SO DOES THE COMPONENT. Every page holding a Grid still renders through it and can still be edited, dragged and added to; no document is rewritten and NO MIGRATION RUNS — the owner redoes live sections himself.\n\n⚠️ AND IT STAYS DROPPABLE WHERE IT LIVES. The drop rule in lib/puck/config.tsx read the nestable containers off the catalogue\'s `structure` row, so leaving the catalogue would have made every open slot — a Box included — refuse a Grid already stored inside one. `scripts/test-retired-grid-stays-editable.tsx` §3 pins it.',CompatibleTyres:'Lewis, 2026-09-09: "Do not bother counting. Delete Compatible Tyres and use the new one. The new one needs complete settings — it must have every setting our cards have." Shown three ways to finish it, he chose C: retire from the menu now, and take styling the card text as its own card.\n\nWHAT REPLACES IT: `RelatedProducts` with its `Show` dropdown set to "Tyres that fit this product". That block is the GENERIC relationship section from card 1128 — it knows nothing about wheels or tyres, it asks the registry for a relationship by name. The two have rendered through the SAME renderer (`DynamicRelatedProducts`) since that card; this one is the pre-registry one-off that was never removed.\n\n⚠️ THE SWAP WAS NOT SAFE UNTIL #1305, AND SAYING SO IS THE POINT. The first old-versus-new table put in front of the owner was WRONG. `DynamicRelatedProducts` returns EARLY on a tyre answer and draws this component, so the seven card settings, the head split and the Width control all stopped at that call — nine settings, measured NO EFFECT, on precisely the relationship that replaces this block. #1283 gave the new block the card settings, #1299 the head split and its own Width, #1305 carried all of it onto the tyre branch. Only then was the claim true.\n\nWHAT WAS MEASURED BEFORE RETIRING, four ways, because the premise decides the card:\n • settable text kinds (SECTION_TEXT_KINDS): both blocks ABSENT — the map holds only TimelineTrack and Footprint, both AUTHORED lists\n • read-only text regions: neither `components/CompatibleTyres` nor `components/TyreCard` stamps one\n • panel rows through resolveFields: this block 31, `RelatedProducts` 42. This block\'s only style-ish key is `_sStyle`, the bucket HEADER, not a control\n • head text (num/title/description) is authored on both, so `_partStyles` reaches it identically\nSo on the CARD runs the two are zero against zero — neither can style them — and everywhere else the new block is ahead. Styling those runs needs a section-level prop family and a paint path for DERIVED text, which no section has yet: a part is anchored by matching an AUTHORED prop\'s string against the DOM, and `lib/puck/source-bound-parts` keeps hydrated copy unreachable on purpose so a merchant cannot type into a copy of a product name and lose it at the next sync. That is its own card, and `lib/sections/text-regions` is the read-only half that already ships.\n\nTHE TYPE KEY STAYS AND SO DOES THE COMPONENT, the same contract Subscribe, Split and SubscribeBlogs have. Every page holding a `CompatibleTyres` still renders through it, no published document is rewritten, and NO MIGRATION RUNS — the owner\'s standing ruling of 2026-09-05: he redoes live sections himself and deletes the old ones when he does. He also said explicitly not to count them first.\n\n⚠️ AND THIS COMPONENT IS LOAD-BEARING FOR ITS OWN REPLACEMENT: `DynamicRelatedProducts` draws it for EVERY tyre answer. Deleting it would blank the block that replaces it, not just the pages still holding this one.',SubscribeBlogs:"card 66396 — folded into `Subscribe`, which now chooses its rail's content with a `Type` control (the newsletter's own issues, or live blog posts). The two shared 21 of their fields and rendered THE SAME BYTES from equivalent rows, so offering both asked a merchant to choose between two names for one section. The type key stays because it is stored on published pages, and the component stays because those pages still render through it."},m=["Grid"],y=Array.from(new Set([...o.flatMap(e=>e.sections.map(t=>t.type)),...Object.keys(l)])),u=Object.fromEntries(o.flatMap(e=>e.sections.map(t=>[t.type,t.label]))),i={Heading:"general",Text:"general",Eyebrow:"general",ImageBlock:"general",LogoBlock:"general",ButtonBlock:"general",PostCard:"general",HeroSlide:"general",HeroGroup:"general",Integrations:"general",Grid:"general",CollectionCard:"commerce",GalleryItem:"commerce",ProductRef:"commerce",CompatibleTyres:"commerce",Header:"commerce",SearchBlock:"commerce",CartBlock:"commerce",CountrySwitchBlock:"commerce",SubmitBox:"service",BookingBox:"service",SubscribeBlogs:"service",Footer:"service",FooterLogo:"service",FooterMenu:"service",FooterDivider:"service",FooterGroup:"service",FooterText:"service",FooterEmailSignup:"service",FooterSocial:"service",MenuBlock:"service",EmailFormBlock:"service",SocialLogosBlock:"service",LegalLineBlock:"service",SiteHeader:"service",SiteFooter:"service"},a={...i,...Object.fromEntries(o.flatMap(e=>e.sections.map(t=>[t.type,t.set])))};function b(e){return Object.prototype.hasOwnProperty.call(a,e)?a[e]:void 0}function r(e){return Object.keys(a).filter(t=>a[t]===e).sort()}const g=r("general"),T=r("commerce"),S=r("service"),c="Flex",s="Box",E=`${s}es`;function d(e){return e==="section"?"Blank section":s}function w(e,t,n){return e===c?d(t):n}export{y as ALL_SECTION_TYPES,p as BLOCK_SETS,s as BOX_LABEL,E as BOX_LABEL_PLURAL,T as COMMERCE_BLOCKS,g as GENERAL_BLOCKS,c as LEVEL_NAMED_TYPE,m as RETIRED_NESTABLE_SECTIONS,l as RETIRED_SECTIONS,o as SECTION_CATALOG,u as SECTION_LABELS,S as SERVICE_BLOCKS,i as UNLISTED_BLOCK_SETS,b as blockSetOf,r as blocksInSet,h as buildPuckCategories,w as labelAtLevel,d as labelForFlex};
@@ -1 +1 @@
1
- const e={Hero:"Full-screen opener with rotating slides.",HeroSingle:"Full-screen image or video with a headline.",IntroBleed:"Full-bleed intro image the next section scrolls over.",Collections:"Product collections in a tidy grid.",AlloyWheelsListing:"A full product-listing page: filter rail, sortable card grid and pagination, from the live catalog. Choose which product types it shows.",CatalogFilterBar:"Filters the collection grids below it \u2014 a sticky row of Pills (the default), or Columns, one per filter you switch on.",ProductGrid:"Hand-picked products in a tidy grid.",FitmentFinder:"A vehicle picker \u2014 make, model, year and trim resolve the OEM stock fitment live.",Gallery:"Two image galleries with tabs and filters.",GalleryB:"Customer photos in a card grid with a click-to-open lightbox.",SpecStrip:"A thin one-line spec strip \u2014 small-caps items spread across the width, as a dark band or a bare hairline row.",ProductHero:"A product hero \u2014 image beside price, bullets and a buy button.",SpecTable:"A spec sheet \u2014 labeled rows of product specifications.",ProcessSteps:"A heading over columns of image, title, blurb and specs.",DetailRows:"Repeating full-width rows \u2014 eyebrow, title and copy beside a numbered close-up image, alternating sides each row.",FitmentTable:"A fitment table \u2014 per-size bolt patterns, offsets and load ratings, filterable by diameter.",CompatibleTyres:"The current product's compatible tyres \u2014 image, size, key specs and price per tyre, with an inquiry link. Display only.",RelatedProducts:"The current product's related items in a tidy row \u2014 the product page's built-in 'Pair with' cross-sell.",OrderSummary:"A display order-summary panel \u2014 items, totals and a note.",DeliveryOptions:"Delivery tiers as cards \u2014 name, window and price, one highlighted.",PaymentBadges:"A payment-methods trust strip with reassurance lines.",CheckoutCTA:"A centered 'ready to order?' headline with the checkout button.",Manifesto:"A bold brand statement beside a full-height image, with a stat strip.",ParagraphsA:"An editorial text panel with a stat strip and founders' note.",StatsParagraph:"An editorial heading and paragraph over a row of theme-color-accented stat cards.",Chart:"A titled chart \u2014 bars, line or donut \u2014 drawn from your own labeled data points.",Mockup:"A headline beside a stylized product-UI mockup card \u2014 spec sheet, fitment, portal, kanban and more.",Prose:"A plain editorial text section \u2014 eyebrow, heading and body copy.",Timeline:"An editorial aside beside a rail-marked timeline of dated entries.",TimelineTrack:"A horizontal year track of milestones \u2014 company history or roadmap.",Footprint:"A world map of your locations with a legend \u2014 designed here, sold worldwide.",TitleWithButtons:"A big headline beside a stack of call-to-action buttons.",Split:"Side-by-side image and text in two columns.",Cards:"A heading over a grid of free-text title-and-blurb cards.",FAQ:"A frequently-asked-questions accordion with a header.",Testimonials:"Customer quotes in a grid \u2014 the quote, a name and a role.",Masthead:"A journal issue header with a lead statement and contents.",Breadcrumb:"A slim trail showing where the reader is (Home / Journal\u2026).",CategoryBar:"A sticky row of category tabs with counts and links.",FeaturedCover:"A featured article \u2014 big cover image beside a headline and dek.",LongRead:"A long-form article spread with a pull-quote and cover image.",Series:"A section heading over a grid of column cards.",Subscribe:"A newsletter signup beside a list of recent issues.",SubscribeBlogs:"A newsletter signup beside your latest blog posts, pulled live.",Events:"Latest journal posts in a three-card row.",DocMasthead:"A document header \u2014 headline, metadata and standfirst.",DocTabs:"A sticky tab bar to switch between documents.",DocBody:"Long-form document body with an 'On this page' contents list.",DocMetaCards:"A three-up meta strip for the foot of a document.",Banner:"Wide promo strip with background image.",Stats:"Highlight key numbers \u2014 years, designs.",KeyFeatures:"A theme-color chip and heading over a grid of short feature labels.",Logos:"A row of partner or brand logos.",Ticker:"A scrolling marquee of short phrases, full width.",Reviews:"Approved customer reviews in a star-rated card grid, pulled live.",SubmitBuild:"A 'Show us yours.' headline with a photo-submission form.",ReviewForm:"A write-a-review form \u2014 submissions wait for your approval before they show.",ContactRouting:"Pick who to reach, then a form that routes the message.",ContactLocations:"Your locations as a city gallery plus address and map cards.",SectionHeader:"A numbered section title with optional link.",Grid:"Multi-column container for blocks.",Flex:"An empty section. Add whatever you want inside.",Spacer:"Vertical breathing room between sections.",Divider:"A thin horizontal rule between sections.",CustomHTML:"Drop in your own sanitized HTML.",Heading:"A single heading line.",Text:"A paragraph of body copy.",ImageBlock:"A standalone image, at the width you set.",ButtonBlock:"A call-to-action button."};export{e as SECTION_DESCRIPTIONS};
1
+ const e={Hero:"Full-screen opener with rotating slides.",HeroSingle:"Full-screen image or video with a headline.",IntroBleed:"Full-bleed intro image the next section scrolls over.",Collections:"Product collections in a tidy grid.",AlloyWheelsListing:"A full product-listing page: filter rail, sortable card grid and pagination, from the live catalog. Choose which product types it shows.",CatalogFilterBar:"Filters the collection grids below it — a sticky row of Pills (the default), or Columns, one per filter you switch on.",ProductGrid:"Hand-picked products in a tidy grid.",FitmentFinder:"A vehicle picker — make, model, year and trim resolve the OEM stock fitment live.",Gallery:"Two image galleries with tabs and filters.",GalleryB:"Customer photos in a card grid with a click-to-open lightbox.",SpecStrip:"A thin one-line spec strip — small-caps items spread across the width, as a dark band or a bare hairline row.",ProductHero:"A product hero — image beside price, bullets and a buy button.",SpecTable:"A spec sheet — labeled rows of product specifications.",ProcessSteps:"A heading over columns of image, title, blurb and specs.",DetailRows:"Repeating full-width rows — eyebrow, title and copy beside a numbered close-up image, alternating sides each row.",FitmentTable:"A fitment table — per-size bolt patterns, offsets and load ratings, filterable by diameter.",CompatibleTyres:"The current product's compatible tyres — image, size, key specs and price per tyre, with an inquiry link. Display only.",RelatedProducts:"The current product's related items in a tidy row — the product page's built-in 'Pair with' cross-sell.",OrderSummary:"A display order-summary panel — items, totals and a note.",DeliveryOptions:"Delivery tiers as cards — name, window and price, one highlighted.",PaymentBadges:"A payment-methods trust strip with reassurance lines.",CheckoutCTA:"A centered 'ready to order?' headline with the checkout button.",Manifesto:"A bold brand statement beside a full-height image, with a stat strip.",ParagraphsA:"An editorial text panel with a stat strip and founders' note.",StatsParagraph:"An editorial heading and paragraph over a row of theme-color-accented stat cards.",Chart:"A titled chart — bars, line or donut — drawn from your own labeled data points.",Mockup:"A headline beside a stylized product-UI mockup card — spec sheet, fitment, portal, kanban and more.",Prose:"A plain editorial text section — eyebrow, heading and body copy.",Timeline:"An editorial aside beside a rail-marked timeline of dated entries.",TimelineTrack:"A horizontal year track of milestones — company history or roadmap.",Footprint:"A world map of your locations with a legend — designed here, sold worldwide.",TitleWithButtons:"A big headline beside a stack of call-to-action buttons.",Split:"Side-by-side image and text in two columns.",Cards:"A heading over a grid of free-text title-and-blurb cards.",FAQ:"A frequently-asked-questions accordion with a header.",Testimonials:"Customer quotes in a grid — the quote, a name and a role.",Masthead:"A journal issue header with a lead statement and contents.",Breadcrumb:"A slim trail showing where the reader is (Home / Journal…).",CategoryBar:"A sticky row of category tabs with counts and links.",FeaturedCover:"A featured article — big cover image beside a headline and dek.",LongRead:"A long-form article spread with a pull-quote and cover image.",Series:"A section heading over a grid of column cards.",Subscribe:"A newsletter signup beside a list of recent issues.",SubscribeBlogs:"A newsletter signup beside your latest blog posts, pulled live.",Events:"Latest journal posts in a three-card row.",DocMasthead:"A document header — headline, metadata and standfirst.",DocTabs:"A sticky tab bar to switch between documents.",DocBody:"Long-form document body with an 'On this page' contents list.",DocMetaCards:"A three-up meta strip for the foot of a document.",Banner:"Wide promo strip with background image.",Stats:"Highlight key numbers — years, designs.",KeyFeatures:"A theme-color chip and heading over a grid of short feature labels.",Logos:"A row of partner or brand logos.",Ticker:"A scrolling marquee of short phrases, full width.",Reviews:"Approved customer reviews in a star-rated card grid, pulled live.",SubmitBuild:"A 'Show us yours.' headline with a photo-submission form.",ReviewForm:"A write-a-review form — submissions wait for your approval before they show.",ContactRouting:"Pick who to reach, then a form that routes the message.",ContactLocations:"Your locations as a city gallery plus address and map cards.",SectionHeader:"A numbered section title with optional link.",Grid:"Multi-column container for blocks.",Flex:"An empty section. Add whatever you want inside.",Spacer:"Vertical breathing room between sections.",Divider:"A thin horizontal rule between sections.",CustomHTML:"Drop in your own sanitized HTML.",Heading:"A single heading line.",Text:"A paragraph of body copy.",ImageBlock:"A standalone image, at the width you set.",ButtonBlock:"A call-to-action button."};export{e as SECTION_DESCRIPTIONS};
@@ -1 +1 @@
1
- import{parseOption as t}from"../parse-option.js";const n="line",o=[{label:"Line",value:"line"},{label:"Stripes \xB7 fine",value:"stripes-fine"},{label:"Stripes \xB7 medium",value:"stripes-medium"},{label:"Stripes \xB7 bold",value:"stripes-bold"}],p={"stripes-fine":{ink:4,period:12},"stripes-medium":{ink:8,period:18},"stripes-bold":{ink:14,period:28}},d=28,s="-45deg",l="color-mix(in oklab, var(--accent) 50%, var(--bg))";function a(e){return e!=="line"}function c(e){return t(o,n,e)}function E(e){if(!a(e))return null;const{ink:r,period:i}=p[e];return`repeating-linear-gradient(${s}, ${l} 0 ${r}px, transparent ${r}px ${i}px)`}export{n as DEFAULT_DIVIDER_STYLE,o as DIVIDER_STYLE_OPTIONS,s as STRIPE_ANGLE,d as STRIPE_BAND_HEIGHT_PX,l as STRIPE_INK,a as isStriped,c as parseDividerStyle,E as stripeBackground};
1
+ import{parseOption as t}from"../parse-option.js";const n="line",o=[{label:"Line",value:"line"},{label:"Stripes · fine",value:"stripes-fine"},{label:"Stripes · medium",value:"stripes-medium"},{label:"Stripes · bold",value:"stripes-bold"}],p={"stripes-fine":{ink:4,period:12},"stripes-medium":{ink:8,period:18},"stripes-bold":{ink:14,period:28}},d=28,s="-45deg",l="color-mix(in oklab, var(--accent) 50%, var(--bg))";function a(e){return e!=="line"}function c(e){return t(o,n,e)}function E(e){if(!a(e))return null;const{ink:r,period:i}=p[e];return`repeating-linear-gradient(${s}, ${l} 0 ${r}px, transparent ${r}px ${i}px)`}export{n as DEFAULT_DIVIDER_STYLE,o as DIVIDER_STYLE_OPTIONS,s as STRIPE_ANGLE,d as STRIPE_BAND_HEIGHT_PX,l as STRIPE_INK,a as isStriped,c as parseDividerStyle,E as stripeBackground};
@@ -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\xB001, N\xB002",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\xB0" 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
+ 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 \u25CF",value:"dot"},{label:"Cross \u2715",value:"cross"},{label:"Slash /",value:"slash"},{label:"Triangle \u25BC",value:"triangle"},{label:"None",value:"none"}],x={dot:"\u25CF",cross:"\u2715",slash:"/",triangle:"\u25BC",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};
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};
@@ -1,4 +1,4 @@
1
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 \u2014 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):
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
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 \u2014 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 \u2014 ${e.why}`)}return h()}export{f as ACCENT_TOKEN_EXTENSIONS,k as accentTokenName,p as goldTokenUses};
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};
@@ -1,2 +1,2 @@
1
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("\xA76/\xA77 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(`\xA71 read the stylesheet ${e.stylesheet}`,!!t)){const s=/\.sf-button\s*\{([^}]*)\}/.exec(p(t.text));n(`\xA71 ${e.stylesheet} carries a \`.sf-button\` rule`,s!==null,"without it the class is decoration and the setting reaches no button"),s&&(n(`\xA71 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 \u2014 got: ${s[1].trim()}`),n("\xA71 the .sf-button rule does not clip",!/overflow/.test(s[1]),`the trailing arrow translates 3px right on hover and would be shaved \u2014 got: ${s[1].trim()}`))}}if(e.button!==void 0){const t=h(e.button);n(`\xA72 read the shared button ${e.button}`,!!t)&&n(`\xA72 ${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(`\xA72 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(`\xA72 ${r}: the rendered button carries \`${f}\``,new RegExp(`class="${f}\\b`).test(a),`it is the element's only ear for Corners \u2014 got: ${a.slice(0,140)}`)}const o=e.cornerRadiusTokens;if(o){n("\xA73 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 \u2014 got: ${JSON.stringify(o(null))}`),n("\xA73 `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(`\xA73 ${d}px publishes ${l}: ${c}`,o(d)[l]===c,`a button is half the card, which is what inputs and panels beside it read \u2014 got: ${String(o(d)[l])}`);const[t,s]=e.radiusRange??[Number.NaN,Number.NaN];n(`\xA73 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("\xA74 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 \u2014 got: ${JSON.stringify(r)}`),n("\xA74 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 \u2014 got: ${JSON.stringify(r)}`);const a="--radius-full";for(const d of[0,9,16,40])n(`\xA75 ${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(`\xA76 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 \u2014 ${t.join(" | ")}`)}{const t=i.filter(a=>a.path.endsWith(".tsx")&&b(a.text)).map(a=>a.path).sort();n(`\xA77 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 \xA77 is measuring nothing \u2014 ${JSON.stringify(t)}`);const s=t.filter(a=>!e.callers.includes(a));n("\xA77 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 \u2014 a caller missing from it is a caller \xA76 never looks at. Add the path \u2014 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("\xA77 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 \u2014 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};
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};
@@ -7,7 +7,7 @@ import{ruleChecks as z}from"./rule.js";const g="sf-note";function y(o){const i=[
7
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
8
  font-size: 12px;
9
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. */`,`// \u2026and .${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 \u2014 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 \u2014 the text simply renders at whatever it inherits:
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
12
  ${a.join(`
13
13
  `)}`),c()}export{g as CHROME_NOTE_CLASS,m as declaration,S as editorChromeNote,y as leafRules,u as noteClasses,b as withoutComments};
@@ -1,4 +1,4 @@
1
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("\xA78'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) \xA78 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:
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
3
  `+o.join(`
4
- `):`the walk found too few entries importing ${i} \u2014 it is not sweeping the editor at all, so this check is asking nothing.`)}return l()}export{p as editorContextMenu};
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};
@@ -1,4 +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 \u2014 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=\u2026) 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:
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
3
  ${l.join(`
4
4
  `)}`),f()}export{w as editorPuckDragOff};
@@ -1,2 +1,2 @@
1
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 \u2014 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 \u2014 ${c.join(", ")}`,e.length===0,`the installed package does not export: ${e.join(", ")}. The pin resolves and the lockfile is valid \u2014 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 \u2014 this check cannot fail"),a()}export{x as efficientEaselVersion,v as importedSubpaths,m as satisfiesCaret};
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};
@@ -1,7 +1,7 @@
1
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" /> \u2014 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("\xA74'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 \u2014 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(`\xA71 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:
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
4
  `+e.join(`
5
- `))}{const e=d(m);n(`\xA72 ${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) \u2014 this count is derived from the tree, so a new call site has to come past it.");const a=d(p);n(`\xA72 ${t.buttonFills} bg-btn fills (found ${a})`,a===t.buttonFills)}for(const e of t.chips??[])n(`\xA73 ${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(`\xA74 ${t.buttonSites} sites paint bg-btn (found ${c.length})`,c.length===t.buttonSites,"\xA74 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("\xA74 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:
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
6
  `+e.join(`
7
- `));const a=c.filter(s=>w(s.text)).length;n(`\xA74 ${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(`\xA75 read ${t.runtimeFallbacks}`,!!e)){const a=[...e.text.matchAll(/const (btnBg|btnFg) = \w+ \|\| "([^"]+)";/g)].map(s=>[s[1],s[2]]);n(`\xA75 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("\xA75 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 \u2014 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};
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};
@@ -3,6 +3,6 @@ import{isRuleSource as y,ruleChecks as S}from"./rule.js";import{registryFiles as
3
3
  const b = contextLighting;
4
4
  `},{path:"fixture.css",text:`.x { color: var(--theme-accent-light-2); }
5
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) \u2014 ${n} (66238):
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
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(", ")} \u2014 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};
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};
@@ -1,2 +1,2 @@
1
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("\u2026and 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(`\u2026and 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 \u2014 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 \u2014 a merchant choosing Full width would get a capped section, which is the defect this card removed"),s("\u2026and still draws the column when it does not",h,"the other arm lost the cap \u2014 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};
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};
@@ -1,8 +1,8 @@
1
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} \xB7 ${a.token}`)),i=[];if(o.length){i.push(` note \u2014 ${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 \u2014 not swept: ${l} \u2014 ${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 \u2014 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-[\u2026] 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} \u2192 ${d}px \xB7 ${r.token}`)}s("no swept display heading renders above the site's 40px ceiling",c.length===0,`${c.length} step(s) ignore it:
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
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 \u2192 ${w}px`)}}if(s("a tenant that sets no ceiling renders exactly what it renders today",g.length===0,`${g.length} step(s) moved:
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
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} \xB7 ${r.token}`);return s("every capped step is spelt the reference's way",x.length===0,`${x.length} second spelling(s):
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
7
  ${x.join(`
8
8
  `)}`),o()}export{O as enclosingLiteral,D as headingCeilingNotes,H as headingCeilingSweep};
@@ -1,5 +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(" \u2014 ")&&e.push("carries a ` \u2014 ` 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 \u2014 the manual does the teaching:
2
- `+t.map(e=>` ${e.where} ${JSON.stringify(e.label)} \u2014 ${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 \u2014 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 ` \u2014 ` hint tail",s(4).includes("carries a ` \u2014 ` 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(`
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
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};
@@ -1,5 +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 \u2014 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\u2013N10.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)} \u2014 ${n.why.join(", ")}`).join(`
3
- `));for(const n of c.parked)t(`PARKED ${n.file} ${JSON.stringify(n.label)} still breaks N10 \u2014 ${n.why}`,g.some(s=>s.file===n.file&&s.label===n.label),"it no longer breaks N10 (renamed, or its row deleted) \u2014 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 \u2026but 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 \u2026and 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 \u2026and 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 \u2014 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(`
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
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(" \xB7 ")==="Heading \xB7 Kicker \xB7 Title \xB7 Cards \xB7 Heading \xB7 Card title \xB7 Layout \xB7 Photo \xB7 Colour \xB7 Gutter",f.join(" \xB7 ")),t("\u2026and the breaches are the ones it wrote, the parked one waived",h.map(i=>`${i.label}=${i.why.join("+")}`).join(" \xB7 ")==='Kicker=N10.1 "kicker" \xB7 Title=N10.2 Title at section level \xB7 Heading=N10.2 Heading at item level \xB7 Photo=N10.1 "photo" \xB7 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("\u2026and 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};
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};
@@ -1,10 +1,10 @@
1
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
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
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("\u2026and carries the label",/label:\s*"/.test(o[0]),o[0].slice(0,120)),t("\u2026and 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(`\u2026and 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(" \xB7 ");g.push(`${e.file}:${e.line} "${e.label}" \u2014 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,`
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
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(" \xB7 "));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} \u2192 ${r.glyph}`);for(const r of f.get(e)??[]){const i=y(r.file.text.split(`
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
7
  `).slice(r.line-1,r.line+6).join(`
8
- `));o.add(i),n.push(`${r.file.path}:${r.line} \u2192 ${i}`)}t(`"${e}" draws ONE picture wherever it is declared`,o.size===1,`${o.size} different:
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
9
  ${n.join(`
10
- `)}`),t(`\u2026and "${e}" was actually found in both places`,n.length>=2,`${n.length}`)}return p()}export{y as glyphOf,W as mirroredRowsAgree};
10
+ `)}`),t(`…and "${e}" was actually found in both places`,n.length>=2,`${n.length}`)}return p()}export{y as glyphOf,W as mirroredRowsAgree};
@@ -1,7 +1,7 @@
1
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
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
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)}\u2026`: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 \u2014 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 \u2014 wrap them in {/* ... */}:
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
6
  `+r.map(e=>` ${e.file}:${e.line} ${e.text}`).join(`
7
7
  `)),l()}export{h as jsxCommentLines,d as noCommentInJsx};
@@ -1,4 +1,4 @@
1
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
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)` \u2014 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 \u2014 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("\u2026reads a concatenated reason whole, so it can be checked for a source",t[0]?.reason==="A facet's options grow out of the CATALOGUE \u2014 owner, 2026-08-29."&&u.test(t[0].reason),JSON.stringify(t[0])),o("\u2026and 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 \u2014 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(" \xB7 "));const a=s.filter(t=>t.reason.trim()&&!u.test(t.reason));if(o("\u2026and every reason names a source that can be checked",a.length===0,a.map(t=>`${t.at} \u2014 "${t.reason.slice(0,60)}"`).join(" \xB7 ")),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 \u2014 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("\u2026and 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(`\u2026and 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};
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};
@@ -1,2 +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("\u2026and not one a comment talks about",!l.test(n(`/* ${e} */
2
- // ${e}`))),t("the reader sees a ColourPicker",d.test(n(m))),t("\u2026and 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("\xA71 nothing draws a native colour input \u2014 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("\xA71 exactly one file draws efficient-easel's ColourPicker",o.length===1,o.length===0?"NONE \u2014 the reader is broken, and every check below is vacuous":`${o.length}: ${o.join(", ")}`),t(`\xA71 \u2026and it is the shared control, ${r.sharedControl}`,o[0]===r.sharedControl,o.join(", "))):t("\xA71 no file here draws efficient-easel's ColourPicker \u2014 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("\xA79 nothing draws a `sf-ink-` or `sf-colorinput` class any more",u.length===0,u.join(", ")),p()}export{k as oneColourControl,n as stripComments};
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};
@@ -1,4 +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> \u2014 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") \u2014 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(`
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
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
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} \u2014 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} \u2014 a NEW raw <select> in a file that is being replaced; use FieldSelect`:`found ${l} \u2014 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} \u2014 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} \u2014 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};
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};