@firstpick/pi-package-webui 0.3.3 → 0.3.4
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/package.json +1 -1
- package/public/app.js +56 -0
- package/public/index.html +10 -2
- package/public/styles.css +96 -0
- package/tests/mobile-static.test.mjs +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-package-webui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Pi Web UI companion package with a local browser UI CLI plus /webui-start and /webui-status commands.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/Firstp1ck/npm-packages/tree/main/pi-package-webui#readme",
|
package/public/app.js
CHANGED
|
@@ -83,6 +83,8 @@ const elements = {
|
|
|
83
83
|
setThinkingButton: $("#setThinkingButton"),
|
|
84
84
|
thinkingVisibilityToggle: $("#thinkingVisibilityToggle"),
|
|
85
85
|
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
86
|
+
terminalTabsLayoutSelect: $("#terminalTabsLayoutSelect"),
|
|
87
|
+
terminalTabsLayoutStatus: $("#terminalTabsLayoutStatus"),
|
|
86
88
|
themeSelect: $("#themeSelect"),
|
|
87
89
|
backgroundInput: $("#backgroundInput"),
|
|
88
90
|
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
@@ -251,6 +253,7 @@ let blockedTabNotificationPermissionRequested = false;
|
|
|
251
253
|
let blockedTabNotificationFallbackNoted = false;
|
|
252
254
|
let agentDoneNotificationsEnabled = false;
|
|
253
255
|
let thinkingOutputVisible = true;
|
|
256
|
+
let terminalTabsLayout = "top";
|
|
254
257
|
let webuiSettings = {};
|
|
255
258
|
let busyPromptBehavior = "followUp";
|
|
256
259
|
let autocompleteMaxVisible = 12;
|
|
@@ -296,6 +299,7 @@ const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
|
296
299
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
297
300
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
298
301
|
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
302
|
+
const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
|
|
299
303
|
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
300
304
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
301
305
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
@@ -325,6 +329,8 @@ const LONG_INPUT_ATTACHMENT_MIME_TYPE = "text/plain";
|
|
|
325
329
|
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
326
330
|
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
327
331
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
332
|
+
const TERMINAL_TABS_LAYOUTS = new Set(["top", "left"]);
|
|
333
|
+
const TERMINAL_TABS_LAYOUT_LABELS = { top: "Top bar", left: "Left sidebar" };
|
|
328
334
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
329
335
|
const SIDE_PANEL_OVERLAY_QUERY = "(max-width: 1050px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
330
336
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
@@ -789,6 +795,26 @@ function persistThinkingOutputVisible(visible) {
|
|
|
789
795
|
}
|
|
790
796
|
}
|
|
791
797
|
|
|
798
|
+
function normalizeTerminalTabsLayout(value) {
|
|
799
|
+
return TERMINAL_TABS_LAYOUTS.has(value) ? value : "top";
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function readStoredTerminalTabsLayout() {
|
|
803
|
+
try {
|
|
804
|
+
return normalizeTerminalTabsLayout(localStorage.getItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY));
|
|
805
|
+
} catch {
|
|
806
|
+
return "top";
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function persistTerminalTabsLayout(layout) {
|
|
811
|
+
try {
|
|
812
|
+
localStorage.setItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY, normalizeTerminalTabsLayout(layout));
|
|
813
|
+
} catch {
|
|
814
|
+
// Ignore storage failures; the layout control should still work for this page load.
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
792
818
|
function readStoredToolOutputExpanded() {
|
|
793
819
|
try {
|
|
794
820
|
return localStorage.getItem(TOOL_OUTPUT_EXPANDED_STORAGE_KEY) === "1";
|
|
@@ -816,6 +842,30 @@ function renderThinkingVisibilityToggle() {
|
|
|
816
842
|
if (elements.thinkingVisibilityStatus) elements.thinkingVisibilityStatus.textContent = thinkingVisibilityStatusText();
|
|
817
843
|
}
|
|
818
844
|
|
|
845
|
+
function terminalTabsLayoutStatusText(layout = terminalTabsLayout) {
|
|
846
|
+
return TERMINAL_TABS_LAYOUT_LABELS[normalizeTerminalTabsLayout(layout)] || TERMINAL_TABS_LAYOUT_LABELS.top;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function renderTerminalTabsLayoutControl() {
|
|
850
|
+
const layout = normalizeTerminalTabsLayout(terminalTabsLayout);
|
|
851
|
+
if (elements.terminalTabsLayoutSelect) elements.terminalTabsLayoutSelect.value = layout;
|
|
852
|
+
if (elements.terminalTabsLayoutStatus) elements.terminalTabsLayoutStatus.textContent = terminalTabsLayoutStatusText(layout);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function setTerminalTabsLayout(layout, { persist = true, announce = false } = {}) {
|
|
856
|
+
const next = normalizeTerminalTabsLayout(layout);
|
|
857
|
+
terminalTabsLayout = next;
|
|
858
|
+
document.body.classList.toggle("terminal-tabs-left", next === "left");
|
|
859
|
+
if (next === "left" && mobileTabsExpanded) setMobileTabsExpanded(false);
|
|
860
|
+
if (persist) persistTerminalTabsLayout(next);
|
|
861
|
+
renderTerminalTabsLayoutControl();
|
|
862
|
+
if (announce) addEvent(`terminal tabs layout changed to ${terminalTabsLayoutStatusText(next).toLowerCase()}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function restoreTerminalTabsLayoutSetting() {
|
|
866
|
+
setTerminalTabsLayout(readStoredTerminalTabsLayout(), { persist: false });
|
|
867
|
+
}
|
|
868
|
+
|
|
819
869
|
function removeStreamingThinkingBubble() {
|
|
820
870
|
streamThinkingBubble?.remove();
|
|
821
871
|
streamThinkingBubble = null;
|
|
@@ -12831,6 +12881,11 @@ if (elements.thinkingVisibilityToggle) {
|
|
|
12831
12881
|
setThinkingOutputVisible(elements.thinkingVisibilityToggle.checked, { announce: true });
|
|
12832
12882
|
});
|
|
12833
12883
|
}
|
|
12884
|
+
if (elements.terminalTabsLayoutSelect) {
|
|
12885
|
+
elements.terminalTabsLayoutSelect.addEventListener("change", () => {
|
|
12886
|
+
setTerminalTabsLayout(elements.terminalTabsLayoutSelect.value, { announce: true });
|
|
12887
|
+
});
|
|
12888
|
+
}
|
|
12834
12889
|
elements.toggleSidePanelButton.addEventListener("click", () => {
|
|
12835
12890
|
setSidePanelCollapsed(true);
|
|
12836
12891
|
});
|
|
@@ -13142,6 +13197,7 @@ initializeThemes().catch((error) => {
|
|
|
13142
13197
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
13143
13198
|
restoreAgentDoneNotificationsSetting();
|
|
13144
13199
|
restoreThinkingVisibilitySetting();
|
|
13200
|
+
restoreTerminalTabsLayoutSetting();
|
|
13145
13201
|
restoreToolOutputExpansionSetting();
|
|
13146
13202
|
restoreSidePanelSectionState();
|
|
13147
13203
|
bindSidePanelSectionToggles();
|
package/public/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
13
13
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
14
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
15
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
15
|
+
<link rel="stylesheet" href="/styles.css?v=36" />
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<button id="sidePanelExpandButton" class="side-panel-expand-button" type="button" aria-controls="sidePanel" aria-expanded="false" aria-label="Expand side panel" title="Expand side panel">
|
|
@@ -294,6 +294,14 @@
|
|
|
294
294
|
</span>
|
|
295
295
|
</label>
|
|
296
296
|
</div>
|
|
297
|
+
<div class="control-field terminal-tabs-layout-control-field">
|
|
298
|
+
<label for="terminalTabsLayoutSelect">Tabs layout</label>
|
|
299
|
+
<select id="terminalTabsLayoutSelect" title="Terminal tabs layout" aria-describedby="terminalTabsLayoutStatus">
|
|
300
|
+
<option value="top">Top bar</option>
|
|
301
|
+
<option value="left">Left sidebar</option>
|
|
302
|
+
</select>
|
|
303
|
+
<div id="terminalTabsLayoutStatus" class="terminal-tabs-layout-status toggle-control-hint">Top bar</div>
|
|
304
|
+
</div>
|
|
297
305
|
<div class="control-field">
|
|
298
306
|
<label for="themeSelect">Theme</label>
|
|
299
307
|
<select id="themeSelect" title="Theme"></select>
|
|
@@ -541,6 +549,6 @@
|
|
|
541
549
|
</form>
|
|
542
550
|
</dialog>
|
|
543
551
|
|
|
544
|
-
<script type="module" src="/app.js?v=
|
|
552
|
+
<script type="module" src="/app.js?v=37"></script>
|
|
545
553
|
</body>
|
|
546
554
|
</html>
|
package/public/styles.css
CHANGED
|
@@ -1494,6 +1494,102 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
1494
1494
|
box-shadow: 0 0 1rem rgba(245, 194, 231, 0.20);
|
|
1495
1495
|
}
|
|
1496
1496
|
|
|
1497
|
+
@media (min-width: 721px) {
|
|
1498
|
+
body.terminal-tabs-left .chat-panel {
|
|
1499
|
+
display: grid;
|
|
1500
|
+
grid-template-columns: clamp(13rem, 18vw, 19rem) minmax(0, 1fr);
|
|
1501
|
+
grid-template-rows: auto minmax(0, 1fr) auto auto auto auto auto;
|
|
1502
|
+
align-items: stretch;
|
|
1503
|
+
}
|
|
1504
|
+
body.terminal-tabs-left .terminal-tabs-shell {
|
|
1505
|
+
grid-column: 1;
|
|
1506
|
+
grid-row: 1 / -1;
|
|
1507
|
+
flex-direction: column;
|
|
1508
|
+
align-items: stretch;
|
|
1509
|
+
gap: 0.58rem;
|
|
1510
|
+
min-width: 0;
|
|
1511
|
+
min-height: 0;
|
|
1512
|
+
padding: 0.76rem;
|
|
1513
|
+
border-right: 1px solid rgba(180, 190, 254, 0.16);
|
|
1514
|
+
border-bottom: 0;
|
|
1515
|
+
background:
|
|
1516
|
+
linear-gradient(180deg, rgba(var(--ctp-crust-rgb), 0.96), rgba(var(--ctp-base-rgb), 0.82), rgba(var(--ctp-mantle-rgb), 0.92)),
|
|
1517
|
+
radial-gradient(circle at 0% 0%, rgba(245, 194, 231, 0.12), transparent 18rem);
|
|
1518
|
+
box-shadow: inset -1px 0 0 rgba(255,255,255,0.035), 0.45rem 0 1rem rgba(var(--ctp-crust-rgb), 0.18);
|
|
1519
|
+
}
|
|
1520
|
+
body.terminal-tabs-left.side-panel-collapsed .terminal-tabs-shell {
|
|
1521
|
+
padding-right: 0.76rem;
|
|
1522
|
+
}
|
|
1523
|
+
body.terminal-tabs-left .terminal-tabs {
|
|
1524
|
+
flex: 1 1 auto;
|
|
1525
|
+
flex-direction: column;
|
|
1526
|
+
align-items: stretch;
|
|
1527
|
+
min-height: 0;
|
|
1528
|
+
padding-right: 0.08rem;
|
|
1529
|
+
overflow-x: hidden;
|
|
1530
|
+
overflow-y: auto;
|
|
1531
|
+
}
|
|
1532
|
+
body.terminal-tabs-left .terminal-tabs.terminal-tabs-dense {
|
|
1533
|
+
flex-wrap: nowrap;
|
|
1534
|
+
max-height: none;
|
|
1535
|
+
overflow-x: hidden;
|
|
1536
|
+
overflow-y: auto;
|
|
1537
|
+
}
|
|
1538
|
+
body.terminal-tabs-left .terminal-tab,
|
|
1539
|
+
body.terminal-tabs-left .terminal-tabs.terminal-tabs-dense .terminal-tab {
|
|
1540
|
+
flex: 0 0 auto;
|
|
1541
|
+
width: 100%;
|
|
1542
|
+
min-width: 0;
|
|
1543
|
+
max-width: none;
|
|
1544
|
+
}
|
|
1545
|
+
body.terminal-tabs-left .terminal-tab-group-menu {
|
|
1546
|
+
--terminal-left-dropdown-bridge: 0.78rem;
|
|
1547
|
+
inset: 0 auto auto 100%;
|
|
1548
|
+
width: clamp(13rem, 18vw, 20rem);
|
|
1549
|
+
min-width: 13rem;
|
|
1550
|
+
max-width: min(22rem, calc(100vw - 2rem));
|
|
1551
|
+
padding-top: 0;
|
|
1552
|
+
padding-left: var(--terminal-left-dropdown-bridge);
|
|
1553
|
+
}
|
|
1554
|
+
body.terminal-tabs-left .terminal-new-tab-menu.composer-publish-menu {
|
|
1555
|
+
width: 100%;
|
|
1556
|
+
}
|
|
1557
|
+
body.terminal-tabs-left .terminal-new-tab-button,
|
|
1558
|
+
body.terminal-tabs-left .terminal-close-all-button {
|
|
1559
|
+
width: 100%;
|
|
1560
|
+
justify-content: flex-start;
|
|
1561
|
+
text-align: left;
|
|
1562
|
+
}
|
|
1563
|
+
body.terminal-tabs-left .terminal-new-tab-menu .composer-publish-menu-panel {
|
|
1564
|
+
--terminal-left-dropdown-bridge: 0.78rem;
|
|
1565
|
+
inset: 0 auto auto 100%;
|
|
1566
|
+
width: clamp(12rem, 16vw, 18rem);
|
|
1567
|
+
min-width: 12rem;
|
|
1568
|
+
padding-top: 0;
|
|
1569
|
+
padding-left: var(--terminal-left-dropdown-bridge);
|
|
1570
|
+
}
|
|
1571
|
+
body.terminal-tabs-left .terminal-close-all-button {
|
|
1572
|
+
margin-top: auto;
|
|
1573
|
+
}
|
|
1574
|
+
body.terminal-tabs-left .widget-area,
|
|
1575
|
+
body.terminal-tabs-left .chat,
|
|
1576
|
+
body.terminal-tabs-left .feedback-tray,
|
|
1577
|
+
body.terminal-tabs-left .jump-to-latest-button,
|
|
1578
|
+
body.terminal-tabs-left .statusbar,
|
|
1579
|
+
body.terminal-tabs-left .git-workflow-panel,
|
|
1580
|
+
body.terminal-tabs-left .composer {
|
|
1581
|
+
grid-column: 2;
|
|
1582
|
+
min-width: 0;
|
|
1583
|
+
}
|
|
1584
|
+
body.terminal-tabs-left .widget-area { grid-row: 1; }
|
|
1585
|
+
body.terminal-tabs-left .chat { grid-row: 2; }
|
|
1586
|
+
body.terminal-tabs-left .feedback-tray { grid-row: 3; }
|
|
1587
|
+
body.terminal-tabs-left .jump-to-latest-button { grid-row: 4; }
|
|
1588
|
+
body.terminal-tabs-left .statusbar { grid-row: 5; }
|
|
1589
|
+
body.terminal-tabs-left .git-workflow-panel { grid-row: 6; }
|
|
1590
|
+
body.terminal-tabs-left .composer { grid-row: 7; }
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1497
1593
|
.widget-area {
|
|
1498
1594
|
flex: 0 0 auto;
|
|
1499
1595
|
border-bottom: 1px solid rgba(180, 190, 254, 0.16);
|
|
@@ -61,6 +61,8 @@ assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expos
|
|
|
61
61
|
assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
|
|
62
62
|
assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
|
|
63
63
|
assert.match(html, /id="thinkingVisibilityStatus"/, "thinking-output visibility toggle should expose status text");
|
|
64
|
+
assert.match(html, /id="terminalTabsLayoutSelect"[\s\S]*<option value="left">Left sidebar<\/option>/, "side panel controls should expose a terminal-tabs layout selector");
|
|
65
|
+
assert.match(html, /id="terminalTabsLayoutStatus"/, "terminal-tabs layout selector should expose status text");
|
|
64
66
|
assert.match(html, /id="nativeCommandDialog"/, "native slash selector UI should have a dedicated dialog");
|
|
65
67
|
assert.match(html, /id="nativeCommandSearch"[^>]*type="search"/, "native slash selector dialog should expose a filter box");
|
|
66
68
|
assert.match(html, /id="pathPickerCreateNameInput"[^>]*placeholder="New directory name"/, "cwd picker should expose a new-directory name input");
|
|
@@ -253,6 +255,11 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
|
|
|
253
255
|
assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
|
|
254
256
|
assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
|
|
255
257
|
assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
|
|
258
|
+
assert.match(css, /body\.terminal-tabs-left \.chat-panel \{[\s\S]*?grid-template-columns:\s*clamp\(13rem, 18vw, 19rem\) minmax\(0, 1fr\)/, "terminal tabs left layout should split the chat panel into a sidebar and transcript area");
|
|
259
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-tabs-shell \{[\s\S]*?grid-column:\s*1;[\s\S]*?grid-row:\s*1 \/ -1;[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should turn the top tab strip into a vertical sidebar");
|
|
260
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-tabs \{[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should stack tabs vertically");
|
|
261
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-tab-group-menu \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar grouped tab menus should include a hover bridge so they do not vanish between button and dropdown");
|
|
262
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar new-tab dropdown should include a hover bridge so it does not vanish between button and dropdown");
|
|
256
263
|
assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*100% 0 auto auto;[\s\S]*?padding-top:\s*0\.38rem/, "new-tab dropdown should reuse the shared composer panel and open below the tab bar");
|
|
257
264
|
assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-item \{[\s\S]*?color:\s*var\(--ctp-pink\)/, "new-tab dropdown items should reuse shared composer menu items with a tab-specific color");
|
|
258
265
|
assert.match(css, /\.terminal-close-all-button \{[\s\S]*?color:\s*var\(--ctp-red\)/, "close-all tabs action should render as a top-right destructive tab action");
|
|
@@ -334,6 +341,9 @@ assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backg
|
|
|
334
341
|
assert.match(app, /const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background"/, "custom backgrounds should prefer IndexedDB persistence for large images");
|
|
335
342
|
assert.match(app, /const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed"/, "side-panel section collapse state should be persisted in browser storage");
|
|
336
343
|
assert.match(app, /const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications"/, "agent-done notification preference should be persisted in browser storage");
|
|
344
|
+
assert.match(app, /const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout"/, "terminal-tabs layout preference should be persisted in browser storage");
|
|
345
|
+
assert.match(app, /document\.body\.classList\.toggle\("terminal-tabs-left", next === "left"\)/, "terminal-tabs layout should toggle a body class for CSS layout");
|
|
346
|
+
assert.match(app, /terminalTabsLayoutSelect\.addEventListener\("change"/, "terminal-tabs layout selector should update the browser layout immediately");
|
|
337
347
|
assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
|
|
338
348
|
assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
|
|
339
349
|
assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
|