@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.
- package/LICENSE +385 -0
- package/README.md +536 -2
- package/components/norn-accordion.webc +124 -0
- package/components/norn-breadcrumbs.webc +71 -0
- package/components/norn-callout.webc +97 -0
- package/components/norn-card-grid.webc +60 -0
- package/components/norn-card.webc +143 -0
- package/components/norn-code-tabs.webc +270 -0
- package/components/norn-copy-button.webc +220 -0
- package/components/norn-fact-list.webc +52 -0
- package/components/norn-footer.webc +122 -0
- package/components/norn-head.webc +59 -0
- package/components/norn-header.webc +238 -0
- package/components/norn-heading-anchor.webc +71 -0
- package/components/norn-hero.webc +81 -0
- package/components/norn-icon.webc +140 -0
- package/components/norn-link-button.webc +96 -0
- package/components/norn-notice.webc +94 -0
- package/components/norn-pager.webc +67 -0
- package/components/norn-rich-text.webc +105 -0
- package/components/norn-search.webc +303 -0
- package/components/norn-segmented-control.webc +143 -0
- package/components/norn-side-nav.webc +149 -0
- package/components/norn-sidebar-layout.webc +185 -0
- package/components/norn-simple-table.webc +120 -0
- package/components/norn-skip-link.webc +41 -0
- package/components/norn-theme-toggle.webc +127 -0
- package/components/norn-toc.webc +260 -0
- package/css/base.css +75 -0
- package/css/print.css +8 -0
- package/css/reset.css +45 -0
- package/fallback/norn-code-block.webc +13 -0
- package/index.js +177 -0
- package/layouts/norn-base.webc +137 -0
- package/layouts/norn-docs-home.webc +109 -0
- package/layouts/norn-docs.webc +77 -0
- package/layouts/norn-landing.webc +73 -0
- package/layouts/norn-workspace.webc +25 -0
- package/layouts/norn-workspaces-home.webc +44 -0
- package/lib/icons.js +166 -0
- package/lib/npm.js +49 -0
- package/lib/optional.js +25 -0
- package/lib/site.js +87 -0
- package/lib/workspaces.js +71 -0
- package/markdown.js +278 -0
- package/package.json +71 -5
- package/plugin.js +279 -0
- package/project.js +15 -0
- package/schema/project.schema.json +112 -0
|
@@ -0,0 +1,94 @@
|
|
|
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 SEVERITIES = { info: ["Note", "info"], success: ["Success", "success"], warning: ["Warning", "warning"], danger: ["Error", "error"] };
|
|
11
|
+
|
|
12
|
+
/** A known severity, else `info`. */
|
|
13
|
+
const level = (severity) => (Object.hasOwn(SEVERITIES, severity) ? severity : "info");
|
|
14
|
+
|
|
15
|
+
/** The live role: `alert` interrupts, for a warning or a danger; `status` waits. */
|
|
16
|
+
const liveRole = (severity) => (["warning", "danger"].includes(level(severity)) ? "alert" : "status");
|
|
17
|
+
</script>
|
|
18
|
+
<div :data-severity="level(severity)" :role="live === true || live === '' || live === 'true' ? liveRole(severity) : undefined">
|
|
19
|
+
<norn-icon :@name="SEVERITIES[level(severity)][1]" @size="m"></norn-icon>
|
|
20
|
+
<div>
|
|
21
|
+
<span data-prefix><slot name="prefix"><span webc:nokeep @text="nornComponentStrings?.('norn-notice', level(severity), $data.page) || SEVERITIES[level(severity)][0] + ': '"></span></slot></span>
|
|
22
|
+
<slot></slot>
|
|
23
|
+
</div>
|
|
24
|
+
<div><slot name="action"></slot></div>
|
|
25
|
+
</div>
|
|
26
|
+
<style>
|
|
27
|
+
@layer norn.components {
|
|
28
|
+
norn-notice {
|
|
29
|
+
--_color: var(--norn-status-info, light-dark(#1b4fc4, #8fb0ff));
|
|
30
|
+
display: block;
|
|
31
|
+
|
|
32
|
+
&:has(> [data-severity="success"]) {
|
|
33
|
+
--_color: var(--norn-status-success, light-dark(#0b6b3a, #7ed6a1));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&:has(> [data-severity="warning"]) {
|
|
37
|
+
--_color: var(--norn-status-warning, light-dark(#9a4a00, #ffb36b));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&:has(> [data-severity="danger"]) {
|
|
41
|
+
--_color: var(--norn-status-danger, light-dark(#b42318, #ff8a80));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* A tint and a bar, as in norn-callout. */
|
|
45
|
+
& > div {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: var(--norn-space-s, 0.75rem) var(--norn-space-m, 1.25rem);
|
|
50
|
+
overflow-wrap: anywhere;
|
|
51
|
+
padding: var(--norn-space-s, 0.75rem) var(--norn-space-m, 1.25rem);
|
|
52
|
+
border-inline-start: 3px solid var(--_color);
|
|
53
|
+
border-start-end-radius: var(--norn-ui-border-radius, 6px);
|
|
54
|
+
border-end-end-radius: var(--norn-ui-border-radius, 6px);
|
|
55
|
+
background: color-mix(in oklab, var(--_color) 8%, var(--norn-surface-base, light-dark(#ffffff, #101216)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
& > div > norn-icon {
|
|
59
|
+
flex: none;
|
|
60
|
+
align-self: start;
|
|
61
|
+
margin-block-start: calc((1lh - var(--norn-ui-icon-m, 1.25rem)) / 2);
|
|
62
|
+
color: var(--_color);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
& > div > div:first-of-type {
|
|
66
|
+
flex: 1 1 16rem;
|
|
67
|
+
min-inline-size: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
& > div > div:first-of-type > :is(p, ul, ol) {
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
& > div > div:last-child:not(:has(*)) {
|
|
75
|
+
display: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
& > div > div > [data-prefix] {
|
|
79
|
+
position: absolute;
|
|
80
|
+
inline-size: 1px;
|
|
81
|
+
block-size: 1px;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
clip-path: inset(50%);
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@media (forced-colors: active) {
|
|
88
|
+
& > div {
|
|
89
|
+
border-inline-start-color: CanvasText;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
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-pager-' + uid" webc:if="prev || next">
|
|
10
|
+
<span data-label :id="'norn-pager-' + uid"><slot name="label"><span webc:nokeep @text="nornComponentStrings?.('norn-pager', 'label', $data.page) || 'Pager'"></span></slot></span>
|
|
11
|
+
<a webc:if="prev" rel="prev" :href="prev.href"><norn-icon @name="arrow-start"></norn-icon><span @text="prev.title"></span></a>
|
|
12
|
+
<a webc:if="next" rel="next" :href="next.href"><span @text="next.title"></span><norn-icon @name="arrow-end"></norn-icon></a>
|
|
13
|
+
</nav>
|
|
14
|
+
<style>
|
|
15
|
+
@layer norn.components {
|
|
16
|
+
norn-pager {
|
|
17
|
+
display: block;
|
|
18
|
+
|
|
19
|
+
& nav {
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
gap: var(--norn-space-m, 1.25rem);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
& nav > [data-label] {
|
|
26
|
+
position: absolute;
|
|
27
|
+
inline-size: 1px;
|
|
28
|
+
block-size: 1px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
clip-path: inset(50%);
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
& nav > a {
|
|
35
|
+
--_state: 0%;
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--norn-space-xs, 0.5rem);
|
|
39
|
+
padding: var(--norn-space-xs, 0.5rem) var(--norn-space-s, 0.75rem);
|
|
40
|
+
border-radius: var(--norn-ui-radius-s, 4px);
|
|
41
|
+
background-color: color-mix(in oklab, currentcolor var(--_state), transparent);
|
|
42
|
+
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)));
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
overflow-wrap: break-word;
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
--_state: var(--norn-ui-state-hover, 8%);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&:active {
|
|
51
|
+
--_state: var(--norn-ui-state-press, 12%);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&[rel="next"] {
|
|
55
|
+
margin-inline-start: auto;
|
|
56
|
+
text-align: end;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
61
|
+
& nav > a {
|
|
62
|
+
transition: background-color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
<norn-rich-text webc:root="override" :data-measure="measure === 'none' ? 'none' : undefined"><slot></slot></norn-rich-text>
|
|
10
|
+
<style>
|
|
11
|
+
@layer norn.components {
|
|
12
|
+
norn-rich-text {
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: minmax(0, 1fr);
|
|
15
|
+
align-content: start;
|
|
16
|
+
gap: var(--norn-space-m, 1.25rem);
|
|
17
|
+
max-inline-size: var(--norn-text-measure, 40em);
|
|
18
|
+
|
|
19
|
+
&[data-measure="none"] {
|
|
20
|
+
max-inline-size: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Prose styles stop at the components that style all their own markup. A callout, a notice, a card, or a table holds prose of its own (a list, a code span), so it stays in scope. */
|
|
25
|
+
@scope (norn-rich-text) to (norn-code-block, norn-code-tabs, norn-fact-list) {
|
|
26
|
+
h2,
|
|
27
|
+
h3,
|
|
28
|
+
h4,
|
|
29
|
+
h5,
|
|
30
|
+
h6 {
|
|
31
|
+
margin: var(--norn-space-s, 0.75rem) 0 0;
|
|
32
|
+
font-family: var(--norn-font-heading, var(--norn-font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));
|
|
33
|
+
line-height: 1.15;
|
|
34
|
+
text-wrap: balance;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* norn-heading-anchor zeroes its heading's margin, so the space goes on its tag. */
|
|
38
|
+
norn-heading-anchor:has(> :is(h2, h3, h4, h5, h6)) {
|
|
39
|
+
margin-block-start: var(--norn-space-s, 0.75rem);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h2 {
|
|
43
|
+
font-size: var(--norn-font-size-xl, 1.5rem);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
h3 {
|
|
47
|
+
font-size: var(--norn-font-size-l, 1.25rem);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
h4,
|
|
51
|
+
h5,
|
|
52
|
+
h6 {
|
|
53
|
+
font-size: var(--norn-font-size-m, 1rem);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p {
|
|
57
|
+
margin: 0;
|
|
58
|
+
text-wrap: pretty;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ul,
|
|
62
|
+
ol {
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: var(--norn-space-xs, 0.5rem);
|
|
65
|
+
margin: 0;
|
|
66
|
+
padding-inline-start: var(--norn-space-m, 1.25rem);
|
|
67
|
+
list-style: revert;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
blockquote {
|
|
71
|
+
margin: 0;
|
|
72
|
+
padding-inline-start: var(--norn-space-m, 1.25rem);
|
|
73
|
+
border-inline-start: 3px solid var(--norn-ui-border-color, light-dark(#d3d8e2, #2d323e));
|
|
74
|
+
color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
table {
|
|
78
|
+
border-collapse: collapse;
|
|
79
|
+
border-block-end: 1px solid var(--norn-ui-border-color, light-dark(#d3d8e2, #2d323e));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
th,
|
|
83
|
+
td {
|
|
84
|
+
padding: var(--norn-space-xs, 0.5rem) var(--norn-space-s, 0.75rem);
|
|
85
|
+
border-block-end: 1px solid var(--norn-ui-border-color, light-dark(#d3d8e2, #2d323e));
|
|
86
|
+
text-align: start;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
hr {
|
|
90
|
+
box-sizing: border-box;
|
|
91
|
+
width: 100%;
|
|
92
|
+
border: 0;
|
|
93
|
+
border-block-start: 1px solid var(--norn-ui-border-color, light-dark(#d3d8e2, #2d323e));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
:not(pre) > code {
|
|
97
|
+
padding: 0.1em 0.35em;
|
|
98
|
+
border-radius: var(--norn-ui-radius-s, 4px);
|
|
99
|
+
background: var(--norn-surface-code, light-dark(hsl(40, 14%, 94%), #2c313b));
|
|
100
|
+
font-family: var(--norn-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
101
|
+
font-size: 0.875em;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,303 @@
|
|
|
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
|
+
<search :aria-labelledby="'norn-search-' + uid" :data-bundle="bundle || '/pagefind/pagefind.js'">
|
|
10
|
+
<form :action="action || 'https://duckduckgo.com/'" method="get">
|
|
11
|
+
<label>
|
|
12
|
+
<span :id="'norn-search-' + uid"><slot name="label"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'label', $data.page) || 'Search'"></span></slot></span>
|
|
13
|
+
<norn-icon @name="search"></norn-icon>
|
|
14
|
+
<input type="search" name="q" autocomplete="off" spellcheck="false" />
|
|
15
|
+
</label>
|
|
16
|
+
<input webc:if="site" type="hidden" name="sites" :value="site" />
|
|
17
|
+
<button type="submit"><norn-icon @name="search"></norn-icon><span><slot name="label"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'label', $data.page) || 'Search'"></span></slot></span></button>
|
|
18
|
+
</form>
|
|
19
|
+
<p role="status"></p>
|
|
20
|
+
<ol role="list" hidden></ol>
|
|
21
|
+
<span hidden>
|
|
22
|
+
<span data-string="none"><slot name="none"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'none', $data.page) || 'No results'"></span></slot></span>
|
|
23
|
+
<span data-string="results-zero"><slot name="results-zero"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'results-zero', $data.page) || ''"></span></slot></span>
|
|
24
|
+
<span data-string="results-one"><slot name="results-one"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'results-one', $data.page) || (nornComponentStrings?.('norn-search', 'results-other', $data.page) ? '' : '{count} result')"></span></slot></span>
|
|
25
|
+
<span data-string="results-two"><slot name="results-two"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'results-two', $data.page) || ''"></span></slot></span>
|
|
26
|
+
<span data-string="results-few"><slot name="results-few"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'results-few', $data.page) || ''"></span></slot></span>
|
|
27
|
+
<span data-string="results-many"><slot name="results-many"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'results-many', $data.page) || ''"></span></slot></span>
|
|
28
|
+
<span data-string="results-other"><slot name="results-other"><span webc:nokeep @text="nornComponentStrings?.('norn-search', 'results-other', $data.page) || '{count} results'"></span></slot></span>
|
|
29
|
+
</span>
|
|
30
|
+
</search>
|
|
31
|
+
<style>
|
|
32
|
+
@layer norn.components {
|
|
33
|
+
norn-search {
|
|
34
|
+
display: block;
|
|
35
|
+
container-type: inline-size;
|
|
36
|
+
|
|
37
|
+
& search {
|
|
38
|
+
position: relative;
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: minmax(0, 1fr);
|
|
41
|
+
gap: var(--norn-space-xs, 0.5rem);
|
|
42
|
+
overflow-wrap: anywhere;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
& form {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: var(--norn-space-xs, 0.5rem);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
& label {
|
|
51
|
+
position: relative;
|
|
52
|
+
display: grid;
|
|
53
|
+
flex: 1;
|
|
54
|
+
min-inline-size: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
& label > span {
|
|
58
|
+
position: absolute;
|
|
59
|
+
inline-size: 1px;
|
|
60
|
+
block-size: 1px;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
clip-path: inset(50%);
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* The magnifier sits in the field, at its start, and lets a click through to it. */
|
|
67
|
+
& label > norn-icon {
|
|
68
|
+
position: absolute;
|
|
69
|
+
inset-block-start: 50%;
|
|
70
|
+
inset-inline-start: var(--norn-space-xs, 0.5rem);
|
|
71
|
+
translate: 0 -50%;
|
|
72
|
+
color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
& :is(input, button) {
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
min-block-size: var(--norn-ui-control-height, 2.25rem);
|
|
79
|
+
border: 0;
|
|
80
|
+
border-radius: var(--norn-ui-border-radius, 6px);
|
|
81
|
+
color: var(--norn-text-base, light-dark(#16181d, #eceef3));
|
|
82
|
+
font: inherit;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
& input {
|
|
86
|
+
inline-size: 100%;
|
|
87
|
+
padding: 0 var(--norn-space-s, 0.75rem) 0 calc(var(--norn-space-xs, 0.5rem) + var(--norn-ui-icon-s, 1em) + var(--norn-space-xs, 0.5rem));
|
|
88
|
+
border: 1px solid var(--norn-ui-border-strong, light-dark(#7a8194, #6f7789));
|
|
89
|
+
background: var(--norn-surface-base, light-dark(#ffffff, #101216));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
& input:dir(rtl) {
|
|
93
|
+
padding: 0 calc(var(--norn-space-xs, 0.5rem) + var(--norn-ui-icon-s, 1em) + var(--norn-space-xs, 0.5rem)) 0 var(--norn-space-s, 0.75rem);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
& button {
|
|
97
|
+
--_state: 0%;
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: var(--norn-space-2xs, 0.25rem);
|
|
101
|
+
padding: 0 var(--norn-space-s, 0.75rem);
|
|
102
|
+
background-color: color-mix(in oklab, currentcolor var(--_state), var(--norn-surface-raised, light-dark(hsl(40, 14%, 96%), #242932)));
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
|
|
105
|
+
&:hover {
|
|
106
|
+
--_state: var(--norn-ui-state-hover, 8%);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&:active {
|
|
110
|
+
--_state: var(--norn-ui-state-press, 12%);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* The button's own icon is for a field too narrow for its text. */
|
|
115
|
+
& button > norn-icon {
|
|
116
|
+
display: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* With live results, the button is only a fallback. */
|
|
120
|
+
&[data-live] button {
|
|
121
|
+
display: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@container (max-width: 16rem) {
|
|
125
|
+
& button > norn-icon {
|
|
126
|
+
display: inline-flex;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
& button > span {
|
|
130
|
+
position: absolute;
|
|
131
|
+
inline-size: 1px;
|
|
132
|
+
block-size: 1px;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
clip-path: inset(50%);
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
& [role="status"] {
|
|
140
|
+
margin: 0;
|
|
141
|
+
color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
|
|
142
|
+
font-size: var(--norn-font-size-s, 0.875rem);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* The results float over what is below the field. */
|
|
146
|
+
& ol {
|
|
147
|
+
position: absolute;
|
|
148
|
+
inset-inline: 0;
|
|
149
|
+
inset-block-start: calc(100% + var(--norn-space-2xs, 0.25rem));
|
|
150
|
+
z-index: 10;
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: minmax(0, 1fr);
|
|
153
|
+
gap: var(--norn-space-s, 0.75rem);
|
|
154
|
+
max-block-size: min(60dvh, 24rem);
|
|
155
|
+
margin: 0;
|
|
156
|
+
padding: var(--norn-space-s, 0.75rem);
|
|
157
|
+
overflow-y: auto;
|
|
158
|
+
border-radius: var(--norn-ui-border-radius, 6px);
|
|
159
|
+
background: var(--norn-surface-raised, light-dark(hsl(40, 14%, 96%), #242932));
|
|
160
|
+
box-shadow: var(--norn-ui-shadow, 0 6px 20px light-dark(rgb(22 24 29 / 0.08), rgb(0 0 0 / 0.4)));
|
|
161
|
+
list-style: none;
|
|
162
|
+
|
|
163
|
+
&[hidden] {
|
|
164
|
+
display: none;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
& li {
|
|
169
|
+
display: grid;
|
|
170
|
+
grid-template-columns: minmax(0, 1fr);
|
|
171
|
+
gap: var(--norn-space-2xs, 0.25rem);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
& li p {
|
|
175
|
+
margin: 0;
|
|
176
|
+
color: var(--norn-text-muted, light-dark(#4b5160, #a4abba));
|
|
177
|
+
font-size: var(--norn-font-size-s, 0.875rem);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
& mark {
|
|
181
|
+
background: none;
|
|
182
|
+
color: var(--norn-text-base, light-dark(#16181d, #eceef3));
|
|
183
|
+
font-weight: 700;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (pointer: coarse) {
|
|
187
|
+
& :is(input, button) {
|
|
188
|
+
min-block-size: 2.75rem;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
193
|
+
& button {
|
|
194
|
+
transition: background-color var(--norn-ui-motion-duration, 150ms) var(--norn-ui-motion-easing, cubic-bezier(0.2, 0, 0, 1));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@media (forced-colors: active) {
|
|
199
|
+
& ol {
|
|
200
|
+
border: 1px solid CanvasText;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
206
|
+
<script>
|
|
207
|
+
{
|
|
208
|
+
const LIMIT = 8;
|
|
209
|
+
const DELAY_MS = 200;
|
|
210
|
+
|
|
211
|
+
/** Pagefind's excerpt as nodes: text, and `<mark>` for its highlights. Anything else stays text. */
|
|
212
|
+
const excerpt = (html) =>
|
|
213
|
+
html.split(/(<mark>.*?<\/mark>)/).map((part) => {
|
|
214
|
+
const holder = document.createElement("template");
|
|
215
|
+
holder.innerHTML = part.replace(/<\/?mark>/g, "");
|
|
216
|
+
const text = holder.content.textContent;
|
|
217
|
+
if (!part.startsWith("<mark>")) return document.createTextNode(text);
|
|
218
|
+
const mark = document.createElement("mark");
|
|
219
|
+
mark.textContent = text;
|
|
220
|
+
return mark;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/** Live results from a Pagefind index. Without one, the form searches the web. */
|
|
224
|
+
class SiteSearch extends HTMLElement {
|
|
225
|
+
#wired = false;
|
|
226
|
+
#pagefind;
|
|
227
|
+
#latest = 0;
|
|
228
|
+
|
|
229
|
+
async connectedCallback() {
|
|
230
|
+
if (this.#wired) return;
|
|
231
|
+
this.#wired = true;
|
|
232
|
+
// The label names the field, and the placeholder says the same in the field.
|
|
233
|
+
const input = this.querySelector('input[type="search"]');
|
|
234
|
+
input.placeholder = this.querySelector("label > span")?.textContent.trim() ?? "";
|
|
235
|
+
const search = this.querySelector("search");
|
|
236
|
+
try {
|
|
237
|
+
this.#pagefind = await import(new URL(search.dataset.bundle, document.baseURI).href);
|
|
238
|
+
await this.#pagefind.options?.({ excerptLength: 20 });
|
|
239
|
+
} catch {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.dataset.live = "";
|
|
243
|
+
input.addEventListener("input", () => this.#search(input.value));
|
|
244
|
+
this.querySelector("form").addEventListener("submit", (event) => {
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
this.querySelector("ol a")?.focus();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async #search(query) {
|
|
251
|
+
const run = ++this.#latest;
|
|
252
|
+
const list = this.querySelector("ol");
|
|
253
|
+
const status = this.querySelector('[role="status"]');
|
|
254
|
+
if (!query.trim()) {
|
|
255
|
+
list.hidden = true;
|
|
256
|
+
list.replaceChildren();
|
|
257
|
+
status.textContent = "";
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const found = await this.#pagefind.debouncedSearch(query, {}, DELAY_MS);
|
|
261
|
+
if (found === null || run !== this.#latest) return;
|
|
262
|
+
const pages = await Promise.all(found.results.slice(0, LIMIT).map((result) => result.data()));
|
|
263
|
+
if (run !== this.#latest) return;
|
|
264
|
+
list.replaceChildren(
|
|
265
|
+
...pages.map((page) => {
|
|
266
|
+
const item = document.createElement("li");
|
|
267
|
+
const link = document.createElement("a");
|
|
268
|
+
link.href = page.url;
|
|
269
|
+
link.textContent = page.meta?.title || page.url;
|
|
270
|
+
const text = document.createElement("p");
|
|
271
|
+
text.append(...excerpt(page.excerpt ?? ""));
|
|
272
|
+
item.append(link, text);
|
|
273
|
+
return item;
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
list.hidden = !pages.length;
|
|
277
|
+
const total = found.results.length;
|
|
278
|
+
status.textContent = total ? this.#count(total) : this.#string("none");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** One of the status texts the page holds, or "" when its slot and fallback are empty. */
|
|
282
|
+
#string(key) {
|
|
283
|
+
return this.querySelector(`search > [hidden] > [data-string="${key}"]`)?.textContent.trim() ?? "";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The status for a count, in the plural form and number format of the nearest `lang`. */
|
|
287
|
+
#count(total) {
|
|
288
|
+
const lang = this.closest("[lang]")?.lang || undefined;
|
|
289
|
+
let category = "other";
|
|
290
|
+
let number = String(total);
|
|
291
|
+
try {
|
|
292
|
+
category = new Intl.PluralRules(lang).select(total);
|
|
293
|
+
number = new Intl.NumberFormat(lang).format(total);
|
|
294
|
+
} catch {
|
|
295
|
+
// An invalid lang keeps the defaults.
|
|
296
|
+
}
|
|
297
|
+
return (this.#string(`results-${category}`) || this.#string("results-other")).replaceAll("{count}", number);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!customElements.get("norn-search")) customElements.define("norn-search", SiteSearch);
|
|
302
|
+
}
|
|
303
|
+
</script>
|