@allons-y/webc-patterns 0.1.0-beta.0 → 0.1.0-beta.1

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 (48) hide show
  1. package/README.md +506 -5
  2. package/components/norn-accordion.webc +124 -0
  3. package/components/norn-breadcrumbs.webc +71 -0
  4. package/components/norn-callout.webc +97 -0
  5. package/components/norn-card-grid.webc +60 -0
  6. package/components/norn-card.webc +143 -0
  7. package/components/norn-code-tabs.webc +270 -0
  8. package/components/norn-copy-button.webc +220 -0
  9. package/components/norn-fact-list.webc +52 -0
  10. package/components/norn-footer.webc +122 -0
  11. package/components/norn-head.webc +59 -0
  12. package/components/norn-header.webc +238 -0
  13. package/components/norn-heading-anchor.webc +71 -0
  14. package/components/norn-hero.webc +81 -0
  15. package/components/norn-icon.webc +140 -0
  16. package/components/norn-link-button.webc +96 -0
  17. package/components/norn-notice.webc +94 -0
  18. package/components/norn-pager.webc +67 -0
  19. package/components/norn-rich-text.webc +105 -0
  20. package/components/norn-search.webc +303 -0
  21. package/components/norn-segmented-control.webc +143 -0
  22. package/components/norn-side-nav.webc +149 -0
  23. package/components/norn-sidebar-layout.webc +185 -0
  24. package/components/norn-simple-table.webc +120 -0
  25. package/components/norn-skip-link.webc +41 -0
  26. package/components/norn-theme-toggle.webc +127 -0
  27. package/components/norn-toc.webc +260 -0
  28. package/css/base.css +75 -0
  29. package/css/print.css +8 -0
  30. package/css/reset.css +45 -0
  31. package/fallback/norn-code-block.webc +13 -0
  32. package/index.js +170 -1
  33. package/layouts/norn-base.webc +137 -0
  34. package/layouts/norn-docs-home.webc +109 -0
  35. package/layouts/norn-docs.webc +77 -0
  36. package/layouts/norn-landing.webc +73 -0
  37. package/layouts/norn-workspace.webc +25 -0
  38. package/layouts/norn-workspaces-home.webc +44 -0
  39. package/lib/icons.js +166 -0
  40. package/lib/npm.js +49 -0
  41. package/lib/optional.js +25 -0
  42. package/lib/site.js +87 -0
  43. package/lib/workspaces.js +71 -0
  44. package/markdown.js +278 -0
  45. package/package.json +32 -3
  46. package/plugin.js +279 -0
  47. package/project.js +15 -0
  48. package/schema/project.schema.json +112 -0
package/README.md CHANGED
@@ -1,25 +1,526 @@
1
1
  # @allons-y/webc-patterns
2
2
 
