@rcarls/rc-textarea 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.
@@ -1,39 +1,52 @@
1
1
  /**
2
2
  * A styled inline range over characters [from, to).
3
+ *
3
4
  * Rich formatting properties (bold, italic, color, etc.) are rendered as
4
5
  * inline styles on the blot span; `className` adds an extra CSS class.
5
6
  */
6
7
  export interface MarkDecoration {
8
+ /** Unique identifier assigned at creation time. */
7
9
  id: string;
10
+ /** Decoration type discriminant. */
8
11
  type: 'mark';
9
12
  /** Inclusive start offset (0-based character index into plain text). */
10
13
  from: number;
11
14
  /** Exclusive end offset. */
12
15
  to: number;
16
+ /** Space-separated CSS class name(s) added to the blot span. */
13
17
  className?: string;
18
+ /** Renders the range bold (`font-weight: bold`). */
14
19
  bold?: boolean;
20
+ /** Renders the range italic (`font-style: italic`). */
15
21
  italic?: boolean;
22
+ /** Text color (`color` CSS property). Accepts any CSS color value. */
16
23
  color?: string;
24
+ /** Background color (`background` CSS property). Accepts any CSS color value. */
17
25
  background?: string;
26
+ /** Underline style (`text-decoration-style`). */
18
27
  underline?: 'solid' | 'wavy' | 'dotted' | 'dashed';
28
+ /** Underline color (`text-decoration-color`). Defaults to the text `color` when omitted. */
19
29
  underlineColor?: string;
30
+ /** Additional HTML attributes set directly on the blot span element. */
20
31
  attributes?: Record<string, string>;
21
32
  }
22
33
  /**
23
34
  * A decoration applied to an entire logical line.
35
+ *
24
36
  * Adds a CSS class and/or data attributes to the line div, and optionally
25
- * appends an error-lens style annotation message at the end of the line.
37
+ * appends a diagnostic message at the end of the line.
26
38
  */
27
39
  export interface LineDecoration {
40
+ /** Unique identifier assigned at creation time. */
28
41
  id: string;
42
+ /** Decoration type discriminant. */
29
43
  type: 'line';
30
44
  /** 1-based logical line number. */
31
45
  line: number;
46
+ /** Space-separated CSS class name(s) added to the line div. */
32
47
  className?: string;
33
48
  /**
34
- * Error-lens style annotation text shown at the end of the line.
35
- * Rendered via `::after` pseudo-element on the line div (not a real DOM node),
36
- * so it is completely excluded from selection and clipboard operations.
49
+ * Error-lens style diagnostic text shown at the end of the line.
37
50
  */
38
51
  message?: string;
39
52
  /**
@@ -41,25 +54,30 @@ export interface LineDecoration {
41
54
  * on the line div. Use attribute selectors in CSS to style the message:
42
55
  *
43
56
  * ```css
44
- * .v2-line[data-message-class~="error"][data-message]::after { color: red; }
57
+ * .line[data-message-class~="error"][data-message]::after { color: red; }
45
58
  * ```
46
59
  */
47
60
  messageClassName?: string;
61
+ /** Additional HTML attributes set directly on the line element. */
48
62
  attributes?: Record<string, string>;
49
63
  /**
50
64
  * Override the built-in gutter cell text for this line.
65
+ *
51
66
  * - `string` — custom label (e.g. `"!"`, `"▶"`)
52
67
  * - `null` — force empty cell (suppress built-in content)
53
- * - `undefined` — use the built-in mode default (`line-numbers`, `list-numbers`, etc.)
68
+ * - `undefined` — use the built-in mode default (`line-numbers`, `gutter`, etc.)
54
69
  */
55
70
  gutterContent?: string | null;
56
71
  }
57
72
  /**
58
73
  * A non-editable DOM element inserted at a character offset.
74
+ *
59
75
  * Widgets are purely visual and do not appear in the plain text value.
60
76
  */
