@bunnyland/ui-web 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -99,7 +99,7 @@ source directory or a checked-in tarball.
99
99
  The package owns the shared `--bl-*` CSS variables and theme selector helpers. Built-in
100
100
  palette names are:
101
101
 
102
- - `purple-blue`
102
+ - `midnight`
103
103
  - `candy`
104
104
  - `earth`
105
105
  - `ocean`
@@ -110,8 +110,9 @@ Each palette follows `prefers-color-scheme` by default. The shared client menu c
110
110
  Dark or Light appearance, or return to Auto (System). Use `bindThemeSelect(select)` for
111
111
  palette selectors, `bindColorSchemeSelect(select)` for appearance selectors, and
112
112
  `setTheme(theme)` or `setColorScheme(scheme)` for direct changes. Old paired values such as
113
- `anime-light` continue to load as migration aliases (`candy` with Light forced). New colors
114
- should be added as CSS variables before clients depend on them.
113
+ `anime-light` continue to load as migration aliases (`candy` with Light forced), as do the
114
+ former `purple-blue` values (`midnight`). New colors should be added as CSS variables before
115
+ clients depend on them.
115
116
 
116
117
  Deployments can add their own theme choices without changing this package:
117
118
 
@@ -196,15 +196,19 @@
196
196
  }
197
197
 
198
198
  async function promptPlayerAuth(base) {
199
- const username = window.prompt('Bunnyland username');
200
- if (!username) return null;
201
- const password = window.prompt('Bunnyland password');
202
- if (password == null) return null;
199
+ const credentials = await window.BunnylandUI.credentialsDialog({
200
+ message: 'Enter your player credentials to continue.',
201
+ title: 'Sign in to Bunnyland',
202
+ });
203
+ if (!credentials) return null;
203
204
  try {
204
- await login(base, username, password);
205
+ await login(base, credentials.username, credentials.password);
205
206
  return true;
206
207
  } catch (error) {
207
- window.alert(error.message || 'Login failed');
208
+ await window.BunnylandUI.alertDialog(error.message || 'Login failed', {
209
+ title: 'Sign in failed',
210
+ tone: 'danger',
211
+ });
208
212
  return false;
209
213
  }
210
214
  }
@@ -1,15 +1,19 @@
1
1
  :root,
2
2
  :root.bl-theme-dark,
3
3
  :root.bl-theme-purple-blue-dark,
