@allons-y/webc-patterns 0.0.0-stage → 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 (49) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +536 -2
  3. package/components/norn-accordion.webc +124 -0
  4. package/components/norn-breadcrumbs.webc +71 -0
  5. package/components/norn-callout.webc +97 -0
  6. package/components/norn-card-grid.webc +60 -0
  7. package/components/norn-card.webc +143 -0
  8. package/components/norn-code-tabs.webc +270 -0
  9. package/components/norn-copy-button.webc +220 -0
  10. package/components/norn-fact-list.webc +52 -0
  11. package/components/norn-footer.webc +122 -0
  12. package/components/norn-head.webc +59 -0
  13. package/components/norn-header.webc +238 -0
  14. package/components/norn-heading-anchor.webc +71 -0
  15. package/components/norn-hero.webc +81 -0
  16. package/components/norn-icon.webc +140 -0
  17. package/components/norn-link-button.webc +96 -0
  18. package/components/norn-notice.webc +94 -0
  19. package/components/norn-pager.webc +67 -0
  20. package/components/norn-rich-text.webc +105 -0
  21. package/components/norn-search.webc +303 -0
  22. package/components/norn-segmented-control.webc +143 -0
  23. package/components/norn-side-nav.webc +149 -0
  24. package/components/norn-sidebar-layout.webc +185 -0
  25. package/components/norn-simple-table.webc +120 -0
  26. package/components/norn-skip-link.webc +41 -0
  27. package/components/norn-theme-toggle.webc +127 -0
  28. package/components/norn-toc.webc +260 -0
  29. package/css/base.css +75 -0
  30. package/css/print.css +8 -0
  31. package/css/reset.css +45 -0
  32. package/fallback/norn-code-block.webc +13 -0
  33. package/index.js +177 -0
  34. package/layouts/norn-base.webc +137 -0
  35. package/layouts/norn-docs-home.webc +109 -0
  36. package/layouts/norn-docs.webc +77 -0
  37. package/layouts/norn-landing.webc +73 -0
  38. package/layouts/norn-workspace.webc +25 -0
  39. package/layouts/norn-workspaces-home.webc +44 -0
  40. package/lib/icons.js +166 -0
  41. package/lib/npm.js +49 -0
  42. package/lib/optional.js +25 -0
  43. package/lib/site.js +87 -0
  44. package/lib/workspaces.js +71 -0
  45. package/markdown.js +278 -0
  46. package/package.json +71 -5
  47. package/plugin.js +279 -0
  48. package/project.js +15 -0
  49. package/schema/project.schema.json +112 -0
