@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.
@@ -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
+ }
@@ -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,2 @@
1
+ import { TRANSPARENT_PIXEL, boneAttributes, isMinMax, minMax, resolveLength } from "./core/attributes.mjs";
2
+ export { TRANSPARENT_PIXEL, boneAttributes, isMinMax, minMax, resolveLength };
@@ -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
+ }