4
- :root.bl-theme-purple-blue {
4
+ :root.bl-theme-midnight-dark,
5
+ :root.bl-theme-purple-blue,
6
+ :root.bl-theme-midnight {
5
7
  color-scheme: dark;
6
8
 
7
9
  --bl-font-mono: 'Courier New', monospace;
8
10
  --bl-radius-sm: 3px;
9
11
  --bl-radius-md: 4px;
12
+ --bl-radius-lg: 6px;
10
13
  --bl-text-xs: 10px;
11
14
  --bl-text-sm: 11px;
12
15
  --bl-text-md: 12px;
16
+ --bl-text-xl: 18px;
13
17
 
14
18
  --bl-bg: #1e1e2e;
15
19
  --bl-bg-strong: #181825;
@@ -34,6 +38,8 @@
34
38
  --bl-ok: #a6e3a1;
35
39
  --bl-error: #f38ba8;
36
40
  --bl-warn: #fab387;
41
+ --bl-danger: var(--bl-error);
42
+ --bl-warning: var(--bl-warn);
37
43
  --bl-info: #74c7ec;
38
44
 
39
45
  --bl-shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.5);
@@ -50,7 +56,9 @@
50
56
  :root,
51
57
  :root.bl-theme-light,
52
58
  :root.bl-theme-purple-blue-light,
53
- :root.bl-theme-purple-blue {
59
+ :root.bl-theme-midnight-light,
60
+ :root.bl-theme-purple-blue,
61
+ :root.bl-theme-midnight {
54
62
 
55
63
  --bl-light-bg: #f4f1ea;
56
64
  --bl-light-bg-strong: #ebe6dc;
@@ -487,6 +495,7 @@
487
495
 
488
496
  @media (prefers-color-scheme: light) {
489
497
  :root:not([class*="bl-theme-"]):not(.bl-color-scheme-dark),
498
+ :root.bl-theme-midnight:not(.bl-color-scheme-dark),
490
499
  :root.bl-theme-purple-blue:not(.bl-color-scheme-dark),
491
500
  :root.bl-theme-candy:not(.bl-color-scheme-dark),
492
501
  :root.bl-theme-earth:not(.bl-color-scheme-dark),
@@ -530,6 +539,7 @@
530
539
  }
531
540
  }
532
541
 
542
+ :root.bl-theme-midnight.bl-color-scheme-light,
533
543
  :root.bl-theme-purple-blue.bl-color-scheme-light,
534
544
  :root.bl-theme-candy.bl-color-scheme-light,
535
545
  :root.bl-theme-earth.bl-color-scheme-light,
@@ -537,6 +547,7 @@
537
547
  :root.bl-theme-sunset.bl-color-scheme-light,
538
548
  :root.bl-theme-high-contrast.bl-color-scheme-light,
539
549
  :root.bl-theme-light,
550
+ :root.bl-theme-midnight-light,
540
551
  :root.bl-theme-purple-blue-light,
541
552
  :root.bl-theme-anime-light,
542
553
  :root.bl-theme-candy-light,
@@ -655,10 +666,14 @@ input[type=file] {
655
666
  }
656
667
 
657
668
  input:focus, select:focus, textarea:focus {
658
- outline: none;
659
669
  border-color: var(--bl-accent);
660
670
  }
661
671
 
672
+ :where(a, button, input, select, textarea, [tabindex]):focus-visible {
673
+ outline: 2px solid var(--bl-accent);
674
+ outline-offset: 2px;
675
+ }
676
+
662
677
  button {
663
678
  padding: 3px 10px;
664
679
  background: var(--bl-surface);
@@ -736,6 +751,74 @@ button:disabled { opacity: 0.4; cursor: default; }
736
751
  gap: 8px;
737
752
  }
738
753
 
754
+ .bl-dialog {
755
+ width: min(460px, calc(100vw - 24px));
756
+ max-height: calc(100dvh - 24px);
757
+ overflow: hidden;
758
+ padding: 0;
759
+ border: 1px solid var(--bl-border-control);
760
+ border-radius: var(--bl-radius-md);
761
+ color: var(--bl-text);
762
+ background: var(--bl-bg-strong);
763
+ box-shadow: var(--bl-shadow-popover);
764
+ }
765
+
766
+ .bl-dialog::backdrop { background: var(--bl-overlay); }
767
+
768
+ .bl-dialog-form {
769
+ display: flex;
770
+ flex-direction: column;
771
+ max-height: calc(100dvh - 24px);
772
+ }
773
+
774
+ .bl-dialog-header,
775
+ .bl-dialog-body,
776
+ .bl-dialog-actions {
777
+ padding: 14px;
778
+ }
779
+
780
+ .bl-dialog-header {
781
+ border-bottom: 1px solid var(--bl-border);
782
+ background: var(--bl-bg);
783
+ }
784
+
785
+ .bl-dialog-header h2 {
786
+ margin: 0;
787
+ color: var(--bl-text);
788
+ font-size: var(--bl-text-xl);
789
+ }
790
+
791
+ .bl-dialog-body {
792
+ display: grid;
793
+ gap: 12px;
794
+ overflow-y: auto;
795
+ }
796
+
797
+ .bl-dialog-message {
798
+ margin: 0;
799
+ color: var(--bl-text-soft);
800
+ font-size: var(--bl-text-md);
801
+ line-height: 1.5;
802
+ overflow-wrap: anywhere;
803
+ white-space: pre-wrap;
804
+ }
805
+
806
+ .bl-dialog-field {
807
+ display: grid;
808
+ gap: 5px;
809
+ font-size: var(--bl-text-sm);
810
+ }
811
+
812
+ .bl-dialog-field input { width: 100%; }
813
+
814
+ .bl-dialog-actions {
815
+ display: flex;
816
+ justify-content: flex-end;
817
+ gap: 8px;
818
+ border-top: 1px solid var(--bl-border);
819
+ background: var(--bl-bg);
820
+ }
821
+
739
822
  #toolbar {
740
823
  display: flex;
741
824
  flex-direction: column;
@@ -903,6 +986,19 @@ button:disabled { opacity: 0.4; cursor: default; }
903
986
  padding: 6px;
904
987
  }
905
988
 
989
+ .client-menu-section-title {
990
+ padding: 10px 10px 3px;
991
+ color: var(--bl-accent-strong);
992
+ font-size: var(--bl-text-xs);
993
+ font-weight: bold;
994
+ letter-spacing: 0.5px;
995
+ text-transform: uppercase;
996
+ }
997
+
998
+ .client-menu-section-title:first-child {
999
+ padding-top: 4px;
1000
+ }
1001
+
906
1002
  .client-menu-item {
907
1003
  display: grid;
908
1004
  grid-template-columns: minmax(0, 1fr) auto;
@@ -945,6 +1041,18 @@ button:disabled { opacity: 0.4; cursor: default; }
945
1041
  margin-left: 6px;
946
1042
  }
947
1043
 
1044
+ .client-menu-recommended {
1045
+ display: inline-block;
1046
+ margin-left: 7px;
1047
+ padding: 1px 5px;
1048
+ border: 1px solid var(--bl-ok);
1049
+ border-radius: var(--bl-radius-sm);
1050
+ color: var(--bl-ok);
1051
+ font-size: var(--bl-text-xs);
1052
+ font-weight: normal;
1053
+ vertical-align: middle;
1054
+ }
1055
+
948
1056
  .client-menu-item-desc {
949
1057
  color: var(--bl-text-soft);
950
1058
  font-size: var(--bl-text-sm);
@@ -1582,6 +1690,17 @@ textarea {
1582
1690
  min-height: 30px;
1583
1691
  }
1584
1692
 
1693
+ :is(.bl-page-welcome, .bl-page-toon, .bl-page-character, .bl-page-web-tui, .bl-page-web-repl) {
1694
+ --bl-text-xs: 12px;
1695
+ --bl-text-sm: 14px;
1696
+ --bl-text-md: 16px;
1697
+ }
1698
+
1699
+ :is(.bl-page-welcome, .bl-page-toon, .bl-page-character, .bl-page-web-tui, .bl-page-web-repl)
1700
+ :is(button, input[type=file], input[type=text], input[type=number], select, .button-link) {
1701
+ min-height: 44px;
1702
+ }
1703
+
1585
1704
  .client-menu-backdrop {
1586
1705
  padding: 12px;
1587
1706
  }
@@ -5,16 +5,19 @@
5
5
  const THEME_CLASS_PREFIX = 'bl-theme-';
6
6
  const COLOR_SCHEME_KEY = 'bunnyland.color-scheme';
7
7
  const COLOR_SCHEME_CLASS_PREFIX = 'bl-color-scheme-';
8
- const DEFAULT_THEME = 'purple-blue';
8
+ const DEFAULT_THEME = 'midnight';
9
9
  const THEME_ALIASES = {
10
10
  anime: 'candy',
11
11
  'anime-dark': 'candy-dark',
12
12
  'anime-light': 'candy-light',
13
- dark: 'purple-blue-dark',
14
- light: 'purple-blue-light',
13
+ dark: 'midnight-dark',
14
+ light: 'midnight-light',
15
+ 'purple-blue': 'midnight',
16
+ 'purple-blue-dark': 'midnight-dark',
17
+ 'purple-blue-light': 'midnight-light',
15
18
  };
16
19
  const DEFAULT_THEME_OPTIONS = [
17
- { value: 'purple-blue', label: 'Purple / Blue' },
20
+ { value: 'midnight', label: 'Midnight Blue / Lavender' },
18
21
  { value: 'candy', label: 'Candy Pink / Cyan' },
19
22
  { value: 'earth', label: 'Earth Green / Gold' },
20
23
  { value: 'ocean', label: 'Ocean Teal / Coral' },
@@ -31,6 +34,7 @@
31
34
  const boundThemeSelects = new Set();
32
35
  const boundColorSchemeSelects = new Set();
33
36
  const CLIENT_MENU_SEEN_KEY = 'bunnyland.clientMenu.seen';
37
+ let actionDialogQueue = Promise.resolve();
34
38
  let clientMenuBaseUrl = '';
35
39
  // Admin tools order: World Generator, World Graph, editor tools alphabetically, then miscellaneous tools.
36
40
  const CLIENT_MENU_ITEMS = [
@@ -39,6 +43,7 @@
39
43
  title: 'Welcome',
40
44
  label: 'Start here',
41
45
  description: 'Project overview, docs, admin notes, and client chooser.',
46
+ group: 'Bunnyland',
42
47
  supportsServer: true,
43
48
  },
44
49
  {
@@ -46,21 +51,25 @@
46
51
  title: 'Bunnyland.dev',
47
52
  label: 'Website',
48
53
  description: 'Project homepage, feature overview, guides, and public docs.',
54
+ group: 'Bunnyland',
49
55
  supportsServer: false,
50
56
  },
51
- {
52
- href: 'toon-client.html',
53
- title: 'Toon Client',
54
- label: 'Player room view',
55
- description: 'Claim a character and play from the room-focused visual client.',
56
- supportsFocus: true,
57
- supportsServer: true,
58
- },
59
57
  {
60
58
  href: 'web-tui.html',
61
59
  title: 'Web TUI',
62
60
  label: 'Player action menu',
63
61
  description: 'Claim a character and play from the terminal TUI-style browser client.',
62
+ group: 'Player clients',
63
+ recommended: true,
64
+ supportsFocus: true,
65
+ supportsServer: true,
66
+ },
67
+ {
68
+ href: 'toon-client.html',
69
+ title: 'Toon Client',
70
+ label: 'Player room view',
71
+ description: 'Claim a character and play from the room-focused visual client.',
72
+ group: 'Player clients',
64
73
  supportsFocus: true,
65
74
  supportsServer: true,
66
75
  },
@@ -69,6 +78,7 @@
69
78
  title: 'Web REPL',
70
79
  label: 'Text-based play',
71
80
  description: 'Claim a character and play with typed commands in the browser.',
81
+ group: 'Player clients',
72
82
  supportsFocus: true,
73
83
  supportsServer: true,
74
84
  },
@@ -77,6 +87,7 @@
77
87
  title: 'Character Profile',
78
88
  label: 'Profile and chat',
79
89
  description: 'Open a character profile, inspect live state, and chat in character.',
90
+ group: 'Player clients',
80
91
  supportsFocus: true,
81
92
  supportsServer: true,
82
93
  },
@@ -85,6 +96,7 @@
85
96
  title: 'World Generator',
86
97
  label: 'Admin generator',
87
98
  description: 'Generate or replace a live world using enabled server generators.',
99
+ group: 'Builder & admin',
88
100
  supportsServer: true,
89
101
  admin: true,
90
102
  },
