@saltcorn/bootstrap-prompt-theme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,719 @@
1
+ const {
2
+ div,
3
+ text,
4
+ p,
5
+ footer,
6
+ section,
7
+ a,
8
+ style,
9
+ h1,
10
+ ul,
11
+ img,
12
+ li,
13
+ form,
14
+ input,
15
+ nav,
16
+ button,
17
+ i,
18
+ hr,
19
+ } = require("@saltcorn/markup/tags");
20
+ const {
21
+ navbar,
22
+ navbarSolidOnScroll,
23
+ mobileBottomNavBar,
24
+ activeChecker,
25
+ } = require("@saltcorn/markup/layout_utils");
26
+ const renderLayout = require("@saltcorn/markup/layout");
27
+ const db = require("@saltcorn/data/db");
28
+ const Form = require("@saltcorn/data/models/form");
29
+ const Workflow = require("@saltcorn/data/models/workflow");
30
+ const Plugin = require("@saltcorn/data/models/plugin");
31
+ const { renderForm, link } = require("@saltcorn/markup");
32
+ const {
33
+ alert,
34
+ headersInHead,
35
+ headersInBody,
36
+ } = require("@saltcorn/markup/layout_utils");
37
+ const { features, getState } = require("@saltcorn/data/db/state");
38
+ const {
39
+ generateThemeCSS,
40
+ writeOverlayCSS,
41
+ deleteOldOverlays,
42
+ } = require("./generate_theme");
43
+
44
+ const isNode = typeof window === "undefined";
45
+ let hasCapacitor = false;
46
+ try {
47
+ hasCapacitor =
48
+ require("@saltcorn/plugins-loader/stable_versioning").isEngineSatisfied(
49
+ ">=1.1.0-beta.11"
50
+ );
51
+ } catch {
52
+ getState().log(5, "stable_versioning not available, assuming no Capacitor");
53
+ }
54
+
55
+ const _activeChecker = activeChecker
56
+ ? activeChecker
57
+ : (link, currentUrl) => new RegExp(`^${link}(\\/|\\?|#|$)`).test(currentUrl);
58
+
59
+ const blockDispatch = (config) => ({
60
+ pageHeader: ({ title, blurb }) =>
61
+ div(
62
+ h1({ class: "h3 mb-0 mt-2 text-gray-800" }, title),
63
+ blurb && p({ class: "mb-0 text-gray-800" }, blurb)
64
+ ),
65
+ footer: ({ contents }) =>
66
+ div(
67
+ { class: "container" },
68
+ footer(
69
+ { id: "footer" },
70
+ div({ class: "row" }, div({ class: "col-sm-12" }, contents))
71
+ )
72
+ ),
73
+ hero: ({ caption, blurb, cta, backgroundImage }) =>
74
+ section(
75
+ {
76
+ class:
77
+ "jumbotron text-center m-0 bg-info d-flex flex-column justify-content-center",
78
+ },
79
+ div(
80
+ { class: "container" },
81
+ h1({ class: "jumbotron-heading" }, caption),
82
+ p({ class: "lead" }, blurb),
83
+ cta
84
+ ),
85
+ backgroundImage &&
86
+ style(`.jumbotron {
87
+ background-image: url("${backgroundImage}");
88
+ background-size: cover;
89
+ min-height: 75vh !important;
90
+ }`)
91
+ ),
92
+ noBackgroundAtTop: () => true,
93
+ wrapTop: (segment, ix, s) =>
94
+ ["hero", "footer"].includes(segment.type) || segment.noWrapTop
95
+ ? s
96
+ : section(
97
+ {
98
+ class: [
99
+ "page-section",
100
+ ix === 0 && `pt-${config.toppad || 0}`,
101
+ ix === 0 && config.fixedTop && isNode && "mt-5",
102
+ ix === 0 && config.fixedTop && !isNode && "mt-6",
103
+ segment.class,
104
+ segment.invertColor && "bg-primary",
105
+ ],
106
+ style: `${
107
+ segment.bgType === "Color"
108
+ ? `background-color: ${segment.bgColor};`
109
+ : ""
110
+ }`,
111
+ },
112
+ div(
113
+ { class: [config.fluid ? "container-fluid" : "container"] },
114
+ segment.textStyle && segment.textStyle === "h1" ? h1(s) : s
115
+ )
116
+ ),
117
+ });
118
+
119
+ const buildHints = (config = {}) => ({
120
+ cardTitleClass: "m-0 fw-bold d-inline",
121
+ });
122
+
123
+ const renderBody = (title, body, alerts, config, role, req) =>
124
+ renderLayout({
125
+ blockDispatch: blockDispatch(config),
126
+ role,
127
+ req,
128
+ layout:
129
+ typeof body === "string" && config.in_card
130
+ ? { type: "card", title, contents: body }
131
+ : body,
132
+ alerts,
133
+ hints: buildHints(config),
134
+ });
135
+
136
+ const safeSlash = () => (isNode ? "/" : "");
137
+
138
+ const linkPrefix = () =>
139
+ isNode ? "/plugins" : hasCapacitor ? "sc_plugins" : "plugins";
140
+
141
+ const base_public_serve = `${linkPrefix()}/public/bootstrap-prompt-theme${
142
+ features?.version_plugin_serve_path
143
+ ? "@" + require("./package.json").version
144
+ : ""
145
+ }`;
146
+
147
+ const wrapIt = (config, bodyAttr, headers, title, body) => {
148
+ return `<!doctype html>
149
+ <html lang="en" data-bs-theme="${config.mode || "light"}">
150
+ <head>
151
+ <meta charset="utf-8" />
152
+ <meta name="viewport" content="width=device-width, initial-scale=1">
153
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
154
+ <link href="${base_public_serve}/bootstrap.min.css" rel="stylesheet">
155
+ <link rel="stylesheet" href="${base_public_serve}/sidebar-3.css" />
156
+ <link rel="stylesheet" href="${base_public_serve}/${
157
+ config.overlay_file || "overlay.css"
158
+ }" />
159
+ ${headersInHead(headers, config?.mode === "dark")}
160
+ <title>${text(title)}</title>
161
+ </head>
162
+ <body ${bodyAttr}>
163
+ ${body}
164
+ ${
165
+ features && features.deep_public_plugin_serve
166
+ ? `<link rel="stylesheet" href="${base_public_serve}/fontawesome/fontawesome.min.css" />`
167
+ : '<script defer src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/js/all.min.js" integrity="sha512-F5QTlBqZlvuBEs9LQPqc1iZv2UMxcVXezbHzomzS6Df4MZMClge/8+gXrKw2fl5ysdk4rWjR0vKS7NNkfymaBQ==" crossorigin="anonymous"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/fontawesome.min.css" integrity="sha512-kJ30H6g4NGhWopgdseRb8wTsyllFUYIx3hiUwmGAkgA9B/JbzUBDQVr2VVlWGde6sdBVOG7oU8AL35ORDuMm8g==" crossorigin="anonymous" />'
168
+ }
169
+ <script src="${safeSlash()}static_assets/${
170
+ db.connectObj.version_tag
171
+ }/jquery-3.6.0.min.js"></script>
172
+ <script src="${base_public_serve}/bootstrap.bundle.min.js"></script>
173
+ ${headersInBody(headers)}
174
+ ${config.colorscheme === "navbar-light" ? navbarSolidOnScroll : ""}
175
+ </body>
176
+ </html>`;
177
+ };
178
+
179
+ const active = (currentUrl, item, originalUrl) =>
180
+ (item.link &&
181
+ (_activeChecker(item.link, currentUrl) ||
182
+ (originalUrl && _activeChecker(item.link, originalUrl)))) ||
183
+ (item.altlinks &&
184
+ item.altlinks.some(
185
+ (l) =>
186
+ _activeChecker(l, currentUrl) ||
187
+ (originalUrl && _activeChecker(l, originalUrl))
188
+ )) ||
189
+ (item.subitems &&
190
+ item.subitems.some(
191
+ (si) =>
192
+ si.link &&
193
+ (_activeChecker(si.link, currentUrl) ||
194
+ (originalUrl && _activeChecker(si.link, originalUrl)) ||
195
+ (si.altlinks &&
196
+ si.altlinks.some((l) => _activeChecker(l, currentUrl))))
197
+ ));
198
+
199
+ const verticalMenu = ({ menu, currentUrl, originalUrl, brand }) => {
200
+ const brandLogo = a(
201
+ { class: "navbar-brand mt-1 ms-3 mb-2", href: "/" },
202
+ brand.logo &&
203
+ img({
204
+ src: brand.logo,
205
+ width: "30",
206
+ height: "30",
207
+ class: "me-2 d-inline-block align-top",
208
+ alt: "Logo",
209
+ loading: "lazy",
210
+ }),
211
+ brand.name
212
+ );
213
+ const vertNavSubItemsIterator = (subitem) =>
214
+ subitem.type === "Separator"
215
+ ? hr({ class: "mx-4 my-0" })
216
+ : subitem?.subitems
217
+ ? li(
218
+ { class: ["nav-item"] },
219
+ div(
220
+ { class: "dropdown-item btn-group dropend" },
221
+ a(
222
+ {
223
+ type: "button",
224
+ class: "nav-link sublink dropdown-item dropdown-toggle",
225
+ "data-bs-toggle": "dropdown",
226
+ "aria-expanded": "false",
227
+ },
228
+ subitem.label
229
+ ),
230
+ ul(
231
+ { class: "dropdown-menu" },
232
+ subitem?.subitems.map((si1) => li(vertNavSubItemsIterator(si1)))
233
+ )
234
+ )
235
+ )
236
+ : li(
237
+ {
238
+ class: [
239
+ "nav-item",
240
+ active(currentUrl, subitem, originalUrl) && "active",
241
+ ],
242
+ },
243
+ a(
244
+ {
245
+ class: "nav-link sublink",
246
+ href: subitem.link,
247
+ target: subitem.target_blank ? "_blank" : undefined,
248
+ },
249
+ subitem.icon ? i({ class: `fa-fw me-1 ${subitem.icon}` }) : "",
250
+ subitem.label
251
+ )
252
+ );
253
+
254
+ let items = [];
255
+ menu.forEach((m, ix) => {
256
+ if (m.items && m.items.length > 0) {
257
+ m.items.forEach((item, ix1) => {
258
+ if (item.location === "Mobile Bottom") return;
259
+ if (item.subitems) {
260
+ items.push(
261
+ li(
262
+ {
263
+ class: [
264
+ "nav-item",
265
+ active(currentUrl, item, originalUrl) && "active",
266
+ ],
267
+ },
268
+ a(
269
+ {
270
+ href: `#menuCollapse${ix}_${ix1}`,
271
+ "aria-expanded": false,
272
+ class: "dropdown-toggle nav-link",
273
+ ...(features && features.bootstrap5
274
+ ? { "data-bs-toggle": "collapse" }
275
+ : { "data-toggle": "collapse" }),
276
+ },
277
+ item.icon ? i({ class: `fa-fw me-1 ${item.icon}` }) : "",
278
+ item.label
279
+ ),
280
+ ul(
281
+ {
282
+ class: [
283
+ active(currentUrl, item, originalUrl)
284
+ ? "collapse.show"
285
+ : "collapse",
286
+ "list-unstyled",
287
+ ],
288
+ id: `menuCollapse${ix}_${ix1}`,
289
+ },
290
+ item.subitems.map(vertNavSubItemsIterator)
291
+ )
292
+ )
293
+ );
294
+ } else if (item.link)
295
+ items.push(
296
+ li(
297
+ {
298
+ class: [
299
+ "nav-item",
300
+ active(currentUrl, item, originalUrl) && "active",
301
+ ],
302
+ },
303
+ a(
304
+ {
305
+ class: "nav-link",
306
+ href: item.link,
307
+ target: item.target_blank ? "_blank" : undefined,
308
+ },
309
+ item.icon ? i({ class: `fa-fw me-1 ${item.icon}` }) : "",
310
+ item.label
311
+ )
312
+ )
313
+ );
314
+ else if (item.type === "Separator")
315
+ items.push(hr({ class: "mx-4 my-0" }));
316
+ else if (item.type === "Search")
317
+ items.push(
318
+ li(
319
+ form(
320
+ { action: "/search", class: "menusearch", method: "get" },
321
+ div(
322
+ { class: "input-group search-bar" },
323
+ input({
324
+ type: "search",
325
+ class: "form-control search-bar ps-2 hasbl",
326
+ placeholder: item.label,
327
+ id: "inputq",
328
+ name: "q",
329
+ "aria-label": "Search",
330
+ "aria-describedby": "button-search-submit",
331
+ }),
332
+ button(
333
+ {
334
+ class: "btn btn-outline-secondary search-bar",
335
+ type: "submit",
336
+ },
337
+ i({ class: "fas fa-search" })
338
+ )
339
+ )
340
+ )
341
+ )
342
+ );
343
+ });
344
+ }
345
+ });
346
+ const toggler =
347
+ hr({ class: "mx-4 my-0" }) +
348
+ div(
349
+ { class: "text-center" },
350
+ button({
351
+ class: "rounded-circle border-0",
352
+ id: "sidebarToggle",
353
+ "data-sidebar-toggler": true,
354
+ onclick: "$('#wrapper').toggleClass('narrowed')",
355
+ })
356
+ );
357
+ return (
358
+ brandLogo +
359
+ ul({ class: "navbar-nav list-unstyled components" }, items) +
360
+ toggler
361
+ );
362
+ };
363
+
364
+ const authBrand = (config, { name, logo }) =>
365
+ logo
366
+ ? `<img class="mb-4" src="${logo}" alt="Logo" width="72" height="72">`
367
+ : "";
368
+
369
+ const menuWrap = ({
370
+ brand,
371
+ menu,
372
+ config,
373
+ currentUrl,
374
+ originalUrl,
375
+ body,
376
+ req,
377
+ }) => {
378
+ const colschm = (config.colorscheme || "").split(" ");
379
+ const navbarCol = colschm[0];
380
+ const bg = colschm[1];
381
+ const txt = (colschm[0] || "").includes("dark") ? "text-light" : "";
382
+
383
+ const mobileNav = mobileBottomNavBar
384
+ ? mobileBottomNavBar(currentUrl, menu, bg, txt)
385
+ : "";
386
+ const role = !req ? 1 : req.user ? req.user.role_id : 100;
387
+ if ((config.menu_style === "No Menu" && role > 1) || (!menu && !brand))
388
+ return div({ id: "wrapper" }, div({ id: "page-inner-content" }, body));
389
+ else if (config.menu_style === "Side Navbar" && isNode) {
390
+ return (
391
+ navbar(brand, menu, currentUrl, { class: "d-md-none", ...config }) +
392
+ div(
393
+ { id: "wrapper", class: "d-flex with-sidebar" },
394
+ nav(
395
+ {
396
+ class: [
397
+ "d-none d-md-flex flex-column align-center d-print-none",
398
+ navbarCol,
399
+ bg,
400
+ txt,
401
+ ],
402
+ id: "sidebar",
403
+ },
404
+ verticalMenu({ brand, menu, currentUrl, originalUrl })
405
+ ),
406
+ div(
407
+ { id: "content-wrapper", class: "d-flex flex-column" },
408
+ div({ id: "content" }, div({ id: "page-inner-content" }, body))
409
+ )
410
+ ) +
411
+ mobileNav
412
+ );
413
+ } else
414
+ return (
415
+ div(
416
+ { id: "wrapper" },
417
+ navbar(brand, menu, currentUrl, config),
418
+ div({ id: "page-inner-content" }, body)
419
+ ) + mobileNav
420
+ );
421
+ };
422
+
423
+ const layout = (config) => ({
424
+ hints: buildHints(config),
425
+ renderBody: ({ title, body, alerts, role, req }) =>
426
+ renderBody(title, body, alerts, config, role, req),
427
+ wrap: ({
428
+ title,
429
+ menu,
430
+ brand,
431
+ alerts,
432
+ currentUrl,
433
+ originalUrl,
434
+ body,
435
+ headers,
436
+ role,
437
+ req,
438
+ bodyClass,
439
+ requestFluidLayout,
440
+ }) =>
441
+ wrapIt(
442
+ config,
443
+ `id="page-top" class="${bodyClass || ""}"`,
444
+ headers,
445
+ title,
446
+ menuWrap({
447
+ brand,
448
+ menu,
449
+ config,
450
+ currentUrl,
451
+ originalUrl,
452
+ body: renderBody(
453
+ title,
454
+ body,
455
+ alerts,
456
+ requestFluidLayout ? { ...config, fluid: true } : config,
457
+ role,
458
+ req
459
+ ),
460
+ req,
461
+ })
462
+ ),
463
+ authWrap: ({
464
+ title,
465
+ alerts,
466
+ form,
467
+ afterForm,
468
+ headers,
469
+ brand,
470
+ csrfToken,
471
+ authLinks,
472
+ bodyClass,
473
+ req,
474
+ }) =>
475
+ wrapIt(
476
+ config,
477
+ `class="text-center ${bodyClass || ""}"`,
478
+ headers,
479
+ title,
480
+ `
481
+ <div class="form-signin">
482
+ ${alerts.map((a) => alert(a.type, a.msg)).join("")}
483
+ ${authBrand(config, brand)}
484
+ <h3>${title}</h3>
485
+ ${renderForm(formModify(form), csrfToken)}
486
+ ${renderAuthLinks(authLinks, req)}
487
+ ${afterForm}
488
+ <style>
489
+ html, body { min-height: 100%; }
490
+ body {
491
+ display: flex;
492
+ align-items: center;
493
+ justify-content: center;
494
+ padding-top: 40px;
495
+ padding-bottom: 40px;
496
+ background-color: #f5f5f5;
497
+ }
498
+ .form-signin {
499
+ width: 100%;
500
+ max-width: 330px;
501
+ padding: 15px;
502
+ margin: 0 auto;
503
+ }
504
+ .form-signin .form-control {
505
+ position: relative;
506
+ box-sizing: border-box;
507
+ height: auto;
508
+ padding: 10px;
509
+ font-size: 16px;
510
+ }
511
+ .form-signin .form-control:focus { z-index: 2; }
512
+ .form-signin input[type="email"] {
513
+ margin-bottom: -1px;
514
+ border-bottom-right-radius: 0;
515
+ border-bottom-left-radius: 0;
516
+ }
517
+ .form-signin input[type="password"] {
518
+ margin-bottom: 10px;
519
+ border-top-left-radius: 0;
520
+ border-top-right-radius: 0;
521
+ }
522
+ </style>
523
+ </div>
524
+ `
525
+ ),
526
+ });
527
+
528
+ const renderAuthLinks = (authLinks, req) => {
529
+ var links = [];
530
+ const __ = req?.__ || ((s) => s);
531
+ if (authLinks.login)
532
+ links.push(link(authLinks.login, __("Already have an account? Login!")));
533
+ if (authLinks.forgot)
534
+ links.push(link(authLinks.forgot, __("Forgot password?")));
535
+ if (authLinks.signup)
536
+ links.push(link(authLinks.signup, __("Create an account!")));
537
+ const meth_links = (authLinks.methods || [])
538
+ .map(({ url, icon, label }) =>
539
+ a(
540
+ { href: url, class: "btn btn-secondary btn-user btn-block" },
541
+ icon || "",
542
+ `&nbsp;Login with ${label}`
543
+ )
544
+ )
545
+ .join("");
546
+ return (
547
+ meth_links + links.map((l) => div({ class: "text-center" }, l)).join("")
548
+ );
549
+ };
550
+
551
+ const formModify = (form) => {
552
+ form.formStyle = "vert";
553
+ form.submitButtonClass = "btn-primary btn-user btn-block";
554
+ return form;
555
+ };
556
+
557
+ const configuration_workflow = () =>
558
+ new Workflow({
559
+ onDone: async (context) => {
560
+ return {
561
+ context,
562
+ cleanup: async () => {
563
+ if (context.overlay_file)
564
+ await deleteOldOverlays(context.overlay_file);
565
+ },
566
+ };
567
+ },
568
+ onStepSuccess: async (step, ctx) => {
569
+ const prompt = ctx?.prompt;
570
+ if (!prompt) return;
571
+ try {
572
+ let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
573
+ if (!plugin)
574
+ plugin = await Plugin.findOne({
575
+ name: "@saltcorn/bootstrap-prompt-theme",
576
+ });
577
+ if (plugin?.configuration?.last_applied_prompt === prompt) return;
578
+ const css = await generateThemeCSS(prompt);
579
+ if (css) {
580
+ ctx.overlay_css = css;
581
+ ctx.last_applied_prompt = prompt;
582
+ const filename = await writeOverlayCSS(css);
583
+ ctx.overlay_file = filename;
584
+ }
585
+ } catch (error) {
586
+ const msg = error.message || "unknown error";
587
+ getState().log(2, `bootstrap-prompt-theme onLoad failed: ${msg}`);
588
+ }
589
+ },
590
+ steps: [
591
+ {
592
+ name: "Theme",
593
+ form: async (ctx) => {
594
+ return new Form({
595
+ fields: [
596
+ {
597
+ name: "prompt",
598
+ label: "Theme prompt",
599
+ sublabel:
600
+ "Describe the visual style you want (LLM generation coming soon)",
601
+ type: "String",
602
+ fieldview: "textarea",
603
+ },
604
+ {
605
+ name: "in_card",
606
+ label: "Default content in card?",
607
+ type: "Bool",
608
+ required: true,
609
+ },
610
+ {
611
+ name: "menu_style",
612
+ label: "Menu style",
613
+ type: "String",
614
+ required: true,
615
+ attributes: {
616
+ inline: true,
617
+ options: ["Top Navbar", "Side Navbar", "No Menu"],
618
+ },
619
+ },
620
+ {
621
+ name: "colorscheme",
622
+ label: "Navbar color scheme",
623
+ type: "String",
624
+ required: true,
625
+ default: "navbar-light",
626
+ attributes: {
627
+ options: [
628
+ { name: "navbar-dark bg-dark", label: "Dark" },
629
+ { name: "navbar-dark bg-primary", label: "Dark Primary" },
630
+ {
631
+ name: "navbar-dark bg-secondary",
632
+ label: "Dark Secondary",
633
+ },
634
+ { name: "navbar-light bg-light", label: "Light" },
635
+ { name: "navbar-light bg-white", label: "White" },
636
+ { name: "navbar-light", label: "Transparent Light" },
637
+ ],
638
+ },
639
+ },
640
+ {
641
+ name: "fixedTop",
642
+ label: "Navbar Fixed Top",
643
+ type: "Bool",
644
+ required: true,
645
+ },
646
+ {
647
+ name: "toppad",
648
+ label: "Top padding",
649
+ sublabel: "0-5 depending on Navbar height and configuration",
650
+ type: "Integer",
651
+ required: true,
652
+ default: 2,
653
+ attributes: { max: 5, min: 0 },
654
+ },
655
+ {
656
+ name: "fluid",
657
+ label: "Fluid full-width container",
658
+ type: "Bool",
659
+ },
660
+ {
661
+ name: "mode",
662
+ label: "Mode",
663
+ type: "String",
664
+ required: true,
665
+ default: "light",
666
+ attributes: {
667
+ options: [
668
+ { name: "light", label: "Light" },
669
+ { name: "dark", label: "Dark" },
670
+ ],
671
+ },
672
+ },
673
+ ],
674
+ });
675
+ },
676
+ },
677
+ ],
678
+ });
679
+
680
+ module.exports = {
681
+ sc_plugin_api_version: 1,
682
+ plugin_name: "bootstrap-prompt-theme",
683
+ dependencies: ["@saltcorn/large-language-model"],
684
+ layout,
685
+ configuration_workflow,
686
+ onLoad: async (configuration) => {
687
+ if (!configuration?.overlay_css) {
688
+ getState().log(
689
+ 5,
690
+ "bootstrap-prompt-theme onLoad: no overlay_css in configuration"
691
+ );
692
+ return;
693
+ }
694
+ if (!configuration?.overlay_file) {
695
+ getState().log(
696
+ 5,
697
+ "bootstrap-prompt-theme onLoad: no overlay_file in configuration"
698
+ );
699
+ return;
700
+ }
701
+ try {
702
+ const { access } = require("fs").promises;
703
+ const { join } = require("path");
704
+ const dest = join(__dirname, "public", configuration.overlay_file);
705
+ try {
706
+ await access(dest);
707
+ } catch {
708
+ await writeOverlayCSS(
709
+ configuration.overlay_css,
710
+ configuration.overlay_file
711
+ );
712
+ }
713
+ } catch (error) {
714
+ const msg = error.message || "Failed to write overlay CSS";
715
+ getState().log(2, `bootstrap-prompt-theme onLoad failed: ${msg}`);
716
+ }
717
+ },
718
+ ready_for_mobile: true,
719
+ };