61
77
  export interface WidgetDecoration {
78
+ /** Unique identifier assigned at creation time. */
62
79
  id: string;
80
+ /** Decoration type discriminant. */
63
81
  type: 'widget';
64
82
  /** Character offset. Widget is placed before (or after) the character at this position. */
65
83
  offset: number;
@@ -68,10 +86,15 @@ export interface WidgetDecoration {
68
86
  /** Whether to place the widget before or after the character at `offset`. Default: 'before'. */
69
87
  side?: 'before' | 'after';
70
88
  }
89
+ /** Decorations are visual formatting applied to the field content without affecting
90
+ * the underlying plain text value.
91
+ */
71
92
  export type Decoration = MarkDecoration | LineDecoration | WidgetDecoration;
93
+ /** Input shape for creating a decoration — id-less variant of each decoration type. */
72
94
  export type DecorationInput = Omit<MarkDecoration, 'id'> | Omit<LineDecoration, 'id'> | Omit<WidgetDecoration, 'id'>;
73
95
  /**
74
96
  * A semantic token produced by an external tokenizer (lezer, tree-sitter, shiki, etc.).
97
+ *
75
98
  * Offsets are absolute character indices into the plain-text value — the same coordinate
76
99
  * space as `MarkDecoration.from` / `MarkDecoration.to`.
77
100
  *
@@ -83,9 +106,9 @@ export interface Token {
83
106
  from: number;
84
107
  /** Exclusive end offset. */
85
108
  to: number;
86
- /** Semantic token type, e.g. `"keyword"`, `"string"`, `"comment"`. */
109
+ /** Semantic token type. */
87
110
  type: string;
88
- /** Optional TextMate / VS Code scope list (e.g. `["keyword.control", "source.js"]`). */
111
+ /** Optional scope list. */
89
112
  scopes?: string[];
90
113
  }
91
114
  /**
@@ -100,28 +123,35 @@ export interface Token {
100
123
  * mount(a) { api = a; },
101
124
  * update() {},
102
125
  * });
126
+ *
103
127
  * // later:
104
128
  * api.setDecorations([...]);
105
129
  * api.scheduleUpdate();
106
130
  * ```
107
131
  */