@@ -93,6 +105,7 @@
93
105
  title: 'World Graph',
94
106
  label: 'Graph editor',
95
107
  description: 'Browse and extend the ECS world graph from a snapshot or live server.',
108
+ group: 'Builder & admin',
96
109
  supportsServer: true,
97
110
  admin: true,
98
111
  },
@@ -101,6 +114,7 @@
101
114
  title: 'Behavior Editor',
102
115
  label: 'Behavior trees',
103
116
  description: 'Author behavior-tree JSON for behavioral controllers and register it live.',
117
+ group: 'Builder & admin',
104
118
  supportsServer: true,
105
119
  admin: true,
106
120
  },
@@ -109,6 +123,7 @@
109
123
  title: 'Memory Editor',
110
124
  label: 'Memory editor',
111
125
  description: 'Inspect and edit character memory collections on a live server.',
126
+ group: 'Builder & admin',
112
127
  supportsServer: true,
113
128
  admin: true,
114
129
  },
@@ -117,6 +132,7 @@
117
132
  title: 'Script Editor',
118
133
  label: 'Automation scripts',
119
134
  description: 'Create and validate script JSON blocks against a snapshot.',
135
+ group: 'Builder & admin',
120
136
  supportsServer: false,
121
137
  admin: true,
122
138
  },
@@ -125,6 +141,7 @@
125
141
  title: 'World Editor',
126
142
  label: 'Admin editor',
127
143
  description: 'Edit entities, components, relationships, fragments, and live snapshots.',
144
+ group: 'Builder & admin',
128
145
  supportsServer: true,
129
146
  admin: true,
130
147
  },
@@ -133,6 +150,7 @@
133
150
  title: 'Event Stream',
134
151
  label: 'Event viewer',
135
152
  description: 'Watch the live world event feed with expandable records and entity references.',
153
+ group: 'Builder & admin',
136
154
  supportsServer: true,
137
155
  admin: true,
138
156
  },
@@ -141,6 +159,7 @@
141
159
  title: 'Trace Analyzer',
142
160
  label: 'Trace inspection',
143
161
  description: 'Inspect live Tempo traces or load JSON and JSONL trace artifacts.',
162
+ group: 'Builder & admin',
144
163
  supportsServer: true,
145
164
  admin: true,
146
165
  },
@@ -473,6 +492,138 @@
473
492
  return url.origin === location.origin ? '' : ' target="_blank" rel="noopener"';
474
493
  }
475
494
 
