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

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