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.
@@ -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
+ }
@@ -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);