oat-glassed 0.1.0
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 +21 -0
- package/README.md +41 -0
- package/css/00-base.css +194 -0
- package/css/01-theme.css +148 -0
- package/css/accordion.css +67 -0
- package/css/alert.css +68 -0
- package/css/animations.css +61 -0
- package/css/avatar.css +49 -0
- package/css/badge.css +56 -0
- package/css/button.css +155 -0
- package/css/card.css +21 -0
- package/css/command.css +107 -0
- package/css/dialog.css +94 -0
- package/css/dropdown.css +59 -0
- package/css/empty-state.css +38 -0
- package/css/form.css +264 -0
- package/css/grid.css +65 -0
- package/css/nav.css +89 -0
- package/css/progress.css +75 -0
- package/css/sidebar.css +189 -0
- package/css/skeleton.css +39 -0
- package/css/spinner.css +52 -0
- package/css/table.css +54 -0
- package/css/tabs.css +48 -0
- package/css/tag.css +67 -0
- package/css/toast.css +126 -0
- package/css/tooltip.css +49 -0
- package/css/utilities.css +54 -0
- package/js/base.js +107 -0
- package/js/command.js +137 -0
- package/js/dropdown.js +74 -0
- package/js/index.js +13 -0
- package/js/sidebar.js +22 -0
- package/js/tabs.js +94 -0
- package/js/toast.js +144 -0
- package/js/tooltip.js +36 -0
- package/oat-glassed.min.css +1 -0
- package/oat-glassed.min.js +1 -0
- package/package.json +27 -0
package/css/spinner.css
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
[aria-busy="true"] {
|
|
3
|
+
&::before {
|
|
4
|
+
content: "";
|
|
5
|
+
display: inline-block;
|
|
6
|
+
inset: 0;
|
|
7
|
+
margin: auto;
|
|
8
|
+
width: 1.5rem;
|
|
9
|
+
height: 1.5rem;
|
|
10
|
+
border: 2px solid var(--muted);
|
|
11
|
+
border-top-color: var(--primary);
|
|
12
|
+
border-radius: var(--radius-full);
|
|
13
|
+
animation: spin 1s linear infinite;
|
|
14
|
+
text-align: center;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&[data-spinner~="small"]::before {
|
|
18
|
+
width: 1rem;
|
|
19
|
+
height: 1rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&[data-spinner~="large"]::before {
|
|
23
|
+
width: 2rem;
|
|
24
|
+
height: 2rem;
|
|
25
|
+
border-width: 3px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&[data-spinner~="overlay"] {
|
|
29
|
+
position: relative;
|
|
30
|
+
|
|
31
|
+
> * {
|
|
32
|
+
opacity: 0.3;
|
|
33
|
+
|
|
34
|
+
/* "disable" all elements in the container while it's busy */
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&::before {
|
|
39
|
+
position: absolute;
|
|
40
|
+
inset: 0;
|
|
41
|
+
margin: auto;
|
|
42
|
+
z-index: 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes spin {
|
|
48
|
+
to {
|
|
49
|
+
transform: rotate(360deg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/css/table.css
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
@layer base {
|
|
2
|
+
.table {
|
|
3
|
+
min-width: 320px;
|
|
4
|
+
width: 100%;
|
|
5
|
+
overflow-x: auto;
|
|
6
|
+
border-radius: var(--radius-medium);
|
|
7
|
+
border: 1px solid var(--glass-border);
|
|
8
|
+
background-color: light-dark(rgb(255 255 255 / 0.3), rgb(255 255 255 / 0.02));
|
|
9
|
+
box-shadow: var(--glass-edge);
|
|
10
|
+
contain: paint;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
table {
|
|
14
|
+
border-collapse: collapse;
|
|
15
|
+
width: 100%;
|
|
16
|
+
font-size: var(--text-7);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
thead {
|
|
20
|
+
border-bottom: 1px solid var(--border);
|
|
21
|
+
background-color: light-dark(rgb(255 255 255 / 0.25), rgb(255 255 255 / 0.03));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
th, td {
|
|
25
|
+
overflow-wrap: break-word;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
th {
|
|
29
|
+
padding: var(--space-3) var(--space-3);
|
|
30
|
+
text-align: start;
|
|
31
|
+
font-weight: var(--font-medium);
|
|
32
|
+
color: var(--muted-foreground);
|
|
33
|
+
font-size: var(--text-8);
|
|
34
|
+
text-transform: uppercase;
|
|
35
|
+
letter-spacing: 0.03em;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
td {
|
|
39
|
+
padding: var(--space-3);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
tbody tr {
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
transition: background-color var(--transition-fast);
|
|
45
|
+
|
|
46
|
+
&:last-child {
|
|
47
|
+
border-bottom: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
background-color: var(--accent);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/css/tabs.css
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
[role="tablist"] {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--space-1);
|
|
6
|
+
padding: var(--space-1);
|
|
7
|
+
background-color: var(--muted);
|
|
8
|
+
border-radius: var(--radius-medium);
|
|
9
|
+
border: 1px solid var(--glass-border);
|
|
10
|
+
box-shadow: var(--glass-edge);
|
|
11
|
+
contain: paint;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
[role="tab"] {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
padding: var(--space-2) var(--space-3);
|
|
19
|
+
font-size: var(--text-7);
|
|
20
|
+
font-weight: var(--font-medium);
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
background-color: transparent;
|
|
23
|
+
color: var(--muted-foreground);
|
|
24
|
+
border: none;
|
|
25
|
+
border-radius: calc(var(--radius-medium) - 2px);
|
|
26
|
+
box-shadow: none;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition: background-color var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);
|
|
29
|
+
|
|
30
|
+
&:hover {
|
|
31
|
+
color: var(--foreground);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&[aria-selected="true"] {
|
|
35
|
+
background-color: light-dark(rgb(255 255 255 / 0.75), rgb(255 255 255 / 0.1));
|
|
36
|
+
color: var(--foreground);
|
|
37
|
+
box-shadow: var(--shadow-small);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[role="tabpanel"] {
|
|
42
|
+
padding: var(--space-4) 0;
|
|
43
|
+
|
|
44
|
+
&:focus-visible {
|
|
45
|
+
outline: none;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/css/tag.css
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.tag {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--space-1);
|
|
6
|
+
padding: var(--space-1) var(--space-3);
|
|
7
|
+
font-size: var(--text-8);
|
|
8
|
+
font-weight: var(--font-medium);
|
|
9
|
+
line-height: var(--leading-normal);
|
|
10
|
+
background-color: var(--secondary);
|
|
11
|
+
color: var(--secondary-foreground);
|
|
12
|
+
border: 1px solid light-dark(rgb(255 255 255 / 0.08), rgb(255 255 255 / 0.06));
|
|
13
|
+
border-radius: var(--radius-full);
|
|
14
|
+
|
|
15
|
+
/* Dismiss button */
|
|
16
|
+
& > button {
|
|
17
|
+
all: unset;
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
width: 0.875rem;
|
|
22
|
+
height: 0.875rem;
|
|
23
|
+
border-radius: var(--radius-full);
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
opacity: 0.5;
|
|
26
|
+
transition: opacity var(--transition-fast), background-color var(--transition-fast);
|
|
27
|
+
|
|
28
|
+
&:hover {
|
|
29
|
+
opacity: 1;
|
|
30
|
+
background-color: rgb(from currentColor r g b / 0.15);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&.primary {
|
|
35
|
+
background-color: var(--primary);
|
|
36
|
+
color: var(--primary-foreground);
|
|
37
|
+
border-color: rgb(from #fff r g b / 0.12);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&.success {
|
|
41
|
+
color: var(--success);
|
|
42
|
+
border-color: light-dark(rgb(from var(--success) r g b / 0.15), rgb(from var(--success) r g b / 0.2));
|
|
43
|
+
background-color: light-dark(
|
|
44
|
+
color-mix(in srgb, var(--success) 8%, rgb(255 255 255 / 0.35)),
|
|
45
|
+
color-mix(in srgb, var(--success) 15%, rgb(0 0 0 / 0.25))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&.warning {
|
|
50
|
+
color: var(--warning);
|
|
51
|
+
border-color: light-dark(rgb(from var(--warning) r g b / 0.15), rgb(from var(--warning) r g b / 0.2));
|
|
52
|
+
background-color: light-dark(
|
|
53
|
+
color-mix(in srgb, var(--warning) 8%, rgb(255 255 255 / 0.35)),
|
|
54
|
+
color-mix(in srgb, var(--warning) 15%, rgb(0 0 0 / 0.25))
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&.danger {
|
|
59
|
+
color: var(--danger);
|
|
60
|
+
border-color: light-dark(rgb(from var(--danger) r g b / 0.15), rgb(from var(--danger) r g b / 0.2));
|
|
61
|
+
background-color: light-dark(
|
|
62
|
+
color-mix(in srgb, var(--danger) 8%, rgb(255 255 255 / 0.35)),
|
|
63
|
+
color-mix(in srgb, var(--danger) 15%, rgb(0 0 0 / 0.25))
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/css/toast.css
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.toast-container {
|
|
3
|
+
position: fixed;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
pointer-events: none;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
border: none;
|
|
10
|
+
background: transparent;
|
|
11
|
+
content-visibility: auto;
|
|
12
|
+
|
|
13
|
+
overflow: visible;
|
|
14
|
+
|
|
15
|
+
&::backdrop {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&[data-placement="top-left"] {
|
|
20
|
+
inset: var(--space-4) auto auto var(--space-4);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&[data-placement="top-center"] {
|
|
24
|
+
inset: var(--space-4) auto auto 50%;
|
|
25
|
+
transform: translateX(-50%);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&[data-placement="top-right"] {
|
|
29
|
+
inset: var(--space-4) var(--space-4) auto auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&[data-placement="bottom-left"] {
|
|
33
|
+
inset: auto auto var(--space-4) var(--space-4);
|
|
34
|
+
flex-direction: column-reverse;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&[data-placement="bottom-center"] {
|
|
38
|
+
inset: auto auto var(--space-4) 50%;
|
|
39
|
+
transform: translateX(-50%);
|
|
40
|
+
flex-direction: column-reverse;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&[data-placement="bottom-right"] {
|
|
44
|
+
inset: auto var(--space-4) var(--space-4) auto;
|
|
45
|
+
flex-direction: column-reverse;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.toast {
|
|
50
|
+
--transition: 300ms;
|
|
51
|
+
--transition-in: calc(var(--transition) - 50ms);
|
|
52
|
+
|
|
53
|
+
padding: var(--space-5) var(--space-4);
|
|
54
|
+
max-width: 28rem;
|
|
55
|
+
min-width: 20rem;
|
|
56
|
+
pointer-events: auto;
|
|
57
|
+
background-color: var(--card);
|
|
58
|
+
background-image: var(--glass-highlight);
|
|
59
|
+
border: 1px solid var(--glass-border);
|
|
60
|
+
border-inline-start-width: var(--space-1);
|
|
61
|
+
border-inline-start-style: solid;
|
|
62
|
+
border-radius: var(--radius-large);
|
|
63
|
+
box-shadow: var(--glass-edge), var(--shadow-large);
|
|
64
|
+
will-change: transform, opacity;
|
|
65
|
+
transition: opacity var(--transition-in), transform var(--transition-in), margin var(--transition-in);
|
|
66
|
+
line-height: 1;
|
|
67
|
+
|
|
68
|
+
.toast-title {
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
margin: 0 0 var(--space-3) 0;
|
|
71
|
+
}
|
|
72
|
+
.toast-message {
|
|
73
|
+
color: var(--muted-foreground);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&[data-variant="success"] {
|
|
77
|
+
border-inline-start-color: var(--success);
|
|
78
|
+
.toast-title {
|
|
79
|
+
color: var(--success);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&[data-variant="danger"] {
|
|
84
|
+
border-inline-start-color: var(--danger);
|
|
85
|
+
.toast-title {
|
|
86
|
+
color: var(--danger);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&[data-variant="warning"] {
|
|
91
|
+
border-inline-start-color: var(--warning);
|
|
92
|
+
.toast-title {
|
|
93
|
+
color: var(--warning);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
& > [data-close] {
|
|
98
|
+
margin-inline-start: auto;
|
|
99
|
+
background: none;
|
|
100
|
+
border: none;
|
|
101
|
+
padding: 0;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
opacity: 0.5;
|
|
104
|
+
|
|
105
|
+
&:hover {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
margin: var(--space-2) 0;
|
|
111
|
+
|
|
112
|
+
&[data-entering] {
|
|
113
|
+
opacity: 0;
|
|
114
|
+
transform: translateY(-1rem) scale(0.95);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&[data-exiting] {
|
|
118
|
+
opacity: 0;
|
|
119
|
+
margin: 0;
|
|
120
|
+
padding-block: 0;
|
|
121
|
+
max-height: 0;
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
transition: opacity var(--transition), margin var(--transition), padding var(--transition), max-height var(--transition);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
package/css/tooltip.css
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
[data-tooltip] {
|
|
3
|
+
position: relative;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
[data-tooltip]::before,
|
|
7
|
+
[data-tooltip]::after {
|
|
8
|
+
position: absolute;
|
|
9
|
+
inset-inline-start: 50%;
|
|
10
|
+
opacity: 0;
|
|
11
|
+
visibility: hidden;
|
|
12
|
+
transition: opacity var(--transition-fast), transform var(--transition-fast), visibility var(--transition-fast);
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
z-index: 1000;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Text */
|
|
18
|
+
[data-tooltip]::after {
|
|
19
|
+
content: attr(data-tooltip);
|
|
20
|
+
inset-block-end: calc(100% + 10px);
|
|
21
|
+
transform: translateX(-50%) translateY(4px);
|
|
22
|
+
padding: var(--space-2) var(--space-3);
|
|
23
|
+
font-size: var(--text-7);
|
|
24
|
+
line-height: 1;
|
|
25
|
+
white-space: nowrap;
|
|
26
|
+
background: light-dark(rgb(20 20 30 / 0.75), rgb(40 40 55 / 0.8));
|
|
27
|
+
color: #ececf1;
|
|
28
|
+
border: 1px solid light-dark(rgb(255 255 255 / 0.08), rgb(255 255 255 / 0.12));
|
|
29
|
+
border-radius: var(--radius-medium);
|
|
30
|
+
box-shadow: var(--shadow-medium);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Arrow */
|
|
34
|
+
[data-tooltip]::before {
|
|
35
|
+
content: '';
|
|
36
|
+
inset-block-end: calc(100% - 5px);
|
|
37
|
+
transform: translateX(-50%) translateY(4px);
|
|
38
|
+
border: 8px solid transparent;
|
|
39
|
+
border-top-color: light-dark(rgb(20 20 30 / 0.75), rgb(40 40 55 / 0.8));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[data-tooltip]:is(:hover, :focus-visible)::before,
|
|
43
|
+
[data-tooltip]:is(:hover, :focus-visible)::after {
|
|
44
|
+
opacity: 1;
|
|
45
|
+
visibility: visible;
|
|
46
|
+
transition-delay: 700ms;
|
|
47
|
+
transform: translateX(-50%) translateY(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
@layer utilities {
|
|
2
|
+
.align-left { text-align: start; }
|
|
3
|
+
.align-center { text-align: center; }
|
|
4
|
+
.align-right { text-align: end; }
|
|
5
|
+
.text-light { color: var(--muted-foreground); }
|
|
6
|
+
.text-lighter { color: var(--faint-foreground); }
|
|
7
|
+
|
|
8
|
+
.flex { display: flex; }
|
|
9
|
+
.flex-col { flex-direction: column; }
|
|
10
|
+
.items-center { align-items: center; }
|
|
11
|
+
.justify-center { justify-content: center; }
|
|
12
|
+
.justify-between { justify-content: space-between; }
|
|
13
|
+
.justify-end { justify-content: flex-end; }
|
|
14
|
+
|
|
15
|
+
/* Bootstrap inspired. */
|
|
16
|
+
.hstack {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: var(--space-3);
|
|
20
|
+
flex-wrap: wrap;
|
|
21
|
+
align-content: flex-start;
|
|
22
|
+
height: auto;
|
|
23
|
+
|
|
24
|
+
* {
|
|
25
|
+
margin: 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
.vstack {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
gap: var(--space-3);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.gap-1 { gap: var(--space-1); }
|
|
35
|
+
.gap-2 { gap: var(--space-2); }
|
|
36
|
+
.gap-4 { gap: var(--space-4); }
|
|
37
|
+
|
|
38
|
+
.mt-2 { margin-block-start: var(--space-2); }
|
|
39
|
+
.mt-4 { margin-block-start: var(--space-4); }
|
|
40
|
+
.mt-6 { margin-block-start: var(--space-6); }
|
|
41
|
+
|
|
42
|
+
.mb-2 { margin-block-end: var(--space-2); }
|
|
43
|
+
.mb-4 { margin-block-end: var(--space-4); }
|
|
44
|
+
.mb-6 { margin-block-end: var(--space-6); }
|
|
45
|
+
.p-4 { padding: var(--space-4); }
|
|
46
|
+
|
|
47
|
+
.w-100 { width: 100%; }
|
|
48
|
+
|
|
49
|
+
:is(ul, ol, a).unstyled {
|
|
50
|
+
list-style: none;
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
padding: 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/js/base.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// oat - Base Web Component Class
|
|
2
|
+
// Provides lifecycle management, event handling, and utilities.
|
|
3
|
+
|
|
4
|
+
export class OtBase extends HTMLElement {
|
|
5
|
+
#initialized = false;
|
|
6
|
+
|
|
7
|
+
// Called when element is added to DOM.
|
|
8
|
+
connectedCallback() {
|
|
9
|
+
if (this.#initialized) return;
|
|
10
|
+
|
|
11
|
+
// Wait for DOM to be ready.
|
|
12
|
+
if (document.readyState === 'loading') {
|
|
13
|
+
document.addEventListener('DOMContentLoaded', () => this.#setup(), { once: true });
|
|
14
|
+
} else {
|
|
15
|
+
this.#setup();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Private setup to ensure that init() is only called once.
|
|
20
|
+
#setup() {
|
|
21
|
+
if (this.#initialized) return;
|
|
22
|
+
this.#initialized = true;
|
|
23
|
+
this.init();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Called when element is removed from DOM.
|
|
27
|
+
disconnectedCallback() {
|
|
28
|
+
this.cleanup();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Override in subclass for cleanup logic.
|
|
32
|
+
cleanup() {}
|
|
33
|
+
|
|
34
|
+
// Central event handler - enables automatic cleanup.
|
|
35
|
+
// Usage: element.addEventListener('click', this)
|
|
36
|
+
handleEvent(event) {
|
|
37
|
+
const handler = this[`on${event.type}`];
|
|
38
|
+
if (handler) handler.call(this, event);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Given a keyboard event (left, right, home, end), the current selection idx
|
|
42
|
+
// total items in a list, return 0-n index of the next/previous item
|
|
43
|
+
// for doing a roving keyboard nav.
|
|
44
|
+
keyNav(event, idx, len, prevKey, nextKey, homeEnd = false) {
|
|
45
|
+
const { key } = event;
|
|
46
|
+
let next = -1;
|
|
47
|
+
|
|
48
|
+
if (key === nextKey) {
|
|
49
|
+
next = (idx + 1) % len;
|
|
50
|
+
} else if (key === prevKey) {
|
|
51
|
+
next = (idx - 1 + len) % len;
|
|
52
|
+
} else if (homeEnd) {
|
|
53
|
+
if (key === 'Home') {
|
|
54
|
+
next = 0;
|
|
55
|
+
} else if (key === 'End') {
|
|
56
|
+
next = len - 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (next >= 0) event.preventDefault();
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Emit a custom event.
|
|
65
|
+
emit(name, detail = null) {
|
|
66
|
+
return this.dispatchEvent(new CustomEvent(name, {
|
|
67
|
+
bubbles: true,
|
|
68
|
+
composed: true,
|
|
69
|
+
cancelable: true,
|
|
70
|
+
detail
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Query selector within this element.
|
|
75
|
+
$(selector) {
|
|
76
|
+
return this.querySelector(selector);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Query selector all within this element.
|
|
80
|
+
$$(selector) {
|
|
81
|
+
return Array.from(this.querySelectorAll(selector));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Generate a unique ID string.
|
|
85
|
+
uid() {
|
|
86
|
+
return Math.random().toString(36).slice(2, 10);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Polyfill for command/commandfor (Safari)
|
|
91
|
+
if (!('commandForElement' in HTMLButtonElement.prototype)) {
|
|
92
|
+
document.addEventListener('click', e => {
|
|
93
|
+
const btn = e.target.closest('button[commandfor]');
|
|
94
|
+
if (!btn) return;
|
|
95
|
+
|
|
96
|
+
const target = document.getElementById(btn.getAttribute('commandfor'));
|
|
97
|
+
if (!target) return;
|
|
98
|
+
|
|
99
|
+
const command = btn.getAttribute('command') || 'toggle';
|
|
100
|
+
|
|
101
|
+
if (target instanceof HTMLDialogElement) {
|
|
102
|
+
if (command === 'show-modal') target.showModal();
|
|
103
|
+
else if (command === 'close') target.close();
|
|
104
|
+
else target.open ? target.close() : target.showModal();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
package/js/command.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oat - Command Palette Component
|
|
3
|
+
* Global Cmd/Ctrl+K command palette with search and keyboard navigation.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <ot-command>
|
|
7
|
+
* <dialog id="cmd" closedby="any">
|
|
8
|
+
* <input type="search" placeholder="Type a command...">
|
|
9
|
+
* <div role="listbox">
|
|
10
|
+
* <span>Section</span>
|
|
11
|
+
* <button role="option">Item 1</button>
|
|
12
|
+
* <button role="option">Item 2</button>
|
|
13
|
+
* </div>
|
|
14
|
+
* </dialog>
|
|
15
|
+
* </ot-command>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { OtBase } from './base.js';
|
|
19
|
+
|
|
20
|
+
class OtCommand extends OtBase {
|
|
21
|
+
#dialog;
|
|
22
|
+
#input;
|
|
23
|
+
#items;
|
|
24
|
+
#idx = -1;
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
this.#dialog = this.$('dialog') || this.#wrap();
|
|
28
|
+
this.#input = this.$('input[type="search"]');
|
|
29
|
+
|
|
30
|
+
document.addEventListener('keydown', this);
|
|
31
|
+
this.#dialog.addEventListener('keydown', this);
|
|
32
|
+
|
|
33
|
+
if (this.#input) this.#input.addEventListener('input', this);
|
|
34
|
+
|
|
35
|
+
this.#dialog.addEventListener('click', this);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#wrap() {
|
|
39
|
+
const d = document.createElement('dialog');
|
|
40
|
+
d.setAttribute('closedby', 'any');
|
|
41
|
+
while (this.firstChild) d.appendChild(this.firstChild);
|
|
42
|
+
this.appendChild(d);
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Open the command palette programmatically. */
|
|
47
|
+
open() {
|
|
48
|
+
this.#dialog.showModal();
|
|
49
|
+
if (this.#input) {
|
|
50
|
+
this.#input.value = '';
|
|
51
|
+
this.#input.focus();
|
|
52
|
+
}
|
|
53
|
+
this.#reset();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#reset() {
|
|
57
|
+
this.#idx = -1;
|
|
58
|
+
this.$$('[role="listbox"] > *').forEach(el => (el.hidden = false));
|
|
59
|
+
this.$$('[role="option"]').forEach(el => el.removeAttribute('aria-selected'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onkeydown(e) {
|
|
63
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
this.#dialog.open ? this.#dialog.close() : this.open();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!this.#dialog.open) return;
|
|
70
|
+
|
|
71
|
+
if (e.key === 'Escape') {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
this.#dialog.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.#items = this.$$('[role="option"]:not([hidden])');
|
|
78
|
+
if (!this.#items.length) return;
|
|
79
|
+
|
|
80
|
+
const next = this.keyNav(e, this.#idx, this.#items.length, 'ArrowUp', 'ArrowDown', true);
|
|
81
|
+
if (next >= 0) {
|
|
82
|
+
this.#idx = next;
|
|
83
|
+
this.#items.forEach((el, i) =>
|
|
84
|
+
el.setAttribute('aria-selected', String(i === next))
|
|
85
|
+
);
|
|
86
|
+
this.#items[next].scrollIntoView({ block: 'nearest' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (e.key === 'Enter' && this.#idx >= 0) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
this.#items[this.#idx].click();
|
|
92
|
+
this.#dialog.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
oninput() {
|
|
97
|
+
const q = this.#input.value.toLowerCase();
|
|
98
|
+
let section = null;
|
|
99
|
+
let hasVisible = false;
|
|
100
|
+
|
|
101
|
+
for (const el of this.$$('[role="listbox"] > *')) {
|
|
102
|
+
if (el.matches('[role="option"]')) {
|
|
103
|
+
const match = !q || el.textContent.toLowerCase().includes(q);
|
|
104
|
+
el.hidden = !match;
|
|
105
|
+
if (match) hasVisible = true;
|
|
106
|
+
} else {
|
|
107
|
+
if (section) section.hidden = !hasVisible;
|
|
108
|
+
section = el;
|
|
109
|
+
hasVisible = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (section) section.hidden = !hasVisible;
|
|
113
|
+
|
|
114
|
+
this.#idx = -1;
|
|
115
|
+
this.$$('[role="option"]').forEach(el => el.removeAttribute('aria-selected'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onclick(e) {
|
|
119
|
+
if (e.target === this.#dialog) {
|
|
120
|
+
this.#dialog.close();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (e.target.closest('[role="option"]')) {
|
|
124
|
+
this.#dialog.close();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (this.#input && !e.target.closest('input')) {
|
|
128
|
+
this.#input.focus();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
cleanup() {
|
|
133
|
+
document.removeEventListener('keydown', this);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
customElements.define('ot-command', OtCommand);
|