@anweb/nuxt-ancore 1.16.0 → 1.16.2

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
@@ -4,33 +4,51 @@
4
4
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
5
  [![Nuxt][nuxt-src]][nuxt-href]
6
6
 
7
- My new Nuxt module for doing amazing things.
7
+ A set of composables, components and utilities for Nuxt to handle common tasks: data fetching with SSR, paginated lists, form validation, dialogs, i18n and more.
8
8
 
9
- - [✨  Release Notes](/CHANGELOG.md)
10
-
11
-
12
- ## Quick Setup
13
-
14
- Install the module to your Nuxt application with one command:
9
+ ## Setup
15
10
 
16
11
  ```bash
17
12
  npm i @anweb/nuxt-ancore
18
13
  ```
19
14
 
20
- Then, add it to your nuxt.config.ts:
21
15
  ```ts
22
16
  export default defineNuxtConfig({
23
- modules: ['@anweb/nuxt-ancore']
17
+ modules: ['@anweb/nuxt-ancore']
24
18
  })
25
19
  ```
26
20
 
21
+ ## Composables
22
+
23
+ | Name | Description |
24
+ |---|---|
25
+ | `useAnData` | Data fetching with SSR/CSR support, URL params, query and auto-refresh |
26
+ | `useAnList` | Paginated lists with infinite scroll, filters and reverse mode |
27
+ | `useAnForm` | Form state, validation (async-validator), history and diff tracking |
28
+ | `useAnDialogs` | Dynamic dialogs: open, close, closeAll, swipe-to-dismiss |
29
+ | `useAnI18n` | i18next integration with per-component namespaces and language switching |
30
+
31
+ ## Components
32
+
33
+ | Name | Description |
34
+ |---|---|
35
+ | `AnTab` | Headless tab with lazy rendering and state preservation via `display: none` |
36
+ | `AnDropdown` | Dropdown with CSS Anchor Positioning, teleport and ARIA |
37
+
38
+ ## Utilities
39
+
40
+ | Name | Description |
41
+ |---|---|
42
+ | `toQuery` | Converts an object to a URL query string, filtering out null/undefined |
43
+ | `asyncInit` | SSR-aware init helper: awaits during SSR, fires without await on CSR |
44
+ | `coreApi` | Core API wrapper around `$fetch` with typed responses |
27
45
 
28
46
  <!-- Badges -->
