@knadh/oat 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,43 @@
1
+ @layer components {
2
+ [role="status"].skeleton {
3
+ margin-bottom: var(--space-3);
4
+ background: var(--muted);
5
+ border-radius: var(--radius-medium);
6
+ animation: shimmer 2s infinite;
7
+ background-size: 200% 100%;
8
+ background-image: linear-gradient(
9
+ 90deg,
10
+ var(--muted) 0%,
11
+ color-mix(in srgb, var(--muted) 30%, white) 30%,
12
+ var(--muted) 100%
13
+ );
14
+
15
+ [data-theme="dark"] & {
16
+ background-image: linear-gradient(
17
+ 90deg,
18
+ var(--muted) 0%,
19
+ color-mix(in srgb, var(--muted) 90%, var(--foreground)) 90%,
20
+ var(--muted) 100%
21
+ );
22
+ }
23
+
24
+ &.box {
25
+ width: 4rem;
26
+ height: 4rem;
27
+ }
28
+
29
+ &.line {
30
+ height: 1rem;
31
+ width: 100%;
32
+ }
33
+ }
34
+
35
+ [role="status"].skeleton:last-child {
36
+ margin-bottom: 0;
37
+ }
38
+
39
+ @keyframes shimmer {
40
+ from { background-position: 200% 0; }
41
+ to { background-position: -200% 0; }
42
+ }
43
+ }
@@ -0,0 +1,27 @@
1
+ @layer components {
2
+ .spinner {
3
+ width: 1.5rem;
4
+ height: 1.5rem;
5
+ border: 2px solid var(--muted);
6
+ border-top-color: var(--primary);
7
+ border-radius: var(--radius-full);
8
+ animation: spin 1s linear infinite;
9
+
10
+ &.small {
11
+ width: 1rem;
12
+ height: 1rem;
13
+ }
14
+
15
+ &.large {
16
+ width: 2rem;
17
+ height: 2rem;
18
+ border-width: 3px;
19
+ }
20
+ }
21
+
22
+ @keyframes spin {
23
+ to {
24
+ transform: rotate(360deg);
25
+ }
26
+ }
27
+ }
package/css/table.css ADDED
@@ -0,0 +1,40 @@
1
+ @layer base {
2
+ table {
3
+ border-collapse: collapse;
4
+ table-layout: fixed;
5
+ width: 100%;
6
+ font-size: var(--text-7);
7
+ }
8
+
9
+ thead {
10
+ border-bottom: 1px solid var(--border);
11
+ }
12
+
13
+ th, td {
14
+ overflow-wrap: break-word;
15
+ }
16
+
17
+ th {
18
+ padding: var(--space-3) var(--space-2);
19
+ text-align: left;
20
+ font-weight: var(--font-medium);
21
+ color: var(--muted-foreground);
22
+ }
23
+
24
+ td {
25
+ padding: var(--space-3) var(--space-2);
26
+ }
27
+
28
+ tbody tr {
29
+ border-bottom: 1px solid var(--border);
30
+ transition: background-color var(--transition-fast);
31
+
32
+ &:last-child {
33
+ border-bottom: none;
34
+ }
35
+
36
+ &:hover {
37
+ background-color: rgb(from var(--muted) r g b / 0.5);
38
+ }
39
+ }
40
+ }
package/css/tabs.css ADDED
@@ -0,0 +1,44 @@
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
+ }
10
+
11
+ [role="tab"] {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ padding: var(--space-2) var(--space-3);
16
+ font-size: var(--text-7);
17
+ font-weight: var(--font-medium);
18
+ white-space: nowrap;
19
+ background-color: transparent;
20
+ color: var(--muted-foreground);
21
+ border: none;
22
+ border-radius: calc(var(--radius-medium) - 2px);
23
+ cursor: pointer;
24
+ transition: background-color var(--transition-fast), color var(--transition-fast);
25
+
26
+ &:hover {
27
+ color: var(--foreground);
28
+ }
29
+
30
+ &[aria-selected="true"] {
31
+ background-color: var(--background);
32
+ color: var(--foreground);
33
+ box-shadow: var(--shadow-small);
34
+ }
35
+ }
36
+
37
+ [role="tabpanel"] {
38
+ padding: var(--space-4) 0;
39
+
40
+ &:focus-visible {
41
+ outline: none;
42
+ }
43
+ }
44
+ }
package/css/toast.css ADDED
@@ -0,0 +1,115 @@
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
+
12
+ overflow: visible;
13
+
14
+ &::backdrop {
15
+ display: none;
16
+ }
17
+
18
+ &[data-placement="top-left"] {
19
+ inset: var(--space-4) auto auto var(--space-4);
20
+ }
21
+
22
+ &[data-placement="top-center"] {
23
+ inset: var(--space-4) auto auto 50%;
24
+ transform: translateX(-50%);
25
+ }
26
+
27
+ &[data-placement="top-right"] {
28
+ inset: var(--space-4) var(--space-4) auto auto;
29
+ }
30
+
31
+ &[data-placement="bottom-left"] {
32
+ inset: auto auto var(--space-4) var(--space-4);
33
+ flex-direction: column-reverse;
34
+ }
35
+
36
+ &[data-placement="bottom-center"] {
37
+ inset: auto auto var(--space-4) 50%;
38
+ transform: translateX(-50%);
39
+ flex-direction: column-reverse;
40
+ }
41
+
42
+ &[data-placement="bottom-right"] {
43
+ inset: auto var(--space-4) var(--space-4) auto;
44
+ flex-direction: column-reverse;
45
+ }
46
+ }
47
+
48
+ .toast {
49
+ padding: var(--space-5) var(--space-4);
50
+ max-width: 28rem;
51
+ min-width: 20rem;
52
+ pointer-events: auto;
53
+ background-color: var(--card);
54
+ border: 1px solid var(--border);
55
+ border-left-width: var(--space-1);
56
+ border-left-style: solid;
57
+ border-radius: var(--radius-medium);
58
+ box-shadow: var(--shadow-small);
59
+ transition: opacity 150ms, transform 150ms, margin 150ms;
60
+ line-height: 1;
61
+
62
+ .toast-title {
63
+ font-weight: 600;
64
+ margin: 0 0 var(--space-3) 0;
65
+ }
66
+ .toast-message {
67
+ color: var(--muted-foreground);
68
+ }
69
+
70
+ &[data-variant="success"] {
71
+ border-left-color: var(--success);
72
+ }
73
+
74
+ &[data-variant="danger"] {
75
+ border-left-color: var(--danger);
76
+ }
77
+
78
+ &[data-variant="warning"] {
79
+ border-left-color: var(--warning);
80
+ }
81
+
82
+ &[data-variant="info"] {
83
+ border-left-color: var(--secondary);
84
+ }
85
+
86
+ & > [data-close] {
87
+ margin-inline-start: auto;
88
+ background: none;
89
+ border: none;
90
+ padding: 0;
91
+ cursor: pointer;
92
+ opacity: 0.5;
93
+
94
+ &:hover {
95
+ opacity: 1;
96
+ }
97
+ }
98
+
99
+ margin: var(--space-2) 0;
100
+
101
+ &[data-entering] {
102
+ opacity: 0;
103
+ transform: translateY(-1rem);
104
+ }
105
+
106
+ &[data-exiting] {
107
+ opacity: 0;
108
+ margin: 0;
109
+ padding-block: 0;
110
+ max-height: 0;
111
+ overflow: hidden;
112
+ transition: opacity 200ms, margin 200ms, padding 200ms, max-height 200ms;
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,47 @@
1
+ @layer components {
2
+ [data-tooltip] {
3
+ position: relative;
4
+ }
5
+
6
+ [data-tooltip]::before,
7
+ [data-tooltip]::after {
8
+ position: absolute;
9
+ left: 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
+ bottom: 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: var(--foreground);
27
+ color: var(--background);
28
+ border-radius: var(--radius-medium);
29
+ }
30
+
31
+ /* Arrow */
32
+ [data-tooltip]::before {
33
+ content: '';
34
+ bottom: calc(100% - 5px);
35
+ transform: translateX(-50%) translateY(4px);
36
+ border: 8px solid transparent;
37
+ border-top-color: var(--foreground);
38
+ }
39
+
40
+ [data-tooltip]:is(:hover, :focus-visible)::before,
41
+ [data-tooltip]:is(:hover, :focus-visible)::after {
42
+ opacity: 1;
43
+ visibility: visible;
44
+ transition-delay: 700ms;
45
+ transform: translateX(-50%) translateY(0);
46
+ }
47
+ }
@@ -0,0 +1,64 @@
1
+ @layer utilities {
2
+ [hidden] {
3
+ display: none !important;
4
+ }
5
+
6
+ .sr-only {
7
+ position: absolute;
8
+ width: 1px;
9
+ height: 1px;
10
+ padding: 0;
11
+ margin: -1px;
12
+ overflow: hidden;
13
+ clip: rect(0, 0, 0, 0);
14
+ white-space: nowrap;
15
+ border-width: 0;
16
+ }
17
+
18
+ .text-left { text-align: left; }
19
+ .text-center { text-align: center; }
20
+ .text-right { text-align: right; }
21
+ .text-light { color: var(--muted-foreground); }
22
+ .text-lighter { color: var(--muted-foreground); }
23
+
24
+ .flex { display: flex; }
25
+ .flex-col { flex-direction: column; }
26
+ .items-center { align-items: center; }
27
+ .justify-center { justify-content: center; }
28
+ .justify-between { justify-content: space-between; }
29
+
30
+ /* Bootstrap inspired. */
31
+ .hstack {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: var(--space-3);
35
+ }
36
+ .vstack {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: var(--space-3);
40
+ }
41
+
42
+ .gap-1 { gap: var(--space-1); }
43
+ .gap-2 { gap: var(--space-2); }
44
+ .gap-4 { gap: var(--space-4); }
45
+
46
+ .mt-2 { margin-top: var(--space-2); }
47
+ .mt-4 { margin-top: var(--space-4); }
48
+ .mt-6 { margin-top: var(--space-6); }
49
+
50
+ .mb-2 { margin-bottom: var(--space-2); }
51
+ .mb-4 { margin-bottom: var(--space-4); }
52
+ .mb-6 { margin-bottom: var(--space-6); }
53
+ .p-4 { padding: var(--space-4); }
54
+
55
+ .w-100 { width: 100%; }
56
+ }
57
+
58
+ ul, ol {
59
+ &.unstyled {
60
+ list-style: none;
61
+ padding-left: 0;
62
+ margin-left: 0;
63
+ }
64
+ }
package/js/base.js ADDED
@@ -0,0 +1,106 @@
1
+ // oat - Base Web Component Class
2
+ // Provides lifecycle management, event handling, and utilities.
3
+
4
+ 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
+ // Override in WebComponent subclasses for init logic.
27
+ init() {}
28
+
29
+ // Called when element is removed from DOM.
30
+ disconnectedCallback() {
31
+ this.cleanup();
32
+ }
33
+
34
+ // Override in subclass for cleanup logic.
35
+ cleanup() {}
36
+
37
+ // Central event handler - enables automatic cleanup.
38
+ // Usage: element.addEventListener('click', this)
39
+ handleEvent(event) {
40
+ const handler = this[`on${event.type}`];
41
+ if (handler) handler.call(this, event);
42
+ }
43
+
44
+ // Emit a custom event.
45
+ emit(name, detail = null) {
46
+ return this.dispatchEvent(new CustomEvent(name, {
47
+ bubbles: true,
48
+ composed: true,
49
+ cancelable: true,
50
+ detail
51
+ }));
52
+ }
53
+
54
+ // Get boolean attribute value.
55
+ getBool(name) {
56
+ return this.hasAttribute(name);
57
+ }
58
+
59
+ // Set or remove boolean attribute.
60
+ setBool(name, value) {
61
+ if (value) {
62
+ this.setAttribute(name, '');
63
+ } else {
64
+ this.removeAttribute(name);
65
+ }
66
+ }
67
+
68
+ // Query selector within this element.
69
+ $(selector) {
70
+ return this.querySelector(selector);
71
+ }
72
+
73
+ // Query selector all within this element.
74
+ $$(selector) {
75
+ return Array.from(this.querySelectorAll(selector));
76
+ }
77
+
78
+ // Generate a unique ID string.
79
+ uid() {
80
+ return Math.random().toString(36).slice(2, 10);
81
+ }
82
+ }
83
+
84
+ // Export for use in other files
85
+ if (typeof window !== 'undefined') {
86
+ window.OtBase = OtBase;
87
+ }
88
+
89
+ // Polyfill for command/commandfor (Safari)
90
+ if (!('commandForElement' in HTMLButtonElement.prototype)) {
91
+ document.addEventListener('click', e => {
92
+ const btn = e.target.closest('[commandfor]');
93
+ if (!btn) return;
94
+
95
+ const target = document.getElementById(btn.getAttribute('commandfor'));
96
+ if (!target) return;
97
+
98
+ const command = btn.getAttribute('command') || 'toggle';
99
+
100
+ if (target instanceof HTMLDialogElement) {
101
+ if (command === 'show-modal') target.showModal();
102
+ else if (command === 'close') target.close();
103
+ else target.open ? target.close() : target.showModal();
104
+ }
105
+ });
106
+ }
package/js/dropdown.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * oat - Dropdown Component
3
+ * Provides positioning, keyboard navigation, and ARIA state management.
4
+ *
5
+ * Usage:
6
+ * <ot-dropdown>
7
+ * <button popovertarget="menu-id">Options</button>
8
+ * <menu popover id="menu-id">
9
+ * <button role="menuitem">Item 1</button>
10
+ * <button role="menuitem">Item 2</button>
11
+ * </menu>
12
+ * </ot-dropdown>
13
+ */
14
+
15
+ class OtDropdown extends OtBase {
16
+ #menu;
17
+ #trigger;
18
+ #position;
19
+
20
+ init() {
21
+ this.#menu = this.$('menu[popover]');
22
+ this.#trigger = this.$('[popovertarget]');
23
+
24
+ if (!this.#menu || !this.#trigger) return;
25
+
26
+ this.#menu.addEventListener('toggle', this);
27
+ this.#menu.addEventListener('keydown', this);
28
+
29
+ this.#position = () => {
30
+ // Position has to be calculated and applied manually because
31
+ // popover positioning is like fixed, relative to the window.
32
+ const rect = this.#trigger.getBoundingClientRect();
33
+ this.#menu.style.top = `${rect.bottom}px`;
34
+ this.#menu.style.left = `${rect.left}px`;
35
+ };
36
+ }
37
+
38
+ ontoggle(e) {
39
+ if (e.newState === 'open') {
40
+ this.#position();
41
+ window.addEventListener('scroll', this.#position, true);
42
+ this.$('[role="menuitem"]')?.focus();
43
+ this.#trigger.ariaExpanded = 'true';
44
+ } else {
45
+ window.removeEventListener('scroll', this.#position, true);
46
+ this.#trigger.ariaExpanded = 'false';
47
+ this.#trigger.focus();
48
+ }
49
+ }
50
+
51
+ onkeydown(e) {
52
+ if (!e.target.matches('[role="menuitem"]')) return;
53
+
54
+ const items = this.$$('[role="menuitem"]');
55
+ const idx = items.indexOf(e.target);
56
+
57
+ switch (e.key) {
58
+ case 'ArrowDown':
59
+ e.preventDefault();
60
+ items[(idx + 1) % items.length]?.focus();
61
+ break;
62
+ case 'ArrowUp':
63
+ e.preventDefault();
64
+ items[idx - 1 < 0 ? items.length - 1 : idx - 1]?.focus();
65
+ break;
66
+ }
67
+ }
68
+
69
+ cleanup() {
70
+ window.removeEventListener('scroll', this.#position, true);
71
+ }
72
+ }
73
+
74
+ customElements.define('ot-dropdown', OtDropdown);
package/js/sidebar.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Sidebar toggle handler
3
+ * Toggles data-sidebar-open on layout when toggle button is clicked
4
+ */
5
+ document.addEventListener('click', (e) => {
6
+ const toggle = e.target.closest('[data-sidebar-toggle]');
7
+ if (toggle) {
8
+ const layout = toggle.closest('[data-sidebar-layout]');
9
+ layout?.toggleAttribute('data-sidebar-open');
10
+ }
11
+ });
package/js/tabs.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * oat - Tabs Component
3
+ * Provides keyboard navigation and ARIA state management.
4
+ *
5
+ * Usage:
6
+ * <ot-tabs>
7
+ * <div role="tablist">
8
+ * <button role="tab">Tab 1</button>
9
+ * <button role="tab">Tab 2</button>
10
+ * </div>
11
+ * <div role="tabpanel">Content 1</div>
12
+ * <div role="tabpanel">Content 2</div>
13
+ * </ot-tabs>
14
+ */
15
+
16
+ class OtTabs extends OtBase {
17
+ #tabs = [];
18
+ #panels = [];
19
+
20
+ init() {
21
+ const tablist = this.$(':scope > [role="tablist"]');
22
+ this.#tabs = tablist ? [...tablist.querySelectorAll('[role="tab"]')] : [];
23
+ this.#panels = this.$$(':scope > [role="tabpanel"]');
24
+
25
+ if (this.#tabs.length === 0 || this.#panels.length === 0) {
26
+ console.warn('ot-tabs: Missing tab or tabpanel elements');
27
+ return;
28
+ }
29
+
30
+ // Generate IDs and set up ARIA.
31
+ this.#tabs.forEach((tab, i) => {
32
+ const panel = this.#panels[i];
33
+ if (!panel) return;
34
+
35
+ const tabId = tab.id || `ot-tab-${this.uid()}`;
36
+ const panelId = panel.id || `ot-panel-${this.uid()}`;
37
+
38
+ tab.id = tabId;
39
+ panel.id = panelId;
40
+ tab.setAttribute('aria-controls', panelId);
41
+ panel.setAttribute('aria-labelledby', tabId);
42
+
43
+ tab.addEventListener('click', this);
44
+ tab.addEventListener('keydown', this);
45
+ });
46
+
47
+ // Find initially active tab or default to first.
48
+ const activeTab = this.#tabs.findIndex(t => t.ariaSelected === 'true');
49
+ this.#activate(activeTab >= 0 ? activeTab : 0);
50
+ }
51
+
52
+ onclick(e) {
53
+ const index = this.#tabs.indexOf(e.target.closest('[role="tab"]'));
54
+ if (index >= 0) this.#activate(index);
55
+ }
56
+
57
+ onkeydown(e) {
58
+ const { key } = e;
59
+ const idx = this.activeIndex;
60
+ let newIdx = idx;
61
+
62
+ switch (key) {
63
+ case 'ArrowLeft':
64
+ e.preventDefault();
65
+ newIdx = idx - 1;
66
+ if (newIdx < 0) newIdx = this.#tabs.length - 1;
67
+ break;
68
+ case 'ArrowRight':
69
+ e.preventDefault();
70
+ newIdx = (idx + 1) % this.#tabs.length;
71
+ break;
72
+ default:
73
+ return;
74
+ }
75
+
76
+ this.#activate(newIdx);
77
+ this.#tabs[newIdx].focus();
78
+ }
79
+
80
+ #activate(idx) {
81
+ this.#tabs.forEach((tab, i) => {
82
+ const isActive = i === idx;
83
+ tab.ariaSelected = String(isActive);
84
+ tab.tabIndex = isActive ? 0 : -1;
85
+ });
86
+
87
+ this.#panels.forEach((panel, i) => {
88
+ panel.hidden = i !== idx;
89
+ });
90
+
91
+ this.emit('ot-tab-change', { index: idx, tab: this.#tabs[idx] });
92
+ }
93
+
94
+ get activeIndex() {
95
+ return this.#tabs.findIndex(t => t.ariaSelected === 'true');
96
+ }
97
+
98
+ set activeIndex(value) {
99
+ if (value >= 0 && value < this.#tabs.length) {
100
+ this.#activate(value);
101
+ }
102
+ }
103
+ }
104
+
105
+ customElements.define('ot-tabs', OtTabs);