nano-skeleton 0.5.0 → 0.6.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - refactor: replace `init()` with static `define()` methods
8
+
9
+ BREAKING CHANGES:
10
+
11
+ - `init()` is no more, use `SkeletonProvider.define()` and `Skeleton.define()` instead
12
+
13
+ ## 0.5.1
14
+
15
+ ### Patch Changes
16
+
17
+ - chore: add oxlint
18
+
3
19
  ## 0.5.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -54,16 +54,18 @@ yarn add nano-skeleton
54
54
 
55
55
  ## Quick start
56
56
 
57
- Call `init()` once to register the custom elements:
57
+ Register the custom elements:
58
58
 
59
59
  ```js
60
- import { init } from "nano-skeleton";
60
+ import { Skeleton, SkeletonProvider } from "nano-skeleton";
61
61
 
62
- init();
62
+ SkeletonProvider.define();
63
+ Skeleton.define();
63
64
  ```
64
65
 
65
- `init()` is idempotent, so calling it more than once (for example from several
66
- entry points or during hot reload) is safe.
66
+ Each `define()` does nothing if the element is already registered, so calling
67
+ it more than once (for example from several entry points or during hot reload)
68
+ is safe.
67
69
 
68
70
  Then use the elements in your markup:
69
71
 
@@ -103,9 +105,9 @@ all of its skeletons at once.
103
105
  A `<nano-skeleton>` without a provider simply renders its content as usual.
104
106
 
105
107
  `<nano-skeleton>` works in one of two modes, picked automatically from its
106
- content (text mode can also be forced with the `text` attribute).
108
+ content (inline mode can also be forced with the `inline` attribute).
107
109
 
108
- ### Text mode
110
+ ### Inline mode
109
111
 
110
112
  If the skeleton directly contains non-empty text, it renders inline and the text
111
113
  itself becomes the placeholder. Each line of wrapped text gets its own rounded
@@ -136,11 +138,11 @@ placeholder text first and replace it with real data later:
136
138
 
137
139
  Automatic detection only looks at text placed directly inside the skeleton.
138
140
  When the text is wrapped in an inline element, like a link or `<strong>`, add
139
- `text` to force text mode:
141
+ `inline` to force inline mode:
140
142
 
141
143
  ```html
142
144
  <p>
143
- <nano-skeleton text>
145
+ <nano-skeleton inline>
144
146
  <a href="/posts/42">Read the full post</a>
145
147
  </nano-skeleton>
146
148
  </p>
@@ -155,7 +157,7 @@ placeholder still reads as a link. To hide them too, wrap the link's content in
155
157
  a `<span>`:
156
158
 
157
159
  ```html
158
- <nano-skeleton text>
160
+ <nano-skeleton inline>
159
161
  <a href="/posts/42"><span>Read the full post</span></a>
160
162
  </nano-skeleton>
161
163
  ```
@@ -304,10 +306,11 @@ React 19 passes properties to custom elements, so `loading` can be bound
304
306
  directly:
305
307
 
306
308
  ```tsx
307
- import { init } from "nano-skeleton";
308
309
  import { useQuery } from "@tanstack/react-query";
310
+ import { Skeleton, SkeletonProvider } from "nano-skeleton";
309
311
 
310
- init();
312
+ SkeletonProvider.define();
313
+ Skeleton.define();
311
314
 
