@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 ADDED
@@ -0,0 +1,13 @@
1
+ # @oxcide-ui/core
2
+
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 1961991: chore: verify automated release with updated npm token
8
+
9
+ ## 0.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 04c4ff6: feat: initial public release setup with automated CI/CD pipeline.
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,2 @@
1
+ export { useOxcideUI } from './useOxcideUI'
2
+ export { useLocale } from './useLocale'
@@ -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
+ }
@@ -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
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@oxcide-ui/themes": ["../themes/src"],
7
+ "@oxcide-ui/schema": ["../schema/src"]
8
+ }
9
+ },
10
+ "include": ["src/**/*.ts", "src/**/*.vue"]
11
+ }