@bunnyland/ui-web 0.2.1 → 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.
@@ -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
  }
@@ -9,9 +9,11 @@
9
9
  --bl-font-mono: 'Courier New', monospace;
10
10
  --bl-radius-sm: 3px;
11
11
  --bl-radius-md: 4px;
12
+ --bl-radius-lg: 6px;
12
13
  --bl-text-xs: 10px;
13
14
  --bl-text-sm: 11px;
14
15
  --bl-text-md: 12px;
16
+ --bl-text-xl: 18px;
15
17
 
16
18
  --bl-bg: #1e1e2e;
17
19
  --bl-bg-strong: #181825;
@@ -36,6 +38,8 @@
36
38
  --bl-ok: #a6e3a1;
37
39
  --bl-error: #f38ba8;
38
40
  --bl-warn: #fab387;
41
+ --bl-danger: var(--bl-error);
42
+ --bl-warning: var(--bl-warn);
39
43
  --bl-info: #74c7ec;
40
44
 
41
45
  --bl-shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.5);
@@ -662,10 +666,14 @@ input[type=file] {
662
666
  }
663
667
 
664
668
  input:focus, select:focus, textarea:focus {
665
- outline: none;
666
669
  border-color: var(--bl-accent);
667
670
  }
668
671
 
672
+ :where(a, button, input, select, textarea, [tabindex]):focus-visible {
673
+ outline: 2px solid var(--bl-accent);
674
+ outline-offset: 2px;
675
+ }
676
+
669
677
  button {
670
678
  padding: 3px 10px;
671
679
  background: var(--bl-surface);
@@ -743,6 +751,74 @@ button:disabled { opacity: 0.4; cursor: default; }
743
751
  gap: 8px;
744
752
  }
745
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
+
746
822
  #toolbar {
747
823
  display: flex;
748
824
  flex-direction: column;
@@ -910,6 +986,19 @@ button:disabled { opacity: 0.4; cursor: default; }
910
986
  padding: 6px;
911
987
  }
912
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
+
913
1002
  .client-menu-item {
914
1003
  display: grid;
915
1004
  grid-template-columns: minmax(0, 1fr) auto;
@@ -952,6 +1041,18 @@ button:disabled { opacity: 0.4; cursor: default; }
952
1041
  margin-left: 6px;
953
1042
  }
954
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
+
955
1056
  .client-menu-item-desc {
956
1057
  color: var(--bl-text-soft);
957
1058
  font-size: var(--bl-text-sm);
@@ -1589,6 +1690,17 @@ textarea {
1589
1690
  min-height: 30px;
1590
1691
  }
1591
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
+
1592
1704
  .client-menu-backdrop {
1593
1705
  padding: 12px;
1594
1706
  }
@@ -34,6 +34,7 @@
34
34
  const boundThemeSelects = new Set();
35
35
  const boundColorSchemeSelects = new Set();
36
36
  const CLIENT_MENU_SEEN_KEY = 'bunnyland.clientMenu.seen';
37
+ let actionDialogQueue = Promise.resolve();
37
38
  let clientMenuBaseUrl = '';
38
39
  // Admin tools order: World Generator, World Graph, editor tools alphabetically, then miscellaneous tools.
39
40
  const CLIENT_MENU_ITEMS = [
@@ -42,6 +43,7 @@
42
43
  title: 'Welcome',
43
44
  label: 'Start here',
44
45
  description: 'Project overview, docs, admin notes, and client chooser.',
46
+ group: 'Bunnyland',
45
47
  supportsServer: true,
46
48
  },
47
49
  {
@@ -49,21 +51,25 @@
49
51
  title: 'Bunnyland.dev',
50
52
  label: 'Website',
51
53
  description: 'Project homepage, feature overview, guides, and public docs.',
54
+ group: 'Bunnyland',
52
55
  supportsServer: false,
53
56
  },
54
- {
55
- href: 'toon-client.html',
56
- title: 'Toon Client',
57
- label: 'Player room view',
58
- description: 'Claim a character and play from the room-focused visual client.',
59
- supportsFocus: true,
60
- supportsServer: true,
61
- },
62
57
  {
63
58
  href: 'web-tui.html',
64
59
  title: 'Web TUI',
65
60
  label: 'Player action menu',
66
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',
67
73
  supportsFocus: true,
68
74
  supportsServer: true,
69
75
  },
@@ -72,6 +78,7 @@
72
78
  title: 'Web REPL',
73
79
  label: 'Text-based play',
74
80
  description: 'Claim a character and play with typed commands in the browser.',
81
+ group: 'Player clients',
75
82
  supportsFocus: true,
76
83
  supportsServer: true,
77
84
  },
@@ -80,6 +87,7 @@
80
87
  title: 'Character Profile',
81
88
  label: 'Profile and chat',
82
89
  description: 'Open a character profile, inspect live state, and chat in character.',
90
+ group: 'Player clients',
83
91
  supportsFocus: true,
