@forumone/throughline-design-system 0.0.0 → 1.0.0-next.2

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 (122) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/LICENSE +21 -0
  3. package/README.md +23 -0
  4. package/bin/check-block-props.mjs +51 -0
  5. package/bin/stub-styles.mjs +28 -0
  6. package/dist/admin/BlockGuidance.d.ts +13 -0
  7. package/dist/admin/BlockGuidance.d.ts.map +1 -0
  8. package/dist/admin/BlockGuidance.js +13 -0
  9. package/dist/admin/BlockGuidance.js.map +1 -0
  10. package/dist/admin/BlockSummary.d.ts +10 -0
  11. package/dist/admin/BlockSummary.d.ts.map +1 -0
  12. package/dist/admin/BlockSummary.js +73 -0
  13. package/dist/admin/BlockSummary.js.map +1 -0
  14. package/dist/admin/RowSummary.d.ts +15 -0
  15. package/dist/admin/RowSummary.d.ts.map +1 -0
  16. package/dist/admin/RowSummary.js +16 -0
  17. package/dist/admin/RowSummary.js.map +1 -0
  18. package/dist/admin/summary.d.ts +18 -0
  19. package/dist/admin/summary.d.ts.map +1 -0
  20. package/dist/admin/summary.js +48 -0
  21. package/dist/admin/summary.js.map +1 -0
  22. package/dist/altText.d.ts +16 -0
  23. package/dist/altText.d.ts.map +1 -0
  24. package/dist/altText.js +54 -0
  25. package/dist/altText.js.map +1 -0
  26. package/dist/client.d.ts +15 -0
  27. package/dist/client.d.ts.map +1 -0
  28. package/dist/client.js +12 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/contract/index.d.ts +5 -0
  31. package/dist/contract/index.d.ts.map +1 -0
  32. package/dist/contract/index.js +4 -0
  33. package/dist/contract/index.js.map +1 -0
  34. package/dist/contract/lint.d.ts +38 -0
  35. package/dist/contract/lint.d.ts.map +1 -0
  36. package/dist/contract/lint.js +110 -0
  37. package/dist/contract/lint.js.map +1 -0
  38. package/dist/contract/loader.d.ts +49 -0
  39. package/dist/contract/loader.d.ts.map +1 -0
  40. package/dist/contract/loader.js +95 -0
  41. package/dist/contract/loader.js.map +1 -0
  42. package/dist/contract/manifest.d.ts +454 -0
  43. package/dist/contract/manifest.d.ts.map +1 -0
  44. package/dist/contract/manifest.js +29 -0
  45. package/dist/contract/manifest.js.map +1 -0
  46. package/dist/contract/schema.d.ts +334 -0
  47. package/dist/contract/schema.d.ts.map +1 -0
  48. package/dist/contract/schema.js +224 -0
  49. package/dist/contract/schema.js.map +1 -0
  50. package/dist/generate/blocks.d.ts +79 -0
  51. package/dist/generate/blocks.d.ts.map +1 -0
  52. package/dist/generate/blocks.js +118 -0
  53. package/dist/generate/blocks.js.map +1 -0
  54. package/dist/generate/fields.d.ts +60 -0
  55. package/dist/generate/fields.d.ts.map +1 -0
  56. package/dist/generate/fields.js +478 -0
  57. package/dist/generate/fields.js.map +1 -0
  58. package/dist/generate/guidance.d.ts +5 -0
  59. package/dist/generate/guidance.d.ts.map +1 -0
  60. package/dist/generate/guidance.js +29 -0
  61. package/dist/generate/guidance.js.map +1 -0
  62. package/dist/generate/index.d.ts +13 -0
  63. package/dist/generate/index.d.ts.map +1 -0
  64. package/dist/generate/index.js +9 -0
  65. package/dist/generate/index.js.map +1 -0
  66. package/dist/generate/labels.d.ts +42 -0
  67. package/dist/generate/labels.d.ts.map +1 -0
  68. package/dist/generate/labels.js +226 -0
  69. package/dist/generate/labels.js.map +1 -0
  70. package/dist/generate/layout.d.ts +42 -0
  71. package/dist/generate/layout.d.ts.map +1 -0
  72. package/dist/generate/layout.js +252 -0
  73. package/dist/generate/layout.js.map +1 -0
  74. package/dist/generate/selectOptionSnapshot.d.ts +26 -0
  75. package/dist/generate/selectOptionSnapshot.d.ts.map +1 -0
  76. package/dist/generate/selectOptionSnapshot.js +47 -0
  77. package/dist/generate/selectOptionSnapshot.js.map +1 -0
  78. package/dist/generate/selectOptions.d.ts +64 -0
  79. package/dist/generate/selectOptions.d.ts.map +1 -0
  80. package/dist/generate/selectOptions.js +145 -0
  81. package/dist/generate/selectOptions.js.map +1 -0
  82. package/dist/overrides.d.ts +71 -0
  83. package/dist/overrides.d.ts.map +1 -0
  84. package/dist/overrides.js +16 -0
  85. package/dist/overrides.js.map +1 -0
  86. package/dist/render/RenderBlocks.d.ts +67 -0
  87. package/dist/render/RenderBlocks.d.ts.map +1 -0
  88. package/dist/render/RenderBlocks.js +31 -0
  89. package/dist/render/RenderBlocks.js.map +1 -0
  90. package/dist/render/coerce.d.ts +78 -0
  91. package/dist/render/coerce.d.ts.map +1 -0
  92. package/dist/render/coerce.js +243 -0
  93. package/dist/render/coerce.js.map +1 -0
  94. package/dist/render/index.d.ts +5 -0
  95. package/dist/render/index.d.ts.map +1 -0
  96. package/dist/render/index.js +3 -0
  97. package/dist/render/index.js.map +1 -0
  98. package/dist/testing/checkBlockProps.d.ts +66 -0
  99. package/dist/testing/checkBlockProps.d.ts.map +1 -0
  100. package/dist/testing/checkBlockProps.js +241 -0
  101. package/dist/testing/checkBlockProps.js.map +1 -0
  102. package/dist/testing/checkBlockPropsCli.d.ts +12 -0
  103. package/dist/testing/checkBlockPropsCli.d.ts.map +1 -0
  104. package/dist/testing/checkBlockPropsCli.js +86 -0
  105. package/dist/testing/checkBlockPropsCli.js.map +1 -0
  106. package/dist/testing/contractDefaults.d.ts +21 -0
  107. package/dist/testing/contractDefaults.d.ts.map +1 -0
  108. package/dist/testing/contractDefaults.js +65 -0
  109. package/dist/testing/contractDefaults.js.map +1 -0
  110. package/dist/testing/describeBlockInvariants.d.ts +39 -0
  111. package/dist/testing/describeBlockInvariants.d.ts.map +1 -0
  112. package/dist/testing/describeBlockInvariants.js +71 -0
  113. package/dist/testing/describeBlockInvariants.js.map +1 -0
  114. package/dist/testing/index.d.ts +18 -0
  115. package/dist/testing/index.d.ts.map +1 -0
  116. package/dist/testing/index.js +14 -0
  117. package/dist/testing/index.js.map +1 -0
  118. package/dist/testing/untouchedBlocks.d.ts +48 -0
  119. package/dist/testing/untouchedBlocks.d.ts.map +1 -0
  120. package/dist/testing/untouchedBlocks.js +155 -0
  121. package/dist/testing/untouchedBlocks.js.map +1 -0
  122. package/package.json +108 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,182 @@
