nano-skeleton 0.2.0 → 0.4.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/CHANGELOG.md +12 -0
- package/README.md +40 -2
- package/dist/Skeleton.d.ts +4 -1
- package/dist/Skeleton.js +1 -1
- package/dist/Skeleton.js.map +1 -1
- package/dist/constants.js +25 -2
- package/dist/constants.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A tiny, framework-agnostic skeleton loader built on Web Components.
|
|
4
4
|
|
|
5
|
+
**[Live demo](https://smashercosmo.github.io/nano-skeleton/)**
|
|
6
|
+
|
|
5
7
|
Instead of maintaining a separate "loading" version of every component, wrap the
|
|
6
8
|
pieces of your real UI in `<nano-skeleton>` and flip a single `loading` switch.
|
|
7
9
|
The layout stays exactly where it is, nothing jumps, and the content is swapped
|
|
@@ -101,7 +103,7 @@ all of its skeletons at once.
|
|
|
101
103
|
A `<nano-skeleton>` without a provider simply renders its content as usual.
|
|
102
104
|
|
|
103
105
|
`<nano-skeleton>` works in one of two modes, picked automatically from its
|
|
104
|
-
content.
|
|
106
|
+
content (text mode can also be forced with the `text` attribute).
|
|
105
107
|
|
|
106
108
|
### Text mode
|
|
107
109
|
|
|
@@ -130,6 +132,34 @@ placeholder text first and replace it with real data later:
|
|
|
130
132
|
<h2><nano-skeleton>Loading title…</nano-skeleton></h2>
|
|
131
133
|
```
|
|
132
134
|
|
|
135
|
+
#### Text wrapped in elements
|
|
136
|
+
|
|
137
|
+
Automatic detection only looks at text placed directly inside the skeleton.
|
|
138
|
+
When the text is wrapped in an inline element, like a link or `<strong>`, add
|
|
139
|
+
`text` to force text mode:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<p>
|
|
143
|
+
<nano-skeleton text>
|
|
144
|
+
<a href="/posts/42">Read the full post</a>
|
|
145
|
+
</nano-skeleton>
|
|
146
|
+
</p>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The placeholder follows the text lines exactly like plain text does. The
|
|
150
|
+
wrapping elements should be inline: a block or `inline-block` child gets a
|
|
151
|
+
single bar for its whole box.
|
|
152
|
+
|
|
153
|
+
Text decorations, such as a link's underline, stay visible while loading, so the
|
|
154
|
+
placeholder still reads as a link. To hide them too, wrap the link's content in
|
|
155
|
+
a `<span>`:
|
|
156
|
+
|
|
157
|
+
```html
|
|
158
|
+
<nano-skeleton text>
|
|
159
|
+
<a href="/posts/42"><span>Read the full post</span></a>
|
|
160
|
+
</nano-skeleton>
|
|
161
|
+
```
|
|
162
|
+
|
|
133
163
|
### Element mode
|
|
134
164
|
|
|
135
165
|
If the skeleton contains elements, it becomes `display: contents` (it doesn't
|
|
@@ -329,6 +359,7 @@ declare module "react" {
|
|
|
329
359
|
Skeleton
|
|
330
360
|
> & {
|
|
331
361
|
interactive?: boolean;
|
|
362
|
+
text?: boolean;
|
|
332
363
|
};
|
|
333
364
|
[PROVIDER_NAME]: DetailedHTMLProps<
|
|
334
365
|
HTMLAttributes<SkeletonProvider>,
|
|
@@ -362,13 +393,14 @@ While loading, the provider is exposed to assistive technology as
|
|
|
362
393
|
| Attribute / property | Type | Description |
|
|
363
394
|
| -------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
|
|
364
395
|
| `interactive` | `boolean` | Keeps the content focusable and clickable while loading. See [Interactive skeletons](#interactive-skeletons). |
|
|
396
|
+
| `text` | `boolean` | Forces text mode, e.g. for text inside a link. See [Text wrapped in elements](#text-wrapped-in-elements). |
|
|
365
397
|
|
|
366
398
|
It also exposes these state attributes, which you can use as styling hooks:
|
|
367
399
|
|
|
368
400
|
| Attribute | Set when |
|
|
369
401
|
| -------------- | ------------------------------------------------------------------------------------- |
|
|
370
402
|
| `data-loading` | The nearest provider is loading. |
|
|
371
|
-
| `data-text` | The skeleton directly contains text (text mode).
|
|
403
|
+
| `data-text` | The skeleton directly contains text, or `text` is set (text mode). |
|
|
372
404
|
| `inert` | The nearest provider is loading and `interactive` isn't set. Blocks focus and clicks. |
|
|
373
405
|
|
|
374
406
|
### Exports
|
|
@@ -452,6 +484,12 @@ The default colors use `light-dark()`, so they follow the page's
|
|
|
452
484
|
}
|
|
453
485
|
```
|
|
454
486
|
|
|
487
|
+
### Right-to-left
|
|
488
|
+
|
|
489
|
+
The shimmer follows the reading direction: in right-to-left content, set with
|
|
490
|
+
`dir="rtl"` on the skeleton or any ancestor, it moves from right to left. A
|
|
491
|
+
custom `--nano-skeleton-animation` with its own keyframes isn't affected.
|
|
492
|
+
|
|
455
493
|
### Reduced motion
|
|
456
494
|
|
|
457
495
|
The animation is disabled automatically for users with
|
package/dist/Skeleton.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ export declare class Skeleton extends HTMLElement {
|
|
|
5
5
|
/** Keeps the content focusable and clickable while loading */
|
|
6
6
|
get interactive(): boolean;
|
|
7
7
|
set interactive(value: boolean);
|
|
8
|
-
|
|
8
|
+
/** Forces text mode, even when the text is wrapped in elements */
|
|
9
|
+
get text(): boolean;
|
|
10
|
+
set text(value: boolean);
|
|
11
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
9
12
|
connectedCallback(): void;
|
|
10
13
|
disconnectedCallback(): void;
|
|
11
14
|
}
|
package/dist/Skeleton.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{SKELETON_CONTEXT as e}from"./constants.js";import{ContextRequestEvent as t}from"./ContextRequestEvent.js";import{adoptStyles as n}from"./styles.js";function r(e){return`adoptedStyleSheets`in e}var i=class extends HTMLElement{static observedAttributes=[`interactive`];#e=!1;#t;#n=new MutationObserver(()=>this.#r());get interactive(){return this.hasAttribute(`interactive`)}set interactive(e){this.toggleAttribute(`interactive`,!!e)}attributeChangedCallback(e,t,n){t!==n&&this.#i(this.#e)}connectedCallback(){let i=this.getRootNode();r(i)&&n(i),this.#r(),this.#i(!1),this.dispatchEvent(new t(e,(e,t)=>{t&&(this.#t=t),this.#i(e)},!0)),this.#n.observe(this,{childList:!0,characterData:!0,subtree:!0})}disconnectedCallback(){this.#t?.(),this.#t=void 0,this.#n.disconnect()}#r(){let e
|
|
1
|
+
import{SKELETON_CONTEXT as e}from"./constants.js";import{ContextRequestEvent as t}from"./ContextRequestEvent.js";import{adoptStyles as n}from"./styles.js";function r(e){return`adoptedStyleSheets`in e}var i=class extends HTMLElement{static observedAttributes=[`interactive`,`text`];#e=!1;#t;#n=new MutationObserver(()=>this.#r());get interactive(){return this.hasAttribute(`interactive`)}set interactive(e){this.toggleAttribute(`interactive`,!!e)}get text(){return this.hasAttribute(`text`)}set text(e){this.toggleAttribute(`text`,!!e)}attributeChangedCallback(e,t,n){t!==n&&(e===`text`?this.#r():this.#i(this.#e))}connectedCallback(){let i=this.getRootNode();r(i)&&n(i),this.#r(),this.#i(!1),this.dispatchEvent(new t(e,(e,t)=>{t&&(this.#t=t),this.#i(e)},!0)),this.#n.observe(this,{childList:!0,characterData:!0,subtree:!0})}disconnectedCallback(){this.#t?.(),this.#t=void 0,this.#n.disconnect()}#r(){let e=this.text;for(let t of this.childNodes){if(e)break;t instanceof Text&&t.data.trim()!==``&&(e=!0)}this.toggleAttribute(`data-text`,e)}#i(e){this.#e=e,this.toggleAttribute(`inert`,e&&!this.interactive),this.toggleAttribute(`data-loading`,e)}};export{i as Skeleton};
|
|
2
2
|
//# sourceMappingURL=Skeleton.js.map
|
package/dist/Skeleton.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Skeleton.js","names":["isDocumentOrShadowRoot","node","Skeleton","#update","value","oldValue","newValue","#render","#loading","root","adoptStyles","ContextRequestEvent","SKELETON_CONTEXT","unsubscribe","#unsubscribe","loading","#observer","isText"],"sources":["../src/Skeleton.ts"],"sourcesContent":["import { SKELETON_CONTEXT } from \"#root/constants.js\";\nimport { ContextRequestEvent } from \"#root/ContextRequestEvent.js\";\nimport { adoptStyles } from \"#root/styles.js\";\nimport { type Unsubscribe } from \"#root/types.js\";\n\nfunction isDocumentOrShadowRoot(\n node: Node | Document | ShadowRoot,\n): node is Document | ShadowRoot {\n return \"adoptedStyleSheets\" in node;\n}\n\nexport class Skeleton extends HTMLElement {\n static observedAttributes = [\"interactive\"];\n\n #loading = false;\n #unsubscribe?: Unsubscribe | undefined;\n #observer = new MutationObserver(() => this.#update());\n\n /** Keeps the content focusable and clickable while loading */\n get interactive() {\n return this.hasAttribute(\"interactive\");\n }\n\n set interactive(value: boolean) {\n this.toggleAttribute(\"interactive\", Boolean(value));\n }\n\n attributeChangedCallback(\n
|
|
1
|
+
{"version":3,"file":"Skeleton.js","names":["isDocumentOrShadowRoot","node","Skeleton","#update","value","oldValue","newValue","name","#render","#loading","root","adoptStyles","ContextRequestEvent","SKELETON_CONTEXT","unsubscribe","#unsubscribe","loading","#observer","isText"],"sources":["../src/Skeleton.ts"],"sourcesContent":["import { SKELETON_CONTEXT } from \"#root/constants.js\";\nimport { ContextRequestEvent } from \"#root/ContextRequestEvent.js\";\nimport { adoptStyles } from \"#root/styles.js\";\nimport { type Unsubscribe } from \"#root/types.js\";\n\nfunction isDocumentOrShadowRoot(\n node: Node | Document | ShadowRoot,\n): node is Document | ShadowRoot {\n return \"adoptedStyleSheets\" in node;\n}\n\nexport class Skeleton extends HTMLElement {\n static observedAttributes = [\"interactive\", \"text\"];\n\n #loading = false;\n #unsubscribe?: Unsubscribe | undefined;\n #observer = new MutationObserver(() => this.#update());\n\n /** Keeps the content focusable and clickable while loading */\n get interactive() {\n return this.hasAttribute(\"interactive\");\n }\n\n set interactive(value: boolean) {\n this.toggleAttribute(\"interactive\", Boolean(value));\n }\n\n /** Forces text mode, even when the text is wrapped in elements */\n get text() {\n return this.hasAttribute(\"text\");\n }\n\n set text(value: boolean) {\n this.toggleAttribute(\"text\", Boolean(value));\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue) return;\n if (name === \"text\") this.#update();\n else this.#render(this.#loading);\n }\n\n connectedCallback() {\n const root = this.getRootNode();\n\n if (isDocumentOrShadowRoot(root)) {\n adoptStyles(root);\n }\n\n this.#update();\n this.#render(false);\n\n this.dispatchEvent(\n new ContextRequestEvent(\n SKELETON_CONTEXT,\n (loading, unsubscribe) => {\n if (unsubscribe) this.#unsubscribe = unsubscribe;\n this.#render(loading);\n },\n true,\n ),\n );\n\n this.#observer.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n disconnectedCallback() {\n this.#unsubscribe?.();\n this.#unsubscribe = undefined;\n this.#observer.disconnect();\n }\n\n #update() {\n let isText = this.text;\n for (const node of this.childNodes) {\n if (isText) break;\n if (node instanceof Text && node.data.trim() !== \"\") {\n isText = true;\n }\n }\n this.toggleAttribute(\"data-text\", isText);\n }\n\n #render(loading: boolean) {\n this.#loading = loading;\n this.toggleAttribute(\"inert\", loading && !this.interactive);\n this.toggleAttribute(\"data-loading\", loading);\n }\n}\n"],"mappings":"A,2JAKA,SAASA,EACP,EAC+B,CAC/B,MAAO,uBAAwBC,CACjC,CAEA,IAAaC,EAAb,cAA8B,WAAY,CACxC,OAAO,mBAAqB,CAAC,cAAe,MAAM,EAElD,GAAW,GACX,GACA,GAAY,IAAI,qBAAuB,KAAKC,GAAQ,CAAC,EAGrD,IAAI,aAAc,CAChB,OAAO,KAAK,aAAa,aAAa,CACxC,CAEA,IAAI,YAAY,EAAgB,CAC9B,KAAK,gBAAgB,cAAe,EAAQC,CAAM,CACpD,CAGA,IAAI,MAAO,CACT,OAAO,KAAK,aAAa,MAAM,CACjC,CAEA,IAAI,KAAK,EAAgB,CACvB,KAAK,gBAAgB,OAAQ,EAAQA,CAAM,CAC7C,CAEA,yBACE,EACA,EACA,EACA,CACIC,IAAaC,IACbC,IAAS,OAAQ,KAAKJ,GAAQ,EAC7B,KAAKK,GAAQ,KAAKC,EAAQ,EACjC,CAEA,mBAAoB,CAClB,IAAMC,EAAO,KAAK,YAAY,EAE1BV,EAAuBU,CAAI,GAC7BC,EAAYD,CAAI,EAGlB,KAAKP,GAAQ,EACb,KAAKK,GAAQ,EAAK,EAElB,KAAK,cACH,IAAII,EACFC,GACC,EAAS,IAAgB,CACpBC,IAAa,KAAKC,GAAeD,GACrC,KAAKN,GAAQQ,CAAO,CACtB,EACA,EACF,CACF,EAEA,KAAKC,GAAU,QAAQ,KAAM,CAC3B,UAAW,GACX,cAAe,GACf,QAAS,EACX,CAAC,CACH,CAEA,sBAAuB,CACrB,KAAKF,KAAe,EACpB,KAAKA,GAAe,IAAA,GACpB,KAAKE,GAAU,WAAW,CAC5B,CAEA,IAAU,CACR,IAAIC,EAAS,KAAK,KAClB,IAAK,IAAMjB,KAAQ,KAAK,WAAY,CAClC,GAAIiB,EAAQ,MACRjB,aAAgB,MAAQA,EAAK,KAAK,KAAK,IAAM,KAC/C,EAAS,GAEb,CACA,KAAK,gBAAgB,YAAaiB,CAAM,CAC1C,CAEA,GAAQ,EAAkB,CACxB,KAAKT,GAAWO,EAChB,KAAK,gBAAgB,QAASA,GAAW,CAAC,KAAK,WAAW,EAC1D,KAAK,gBAAgB,eAAgBA,CAAO,CAC9C,CACF,E"}
|
package/dist/constants.js
CHANGED
|
@@ -44,6 +44,20 @@ const e=`nano-skeleton`,t=`${e}-provider`,{default:n}={default:`:scope {
|
|
|
44
44
|
* -webkit-text-fill-color overrides any "color" property that is also set.
|
|
45
45
|
*/
|
|
46
46
|
-webkit-text-fill-color: transparent;
|
|
47
|
+
/**
|
|
48
|
+
* Text might get visible when selected
|
|
49
|
+
*/
|
|
50
|
+
user-select: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Children and grandchildren need to inherit these styles
|
|
55
|
+
* for consistent outlines
|
|
56
|
+
*/
|
|
57
|
+
:scope[data-loading][data-text] > *,
|
|
58
|
+
:scope[data-loading][data-text] > * > * {
|
|
59
|
+
border-radius: inherit;
|
|
60
|
+
text-box: inherit;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
:scope[data-loading] > :is(video, img):only-child {
|
|
@@ -72,12 +86,21 @@ const e=`nano-skeleton`,t=`${e}-provider`,{default:n}={default:`:scope {
|
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The shimmer follows the reading direction
|
|
91
|
+
*/
|
|
92
|
+
:scope[data-loading][data-text]:dir(rtl),
|
|
93
|
+
:scope[data-loading]:not([data-text]) > :dir(rtl) {
|
|
94
|
+
--nano-skeleton-shimmer-from: 0%;
|
|
95
|
+
--nano-skeleton-shimmer-to: 100%;
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
@keyframes shimmer {
|
|
76
99
|
from {
|
|
77
|
-
background-position-x: 100
|
|
100
|
+
background-position-x: var(--nano-skeleton-shimmer-from, 100%);
|
|
78
101
|
}
|
|
79
102
|
to {
|
|
80
|
-
background-position-x: 0;
|
|
103
|
+
background-position-x: var(--nano-skeleton-shimmer-to, 0%);
|
|
81
104
|
}
|
|
82
105
|
}
|
|
83
106
|
`},r=Symbol.for(`${e}-context`);export{e as COMPONENT_NAME,n as CSS_CONTENT,t as PROVIDER_NAME,r as SKELETON_CONTEXT};
|
package/dist/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":["COMPONENT_NAME","PROVIDER_NAME","CSS_CONTENT","SKELETON_CONTEXT"],"sources":["../src/constants.ts"],"sourcesContent":["const isDev = import.meta.env.MODE !== \"production\";\n\nexport const COMPONENT_NAME = \"nano-skeleton\" as const;\n\nexport const PROVIDER_NAME = `${COMPONENT_NAME}-provider` as const;\n\nconst { default: CSS_CONTENT } = isDev\n ? await import(\"./styles.css?inline\")\n : { default: `__CSS_CONTENT__` };\n\nexport { CSS_CONTENT };\n\nexport const SKELETON_CONTEXT = Symbol.for(`${COMPONENT_NAME}-context`);\n"],"mappings":"AAEA,MAAaA,EAAiB,gBAEjBC,EAAgB,GAAGD,EAAe,WAEzC,CAAE,QAASE,GAEb,CAAE,QAAS
|
|
1
|
+
{"version":3,"file":"constants.js","names":["COMPONENT_NAME","PROVIDER_NAME","CSS_CONTENT","SKELETON_CONTEXT"],"sources":["../src/constants.ts"],"sourcesContent":["const isDev = import.meta.env.MODE !== \"production\";\n\nexport const COMPONENT_NAME = \"nano-skeleton\" as const;\n\nexport const PROVIDER_NAME = `${COMPONENT_NAME}-provider` as const;\n\nconst { default: CSS_CONTENT } = isDev\n ? await import(\"./styles.css?inline\")\n : { default: `__CSS_CONTENT__` };\n\nexport { CSS_CONTENT };\n\nexport const SKELETON_CONTEXT = Symbol.for(`${COMPONENT_NAME}-context`);\n"],"mappings":"AAEA,MAAaA,EAAiB,gBAEjBC,EAAgB,GAAGD,EAAe,WAEzC,CAAE,QAASE,GAEb,CAAE,QAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyGb,EAIWC,EAAmB,OAAO,IAAI,GAAGH,EAAe,SAAS,E"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nano-skeleton",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The perfect skeleton component",
|
|
5
|
-
"homepage": "https://github.
|
|
5
|
+
"homepage": "https://smashercosmo.github.io/nano-skeleton/",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/smashercosmo/nano-skeleton/issues"
|
|
8
8
|
},
|