@alikhalilll/a-smart-skeleton 0.0.0-stage → 2.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 +356 -2
- package/dist/_chunks/core.js +2314 -0
- package/dist/_chunks/core.js.map +1 -0
- package/dist/_chunks/index.d.ts +431 -0
- package/dist/core/index.cjs +67 -0
- package/dist/core/index.d.cts +2 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/element/index.cjs +47 -0
- package/dist/element/index.cjs.map +1 -0
- package/dist/element/index.d.cts +24 -0
- package/dist/element/index.d.ts +24 -0
- package/dist/element/index.js +46 -0
- package/dist/element/index.js.map +1 -0
- package/dist/index.cjs +512 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +204 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +441 -0
- package/dist/index.js.map +1 -0
- package/dist/nuxt/index.cjs +36 -0
- package/dist/nuxt/index.cjs.map +1 -0
- package/dist/nuxt/index.d.cts +30 -0
- package/dist/nuxt/index.d.ts +30 -0
- package/dist/nuxt/index.js +36 -0
- package/dist/nuxt/index.js.map +1 -0
- package/dist/resolver/index.cjs +26 -0
- package/dist/resolver/index.cjs.map +1 -0
- package/dist/resolver/index.d.cts +19 -0
- package/dist/resolver/index.d.ts +19 -0
- package/dist/resolver/index.js +26 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/styles.css +136 -0
- package/package.json +144 -3
- package/src/components/ASmartSkeleton.vue +228 -0
- package/src/components/ASmartSkeletonBox.vue +23 -0
- package/src/components/ASmartSkeletonProvider.vue +34 -0
- package/src/components/ASmartSkeletonSuspense.vue +142 -0
- package/src/composables/useSmartSkeleton.ts +20 -0
- package/src/composables/useStyles.ts +8 -0
- package/src/core/boot.ts +63 -0
- package/src/core/canvas.ts +7 -0
- package/src/core/constants.ts +76 -0
- package/src/core/debug.ts +154 -0
- package/src/core/dev.ts +53 -0
- package/src/core/fade.ts +490 -0
- package/src/core/ghost.ts +236 -0
- package/src/core/images.ts +28 -0
- package/src/core/index.ts +71 -0
- package/src/core/inline.generated.ts +9 -0
- package/src/core/measure.ts +571 -0
- package/src/core/memory.ts +146 -0
- package/src/core/motion.ts +12 -0
- package/src/core/plan.ts +368 -0
- package/src/core/reveal.ts +89 -0
- package/src/core/reveals.ts +39 -0
- package/src/core/shapes.ts +84 -0
- package/src/core/shimmer.ts +229 -0
- package/src/core/skeleton.ts +43 -0
- package/src/core/snapshot.ts +159 -0
- package/src/core/styles.ts +187 -0
- package/src/core/sweep.ts +203 -0
- package/src/core/theme.ts +162 -0
- package/src/element/index.ts +59 -0
- package/src/index.ts +27 -0
- package/src/nuxt/index.ts +59 -0
- package/src/resolver/index.ts +35 -0
- package/src/types.ts +100 -0
- package/src/vue/Skeleton.ts +275 -0
- package/src/vue/Suspense.ts +157 -0
- package/src/vue/config.ts +78 -0
- package/src/vue/index.ts +7 -0
- package/web-types.json +256 -0
|
@@ -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-smart-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 ASmartSkeletonResolver from '@alikhalilll/a-smart-skeleton/resolver';\n * export default { plugins: [Components({ resolvers: [ASmartSkeletonResolver()] })] };\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 ASmartSkeleton: '@alikhalilll/a-smart-skeleton',\n ASmartSkeletonBox: '@alikhalilll/a-smart-skeleton',\n ASmartSkeletonSuspense: '@alikhalilll/a-smart-skeleton',\n ASmartSkeletonProvider: '@alikhalilll/a-smart-skeleton',\n};\n\nexport default function ASmartSkeletonResolver(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":";AAgBA,MAAM,qBAA6C;CACjD,gBAAgB;CAChB,mBAAmB;CACnB,wBAAwB;CACxB,wBAAwB;AAC1B;AAEA,SAAwB,uBAAuB,OAAwB,CAAC,GAAsB;CAC5F,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,19 @@
|
|
|
1
|
+
import { ComponentResolver } from "unplugin-vue-components";
|
|
2
|
+
|
|
3
|
+
//#region src/resolver/index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@alikhalilll/a-smart-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 ASmartSkeletonResolver from '@alikhalilll/a-smart-skeleton/resolver';
|
|
10
|
+
* export default { plugins: [Components({ resolvers: [ASmartSkeletonResolver()] })] };
|
|
11
|
+
*/
|
|
12
|
+
interface ResolverOptions {
|
|
13
|
+
/** Optional prefix the consumer uses (e.g. `'A'`). Defaults to the native `A*` names. */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function ASmartSkeletonResolver(opts?: ResolverOptions): ComponentResolver;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ResolverOptions, ASmartSkeletonResolver as default };
|
|
19
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ComponentResolver } from "unplugin-vue-components";
|
|
2
|
+
|
|
3
|
+
//#region src/resolver/index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@alikhalilll/a-smart-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 ASmartSkeletonResolver from '@alikhalilll/a-smart-skeleton/resolver';
|
|
10
|
+
* export default { plugins: [Components({ resolvers: [ASmartSkeletonResolver()] })] };
|
|
11
|
+
*/
|
|
12
|
+
interface ResolverOptions {
|
|
13
|
+
/** Optional prefix the consumer uses (e.g. `'A'`). Defaults to the native `A*` names. */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function ASmartSkeletonResolver(opts?: ResolverOptions): ComponentResolver;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ResolverOptions, ASmartSkeletonResolver as default };
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/resolver/index.ts
|
|
2
|
+
const COMPONENT_TO_ENTRY = {
|
|
3
|
+
ASmartSkeleton: "@alikhalilll/a-smart-skeleton",
|
|
4
|
+
ASmartSkeletonBox: "@alikhalilll/a-smart-skeleton",
|
|
5
|
+
ASmartSkeletonSuspense: "@alikhalilll/a-smart-skeleton",
|
|
6
|
+
ASmartSkeletonProvider: "@alikhalilll/a-smart-skeleton"
|
|
7
|
+
};
|
|
8
|
+
function ASmartSkeletonResolver(opts = {}) {
|
|
9
|
+
const prefix = opts.prefix ?? "";
|
|
10
|
+
return {
|
|
11
|
+
type: "component",
|
|
12
|
+
resolve(name) {
|
|
13
|
+
const bare = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
14
|
+
const from = COMPONENT_TO_ENTRY[bare];
|
|
15
|
+
if (!from) return;
|
|
16
|
+
return {
|
|
17
|
+
name: bare,
|
|
18
|
+
from
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ASmartSkeletonResolver as default };
|
|
25
|
+
|
|
26
|
+
//# 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-smart-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 ASmartSkeletonResolver from '@alikhalilll/a-smart-skeleton/resolver';\n * export default { plugins: [Components({ resolvers: [ASmartSkeletonResolver()] })] };\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 ASmartSkeleton: '@alikhalilll/a-smart-skeleton',\n ASmartSkeletonBox: '@alikhalilll/a-smart-skeleton',\n ASmartSkeletonSuspense: '@alikhalilll/a-smart-skeleton',\n ASmartSkeletonProvider: '@alikhalilll/a-smart-skeleton',\n};\n\nexport default function ASmartSkeletonResolver(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":";AAgBA,MAAM,qBAA6C;CACjD,gBAAgB;CAChB,mBAAmB;CACnB,wBAAwB;CACxB,wBAAwB;AAC1B;AAEA,SAAwB,uBAAuB,OAAwB,CAAC,GAAsB;CAC5F,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,136 @@
|
|
|
1
|
+
[ask-reveal] {
|
|
2
|
+
position: relative;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
a-smart-skeleton-reveal,
|
|
6
|
+
ask-reveal {
|
|
7
|
+
display: block;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
a-smart-skeleton-reveal[loading]:not(:defined),
|
|
11
|
+
ask-reveal[loading]:not(:defined) {
|
|
12
|
+
visibility: hidden;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[ask-pane] {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[ask-loading] {
|
|
20
|
+
position: relative;
|
|
21
|
+
-webkit-user-select: none;
|
|
22
|
+
user-select: none;
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[ask-inactive] {
|
|
27
|
+
display: none !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[ask-layer] {
|
|
31
|
+
position: absolute;
|
|
32
|
+
inset: 0;
|
|
33
|
+
overflow: visible;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[ask-bar] {
|
|
37
|
+
display: block;
|
|
38
|
+
background-color: var(--ask-base, rgb(128 128 128 / 0.2));
|
|
39
|
+
background-repeat: no-repeat;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* Bones drawn by `drawSkeleton` live in the light DOM (no shadow root), so a
|
|
44
|
+
* consumer can target them with their own stylesheet. Positioning stays inline
|
|
45
|
+
* (per-bone), but the fill colour reads from a CSS custom property so the
|
|
46
|
+
* consumer's theme flows through — set `--ask-base` on any ancestor to
|
|
47
|
+
* recolour every bone inside it, or use the type modifier to style text / media
|
|
48
|
+
* / box bones separately.
|
|
49
|
+
*
|
|
50
|
+
* Chrome bones are excluded: they reproduce the consumer's own container paint
|
|
51
|
+
* (background, border, shadow) that `captureChrome` snapshotted, so they must
|
|
52
|
+
* NOT get a generic fill underneath. Without the `:not()`, a bordered-only
|
|
53
|
+
* container (e.g. a footer with `border-t`) would show as a solid filled
|
|
54
|
+
* rectangle instead of just its captured divider.
|
|
55
|
+
*/
|
|
56
|
+
[ask-rect]:not([ask-rect="chrome"]) {
|
|
57
|
+
background-color: var(--ask-base, var(--ask-block, var(--ak-skeleton-block, rgb(128 128 128 / 0.2))));
|
|
58
|
+
background-repeat: no-repeat;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Containers keep their surface; what they hold is drawn by bones, measured before the first paint */
|
|
62
|
+
@scope ([ask-loading]) to ([ask-own], [ask-layer]) {
|
|
63
|
+
:scope,
|
|
64
|
+
:scope * {
|
|
65
|
+
color: transparent !important;
|
|
66
|
+
-webkit-text-fill-color: transparent !important;
|
|
67
|
+
text-shadow: none !important;
|
|
68
|
+
text-decoration-color: transparent !important;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:scope ::placeholder {
|
|
72
|
+
color: transparent !important;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* A line to measure in empty text; content only, never a size, or a dot-sized span would grow round it */
|
|
76
|
+
:scope :is(a, b, code, dd, dt, em, h1, h2, h3, h4, h5, h6, i, label, li, p, small, span, strong, td, th, time):empty:not(svg *)::before {
|
|
77
|
+
content: "000000";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Leaves are drawn by bones, so their own paint never shows; visibility, as measuring reads it */
|
|
81
|
+
:scope :is(:not(:has(*)), audio, canvas, embed, iframe, img, input, object, picture, select, svg, textarea, video, [ask-covered], [ask-block]):not(svg *) {
|
|
82
|
+
visibility: hidden !important;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* No script to measure: a bar struck through each line of text instead */
|
|
86
|
+
@media (scripting: none) {
|
|
87
|
+
:scope :not(:has(*)):not(svg *) {
|
|
88
|
+
visibility: visible !important;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
:scope,
|
|
92
|
+
:scope * {
|
|
93
|
+
text-decoration: line-through var(--ask-base, rgb(128 128 128 / 0.2)) !important;
|
|
94
|
+
text-decoration-thickness: 0.72em !important;
|
|
95
|
+
text-decoration-skip-ink: none !important;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
:scope :is(audio, canvas, embed, iframe, img, input, object, picture, select, svg, textarea, video, [ask-block]):not(svg *) {
|
|
99
|
+
visibility: visible !important;
|
|
100
|
+
background: var(--ask-base, rgb(128 128 128 / 0.2)) !important;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
:scope svg * {
|
|
104
|
+
fill: transparent !important;
|
|
105
|
+
stroke: transparent !important;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:scope :is(img, video) {
|
|
109
|
+
object-position: -100vw 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Read by screen readers in the skeleton's place, never seen */
|
|
115
|
+
[ask-status] {
|
|
116
|
+
position: absolute;
|
|
117
|
+
width: 1px;
|
|
118
|
+
height: 1px;
|
|
119
|
+
margin: -1px;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
clip-path: inset(50%);
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Debug x-ray: the ghost shown through its bones, to see what was measured */
|
|
126
|
+
[ask-xray] [ask-loading] [ask-ghost],
|
|
127
|
+
[ask-xray] [ask-loading] [ask-ghost] *:not(svg *) {
|
|
128
|
+
visibility: visible !important;
|
|
129
|
+
color: rgb(255 0 80 / 0.7) !important;
|
|
130
|
+
-webkit-text-fill-color: rgb(255 0 80 / 0.7) !important;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Lets addStyles see a stylesheet already on the page, from styles.css or a server render */
|
|
134
|
+
:root {
|
|
135
|
+
--ask: 1;
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,147 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alikhalilll/a-smart-skeleton",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Smart, measured skeleton loaders for Vue 3 / Nuxt 3+. Lays the real content out unseen, draws a skeleton from its geometry, then reveals it with smart, sequence, sweep or together animations.",
|
|
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/ASmartSkeleton"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://alikhalilll.github.io/ali-nuxt-toolkit/ui/smart-skeleton",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/alikhalilll/ali-nuxt-toolkit/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"**/*.css",
|
|
19
|
+
"./dist/element/*"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"vue",
|
|
26
|
+
"vue3",
|
|
27
|
+
"nuxt",
|
|
28
|
+
"skeleton",
|
|
29
|
+
"loader",
|
|
30
|
+
"shimmer",
|
|
31
|
+
"placeholder",
|
|
32
|
+
"suspense",
|
|
33
|
+
"ssr",
|
|
34
|
+
"headless",
|
|
35
|
+
"web-components"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"import": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"default": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"require": {
|
|
44
|
+
"types": "./dist/index.d.cts",
|
|
45
|
+
"default": "./dist/index.cjs"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"./core": {
|
|
49
|
+
"import": {
|
|
50
|
+
"types": "./dist/core/index.d.ts",
|
|
51
|
+
"default": "./dist/core/index.js"
|
|
52
|
+
},
|
|
53
|
+
"require": {
|
|
54
|
+
"types": "./dist/core/index.d.cts",
|
|
55
|
+
"default": "./dist/core/index.cjs"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"./element": {
|
|
59
|
+
"import": {
|
|
60
|
+
"types": "./dist/element/index.d.ts",
|
|
61
|
+
"default": "./dist/element/index.js"
|
|
62
|
+
},
|
|
63
|
+
"require": {
|
|
64
|
+
"types": "./dist/element/index.d.cts",
|
|
65
|
+
"default": "./dist/element/index.cjs"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"./nuxt": {
|
|
69
|
+
"import": {
|
|
70
|
+
"types": "./dist/nuxt/index.d.ts",
|
|
71
|
+
"default": "./dist/nuxt/index.js"
|
|
72
|
+
},
|
|
73
|
+
"require": {
|
|
74
|
+
"types": "./dist/nuxt/index.d.cts",
|
|
75
|
+
"default": "./dist/nuxt/index.cjs"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"./resolver": {
|
|
79
|
+
"import": {
|
|
80
|
+
"types": "./dist/resolver/index.d.ts",
|
|
81
|
+
"default": "./dist/resolver/index.js"
|
|
82
|
+
},
|
|
83
|
+
"require": {
|
|
84
|
+
"types": "./dist/resolver/index.d.cts",
|
|
85
|
+
"default": "./dist/resolver/index.cjs"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"./styles.css": "./dist/styles.css",
|
|
89
|
+
"./package.json": "./package.json"
|
|
90
|
+
},
|
|
91
|
+
"types": "./dist/index.d.ts",
|
|
92
|
+
"web-types": "./web-types.json",
|
|
93
|
+
"files": [
|
|
94
|
+
"dist",
|
|
95
|
+
"src",
|
|
96
|
+
"web-types.json",
|
|
97
|
+
"README.md",
|
|
98
|
+
"LICENSE"
|
|
99
|
+
],
|
|
100
|
+
"engines": {
|
|
101
|
+
"node": ">=18"
|
|
102
|
+
},
|
|
103
|
+
"peerDependencies": {
|
|
104
|
+
"@nuxt/kit": "^3.0.0 || ^4.0.0",
|
|
105
|
+
"@vueuse/core": "^14.0.0",
|
|
106
|
+
"unplugin-vue-components": "^28.0.0 || ^29.0.0 || ^30.0.0 || ^31.0.0 || ^32.0.0",
|
|
107
|
+
"vue": "^3.5.0"
|
|
108
|
+
},
|
|
109
|
+
"peerDependenciesMeta": {
|
|
110
|
+
"@nuxt/kit": {
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
113
|
+
"unplugin-vue-components": {
|
|
114
|
+
"optional": true
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"devDependencies": {
|
|
118
|
+
"@nuxt/kit": "^4.4.2",
|
|
119
|
+
"@nuxt/schema": "^4.4.2",
|
|
120
|
+
"@tailwindcss/cli": "^4.0.0",
|
|
121
|
+
"@types/node": "^22.13.0",
|
|
122
|
+
"@vueuse/core": "^14.0.0",
|
|
123
|
+
"esbuild": "^0.27.7",
|
|
124
|
+
"happy-dom": "^20.12.0",
|
|
125
|
+
"rimraf": "^6.0.1",
|
|
126
|
+
"tailwindcss": "^4.0.0",
|
|
127
|
+
"tsdown": "0.22.0",
|
|
128
|
+
"typescript": "^5.9.2",
|
|
129
|
+
"unplugin-vue": "7.2.0",
|
|
130
|
+
"unplugin-vue-components": "^32.1.0",
|
|
131
|
+
"vitest": "^4.1.11",
|
|
132
|
+
"vue": "^3.5.0",
|
|
133
|
+
"vue-tsc": "^3.2.4",
|
|
134
|
+
"@alikhalilll/a-ui-base": "1.0.0"
|
|
135
|
+
},
|
|
136
|
+
"scripts": {
|
|
137
|
+
"clean": "rimraf dist web-types.json",
|
|
138
|
+
"build": "node scripts/build-inline.mjs && tsx ../../../scripts/build/run-component-build.ts && node scripts/build-css.mjs",
|
|
139
|
+
"dev": "node scripts/build-inline.mjs && tsdown --watch",
|
|
140
|
+
"test": "vitest run",
|
|
141
|
+
"test:watch": "vitest",
|
|
142
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
|
|
143
|
+
"validate-dist": "tsx ../../../scripts/validate/dist-validate.ts --pkg a-smart-skeleton",
|
|
144
|
+
"validate-consumer": "tsx ../../../scripts/validate/consumer-validate.ts --pkg a-smart-skeleton",
|
|
145
|
+
"validate": "pnpm build && pnpm validate-dist && pnpm validate-consumer"
|
|
146
|
+
}
|
|
6
147
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
getCurrentInstance,
|
|
5
|
+
h,
|
|
6
|
+
onBeforeUnmount,
|
|
7
|
+
onMounted,
|
|
8
|
+
ref,
|
|
9
|
+
Transition,
|
|
10
|
+
watch,
|
|
11
|
+
type PropType,
|
|
12
|
+
type VNodeChild,
|
|
13
|
+
} from 'vue';
|
|
14
|
+
import {
|
|
15
|
+
ATTR_GHOST,
|
|
16
|
+
ATTR_LAYER,
|
|
17
|
+
ATTR_LOADING,
|
|
18
|
+
ATTR_OWN,
|
|
19
|
+
ATTR_PANE,
|
|
20
|
+
ATTR_REVEAL,
|
|
21
|
+
ATTR_SNAPSHOT,
|
|
22
|
+
ATTR_STATUS,
|
|
23
|
+
checkNonce,
|
|
24
|
+
fadeIn,
|
|
25
|
+
fadeOut,
|
|
26
|
+
Ghost,
|
|
27
|
+
INLINE_SCRIPT,
|
|
28
|
+
INLINE_STUB,
|
|
29
|
+
Memory,
|
|
30
|
+
type MemoryOptions,
|
|
31
|
+
type RevealMode,
|
|
32
|
+
} from '../core';
|
|
33
|
+
import { claimStub, useSmartSkeleton } from '../composables/useSmartSkeleton';
|
|
34
|
+
import { useStyles } from '../composables/useStyles';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The measured loading pane. Internal — `<ASmartSkeleton>` renders it.
|
|
38
|
+
*
|
|
39
|
+
* The ghost is laid out but never seen; the layer holds bones in a shadow root,
|
|
40
|
+
* so hydration never touches what was measured.
|
|
41
|
+
*/
|
|
42
|
+
export const Loading = defineComponent({
|
|
43
|
+
name: 'ASmartSkeletonLoading',
|
|
44
|
+
props: {
|
|
45
|
+
id: String,
|
|
46
|
+
exact: Boolean,
|
|
47
|
+
refetch: Boolean,
|
|
48
|
+
label: String,
|
|
49
|
+
},
|
|
50
|
+
setup(props, { slots }) {
|
|
51
|
+
const element = ref<HTMLElement | null>(null);
|
|
52
|
+
const config = useSmartSkeleton();
|
|
53
|
+
const script = claimStub() ? INLINE_STUB : INLINE_SCRIPT;
|
|
54
|
+
let ghost: Ghost | null = null;
|
|
55
|
+
|
|
56
|
+
const optionsOf = () => ({ id: props.id, exact: props.exact, refetch: props.refetch });
|
|
57
|
+
onMounted(() => {
|
|
58
|
+
checkNonce(config.nonce);
|
|
59
|
+
if (element.value) ghost = new Ghost(element.value, optionsOf());
|
|
60
|
+
});
|
|
61
|
+
// Options-only changes update the live ghost in place; the observers and shadow sheet
|
|
62
|
+
// outlive a prop flip, which happens every data arrival in data-driven mode.
|
|
63
|
+
watch(
|
|
64
|
+
() => [props.id, props.exact, props.refetch],
|
|
65
|
+
() => ghost?.update(optionsOf())
|
|
66
|
+
);
|
|
67
|
+
onBeforeUnmount(() => ghost?.destroy());
|
|
68
|
+
|
|
69
|
+
return () => [
|
|
70
|
+
props.label ? h('span', { [ATTR_STATUS]: '' }, props.label) : null,
|
|
71
|
+
h(
|
|
72
|
+
'div',
|
|
73
|
+
{
|
|
74
|
+
ref: element,
|
|
75
|
+
[ATTR_LOADING]: '',
|
|
76
|
+
'aria-hidden': 'true',
|
|
77
|
+
inert: '',
|
|
78
|
+
},
|
|
79
|
+
[
|
|
80
|
+
h('div', { [ATTR_GHOST]: '' }, slots.default?.()),
|
|
81
|
+
h('div', { [ATTR_SNAPSHOT]: '' }),
|
|
82
|
+
h('div', { [ATTR_LAYER]: '' }),
|
|
83
|
+
]
|
|
84
|
+
),
|
|
85
|
+
h('script', { nonce: config.nonce, innerHTML: script }),
|
|
86
|
+
];
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const Content = defineComponent({
|
|
91
|
+
name: 'ASmartSkeletonContent',
|
|
92
|
+
props: {
|
|
93
|
+
memory: { type: Object as PropType<MemoryOptions>, required: true },
|
|
94
|
+
},
|
|
95
|
+
setup(props, { slots }) {
|
|
96
|
+
const element = ref<HTMLElement | null>(null);
|
|
97
|
+
let memory: Memory | null = null;
|
|
98
|
+
|
|
99
|
+
onMounted(() => {
|
|
100
|
+
if (element.value) memory = new Memory(element.value, props.memory);
|
|
101
|
+
});
|
|
102
|
+
onBeforeUnmount(() => memory?.destroy());
|
|
103
|
+
|
|
104
|
+
return () => h('div', { ref: element }, slots.default?.());
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Shows a skeleton measured from the content it wraps, then reveals the content.
|
|
110
|
+
*
|
|
111
|
+
* ```vue
|
|
112
|
+
* <ASmartSkeleton :data="user" :sample="SAMPLE_USER">
|
|
113
|
+
* <template #default="{ data }">
|
|
114
|
+
* <UserCard :user="data" />
|
|
115
|
+
* </template>
|
|
116
|
+
* </ASmartSkeleton>
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export default defineComponent({
|
|
120
|
+
name: 'ASmartSkeleton',
|
|
121
|
+
props: {
|
|
122
|
+
/** Shows the skeleton, even over `data`. With `data`, it also shows while there is none. */
|
|
123
|
+
loading: { type: Boolean, default: undefined },
|
|
124
|
+
/** Loading while `null` or `undefined`; handed to the default slot as `{ data }`. */
|
|
125
|
+
data: { type: null as unknown as PropType<unknown>, required: false },
|
|
126
|
+
/** Stands in for `data` to draw the skeleton before any has loaded. */
|
|
127
|
+
sample: { type: null as unknown as PropType<unknown>, required: false },
|
|
128
|
+
/** Remembers the shape under this key across visits. */
|
|
129
|
+
id: String,
|
|
130
|
+
/** Read to screen readers while loading, in place of the skeleton — "Loading profile". */
|
|
131
|
+
label: String,
|
|
132
|
+
empty: Boolean,
|
|
133
|
+
error: { type: null as unknown as PropType<unknown>, required: false },
|
|
134
|
+
/** Overrides the provider's reveal for this skeleton alone. */
|
|
135
|
+
reveal: [String, Object] as PropType<RevealMode>,
|
|
136
|
+
},
|
|
137
|
+
/** `reveal`: content has come in over a skeleton. */
|
|
138
|
+
emits: ['reveal'],
|
|
139
|
+
setup(props, { slots, emit }) {
|
|
140
|
+
const instance = getCurrentInstance();
|
|
141
|
+
const config = useSmartSkeleton();
|
|
142
|
+
let last: { id?: string; data: unknown } | null = null;
|
|
143
|
+
let shown: string | null = null;
|
|
144
|
+
let from: string | null = null;
|
|
145
|
+
useStyles();
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
const driven = !!instance?.vnode.props && 'data' in instance.vnode.props;
|
|
149
|
+
const data = props.data;
|
|
150
|
+
if (data != null) last = { id: props.id, data };
|
|
151
|
+
|
|
152
|
+
const show = (value: unknown): VNodeChild =>
|
|
153
|
+
driven
|
|
154
|
+
? value != null
|
|
155
|
+
? slots.default?.({ data: value })
|
|
156
|
+
: null
|
|
157
|
+
: slots.default?.({ data: undefined });
|
|
158
|
+
|
|
159
|
+
const loading = !!props.loading || (driven && data == null && !props.error && !props.empty);
|
|
160
|
+
|
|
161
|
+
let key: string;
|
|
162
|
+
let content: VNodeChild;
|
|
163
|
+
if (loading) {
|
|
164
|
+
const previous = last as { id?: string; data: unknown } | null;
|
|
165
|
+
const real = data ?? (previous && previous.id === props.id ? previous.data : null);
|
|
166
|
+
key = 'skeleton';
|
|
167
|
+
content = slots.skeleton
|
|
168
|
+
? h('div', { [ATTR_OWN]: '', 'aria-hidden': 'true' }, slots.skeleton())
|
|
169
|
+
: h(
|
|
170
|
+
Loading,
|
|
171
|
+
{
|
|
172
|
+
id: props.id,
|
|
173
|
+
exact: real != null,
|
|
174
|
+
refetch: data != null,
|
|
175
|
+
label: props.label,
|
|
176
|
+
},
|
|
177
|
+
() => show(real ?? props.sample)
|
|
178
|
+
);
|
|
179
|
+
} else if (props.error) {
|
|
180
|
+
key = 'error';
|
|
181
|
+
content = slots.error?.({ error: props.error });
|
|
182
|
+
} else if (props.empty) {
|
|
183
|
+
key = 'empty';
|
|
184
|
+
content = slots.empty?.() ?? show(data);
|
|
185
|
+
} else {
|
|
186
|
+
key = 'content';
|
|
187
|
+
content = h(
|
|
188
|
+
Content,
|
|
189
|
+
{
|
|
190
|
+
memory: {
|
|
191
|
+
id: props.id,
|
|
192
|
+
data,
|
|
193
|
+
rendered: driven,
|
|
194
|
+
sample: props.sample !== undefined,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
() => show(data)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (key !== shown) {
|
|
202
|
+
from = shown;
|
|
203
|
+
shown = key;
|
|
204
|
+
}
|
|
205
|
+
const revealing = key === 'content' && from === 'skeleton';
|
|
206
|
+
|
|
207
|
+
const reveal = props.reveal ?? config.reveal;
|
|
208
|
+
return h('div', { [ATTR_REVEAL]: '', 'aria-busy': loading || undefined }, [
|
|
209
|
+
h(
|
|
210
|
+
Transition,
|
|
211
|
+
{
|
|
212
|
+
css: false,
|
|
213
|
+
appear: true,
|
|
214
|
+
onAppear: (el, done) => fadeIn(el as HTMLElement, reveal, { initial: true }).then(done),
|
|
215
|
+
onEnter: (el, done) =>
|
|
216
|
+
fadeIn(el as HTMLElement, reveal).then(() => {
|
|
217
|
+
done();
|
|
218
|
+
if (revealing) emit('reveal');
|
|
219
|
+
}),
|
|
220
|
+
onLeave: (el, done) => fadeOut(el as HTMLElement).then(done),
|
|
221
|
+
},
|
|
222
|
+
() => h('div', { key, [ATTR_PANE]: '' }, [content])
|
|
223
|
+
),
|
|
224
|
+
]);
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue';
|
|
3
|
+
import { shimmer, skeletonBoxStyle, ATTR_BAR } from '../core';
|
|
4
|
+
import { useStyles } from '../composables/useStyles';
|
|
5
|
+
import type { ASmartSkeletonBoxProps } from '../types';
|
|
6
|
+
|
|
7
|
+
defineOptions({
|
|
8
|
+
name: 'ASmartSkeletonBox',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const props = defineProps<ASmartSkeletonBoxProps>();
|
|
12
|
+
|
|
13
|
+
const element = ref<HTMLElement | null>(null);
|
|
14
|
+
useStyles();
|
|
15
|
+
|
|
16
|
+
onMounted(() => {
|
|
17
|
+
if (element.value) shimmer(element.value);
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<span ref="element" v-bind="{ [ATTR_BAR]: '' }" :style="skeletonBoxStyle(props)" />
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, provide, watch } from 'vue';
|
|
3
|
+
import { setDebug, skeletonCSS } from '../core';
|
|
4
|
+
import {
|
|
5
|
+
SMART_SKELETON_CONFIG_KEY,
|
|
6
|
+
SMART_SKELETON_BOOT_KEY,
|
|
7
|
+
} from '../composables/useSmartSkeleton';
|
|
8
|
+
import type { ASmartSkeletonProviderProps } from '../types';
|
|
9
|
+
|
|
10
|
+
defineOptions({
|
|
11
|
+
name: 'ASmartSkeletonProvider',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const props = defineProps<ASmartSkeletonProviderProps>();
|
|
15
|
+
|
|
16
|
+
provide(SMART_SKELETON_CONFIG_KEY, props);
|
|
17
|
+
provide(SMART_SKELETON_BOOT_KEY, { sent: false });
|
|
18
|
+
|
|
19
|
+
watch(
|
|
20
|
+
() => props.debug,
|
|
21
|
+
(debug) => {
|
|
22
|
+
if (debug !== undefined && typeof window !== 'undefined') setDebug(debug);
|
|
23
|
+
},
|
|
24
|
+
{ immediate: true }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const css = computed(() => skeletonCSS(props));
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<!-- eslint-disable-next-line vue/no-v-text-v-html-on-component -->
|
|
32
|
+
<component :is="'style'" v-if="css" :nonce="nonce" v-text="css" />
|
|
33
|
+
<slot />
|
|
34
|
+
</template>
|