package/index.js ADDED
@@ -0,0 +1,177 @@
1
+ /*
2
+ * SPDX-License-Identifier: MPL-2.0
3
+ * This Source Code Form is subject to the terms of the Mozilla Public
4
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ const COMPONENTS = path.join(import.meta.dirname, "components");
12
+
13
+ /**
14
+ * A folder path to put at the start of a glob: forward slashes, and a backslash before each character a glob reads as syntax,
15
+ * so a folder such as `Dropbox (Personal)` matches itself and not `Dropbox Personal`.
16
+ *
17
+ * @param {string} folder
18
+ * @returns {string}
19
+ */
20
+ const globPath = (folder) => folder.split(path.sep).join("/").replace(/[\\()*?[\]{|}]|[!+@](?=\()/g, "\\$&");
21
+
22
+ /** Component names in the package, from the files in `components/`. */
23
+ const names = () =>
24
+ fs
25
+ .readdirSync(COMPONENTS)
26
+ .filter((file) => file.endsWith(".webc"))
27
+ .map((file) => file.slice(0, -".webc".length))
28
+ .sort();
29
+
30
+ /**
31
+ * Absolute glob for the package's components, for the project's WebC `components` list.
32
+ *
33
+ * @param {{ exclude?: string[] }} [options] - Component names to leave out, so the project can supply its own
34
+ * @returns {string}
35
+ * @throws {Error} When `exclude` names a component the package does not have
36
+ */
37
+ export function patterns({ exclude = [] } = {}) {
38
+ const available = names();
39
+ const unknown = exclude.filter((name) => !available.includes(name));
40
+ if (unknown.length) {
41
+ throw new Error(`patterns: no component named ${unknown.map((name) => `"${name}"`).join(", ")}. Available: ${available.join(", ")}`);
42
+ }
43
+ const dir = globPath(COMPONENTS);
44
+ return exclude.length ? `${dir}/!(${exclude.join("|")}).webc` : `${dir}/*.webc`;
45
+ }
46
+
47
+ /**
48
+ * The pages before and after one in a collection, for `norn-pager`.
49
+ *
50
+ * @param {{ href: string, title: string }[]} pages - The items, in order
51
+ * @param {{ url: string }} page - The current page, as Eleventy's `page`
52
+ * @returns {{ prev?: object, next?: object }} The items before and after the one whose `href` is the page's `url`; both undefined when none is
53
+ */
54
+ export function adjacent(pages, page) {
55
+ const index = pages.findIndex((candidate) => candidate.href === page.url);
56
+ if (index === -1) return { prev: undefined, next: undefined };
57
+ return { prev: pages[index - 1], next: pages[index + 1] };
58
+ }
59
+
60
+ /** The empty bundle tags a page without CSS or JS is left with. HtmlBasePlugin may have rewritten the empty URL to `.`. */
61
+ const EMPTY_BUNDLE_TAGS = /[\t ]*<link rel="stylesheet" href="[./]*">\n?|[\t ]*<script src="[./]*"[^>]*><\/script>\n?/g;
62
+
63
+ /**
64
+ * Eleventy transform: drop the empty bundle `<link>` and `<script>` that `norn-head` leaves on a page with no bundles.
65
+ * `webc:if` can't guard them, because the bundle URL resolves after render. The Norn plugin registers it; without the plugin,
66
+ * register it yourself: `config.addTransform("empty-bundle-tags", dropEmptyBundleTags)`.
67
+ *
68
+ * @param {string} content
69
+ * @returns {string}
70
+ */
71
+ export function dropEmptyBundleTags(content) {
72
+ return typeof content === "string" ? content.replace(EMPTY_BUNDLE_TAGS, "") : content;
73
+ }
74
+
75
+ /** The English defaults, frozen below as `defaultStrings`. The `norn-*` layouts have no caller to fill a slot, so their texts are keyed here by layout. */
76
+ const STRINGS = {
77
+ "norn-base": { footer: "Footer", license: "Licensed under {license}" },
78
+ "norn-docs": { nav: "Documentation", search: "Search the docs" },
79
+ "norn-docs-home": { actions: "Get started", docs: "Read the docs", repo: "View on GitHub", contents: "Documentation" },
80
+ "norn-landing": {
81
+ actions: "Get started",
82
+ start: "Get started",
83
+ repo: "View on GitHub",
84
+ free: "Free & open source",
85
+ license: "License: {license}",
86
+ node: "Node {node}",
87
+ install: "Install",
88
+ features: "Features",
89
+ packages: "Packages",
90
+ },
91
+ "norn-workspace": { nav: "Packages" },
92
+ "norn-workspaces-home": { actions: "Get started", browse: "Browse packages", repo: "View on GitHub", packages: "Packages" },
93
+ "norn-breadcrumbs": { label: "Breadcrumb" },
94
+ "norn-callout": { note: "Note", tip: "Tip", important: "Important", warning: "Warning", caution: "Caution" },
95
+ "norn-code-block": { label: "{language} code", "copy-label": "Copy {name} to clipboard" },
96
+ "norn-code-tabs": { label: "Code samples", sample: "Sample {n}" },
97
+ "norn-copy-button": { copy: "Copy", copied: "Copied", failed: "Copy failed" },
98
+ "norn-header": { label: "Primary", summary: "Menu" },
99
+ "norn-notice": { info: "Note: ", success: "Success: ", warning: "Warning: ", danger: "Error: " },
100
+ "norn-pager": { label: "Pager" },
101
+ "norn-toc": { label: "On this page" },
102
+ "norn-search": {
103
+ label: "Search",
104
+ none: "No results",
105
+ "results-zero": null,
106
+ "results-one": "{count} result",
107
+ "results-two": null,
108
+ "results-few": null,
109
+ "results-many": null,
110
+ "results-other": "{count} results",
111
+ },
112
+ "norn-side-nav": { label: "Pages" },
113
+ "norn-sidebar-layout": { summary: "Menu" },
114
+ "norn-skip-link": { label: "Skip to content" },
115
+ "norn-theme-toggle": { label: "Color scheme", light: "Light", system: "System", dark: "Dark" },
116
+ };
117
+
118
+ /**
119
+ * Every string the components and layouts show or read, with its English default, by component or layout and key.
120
+ * A component's key is the slot it fills when the page leaves that slot empty, or for `norn-callout` and `norn-notice` the variant or severity whose `heading` or `prefix` it is.
121
+ * A layout's key names a text it writes itself, such as `norn-landing`'s `license`, a template with `{license}` for the value.
122
+ * `norn-code-block` takes its button texts from the `norn-copy-button` keys.
123
+ * A `null` default means the component has none and falls back to another key.
124
+ */
125
+ export const defaultStrings = Object.freeze(Object.fromEntries(Object.entries(STRINGS).map(([component, keys]) => [component, Object.freeze(keys)])));
126
+
127
+ /** Throw on a component or key `defaultStrings` doesn't list, naming the ones it does. */
128
+ function checkStrings(table, where) {
129
+ for (const [component, keys] of Object.entries(table)) {
130
+ if (!Object.hasOwn(defaultStrings, component)) throw new Error(`componentStrings: no component named "${component}"${where}. Available: ${Object.keys(defaultStrings).join(", ")}`);
131
+ for (const [key, value] of Object.entries(keys ?? {})) {
132
+ if (!Object.hasOwn(defaultStrings[component], key)) {
133
+ const hint = /[A-Z]/.test(key) ? `; keys are slot names, as "${key.replace(/[A-Z]/g, (letter) => "-" + letter.toLowerCase())}"` : "";
134
+ throw new Error(`componentStrings: ${component} has no string "${key}"${where}${hint}. Available: ${Object.keys(defaultStrings[component]).join(", ")}`);
135
+ }
136
+ if (typeof value !== "string") throw new Error(`componentStrings: ${component}.${key}${where} must be a string`);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Site-wide strings for every component and layout, so a page in another language needs no slot on each tag.
143
+ * The Norn plugin registers the result of its `strings` option. Without the plugin, register it once:
144
+ * `config.addJavaScriptFunction("nornComponentStrings", componentStrings({ "norn-toc": { label: "Sur cette page" } }))`.
145
+ * Keys are the slot names, as `label` or `results-one`. For several languages, key the tables by language tag, as `{ fr: { … }, de: { … } }`:
146
+ * a page picks its table by `page.lang`, which Eleventy's i18n plugin sets.
147
+ * A slot filled on the tag still wins, a missing string keeps the English default, and an unknown component or key throws.
148
+ *
149
+ * @param {Record<string, Record<string, string>>} table - Strings by component and key, or such tables by language tag
150
+ * @returns {(component: string, key: string, page?: { lang?: string }) => string | undefined}
151
+ */
152
+ export function componentStrings(table) {
153
+ const byLanguage = new Map();
154
+ for (const [key, value] of Object.entries(table)) {
155
+ if (Object.hasOwn(defaultStrings, key)) continue;
156
+ const [lang] = canonical(key) ?? [];
157
+ if (!lang) throw new Error(`componentStrings: "${key}" is neither a component nor a language tag. Components: ${Object.keys(defaultStrings).join(", ")}`);
158
+ checkStrings(value, ` (in "${key}")`);
159
+ byLanguage.set(lang, value);
160
+ }
161
+ const shared = Object.fromEntries(Object.entries(table).filter(([key]) => Object.hasOwn(defaultStrings, key)));
162
+ checkStrings(shared, "");
163
+ return (component, key, page) => {
164
+ const [lang] = canonical(page?.lang) ?? [];
165
+ const own = lang && (byLanguage.get(lang) ?? byLanguage.get(lang.split("-")[0]));
166
+ return own?.[component]?.[key] ?? shared[component]?.[key];
167
+ };
168
+ }
169
+
170
+ /** A language tag in its canonical case, or nothing for a missing or malformed one. */
171
+ function canonical(tag) {
172
+ try {
173
+ return tag ? Intl.getCanonicalLocales(tag) : undefined;
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ }
@@ -0,0 +1,137 @@
1
+ <!doctype html>
2
+ <html
3
+ :lang="$data.norn.lang"
4
+ :dir="['ltr', 'rtl', 'auto'].includes($data.norn.dir) ? $data.norn.dir : /^(ar|fa|he|iw|ur|ps|yi|ji|dv|ckb|sd|ug)$/i.test(String($data.norn.lang).split(/[-_]/)[0]) ? 'rtl' : 'ltr'"
5
+ >
6
+ <head>
7
+ <!---
8
+ /*
9
+ * SPDX-License-Identifier: MPL-2.0
10
+ * This Source Code Form is subject to the terms of the Mozilla Public
11
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
12
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
13
+ */
14
+ --->
15
+ <meta
16
+ webc:is="norn-head"
17
+ webc:nokeep
18
+ :@title="$data.norn.title ? ($data.norn.name && $data.norn.title !== $data.norn.name ? $data.norn.title + ': ' + $data.norn.name : $data.norn.title) : $data.norn.tagline ? ($data.norn.name ? $data.norn.name + ': ' + $data.norn.tagline : $data.norn.tagline) : $data.norn.name || ''"
19
+ :@description="$data.norn.description || ''"
20
+ :@url="typeof $data.norn.url === 'string' ? $data.norn.url : false"
21
+ :@theme-color="$data.norn.themeColor || ''"
22
+ :@og-title="$data.norn.ogTitle || ''"
23
+ :@og-image="$data.norn.ogImage ? $data.norn.ogImage.src : ''"
24
+ :@og-image-alt="$data.norn.ogImage ? $data.norn.ogImage.alt || '' : ''"
25
+ :@theme-toggle="$data.norn.themeToggle ?? false"
26
+ />
27
+ <meta webc:if="$data.norn.name" property="og:site_name" :content="$data.norn.name" />
28
+ <meta webc:if="$data.norn.locale" property="og:locale" :content="$data.norn.locale" />
29
+ <link webc:if="$data.norn.icon" rel="icon" :href="$data.norn.icon.href" :type="$data.norn.icon.type" />
30
+ <link webc:if="!$data.norn.icon" rel="icon" href="data:," />
31
+ <link webc:if="$data.norn.touchIcon" rel="apple-touch-icon" :href="$data.norn.touchIcon" />
32
+ <template webc:nokeep @html="nornHook('head')"></template>
33
+ <style>/* norn: css/base.css */</style>
34
+ <style>/* norn: css/print.css */</style>
35
+ <style>
36
+ @layer norn.layout {
37
+ /* A section directly in main is a centered column at the page width, its heading the second level. A section anywhere else is left alone. The :where() keeps the rule easy for a project to override. */
38
+ :where(main) > section {
39
+ display: grid;
40
+ grid-template-columns: minmax(0, 1fr);
41
+ align-content: start;
42
+ gap: var(--norn-space-m, 1.25rem);
43
+ inline-size: min(100% - 2 * var(--norn-space-m, 1.25rem), var(--norn-layout-width, 64rem));
44
+ margin-inline: auto;
45
+ padding-block: var(--norn-space-l, 2rem);
46
+ scroll-margin-block-start: var(--norn-space-m, 1.25rem);
47
+ }
48
+
49
+ :where(main) > section > h2,
50
+ :where(main) > section > norn-heading-anchor > h2 {
51
+ margin: 0;
52
+ font-family: var(--norn-font-heading, var(--norn-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
53
+ font-size: var(--norn-font-size-xl, 1.5rem);
54
+ line-height: 1.15;
55
+ text-wrap: balance;
56
+ }
57
+
58
+ @media print {
59
+ :where(main) > section {
60
+ gap: var(--norn-space-s, 0.75rem);
61
+ padding-block: var(--norn-space-m, 1.25rem);
62
+ }
63
+ }
64
+
65
+ /* The page title and the body's line length in a page with a navigation column, for norn-docs and norn-workspace. The base styles set the title's face and wrapping. */
66
+ norn-sidebar-layout {
67
+ & > div > article > h1 {
68
+ margin: 0;
69
+ font-size: clamp(1.9rem, 4vw, 2.5rem);
70
+ overflow-wrap: anywhere;
71
+ }
72
+
73
+ /* Prose holds to the measure, and code, tables, and callouts take the width. */
74
+ & > div > article > norn-rich-text > :is(p, ul, ol, blockquote, h2, h3, h4, h5, h6, norn-heading-anchor) {
75
+ max-inline-size: var(--norn-text-measure, 40em);
76
+ }
77
+ }
78
+
79
+ /* The space above the footer is the page's, not the footer's. */
80
+ norn-footer {
81
+ margin-block-start: var(--norn-space-xl, 4rem);
82
+
83
+ & [data-badges] {
84
+ display: flex;
85
+ flex-wrap: wrap;
86
+ gap: var(--norn-space-xs, 0.5rem);
87
+ }
88
+
89
+ & [data-badges] img {
90
+ display: block;
91
+ inline-size: auto;
92
+ block-size: 20px;
93
+ }
94
+ }
95
+ }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <norn-skip-link></norn-skip-link>
100
+ <template webc:nokeep @html="nornHook('banner')"></template>
101
+ <norn-header>
102
+ <a webc:if="$data.norn.logo || $data.norn.name" slot="brand" rel="home" href="/">
103
+ <img webc:if="$data.norn.logo" :src="$data.norn.logo.src" :alt="$data.norn.logo.alt || $data.norn.name || ''" />
104
+ <span webc:else @text="$data.norn.name || ''"></span>
105
+ </a>
106
+ <a webc:for="link of $data.norn.nav" :href="link.href" :aria-current="link.href === $data.page.url ? 'page' : undefined"><norn-icon webc:if="link.icon" :@name="link.icon"></norn-icon><span webc:nokeep @text="link.title"></span></a>
107
+ </norn-header>
108
+ <main id="main" tabindex="-1"><template webc:if="nornCompiles($data, 'norn-base')" webc:nokeep @html="content"></template><template webc:else webc:nokeep @raw="content"></template><template webc:nokeep @html="nornHook('cta')"></template></main>
109
+ <norn-footer>
110
+ <div>
111
+ <p webc:if="$data.norn.footer.name || $data.norn.name"><strong @text="$data.norn.footer.name || $data.norn.name"></strong></p>
112
+ <p webc:if="$data.norn.footer.text || $data.norn.tagline" @text="$data.norn.footer.text || $data.norn.tagline"></p>
113
+ <p webc:if="$data.norn.footer.badges.length" data-badges>
114
+ <a webc:for="badge of $data.norn.footer.badges" :href="badge.href"><img :alt="badge.alt || ''" :src="badge.src" :width="badge.width || false" :height="badge.height || false" loading="lazy" /></a>
115
+ </p>
116
+ <p webc:if="$data.norn.footer.license">
117
+ <small
118
+ ><span webc:nokeep @text="(nornComponentStrings?.('norn-base', 'license', $data.page) || 'Licensed under {license}').split('{license}')[0]"></span
119
+ ><a :href="$data.norn.footer.license.url" @text="$data.norn.footer.license.name"></a
120
+ ><span webc:nokeep @text="(nornComponentStrings?.('norn-base', 'license', $data.page) || 'Licensed under {license}').split('{license}').slice(1).join('')"></span
121
+ ></small>
122
+ </p>
123
+ </div>
124
+ <nav webc:if="$data.norn.footer.groups.length" aria-labelledby="norn-base-footer">
125
+ <span id="norn-base-footer" hidden @text="nornComponentStrings?.('norn-base', 'footer', $data.page) || 'Footer'"></span>
126
+ <div webc:for="group of $data.norn.footer.groups">
127
+ <p><strong @text="group.heading"></strong></p>
128
+ <ul>
129
+ <li webc:for="link of group.links"><a :href="link.href" @text="link.title"></a></li>
130
+ </ul>
131
+ </div>
132
+ </nav>
133
+ <template webc:nokeep @html="nornHook('credit')"></template>
134
+ <p webc:if="$data.norn.footer.colophon" slot="colophon"><small @text="$data.norn.footer.colophon"></small></p>
135
+ </norn-footer>
136
+ </body>
137
+ </html>
@@ -0,0 +1,109 @@
1
+ ---
2
+ layout: norn-base.webc
3
+ nornCompiled: true
4
+ ---
5
+ <!---
6
+ /*
7
+ * SPDX-License-Identifier: MPL-2.0
8
+ * This Source Code Form is subject to the terms of the Mozilla Public
9
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
10
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
11
+ */
12
+ --->
13
+ <norn-hero>
14
+ <h1 webc:if="$data.norn.name">
15
+ <span @text="$data.norn.name"></span>
16
+ <span webc:if="$data.norn.tagline" @text="$data.norn.tagline"></span>
17
+ </h1>
18
+ <p webc:if="$data.norn.lede || $data.norn.description" @text="$data.norn.lede || $data.norn.description"></p>
19
+ <nav webc:if="$data.norn.docs.length || $data.norn.repo" aria-labelledby="norn-docs-home-actions">
20
+ <span id="norn-docs-home-actions" hidden @text="nornComponentStrings?.('norn-docs-home', 'actions', $data.page) || 'Get started'"></span>
21
+ <ul role="list">
22
+ <li webc:if="$data.norn.docs.length"><norn-link-button :@href="$data.norn.docs[0].href"><span webc:nokeep @text="nornComponentStrings?.('norn-docs-home', 'docs', $data.page) || 'Read the docs'"></span></norn-link-button></li>
23
+ <li webc:if="$data.norn.repo"><norn-link-button :@href="$data.norn.repo" :@variant="$data.norn.docs.length ? 'secondary' : 'primary'"><span webc:nokeep @text="nornComponentStrings?.('norn-docs-home', 'repo', $data.page) || 'View on GitHub'"></span></norn-link-button></li>
24
+ </ul>
25
+ </nav>
26
+ <norn-code-block webc:if="$data.norn.install" id="install" @language="sh" :@code="$data.norn.install"></norn-code-block>
27
+ </norn-hero>
28
+ <section webc:if="$data.norn.docs.length" id="docs">
29
+ <h2 @text="nornComponentStrings?.('norn-docs-home', 'contents', $data.page) || 'Documentation'"></h2>
30
+ <dl data-norn-contents>
31
+ <div webc:for="page of $data.norn.docs">
32
+ <dt><a :href="page.href" @text="page.title"></a></dt>
33
+ <dd webc:if="page.description" @text="page.description"></dd>
34
+ </div>
35
+ </dl>
36
+ </section>
37
+ <section webc:if="String($data.content ?? '').trim()">
38
+ <norn-rich-text
39
+ ><template webc:if="nornCompiles($data, 'norn-docs-home')" webc:nokeep @html="$data.content"></template
40
+ ><template webc:else webc:nokeep @raw="$data.content"></template
41
+ ></norn-rich-text>
42
+ </section>
43
+ <style>
44
+ @layer norn.layout {
45
+ /* Contents: a title and its description, in two columns when wide. Each row is one link, with no rules between them. */
46
+ [data-norn-contents] {
47
+ display: grid;
48
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
49
+ gap: var(--norn-space-s, 0.75rem);
50
+ /* The rows are padded, so the text lines up with the heading above. */
51
+ margin: 0 calc(-1 * var(--norn-space-m, 1.25rem));
52
+
53
+ & > div {
54
+ --_state: 0%;
55
+ position: relative;
56
+ display: grid;
57
+ align-content: start;
58
+ gap: var(--norn-space-2xs, 0.25rem);
59
+ padding: var(--norn-space-s, 0.75rem) var(--norn-space-m, 1.25rem);
60
+ border-radius: var(--norn-ui-border-radius, 6px);
61
+ background-color: color-mix(in oklab, currentcolor var(--_state), transparent);
62
+
63
+ &:has(a:hover) {
64
+ --_state: var(--norn-ui-state-hover, 8%);
65
+ }
66
+
67
+ &:has(a:active) {
68
+ --_state: var(--norn-ui-state-press, 12%);
69
+ }
70
+
71
+ /* The ring goes around the row, not around the link text. */
72
+ &:has(a:focus-visible) {
73
+ outline: var(--norn-ui-focus-width, 2px) solid var(--norn-text-accent, light-dark(oklch(from color-mix(in oklch, var(--norn-ui-accent, #2346d4) 82%, black) min(l, 0.5) c h), oklch(from color-mix(in oklch, var(--norn-ui-accent, #2346d4) 55%, white) max(l, 0.72) c h)));
74
+ outline-offset: calc(-1 * var(--norn-ui-focus-offset, 2px));
75
+ }
76
+ }
77
+
78
+ & dt {
79
+ font-family: var(--norn-font-heading, var(--norn-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
80
+ font-size: var(--norn-font-size-m, 1rem);
81
+ font-weight: 700;
82
+ }
83
+
84
+ /* The link reaches over the whole row. */
85
+ & dt a {
86
+ text-decoration: none;
87
+
88
+ &::after {
89
+ content: "";
90
+ position: absolute;
91
+ inset: 0;
92
+ }
93
+
94
+ &:hover {
95
+ text-decoration: underline;
96
+ }
97
+
98
+ &:focus-visible {
99
+ outline: none;
100
+ }
101
+ }
102
+
103
+ & dd {
104
+ margin: 0;
105
+ color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
106
+ }
107
+ }
108
+ }
109
+ </style>
@@ -0,0 +1,77 @@
1
+ ---
2
+ layout: norn-base.webc
3
+ nornCompiled: true
4
+ ---
5
+ <!---
6
+ /*
7
+ * SPDX-License-Identifier: MPL-2.0
8
+ * This Source Code Form is subject to the terms of the Mozilla Public
9
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
10
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
11
+ */
12
+ --->
13
+ <script webc:setup>
14
+ /**
15
+ * The host the plain search form keeps results on: `search.site`, else the host of an absolute `url`, else none.
16
+ *
17
+ * @param {object} norn - The layout props, `$data.norn`
18
+ * @returns {string}
19
+ * @throws {Error} When `url` starts with a scheme but does not parse, naming the layout and the prop
20
+ */
21
+ const searchSite = (norn) => {
22
+ if (norn.search && norn.search.site) return norn.search.site;
23
+ if (!/^https?:\/\//.test(typeof norn.url === "string" ? norn.url : "")) return "";
24
+ try {
25
+ return new URL(norn.url).host;
26
+ } catch (cause) {
27
+ throw new Error(`norn-docs: \`url\` "${norn.url}" is not a valid URL, so the search has no host to keep results on. Fix \`url\`, or set \`search.site\`.`, { cause });
28
+ }
29
+ };
30
+ </script>
31
+ <norn-sidebar-layout>
32
+ <span slot="summary" @text="nornComponentStrings?.('norn-docs', 'nav', $data.page) || 'Documentation'"></span>
33
+ <norn-side-nav slot="nav" :@items="$data.norn.docs">
34
+ <span slot="label" @text="nornComponentStrings?.('norn-docs', 'nav', $data.page) || 'Documentation'"></span>
35
+ <norn-search
36
+ webc:if="$data.norn.search !== false"
37
+ slot="search"
38
+ :@bundle="($data.norn.search && $data.norn.search.bundle) || url('/pagefind/pagefind.js')"
39
+ :@site="searchSite($data.norn)"
40
+ ><span slot="label" @text="nornComponentStrings?.('norn-docs', 'search', $data.page) || 'Search the docs'"></span
41
+ ></norn-search>
42
+ </norn-side-nav>
43
+ <h1 @text="$data.norn.title || ''"></h1>
44
+ <template webc:nokeep webc:for="toc of [nornHeadings($data.content)]"><norn-toc webc:if="toc.length > 1" :@items="toc" @fold="narrow"></norn-toc></template>
45
+ <norn-rich-text @measure="none" data-pagefind-body
46
+ ><template webc:if="nornCompiles($data, 'norn-docs')" webc:nokeep @html="$data.content"></template
47
+ ><template webc:else webc:nokeep @raw="$data.content"></template
48
+ ></norn-rich-text>
49
+ <template webc:nokeep webc:for="pair of [nornAdjacent($data.norn.docs, $data.page)]"
50
+ ><norn-pager :@prev="$data.norn.prev !== undefined ? $data.norn.prev : pair.prev" :@next="$data.norn.next !== undefined ? $data.norn.next : pair.next"></norn-pager
51
+ ></template>
52
+ </norn-sidebar-layout>
53
+ <style>
54
+ @layer norn.layout {
55
+ /* The contents sit beside the body once the page is 60rem wide. Narrower, they fold under their title, by norn-toc's fold prop. */
56
+ @container (width >= 60rem) {
57
+ /* The contents take a second, implicit column beside the body's one. */
58
+ norn-sidebar-layout > div > article:has(> norn-toc > nav) {
59
+ grid-auto-columns: 12rem;
60
+ }
61
+
62
+ norn-sidebar-layout > div > article > * {
63
+ grid-column: 1;
64
+ }
65
+
66
+ norn-sidebar-layout > div > article > norn-toc {
67
+ grid-column: 2;
68
+ grid-row: 1 / span 2;
69
+ position: sticky;
70
+ inset-block-start: var(--norn-space-m, 1.25rem);
71
+ align-self: start;
72
+ max-block-size: calc(100dvh - 2 * var(--norn-space-m, 1.25rem));
73
+ overflow-y: auto;
74
+ }
75
+ }
76
+ }
77
+ </style>
@@ -0,0 +1,73 @@
1
+ ---
2
+ layout: norn-base.webc
3
+ nornCompiled: true
4
+ ---
5
+ <!---
6
+ /*
7
+ * SPDX-License-Identifier: MPL-2.0
8
+ * This Source Code Form is subject to the terms of the Mozilla Public
9
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
10
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
11
+ */
12
+ --->
13
+ <norn-hero>
14
+ <h1 webc:if="$data.norn.name">
15
+ <span @text="$data.norn.name"></span>
16
+ <span webc:if="$data.norn.tagline" @text="$data.norn.tagline"></span>
17
+ </h1>
18
+ <p webc:if="$data.norn.lede || $data.norn.description" @text="$data.norn.lede || $data.norn.description"></p>
19
+ <nav webc:if="$data.norn.install || $data.norn.repo" aria-labelledby="norn-landing-actions">
20
+ <span id="norn-landing-actions" hidden @text="nornComponentStrings?.('norn-landing', 'actions', $data.page) || 'Get started'"></span>
21
+ <ul role="list">
22
+ <li webc:if="$data.norn.install"><norn-link-button @href="#install"><span webc:nokeep @text="nornComponentStrings?.('norn-landing', 'start', $data.page) || 'Get started'"></span></norn-link-button></li>
23
+ <li webc:if="$data.norn.repo"><norn-link-button :@href="$data.norn.repo" :@variant="$data.norn.install ? 'secondary' : 'primary'"><span webc:nokeep @text="nornComponentStrings?.('norn-landing', 'repo', $data.page) || 'View on GitHub'"></span></norn-link-button></li>
24
+ </ul>
25
+ </nav>
26
+ <norn-fact-list @gap="l">
27
+ <li><norn-icon @name="check"></norn-icon><span webc:nokeep @text="nornComponentStrings?.('norn-landing', 'free', $data.page) || 'Free & open source'"></span></li>
28
+ <li webc:if="$data.norn.license"><norn-icon @name="check"></norn-icon><span @text="(nornComponentStrings?.('norn-landing', 'license', $data.page) || 'License: {license}').replace('{license}', $data.norn.license)"></span></li>
29
+ <li webc:if="$data.norn.node"><norn-icon @name="check"></norn-icon><span @text="(nornComponentStrings?.('norn-landing', 'node', $data.page) || 'Node {node}').replace('{node}', $data.norn.node)"></span></li>
30
+ </norn-fact-list>
31
+ <template webc:nokeep>
32
+ <norn-code-block webc:if="$data.norn.usage" id="usage" :@language="$data.norn.usage.language" :@code="$data.norn.usage.code" :@caption="$data.norn.usage.caption"></norn-code-block>
33
+ <norn-code-block webc:elseif="$data.norn.install" id="install" @language="sh" :@code="$data.norn.install"></norn-code-block>
34
+ </template>
35
+ </norn-hero>
36
+ <section webc:if="$data.norn.usage && $data.norn.install" id="install">
37
+ <h2 @text="nornComponentStrings?.('norn-landing', 'install', $data.page) || 'Install'"></h2>
38
+ <norn-code-block @language="sh" :@code="$data.norn.install"></norn-code-block>
39
+ </section>
40
+ <section webc:if="$data.norn.features.length" id="features">
41
+ <h2 @text="nornComponentStrings?.('norn-landing', 'features', $data.page) || 'Features'"></h2>
42
+ <norn-card-grid>
43
+ <li webc:for="feature of $data.norn.features">
44
+ <norn-card>
45
+ <h3>
46
+ <a webc:if="feature.href" :href="feature.href" @text="feature.title"></a>
47
+ <span webc:else @text="feature.title"></span>
48
+ </h3>
49
+ <p @text="feature.description"></p>
50
+ </norn-card>
51
+ </li>
52
+ </norn-card-grid>
53
+ </section>
54
+ <section webc:if="$data.norn.packages.length" id="packages">
55
+ <h2 @text="nornComponentStrings?.('norn-landing', 'packages', $data.page) || 'Packages'"></h2>
56
+ <norn-card-grid>
57
+ <li webc:for="pkg of $data.norn.packages">
58
+ <norn-card @title-face="mono">
59
+ <h3>
60
+ <a webc:if="pkg.href" :href="pkg.href" @text="pkg.title"></a>
61
+ <span webc:else @text="pkg.title"></span>
62
+ </h3>
63
+ <p @text="pkg.description"></p>
64
+ </norn-card>
65
+ </li>
66
+ </norn-card-grid>
67
+ </section>
68
+ <section webc:if="String($data.content ?? '').trim()">
69
+ <norn-rich-text
70
+ ><template webc:if="nornCompiles($data, 'norn-landing')" webc:nokeep @html="$data.content"></template
71
+ ><template webc:else webc:nokeep @raw="$data.content"></template
72
+ ></norn-rich-text>
73
+ </section>
@@ -0,0 +1,25 @@
1
+ ---
2
+ layout: norn-base.webc
3
+ nornCompiled: true
4
+ ---
5
+ <!---
6
+ /*
7
+ * SPDX-License-Identifier: MPL-2.0
8
+ * This Source Code Form is subject to the terms of the Mozilla Public
9
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
10
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
11
+ */
12
+ --->
13
+ <norn-sidebar-layout>
14
+ <span slot="summary" @text="nornComponentStrings?.('norn-workspace', 'nav', $data.page) || 'Packages'"></span>
15
+ <norn-side-nav slot="nav" :@items="$data.norn.workspaces.map((workspace) => ({ href: workspace.href, title: workspace.title, icon: workspace.icon }))"
16
+ ><span slot="label" @text="nornComponentStrings?.('norn-workspace', 'nav', $data.page) || 'Packages'"></span
17
+ ></norn-side-nav>
18
+ <h1 @text="($data.norn.workspace && $data.norn.workspace.name) || $data.norn.title || ''"></h1>
19
+ <norn-rich-text @measure="none">
20
+ <p webc:if="$data.norn.workspace && $data.norn.workspace.description" @text="$data.norn.workspace.description"></p>
21
+ <template webc:nokeep @html="($data.norn.workspace && $data.norn.workspace.html) || ''"></template>
22
+ <template webc:if="nornCompiles($data, 'norn-workspace')" webc:nokeep @html="$data.content || ''"></template>
23
+ <template webc:else webc:nokeep @raw="$data.content || ''"></template>
24
+ </norn-rich-text>
25
+ </norn-sidebar-layout>
@@ -0,0 +1,44 @@
1
+ ---
2
+ layout: norn-base.webc
3
+ nornCompiled: true
4
+ ---
5
+ <!---
6
+ /*
7
+ * SPDX-License-Identifier: MPL-2.0
8
+ * This Source Code Form is subject to the terms of the Mozilla Public
9
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
10
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
11
+ */
12
+ --->
13
+ <norn-hero>
14
+ <h1 webc:if="$data.norn.name">
15
+ <span @text="$data.norn.name"></span>
16
+ <span webc:if="$data.norn.tagline" @text="$data.norn.tagline"></span>
17
+ </h1>
18
+ <p webc:if="$data.norn.lede || $data.norn.description" @text="$data.norn.lede || $data.norn.description"></p>
19
+ <nav webc:if="$data.norn.workspaces.length || $data.norn.repo" aria-labelledby="norn-workspaces-home-actions">
20
+ <span id="norn-workspaces-home-actions" hidden @text="nornComponentStrings?.('norn-workspaces-home', 'actions', $data.page) || 'Get started'"></span>
21
+ <ul role="list">
22
+ <li webc:if="$data.norn.workspaces.length"><norn-link-button @href="#packages"><span webc:nokeep @text="nornComponentStrings?.('norn-workspaces-home', 'browse', $data.page) || 'Browse packages'"></span></norn-link-button></li>
23
+ <li webc:if="$data.norn.repo"><norn-link-button :@href="$data.norn.repo" :@variant="$data.norn.workspaces.length ? 'secondary' : 'primary'"><span webc:nokeep @text="nornComponentStrings?.('norn-workspaces-home', 'repo', $data.page) || 'View on GitHub'"></span></norn-link-button></li>
24
+ </ul>
25
+ </nav>
26
+ <norn-code-block webc:if="$data.norn.install" id="install" @language="sh" :@code="$data.norn.install"></norn-code-block>
27
+ </norn-hero>
28
+ <section webc:if="$data.norn.workspaces.length" id="packages">
29
+ <h2 @text="nornComponentStrings?.('norn-workspaces-home', 'packages', $data.page) || 'Packages'"></h2>
30
+ <norn-card-grid>
31
+ <li webc:for="workspace of $data.norn.workspaces">
32
+ <norn-card @title-face="mono">
33
+ <h3><a :href="workspace.href" @text="workspace.title"></a></h3>
34
+ <p webc:if="workspace.description" @text="workspace.description"></p>
35
+ </norn-card>
36
+ </li>
37
+ </norn-card-grid>
38
+ </section>
39
+ <section webc:if="String($data.content ?? '').trim()">
40
+ <norn-rich-text
41
+ ><template webc:if="nornCompiles($data, 'norn-workspaces-home')" webc:nokeep @html="$data.content"></template
42
+ ><template webc:else webc:nokeep @raw="$data.content"></template
43
+ ></norn-rich-text>
44
+ </section>