@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
@@ -0,0 +1,59 @@
1
+ <!---
2
+ /*
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+ --->
9
+ <script webc:setup>
10
+ /** The default theme colors: `surface.base` from tokens.json, written out because a `<meta>` can't read a custom property. */
11
+ const SURFACE = { light: "#ffffff", dark: "#101216" };
12
+
13
+ /** Whether the page sets a share image. Checked with `typeof`: a missing `og-image` prop can resolve to page data. */
14
+ function hasImage(src) {
15
+ return typeof src === "string" && src !== "";
16
+ }
17
+
18
+ /**
19
+ * The absolute URL for `og:image`: the image resolved against the canonical URL.
20
+ * A relative image with no absolute canonical URL logs a warning and returns `''`, because Open Graph readers ignore a relative one.
21
+ * Call it once per render: it warns each time.
22
+ */
23
+ function absoluteImage(src, base) {
24
+ if (!hasImage(src)) return "";
25
+ try {
26
+ return new URL(src, typeof base === "string" && base ? base : undefined).href;
27
+ } catch {
28
+ console.warn(`norn-head: og-image "${src}" is relative, so the canonical url must be absolute to resolve it against. The og:image and og:image:alt tags are left out. Pass an absolute og-image or url.`);
29
+ return "";
30
+ }
31
+ }
32
+ </script>
33
+ <meta charset="utf-8" />
34
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
35
+ <style webc:keep>@layer norn.reset, norn.base, norn.layout, norn.components, norn.oss, norn.print;</style>
36
+ <title @text="title"></title>
37
+ <meta webc:if="description" name="description" :content="description" />
38
+ <link webc:if="typeof url === 'string'" rel="canonical" :href="url" />
39
+ <meta name="color-scheme" content="light dark" />
40
+ <script webc:if="themeToggle === true || themeToggle === '' || themeToggle === 'true'" webc:keep>
41
+ try {
42
+ const scheme = localStorage.getItem("color-scheme");
43
+ if (scheme === "light" || scheme === "dark") document.documentElement.style.colorScheme = scheme;
44
+ } catch {}
45
+ </script>
46
+ <meta webc:if="themeColor" name="theme-color" :content="themeColor" />
47
+ <meta webc:if="!themeColor" name="theme-color" :content="SURFACE.light" media="(prefers-color-scheme: light)" />
48
+ <meta webc:if="!themeColor" name="theme-color" :content="SURFACE.dark" media="(prefers-color-scheme: dark)" />
49
+ <meta property="og:type" content="website" />
50
+ <meta property="og:title" :content="ogTitle || title" />
51
+ <meta webc:if="description" property="og:description" :content="description" />
52
+ <meta webc:if="typeof url === 'string'" property="og:url" :content="url" />
53
+ <template webc:for="image of [absoluteImage(ogImage, url)]" webc:nokeep>
54
+ <meta webc:if="image" property="og:image" :content="image" />
55
+ <meta webc:if="image && typeof ogImageAlt === 'string' && ogImageAlt" property="og:image:alt" :content="ogImageAlt" />
56
+ <meta name="twitter:card" :content="image ? 'summary_large_image' : 'summary'" />
57
+ </template>
58
+ <link rel="stylesheet" :href="getBundleFileUrl('css')" webc:keep />
59
+ <script :src="getBundleFileUrl('js')" defer webc:keep></script>
@@ -0,0 +1,238 @@
1
+ <!---
2
+ /*
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+ --->
9
+ <header :style="typeof logoHeight === 'string' && logoHeight ? '--_logo: ' + logoHeight : ''">
10
+ <slot name="brand"></slot>
11
+ <details>
12
+ <summary><slot name="summary"><span webc:nokeep @text="nornComponentStrings?.('norn-header', 'summary', $data.page) || 'Menu'"></span></slot></summary>
13
+ <nav :aria-labelledby="'norn-header-' + uid"><span data-label :id="'norn-header-' + uid"><slot name="label"><span webc:nokeep @text="nornComponentStrings?.('norn-header', 'label', $data.page) || 'Primary'"></span></slot></span><slot></slot></nav>
14
+ </details>
15
+ </header>
16
+ <style>
17
+ @layer norn.components {
18
+ norn-header {
19
+ display: block;
20
+ container-type: inline-size;
21
+
22
+ & > header {
23
+ position: relative;
24
+ display: flex;
25
+ flex-wrap: wrap;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ gap: var(--norn-space-xs, 0.5rem) var(--norn-space-m, 1.25rem);
29
+ padding: var(--norn-space-s, 0.75rem) var(--norn-space-m, 1.25rem);
30
+ background: var(--norn-surface-raised, light-dark(hsl(40, 14%, 96%), #242932));
31
+ }
32
+
33
+ & > header > a[rel="home"] {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: var(--norn-space-xs, 0.5rem);
37
+ color: var(--norn-text-base, light-dark(#16181d, #eceef3));
38
+ font-family: var(--norn-font-heading, var(--norn-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
39
+ font-size: var(--norn-font-size-l, 1.25rem);
40
+ font-weight: 700;
41
+ text-decoration: none;
42
+ }
43
+
44
+ & > header > [rel="home"] img {
45
+ display: block;
46
+ inline-size: auto;
47
+ max-inline-size: 100%;
48
+ block-size: var(--_logo, 2rem);
49
+ }
50
+
51
+ /* Wide, the details is only a wrapper: its summary is hidden, and the navigation sits in the row. */
52
+ & > header > details {
53
+ display: contents;
54
+
55
+ &::details-content {
56
+ content-visibility: visible;
57
+ display: contents;
58
+ }
59
+ }
60
+
61
+ & > header > details > summary {
62
+ display: none;
63
+ }
64
+
65
+ & > header > details > nav {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ gap: var(--norn-space-2xs, 0.25rem) var(--norn-space-xs, 0.5rem);
69
+ font-size: var(--norn-font-size-m, 1rem);
70
+ }
71
+
72
+ & > header > details > nav > [data-label] {
73
+ position: absolute;
74
+ inline-size: 1px;
75
+ block-size: 1px;
76
+ overflow: hidden;
77
+ clip-path: inset(50%);
78
+ white-space: nowrap;
79
+ }
80
+
81
+ & > header > details > nav > a {
82
+ --_state: 0%;
83
+ box-sizing: border-box;
84
+ display: inline-flex;
85
+ align-items: center;
86
+ gap: var(--norn-space-2xs, 0.25rem);
87
+ min-block-size: var(--norn-ui-control-height, 2.25rem);
88
+ padding-inline: var(--norn-space-xs, 0.5rem);
89
+ border-block-end: 2px solid transparent;
90
+ border-radius: var(--norn-ui-radius-s, 4px);
91
+ background-color: color-mix(in oklab, currentcolor var(--_state), transparent);
92
+ color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
93
+ text-decoration: none;
94
+
95
+ &:hover {
96
+ --_state: var(--norn-ui-state-hover, 8%);
97
+ color: var(--norn-text-base, light-dark(#16181d, #eceef3));
98
+ }
99
+
100
+ &:active {
101
+ --_state: var(--norn-ui-state-press, 12%);
102
+ }
103
+
104
+ &[aria-current] {
105
+ border-block-end-color: 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)));
106
+ color: var(--norn-text-base, light-dark(#16181d, #eceef3));
107
+ }
108
+ }
109
+
110
+ /* Narrow, the links fold into a control at the end of the brand's row. Open, they take the full width below it. */
111
+ @container (max-width: 40rem) {
112
+ & > header {
113
+ row-gap: 0;
114
+ }
115
+
116
+ & > header > details:not([open])::details-content {
117
+ content-visibility: hidden;
118
+ }
119
+
120
+ & > header > details::details-content {
121
+ display: block;
122
+ flex: 1 0 100%;
123
+ }
124
+
125
+ & > header > details > nav {
126
+ flex-direction: column;
127
+ padding-block-start: var(--norn-space-xs, 0.5rem);
128
+ flex-wrap: nowrap;
129
+ gap: 0;
130
+ }
131
+
132
+ & > header > details > nav > a {
133
+ padding-inline: var(--norn-space-s, 0.75rem);
134
+ }
135
+
136
+ & > header > details > summary {
137
+ --_state: 0%;
138
+ box-sizing: border-box;
139
+ display: flex;
140
+ align-items: center;
141
+ gap: var(--norn-space-s, 0.75rem);
142
+ min-block-size: var(--norn-ui-control-height, 2.25rem);
143
+ margin-inline-start: auto;
144
+ padding-inline: var(--norn-space-s, 0.75rem);
145
+ border-radius: var(--norn-ui-radius-s, 4px);
146
+ background-color: color-mix(in oklab, currentcolor var(--_state), transparent);
147
+ font-weight: 600;
148
+ list-style: none;
149
+ cursor: pointer;
150
+
151
+ &::-webkit-details-marker {
152
+ display: none;
153
+ }
154
+
155
+ &:hover {
156
+ --_state: var(--norn-ui-state-hover, 8%);
157
+ }
158
+
159
+ &:active {
160
+ --_state: var(--norn-ui-state-press, 12%);
161
+ }
162
+
163
+ &:focus-visible {
164
+ outline-offset: calc(-1 * var(--norn-ui-focus-offset, 2px));
165
+ }
166
+
167
+ /* A chevron drawn with two borders, pointing down when closed and up when open. */
168
+ &::after {
169
+ box-sizing: border-box;
170
+ content: "";
171
+ flex: none;
172
+ inline-size: 0.5em;
173
+ block-size: 0.5em;
174
+ border-block-end: 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)));
175
+ border-inline-end: 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)));
176
+ rotate: 45deg;
177
+ }
178
+ }
179
+
180
+ & > header > details[open] > summary::after {
181
+ rotate: 225deg;
182
+ }
183
+ }
184
+
185
+ @media (pointer: coarse) {
186
+ & > header > details > nav > a,
187
+ & > header > details > summary {
188
+ min-block-size: 2.75rem;
189
+ }
190
+ }
191
+
192
+ @media (prefers-reduced-motion: no-preference) {
193
+ & > header > details > nav > a,
194
+ & > header > details > summary {
195
+ transition: background-color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
196
+ }
197
+ }
198
+
199
+ /* A clear border would be drawn in the text color, so the links that aren't current take the page's. */
200
+ @media (forced-colors: active) {
201
+ & > header > details > nav > a {
202
+ border-block-end-color: Canvas;
203
+ }
204
+
205
+ & > header > details > nav > a[aria-current] {
206
+ border-block-end-color: Highlight;
207
+ }
208
+
209
+ & > header > details > summary::after {
210
+ border-color: CanvasText;
211
+ }
212
+ }
213
+
214
+ @media print {
215
+ & > header > details {
216
+ display: none;
217
+ }
218
+ }
219
+
220
+ /* Last, so a header with no links drops the whole control. */
221
+ & > header > details:not(:has(a)) {
222
+ display: none;
223
+ }
224
+ }
225
+
226
+ /* Without ::details-content a closed details can't show its links, so the control is always there. */
227
+ @supports not selector(::details-content) {
228
+ norn-header > header > details > summary {
229
+ display: flex;
230
+ align-items: center;
231
+ min-block-size: var(--norn-ui-control-height, 2.25rem);
232
+ margin-inline-start: auto;
233
+ font-weight: 600;
234
+ cursor: pointer;
235
+ }
236
+ }
237
+ }
238
+ </style>
@@ -0,0 +1,71 @@
1
+ <!---
2
+ /*
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+ --->
9
+ <script webc:setup>
10
+ /** The heading's id, after checking it was passed. An unpassed `slug` resolves to a helper of that name under Eleventy, so a type check comes first. */
11
+ const required = (value) => {
12
+ if (typeof value !== "string" || !value) throw new Error("norn-heading-anchor: `@slug` is required. It is the heading's id and the link's fragment.");
13
+ return value;
14
+ };
15
+ </script>
16
+ <h3 webc:if="level == 3" :id="required(slug)"><a :href="'#' + slug"><slot></slot></a></h3>
17
+ <h4 webc:elseif="level == 4" :id="required(slug)"><a :href="'#' + slug"><slot></slot></a></h4>
18
+ <h5 webc:elseif="level == 5" :id="required(slug)"><a :href="'#' + slug"><slot></slot></a></h5>
19
+ <h6 webc:elseif="level == 6" :id="required(slug)"><a :href="'#' + slug"><slot></slot></a></h6>
20
+ <h2 webc:else :id="required(slug)"><a :href="'#' + slug"><slot></slot></a></h2>
21
+ <style>
22
+ @layer norn.components {
23
+ norn-heading-anchor {
24
+ display: block;
25
+
26
+ & > :is(h2, h3, h4, h5, h6) {
27
+ margin: 0;
28
+ scroll-margin-block-start: var(--norn-space-m, 1.25rem);
29
+
30
+ &::before {
31
+ content: "#" / "";
32
+ margin-inline-end: var(--norn-space-2xs, 0.25rem);
33
+ color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
34
+ }
35
+ }
36
+
37
+ /* At least 24px tall, the WCAG 2.2 target size, even for a short heading. */
38
+ & a {
39
+ display: inline-block;
40
+ min-block-size: 1.5rem;
41
+ color: inherit;
42
+ text-decoration: none;
43
+ text-underline-offset: 0.1em;
44
+
45
+ &:is(:hover, :focus-visible) {
46
+ text-decoration: underline;
47
+ }
48
+ }
49
+
50
+ /* The page's margin is at least 1rem on a narrow screen and wider past the content width, so the mark hangs only where the margin holds it: 1.15ch is under 20px for a second-level heading. */
51
+ @media (hover: hover) and (min-width: 70rem) {
52
+ & > :is(h2, h3, h4, h5, h6)::before {
53
+ display: inline-block;
54
+ inline-size: 1ch;
55
+ margin-inline: -1.15ch 0.15ch;
56
+ opacity: 0;
57
+ }
58
+
59
+ & > :is(h2, h3, h4, h5, h6):is(:hover, :focus-within)::before {
60
+ opacity: 1;
61
+ }
62
+ }
63
+
64
+ @media (hover: hover) and (min-width: 70rem) and (prefers-reduced-motion: no-preference) {
65
+ & > :is(h2, h3, h4, h5, h6)::before {
66
+ transition: opacity var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
67
+ }
68
+ }
69
+ }
70
+ }
71
+ </style>
@@ -0,0 +1,81 @@
1
+ <!---
2
+ /*
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+ --->
9
+ <header :data-width="width === 'measure' ? 'measure' : undefined"><slot></slot></header>
10
+ <style>
11
+ @layer norn.components {
12
+ norn-hero {
13
+ display: block;
14
+
15
+ & > header {
16
+ --_width: var(--norn-layout-width, 64rem);
17
+ display: grid;
18
+ grid-auto-flow: row;
19
+ align-content: start;
20
+ gap: var(--norn-space-m, 1.25rem);
21
+ inline-size: min(100% - 2 * var(--norn-space-m, 1.25rem), var(--_width));
22
+ margin-inline: auto;
23
+ padding-block: var(--norn-space-xl, 4rem) var(--norn-space-l, 2rem);
24
+ }
25
+
26
+ & > header[data-width="measure"] {
27
+ --_width: var(--norn-text-measure, 40em);
28
+ }
29
+
30
+ & > header > h1 {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: var(--norn-space-xs, 0.5rem);
34
+ margin: 0;
35
+ font-family: var(--norn-font-heading, var(--norn-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
36
+ font-size: clamp(2rem, 5vw, 3.25rem);
37
+ line-height: 1.15;
38
+ overflow-wrap: anywhere;
39
+ text-wrap: balance;
40
+ }
41
+
42
+ & > header > h1 > :first-child {
43
+ font-size: 1.15em;
44
+ }
45
+
46
+ & > header > h1 > :last-child:not(:first-child) {
47
+ color: 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)));
48
+ font-size: 0.55em;
49
+ font-weight: 400;
50
+ }
51
+
52
+ & > header > p {
53
+ max-inline-size: var(--norn-text-measure, 40em);
54
+ margin: 0;
55
+ color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
56
+ font-size: var(--norn-font-size-l, 1.25rem);
57
+ text-wrap: pretty;
58
+ }
59
+
60
+ & > header > nav ul {
61
+ display: flex;
62
+ flex-wrap: wrap;
63
+ gap: var(--norn-space-s, 0.75rem);
64
+ margin: 0;
65
+ padding: 0;
66
+ list-style: none;
67
+ }
68
+
69
+ @media print {
70
+ & > header > nav {
71
+ display: none;
72
+ }
73
+
74
+ & > header {
75
+ gap: var(--norn-space-s, 0.75rem);
76
+ padding-block: var(--norn-space-m, 1.25rem);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ </style>
@@ -0,0 +1,140 @@
1
+ <!---
2
+ /*
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+ --->
9
+ <script webc:setup>
10
+ const RING = "M21 12a9 9 0 1 1-18 0a9 9 0 1 1 18 0";
11
+
12
+ /** Stroke paths on a 24-unit grid, plus an optional filled path. */
13
+ const ICONS = {
14
+ accessibility: { d: `${RING}M12 7v.5M7.5 10l4.5 1l4.5-1M12 11v3M9.5 18l2.5-4l2.5 4` },
15
+ "arrow-end": { d: "M5 12h14M13 6l6 6l-6 6" },
16
+ "arrow-start": { d: "M19 12H5M11 6l-6 6l6 6" },
17
+ book: { d: "M2 5h6a4 4 0 0 1 4 4v12a3 3 0 0 0-3-3H2zM22 5h-6a4 4 0 0 0-4 4v12a3 3 0 0 1 3-3h7z" },
18
+ caution: { d: "M8 3h8l5 5v8l-5 5H8l-5-5V8zM12 8v5M12 16v.5" },
19
+ check: { d: "M5 12l5 5L20 7" },
20
+ "chevron-down": { d: "M6 9l6 6l6-6" },
21
+ "chevron-end": { d: "M9 6l6 6l-6 6" },
22
+ copy: { d: "M9 9h11v11H9zM15 9V4H4v11h5" },
23
+ desktop: { d: "M3 4h18v12H3zM8 20h8M12 16v4" },
24
+ error: { d: `${RING}M15 9l-6 6M9 9l6 6` },
25
+ external: { d: "M14 4h6v6M20 4l-9 9M18 14v6H4V6h6" },
26
+ fill: { d: "M3 12h18M7 8l-4 4l4 4M17 8l4 4l-4 4" },
27
+ hash: { d: "M5 9h14M5 15h14M10 4L8 20M16 4l-2 16" },
28
+ important: { d: "M4 4h16v12H9l-5 4zM12 7v4M12 13.5v.5" },
29
+ info: { d: `${RING}M12 11v6M12 7.5v.5` },
30
+ link: { d: "M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" },
31
+ loading: { d: "M21 12a9 9 0 1 1-9-9" },
32
+ moon: { d: "M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" },
33
+ phone: { d: "M7 2h10v20H7zM11 18h2" },
34
+ search: { d: "M18 11a7 7 0 1 1-14 0a7 7 0 1 1 14 0M21 21l-5-5" },
35
+ success: { d: `${RING}M8 12l3 3l5-6` },
36
+ sun: { d: "M16 12a4 4 0 1 1-8 0a4 4 0 1 1 8 0M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4" },
37
+ system: { d: RING, fill: "M12 3a9 9 0 0 1 0 18z" },
38
+ tablet: { d: "M4 3h16v18H4zM11 18h2" },
39
+ tip: { d: "M9 18h6M10 21h4M12 3a6 6 0 0 0-4 10.5c.6.6 1 1.5 1 2.5h6c0-1 .4-1.9 1-2.5A6 6 0 0 0 12 3z" },
40
+ warning: { d: "M12 3L2 21h20zM12 10v5M12 18v.5" },
41
+ };
42
+
43
+ const MIRROR = ["arrow-end", "arrow-start", "chevron-end"];
44
+
45
+ /** The sizes, each a shared icon token. */
46
+ const SIZES = ["s", "m"];
47
+
48
+ /** The project's icons from the Norn plugin, by name. None without the plugin. */
49
+ const project = () => (typeof nornIcons === "function" ? nornIcons() : {});
50
+
51
+ /**
52
+ * The icon for a name: a project icon, else a built-in. An unknown name fails the build rather than rendering nothing.
53
+ * @returns {{ mirror: boolean, viewBox?: string, body?: string, d?: string, fill?: string }}
54
+ */
55
+ function icon(name) {
56
+ const own = project();
57
+ if (Object.hasOwn(own, name)) return own[name];
58
+ if (!Object.hasOwn(ICONS, name)) {
59
+ throw new Error(`norn-icon: no icon named "${name}". Built-in: ${Object.keys(ICONS).join(", ")}. Project: ${Object.keys(own).join(", ") || "none"}`);
60
+ }
61
+ return { ...ICONS[name], mirror: MIRROR.includes(name) };
62
+ }
63
+
64
+ /** An attribute value, escaped. */
65
+ const attr = (value) => String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
66
+
67
+ /**
68
+ * The size attribute: none for the default, `s`. An unknown size fails the build rather than drawing at the wrong size.
69
+ * @param {string} size
70
+ */
71
+ function sized(size) {
72
+ if (!size || size === "s") return "";
73
+ if (!SIZES.includes(size)) throw new Error(`norn-icon: no size "${size}". Sizes: ${SIZES.join(", ")}`);
74
+ return ` data-size="${size}"`;
75
+ }
76
+
77
+ /**
78
+ * The whole `<svg>` for a name and a size, as markup, so the template has one expression to compile.
79
+ * A project icon keeps its own `viewBox` and body.
80
+ */
81
+ function drawing(name, size) {
82
+ const { viewBox, mirror, body, d, fill } = icon(name);
83
+ const flip = mirror ? " data-mirror" : "";
84
+ const scale = sized(size);
85
+ if (body) return `<svg${viewBox ? ` viewBox="${attr(viewBox)}"` : ""} aria-hidden="true"${flip}${scale} data-project>\n\t${body}\n</svg>`;
86
+ const extra = fill ? `\n\t<path d="${attr(fill)}" data-fill></path>` : "";
87
+ return `<svg viewBox="0 0 24 24" aria-hidden="true"${flip}${scale}>\n\t<path d="${attr(d)}"></path>${extra}\n</svg>`;
88
+ }
89
+ </script>
90
+ <template webc:nokeep @raw="drawing(name, typeof size === 'string' ? size : '')"></template>
91
+ <slot name="label"></slot>
92
+ <style>
93
+ @layer norn.components {
94
+ norn-icon {
95
+ display: inline-flex;
96
+ flex: none;
97
+ vertical-align: -0.125em;
98
+
99
+ & svg {
100
+ inline-size: var(--norn-ui-icon-s, 1em);
101
+ block-size: var(--norn-ui-icon-s, 1em);
102
+
103
+ &[data-size="m"] {
104
+ inline-size: var(--norn-ui-icon-m, 1.25rem);
105
+ block-size: var(--norn-ui-icon-m, 1.25rem);
106
+ }
107
+
108
+ &:not([data-project]) {
109
+ fill: none;
110
+ stroke: currentcolor;
111
+ stroke-linecap: round;
112
+ stroke-linejoin: round;
113
+ stroke-width: 2;
114
+ }
115
+
116
+ &[data-project] {
117
+ fill: currentcolor;
118
+ }
119
+ }
120
+
121
+ & > :not(svg) {
122
+ position: absolute;
123
+ inline-size: 1px;
124
+ block-size: 1px;
125
+ overflow: hidden;
126
+ clip-path: inset(50%);
127
+ white-space: nowrap;
128
+ }
129
+
130
+ & [data-fill] {
131
+ fill: currentcolor;
132
+ stroke: none;
133
+ }
134
+
135
+ &:dir(rtl) [data-mirror] {
136
+ transform: scaleX(-1);
137
+ }
138
+ }
139
+ }
140
+ </style>
@@ -0,0 +1,96 @@
1
+ <!---
2
+ /*
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+ --->
9
+ <a :href="href" :data-variant="variant === 'secondary' || variant === 'quiet' ? variant : 'primary'" :data-tone="tone === 'current' ? 'current' : undefined"><slot></slot></a>
10
+ <style>
11
+ @layer norn.components {
12
+ norn-link-button {
13
+ display: inline-flex;
14
+ max-inline-size: 100%;
15
+
16
+ & a[data-variant] {
17
+ --_state: 0%;
18
+ --_bg: transparent;
19
+ --_border: transparent;
20
+ box-sizing: border-box;
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: var(--norn-space-xs, 0.5rem);
24
+ min-block-size: var(--norn-ui-control-height, 2.25rem);
25
+ padding: var(--norn-space-2xs, 0.25rem) var(--norn-space-m, 1.25rem);
26
+ border: 1px solid var(--_border);
27
+ border-radius: var(--norn-ui-border-radius, 6px);
28
+ background-color: color-mix(in oklab, currentcolor var(--_state), var(--_bg));
29
+ color: 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)));
30
+ font-weight: 600;
31
+ line-height: 1.3;
32
+ overflow-wrap: anywhere;
33
+ text-decoration: none;
34
+
35
+ &:hover {
36
+ --_state: var(--norn-ui-state-hover, 8%);
37
+ }
38
+
39
+ &:active {
40
+ --_state: var(--norn-ui-state-press, 12%);
41
+ }
42
+ }
43
+
44
+ /* The surrounding text color, for a secondary or quiet button. The primary rule below keeps its own. */
45
+ & a[data-tone="current"] {
46
+ color: inherit;
47
+ }
48
+
49
+ & a[data-variant="primary"] {
50
+ --_bg: var(--norn-ui-accent, #2346d4);
51
+ color: var(--norn-text-on-accent, color(from var(--norn-ui-accent, #2346d4) xyz-d65 calc(0.95047 * clamp(0, calc((0.1791 - y) * 1000000), 1)) clamp(0, calc((0.1791 - y) * 1000000), 1) calc(1.08883 * clamp(0, calc((0.1791 - y) * 1000000), 1))));
52
+ }
53
+
54
+ & a[data-variant="secondary"] {
55
+ --_border: 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)));
56
+ }
57
+
58
+ @media (pointer: coarse) {
59
+ & a[data-variant] {
60
+ min-block-size: 2.75rem;
61
+ }
62
+ }
63
+
64
+ @media (prefers-reduced-motion: no-preference) {
65
+ & a[data-variant] {
66
+ transition: background-color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
67
+ }
68
+ }
69
+
70
+ /* The primary button stays filled, since the browser would turn its fill into the page color. */
71
+ @media (forced-colors: active) {
72
+ & a[data-variant="primary"] {
73
+ border-color: LinkText;
74
+ background-color: LinkText;
75
+ color: Canvas;
76
+ forced-color-adjust: none;
77
+
78
+ &:focus-visible {
79
+ outline-color: CanvasText;
80
+ }
81
+ }
82
+
83
+ & a[data-variant="secondary"] {
84
+ border-color: LinkText;
85
+ color: LinkText;
86
+ }
87
+
88
+ /* A clear border would be drawn in the text color, so the quiet button's takes the page's. */
89
+ & a[data-variant="quiet"] {
90
+ border-color: Canvas;
91
+ color: LinkText;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ </style>