29
- [npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=020420&colorB=00DC82
30
- [npm-version-href]: https://npmjs.com/package/my-module
47
+ [npm-version-src]: https://img.shields.io/npm/v/@anweb/nuxt-ancore/latest.svg?style=flat&colorA=020420&colorB=00DC82
48
+ [npm-version-href]: https://npmjs.com/package/@anweb/nuxt-ancore
31
49
 
32
- [npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=020420&colorB=00DC82
33
- [npm-downloads-href]: https://npm.chart.dev/my-module
50
+ [npm-downloads-src]: https://img.shields.io/npm/dm/@anweb/nuxt-ancore.svg?style=flat&colorA=020420&colorB=00DC82
51
+ [npm-downloads-href]: https://npm.chart.dev/@anweb/nuxt-ancore
34
52
 
35
53
  [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
36
54
  [nuxt-href]: https://nuxt.com
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "AnCore",
3
3
  "configKey": "ancore",
4
- "version": "1.16.0",
4
+ "version": "1.16.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,21 +1,42 @@
1
1
  <script setup>
2
- import { useScrollLock } from "@vueuse/core";
2
+ import { watch } from "vue";
3
3
  import { useAnDialogs } from "#imports";
4
4
  import { AnDialogsItem } from "#components";
5
5
  const Dialogs = useAnDialogs();
6
- const isLocked = useScrollLock(window);
7
- const onAnimation = () => {
8
- isLocked.value = !!Dialogs.items.length;
6
+ let scrollY = 0;
7
+ const lockScroll = () => {
8
+ scrollY = window.scrollY;
9
+ document.documentElement.style.overflowY = "scroll";
10
+ document.body.style.position = "fixed";
11
+ document.body.style.top = `-${scrollY}px`;
12
+ document.body.style.left = "0";
13
+ document.body.style.width = "100%";
9
14
  };
15
+ const unlockScroll = () => {
16
+ document.body.style.position = "";
17
+ document.body.style.top = "";
18
+ document.body.style.left = "";
19
+ document.body.style.width = "";
20
+ document.documentElement.style.overflowY = "";
21
+ window.scrollTo(0, scrollY);
22
+ };
23
+ if (import.meta.client) {
24
+ watch(
25
+ () => Dialogs.items.length,
26
+ (count, prev) => {
27
+ if (count > 0 && (!prev || prev === 0)) lockScroll();
28
+ else if (count === 0 && prev && prev > 0) unlockScroll();
29
+ }
30
+ );
31
+ }
10
32
  </script>
11
33
 
12
34
  <template>
13
35
  <transition-group
36
+ v-bind="$attrs"
14
37
  name="an-dialogs"
15
38
  tag="div"
16
-
17
- @before-enter="onAnimation"
18
- @after-leave="onAnimation"
39
+ aria-live="polite"
19
40
  >
20
41
  <AnDialogsItem
21
42
  v-for="dialog of Dialogs.items"
@@ -19,6 +19,7 @@ const top = ref(0);
19
19
  const active = ref(false);
20
20
  const target = ref(null);
21
21
  let raf = 0;
22
+ let mouseDownOnBackdrop = false;
22
23
  const onSwipeStart = () => {
23
24
  active.value = canSwipe.value;
24
25
  };
@@ -50,7 +51,9 @@ const canSwipe = computed(() => Scroll.arrivedState.top && !config.value.fullscr
50
51
  onMounted(() => {
51
52
  target.value = refDialog.value?.$el || null;
52
53
  if (!config.value.fullscreen) {
53
- onClickOutside(refDialog, () => Dialogs.close(props.dialog));
54
+ onClickOutside(refDialog, () => Dialogs.close(props.dialog), {
55
+ ignore: [".an-dialog"]
56
+ });
54
57
  }
55
58
  Swipe = useSwipe(target, {
56
59
  onSwipeStart,
@@ -65,8 +68,14 @@ onMounted(() => {
65
68
 
66
69
  <template>
67
70
  <div
71
+ v-bind="$attrs"
72
+ role="dialog"
73
+ aria-modal="true"
68
74
  class="an-dialog -flex -flex__column"
69
75
  :class="[{ '-fullscreen': config.fullscreen }, config.class]"
76
+ @mousedown.self="mouseDownOnBackdrop = true"
77
+ @click.self="mouseDownOnBackdrop && !config.fullscreen && Dialogs.close(props.dialog);
78
+ mouseDownOnBackdrop = false"
70
79
  >
71
80
  <component
72
81
  ref="refDialog"
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { v4 } from "uuid";
3
3
  import { computed, onMounted, ref, useTemplateRef } from "vue";
4
- import { onClickOutside } from "@vueuse/core";
4
+ import { onClickOutside, useEventListener } from "@vueuse/core";
5
5
  const props = defineProps({
6
6
  area: { type: String, required: false }
7
7
  });
@@ -14,24 +14,41 @@ const toggle = (value) => {
14
14
  const close = () => {
15
15
  toggle(false);
16
16
  };
17
+ const onKeydown = (e) => {
18
+ if (e.key === "Escape" && state.value) close();
19
+ };
17
20
  const area = computed(() => {
18
21
  return props.area || "bottom span-right";
19
22
  });
20
23
  const name = computed(() => {
21
24
  return id.value ? `--an-dropdown-${id.value}` : "";
22
25
  });
26
+ const menuId = computed(() => id.value ? `an-dropdown-menu-${id.value}` : void 0);
23
27
  onMounted(() => {
24
28
  id.value = v4();
25
29
  });
26
30
  onClickOutside(refTarget, close);
31
+ useEventListener(document, "keydown", onKeydown);
32
+ defineExpose({ toggle, close });
27
33
  </script>
28
34
 
29
35
  <template>
30
- <div ref="refTarget" class="an-dropdown__button">
31
- <slot name="button" :toggle="toggle" />
36
+ <div ref="refTarget" class="an-dropdown__button" v-bind="$attrs">
37
+ <div
38
+ :aria-expanded="state"
39
+ :aria-controls="menuId"
40
+ aria-haspopup="true"
41
+ >
42
+ <slot name="button" :toggle="toggle" />
43
+ </div>
32
44
 
33
45
  <teleport to="body">
34
- <div v-show="state" class="an-dropdown__menu">
46
+ <div
47
+ v-show="state"
48
+ :id="menuId"
49
+ role="menu"
50
+ class="an-dropdown__menu"
51
+ >
35
52
  <slot name="menu" :close="close" />
36
53
  </div>
37
54
  </teleport>
@@ -30,7 +30,7 @@ const Tab = defineComponent({
30
30
  display: "none"
31
31
  }
32
32
  },
33
- vnode.children
33
+ vnode.children ?? void 0
34
34
  )
35
35
  );
36
36
  };
@@ -12,10 +12,10 @@ interface TUseAnData<TData, TError> {
12
12
  refresh: () => void;
13
13
  config: Ref<TConfig>;
14
14
  params: TConfig['params'];
15
- data: ComputedRef<TData | undefined>;
16
- status: ComputedRef<AsyncDataRequestStatus>;
15
+ data: Readonly<Ref<TData | undefined>>;
16
+ status: Readonly<Ref<AsyncDataRequestStatus>>;
17
17
  loading: ComputedRef<boolean>;
18
- error: ComputedRef<TError | undefined>;
18
+ error: Readonly<Ref<TError | undefined>>;
19
19
  }
20
- export declare const useAnData: <TData = unknown, TError = unknown>(initConfig: Omit<TConfig, "params"> & Partial<TConfig["params"]>) => TUseAnData<TData, TError>;
20
+ export declare const useAnData: <TData = unknown, TError = unknown>(initConfig: Omit<TConfig, "params"> & Partial<Pick<TConfig, "params">>) => TUseAnData<TData, TError>;
21
21
  export {};
@@ -8,14 +8,15 @@ export const useAnData = (initConfig) => {
8
8
  const status = ref("idle");
9
9
  const isMounted = ref(false);
10
10
  const time = ref(0);
11
+ const fetchData = () => userApi(
12
+ path.value.url,
13
+ { method: "GET", ...config.value.apiConfig || {} }
14
+ );
11
15
  const init = async () => {
12
16
  if (isMounted.value) {
13
17
  const execute = () => {
14
18
  status.value = "pending";
15
- userApi(
16
- path.value.url,
17
- { method: "GET", ...config.value.apiConfig || {} }
18
- ).then((response) => {
19
+ fetchData().then((response) => {
19
20
  data.value = response;
20
21
  nextTick().then(() => {
21
22
  status.value = "success";
@@ -25,14 +26,11 @@ export const useAnData = (initConfig) => {
25
26
  error.value = e;
26
27
  });
27
28
  };
28
- watch(() => key.value, execute, { immediate: true, deep: true });
29
+ watch(() => key.value, execute, { immediate: true });
29
30
  } else {
30
31
  const Data = useAsyncData(
31
32
  key,
32
- () => userApi(
33
- path.value.url,
34
- { method: "GET", ...config.value.apiConfig || {} }
35
- ),
33
+ () => fetchData(),
36
34
  { immediate: false }
37
35
  );
38
36
  await Data.execute();
@@ -1,6 +1,7 @@
1
+ import { type Component } from 'vue';
1
2
  import type { TDialog } from '#ancore/types';
2
3
  export declare const useAnDialogs: () => {
3
- open: (component: any, params?: Record<string, unknown>, config?: Partial<TDialog>) => TDialog;
4
+ open: (component: Component, params?: Record<string, unknown>, config?: Partial<TDialog>) => TDialog;
4
5
  close: (dialog: TDialog) => void;
5
6
  closeAll: () => void;
6
7
  items: TDialog[];
@@ -14,8 +14,10 @@ export const useAnDialogs = () => {
14
14
  return data;
15
15
  };
16
16
  const close = (dialog) => {
17
- StateDialogs.value.splice(StateDialogs.value.indexOf(dialog), 1);
18
- if (dialog.onClose) dialog.onClose();
17
+ const index = StateDialogs.value.indexOf(dialog);
18
+ if (index === -1) return;
19
+ StateDialogs.value.splice(index, 1);
20
+ dialog.onClose?.();
19
21
  };
20
22
  const closeAll = () => {
21
23
  while (StateDialogs.value.length) {
@@ -4,7 +4,7 @@ interface TStructureItem<TData> {
4
4
  default: TData;
5
5
  rules?: RuleItem[];
6
6
  }
7
- export declare const useAnForm: <TForm extends object, TData extends Partial<TForm> | null | (Record<string, any> & Partial<TForm>) = Partial<TForm> | null>(params: Record<keyof TForm, TStructureItem<TForm[keyof TForm]>>, data?: TData) => {
7
+ export declare const useAnForm: <TForm extends object, TData extends Partial<TForm> | null | (Record<string, unknown> & Partial<TForm>) = Partial<TForm> | null>(params: Record<keyof TForm, TStructureItem<TForm[keyof TForm]>>, data?: TData) => {
8
8
  state: [TForm] extends [import("vue").Ref<any, any>] ? import("@vue/shared").IfAny<TForm, import("vue").Ref<TForm, TForm>, TForm> : import("vue").Ref<import("vue").UnwrapRef<TForm>, TForm | import("vue").UnwrapRef<TForm>>;
9
9
  diff: import("vue").ComputedRef<Partial<TForm>>;
10
10
  merge: (data: Partial<TForm>, commit?: true) => void;
@@ -1,4 +1,4 @@
1
- export declare const useAnI18n: (resources?: Record<string, any>) => {
1
+ export declare const useAnI18n: (resources?: Record<string, Record<string, string>>) => {
2
2
  lang: string;
3
3
  set: (lang: string) => void;
4
4
  t: import("i18next").TFunction<["translation", ...string[]], undefined>;
@@ -4,23 +4,33 @@ export const useAnI18n = (resources) => {
4
4
  const returnObj = {
5
5
  lang: i18next.language,
6
6
  set: (lang) => {
7
- const config = useRuntimeConfig().public.i18n;
8
- if (config?.cookie) {
9
- const cookie = useCookie(config?.cookie, {
10
- maxAge: 60 * 60 * 24 * 365,
11
- path: "/",
12
- sameSite: "lax",
13
- secure: true
14
- });
15
- cookie.value = lang;
7
+ try {
8
+ i18next.changeLanguage(lang);
9
+ returnObj.lang = lang;
10
+ const config = useRuntimeConfig().public.i18n;
11
+ if (config?.cookie) {
12
+ const cookie = useCookie(config?.cookie, {
13
+ maxAge: 60 * 60 * 24 * 365,
14
+ path: "/",
15
+ sameSite: "lax",
16
+ secure: true
17
+ });
18
+ cookie.value = lang;
19
+ }
20
+ } catch (e) {
21
+ console.error("[AnI18n] Failed to set language:", e);
16
22
  }
17
23
  }
18
24
  };
19
25
  if (!resources) return { t: i18next.t, ...returnObj };
20
26
  const ns = JSON.stringify(resources);
21
27
  for (const lang in resources) {
22
- if (!i18next.hasResourceBundle(lang, ns)) {
23
- i18next.addResourceBundle(lang, ns, resources[lang]);
28
+ try {
29
+ if (!i18next.hasResourceBundle(lang, ns)) {
30
+ i18next.addResourceBundle(lang, ns, resources[lang]);
31
+ }
32
+ } catch (e) {
33
+ console.error(`[AnI18n] Failed to add resource bundle for ${lang}:`, e);
24
34
  }
25
35
  }
26
36
  const t = (key, options = {}) => i18next.t(key, { ns: [ns, "translation"], ...options });
@@ -1,5 +1,7 @@
1
1
  import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack';
2
+ import { type Ref, type ComputedRef } from 'vue';
2
3
  import type { TInfiniteScroll } from '#ancore/types';
4
+ import { type AsyncDataRequestStatus } from '#app';
3
5
  interface TConfig<TFilter> {
4
6
  request: NitroFetchRequest;
5
7
  apiConfig?: NitroFetchOptions<string>;
@@ -8,16 +10,17 @@ interface TConfig<TFilter> {
8
10
  skipField?: string;
9
11
  reverse?: boolean;
10
12
  }
11
- export declare const useAnList: <TData, TFilter extends object = {}>(initConfig: TConfig<TFilter>) => {
13
+ interface TUseAnList<TData, TFilter> {
12
14
  init: () => Promise<void>;
13
15
  infiniteScroll: (scrollConfig?: TInfiniteScroll) => () => void;
14
16
  filter: TFilter;
15
17
  params: Record<string, string> | undefined;
16
18
  items: TData[];
17
- count: import("vue").Ref<number | null, number | null>;
18
- inited: import("vue").ComputedRef<boolean>;
19
- status: import("vue").ComputedRef<import("nuxt/app").AsyncDataRequestStatus>;
20
- loading: import("vue").ComputedRef<boolean>;
21
- error: import("vue").ComputedRef<unknown>;
22
- };
19
+ count: Ref<number | null>;
20
+ inited: ComputedRef<boolean>;
21
+ status: Readonly<Ref<AsyncDataRequestStatus>>;
22
+ loading: ComputedRef<boolean>;
23
+ error: Readonly<Ref<unknown | undefined>>;
24
+ }
25
+ export declare const useAnList: <TData, TFilter extends object = {}>(initConfig: TConfig<TFilter>) => TUseAnList<TData, TFilter>;
23
26
  export {};
@@ -12,7 +12,7 @@ export const useAnList = (initConfig) => {
12
12
  const refresh = () => {
13
13
  if (!data.data.value) return;
14
14
  if (!config.value.apiConfig?.query?.[config.value.skipField || "skip"]) {
15
- setCount(null);
15
+ count.value = null;
16
16
  items.length = 0;
17
17
  }
18
18
  if (config.value.reverse) {
@@ -20,10 +20,7 @@ export const useAnList = (initConfig) => {
20
20
  } else {
21
21
  items.push(...data.data.value.items);
22
22
  }
23
- setCount(data.data.value.count);
24
- };
25
- const setCount = (value) => {
26
- count.value = value;
23
+ count.value = data.data.value.count;
27
24
  };
28
25
  const infiniteScroll = (scrollConfig) => {
29
26
  const onLoadMore = scrollConfig?.onLoadMore || (() => {
@@ -31,8 +28,7 @@ export const useAnList = (initConfig) => {
31
28
  config.value.filter[config.value.skipField || "skip"] = items.length;
32
29
  });
33
30
  const canLoadMore = scrollConfig?.options?.canLoadMore || (() => {
34
- return (scrollConfig?.canLoadMore?.() ?? true) && inited.value && data.status.value !== "pending" && items.length < (count.value || 0) && // @ts-ignore
35
- !!config.value.filter.limit;
31
+ return (scrollConfig?.canLoadMore?.() ?? true) && inited.value && data.status.value !== "pending" && items.length < (count.value || 0) && !!config.value.filter?.limit;
36
32
  });
37
33
  const { reset } = useInfiniteScroll(
38
34
  scrollConfig?.element || window,
@@ -1,7 +1,7 @@
1
- import type { Component } from '@nuxt/schema';
1
+ import type { Component } from 'vue';
2
2
  export interface TDialog {
3
3
  id: string;
4
- component: Component | Function;
4
+ component: Component;
5
5
  params: Record<string, unknown>;
6
6
  fullscreen?: boolean;
7
7
  class?: string;
@@ -14,7 +14,7 @@ declare module 'nuxt/schema' {
14
14
  interface PublicRuntimeConfig {
15
15
  i18n?: InitOptions<unknown> & {
16
16
  cookie?: string
17
- resources?: Record<string, { translation: any }>
17
+ resources?: Record<string, { translation: string | Record<string, unknown> }>
18
18
  }
19
19
  }
20
20
  }
@@ -1,4 +1,4 @@
1
- import type { TArgument } from '#ancore/types';
1
+ import type { TArgument } from './argument.js';
2
2
  import type { useInfiniteScroll } from '@vueuse/core';
3
3
  export interface TInfiniteScroll {
4
4
  element?: TArgument<typeof useInfiniteScroll, 0>;
@@ -1,10 +1,3 @@
1
1
  export const coreApi = (request, opts) => {
2
- try {
3
- return $fetch(
4
- request,
5
- { ...opts }
6
- );
7
- } catch (error) {
8
- throw error;
9
- }
2
+ return $fetch(request, { ...opts });
10
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anweb/nuxt-ancore",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "description": "AnCore Nuxt module",
5
5
  "repository": "https://github.com/ANLTD/ancore",
6
6
  "license": "MIT",
@@ -27,25 +27,26 @@
27
27
  "dev": "npm run dev:prepare && nuxi dev playground",
28
28
  "dev:build": "nuxi build playground",
29
29
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
30
- "release:patch": "npm run prepack && changelogen --patch --release && npm publish --access public && git push --follow-tags",
31
- "release:minor": "npm run prepack && changelogen --minor --release && npm publish --access public && git push --follow-tags",
32
- "release:major": "npm run prepack && changelogen --major --release && npm publish --access public && git push --follow-tags"
30
+ "release:patch": "npm run prepack && changelogen --patch --release && npm login && npm publish --access public && git push --follow-tags",
31
+ "release:minor": "npm run prepack && changelogen --minor --release && npm login && npm publish --access public && git push --follow-tags",
32
+ "release:major": "npm run prepack && changelogen --major --release && npm login && npm publish --access public && git push --follow-tags",
33
+ "update": "npx -y npm-check-updates -u"
33
34
  },
34
35
  "dependencies": {
35
- "@vueuse/core": "^14.2.0",
36
- "@vueuse/integrations": "^14.2.0",
36
+ "@vueuse/core": "^14.2.1",
37
+ "@vueuse/integrations": "^14.2.1",
37
38
  "async-validator": "^4.2.5",
38
- "i18next": "^25.8.0",
39
+ "i18next": "^25.8.10",
39
40
  "uuid": "^13.0.0"
40
41
  },
41
42
  "devDependencies": {
42
- "@nuxt/devtools": "^3.1.1",
43
- "@nuxt/kit": "^4.3.0",
43
+ "@nuxt/devtools": "^3.2.1",
44
+ "@nuxt/kit": "^4.3.1",
44
45
  "@nuxt/module-builder": "^1.0.2",
45
- "@nuxt/schema": "^4.3.0",
46
+ "@nuxt/schema": "^4.3.1",
46
47
  "@types/node": "latest",
47
48
  "changelogen": "^0.6.2",
48
- "nuxt": "^4.3.0",
49
+ "nuxt": "^4.3.1",
49
50
  "typescript": "~5.9.3"
50
51
  }
51
52
  }