3
- Generic page and docs patterns for Eleventy sites that use WebC: head, header, footer, hero, card, card grid, side nav, docs shell, pager, and skip link. Each component carries its own CSS in-file, so a page ships only the styles of the components it uses. No runtime dependencies.
3
+ Part of [Norn](https://github.com/allonsy-studio/webc-resources#readme). Generic page and docs patterns for Eleventy sites that use WebC: the page frame and docs shell as Eleventy layouts, and the components they are built from, such as the header, footer, hero, cards, side nav, pager, skip link, icons, link buttons, and tags. One Eleventy plugin registers all of it. Content goes in slots: a component styles the markup it's given. Each component carries its own CSS in-file, so a page ships only the styles of the components it uses.
4
+
5
+ See it in the [component preview](https://allonsy-studio.github.io/webc-resources/).
4
6
 
5
7
  ## Status
6
8
 
7
- Pre-release. The package is scaffolded; components land in phases 3 and 4 of the migration plan.
9
+ Beta. Versions are `0.1.0-beta.N` until 1.0.
8
10
 
9
11
  ## Install
10
12
 
11
13
  ```sh
12
- npm install --save-dev @allons-y/webc-patterns
14
+ npm install --save-dev @11ty/eleventy @11ty/eleventy-plugin-webc @allons-y/webc-patterns
15
+ npm install --save-dev @allons-y/webc-code-sample # optional: highlighted code samples
16
+ ```
17
+
18
+ `@11ty/eleventy` and `@11ty/eleventy-plugin-webc` are peer dependencies. `@allons-y/webc-code-sample` is an optional one: with it installed, the plugin registers `norn-code-block` and highlights Markdown fences, and the layouts show code with it; without it, the plugin registers a plain `norn-code-block` under the same tag, so a hand-written sample and the layouts' samples show as `<pre><code>`, with their `id` and caption. The project is an ES module (`"type": "module"`). Node 22 or later.
19
+
20
+ Evergreen browsers from about 2024. Features newer than that, such as `@scope`, degrade gracefully.
21
+
22
+ ## Setup
23
+
24
+ Add the Norn plugin, with a `props` function that maps a page's data, in whatever shape the project keeps it, to the layout props:
25
+
26
+ ```js
27
+ import norn from "@allons-y/webc-patterns/plugin";
28
+
29
+ export default function (config) {
30
+ config.addPlugin(norn, {
31
+ props: (data) => ({ title: data.title, name: "My project", nav: [{ href: "/docs/", title: "Docs" }] }),
32
+ });
33
+
34
+ return { markdownTemplateEngine: false };
35
+ }
36
+ ```
37
+
38
+ Then pick a layout per page in its front matter: `layout: norn-docs.webc`. The plugin owns the project's one `@11ty/eleventy-plugin-webc` registration: pass more component globs as `components`, and don't add the WebC plugin separately. For the OSS site theme, add `@allons-y/webc-oss-site`'s plugin in place of this one (see its [README](https://github.com/allonsy-studio/webc-resources/tree/main/packages/webc-oss-site#readme)).
39
+
40
+ | Option | Default | Does |
41
+ | --- | --- | --- |
42
+ | `props` | the same-named page data | Maps a page's data to the layout props, added as `eleventyComputed.norn` |
43
+ | `layouts` | `true` | `false` ships no layout; a list of names, as `["norn-landing"]`, leaves those out so the project can supply its own |
44
+ | `components` | `[]` | More component globs: a theme's, the project's `_includes/*.webc` |
45
+ | `exclude` | `[]` | Packaged components to leave out, so the project can supply its own |
46
+ | `strings` | `{}` | The site's strings for every component and layout (see Translating) |
47
+ | `markdown` | `true` | Amend Markdown with the add-ons below |
48
+ | `defaults` | `{}` | Prop values a theme sets, under the mapping's |
49
+ | `icons` | none | Project icons for `norn-icon` (see Project icons) |
50
+
51
+ The plugin registers:
52
+
53
+ - `@11ty/eleventy-plugin-webc` once, with the patterns components, `norn-code-block` from the code sample when it is installed (else the plain one), and the `components` globs.
54
+ - The six layouts, under the project's configured layouts folder (or its includes folder), through `addTemplate`.
55
+ - The helpers the components and layouts call: `nornAdjacent`, `nornHeadings`, `nornComponentStrings`, `nornHook`, `nornIcons`, `nornCompiles`, and `nornTokenRanges` with the code sample.
56
+ - With `markdown`, the Markdown add-ons through `amendLibrary`: `webcTags`, `highlightFences` from the code sample, `scrollTables`, `callouts`, `singleTitle`, and `headingAnchors`.
57
+ - `dropEmptyBundleTags` as a transform, so a page with no CSS or JS keeps no empty `<link>` or `<script>`.
58
+
59
+ ### Project icons
60
+
61
+ `norn-icon` draws the project's own icons beside its built-in set. Name them in the plugin's `icons` option, as a glob or a list of globs of `.svg` files, each named for its file without the extension, or as a record of names to a path or `{ src, mirror }`:
62
+
63
+ ```js
64
+ config.addPlugin(norn, {
65
+ icons: "src/icons/*.svg",
66
+ // or: icons: { "arrow-end": { src: "src/arrow.svg", mirror: true }, logo: "src/logo.svg" },
67
+ });
68
+ ```
69
+
70
+ Then `<norn-icon @name="logo"></norn-icon>`. Paths resolve against the working directory. A name is lowercase kebab case. A missing file, a bad name, or two files with one name fails the build, and so does a name no icon has: the error lists the built-in and project names.
71
+
72
+ - A project icon with a built-in's name replaces the built-in.
73
+ - The icon keeps its `viewBox`, or takes one from a numeric `width` and `height`, or gets `0 0 24 24`, and its own paint. Draw it with `currentColor` so it follows the text color; a file with no paint is filled with `currentColor`. The built-in stroke styling applies to the built-in icons only, so a filled SVG renders filled.
74
+ - The size, `<title>`, `<desc>`, scripts, and event handler attributes are dropped, and the drawing is always hidden from assistive tech. Name an icon that carries meaning in the `label` slot, as for any icon.
75
+ - Ids in the drawing, and the references to them (`url(#id)`, `href`, and a `<style>`'s `#id` selectors), are prefixed with `norn-icon-` and the icon's name. Inlined icons share the page's one id space, so two icons that each define `id="a"` would otherwise clip and fill each other.
76
+ - `mirror: true` flips the icon in right-to-left text, as the built-in arrows do.
77
+
78
+ Without the plugin, `norn-icon` draws the built-in set only.
79
+
80
+ Without the plugin, register the components as `patterns()` (or `npm:@allons-y/webc-patterns/components/*.webc`) in the project's own WebC `components` list. The layouts, helpers, and Markdown add-ons are then the project's to wire up.
81
+
82
+ ### Layout props
83
+
84
+ The layouts read their props as `$data.norn`. Without `props`, each prop is read off the page data of the same name. Every list defaults to `[]`, and a value that is not a list counts as empty. Eleventy also runs the mapping against placeholder data to learn what it reads, so it must not throw on missing data: write `data.collections?.docs ?? []`, not `data.collections.docs`. `norn` is a reserved data key: a page, or a data file, that sets it fails the build with the page's path.
85
+
86
+ One mapping serves every layout, because each layout reads only its own props and ignores the rest:
87
+
88
+ ```js
89
+ config.addPlugin(norn, {
90
+ props: (data) => ({
91
+ title: data.title,
92
+ name: data.site?.name,
93
+ tagline: data.site?.tagline,
94
+ description: data.description ?? data.site?.description,
95
+ url: data.site?.url && new URL(data.page?.url ?? "", data.site.url).href,
96
+ ogImage: { src: "/img/share.png", alt: data.site?.name },
97
+ nav: [{ href: "/docs/", title: "Docs", icon: "book" }],
98
+ footer: { license: { name: "MIT", url: "https://opensource.org/license/mit" } },
99
+ docs: (data.collections?.docs ?? []).map((page) => ({ href: page.url, title: page.data.title })),
100
+ }),
101
+ });
13
102
  ```
14
103
 
15
- The package does not register `@11ty/eleventy-plugin-webc`. The project registers it once and adds this package's components to its `components` list with `patterns()`.
104
+ Every list item that links somewhere or describes something has one shape: `href` (the address), `title` (the label), and `description` (the short text), plus `icon` where a layout draws one. That holds for `nav`, the footer's links, `docs`, `prev` and `next`, `features`, `packages`, and `workspaces`, and for the `items` of `norn-side-nav` and `norn-breadcrumbs`. A mapping turns an Eleventy page into an item with `href: page.url`. The props that are not list items keep their names: `name` is the project's own name, `url` the page's canonical address, and `footer.license` is `{ name, url }`.
105
+
106
+ `norn-base` is the frame of every layout, so its props apply to all of them:
107
+
108
+ | Prop | Shape | Default | Rendered as |
109
+ | --- | --- | --- | --- |
110
+ | `lang` | string | `page.lang`, then `en` | `<html lang>` |
111
+ | `dir` | `"ltr"`, `"rtl"`, or `"auto"` | `rtl` for a right-to-left `lang`, else `ltr` | `<html dir>`. Right-to-left languages are `ar`, `fa`, `he`, `ur`, `ps`, `yi`, `dv`, `ckb`, `sd`, and `ug`, matched on the primary subtag of `lang` |
112
+ | `title` | string | none | `<title>`, as "title: name"; the `<h1>` of the docs layouts |
113
+ | `name` | string | none | The brand link, the footer's lead, `og:site_name`, and the hero `<h1>`. Each is left out without one (the brand link also shows for a logo) |
114
+ | `tagline` | string | none | After the name in the title and the hero `<h1>`; the footer's text |
115
+ | `description` | string | none | `<meta name="description">` and `og:description`; the hero paragraph |
116
+ | `url` | string | none | `<link rel="canonical">` and `og:url`. Absolute, for a relative `ogImage` |
117
+ | `ogTitle` | string | the document title | `og:title` |
118
+ | `ogImage` | `{ src, alt? }` or a URL | none | `og:image`, `og:image:alt`, and `twitter:card` as `summary_large_image` |
119
+ | `locale` | string | none | `og:locale`, such as `en_US` |
120
+ | `themeColor` | string | the page surface in each scheme | `<meta name="theme-color">` |
121
+ | `themeToggle` | boolean | `false` | An inline script that applies the scheme a `norn-theme-toggle` saved, before the first paint |
122
+ | `icon` | `{ href, type? }` or a URL | an empty icon | `<link rel="icon">`. The type comes from the extension. The empty icon stops the `/favicon.ico` request |
123
+ | `touchIcon` | string | none | `<link rel="apple-touch-icon">`, a 180px PNG |
124
+ | `logo` | `{ src, alt? }` or a URL | none | An image in the header in place of the name. `alt` falls back to the name |
125
+ | `nav` | `{ href, title, icon? }[]` | `[]` | The links of `norn-header`. `icon` names a `norn-icon` |
126
+ | `footer` | object | `{ badges: [], groups: [] }` | `norn-footer`, from the fields below |
127
+ | `footer.name`, `footer.text` | string | the name, the tagline | The lead column's title and line |
128
+ | `footer.license` | `{ name, url }` | none | "Licensed under {license}", linked |
129
+ | `footer.badges` | `{ href, src, alt, width?, height? }[]` | `[]` | Linked images under the lead |
130
+ | `footer.groups` | `{ heading, links: { href, title }[] }[]` | `[]` | The footer navigation, one list per group |
131
+ | `footer.colophon` | string | none | A closing line |
132
+
133
+ A relative `ogImage.src` resolves against `url`. When `url` is not absolute, the build logs a `norn-head:` warning, leaves out `og:image` and `og:image:alt`, and sets the card to `summary`. A path with a leading slash (`/img/share.png`) resolves against the origin of `url`, one without (`share.png`) against the page. Without an image, no image tag renders and the card is `summary`.
134
+
135
+ Each of the other layouts adds its own props, below. The base props a layout reads are named under its heading.
136
+
137
+ #### `norn-base.webc`
138
+
139
+ The page frame: head, skip link, header, `main` around the content, and footer. It takes the props above. It compiles Markdown content once, so a Markdown page's callouts, code fences, and tables render as components, also through a Nunjucks or Liquid layout of the project's or a layout alias. Content that WebC already compiled goes in as it is: a WebC page, a `templateEngineOverride` with `webc`, a `{% renderTemplate "webc" %}` block, and a page on any shipped layout or on a WebC layout of the project's own. A Nunjucks, Liquid, or HTML page renders its components through `{% renderTemplate "webc" %}`. The `nornCompiles` helper decides, from `page.templateSyntax`. Every other shipped layout sets `nornCompiled: true` in its front matter, so the frame leaves the content it compiled; a WebC layout of the project's own that compiles the content and sits deeper in the chain than the page's own layout sets it too.
140
+
141
+ A `<section>` directly in `main` is a centered column at `--norn-layout-width`, with block padding, a gap, and a second-level heading style. Its `id` is the anchor. The rule is in the `norn.layout` layer under `:where(main)`, so a project rule overrides it, and a section anywhere else is left alone.
142
+
143
+ #### `norn-docs.webc`
144
+
145
+ A docs page: the sidebar with site search, the title, the contents, the body in `norn-rich-text`, and the pager. Reads `title`.
146
+
147
+ | Prop | Shape | Default | Rendered as |
148
+ | --- | --- | --- | --- |
149
+ | `docs` | `{ href, title, icon? }[]` | `[]` | The sidebar's `norn-side-nav`, in order, with the current page marked |
150
+ | `prev`, `next` | `{ href, title }` | the neighbors in `docs` | The `norn-pager` |
151
+ | `search` | `{ bundle?, site? }` or `false` | the Pagefind script at `/pagefind/pagefind.js`, and the host of `url` | `norn-search` above the sidebar list. `false` leaves it out |
152
+
153
+ The contents come from `nornHeadings` over the body, so they list the headings the Markdown add-on anchors and any `norn-heading-anchor` written by hand. The layout shows the title as the page's h1, so a Markdown body keeps no h1 of its own: a leading `# Title` that repeats the title is dropped, and any other `#` heading renders as an h2 (`singleTitle`). A page 60rem wide or more has them as a sticky column beside the body; narrower, they fold under their title, and the sidebar folds into a closed `<details>` named "Documentation".
154
+
155
+ #### `norn-docs-home.webc`
156
+
157
+ The hero with the install sample, then the docs pages as a contents list, then the page body. Reads `name`, `tagline`, and `description`.
158
+
159
+ | Prop | Shape | Default | Rendered as |
160
+ | --- | --- | --- | --- |
161
+ | `lede` | string | `description` | The hero paragraph |
162
+ | `install` | string | none | The install sample in the hero |
163
+ | `repo` | string | none | The "View on GitHub" button |
164
+ | `docs` | `{ href, title, description? }[]` | `[]` | The contents list, and the "Read the docs" button to the first page |
165
+
166
+ #### `norn-landing.webc`
167
+
168
+ The hero with a sample, then install, features, and packages sections, then the page body. Reads `name`, `tagline`, and `description`.
169
+
170
+ | Prop | Shape | Default | Rendered as |
171
+ | --- | --- | --- | --- |
172
+ | `lede` | string | `description` | The hero paragraph |
173
+ | `repo` | string | none | The "View on GitHub" button |
174
+ | `license`, `node` | string | none | Facts under the hero, such as "License: MIT" and "Node 24+" |
175
+ | `install` | string | none | The install sample; with `usage`, its own section |
176
+ | `usage` | `{ language, code, caption? }` | none | The hero's sample |
177
+ | `features` | `{ title, description, href? }[]` | `[]` | Cards in the features section |
178
+ | `packages` | `{ title, description, href? }[]` | `[]` | Cards in the packages section, titles in the mono face |
179
+
180
+ #### `norn-workspaces-home.webc`
181
+
182
+ A monorepo's home page: the hero with the install sample, then an index of the workspaces, then the page body. Reads `name`, `tagline`, and `description`.
183
+
184
+ | Prop | Shape | Default | Rendered as |
185
+ | --- | --- | --- | --- |
186
+ | `lede` | string | `description` | The hero paragraph |
187
+ | `install` | string | none | The install sample |
188
+ | `repo` | string | none | The "View on GitHub" button |
189
+ | `workspaces` | `{ href, title, description? }[]` | `[]` | Cards in the packages section |
190
+
191
+ #### `norn-workspace.webc`
192
+
193
+ One workspace's page in the docs frame. Reads `title`.
194
+
195
+ | Prop | Shape | Default | Rendered as |
196
+ | --- | --- | --- | --- |
197
+ | `workspaces` | `{ href, title, icon? }[]` | `[]` | The sidebar |
198
+ | `workspace` | `{ name, description?, html? }` | none | The `<h1>`, the description, and `html`, compiled as WebC. The page's own content follows |
199
+
200
+ On the three home layouts, the page body follows the shipped sections in a `<section>` of its own, in `norn-rich-text`, and a page with no body renders none. Markdown there is compiled once, as on `norn-docs`.
201
+
202
+ The layouts' texts ("Get started", "Read the docs", "Packages") come from `componentStrings` under the layout's name, then English (see Translating).
203
+
204
+ ### Copy a layout
205
+
206
+ The layouts ship as files in the package's `layouts/` folder. To change one, copy it into the project and turn the shipped one off:
207
+
208
+ 1. Leave the layout out: `config.addPlugin(norn, { layouts: ["norn-docs"], props })`. The other five stay.
209
+ 2. Copy `node_modules/@allons-y/webc-patterns/layouts/norn-docs.webc` into the project's layouts folder (`_includes/` unless `dir.layouts` says otherwise).
210
+ 3. Edit the copy. It still extends `norn-base.webc` and reads `$data.norn`, so the mapping needs no change.
211
+
212
+ A file in the layouts folder with the name of a shipped layout, and no matching entry in `layouts`, fails the build: it would replace the shipped one without a word. A copy of `norn-base.webc` also needs the text of `css/base.css` and `css/print.css` pasted over its two style placeholders, which the plugin fills only in the shipped file.
213
+
214
+ ### Hooks
215
+
216
+ `norn-base` has four hooks that a theme or the project fills with markup:
217
+
218
+ | Hook | Where |
219
+ | --- | --- |
220
+ | `head` | The end of `<head>`: a stylesheet, a favicon, a font preload |
221
+ | `banner` | Before the header |
222
+ | `cta` | The end of `main` |
223
+ | `credit` | The footer's last column |
224
+
225
+ ```js
226
+ import { fillHooks } from "@allons-y/webc-patterns/plugin";
227
+
228
+ fillHooks(config, { head: '<link rel="stylesheet" href="/assets/tokens.css" webc:keep />', credit: "<my-credit></my-credit>" });
229
+ ```
230
+
231
+ A fill is WebC markup, compiled with the page, so it may call components; give a `<link>` or `<script src>` `webc:keep`. Fills are appended in the order they are added, before or after the plugin. An unknown hook name throws, and no page data can change a hook: `norn-base` reads them through the `nornHook` helper. Fills are trusted markup, written in the config.
232
+
233
+ ### Theming
234
+
235
+ A shared token is a custom property named for its path in `tokens.json`: `ui.border.color` is `--norn-ui-border-color`. Every component reads its tokens with the default compiled in as the fallback, so nothing has to define them and a component renders the same with none set. Set any of them in the project's own `tokens.css`, unlayered, and every component follows. Link it from the `head` hook, and copy the file to the output:
236
+
237
+ ```css
238
+ /* assets/tokens.css */
239
+ :root {
240
+ --norn-ui-accent: #1f6f8b;
241
+ --norn-font-body: "Instrument Sans", system-ui, sans-serif;
242
+ --norn-space-m: 1rem;
243
+ }
244
+ ```
245
+
246
+ ```js
247
+ fillHooks(config, { head: '<link rel="stylesheet" href="/assets/tokens.css" webc:keep />' });
248
+ config.addPassthroughCopy("assets");
249
+ ```
250
+
251
+ The accent drives the link color (`--norn-text-accent`) and the text on an accent surface (`--norn-text-on-accent`) too, since their defaults are derived from it. The heading face (`--norn-font-heading`) follows the body face until it is set. The properties sit in no layer, so the order of the stylesheets does not matter. To read a token in the project's own CSS, give it the default as the fallback, as the components do: `var(--norn-space-m, 1.25rem)`. The component preview lists every token with its default.
252
+
253
+ | Group | Tokens |
254
+ | --- | --- |
255
+ | `ui` | `--norn-ui-accent`, `--norn-ui-border-color`, `--norn-ui-border-strong`, `--norn-ui-border-radius`, `--norn-ui-radius-s`, `--norn-ui-focus-width`, `--norn-ui-focus-offset`, `--norn-ui-state-hover`, `--norn-ui-state-press`, `--norn-ui-motion-duration`, `--norn-ui-motion-easing`, `--norn-ui-control-height`, `--norn-ui-icon-s`, `--norn-ui-icon-m`, `--norn-ui-shadow` |
256
+ | `surface` | `--norn-surface-base`, `--norn-surface-raised`, `--norn-surface-code` |
257
+ | `text` | `--norn-text-base`, `--norn-text-muted`, `--norn-text-accent`, `--norn-text-on-accent`, `--norn-text-measure` |
258
+ | `font` | `--norn-font-body`, `--norn-font-heading`, `--norn-font-mono`, `--norn-font-size-s`, `--norn-font-size-m`, `--norn-font-size-l`, `--norn-font-size-xl` |
259
+ | `code` | `--norn-code-comment`, `--norn-code-string`, `--norn-code-keyword`, `--norn-code-function`, `--norn-code-number`, `--norn-code-flag` |
260
+ | `space` | `--norn-space-2xs`, `--norn-space-xs`, `--norn-space-s`, `--norn-space-m`, `--norn-space-l`, `--norn-space-xl` |
261
+ | `layout` | `--norn-layout-width` |
262
+ | `status` | `--norn-status-info`, `--norn-status-success`, `--norn-status-warning`, `--norn-status-danger` |
263
+
264
+ A theme is a file like that, plus the components and hook fills it adds through the plugin's `components` option and `fillHooks`. A theme styles a component with CSS against its tag, in its own layer; no component exposes a custom property of its own. [`@allons-y/webc-oss-site`](https://github.com/allonsy-studio/webc-resources/tree/main/packages/webc-oss-site#readme) is a theme. Its `css/tokens.css` sets the mono face, its `css/brand.css` draws the header strip and styles the footer name by the component tags, both in the `norn.oss` layer, and its plugin fills all four hooks:
265
+
266
+ ```css
267
+ @layer norn.oss {
268
+ :root {
269
+ --norn-font-mono: "PT Mono", ui-monospace, monospace;
270
+ }
271
+
272
+ norn-header > header::after {
273
+ content: "";
274
+ position: absolute;
275
+ inset: auto 0 0;
276
+ block-size: 3px;
277
+ background: linear-gradient(90deg, #d22d6f, #ff944d);
278
+ }
279
+ }
280
+ ```
281
+
282
+ ### Collisions
283
+
284
+ Tag names are the file names of the components: `norn-*` for the patterns, and `oss-*` for the OSS site theme. A project component with the tag of a packaged one, a `norn-link-button.webc` of its own, fails the build with "Global component name collision". Give the project's components another prefix, or replace a packaged one on purpose by leaving it out:
285
+
286
+ ```js
287
+ config.addPlugin(norn, { props, exclude: ["norn-card"], components: ["_includes/*.webc"] });
288
+ ```
289
+
290
+ `exclude` throws when it names a component the package does not have. The plugin also fails, with a message that says what to remove, when:
291
+
292
+ - A file in the project's includes or layouts folder has the name of a shipped layout (see Copy a layout).
293
+ - `@11ty/eleventy-plugin-webc` is registered a second time, by the project or by another plugin.
294
+ - The Norn plugin is added twice, which includes adding it beside a theme that adds it.
295
+
296
+ ### The reset
297
+
298
+ `css/reset.css` is optional, in the `norn.reset` layer: the components and layouts look the same without it. The plugin exports its path as `reset`. Copy it to the site and link it from the head hook, or embed its text:
299
+
300
+ ```js
301
+ import fs from "node:fs";
302
+ import { fillHooks, reset } from "@allons-y/webc-patterns/plugin";
303
+
304
+ fillHooks(config, { head: `<style>${fs.readFileSync(reset, "utf8")}</style>` });
305
+ ```
306
+
307
+ ### A layout of the project's own
308
+
309
+ A layout that does not extend `norn-base` calls `norn-head` in its head, which declares the layer order and links the bundles, and adds the base and print styles, `css/base.css` and `css/print.css` (the plugin exports their paths as `base` and `print`):
310
+
311
+ ```html
312
+ <!doctype html>
313
+ <html lang="en">
314
+ <head>
315
+ <meta webc:is="norn-head" webc:nokeep :@title="title" :@description="description" />
316
+ <link rel="stylesheet" href="/assets/norn/base.css" webc:keep />
317
+ <link rel="stylesheet" href="/assets/norn/print.css" webc:keep />
318
+ </head>
319
+ <body>
320
+ <norn-skip-link></norn-skip-link>
321
+ <main id="main" tabindex="-1"><template webc:nokeep @html="content"></template></main>
322
+ </body>
323
+ </html>
324
+ ```
325
+
326
+ - **Render a Markdown page's `content` with `@html`.** WebC then compiles the `norn-callout` and `norn-code-block` tags the add-ons write.
327
+ - **Call head components with `<meta webc:is>`.** A custom tag in `<head>` makes the parser close the head early, so a head component is called as a `<meta>` with `webc:is` and `webc:nokeep`. A void tag has no children, so `norn-head` has no slot: write extra head tags after it.
328
+ - **The layer order comes first.** `norn-head` declares it in an inline style ahead of the bundle links, so it holds whatever order the page's styles bundle in.
329
+ - **Link bundles with `webc:keep`.** WebC removes `<link>` and `<script src>` without it. `norn-head` does this for the bundles.
330
+ - **Every packaged layer sits inside one `norn` layer:** `norn.reset`, `norn.base`, `norn.layout`, `norn.components`, `norn.oss`, `norn.print`. A project that uses its own layers, or a framework such as Tailwind, orders the whole package with one name, as in `@layer norn, base, components, utilities;`, and no layer names collide.
331
+ - **Project CSS stays unlayered.** It beats every layer without `!important`.
332
+ - **`.visually-hidden` hides text but keeps it read.** `css/base.css` defines it. It shows again while it or a child has focus.
333
+
334
+ Copy the two stylesheets to the output with `addPassthroughCopy`, or embed their text as `norn-base` does. See the [neutral fixture](https://github.com/allonsy-studio/webc-resources/tree/main/fixtures/neutral) for a working site on the plugin and the layouts, with no theme.
335
+
336
+ ### Markdown
337
+
338
+ The components are made for hand-written HTML. The plugin's Markdown add-ons turn Markdown into the markup an author would write for the same components. Without the plugin, amend the library yourself:
339
+
340
+ ```js
341
+ import { callouts, headingAnchors, headings, scrollTables, singleTitle, webcTags } from "@allons-y/webc-patterns/markdown";
342
+
343
+ config.amendLibrary("md", webcTags);
344
+ config.amendLibrary("md", scrollTables);
345
+ config.amendLibrary("md", callouts);
346
+ config.amendLibrary("md", singleTitle);
347
+ config.amendLibrary("md", headingAnchors);
348
+ config.addJavaScriptFunction("nornHeadings", headings);
349
+ ```
350
+
351
+ - `webcTags` keeps a component tag with WebC attributes (`@variant`, `:@level`, `webc:if`) as HTML. Without it, markdown-it escapes the tag to text: CommonMark's attribute names have no `@`. It acts only on custom elements, names with a hyphen, and only when the library's `html` option is on, as it is in Eleventy. It also gives a `<script>` or a `<link>` in Markdown `webc:keep`, so an embed (`<script async src="https://…">`, `<script src="/local.js">`) reaches the browser as written: without it, WebC fails the build on a full URL or a site path, and moves an inline script into the page bundle. Where a layout inserts the HTML without compiling it, the attribute stays in the output, where browsers ignore it.
352
+ - `scrollTables` wraps each table in `norn-simple-table`.
353
+ - `callouts` turns a `> [!NOTE]` blockquote into a `norn-callout` of that kind.
354
+ - `singleTitle` keeps a page on `norn-docs` or `norn-workspace`, which show the title as their h1, to one h1: a leading `# Title` that repeats the title goes, and any other h1 becomes an h2. `{ layouts }` names a project layout that shows the title too.
355
+ - `headingAnchors` renders each h2 and h3 as `norn-heading-anchor`, with an id from `slugify`, as an author writes it: `## Install` becomes `<norn-heading-anchor @slug="install">Install</norn-heading-anchor>`. A heading whose text holds a link keeps that link and gets the id only, since a link can't hold another. Like the callouts, the tag needs WebC to compile it: on a layout that doesn't, such as a Nunjucks one of the project's own, or in a feed built from `templateContent`, it stays an uncompiled tag.
356
+ - `headings(html)` lists the anchored headings for `norn-toc`, from compiled HTML or from Markdown output that WebC has not compiled yet, `norn-heading-anchor` tags included. Register it as `nornHeadings`.
357
+ - `slugify(text)` is the id function `headingAnchors` uses. It keeps the letters and digits of any script (`## 日本語の見出し` gets `id="日本語の見出し"`), lowercases them, takes the accents off Latin letters, and joins the rest with hyphens. A repeated heading gets `-2`, `-3`, and so on, counted across the page, so two Markdown blocks on one page never share an id.
358
+
359
+ The add-on writes only what an author would write by hand, and no text of its own. A callout title, a code block's labels, and every other string come from the component's slots, which fall back to `componentStrings` and then English (see Translating).
360
+
361
+ ### Components in Markdown
362
+
363
+ Write a component in Markdown as its tag, with its props, as in a WebC file. Two routes work:
364
+
365
+ - A Markdown page (`.md`) on `norn-base` or `norn-docs`. The layout compiles the page's HTML as WebC.
366
+ - A WebC page with Markdown blocks, for a page that is mostly components:
367
+
368
+ ```html
369
+ <template webc:type="11ty" 11ty:type="md" webc:nokeep>
370
+ ## Install
371
+
372
+ Run the installer once.
373
+ </template>
374
+ <norn-callout @variant="tip">
375
+ <span slot="heading">Before you start</span>
376
+ <template webc:type="11ty" 11ty:type="md" webc:nokeep>
377
+ Back up the **config** first.
378
+ </template>
379
+ </norn-callout>
380
+ ```
381
+
382
+ plugin-webc renders each block with the site's Markdown library, so the add-ons apply, and the page then compiles the output. Keep the Markdown in a block flush left: an indented `## Heading` renders as a paragraph. `11ty:type="liquid,md"` runs Liquid first, for page data.
383
+
384
+ In both routes:
385
+
386
+ - A block component takes its open tag alone on a line, a blank line, the Markdown body, a blank line, and its close tag alone on a line. Without the blank lines, CommonMark keeps the body as raw HTML, so `**bold**` stays literal. On one line with text, the component lands inside a `<p>`.
387
+ - An inline component, such as `norn-icon` or `norn-link-button`, may sit in a sentence.
388
+ - A tag fits on one line. CommonMark reads no HTML tag whose attributes span lines.
389
+ - Code spans and fences stay literal: `` `<norn-icon @name="info">` `` shows the tag.
390
+ - A `<script>` or `<link>` stays where it is written, for the browser, and WebC's bundler leaves it alone.
391
+ - The content is compiled as WebC, so it is as trusted as a `.webc` file: a dynamic attribute (`:data-x="…"`) runs at build time. Render content from outside the repository with `markdown: false` or through `@raw`.
392
+
393
+ Avoid `markdownTemplateEngine: "webc"`. WebC then runs before Markdown, and Markdown parses WebC's output again: a blank line in a component's output turns into a paragraph, `> [!TIP]` loses its `>`, a tag in a fence is compiled, so a code sample shows rendered HTML, and any dynamic attribute in the page text runs at build time. Keep `markdownTemplateEngine: false`, as in Setup, and use one of the two routes above.
394
+
395
+ ### Project data
396
+
397
+ `@allons-y/webc-patterns/project` has optional builders for the data a project site maps to the props. They read files; nothing in Norn reads their output unless the project's mapping passes it on.
398
+
399
+ | Export | Purpose |
400
+ | --- | --- |
401
+ | `buildSite({ packagePath, projectPath, host })` | Site facts from `package.json` and `project.json`: name, tagline, description, license, repository links, the Node version, and the URL (the project's, or `host` plus the repository name) |
402
+ | `buildWorkspaces({ root })` | One entry per workspace, an item for `workspaces`: `href`, `title`, and `description`, with the package `name` and `slug`, and its SKILL.md or README.md as `html`, rendered with the site's Markdown add-ons, so headings get the same ids and anchors as a docs page. The README's raw HTML renders as HTML, as on GitHub. Nothing is sanitized: the README is the project's own, and `norn-workspace` compiles it as WebC, as trusted as any page |
403
+ | `npmRelease(packageName)` | Whether the package has a `latest` release on npm, for badges. Cached an hour; offline is no release |
404
+ | `accentHex(cssPath)` | The `--norn-ui-accent` hex digits a stylesheet sets, for a badge color |
405
+
406
+ The `project.json` schema is `@allons-y/webc-patterns/schema/project.schema.json`.
407
+
408
+ ## Components
409
+
410
+ | Component | Props and slots | Purpose |
411
+ | --- | --- | --- |
412
+ | `norn-head` | `@title`, `@description`, `@url`, `@theme-color`, `@og-title`, `@og-image`, `@og-image-alt`, `:@theme-toggle` | Head metadata, Open Graph and Twitter card tags, the layer order, and the bundle links. `norn-base` calls it |
413
+ | `norn-skip-link` | `@href`, slot `label` (the text) | Link past the page chrome to the main content |
414
+ | `norn-header` | slot `brand` (the home link), default slot (the links), slots `label` and `summary`, `@logo-height` (a CSS length, 2rem by default) | Brand link and primary navigation. Below 40rem the links fold into a closed `<details>` |
415
+ | `norn-footer` | default slot (the columns), slot `colophon`, `@width` and `@lead-width` (CSS lengths) | Footer lock-up: a lead column up to 25rem, then columns sized to their content |
416
+ | `norn-hero` | `@width` (`layout` or `measure`), default slot (h1, lede, actions, `norn-fact-list` with `@gap="l"`, extras) | Page intro |
417
+ | `norn-card` | `@accent` (`accent`, `info`, `success`, `warning`, `danger`, or `none`), `@title-face` (`mono`), default slot (a heading, then the body) | Card on a raised surface. A heading link covers the whole card, and an accent draws a 2px line along the top |
418
+ | `norn-card-grid` | `@gap` (`s`, `m`, or `l`), `@min` (a length), default slot (`<li>` cards) | Responsive grid of cards |
419
+ | `norn-side-nav` | `:@items` (`{ href, title, icon?, children? }[]`), slots `label` and `search` | Page links, with `aria-current` on the current page and its parent |
420
+ | `norn-sidebar-layout` | slots `nav` and `summary`, default slot (the body, from its h1), `@nav-width` (a CSS length), `@gap` (`s`, `m`, `l`, `xl`), `@columns` (the body's grid columns) | Navigation column beside a page body. The navigation stays in view; below 37.5rem the columns stack and the navigation folds into a closed `<details>` |
421
+ | `norn-rich-text` | `@measure` (`text` or `none`), default slot | Prose styles for long-form content |
422
+ | `norn-pager` | `:@prev`, `:@next` (`{ href, title }`), slot `label` | Links to the neighboring pages |
423
+ | `norn-link-button` | `@href`, `@variant` (`primary`, `secondary`, or `quiet`), `@tone` (`accent` or `current`), default slot | A link styled as a button, for a call to action |
424
+ | `norn-fact-list` | `@gap` (`xs`, `s`, `m`, or `l`, the space between rows), default slot (`<li>` facts) | A row of short facts, each with an optional icon |
425
+ | `norn-segmented-control` | `:@icon-only`, slot `legend` (required), default slot of `<label>` + radio, or `@name` and `:@options` | One choice from a few, from native radios. The checked option is tinted in the accent. A missing legend fails the build |
426
+ | `norn-theme-toggle` | slots `label`, `light`, `system`, `dark` | Light, system, or dark, saved in this browser. Hidden without a script. Pair with `norn-head`'s `:@theme-toggle="true"` |
427
+ | `norn-copy-button` | `@value` or `@source` (an id), `:@compact`, `@surface` (`base`, `raised`, or `code`), slots `copy`, `copied`, `failed`, `label` | Copies text and says so. Hidden where the Clipboard API is missing |
428
+ | `norn-simple-table` | default slot (a `<table>`), slot `label` | A wide table scrolls in its own box, with a shadow at the edge that has more, and a tab stop named by the caption while it scrolls |
429
+ | `norn-notice` | `@severity` (`info`, `success`, `warning`, `danger`), `:@live`, default slot, slots `prefix` and `action` | A message about the page, with an icon and a severity word read first |
430
+ | `norn-callout` | `@variant` (`note`, `tip`, `important`, `warning`, `caution`), slot `heading`, default slot | An aside in long-form text, set like a pull quote on a raised panel with an eyebrow label, as `role="note"`. For a status message, use `norn-notice` |
431
+ | `norn-accordion` | default slot (`<details>` sections) | Disclosures from native `details`. Give them one `name` to open one at a time |
432
+ | `norn-heading-anchor` | `@slug`, `:@level`, default slot | A heading whose text links to its own section |
433
+ | `norn-toc` | `:@items` (`{ id, text, level }[]`, from `nornHeadings`), `@fold` (`always`, or `narrow` below 60rem), slot `label` | The page's contents, marking the section in view. Folds under its title as `@fold` says |
434
+ | `norn-breadcrumbs` | `:@items` (`{ href, title }[]`), slot `label` | The trail to the current page, with chevrons that mirror in right-to-left text |
435
+ | `norn-code-tabs` | `@group`, default slot (`norn-code-block`s), slots `label` and `sample` | Tabs over code samples, kept in step by group; stacked without a script |
436
+ | `norn-search` | `@bundle`, `@action`, `@site`, slots `label`, `none`, `results-one`, `results-other` (and `-zero`, `-two`, `-few`, `-many`) | Live results from a Pagefind index; a plain web search form without one |
437
+ | `norn-icon` | `@name`, `@size` (`s` or `m`), slot `label` | An inline icon from a built-in set or the plugin's `icons` option, at 1em, or the title size for `m`. Decorative unless it has a label; directional icons mirror in right-to-left text |
438
+
439
+ A component is styled through the shared tokens in `tokens.json` (`--norn-ui-accent`, `--norn-ui-border-color`, `--norn-space-m`, and so on). The component preview lists the tokens each one reads, from its source. Each token falls back to its default, compiled into the component, so a component renders the same with no tokens set, and no stylesheet defines them. A custom property of a component's own is not part of its documented API.
440
+
441
+ ### Pager
442
+
443
+ `norn-docs` links the neighbors itself. In a page of the project's own, `adjacent` returns the pages around the current one: map them to `{ href, title }` at the call site, and keep the lookup in the expression: a `webc:setup` block in a layout runs once per template, so a value computed there is shared by every page.
444
+
445
+ ```html
446
+ <norn-pager
447
+ :@prev="link(nornAdjacent($data.collections.docs, $data.page).prev)"
448
+ :@next="link(nornAdjacent($data.collections.docs, $data.page).next)"
449
+ ></norn-pager>
450
+ ```
451
+
452
+ Here `link` is a pure helper in the layout's `webc:setup` block: `(page) => (page ? { href: page.url, title: page.data.title } : undefined)`.
453
+
454
+ ### Translating
455
+
456
+ Every string a visitor sees or hears, accessible names and text a script shows included, is content in the page: a named slot on the component. Fill the slot on the tag for one instance, as `<span slot="label">Rechercher</span>`. An empty slot falls back to the site's strings from `componentStrings`, then to the English default:
457
+
458
+ ```html
459
+ <norn-search @bundle="/pagefind/pagefind.js">
460
+ <span slot="label">Rechercher</span>
461
+ <span slot="results-one">{count} résultat</span>
462
+ <span slot="results-other">{count} résultats</span>
463
+ </norn-search>
464
+ ```
465
+
466
+ A line break inside the tag leaves every named slot's fallback in place, so `<norn-skip-link>` and `</norn-skip-link>` can sit on their own lines. Set the strings once for the whole site with the plugin's `strings` option, keyed by component and slot name. The plugin registers them through `componentStrings`:
467
+
468
+ ```js
469
+ config.addPlugin(norn, {
470
+ props,
471
+ strings: {
472
+ "norn-skip-link": { label: "Aller au contenu" },
473
+ "norn-toc": { label: "Sur cette page" },
474
+ "norn-search": { label: "Rechercher", none: "Aucun résultat", "results-one": "{count} résultat", "results-other": "{count} résultats" },
475
+ "norn-callout": { note: "Remarque", tip: "Astuce" },
476
+ "norn-landing": { start: "Commencer", license: "Licence : {license}" },
477
+ },
478
+ });
479
+ ```
480
+
481
+ A layout has no caller to fill a slot, so its texts are keys of their own under the layout's name: `norn-landing`'s `start` ("Get started") and `license` ("License: {license}"), `norn-base`'s `license` ("Licensed under {license}"), and so on. `defaultStrings` lists every component, layout, and key with its English default. A key is the slot it fills; `norn-callout`'s `heading` and `norn-notice`'s `prefix` take one key per variant or severity, and `norn-code-block` reads its button texts from the `norn-copy-button` keys. An unknown component or key fails the build with the list of valid ones. For a site in several languages, key the tables by language tag, as `{ fr: { … }, de: { … } }`: each page picks its table by `page.lang`, which [Eleventy's i18n plugin](https://www.11ty.dev/docs/plugins/i18n/) sets from the URL (`/fr/…`), and `fr-CA` falls back to `fr`. `norn-base` sets `<html lang>` from the `lang` prop, which falls back to `page.lang`. Then:
482
+
483
+ - A landmark names itself with `aria-labelledby`, pointing at its own text: the title of `norn-toc`, or visually hidden text in `norn-header`, `norn-side-nav`, `norn-pager`, and `norn-breadcrumbs`.
484
+ - Text only a script shows sits in the page as hidden elements that the script reads: `norn-copy-button`'s texts, `norn-search`'s statuses, `norn-theme-toggle`'s names, and `norn-code-tabs`'s fallback tab name.
485
+ - `norn-search` picks `results-one`, `results-other`, or another plural form with `Intl.PluralRules` for the nearest `lang`, and formats the count for it. `{count}` stands in for the number. An empty form falls back to `results-other`.
486
+ - `norn-notice`'s `prefix` carries its own punctuation, as in `"Remarque : "`.
487
+ - `norn-simple-table` without a caption or a `label` slot names its scroll box from the column headings, so it needs no string of its own.
488
+ - Markdown callouts take their titles from `componentStrings`, like any callout: the Markdown add-on writes no `heading` slot.
489
+ - `norn-pager`'s arrows are drawn in CSS and point the other way in right-to-left text.
16
490
 
17
491
  ## Exports
18
492
 
19
493
  | Export | Purpose |
20
494
  | --- | --- |
21
495
  | `patterns({ exclude })` | Absolute glob for the package's components, minus any names in `exclude` |
22
- | `adjacent(pages, page)` | Previous and next page in a collection, for `page-pager` |
496
+ | `adjacent(pages, page)` | Previous and next page in a collection, for `norn-pager` |
497
+ | `dropEmptyBundleTags` | Eleventy transform that drops the empty bundle tags a page without bundles is left with |
498
+ | `componentStrings(table)` | Site-wide fallbacks for the strings, by component or layout and key, or by language tag, as the `nornComponentStrings` helper |
499
+ | `defaultStrings` | Every component and layout string and its English default, by name and key |
500
+
501
+ `@allons-y/webc-patterns/plugin` exports the plugin:
502
+
503
+ | Export | Purpose |
504
+ | --- | --- |
505
+ | `default` | The Norn plugin, `config.addPlugin(norn, options)` |
506
+ | `fillHooks(config, { head, banner, cta, credit })` | Adds markup to `norn-base`'s hooks |
507
+ | `reset`, `base`, `print` | Absolute paths to `css/reset.css`, `css/base.css`, and `css/print.css` |
508
+ | `HOOKS`, `LAYOUTS` | The hook names and the layout names |
509
+ | `compiles(data, layout, aliases)` | Whether a shipped layout compiles the page content: the rule behind `nornCompiles` |
510
+
511
+ `@allons-y/webc-patterns/markdown` exports the Markdown add-on:
512
+
513
+ | Export | Purpose |
514
+ | --- | --- |
515
+ | `webcTags(md)` | markdown-it amend that keeps component tags with WebC attributes, such as `@variant`, as HTML |
516
+ | `scrollTables(md)` | markdown-it amend that wraps each table in `norn-simple-table` |
517
+ | `callouts(md)` | markdown-it amend that renders `> [!NOTE]` blockquotes as `norn-callout` |
518
+ | `singleTitle(md, { layouts })` | markdown-it amend that drops a leading h1 that repeats the title, and turns any other h1 into an h2, on `norn-docs` and `norn-workspace` by default |
519
+ | `headingAnchors(md, { levels })` | markdown-it amend that renders headings as `norn-heading-anchor` with an id; levels 2 and 3 by default |
520
+ | `headings(html, { levels })` | The anchored headings of rendered HTML or Markdown output as `{ id, text, level }[]`, for `norn-toc`; register it as `nornHeadings` |
521
+ | `slugify(text)` | The URL fragment `headingAnchors` makes from heading text, in any script |
522
+
523
+ `@allons-y/webc-patterns/project` exports the project data builders above. The stylesheets are exported as `@allons-y/webc-patterns/css/reset.css`, `/css/base.css`, and `/css/print.css`, and the schema as `@allons-y/webc-patterns/schema/project.schema.json`.
23
524
 
24
525
  ## License
25
526