1
+ # @forumone/throughline-design-system
2
+
3
+ ## 1.0.0-next.2
4
+
5
+ ## 1.0.0-next.0
6
+
7
+ ### Major Changes
8
+
9
+ - 825f4e9: `@forumone/throughline-design-system`: the 1.0 design-system package, from `@forumone/throughline-design-contract` and `@forumone/throughline-design-system-payload`, by `docs/spec/1.0-exports.md`. It is published for the first time: design-system-payload was private and shipped TypeScript source, and this package builds to `dist`.
10
+
11
+ - `@forumone/throughline-design-contract` is `/contract`, and its `/lint` is `/lint`.
12
+ - design-system-payload's `/generate`, `/render`, `/client` and `/testing` keep their names. Its root (`fieldOverride` and the override types) is part of `/generate`.
13
+ - The `check-block-props` bin is unchanged, and runs the built CLI.
14
+ - Admin component paths are `@forumone/throughline-design-system/client#BlockSummary`, `#BlockGuidance` and `#RowSummary`, so a site's `importMap.js` changes.
15
+ - `/contract` and `/lint` need no peers. `payload`, `react`, `@payloadcms/ui`, `typescript` (which `/generate` uses to read component source) and `vitest` (for `/testing`) are optional peers.
16
+
17
+ `@forumone/throughline/components` now reads manifests through `@forumone/throughline-design-system/contract`.
18
+
19
+ The 0.x entries below are `@forumone/throughline-design-contract`'s. `@forumone/throughline-design-system-payload` was never published.
20
+
21
+ ## 0.6.0
22
+
23
+ ### Minor Changes
24
+
25
+ - 2bf29e6: Generated blocks are arranged for an author to read, not in the shape the
26
+ component's props happen to take.
27
+ - Every generated field has an explicit, sentence-case label. Payload's own
28
+ fallback title-cased the prop name, so authors were asked for a "Cta Href"
29
+ and an "Image Alt". Now they see "Call to action", "Alt text", "Open on page
30
+ load", and a link's own controls read "Links to", "Page" and "URL".
31
+ - A `<prefix>Label` text field and its `<prefix>Href` or `<prefix>Url` link
32
+ are drawn as one group headed by what they are together ("Call to action",
33
+ "View all link"), with a matching `<prefix>Icon` inside it. This applies at
34
+ every level, array rows included.
35
+ - Top-level selects, checkboxes and numbers, plus any field a contract marks
36
+ with the new `advanced: true`, move to one collapsed "More options" section
37
+ at the end of the block. A required field is never tucked away.
38
+
39
+ All of this is presentational. An unnamed group and a collapsible store their
40
+ children flat, so the stored data, the generated types and `coerce` are
41
+ unchanged. A host test that walks `block.fields` for named fields now has to
42
+ look through those wrappers.
43
+
44
+ `advanced` is a new optional contract field property. It hints that most
45
+ authors, and most compositions, should leave the field empty. It is refused on
46
+ a required field.
47
+
48
+ ## 0.5.1
49
+
50
+ ### Patch Changes
51
+
52
+ - 957403b: One `@types/node`, so a host does not end up with two copies of `@payloadcms/ui`
53
+
54
+ Twelve packages asked for `@types/node@^20.17.0` and `design-system-payload`
55
+ asked for `^24.13.2`. Inside this repository that is untidy. Inside a host that
56
+ consumes the suite from source — which is how `forumone/forumone-2026` uses it,
57
+ as a git submodule in one pnpm workspace — it is a runtime failure.
58
+
59
+ pnpm hashes a package's identity with its resolved peers. `publishing` and
60
+ `integrations` both take `@payloadcms/ui` as a peer _and_ as a devDependency, so
61
+ each got its own copy resolved against `@types/node@20`, while the host's copy
62
+ resolved against `@types/node@24`. Same version, 3.87.1, two directories:
63
+
64
+ apps/web → @payloadcms+ui@3.87.1_…_9ce0de5c…
65
+ packages/publishing → @payloadcms+ui@3.87.1_…_13184ec4…
66
+ packages/integrations → @payloadcms+ui@3.87.1_…_13184ec4…
67
+
68
+ Two directories are two module instances. Two instances of `@payloadcms/ui` are
69
+ two `ConfigContext` objects, and `PublishButton` read the one the admin's
70
+ provider had never populated:
71
+
72
+ TypeError: Cannot destructure property 'config' of useConfig() as it is undefined
73
+
74
+ The host saw an intermittent 500 on every admin document view — `PublishButton`
75
+ is installed on each collection with a publish policy, so lists, `/admin` and
76
+ the login screen were all fine and only editing broke. Nothing caught it:
77
+ install, `--frozen-lockfile`, typecheck, lint and every test passed, because the
78
+ two copies are byte-identical and the split exists only at module resolution.
79
+ forumone/forumone-2026#498.
80
+
81
+ Aligning on `^24.13.2` collapses them to one instance. Nothing here targets a
82
+ Node 20 API deliberately; the packages typecheck and test unchanged against the
83
+ newer types.
84
+
85
+ `create-throughline` keeps `^20.17.0` on purpose. It is the one package
86
+ declaring `engines.node: >=20.9.0`, and typechecking a CLI against types newer
87
+ than the runtime it promises to support is how a Node 24-only call ships to
88
+ somebody on Node 20.
89
+
90
+ ## 0.5.0
91
+
92
+ ### Minor Changes
93
+
94
+ - 45724ee: A contract can say a boolean starts ticked, and be believed
95
+
96
+ `boolean` fields were generated as `{ type: 'checkbox', defaultValue: false }`,
97
+ with the `false` hardcoded. That made a component's own default unreachable from
98
+ the CMS. A checkbox is stored ticked or unticked and never absent, so `coerce`
99
+ always had a value to turn into a real boolean, the prop was never `undefined`,
100
+ and a signature default like `hasFacade = true` could not apply. Every boolean a
101
+ contract described arrived at its component as `false`, whatever the component
102
+ said.
103
+
104
+ It was not theoretical. `VideoEmbed.hasFacade` exists to keep a provider's
105
+ iframe — several hundred kilobytes and its third-party cookies — off the page
106
+ until a reader presses play, and its contract says "Leave on". Every embed an
107
+ author added shipped with it off: the YouTube iframe was in the server HTML from
108
+ first paint, setting cookies on readers who never pressed play, on a site whose
109
+ stated rule is that no third-party tracking runs before consent.
110
+
111
+ So `ContentField` gains an optional `defaultValue`, read by the checkbox branch
112
+ and rejected on any other field type — anywhere else it is a value the author
113
+ expects to take effect and nothing ever would.
114
+
115
+ `allOrNothing` had to learn about it too. That rule treats `false` as "nobody
116
+ touched this", which is right for a checkbox that starts unticked and exactly
117
+ wrong for one that starts ticked: left alone, a group holding a ticked-by-default
118
+ boolean would never look empty, and the rule would demand the group's required
119
+ children of an author who had typed nothing. It now takes a field's declared
120
+ default into account rather than the value alone.
121
+
122
+ **Existing stored values are untouched.** `defaultValue` applies to a field an
123
+ author has not yet filled in, so blocks already saved keep whatever is in the
124
+ database; a `VideoEmbed` saved before this change still renders without its
125
+ facade until someone edits it.
126
+
127
+ ## 0.4.0
128
+
129
+ ### Minor Changes
130
+
131
+ - 14f2be4: A contract can say a boolean starts ticked, and be believed
132
+
133
+ `boolean` fields were generated as `{ type: 'checkbox', defaultValue: false }`,
134
+ with the `false` hardcoded. That made a component's own default unreachable from
135
+ the CMS. A checkbox is stored ticked or unticked and never absent, so `coerce`
136
+ always had a value to turn into a real boolean, the prop was never `undefined`,
137
+ and a signature default like `hasFacade = true` could not apply. Every boolean a
138
+ contract described arrived at its component as `false`, whatever the component
139
+ said.
140
+
141
+ It was not theoretical. `VideoEmbed.hasFacade` exists to keep a provider's
142
+ iframe — several hundred kilobytes and its third-party cookies — off the page
143
+ until a reader presses play, and its contract says "Leave on". Every embed an
144
+ author added shipped with it off: the YouTube iframe was in the server HTML from
145
+ first paint, setting cookies on readers who never pressed play, on a site whose
146
+ stated rule is that no third-party tracking runs before consent.
147
+
148
+ So `ContentField` gains an optional `defaultValue`, read by the checkbox branch
149
+ and rejected on any other field type — anywhere else it is a value the author
150
+ expects to take effect and nothing ever would.
151
+
152
+ `allOrNothing` had to learn about it too. That rule treats `false` as "nobody
153
+ touched this", which is right for a checkbox that starts unticked and exactly
154
+ wrong for one that starts ticked: left alone, a group holding a ticked-by-default
155
+ boolean would never look empty, and the rule would demand the group's required
156
+ children of an author who had typed nothing. It now takes a field's declared
157
+ default into account rather than the value alone.
158
+
159
+ **Existing stored values are untouched.** `defaultValue` applies to a field an
160
+ author has not yet filled in, so blocks already saved keep whatever is in the
161
+ database; a `VideoEmbed` saved before this change still renders without its
162
+ facade until someone edits it.
163
+
164
+ ## 0.3.0
165
+
166
+ ### Minor Changes
167
+
168
+ - 24bd325: Add an optional `group` to the component contract, so an authoring UI can shelve components separately from what they are.
169
+
170
+ `category` was answering two questions at once — what a component _is_, and where an editor looks for it — and it is a bad answer to the second at any real size. A design system of sixty blocks files roughly half of them under `section`, so a picker grouped on `category` hands back the flat list the grouping was meant to avoid while `card` and `navigation` hold one entry each. Evening the shelves out within `category` would file components under the wrong kind for every consumer that reasons about kind, including `list_components`.
171
+
172
+ So `category` keeps its meaning and its enum, and `group` takes the second question with a vocabulary of shelf labels: `hero`, `narrative`, `proof`, `listing`, `media`, `form`, `cta`, `navigation`, `utility`. No `section`, which is the problem being solved; no `card` or `data`, which name a kind rather than a place to look.
173
+
174
+ New API: `groupOf(component)` resolves `group ?? category`, and `LoadedManifest` gains `listByGroup()` and `listGroups()` which match on the resolved value. Grouping consumers should call `groupOf` rather than reading either field.
175
+
176
+ Non-breaking. `group` is optional, and the fallback means a design system that sets none groups exactly as it did before.
177
+
178
+ ## 0.2.0
179
+
180
+ ### Minor Changes
181
+
182
+ - [#9](https://github.com/forumone/throughline/pull/9) [`337f2ca`](https://github.com/forumone/throughline/commit/337f2ca779a30d2f135845259bbae8e961a625ed) Thanks [@briangraves](https://github.com/briangraves)! - Initial release. Defines `ComponentContractSchema`, `ManifestSchema`, `loadManifest`, `loadManifestFromUrl`, and `lintManifest`. Every design system that satisfies this contract is a valid input to the framework's Component Server.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Forum One Communications Corporation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @forumone/throughline-design-system
2
+
3
+ The design-system side of Throughline. A design system describes its components in a manifest; this package defines that manifest, lints it, and turns it into Payload blocks, rendering and admin components for a Throughline site.
4
+
5
+ > **1.0 is in progress.** This package is `@forumone/throughline-design-contract` and `@forumone/throughline-design-system-payload` together; [Upgrading from 0.x](https://github.com/forumone/throughline/blob/main/docs/guides/upgrading.md) moves a 0.x site onto it. Pre-releases publish as `1.0.0-next.N` under the `next` dist-tag.
6
+
7
+ | Subpath | Holds |
8
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `/contract` | `ComponentContractSchema`, `ManifestSchema`, `loadManifest`: the contract every component declares and the manifest they make; see [docs/contract.md](https://github.com/forumone/throughline/blob/main/docs/reference/design-system/contract.md) |
10
+ | `/lint` | `lintManifest`, `formatLintIssues`, `assertManifestClean` |
11
+ | `/generate` | `generateBlocks` and friends, manifest to Payload blocks, and `fieldOverride` for a site's exceptions; see [docs/payload.md](https://github.com/forumone/throughline/blob/main/docs/reference/design-system/generate.md) |
12
+ | `/render` | `RenderBlocks` and `coerceBlock`: stored blocks back to React |
13
+ | `/client` | `BlockSummary`, `BlockGuidance`, `RowSummary`: admin components named in Payload's import map |
14
+ | `/testing` | `describeBlockInvariants`, `checkUntouchedBlocks`, `checkBlockProps`: invariants a site runs over its blocks |
15
+ | bin | `check-block-props <manifest.json> <components-dir>…`: does each contract produce the props its component takes? |
16
+
17
+ ```bash
18
+ pnpm add @forumone/throughline-design-system@next
19
+ ```
20
+
21
+ `/contract` and `/lint` have no peers, so a design system that only publishes a manifest needs nothing else. The rest expects `payload` and `react`, `/client` expects `@payloadcms/ui`, `/generate` reads component source with `typescript`, and `/testing` is a vitest suite.
22
+
23
+ **Reference: [`docs/reference/design-system.md`](https://github.com/forumone/throughline/blob/main/docs/reference/design-system.md).**
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ Does each design-system contract produce the props its component takes?
4
+
5
+ check-block-props <manifest.json> <components-dir>... [--overrides <module>] [--tsconfig <file>]
6
+
7
+ The args files and overrides it loads are TypeScript, so `tsx` is registered
8
+ before anything is imported. The CLI itself is this package's built output.
9
+
10
+ Both of tsx's hooks, not just the ESM one. A `.ts` file with no
11
+ `"type": "module"` above it is CommonJS to Node, and before Node 22 the ESM
12
+ hook hands such a file to Node's own CommonJS loader, which cannot read
13
+ TypeScript. A design system's directory need not be an ES module package.
14
+
15
+ `--tsconfig` is read here rather than by the CLI, because it has to be known
16
+ before tsx is registered. An args file written in TSX compiles with whatever
17
+ `jsx` the tsconfig tsx finds says, and tsx looks beside the working directory:
18
+ for a site running this from its app, that is the app's tsconfig, not the
19
+ design system's. A design system that keeps its `jsx` in a project reference
20
+ (`tsconfig.app.json`) has to be named. #207.
21
+
22
+ Stylesheets are stubbed (`./stub-styles.mjs`): a component imports its CSS, an
23
+ args file imports its component, and Node cannot load a `.css` file.
24
+ */
25
+ import { createRequire, register as registerHooks } from 'node:module'
26
+ import path from 'node:path'
27
+ import { register as registerCommonJs } from 'tsx/cjs/api'
28
+ import { register } from 'tsx/esm/api'
29
+
30
+ const argv = process.argv.slice(2)
31
+ const flag = argv.indexOf('--tsconfig')
32
+ const tsconfig = flag === -1 ? undefined : argv[flag + 1]
33
+ if (tsconfig) process.env.TSX_TSCONFIG_PATH = path.resolve(tsconfig)
34
+
35
+ registerCommonJs()
36
+ register(tsconfig ? { tsconfig: path.resolve(tsconfig) } : {})
37
+ // After tsx, so it runs first: hooks registered later are consulted earlier.
38
+ registerHooks('./stub-styles.mjs', import.meta.url)
39
+
40
+ // The CommonJS half of the same stub, for a design system that is not an ES
41
+ // module package. A `require`d stylesheet becomes the same answer-any-name
42
+ // object.
43
+ const extensions = createRequire(import.meta.url).extensions
44
+ for (const ext of ['.css', '.scss', '.sass', '.less']) {
45
+ extensions[ext] = (module) => {
46
+ module.exports = new Proxy({}, { get: (_, key) => (typeof key === 'string' ? key : undefined) })
47
+ }
48
+ }
49
+
50
+ const { main } = await import('../dist/testing/checkBlockPropsCli.js')
51
+ process.exitCode = await main(argv)
@@ -0,0 +1,28 @@
1
+ /*
2
+ Stylesheet imports, answered with a stand-in.
3
+
4
+ An args file imports its component for the types, and a component imports its
5
+ stylesheet — `import styles from './card.module.css'` — so loading an args file
6
+ in Node reaches a `.css` file, which Node cannot load, and the whole check
7
+ stops on a file it never needed. What this compares is the shape of the args,
8
+ not anything a stylesheet holds.
9
+
10
+ So each stylesheet becomes a module whose default export answers any class
11
+ name with that name, the way a CSS module does at runtime, and which has
12
+ nothing else in it. Registered from `check-block-props.mjs` after tsx, so it
13
+ runs before tsx's own hook.
14
+ */
15
+
16
+ const STYLESHEET = /\.(css|scss|sass|less)$/
17
+
18
+ export async function load(url, context, nextLoad) {
19
+ if (STYLESHEET.test(new URL(url).pathname)) {
20
+ return {
21
+ format: 'module',
22
+ shortCircuit: true,
23
+ source:
24
+ "export default new Proxy({}, { get: (_, key) => (typeof key === 'string' ? key : undefined) })\n",
25
+ }
26
+ }
27
+ return nextLoad(url, context)
28
+ }
@@ -0,0 +1,13 @@
1
+ export interface BlockGuidanceProps {
2
+ /** The sentence to show — see `../generate/guidance.ts`. Injected via `clientProps`. */
3
+ text: string;
4
+ }
5
+ /**
6
+ * The line at the head of an opened block saying what the block is for.
7
+ *
8
+ * A `ui` field, so it stores nothing and appears in no generated type. Drawn
9
+ * with Payload's own description class, so it reads as help text rather than
10
+ * as a field.
11
+ */
12
+ export declare function BlockGuidance({ text }: BlockGuidanceProps): import("react").JSX.Element;
13
+ //# sourceMappingURL=BlockGuidance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockGuidance.d.ts","sourceRoot":"","sources":["../../src/admin/BlockGuidance.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,EAAE,kBAAkB,+BAMzD"}
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * The line at the head of an opened block saying what the block is for.
5
+ *
6
+ * A `ui` field, so it stores nothing and appears in no generated type. Drawn
7
+ * with Payload's own description class, so it reads as help text rather than
8
+ * as a field.
9
+ */
10
+ export function BlockGuidance({ text }) {
11
+ return (_jsx("p", { className: "field-description", style: { margin: '0 0 calc(var(--base) * 0.75)' }, children: text }));
12
+ }
13
+ //# sourceMappingURL=BlockGuidance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockGuidance.js","sourceRoot":"","sources":["../../src/admin/BlockGuidance.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAOZ;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,IAAI,EAAsB;IACxD,OAAO,CACL,YAAG,SAAS,EAAC,mBAAmB,EAAC,KAAK,EAAE,EAAE,MAAM,EAAE,8BAA8B,EAAE,YAC/E,IAAI,GACH,CACL,CAAA;AACH,CAAC"}
@@ -0,0 +1,10 @@
1
+ export interface BlockSummaryProps {
2
+ /** The block's field that names it, if it has one. Injected by the generator via `clientProps`. */
3
+ fields: string[];
4
+ /** The block's label, as the picker shows it — "Collage Hero". */
5
+ singular: string;
6
+ /** The block's slug, for Payload's per-block pill class. */
7
+ slug: string;
8
+ }
9
+ export declare function BlockSummary({ fields, singular, slug }: BlockSummaryProps): import("react").JSX.Element;
10
+ //# sourceMappingURL=BlockSummary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockSummary.d.ts","sourceRoot":"","sources":["../../src/admin/BlockSummary.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,mGAAmG;IACnG,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;CACb;AAwDD,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,iBAAiB,+BAgDzE"}
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Pill, useForm, useRowLabel, useTranslation } from '@payloadcms/ui';
4
+ import { summaryText } from './summary.js';
5
+ /**
6
+ * The header of a generated block — Payload's own, with a better placeholder.
7
+ *
8
+ * Payload draws a block's header as its number, a pill naming the block type,
9
+ * and an input for the optional `blockName` whose placeholder is "Untitled".
10
+ * Nobody types a block name, so a collapsed page read "01 Collage Hero
11
+ * Untitled, 02 Logos Untitled, 03 Statement Section Untitled…", and finding the
12
+ * section about pricing meant opening each block in turn.
13
+ *
14
+ * So this is the same number and pill, with the same classes, followed by what
15
+ * the block holds: its heading as the author is typing it. A block with no text
16
+ * of its own (a carousel, a pair of images) reads "Untitled", as before.
17
+ *
18
+ * A name somebody did type — the importer writes one for a form it could not
19
+ * convert — still wins, in Payload's own editable input. An unnamed block shows
20
+ * its summary as text rather than as the input's placeholder, because the input
21
+ * is sized to what it shows and would then cover the header: a click meant to
22
+ * open the block would start editing a name instead. The trade is that a name
23
+ * can no longer be added to a block that has none, which the summary is what
24
+ * makes unnecessary.
25
+ *
26
+ * Neither the name nor the form is read through `useField`. Inside a custom
27
+ * block label it sent the edit view into a render loop that pinned the server
28
+ * and never painted; the row's data is already in hand from `useRowLabel`,
29
+ * and an edit is one `dispatchFields`.
30
+ *
31
+ * A custom block label replaces Payload's whole header rather than the part of
32
+ * it after the pill, which is why the number and the pill are redrawn here.
33
+ * Returned as a fragment so they stay direct children of Payload's
34
+ * `blocks-field__block-header`, which spaces them.
35
+ */
36
+ /*
37
+ One line, whatever the heading.
38
+
39
+ Payload's header is a flex row whose items stretch to the tallest, so a
40
+ heading that wrapped to three lines made the block pill three lines tall and
41
+ the collapsed page a column of uneven boxes — the list this header exists to
42
+ make scannable. So nothing here stretches, and the summary is a single line
43
+ that ends in an ellipsis where it runs out of room. The row opens on a click
44
+ anywhere on it, which shows the whole heading.
45
+ */
46
+ const CENTRED = { alignSelf: 'center' };
47
+ const SUMMARY = {
48
+ ...CENTRED,
49
+ color: 'var(--theme-elevation-500)',
50
+ pointerEvents: 'none',
51
+ flex: '1 1 auto',
52
+ minWidth: 0,
53
+ overflow: 'hidden',
54
+ textOverflow: 'ellipsis',
55
+ whiteSpace: 'nowrap',
56
+ };
57
+ export function BlockSummary({ fields, singular, slug }) {
58
+ const { data, path, rowNumber } = useRowLabel();
59
+ const { t } = useTranslation();
60
+ const { dispatchFields, setModified } = useForm();
61
+ const namePath = `${path}.blockName`;
62
+ const name = typeof data?.blockName === 'string' ? data.blockName : '';
63
+ const summary = summaryText(data, fields) || t('general:untitled');
64
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "blocks-field__block-number", style: CENTRED, children: String((rowNumber ?? 0) + 1).padStart(2, '0') }), _jsx("span", { style: { ...CENTRED, display: 'flex', flexShrink: 0 }, children: _jsx(Pill, { className: `blocks-field__block-pill blocks-field__block-pill-${slug}`, pillStyle: "white", size: "small", children: singular }) }), name ? (
65
+ // `data-value` is what sizes the input — see Payload's SectionTitle.
66
+ _jsx("div", { className: "section-title", "data-value": name, children: _jsx("input", { "aria-label": `Name for ${singular} block`, className: "section-title__input", id: namePath, name: namePath, onChange: event => {
67
+ event.stopPropagation();
68
+ event.preventDefault();
69
+ dispatchFields({ type: 'UPDATE', path: namePath, value: event.target.value });
70
+ setModified(true);
71
+ }, type: "text", value: name }) })) : (_jsx("span", { className: "row-label", style: SUMMARY, children: summary }))] }));
72
+ }
73
+ //# sourceMappingURL=BlockSummary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockSummary.js","sourceRoot":"","sources":["../../src/admin/BlockSummary.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAW1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH;;;;;;;;;EASE;AACF,MAAM,OAAO,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAW,CAAA;AAEhD,MAAM,OAAO,GAAG;IACd,GAAG,OAAO;IACV,KAAK,EAAE,4BAA4B;IACnC,aAAa,EAAE,MAAM;IACrB,IAAI,EAAE,UAAU;IAChB,QAAQ,EAAE,CAAC;IACX,QAAQ,EAAE,QAAQ;IAClB,YAAY,EAAE,UAAU;IACxB,UAAU,EAAE,QAAQ;CACZ,CAAA;AAEV,MAAM,UAAU,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAqB;IACxE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,WAAW,EAA2B,CAAA;IACxE,MAAM,EAAE,CAAC,EAAE,GAAG,cAAc,EAAE,CAAA;IAC9B,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,OAAO,EAAE,CAAA;IACjD,MAAM,QAAQ,GAAG,GAAG,IAAI,YAAY,CAAA;IACpC,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACtE,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC,CAAA;IAElE,OAAO,CACL,8BACE,eAAM,SAAS,EAAC,4BAA4B,EAAC,KAAK,EAAE,OAAO,YACxD,MAAM,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,GACzC,EAEP,eAAM,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,EAAE,YACzD,KAAC,IAAI,IACH,SAAS,EAAE,qDAAqD,IAAI,EAAE,EACtE,SAAS,EAAC,OAAO,EACjB,IAAI,EAAC,OAAO,YAEX,QAAQ,GACJ,GACF,EACN,IAAI,CAAC,CAAC,CAAC;YACN,qEAAqE;YACrE,cAAK,SAAS,EAAC,eAAe,gBAAa,IAAI,YAC7C,8BACc,YAAY,QAAQ,QAAQ,EACxC,SAAS,EAAC,sBAAsB,EAChC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,KAAK,CAAC,EAAE;wBAChB,KAAK,CAAC,eAAe,EAAE,CAAA;wBACvB,KAAK,CAAC,cAAc,EAAE,CAAA;wBACtB,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAA;wBAC7E,WAAW,CAAC,IAAI,CAAC,CAAA;oBACnB,CAAC,EACD,IAAI,EAAC,MAAM,EACX,KAAK,EAAE,IAAI,GACX,GACE,CACP,CAAC,CAAC,CAAC,CACF,eAAM,SAAS,EAAC,WAAW,EAAC,KAAK,EAAE,OAAO,YACvC,OAAO,GACH,CACR,IACA,CACJ,CAAA;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ export interface RowSummaryProps {
2
+ /** The row's fields to show, in order. Injected by the generator via `clientProps`. */
3
+ fields: string[];
4
+ /** Payload's singular label for the array, for a row with nothing typed yet. */
5
+ singular: string;
6
+ }
7
+ /**
8
+ * The header of a generated array row — see `./summary.ts`.
9
+ *
10
+ * Reads the row from the form rather than from saved data, so the header
11
+ * follows what the author is typing. The class and the `pointer-events` are
12
+ * Payload's own fallback label's, so a click on the text still toggles the row.
13
+ */
14
+ export declare function RowSummary({ fields, singular }: RowSummaryProps): import("react").JSX.Element;
15
+ //# sourceMappingURL=RowSummary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RowSummary.d.ts","sourceRoot":"","sources":["../../src/admin/RowSummary.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,uFAAuF;IACvF,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,eAAe,+BAO/D"}
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useRowLabel } from '@payloadcms/ui';
4
+ import { rowSummary } from './summary.js';
5
+ /**
6
+ * The header of a generated array row — see `./summary.ts`.
7
+ *
8
+ * Reads the row from the form rather than from saved data, so the header
9
+ * follows what the author is typing. The class and the `pointer-events` are
10
+ * Payload's own fallback label's, so a click on the text still toggles the row.
11
+ */
12
+ export function RowSummary({ fields, singular }) {
13
+ const { data, rowNumber } = useRowLabel();
14
+ return (_jsx("span", { className: "row-label", style: { pointerEvents: 'none' }, children: rowSummary(data, fields, singular, rowNumber) }));
15
+ }
16
+ //# sourceMappingURL=RowSummary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RowSummary.js","sourceRoot":"","sources":["../../src/admin/RowSummary.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AASzC;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAmB;IAC9D,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,WAAW,EAA2B,CAAA;IAClE,OAAO,CACL,eAAM,SAAS,EAAC,WAAW,EAAC,KAAK,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,YACzD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,GACzC,CACR,CAAA;AACH,CAAC"}
@@ -0,0 +1,18 @@
1
+ /** How much of a row's text its header shows before it gives up. */
2
+ export declare const SUMMARY_MAX = 80;
3
+ /**
4
+ * The header for one row: its summary fields' text, or Payload's own
5
+ * "Stat 03" while the row is still empty.
6
+ *
7
+ * `rowIndex` is zero-based, as Payload's row-label context hands it over.
8
+ */
9
+ export declare function rowSummary(data: unknown, fields: readonly string[], singular: string, rowIndex: number | undefined): string;
10
+ /**
11
+ * The named fields' text, joined on one line and cut to a header's length —
12
+ * or `''` when none of them holds any.
13
+ *
14
+ * `rowSummary` falls back to a counter; a block header already has one, and
15
+ * falls back to Payload's "Untitled" instead. See `./BlockSummary.tsx`.
16
+ */
17
+ export declare function summaryText(data: unknown, fields: readonly string[]): string;
18
+ //# sourceMappingURL=summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/admin/summary.ts"],"names":[],"mappings":"AAcA,oEAAoE;AACpE,eAAO,MAAM,WAAW,KAAK,CAAA;AAI7B;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAOR;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAS5E"}
@@ -0,0 +1,48 @@
1
+ /*
2
+ What a collapsed array row says about itself.
3
+
4
+ Payload's own row header is the array's name and a counter — "Stat 01",
5
+ "Stat 02", "Stat 03" — which is true of every row and tells an author nothing
6
+ about any of them. With six stats collapsed, finding the one that says "96%"
7
+ means opening them in turn, and reordering them means doing it blind. So a
8
+ generated array names its rows by what they hold: the first one or two short
9
+ text fields of the row, joined, as the author typed them.
10
+
11
+ Pure, and apart from the component that draws it, so the rule can be tested
12
+ without React or a form.
13
+ */
14
+ /** How much of a row's text its header shows before it gives up. */
15
+ export const SUMMARY_MAX = 80;
16
+ const SEPARATOR = ' · ';
17
+ /**
18
+ * The header for one row: its summary fields' text, or Payload's own
19
+ * "Stat 03" while the row is still empty.
20
+ *
21
+ * `rowIndex` is zero-based, as Payload's row-label context hands it over.
22
+ */
23
+ export function rowSummary(data, fields, singular, rowIndex) {
24
+ const text = summaryText(data, fields);
25
+ if (text === '') {
26
+ const number = rowIndex === undefined ? '' : ` ${String(rowIndex + 1).padStart(2, '0')}`;
27
+ return `${singular}${number}`;
28
+ }
29
+ return text;
30
+ }
31
+ /**
32
+ * The named fields' text, joined on one line and cut to a header's length —
33
+ * or `''` when none of them holds any.
34
+ *
35
+ * `rowSummary` falls back to a counter; a block header already has one, and
36
+ * falls back to Payload's "Untitled" instead. See `./BlockSummary.tsx`.
37
+ */
38
+ export function summaryText(data, fields) {
39
+ const row = (data ?? {});
40
+ const text = fields
41
+ .map(name => row[name])
42
+ .filter((value) => typeof value === 'string')
43
+ .map(value => value.replace(/\s+/g, ' ').trim())
44
+ .filter(Boolean)
45
+ .join(SEPARATOR);
46
+ return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1).trimEnd()}…` : text;
47
+ }
48
+ //# sourceMappingURL=summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/admin/summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;EAYE;AAEF,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAA;AAE7B,MAAM,SAAS,GAAG,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,IAAa,EACb,MAAyB,EACzB,QAAgB,EAChB,QAA4B;IAE5B,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACtC,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;QACxF,OAAO,GAAG,QAAQ,GAAG,MAAM,EAAE,CAAA;IAC/B,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAa,EAAE,MAAyB;IAClE,MAAM,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAA;IACnD,MAAM,IAAI,GAAG,MAAM;SAChB,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;SACtB,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC;SAC7D,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;SAC/C,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,SAAS,CAAC,CAAA;IAClB,OAAO,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;AAC1F,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { ContentField } from './generate/fields.js';
2
+ /** The name of the field that carries an image field's alt text. */
3
+ export declare function altFieldFor(imageField: string): string;
4
+ /** Image field name → alt field name, for every image among `siblings` that has one. */
5
+ export declare function pairedAltFields(siblings: readonly ContentField[]): Map<string, string>;
6
+ /** Told to the author beside every alt field that falls back. */
7
+ export declare const ALT_FALLBACK_NOTE = "Leave empty to use the alt text from the media library.";
8
+ /**
9
+ * `siblings`, with every paired alt field made optional and saying why.
10
+ *
11
+ * Optional because a required alt would make the author type the fallback by
12
+ * hand, which is the duplication this exists to remove. Nothing is lost: the
13
+ * media collection already refuses an image without alt.
14
+ */
15
+ export declare function withAltFallback(siblings: readonly ContentField[]): ContentField[];
16
+ //# sourceMappingURL=altText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"altText.d.ts","sourceRoot":"","sources":["../src/altText.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAkBxD,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAStF;AAED,iEAAiE;AACjE,eAAO,MAAM,iBAAiB,4DAA4D,CAAA;AAE1F;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,EAAE,CAajF"}
@@ -0,0 +1,54 @@
1
+ /*
2
+ An image's alt text, beside the image, falls back to the media library's.
3
+
4
+ The media collection asks for alt at upload, where the person who chose the
5
+ image is still looking at it. A block that holds that image used to ask again,
6
+ and render only what it was told — so the same description was typed twice,
7
+ or not at all and the image went out as decorative. Now the block's own alt is
8
+ an override: left empty, the media document's alt is used.
9
+
10
+ Pairing is by name, the same convention `responsiveProps` in `render/coerce.ts`
11
+ relies on: an image prop `image` has its alt in `imageAlt`, except where the
12
+ image prop is `src` inside an `ImageRef` group or row, whose alt is the bare
13
+ `alt` of the HTML attribute. Only siblings pair — the image and its alt have to
14
+ sit in the same block, group or array row.
15
+ */
16
+ /** The name of the field that carries an image field's alt text. */
17
+ export function altFieldFor(imageField) {
18
+ return imageField === 'src' ? 'alt' : `${imageField}Alt`;
19
+ }
20
+ /** Image field name → alt field name, for every image among `siblings` that has one. */
21
+ export function pairedAltFields(siblings) {
22
+ const names = new Set(siblings.map(field => field.name));
23
+ const pairs = new Map();
24
+ for (const field of siblings) {
25
+ if (field.type !== 'image')
26
+ continue;
27
+ const alt = altFieldFor(field.name);
28
+ if (names.has(alt))
29
+ pairs.set(field.name, alt);
30
+ }
31
+ return pairs;
32
+ }
33
+ /** Told to the author beside every alt field that falls back. */
34
+ export const ALT_FALLBACK_NOTE = 'Leave empty to use the alt text from the media library.';
35
+ /**
36
+ * `siblings`, with every paired alt field made optional and saying why.
37
+ *
38
+ * Optional because a required alt would make the author type the fallback by
39
+ * hand, which is the duplication this exists to remove. Nothing is lost: the
40
+ * media collection already refuses an image without alt.
41
+ */
42
+ export function withAltFallback(siblings) {
43
+ const alts = new Set(pairedAltFields(siblings).values());
44
+ return siblings.map(field => alts.has(field.name)
45
+ ? {
46
+ ...field,
47
+ required: false,
48
+ constraints: field.constraints
49
+ ? `${field.constraints} ${ALT_FALLBACK_NOTE}`
50
+ : ALT_FALLBACK_NOTE,
51
+ }
52
+ : field);
53
+ }
54
+ //# sourceMappingURL=altText.js.map