@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.
@@ -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"}
@@ -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>