312
315
  export function UserCard({ id }: { id: string }) {
313
316
  const { data, isPending } = useQuery({
@@ -359,7 +362,7 @@ declare module "react" {
359
362
  Skeleton
360
363
  > & {
361
364
  interactive?: boolean;
362
- text?: boolean;
365
+ inline?: boolean;
363
366
  };
364
367
  [PROVIDER_NAME]: DetailedHTMLProps<
365
368
  HTMLAttributes<SkeletonProvider>,
@@ -374,10 +377,15 @@ declare module "react" {
374
377
 
375
378
  ## API
376
379
 
377
- ### `init()`
380
+ ### `SkeletonProvider.define()`
378
381
 
379
- Registers `<nano-skeleton-provider>` and `<nano-skeleton>` with
380
- `customElements`. Safe to call multiple times.
382
+ Registers `<nano-skeleton-provider>` with `customElements`. Does nothing if the
383
+ element is already registered, so it's safe to call multiple times.
384
+
385
+ ### `Skeleton.define()`
386
+
387
+ Registers `<nano-skeleton>` with `customElements`. Does nothing if the element
388
+ is already registered, so it's safe to call multiple times.
381
389
 
382
390
  ### `<nano-skeleton-provider>`
383
391
 
@@ -393,21 +401,21 @@ While loading, the provider is exposed to assistive technology as
393
401
  | Attribute / property | Type | Description |
394
402
  | -------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
395
403
  | `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). |
404
+ | `inline` | `boolean` | Forces inline mode, e.g. for text inside a link. See [Text wrapped in elements](#text-wrapped-in-elements). |
397
405
 
398
- It also exposes these state attributes, which you can use as styling hooks:
406
+ The component also manages these attributes itself, which you can use as
407
+ styling hooks:
399
408
 
400
- | Attribute | Set when |
401
- | -------------- | ------------------------------------------------------------------------------------- |
402
- | `data-loading` | The nearest provider is loading. |
403
- | `data-text` | The skeleton directly contains text, or `text` is set (text mode). |
404
- | `inert` | The nearest provider is loading and `interactive` isn't set. Blocks focus and clicks. |
409
+ | Attribute | Set when |
410
+ | --------- | ------------------------------------------------------------------------------------- |
411
+ | `loading` | The nearest provider is loading. |
412
+ | `inline` | The skeleton directly contains text, or `inline` is set (inline mode). |
413
+ | `inert` | The nearest provider is loading and `interactive` isn't set. Blocks focus and clicks. |
405
414
 
406
415
  ### Exports
407
416
 
408
417
  ```ts
409
418
  import {
410
- init,
411
419
  Skeleton, // <nano-skeleton> class
412
420
  SkeletonProvider, // <nano-skeleton-provider> class
413
421
  COMPONENT_NAME, // "nano-skeleton"
@@ -419,12 +427,11 @@ import {
419
427
 
420
428
  The look is controlled by CSS custom properties:
421
429
 
422
- | Property | Default |
423
- | ------------------------------------ | -------------------------------------- |
424
- | `--nano-skeleton-background-image` | A light/dark-aware horizontal gradient |
425
- | `--nano-skeleton-background-size` | `300%` |
426
- | `--nano-skeleton-animation` | `shimmer 2s infinite ease-in-out` |
427
- | `--nano-skeleton-text-border-radius` | `0.375rem` (text mode only) |
430
+ | Property | Default |
431
+ | ---------------------------------- | -------------------------------------- |
432
+ | `--nano-skeleton-background-image` | A light/dark-aware horizontal gradient |
433
+ | `--nano-skeleton-background-size` | `300%` |
434
+ | `--nano-skeleton-animation` | `shimmer 2s infinite ease-in-out` |
428
435
 
429
436
  Each `<nano-skeleton>` inherits these from its ancestors, so set them on a
430
437
  parent element (or on `:root` to change all skeletons at once), not on the
@@ -1,5 +1,4 @@
1
- //#region src/constants.d.ts
2
- export declare const COMPONENT_NAME: "nano-skeleton";
3
- export declare const PROVIDER_NAME: "nano-skeleton-provider";
4
- //#endregion
1
+ declare const COMPONENT_NAME: "nano-skeleton";
2
+ declare const PROVIDER_NAME: "nano-skeleton-provider";
3
+ export { COMPONENT_NAME, PROVIDER_NAME };
5
4
  //# sourceMappingURL=constants.d.ts.map
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Gb,EAIWC,EAAmB,OAAO,IAAI,GAAGH,EAAe,SAAS,E"}
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\nconst COMPONENT_NAME = \"nano-skeleton\" as const;\n\nconst PROVIDER_NAME = `${COMPONENT_NAME}-provider` as const;\n\nconst { default: CSS_CONTENT } = isDev\n ? // oxlint-disable-next-line node/no-top-level-await -- this is fine for dev mode\n await import(\"./styles.css?inline\")\n : { default: `__CSS_CONTENT__` };\n\nconst SKELETON_CONTEXT = Symbol.for(`${COMPONENT_NAME}-context`);\n\nexport { COMPONENT_NAME, PROVIDER_NAME, CSS_CONTENT, SKELETON_CONTEXT };\n"],"mappings":"AAEA,MAAMA,EAAiB,gBAEjBC,EAAgB,GAAGD,EAAe,WAElC,CAAE,QAASE,GAGb,CAAE,QAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Gb,EAEIC,EAAmB,OAAO,IAAI,GAAGH,EAAe,SAAS,E"}
@@ -1,2 +1,2 @@
1
1
  var e=class extends Event{context;callback;subscribe;constructor(e,t,n=!1){super(`context-request`,{bubbles:!0,composed:!0}),this.context=e,this.callback=t,this.subscribe=n}};export{e as ContextRequestEvent};
2
- //# sourceMappingURL=ContextRequestEvent.js.map
2
+ //# sourceMappingURL=context-request-event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-request-event.js","names":["ContextRequestEvent","context","callback","subscribe"],"sources":["../src/context-request-event.ts"],"sourcesContent":["import type { LoadingCallback } from \"#root/types.js\";\n\n/**\n * Follows the community Context Protocol (@see\n * https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md)\n */\nexport class ContextRequestEvent extends Event {\n public readonly context: unknown;\n public readonly callback: LoadingCallback;\n public readonly subscribe: boolean;\n\n public constructor(\n context: unknown,\n callback: LoadingCallback,\n subscribe = false,\n ) {\n /** Composed, so skeletons inside shadow roots can reach a provider outside */\n super(\"context-request\", { bubbles: true, composed: true });\n this.context = context;\n this.callback = callback;\n this.subscribe = subscribe;\n }\n}\n"],"mappings":"AAMA,IAAaA,EAAb,cAAyC,KAAM,CAC7C,QACA,SACA,UAEA,YACE,EACA,EACA,EAAY,GACZ,CAEA,MAAM,kBAAmB,CAAE,QAAS,GAAM,SAAU,EAAK,CAAC,EAC1D,KAAK,QAAUC,EACf,KAAK,SAAWC,EAChB,KAAK,UAAYC,CACnB,CACF,E"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,4 @@
1
1
  import { COMPONENT_NAME, PROVIDER_NAME } from "./constants.js";
2
- import { Skeleton } from "./Skeleton.js";
3
- import { SkeletonProvider } from "./SkeletonProvider.js";
4
- //#region src/index.d.ts
5
- export declare function init(): void;
6
- //#endregion
7
- export { COMPONENT_NAME, PROVIDER_NAME, Skeleton, SkeletonProvider };
8
- //# sourceMappingURL=index.d.ts.map
2
+ import { Skeleton } from "./skeleton.js";
3
+ import { SkeletonProvider } from "./skeleton-provider.js";
4
+ export { COMPONENT_NAME, PROVIDER_NAME, Skeleton, SkeletonProvider };
package/dist/index.js CHANGED
@@ -1,2 +1 @@
1
- import{COMPONENT_NAME as e,PROVIDER_NAME as t}from"./constants.js";import{Skeleton as n}from"./Skeleton.js";import{SkeletonProvider as r}from"./SkeletonProvider.js";function i(){customElements.get(`nano-skeleton-provider`)||customElements.define(t,r),customElements.get(`nano-skeleton`)||customElements.define(e,n)}export{e as COMPONENT_NAME,t as PROVIDER_NAME,n as Skeleton,r as SkeletonProvider,i as init};
2
- //# sourceMappingURL=index.js.map
1
+ import{COMPONENT_NAME as e,PROVIDER_NAME as t}from"./constants.js";import{Skeleton as n}from"./skeleton.js";import{SkeletonProvider as r}from"./skeleton-provider.js";export{e as COMPONENT_NAME,t as PROVIDER_NAME,n as Skeleton,r as SkeletonProvider};
@@ -1,11 +1,10 @@
1
- //#region src/SkeletonProvider.d.ts
2
1
  export declare class SkeletonProvider extends HTMLElement {
3
2
  #private;
4
- static observedAttributes: string[];
3
+ static readonly observedAttributes: string[];
4
+ static define(): void;
5
5
  constructor();
6
6
  get loading(): boolean;
7
7
  set loading(value: boolean);
8
8
  attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
9
9
  }
10
- //#endregion
11
- //# sourceMappingURL=SkeletonProvider.d.ts.map
10
+ //# sourceMappingURL=skeleton-provider.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{PROVIDER_NAME as e,SKELETON_CONTEXT as t}from"./constants.js";var n=class extends HTMLElement{static observedAttributes=[`loading`];static define(){customElements.get(`nano-skeleton-provider`)||customElements.define(e,this)}#e=/* @__PURE__ */ new Set;#t=this.attachInternals();constructor(){super(),this.addEventListener(`context-request`,e=>{if(e.context!==t||typeof e.callback!=`function`)return;e.stopPropagation();let{callback:n}=e;e.subscribe?(this.#e.add(n),n(this.loading,()=>{this.#e.delete(n)})):n(this.loading)})}get loading(){return this.hasAttribute(`loading`)}set loading(e){this.toggleAttribute(`loading`,e)}attributeChangedCallback(e,t,n){if(t!==n){this.#t.ariaBusy=this.loading?`true`:null;for(let e of this.#e)e(this.loading)}}};export{n as SkeletonProvider};
2
+ //# sourceMappingURL=skeleton-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skeleton-provider.js","names":["SkeletonProvider","PROVIDER_NAME","event","SKELETON_CONTEXT","subscriber","#subscribers","value","oldValue","newValue","#internals"],"sources":["../src/skeleton-provider.ts"],"sourcesContent":["import type { ContextRequestEvent } from \"#root/context-request-event.js\";\nimport type { LoadingCallback } from \"#root/types.js\";\n\nimport { PROVIDER_NAME, SKELETON_CONTEXT } from \"#root/constants.js\";\n\nexport class SkeletonProvider extends HTMLElement {\n public static readonly observedAttributes = [\"loading\"];\n public static define() {\n if (!customElements.get(PROVIDER_NAME)) {\n customElements.define(PROVIDER_NAME, this);\n }\n }\n\n readonly #subscribers = new Set<LoadingCallback>();\n readonly #internals = this.attachInternals();\n\n public constructor() {\n super();\n /**\n * Adding listener from the constructor so the provider is ready\n * before any descendant connects and asks for context.\n */\n this.addEventListener(\n \"context-request\",\n (event: Readonly<ContextRequestEvent>) => {\n if (\n event.context !== SKELETON_CONTEXT ||\n typeof event.callback !== \"function\"\n ) {\n return;\n }\n\n /** The nearest provider wins */\n event.stopPropagation();\n\n const { callback: subscriber } = event;\n\n if (event.subscribe) {\n this.#subscribers.add(subscriber);\n subscriber(this.loading, () => {\n this.#subscribers.delete(subscriber);\n });\n } else {\n subscriber(this.loading);\n }\n },\n );\n }\n\n public get loading() {\n return this.hasAttribute(\"loading\");\n }\n\n public set loading(value: boolean) {\n this.toggleAttribute(\"loading\", value);\n }\n\n public attributeChangedCallback(\n _name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue) {\n return;\n }\n /** Setting `aria-busy` Via internals so it can be overridden by user */\n // oxlint-disable-next-line unicorn/no-null -- ariaBusy accepts either string or null\n this.#internals.ariaBusy = this.loading ? \"true\" : null;\n for (const subscriber of this.#subscribers) {\n subscriber(this.loading);\n }\n }\n}\n"],"mappings":"A,qEAKA,IAAaA,EAAb,cAAsC,WAAY,CAChD,OAAuB,mBAAqB,CAAC,SAAS,EACtD,OAAc,QAAS,CAChB,eAAe,IAAA,wBAAiB,GACnC,eAAe,OAAOC,EAAe,IAAI,CAE7C,CAEA,mBAAwB,IAAI,IAC5B,GAAsB,KAAK,gBAAgB,EAE3C,aAAqB,CACnB,MAAM,EAKN,KAAK,iBACH,kBACC,GAAyC,CACxC,GACEC,EAAM,UAAYC,GAClB,OAAOD,EAAM,UAAa,WAE1B,OAIFA,EAAM,gBAAgB,EAEtB,GAAM,CAAE,SAAUE,GAAeF,EAE7BA,EAAM,WACR,KAAKG,GAAa,IAAID,CAAU,EAChCA,EAAW,KAAK,YAAe,CAC7B,KAAKC,GAAa,OAAOD,CAAU,CACrC,CAAC,GAEDA,EAAW,KAAK,OAAO,CAE3B,CACF,CACF,CAEA,IAAW,SAAU,CACnB,OAAO,KAAK,aAAa,SAAS,CACpC,CAEA,IAAW,QAAQ,EAAgB,CACjC,KAAK,gBAAgB,UAAWE,CAAK,CACvC,CAEA,yBACE,EACA,EACA,EACA,CACA,GAAIC,IAAaC,EAKjB,MAAKC,GAAW,SAAW,KAAK,QAAU,OAAS,KACnD,IAAK,IAAML,KAAc,KAAKC,GAC5BD,EAAW,KAAK,OAAO,CAF0B,CAIrD,CACF,E"}
@@ -1,18 +1,15 @@
1
- //#region src/Skeleton.d.ts
2
1
  export declare class Skeleton extends HTMLElement {
3
2
  #private;
4
- static observedAttributes: string[];
3
+ static readonly observedAttributes: string[];
4
+ static define(): void;
5
5
  get loading(): boolean;
6
6
  set loading(value: boolean);
7
- /** Keeps the content focusable and clickable while loading */
8
7
  get interactive(): boolean;
9
8
  set interactive(value: boolean);
10
- /** Forces text mode, even when the text is wrapped in elements */
11
9
  get inline(): boolean;
12
10
  set inline(value: boolean);
13
11
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
14
- connectedCallback(): void;
12
+ connectedCallback(): Promise<void>;
15
13
  disconnectedCallback(): void;
16
14
  }
17
- //#endregion
18
- //# sourceMappingURL=Skeleton.d.ts.map
15
+ //# sourceMappingURL=skeleton.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{COMPONENT_NAME as e,SKELETON_CONTEXT as t}from"./constants.js";import{ContextRequestEvent as n}from"./context-request-event.js";import{adoptStyles as r}from"./styles.js";function i(e){return`adoptedStyleSheets`in e}var a=class extends HTMLElement{static observedAttributes=[`interactive`,`inline`];static define(){customElements.get(`nano-skeleton`)||customElements.define(e,this)}#e;#t=new MutationObserver(()=>{this.#n()});get loading(){return this.hasAttribute(`loading`)}set loading(e){this.toggleAttribute(`loading`,e)}get interactive(){return this.hasAttribute(`interactive`)}set interactive(e){this.toggleAttribute(`interactive`,e)}get inline(){return this.hasAttribute(`inline`)}set inline(e){this.toggleAttribute(`inline`,e)}attributeChangedCallback(e,t,n){t!==n&&(e===`inline`?this.#n():this.#r(this.loading))}async connectedCallback(){let e=this.getRootNode();i(e)&&await r(e),this.#n(),this.#r(!1),this.dispatchEvent(new n(t,(e,t)=>{t&&(this.#e=t),this.#r(e)},!0)),this.#t.observe(this,{childList:!0,characterData:!0,subtree:!0})}disconnectedCallback(){this.#e?.(),this.#e=void 0,this.#t.disconnect()}#n(){let e=this.inline;for(let t of this.childNodes){if(e)break;t instanceof Text&&t.data.trim()!==``&&(e=!0)}this.toggleAttribute(`inline`,e)}#r(e){this.toggleAttribute(`loading`,e),this.toggleAttribute(`inert`,e&&!this.interactive)}};export{a as Skeleton};
2
+ //# sourceMappingURL=skeleton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skeleton.js","names":["isDocumentOrShadowRoot","node","Skeleton","COMPONENT_NAME","#update","value","oldValue","newValue","name","#render","root","adoptStyles","ContextRequestEvent","SKELETON_CONTEXT","unsubscribe","#unsubscribe","loading","#observer","isInline"],"sources":["../src/skeleton.ts"],"sourcesContent":["import type { Unsubscribe } from \"#root/types.js\";\n\nimport { COMPONENT_NAME, SKELETON_CONTEXT } from \"#root/constants.js\";\nimport { ContextRequestEvent } from \"#root/context-request-event.js\";\nimport { adoptStyles } from \"#root/styles.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 public static readonly observedAttributes = [\"interactive\", \"inline\"];\n public static define() {\n if (!customElements.get(COMPONENT_NAME)) {\n customElements.define(COMPONENT_NAME, this);\n }\n }\n\n #unsubscribe?: Unsubscribe | undefined;\n\n readonly #observer = new MutationObserver(() => {\n this.#update();\n });\n\n public get loading() {\n return this.hasAttribute(\"loading\");\n }\n\n public set loading(value: boolean) {\n this.toggleAttribute(\"loading\", value);\n }\n\n public get interactive() {\n return this.hasAttribute(\"interactive\");\n }\n\n public set interactive(value: boolean) {\n this.toggleAttribute(\"interactive\", value);\n }\n\n public get inline() {\n return this.hasAttribute(\"inline\");\n }\n\n public set inline(value: boolean) {\n this.toggleAttribute(\"inline\", value);\n }\n\n public attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue) {\n return;\n }\n if (name === \"inline\") {\n this.#update();\n } else {\n this.#render(this.loading);\n }\n }\n\n public async connectedCallback() {\n const root = this.getRootNode();\n\n if (isDocumentOrShadowRoot(root)) {\n await 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) {\n this.#unsubscribe = unsubscribe;\n }\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 public disconnectedCallback() {\n this.#unsubscribe?.();\n this.#unsubscribe = undefined;\n this.#observer.disconnect();\n }\n\n #update() {\n let isInline = this.inline;\n /**\n * Here we auto-determine `inline` mode.\n * Skeleton is considered `inline` if it contains\n * text nodes only.\n */\n for (const node of this.childNodes) {\n if (isInline) {\n break;\n }\n if (node instanceof Text && node.data.trim() !== \"\") {\n isInline = true;\n }\n }\n this.toggleAttribute(\"inline\", isInline);\n }\n\n #render(loading: boolean) {\n this.toggleAttribute(\"loading\", loading);\n this.toggleAttribute(\"inert\", loading && !this.interactive);\n }\n}\n"],"mappings":"A,iLAMA,SAASA,EACP,EAC+B,CAC/B,MAAO,uBAAwBC,CACjC,CAEA,IAAaC,EAAb,cAA8B,WAAY,CACxC,OAAuB,mBAAqB,CAAC,cAAe,QAAQ,EACpE,OAAc,QAAS,CAChB,eAAe,IAAA,eAAkB,GACpC,eAAe,OAAOC,EAAgB,IAAI,CAE9C,CAEA,GAEA,GAAqB,IAAI,qBAAuB,CAC9C,KAAKC,GAAQ,CACf,CAAC,EAED,IAAW,SAAU,CACnB,OAAO,KAAK,aAAa,SAAS,CACpC,CAEA,IAAW,QAAQ,EAAgB,CACjC,KAAK,gBAAgB,UAAWC,CAAK,CACvC,CAEA,IAAW,aAAc,CACvB,OAAO,KAAK,aAAa,aAAa,CACxC,CAEA,IAAW,YAAY,EAAgB,CACrC,KAAK,gBAAgB,cAAeA,CAAK,CAC3C,CAEA,IAAW,QAAS,CAClB,OAAO,KAAK,aAAa,QAAQ,CACnC,CAEA,IAAW,OAAO,EAAgB,CAChC,KAAK,gBAAgB,SAAUA,CAAK,CACtC,CAEA,yBACE,EACA,EACA,EACA,CACIC,IAAaC,IAGbC,IAAS,SACX,KAAKJ,GAAQ,EAEb,KAAKK,GAAQ,KAAK,OAAO,EAE7B,CAEA,MAAa,mBAAoB,CAC/B,IAAMC,EAAO,KAAK,YAAY,EAE1BV,EAAuBU,CAAI,GAC7B,MAAMC,EAAYD,CAAI,EAGxB,KAAKN,GAAQ,EACb,KAAKK,GAAQ,EAAK,EAElB,KAAK,cACH,IAAIG,EACFC,GACC,EAAS,IAAgB,CACpBC,IACF,KAAKC,GAAeD,GAEtB,KAAKL,GAAQO,CAAO,CACtB,EACA,EACF,CACF,EAEA,KAAKC,GAAU,QAAQ,KAAM,CAC3B,UAAW,GACX,cAAe,GACf,QAAS,EACX,CAAC,CACH,CAEA,sBAA8B,CAC5B,KAAKF,KAAe,EACpB,KAAKA,GAAe,IAAA,GACpB,KAAKE,GAAU,WAAW,CAC5B,CAEA,IAAU,CACR,IAAIC,EAAW,KAAK,OAMpB,IAAK,IAAMjB,KAAQ,KAAK,WAAY,CAClC,GAAIiB,EACF,MAEEjB,aAAgB,MAAQA,EAAK,KAAK,KAAK,IAAM,KAC/C,EAAW,GAEf,CACA,KAAK,gBAAgB,SAAUiB,CAAQ,CACzC,CAEA,GAAQ,EAAkB,CACxB,KAAK,gBAAgB,UAAWF,CAAO,EACvC,KAAK,gBAAgB,QAASA,GAAW,CAAC,KAAK,WAAW,CAC5D,CACF,E"}
package/dist/styles.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import{COMPONENT_NAME as e,CSS_CONTENT as t}from"./constants.js";const n=`@scope (${e}) {
2
2
  ${t}
3
- }`;let r;function i(e){r||(r=new CSSStyleSheet,r.replaceSync(n)),e.adoptedStyleSheets.includes(r)||(e.adoptedStyleSheets=[...e.adoptedStyleSheets,r])}export{i as adoptStyles};
3
+ }`,r={};async function i(e){r.sheet||(r.sheet=new CSSStyleSheet,await r.sheet.replace(n));let{sheet:t}=r;e.adoptedStyleSheets.includes(t)||(e.adoptedStyleSheets=[...e.adoptedStyleSheets,t])}export{i as adoptStyles};
4
4
  //# sourceMappingURL=styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.js","names":["css","COMPONENT_NAME","CSS_CONTENT","sheet","adoptStyles","root"],"sources":["../src/styles.ts"],"sourcesContent":["import { CSS_CONTENT, COMPONENT_NAME } from \"#root/constants.js\";\n\n/** It gets replaced with styles.css content during build. */\nconst css = `@scope (${COMPONENT_NAME}) {\n ${CSS_CONTENT}\n}`;\n\nlet sheet: CSSStyleSheet | undefined;\n\nexport function adoptStyles(root: Document | ShadowRoot) {\n if (!sheet) {\n sheet = new CSSStyleSheet();\n sheet.replaceSync(css);\n }\n if (!root.adoptedStyleSheets.includes(sheet)) {\n root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];\n }\n}\n"],"mappings":"A,iEAGA,MAAMA,EAAM,WAAWC,EAAe;IAClCC,EAAY;GAGhB,IAAIC,EAEJ,SAAgBC,EAAY,EAA6B,CAClDD,IACH,EAAQ,IAAI,cACZA,EAAM,YAAYH,CAAG,GAElBK,EAAK,mBAAmB,SAASF,CAAK,IACzC,EAAK,mBAAqB,CAAC,GAAGE,EAAK,mBAAoBF,CAAK,EAEhE,C"}
1
+ {"version":3,"file":"styles.js","names":["css","COMPONENT_NAME","CSS_CONTENT","cache","adoptStyles","root","sheet"],"sources":["../src/styles.ts"],"sourcesContent":["import { CSS_CONTENT, COMPONENT_NAME } from \"#root/constants.js\";\n\n/** CSS_CONTENT constant gets replaced with styles.css content during build. */\nconst css = `@scope (${COMPONENT_NAME}) {\n ${CSS_CONTENT}\n}`;\n\nconst cache: { sheet?: CSSStyleSheet } = {};\n\nexport async function adoptStyles(root: Document | ShadowRoot) {\n if (!cache.sheet) {\n cache.sheet = new CSSStyleSheet();\n await cache.sheet.replace(css);\n }\n const { sheet } = cache;\n if (!root.adoptedStyleSheets.includes(sheet)) {\n root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];\n }\n}\n"],"mappings":"A,iEAGA,MAAMA,EAAM,WAAWC,EAAe;IAClCC,EAAY;GAGVC,EAAmC,CAAC,EAE1C,eAAsBC,EAAY,EAA6B,CACxDD,EAAM,QACT,EAAM,MAAQ,IAAI,cAClB,MAAMA,EAAM,MAAM,QAAQH,CAAG,GAE/B,GAAM,CAAE,SAAUG,EACbE,EAAK,mBAAmB,SAASC,CAAK,IACzC,EAAK,mBAAqB,CAAC,GAAGD,EAAK,mBAAoBC,CAAK,EAEhE,C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-skeleton",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "The perfect skeleton component",
5
5
  "homepage": "https://smashercosmo.github.io/nano-skeleton/",
6
6
  "bugs": {
@@ -22,11 +22,11 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@internal/tsconfig": "0.0.0",
25
- "@tsdown/css": "0.23.1",
26
- "@types/node": "26.6.5",
25
+ "@tsdown/css": "0.23.4",
26
+ "@types/node": "26.6.6",
27
27
  "bumpp": "12.3.0",
28
- "rolldown": "1.2.14",
29
- "tsdown": "0.23.1"
28
+ "rolldown": "1.2.16",
29
+ "tsdown": "0.23.4"
30
30
  },
31
31
  "types": "./dist/index.d.ts",
32
32
  "exports": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"ContextRequestEvent.js","names":["ContextRequestEvent","context","callback","subscribe"],"sources":["../src/ContextRequestEvent.ts"],"sourcesContent":["import type { LoadingCallback } from \"#root/types.js\";\n\n/**\n * Follows the community Context Protocol (@see\n * https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md)\n */\nexport class ContextRequestEvent extends Event {\n readonly context: unknown;\n readonly callback: LoadingCallback;\n readonly subscribe: boolean;\n\n constructor(context: unknown, callback: LoadingCallback, subscribe = false) {\n /** Composed, so skeletons inside shadow roots can reach a provider outside */\n super(\"context-request\", { bubbles: true, composed: true });\n this.context = context;\n this.callback = callback;\n this.subscribe = subscribe;\n }\n}\n"],"mappings":"AAMA,IAAaA,EAAb,cAAyC,KAAM,CAC7C,QACA,SACA,UAEA,YAAY,EAAkB,EAA2B,EAAY,GAAO,CAE1E,MAAM,kBAAmB,CAAE,QAAS,GAAM,SAAU,EAAK,CAAC,EAC1D,KAAK,QAAUC,EACf,KAAK,SAAWC,EAChB,KAAK,UAAYC,CACnB,CACF,E"}
package/dist/Skeleton.js DELETED
@@ -1,2 +0,0 @@
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`,`inline`];#e;#t=new MutationObserver(()=>this.#n());get loading(){return this.hasAttribute(`loading`)}set loading(e){this.toggleAttribute(`loading`,!!e)}get interactive(){return this.hasAttribute(`interactive`)}set interactive(e){this.toggleAttribute(`interactive`,!!e)}get inline(){return this.hasAttribute(`inline`)}set inline(e){this.toggleAttribute(`inline`,!!e)}attributeChangedCallback(e,t,n){t!==n&&(e===`inline`?this.#n():this.#r(this.loading))}connectedCallback(){let i=this.getRootNode();r(i)&&n(i),this.#n(),this.#r(!1),this.dispatchEvent(new t(e,(e,t)=>{t&&(this.#e=t),this.#r(e)},!0)),this.#t.observe(this,{childList:!0,characterData:!0,subtree:!0})}disconnectedCallback(){this.#e?.(),this.#e=void 0,this.#t.disconnect()}#n(){let e=this.inline;for(let t of this.childNodes){if(e)break;t instanceof Text&&t.data.trim()!==``&&(e=!0)}this.toggleAttribute(`inline`,e)}#r(e){this.toggleAttribute(`loading`,e),this.toggleAttribute(`inert`,e&&!this.interactive)}};export{i as Skeleton};
2
- //# sourceMappingURL=Skeleton.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Skeleton.js","names":["isDocumentOrShadowRoot","node","Skeleton","#update","value","oldValue","newValue","name","#render","root","adoptStyles","ContextRequestEvent","SKELETON_CONTEXT","unsubscribe","#unsubscribe","loading","#observer","isInline"],"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\", \"inline\"];\n\n #unsubscribe?: Unsubscribe | undefined;\n #observer = new MutationObserver(() => this.#update());\n\n get loading() {\n return this.hasAttribute(\"loading\");\n }\n\n set loading(value: boolean) {\n this.toggleAttribute(\"loading\", Boolean(value));\n }\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 inline() {\n return this.hasAttribute(\"inline\");\n }\n\n set inline(value: boolean) {\n this.toggleAttribute(\"inline\", 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 === \"inline\") 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 isInline = this.inline;\n for (const node of this.childNodes) {\n if (isInline) break;\n if (node instanceof Text && node.data.trim() !== \"\") {\n isInline = true;\n }\n }\n this.toggleAttribute(\"inline\", isInline);\n }\n\n #render(loading: boolean) {\n this.toggleAttribute(\"loading\", loading);\n this.toggleAttribute(\"inert\", loading && !this.interactive);\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,QAAQ,EAEpD,GACA,GAAY,IAAI,qBAAuB,KAAKC,GAAQ,CAAC,EAErD,IAAI,SAAU,CACZ,OAAO,KAAK,aAAa,SAAS,CACpC,CAEA,IAAI,QAAQ,EAAgB,CAC1B,KAAK,gBAAgB,UAAW,EAAQC,CAAM,CAChD,CAGA,IAAI,aAAc,CAChB,OAAO,KAAK,aAAa,aAAa,CACxC,CAEA,IAAI,YAAY,EAAgB,CAC9B,KAAK,gBAAgB,cAAe,EAAQA,CAAM,CACpD,CAGA,IAAI,QAAS,CACX,OAAO,KAAK,aAAa,QAAQ,CACnC,CAEA,IAAI,OAAO,EAAgB,CACzB,KAAK,gBAAgB,SAAU,EAAQA,CAAM,CAC/C,CAEA,yBACE,EACA,EACA,EACA,CACIC,IAAaC,IACbC,IAAS,SAAU,KAAKJ,GAAQ,EAC/B,KAAKK,GAAQ,KAAK,OAAO,EAChC,CAEA,mBAAoB,CAClB,IAAMC,EAAO,KAAK,YAAY,EAE1BT,EAAuBS,CAAI,GAC7BC,EAAYD,CAAI,EAGlB,KAAKN,GAAQ,EACb,KAAKK,GAAQ,EAAK,EAElB,KAAK,cACH,IAAIG,EACFC,GACC,EAAS,IAAgB,CACpBC,IAAa,KAAKC,GAAeD,GACrC,KAAKL,GAAQO,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,EAAW,KAAK,OACpB,IAAK,IAAMhB,KAAQ,KAAK,WAAY,CAClC,GAAIgB,EAAU,MACVhB,aAAgB,MAAQA,EAAK,KAAK,KAAK,IAAM,KAC/C,EAAW,GAEf,CACA,KAAK,gBAAgB,SAAUgB,CAAQ,CACzC,CAEA,GAAQ,EAAkB,CACxB,KAAK,gBAAgB,UAAWF,CAAO,EACvC,KAAK,gBAAgB,QAASA,GAAW,CAAC,KAAK,WAAW,CAC5D,CACF,E"}
@@ -1,2 +0,0 @@
1
- import{SKELETON_CONTEXT as e}from"./constants.js";var t=class extends HTMLElement{static observedAttributes=[`loading`];#e=/* @__PURE__ */ new Set;#t=this.attachInternals();constructor(){super(),this.addEventListener(`context-request`,t=>{if(t.context!==e||typeof t.callback!=`function`)return;t.stopPropagation();let{callback:n}=t;t.subscribe?(this.#e.add(n),n(this.loading,()=>this.#e.delete(n))):n(this.loading)})}get loading(){return this.hasAttribute(`loading`)}set loading(e){this.toggleAttribute(`loading`,!!e)}attributeChangedCallback(e,t,n){if(t!==n){this.#t.ariaBusy=this.loading?`true`:null;for(let e of this.#e)e(this.loading)}}};export{t as SkeletonProvider};
2
- //# sourceMappingURL=SkeletonProvider.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SkeletonProvider.js","names":["SkeletonProvider","event","SKELETON_CONTEXT","#subscribers","callback","value","oldValue","newValue","#internals"],"sources":["../src/SkeletonProvider.ts"],"sourcesContent":["import type { ContextRequestEvent } from \"#root/ContextRequestEvent.js\";\nimport type { LoadingCallback } from \"#root/types.js\";\n\nimport { SKELETON_CONTEXT } from \"#root/constants.js\";\n\nexport class SkeletonProvider extends HTMLElement {\n static observedAttributes = [\"loading\"];\n\n #subscribers = new Set<LoadingCallback>();\n #internals = this.attachInternals();\n\n constructor() {\n super();\n /**\n * Adding listener from the constructor so the provider is ready before any\n * descendant connects and asks for context.\n */\n this.addEventListener(\"context-request\", (event: ContextRequestEvent) => {\n if (\n event.context !== SKELETON_CONTEXT ||\n typeof event.callback !== \"function\"\n )\n return;\n\n /** The nearest provider wins */\n event.stopPropagation();\n\n const { callback } = event;\n\n if (event.subscribe) {\n this.#subscribers.add(callback);\n callback(this.loading, () => this.#subscribers.delete(callback));\n } else {\n callback(this.loading);\n }\n });\n }\n\n get loading() {\n return this.hasAttribute(\"loading\");\n }\n\n set loading(value: boolean) {\n this.toggleAttribute(\"loading\", Boolean(value));\n }\n\n attributeChangedCallback(\n _name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue) return;\n /**\n * Via internals, so an aria-busy attribute set by the user still takes\n * precedence.\n */\n this.#internals.ariaBusy = this.loading ? \"true\" : null;\n for (const callback of this.#subscribers) callback(this.loading);\n }\n}\n"],"mappings":"A,kDAKA,IAAaA,EAAb,cAAsC,WAAY,CAChD,OAAO,mBAAqB,CAAC,SAAS,EAEtC,mBAAe,IAAI,IACnB,GAAa,KAAK,gBAAgB,EAElC,aAAc,CACZ,MAAM,EAKN,KAAK,iBAAiB,kBAAoB,GAA+B,CACvE,GACEC,EAAM,UAAYC,GAClB,OAAOD,EAAM,UAAa,WAE1B,OAGFA,EAAM,gBAAgB,EAEtB,GAAM,CAAE,YAAaA,EAEjBA,EAAM,WACR,KAAKE,GAAa,IAAIC,CAAQ,EAC9BA,EAAS,KAAK,YAAe,KAAKD,GAAa,OAAOC,CAAQ,CAAC,GAE/DA,EAAS,KAAK,OAAO,CAEzB,CAAC,CACH,CAEA,IAAI,SAAU,CACZ,OAAO,KAAK,aAAa,SAAS,CACpC,CAEA,IAAI,QAAQ,EAAgB,CAC1B,KAAK,gBAAgB,UAAW,EAAQC,CAAM,CAChD,CAEA,yBACE,EACA,EACA,EACA,CACA,GAAIC,IAAaC,EAKjB,MAAKC,GAAW,SAAW,KAAK,QAAU,OAAS,KACnD,IAAK,IAAMJ,KAAY,KAAKD,GAAcC,EAAS,KAAK,OAAO,CADZ,CAErD,CACF,E"}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":["init","PROVIDER_NAME","SkeletonProvider","COMPONENT_NAME","Skeleton"],"sources":["../src/index.ts"],"sourcesContent":["import { PROVIDER_NAME, COMPONENT_NAME } from \"#root/constants.js\";\nimport { Skeleton } from \"#root/Skeleton.js\";\nimport { SkeletonProvider } from \"#root/SkeletonProvider.js\";\n\nexport function init() {\n if (!customElements.get(PROVIDER_NAME)) {\n customElements.define(PROVIDER_NAME, SkeletonProvider);\n }\n if (!customElements.get(COMPONENT_NAME)) {\n customElements.define(COMPONENT_NAME, Skeleton);\n }\n}\n\nexport { Skeleton, SkeletonProvider };\nexport { PROVIDER_NAME, COMPONENT_NAME };\n"],"mappings":"A,qKAIA,SAAgBA,GAAO,CAChB,eAAe,IAAA,wBAAiB,GACnC,eAAe,OAAOC,EAAeC,CAAgB,EAElD,eAAe,IAAA,eAAkB,GACpC,eAAe,OAAOC,EAAgBC,CAAQ,CAElD,C"}