495
+ function ensureActionDialog() {
496
+ let dialog = document.getElementById('bl-action-dialog');
497
+ if (dialog) return dialog;
498
+ dialog = document.createElement('dialog');
499
+ dialog.id = 'bl-action-dialog';
500
+ dialog.className = 'bl-dialog';
501
+ dialog.setAttribute('aria-labelledby', 'bl-action-dialog-title');
502
+ document.body.appendChild(dialog);
503
+ return dialog;
504
+ }
505
+
506
+ function actionDialog({
507
+ cancelLabel = 'Cancel',
508
+ cancellable = true,
509
+ confirmLabel = 'OK',
510
+ fields = [],
511
+ message = '',
512
+ title = 'Bunnyland',
513
+ tone = 'default',
514
+ } = {}) {
515
+ const open = () => new Promise((resolve) => {
516
+ const dialog = ensureActionDialog();
517
+ const previousFocus = document.activeElement;
518
+ dialog.innerHTML = `
519
+ <form class="bl-dialog-form" method="dialog">
520
+ <div class="bl-dialog-header"><h2 id="bl-action-dialog-title">${escapeHtml(title)}</h2></div>
521
+ <div class="bl-dialog-body">
522
+ ${message ? `<p class="bl-dialog-message">${escapeHtml(message)}</p>` : ''}
523
+ ${fields.map((field, index) => `
524
+ <label class="bl-dialog-field">
525
+ <span>${escapeHtml(field.label || field.name || `Value ${index + 1}`)}</span>
526
+ <input name="${escapeHtml(field.name || `field-${index}`)}"
527
+ type="${escapeHtml(field.type || 'text')}"
528
+ value="${escapeHtml(field.value || '')}"
529
+ ${field.autocomplete ? `autocomplete="${escapeHtml(field.autocomplete)}"` : ''}
530
+ ${field.placeholder ? `placeholder="${escapeHtml(field.placeholder)}"` : ''}
531
+ ${field.required ? 'required' : ''}>
532
+ </label>
533
+ `).join('')}
534
+ </div>
535
+ <div class="bl-dialog-actions">
536
+ ${cancellable ? `<button class="bl-dialog-cancel" type="button">${escapeHtml(cancelLabel)}</button>` : ''}
537
+ <button class="bl-dialog-confirm ${tone === 'danger' ? 'bl-button-danger' : 'bl-button-primary'}" type="submit">${escapeHtml(confirmLabel)}</button>
538
+ </div>
539
+ </form>
540
+ `;
541
+ const form = dialog.querySelector('.bl-dialog-form');
542
+ const cancel = dialog.querySelector('.bl-dialog-cancel');
543
+ let settled = false;
544
+
545
+ const finish = (value) => {
546
+ if (settled) return;
547
+ settled = true;
548
+ dialog.removeEventListener('cancel', onCancel);
549
+ dialog.removeEventListener('click', onBackdropClick);
550
+ if (dialog.open && typeof dialog.close === 'function') dialog.close();
551
+ else dialog.removeAttribute('open');
552
+ previousFocus?.focus?.();
553
+ resolve(value);
554
+ };
555
+ const onCancel = (event) => {
556
+ event.preventDefault();
557
+ if (cancellable) finish(null);
558
+ };
559
+ const onBackdropClick = (event) => {
560
+ if (cancellable && event.target === dialog) finish(null);
561
+ };
562
+
563
+ form.addEventListener('submit', (event) => {
564
+ event.preventDefault();
565
+ const values = {};
566
+ for (const field of fields) {
567
+ const input = form.elements.namedItem(field.name);
568
+ values[field.name] = input?.value || '';
569
+ }
570
+ finish(values);
571
+ });
572
+ cancel?.addEventListener('click', () => finish(null));
573
+ dialog.addEventListener('cancel', onCancel);
574
+ dialog.addEventListener('click', onBackdropClick);
575
+ if (typeof dialog.showModal === 'function') dialog.showModal();
576
+ else dialog.setAttribute('open', '');
577
+ (dialog.querySelector('input') || dialog.querySelector('.bl-dialog-confirm'))?.focus();
578
+ });
579
+ const queued = actionDialogQueue.then(open, open);
580
+ actionDialogQueue = queued.then(() => undefined, () => undefined);
581
+ return queued;
582
+ }
583
+
584
+ async function promptDialog(message, options = {}) {
585
+ const fieldName = 'value';
586
+ const values = await actionDialog({
587
+ ...options,
588
+ fields: [{
589
+ autocomplete: options.autocomplete,
590
+ label: options.label || 'Value',
591
+ name: fieldName,
592
+ placeholder: options.placeholder,
593
+ required: options.required,
594
+ type: options.type || 'text',
595
+ value: options.value || '',
596
+ }],
597
+ message,
598
+ });
599
+ return values ? values[fieldName] : null;
600
+ }
601
+
602
+ async function confirmDialog(message, options = {}) {
603
+ return Boolean(await actionDialog({
604
+ ...options,
605
+ confirmLabel: options.confirmLabel || 'Confirm',
606
+ fields: [],
607
+ message,
608
+ }));
609
+ }
610
+
611
+ async function credentialsDialog(options = {}) {
612
+ return actionDialog({
613
+ ...options,
614
+ confirmLabel: options.confirmLabel || 'Sign in',
615
+ fields: [
616
+ { autocomplete: 'username', label: options.usernameLabel || 'Username', name: 'username', required: true },
617
+ { autocomplete: 'current-password', label: options.passwordLabel || 'Password', name: 'password', required: true, type: 'password' },
618
+ ],
619
+ title: options.title || 'Sign in to Bunnyland',
620
+ });
621
+ }
622
+
623
+ async function alertDialog(message, options = {}) {
624
+ await actionDialog({ ...options, cancellable: false, fields: [], message });
625
+ }
626
+
476
627
  function ensureClientMenu() {
477
628
  let dialog = document.getElementById('client-menu-dialog');
478
629
  if (dialog) return dialog;
@@ -501,12 +652,14 @@
501
652
  <button class="client-menu-close" type="button" aria-label="Close client menu">x</button>
502
653
  </div>
503
654
  <div class="client-menu-list">
504
- ${CLIENT_MENU_ITEMS.map((item) => {
655
+ ${CLIENT_MENU_ITEMS.map((item, index) => {
505
656
  const active = item.href === current || (current === '' && item.href === 'index.html');
657
+ const previousGroup = CLIENT_MENU_ITEMS[index - 1]?.group;
506
658
  return `
659
+ ${item.group !== previousGroup ? `<div class="client-menu-section-title">${escapeHtml(item.group)}</div>` : ''}
507
660
  <a class="client-menu-item ${active ? 'active' : ''}" href="${escapeHtml(clientHref(item))}"${clientTargetAttrs(item)}>
508
661
  <span class="client-menu-item-main">
509
- <span class="client-menu-item-title">${escapeHtml(item.title)}${item.admin ? '<span class="client-menu-admin-badge" title="Requires authentication" aria-label="Requires authentication">●</span>' : ''}</span>
662
+ <span class="client-menu-item-title">${escapeHtml(item.title)}${item.recommended ? '<span class="client-menu-recommended">Recommended</span>' : ''}${item.admin ? '<span class="client-menu-admin-badge" title="Requires authentication" aria-label="Requires authentication">●</span>' : ''}</span>
510
663
  <span class="client-menu-item-desc">${escapeHtml(item.description)}</span>
511
664
  </span>
512
665
  <span class="client-menu-item-label">${escapeHtml(active ? 'Current' : item.label)}</span>
@@ -765,6 +918,7 @@
765
918
  }
766
919
 
767
920
  window.BunnylandUI = {
921
+ alertDialog,
768
922
  bindColorSchemeSelect,
769
923
  bindTagEditor,
770
924
  bindSearchDropdown,
@@ -774,12 +928,15 @@
774
928
  currentColorScheme,
775
929
  escapeHtml,
776
930
  currentTheme,
931
+ confirmDialog,
932
+ credentialsDialog,
777
933
  initClientMenu,
778
934
  initHelp,
779
935
  initTheme,
780
936
  loadConfig,
781
937
  normalizeTags,
782
938
  normalizeTheme,
939
+ promptDialog,
783
940
  registerThemeOption,
784
941
  registerThemeOptions,
785
942
  renderTagEditorTags,
package/dist/theme.d.ts CHANGED
@@ -2,7 +2,7 @@ export declare const THEME_KEY = "bunnyland.theme";
2
2
  export declare const THEME_CLASS_PREFIX = "bl-theme-";
3
3
  export declare const COLOR_SCHEME_KEY = "bunnyland.color-scheme";
4
4
  export declare const COLOR_SCHEME_CLASS_PREFIX = "bl-color-scheme-";
5
- export declare const DEFAULT_THEME = "purple-blue";
5
+ export declare const DEFAULT_THEME = "midnight";
6
6
  export declare const THEME_CHANGE_EVENT = "bunnyland:themechange";
7
7
  export interface ThemeOption {
8
8
  value: string;
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,oBAAoB,CAAC;AAC3C,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAC9C,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AACzD,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAC5D,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAC3C,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpD,eAAO,MAAM,qBAAqB,EAAE,WAAW,EAO9C,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,WAAW,EAAyD,CAAC;AAsEjG,wBAAgB,eAAe,CAAC,MAAM,SAAoC,GAAG,MAAM,GAAG,IAAI,CAKzF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGtE;AAED,wBAAgB,YAAY,CAAC,IAAI,GAAE,WAAsC,GAAG,MAAM,CAEjF;AAED,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,WAAsC,GAAG,WAAW,CAE5F;AAkCD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAsC,GAAG,WAAW,CAEtG;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAsC,GAAG,MAAM,CAK3F;AAED,wBAAgB,SAAS,CACvB,IAAI,GAAE,WAAsC,EAC5C,YAAY,SAAgB,EAC5B,MAAM,SAAoC,GACzC,MAAM,CAUR;AAED,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAE5C;AAED,wBAAgB,kBAAkB,IAAI,WAAW,EAAE,CAElD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI,CAQ3E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,EAAE,CAK7F;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG;IAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,IAAI,CAY9G;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG;IAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,IAAI,CAWpH"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,oBAAoB,CAAC;AAC3C,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAC9C,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AACzD,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAC5D,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpD,eAAO,MAAM,qBAAqB,EAAE,WAAW,EAO9C,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,WAAW,EAAyD,CAAC;AAyEjG,wBAAgB,eAAe,CAAC,MAAM,SAAoC,GAAG,MAAM,GAAG,IAAI,CAKzF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGtE;AAED,wBAAgB,YAAY,CAAC,IAAI,GAAE,WAAsC,GAAG,MAAM,CAEjF;AAED,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,WAAsC,GAAG,WAAW,CAE5F;AAkCD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAsC,GAAG,WAAW,CAEtG;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAsC,GAAG,MAAM,CAK3F;AAED,wBAAgB,SAAS,CACvB,IAAI,GAAE,WAAsC,EAC5C,YAAY,SAAgB,EAC5B,MAAM,SAAoC,GACzC,MAAM,CAUR;AAED,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAE5C;AAED,wBAAgB,kBAAkB,IAAI,WAAW,EAAE,CAElD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI,CAQ3E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,EAAE,CAK7F;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG;IAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,IAAI,CAY9G;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG;IAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,IAAI,CAWpH"}
package/dist/theme.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { escapeHtml as e, storageGet as t, storageSet as n } from "./widgets.js";
2
2
  //#region src/theme.ts
3
- var r = "bunnyland.theme", i = "bl-theme-", a = "bunnyland.color-scheme", o = "bl-color-scheme-", s = "purple-blue", c = "bunnyland:themechange", l = [
3
+ var r = "bunnyland.theme", i = "bl-theme-", a = "bunnyland.color-scheme", o = "bl-color-scheme-", s = "midnight", c = "bunnyland:themechange", l = [
4
4
  {
5
- value: "purple-blue",
6
- label: "Purple / Blue"
5
+ value: "midnight",
6
+ label: "Midnight Blue / Lavender"
7
7
  },
8
8
  {
9
9
  value: "candy",
@@ -29,8 +29,11 @@ var r = "bunnyland.theme", i = "bl-theme-", a = "bunnyland.color-scheme", o = "b
29
29
  anime: "candy",
30
30
  "anime-dark": "candy-dark",
31
31
  "anime-light": "candy-light",
32
- dark: "purple-blue-dark",
33
- light: "purple-blue-light"
32
+ dark: "midnight-dark",
33
+ light: "midnight-light",
34
+ "purple-blue": "midnight",
35
+ "purple-blue-dark": "midnight-dark",
36
+ "purple-blue-light": "midnight-light"
34
37
  }, f = [
35
38
  {
36
39
  value: "auto",
@@ -46,7 +49,7 @@ var r = "bunnyland.theme", i = "bl-theme-", a = "bunnyland.color-scheme", o = "b
46
49
  }
47
50
  ], p = /^[a-z0-9][a-z0-9-]*$/, m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
48
51
  function g(e) {
49
- let t = d[String(e || "purple-blue").trim()] || String(e || "purple-blue").trim();
52
+ let t = d[String(e || "midnight").trim()] || String(e || "midnight").trim();
50
53
  if (v(t)) return {
51
54
  theme: t,
52
55
  scheme: null
@@ -131,7 +134,7 @@ function N(e, t = document.documentElement) {
131
134
  return n.scheme && j(n.scheme, t, !0), A(E(n.theme), t, !0);
132
135
  }
133
136
  function P(e = document.documentElement, n = s, i = globalThis.location?.search || "") {
134
- let o = new URLSearchParams(i).get("theme"), c = T(i), l = t(r), u = g(c && o || l || n || "purple-blue"), d = t(a);
137
+ let o = new URLSearchParams(i).get("theme"), c = T(i), l = t(r), u = g(c && o || l || n || "midnight"), d = t(a);
135
138
  return j(u.scheme || S(d), e, !!(u.scheme || d)), A(E(u.theme), e, !!(c || l && v(u.theme)));
136
139
  }
137
140
  function F() {
package/dist/theme.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"theme.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["import { escapeHtml, storageGet, storageSet } from './widgets';\n\nexport const THEME_KEY = 'bunnyland.theme';\nexport const THEME_CLASS_PREFIX = 'bl-theme-';\nexport const COLOR_SCHEME_KEY = 'bunnyland.color-scheme';\nexport const COLOR_SCHEME_CLASS_PREFIX = 'bl-color-scheme-';\nexport const DEFAULT_THEME = 'purple-blue';\nexport const THEME_CHANGE_EVENT = 'bunnyland:themechange';\n\nexport interface ThemeOption {\n value: string;\n label: string;\n}\n\nexport type ColorScheme = 'auto' | 'dark' | 'light';\n\nexport const DEFAULT_THEME_OPTIONS: ThemeOption[] = [\n { value: 'purple-blue', label: 'Purple / Blue' },\n { value: 'candy', label: 'Candy Pink / Cyan' },\n { value: 'earth', label: 'Earth Green / Gold' },\n { value: 'ocean', label: 'Ocean Teal / Coral' },\n { value: 'sunset', label: 'Sunset Orange / Plum' },\n { value: 'high-contrast', label: 'High Contrast' },\n];\n\nexport const THEME_OPTIONS: ThemeOption[] = DEFAULT_THEME_OPTIONS.map(option => ({ ...option }));\n\nconst THEME_ALIASES: Record<string, string> = {\n anime: 'candy',\n 'anime-dark': 'candy-dark',\n 'anime-light': 'candy-light',\n dark: 'purple-blue-dark',\n light: 'purple-blue-light',\n};\n\nconst COLOR_SCHEME_OPTIONS: ThemeOption[] = [\n { value: 'auto', label: 'Auto (System)' },\n { value: 'dark', label: 'Dark' },\n { value: 'light', label: 'Light' },\n];\n\nconst THEME_VALUE_PATTERN = /^[a-z0-9][a-z0-9-]*$/;\nconst boundThemeSelects = new Set<HTMLSelectElement>();\nconst boundColorSchemeSelects = new Set<HTMLSelectElement>();\n\nfunction parseThemeSelection(name: string | null | undefined): { scheme: ColorScheme | null; theme: string } {\n const raw = THEME_ALIASES[String(name || DEFAULT_THEME).trim()] || String(name || DEFAULT_THEME).trim();\n if (isKnownTheme(raw)) return { theme: raw, scheme: null };\n const match = raw.match(/^(.*)-(dark|light)$/);\n if (match && isKnownTheme(match[1])) return { theme: match[1], scheme: match[2] as ColorScheme };\n return { theme: raw, scheme: null };\n}\n\nfunction normalizeThemeValue(name: string | null | undefined): string {\n return parseThemeSelection(name).theme;\n}\n\nfunction isKnownTheme(name: string): boolean {\n return THEME_OPTIONS.some(option => option.value === name);\n}\n\nfunction sanitizeThemeOption(option: ThemeOption): ThemeOption | null {\n const value = String(option?.value || '').trim();\n if (!THEME_VALUE_PATTERN.test(value)) return null;\n const label = String(option?.label || value).trim() || value;\n return { value, label };\n}\n\nfunction renderThemeSelect(select: HTMLSelectElement): void {\n const theme = currentTheme();\n select.innerHTML = themeOptions().map(option => `\n <option value=\"${escapeHtml(option.value)}\">${escapeHtml(option.label)}</option>\n `).join('');\n select.value = theme;\n}\n\nfunction refreshThemeSelects(): void {\n for (const select of boundThemeSelects) renderThemeSelect(select);\n}\n\nfunction normalizeColorSchemeValue(name: string | null | undefined): ColorScheme {\n return name === 'dark' || name === 'light' ? name : 'auto';\n}\n\nfunction renderColorSchemeSelect(select: HTMLSelectElement): void {\n select.innerHTML = COLOR_SCHEME_OPTIONS.map(option => `\n <option value=\"${option.value}\">${option.label}</option>\n `).join('');\n select.value = currentColorScheme();\n}\n\nfunction refreshColorSchemeSelects(): void {\n for (const select of boundColorSchemeSelects) renderColorSchemeSelect(select);\n}\n\nexport function themeFromSearch(search = globalThis.location?.search || ''): string | null {\n const requested = new URLSearchParams(search).get('theme');\n if (!requested) return null;\n const theme = normalizeThemeValue(requested);\n return isKnownTheme(theme) ? theme : null;\n}\n\nexport function normalizeTheme(name: string | null | undefined): string {\n const theme = normalizeThemeValue(name);\n return isKnownTheme(theme) ? theme : DEFAULT_THEME;\n}\n\nexport function currentTheme(root: HTMLElement = document.documentElement): string {\n return normalizeTheme(root.dataset.theme);\n}\n\nexport function currentColorScheme(root: HTMLElement = document.documentElement): ColorScheme {\n return normalizeColorSchemeValue(root.dataset.colorScheme);\n}\n\nfunction dispatchThemeChange(root: HTMLElement): void {\n if (typeof root.dispatchEvent === 'function' && typeof CustomEvent === 'function') {\n root.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, {\n detail: { colorScheme: currentColorScheme(root), theme: currentTheme(root) },\n }));\n }\n}\n\nfunction applyTheme(theme: string, root: HTMLElement, persist: boolean): string {\n for (const className of [...root.classList]) {\n if (className.startsWith(THEME_CLASS_PREFIX)) root.classList.remove(className);\n }\n root.classList.add(`${THEME_CLASS_PREFIX}${theme}`);\n root.dataset.theme = theme;\n if (persist) storageSet(THEME_KEY, theme);\n refreshThemeSelects();\n dispatchThemeChange(root);\n return theme;\n}\n\nfunction applyColorScheme(scheme: ColorScheme, root: HTMLElement, persist: boolean): ColorScheme {\n for (const className of [...root.classList]) {\n if (className.startsWith(COLOR_SCHEME_CLASS_PREFIX)) root.classList.remove(className);\n }\n if (scheme !== 'auto') root.classList.add(`${COLOR_SCHEME_CLASS_PREFIX}${scheme}`);\n root.dataset.colorScheme = scheme;\n if (persist) storageSet(COLOR_SCHEME_KEY, scheme);\n refreshColorSchemeSelects();\n dispatchThemeChange(root);\n return scheme;\n}\n\nexport function setColorScheme(name: string, root: HTMLElement = document.documentElement): ColorScheme {\n return applyColorScheme(normalizeColorSchemeValue(name), root, true);\n}\n\nexport function setTheme(name: string, root: HTMLElement = document.documentElement): string {\n const selection = parseThemeSelection(name);\n if (selection.scheme) applyColorScheme(selection.scheme, root, true);\n const theme = normalizeTheme(selection.theme);\n return applyTheme(theme, root, true);\n}\n\nexport function initTheme(\n root: HTMLElement = document.documentElement,\n defaultTheme = DEFAULT_THEME,\n search = globalThis.location?.search || '',\n): string {\n const linkedValue = new URLSearchParams(search).get('theme');\n const linkedTheme = themeFromSearch(search);\n const stored = storageGet(THEME_KEY);\n const selection = parseThemeSelection((linkedTheme && linkedValue) || stored || defaultTheme || DEFAULT_THEME);\n const storedScheme = storageGet(COLOR_SCHEME_KEY);\n const scheme = selection.scheme || normalizeColorSchemeValue(storedScheme);\n applyColorScheme(scheme, root, Boolean(selection.scheme || storedScheme));\n const theme = normalizeTheme(selection.theme);\n return applyTheme(theme, root, Boolean(linkedTheme || (stored && isKnownTheme(selection.theme))));\n}\n\nexport function themeOptions(): ThemeOption[] {\n return THEME_OPTIONS.map(option => ({ ...option }));\n}\n\nexport function colorSchemeOptions(): ThemeOption[] {\n return COLOR_SCHEME_OPTIONS.map(option => ({ ...option }));\n}\n\nexport function registerThemeOption(option: ThemeOption): ThemeOption | null {\n const theme = sanitizeThemeOption(option);\n if (!theme) return null;\n const index = THEME_OPTIONS.findIndex(existing => existing.value === theme.value);\n if (index === -1) THEME_OPTIONS.push(theme);\n else THEME_OPTIONS[index] = theme;\n refreshThemeSelects();\n return { ...theme };\n}\n\nexport function registerThemeOptions(options: ThemeOption[] | null | undefined): ThemeOption[] {\n if (!Array.isArray(options)) return [];\n return options\n .map(option => registerThemeOption(option))\n .filter((option): option is ThemeOption => option !== null);\n}\n\nexport function bindThemeSelect(select: HTMLSelectElement | null): { setValue: (value: string) => void } | null {\n if (!select) return null;\n boundThemeSelects.add(select);\n renderThemeSelect(select);\n select.value = initTheme();\n select.addEventListener('change', () => setTheme(select.value));\n return {\n setValue(value: string): void {\n select.value = normalizeTheme(value);\n setTheme(select.value);\n },\n };\n}\n\nexport function bindColorSchemeSelect(select: HTMLSelectElement | null): { setValue: (value: string) => void } | null {\n if (!select) return null;\n boundColorSchemeSelects.add(select);\n renderColorSchemeSelect(select);\n select.addEventListener('change', () => setColorScheme(select.value));\n return {\n setValue(value: string): void {\n select.value = normalizeColorSchemeValue(value);\n setColorScheme(select.value);\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"theme.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["import { escapeHtml, storageGet, storageSet } from './widgets';\n\nexport const THEME_KEY = 'bunnyland.theme';\nexport const THEME_CLASS_PREFIX = 'bl-theme-';\nexport const COLOR_SCHEME_KEY = 'bunnyland.color-scheme';\nexport const COLOR_SCHEME_CLASS_PREFIX = 'bl-color-scheme-';\nexport const DEFAULT_THEME = 'midnight';\nexport const THEME_CHANGE_EVENT = 'bunnyland:themechange';\n\nexport interface ThemeOption {\n value: string;\n label: string;\n}\n\nexport type ColorScheme = 'auto' | 'dark' | 'light';\n\nexport const DEFAULT_THEME_OPTIONS: ThemeOption[] = [\n { value: 'midnight', label: 'Midnight Blue / Lavender' },\n { value: 'candy', label: 'Candy Pink / Cyan' },\n { value: 'earth', label: 'Earth Green / Gold' },\n { value: 'ocean', label: 'Ocean Teal / Coral' },\n { value: 'sunset', label: 'Sunset Orange / Plum' },\n { value: 'high-contrast', label: 'High Contrast' },\n];\n\nexport const THEME_OPTIONS: ThemeOption[] = DEFAULT_THEME_OPTIONS.map(option => ({ ...option }));\n\nconst THEME_ALIASES: Record<string, string> = {\n anime: 'candy',\n 'anime-dark': 'candy-dark',\n 'anime-light': 'candy-light',\n dark: 'midnight-dark',\n light: 'midnight-light',\n 'purple-blue': 'midnight',\n 'purple-blue-dark': 'midnight-dark',\n 'purple-blue-light': 'midnight-light',\n};\n\nconst COLOR_SCHEME_OPTIONS: ThemeOption[] = [\n { value: 'auto', label: 'Auto (System)' },\n { value: 'dark', label: 'Dark' },\n { value: 'light', label: 'Light' },\n];\n\nconst THEME_VALUE_PATTERN = /^[a-z0-9][a-z0-9-]*$/;\nconst boundThemeSelects = new Set<HTMLSelectElement>();\nconst boundColorSchemeSelects = new Set<HTMLSelectElement>();\n\nfunction parseThemeSelection(name: string | null | undefined): { scheme: ColorScheme | null; theme: string } {\n const raw = THEME_ALIASES[String(name || DEFAULT_THEME).trim()] || String(name || DEFAULT_THEME).trim();\n if (isKnownTheme(raw)) return { theme: raw, scheme: null };\n const match = raw.match(/^(.*)-(dark|light)$/);\n if (match && isKnownTheme(match[1])) return { theme: match[1], scheme: match[2] as ColorScheme };\n return { theme: raw, scheme: null };\n}\n\nfunction normalizeThemeValue(name: string | null | undefined): string {\n return parseThemeSelection(name).theme;\n}\n\nfunction isKnownTheme(name: string): boolean {\n return THEME_OPTIONS.some(option => option.value === name);\n}\n\nfunction sanitizeThemeOption(option: ThemeOption): ThemeOption | null {\n const value = String(option?.value || '').trim();\n if (!THEME_VALUE_PATTERN.test(value)) return null;\n const label = String(option?.label || value).trim() || value;\n return { value, label };\n}\n\nfunction renderThemeSelect(select: HTMLSelectElement): void {\n const theme = currentTheme();\n select.innerHTML = themeOptions().map(option => `\n <option value=\"${escapeHtml(option.value)}\">${escapeHtml(option.label)}</option>\n `).join('');\n select.value = theme;\n}\n\nfunction refreshThemeSelects(): void {\n for (const select of boundThemeSelects) renderThemeSelect(select);\n}\n\nfunction normalizeColorSchemeValue(name: string | null | undefined): ColorScheme {\n return name === 'dark' || name === 'light' ? name : 'auto';\n}\n\nfunction renderColorSchemeSelect(select: HTMLSelectElement): void {\n select.innerHTML = COLOR_SCHEME_OPTIONS.map(option => `\n <option value=\"${option.value}\">${option.label}</option>\n `).join('');\n select.value = currentColorScheme();\n}\n\nfunction refreshColorSchemeSelects(): void {\n for (const select of boundColorSchemeSelects) renderColorSchemeSelect(select);\n}\n\nexport function themeFromSearch(search = globalThis.location?.search || ''): string | null {\n const requested = new URLSearchParams(search).get('theme');\n if (!requested) return null;\n const theme = normalizeThemeValue(requested);\n return isKnownTheme(theme) ? theme : null;\n}\n\nexport function normalizeTheme(name: string | null | undefined): string {\n const theme = normalizeThemeValue(name);\n return isKnownTheme(theme) ? theme : DEFAULT_THEME;\n}\n\nexport function currentTheme(root: HTMLElement = document.documentElement): string {\n return normalizeTheme(root.dataset.theme);\n}\n\nexport function currentColorScheme(root: HTMLElement = document.documentElement): ColorScheme {\n return normalizeColorSchemeValue(root.dataset.colorScheme);\n}\n\nfunction dispatchThemeChange(root: HTMLElement): void {\n if (typeof root.dispatchEvent === 'function' && typeof CustomEvent === 'function') {\n root.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, {\n detail: { colorScheme: currentColorScheme(root), theme: currentTheme(root) },\n }));\n }\n}\n\nfunction applyTheme(theme: string, root: HTMLElement, persist: boolean): string {\n for (const className of [...root.classList]) {\n if (className.startsWith(THEME_CLASS_PREFIX)) root.classList.remove(className);\n }\n root.classList.add(`${THEME_CLASS_PREFIX}${theme}`);\n root.dataset.theme = theme;\n if (persist) storageSet(THEME_KEY, theme);\n refreshThemeSelects();\n dispatchThemeChange(root);\n return theme;\n}\n\nfunction applyColorScheme(scheme: ColorScheme, root: HTMLElement, persist: boolean): ColorScheme {\n for (const className of [...root.classList]) {\n if (className.startsWith(COLOR_SCHEME_CLASS_PREFIX)) root.classList.remove(className);\n }\n if (scheme !== 'auto') root.classList.add(`${COLOR_SCHEME_CLASS_PREFIX}${scheme}`);\n root.dataset.colorScheme = scheme;\n if (persist) storageSet(COLOR_SCHEME_KEY, scheme);\n refreshColorSchemeSelects();\n dispatchThemeChange(root);\n return scheme;\n}\n\nexport function setColorScheme(name: string, root: HTMLElement = document.documentElement): ColorScheme {\n return applyColorScheme(normalizeColorSchemeValue(name), root, true);\n}\n\nexport function setTheme(name: string, root: HTMLElement = document.documentElement): string {\n const selection = parseThemeSelection(name);\n if (selection.scheme) applyColorScheme(selection.scheme, root, true);\n const theme = normalizeTheme(selection.theme);\n return applyTheme(theme, root, true);\n}\n\nexport function initTheme(\n root: HTMLElement = document.documentElement,\n defaultTheme = DEFAULT_THEME,\n search = globalThis.location?.search || '',\n): string {\n const linkedValue = new URLSearchParams(search).get('theme');\n const linkedTheme = themeFromSearch(search);\n const stored = storageGet(THEME_KEY);\n const selection = parseThemeSelection((linkedTheme && linkedValue) || stored || defaultTheme || DEFAULT_THEME);\n const storedScheme = storageGet(COLOR_SCHEME_KEY);\n const scheme = selection.scheme || normalizeColorSchemeValue(storedScheme);\n applyColorScheme(scheme, root, Boolean(selection.scheme || storedScheme));\n const theme = normalizeTheme(selection.theme);\n return applyTheme(theme, root, Boolean(linkedTheme || (stored && isKnownTheme(selection.theme))));\n}\n\nexport function themeOptions(): ThemeOption[] {\n return THEME_OPTIONS.map(option => ({ ...option }));\n}\n\nexport function colorSchemeOptions(): ThemeOption[] {\n return COLOR_SCHEME_OPTIONS.map(option => ({ ...option }));\n}\n\nexport function registerThemeOption(option: ThemeOption): ThemeOption | null {\n const theme = sanitizeThemeOption(option);\n if (!theme) return null;\n const index = THEME_OPTIONS.findIndex(existing => existing.value === theme.value);\n if (index === -1) THEME_OPTIONS.push(theme);\n else THEME_OPTIONS[index] = theme;\n refreshThemeSelects();\n return { ...theme };\n}\n\nexport function registerThemeOptions(options: ThemeOption[] | null | undefined): ThemeOption[] {\n if (!Array.isArray(options)) return [];\n return options\n .map(option => registerThemeOption(option))\n .filter((option): option is ThemeOption => option !== null);\n}\n\nexport function bindThemeSelect(select: HTMLSelectElement | null): { setValue: (value: string) => void } | null {\n if (!select) return null;\n boundThemeSelects.add(select);\n renderThemeSelect(select);\n select.value = initTheme();\n select.addEventListener('change', () => setTheme(select.value));\n return {\n setValue(value: string): void {\n select.value = normalizeTheme(value);\n setTheme(select.value);\n },\n };\n}\n\nexport function bindColorSchemeSelect(select: HTMLSelectElement | null): { setValue: (value: string) => void } | null {\n if (!select) return null;\n boundColorSchemeSelects.add(select);\n renderColorSchemeSelect(select);\n select.addEventListener('change', () => setColorScheme(select.value));\n return {\n setValue(value: string): void {\n select.value = normalizeColorSchemeValue(value);\n setColorScheme(select.value);\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnyland/ui-web",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Shared web UI helpers, themes, and widgets for Bunnyland browser clients.",
6
6
  "license": "AGPL-3.0-or-later",
package/src/theme.ts CHANGED
@@ -4,7 +4,7 @@ export const THEME_KEY = 'bunnyland.theme';
4
4
  export const THEME_CLASS_PREFIX = 'bl-theme-';
5
5
  export const COLOR_SCHEME_KEY = 'bunnyland.color-scheme';
6
6
  export const COLOR_SCHEME_CLASS_PREFIX = 'bl-color-scheme-';
7
- export const DEFAULT_THEME = 'purple-blue';
7
+ export const DEFAULT_THEME = 'midnight';
8
8
  export const THEME_CHANGE_EVENT = 'bunnyland:themechange';
9
9
 
10
10
  export interface ThemeOption {
@@ -15,7 +15,7 @@ export interface ThemeOption {
15
15
  export type ColorScheme = 'auto' | 'dark' | 'light';
16
16
 
17
17
  export const DEFAULT_THEME_OPTIONS: ThemeOption[] = [
18
- { value: 'purple-blue', label: 'Purple / Blue' },
18
+ { value: 'midnight', label: 'Midnight Blue / Lavender' },
19
19
  { value: 'candy', label: 'Candy Pink / Cyan' },
20
20
  { value: 'earth', label: 'Earth Green / Gold' },
21
21
  { value: 'ocean', label: 'Ocean Teal / Coral' },
@@ -29,8 +29,11 @@ const THEME_ALIASES: Record<string, string> = {
29
29
  anime: 'candy',
30
30
  'anime-dark': 'candy-dark',
31
31
  'anime-light': 'candy-light',
32
- dark: 'purple-blue-dark',
33
- light: 'purple-blue-light',
32
+ dark: 'midnight-dark',
33
+ light: 'midnight-light',
34
+ 'purple-blue': 'midnight',
35
+ 'purple-blue-dark': 'midnight-dark',
36
+ 'purple-blue-light': 'midnight-light',
34
37
  };
35
38
 
36
39
  const COLOR_SCHEME_OPTIONS: ThemeOption[] = [