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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }
@@ -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
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Next.js Configuration Wrapper: withSkelly(nextConfig)
3
+ */
4
+ export declare function withSkelly(nextConfig?: any): any;
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
+ }
@@ -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
+ }
@@ -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
+ }