@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 CHANGED
@@ -1,22 +1,12 @@
1
1
  # Bones
2
2
 
3
- [![Bundle Size](https://deno.bundlejs.com/badge?q=@camp.dev/bones)](https://bundlejs.com/?q=%40camp.dev%2Fbones)
3
+ Automatic skeleton loaders for any stack. One stylesheet, ~5.7 kB gzipped, no JavaScript, 0 dependencies.
4
4
 
5
- Skeleton loaders designed for React Server Components and streaming. ~2.7 kB gzipped, 0 dependencies.
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
- `createBones` accepts data or a promise of data. While loading, its `bone` function returns HTML attributes that style elements as skeletons via CSS. Once the data resolves, `bone` returns an empty object and your component renders normally. There are no hooks and no context providers.
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 CSS once in your root layout or entry point:
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
- ## Entry points
23
+ Without a bundler, link it from a CDN:
34
24
 
35
- | Import | Contents |
36
- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
- | `@camp.dev/bones/react` | `createBones`, `readPromise`, `forceBones`, `minMax`, `isMinMax`, `BonesBoundary` |
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
- React is an optional peer dependency: installing the package without React is supported and only the `/react` entry requires it.
29
+ ## Entry points
45
30
 
46
- ## Basic usage
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
- Pass data (or a promise of data) to `createBones`. Spread the `bone` function's return value onto elements that should show skeletons while loading.
35
+ Zero dependencies. No framework is required or assumed, and nothing ships JavaScript.
49
36
 
50
- ```tsx
51
- import { createBones } from "@camp.dev/bones/react";
37
+ ## Basic usage
52
38
 
53
- function ProfileCard({ user }: { user: Promise<User> | User }) {
54
- const { bone, data, lines } = createBones(user);
39
+ Plain HTML:
55
40
 
56
- return (
57
- <div>
58
- <img src={data?.avatar} width={80} height={80} {...bone("block")} />
59
- <h3 {...bone("text", { length: 10 })}>{data?.name}</h3>
60
- {lines(data?.bio, 3, (item) => (
61
- <p>{item}</p>
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
- Pass the promise to the component, and reuse the same component with `forceBones` as the fallback:
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 { forceBones } from "@camp.dev/bones/react";
72
- import { Suspense } from "react";
52
+ import type { ComponentProps } from "react";
73
53
 
74
- export default function Page() {
75
- const user = fetchUser();
54
+ function ProfileCard({ user, ...rest }: { user?: User } & ComponentProps<"div">) {
76
55
  return (
77
- <Suspense fallback={<ProfileCard user={forceBones} />}>
78
- <ProfileCard user={user} />
79
- </Suspense>
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
- | Type | Use for | Example |
89
- | ------------- | ------------------------------ | ------------------------------------ |
90
- | `"text"` | Headings, paragraphs, labels | `<h2 {...bone("text")}>` |
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
- ## Previewing skeletons
73
+ <Suspense fallback={<ProfileCard aria-busy="true" />}>
74
+ <Profile />
75
+ </Suspense>;
76
+ ```
95
77
 
96
- Use `forceBones` to see a component's skeleton state without setting up real data:
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
- ```tsx
99
- import { createBones, forceBones } from "@camp.dev/bones/react";
80
+ ## Adjust a bone
100
81
 
101
- <ProfileCard user={forceBones} />;
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
- To force a subtree into skeleton mode at once, pass `forceBones` to each component in it:
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
- ```tsx
107
- <ProfileCard user={forceBones} />
108
- <PostList posts={forceBones} />
109
- ```
93
+ ## Previewing skeletons
110
94
 
111
- `forceBones` only forces the component it's passed to. A component that derives child props from its own data must forward it itself, such as `PostList` mapping items into cards. `repeat` yields `undefined` for items that don't exist yet:
95
+ Render the component busy with no data. No promise, no boundary, no Storybook addon:
112
96
 
113
97
  ```tsx
114
- <PostCard key={item?.id ?? i} post={item ?? forceBones} />
98
+ <ProfileCard aria-busy="true" />
115
99
  ```
116
100
 
117
- For content that has no bone markup at all, wrap it in [`<bones-boundary force>`](https://github.com/campdotdev/bones/blob/main/apps/docs/content/docs/api/bones-boundary.mdx) and let `auto.css` draw leaf bones.
118
-
119
- ## Automatic skeletons
101
+ ## Focus and timing
120
102
 
121
- For markup you haven't wired up with `bone()` third-party components, server-rendered HTML, anything without explicit attributes — import the auto stylesheet. It imports `/css` itself, so this one file is a complete setup:
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
- import "@camp.dev/bones/auto.css";
106
+ <Suspense fallback={<ProfileCard aria-busy="true" inert />}>
125
107
  ```
126
108
 
127
- Set `aria-busy="true"` on the loading region and every unmarked leaf inside it becomes a skeleton, no `bone()` calls required:
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
- `@camp.dev/bones/element` is a bare specifier. A browser cannot resolve it on its own, so this snippet needs a bundler or an import map. To load the element straight from a CDN in a plain HTML file, see the URL form on the [bones-boundary docs page](https://github.com/campdotdev/bones/blob/main/apps/docs/content/docs/api/bones-boundary.mdx).
111
+ ## Theming
156
112
 
157
- Pair it with `auto.css` for zero-markup skeletons, or with `data-bone` markup from `boneAttributes`. The element is also exported for React as `<BonesBoundary>` from `@camp.dev/bones/react`.
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.0",
4
- "description": "Skeleton loaders designed for React Server Components and streaming.",
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
  }