84
92
  supportsServer: true,
85
93
  },
@@ -88,6 +96,7 @@
88
96
  title: 'World Generator',
89
97
  label: 'Admin generator',
90
98
  description: 'Generate or replace a live world using enabled server generators.',
99
+ group: 'Builder & admin',
91
100
  supportsServer: true,
92
101
  admin: true,
93
102
  },
@@ -96,6 +105,7 @@
96
105
  title: 'World Graph',
97
106
  label: 'Graph editor',
98
107
  description: 'Browse and extend the ECS world graph from a snapshot or live server.',
108
+ group: 'Builder & admin',
99
109
  supportsServer: true,
100
110
  admin: true,
101
111
  },
@@ -104,6 +114,7 @@
104
114
  title: 'Behavior Editor',
105
115
  label: 'Behavior trees',
106
116
  description: 'Author behavior-tree JSON for behavioral controllers and register it live.',
117
+ group: 'Builder & admin',
107
118
  supportsServer: true,
108
119
  admin: true,
109
120
  },
@@ -112,6 +123,7 @@
112
123
  title: 'Memory Editor',
113
124
  label: 'Memory editor',
114
125
  description: 'Inspect and edit character memory collections on a live server.',
126
+ group: 'Builder & admin',
115
127
  supportsServer: true,
116
128
  admin: true,
117
129
  },
@@ -120,6 +132,7 @@
120
132
  title: 'Script Editor',
121
133
  label: 'Automation scripts',
122
134
  description: 'Create and validate script JSON blocks against a snapshot.',
135
+ group: 'Builder & admin',
123
136
  supportsServer: false,
124
137
  admin: true,
125
138
  },
@@ -128,6 +141,7 @@
128
141
  title: 'World Editor',
129
142
  label: 'Admin editor',
130
143
  description: 'Edit entities, components, relationships, fragments, and live snapshots.',
144
+ group: 'Builder & admin',
131
145
  supportsServer: true,
132
146
  admin: true,
133
147
  },
@@ -136,6 +150,7 @@
136
150
  title: 'Event Stream',
137
151
  label: 'Event viewer',
138
152
  description: 'Watch the live world event feed with expandable records and entity references.',
153
+ group: 'Builder & admin',
139
154
  supportsServer: true,
140
155
  admin: true,
141
156
  },
@@ -144,6 +159,7 @@
144
159
  title: 'Trace Analyzer',
145
160
  label: 'Trace inspection',
146
161
  description: 'Inspect live Tempo traces or load JSON and JSONL trace artifacts.',
162
+ group: 'Builder & admin',
147
163
  supportsServer: true,
148
164
  admin: true,
149
165
  },
@@ -476,6 +492,138 @@
476
492
  return url.origin === location.origin ? '' : ' target="_blank" rel="noopener"';
477
493
  }
478
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
+
479
627
  function ensureClientMenu() {
480
628
  let dialog = document.getElementById('client-menu-dialog');
481
629
  if (dialog) return dialog;
@@ -504,12 +652,14 @@
504
652
  <button class="client-menu-close" type="button" aria-label="Close client menu">x</button>
505
653
  </div>
506
654
  <div class="client-menu-list">
507
- ${CLIENT_MENU_ITEMS.map((item) => {
655
+ ${CLIENT_MENU_ITEMS.map((item, index) => {
508
656
  const active = item.href === current || (current === '' && item.href === 'index.html');
657
+ const previousGroup = CLIENT_MENU_ITEMS[index - 1]?.group;
509
658
  return `
659
+ ${item.group !== previousGroup ? `<div class="client-menu-section-title">${escapeHtml(item.group)}</div>` : ''}
510
660
  <a class="client-menu-item ${active ? 'active' : ''}" href="${escapeHtml(clientHref(item))}"${clientTargetAttrs(item)}>
511
661
  <span class="client-menu-item-main">
512
- <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>
513
663
  <span class="client-menu-item-desc">${escapeHtml(item.description)}</span>
514
664
  </span>
515
665
  <span class="client-menu-item-label">${escapeHtml(active ? 'Current' : item.label)}</span>
@@ -768,6 +918,7 @@
768
918
  }
769
919
 
770
920
  window.BunnylandUI = {
921
+ alertDialog,
771
922
  bindColorSchemeSelect,
772
923
  bindTagEditor,
773
924
  bindSearchDropdown,
@@ -777,12 +928,15 @@
777
928
  currentColorScheme,
778
929
  escapeHtml,
779
930
  currentTheme,
931
+ confirmDialog,
932
+ credentialsDialog,
780
933
  initClientMenu,
781
934
  initHelp,
782
935
  initTheme,
783
936
  loadConfig,
784
937
  normalizeTags,
785
938
  normalizeTheme,
939
+ promptDialog,
786
940
  registerThemeOption,
787
941
  registerThemeOptions,
788
942
  renderTagEditorTags,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnyland/ui-web",
3
- "version": "0.2.1",
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",