orio-ui 1.18.1 → 1.19.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/README.md CHANGED
@@ -8,7 +8,7 @@ A delightful, lightweight component library for Nuxt 3+ applications. Built with
8
8
 
9
9
  ## Features
10
10
 
11
- ✨ **31 Components** - Beautiful, accessible components ready to use
11
+ ✨ **32 Components** - Beautiful, accessible components ready to use
12
12
  🎨 **Themeable** - 5 built-in accent themes with light/dark mode support
13
13
  🚀 **Auto-imported** - Works seamlessly with Nuxt's auto-import system
14
14
  📦 **Tree-shakeable** - Only bundle what you use
@@ -66,7 +66,7 @@ function handleClick() {
66
66
 
67
67
  ## What's Included
68
68
 
69
- ### Components (31)
69
+ ### Components (32)
70
70
 
71
71
  #### Form Controls
72
72
 
@@ -112,7 +112,7 @@ function handleClick() {
112
112
 
113
113
  - **Upload** - File upload component
114
114
 
115
- ### Composables (10)
115
+ ### Composables (11)
116
116
 
117
117
  - **useTheme** - Theme and color mode management
118
118
  - **useModal** - Modal state with animation origin tracking
@@ -187,8 +187,8 @@ npm run docs:dev
187
187
  orio-ui/
188
188
  ├── src/
189
189
  │ ├── runtime/
190
- │ │ ├── components/ # 31 Vue components
191
- │ │ ├── composables/ # 10 composables
190
+ │ │ ├── components/ # 32 Vue components
191
+ │ │ ├── composables/ # 11 composables
192
192
  │ │ ├── assets/css/ # Theme CSS files
193
193
  │ │ └── utils/ # Icon registry
194
194
  │ └── module.ts # Nuxt Module definition
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "1.18.1",
7
+ "version": "1.19.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -2,7 +2,7 @@ interface Props {
2
2
  direction?: "column" | "row";
3
3
  }
4
4
  declare var __VLS_1: {
5
- play: () => void;
5
+ play: () => Promise<void>;
6
6
  };
7
7
  type __VLS_Slots = {} & {
8
8
  default?: (props: typeof __VLS_1) => any;
@@ -2,7 +2,7 @@ interface Props {
2
2
  direction?: "column" | "row";
3
3
  }
4
4
  declare var __VLS_1: {
5
- play: () => void;
5
+ play: () => Promise<void>;
6
6
  };
7
7
  type __VLS_Slots = {} & {
8
8
  default?: (props: typeof __VLS_1) => any;
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
- import { useId } from "vue";
2
+ import { computed, toRef, useId } from "vue";
3
+ import { provideControlSize, sizeTokens } from "../composables/useControlSize";
3
4
  defineOptions({ inheritAttrs: false });
4
5
  const props = defineProps({
5
6
  appearance: { type: String, required: false, default: "normal" },
@@ -10,12 +11,15 @@ const props = defineProps({
10
11
  layout: { type: String, required: false, default: "vertical" },
11
12
  size: { type: String, required: false, default: "md" }
12
13
  });
14
+ provideControlSize(toRef(props, "size"));
15
+ const sizeStyle = computed(() => sizeTokens[props.size]);
13
16
  </script>
14
17
 
15
18
  <template>
16
19
  <div
17
20
  class="control"
18
21
  :class="[appearance, layout, `size-${size}`, { 'has-error': error, group }]"
22
+ :style="sizeStyle"
19
23
  v-bind="{
20
24
  ...$attrs,
21
25
  ...group ? { role: 'group', ...label ? { 'aria-labelledby': id } : {} } : {}
@@ -39,54 +43,6 @@ const props = defineProps({
39
43
  </template>
40
44
 
41
45
  <style scoped>
42
- .control {
43
- --control-font-size: var(--font-md);
44
- --control-label-font-size: var(--font-sm);
45
- --control-py: 0.5rem;
46
- --control-px: 0.75rem;
47
- --control-gap: 0.5rem;
48
- --control-radius: var(--border-radius-md);
49
- --control-icon-size: 1rem;
50
- --control-inner-block-start: 1.25rem;
51
- --control-inner-block-end: 0.2rem;
52
- --control-label-block-start: 0.25rem;
53
- }
54
- .control.size-sm {
55
- --control-font-size: var(--font-sm);
56
- --control-label-font-size: var(--font-xs);
57
- --control-py: 0.25rem;
58
- --control-px: 0.5rem;
59
- --control-gap: 0.25rem;
60
- --control-radius: var(--border-radius-sm);
61
- --control-icon-size: 0.75rem;
62
- --control-inner-block-start: 1rem;
63
- --control-inner-block-end: 0.1rem;
64
- --control-label-block-start: 0.2rem;
65
- }
66
- .control.size-lg {
67
- --control-font-size: var(--font-lg);
68
- --control-label-font-size: var(--font-md);
69
- --control-py: 0.625rem;
70
- --control-px: 1rem;
71
- --control-gap: 0.5rem;
72
- --control-radius: var(--border-radius-md);
73
- --control-icon-size: 1.25rem;
74
- --control-inner-block-start: 1.1rem;
75
- --control-inner-block-end: 0.2rem;
76
- --control-label-block-start: 0.25rem;
77
- }
78
- .control.size-xl {
79
- --control-font-size: var(--font-xl);
80
- --control-label-font-size: var(--font-lg);
81
- --control-py: 0.75rem;
82
- --control-px: 1.25rem;
83
- --control-gap: 0.75rem;
84
- --control-radius: var(--border-radius-lg);
85
- --control-icon-size: 1.5rem;
86
- --control-inner-block-start: 1.5rem;
87
- --control-inner-block-end: 0;
88
- --control-label-block-start: 0.25rem;
89
- }
90
46
  .control {
91
47
  margin: 0.5rem;
92
48
  display: flex;
@@ -0,0 +1,33 @@
1
+ export interface FormProps {
2
+ /**
3
+ * Disables all form controls and the submit button
4
+ */
5
+ disabled?: boolean;
6
+ /**
7
+ * Shows a loading state (e.g. during async submission)
8
+ */
9
+ loading?: boolean;
10
+ }
11
+ declare const __VLS_export: <T extends object>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
12
+ props: import("vue").PublicProps & __VLS_PrettifyLocal<(FormProps & {
13
+ modelValue?: T;
14
+ }) & {
15
+ "onUpdate:modelValue"?: ((value: T | undefined) => any) | undefined;
16
+ onSubmit?: (() => any) | undefined;
17
+ }> & (typeof globalThis extends {
18
+ __VLS_PROPS_FALLBACK: infer P;
19
+ } ? P : {});
20
+ expose: (exposed: {}) => void;
21
+ attrs: any;
22
+ slots: {};
23
+ emit: ((evt: "submit") => void) & ((event: "update:modelValue", value: T | undefined) => void);
24
+ }>) => import("vue").VNode & {
25
+ __ctx?: Awaited<typeof __VLS_setup>;
26
+ };
27
+ declare const _default: typeof __VLS_export;
28
+ export default _default;
29
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
30
+ [K in keyof T]: T[K];
31
+ } : {
32
+ [K in keyof T as K]: T[K];
33
+ }) & {};
@@ -0,0 +1,102 @@
1
+ <script setup>
2
+ import { useSlots, cloneVNode } from "vue";
3
+ const { disabled = false, loading = false } = defineProps({
4
+ disabled: { type: Boolean, required: false },
5
+ loading: { type: Boolean, required: false }
6
+ });
7
+ const modelValue = defineModel({ type: null });
8
+ const emit = defineEmits(["submit"]);
9
+ const slots = useSlots();
10
+ function getByPath(obj, path) {
11
+ return path.split(".").reduce((current, key) => current?.[key], obj);
12
+ }
13
+ function setByPath(obj, path, value) {
14
+ const keys = path.split(".");
15
+ const lastKey = keys.pop();
16
+ const parent = keys.reduce((current, key) => current?.[key], obj);
17
+ if (parent) parent[lastKey] = value;
18
+ }
19
+ function wrapSlotFn(fn) {
20
+ return (...args) => bindFields(fn(...args));
21
+ }
22
+ function processChildren(children) {
23
+ if (Array.isArray(children)) {
24
+ return bindFields(children);
25
+ }
26
+ if (typeof children === "function") {
27
+ return wrapSlotFn(children);
28
+ }
29
+ if (children && typeof children === "object") {
30
+ const slotObj = children;
31
+ return Object.fromEntries(
32
+ Object.entries(slotObj).map(([key, fn]) => [key, wrapSlotFn(fn)])
33
+ );
34
+ }
35
+ return children;
36
+ }
37
+ function bindFields(vnodes) {
38
+ return vnodes.map((vnode) => {
39
+ const name = vnode.props?.name;
40
+ if (name && modelValue.value && getByPath(modelValue.value, name) !== void 0) {
41
+ return cloneVNode(vnode, {
42
+ modelValue: getByPath(modelValue.value, name),
43
+ "onUpdate:modelValue": (newValue) => {
44
+ setByPath(modelValue.value, name, newValue);
45
+ }
46
+ });
47
+ }
48
+ if (vnode.children) {
49
+ const clone = cloneVNode(vnode);
50
+ clone.children = processChildren(vnode.children);
51
+ return clone;
52
+ }
53
+ return vnode;
54
+ });
55
+ }
56
+ function renderSlot() {
57
+ const children = slots.default?.();
58
+ if (!children || !modelValue.value) return children;
59
+ return bindFields(children);
60
+ }
61
+ const slotRenderer = () => renderSlot();
62
+ function onSubmit(e) {
63
+ e.preventDefault();
64
+ if (disabled || loading) return;
65
+ emit("submit");
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <form
71
+ :class="{ disabled, loading }"
72
+ :aria-disabled="disabled || void 0"
73
+ :aria-busy="loading || void 0"
74
+ novalidate
75
+ @submit="onSubmit"
76
+ >
77
+ <fieldset v-if="disabled" disabled>
78
+ <component :is="slotRenderer" />
79
+ </fieldset>
80
+ <component :is="slotRenderer" v-else />
81
+ </form>
82
+ </template>
83
+
84
+ <style scoped>
85
+ form {
86
+ display: flex;
87
+ flex-direction: column;
88
+ width: 100%;
89
+ }
90
+ form.disabled {
91
+ opacity: 0.6;
92
+ }
93
+ form.loading {
94
+ pointer-events: none;
95
+ }
96
+ form fieldset {
97
+ display: contents;
98
+ border: none;
99
+ padding: 0;
100
+ margin: 0;
101
+ }
102
+ </style>
@@ -0,0 +1,33 @@
1
+ export interface FormProps {
2
+ /**
3
+ * Disables all form controls and the submit button
4
+ */
5
+ disabled?: boolean;
6
+ /**
7
+ * Shows a loading state (e.g. during async submission)
8
+ */
9
+ loading?: boolean;
10
+ }
11
+ declare const __VLS_export: <T extends object>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
12
+ props: import("vue").PublicProps & __VLS_PrettifyLocal<(FormProps & {
13
+ modelValue?: T;
14
+ }) & {
15
+ "onUpdate:modelValue"?: ((value: T | undefined) => any) | undefined;
16
+ onSubmit?: (() => any) | undefined;
17
+ }> & (typeof globalThis extends {
18
+ __VLS_PROPS_FALLBACK: infer P;
19
+ } ? P : {});
20
+ expose: (exposed: {}) => void;
21
+ attrs: any;
22
+ slots: {};
23
+ emit: ((evt: "submit") => void) & ((event: "update:modelValue", value: T | undefined) => void);
24
+ }>) => import("vue").VNode & {
25
+ __ctx?: Awaited<typeof __VLS_setup>;
26
+ };
27
+ declare const _default: typeof __VLS_export;
28
+ export default _default;
29
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
30
+ [K in keyof T]: T[K];
31
+ } : {
32
+ [K in keyof T as K]: T[K];
33
+ }) & {};
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
- import { computed, ref, toRefs } from "vue";
2
+ import { computed, ref, toRef, toRefs } from "vue";
3
+ import { useControlTokens } from "../composables/useControlSize";
3
4
  import { useListKeyboard } from "../composables/useListKeyboard";
4
5
  const props = defineProps({
5
6
  options: { type: Array, required: true },
@@ -67,6 +68,7 @@ function getOptionKey(option) {
67
68
  if (typeof option === "string") return option;
68
69
  return String(option[key.value]);
69
70
  }
71
+ const { tokens: controlTokens } = useControlTokens(toRef(props, "size"));
70
72
  const controlProps = computed(() => {
71
73
  const {
72
74
  options: _options,
@@ -142,7 +144,7 @@ const {
142
144
  </template>
143
145
 
144
146
  <template #content="{ toggle }">
145
- <div class="selector-content">
147
+ <div class="selector-content" :style="controlTokens">
146
148
  <ul
147
149
  v-if="options.length"
148
150
  ref="listRef"
@@ -25,23 +25,23 @@ const carousel = useTemplateRef("carousel");
25
25
  const measureContainer = useTemplateRef("measureContainer");
26
26
  const { width: carouselWidth } = useElementSize(carousel);
27
27
  const { width: contentWidth, height: contentHeight } = useElementSize(measureContainer);
28
- const contentAspectRatio = computed(() => {
29
- if (!contentWidth.value || !contentHeight.value) return 1;
30
- return contentWidth.value / contentHeight.value;
31
- });
28
+ const measureWidth = computed(
29
+ () => isDynamicHeight.value ? `${rawSizes.value.width}px` : "max-content"
30
+ );
31
+ const measureHeight = computed(
32
+ () => isDynamicWidth.value ? `${rawSizes.value.height}px` : "max-content"
33
+ );
32
34
  const calculatedSize = computed(() => {
33
35
  const { width, height } = rawSizes.value;
34
36
  if (isDynamicHeight.value) {
35
- const dynamicHeight = contentAspectRatio.value ? width / contentAspectRatio.value : width;
36
37
  return {
37
38
  width: `${width}px`,
38
- height: `${dynamicHeight}px`
39
+ height: contentHeight.value ? `${contentHeight.value}px` : `${width}px`
39
40
  };
40
41
  }
41
42
  if (isDynamicWidth.value) {
42
- const dynamicWidth = contentAspectRatio.value ? height * contentAspectRatio.value : height;
43
43
  return {
44
- width: `${dynamicWidth}px`,
44
+ width: contentWidth.value ? `${contentWidth.value}px` : `${height}px`,
45
45
  height: `${height}px`
46
46
  };
47
47
  }
@@ -157,5 +157,5 @@ onMounted(() => {
157
157
  </template>
158
158
 
159
159
  <style scoped>
160
- .carousel-wrapper{display:block;overflow:hidden;position:relative}.carousel-measure{height:-moz-max-content;height:max-content;pointer-events:none;position:absolute;visibility:hidden;width:-moz-max-content;width:max-content}.carousel-measure img{display:block}.carousel{border:1px solid var(--color-border);border-radius:var(--border-radius-lg);height:v-bind("calculatedSize.height");max-height:v-bind(maxHeight);max-width:100%;overflow:hidden;transition:width .3s ease,height .3s ease;width:v-bind("calculatedSize.width")}.carousel-track{align-items:center;cursor:grab;display:flex;gap:.75rem;height:100%;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.carousel-track:active{cursor:grabbing}.carousel-item{background:var(--color-surface);border-radius:var(--border-radius-sm);color:var(--color-text);height:100%;inset:0;opacity:0;overflow:hidden;padding:.5rem .75rem;pointer-events:none;position:absolute;transition:opacity .5s ease-in-out,transform .5s ease-in-out;white-space:nowrap;width:100%}.carousel-item.previous-image{transform:translateX(-100%)}.carousel-item.next-image{transform:translateX(100%)}.carousel-item.active-image{opacity:1;pointer-events:auto;transform:translateX(0)}.carousel-item img{height:100%;-o-object-fit:v-bind(fit);object-fit:v-bind(fit);width:100%}.carousel-empty{color:var(--color-muted)}.switch-button{position:absolute}.switch-button :deep(button){background:transparent!important;border:none!important;color:transparent!important}.switch-button :deep(button:hover){color:transparent!important}.switch-button :deep(.orio-icon){color:#fff!important;fill:#fff!important;filter:drop-shadow(0 0 2px rgba(0,0,0,.8)) drop-shadow(0 0 4px rgba(0,0,0,.6))}@supports (mix-blend-mode:difference) and (not (-webkit-hyphens:none)){.switch-button :deep(.orio-icon){color:#000!important;fill:#000!important;filter:grayscale(1) contrast(9) invert(1) drop-shadow(0 0 1px black) drop-shadow(0 0 2px black);mix-blend-mode:difference}}.switch-button.previous-button{left:0}.switch-button.next-button{right:0}.carousel--minimal{background:none;border:none}.carousel--minimal .carousel-item{background:none}.carousel--minimal .switch-button{opacity:0;transition:opacity .2s ease}.carousel--minimal:hover .switch-button{opacity:1}
160
+ .carousel-wrapper{display:block;position:relative}.carousel-measure{height:v-bind(measureHeight);left:-9999px;pointer-events:none;position:fixed;visibility:hidden;width:v-bind(measureWidth)}.carousel-measure :deep(>*){display:block;height:100%;width:100%}.carousel{border:1px solid var(--color-border);border-radius:var(--border-radius-lg);height:v-bind("calculatedSize.height");max-height:v-bind(maxHeight);max-width:100%;overflow:hidden;transition:width .3s ease,height .3s ease;width:v-bind("calculatedSize.width")}.carousel-track{align-items:center;cursor:grab;display:flex;gap:.75rem;height:100%;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.carousel-track:active{cursor:grabbing}.carousel-item{background:var(--color-surface);border-radius:var(--border-radius-sm);color:var(--color-text);height:100%;inset:0;opacity:0;overflow:hidden;padding:.5rem .75rem;pointer-events:none;position:absolute;transition:opacity .5s ease-in-out,transform .5s ease-in-out;white-space:nowrap;width:100%}.carousel-item.previous-image{transform:translateX(-100%)}.carousel-item.next-image{transform:translateX(100%)}.carousel-item.active-image{opacity:1;pointer-events:auto;transform:translateX(0)}.carousel-item :deep(>*){height:100%;-o-object-fit:v-bind(fit);object-fit:v-bind(fit);width:100%}.carousel-empty{color:var(--color-muted)}.switch-button{position:absolute}.switch-button :deep(button){background:transparent!important;border:none!important;color:transparent!important}.switch-button :deep(button:hover){color:transparent!important}.switch-button :deep(.orio-icon){color:#fff!important;fill:#fff!important;filter:drop-shadow(0 0 2px rgba(0,0,0,.8)) drop-shadow(0 0 4px rgba(0,0,0,.6))}@supports (mix-blend-mode:difference) and (not (-webkit-hyphens:none)){.switch-button :deep(.orio-icon){color:#000!important;fill:#000!important;filter:grayscale(1) contrast(9) invert(1) drop-shadow(0 0 1px black) drop-shadow(0 0 2px black);mix-blend-mode:difference}}.switch-button.previous-button{left:0}.switch-button.next-button{right:0}.carousel--minimal{background:none;border:none}.carousel--minimal .carousel-item{background:none}.carousel--minimal .switch-button{opacity:0;transition:opacity .2s ease}.carousel--minimal:hover .switch-button{opacity:1}
161
161
  </style>
@@ -0,0 +1,9 @@
1
+ import { type ComputedRef, type Ref } from "vue";
2
+ import type { ControlSize } from "../components/ControlElement.vue.js";
3
+ declare const sizeTokens: Record<ControlSize, Record<string, string>>;
4
+ export declare function provideControlSize(size: Ref<ControlSize> | ComputedRef<ControlSize>): void;
5
+ export declare function useControlTokens(explicit?: Ref<ControlSize | undefined> | ComputedRef<ControlSize | undefined>, fallback?: ControlSize): {
6
+ size: ComputedRef<ControlSize>;
7
+ tokens: ComputedRef<Record<string, string>>;
8
+ };
9
+ export { sizeTokens };
@@ -0,0 +1,67 @@
1
+ import {
2
+ computed,
3
+ inject,
4
+ provide,
5
+ ref
6
+ } from "vue";
7
+ const CONTROL_SIZE_KEY = Symbol("control-size");
8
+ const sizeTokens = {
9
+ sm: {
10
+ "--control-font-size": "var(--font-sm)",
11
+ "--control-label-font-size": "var(--font-xs)",
12
+ "--control-py": "0.25rem",
13
+ "--control-px": "0.5rem",
14
+ "--control-gap": "0.25rem",
15
+ "--control-radius": "var(--border-radius-sm)",
16
+ "--control-icon-size": "0.75rem",
17
+ "--control-inner-block-start": "1rem",
18
+ "--control-inner-block-end": "0.1rem",
19
+ "--control-label-block-start": "0.2rem"
20
+ },
21
+ md: {
22
+ "--control-font-size": "var(--font-md)",
23
+ "--control-label-font-size": "var(--font-sm)",
24
+ "--control-py": "0.5rem",
25
+ "--control-px": "0.75rem",
26
+ "--control-gap": "0.5rem",
27
+ "--control-radius": "var(--border-radius-md)",
28
+ "--control-icon-size": "1rem",
29
+ "--control-inner-block-start": "1.25rem",
30
+ "--control-inner-block-end": "0.2rem",
31
+ "--control-label-block-start": "0.25rem"
32
+ },
33
+ lg: {
34
+ "--control-font-size": "var(--font-lg)",
35
+ "--control-label-font-size": "var(--font-md)",
36
+ "--control-py": "0.625rem",
37
+ "--control-px": "1rem",
38
+ "--control-gap": "0.5rem",
39
+ "--control-radius": "var(--border-radius-md)",
40
+ "--control-icon-size": "1.25rem",
41
+ "--control-inner-block-start": "1.1rem",
42
+ "--control-inner-block-end": "0.2rem",
43
+ "--control-label-block-start": "0.25rem"
44
+ },
45
+ xl: {
46
+ "--control-font-size": "var(--font-xl)",
47
+ "--control-label-font-size": "var(--font-lg)",
48
+ "--control-py": "0.75rem",
49
+ "--control-px": "1.25rem",
50
+ "--control-gap": "0.75rem",
51
+ "--control-radius": "var(--border-radius-lg)",
52
+ "--control-icon-size": "1.5rem",
53
+ "--control-inner-block-start": "1.5rem",
54
+ "--control-inner-block-end": "0",
55
+ "--control-label-block-start": "0.25rem"
56
+ }
57
+ };
58
+ export function provideControlSize(size) {
59
+ provide(CONTROL_SIZE_KEY, size);
60
+ }
61
+ export function useControlTokens(explicit, fallback = "md") {
62
+ const injected = inject(CONTROL_SIZE_KEY, ref(fallback));
63
+ const size = computed(() => explicit?.value ?? injected.value);
64
+ const tokens = computed(() => sizeTokens[size.value]);
65
+ return { size, tokens };
66
+ }
67
+ export { sizeTokens };
@@ -1,7 +1,9 @@
1
1
  export interface SoundOptions {
2
2
  src?: string;
3
3
  volume?: number;
4
+ prefetch?: boolean;
4
5
  }
5
6
  export declare function useSound(options?: SoundOptions): {
6
- play: () => void;
7
+ play: () => Promise<void>;
8
+ prefetch: () => void;
7
9
  };
@@ -1,13 +1,53 @@
1
1
  const DEFAULT_SOUND = "https://cdn.jsdelivr.net/gh/oriondor/orio-ui@main/docs/public/sounds/mechanical-switch.wav";
2
+ let audioContext = null;
3
+ const bufferCache = /* @__PURE__ */ new Map();
4
+ function getAudioContext() {
5
+ if (typeof window === "undefined") return null;
6
+ if (audioContext) return audioContext;
7
+ const ContextClass = window.AudioContext ?? window.webkitAudioContext;
8
+ if (!ContextClass) return null;
9
+ try {
10
+ audioContext = new ContextClass();
11
+ } catch (e) {
12
+ console.error("[useSound] Failed to create AudioContext:", e);
13
+ return null;
14
+ }
15
+ return audioContext;
16
+ }
17
+ function fetchBuffer(ctx, src) {
18
+ const cached = bufferCache.get(src);
19
+ if (cached) return cached;
20
+ const promise = fetch(src).then((response) => response.arrayBuffer()).then((arrayBuffer) => ctx.decodeAudioData(arrayBuffer)).catch((e) => {
21
+ console.error(`[useSound] Failed to load sound "${src}":`, e);
22
+ bufferCache.delete(src);
23
+ return null;
24
+ });
25
+ bufferCache.set(src, promise);
26
+ return promise;
27
+ }
2
28
  export function useSound(options = {}) {
3
- const { src = DEFAULT_SOUND, volume = 0.3 } = options;
4
- const audio = typeof window !== "undefined" ? new Audio(src) : null;
5
- const play = () => {
6
- if (!audio) return;
7
- audio.currentTime = 0;
8
- audio.volume = volume;
9
- audio.play().catch(() => {
10
- });
29
+ const { src = DEFAULT_SOUND, volume = 0.3, prefetch = false } = options;
30
+ const warmUp = () => {
31
+ const ctx = getAudioContext();
32
+ if (ctx) fetchBuffer(ctx, src);
33
+ };
34
+ if (prefetch) warmUp();
35
+ const play = async () => {
36
+ const ctx = getAudioContext();
37
+ if (!ctx) return;
38
+ if (ctx.state === "suspended") {
39
+ await ctx.resume().catch(() => {
40
+ });
41
+ }
42
+ const buffer = await fetchBuffer(ctx, src);
43
+ if (!buffer) return;
44
+ const source = ctx.createBufferSource();
45
+ source.buffer = buffer;
46
+ const gainNode = ctx.createGain();
47
+ gainNode.gain.value = volume;
48
+ source.connect(gainNode);
49
+ gainNode.connect(ctx.destination);
50
+ source.start(0);
11
51
  };
12
- return { play };
52
+ return { play, prefetch: warmUp };
13
53
  }
@@ -1,5 +1,6 @@
1
1
  export { default as AnimatedContainer } from "./components/AnimatedContainer.vue.js";
2
2
  export { default as Button } from "./components/Button.vue.js";
3
+ export { default as Form, type FormProps } from "./components/Form.vue.js";
3
4
  export { default as NavButton } from "./components/NavButton.vue.js";
4
5
  export { default as Input } from "./components/Input.vue.js";
5
6
  export { default as NumberInput } from "./components/NumberInput/index.vue.js";
@@ -1,5 +1,6 @@
1
1
  export { default as AnimatedContainer } from "./components/AnimatedContainer.vue";
2
2
  export { default as Button } from "./components/Button.vue";
3
+ export { default as Form } from "./components/Form.vue";
3
4
  export { default as NavButton } from "./components/NavButton.vue";
4
5
  export { default as Input } from "./components/Input.vue";
5
6
  export { default as NumberInput } from "./components/NumberInput/index.vue";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orio-ui",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "Modern Nuxt component library with theme support",
5
5
  "type": "module",
6
6
  "main": "./dist/module.mjs",