@alikhalilll/a-skeleton 1.0.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/LICENSE +21 -0
- package/README.md +324 -0
- package/dist/index.cjs +4513 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +351 -0
- package/dist/index.d.ts +351 -0
- package/dist/index.js +4501 -0
- package/dist/index.js.map +1 -0
- package/dist/nuxt/index.cjs +30 -0
- package/dist/nuxt/index.cjs.map +1 -0
- package/dist/nuxt/index.d.cts +25 -0
- package/dist/nuxt/index.d.ts +25 -0
- package/dist/nuxt/index.js +30 -0
- package/dist/nuxt/index.js.map +1 -0
- package/dist/resolver/index.cjs +25 -0
- package/dist/resolver/index.cjs.map +1 -0
- package/dist/resolver/index.d.cts +21 -0
- package/dist/resolver/index.d.ts +21 -0
- package/dist/resolver/index.js +25 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/styles.css +35 -0
- package/package.json +120 -0
- package/src/components/ASkeleton.vue +167 -0
- package/src/components/ASkeletonBlock.vue +74 -0
- package/src/components/ASkeletonLayer.vue +52 -0
- package/src/components/StructuralSkeleton.ts +41 -0
- package/src/composables/useShapeProbe.ts +108 -0
- package/src/composables/useSkeleton.ts +112 -0
- package/src/composables/useSkeletonCache.ts +141 -0
- package/src/index.ts +32 -0
- package/src/nuxt/index.ts +47 -0
- package/src/resolver/index.ts +36 -0
- package/src/types.ts +108 -0
- package/src/utils/buildStructuralSkeleton.ts +261 -0
- package/src/utils/fingerprint.ts +42 -0
- package/src/utils/walkDom.ts +226 -0
- package/web-types.json +172 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NuxtModule } from "@nuxt/schema";
|
|
2
|
+
|
|
3
|
+
//#region src/nuxt/index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@alikhalilll/a-skeleton/nuxt` — registers the skeleton components for Nuxt
|
|
6
|
+
* auto-import. The auto-imported names are the package's native names:
|
|
7
|
+
*
|
|
8
|
+
* <ASkeleton :loading>…</ASkeleton> <!-- slot wrapper, two-layer flow -->
|
|
9
|
+
* <ASkeletonLayer :shape="…" /> <!-- replay a CachedShape directly -->
|
|
10
|
+
* <ASkeletonBlock type="circle" w="64" h="64" /> <!-- hand-crafted skeleton primitive -->
|
|
11
|
+
*
|
|
12
|
+
* modules: ['@alikhalilll/a-skeleton/nuxt'],
|
|
13
|
+
* aSkeleton: { prefix: 'A' }, // optional
|
|
14
|
+
*
|
|
15
|
+
* Styles are not auto-injected; add `'@alikhalilll/a-skeleton/styles.css'` to
|
|
16
|
+
* your Nuxt `css` array.
|
|
17
|
+
*/
|
|
18
|
+
interface ModuleOptions {
|
|
19
|
+
/** Optional prefix prepended to the registered component name. */
|
|
20
|
+
prefix?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const module: NuxtModule<ModuleOptions>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ModuleOptions, module as default };
|
|
25
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NuxtModule } from "@nuxt/schema";
|
|
2
|
+
|
|
3
|
+
//#region src/nuxt/index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@alikhalilll/a-skeleton/nuxt` — registers the skeleton components for Nuxt
|
|
6
|
+
* auto-import. The auto-imported names are the package's native names:
|
|
7
|
+
*
|
|
8
|
+
* <ASkeleton :loading>…</ASkeleton> <!-- slot wrapper, two-layer flow -->
|
|
9
|
+
* <ASkeletonLayer :shape="…" /> <!-- replay a CachedShape directly -->
|
|
10
|
+
* <ASkeletonBlock type="circle" w="64" h="64" /> <!-- hand-crafted skeleton primitive -->
|
|
11
|
+
*
|
|
12
|
+
* modules: ['@alikhalilll/a-skeleton/nuxt'],
|
|
13
|
+
* aSkeleton: { prefix: 'A' }, // optional
|
|
14
|
+
*
|
|
15
|
+
* Styles are not auto-injected; add `'@alikhalilll/a-skeleton/styles.css'` to
|
|
16
|
+
* your Nuxt `css` array.
|
|
17
|
+
*/
|
|
18
|
+
interface ModuleOptions {
|
|
19
|
+
/** Optional prefix prepended to the registered component name. */
|
|
20
|
+
prefix?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const module: NuxtModule<ModuleOptions>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ModuleOptions, module as default };
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { addComponent, defineNuxtModule } from "@nuxt/kit";
|
|
2
|
+
//#region src/nuxt/index.ts
|
|
3
|
+
const COMPONENTS = {
|
|
4
|
+
ASkeleton: "@alikhalilll/a-skeleton",
|
|
5
|
+
ASkeletonLayer: "@alikhalilll/a-skeleton",
|
|
6
|
+
ASkeletonBlock: "@alikhalilll/a-skeleton"
|
|
7
|
+
};
|
|
8
|
+
const module = defineNuxtModule({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "@alikhalilll/a-skeleton",
|
|
11
|
+
configKey: "aSkeleton",
|
|
12
|
+
compatibility: { nuxt: ">=3.0.0" }
|
|
13
|
+
},
|
|
14
|
+
defaults: { prefix: "" },
|
|
15
|
+
setup(opts) {
|
|
16
|
+
const prefix = opts.prefix ?? "";
|
|
17
|
+
for (const [exportName, from] of Object.entries(COMPONENTS)) {
|
|
18
|
+
const baseName = exportName.startsWith("A") ? exportName.slice(1) : exportName;
|
|
19
|
+
addComponent({
|
|
20
|
+
name: `${prefix}${prefix ? baseName : exportName}`,
|
|
21
|
+
export: exportName,
|
|
22
|
+
filePath: from
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
//#endregion
|
|
28
|
+
export { module as default };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/nuxt/index.ts"],"sourcesContent":["import { defineNuxtModule, addComponent } from '@nuxt/kit';\nimport type { NuxtModule } from '@nuxt/schema';\n\n/**\n * `@alikhalilll/a-skeleton/nuxt` — registers the skeleton components for Nuxt\n * auto-import. The auto-imported names are the package's native names:\n *\n * <ASkeleton :loading>…</ASkeleton> <!-- slot wrapper, two-layer flow -->\n * <ASkeletonLayer :shape=\"…\" /> <!-- replay a CachedShape directly -->\n * <ASkeletonBlock type=\"circle\" w=\"64\" h=\"64\" /> <!-- hand-crafted skeleton primitive -->\n *\n * modules: ['@alikhalilll/a-skeleton/nuxt'],\n * aSkeleton: { prefix: 'A' }, // optional\n *\n * Styles are not auto-injected; add `'@alikhalilll/a-skeleton/styles.css'` to\n * your Nuxt `css` array.\n */\n\nexport interface ModuleOptions {\n /** Optional prefix prepended to the registered component name. */\n prefix?: string;\n}\n\nconst COMPONENTS: Record<string, string> = {\n ASkeleton: '@alikhalilll/a-skeleton',\n ASkeletonLayer: '@alikhalilll/a-skeleton',\n ASkeletonBlock: '@alikhalilll/a-skeleton',\n};\n\nconst module: NuxtModule<ModuleOptions> = defineNuxtModule<ModuleOptions>({\n meta: {\n name: '@alikhalilll/a-skeleton',\n configKey: 'aSkeleton',\n compatibility: { nuxt: '>=3.0.0' },\n },\n defaults: { prefix: '' },\n setup(opts) {\n const prefix = opts.prefix ?? '';\n for (const [exportName, from] of Object.entries(COMPONENTS)) {\n const baseName = exportName.startsWith('A') ? exportName.slice(1) : exportName;\n const registeredName = `${prefix}${prefix ? baseName : exportName}`;\n addComponent({ name: registeredName, export: exportName, filePath: from });\n }\n },\n});\n\nexport default module;\n"],"mappings":";;AAuBA,MAAM,aAAqC;CACzC,WAAW;CACX,gBAAgB;CAChB,gBAAgB;AAClB;AAEA,MAAM,SAAoC,iBAAgC;CACxE,MAAM;EACJ,MAAM;EACN,WAAW;EACX,eAAe,EAAE,MAAM,UAAU;CACnC;CACA,UAAU,EAAE,QAAQ,GAAG;CACvB,MAAM,MAAM;EACV,MAAM,SAAS,KAAK,UAAU;EAC9B,KAAK,MAAM,CAAC,YAAY,SAAS,OAAO,QAAQ,UAAU,GAAG;GAC3D,MAAM,WAAW,WAAW,WAAW,GAAG,IAAI,WAAW,MAAM,CAAC,IAAI;GAEpE,aAAa;IAAE,MAAM,GADK,SAAS,SAAS,WAAW;IAClB,QAAQ;IAAY,UAAU;GAAK,CAAC;EAC3E;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/resolver/index.ts
|
|
2
|
+
const COMPONENT_TO_ENTRY = {
|
|
3
|
+
ASkeleton: "@alikhalilll/a-skeleton",
|
|
4
|
+
ASkeletonLayer: "@alikhalilll/a-skeleton",
|
|
5
|
+
ASkeletonBlock: "@alikhalilll/a-skeleton"
|
|
6
|
+
};
|
|
7
|
+
function ASkeletonResolver(opts = {}) {
|
|
8
|
+
const prefix = opts.prefix ?? "";
|
|
9
|
+
return {
|
|
10
|
+
type: "component",
|
|
11
|
+
resolve(name) {
|
|
12
|
+
const bare = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
13
|
+
const from = COMPONENT_TO_ENTRY[bare];
|
|
14
|
+
if (!from) return;
|
|
15
|
+
return {
|
|
16
|
+
name: bare,
|
|
17
|
+
from
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
module.exports = ASkeletonResolver;
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/resolver/index.ts"],"sourcesContent":["import type { ComponentResolver } from 'unplugin-vue-components';\n\n/**\n * `@alikhalilll/a-skeleton/resolver` — auto-import resolver for non-Nuxt\n * Vite/Webpack consumers via `unplugin-vue-components`.\n *\n * import Components from 'unplugin-vue-components/vite';\n * import ASkeletonResolver from '@alikhalilll/a-skeleton/resolver';\n * export default { plugins: [Components({ resolvers: [ASkeletonResolver()] })] };\n *\n * Registers `<ASkeleton>`, `<ASkeletonLayer>`, `<ASkeletonBlock>`.\n */\n\nexport interface ResolverOptions {\n /** Optional prefix the consumer uses (e.g. `'A'`). Defaults to the native `A*` names. */\n prefix?: string;\n}\n\nconst COMPONENT_TO_ENTRY: Record<string, string> = {\n ASkeleton: '@alikhalilll/a-skeleton',\n ASkeletonLayer: '@alikhalilll/a-skeleton',\n ASkeletonBlock: '@alikhalilll/a-skeleton',\n};\n\nexport default function ASkeletonResolver(opts: ResolverOptions = {}): ComponentResolver {\n const prefix = opts.prefix ?? '';\n return {\n type: 'component',\n resolve(name) {\n const bare = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;\n const from = COMPONENT_TO_ENTRY[bare];\n if (!from) return;\n return { name: bare, from };\n },\n };\n}\n"],"mappings":";AAkBA,MAAM,qBAA6C;CACjD,WAAW;CACX,gBAAgB;CAChB,gBAAgB;AAClB;AAEA,SAAwB,kBAAkB,OAAwB,CAAC,GAAsB;CACvF,MAAM,SAAS,KAAK,UAAU;CAC9B,OAAO;EACL,MAAM;EACN,QAAQ,MAAM;GACZ,MAAM,OAAO,UAAU,KAAK,WAAW,MAAM,IAAI,KAAK,MAAM,OAAO,MAAM,IAAI;GAC7E,MAAM,OAAO,mBAAmB;GAChC,IAAI,CAAC,MAAM;GACX,OAAO;IAAE,MAAM;IAAM;GAAK;EAC5B;CACF;AACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ComponentResolver } from "unplugin-vue-components";
|
|
2
|
+
|
|
3
|
+
//#region src/resolver/index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@alikhalilll/a-skeleton/resolver` — auto-import resolver for non-Nuxt
|
|
6
|
+
* Vite/Webpack consumers via `unplugin-vue-components`.
|
|
7
|
+
*
|
|
8
|
+
* import Components from 'unplugin-vue-components/vite';
|
|
9
|
+
* import ASkeletonResolver from '@alikhalilll/a-skeleton/resolver';
|
|
10
|
+
* export default { plugins: [Components({ resolvers: [ASkeletonResolver()] })] };
|
|
11
|
+
*
|
|
12
|
+
* Registers `<ASkeleton>`, `<ASkeletonLayer>`, `<ASkeletonBlock>`.
|
|
13
|
+
*/
|
|
14
|
+
interface ResolverOptions {
|
|
15
|
+
/** Optional prefix the consumer uses (e.g. `'A'`). Defaults to the native `A*` names. */
|
|
16
|
+
prefix?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function ASkeletonResolver(opts?: ResolverOptions): ComponentResolver;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ResolverOptions, ASkeletonResolver as default };
|
|
21
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ComponentResolver } from "unplugin-vue-components";
|
|
2
|
+
|
|
3
|
+
//#region src/resolver/index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@alikhalilll/a-skeleton/resolver` — auto-import resolver for non-Nuxt
|
|
6
|
+
* Vite/Webpack consumers via `unplugin-vue-components`.
|
|
7
|
+
*
|
|
8
|
+
* import Components from 'unplugin-vue-components/vite';
|
|
9
|
+
* import ASkeletonResolver from '@alikhalilll/a-skeleton/resolver';
|
|
10
|
+
* export default { plugins: [Components({ resolvers: [ASkeletonResolver()] })] };
|
|
11
|
+
*
|
|
12
|
+
* Registers `<ASkeleton>`, `<ASkeletonLayer>`, `<ASkeletonBlock>`.
|
|
13
|
+
*/
|
|
14
|
+
interface ResolverOptions {
|
|
15
|
+
/** Optional prefix the consumer uses (e.g. `'A'`). Defaults to the native `A*` names. */
|
|
16
|
+
prefix?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function ASkeletonResolver(opts?: ResolverOptions): ComponentResolver;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ResolverOptions, ASkeletonResolver as default };
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/resolver/index.ts
|
|
2
|
+
const COMPONENT_TO_ENTRY = {
|
|
3
|
+
ASkeleton: "@alikhalilll/a-skeleton",
|
|
4
|
+
ASkeletonLayer: "@alikhalilll/a-skeleton",
|
|
5
|
+
ASkeletonBlock: "@alikhalilll/a-skeleton"
|
|
6
|
+
};
|
|
7
|
+
function ASkeletonResolver(opts = {}) {
|
|
8
|
+
const prefix = opts.prefix ?? "";
|
|
9
|
+
return {
|
|
10
|
+
type: "component",
|
|
11
|
+
resolve(name) {
|
|
12
|
+
const bare = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
13
|
+
const from = COMPONENT_TO_ENTRY[bare];
|
|
14
|
+
if (!from) return;
|
|
15
|
+
return {
|
|
16
|
+
name: bare,
|
|
17
|
+
from
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ASkeletonResolver as default };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/resolver/index.ts"],"sourcesContent":["import type { ComponentResolver } from 'unplugin-vue-components';\n\n/**\n * `@alikhalilll/a-skeleton/resolver` — auto-import resolver for non-Nuxt\n * Vite/Webpack consumers via `unplugin-vue-components`.\n *\n * import Components from 'unplugin-vue-components/vite';\n * import ASkeletonResolver from '@alikhalilll/a-skeleton/resolver';\n * export default { plugins: [Components({ resolvers: [ASkeletonResolver()] })] };\n *\n * Registers `<ASkeleton>`, `<ASkeletonLayer>`, `<ASkeletonBlock>`.\n */\n\nexport interface ResolverOptions {\n /** Optional prefix the consumer uses (e.g. `'A'`). Defaults to the native `A*` names. */\n prefix?: string;\n}\n\nconst COMPONENT_TO_ENTRY: Record<string, string> = {\n ASkeleton: '@alikhalilll/a-skeleton',\n ASkeletonLayer: '@alikhalilll/a-skeleton',\n ASkeletonBlock: '@alikhalilll/a-skeleton',\n};\n\nexport default function ASkeletonResolver(opts: ResolverOptions = {}): ComponentResolver {\n const prefix = opts.prefix ?? '';\n return {\n type: 'component',\n resolve(name) {\n const bare = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;\n const from = COMPONENT_TO_ENTRY[bare];\n if (!from) return;\n return { name: bare, from };\n },\n };\n}\n"],"mappings":";AAkBA,MAAM,qBAA6C;CACjD,WAAW;CACX,gBAAgB;CAChB,gBAAgB;AAClB;AAEA,SAAwB,kBAAkB,OAAwB,CAAC,GAAsB;CACvF,MAAM,SAAS,KAAK,UAAU;CAC9B,OAAO;EACL,MAAM;EACN,QAAQ,MAAM;GACZ,MAAM,OAAO,UAAU,KAAK,WAAW,MAAM,IAAI,KAAK,MAAM,OAAO,MAAM,IAAI;GAC7E,MAAM,OAAO,mBAAmB;GAChC,IAAI,CAAC,MAAM;GACX,OAAO;IAAE,MAAM;IAAM;GAAK;EAC5B;CACF;AACF"}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}:root,.light{--ak-ui-background:0 0% 100%;--ak-ui-foreground:240 10% 3.9%;--ak-ui-card:0 0% 100%;--ak-ui-card-foreground:240 10% 3.9%;--ak-ui-popover:0 0% 100%;--ak-ui-popover-foreground:240 10% 3.9%;--ak-ui-primary:240 5.9% 10%;--ak-ui-primary-foreground:0 0% 98%;--ak-ui-secondary:240 4.8% 95.9%;--ak-ui-secondary-foreground:240 5.9% 10%;--ak-ui-muted:240 4.8% 95.9%;--ak-ui-muted-foreground:240 3.8% 46.1%;--ak-ui-accent:240 4.8% 95.9%;--ak-ui-accent-foreground:240 5.9% 10%;--ak-ui-destructive:0 84.2% 60.2%;--ak-ui-destructive-foreground:0 0% 98%;--ak-ui-border:240 5.9% 90%;--ak-ui-input:240 5.9% 90%;--ak-ui-ring:240 5.9% 10%;--ak-ui-radius:.5rem}.dark{--ak-ui-background:240 10% 3.9%;--ak-ui-foreground:0 0% 98%;--ak-ui-card:240 10% 3.9%;--ak-ui-card-foreground:0 0% 98%;--ak-ui-popover:240 10% 3.9%;--ak-ui-popover-foreground:0 0% 98%;--ak-ui-primary:0 0% 98%;--ak-ui-primary-foreground:240 5.9% 10%;--ak-ui-secondary:240 3.7% 15.9%;--ak-ui-secondary-foreground:0 0% 98%;--ak-ui-muted:240 3.7% 15.9%;--ak-ui-muted-foreground:240 5% 64.9%;--ak-ui-accent:240 3.7% 15.9%;--ak-ui-accent-foreground:0 0% 98%;--ak-ui-destructive:0 62.8% 30.6%;--ak-ui-destructive-foreground:0 0% 98%;--ak-ui-border:240 3.7% 15.9%;--ak-ui-input:240 3.7% 15.9%;--ak-ui-ring:240 4.9% 83.9%}:root,:host{--spacing:.25rem}.relative{position:relative}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.\!mt-3{margin-top:calc(var(--spacing) * 3)!important}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.items-start{align-items:flex-start}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.p-4{padding:calc(var(--spacing) * 4)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}:root{--ak-skeleton-block:hsl(var(--ak-ui-muted) / .55);--ak-skeleton-block-soft:hsl(var(--ak-ui-muted) / .32);--ak-skeleton-shimmer:hsl(var(--ak-ui-foreground) / .08);--ak-skeleton-radius:.375rem;--ak-skeleton-duration:1.6s;--ak-skeleton-pulse-opacity:.48;--ak-skeleton-shimmer-angle:110deg;--ak-skeleton-ring:hsl(var(--ak-ui-foreground) / .04)}.light{--ak-skeleton-shimmer:#ffffffd9;--ak-skeleton-ring:#0000000d}.a-skel-block{background-image:linear-gradient(180deg, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 100%);background-color:var(--ak-skeleton-block);border-radius:var(--ak-skeleton-radius);box-shadow:inset 0 0 0 1px var(--ak-skeleton-ring);isolation:isolate;position:relative;overflow:hidden}.a-skel-block--anim-shimmer:after{content:"";background-image:linear-gradient(var(--ak-skeleton-shimmer-angle), transparent 30%, var(--ak-skeleton-shimmer) 50%, transparent 70%);will-change:transform;animation:a-skel-shimmer var(--ak-skeleton-duration) cubic-bezier(.42, 0, .58, 1) infinite;z-index:1;position:absolute;inset:0 -25%;transform:translate(-110%)}.a-skel-block--anim-pulse{animation:a-skel-pulse var(--ak-skeleton-duration) cubic-bezier(.42, 0, .58, 1) infinite;will-change:opacity}@keyframes a-skel-shimmer{0%{transform:translate(-110%)}to{transform:translate(110%)}}@keyframes a-skel-pulse{0%,to{opacity:1}50%{opacity:var(--ak-skeleton-pulse-opacity)}}@media (prefers-reduced-motion:reduce){.a-skel-block--anim-shimmer:after,.a-skel-block--anim-pulse{will-change:auto;animation:none}.a-skel-block--anim-shimmer:after{opacity:.5;transform:translate(0)}}.a-skeleton__layer{contain:layout style paint;content-visibility:auto;position:relative}.a-skeleton__layer>.a-skel-block{contain:strict;position:absolute}.a-skel-block--text{border-radius:calc(var(--ak-skeleton-radius) * .6)}.a-skel-block--circle{background-image:radial-gradient(circle at 35% 30%, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 100%)}.a-skel-block--image{background-image:linear-gradient(160deg, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 60%, var(--ak-skeleton-block) 100%)}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
3
|
+
/* --- bundled SFC styles --- */
|
|
4
|
+
|
|
5
|
+
.a-skeleton[data-v-16717541] {
|
|
6
|
+
display: block;
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* `.a-skeleton__layer` + `.a-skeleton__layer > .a-skel-block` layout/containment
|
|
11
|
+
* live in `styles.src.css` so they're shared with the public `<ASkeletonLayer>`
|
|
12
|
+
* component. */
|
|
13
|
+
.a-skeleton__structural[data-v-16717541] * {
|
|
14
|
+
/* Disable text caret/selection on the structural copy so it doesn't look
|
|
15
|
+
* interactive. Layout (flex/grid/spacing/sizing) flows through unchanged. */
|
|
16
|
+
user-select: none;
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
}
|
|
19
|
+
.a-skeleton__structural[data-v-16717541] button,
|
|
20
|
+
.a-skeleton__structural[data-v-16717541] input,
|
|
21
|
+
.a-skeleton__structural[data-v-16717541] a {
|
|
22
|
+
cursor: default;
|
|
23
|
+
}
|
|
24
|
+
.a-skeleton__fallback[data-v-16717541] .a-skel-fallback-default {
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 4rem;
|
|
27
|
+
border-radius: 0.5rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.a-skel-block-stack[data-v-bdfba69a] {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 0.35rem;
|
|
34
|
+
width: 100%;
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alikhalilll/a-skeleton",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Self-generating Vue 3 / Nuxt 3+ skeleton loader. First paint mirrors the slot's HTML structure; second load replays a pixel-aligned shape captured from the real DOM. Themeable via CSS vars. Part of the @alikhalilll/a-* toolkit.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "alikhalilll",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/alikhalilll/ali-nuxt-toolkit.git",
|
|
10
|
+
"directory": "packages/ui-components/ASkeleton"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://alikhalilll.github.io/ali-nuxt-toolkit/ui",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/alikhalilll/ali-nuxt-toolkit/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"**/*.css"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"vue",
|
|
25
|
+
"vue3",
|
|
26
|
+
"nuxt",
|
|
27
|
+
"skeleton",
|
|
28
|
+
"loader",
|
|
29
|
+
"shimmer",
|
|
30
|
+
"placeholder",
|
|
31
|
+
"auto",
|
|
32
|
+
"headless"
|
|
33
|
+
],
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"default": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/index.d.cts",
|
|
42
|
+
"default": "./dist/index.cjs"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"./nuxt": {
|
|
46
|
+
"import": {
|
|
47
|
+
"types": "./dist/nuxt/index.d.ts",
|
|
48
|
+
"default": "./dist/nuxt/index.js"
|
|
49
|
+
},
|
|
50
|
+
"require": {
|
|
51
|
+
"types": "./dist/nuxt/index.d.cts",
|
|
52
|
+
"default": "./dist/nuxt/index.cjs"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"./resolver": {
|
|
56
|
+
"import": {
|
|
57
|
+
"types": "./dist/resolver/index.d.ts",
|
|
58
|
+
"default": "./dist/resolver/index.js"
|
|
59
|
+
},
|
|
60
|
+
"require": {
|
|
61
|
+
"types": "./dist/resolver/index.d.cts",
|
|
62
|
+
"default": "./dist/resolver/index.cjs"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"./styles.css": "./dist/styles.css",
|
|
66
|
+
"./package.json": "./package.json"
|
|
67
|
+
},
|
|
68
|
+
"types": "./dist/index.d.ts",
|
|
69
|
+
"web-types": "./web-types.json",
|
|
70
|
+
"files": [
|
|
71
|
+
"dist",
|
|
72
|
+
"src",
|
|
73
|
+
"web-types.json",
|
|
74
|
+
"README.md",
|
|
75
|
+
"LICENSE"
|
|
76
|
+
],
|
|
77
|
+
"engines": {
|
|
78
|
+
"node": ">=18"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@nuxt/kit": "^3.0.0 || ^4.0.0",
|
|
82
|
+
"@vueuse/core": "^14.0.0",
|
|
83
|
+
"unplugin-vue-components": "^28.0.0 || ^29.0.0 || ^30.0.0 || ^31.0.0 || ^32.0.0",
|
|
84
|
+
"vue": "^3.5.0"
|
|
85
|
+
},
|
|
86
|
+
"peerDependenciesMeta": {
|
|
87
|
+
"@nuxt/kit": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"unplugin-vue-components": {
|
|
91
|
+
"optional": true
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"dependencies": {},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@nuxt/kit": "^4.4.2",
|
|
97
|
+
"@nuxt/schema": "^4.4.2",
|
|
98
|
+
"@tailwindcss/cli": "^4.0.0",
|
|
99
|
+
"@tsdown/css": "0.22.0",
|
|
100
|
+
"@types/node": "^22.13.0",
|
|
101
|
+
"@vueuse/core": "^14.0.0",
|
|
102
|
+
"rimraf": "^6.0.1",
|
|
103
|
+
"tailwindcss": "^4.0.0",
|
|
104
|
+
"tsdown": "0.22.0",
|
|
105
|
+
"typescript": "^5.9.2",
|
|
106
|
+
"unplugin-vue": "7.2.0",
|
|
107
|
+
"unplugin-vue-components": "^32.1.0",
|
|
108
|
+
"vue": "^3.5.0",
|
|
109
|
+
"vue-tsc": "^3.2.4",
|
|
110
|
+
"@alikhalilll/a-ui-base": "1.0.0"
|
|
111
|
+
},
|
|
112
|
+
"scripts": {
|
|
113
|
+
"clean": "rimraf dist web-types.json",
|
|
114
|
+
"build": "tsx ../../../scripts/build/run-component-build.ts",
|
|
115
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
|
|
116
|
+
"validate-dist": "tsx ../../../scripts/validate/dist-validate.ts --pkg a-skeleton",
|
|
117
|
+
"validate-consumer": "tsx ../../../scripts/validate/consumer-validate.ts --pkg a-skeleton",
|
|
118
|
+
"validate": "pnpm build && pnpm validate-dist && pnpm validate-consumer"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, shallowRef, useSlots, watch, type CSSProperties } from 'vue';
|
|
3
|
+
import { cn } from '@alikhalilll/a-ui-base';
|
|
4
|
+
import type { ASkeletonProps, ASkeletonSlots, CachedShape, ShapeNodeType } from '../types';
|
|
5
|
+
import { useShapeProbe } from '../composables/useShapeProbe';
|
|
6
|
+
import { getCached, setCached } from '../composables/useSkeletonCache';
|
|
7
|
+
import { fingerprintSlot } from '../utils/fingerprint';
|
|
8
|
+
import { StructuralSkeleton } from './StructuralSkeleton';
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<ASkeletonProps>(), {
|
|
11
|
+
maxDepth: 6,
|
|
12
|
+
maxNodes: 500,
|
|
13
|
+
minNodeSize: 4,
|
|
14
|
+
persist: false,
|
|
15
|
+
animation: 'shimmer',
|
|
16
|
+
fallback: 'shimmer',
|
|
17
|
+
});
|
|
18
|
+
defineSlots<ASkeletonSlots>();
|
|
19
|
+
|
|
20
|
+
const slots = useSlots();
|
|
21
|
+
|
|
22
|
+
const resolvedKey = computed(() => props.cacheKey ?? fingerprintSlot(slots.default?.()));
|
|
23
|
+
|
|
24
|
+
const cached = shallowRef<CachedShape | undefined>(getCached(resolvedKey.value, props.persist));
|
|
25
|
+
|
|
26
|
+
watch(resolvedKey, (key) => {
|
|
27
|
+
cached.value = getCached(key, props.persist);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const wrapperRef = ref<HTMLElement | null>(null);
|
|
31
|
+
|
|
32
|
+
/* Probe runs whenever the real content is mounted (loading=false). The getter
|
|
33
|
+
* returns null during loading so the watch tears down its ResizeObserver. */
|
|
34
|
+
useShapeProbe(() => (props.loading ? null : wrapperRef.value), {
|
|
35
|
+
maxDepth: props.maxDepth,
|
|
36
|
+
maxNodes: props.maxNodes,
|
|
37
|
+
minSize: props.minNodeSize,
|
|
38
|
+
onCapture: (shape) => {
|
|
39
|
+
setCached(resolvedKey.value, shape, props.persist);
|
|
40
|
+
cached.value = shape;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const animationClass = computed(() =>
|
|
45
|
+
props.animation === 'none' ? null : `a-skel-block--anim-${props.animation}`
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const layerStyle = computed<CSSProperties>(() =>
|
|
49
|
+
cached.value ? { width: `${cached.value.width}px`, height: `${cached.value.height}px` } : {}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/* Pre-join the per-type class strings once per animation value so the render
|
|
53
|
+
* loop doesn't allocate a new `[a, b, c]` array per node per frame — meaningful
|
|
54
|
+
* when a cache holds hundreds of nodes. */
|
|
55
|
+
const blockClassByType = computed<Readonly<Record<ShapeNodeType, string>>>(() => {
|
|
56
|
+
const anim = animationClass.value;
|
|
57
|
+
const suffix = anim ? ` ${anim}` : '';
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
block: `a-skel-block a-skel-block--block${suffix}`,
|
|
60
|
+
text: `a-skel-block a-skel-block--text${suffix}`,
|
|
61
|
+
image: `a-skel-block a-skel-block--image${suffix}`,
|
|
62
|
+
circle: `a-skel-block a-skel-block--circle${suffix}`,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/* Cache-miss fallback path: walk the slot's vnodes synchronously so the FIRST
|
|
67
|
+
* paint already shows a skeleton that mirrors the component's HTML structure
|
|
68
|
+
* (same tags, classes, hierarchy → same flex/grid/spacing/sizing utilities
|
|
69
|
+
* still apply). If the slot is empty / only renders comments (e.g. the user
|
|
70
|
+
* gates the whole template on `v-if="data"`), we get an empty array back and
|
|
71
|
+
* fall through to the generic shimmer block. */
|
|
72
|
+
const structuralVNodes = computed(() => (props.loading ? (slots.default?.() ?? []) : []));
|
|
73
|
+
const hasStructure = computed(() => structuralVNodes.value.length > 0);
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<div
|
|
78
|
+
ref="wrapperRef"
|
|
79
|
+
:class="cn('a-skeleton', props.class)"
|
|
80
|
+
:data-loading="props.loading ? '' : undefined"
|
|
81
|
+
>
|
|
82
|
+
<template v-if="props.loading">
|
|
83
|
+
<!-- Cache hit: pixel-aligned positioned blocks from a previous measurement.
|
|
84
|
+
Styles are pre-computed during capture so the loop below never calls
|
|
85
|
+
a function or allocates a style object per node. -->
|
|
86
|
+
<div
|
|
87
|
+
v-if="cached"
|
|
88
|
+
class="a-skeleton__layer"
|
|
89
|
+
:style="layerStyle"
|
|
90
|
+
role="status"
|
|
91
|
+
aria-live="polite"
|
|
92
|
+
aria-busy="true"
|
|
93
|
+
>
|
|
94
|
+
<template v-for="(node, idx) in cached.nodes" :key="idx">
|
|
95
|
+
<template v-if="node.lineStyles">
|
|
96
|
+
<div
|
|
97
|
+
v-for="(lineStyle, i) in node.lineStyles"
|
|
98
|
+
:key="`${idx}-${i}`"
|
|
99
|
+
:class="blockClassByType.text"
|
|
100
|
+
:style="lineStyle"
|
|
101
|
+
/>
|
|
102
|
+
</template>
|
|
103
|
+
<div v-else :class="blockClassByType[node.type]" :style="node.style" />
|
|
104
|
+
</template>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Cache miss + slot has structure: render a structural skeleton derived
|
|
108
|
+
from the slot's vnode tree. First paint already looks right. -->
|
|
109
|
+
<div
|
|
110
|
+
v-else-if="hasStructure"
|
|
111
|
+
class="a-skeleton__structural"
|
|
112
|
+
role="status"
|
|
113
|
+
aria-live="polite"
|
|
114
|
+
aria-busy="true"
|
|
115
|
+
>
|
|
116
|
+
<StructuralSkeleton
|
|
117
|
+
:vnodes="structuralVNodes"
|
|
118
|
+
:animation="animationClass"
|
|
119
|
+
:max-depth="maxDepth"
|
|
120
|
+
:max-nodes="maxNodes"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Cache miss + nothing to walk: generic shimmer. -->
|
|
125
|
+
<div v-else class="a-skeleton__fallback" role="status" aria-busy="true">
|
|
126
|
+
<slot name="fallback">
|
|
127
|
+
<div
|
|
128
|
+
class="a-skel-block a-skel-block--block a-skel-fallback-default"
|
|
129
|
+
:class="animationClass"
|
|
130
|
+
/>
|
|
131
|
+
</slot>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<slot v-else />
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<style scoped>
|
|
140
|
+
.a-skeleton {
|
|
141
|
+
display: block;
|
|
142
|
+
position: relative;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* `.a-skeleton__layer` + `.a-skeleton__layer > .a-skel-block` layout/containment
|
|
146
|
+
* live in `styles.src.css` so they're shared with the public `<ASkeletonLayer>`
|
|
147
|
+
* component. */
|
|
148
|
+
|
|
149
|
+
.a-skeleton__structural :deep(*) {
|
|
150
|
+
/* Disable text caret/selection on the structural copy so it doesn't look
|
|
151
|
+
* interactive. Layout (flex/grid/spacing/sizing) flows through unchanged. */
|
|
152
|
+
user-select: none;
|
|
153
|
+
pointer-events: none;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.a-skeleton__structural :deep(button),
|
|
157
|
+
.a-skeleton__structural :deep(input),
|
|
158
|
+
.a-skeleton__structural :deep(a) {
|
|
159
|
+
cursor: default;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.a-skeleton__fallback :deep(.a-skel-fallback-default) {
|
|
163
|
+
width: 100%;
|
|
164
|
+
height: 4rem;
|
|
165
|
+
border-radius: 0.5rem;
|
|
166
|
+
}
|
|
167
|
+
</style>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, type CSSProperties } from 'vue';
|
|
3
|
+
import { cn } from '@alikhalilll/a-ui-base';
|
|
4
|
+
import type { ASkeletonBlockProps } from '../types';
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<ASkeletonBlockProps>(), {
|
|
7
|
+
type: 'block',
|
|
8
|
+
animation: 'shimmer',
|
|
9
|
+
lines: 1,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const animationClass = computed(() =>
|
|
13
|
+
props.animation === 'none' ? null : `a-skel-block--anim-${props.animation}`
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const blockClass = computed(() => [
|
|
17
|
+
'a-skel-block',
|
|
18
|
+
`a-skel-block--${props.type}`,
|
|
19
|
+
animationClass.value,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function toLength(v: number | string | undefined): string | undefined {
|
|
23
|
+
if (v === undefined) return undefined;
|
|
24
|
+
return typeof v === 'number' ? `${v}px` : v;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const radiusValue = computed(() =>
|
|
28
|
+
props.type === 'circle' && props.radius === undefined ? '50%' : toLength(props.radius)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const blockStyle = computed<CSSProperties>(() => ({
|
|
32
|
+
width: toLength(props.w),
|
|
33
|
+
height: toLength(props.h),
|
|
34
|
+
borderRadius: radiusValue.value,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const isMultiLineText = computed(() => props.type === 'text' && props.lines > 1);
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<div
|
|
42
|
+
v-if="isMultiLineText"
|
|
43
|
+
:class="cn('a-skel-block-stack', props.class)"
|
|
44
|
+
role="status"
|
|
45
|
+
aria-busy="true"
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
v-for="i in props.lines"
|
|
49
|
+
:key="i"
|
|
50
|
+
:class="blockClass"
|
|
51
|
+
:style="{
|
|
52
|
+
height: blockStyle.height ?? '0.75em',
|
|
53
|
+
width: i === props.lines ? '70%' : '100%',
|
|
54
|
+
borderRadius: blockStyle.borderRadius ?? '4px',
|
|
55
|
+
}"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<div
|
|
59
|
+
v-else
|
|
60
|
+
:class="cn(blockClass, props.class)"
|
|
61
|
+
:style="blockStyle"
|
|
62
|
+
role="status"
|
|
63
|
+
aria-busy="true"
|
|
64
|
+
/>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style scoped>
|
|
68
|
+
.a-skel-block-stack {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
gap: 0.35rem;
|
|
72
|
+
width: 100%;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, type CSSProperties } from 'vue';
|
|
3
|
+
import { cn } from '@alikhalilll/a-ui-base';
|
|
4
|
+
import type { ASkeletonLayerProps, ShapeNodeType } from '../types';
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<ASkeletonLayerProps>(), {
|
|
7
|
+
animation: 'shimmer',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const animationClass = computed(() =>
|
|
11
|
+
props.animation === 'none' ? null : `a-skel-block--anim-${props.animation}`
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const layerStyle = computed<CSSProperties>(() =>
|
|
15
|
+
props.shape ? { width: `${props.shape.width}px`, height: `${props.shape.height}px` } : {}
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/* Pre-joined per-type class strings — see ASkeleton.vue for the rationale. */
|
|
19
|
+
const blockClassByType = computed<Readonly<Record<ShapeNodeType, string>>>(() => {
|
|
20
|
+
const anim = animationClass.value;
|
|
21
|
+
const suffix = anim ? ` ${anim}` : '';
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
block: `a-skel-block a-skel-block--block${suffix}`,
|
|
24
|
+
text: `a-skel-block a-skel-block--text${suffix}`,
|
|
25
|
+
image: `a-skel-block a-skel-block--image${suffix}`,
|
|
26
|
+
circle: `a-skel-block a-skel-block--circle${suffix}`,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div
|
|
33
|
+
v-if="shape"
|
|
34
|
+
:class="cn('a-skeleton__layer', props.class)"
|
|
35
|
+
:style="layerStyle"
|
|
36
|
+
role="status"
|
|
37
|
+
aria-live="polite"
|
|
38
|
+
aria-busy="true"
|
|
39
|
+
>
|
|
40
|
+
<template v-for="(node, idx) in shape.nodes" :key="idx">
|
|
41
|
+
<template v-if="node.lineStyles">
|
|
42
|
+
<div
|
|
43
|
+
v-for="(lineStyle, i) in node.lineStyles"
|
|
44
|
+
:key="`${idx}-${i}`"
|
|
45
|
+
:class="blockClassByType.text"
|
|
46
|
+
:style="lineStyle"
|
|
47
|
+
/>
|
|
48
|
+
</template>
|
|
49
|
+
<div v-else :class="blockClassByType[node.type]" :style="node.style" />
|
|
50
|
+
</template>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|