@cssints/skeleton 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +144 -2
- package/dist/index.d.mts +23 -0
- package/dist/index.mjs +58 -0
- package/package.json +30 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexei Accio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,145 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cssints/skeleton
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Loading placeholders for [cssints](../../README.md) whose shimmer is in phase across the page, whenever each one was
|
|
4
|
+
mounted: one chain member of `cssints/plugin` `scope()`, after [sync-skeleton](https://github.com/crutchcorn/sync-skeleton).
|
|
5
|
+
No runtime.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { cn, height, hover, width } from "cssints" with { type: "cssints" };
|
|
9
|
+
import { skeleton } from "@cssints/skeleton" with { type: "cssints" };
|
|
10
|
+
|
|
11
|
+
export const line = cn(skeleton(), height("1em"), width("12rem"));
|
|
12
|
+
export const avatar = cn(skeleton().radius("50%"), width("3rem"), height("3rem"));
|
|
13
|
+
export const branded = cn(skeleton().color("#e4e4f0").highlight("#f4f4fa"), height("8rem"));
|
|
14
|
+
export const card = cn(skeleton(), hover(skeleton().highlight("white")));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
@property --skeleton-p {
|
|
19
|
+
syntax: "<percentage>";
|
|
20
|
+
inherits: true;
|
|
21
|
+
initial-value: 100%;
|
|
22
|
+
}
|
|
23
|
+
@layer _.k {
|
|
24
|
+
@keyframes kf-1a2b3c4d-skeleton {
|
|
25
|
+
from {
|
|
26
|
+
--skeleton-p: 100%;
|
|
27
|
+
}
|
|
28
|
+
to {
|
|
29
|
+
--skeleton-p: 0%;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
33
|
+
:root:has(._skeleton-clock) {
|
|
34
|
+
animation: kf-1a2b3c4d-skeleton 1.5s ease-in-out infinite;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
._skeleton-core {
|
|
38
|
+
border-radius: var(--skeleton-r, 0.25rem);
|
|
39
|
+
background-position: var(--skeleton-p) 0;
|
|
40
|
+
background-image: linear-gradient(
|
|
41
|
+
90deg,
|
|
42
|
+
var(--skeleton-c, color-mix(in oklab, currentColor 12%, transparent)) 40%,
|
|
43
|
+
var(--skeleton-h, color-mix(in oklab, currentColor 4%, transparent)) 50%,
|
|
44
|
+
var(--skeleton-c, color-mix(in oklab, currentColor 12%, transparent)) 60%
|
|
45
|
+
);
|
|
46
|
+
background-size: 300% 100%;
|
|
47
|
+
background-attachment: fixed;
|
|
48
|
+
}
|
|
49
|
+
/* only for targets without color-mix() (the defaults have Chrome 109) */
|
|
50
|
+
@supports not (color: color-mix(in oklab, red, red)) {
|
|
51
|
+
._skeleton-core {
|
|
52
|
+
background-image: linear-gradient(
|
|
53
|
+
90deg,
|
|
54
|
+
var(--skeleton-c, rgb(128 128 128 / 20%)) 40%,
|
|
55
|
+
var(--skeleton-h, rgb(128 128 128 / 8%)) 50%,
|
|
56
|
+
var(--skeleton-c, rgb(128 128 128 / 20%)) 60%
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
@layer _.a {
|
|
62
|
+
._kkw3xj {
|
|
63
|
+
--skeleton-r: 50%;
|
|
64
|
+
}
|
|
65
|
+
/* ... */
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`skeleton()` is `"_skeleton-core _skeleton-clock"`; each method adds one class. Both kernels are written as styles, no raw
|
|
70
|
+
CSS: the clock is `media("(prefers-reduced-motion: no-preference)")(nest(":root:has(&)")(animation(keyframes({ from:
|
|
71
|
+
vars({ "--_p": "100%" }), to: vars({ "--_p": "0%" }) }), "1.5s ease-in-out infinite")))`, and only its `@property`
|
|
72
|
+
registration is a string (`atProperty`). The keyframes name is a hash of the steps with the scope's name after it.
|
|
73
|
+
|
|
74
|
+
## How it stays in sync
|
|
75
|
+
|
|
76
|
+
A CSS animation starts when its element gets its style, so a skeleton that mounts 700ms after another runs 700ms behind
|
|
77
|
+
it, and the page flickers out of step. No skeleton here runs an animation. One animation on `:root` moves a registered,
|
|
78
|
+
inherited percentage, `--skeleton-p`, and every skeleton reads it as its `background-position`: there is one clock, so
|
|
79
|
+
there is one phase, in every browser that has `@property`. The animation is on only while a skeleton is in the document
|
|
80
|
+
(`:root:has(._skeleton-clock)`), so a page with none runs nothing; when the last one goes and a new one comes, the clock
|
|
81
|
+
starts again, with nothing to be out of step with.
|
|
82
|
+
|
|
83
|
+
The gradient is three viewports wide and `fixed` to the viewport, so the band sweeps the page once, across every
|
|
84
|
+
skeleton, as one picture (the same pixel at the same viewport x, as the browser test reads it). Where `fixed` has no
|
|
85
|
+
effect (iOS Safari, which BCD marks as a partial implementation) each skeleton has a band of its own size, still on the
|
|
86
|
+
same clock: in phase in time, not in space.
|
|
87
|
+
|
|
88
|
+
The other ways were weighed and left: `background-attachment: fixed` with an animation on each skeleton lines up the
|
|
89
|
+
space but not the time (each animation still starts at its own mount); a negative `animation-delay` from
|
|
90
|
+
`document.timeline.currentTime` is script; CSS has no document timeline to name in `animation-timeline` (only scroll and
|
|
91
|
+
view timelines), and `animation-delay: calc()` cannot read the time.
|
|
92
|
+
|
|
93
|
+
## Rules
|
|
94
|
+
|
|
95
|
+
- **Methods**, each once, in any order, all optional: `radius(<length-percentage [0,∞]>)` (`0.25rem`), `color(<color>)`
|
|
96
|
+
(the base, the text colour at 12%), `highlight(<color>)` (the band, the text colour at 4%). Each is one class that sets
|
|
97
|
+
a custom property, so a method works under a condition (`hover(skeleton().highlight("white"))`).
|
|
98
|
+
- **Colours.** The defaults mix `currentColor` into transparent, so a skeleton is a darker box on a light page and a
|
|
99
|
+
lighter one on a dark page, and its band is less of it (a light sweep on light, a dark one on dark). The kernel reads
|
|
100
|
+
`--skeleton-c`, `--skeleton-h` and `--skeleton-r` with the defaults as fallbacks, so a value set on an ancestor themes
|
|
101
|
+
every skeleton under it. A `background-color` of your own shows through a translucent colour.
|
|
102
|
+
- **Reduced motion.** Under `prefers-reduced-motion: reduce` the clock does not run: `--skeleton-p` stays at its initial
|
|
103
|
+
`100%`, which puts the band off the page, and every skeleton is its base colour, still.
|
|
104
|
+
- **Conflicts.** `skeleton()` sets the background image, size, position and attachment and the corners: a `cn()` that
|
|
105
|
+
holds it and the `background` shorthand, one of those or a `border-radius*` property is a build error, derived from the
|
|
106
|
+
kernel: `cssints: skeleton: kernel core sets border-radius, background-image, background-size, background-position, background-attachment (this style also sets background)`.
|
|
107
|
+
`hover(skeleton())` is a build error too (a kernel cannot be conditional); wrap a method instead.
|
|
108
|
+
- **Errors** are the grammar's, in the types and at build time: `skeleton.radius: unexpected "-4px" in "-4px", expected
|
|
109
|
+
<length-percentage [0,∞]>`, `skeleton.color: unexpected "4px" in "4px", expected <color>`.
|
|
110
|
+
|
|
111
|
+
## Browsers
|
|
112
|
+
|
|
113
|
+
`bun run test:browsers` mounts a 600px skeleton, then 700ms later a 300px one under it and a control that runs the same
|
|
114
|
+
keyframes on itself, pauses the animations and reads a row of each from a screenshot:
|
|
115
|
+
|
|
116
|
+
| Browser | Animations | A and B (300 columns) | Control | Reduced motion |
|
|
117
|
+
| ------------------------------- | --------------- | --------------------- | ------- | --------------------- |
|
|
118
|
+
| Chrome (installed channel) | one, on `:root` | 0 columns differ | 132 | none, the base colour |
|
|
119
|
+
| Firefox (Playwright build) | one, on `:root` | 0 columns differ | 132 | none, the base colour |
|
|
120
|
+
| WebKit (Playwright trunk build) | one, on `:root` | 0 columns differ | 132 | none, the base colour |
|
|
121
|
+
|
|
122
|
+
`@property` (Chrome 85, Firefox 128, Safari 16.4) and `:has()` (Chrome 105, Firefox 121, Safari 15.4) are the kernel's
|
|
123
|
+
`requires`: a target that lacks one is a build warning. Without `@property` the percentage animates as a discrete value
|
|
124
|
+
and the band never shows (the base colour, still); without `:has()` nothing animates. For targets without `color-mix()`
|
|
125
|
+
the build adds the grey defaults under `@supports not`, and only then.
|
|
126
|
+
|
|
127
|
+
## Limits
|
|
128
|
+
|
|
129
|
+
- The clock animates an inherited custom property on `:root`, so while a skeleton is mounted the browser updates the
|
|
130
|
+
style of the whole document on every frame. Fine for a loading page; keep skeletons out of a page that stays busy.
|
|
131
|
+
- One clock for the page: the duration (1.5s), the easing and the direction are the same for every skeleton, and there are
|
|
132
|
+
no methods for them.
|
|
133
|
+
- Text inside a skeleton shows: write it empty, or hide its children. A transparent text colour on the skeleton makes
|
|
134
|
+
the defaults transparent too (they mix `currentColor`); give it `skeleton().color(...).highlight(...)` then.
|
|
135
|
+
- The band is horizontal, left to right; there is no pulse variant.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
cd packages/skeleton && bun run test # fixtures through the engine (output, conflicts, errors); bun run typecheck for the type test
|
|
139
|
+
cd packages/skeleton && bun run test:browsers # the phase and reduced motion in Chrome, Firefox and WebKit
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Credits
|
|
143
|
+
|
|
144
|
+
The shared clock follows [sync-skeleton](https://github.com/crutchcorn/sync-skeleton) (MIT, Corbin Crutchley). The
|
|
145
|
+
CSS is written here; no code is copied.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `skeleton().radius("8px").color("#ddd").highlight("#eee")`: a loading placeholder, each method once and optional
|
|
4
|
+
* (`0.25rem`, the text colour at 12% and 4%). The shimmer of every skeleton on the page is one animation on `:root`, so
|
|
5
|
+
* they are in phase whenever each was mounted; under `prefers-reduced-motion: reduce` it is the base colour, still. It
|
|
6
|
+
* sets the background and the corners, so a `background*` or `border-radius*` property in the same `cn()` is a build
|
|
7
|
+
* error.
|
|
8
|
+
*/
|
|
9
|
+
export declare const skeleton: () => import("cssints/plugin").Chain<"skeleton", {
|
|
10
|
+
readonly radius: {
|
|
11
|
+
readonly args: "<length-percentage [0,∞]>";
|
|
12
|
+
readonly emit: (v: string) => [string, string][];
|
|
13
|
+
};
|
|
14
|
+
readonly color: {
|
|
15
|
+
readonly args: "<color>";
|
|
16
|
+
readonly emit: (v: string) => [string, string][];
|
|
17
|
+
};
|
|
18
|
+
readonly highlight: {
|
|
19
|
+
readonly args: "<color>";
|
|
20
|
+
readonly emit: (v: string) => [string, string][];
|
|
21
|
+
};
|
|
22
|
+
}, {}>;
|
|
23
|
+
//#endregion
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { scope } from "cssints/plugin";
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
const BASE = "color-mix(in oklab, currentColor 12%, transparent)";
|
|
4
|
+
const HIGHLIGHT = "color-mix(in oklab, currentColor 4%, transparent)";
|
|
5
|
+
const GREY = "rgb(128 128 128 / 20%)";
|
|
6
|
+
const GREY_HIGHLIGHT = "rgb(128 128 128 / 8%)";
|
|
7
|
+
const gradient = (base, highlight) => `linear-gradient(90deg, var(--_c, ${base}) 40%, var(--_h, ${highlight}) 50%, var(--_c, ${base}) 60%)`;
|
|
8
|
+
/**
|
|
9
|
+
* `skeleton().radius("8px").color("#ddd").highlight("#eee")`: a loading placeholder, each method once and optional
|
|
10
|
+
* (`0.25rem`, the text colour at 12% and 4%). The shimmer of every skeleton on the page is one animation on `:root`, so
|
|
11
|
+
* they are in phase whenever each was mounted; under `prefers-reduced-motion: reduce` it is the base colour, still. It
|
|
12
|
+
* sets the background and the corners, so a `background*` or `border-radius*` property in the same `cn()` is a build
|
|
13
|
+
* error.
|
|
14
|
+
*/
|
|
15
|
+
const { skeleton } = scope("skeleton", {
|
|
16
|
+
core: { css: (css) => [
|
|
17
|
+
css.borderRadius("var(--_r, 0.25rem)"),
|
|
18
|
+
css.backgroundImage(gradient(BASE, HIGHLIGHT)),
|
|
19
|
+
css.backgroundSize("300% 100%"),
|
|
20
|
+
css.backgroundPosition("var(--_p) 0"),
|
|
21
|
+
css.backgroundAttachment("fixed")
|
|
22
|
+
] },
|
|
23
|
+
clock: {
|
|
24
|
+
css: (css) => css.media("(prefers-reduced-motion: no-preference)")(css.nest(":root:has(&)")(css.animation(css.keyframes({
|
|
25
|
+
from: css.vars({ "--_p": "100%" }),
|
|
26
|
+
to: css.vars({ "--_p": "0%" })
|
|
27
|
+
}), "1.5s ease-in-out infinite"))),
|
|
28
|
+
atProperty: "@property --_p{syntax:\"<percentage>\";inherits:true;initial-value:100%}",
|
|
29
|
+
requires: ["css.at-rules.property", "css.selectors.has"]
|
|
30
|
+
},
|
|
31
|
+
"#grey": {
|
|
32
|
+
polyfills: ["css.types.color.color-mix", "css.types.color.oklab"],
|
|
33
|
+
css: `@supports not (color:color-mix(in oklab,red,red)){&{background-image:${gradient(GREY, GREY_HIGHLIGHT)}}}`
|
|
34
|
+
},
|
|
35
|
+
skeleton: {
|
|
36
|
+
uses: [
|
|
37
|
+
"core",
|
|
38
|
+
"clock",
|
|
39
|
+
"grey"
|
|
40
|
+
],
|
|
41
|
+
chain: {
|
|
42
|
+
radius: {
|
|
43
|
+
args: "<length-percentage [0,∞]>",
|
|
44
|
+
emit: (v) => [["--_r", v]]
|
|
45
|
+
},
|
|
46
|
+
color: {
|
|
47
|
+
args: "<color>",
|
|
48
|
+
emit: (v) => [["--_c", v]]
|
|
49
|
+
},
|
|
50
|
+
highlight: {
|
|
51
|
+
args: "<color>",
|
|
52
|
+
emit: (v) => [["--_h", v]]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}).export();
|
|
57
|
+
//#endregion
|
|
58
|
+
export { skeleton };
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cssints/skeleton",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skeleton loaders for cssints whose shimmer is in phase across the page: one animation on :root, read by every skeleton",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/alexeiaccio/css-in-ts-compiler.git",
|
|
9
|
+
"directory": "packages/skeleton"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://cssints.dev",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"cssints": "^0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|