@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 ADDED
@@ -0,0 +1,138 @@
1
+ # Bones
2
+
3
+ [![Bundle Size](https://deno.bundlejs.com/badge?q=@camp.dev/bones)](https://bundlejs.com/?q=%40camp.dev%2Fbones)
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.
10
+
11
+ ## How it works
12
+
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 a promise as a prop. Bones wires up Suspense for you.
18
+ - Skeletons are pure CSS, themed with custom properties.
19
+ - Loading elements get `aria-busy="true"` automatically.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @camp.dev/bones
25
+ ```
26
+
27
+ Import the CSS once in your root layout or entry point:
28
+
29
+ ```tsx
30
+ import "@camp.dev/bones/css";
31
+ ```
32
+
33
+ ## Entry points
34
+
35
+ | Import | Contents |
36
+ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
37
+ | `@camp.dev/bones/react` | `createBones`, `readPromise`, `forceBones`, `minMax`, `<Bones>`, `<BonesForce>` |
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` | The framework-agnostic core (`boneAttributes`, `minMax`). You only need this to build your own renderer or adapter. |
41
+
42
+ React is an optional peer dependency: installing the package without React is supported and only the `/react` entry requires it.
43
+
44
+ ## Basic usage
45
+
46
+ Pass data (or a promise of data) to `createBones`. Spread the `bone` function's return value onto elements that should show skeletons while loading.
47
+
48
+ ```tsx
49
+ import { createBones } from "@camp.dev/bones/react";
50
+
51
+ function ProfileCard({ user }: { user: Promise<User> | User }) {
52
+ const { bone, data, lines } = createBones(user);
53
+
54
+ return (
55
+ <div>
56
+ <img src={data?.avatar} width={80} height={80} {...bone("block")} />
57
+ <h3 {...bone("text", { length: 10 })}>{data?.name}</h3>
58
+ {lines(data?.bio, 3, (item) => (
59
+ <p>{item}</p>
60
+ ))}
61
+ </div>
62
+ );
63
+ }
64
+ ```
65
+
66
+ Wrap components that receive promises in `<Bones>`. It creates a Suspense boundary and generates the skeleton fallback for you:
67
+
68
+ ```tsx
69
+ import { Bones } from "@camp.dev/bones/react";
70
+
71
+ export default function Page() {
72
+ return (
73
+ <Bones>
74
+ <ProfileCard user={fetchUser()} />
75
+ </Bones>
76
+ );
77
+ }
78
+ ```
79
+
80
+ While the promise is pending, `<Bones>` renders the same `<ProfileCard>` tree with skeletons visible. Once it resolves, the real content swaps in.
81
+
82
+ ## Bone types
83
+
84
+ | Type | Use for | Example |
85
+ | ------------- | ------------------------------ | ------------------------------------ |
86
+ | `"text"` | Headings, paragraphs, labels | `<h2 {...bone("text")}>` |
87
+ | `"block"` | Images, avatars, thumbnails | `<img src={…} {...bone("block")} />` |
88
+ | `"container"` | Wrappers with complex children | `<div {...bone("container")}>` |
89
+
90
+ ## Previewing skeletons
91
+
92
+ Use `forceBones` to see a component's skeleton state without setting up real data:
93
+
94
+ ```tsx
95
+ import { createBones, forceBones } from "@camp.dev/bones/react";
96
+
97
+ <ProfileCard user={forceBones} />;
98
+ ```
99
+
100
+ To force an entire subtree into skeleton mode at once, wrap it with `<BonesForce>`:
101
+
102
+ ```tsx
103
+ import { BonesForce } from "@camp.dev/bones/react";
104
+
105
+ <BonesForce>
106
+ <ProfileCard />
107
+ <PostList />
108
+ </BonesForce>;
109
+ ```
110
+
111
+ ## Automatic skeletons
112
+
113
+ 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:
114
+
115
+ ```tsx
116
+ import "@camp.dev/bones/auto.css";
117
+ ```
118
+
119
+ Set `aria-busy="true"` on the loading region and every unmarked leaf inside it becomes a skeleton, no `bone()` calls required:
120
+
121
+ ```html
122
+ <section aria-busy="true">
123
+ <h2>Title</h2>
124
+ <p>Summary text goes here.</p>
125
+ </section>
126
+ ```
127
+
128
+ `[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.
129
+
130
+ 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` also has to sit on an ancestor of the `aria-busy` element — set directly on it, it has no effect. The `data-bone-animate` overrides rely on `@scope`. In a browser without `@scope`, auto bones always shimmer, and `data-bone-animate="pulse"` and `"none"` cannot change that. The `prefers-reduced-motion` fallback to pulse still applies.
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ vp install # install dependencies
136
+ vp test # run tests
137
+ vp pack # build the library
138
+ ```
@@ -0,0 +1,25 @@
1
+ //#region src/core/attributes.d.ts
2
+ type BoneType = "text" | "block" | "container";
3
+ declare const MIN_MAX_BRAND: unique symbol;
4
+ interface MinMax {
5
+ readonly [MIN_MAX_BRAND]: true;
6
+ readonly min: number;
7
+ readonly max: number;
8
+ }
9
+ declare function minMax(min: number, max: number): MinMax;
10
+ declare function isMinMax(value: unknown): value is MinMax;
11
+ interface BoneOptions {
12
+ length?: number | MinMax;
13
+ contained?: boolean;
14
+ }
15
+ interface BoneAttributes {
16
+ "data-bone": BoneType;
17
+ "aria-busy": true;
18
+ src?: string;
19
+ style?: Record<string, number>;
20
+ }
21
+ declare const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
22
+ declare function resolveLength(length: number | MinMax | undefined, callIndex: number): number | undefined;
23
+ declare function boneAttributes(type: BoneType, options?: BoneOptions, callIndex?: number): BoneAttributes;
24
+ //#endregion
25
+ export { BoneAttributes, BoneOptions, BoneType, MinMax, TRANSPARENT_PIXEL, boneAttributes, isMinMax, minMax, resolveLength };
@@ -0,0 +1,36 @@
1
+ //#region src/core/attributes.ts
2
+ const MIN_MAX_BRAND = Symbol("minMax");
3
+ function minMax(min, max) {
4
+ return {
5
+ [MIN_MAX_BRAND]: true,
6
+ min,
7
+ max
8
+ };
9
+ }
10
+ function isMinMax(value) {
11
+ return typeof value === "object" && value !== null && MIN_MAX_BRAND in value;
12
+ }
13
+ const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
14
+ function resolveLength(length, callIndex) {
15
+ if (length == null) return void 0;
16
+ if (typeof length === "number") return length;
17
+ const range = length.max - length.min + 1;
18
+ return length.min + (callIndex * 7 + 3) % range;
19
+ }
20
+ function boneAttributes(type, options, callIndex = 0) {
21
+ const attrs = {
22
+ "data-bone": type,
23
+ "aria-busy": true
24
+ };
25
+ if (type === "text") {
26
+ const style = {};
27
+ if (options?.contained) style["--bone-contained"] = 1;
28
+ const length = resolveLength(options?.length, callIndex);
29
+ if (length) style["--bone-length"] = length;
30
+ if (Object.keys(style).length > 0) attrs.style = style;
31
+ }
32
+ if (type === "block") attrs.src = TRANSPARENT_PIXEL;
33
+ return attrs;
34
+ }
35
+ //#endregion
36
+ export { TRANSPARENT_PIXEL, boneAttributes, isMinMax, minMax, resolveLength };