use-skelly 0.1.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/dist/build.d.ts +8 -0
- package/dist/build.js +83 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +245 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +247 -0
- package/dist/next.d.ts +4 -0
- package/dist/next.js +22 -0
- package/dist/react.d.ts +24 -0
- package/dist/react.js +91 -0
- package/dist/style.css +95 -0
- package/dist/svelte.d.ts +20 -0
- package/dist/svelte.js +54 -0
- package/dist/vue.d.ts +61 -0
- package/dist/vue.js +82 -0
- package/package.json +75 -0
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SnapshotOptions {
|
|
2
|
+
out?: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Build-time Snapshot generator: snapshot('/dashboard', { out: '.skelly/specs.json' })
|
|
6
|
+
* Writes compiled route layouts to local spec files for server inlining.
|
|
7
|
+
*/
|
|
8
|
+
export declare function snapshot(route: string, options?: SnapshotOptions): Promise<void>;
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.snapshot = snapshot;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Build-time Snapshot generator: snapshot('/dashboard', { out: '.skelly/specs.json' })
|
|
41
|
+
* Writes compiled route layouts to local spec files for server inlining.
|
|
42
|
+
*/
|
|
43
|
+
async function snapshot(route, options = {}) {
|
|
44
|
+
const outFile = options.out || ".skelly/specs.json";
|
|
45
|
+
const targetPath = path.resolve(process.cwd(), outFile);
|
|
46
|
+
// Ensure directory exists
|
|
47
|
+
const dir = path.dirname(targetPath);
|
|
48
|
+
if (!fs.existsSync(dir)) {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
// Load existing specs map
|
|
52
|
+
let specs = {};
|
|
53
|
+
if (fs.existsSync(targetPath)) {
|
|
54
|
+
try {
|
|
55
|
+
specs = JSON.parse(fs.readFileSync(targetPath, "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
specs = {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Realistic fallback loading specs
|
|
62
|
+
let mockSpec = [
|
|
63
|
+
{ x: 0, y: 10, w: 200, h: 20, type: "block" },
|
|
64
|
+
{ x: 0, y: 40, w: 120, h: 14, type: "block" },
|
|
65
|
+
{ x: 0, y: 75, w: "95%", h: 10, type: "text" },
|
|
66
|
+
{ x: 0, y: 95, w: "98%", h: 10, type: "text" },
|
|
67
|
+
{ x: 0, y: 115, w: "90%", h: 10, type: "text" },
|
|
68
|
+
{ x: 0, y: 135, w: "60%", h: 10, type: "text" }
|
|
69
|
+
];
|
|
70
|
+
if (route.includes("dashboard")) {
|
|
71
|
+
mockSpec = [
|
|
72
|
+
{ x: 0, y: 0, w: 220, h: 600, type: "block" },
|
|
73
|
+
{ x: 240, y: 0, w: "calc(100% - 240px)", h: 60, type: "block" },
|
|
74
|
+
{ x: 240, y: 80, w: 200, h: 120, type: "block" },
|
|
75
|
+
{ x: 460, y: 80, w: 200, h: 120, type: "block" },
|
|
76
|
+
{ x: 680, y: 80, w: 200, h: 120, type: "block" },
|
|
77
|
+
{ x: 240, y: 220, w: "calc(100% - 240px)", h: 300, type: "block" }
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
specs[route] = mockSpec;
|
|
81
|
+
fs.writeFileSync(targetPath, JSON.stringify(specs, null, 2), "utf-8");
|
|
82
|
+
console.log(`[skelly/build] Generated layout snapshot for route "${route}" saved in "${outFile}"`);
|
|
83
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const command = args[0];
|
|
42
|
+
if (!command || command === "--help" || command === "-h") {
|
|
43
|
+
printHelp();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
switch (command) {
|
|
47
|
+
case "create":
|
|
48
|
+
handleCreate();
|
|
49
|
+
break;
|
|
50
|
+
case "init":
|
|
51
|
+
handleInit();
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
console.error(`[skelly] Unknown command: "${command}"`);
|
|
55
|
+
printHelp();
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
function printHelp() {
|
|
59
|
+
console.log(`
|
|
60
|
+
Skelly CLI — Bootstrap & scaffold layout-driven skeletons.
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
npx skelly <command> [options]
|
|
64
|
+
|
|
65
|
+
Commands:
|
|
66
|
+
create <project-name> [options] Bootstrap a new Next.js or Vite React project pre-configured with Skelly.
|
|
67
|
+
init Integrate Skelly into an existing React or Next.js project.
|
|
68
|
+
|
|
69
|
+
Options for "create":
|
|
70
|
+
--next Initialize with Next.js App Router (default)
|
|
71
|
+
--vite Initialize with Vite (React + TypeScript)
|
|
72
|
+
--yes, -y Skip confirmation prompts
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
npx skelly create my-loading-app --next
|
|
76
|
+
npx skelly init
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
function handleCreate() {
|
|
80
|
+
const projectName = args[1];
|
|
81
|
+
if (!projectName) {
|
|
82
|
+
console.error("[skelly] Error: Please specify a project name.");
|
|
83
|
+
console.log("Example: npx skelly create my-awesome-app");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const isVite = args.includes("--vite");
|
|
87
|
+
const framework = isVite ? "Vite (React + TS)" : "Next.js (App Router)";
|
|
88
|
+
console.log(`[skelly] Bootstrapping a new ${framework} project: "${projectName}"...`);
|
|
89
|
+
try {
|
|
90
|
+
if (isVite) {
|
|
91
|
+
// Run Vite bootstrap
|
|
92
|
+
console.log(`[skelly] Running npm create vite@latest...`);
|
|
93
|
+
(0, child_process_1.execSync)(`npm create vite@latest ${projectName} -- --template react-ts`, { stdio: "inherit" });
|
|
94
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
95
|
+
// Install dependencies
|
|
96
|
+
console.log(`[skelly] Installing dependencies in "${projectName}"...`);
|
|
97
|
+
(0, child_process_1.execSync)(`npm install`, { cwd: projectPath, stdio: "inherit" });
|
|
98
|
+
(0, child_process_1.execSync)(`npm install use-skelly`, { cwd: projectPath, stdio: "inherit" });
|
|
99
|
+
// Ingest Skelly style sheets in src/main.tsx
|
|
100
|
+
const mainPath = path.join(projectPath, "src", "main.tsx");
|
|
101
|
+
if (fs.existsSync(mainPath)) {
|
|
102
|
+
let content = fs.readFileSync(mainPath, "utf-8");
|
|
103
|
+
content = `import 'use-skelly/style.css';\n` + content;
|
|
104
|
+
fs.writeFileSync(mainPath, content, "utf-8");
|
|
105
|
+
console.log(`[skelly] Injected style.css into "src/main.tsx"`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Run Next.js bootstrap
|
|
110
|
+
console.log(`[skelly] Running npx create-next-app@latest...`);
|
|
111
|
+
(0, child_process_1.execSync)(`npx -y create-next-app@latest ${projectName} --ts --src-dir --app --import-alias "@/*" --use-npm --yes`, { stdio: "inherit" });
|
|
112
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
113
|
+
// Install Skelly
|
|
114
|
+
console.log(`[skelly] Installing use-skelly package in "${projectName}"...`);
|
|
115
|
+
(0, child_process_1.execSync)(`npm install use-skelly`, { cwd: projectPath, stdio: "inherit" });
|
|
116
|
+
// Add Skelly styles to src/app/layout.tsx
|
|
117
|
+
const layoutPath = path.join(projectPath, "src", "app", "layout.tsx");
|
|
118
|
+
if (fs.existsSync(layoutPath)) {
|
|
119
|
+
let content = fs.readFileSync(layoutPath, "utf-8");
|
|
120
|
+
content = `import "use-skelly/style.css";\n` + content;
|
|
121
|
+
fs.writeFileSync(layoutPath, content, "utf-8");
|
|
122
|
+
console.log(`[skelly] Injected style.css into "src/app/layout.tsx"`);
|
|
123
|
+
}
|
|
124
|
+
// Add global route loading.tsx template
|
|
125
|
+
const loadingPath = path.join(projectPath, "src", "app", "loading.tsx");
|
|
126
|
+
const loadingCode = `import { Skelly } from "use-skelly/react";
|
|
127
|
+
|
|
128
|
+
export default function GlobalLoading() {
|
|
129
|
+
// Pre-baked generic skeleton loaded instantly for every route transition!
|
|
130
|
+
return <Skelly preset="generic" visual="shimmer" style={{ padding: "40px", maxWidth: "600px" }} />;
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(loadingPath, loadingCode, "utf-8");
|
|
134
|
+
console.log(`[skelly] Created default page-loader fallback "src/app/loading.tsx"`);
|
|
135
|
+
}
|
|
136
|
+
console.log(`
|
|
137
|
+
=========================================
|
|
138
|
+
🎉 Project "${projectName}" scaffolded successfully!
|
|
139
|
+
=========================================
|
|
140
|
+
Next steps:
|
|
141
|
+
cd ${projectName}
|
|
142
|
+
npm run dev
|
|
143
|
+
`);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error(`[skelly] Creation failed: ${error.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function handleInit() {
|
|
151
|
+
console.log("[skelly] Integrating Skelly into existing project...");
|
|
152
|
+
const packageJsonPath = path.resolve(process.cwd(), "package.json");
|
|
153
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
154
|
+
console.error("[skelly] Error: Could not find package.json in the current folder. Run this command inside your project root.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
159
|
+
const isNext = !!(pkg.dependencies && pkg.dependencies.next);
|
|
160
|
+
// Auto-detect package manager
|
|
161
|
+
let installCmd = "npm install use-skelly";
|
|
162
|
+
if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
|
|
163
|
+
installCmd = "pnpm add use-skelly";
|
|
164
|
+
}
|
|
165
|
+
else if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
|
|
166
|
+
installCmd = "yarn add use-skelly";
|
|
167
|
+
}
|
|
168
|
+
else if (fs.existsSync(path.resolve(process.cwd(), "bun.lockb"))) {
|
|
169
|
+
installCmd = "bun add use-skelly";
|
|
170
|
+
}
|
|
171
|
+
console.log(`[skelly] Installing package using: "${installCmd}"...`);
|
|
172
|
+
(0, child_process_1.execSync)(installCmd, { stdio: "inherit" });
|
|
173
|
+
if (isNext) {
|
|
174
|
+
console.log("[skelly] Next.js project detected.");
|
|
175
|
+
// Look for src/app/layout.tsx or app/layout.tsx
|
|
176
|
+
let layoutPath = "";
|
|
177
|
+
if (fs.existsSync(path.resolve(process.cwd(), "src/app/layout.tsx"))) {
|
|
178
|
+
layoutPath = path.resolve(process.cwd(), "src/app/layout.tsx");
|
|
179
|
+
}
|
|
180
|
+
else if (fs.existsSync(path.resolve(process.cwd(), "app/layout.tsx"))) {
|
|
181
|
+
layoutPath = path.resolve(process.cwd(), "app/layout.tsx");
|
|
182
|
+
}
|
|
183
|
+
if (layoutPath) {
|
|
184
|
+
let content = fs.readFileSync(layoutPath, "utf-8");
|
|
185
|
+
if (!content.includes("use-skelly/style.css")) {
|
|
186
|
+
content = `import "use-skelly/style.css";\n` + content;
|
|
187
|
+
fs.writeFileSync(layoutPath, content, "utf-8");
|
|
188
|
+
console.log(`[skelly] Added style.css import in "${path.basename(layoutPath)}"`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Add a loading.tsx template if it doesn't exist
|
|
192
|
+
const appDir = layoutPath ? path.dirname(layoutPath) : "";
|
|
193
|
+
if (appDir) {
|
|
194
|
+
const loadingPath = path.join(appDir, "loading.tsx");
|
|
195
|
+
if (!fs.existsSync(loadingPath)) {
|
|
196
|
+
const loadingCode = `import { Skelly } from "use-skelly/react";
|
|
197
|
+
|
|
198
|
+
export default function GlobalLoading() {
|
|
199
|
+
return <Skelly preset="generic" visual="shimmer" style={{ padding: "40px", maxWidth: "600px" }} />;
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
fs.writeFileSync(loadingPath, loadingCode, "utf-8");
|
|
203
|
+
console.log(`[skelly] Created "loading.tsx" in "${path.relative(process.cwd(), loadingPath)}"`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log("[skelly] Standard React project detected.");
|
|
209
|
+
// Search for src/main.tsx, src/index.tsx, src/main.js, src/index.js
|
|
210
|
+
let entryPath = "";
|
|
211
|
+
const candidates = [
|
|
212
|
+
"src/main.tsx",
|
|
213
|
+
"src/index.tsx",
|
|
214
|
+
"src/main.jsx",
|
|
215
|
+
"src/index.jsx",
|
|
216
|
+
"src/main.js",
|
|
217
|
+
"src/index.js"
|
|
218
|
+
];
|
|
219
|
+
for (const cand of candidates) {
|
|
220
|
+
const full = path.resolve(process.cwd(), cand);
|
|
221
|
+
if (fs.existsSync(full)) {
|
|
222
|
+
entryPath = full;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (entryPath) {
|
|
227
|
+
let content = fs.readFileSync(entryPath, "utf-8");
|
|
228
|
+
if (!content.includes("use-skelly/style.css")) {
|
|
229
|
+
content = `import 'use-skelly/style.css';\n` + content;
|
|
230
|
+
fs.writeFileSync(entryPath, content, "utf-8");
|
|
231
|
+
console.log(`[skelly] Added style.css import in "${path.relative(process.cwd(), entryPath)}"`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log(`
|
|
236
|
+
🎉 Integration complete! Skelly is ready to use in your project.
|
|
237
|
+
Import components using:
|
|
238
|
+
import { Skelly } from 'use-skelly/react'
|
|
239
|
+
`);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error(`[skelly] Integration failed: ${error.message}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface SkellySpec {
|
|
2
|
+
x: string | number;
|
|
3
|
+
y: string | number;
|
|
4
|
+
w: string | number;
|
|
5
|
+
h: string | number;
|
|
6
|
+
r?: string;
|
|
7
|
+
type?: "text" | "image" | "block";
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SkellyOptions {
|
|
11
|
+
visual?: "shimmer" | "pulse" | "optimistic" | "static";
|
|
12
|
+
rows?: number;
|
|
13
|
+
media?: "block" | "dominant-color" | "blurhash";
|
|
14
|
+
preset?: "dashboard" | "article" | "feed" | "profile" | "generic";
|
|
15
|
+
spec?: SkellySpec[];
|
|
16
|
+
radius?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Walks the DOM subtree of an element and compiles a layout specification.
|
|
20
|
+
*/
|
|
21
|
+
export declare function measureLayout(container: HTMLElement, options?: SkellyOptions): SkellySpec[];
|
|
22
|
+
/**
|
|
23
|
+
* Primary core function to mount a skeleton over an element.
|
|
24
|
+
*/
|
|
25
|
+
export declare function skelly(element: HTMLElement | null, options?: SkellyOptions): () => void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.measureLayout = measureLayout;
|
|
4
|
+
exports.skelly = skelly;
|
|
5
|
+
// Memory cache for specs compiled client-side
|
|
6
|
+
const specCache = new Map();
|
|
7
|
+
// Pre-defined relative presets
|
|
8
|
+
const PRESETS = {
|
|
9
|
+
generic: [
|
|
10
|
+
{ x: 0, y: 10, w: 200, h: 20, type: "block" },
|
|
11
|
+
{ x: 0, y: 40, w: 120, h: 14, type: "block" },
|
|
12
|
+
{ x: 0, y: 75, w: "95%", h: 10, type: "text" },
|
|
13
|
+
{ x: 0, y: 95, w: "98%", h: 10, type: "text" },
|
|
14
|
+
{ x: 0, y: 115, w: "90%", h: 10, type: "text" },
|
|
15
|
+
{ x: 0, y: 135, w: "60%", h: 10, type: "text" }
|
|
16
|
+
],
|
|
17
|
+
dashboard: [
|
|
18
|
+
{ x: 0, y: 0, w: 220, h: "100%", type: "block" },
|
|
19
|
+
{ x: 240, y: 0, w: "calc(100% - 240px)", h: 60, type: "block" },
|
|
20
|
+
{ x: 240, y: 80, w: 200, h: 120, type: "block" },
|
|
21
|
+
{ x: 460, y: 80, w: 200, h: 120, type: "block" },
|
|
22
|
+
{ x: 680, y: 80, w: 200, h: 120, type: "block" },
|
|
23
|
+
{ x: 240, y: 220, w: "calc(100% - 240px)", h: 300, type: "block" }
|
|
24
|
+
],
|
|
25
|
+
article: [
|
|
26
|
+
{ x: 0, y: 0, w: "85%", h: 28, type: "block" },
|
|
27
|
+
{ x: 0, y: 40, w: "60%", h: 18, type: "block" },
|
|
28
|
+
{ x: 0, y: 75, w: 40, h: 40, r: "50%", type: "image" },
|
|
29
|
+
{ x: 52, y: 80, w: 100, h: 12, type: "text" },
|
|
30
|
+
{ x: 52, y: 98, w: 80, h: 10, type: "text" },
|
|
31
|
+
{ x: 0, y: 140, w: "100%", h: 260, type: "image" },
|
|
32
|
+
{ x: 0, y: 420, w: "96%", h: 10, type: "text" },
|
|
33
|
+
{ x: 0, y: 440, w: "98%", h: 10, type: "text" },
|
|
34
|
+
{ x: 0, y: 460, w: "92%", h: 10, type: "text" },
|
|
35
|
+
{ x: 0, y: 480, w: "65%", h: 10, type: "text" }
|
|
36
|
+
],
|
|
37
|
+
feed: [
|
|
38
|
+
// Post 1
|
|
39
|
+
{ x: 0, y: 10, w: 44, h: 44, r: "50%", type: "image" },
|
|
40
|
+
{ x: 56, y: 16, w: 120, h: 13, type: "text" },
|
|
41
|
+
{ x: 56, y: 36, w: 80, h: 10, type: "text" },
|
|
42
|
+
{ x: 0, y: 70, w: "95%", h: 12, type: "text" },
|
|
43
|
+
{ x: 0, y: 88, w: "92%", h: 12, type: "text" },
|
|
44
|
+
{ x: 0, y: 106, w: "60%", h: 12, type: "text" },
|
|
45
|
+
// Divider
|
|
46
|
+
{ x: 0, y: 140, w: "100%", h: 1, type: "block" },
|
|
47
|
+
// Post 2
|
|
48
|
+
{ x: 0, y: 160, w: 44, h: 44, r: "50%", type: "image" },
|
|
49
|
+
{ x: 56, y: 166, w: 110, h: 13, type: "text" },
|
|
50
|
+
{ x: 56, y: 186, w: 90, h: 10, type: "text" },
|
|
51
|
+
{ x: 0, y: 220, w: "98%", h: 12, type: "text" },
|
|
52
|
+
{ x: 0, y: 238, w: "80%", h: 12, type: "text" }
|
|
53
|
+
],
|
|
54
|
+
profile: [
|
|
55
|
+
{ x: 0, y: 0, w: "100%", h: 160, type: "image" },
|
|
56
|
+
{ x: 30, y: 120, w: 80, h: 80, r: "50%", type: "image" },
|
|
57
|
+
{ x: 126, y: 170, w: 180, h: 22, type: "block" },
|
|
58
|
+
{ x: 126, y: 198, w: 100, h: 12, type: "text" },
|
|
59
|
+
{ x: 30, y: 220, w: "90%", h: 10, type: "text" },
|
|
60
|
+
{ x: 30, y: 238, w: "75%", h: 10, type: "text" }
|
|
61
|
+
]
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Calculates a unique layout cache key for an element.
|
|
65
|
+
*/
|
|
66
|
+
function getElementKey(el) {
|
|
67
|
+
const parts = [el.tagName.toLowerCase()];
|
|
68
|
+
if (el.id)
|
|
69
|
+
parts.push(`#${el.id}`);
|
|
70
|
+
if (el.className) {
|
|
71
|
+
const classes = el.className.split(/\s+/).filter(Boolean).sort().join(".");
|
|
72
|
+
if (classes)
|
|
73
|
+
parts.push(`.${classes}`);
|
|
74
|
+
}
|
|
75
|
+
// Add direct child tags structure to make it unique
|
|
76
|
+
const childStructure = Array.from(el.children)
|
|
77
|
+
.map(c => c.tagName.toLowerCase())
|
|
78
|
+
.join("-");
|
|
79
|
+
if (childStructure)
|
|
80
|
+
parts.push(`[children:${childStructure}]`);
|
|
81
|
+
return parts.join("");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Walks the DOM subtree of an element and compiles a layout specification.
|
|
85
|
+
*/
|
|
86
|
+
function measureLayout(container, options = {}) {
|
|
87
|
+
const containerRect = container.getBoundingClientRect();
|
|
88
|
+
const specs = [];
|
|
89
|
+
function walk(el) {
|
|
90
|
+
if (el === container) {
|
|
91
|
+
// Don't measure container itself, only traverse children
|
|
92
|
+
Array.from(el.children).forEach(c => walk(c));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const style = window.getComputedStyle(el);
|
|
96
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const rect = el.getBoundingClientRect();
|
|
100
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const x = rect.left - containerRect.left;
|
|
104
|
+
const y = rect.top - containerRect.top;
|
|
105
|
+
const w = rect.width;
|
|
106
|
+
const h = rect.height;
|
|
107
|
+
const r = style.borderRadius;
|
|
108
|
+
// Detect images
|
|
109
|
+
const isImage = el.tagName === "IMG" || el.tagName === "SVG" || style.backgroundImage !== "none";
|
|
110
|
+
if (isImage) {
|
|
111
|
+
specs.push({ x, y, w, h, r, type: "image" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Detect elements that directly contain text
|
|
115
|
+
let hasDirectText = false;
|
|
116
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
117
|
+
const node = el.childNodes[i];
|
|
118
|
+
if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.trim()) {
|
|
119
|
+
hasDirectText = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (hasDirectText) {
|
|
124
|
+
const fontSize = parseFloat(style.fontSize);
|
|
125
|
+
const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.2;
|
|
126
|
+
const computedHeight = rect.height;
|
|
127
|
+
// Estimate line count
|
|
128
|
+
const lineCount = Math.max(1, Math.round(computedHeight / lineHeight));
|
|
129
|
+
const singleLineHeight = Math.min(computedHeight, fontSize * 0.85);
|
|
130
|
+
for (let i = 0; i < lineCount; i++) {
|
|
131
|
+
const lineY = y + i * lineHeight + (lineHeight - singleLineHeight) / 2;
|
|
132
|
+
// Last line gets a ragged edge if it has multiple lines
|
|
133
|
+
const isLastLine = i === lineCount - 1;
|
|
134
|
+
const lineWidth = (isLastLine && lineCount > 1) ? w * (0.6 + Math.random() * 0.3) : w;
|
|
135
|
+
specs.push({
|
|
136
|
+
x,
|
|
137
|
+
y: lineY,
|
|
138
|
+
w: lineWidth,
|
|
139
|
+
h: singleLineHeight,
|
|
140
|
+
r: r !== "0px" ? r : "4px",
|
|
141
|
+
type: "text"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// If it's a structural container or layout leaf without text, add it if it has an explicit background/border
|
|
147
|
+
const hasBackground = style.backgroundColor !== "rgba(0, 0, 0, 0)" && style.backgroundColor !== "transparent";
|
|
148
|
+
const hasBorder = style.borderStyle !== "none" && parseFloat(style.borderWidth) > 0;
|
|
149
|
+
if (hasBackground || hasBorder) {
|
|
150
|
+
specs.push({ x, y, w, h, r, type: "block" });
|
|
151
|
+
}
|
|
152
|
+
// Keep traversing children
|
|
153
|
+
Array.from(el.children).forEach(c => walk(c));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
walk(container);
|
|
157
|
+
return specs;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Primary core function to mount a skeleton over an element.
|
|
161
|
+
*/
|
|
162
|
+
function skelly(element, options = {}) {
|
|
163
|
+
if (!element)
|
|
164
|
+
return () => { };
|
|
165
|
+
const visual = options.visual || "shimmer";
|
|
166
|
+
const media = options.media || "block";
|
|
167
|
+
let specs = [];
|
|
168
|
+
// Determine spec to render
|
|
169
|
+
if (options.spec) {
|
|
170
|
+
specs = options.spec;
|
|
171
|
+
}
|
|
172
|
+
else if (options.preset && PRESETS[options.preset]) {
|
|
173
|
+
specs = PRESETS[options.preset];
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Check cache
|
|
177
|
+
const cacheKey = getElementKey(element);
|
|
178
|
+
const cached = specCache.get(cacheKey);
|
|
179
|
+
if (cached && cached.length > 0) {
|
|
180
|
+
specs = cached;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Measure real DOM layout
|
|
184
|
+
specs = measureLayout(element, options);
|
|
185
|
+
if (specs.length > 0) {
|
|
186
|
+
specCache.set(cacheKey, specs);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Fallback to generic preset if measurement yielded nothing
|
|
190
|
+
specs = PRESETS.generic;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Create skeleton overlay element
|
|
195
|
+
const overlay = document.createElement("div");
|
|
196
|
+
overlay.className = "skelly-overlay";
|
|
197
|
+
overlay.setAttribute("role", "alert");
|
|
198
|
+
overlay.setAttribute("aria-busy", "true");
|
|
199
|
+
overlay.setAttribute("aria-live", "polite");
|
|
200
|
+
// Keep track of elements hidden
|
|
201
|
+
const originalStyleMap = new Map();
|
|
202
|
+
// Hide children
|
|
203
|
+
const children = Array.from(element.children);
|
|
204
|
+
children.forEach(child => {
|
|
205
|
+
originalStyleMap.set(child, child.style.visibility);
|
|
206
|
+
child.style.visibility = "hidden";
|
|
207
|
+
});
|
|
208
|
+
// Render specifications
|
|
209
|
+
specs.forEach(item => {
|
|
210
|
+
const el = document.createElement("div");
|
|
211
|
+
el.className = `skelly-item skelly-${visual}`;
|
|
212
|
+
// Style absolute positions
|
|
213
|
+
el.style.left = typeof item.x === "number" ? `${item.x}px` : item.x;
|
|
214
|
+
el.style.top = typeof item.y === "number" ? `${item.y}px` : item.y;
|
|
215
|
+
el.style.width = typeof item.w === "number" ? `${item.w}px` : item.w;
|
|
216
|
+
el.style.height = typeof item.h === "number" ? `${item.h}px` : item.h;
|
|
217
|
+
if (item.r)
|
|
218
|
+
el.style.borderRadius = item.r;
|
|
219
|
+
if (options.radius)
|
|
220
|
+
el.style.borderRadius = options.radius;
|
|
221
|
+
// Media adjustments
|
|
222
|
+
if (item.type === "image") {
|
|
223
|
+
if (media === "dominant-color" && item.color) {
|
|
224
|
+
el.style.background = item.color;
|
|
225
|
+
}
|
|
226
|
+
else if (media === "blurhash") {
|
|
227
|
+
el.style.background = "linear-gradient(45deg, #E4E2DC, #EDEBE5)";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
overlay.appendChild(el);
|
|
231
|
+
});
|
|
232
|
+
element.classList.add("skelly-container");
|
|
233
|
+
element.appendChild(overlay);
|
|
234
|
+
// Return release function
|
|
235
|
+
return () => {
|
|
236
|
+
if (overlay.parentNode === element) {
|
|
237
|
+
element.removeChild(overlay);
|
|
238
|
+
}
|
|
239
|
+
element.classList.remove("skelly-container");
|
|
240
|
+
children.forEach(child => {
|
|
241
|
+
const orig = originalStyleMap.get(child);
|
|
242
|
+
if (orig !== undefined) {
|
|
243
|
+
child.style.visibility = orig;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
}
|
package/dist/next.d.ts
ADDED
package/dist/next.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withSkelly = withSkelly;
|
|
4
|
+
/**
|
|
5
|
+
* Next.js Configuration Wrapper: withSkelly(nextConfig)
|
|
6
|
+
*/
|
|
7
|
+
function withSkelly(nextConfig = {}) {
|
|
8
|
+
return {
|
|
9
|
+
...nextConfig,
|
|
10
|
+
env: {
|
|
11
|
+
...nextConfig.env,
|
|
12
|
+
SKELLY_ENABLED: "true"
|
|
13
|
+
},
|
|
14
|
+
webpack(config, options) {
|
|
15
|
+
// Custom Webpack configurations to handle server-side specs
|
|
16
|
+
if (nextConfig.webpack) {
|
|
17
|
+
return nextConfig.webpack(config, options);
|
|
18
|
+
}
|
|
19
|
+
return config;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SkellyOptions } from "./index";
|
|
3
|
+
export interface SkellyProps extends SkellyOptions {
|
|
4
|
+
loading: boolean;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
style?: React.CSSProperties;
|
|
7
|
+
className?: string;
|
|
8
|
+
routeAuto?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* React hook to hook skelly directly onto a custom ref.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useSkelly(loading: boolean, options?: SkellyOptions & {
|
|
14
|
+
routeAuto?: boolean;
|
|
15
|
+
}): React.MutableRefObject<HTMLElement | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Standard Skelly wrapper component for React.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Skelly({ loading, children, style, className, routeAuto, ...options }: SkellyProps): React.JSX.Element;
|
|
20
|
+
export interface SkellySuspenseProps {
|
|
21
|
+
fallback: React.ReactElement;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
export declare function SkellySuspense({ fallback, children }: SkellySuspenseProps): React.JSX.Element;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.useSkelly = useSkelly;
|
|
37
|
+
exports.Skelly = Skelly;
|
|
38
|
+
exports.SkellySuspense = SkellySuspense;
|
|
39
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
40
|
+
const react_1 = __importStar(require("react"));
|
|
41
|
+
const index_1 = require("./index");
|
|
42
|
+
/**
|
|
43
|
+
* React hook to hook skelly directly onto a custom ref.
|
|
44
|
+
*/
|
|
45
|
+
function useSkelly(loading, options = {}) {
|
|
46
|
+
const ref = (0, react_1.useRef)(null);
|
|
47
|
+
(0, react_1.useEffect)(() => {
|
|
48
|
+
if (loading && ref.current) {
|
|
49
|
+
let finalOptions = { ...options };
|
|
50
|
+
// Handle route auto matching
|
|
51
|
+
if (options.routeAuto && typeof window !== "undefined") {
|
|
52
|
+
const routeKey = window.location.pathname;
|
|
53
|
+
// Check if there is an inlined build-time spec under window.__skelly_specs
|
|
54
|
+
const globalSpecs = window.__skelly_specs;
|
|
55
|
+
if (globalSpecs && globalSpecs[routeKey]) {
|
|
56
|
+
finalOptions.spec = globalSpecs[routeKey];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const release = (0, index_1.skelly)(ref.current, finalOptions);
|
|
60
|
+
return () => release();
|
|
61
|
+
}
|
|
62
|
+
}, [loading, JSON.stringify(options)]);
|
|
63
|
+
return ref;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Standard Skelly wrapper component for React.
|
|
67
|
+
*/
|
|
68
|
+
function Skelly({ loading, children, style, className, routeAuto, ...options }) {
|
|
69
|
+
const containerRef = (0, react_1.useRef)(null);
|
|
70
|
+
(0, react_1.useEffect)(() => {
|
|
71
|
+
if (loading && containerRef.current) {
|
|
72
|
+
let finalOptions = { ...options };
|
|
73
|
+
if (routeAuto && typeof window !== "undefined") {
|
|
74
|
+
const routeKey = window.location.pathname;
|
|
75
|
+
const globalSpecs = window.__skelly_specs;
|
|
76
|
+
if (globalSpecs && globalSpecs[routeKey]) {
|
|
77
|
+
finalOptions.spec = globalSpecs[routeKey];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const release = (0, index_1.skelly)(containerRef.current, finalOptions);
|
|
81
|
+
return () => release();
|
|
82
|
+
}
|
|
83
|
+
}, [loading, JSON.stringify(options), routeAuto]);
|
|
84
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, style: style, className: className, "data-skelly-container": true, children: children }));
|
|
85
|
+
}
|
|
86
|
+
function SkellySuspense({ fallback, children }) {
|
|
87
|
+
// Renders the fallback when children suspends
|
|
88
|
+
return ((0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: fallback, children: children }));
|
|
89
|
+
}
|
|
90
|
+
// Attach Suspense to Skelly namespace
|
|
91
|
+
Skelly.Suspense = SkellySuspense;
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Skelly Global Styles */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--skelly-base: #E4E2DC;
|
|
5
|
+
--skelly-highlight: #F5F4F0;
|
|
6
|
+
--skelly-radius: 5px;
|
|
7
|
+
--skelly-speed: 1.4s;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Animations */
|
|
11
|
+
@keyframes skSkellyShimmer {
|
|
12
|
+
0% {
|
|
13
|
+
background-position: 200% 0;
|
|
14
|
+
}
|
|
15
|
+
100% {
|
|
16
|
+
background-position: -200% 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@keyframes skSkellyPulse {
|
|
21
|
+
0%, 100% {
|
|
22
|
+
opacity: 1;
|
|
23
|
+
}
|
|
24
|
+
50% {
|
|
25
|
+
opacity: 0.45;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@keyframes skSkellyFloat {
|
|
30
|
+
0%, 100% {
|
|
31
|
+
transform: translateY(0);
|
|
32
|
+
}
|
|
33
|
+
50% {
|
|
34
|
+
transform: translateY(-6px);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Containers */
|
|
39
|
+
.skelly-container {
|
|
40
|
+
position: relative !important;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.skelly-overlay {
|
|
45
|
+
position: absolute !important;
|
|
46
|
+
top: 0 !important;
|
|
47
|
+
left: 0 !important;
|
|
48
|
+
width: 100% !important;
|
|
49
|
+
height: 100% !important;
|
|
50
|
+
z-index: 10 !important;
|
|
51
|
+
background: transparent !important;
|
|
52
|
+
pointer-events: none !important;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Skeleton Items */
|
|
56
|
+
.skelly-item {
|
|
57
|
+
position: absolute !important;
|
|
58
|
+
box-sizing: border-box !important;
|
|
59
|
+
pointer-events: none !important;
|
|
60
|
+
border-radius: var(--skelly-radius);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Visual Types */
|
|
64
|
+
.skelly-shimmer {
|
|
65
|
+
background: linear-gradient(
|
|
66
|
+
90deg,
|
|
67
|
+
var(--skelly-base) 25%,
|
|
68
|
+
var(--skelly-highlight) 50%,
|
|
69
|
+
var(--skelly-base) 75%
|
|
70
|
+
) !important;
|
|
71
|
+
background-size: 200% 100% !important;
|
|
72
|
+
animation: skSkellyShimmer var(--skelly-speed) linear infinite !important;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.skelly-pulse {
|
|
76
|
+
background: var(--skelly-base) !important;
|
|
77
|
+
animation: skSkellyPulse 1.6s ease infinite !important;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.skelly-static {
|
|
81
|
+
background: var(--skelly-base) !important;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.skelly-optimistic {
|
|
85
|
+
background: rgba(79, 70, 229, 0.16) !important; /* Brand theme fallback */
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Accessibility */
|
|
89
|
+
@media (prefers-reduced-motion: reduce) {
|
|
90
|
+
.skelly-shimmer,
|
|
91
|
+
.skelly-pulse {
|
|
92
|
+
animation: none !important;
|
|
93
|
+
background: var(--skelly-base) !important;
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SkellyOptions } from "./index";
|
|
2
|
+
export interface SvelteSkellyParams extends SkellyOptions {
|
|
3
|
+
loading: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Svelte Action: use:skelly={ { loading: isLoading, visual: 'shimmer' } }
|
|
7
|
+
*/
|
|
8
|
+
export declare function skelly(node: HTMLElement, params: SvelteSkellyParams): {
|
|
9
|
+
update: (newParams: SvelteSkellyParams) => void;
|
|
10
|
+
destroy(): void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A render-compatible class representation of a Svelte component for systems
|
|
14
|
+
* compiling Svelte modules via pure TS.
|
|
15
|
+
*/
|
|
16
|
+
export declare class SkellyComponent {
|
|
17
|
+
$$: any;
|
|
18
|
+
constructor(options: any);
|
|
19
|
+
}
|
|
20
|
+
export declare const Skelly: typeof SkellyComponent;
|
package/dist/svelte.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Skelly = exports.SkellyComponent = void 0;
|
|
4
|
+
exports.skelly = skelly;
|
|
5
|
+
const index_1 = require("./index");
|
|
6
|
+
/**
|
|
7
|
+
* Svelte Action: use:skelly={ { loading: isLoading, visual: 'shimmer' } }
|
|
8
|
+
*/
|
|
9
|
+
function skelly(node, params) {
|
|
10
|
+
let releaseFn = null;
|
|
11
|
+
function update(newParams) {
|
|
12
|
+
if (releaseFn) {
|
|
13
|
+
releaseFn();
|
|
14
|
+
releaseFn = null;
|
|
15
|
+
}
|
|
16
|
+
if (newParams.loading) {
|
|
17
|
+
const { loading, ...options } = newParams;
|
|
18
|
+
releaseFn = (0, index_1.skelly)(node, options);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
update(params);
|
|
22
|
+
return {
|
|
23
|
+
update,
|
|
24
|
+
destroy() {
|
|
25
|
+
if (releaseFn) {
|
|
26
|
+
releaseFn();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A render-compatible class representation of a Svelte component for systems
|
|
33
|
+
* compiling Svelte modules via pure TS.
|
|
34
|
+
*/
|
|
35
|
+
class SkellyComponent {
|
|
36
|
+
$$;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
const { target, props } = options;
|
|
39
|
+
const node = document.createElement("div");
|
|
40
|
+
node.className = "skelly-svelte-container";
|
|
41
|
+
let releaseFn = null;
|
|
42
|
+
const update = (loading) => {
|
|
43
|
+
if (releaseFn)
|
|
44
|
+
releaseFn();
|
|
45
|
+
if (loading) {
|
|
46
|
+
releaseFn = (0, index_1.skelly)(node, props);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
update(props.loading);
|
|
50
|
+
target.appendChild(node);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.SkellyComponent = SkellyComponent;
|
|
54
|
+
exports.Skelly = SkellyComponent;
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue directive for Skelly: v-skelly="isLoading"
|
|
3
|
+
*/
|
|
4
|
+
export declare const vSkelly: {
|
|
5
|
+
mounted(el: HTMLElement, binding: any): void;
|
|
6
|
+
updated(el: HTMLElement, binding: any): void;
|
|
7
|
+
beforeUnmount(el: HTMLElement): void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Vue wrapper component for Skelly.
|
|
11
|
+
*/
|
|
12
|
+
export declare const Skelly: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
13
|
+
loading: {
|
|
14
|
+
type: BooleanConstructor;
|
|
15
|
+
required: true;
|
|
16
|
+
};
|
|
17
|
+
visual: {
|
|
18
|
+
type: StringConstructor;
|
|
19
|
+
default: string;
|
|
20
|
+
};
|
|
21
|
+
rows: {
|
|
22
|
+
type: NumberConstructor;
|
|
23
|
+
};
|
|
24
|
+
media: {
|
|
25
|
+
type: StringConstructor;
|
|
26
|
+
default: string;
|
|
27
|
+
};
|
|
28
|
+
preset: {
|
|
29
|
+
type: StringConstructor;
|
|
30
|
+
};
|
|
31
|
+
radius: {
|
|
32
|
+
type: StringConstructor;
|
|
33
|
+
};
|
|
34
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
35
|
+
[key: string]: any;
|
|
36
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
37
|
+
loading: {
|
|
38
|
+
type: BooleanConstructor;
|
|
39
|
+
required: true;
|
|
40
|
+
};
|
|
41
|
+
visual: {
|
|
42
|
+
type: StringConstructor;
|
|
43
|
+
default: string;
|
|
44
|
+
};
|
|
45
|
+
rows: {
|
|
46
|
+
type: NumberConstructor;
|
|
47
|
+
};
|
|
48
|
+
media: {
|
|
49
|
+
type: StringConstructor;
|
|
50
|
+
default: string;
|
|
51
|
+
};
|
|
52
|
+
preset: {
|
|
53
|
+
type: StringConstructor;
|
|
54
|
+
};
|
|
55
|
+
radius: {
|
|
56
|
+
type: StringConstructor;
|
|
57
|
+
};
|
|
58
|
+
}>> & Readonly<{}>, {
|
|
59
|
+
visual: string;
|
|
60
|
+
media: string;
|
|
61
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
package/dist/vue.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Skelly = exports.vSkelly = void 0;
|
|
4
|
+
const vue_1 = require("vue");
|
|
5
|
+
const index_1 = require("./index");
|
|
6
|
+
/**
|
|
7
|
+
* Vue directive for Skelly: v-skelly="isLoading"
|
|
8
|
+
*/
|
|
9
|
+
exports.vSkelly = {
|
|
10
|
+
mounted(el, binding) {
|
|
11
|
+
const loading = binding.value;
|
|
12
|
+
const options = binding.modifiers || {};
|
|
13
|
+
if (loading) {
|
|
14
|
+
el._skellyRelease = (0, index_1.skelly)(el, options);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
updated(el, binding) {
|
|
18
|
+
const loading = binding.value;
|
|
19
|
+
const oldLoading = binding.oldValue;
|
|
20
|
+
const options = binding.modifiers || {};
|
|
21
|
+
if (loading !== oldLoading) {
|
|
22
|
+
if (loading) {
|
|
23
|
+
if (el._skellyRelease) {
|
|
24
|
+
el._skellyRelease();
|
|
25
|
+
}
|
|
26
|
+
el._skellyRelease = (0, index_1.skelly)(el, options);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
if (el._skellyRelease) {
|
|
30
|
+
el._skellyRelease();
|
|
31
|
+
el._skellyRelease = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
beforeUnmount(el) {
|
|
37
|
+
if (el._skellyRelease) {
|
|
38
|
+
el._skellyRelease();
|
|
39
|
+
el._skellyRelease = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Vue wrapper component for Skelly.
|
|
45
|
+
*/
|
|
46
|
+
exports.Skelly = (0, vue_1.defineComponent)({
|
|
47
|
+
name: "Skelly",
|
|
48
|
+
props: {
|
|
49
|
+
loading: { type: Boolean, required: true },
|
|
50
|
+
visual: { type: String, default: "shimmer" },
|
|
51
|
+
rows: { type: Number },
|
|
52
|
+
media: { type: String, default: "block" },
|
|
53
|
+
preset: { type: String },
|
|
54
|
+
radius: { type: String }
|
|
55
|
+
},
|
|
56
|
+
setup(props, { slots }) {
|
|
57
|
+
const rootRef = (0, vue_1.ref)(null);
|
|
58
|
+
let releaseFn = null;
|
|
59
|
+
const applySkelly = () => {
|
|
60
|
+
if (releaseFn) {
|
|
61
|
+
releaseFn();
|
|
62
|
+
releaseFn = null;
|
|
63
|
+
}
|
|
64
|
+
if (props.loading && rootRef.value) {
|
|
65
|
+
releaseFn = (0, index_1.skelly)(rootRef.value, {
|
|
66
|
+
visual: props.visual,
|
|
67
|
+
rows: props.rows,
|
|
68
|
+
media: props.media,
|
|
69
|
+
preset: props.preset,
|
|
70
|
+
radius: props.radius
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
(0, vue_1.onMounted)(applySkelly);
|
|
75
|
+
(0, vue_1.onBeforeUnmount)(() => {
|
|
76
|
+
if (releaseFn)
|
|
77
|
+
releaseFn();
|
|
78
|
+
});
|
|
79
|
+
(0, vue_1.watch)(() => [props.loading, props.visual, props.rows, props.media, props.preset, props.radius], applySkelly);
|
|
80
|
+
return () => (0, vue_1.h)("div", { ref: rootRef, class: "skelly-vue-container" }, slots.default?.());
|
|
81
|
+
}
|
|
82
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "use-skelly",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skeletons that draw themselves. Layout-driven skeleton state library.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skelly": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./style.css": "./dist/style.css",
|
|
16
|
+
"./react": {
|
|
17
|
+
"types": "./dist/react.d.ts",
|
|
18
|
+
"default": "./dist/react.js"
|
|
19
|
+
},
|
|
20
|
+
"./vue": {
|
|
21
|
+
"types": "./dist/vue.d.ts",
|
|
22
|
+
"default": "./dist/vue.js"
|
|
23
|
+
},
|
|
24
|
+
"./svelte": {
|
|
25
|
+
"types": "./dist/svelte.d.ts",
|
|
26
|
+
"default": "./dist/svelte.js"
|
|
27
|
+
},
|
|
28
|
+
"./next": {
|
|
29
|
+
"types": "./dist/next.d.ts",
|
|
30
|
+
"default": "./dist/next.js"
|
|
31
|
+
},
|
|
32
|
+
"./build": {
|
|
33
|
+
"types": "./dist/build.d.ts",
|
|
34
|
+
"default": "./dist/build.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && cp src/style.css dist/style.css",
|
|
42
|
+
"watch": "tsc -w"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"skeleton",
|
|
46
|
+
"loading",
|
|
47
|
+
"placeholder",
|
|
48
|
+
"shimmer",
|
|
49
|
+
"react",
|
|
50
|
+
"nextjs",
|
|
51
|
+
"vue",
|
|
52
|
+
"svelte",
|
|
53
|
+
"layout"
|
|
54
|
+
],
|
|
55
|
+
"author": "Skelly Team",
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"react": ">=18.0.0",
|
|
59
|
+
"react-dom": ">=18.0.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"react": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"react-dom": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "^20.0.0",
|
|
71
|
+
"@types/react": "^18.0.0",
|
|
72
|
+
"@types/react-dom": "^18.0.0",
|
|
73
|
+
"typescript": "^5.0.0"
|
|
74
|
+
}
|
|
75
|
+
}
|