@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
@@ -0,0 +1,143 @@
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 options, after checking they come with the name that makes their radios one group. */
11
+ const grouped = (options, name) => {
12
+ if (options && (typeof name !== "string" || !name)) throw new Error("norn-segmented-control: `:@options` needs `@name`, or each radio is its own group and several can be checked at once.");
13
+ return options;
14
+ };
15
+
16
+ /** Reached only from the `legend` slot's fallback: a group without a name fails the build. */
17
+ const unnamed = () => {
18
+ throw new Error('norn-segmented-control: the group needs a name. Put one element in the `legend` slot, such as `<span slot="legend">Size</span>`.');
19
+ };
20
+ </script>
21
+ <fieldset :data-icon-only="iconOnly === true || iconOnly === '' || iconOnly === 'true' ? '' : undefined">
22
+ <legend><slot name="legend"><span webc:nokeep @text="unnamed()"></span></slot></legend>
23
+ <template webc:nokeep webc:if="grouped(options, name)">
24
+ <label webc:for="option of options" :title="option.label">
25
+ <input type="radio" :name="name" :value="option.value" :checked="option.checked ? '' : undefined" />
26
+ <norn-icon webc:if="option.icon" :@name="option.icon" @size="m"></norn-icon>
27
+ <span @text="option.label"></span>
28
+ </label>
29
+ </template>
30
+ <slot webc:else></slot>
31
+ </fieldset>
32
+ <style>
33
+ @layer norn.components {
34
+ norn-segmented-control {
35
+ --_gap: 2px;
36
+ --_current: 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)));
37
+ display: inline-flex;
38
+ max-inline-size: 100%;
39
+
40
+ & fieldset {
41
+ box-sizing: border-box;
42
+ display: grid;
43
+ grid-auto-columns: minmax(0, 1fr);
44
+ grid-auto-flow: column;
45
+ gap: var(--_gap);
46
+ min-inline-size: 0;
47
+ margin: 0;
48
+ padding: var(--_gap);
49
+ border: 0;
50
+ border-radius: var(--norn-ui-border-radius, 6px);
51
+ background: var(--norn-surface-raised, light-dark(hsl(40, 14%, 96%), #242932));
52
+ }
53
+
54
+ & legend,
55
+ & [data-icon-only] label > span {
56
+ box-sizing: border-box;
57
+ position: absolute;
58
+ inline-size: 1px;
59
+ block-size: 1px;
60
+ overflow: hidden;
61
+ clip-path: inset(50%);
62
+ white-space: nowrap;
63
+ }
64
+
65
+ /* The radio covers its label, so a click anywhere on the option selects it, and the native control keeps the focus. */
66
+ & label {
67
+ --_state: 0%;
68
+ --_tint: transparent;
69
+ box-sizing: border-box;
70
+ position: relative;
71
+ z-index: 0;
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ gap: var(--norn-space-2xs, 0.25rem);
76
+ min-inline-size: 2.5rem;
77
+ min-block-size: calc(var(--norn-ui-control-height, 2.25rem) - 2 * var(--_gap));
78
+ padding-inline: var(--norn-space-s, 0.75rem);
79
+ border-radius: calc(var(--norn-ui-border-radius, 6px) - var(--_gap));
80
+ background-color: color-mix(in oklab, currentcolor var(--_state), var(--_tint));
81
+ color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
82
+ cursor: pointer;
83
+ overflow-wrap: anywhere;
84
+ text-align: center;
85
+
86
+ &:hover {
87
+ --_state: var(--norn-ui-state-hover, 8%);
88
+ }
89
+
90
+ &:active {
91
+ --_state: var(--norn-ui-state-press, 12%);
92
+ }
93
+
94
+ &:has(input:checked) {
95
+ --_tint: color-mix(in oklab, var(--_current) 14%, transparent);
96
+ color: var(--_current);
97
+ }
98
+
99
+ /* The ring sits inside the option, clear of the track's edge. */
100
+ &:has(input:focus-visible) {
101
+ 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)));
102
+ outline-offset: calc(-1 * var(--norn-ui-focus-offset, 2px));
103
+ }
104
+ }
105
+
106
+ & input {
107
+ box-sizing: border-box;
108
+ position: absolute;
109
+ inset: 0;
110
+ z-index: 1;
111
+ margin: 0;
112
+ opacity: 0;
113
+ cursor: pointer;
114
+ }
115
+
116
+ @media (pointer: coarse) {
117
+ & label {
118
+ min-block-size: 2.75rem;
119
+ }
120
+ }
121
+
122
+ @media (prefers-reduced-motion: no-preference) {
123
+ & label {
124
+ transition: background-color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1)),
125
+ color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
126
+ }
127
+ }
128
+
129
+ /* The checked option fills itself, since the browser replaces the tint. */
130
+ @media (forced-colors: active) {
131
+ & label:has(input:checked) {
132
+ background-color: Highlight;
133
+ color: HighlightText;
134
+ forced-color-adjust: none;
135
+ }
136
+
137
+ & label:has(input:focus-visible) {
138
+ outline-color: CanvasText;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ </style>
@@ -0,0 +1,149 @@
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
+ <nav :aria-labelledby="'norn-side-nav-' + uid">
10
+ <span data-label :id="'norn-side-nav-' + uid"><slot name="label"><span webc:nokeep @text="nornComponentStrings?.('norn-side-nav', 'label', $data.page) || 'Pages'"></span></slot></span>
11
+ <slot name="search"></slot>
12
+ <ul>
13
+ <li webc:for="item of items">
14
+ <a
15
+ :href="item.href"
16
+ :aria-current="item.href === $data.page.url ? 'page' : (item.children || []).some((child) => child.href === $data.page.url) ? 'true' : undefined"
17
+ ><norn-icon webc:if="item.icon" :@name="item.icon"></norn-icon><span @text="item.title"></span></a>
18
+ <ul webc:if="item.children && item.children.length">
19
+ <li webc:for="child of item.children">
20
+ <a :href="child.href" :aria-current="child.href === $data.page.url ? 'page' : undefined"><span @text="child.title"></span></a>
21
+ </li>
22
+ </ul>
23
+ </li>
24
+ </ul>
25
+ </nav>
26
+ <style>
27
+ @layer norn.components {
28
+ norn-side-nav {
29
+ display: flex;
30
+ flex-direction: column;
31
+ min-block-size: 0;
32
+ font-size: var(--norn-font-size-s, 0.875rem);
33
+
34
+ & nav {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--norn-space-s, 0.75rem);
38
+ min-block-size: 0;
39
+ }
40
+
41
+ & nav > [data-label] {
42
+ position: absolute;
43
+ inline-size: 1px;
44
+ block-size: 1px;
45
+ overflow: hidden;
46
+ clip-path: inset(50%);
47
+ white-space: nowrap;
48
+ }
49
+
50
+ & nav > ul,
51
+ & nav > ul ul {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--norn-space-2xs, 0.25rem);
55
+ margin: 0;
56
+ padding: 0;
57
+ list-style: none;
58
+ }
59
+
60
+ /* Padding with an equal negative margin keeps focus rings inside the scroll box, which would clip them. */
61
+ & nav > ul {
62
+ box-sizing: border-box;
63
+ min-block-size: 0;
64
+ margin: -6px;
65
+ padding: 6px;
66
+ overflow-y: auto;
67
+ overscroll-behavior: contain;
68
+ }
69
+
70
+ /* An item with a nested list stacks them with the list's gap, and keeps a little room below. */
71
+ & nav > ul li:has(> ul) {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: var(--norn-space-2xs, 0.25rem);
75
+ padding-block-end: var(--norn-space-xs, 0.5rem);
76
+ }
77
+
78
+ & nav > ul ul {
79
+ margin-inline: var(--norn-space-s, 0.75rem) 0;
80
+ }
81
+
82
+ & nav > ul a {
83
+ --_state: 0%;
84
+ --_tint: transparent;
85
+ box-sizing: border-box;
86
+ display: flex;
87
+ align-items: center;
88
+ gap: var(--norn-space-xs, 0.5rem);
89
+ padding: var(--norn-space-2xs, 0.25rem) var(--norn-space-xs, 0.5rem);
90
+ border-radius: var(--norn-ui-radius-s, 4px);
91
+ background-color: color-mix(in oklab, currentcolor var(--_state), var(--_tint));
92
+ color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
93
+ text-decoration: none;
94
+ overflow-wrap: anywhere;
95
+
96
+ &:hover {
97
+ --_state: var(--norn-ui-state-hover, 8%);
98
+ }
99
+
100
+ &:active {
101
+ --_state: var(--norn-ui-state-press, 12%);
102
+ }
103
+
104
+ &[aria-current="page"] {
105
+ --_tint: color-mix(in oklab, 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))) var(--norn-ui-state-press, 12%), transparent);
106
+ 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)));
107
+ }
108
+ }
109
+
110
+ & nav > ul norn-icon {
111
+ flex: none;
112
+ 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)));
113
+ }
114
+
115
+ @media (pointer: coarse) {
116
+ & nav > ul a {
117
+ min-block-size: 2.75rem;
118
+ }
119
+ }
120
+
121
+ @media (prefers-reduced-motion: no-preference) {
122
+ & nav > ul a {
123
+ transition: background-color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
124
+ }
125
+ }
126
+
127
+ /* The current page fills itself, since the browser replaces the tint. */
128
+ @media (forced-colors: active) {
129
+ & nav > ul a[aria-current="page"] {
130
+ background-color: Highlight;
131
+ color: HighlightText;
132
+ forced-color-adjust: none;
133
+
134
+ & norn-icon {
135
+ color: HighlightText;
136
+ }
137
+ }
138
+
139
+ & nav > ul a:focus-visible {
140
+ outline-color: CanvasText;
141
+ }
142
+ }
143
+
144
+ @media print {
145
+ display: none;
146
+ }
147
+ }
148
+ }
149
+ </style>
@@ -0,0 +1,185 @@
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
+ <div
10
+ :style="[typeof navWidth === 'string' && navWidth ? '--_nav: ' + navWidth : '', typeof columns === 'string' && columns ? '--_columns: ' + columns : ''].filter(Boolean).join('; ')"
11
+ :data-gap="['s', 'm', 'l', 'xl'].includes(gap) ? gap : undefined"
12
+ >
13
+ <details>
14
+ <summary><slot name="summary"><span webc:nokeep @text="nornComponentStrings?.('norn-sidebar-layout', 'summary', $data.page) || 'Menu'"></span></slot></summary>
15
+ <slot name="nav"></slot>
16
+ </details>
17
+ <article><slot></slot></article>
18
+ </div>
19
+ <style>
20
+ @layer norn.components {
21
+ norn-sidebar-layout {
22
+ display: block;
23
+ container-type: inline-size;
24
+ inline-size: min(100% - 2 * var(--norn-space-m, 1.25rem), var(--norn-layout-width, 64rem));
25
+ margin-inline: auto;
26
+ padding-block: var(--norn-space-l, 2rem);
27
+
28
+ & > div {
29
+ display: grid;
30
+ grid-template-columns: var(--_nav, 14rem) minmax(0, 1fr);
31
+ align-items: start;
32
+ gap: var(--norn-space-l, 2rem);
33
+
34
+ &[data-gap="s"] {
35
+ gap: var(--norn-space-s, 0.75rem);
36
+ }
37
+
38
+ &[data-gap="m"] {
39
+ gap: var(--norn-space-m, 1.25rem);
40
+ }
41
+
42
+ &[data-gap="xl"] {
43
+ gap: var(--norn-space-xl, 4rem);
44
+ }
45
+ }
46
+
47
+ /* The navigation stays in view; the norn-side-nav inside lets only its list scroll. The details is only a column here: its summary is hidden, and its content always shows. */
48
+ & > div > details {
49
+ position: sticky;
50
+ inset-block-start: var(--norn-space-m, 1.25rem);
51
+ display: flex;
52
+ flex-direction: column;
53
+ max-block-size: calc(100dvh - 2 * var(--norn-space-m, 1.25rem));
54
+
55
+ &::details-content {
56
+ content-visibility: visible;
57
+ display: contents;
58
+ }
59
+ }
60
+
61
+ & > div > details > summary {
62
+ display: none;
63
+ }
64
+
65
+ & > div > article {
66
+ display: grid;
67
+ grid-template-columns: var(--_columns, minmax(0, 1fr));
68
+ align-content: start;
69
+ gap: var(--norn-space-m, 1.25rem);
70
+ min-inline-size: 0;
71
+ }
72
+
73
+ & > div > article > :is(article, section) {
74
+ display: grid;
75
+ grid-template-columns: minmax(0, 1fr);
76
+ align-content: start;
77
+ gap: var(--norn-space-s, 0.75rem);
78
+ min-inline-size: 0;
79
+ }
80
+
81
+ /* Narrow, the columns stack and the navigation is a control that opens it. */
82
+ @container (max-width: 37.5rem) {
83
+ & > div {
84
+ grid-template-columns: minmax(0, 1fr);
85
+ }
86
+
87
+ & > div > details {
88
+ position: static;
89
+ display: block;
90
+ max-block-size: none;
91
+
92
+ &:not([open])::details-content {
93
+ content-visibility: hidden;
94
+ }
95
+
96
+ &::details-content {
97
+ display: block;
98
+ }
99
+ }
100
+
101
+ & > div > details > summary {
102
+ --_state: 0%;
103
+ box-sizing: border-box;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: space-between;
107
+ gap: var(--norn-space-s, 0.75rem);
108
+ min-block-size: var(--norn-ui-control-height, 2.25rem);
109
+ margin-block-end: var(--norn-space-xs, 0.5rem);
110
+ padding: var(--norn-space-xs, 0.5rem) var(--norn-space-s, 0.75rem);
111
+ border-radius: var(--norn-ui-radius-s, 4px);
112
+ background-color: color-mix(in oklab, currentcolor var(--_state), var(--norn-surface-raised, light-dark(hsl(40, 14%, 96%), #242932)));
113
+ font-weight: 600;
114
+ list-style: none;
115
+ cursor: pointer;
116
+
117
+ &::-webkit-details-marker {
118
+ display: none;
119
+ }
120
+
121
+ &:hover {
122
+ --_state: var(--norn-ui-state-hover, 8%);
123
+ }
124
+
125
+ &:active {
126
+ --_state: var(--norn-ui-state-press, 12%);
127
+ }
128
+
129
+ &:focus-visible {
130
+ outline-offset: calc(-1 * var(--norn-ui-focus-offset, 2px));
131
+ }
132
+
133
+ /* A chevron drawn with two borders, pointing down when closed and up when open. */
134
+ &::after {
135
+ box-sizing: border-box;
136
+ content: "";
137
+ flex: none;
138
+ inline-size: 0.5em;
139
+ block-size: 0.5em;
140
+ 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)));
141
+ 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)));
142
+ rotate: 45deg;
143
+ }
144
+ }
145
+
146
+ & > div > details[open] > summary::after {
147
+ rotate: 225deg;
148
+ }
149
+ }
150
+
151
+ @media (prefers-reduced-motion: no-preference) {
152
+ & > div > details > summary {
153
+ transition: background-color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
154
+ }
155
+ }
156
+
157
+ @media (forced-colors: active) {
158
+ & > div > details > summary::after {
159
+ border-color: CanvasText;
160
+ }
161
+ }
162
+
163
+ @media print {
164
+ & > div {
165
+ grid-template-columns: minmax(0, 1fr);
166
+ }
167
+
168
+ & > div > details > summary {
169
+ display: none;
170
+ }
171
+ }
172
+ }
173
+
174
+ /* Without ::details-content a closed details can't show its content, so the control is always there. */
175
+ @supports not selector(::details-content) {
176
+ norn-sidebar-layout > div > details > summary {
177
+ display: flex;
178
+ min-block-size: var(--norn-ui-control-height, 2.25rem);
179
+ align-items: center;
180
+ font-weight: 600;
181
+ cursor: pointer;
182
+ }
183
+ }
184
+ }
185
+ </style>
@@ -0,0 +1,120 @@
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
+ <span hidden :id="'norn-simple-table-' + uid"><slot name="label"></slot></span>
10
+ <div><slot></slot></div>
11
+ <style>
12
+ @layer norn.components {
13
+ norn-simple-table {
14
+ display: block;
15
+ min-inline-size: 0;
16
+
17
+ /* Shadows at the edges that have more to scroll to: the first two are the page's color and scroll with the content, so they cover the shadows underneath until the content has moved. */
18
+ & > div {
19
+ --_bg: var(--norn-surface-base, light-dark(#ffffff, #101216));
20
+ --_shadow: color-mix(in oklab, var(--norn-text-base, light-dark(#16181d, #eceef3)) 18%, transparent);
21
+ overflow-x: auto;
22
+ background:
23
+ linear-gradient(to right, var(--_bg) 30%, transparent) local 0 0 / 2rem 100% no-repeat,
24
+ linear-gradient(to left, var(--_bg) 30%, transparent) local 100% 0 / 2rem 100% no-repeat,
25
+ radial-gradient(farthest-side at 0 50%, var(--_shadow), transparent) scroll 0 0 / 0.75rem 100% no-repeat,
26
+ radial-gradient(farthest-side at 100% 50%, var(--_shadow), transparent) scroll 100% 0 / 0.75rem 100% no-repeat;
27
+ }
28
+
29
+ & > div > table {
30
+ min-inline-size: 100%;
31
+ border-collapse: collapse;
32
+ border-block-end: 1px solid var(--norn-ui-border-color, light-dark(#d3d8e2, #2d323e));
33
+ }
34
+
35
+ & > div > table > caption {
36
+ padding-block-end: var(--norn-space-xs, 0.5rem);
37
+ color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
38
+ text-align: start;
39
+ }
40
+
41
+ & > div > table :is(th, td) {
42
+ min-inline-size: 7rem;
43
+ padding: var(--norn-space-xs, 0.5rem) var(--norn-space-s, 0.75rem);
44
+ border-block-end: 1px solid var(--norn-ui-border-color, light-dark(#d3d8e2, #2d323e));
45
+ text-align: start;
46
+ vertical-align: top;
47
+ }
48
+
49
+ /* The last column is usually the one with sentences, so it keeps room to wrap into rather than a column of words. */
50
+ & > div > table :is(th, td):last-child {
51
+ min-inline-size: 10rem;
52
+ }
53
+
54
+ /* Wrap between words only: a token split across lines reads as two. */
55
+ & > div > table :is(th, td, code) {
56
+ overflow-wrap: normal;
57
+ word-break: normal;
58
+ }
59
+
60
+ & > div > table code {
61
+ white-space: nowrap;
62
+ }
63
+
64
+ @media print {
65
+ & > div {
66
+ overflow: visible;
67
+ background: none;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ </style>
73
+ <script>
74
+ {
75
+ /** A table box that is a named tab stop only while it scrolls. */
76
+ class SimpleTable extends HTMLElement {
77
+ #observer;
78
+
79
+ connectedCallback() {
80
+ const box = this.querySelector(":scope > div");
81
+ if (!box || typeof ResizeObserver === "undefined") return;
82
+ const label = this.querySelector(":scope > span[hidden][id]");
83
+ const caption = box.querySelector("caption");
84
+ if (caption && label) caption.id ||= `${label.id}-caption`;
85
+ const named = label?.textContent.trim() ? label : caption?.textContent.trim() ? caption : undefined;
86
+ const headings = [...this.querySelectorAll("thead th")].map((cell) => cell.textContent.trim()).filter(Boolean);
87
+ const fit = () => {
88
+ if (box.scrollWidth > box.clientWidth) {
89
+ box.setAttribute("tabindex", "0");
90
+ if (named) {
91
+ box.setAttribute("role", "region");
92
+ box.setAttribute("aria-labelledby", named.id);
93
+ } else if (headings.length) {
94
+ box.setAttribute("role", "region");
95
+ box.setAttribute("aria-label", headings.join(", "));
96
+ }
97
+ } else {
98
+ box.removeAttribute("tabindex");
99
+ box.removeAttribute("role");
100
+ box.removeAttribute("aria-labelledby");
101
+ box.removeAttribute("aria-label");
102
+ }
103
+ };
104
+ this.#observer?.disconnect();
105
+ // The box keeps its width when the table grows, so watch both.
106
+ this.#observer = new ResizeObserver(fit);
107
+ this.#observer.observe(box);
108
+ const table = box.querySelector("table");
109
+ if (table) this.#observer.observe(table);
110
+ }
111
+
112
+ disconnectedCallback() {
113
+ this.#observer?.disconnect();
114
+ this.#observer = undefined;
115
+ }
116
+ }
117
+
118
+ if (!customElements.get("norn-simple-table")) customElements.define("norn-simple-table", SimpleTable);
119
+ }
120
+ </script>
@@ -0,0 +1,41 @@
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 || '#main'"><slot name="label"><span webc:nokeep @text="nornComponentStrings?.('norn-skip-link', 'label', $data.page) || 'Skip to content'"></span></slot></a>
10
+ <style>
11
+ @layer norn.components {
12
+ norn-skip-link {
13
+ display: block;
14
+
15
+ & a {
16
+ position: absolute;
17
+ inset-block-start: -5rem;
18
+ inset-inline-start: var(--norn-space-m, 1.25rem);
19
+ z-index: 10;
20
+ padding: var(--norn-space-xs, 0.5rem) var(--norn-space-s, 0.75rem);
21
+ border-radius: var(--norn-ui-border-radius, 6px);
22
+ background: var(--norn-surface-raised, light-dark(hsl(40, 14%, 96%), #242932));
23
+ color: var(--norn-text-base, light-dark(#16181d, #eceef3));
24
+
25
+ &:focus {
26
+ inset-block-start: var(--norn-space-xs, 0.5rem);
27
+ }
28
+ }
29
+
30
+ @media (forced-colors: active) {
31
+ & a {
32
+ border: 1px solid CanvasText;
33
+ }
34
+ }
35
+
36
+ @media print {
37
+ display: none;
38
+ }
39
+ }
40
+ }
41
+ </style>