@camp.dev/bones 0.2.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/README.md +138 -0
- package/dist/core/attributes.d.mts +25 -0
- package/dist/core/attributes.mjs +36 -0
- package/dist/css/auto.css +1092 -0
- package/dist/css/bones.css +156 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/react/bones.d.mts +15 -0
- package/dist/react/bones.mjs +27 -0
- package/dist/react/create-bones.d.mts +23 -0
- package/dist/react/create-bones.mjs +73 -0
- package/dist/react/index.d.mts +4 -0
- package/dist/react/index.mjs +4 -0
- package/package.json +71 -0
- package/src/css/auto.css +1092 -0
- package/src/css/bones.css +156 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bone-base: rgba(0, 0, 0, 0.12);
|
|
3
|
+
--bone-highlight: rgba(0, 0, 0, 0.06);
|
|
4
|
+
--bone-radius: 4px;
|
|
5
|
+
--bone-duration: 1.5s;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@media (prefers-color-scheme: dark) {
|
|
9
|
+
:root {
|
|
10
|
+
--bone-base: rgba(255, 255, 255, 0.12);
|
|
11
|
+
--bone-highlight: rgba(255, 255, 255, 0.06);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[data-bone="text"] {
|
|
16
|
+
color: transparent;
|
|
17
|
+
position: relative;
|
|
18
|
+
min-width: 4ch;
|
|
19
|
+
min-height: 1lh;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[data-bone-line] {
|
|
23
|
+
display: block;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[data-bone-line]:last-child {
|
|
27
|
+
width: 69%;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[data-bone="text"]::before {
|
|
31
|
+
content: "\200b";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[data-bone="text"]::after {
|
|
35
|
+
content: "";
|
|
36
|
+
position: absolute;
|
|
37
|
+
inset-inline: 0;
|
|
38
|
+
top: calc((1lh + 1cap) / 2 - 1ex);
|
|
39
|
+
height: 1ex;
|
|
40
|
+
background-color: var(--bone-base);
|
|
41
|
+
border-radius: var(--bone-radius);
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[data-bone="text"][style*="--bone-length"] {
|
|
46
|
+
width: calc(var(--bone-length) * 1ch);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[data-bone="text"][style*="--bone-contained"] {
|
|
50
|
+
min-width: unset;
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
box-sizing: content-box;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[data-bone="text"][style*="--bone-contained"]::after {
|
|
57
|
+
flex: 1;
|
|
58
|
+
position: static;
|
|
59
|
+
inset-inline: unset;
|
|
60
|
+
top: unset;
|
|
61
|
+
transform: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[data-bone="text"][style*="--bone-contained"][style*="--bone-length"] {
|
|
65
|
+
min-width: calc(var(--bone-length) * 1ch);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[data-bone="block"] {
|
|
69
|
+
display: inline-block;
|
|
70
|
+
background-color: var(--bone-base);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
img[data-bone="block"],
|
|
74
|
+
video[data-bone="block"] {
|
|
75
|
+
color: transparent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
[data-bone="container"] {
|
|
79
|
+
position: relative;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[data-bone="container"]::before {
|
|
84
|
+
content: "";
|
|
85
|
+
position: absolute;
|
|
86
|
+
inset: 0;
|
|
87
|
+
background-color: var(--bone-base);
|
|
88
|
+
z-index: 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[data-bone="container"] > * {
|
|
92
|
+
visibility: hidden;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes bone-shimmer {
|
|
96
|
+
0% {
|
|
97
|
+
background-position: 200% 0;
|
|
98
|
+
}
|
|
99
|
+
100% {
|
|
100
|
+
background-position: -200% 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@keyframes bone-pulse {
|
|
105
|
+
0%,
|
|
106
|
+
100% {
|
|
107
|
+
opacity: 1;
|
|
108
|
+
}
|
|
109
|
+
50% {
|
|
110
|
+
opacity: 0.5;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@scope ([data-bone-animate="shimmer"]) to ([data-bone-animate]:not([data-bone-animate="shimmer"])) {
|
|
115
|
+
[data-bone="text"]::after,
|
|
116
|
+
[data-bone="text"]::before,
|
|
117
|
+
[data-bone="block"],
|
|
118
|
+
[data-bone="container"]::before {
|
|
119
|
+
animation: bone-shimmer var(--bone-duration) ease-in-out infinite;
|
|
120
|
+
background: linear-gradient(
|
|
121
|
+
90deg,
|
|
122
|
+
var(--bone-base) 25%,
|
|
123
|
+
var(--bone-highlight) 50%,
|
|
124
|
+
var(--bone-base) 75%
|
|
125
|
+
);
|
|
126
|
+
background-size: 200% 100%;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@scope ([data-bone-animate="pulse"]) to ([data-bone-animate]:not([data-bone-animate="pulse"])) {
|
|
131
|
+
[data-bone="text"]::after,
|
|
132
|
+
[data-bone="text"]::before,
|
|
133
|
+
[data-bone="block"],
|
|
134
|
+
[data-bone="container"]::before {
|
|
135
|
+
animation: bone-pulse var(--bone-duration) ease-in-out infinite;
|
|
136
|
+
background: var(--bone-base);
|
|
137
|
+
background-size: auto;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@scope ([data-bone-animate="none"]) to ([data-bone-animate]:not([data-bone-animate="none"])) {
|
|
142
|
+
[data-bone="text"]::after,
|
|
143
|
+
[data-bone="text"]::before,
|
|
144
|
+
[data-bone="block"],
|
|
145
|
+
[data-bone="container"]::before {
|
|
146
|
+
animation: none;
|
|
147
|
+
background: var(--bone-base);
|
|
148
|
+
background-size: auto;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@media (prefers-reduced-motion: reduce) {
|
|
153
|
+
[aria-busy="true"] {
|
|
154
|
+
animation: bone-pulse 2s ease-in-out infinite;
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { BoneAttributes, BoneOptions, BoneType, MinMax, TRANSPARENT_PIXEL, boneAttributes, isMinMax, minMax, resolveLength } from "./core/attributes.mjs";
|
|
2
|
+
export { type BoneAttributes, type BoneOptions, type BoneType, type MinMax, TRANSPARENT_PIXEL, boneAttributes, isMinMax, minMax, resolveLength };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/react/bones.d.ts
|
|
4
|
+
declare function BonesForce({
|
|
5
|
+
children
|
|
6
|
+
}: {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
declare function Bones({
|
|
10
|
+
children
|
|
11
|
+
}: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}): ReactNode;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { Bones, BonesForce };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { forceBones, getBonesContext } from "./create-bones.mjs";
|
|
2
|
+
import { Children, Fragment, Suspense, cloneElement, createElement, isValidElement } from "react";
|
|
3
|
+
//#region src/react/bones.ts
|
|
4
|
+
function BonesStart() {
|
|
5
|
+
getBonesContext().loading = true;
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
function BonesEnd() {
|
|
9
|
+
getBonesContext().loading = false;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function swapPromises(children) {
|
|
13
|
+
return Children.map(children, (child) => {
|
|
14
|
+
if (!isValidElement(child)) return child;
|
|
15
|
+
const props = {};
|
|
16
|
+
for (const [key, value] of Object.entries(child.props)) props[key] = value instanceof Promise ? forceBones : value;
|
|
17
|
+
return cloneElement(child, props);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function BonesForce({ children }) {
|
|
21
|
+
return createElement(Fragment, null, createElement(BonesStart), children, createElement(BonesEnd));
|
|
22
|
+
}
|
|
23
|
+
function Bones({ children }) {
|
|
24
|
+
return createElement(Suspense, { fallback: createElement(Fragment, null, createElement(BonesStart), swapPromises(children), createElement(BonesEnd)) }, children);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { Bones, BonesForce };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BoneOptions, BoneType, MinMax, isMinMax, minMax } from "../core/attributes.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/create-bones.d.ts
|
|
5
|
+
declare function readPromise<T>(promise: Promise<T>): T;
|
|
6
|
+
type BoneProps = Record<string, unknown>;
|
|
7
|
+
declare const forceBones: Promise<never>;
|
|
8
|
+
interface CreateBonesOptions {
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface CreateBonesReturn<T> {
|
|
12
|
+
bone: {
|
|
13
|
+
(type: "text", options?: BoneOptions): BoneProps;
|
|
14
|
+
(type: "block" | "container"): BoneProps;
|
|
15
|
+
};
|
|
16
|
+
data: T | null | undefined;
|
|
17
|
+
repeat: <U>(arr: U[] | undefined | null, count: number, render: (item: U | undefined, index: number) => ReactNode) => ReactNode[];
|
|
18
|
+
lines: <V>(value: V | null | undefined, count: number, render: (item: V | ReactNode) => ReactNode) => ReactNode[];
|
|
19
|
+
}
|
|
20
|
+
declare function createBones(options: CreateBonesOptions): CreateBonesReturn<never>;
|
|
21
|
+
declare function createBones<T>(data: T | Promise<T> | undefined | null, options?: CreateBonesOptions): CreateBonesReturn<T>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { CreateBonesOptions, CreateBonesReturn, createBones, forceBones, readPromise };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { boneAttributes } from "../core/attributes.mjs";
|
|
2
|
+
import { cache, cloneElement, createElement, isValidElement } from "react";
|
|
3
|
+
//#region src/react/create-bones.ts
|
|
4
|
+
const getBonesContext = cache(() => ({ loading: false }));
|
|
5
|
+
function withKey(node, key) {
|
|
6
|
+
return isValidElement(node) ? cloneElement(node, { key }) : node;
|
|
7
|
+
}
|
|
8
|
+
function readPromise(promise) {
|
|
9
|
+
const tracked = promise;
|
|
10
|
+
if (tracked._status === void 0) {
|
|
11
|
+
tracked._status = "pending";
|
|
12
|
+
promise.then((result) => {
|
|
13
|
+
tracked._status = "fulfilled";
|
|
14
|
+
tracked._result = result;
|
|
15
|
+
}, (error) => {
|
|
16
|
+
tracked._status = "rejected";
|
|
17
|
+
tracked._error = error;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (tracked._status === "fulfilled") return tracked._result;
|
|
21
|
+
if (tracked._status === "rejected") throw tracked._error;
|
|
22
|
+
throw promise;
|
|
23
|
+
}
|
|
24
|
+
const forceBones = Object.freeze({});
|
|
25
|
+
function createBones(dataOrOptions, maybeOptions) {
|
|
26
|
+
let data;
|
|
27
|
+
let options;
|
|
28
|
+
if (maybeOptions === void 0 && dataOrOptions !== null && dataOrOptions !== void 0 && typeof dataOrOptions === "object" && !(dataOrOptions instanceof Promise) && "loading" in dataOrOptions) {
|
|
29
|
+
data = void 0;
|
|
30
|
+
options = dataOrOptions;
|
|
31
|
+
} else {
|
|
32
|
+
data = dataOrOptions;
|
|
33
|
+
options = maybeOptions;
|
|
34
|
+
}
|
|
35
|
+
let resolved;
|
|
36
|
+
let isLoading = false;
|
|
37
|
+
if (options?.loading) {
|
|
38
|
+
isLoading = true;
|
|
39
|
+
resolved = void 0;
|
|
40
|
+
} else if (data != null && data === forceBones) {
|
|
41
|
+
isLoading = true;
|
|
42
|
+
resolved = void 0;
|
|
43
|
+
} else if (getBonesContext().loading) {
|
|
44
|
+
isLoading = true;
|
|
45
|
+
resolved = void 0;
|
|
46
|
+
} else if (data != null && data instanceof Promise) resolved = readPromise(data);
|
|
47
|
+
else resolved = data;
|
|
48
|
+
let boneCallIndex = 0;
|
|
49
|
+
const bone = (type, options) => {
|
|
50
|
+
if (!isLoading) return {};
|
|
51
|
+
return { ...boneAttributes(type, options, boneCallIndex++) };
|
|
52
|
+
};
|
|
53
|
+
function repeat(arr, count, render) {
|
|
54
|
+
return (isLoading ? Array.from({ length: count }) : arr ?? []).map(render);
|
|
55
|
+
}
|
|
56
|
+
function lines(value, count, render) {
|
|
57
|
+
if (isLoading) return [withKey(render(Array.from({ length: count }, (_, i) => createElement("span", {
|
|
58
|
+
key: i,
|
|
59
|
+
"data-bone-line": true,
|
|
60
|
+
...bone("text")
|
|
61
|
+
}))), 0)];
|
|
62
|
+
if (value == null) return [];
|
|
63
|
+
return [withKey(render(value), 0)];
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
bone,
|
|
67
|
+
data: isLoading ? void 0 : resolved,
|
|
68
|
+
repeat,
|
|
69
|
+
lines
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { createBones, forceBones, getBonesContext, readPromise };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BoneOptions, BoneType, MinMax, isMinMax, minMax } from "../core/attributes.mjs";
|
|
2
|
+
import { CreateBonesOptions, CreateBonesReturn, createBones, forceBones, readPromise } from "./create-bones.mjs";
|
|
3
|
+
import { Bones, BonesForce } from "./bones.mjs";
|
|
4
|
+
export { type BoneOptions, type BoneType, Bones, BonesForce, type CreateBonesOptions, type CreateBonesReturn, type MinMax, createBones, forceBones, isMinMax, minMax, readPromise };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { isMinMax, minMax } from "../core/attributes.mjs";
|
|
2
|
+
import { createBones, forceBones, readPromise } from "./create-bones.mjs";
|
|
3
|
+
import { Bones, BonesForce } from "./bones.mjs";
|
|
4
|
+
export { Bones, BonesForce, createBones, forceBones, isMinMax, minMax, readPromise };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@camp.dev/bones",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Skeleton loaders designed for React Server Components and streaming.",
|
|
5
|
+
"homepage": "https://github.com/campdotdev/bones#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/campdotdev/bones/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "hunterbecton",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/campdotdev/bones.git"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src/css"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"*.css"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.mjs",
|
|
25
|
+
"./react": "./dist/react/index.mjs",
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
"./css": {
|
|
28
|
+
"style": "./src/css/bones.css",
|
|
29
|
+
"default": "./src/css/bones.css"
|
|
30
|
+
},
|
|
31
|
+
"./auto.css": {
|
|
32
|
+
"style": "./src/css/auto.css",
|
|
33
|
+
"default": "./src/css/auto.css"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
41
|
+
"@testing-library/react": "^16.3.2",
|
|
42
|
+
"@types/node": "^25.5.0",
|
|
43
|
+
"@types/react": "^19.2.14",
|
|
44
|
+
"@types/react-dom": "^19.2.3",
|
|
45
|
+
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
46
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
47
|
+
"jsdom": "^29.0.2",
|
|
48
|
+
"react": "^19.2.5",
|
|
49
|
+
"typescript": "^6.0.2",
|
|
50
|
+
"vite-plus": "^0.1.14"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": ">=18",
|
|
54
|
+
"react-dom": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"react": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"react-dom": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "vp pack",
|
|
66
|
+
"dev": "vp pack --watch",
|
|
67
|
+
"test": "vp test",
|
|
68
|
+
"check": "vp check",
|
|
69
|
+
"health": "vp test --coverage && fallow health --root . --coverage coverage/coverage-final.json --format json --quiet"
|
|
70
|
+
}
|
|
71
|
+
}
|