@bunnyland/ui-web 0.2.1 → 0.2.3
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/assets/bunnyland-api.js +10 -6
- package/assets/bunnyland-ui.css +137 -3
- package/assets/bunnyland-ui.js +164 -10
- package/package.json +1 -1
package/assets/bunnyland-api.js
CHANGED
|
@@ -196,15 +196,19 @@
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
async function promptPlayerAuth(base) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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.
|
|
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
|
}
|
package/assets/bunnyland-ui.css
CHANGED
|
@@ -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);
|
|
@@ -645,6 +649,7 @@ input, select, textarea, button { font-family: var(--bl-font-mono); }
|
|
|
645
649
|
|
|
646
650
|
input[type=file],
|
|
647
651
|
input[type=text],
|
|
652
|
+
input[type=password],
|
|
648
653
|
input[type=number],
|
|
649
654
|
select,
|
|
650
655
|
textarea {
|
|
@@ -662,10 +667,14 @@ input[type=file] {
|
|
|
662
667
|
}
|
|
663
668
|
|
|
664
669
|
input:focus, select:focus, textarea:focus {
|
|
665
|
-
outline: none;
|
|
666
670
|
border-color: var(--bl-accent);
|
|
667
671
|
}
|
|
668
672
|
|
|
673
|
+
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
|
|
674
|
+
outline: 2px solid var(--bl-accent);
|
|
675
|
+
outline-offset: 2px;
|
|
676
|
+
}
|
|
677
|
+
|
|
669
678
|
button {
|
|
670
679
|
padding: 3px 10px;
|
|
671
680
|
background: var(--bl-surface);
|
|
@@ -681,6 +690,31 @@ button {
|
|
|
681
690
|
button:hover:not(:disabled) { background: var(--bl-surface-hover); }
|
|
682
691
|
button:disabled { opacity: 0.4; cursor: default; }
|
|
683
692
|
|
|
693
|
+
dialog {
|
|
694
|
+
position: fixed;
|
|
695
|
+
z-index: 10000;
|
|
696
|
+
inset: 0;
|
|
697
|
+
width: min(460px, calc(100vw - 24px));
|
|
698
|
+
max-width: calc(100vw - 24px);
|
|
699
|
+
max-height: calc(100dvh - 24px);
|
|
700
|
+
margin: auto;
|
|
701
|
+
padding: 14px;
|
|
702
|
+
overflow: auto;
|
|
703
|
+
border: 1px solid var(--bl-border-control);
|
|
704
|
+
border-radius: var(--bl-radius-md);
|
|
705
|
+
color: var(--bl-text);
|
|
706
|
+
background: var(--bl-bg-strong);
|
|
707
|
+
box-shadow: var(--bl-shadow-popover);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
dialog::backdrop { background: var(--bl-overlay); }
|
|
711
|
+
|
|
712
|
+
/* The Toon client and browser fallbacks can render with the open attribute
|
|
713
|
+
instead of showModal. Keep those dialogs modal-looking. */
|
|
714
|
+
dialog[open]:not(:modal):not(.content-warning-dialog) {
|
|
715
|
+
box-shadow: 0 0 0 100vmax var(--bl-overlay), var(--bl-shadow-popover);
|
|
716
|
+
}
|
|
717
|
+
|
|
684
718
|
.bl-button-primary {
|
|
685
719
|
color: var(--bl-text-inverse);
|
|
686
720
|
background: var(--bl-accent);
|
|
@@ -718,8 +752,6 @@ button:disabled { opacity: 0.4; cursor: default; }
|
|
|
718
752
|
box-shadow: var(--bl-shadow-popover);
|
|
719
753
|
}
|
|
720
754
|
|
|
721
|
-
.bl-auth-dialog::backdrop { background: var(--bl-overlay); }
|
|
722
|
-
|
|
723
755
|
.bl-auth-form,
|
|
724
756
|
.bl-auth-form label {
|
|
725
757
|
display: grid;
|
|
@@ -743,6 +775,72 @@ button:disabled { opacity: 0.4; cursor: default; }
|
|
|
743
775
|
gap: 8px;
|
|
744
776
|
}
|
|
745
777
|
|
|
778
|
+
.bl-dialog {
|
|
779
|
+
width: min(460px, calc(100vw - 24px));
|
|
780
|
+
max-height: calc(100dvh - 24px);
|
|
781
|
+
overflow: hidden;
|
|
782
|
+
padding: 0;
|
|
783
|
+
border: 1px solid var(--bl-border-control);
|
|
784
|
+
border-radius: var(--bl-radius-md);
|
|
785
|
+
color: var(--bl-text);
|
|
786
|
+
background: var(--bl-bg-strong);
|
|
787
|
+
box-shadow: var(--bl-shadow-popover);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.bl-dialog-form {
|
|
791
|
+
display: flex;
|
|
792
|
+
flex-direction: column;
|
|
793
|
+
max-height: calc(100dvh - 24px);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.bl-dialog-header,
|
|
797
|
+
.bl-dialog-body,
|
|
798
|
+
.bl-dialog-actions {
|
|
799
|
+
padding: 14px;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.bl-dialog-header {
|
|
803
|
+
border-bottom: 1px solid var(--bl-border);
|
|
804
|
+
background: var(--bl-bg);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.bl-dialog-header h2 {
|
|
808
|
+
margin: 0;
|
|
809
|
+
color: var(--bl-text);
|
|
810
|
+
font-size: var(--bl-text-xl);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.bl-dialog-body {
|
|
814
|
+
display: grid;
|
|
815
|
+
gap: 12px;
|
|
816
|
+
overflow-y: auto;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.bl-dialog-message {
|
|
820
|
+
margin: 0;
|
|
821
|
+
color: var(--bl-text-soft);
|
|
822
|
+
font-size: var(--bl-text-md);
|
|
823
|
+
line-height: 1.5;
|
|
824
|
+
overflow-wrap: anywhere;
|
|
825
|
+
white-space: pre-wrap;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.bl-dialog-field {
|
|
829
|
+
display: grid;
|
|
830
|
+
gap: 5px;
|
|
831
|
+
font-size: var(--bl-text-sm);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.bl-dialog-field input { width: 100%; }
|
|
835
|
+
|
|
836
|
+
.bl-dialog-actions {
|
|
837
|
+
display: flex;
|
|
838
|
+
justify-content: flex-end;
|
|
839
|
+
gap: 8px;
|
|
840
|
+
border-top: 1px solid var(--bl-border);
|
|
841
|
+
background: var(--bl-bg);
|
|
842
|
+
}
|
|
843
|
+
|
|
746
844
|
#toolbar {
|
|
747
845
|
display: flex;
|
|
748
846
|
flex-direction: column;
|
|
@@ -910,6 +1008,19 @@ button:disabled { opacity: 0.4; cursor: default; }
|
|
|
910
1008
|
padding: 6px;
|
|
911
1009
|
}
|
|
912
1010
|
|
|
1011
|
+
.client-menu-section-title {
|
|
1012
|
+
padding: 10px 10px 3px;
|
|
1013
|
+
color: var(--bl-accent-strong);
|
|
1014
|
+
font-size: var(--bl-text-xs);
|
|
1015
|
+
font-weight: bold;
|
|
1016
|
+
letter-spacing: 0.5px;
|
|
1017
|
+
text-transform: uppercase;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.client-menu-section-title:first-child {
|
|
1021
|
+
padding-top: 4px;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
913
1024
|
.client-menu-item {
|
|
914
1025
|
display: grid;
|
|
915
1026
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
@@ -952,6 +1063,18 @@ button:disabled { opacity: 0.4; cursor: default; }
|
|
|
952
1063
|
margin-left: 6px;
|
|
953
1064
|
}
|
|
954
1065
|
|
|
1066
|
+
.client-menu-recommended {
|
|
1067
|
+
display: inline-block;
|
|
1068
|
+
margin-left: 7px;
|
|
1069
|
+
padding: 1px 5px;
|
|
1070
|
+
border: 1px solid var(--bl-ok);
|
|
1071
|
+
border-radius: var(--bl-radius-sm);
|
|
1072
|
+
color: var(--bl-ok);
|
|
1073
|
+
font-size: var(--bl-text-xs);
|
|
1074
|
+
font-weight: normal;
|
|
1075
|
+
vertical-align: middle;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
955
1078
|
.client-menu-item-desc {
|
|
956
1079
|
color: var(--bl-text-soft);
|
|
957
1080
|
font-size: var(--bl-text-sm);
|
|
@@ -1589,6 +1712,17 @@ textarea {
|
|
|
1589
1712
|
min-height: 30px;
|
|
1590
1713
|
}
|
|
1591
1714
|
|
|
1715
|
+
:is(.bl-page-welcome, .bl-page-toon, .bl-page-character, .bl-page-web-tui, .bl-page-web-repl) {
|
|
1716
|
+
--bl-text-xs: 12px;
|
|
1717
|
+
--bl-text-sm: 14px;
|
|
1718
|
+
--bl-text-md: 16px;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
:is(.bl-page-welcome, .bl-page-toon, .bl-page-character, .bl-page-web-tui, .bl-page-web-repl)
|
|
1722
|
+
:is(button, input[type=file], input[type=text], input[type=number], select, .button-link) {
|
|
1723
|
+
min-height: 44px;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1592
1726
|
.client-menu-backdrop {
|
|
1593
1727
|
padding: 12px;
|
|
1594
1728
|
}
|
package/assets/bunnyland-ui.js
CHANGED
|
@@ -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,
|