@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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -2
  3. package/dist/_chunks/core.js +2314 -0
  4. package/dist/_chunks/core.js.map +1 -0
  5. package/dist/_chunks/index.d.ts +431 -0
  6. package/dist/core/index.cjs +67 -0
  7. package/dist/core/index.d.cts +2 -0
  8. package/dist/core/index.d.ts +2 -0
  9. package/dist/core/index.js +2 -0
  10. package/dist/element/index.cjs +47 -0
  11. package/dist/element/index.cjs.map +1 -0
  12. package/dist/element/index.d.cts +24 -0
  13. package/dist/element/index.d.ts +24 -0
  14. package/dist/element/index.js +46 -0
  15. package/dist/element/index.js.map +1 -0
  16. package/dist/index.cjs +512 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +204 -0
  19. package/dist/index.d.ts +204 -0
  20. package/dist/index.js +441 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/nuxt/index.cjs +36 -0
  23. package/dist/nuxt/index.cjs.map +1 -0
  24. package/dist/nuxt/index.d.cts +30 -0
  25. package/dist/nuxt/index.d.ts +30 -0
  26. package/dist/nuxt/index.js +36 -0
  27. package/dist/nuxt/index.js.map +1 -0
  28. package/dist/resolver/index.cjs +26 -0
  29. package/dist/resolver/index.cjs.map +1 -0
  30. package/dist/resolver/index.d.cts +19 -0
  31. package/dist/resolver/index.d.ts +19 -0
  32. package/dist/resolver/index.js +26 -0
  33. package/dist/resolver/index.js.map +1 -0
  34. package/dist/styles.css +136 -0
  35. package/package.json +144 -3
  36. package/src/components/ASmartSkeleton.vue +228 -0
  37. package/src/components/ASmartSkeletonBox.vue +23 -0
  38. package/src/components/ASmartSkeletonProvider.vue +34 -0
  39. package/src/components/ASmartSkeletonSuspense.vue +142 -0
  40. package/src/composables/useSmartSkeleton.ts +20 -0
  41. package/src/composables/useStyles.ts +8 -0
  42. package/src/core/boot.ts +63 -0
  43. package/src/core/canvas.ts +7 -0
  44. package/src/core/constants.ts +76 -0
  45. package/src/core/debug.ts +154 -0
  46. package/src/core/dev.ts +53 -0
  47. package/src/core/fade.ts +490 -0
  48. package/src/core/ghost.ts +236 -0
  49. package/src/core/images.ts +28 -0
  50. package/src/core/index.ts +71 -0
  51. package/src/core/inline.generated.ts +9 -0
  52. package/src/core/measure.ts +571 -0
  53. package/src/core/memory.ts +146 -0
  54. package/src/core/motion.ts +12 -0
  55. package/src/core/plan.ts +368 -0
  56. package/src/core/reveal.ts +89 -0
  57. package/src/core/reveals.ts +39 -0
  58. package/src/core/shapes.ts +84 -0
  59. package/src/core/shimmer.ts +229 -0
  60. package/src/core/skeleton.ts +43 -0
  61. package/src/core/snapshot.ts +159 -0
  62. package/src/core/styles.ts +187 -0
  63. package/src/core/sweep.ts +203 -0
  64. package/src/core/theme.ts +162 -0
  65. package/src/element/index.ts +59 -0
  66. package/src/index.ts +27 -0
  67. package/src/nuxt/index.ts +59 -0
  68. package/src/resolver/index.ts +35 -0
  69. package/src/types.ts +100 -0
  70. package/src/vue/Skeleton.ts +275 -0
  71. package/src/vue/Suspense.ts +157 -0
  72. package/src/vue/config.ts +78 -0
  73. package/src/vue/index.ts +7 -0
  74. 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"}
@@ -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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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>