@karyl-chan/ui 0.4.0 → 0.5.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/package.json +4 -2
- package/src/components/AppModal.vue +140 -29
- package/src/components/AppPopover.vue +29 -7
- package/src/components/AppProgress.vue +113 -0
- package/src/components/AppSelect.vue +3 -0
- package/src/components/AppTabs.vue +55 -11
- package/src/components/AppTabsRouted.vue +76 -11
- package/src/components/AppToast.vue +90 -4
- package/src/components/tab-keyboard.ts +75 -0
- package/src/composables/use-confirm.ts +5 -0
- package/src/composables/use-popover.ts +33 -4
- package/src/composables/use-scroll-lock.ts +61 -0
- package/src/index.ts +14 -4
- package/src/lib/focus.ts +67 -0
- package/src/stores/toastStore.ts +42 -4
- package/src/tokens.css +6 -0
package/src/lib/focus.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus helpers for dialog-style overlays (focus-in on open, Tab trap,
|
|
3
|
+
* focus restore on close).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const FOCUSABLE_SELECTOR = [
|
|
7
|
+
"a[href]",
|
|
8
|
+
"area[href]",
|
|
9
|
+
"button:not([disabled])",
|
|
10
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
11
|
+
"select:not([disabled])",
|
|
12
|
+
"textarea:not([disabled])",
|
|
13
|
+
"iframe",
|
|
14
|
+
"audio[controls]",
|
|
15
|
+
"video[controls]",
|
|
16
|
+
"summary",
|
|
17
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
18
|
+
"[tabindex]",
|
|
19
|
+
].join(",");
|
|
20
|
+
|
|
21
|
+
function isVisible(el: HTMLElement): boolean {
|
|
22
|
+
if (el.closest("[hidden], [inert]")) return false;
|
|
23
|
+
// `checkVisibility` covers display:none / visibility:hidden ancestors.
|
|
24
|
+
// Environments without it (older engines, jsdom) count it as visible.
|
|
25
|
+
const check = (el as HTMLElement & { checkVisibility?: () => boolean })
|
|
26
|
+
.checkVisibility;
|
|
27
|
+
return typeof check === "function" ? check.call(el) : true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Tabbable descendants of `container`, in DOM order. */
|
|
31
|
+
export function getFocusable(container: HTMLElement): HTMLElement[] {
|
|
32
|
+
return Array.from(
|
|
33
|
+
container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
|
|
34
|
+
).filter((el) => el.tabIndex >= 0 && isVisible(el));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Keep Tab / Shift+Tab cycling inside `container`. Call from the
|
|
39
|
+
* container's keydown handler; returns true when it handled the key.
|
|
40
|
+
*/
|
|
41
|
+
export function trapTabKey(event: KeyboardEvent, container: HTMLElement): boolean {
|
|
42
|
+
if (event.key !== "Tab" || event.altKey || event.ctrlKey || event.metaKey) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const items = getFocusable(container);
|
|
46
|
+
if (items.length === 0) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
container.focus();
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const first = items[0];
|
|
52
|
+
const last = items[items.length - 1];
|
|
53
|
+
const active = document.activeElement as HTMLElement | null;
|
|
54
|
+
const inside = !!active && container.contains(active);
|
|
55
|
+
if (event.shiftKey) {
|
|
56
|
+
if (!inside || active === first || active === container) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
last.focus();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
} else if (!inside || active === last) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
first.focus();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
package/src/stores/toastStore.ts
CHANGED
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
import { defineStore } from "pinia";
|
|
2
2
|
import { ref } from "vue";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Toast severity. `error` / `warn` are announced assertively
|
|
6
|
+
* (role="alert"); `info` / `success` politely (role="status").
|
|
7
|
+
*/
|
|
8
|
+
export type ToastType = "error" | "warn" | "info" | "success";
|
|
9
|
+
|
|
10
|
+
export interface ToastOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Auto-dismiss delay in ms. Default 4000. `0` makes the toast sticky:
|
|
13
|
+
* it stays until the user closes it (close button) or `dismiss(id)`.
|
|
14
|
+
*/
|
|
15
|
+
duration?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
export interface ToastItem {
|
|
5
19
|
id: number;
|
|
6
20
|
message: string;
|
|
7
|
-
type:
|
|
21
|
+
type: ToastType;
|
|
22
|
+
/** Auto-dismiss delay in ms; `0` = sticky. Always set by `show()`. */
|
|
23
|
+
duration?: number;
|
|
8
24
|
}
|
|
9
25
|
|
|
26
|
+
/** Default auto-dismiss delay, unchanged from the original store. */
|
|
27
|
+
export const DEFAULT_TOAST_DURATION = 4000;
|
|
28
|
+
|
|
10
29
|
let nextId = 0;
|
|
11
30
|
|
|
12
31
|
export const useToastStore = defineStore("toast", () => {
|
|
13
32
|
const items = ref<ToastItem[]>([]);
|
|
33
|
+
const timers = new Map<number, ReturnType<typeof setTimeout>>();
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Show a toast. `type` defaults to `'error'` (the original behaviour).
|
|
37
|
+
* Returns the toast id, usable with `dismiss(id)`.
|
|
38
|
+
*/
|
|
39
|
+
function show(
|
|
40
|
+
message: string,
|
|
41
|
+
type: ToastType = "error",
|
|
42
|
+
options: ToastOptions = {},
|
|
43
|
+
): number {
|
|
16
44
|
const id = nextId++;
|
|
17
|
-
|
|
18
|
-
|
|
45
|
+
const raw = options.duration ?? DEFAULT_TOAST_DURATION;
|
|
46
|
+
const duration = Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
47
|
+
items.value = [...items.value, { id, message, type, duration }];
|
|
48
|
+
if (duration > 0) {
|
|
49
|
+
timers.set(id, setTimeout(() => dismiss(id), duration));
|
|
50
|
+
}
|
|
51
|
+
return id;
|
|
19
52
|
}
|
|
20
53
|
|
|
21
54
|
function dismiss(id: number) {
|
|
55
|
+
const timer = timers.get(id);
|
|
56
|
+
if (timer !== undefined) {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
timers.delete(id);
|
|
59
|
+
}
|
|
22
60
|
items.value = items.value.filter((t) => t.id !== id);
|
|
23
61
|
}
|
|
24
62
|
|
package/src/tokens.css
CHANGED
|
@@ -57,6 +57,12 @@
|
|
|
57
57
|
|
|
58
58
|
--pill-bg: #f3f4f6;
|
|
59
59
|
|
|
60
|
+
/* Unread / mention indicator (UnreadPill etc.). Same in both themes. */
|
|
61
|
+
--unread-accent: #f23f43;
|
|
62
|
+
|
|
63
|
+
/* Monospaced stack for IDs, keys, code-ish values. */
|
|
64
|
+
--font-mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
|
65
|
+
|
|
60
66
|
--transition-fast: 0.12s;
|
|
61
67
|
--transition-base: 0.15s;
|
|
62
68
|
--transition-slow: 0.2s;
|