108
132
  export interface RCTextareaPluginAPI {
109
- /** The host rc-textarea element. */
133
+ /** The host `rc-textarea` element. */
110
134
  readonly host: HTMLElement;
135
+ /** The current plain-text field value at the time of the callback. */
111
136
  readonly value: string;
112
- /** Normalized selection start offset (always ≤ selectionEnd). */
137
+ /** Normalized selection start offset (always ≤ `selectionEnd`). */
113
138
  readonly selectionStart: number;
114
- /** Normalized selection end offset (always ≥ selectionStart). Equals selectionStart when the cursor is collapsed. */
139
+ /** Normalized selection end offset (always ≥ `selectionStart`). Equals `selectionStart` when the cursor is collapsed. */
115
140
  readonly selectionEnd: number;
116
141
  /**
117
- * Returns the bounding rect of the cursor (caret) in viewport coordinates,
118
- * or `null` if the editor is not focused or the cursor is outside the editor.
142
+ * Returns the bounding rect of the cursor (caret) in viewport coordinates.
143
+ *
119
144
  * Useful for anchoring autocomplete popups or hover tooltips to the cursor.
145
+ *
146
+ * @returns the cursor bounding rect, or `null` if the editor is not focused
147
+ * or the cursor is outside the editor
120
148
  */
121
149
  getCursorRect(): DOMRect | null;
122
150
  /**
123
- * Returns the word at the current cursor position, or `null` if the cursor is
124
- * not on a word character (`\w` — alphanumeric + underscore).
151
+ * Returns the word boundaries at the current cursor position.
152
+ *
153
+ * @returns the word and its `[from, to)` offsets, or `null` if the cursor is
154
+ * not on a word character (`\w` — alphanumeric + underscore)
125
155
  */
126
156
  getWordAtCursor(): {
127
157
  word: string;
@@ -131,7 +161,8 @@ export interface RCTextareaPluginAPI {
131
161
  /**
132
162
  * Subscribe to cursor/selection changes. The callback fires on every cursor
133
163
  * move (arrow keys, mouse clicks, selection changes) while the editor is focused.
134
- * Returns an unsubscribe function — call it in your plugin's `destroy()`.
164
+ *
165
+ * @returns an unsubscribe function. Call this in `destroy()`
135
166
  *
136
167
  * @example
137
168
  * ```ts
@@ -148,24 +179,35 @@ export interface RCTextareaPluginAPI {
148
179
  * ```
149
180
  */
150
181
  onCursorMove(callback: (selectionStart: number, selectionEnd: number) => void): () => void;
182
+ /**
183
+ * Add a single decoration to the plugin's decoration set.
184
+ *
185
+ * Use the returned id with `removeDecoration` to remove it individually.
186
+ *
187
+ * @returns the assigned `id`, used to remove or look up the decoration later
188
+ */
151
189
  addDecoration(d: DecorationInput): string;
190
+ /** Remove a previously added decoration by its id. */
152
191
  removeDecoration(id: string): void;
192
+ /** Remove all decorations set by this plugin. */
153
193
  clearDecorations(): void;
194
+ /**
195
+ * Replace the full decoration set with `decorations`.
196
+ */
154
197
  setDecorations(decorations: DecorationInput[]): void;
155
- /** Returns the decorations this plugin has currently set, in insertion order. */
198
+ /**
199
+ * @returns the decorations this plugin has currently set, in insertion order
200
+ */
156
201
  getDecorations(): readonly Decoration[];
157
202
  /** Trigger a render pass outside of normal text input events. */
158
203
  scheduleUpdate(): void;
159
204
  /**
160
205
  * Inject a stylesheet into the component's shadow root via `adoptedStyleSheets`.
161
206
  *
162
- * Pass a `CSSStyleSheet` object to share an already-constructed sheet (zero
163
- * extra parsing cost), or a raw CSS text string to compile and adopt a new one.
164
- * Returns the adopted `CSSStyleSheet` so it can be passed to `removeStyleSheet`
165
- * if explicit removal is needed before the plugin is unmounted.
207
+ * Adopted sheets are automatically removed when the plugin is unmounted.
166
208
  *
167
- * Adopted sheets are **automatically removed** when the plugin is unmounted
168
- * (via `removePlugin()` or `usePlugin()` with a replacement).
209
+ * @returns the adopted `CSSStyleSheet`, which can be passed to `removeStyleSheet`
210
+ * if explicit removal is needed before unmount
169
211
  *
170
212
  * @example CSS text string
171
213
  * ```ts
@@ -192,8 +234,6 @@ export interface RCTextareaPluginAPI {
192
234
  adoptStyleSheet(sheetOrCssText: CSSStyleSheet | string): CSSStyleSheet;
193
235
  /**
194
236
  * Remove a previously adopted stylesheet from the shadow root.
195
- * The sheet does not need to have been adopted via `adoptStyleSheet` —
196
- * any sheet already present in `adoptedStyleSheets` may be removed.
197
237
  */
198
238
  removeStyleSheet(sheet: CSSStyleSheet): void;
199
239
  /**
@@ -204,15 +244,20 @@ export interface RCTextareaPluginAPI {
204
244
  * them into `MarkDecoration` objects covering the corresponding character
205
245
  * ranges of the plain text value.
206
246
  *
247
+ * @example
207
248
  * ```ts
208
249
  * editor.usePlugin({
209
250
  * highlight(value, api) {
210
251
  * const html = hljs.highlight(value, { language: 'js' }).value;
211
- * api.setDecorations(api.decorationsFromHtml(html));
252
+ * api.setDecorations(api.parseDecorationsFromHtml(html));
212
253
  * },
213
254
  * });
214
255
  * ```
215
256
  */
257
+ parseDecorationsFromHtml(html: string): Omit<MarkDecoration, 'id'>[];
258
+ /**
259
+ * @deprecated Use `parseDecorationsFromHtml` instead.
260
+ */
216
261
  decorationsFromHtml(html: string): Omit<MarkDecoration, 'id'>[];
217
262
  /**
218
263
  * Convert a flat token array from an external tokenizer (lezer, tree-sitter, shiki, moo, etc.)
@@ -222,6 +267,7 @@ export interface RCTextareaPluginAPI {
222
267
  * (same coordinate space as `MarkDecoration.from` / `MarkDecoration.to`).
223
268
  * Types not present in `themeMap` are silently ignored.
224
269
  *
270
+ * @example
225
271
  * ```ts
226
272
  * editor.usePlugin({
227
273
  * update(value, api) {
@@ -238,13 +284,16 @@ export interface RCTextareaPluginAPI {
238
284
  decorationsFromTokens(tokens: Token[], themeMap: Record<string, Omit<MarkDecoration, 'id' | 'type' | 'from' | 'to'>>): Omit<MarkDecoration, 'id'>[];
239
285
  /**
240
286
  * Insert `text` at the current cursor position, replacing any active selection.
287
+ *
241
288
  * Equivalent to typing the text with the keyboard.
242
289
  */
243
290
  insertText(text: string): void;
244
291
  /**
245
292
  * Wrap the current selection with `prefix` and `suffix`.
293
+ *
246
294
  * No-op when the selection is collapsed (no text selected).
247
295
  *
296
+ * @example
248
297
  * ```ts
249
298
  * api.wrapSelection('**', '**'); // bold
250
299
  * api.wrapSelection('`', '`'); // inline code
@@ -253,6 +302,7 @@ export interface RCTextareaPluginAPI {
253
302
  wrapSelection(prefix: string, suffix: string): void;
254
303
  /**
255
304
  * Replace the current selection with `text`.
305
+ *
256
306
  * When the selection is collapsed this is equivalent to `insertText`.
257
307
  */
258
308
  replaceSelection(text: string): void;
@@ -266,12 +316,14 @@ export interface RCTextareaPluginAPI {
266
316
  * ```ts
267
317
  * import hljs from 'highlight.js/lib/core';
268
318
  * import javascript from 'highlight.js/lib/languages/javascript';
319
+ *
269
320
  * hljs.registerLanguage('javascript', javascript);
270
321
  *
271
322
  * editor.usePlugin({
272
323
  * highlight(value, api) {
273
324
  * const html = hljs.highlight(value, { language: 'javascript' }).value;
274
- * api.setDecorations(api.decorationsFromHtml(html));
325
+ *
326
+ * api.setDecorations(api.parseDecorationsFromHtml(html));
275
327
  * },
276
328
  * });
277
329
  * ```
@@ -281,6 +333,7 @@ export interface RCTextareaPluginAPI {
281
333
  * editor.usePlugin({
282
334
  * async update(value, api) {
283
335
  * const decs = await myParser.decorate(value);
336
+ *
284
337
  * api.setDecorations(decs);
285
338
  * },
286
339
  * });
@@ -293,50 +346,69 @@ export interface RCTextareaPlugin {
293
346
  destroy?(): void;
294
347
  /**
295
348
  * Display-layer value transform — called before `update`/`highlight` in
296
- * read-only mode. Return a non-null string to substitute it as the rendered
349
+ * read-only mode.
350
+ *
351
+ * Return a non-null string to substitute it as the rendered
297
352
  * text passed to all subsequent hooks and to the document builder.
298
- * The underlying `element.value` is **never** modified; only the display
353
+ * The underlying `element.value` is never modified; only the display
299
354
  * layer sees the transformed text.
355
+ *
300
356
  * Return `null` or `void` to leave the value unchanged.
301
357
  */
302
358
  transform?(value: string, api: RCTextareaPluginAPI): string | null | void;
303
359
  /**
304
- * Imperative decoration API — called on each value change.
305
- * Use `api.setDecorations()` to apply decorations.
360
+ * Imperative decoration API
306
361
  */
307
362
  update?(value: string, api: RCTextareaPluginAPI): void | Promise<void>;
308
363
  /**
309
- * HTML-based compat — called on each value change.
364
+ * HTML-based compat
365
+ *
310
366
  * Return an HTML string (e.g. from hljs/prism); it will be parsed via
311
- * `api.decorationsFromHtml()` and applied as mark decorations.
367
+ * `api.parseDecorationsFromHtml()` and applied as mark decorations.
368
+ *
312
369
  * Return `null` or `void` to skip.
313
370
  */
314
371
  highlight?(value: string, api: RCTextareaPluginAPI): string | null | void | Promise<string | null | void>;
315
372
  }
316
373
  /**
317
374
  * The subset of `MarkDecoration` properties used for styling.
375
+ *
318
376
  * Used in `TextPattern.captureGroups` to assign a style to each named capture group.
319
377
  */
320
378
  export type MarkDecorationStyle = Pick<MarkDecoration, 'className' | 'bold' | 'italic' | 'color' | 'background' | 'underline' | 'underlineColor' | 'attributes'>;
321
379
  export interface TextPattern {
380
+ /** Unique identifier used to register and unregister the pattern. */
322
381
  id: string;
382
+ /** Regular expression matched against the full editor value. Use the `g` flag for multiple matches. */
323
383
  pattern: RegExp;
384
+ /** Space-separated CSS class name(s) applied to matched ranges. */
324
385
  className?: string;
386
+ /** Renders matched ranges bold (`font-weight: bold`). */
325
387
  bold?: boolean;
388
+ /** Renders matched ranges italic (`font-style: italic`). */
326
389
  italic?: boolean;
390
+ /** Text color for matched ranges. Accepts any CSS color value. */
327
391
  color?: string;
392
+ /** Background color for matched ranges. Accepts any CSS color value. */
328
393
  background?: string;
394
+ /** Underline style for matched ranges (`text-decoration-style`). */
329
395
  underline?: 'solid' | 'wavy' | 'dotted' | 'dashed';
396
+ /** Underline color for matched ranges (`text-decoration-color`). Defaults to the text `color` when omitted. */
330
397
  underlineColor?: string;
398
+ /** Additional HTML attributes set directly on matched-range spans. */
331
399
  attributes?: Record<string, string>;
332
400
  /**
333
- * Factory called for each regex match. Return a partial LineDecoration
334
- * (message, messageClassName, className, attributes) to add an error-lens
335
- * annotation on the matched line, or `null` to skip.
401
+ * Factory called for each regex match.
402
+ *
403
+ * Return a partial LineDecoration
404
+ * (message, messageClassName, className, attributes) to add a diagnostic
405
+ * message on the matched line, or `null` to skip.
336
406
  */
337
407
  createLineDecoration?: (match: RegExpMatchArray) => Omit<LineDecoration, 'id' | 'type' | 'line'> | null;
338
408
  /**
339
- * Per-named-capture-group decoration styles. When provided, one
409
+ * Decoration styles for each named capture group in the pattern.
410
+ *
411
+ * When provided, one
340
412
  * `MarkDecoration` is emitted per captured group (unmatched optional groups
341
413
  * are skipped) instead of one decoration for the whole match.
342
414
  *
@@ -360,8 +432,10 @@ export interface TextPattern {
360
432
  /**
361
433
  * A simplified plugin variant for per-line decoration.
362
434
  *
363
- * Return mark/line decorations with offsets **relative to the start of the
364
- * line** — `createLineDecoratorPlugin()` converts them to absolute document
435
+ * Return mark/line decorations with offsets relative to the start of the
436
+ * line.
437
+ *
438
+ * `createLineDecoratorPlugin()` converts them to absolute document
365
439
  * offsets automatically, so no `lineStart` bookkeeping is needed.
366
440
  *
367
441
  * @example
@@ -371,10 +445,15 @@ export interface TextPattern {
371
445
  * decorateLine(line) {
372
446
  * const results: Omit<MarkDecoration | LineDecoration, 'id'>[] = [];
373
447
  * const m = /\bfunction\b/.exec(line);
374
- * if (m) results.push({ type: 'mark', from: m.index, to: m.index + m[0].length, className: 'kw' });
448
+ *
449
+ * if (m) {
450
+ * results.push({ type: 'mark', from: m.index, to: m.index + m[0].length, className: 'kw' });
451
+ * }
452
+ *
375
453
  * return results;
376
454
  * },
377
455
  * };
456
+ *
378
457
  * editor.usePlugin(createLineDecoratorPlugin(myDecorator));
379
458
  * ```
380
459
  */
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 enhanced textarea web component built with Lit",
6
+ "version": "0.3.0",
7
+ "description": "Textarea wrapper with line decorations, gutter rendering, inline widgets, and plugin hooks.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
11
  "directory": "packages/rc-textarea"
12
12
  },
13
- "homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
13
+ "homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-textarea",
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "files": [
@@ -37,20 +37,19 @@
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
42
  "preview": "vite preview",
44
- "test:browser": "vitest",
45
- "test:browser:chrome": "vitest --project=chromium",
46
- "test:browser:firefox": "vitest --project=firefox"
43
+ "test:browser": "vitest --run",
44
+ "test:browser:chrome": "vitest --run --project=chromium",
45
+ "test:browser:firefox": "vitest --run --project=firefox"
47
46
  },
48
47
  "dependencies": {
48
+ "@rcarls/rc-common": "workspace:*",
49
49
  "parchment": "^3.0.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@custom-elements-manifest/analyzer": "0.11.0",
53
- "@guanghechen/rollup-plugin-copy": "^6.0.9",
54
53
  "@types/node": "^25.6.0",
55
54
  "@vitest/browser-playwright": "4.1.5",
56
55
  "lit": "^3.0.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
- });