@oxcide-ui/core 0.0.3
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/CHANGELOG.md +13 -0
- package/package.json +32 -0
- package/src/composables/index.ts +2 -0
- package/src/composables/useFocusTrap.ts +70 -0
- package/src/composables/useKeydown.ts +41 -0
- package/src/composables/useLocale.ts +28 -0
- package/src/composables/useOxcideUI.ts +14 -0
- package/src/composables/useZIndex.ts +18 -0
- package/src/config.ts +58 -0
- package/src/index.ts +9 -0
- package/src/types.ts +45 -0
- package/src/utils/dom.ts +22 -0
- package/src/utils/zindex.ts +29 -0
- package/tsconfig.json +11 -0
package/CHANGELOG.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oxcide-ui/core",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"import": "./src/index.ts"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"main": "./src/index.ts",
|
|
12
|
+
"types": "./src/index.ts",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"zod": "3.25.76"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"vue": "^3.5.13"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@biomejs/biome": "^2.3.13",
|
|
21
|
+
"typescript": "^5.8.2",
|
|
22
|
+
"vue": "^3.5.13"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"type-check": "vue-tsc --noEmit",
|
|
29
|
+
"lint": "biome check .",
|
|
30
|
+
"lint:fix": "biome check --write ."
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type Ref, onUnmounted, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Traps focus within the specified element.
|
|
7
|
+
* Useful for modal dialogs, drawers, etc.
|
|
8
|
+
*
|
|
9
|
+
* @param target The element to trap focus in
|
|
10
|
+
* @param active Whether the trap is currently active
|
|
11
|
+
*/
|
|
12
|
+
export function useFocusTrap(target: Ref<HTMLElement | null | undefined>, active: Ref<boolean>) {
|
|
13
|
+
let firstFocusableEl: HTMLElement | null = null
|
|
14
|
+
let lastFocusableEl: HTMLElement | null = null
|
|
15
|
+
|
|
16
|
+
const getFocusableElements = () => {
|
|
17
|
+
if (!target.value) return []
|
|
18
|
+
return Array.from(target.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
|
|
19
|
+
(el) => !el.hasAttribute('disabled') && el.getAttribute('aria-hidden') !== 'true'
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
24
|
+
if (!active.value || e.key !== 'Tab') return
|
|
25
|
+
|
|
26
|
+
const focusableElements = getFocusableElements()
|
|
27
|
+
if (focusableElements.length === 0) {
|
|
28
|
+
e.preventDefault()
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
firstFocusableEl = focusableElements[0]
|
|
33
|
+
lastFocusableEl = focusableElements[focusableElements.length - 1]
|
|
34
|
+
|
|
35
|
+
if (e.shiftKey) {
|
|
36
|
+
// Shift + Tab (backwards)
|
|
37
|
+
if (document.activeElement === firstFocusableEl) {
|
|
38
|
+
lastFocusableEl.focus()
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
// Tab (forwards)
|
|
43
|
+
if (document.activeElement === lastFocusableEl) {
|
|
44
|
+
firstFocusableEl.focus()
|
|
45
|
+
e.preventDefault()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
watch(
|
|
51
|
+
active,
|
|
52
|
+
(isActive) => {
|
|
53
|
+
if (isActive) {
|
|
54
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
55
|
+
// Optional: Auto focus the first element
|
|
56
|
+
const focusable = getFocusableElements()
|
|
57
|
+
if (focusable.length > 0) {
|
|
58
|
+
focusable[0].focus()
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{ immediate: true }
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
onUnmounted(() => {
|
|
68
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { onUnmounted, watch, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
type KeyEventHandler = (event: KeyboardEvent) => void
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register a keyboard event handler for a specific key.
|
|
7
|
+
*
|
|
8
|
+
* @param key The keyboard key to listen for (e.g., 'Escape')
|
|
9
|
+
* @param handler The handler function
|
|
10
|
+
* @param active Whether the listener is active
|
|
11
|
+
*/
|
|
12
|
+
export function useKeydown(key: string, handler: KeyEventHandler, active: boolean | Ref<boolean> = true) {
|
|
13
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
14
|
+
if (e.key === key) {
|
|
15
|
+
handler(e)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const activate = () => {
|
|
20
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const deactivate = () => {
|
|
24
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (active === true || (typeof active !== 'boolean' && active.value)) {
|
|
28
|
+
activate()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof active !== 'boolean') {
|
|
32
|
+
watch(active, (isActive) => {
|
|
33
|
+
if (isActive) activate()
|
|
34
|
+
else deactivate()
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onUnmounted(() => {
|
|
39
|
+
deactivate()
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { useOxcideUI } from './useOxcideUI'
|
|
3
|
+
import type { LocaleConfig } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Access locale translations from OxcideUI configuration.
|
|
7
|
+
* Provides reactive locale values that update when config changes.
|
|
8
|
+
*/
|
|
9
|
+
export function useLocale() {
|
|
10
|
+
const config = useOxcideUI()
|
|
11
|
+
|
|
12
|
+
const locale = computed<LocaleConfig>(() => config.locale ?? {})
|
|
13
|
+
|
|
14
|
+
function t(key: string): string {
|
|
15
|
+
const value = locale.value[key]
|
|
16
|
+
if (typeof value === 'string') return value
|
|
17
|
+
|
|
18
|
+
// Check nested aria keys
|
|
19
|
+
if (key.startsWith('aria.')) {
|
|
20
|
+
const ariaKey = key.slice(5)
|
|
21
|
+
return locale.value.aria?.[ariaKey] ?? key
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return key
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { locale, t }
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { inject } from 'vue'
|
|
2
|
+
import { OxcideUISymbol } from '../config'
|
|
3
|
+
import type { OxcideUIConfig } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Access the global OxcideUI configuration instance.
|
|
7
|
+
*/
|
|
8
|
+
export function useOxcideUI(): OxcideUIConfig {
|
|
9
|
+
const config = inject(OxcideUISymbol)
|
|
10
|
+
if (!config) {
|
|
11
|
+
throw new Error('[OxcideUI] Plugin not installed. Call app.use(createOxcideUI()) first.')
|
|
12
|
+
}
|
|
13
|
+
return config
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ref, onMounted } from 'vue'
|
|
2
|
+
import { getNextZIndex, type ZIndexType } from '../utils/zindex'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composable to manage z-index for dynamic overlays.
|
|
6
|
+
* Automatically increments the z-index on mount if autoZIndex is enabled.
|
|
7
|
+
*/
|
|
8
|
+
export function useZIndex(key: ZIndexType = 'overlay', autoZIndex = true, baseZIndex = 0) {
|
|
9
|
+
const z = ref(baseZIndex || 0)
|
|
10
|
+
|
|
11
|
+
onMounted(() => {
|
|
12
|
+
if (autoZIndex) {
|
|
13
|
+
z.value = getNextZIndex(key)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return { zIndex: z }
|
|
18
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { App, InjectionKey } from 'vue'
|
|
2
|
+
import { reactive } from 'vue'
|
|
3
|
+
import type { OxcideUIConfig } from './types'
|
|
4
|
+
|
|
5
|
+
const defaultLocale = {
|
|
6
|
+
accept: 'Yes',
|
|
7
|
+
reject: 'No',
|
|
8
|
+
clear: 'Clear',
|
|
9
|
+
apply: 'Apply',
|
|
10
|
+
aria: {
|
|
11
|
+
close: 'Close',
|
|
12
|
+
previous: 'Previous',
|
|
13
|
+
next: 'Next',
|
|
14
|
+
navigation: 'Navigation',
|
|
15
|
+
selectAll: 'Select All'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultConfig: OxcideUIConfig = {
|
|
20
|
+
locale: defaultLocale,
|
|
21
|
+
theme: 'aura',
|
|
22
|
+
ripple: false,
|
|
23
|
+
zIndex: {
|
|
24
|
+
modal: 1100,
|
|
25
|
+
overlay: 1000,
|
|
26
|
+
menu: 1000,
|
|
27
|
+
tooltip: 1100
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const OxcideUISymbol: InjectionKey<OxcideUIConfig> = Symbol('OxcideUI')
|
|
32
|
+
|
|
33
|
+
export function createOxcideUI(config: OxcideUIConfig = {}) {
|
|
34
|
+
const resolvedConfig = reactive<OxcideUIConfig>({
|
|
35
|
+
...defaultConfig,
|
|
36
|
+
...config,
|
|
37
|
+
locale: {
|
|
38
|
+
...defaultLocale,
|
|
39
|
+
...config.locale,
|
|
40
|
+
aria: {
|
|
41
|
+
...defaultLocale.aria,
|
|
42
|
+
...config.locale?.aria
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
zIndex: {
|
|
46
|
+
...defaultConfig.zIndex,
|
|
47
|
+
...config.zIndex
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
install(app: App) {
|
|
53
|
+
app.provide(OxcideUISymbol, resolvedConfig)
|
|
54
|
+
app.config.globalProperties.$oxcide = resolvedConfig
|
|
55
|
+
},
|
|
56
|
+
config: resolvedConfig
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createOxcideUI } from './config'
|
|
2
|
+
export { useOxcideUI } from './composables/useOxcideUI'
|
|
3
|
+
export { useLocale } from './composables/useLocale'
|
|
4
|
+
export { useFocusTrap } from './composables/useFocusTrap'
|
|
5
|
+
export { useKeydown } from './composables/useKeydown'
|
|
6
|
+
export { useZIndex } from './composables/useZIndex'
|
|
7
|
+
export { getNextZIndex, setBaseZIndex } from './utils/zindex'
|
|
8
|
+
export { ensureTeleportTarget, isClient } from './utils/dom'
|
|
9
|
+
export type { OxcideUIConfig, LocaleConfig } from './types'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface LocaleConfig {
|
|
2
|
+
/** UI text translations */
|
|
3
|
+
accept?: string
|
|
4
|
+
reject?: string
|
|
5
|
+
clear?: string
|
|
6
|
+
apply?: string
|
|
7
|
+
|
|
8
|
+
/** Calendar */
|
|
9
|
+
dayNames?: string[]
|
|
10
|
+
dayNamesShort?: string[]
|
|
11
|
+
dayNamesMin?: string[]
|
|
12
|
+
monthNames?: string[]
|
|
13
|
+
monthNamesShort?: string[]
|
|
14
|
+
|
|
15
|
+
/** ARIA accessibility labels */
|
|
16
|
+
aria?: {
|
|
17
|
+
close?: string
|
|
18
|
+
previous?: string
|
|
19
|
+
next?: string
|
|
20
|
+
navigation?: string
|
|
21
|
+
selectAll?: string
|
|
22
|
+
[key: string]: string | undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[key: string]: unknown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OxcideUIConfig {
|
|
29
|
+
/** Locale configuration for i18n */
|
|
30
|
+
locale?: LocaleConfig
|
|
31
|
+
|
|
32
|
+
/** Theme name */
|
|
33
|
+
theme?: string
|
|
34
|
+
|
|
35
|
+
/** Whether to enable ripple effect */
|
|
36
|
+
ripple?: boolean
|
|
37
|
+
|
|
38
|
+
/** z-index layering config */
|
|
39
|
+
zIndex?: {
|
|
40
|
+
modal?: number
|
|
41
|
+
overlay?: number
|
|
42
|
+
menu?: number
|
|
43
|
+
tooltip?: number
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/utils/dom.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function isClient() {
|
|
2
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensures that a teleport target exist in the DOM.
|
|
7
|
+
* Useful for Nuxt SSR environments where the teleport target might missing in the initial HTML.
|
|
8
|
+
* @param to The selector for the teleport target (e.g., '#teleports')
|
|
9
|
+
*/
|
|
10
|
+
export function ensureTeleportTarget(to: string) {
|
|
11
|
+
if (!isClient()) return
|
|
12
|
+
|
|
13
|
+
if (typeof to === 'string' && to.startsWith('#')) {
|
|
14
|
+
const id = to.slice(1)
|
|
15
|
+
const target = document.getElementById(id)
|
|
16
|
+
if (!target) {
|
|
17
|
+
const el = document.createElement('div')
|
|
18
|
+
el.id = id
|
|
19
|
+
document.body.appendChild(el)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type ZIndexType = 'modal' | 'overlay' | 'menu' | 'tooltip'
|
|
2
|
+
|
|
3
|
+
const zIndex: Record<ZIndexType, number> = {
|
|
4
|
+
modal: 1100,
|
|
5
|
+
overlay: 1000,
|
|
6
|
+
menu: 1000,
|
|
7
|
+
tooltip: 1100
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the next z-index for a specific layer type and increment it.
|
|
12
|
+
*/
|
|
13
|
+
export function getNextZIndex(key: ZIndexType) {
|
|
14
|
+
return ++zIndex[key]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the current z-index for a specific layer type.
|
|
19
|
+
*/
|
|
20
|
+
export function getCurrentZIndex(key: ZIndexType) {
|
|
21
|
+
return zIndex[key]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set the base z-index for a specific layer type.
|
|
26
|
+
*/
|
|
27
|
+
export function setBaseZIndex(key: ZIndexType, value: number) {
|
|
28
|
+
zIndex[key] = value
|
|
29
|
+
}
|
package/tsconfig.json
ADDED