@saltcorn/bootstrap-prompt-theme 0.1.1 → 0.1.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.
@@ -4,7 +4,11 @@
4
4
  "Bash(git -C /home/christian/playground/workspaces/fork/bootstrap-prompt-theme log --oneline -3)",
5
5
  "Bash(git -C /home/christian/playground/workspaces/fork/bootstrap-prompt-theme remote -v)",
6
6
  "Bash(rm -rf .git)",
7
- "Bash(git init:*)"
7
+ "Bash(git init:*)",
8
+ "Bash(find /home/christian/playground/workspaces/fork -name form.js -path */models/*)",
9
+ "Bash(find /home/christian/playground/workspaces/saltcorn/node_modules/@dr.pogodin/csurf -name *.js)",
10
+ "Bash(/home/christian/playground/workspaces/fork/bootstrap-prompt-theme/README.md:*)",
11
+ "Bash(grep -r \"isWorkflow\\\\|xhrSubmit\\\\|submitLabel.*Finish\" /home/christian/playground/workspaces/saltcorn/packages --include=*.ts --include=*.js -l)"
8
12
  ]
9
13
  }
10
14
  }
package/README.md CHANGED
@@ -1,55 +1,48 @@
1
1
  # bootstrap-prompt-theme
2
2
 
3
- A [Saltcorn](https://saltcorn.com/) layout plugin that uses an LLM to generate a CSS overlay on top of standard Bootstrap 5.3.
3
+ A [Saltcorn](https://saltcorn.com/) layout plugin that uses a conversational AI chat to design and refine a CSS overlay on top of standard Bootstrap 5.3.
4
4
 
5
5
  ## Relation to any-bootstrap-theme
6
6
 
7
7
  This plugin is derived from [@saltcorn/any-bootstrap-theme](https://github.com/saltcorn/any-bootstrap-theme). The original plugin offers a large set of configuration variables (colors, card styles, link colors, etc.) that are fed into a Sass build to produce a fully compiled Bootstrap CSS file per theme.
8
8
 
9
- This variant generates a lightweight CSS overlay on top of the Bootstrap 5.3 CSS by prompting an LLM. Bootstrap 5.3 exposes almost everything through CSS custom properties (`--bs-*`), so a well-written overlay can restyle the entire UI without recompiling Sass.
9
+ This variant generates a lightweight CSS overlay by chatting with an LLM. Bootstrap 5.3 exposes almost everything through CSS custom properties (`--bs-*`), so a well-written overlay can restyle the entire UI without recompiling Sass.
10
10
 
11
11
  **Trade-offs:**
12
12
  - Much simpler setup — no Sass toolchain, no build step
13
- - The LLM can produce creative and complex themes from a plain-language description
14
- - Configuration is minimal: a prompt, light/dark mode toggle, and basic layout options
13
+ - Describe or refine your theme in plain language through a persistent chat
14
+ - The AI applies changes immediately without requiring a page save
15
15
 
16
16
  ## Requirements
17
17
 
18
18
  - Saltcorn with the `@saltcorn/large-language-model` plugin installed and configured
19
+ - Saltcorn with the `@saltcorn/agents` plugin installed
19
20
 
20
21
  > **Model recommendation:** Good results have been achieved with `@saltcorn/large-language-model` configured to use **GPT-5.4**.
21
22
 
22
- ## Configuration
23
+ ## Usage
23
24
 
24
25
  1. Go to **Plugins → bootstrap-prompt-theme → Configure**
25
- 2. Enter a theme prompt describing the visual style you want
26
- 3. Save the CSS overlay is generated by the LLM, written to `public/overlay.<timestamp>.css`, and stored in the plugin configuration so it survives restarts
26
+ 2. Use the **AI Theme Chat** to describe the visual style you want
27
+ 3. The AI generates and applies the CSS overlay immediately no Save needed
28
+ 4. Continue the conversation to refine colors, fonts, spacing, dark mode, or anything else
29
+ 5. Use **Clear conversation** to start fresh with a new theme direction
27
30
 
28
- > **Note:** The generation step can take a noticeable amount of time, especially with a detailed prompt. This is normal the save will complete once generation finishes.
31
+ The chat history is persisted across page reloads, so you can return to the configuration page and pick up where you left off.
29
32
 
30
- ### Layout options
33
+ ### Example conversation
31
34
 
32
- | Option | Description |
33
- |---|---|
34
- | Menu style | Top Navbar, Side Navbar, or No Menu |
35
- | Navbar color scheme | Dark, Light, Transparent, etc. |
36
- | Navbar Fixed Top | Pin the navbar to the top of the viewport |
37
- | Top padding | 0–5, adjust to match navbar height |
38
- | Fluid container | Full-width layout |
39
- | Mode | `light` or `dark` — sets `data-bs-theme` on `<html>` |
35
+ ```
36
+ You: A dark cyberpunk theme with neon green accents, deep charcoal
37
+ backgrounds, monospace font, and glowing borders on cards.
40
38
 
41
- ## Example prompt
39
+ AI: [applies CSS overlay]
42
40
 
43
- ```
44
- A clean corporate theme with a deep navy blue (#1a2744) navbar and sidebar,
45
- white card backgrounds with a subtle box shadow, slate-grey body background (#f4f5f7),
46
- and teal (#0d9488) as the primary accent color for buttons, links, and active states.
47
- Use a sans-serif system font stack. In dark mode, use a near-black background (#0f1117)
48
- with dark navy cards (#1a2133) and keep the teal accent.
49
- Rounded corners on buttons (border-radius: 6px) and form inputs.
50
- Table headers should have the navy background with white text.
51
- ```
41
+ You: Make the navbar background slightly lighter and increase the
42
+ card border glow intensity.
52
43
 
44
+ AI: [applies updated CSS overlay]
45
+ ```
53
46
 
54
47
  ## How the overlay works
55
48
 
@@ -57,24 +50,20 @@ The generated file sets CSS custom properties in `:root` that Bootstrap 5.3 read
57
50
 
58
51
  ```css
59
52
  :root {
60
- --my-accent: #0d9488;
61
-
62
- --bs-body-bg: #f4f5f7;
63
- --bs-body-color: #1f2937;
64
- --bs-primary: var(--my-accent);
65
- --bs-link-color: var(--my-accent);
66
- --bs-card-bg: #ffffff;
53
+ --bs-body-bg: #1a1a2e;
54
+ --bs-body-color: #e0e0e0;
55
+ --bs-primary: #00ff88;
56
+ --bs-card-bg: #16213e;
67
57
  /* ... */
68
58
  }
69
59
 
70
60
  [data-bs-theme="dark"] {
71
- --bs-body-bg: #0f1117;
72
- --bs-card-bg: #1a2133;
61
+ --bs-body-bg: #0d0d1a;
73
62
  /* ... */
74
63
  }
75
64
 
76
- .navbar { background-color: #1a2744 !important; }
77
- .btn { border-radius: 6px; }
65
+ .card { border: 1px solid #00ff88; box-shadow: 0 0 8px #00ff88; }
66
+ .navbar { background-color: #16213e; }
78
67
  /* etc. */
79
68
  ```
80
69
 
package/agent-skill.js ADDED
@@ -0,0 +1,240 @@
1
+ const Plugin = require("@saltcorn/data/models/plugin");
2
+ const { pre, code, div } = require("@saltcorn/markup/tags");
3
+ const db = require("@saltcorn/data/db");
4
+ const { getState } = require("@saltcorn/data/db/state");
5
+ const { join } = require("path");
6
+ const { writeFile, readdir, unlink } = require("fs").promises;
7
+
8
+ const writeOverlayCSS = async (css, filename = `overlay.${Date.now()}.css`) => {
9
+ const dest = join(__dirname, "public", filename);
10
+ await writeFile(dest, css);
11
+ return filename;
12
+ };
13
+
14
+ const deleteOldOverlays = async (keepFile) => {
15
+ const publicDir = join(__dirname, "public");
16
+ const files = await readdir(publicDir);
17
+ for (const file of files) {
18
+ if (/^overlay\.\d+\.css$/.test(file) && file !== keepFile)
19
+ await unlink(join(publicDir, file));
20
+ }
21
+ };
22
+
23
+ const SYSTEM_PROMPT = `You are an expert CSS theme designer for Bootstrap 5.3.
24
+ Your task is to generate a complete CSS overlay file that reskins a standard Bootstrap 5.3 app.
25
+
26
+ APPROACH:
27
+ 1. Define theme-specific custom properties in :root (colors, fonts, shadows, etc.)
28
+ 2. Override Bootstrap 5.3 CSS variables in :root using --bs-* tokens for light mode
29
+ 3. Add component-level CSS rules for deeper customisation (navbar, cards, buttons, forms, tables, alerts, badges, modals, pagination, tabs, accordions, etc.)
30
+ 4. Add a dark mode section scoped to [data-bs-theme="dark"] that overrides :root custom properties and --bs-* tokens for dark mode. Component-level rules that depend only on CSS variables will automatically adapt — only add extra component rules under [data-bs-theme="dark"] where variables alone are not sufficient.
31
+
32
+ BOOTSTRAP 5.3 CSS VARIABLES to consider overriding in :root and [data-bs-theme="dark"]:
33
+ --bs-body-bg, --bs-body-color, --bs-secondary-bg, --bs-tertiary-bg,
34
+ --bs-border-color, --bs-primary, --bs-link-color, --bs-link-hover-color,
35
+ --bs-card-bg, --bs-card-border-color, --bs-card-cap-bg,
36
+ --bs-dropdown-bg, --bs-dropdown-border-color, --bs-dropdown-link-color,
37
+ --bs-dropdown-link-hover-bg, --bs-dropdown-link-active-bg,
38
+ --bs-table-color, --bs-table-bg, --bs-table-border-color, --bs-table-hover-bg,
39
+ --bs-modal-bg, --bs-modal-border-color,
40
+ --bs-tooltip-bg, --bs-tooltip-color,
41
+ --bs-body-font-family, --bs-body-font-size
42
+
43
+ COMPONENTS to style: body, navbar (.navbar, .navbar-brand, .nav-link), cards (.card, .card-header, .card-footer, .card-title), buttons (.btn, .btn-primary, .btn-secondary, .btn-danger, .btn-success), forms (.form-control, .form-select, .form-label, .form-check-input), tables (.table, .table th, .table td), alerts (.alert-*), badges (.badge), modals (.modal-content, .modal-header), pagination (.page-link), tabs (.nav-tabs), links (a), headings (h1-h6), code/pre, hr, scrollbar.
44
+
45
+ DARK MODE STRUCTURE:
46
+ :root { /* light mode theme variables and --bs-* overrides */ }
47
+ /* component rules */
48
+ [data-bs-theme="dark"] { /* dark mode theme variables and --bs-* overrides */ }
49
+ /* any extra component rules needed only in dark mode, prefixed with [data-bs-theme="dark"] */
50
+
51
+ CRITICAL RULES — never break these:
52
+ - Never set overflow:hidden or overflow:clip on ANY element — not on .navbar, .card, .card-body, .card-header, .card-footer, section, #wrapper, #page-inner-content, #content-wrapper, .container, .page-section, or any other element. Bootstrap's Popper.js positions dropdowns with position:absolute outside their parent bounds; overflow:hidden on any ancestor will clip them.
53
+ - Always set .navbar { position: relative; z-index: 1030; } to ensure navbar dropdowns render above all page content.
54
+ - Never apply transform, filter, backdrop-filter, will-change, or perspective to .card, .card-body, .card-header, .card-footer, .container, .page-section, section, #wrapper, or any page-level container. These CSS properties create a new stacking context that can rise above the navbar and obscure its open dropdowns.
55
+ - Never set z-index on .card or page content elements — stacking context on page content is what causes navbar dropdowns to be obscured.
56
+ - Do not override --bs-zindex-dropdown, --bs-zindex-fixed, or .dropdown-menu z-index.
57
+ - Dropdowns must always be fully visible and on top of all other page content, including cards, sections, and containers.
58
+
59
+ IMAGES: The user may attach images to the conversation. Use them as design inspiration — extract colors, typography style, spacing feel, or overall mood and translate that into the CSS overlay. If the user attaches an image without further instruction, derive a theme from it. If they describe what they want alongside the image, use the image to inform the details.
60
+
61
+ LAYOUT CONFIG: Besides CSS, you can control structural layout options via set_layout_config:
62
+ - mode: "light" | "dark" — Bootstrap color mode applied to <html data-bs-theme>
63
+ - menu_style: "Top Navbar" | "Side Navbar" | "No Menu"
64
+ - colorscheme: navbar color class pair, e.g. "navbar-dark bg-dark", "navbar-light bg-light", "navbar-dark bg-primary"
65
+ - fixed_top: true | false — fix navbar to top of viewport
66
+ - fluid: true | false — full-width container vs fixed-width
67
+ - top_pad: 0–5 — Bootstrap spacing scale for top padding on page sections
68
+ - in_card: true | false — wrap page body in a Bootstrap card
69
+ Only call set_layout_config when you want to change structural layout, separate from CSS. Call both tools when a request requires both structural and CSS changes.
70
+
71
+ WORKFLOW:
72
+ 1. Call apply_css_overlay with the complete CSS — this is the only way to deliver CSS.
73
+ 2. Optionally call set_layout_config for structural layout changes.
74
+ 3. After all tool calls, reply with one short sentence confirming what changed. Never include CSS in your text reply.`;
75
+
76
+ const findPlugin = async () => {
77
+ return (
78
+ (await Plugin.findOne({ name: "bootstrap-prompt-theme" })) ||
79
+ (await Plugin.findOne({ name: "@saltcorn/bootstrap-prompt-theme" }))
80
+ );
81
+ };
82
+
83
+ const savePluginConfig = async (plugin, patch) => {
84
+ plugin.configuration = { ...(plugin.configuration || {}), ...patch };
85
+ await plugin.upsert();
86
+ getState().processSend({
87
+ refresh_plugin_cfg: plugin.name,
88
+ tenant: db.getTenantSchema(),
89
+ });
90
+ };
91
+
92
+ class GenerateBootstrapThemeSkill {
93
+ static skill_name = "Generate Bootstrap Theme";
94
+
95
+ get skill_label() {
96
+ return "Bootstrap Theme";
97
+ }
98
+
99
+ constructor(cfg) {
100
+ Object.assign(this, cfg);
101
+ }
102
+
103
+ static async configFields() {
104
+ return [];
105
+ }
106
+
107
+ systemPrompt() {
108
+ return SYSTEM_PROMPT;
109
+ }
110
+
111
+ provideTools() {
112
+ return [
113
+ {
114
+ type: "function",
115
+ renderToolCall({ css }) {
116
+ return pre(
117
+ code(css.slice(0, 300) + (css.length > 300 ? "\n..." : ""))
118
+ );
119
+ },
120
+ process: async ({ css }) => {
121
+ const filename = await writeOverlayCSS(css);
122
+ const plugin = await findPlugin();
123
+ if (plugin) {
124
+ await savePluginConfig(plugin, {
125
+ overlay_css: css,
126
+ overlay_file: filename,
127
+ });
128
+ await deleteOldOverlays(filename);
129
+ }
130
+ return { filename };
131
+ },
132
+ renderToolResponse: async ({ filename }) => {
133
+ return div(
134
+ { class: "text-success" },
135
+ `CSS overlay applied: ${filename}`
136
+ );
137
+ },
138
+ function: {
139
+ name: "apply_css_overlay",
140
+ description:
141
+ "Apply a CSS overlay on top of Bootstrap 5.3 to restyle the Saltcorn UI. Call this only when you have CSS ready to apply — not for questions or clarifications.",
142
+ parameters: {
143
+ type: "object",
144
+ required: ["css"],
145
+ properties: {
146
+ css: {
147
+ type: "string",
148
+ description:
149
+ "Complete valid CSS to write as the overlay. Must start with :root { or a comment. No markdown, no code fences.",
150
+ },
151
+ },
152
+ },
153
+ },
154
+ },
155
+ {
156
+ type: "function",
157
+ renderToolCall(params) {
158
+ const entries = Object.entries(params)
159
+ .filter(([, v]) => v !== undefined)
160
+ .map(([k, v]) => `${k}: ${v}`)
161
+ .join(", ");
162
+ return pre(code(entries));
163
+ },
164
+ process: async (params) => {
165
+ const plugin = await findPlugin();
166
+ if (plugin) {
167
+ const allowed = [
168
+ "mode",
169
+ "menu_style",
170
+ "colorscheme",
171
+ "fixed_top",
172
+ "fluid",
173
+ "top_pad",
174
+ "in_card",
175
+ ];
176
+ const patch = Object.fromEntries(
177
+ Object.entries(params).filter(([k]) => allowed.includes(k))
178
+ );
179
+ await savePluginConfig(plugin, patch);
180
+ }
181
+ return { success: true };
182
+ },
183
+ renderToolResponse: async () =>
184
+ div({ class: "text-success" }, "Layout configuration updated."),
185
+ function: {
186
+ name: "set_layout_config",
187
+ description:
188
+ "Set structural layout configuration for the Saltcorn UI. Only pass the parameters you want to change.",
189
+ parameters: {
190
+ type: "object",
191
+ properties: {
192
+ mode: {
193
+ type: "string",
194
+ enum: ["light", "dark"],
195
+ description:
196
+ "Bootstrap color mode applied to <html data-bs-theme>",
197
+ },
198
+ menu_style: {
199
+ type: "string",
200
+ enum: ["Top Navbar", "Side Navbar", "No Menu"],
201
+ description: "Navigation menu style",
202
+ },
203
+ colorscheme: {
204
+ type: "string",
205
+ description:
206
+ "Navbar color class pair, e.g. 'navbar-dark bg-dark', 'navbar-light bg-light', 'navbar-dark bg-primary'",
207
+ },
208
+ fixed_top: {
209
+ type: "boolean",
210
+ description: "Fix the navbar to the top of the viewport",
211
+ },
212
+ fluid: {
213
+ type: "boolean",
214
+ description:
215
+ "Use a full-width container instead of fixed-width",
216
+ },
217
+ top_pad: {
218
+ type: "integer",
219
+ minimum: 0,
220
+ maximum: 5,
221
+ description:
222
+ "Top padding for page sections (Bootstrap spacing scale 0–5)",
223
+ },
224
+ in_card: {
225
+ type: "boolean",
226
+ description: "Wrap page body content in a Bootstrap card",
227
+ },
228
+ },
229
+ },
230
+ },
231
+ },
232
+ ];
233
+ }
234
+ }
235
+
236
+ module.exports = {
237
+ GenerateBootstrapThemeSkill,
238
+ writeOverlayCSS,
239
+ deleteOldOverlays,
240
+ };
package/index.js CHANGED
@@ -28,6 +28,8 @@ const db = require("@saltcorn/data/db");
28
28
  const Form = require("@saltcorn/data/models/form");
29
29
  const Workflow = require("@saltcorn/data/models/workflow");
30
30
  const Plugin = require("@saltcorn/data/models/plugin");
31
+ const WorkflowRun = require("@saltcorn/data/models/workflow_run");
32
+ const File = require("@saltcorn/data/models/file");
31
33
  const { renderForm, link } = require("@saltcorn/markup");
32
34
  const {
33
35
  alert,
@@ -36,10 +38,157 @@ const {
36
38
  } = require("@saltcorn/markup/layout_utils");
37
39
  const { features, getState } = require("@saltcorn/data/db/state");
38
40
  const {
39
- generateThemeCSS,
41
+ GenerateBootstrapThemeSkill,
40
42
  writeOverlayCSS,
41
43
  deleteOldOverlays,
42
- } = require("./generate_theme");
44
+ } = require("./agent-skill");
45
+
46
+ // Layout defaults — will be replaced by tool-generated config in a future version
47
+ const DEFAULT_IN_CARD = false;
48
+ const DEFAULT_MENU_STYLE = "Top Navbar";
49
+ const DEFAULT_COLORSCHEME = "navbar-light bg-light";
50
+ const DEFAULT_FIXED_TOP = false;
51
+ const DEFAULT_TOP_PAD = 2;
52
+ const DEFAULT_FLUID = false;
53
+ const DEFAULT_MODE = "light";
54
+
55
+ const resetThemeRoute = async (req, res) => {
56
+ if (!req.user) return res.status(403).json({ error: "Not authenticated" });
57
+ try {
58
+ let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
59
+ if (!plugin)
60
+ plugin = await Plugin.findOne({
61
+ name: "@saltcorn/bootstrap-prompt-theme",
62
+ });
63
+ if (plugin) {
64
+ await deleteOldOverlays(null);
65
+ plugin.configuration = {
66
+ ...(plugin.configuration || {}),
67
+ overlay_css: null,
68
+ overlay_file: null,
69
+ chat_run_id: null,
70
+ mode: null,
71
+ menu_style: null,
72
+ colorscheme: null,
73
+ fixed_top: null,
74
+ fluid: null,
75
+ top_pad: null,
76
+ in_card: null,
77
+ };
78
+ await plugin.upsert();
79
+ getState().processSend({
80
+ refresh_plugin_cfg: plugin.name,
81
+ tenant: db.getTenantSchema(),
82
+ });
83
+ }
84
+ res.json({ success: "ok" });
85
+ } catch (err) {
86
+ getState().log(
87
+ 2,
88
+ `bootstrap-prompt-theme reset-theme error: ${err.message}`
89
+ );
90
+ res.json({ error: err.message });
91
+ }
92
+ };
93
+
94
+ const chatRoute = async (req, res) => {
95
+ if (!req.user) return res.status(403).json({ error: "Not authenticated" });
96
+ try {
97
+ const { userinput, run_id } = req.body;
98
+ if (!userinput?.trim() && !req.files?.file)
99
+ return res.json({
100
+ success: "ok",
101
+ response: "",
102
+ run_id: run_id || undefined,
103
+ });
104
+
105
+ let agentsLocation =
106
+ getState().plugin_locations?.agents ||
107
+ getState().plugin_locations?.["@saltcorn/agents"];
108
+ if (!agentsLocation)
109
+ return res.json({ error: "Agents plugin not available" });
110
+ const { join } = require("path");
111
+ const {
112
+ process_interaction,
113
+ addToContext,
114
+ wrapSegment,
115
+ saveInteractions,
116
+ } = require(join(agentsLocation, "common.js"));
117
+
118
+ const agentConfig = {
119
+ skills: [{ skill_type: "Generate Bootstrap Theme" }],
120
+ sys_prompt: "",
121
+ };
122
+
123
+ let run;
124
+ if (!run_id || run_id === "undefined" || run_id === "") {
125
+ run = await WorkflowRun.create({
126
+ status: "Running",
127
+ started_by: req.user?.id,
128
+ context: {
129
+ interactions: [],
130
+ html_interactions: [],
131
+ funcalls: {},
132
+ },
133
+ });
134
+ let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
135
+ if (!plugin)
136
+ plugin = await Plugin.findOne({
137
+ name: "@saltcorn/bootstrap-prompt-theme",
138
+ });
139
+ if (plugin) {
140
+ plugin.configuration = {
141
+ ...(plugin.configuration || {}),
142
+ chat_run_id: run.id,
143
+ };
144
+ await plugin.upsert();
145
+ }
146
+ } else {
147
+ run = await WorkflowRun.findOne({ id: +run_id });
148
+ }
149
+
150
+ if (req.files?.file) {
151
+ const rawFiles = Array.isArray(req.files.file)
152
+ ? req.files.file
153
+ : [req.files.file];
154
+ run.context.interactions = run.context.interactions || [];
155
+ for (const rawFile of rawFiles) {
156
+ const file = await File.from_req_files(rawFile, req.user?.id, 100);
157
+ const b64 = await file.get_contents("base64");
158
+ const imageurl = `data:${file.mimetype};base64,${b64}`;
159
+ await getState().functions.llm_add_message.run("image", imageurl, {
160
+ chat: run.context.interactions,
161
+ });
162
+ }
163
+ await saveInteractions(run);
164
+ }
165
+
166
+ const userMsg = wrapSegment(
167
+ `<p>${userinput || "(image)"}</p>`,
168
+ "You",
169
+ true
170
+ );
171
+ await addToContext(run, {
172
+ interactions: [
173
+ ...(run.context.interactions || []),
174
+ ...(userinput?.trim() ? [{ role: "user", content: userinput }] : []),
175
+ ],
176
+ html_interactions: [userMsg],
177
+ });
178
+
179
+ const result = await process_interaction(
180
+ run,
181
+ agentConfig,
182
+ req,
183
+ "Theme Assistant",
184
+ []
185
+ );
186
+ res.json(result?.json || { success: "ok", response: "", run_id: run.id });
187
+ } catch (err) {
188
+ getState().log(2, `bootstrap-prompt-theme chat error: ${err.message}`);
189
+ res.json({ error: err.message });
190
+ }
191
+ };
43
192
 
44
193
  const isNode = typeof window === "undefined";
45
194
  let hasCapacitor = false;
@@ -90,16 +239,19 @@ const blockDispatch = (config) => ({
90
239
  }`)
91
240
  ),
92
241
  noBackgroundAtTop: () => true,
93
- wrapTop: (segment, ix, s) =>
94
- ["hero", "footer"].includes(segment.type) || segment.noWrapTop
242
+ wrapTop: (segment, ix, s) => {
243
+ const topPad = config.top_pad ?? DEFAULT_TOP_PAD;
244
+ const fixedTop = config.fixed_top ?? DEFAULT_FIXED_TOP;
245
+ const fluid = config.fluid ?? DEFAULT_FLUID;
246
+ return ["hero", "footer"].includes(segment.type) || segment.noWrapTop
95
247
  ? s
96
248
  : section(
97
249
  {
98
250
  class: [
99
251
  "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",
252
+ ix === 0 && `pt-${topPad}`,
253
+ ix === 0 && fixedTop && isNode && "mt-5",
254
+ ix === 0 && fixedTop && !isNode && "mt-6",
103
255
  segment.class,
104
256
  segment.invertColor && "bg-primary",
105
257
  ],
@@ -110,10 +262,11 @@ const blockDispatch = (config) => ({
110
262
  }`,
111
263
  },
112
264
  div(
113
- { class: [config.fluid ? "container-fluid" : "container"] },
265
+ { class: [fluid ? "container-fluid" : "container"] },
114
266
  segment.textStyle && segment.textStyle === "h1" ? h1(s) : s
115
267
  )
116
- ),
268
+ );
269
+ },
117
270
  });
118
271
 
119
272
  const buildHints = (config = {}) => ({
@@ -126,7 +279,7 @@ const renderBody = (title, body, alerts, config, role, req) =>
126
279
  role,
127
280
  req,
128
281
  layout:
129
- typeof body === "string" && config.in_card
282
+ typeof body === "string" && (config.in_card ?? DEFAULT_IN_CARD)
130
283
  ? { type: "card", title, contents: body }
131
284
  : body,
132
285
  alerts,
@@ -146,7 +299,7 @@ const base_public_serve = `${linkPrefix()}/public/bootstrap-prompt-theme${
146
299
 
147
300
  const wrapIt = (config, bodyAttr, headers, title, body) => {
148
301
  return `<!doctype html>
149
- <html lang="en" data-bs-theme="${config.mode || "light"}">
302
+ <html lang="en" data-bs-theme="${config.mode || DEFAULT_MODE}">
150
303
  <head>
151
304
  <meta charset="utf-8" />
152
305
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -171,7 +324,11 @@ const wrapIt = (config, bodyAttr, headers, title, body) => {
171
324
  }/jquery-3.6.0.min.js"></script>
172
325
  <script src="${base_public_serve}/bootstrap.bundle.min.js"></script>
173
326
  ${headersInBody(headers)}
174
- ${config.colorscheme === "navbar-light" ? navbarSolidOnScroll : ""}
327
+ ${
328
+ (config.colorscheme || DEFAULT_COLORSCHEME) === "navbar-light"
329
+ ? navbarSolidOnScroll
330
+ : ""
331
+ }
175
332
  </body>
176
333
  </html>`;
177
334
  };
@@ -375,7 +532,10 @@ const menuWrap = ({
375
532
  body,
376
533
  req,
377
534
  }) => {
378
- const colschm = (config.colorscheme || "").split(" ");
535
+ const colorscheme = config.colorscheme || DEFAULT_COLORSCHEME;
536
+ const menuStyle = config.menu_style || DEFAULT_MENU_STYLE;
537
+ const fixedTop = config.fixed_top ?? DEFAULT_FIXED_TOP;
538
+ const colschm = colorscheme.split(" ");
379
539
  const navbarCol = colschm[0];
380
540
  const bg = colschm[1];
381
541
  const txt = (colschm[0] || "").includes("dark") ? "text-light" : "";
@@ -384,11 +544,15 @@ const menuWrap = ({
384
544
  ? mobileBottomNavBar(currentUrl, menu, bg, txt)
385
545
  : "";
386
546
  const role = !req ? 1 : req.user ? req.user.role_id : 100;
387
- if ((config.menu_style === "No Menu" && role > 1) || (!menu && !brand))
547
+ if ((menuStyle === "No Menu" && role > 1) || (!menu && !brand))
388
548
  return div({ id: "wrapper" }, div({ id: "page-inner-content" }, body));
389
- else if (config.menu_style === "Side Navbar" && isNode) {
549
+ else if (menuStyle === "Side Navbar" && isNode) {
390
550
  return (
391
- navbar(brand, menu, currentUrl, { class: "d-md-none", ...config }) +
551
+ navbar(brand, menu, currentUrl, {
552
+ class: "d-md-none",
553
+ colorscheme,
554
+ fixedTop,
555
+ }) +
392
556
  div(
393
557
  { id: "wrapper", class: "d-flex with-sidebar" },
394
558
  nav(
@@ -414,7 +578,7 @@ const menuWrap = ({
414
578
  return (
415
579
  div(
416
580
  { id: "wrapper" },
417
- navbar(brand, menu, currentUrl, config),
581
+ navbar(brand, menu, currentUrl, { colorscheme, fixedTop }),
418
582
  div({ id: "page-inner-content" }, body)
419
583
  ) + mobileNav
420
584
  );
@@ -453,7 +617,7 @@ const layout = (config) => ({
453
617
  title,
454
618
  body,
455
619
  alerts,
456
- requestFluidLayout ? { ...config, fluid: true } : config,
620
+ requestFluidLayout ? { fluid: true } : {},
457
621
  role,
458
622
  req
459
623
  ),
@@ -565,112 +729,199 @@ const configuration_workflow = () =>
565
729
  },
566
730
  };
567
731
  },
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
732
  steps: [
591
733
  {
592
734
  name: "Theme",
593
735
  form: async (ctx) => {
736
+ let chatRunId = "";
737
+ let existingInteractions = "";
738
+ let plugin = await Plugin.findOne({ name: "bootstrap-prompt-theme" });
739
+ if (!plugin)
740
+ plugin = await Plugin.findOne({
741
+ name: "@saltcorn/bootstrap-prompt-theme",
742
+ });
743
+ if (plugin?.configuration?.chat_run_id) {
744
+ const run = await WorkflowRun.findOne({
745
+ id: plugin.configuration.chat_run_id,
746
+ });
747
+ if (run?.context?.html_interactions?.length) {
748
+ chatRunId = String(run.id);
749
+ existingInteractions = run.context.html_interactions.join("");
750
+ }
751
+ }
594
752
  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
- },
753
+ additionalHeaders: [
604
754
  {
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",
755
+ headerTag: `<script>
756
+ window._bptDT = new DataTransfer();
757
+ function bptSetFiles(files) {
758
+ for (const f of files) window._bptDT.items.add(f);
759
+ document.getElementById('bpt-file-input').files = window._bptDT.files;
760
+ bptUpdateFileLabel();
761
+ }
762
+ function bptUpdateFileLabel() {
763
+ const n = window._bptDT.files.length;
764
+ const el = document.getElementById('bpt-file-label');
765
+ if (!el) return;
766
+ if (n === 0) { el.innerHTML = ''; }
767
+ else {
768
+ const txt = n === 1 ? window._bptDT.files[0].name : n + ' files';
769
+ el.innerHTML = '<span class="text-muted">' + txt + '</span> <span class="badge text-bg-secondary" style="cursor:pointer;font-size:.65em" onclick="bptClearFiles()" title="Remove">&times;</span>';
770
+ }
771
+ }
772
+ function bptClearFiles() {
773
+ window._bptDT.items.clear();
774
+ const fi = document.getElementById('bpt-file-input');
775
+ if (fi) fi.value = '';
776
+ bptUpdateFileLabel();
777
+ }
778
+ function bptFileAttach(e) {
779
+ window._bptDT.items.clear();
780
+ bptSetFiles(e.target.files);
781
+ }
782
+ async function bptResetTheme() {
783
+ if (!confirm('Reset theme? This will remove the current CSS overlay and conversation.')) return;
784
+ await fetch('/bootstrap-prompt-theme/reset-theme', {
785
+ method: 'POST',
786
+ headers: { 'Content-Type': 'application/json', 'CSRF-Token': _sc_globalCsrf },
787
+ });
788
+ document.getElementById('bpt-interactions').innerHTML = '';
789
+ document.getElementById('bpt-run-id').value = '';
790
+ }
791
+ async function bptSend() {
792
+ const input = document.getElementById('bpt-input');
793
+ const interactions = document.getElementById('bpt-interactions');
794
+ const runIdInput = document.getElementById('bpt-run-id');
795
+ const sendBtn = document.getElementById('bpt-send-btn');
796
+ const sendIcon = document.getElementById('bpt-send-icon');
797
+ const fileInput = document.getElementById('bpt-file-input');
798
+ const msg = input.value.trim();
799
+ const hasFiles = fileInput?.files?.length > 0;
800
+ if ((!msg && !hasFiles) || sendBtn.disabled) return;
801
+ let fileHtml = '';
802
+ if (hasFiles) {
803
+ fileHtml = Array.from(fileInput.files).map(f => {
804
+ const url = URL.createObjectURL(f);
805
+ return '<img src="' + url + '" style="max-height:60px;max-width:80px;border-radius:4px;margin-top:4px;" alt="' + f.name + '">';
806
+ }).join(' ');
807
+ }
808
+ interactions.innerHTML += '<div class="interaction-segment to-right"><div><div class="badgewrap"><span class="badge bg-secondary">You</span></div>' + (msg ? '<p>' + msg.replace(/</g, '&lt;') + '</p>' : '') + fileHtml + '</div></div>';
809
+ interactions.scrollTop = interactions.scrollHeight;
810
+ input.value = '';
811
+ sendBtn.disabled = true;
812
+ sendIcon.className = 'fas fa-spinner fa-spin';
813
+ const fd = new FormData();
814
+ fd.append('userinput', msg);
815
+ fd.append('run_id', runIdInput.value);
816
+ if (hasFiles) {
817
+ Array.from(fileInput.files).forEach(f => fd.append('file', f));
818
+ bptClearFiles();
819
+ }
820
+ try {
821
+ const resp = await fetch('/bootstrap-prompt-theme/chat', {
822
+ method: 'POST',
823
+ headers: { 'CSRF-Token': _sc_globalCsrf },
824
+ body: fd
825
+ });
826
+ const data = await resp.json();
827
+ if (data.run_id) runIdInput.value = data.run_id;
828
+ if (data.response) { interactions.innerHTML += data.response; interactions.scrollTop = interactions.scrollHeight; }
829
+ if (data.error) interactions.innerHTML += '<div class="alert alert-danger mt-2">' + data.error + '</div>';
830
+ } catch (e) {
831
+ interactions.innerHTML += '<div class="alert alert-danger mt-2">Error: ' + e.message + '</div>';
832
+ }
833
+ sendBtn.disabled = false;
834
+ sendIcon.className = 'far fa-paper-plane';
835
+ }
836
+ document.addEventListener('DOMContentLoaded', () => {
837
+ const el = document.createElement('div');
838
+ el.innerHTML = [
839
+ '<div class="card mb-4">',
840
+ ' <div class="card-header fw-bold"><i class="fas fa-robot me-2"></i>AI Theme Chat</div>',
841
+ ' <div class="card-body" id="bpt-card-body">',
842
+ ' <p class="text-muted small mb-2">Chat with the AI to build your Bootstrap theme. New themes are visible after page reload or when you click Finish.</p>',
843
+ ' <div id="bpt-interactions" style="min-height:80px;max-height:320px;overflow-y:auto;border:1px solid var(--bs-border-color,#dee2e6);padding:10px;margin-bottom:10px;border-radius:4px;background:var(--bs-body-bg,#fff);"></div>',
844
+ ' <div class="d-flex gap-2 align-items-end">',
845
+ ' <textarea id="bpt-input" class="form-control" rows="2" placeholder="Describe the theme changes you want..."></textarea>',
846
+ ' <button type="button" id="bpt-send-btn" class="btn btn-primary" onclick="bptSend()">',
847
+ ' <i id="bpt-send-icon" class="far fa-paper-plane"></i>',
848
+ ' </button>',
849
+ ' </div>',
850
+ ' <div class="d-flex align-items-center mt-1 gap-2">',
851
+ ' <label class="mb-0 text-muted" style="cursor:pointer" for="bpt-file-input" title="Attach image for design inspiration"><i class="fas fa-paperclip"></i></label>',
852
+ ' <input type="file" id="bpt-file-input" class="d-none" accept="image/*" multiple onchange="bptFileAttach(event)">',
853
+ ' <span id="bpt-file-label" class="small"></span>',
854
+ ' </div>',
855
+ ' <input type="hidden" id="bpt-run-id" value="">',
856
+ ' </div>',
857
+ '</div>',
858
+ ].join('');
859
+ const form = document.querySelector('form.form-namespace');
860
+ if (form) form.parentNode.insertBefore(el, form);
861
+ document.getElementById('bpt-run-id').value = ${JSON.stringify(chatRunId)};
862
+ const interactionsEl = document.getElementById('bpt-interactions');
863
+ interactionsEl.innerHTML = ${JSON.stringify(existingInteractions)};
864
+ interactionsEl.scrollTop = interactionsEl.scrollHeight;
865
+ const inp = document.getElementById('bpt-input');
866
+ inp?.addEventListener('keydown', (e) => {
867
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); bptSend(); }
868
+ });
869
+ const dropZone = document.getElementById('bpt-card-body');
870
+ let _dragCtr = 0;
871
+ dropZone?.addEventListener('dragover', (e) => e.preventDefault());
872
+ dropZone?.addEventListener('dragenter', (e) => {
873
+ e.preventDefault();
874
+ _dragCtr++;
875
+ dropZone.style.outline = '2px dashed var(--bs-primary, #0d6efd)';
876
+ dropZone.style.outlineOffset = '-4px';
877
+ });
878
+ dropZone?.addEventListener('dragleave', () => {
879
+ _dragCtr--;
880
+ if (_dragCtr === 0) { dropZone.style.outline = ''; dropZone.style.outlineOffset = ''; }
881
+ });
882
+ dropZone?.addEventListener('drop', (e) => {
883
+ e.preventDefault();
884
+ _dragCtr = 0;
885
+ dropZone.style.outline = '';
886
+ dropZone.style.outlineOffset = '';
887
+ const imgs = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
888
+ if (imgs.length) bptSetFiles(imgs);
889
+ });
890
+ inp?.addEventListener('paste', (e) => {
891
+ const pasted = Array.from(e.clipboardData?.items || [])
892
+ .filter(it => it.type.startsWith('image/'))
893
+ .map(it => it.getAsFile())
894
+ .filter(Boolean);
895
+ if (pasted.length) { e.preventDefault(); bptSetFiles(pasted); }
896
+ });
897
+ const submitBtn = form?.querySelector('[type="submit"]');
898
+ const origSubmitHTML = submitBtn ? submitBtn.innerHTML : null;
899
+ if (form) form.addEventListener('submit', (e) => {
900
+ const draft = document.getElementById('bpt-input')?.value?.trim();
901
+ if (draft && !confirm('You have an unsent message. Finish without sending it?')) {
902
+ e.preventDefault();
903
+ if (submitBtn && origSubmitHTML !== null) {
904
+ setTimeout(() => {
905
+ submitBtn.innerHTML = origSubmitHTML;
906
+ submitBtn.disabled = false;
907
+ submitBtn.removeAttribute('disabled');
908
+ }, 100);
909
+ }
910
+ }
911
+ });
912
+ });
913
+ </script>`,
659
914
  },
915
+ ],
916
+ additionalButtons: [
660
917
  {
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
- },
918
+ label: "Reset theme",
919
+ id: "bpt-reset-btn",
920
+ class: "btn btn-outline-danger",
921
+ onclick: "bptResetTheme()",
672
922
  },
673
923
  ],
924
+ fields: [],
674
925
  });
675
926
  },
676
927
  },
@@ -680,7 +931,20 @@ const configuration_workflow = () =>
680
931
  module.exports = {
681
932
  sc_plugin_api_version: 1,
682
933
  plugin_name: "bootstrap-prompt-theme",
934
+ exchange: () => ({ agent_skills: [GenerateBootstrapThemeSkill] }),
683
935
  dependencies: ["@saltcorn/large-language-model"],
936
+ routes: () => [
937
+ {
938
+ url: "/bootstrap-prompt-theme/chat",
939
+ method: "post",
940
+ callback: chatRoute,
941
+ },
942
+ {
943
+ url: "/bootstrap-prompt-theme/reset-theme",
944
+ method: "post",
945
+ callback: resetThemeRoute,
946
+ },
947
+ ],
684
948
  layout,
685
949
  configuration_workflow,
686
950
  onLoad: async (configuration) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/bootstrap-prompt-theme",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Bootstrap 5 layout plugin with LLM-generated CSS overlay",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/generate_theme.js DELETED
@@ -1,75 +0,0 @@
1
- const { getState } = require("@saltcorn/data/db/state");
2
- const { join } = require("path");
3
- const { writeFile, readdir, unlink } = require("fs").promises;
4
-
5
- const writeOverlayCSS = async (css, filename = `overlay.${Date.now()}.css`) => {
6
- const dest = join(__dirname, "public", filename);
7
- await writeFile(dest, css);
8
- return filename;
9
- };
10
-
11
- const deleteOldOverlays = async (keepFile) => {
12
- const publicDir = join(__dirname, "public");
13
- const files = await readdir(publicDir);
14
- for (const file of files) {
15
- if (/^overlay\.\d+\.css$/.test(file) && file !== keepFile)
16
- await unlink(join(publicDir, file));
17
- }
18
- };
19
-
20
- const SYSTEM_PROMPT = `You are an expert CSS theme designer for Bootstrap 5.3.
21
- Your task is to generate a complete CSS overlay file that reskins a standard Bootstrap 5.3 app.
22
-
23
- APPROACH:
24
- 1. Define theme-specific custom properties in :root (colors, fonts, shadows, etc.)
25
- 2. Override Bootstrap 5.3 CSS variables in :root using --bs-* tokens for light mode
26
- 3. Add component-level CSS rules for deeper customisation (navbar, cards, buttons, forms, tables, alerts, badges, modals, pagination, tabs, accordions, etc.)
27
- 4. Add a dark mode section scoped to [data-bs-theme="dark"] that overrides :root custom properties and --bs-* tokens for dark mode. Component-level rules that depend only on CSS variables will automatically adapt — only add extra component rules under [data-bs-theme="dark"] where variables alone are not sufficient.
28
-
29
- BOOTSTRAP 5.3 CSS VARIABLES to consider overriding in :root and [data-bs-theme="dark"]:
30
- --bs-body-bg, --bs-body-color, --bs-secondary-bg, --bs-tertiary-bg,
31
- --bs-border-color, --bs-primary, --bs-link-color, --bs-link-hover-color,
32
- --bs-card-bg, --bs-card-border-color, --bs-card-cap-bg,
33
- --bs-dropdown-bg, --bs-dropdown-border-color, --bs-dropdown-link-color,
34
- --bs-dropdown-link-hover-bg, --bs-dropdown-link-active-bg,
35
- --bs-table-color, --bs-table-bg, --bs-table-border-color, --bs-table-hover-bg,
36
- --bs-modal-bg, --bs-modal-border-color,
37
- --bs-tooltip-bg, --bs-tooltip-color,
38
- --bs-body-font-family, --bs-body-font-size
39
-
40
- COMPONENTS to style: body, navbar (.navbar, .navbar-brand, .nav-link), cards (.card, .card-header, .card-footer, .card-title), buttons (.btn, .btn-primary, .btn-secondary, .btn-danger, .btn-success), forms (.form-control, .form-select, .form-label, .form-check-input), tables (.table, .table th, .table td), alerts (.alert-*), badges (.badge), modals (.modal-content, .modal-header), pagination (.page-link), tabs (.nav-tabs), links (a), headings (h1-h6), code/pre, hr, scrollbar.
41
-
42
- DARK MODE STRUCTURE:
43
- :root { /* light mode theme variables and --bs-* overrides */ }
44
- /* component rules */
45
- [data-bs-theme="dark"] { /* dark mode theme variables and --bs-* overrides */ }
46
- /* any extra component rules needed only in dark mode, prefixed with [data-bs-theme="dark"] */
47
-
48
- CRITICAL RULES — never break these:
49
- - Never set overflow:hidden or overflow:clip on ANY element — not on .navbar, .card, .card-body, .card-header, .card-footer, section, #wrapper, #page-inner-content, #content-wrapper, .container, .page-section, or any other element. Bootstrap's Popper.js positions dropdowns with position:absolute outside their parent bounds; overflow:hidden on any ancestor will clip them.
50
- - Always set .navbar { position: relative; z-index: 1030; } to ensure navbar dropdowns render above all page content.
51
- - Never apply transform, filter, backdrop-filter, will-change, or perspective to .card, .card-body, .card-header, .card-footer, .container, .page-section, section, #wrapper, or any page-level container. These CSS properties create a new stacking context that can rise above the navbar and obscure its open dropdowns.
52
- - Never set z-index on .card or page content elements — stacking context on page content is what causes navbar dropdowns to be obscured.
53
- - Do not override --bs-zindex-dropdown, --bs-zindex-fixed, or .dropdown-menu z-index.
54
- - Dropdowns must always be fully visible and on top of all other page content, including cards, sections, and containers.
55
-
56
- OUTPUT: Only valid CSS. No explanations. No markdown. No code fences. Start directly with /* theme comment */ or :root {`;
57
-
58
- const generateThemeCSS = async (prompt) => {
59
- const state = getState();
60
- if (!state.functions?.llm_generate) {
61
- state.log(
62
- 5,
63
- "bootstrap-prompt-theme: llm_generate not available, is large-language-model installed?"
64
- );
65
- return null;
66
- }
67
- console.log("bootstrap-prompt-theme: generating theme from prompt...");
68
- const result = await state.functions.llm_generate.run(prompt, {
69
- systemPrompt: SYSTEM_PROMPT,
70
- });
71
- state.log(6, `bootstrap-prompt-theme: LLM result: ${result}`);
72
- return result || null;
73
- };
74
-
75
- module.exports = { generateThemeCSS, writeOverlayCSS, deleteOldOverlays };