@camp.dev/bones 0.4.0 → 0.5.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 +61 -106
- package/package.json +4 -36
- package/src/css/bones.css +1008 -134
- package/dist/core/attributes.d.mts +0 -25
- package/dist/core/attributes.mjs +0 -36
- package/dist/css/auto.css +0 -1105
- package/dist/css/bones.css +0 -197
- package/dist/element/boundary.d.mts +0 -26
- package/dist/element/boundary.mjs +0 -220
- package/dist/element/index.d.mts +0 -14
- package/dist/element/index.mjs +0 -6
- package/dist/element/measure.mjs +0 -99
- package/dist/element/overlay.mjs +0 -184
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -2
- package/dist/react/boundary.d.mts +0 -40
- package/dist/react/boundary.mjs +0 -21
- package/dist/react/create-bones.d.mts +0 -23
- package/dist/react/create-bones.mjs +0 -69
- package/dist/react/index.d.mts +0 -4
- package/dist/react/index.mjs +0 -4
- package/dist/server/bootstrap.d.mts +0 -4
- package/dist/server/bootstrap.mjs +0 -3
- package/dist/server/index.d.mts +0 -17
- package/dist/server/index.mjs +0 -57
- package/src/css/auto.css +0 -1105
package/README.md
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
# Bones
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Automatic skeleton loaders for any stack. One stylesheet, ~5.7 kB gzipped, no JavaScript, 0 dependencies.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
With React Server Components, your component renders once on the server. There's no re-render from "loading" to "loaded," so `{data || <Skeleton />}` doesn't work anymore. The typical workaround is writing a separate skeleton component for every piece of UI and passing it as a Suspense fallback.
|
|
8
|
-
|
|
9
|
-
Bones skips the duplication. You write your markup once and it handles both states. The skeleton and the real UI are the same component, so they can't drift apart.
|
|
5
|
+
Set `aria-busy="true"` on a region and its content becomes a skeleton: a bar for every leaf, a box for every image and control. No skeleton components, no placeholder markup, no JavaScript in the loading path. When inference gets something wrong, a `data-bones-*` attribute on the real markup fixes it, and the attribute does nothing once the region is not busy.
|
|
10
6
|
|
|
11
7
|
## How it works
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Works in Server Components. No hooks, no context, no `'use client'`.
|
|
16
|
-
- One component handles both loading and loaded states.
|
|
17
|
-
- Pass data or a promise. A pending promise suspends to your `<Suspense>` boundary; `forceBones` renders the skeleton.
|
|
18
|
-
- Skeletons are pure CSS, themed with custom properties.
|
|
19
|
-
- Loading elements get `aria-busy="true"` automatically.
|
|
9
|
+
The stylesheet keys on `aria-busy="true"`. On that element and under it, an element with no element children paints as a text bar and an image or form control paints as a block. Text hides by zeroing the alpha of its own color, so bones take their color from the text around them and contrast on any background. `data-bones-type`, `data-bones-lines`, `data-bones-length`, `data-bones-auto`, and `data-bones-animate` adjust the result. That is the whole API.
|
|
20
10
|
|
|
21
11
|
## Installation
|
|
22
12
|
|
|
@@ -24,142 +14,107 @@ Bones skips the duplication. You write your markup once and it handles both stat
|
|
|
24
14
|
npm install @camp.dev/bones
|
|
25
15
|
```
|
|
26
16
|
|
|
27
|
-
Import the
|
|
17
|
+
Import the stylesheet once in your root layout or entry point:
|
|
28
18
|
|
|
29
19
|
```tsx
|
|
30
20
|
import "@camp.dev/bones/css";
|
|
31
21
|
```
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
Without a bundler, link it from a CDN:
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| `@camp.dev/bones/css` | The skeleton stylesheet. Import once in your root layout. |
|
|
39
|
-
| `@camp.dev/bones/auto.css` | Skeletonizes unmarked leaves under `aria-busy="true"`. Imports the base stylesheet itself, so a separate `/css` import is optional. |
|
|
40
|
-
| `@camp.dev/bones/element` | `<bones-boundary>`, a custom element that sets `aria-busy` and `inert` on its subtree with `delay`, `min-duration`, and a crossfade. `precision="measured"` draws pixel-accurate per-line bones measured from the content. |
|
|
41
|
-
| `@camp.dev/bones/server` | `streamBones` and the wire-protocol primitives: stream a shell with busy boundaries, then flush each region's content out of order as it resolves. |
|
|
42
|
-
| `@camp.dev/bones` | The framework-agnostic core (`boneAttributes`, `minMax`). You only need this to build your own renderer or adapter. |
|
|
25
|
+
```html
|
|
26
|
+
<link rel="stylesheet" href="https://unpkg.com/@camp.dev/bones/src/css/bones.css" />
|
|
27
|
+
```
|
|
43
28
|
|
|
44
|
-
|
|
29
|
+
## Entry points
|
|
45
30
|
|
|
46
|
-
|
|
31
|
+
| Import | Contents |
|
|
32
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `@camp.dev/bones/css` | The stylesheet. Paints every leaf under `aria-busy="true"` and honors the `data-bones-*` attributes. Import once. |
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
Zero dependencies. No framework is required or assumed, and nothing ships JavaScript.
|
|
49
36
|
|
|
50
|
-
|
|
51
|
-
import { createBones } from "@camp.dev/bones/react";
|
|
37
|
+
## Basic usage
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
const { bone, data, lines } = createBones(user);
|
|
39
|
+
Plain HTML:
|
|
55
40
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
))}
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
41
|
+
```html
|
|
42
|
+
<section aria-busy="true">
|
|
43
|
+
<img src="/avatar.png" width="64" height="64" alt="" />
|
|
44
|
+
<h2>Sasha Greenfield</h2>
|
|
45
|
+
<p>Collects tape loops, birdsong, and the hum of old refrigerators.</p>
|
|
46
|
+
</section>
|
|
66
47
|
```
|
|
67
48
|
|
|
68
|
-
|
|
49
|
+
In a framework, write the component so it renders its shell when the data is missing and forwards its props to the root. The skeleton is then the component with `aria-busy` and no data:
|
|
69
50
|
|
|
70
51
|
```tsx
|
|
71
|
-
import {
|
|
72
|
-
import { Suspense } from "react";
|
|
52
|
+
import type { ComponentProps } from "react";
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
const user = fetchUser();
|
|
54
|
+
function ProfileCard({ user, ...rest }: { user?: User } & ComponentProps<"div">) {
|
|
76
55
|
return (
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
|
|
56
|
+
<div {...rest}>
|
|
57
|
+
<img src={user?.avatar} width={80} height={80} alt="" />
|
|
58
|
+
<h3>{user?.name}</h3>
|
|
59
|
+
<p data-bones-lines="3">{user?.bio}</p>
|
|
60
|
+
<ul>
|
|
61
|
+
{(user?.posts ?? Array.from({ length: 3 })).map((post, i) => (
|
|
62
|
+
<li key={post?.id ?? i}>{post?.title}</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
</div>
|
|
80
66
|
);
|
|
81
67
|
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
While the promise is pending, the fallback renders `<ProfileCard>` with skeletons visible. Once it resolves, the real content swaps in. There is no separate skeleton component to keep in sync — the fallback is the component.
|
|
85
|
-
|
|
86
|
-
## Bone types
|
|
87
68
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
| `"block"` | Images, avatars, thumbnails | `<img src={…} {...bone("block")} />` |
|
|
92
|
-
| `"container"` | Wrappers with complex children | `<div {...bone("container")}>` |
|
|
69
|
+
async function Profile() {
|
|
70
|
+
return <ProfileCard user={await fetchUser()} />;
|
|
71
|
+
}
|
|
93
72
|
|
|
94
|
-
|
|
73
|
+
<Suspense fallback={<ProfileCard aria-busy="true" />}>
|
|
74
|
+
<Profile />
|
|
75
|
+
</Suspense>;
|
|
76
|
+
```
|
|
95
77
|
|
|
96
|
-
|
|
78
|
+
The fallback is the component, and the async child is what suspends. `data-bones-lines="3"` says the bio is three lines tall while empty, and `Array.from` gives the list three placeholder rows, since CSS cannot add elements.
|
|
97
79
|
|
|
98
|
-
|
|
99
|
-
import { createBones, forceBones } from "@camp.dev/bones/react";
|
|
80
|
+
## Adjust a bone
|
|
100
81
|
|
|
101
|
-
|
|
102
|
-
|
|
82
|
+
| Attribute | Effect |
|
|
83
|
+
| ------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `data-bones-type="text"` | Paint a text bar regardless of inference. |
|
|
85
|
+
| `data-bones-type="block"` | Paint one filled box and hide descendants. A `div` avatar, a card. |
|
|
86
|
+
| `data-bones-lines="3"` | Paint three stacked bars in one element. 2 to 8 everywhere; any integer with modern `attr()`. |
|
|
87
|
+
| `data-bones-length="9"` | Make the bar nine characters wide. 1 to 40 everywhere; any integer with modern `attr()`. |
|
|
88
|
+
| `data-bones-auto="off"` | Keep this subtree readable. On `<body>`, only explicit markup paints. |
|
|
89
|
+
| `data-bones-animate="shimmer\|pulse\|none"` | Pick the animation for the bones inside, or on the bone itself. |
|
|
103
90
|
|
|
104
|
-
|
|
91
|
+
Explicit attributes are unlayered, so page CSS cannot keep their text visible without a more specific rule of its own. Inferred bones live in `@layer bones-auto`, so a page rule that sets `color` on a leaf keeps that text visible over its bar; that is the one thing to know when a bar looks wrong. A bar sits inside its element's padding, so a badge or a pill keeps its shape while it loads. Padding in `px`, `em`, or `rem` is exact; percentage padding misses.
|
|
105
92
|
|
|
106
|
-
|
|
107
|
-
<ProfileCard user={forceBones} />
|
|
108
|
-
<PostList posts={forceBones} />
|
|
109
|
-
```
|
|
93
|
+
## Previewing skeletons
|
|
110
94
|
|
|
111
|
-
|
|
95
|
+
Render the component busy with no data. No promise, no boundary, no Storybook addon:
|
|
112
96
|
|
|
113
97
|
```tsx
|
|
114
|
-
<
|
|
98
|
+
<ProfileCard aria-busy="true" />
|
|
115
99
|
```
|
|
116
100
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
## Automatic skeletons
|
|
101
|
+
## Focus and timing
|
|
120
102
|
|
|
121
|
-
|
|
103
|
+
A skeleton's links and buttons are still focusable. Put `inert` beside `aria-busy` on a fallback that renders any:
|
|
122
104
|
|
|
123
105
|
```tsx
|
|
124
|
-
|
|
106
|
+
<Suspense fallback={<ProfileCard aria-busy="true" inert />}>
|
|
125
107
|
```
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```html
|
|
130
|
-
<section aria-busy="true">
|
|
131
|
-
<h2>Title</h2>
|
|
132
|
-
<p>Summary text goes here.</p>
|
|
133
|
-
</section>
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
`[data-bones-auto="off"]` opts a subtree out — useful for a status message you want to stay readable while its container skeletonizes. Explicit `data-bone` markup is left alone; `auto.css` only styles elements neither `bone()` nor a manual `data-bone` attribute has already claimed.
|
|
137
|
-
|
|
138
|
-
Auto rules live in `@layer bones-auto`, so any page CSS that sets `color` on an element outranks the bone's transparent text, and that text stays visible over its skeleton bar. `data-bone-animate` works on the `aria-busy` element itself or on any ancestor. The `data-bone-animate` overrides rely on `@scope`. In a browser without `@scope`, every bone shimmers, and `data-bone-animate="pulse"` and `"none"` cannot change that. The `prefers-reduced-motion` fallback to pulse still applies.
|
|
139
|
-
|
|
140
|
-
## Without React
|
|
141
|
-
|
|
142
|
-
`<bones-boundary>` manages the loading state for any stack. Set `busy` when a request starts and clear it when the response lands. The element waits 200 ms before showing bones and keeps them for at least 400 ms, then crossfades to content with the View Transitions API where available.
|
|
143
|
-
|
|
144
|
-
```html
|
|
145
|
-
<script type="module">
|
|
146
|
-
import "@camp.dev/bones/element";
|
|
147
|
-
</script>
|
|
148
|
-
|
|
149
|
-
<bones-boundary busy>
|
|
150
|
-
<h2>Title</h2>
|
|
151
|
-
<p>Body copy.</p>
|
|
152
|
-
</bones-boundary>
|
|
153
|
-
```
|
|
109
|
+
Suspense paints the skeleton first, so it never flashes. A region you mark busy around your own `fetch` can. The docs' [Delay and hold](https://github.com/campdotdev/bones/blob/main/apps/docs/content/docs/examples.mdx#delay-and-hold) recipe waits before showing bones and keeps them long enough once shown; [Streaming](https://github.com/campdotdev/bones/blob/main/apps/docs/content/docs/streaming.mdx) shows the swap script for a server with no framework.
|
|
154
110
|
|
|
155
|
-
|
|
111
|
+
## Theming
|
|
156
112
|
|
|
157
|
-
|
|
113
|
+
`--bone-base`, `--bone-highlight`, `--bone-radius`, and `--bone-duration` are CSS custom properties; set them on any ancestor. `--bone-radius` must carry a unit. `prefers-reduced-motion` turns shimmer into a slow pulse; `data-bones-animate="none"` stays still.
|
|
158
114
|
|
|
159
115
|
## Development
|
|
160
116
|
|
|
161
117
|
```bash
|
|
162
118
|
vp install # install dependencies
|
|
163
119
|
vp test # run tests
|
|
164
|
-
vp pack # build the library
|
|
165
120
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camp.dev/bones",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Automatic skeleton loaders for any stack. One stylesheet.",
|
|
5
5
|
"homepage": "https://github.com/campdotdev/bones#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/campdotdev/bones/issues"
|
|
@@ -13,63 +13,31 @@
|
|
|
13
13
|
"url": "git+https://github.com/campdotdev/bones.git"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
-
"dist",
|
|
17
16
|
"src/css"
|
|
18
17
|
],
|
|
19
18
|
"type": "module",
|
|
20
19
|
"sideEffects": [
|
|
21
|
-
"*.css"
|
|
22
|
-
"./dist/element/index.mjs"
|
|
20
|
+
"*.css"
|
|
23
21
|
],
|
|
24
22
|
"exports": {
|
|
25
|
-
".": "./dist/index.mjs",
|
|
26
|
-
"./element": "./dist/element/index.mjs",
|
|
27
|
-
"./react": "./dist/react/index.mjs",
|
|
28
|
-
"./server": "./dist/server/index.mjs",
|
|
29
23
|
"./package.json": "./package.json",
|
|
30
24
|
"./css": {
|
|
31
25
|
"style": "./src/css/bones.css",
|
|
32
26
|
"default": "./src/css/bones.css"
|
|
33
|
-
},
|
|
34
|
-
"./auto.css": {
|
|
35
|
-
"style": "./src/css/auto.css",
|
|
36
|
-
"default": "./src/css/auto.css"
|
|
37
27
|
}
|
|
38
28
|
},
|
|
39
29
|
"publishConfig": {
|
|
40
30
|
"access": "public"
|
|
41
31
|
},
|
|
42
32
|
"devDependencies": {
|
|
43
|
-
"@testing-library/jest-dom": "^6.9.1",
|
|
44
|
-
"@testing-library/react": "^16.3.2",
|
|
45
33
|
"@types/node": "^25.5.0",
|
|
46
|
-
"@types/react": "^19.2.14",
|
|
47
|
-
"@types/react-dom": "^19.2.3",
|
|
48
|
-
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
49
|
-
"@vitest/coverage-v8": "^4.1.5",
|
|
50
34
|
"jsdom": "^29.0.2",
|
|
51
35
|
"playwright": "^1.62.1",
|
|
52
|
-
"react": "^19.2.5",
|
|
53
36
|
"typescript": "^6.0.2",
|
|
54
37
|
"vite-plus": "^0.1.14"
|
|
55
38
|
},
|
|
56
|
-
"peerDependencies": {
|
|
57
|
-
"react": ">=18",
|
|
58
|
-
"react-dom": ">=18"
|
|
59
|
-
},
|
|
60
|
-
"peerDependenciesMeta": {
|
|
61
|
-
"react": {
|
|
62
|
-
"optional": true
|
|
63
|
-
},
|
|
64
|
-
"react-dom": {
|
|
65
|
-
"optional": true
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
39
|
"scripts": {
|
|
69
|
-
"build": "vp pack",
|
|
70
|
-
"dev": "vp pack --watch",
|
|
71
40
|
"test": "vp test",
|
|
72
|
-
"check": "vp check"
|
|
73
|
-
"health": "vp test --project unit --coverage && fallow health --root . --coverage coverage/coverage-final.json --format json --quiet"
|
|
41
|
+
"check": "vp check"
|
|
74
42
|
}
|
|
75
43
|
}
|