@rcarls/rc-disclosure 0.1.0 → 0.3.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.
@@ -46,12 +46,18 @@
46
46
  "declarations": [
47
47
  {
48
48
  "kind": "class",
49
- "description": "Light behavioral wrapper for a direct child native `<details>` element.\n\nThe browser keeps ownership of open/close behavior, keyboard support, and\naccessibility. This element only mirrors state, emits a consistent custom\nevent, and optionally opens matching fragment targets.",
49
+ "description": "Disclosure wrapper for a native <details>/<summary> pair with controlled open state,\nfollowing the WAI-ARIA Disclosure pattern.\n\nThe browser keeps ownership of open/close behavior, keyboard support, and\naccessibility. This element mirrors state, emits a consistent custom event,\nand automatically opens when the URL hash matches any id within its subtree.",
50
50
  "name": "RCDisclosure",
51
51
  "members": [
52
52
  {
53
53
  "kind": "field",
54
- "name": "_details",
54
+ "name": "_observer",
55
+ "privacy": "private",
56
+ "default": "new MutationObserver(() => this._setupDetails())"
57
+ },
58
+ {
59
+ "kind": "field",
60
+ "name": "_$details",
55
61
  "type": {
56
62
  "text": "HTMLDetailsElement | null"
57
63
  },
@@ -60,9 +66,12 @@
60
66
  },
61
67
  {
62
68
  "kind": "field",
63
- "name": "_observer",
69
+ "name": "_scrollFrame",
70
+ "type": {
71
+ "text": "number"
72
+ },
64
73
  "privacy": "private",
65
- "default": "new MutationObserver(() => this._setupDetails())"
74
+ "default": "0"
66
75
  },
67
76
  {
68
77
  "kind": "field",
@@ -78,7 +87,7 @@
78
87
  "type": {
79
88
  "text": "boolean"
80
89
  },
81
- "description": "Enable location-hash opening when the details/summary target matches."
90
+ "deprecated": "No longer required. `rc-disclosure` now automatically opens and\nscrolls when the URL hash matches any id within its subtree. Will be removed\nin v1.0 release."
82
91
  },
83
92
  {
84
93
  "kind": "method",
@@ -1 +1 @@
1
- {"version":3,"file":"rc-disclosure-define.js","sources":["../src/define.ts"],"sourcesContent":["import { RCDisclosure } from './index.js';\n\ncustomElements.get('rc-disclosure') || customElements.define('rc-disclosure', RCDisclosure);\n\nexport * from './index.js';\n"],"names":["RCDisclosure"],"mappings":";AAEA,eAAe,IAAI,eAAe,KAAK,eAAe,OAAO,iBAAiBA,CAAY;"}
1
+ {"version":3,"file":"rc-disclosure-define.js","sources":["../src/define.ts"],"sourcesContent":["import { RCDisclosure } from './index.js';\n\nif (!customElements.get('rc-disclosure')) {\n customElements.define('rc-disclosure', RCDisclosure);\n}\n\nexport * from './index.js';\n"],"names":["RCDisclosure"],"mappings":";AAEK,eAAe,IAAI,eAAe,KACrC,eAAe,OAAO,iBAAiBA,CAAY;"}
@@ -1,12 +1,13 @@
1
- class n extends HTMLElement {
1
+ const i = "rc-disclosure-toggle", r = ":scope > details", n = ":scope > summary";
2
+ class a extends HTMLElement {
2
3
  constructor() {
3
- super(...arguments), this._details = null, this._observer = new MutationObserver(() => this._setupDetails()), this._onToggle = () => {
4
- const t = this._details?.open ?? !1;
5
- this._reflectOpen(t), this.dispatchEvent(
6
- new CustomEvent("rc-disclosure-toggle", {
4
+ super(...arguments), this._observer = new MutationObserver(() => this._setupDetails()), this._$details = null, this._scrollFrame = 0, this._onToggle = () => {
5
+ const e = this._$details?.open ?? !1;
6
+ this._reflectOpen(e), this.dispatchEvent(
7
+ new CustomEvent(i, {
7
8
  bubbles: !0,
8
9
  composed: !0,
9
- detail: { open: t }
10
+ detail: { open: e }
10
11
  })
11
12
  );
12
13
  }, this._onHashChange = () => {
@@ -18,66 +19,77 @@ class n extends HTMLElement {
18
19
  }
19
20
  /** Current open state mirrored to the child `<details>`. */
20
21
  get open() {
21
- return this._details?.open ?? this.hasAttribute("open");
22
+ return this._$details?.open ?? this.hasAttribute("open");
22
23
  }
23
- set open(t) {
24
- this._setOpen(t, !0);
24
+ set open(e) {
25
+ this._setOpen(e, !0);
25
26
  }
26
- /** Enable location-hash opening when the details/summary target matches. */
27
+ /**
28
+ * @deprecated No longer required. `rc-disclosure` now automatically opens and
29
+ * scrolls when the URL hash matches any id within its subtree. Will be removed
30
+ * in v1.0 release.
31
+ */
27
32
  get fragment() {
28
33
  return this.hasAttribute("fragment");
29
34
  }
30
- set fragment(t) {
31
- this.toggleAttribute("fragment", t);
35
+ set fragment(e) {
36
+ this.toggleAttribute("fragment", e);
32
37
  }
33
38
  connectedCallback() {
34
- this._observer.observe(this, { childList: !0 }), window.addEventListener("hashchange", this._onHashChange), this._setupDetails(), this._openForCurrentHash();
39
+ this._observer.observe(this, { childList: !0 }), window.addEventListener("hashchange", this._onHashChange), this._setupDetails();
35
40
  }
36
41
  disconnectedCallback() {
37
- this._observer.disconnect(), window.removeEventListener("hashchange", this._onHashChange), this._teardownDetails();
42
+ this._observer.disconnect(), window.removeEventListener("hashchange", this._onHashChange), cancelAnimationFrame(this._scrollFrame), this._teardownDetails();
38
43
  }
39
- attributeChangedCallback(t, e, s) {
40
- e !== s && (t === "open" && this._syncDetailsOpen(), t === "fragment" && this._openForCurrentHash());
44
+ attributeChangedCallback(e, t, s) {
45
+ if (t !== s) {
46
+ if (e === "open") {
47
+ this._syncDetailsOpen();
48
+ return;
49
+ }
50
+ e === "fragment" && this._openForCurrentHash();
51
+ }
41
52
  }
42
53
  _setupDetails() {
43
- const t = this.querySelector(":scope > details");
44
- if (t === this._details || (this._teardownDetails(), this._details = t, !t))
54
+ const e = this.querySelector(r);
55
+ if (e === this._$details || (this._teardownDetails(), this._$details = e, !e))
45
56
  return;
46
- t.id || (t.id = crypto.randomUUID());
47
- const e = t.querySelector(":scope > summary");
48
- e && e.setAttribute("aria-controls", t.id), t.addEventListener("toggle", this._onToggle), this._syncDetailsOpen(), this._reflectOpen(t.open), this._openForCurrentHash();
57
+ e.id || (e.id = crypto.randomUUID()), e.querySelector(n)?.setAttribute("aria-controls", e.id), e.addEventListener("toggle", this._onToggle), this._syncDetailsOpen(), this._reflectOpen(e.open), this._openForCurrentHash();
49
58
  }
50
59
  _teardownDetails() {
51
- this._details?.querySelector(":scope > summary")?.removeAttribute("aria-controls"), this._details?.removeEventListener("toggle", this._onToggle), this._details = null;
60
+ this._$details?.querySelector(n)?.removeAttribute("aria-controls"), this._$details?.removeEventListener("toggle", this._onToggle), this._$details = null;
52
61
  }
53
62
  _syncDetailsOpen() {
54
- const t = this._details;
55
- if (!t) return;
56
- const e = this.hasAttribute("open");
57
- t.open !== e && (t.open = e);
63
+ const e = this._$details;
64
+ if (!e)
65
+ return;
66
+ const t = this.hasAttribute("open");
67
+ e.open !== t && (e.open = t);
58
68
  }
59
- _setOpen(t, e) {
60
- const s = this._details;
61
- s && s.open !== t && (s.open = t), e && this._reflectOpen(t);
69
+ _setOpen(e, t) {
70
+ const s = this._$details;
71
+ s && s.open !== e && (s.open = e), t && this._reflectOpen(e);
62
72
  }
63
- _reflectOpen(t) {
64
- if (t) {
73
+ _reflectOpen(e) {
74
+ if (e) {
65
75
  this.hasAttribute("open") || this.setAttribute("open", "");
66
76
  return;
67
77
  }
68
78
  this.hasAttribute("open") && this.removeAttribute("open");
69
79
  }
70
80
  _openForCurrentHash() {
71
- if (!this.fragment || !location.hash) return;
72
- const t = decodeURIComponent(location.hash.slice(1));
73
- if (!t) return;
74
- const e = this._details;
75
- if (!e) return;
76
- const s = e.querySelector(":scope > summary");
77
- e.id !== t && s?.id !== t || (this.open = !0);
81
+ if (!location.hash)
82
+ return;
83
+ const e = decodeURIComponent(location.hash.slice(1));
84
+ if (!e || !this.querySelector(`#${CSS.escape(e)}`))
85
+ return;
86
+ const s = this.open;
87
+ this.open = !0, !s && (cancelAnimationFrame(this._scrollFrame), this._scrollFrame = requestAnimationFrame(() => {
88
+ document.getElementById(e)?.scrollIntoView();
89
+ }));
78
90
  }
79
91
  }
80
92
  export {
81
- n as RCDisclosure
93
+ a as RCDisclosure
82
94
  };
83
95
  //# sourceMappingURL=rc-disclosure.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rc-disclosure.js","sources":["../src/rc-disclosure.ts"],"sourcesContent":["export interface RCDisclosureToggleEvent {\n /** Whether the disclosure is now open. */\n open: boolean;\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-disclosure': RCDisclosure;\n }\n}\n\n/**\n * Light behavioral wrapper for a direct child native `<details>` element.\n *\n * The browser keeps ownership of open/close behavior, keyboard support, and\n * accessibility. This element only mirrors state, emits a consistent custom\n * event, and optionally opens matching fragment targets.\n *\n * @fires rc-disclosure-toggle - Fires when the child `<details>` toggles.\n */\nexport class RCDisclosure extends HTMLElement {\n private _details: HTMLDetailsElement | null = null;\n private _observer = new MutationObserver(() => this._setupDetails());\n\n static get observedAttributes(): string[] {\n return ['open', 'fragment'];\n }\n\n /** Current open state mirrored to the child `<details>`. */\n get open(): boolean {\n return this._details?.open ?? this.hasAttribute('open');\n }\n\n set open(value: boolean) {\n this._setOpen(value, true);\n }\n\n /** Enable location-hash opening when the details/summary target matches. */\n get fragment(): boolean {\n return this.hasAttribute('fragment');\n }\n\n set fragment(value: boolean) {\n this.toggleAttribute('fragment', value);\n }\n\n connectedCallback(): void {\n this._observer.observe(this, { childList: true });\n window.addEventListener('hashchange', this._onHashChange);\n this._setupDetails();\n this._openForCurrentHash();\n }\n\n disconnectedCallback(): void {\n this._observer.disconnect();\n window.removeEventListener('hashchange', this._onHashChange);\n this._teardownDetails();\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\n if (name === 'open') this._syncDetailsOpen();\n if (name === 'fragment') this._openForCurrentHash();\n }\n\n private _setupDetails(): void {\n const details = this.querySelector<HTMLDetailsElement>(':scope > details');\n if (details === this._details) return;\n\n this._teardownDetails();\n this._details = details;\n\n if (!details) {\n if (import.meta.env.DEV) {\n console.warn(\n '[rc-disclosure] No direct child <details> found. Place native ' +\n '<details><summary>...</summary>...</details> inside <rc-disclosure>.',\n this,\n );\n }\n return;\n }\n\n if (!details.id) details.id = crypto.randomUUID();\n\n const summary = details.querySelector<HTMLElement>(':scope > summary');\n if (summary) summary.setAttribute('aria-controls', details.id);\n\n details.addEventListener('toggle', this._onToggle);\n this._syncDetailsOpen();\n this._reflectOpen(details.open);\n this._openForCurrentHash();\n }\n\n private _teardownDetails(): void {\n const summary = this._details?.querySelector<HTMLElement>(':scope > summary');\n summary?.removeAttribute('aria-controls');\n\n this._details?.removeEventListener('toggle', this._onToggle);\n this._details = null;\n }\n\n private _syncDetailsOpen(): void {\n const details = this._details;\n if (!details) return;\n\n const nextOpen = this.hasAttribute('open');\n if (details.open !== nextOpen) details.open = nextOpen;\n }\n\n private _setOpen(value: boolean, reflect: boolean): void {\n const details = this._details;\n if (details && details.open !== value) details.open = value;\n // _reflectOpen is called here for programmatic opens because the <details>\n // toggle event is async (queued task per HTML spec). _onToggle also calls\n // _reflectOpen for user-initiated clicks, but that arrives after a tick.\n if (reflect) this._reflectOpen(value);\n }\n\n private _reflectOpen(value: boolean): void {\n if (value) {\n if (!this.hasAttribute('open')) this.setAttribute('open', '');\n return;\n }\n\n if (this.hasAttribute('open')) this.removeAttribute('open');\n }\n\n private _onToggle = (): void => {\n const open = this._details?.open ?? false;\n\n this._reflectOpen(open);\n this.dispatchEvent(\n new CustomEvent<RCDisclosureToggleEvent>('rc-disclosure-toggle', {\n bubbles: true,\n composed: true,\n detail: { open },\n }),\n );\n };\n\n private _onHashChange = (): void => {\n this._openForCurrentHash();\n };\n\n private _openForCurrentHash(): void {\n if (!this.fragment || !location.hash) return;\n\n const targetId = decodeURIComponent(location.hash.slice(1));\n if (!targetId) return;\n\n const details = this._details;\n if (!details) return;\n\n const summary = details.querySelector<HTMLElement>(':scope > summary');\n if (details.id !== targetId && summary?.id !== targetId) return;\n\n this.open = true;\n }\n}\n\nexport default RCDisclosure;\n"],"names":["RCDisclosure","open","value","name","oldValue","newValue","details","summary","nextOpen","reflect","targetId"],"mappings":"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"}
1
+ {"version":3,"file":"rc-disclosure.js","sources":["../src/rc-disclosure.ts"],"sourcesContent":["export interface RCDisclosureToggleEvent {\n /** Whether the disclosure is now open. */\n open: boolean;\n}\n\nconst TOGGLE_EVENT = 'rc-disclosure-toggle';\nconst DETAILS_SELECTOR = ':scope > details';\nconst SUMMARY_SELECTOR = ':scope > summary';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-disclosure': RCDisclosure;\n }\n}\n\n/**\n * Disclosure wrapper for a native <details>/<summary> pair with controlled open state,\n * following the WAI-ARIA Disclosure pattern.\n *\n * The browser keeps ownership of open/close behavior, keyboard support, and\n * accessibility. This element mirrors state, emits a consistent custom event,\n * and automatically opens when the URL hash matches any id within its subtree.\n *\n * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-disclosure rc-disclosure docs}\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/ WAI-ARIA Disclosure pattern}\n *\n * @fires rc-disclosure-toggle - Fires when the child `<details>` toggles.\n */\nexport class RCDisclosure extends HTMLElement {\n private _observer = new MutationObserver(() => this._setupDetails());\n\n private _$details: HTMLDetailsElement | null = null;\n\n private _scrollFrame = 0;\n\n static get observedAttributes(): string[] {\n return ['open', 'fragment'];\n }\n\n /** Current open state mirrored to the child `<details>`. */\n get open(): boolean {\n return this._$details?.open ?? this.hasAttribute('open');\n }\n\n set open(value: boolean) {\n this._setOpen(value, true);\n }\n\n /**\n * @deprecated No longer required. `rc-disclosure` now automatically opens and\n * scrolls when the URL hash matches any id within its subtree. Will be removed\n * in v1.0 release.\n */\n get fragment(): boolean {\n return this.hasAttribute('fragment');\n }\n\n set fragment(value: boolean) {\n this.toggleAttribute('fragment', value);\n }\n\n connectedCallback(): void {\n this._observer.observe(this, { childList: true });\n window.addEventListener('hashchange', this._onHashChange);\n this._setupDetails();\n }\n\n disconnectedCallback(): void {\n this._observer.disconnect();\n window.removeEventListener('hashchange', this._onHashChange);\n cancelAnimationFrame(this._scrollFrame);\n this._teardownDetails();\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) {\n return;\n }\n\n if (name === 'open') {\n this._syncDetailsOpen();\n\n return;\n }\n\n if (name === 'fragment') {\n this._openForCurrentHash();\n }\n }\n\n private _setupDetails(): void {\n const $details = this.querySelector<HTMLDetailsElement>(DETAILS_SELECTOR);\n if ($details === this._$details) {\n return;\n }\n\n this._teardownDetails();\n this._$details = $details;\n\n if (!$details) {\n if (import.meta.env.DEV) {\n console.warn(\n '[rc-disclosure] No direct child <details> found. Place native ' +\n '<details><summary>...</summary>...</details> inside <rc-disclosure>.',\n this,\n );\n }\n\n return;\n }\n\n if (!$details.id) {\n $details.id = crypto.randomUUID();\n }\n\n const $summary = $details.querySelector<HTMLElement>(SUMMARY_SELECTOR);\n $summary?.setAttribute('aria-controls', $details.id);\n\n $details.addEventListener('toggle', this._onToggle);\n this._syncDetailsOpen();\n this._reflectOpen($details.open);\n this._openForCurrentHash();\n }\n\n private _teardownDetails(): void {\n const $summary = this._$details?.querySelector<HTMLElement>(SUMMARY_SELECTOR);\n $summary?.removeAttribute('aria-controls');\n\n this._$details?.removeEventListener('toggle', this._onToggle);\n this._$details = null;\n }\n\n private _syncDetailsOpen(): void {\n const $details = this._$details;\n if (!$details) {\n return;\n }\n\n const nextOpen = this.hasAttribute('open');\n if ($details.open !== nextOpen) {\n $details.open = nextOpen;\n }\n }\n\n private _setOpen(value: boolean, reflect: boolean): void {\n const $details = this._$details;\n if ($details && $details.open !== value) {\n $details.open = value;\n }\n\n // _reflectOpen is called here for programmatic opens because the <details>\n // toggle event is async (queued task per HTML spec). _onToggle also calls\n // _reflectOpen for user-initiated clicks, but that arrives after a tick.\n if (reflect) {\n this._reflectOpen(value);\n }\n }\n\n private _reflectOpen(value: boolean): void {\n if (value) {\n if (!this.hasAttribute('open')) {\n this.setAttribute('open', '');\n }\n\n return;\n }\n\n if (this.hasAttribute('open')) {\n this.removeAttribute('open');\n }\n }\n\n private _onToggle = (): void => {\n const open = this._$details?.open ?? false;\n\n this._reflectOpen(open);\n this.dispatchEvent(\n new CustomEvent<RCDisclosureToggleEvent>(TOGGLE_EVENT, {\n bubbles: true,\n composed: true,\n detail: { open },\n }),\n );\n };\n\n private _onHashChange = (): void => {\n this._openForCurrentHash();\n };\n\n private _openForCurrentHash(): void {\n if (!location.hash) {\n return;\n }\n\n const targetId = decodeURIComponent(location.hash.slice(1));\n if (!targetId) {\n return;\n }\n\n const $target = this.querySelector(`#${CSS.escape(targetId)}`);\n if (!$target) {\n return;\n }\n\n const wasOpen = this.open;\n\n this.open = true;\n\n if (wasOpen) {\n return;\n }\n\n cancelAnimationFrame(this._scrollFrame);\n this._scrollFrame = requestAnimationFrame(() => {\n document.getElementById(targetId)?.scrollIntoView();\n });\n }\n}\n\nexport default RCDisclosure;\n"],"names":["TOGGLE_EVENT","DETAILS_SELECTOR","SUMMARY_SELECTOR","RCDisclosure","open","value","name","oldValue","newValue","$details","nextOpen","reflect","targetId","wasOpen"],"mappings":"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"}
@@ -8,22 +8,31 @@ declare global {
8
8
  }
9
9
  }
10
10
  /**
11
- * Light behavioral wrapper for a direct child native `<details>` element.
11
+ * Disclosure wrapper for a native <details>/<summary> pair with controlled open state,
12
+ * following the WAI-ARIA Disclosure pattern.
12
13
  *
13
14
  * The browser keeps ownership of open/close behavior, keyboard support, and
14
- * accessibility. This element only mirrors state, emits a consistent custom
15
- * event, and optionally opens matching fragment targets.
15
+ * accessibility. This element mirrors state, emits a consistent custom event,
16
+ * and automatically opens when the URL hash matches any id within its subtree.
17
+ *
18
+ * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-disclosure rc-disclosure docs}
19
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/ WAI-ARIA Disclosure pattern}
16
20
  *
17
21
  * @fires rc-disclosure-toggle - Fires when the child `<details>` toggles.
18
22
  */
19
23
  export declare class RCDisclosure extends HTMLElement {
20
- private _details;
21
24
  private _observer;
25
+ private _$details;
26
+ private _scrollFrame;
22
27
  static get observedAttributes(): string[];
23
28
  /** Current open state mirrored to the child `<details>`. */
24
29
  get open(): boolean;
25
30
  set open(value: boolean);
26
- /** Enable location-hash opening when the details/summary target matches. */
31
+ /**
32
+ * @deprecated No longer required. `rc-disclosure` now automatically opens and
33
+ * scrolls when the URL hash matches any id within its subtree. Will be removed
34
+ * in v1.0 release.
35
+ */
27
36
  get fragment(): boolean;
28
37
  set fragment(value: boolean);
29
38
  connectedCallback(): void;
package/package.json CHANGED
@@ -3,14 +3,14 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.0",
7
- "description": "Headless WAI-ARIA disclosure widget built with Lit",
6
+ "version": "0.3.0",
7
+ "description": "Disclosure wrapper for a native <details>/<summary> pair with controlled open state.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
11
  "directory": "packages/rc-disclosure"
12
12
  },
13
- "homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
13
+ "homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-disclosure",
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "files": [
@@ -37,18 +37,14 @@
37
37
  ],
38
38
  "customElements": "dist/custom-elements.json",
39
39
  "scripts": {
40
- "dev": "vite",
41
40
  "build": "tsc && vite build && cem analyze",
42
41
  "cem:analyze": "cem analyze",
43
- "preview": "vite preview",
44
- "test:browser": "vitest",
45
- "test:browser:chrome": "vitest --project=chromium",
46
- "test:browser:firefox": "vitest --project=firefox",
47
- "yalc:publish": "yalc publish --push"
42
+ "test:browser": "vitest --run",
43
+ "test:browser:chrome": "vitest --run --project=chromium",
44
+ "test:browser:firefox": "vitest --run --project=firefox"
48
45
  },
49
46
  "devDependencies": {
50
47
  "@custom-elements-manifest/analyzer": "0.11.0",
51
- "@guanghechen/rollup-plugin-copy": "^6.0.9",
52
48
  "@vitest/browser-playwright": "4.1.5",
53
49
  "lit": "^3.0.0",
54
50
  "playwright": "^1.56.0",
package/dist/demo.css DELETED
@@ -1,85 +0,0 @@
1
- *, *::before, *::after {
2
- box-sizing: border-box;
3
- }
4
-
5
- :root {
6
- color-scheme: light dark;
7
- --rc-accent: Highlight; /* Chrome doesn't support AccentColor on the open web */
8
- }
9
-
10
- @supports (color: AccentColor) {
11
- :root {
12
- --rc-accent: AccentColor;
13
- }
14
- }
15
-
16
- [data-theme="light"] { color-scheme: light; }
17
- [data-theme="dark"] { color-scheme: dark; }
18
-
19
- [data-theme="solarized-light"] {
20
- color-scheme: light;
21
- --rc-surface: #fdf6e3;
22
- --rc-text: #657b83;
23
- --rc-border: 1px solid #93a1a1;
24
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .12);
25
- }
26
-
27
- [data-theme="solarized-dark"] {
28
- color-scheme: dark;
29
- --rc-surface: #002b36;
30
- --rc-text: #839496;
31
- --rc-border: 1px solid #586e75;
32
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .3);
33
- }
34
-
35
- body {
36
- font-family: system-ui, sans-serif;
37
- margin: 0;
38
- }
39
-
40
- .demo-page {
41
- max-width: 60rem;
42
- margin: 0 auto;
43
- padding: 2rem 1.5rem;
44
- }
45
-
46
- .demo-controls {
47
- display: flex;
48
- align-items: center;
49
- gap: 1rem;
50
- margin-bottom: 2rem;
51
- padding-bottom: 1rem;
52
- border-bottom: 1px solid ButtonBorder;
53
- }
54
-
55
- .demo-controls .demo-title {
56
- flex: 1;
57
- }
58
-
59
- .demo-controls h1 {
60
- margin: 0 0 0.15rem;
61
- font-size: 1.5rem;
62
- }
63
-
64
- .demo-controls a {
65
- font-size: 0.8rem;
66
- }
67
-
68
- .demo-section {
69
- margin-bottom: 2rem;
70
- }
71
-
72
- .demo-section h2 {
73
- margin: 0 0 0.5rem;
74
- }
75
-
76
- .theme-picker {
77
- padding: 0.35em 0.75em;
78
- font-family: inherit;
79
- font-size: 0.8rem;
80
- cursor: pointer;
81
- border: 1px solid ButtonBorder;
82
- border-radius: 4px;
83
- background: Canvas;
84
- color: CanvasText;
85
- }
package/dist/demo.js DELETED
@@ -1,40 +0,0 @@
1
- const THEMES = ['', 'light', 'dark', 'solarized-light', 'solarized-dark'];
2
- const LABELS = ['Auto', 'Light', 'Dark', 'Solarized ☀', 'Solarized ☾'];
3
-
4
- const stored = localStorage.getItem('rc-demo-theme') ?? '';
5
- applyTheme(stored);
6
-
7
- function applyTheme(theme) {
8
- if (theme) {
9
- document.documentElement.dataset.theme = theme;
10
- } else {
11
- delete document.documentElement.dataset.theme;
12
- }
13
- }
14
-
15
- function currentIndex() {
16
- const current = document.documentElement.dataset.theme ?? '';
17
- const idx = THEMES.indexOf(current);
18
- return idx === -1 ? 0 : idx;
19
- }
20
-
21
- function updateButtons() {
22
- const label = LABELS[currentIndex()];
23
- document.querySelectorAll('.theme-picker').forEach((btn) => {
24
- btn.textContent = label;
25
- });
26
- }
27
-
28
- window.cycleTheme = function () {
29
- const next = THEMES[(currentIndex() + 1) % THEMES.length];
30
- applyTheme(next);
31
- localStorage.setItem('rc-demo-theme', next);
32
- updateButtons();
33
- };
34
-
35
- document.addEventListener('DOMContentLoaded', () => {
36
- updateButtons();
37
- document.querySelectorAll('.theme-picker').forEach((btn) => {
38
- btn.addEventListener('click', window.cycleTheme);
39
- });
40
- });