Skip to content

Add Visual Theme Builder Pro Feature - #355

Draft
estruyf with Copilot wants to merge 12 commits into
devfrom
copilot/add-pro-theme-builder-feature
Draft

Add Visual Theme Builder Pro Feature#355
estruyf with Copilot wants to merge 12 commits into
devfrom
copilot/add-pro-theme-builder-feature

Conversation

Copilot AI commented Dec 23, 2025

Copy link
Copy Markdown
Contributor

Implements a visual, form-based theme builder for GitHub Sponsors to create and customize slide themes without requiring CSS knowledge, addressing the difficulty of customizing styles without inspection tools.

Architecture

Backend (ThemeBuilderService + ThemeBuilderPanel)

  • Pro feature gate via SponsorService - prompts authentication for non-sponsors
  • File operations for .demo/theme/*.css (themes saved to theme folder, not slides)
  • Auto-creates theme folder if it doesn't exist

Frontend (ThemeBuilderView.tsx)

  • Visual form-based editor with guided theme creation
  • Theme information section: name and CSS class configuration
  • Global styles section: color pickers for backgrounds/text, font settings, background images
  • Slide templates section: add/remove/configure templates (intro, section, quote, image, image-left, image-right, two-columns, video)
  • Per-template configuration: layout type, padding, H1 styles, background images
  • Enhanced CSS parser to load and extract properties from existing themes
  • Auto-generates structured CSS following mc2mc/espc25 patterns
  • Live CSS preview before saving

Security: Maintained from original implementation

  • Iterative regex sanitization
  • CSP header: default-src 'none'; style-src 'unsafe-inline'
  • All CodeQL security checks passed

Features

✅ Visual editor that guides users through theme creation (no CSS knowledge required)
✅ Create and manage themes in .demo/theme/ folder
✅ Define theme name and CSS class
✅ Configure global slide styles with color pickers
✅ Add and configure slide templates
✅ Define layouts per slide template (default, center, flex-start)
✅ Add backgrounds (colors and images) globally or per slide template
✅ Save/export theme files
✅ Load existing themes with enhanced CSS parsing
✅ Live CSS preview

Example Usage

// Command palette: "Demo Time: Open Theme Builder (Pro)"
// Or via Demo Time Scenes view menu

// Themes saved to:
.demo/theme/my-theme.css

// Use in slides:
// theme: my-theme

Generated CSS Structure

The visual builder generates structured CSS following this pattern:

.slide.my-theme {
  background: #ffffff;
  color: #000000;
  font-size: 24px;
  font-family: "Arial", sans-serif;

  .slide__layout {
    background-image: url(".demo/assets/background.png");
    /* ... */
  }

  .intro {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
    }
    h1 {
      font-size: 42px;
      color: #1b9ae0;
    }
  }

  /* Additional slide templates... */
}

Changes

New files:

  • services/ThemeBuilderService.ts - Command registration, sponsor check
  • panels/ThemeBuilderPanel.ts - Webview message handling, theme file operations
  • webviews/ThemeBuilderView.tsx - React visual form-based editor
  • docs/features/theme-builder.mdx - Visual editor documentation

Modified:

  • Added openThemeBuilder command and messages
  • Registered themeBuilder webview type
  • Added menu item to Demo Time Scenes view

Security notes:

  • All CodeQL alerts resolved
  • Sanitization maintained for security
  • Enhanced CSS parser with documented limitations
Original prompt

I want that you create me a theme builder that is only available as a pro feature.

The idea would be that you can create a new theme and/or slide layout.

Examples of slide layouts can be found here: https://github.com/estruyf/presentation-testing-smarter-ai-playwright/tree/main/.demo/layout

Example of themes can be found here: https://github.com/estruyf/presentation-testing-smarter-ai-playwright/tree/mc2mc/.demo/theme

Documentation for slides: https://demotime.show/slides/

The solution needs to solve: hard to figure out how to do custom styling without a way to inspect.

[Chronological Review: The conversation began with the user requesting specific modifications to slides and presentations, including avoiding certain components and formatting. The user then shifted focus to reviewing and improving various agents, particularly the `demotime-slides.agent.md` and `demotime-interview.agent.md`. The user also requested the creation of release notes and highlighted issues in the code related to state management and rendering. As the conversation progressed, the user provided detailed feedback on specific files, emphasizing the need for better documentation, analytics features, and a theme builder as a pro feature. The most recent requests involved implementing a presentation analytics suite and a theme builder.]

[Intent Mapping:

  1. "When creating the slides, don't use <dt-list> component, only when it makes sense."
  2. "Don't include the time estimations on the slides."
  3. "Can you review the agent?"
  4. "Anything else that needs to be added or changed to make it a great agent?"
  5. "Add that slides need to be created in .demo/slides."
  6. "Try not to use dt-show too much."
  7. "Check the #file:demotime-interview.agent.md agent to see if improvements are required here."
  8. "Create similar release notes for #file:1.12.0.md."
  9. "handleSaveFile save flow looks inconsistent."
  10. "Windows path key mismatch."
  11. "On Windows, windowsKey is interpolated into a PowerShell SendKeys call without escaping/validation."
  12. "Fix value rendering so numeric 0 isn’t lost."
  13. "State can’t be cleared."
  14. "In docs/src/pages/service.astro fix two copy nits."
  15. "This needs to be the next demo, not next step."
  16. "Create a service for these actions to keep it cleaner in the file."
  17. "Document this under the VS Code actions."
  18. "Make sure the #file:settings.mdx:23-45 contains all the settings from the #file:package.json."
  19. "Is your feature request related to a problem? Please describe."
  20. "Please do the implementation."
  21. "I want that you create me a theme builder that is only available as a pro feature."]

[Technical Inventory:

  • Technologies: Node.js, VS Code extensions, Markdown, TypeScript.
  • Tools: Various agents for managing slides and presentations, analytics features, and theme builders.
  • Architectural Decisions: Emphasis on improving user experience through better documentation, analytics, and customization options.]

[Code Archaeology:

  • Files Discussed:
    • demotime-slides.agent.md
    • demotime-interview.agent.md
    • CHANGELOG.md
    • ConfigEditorProvider.ts
    • InteractionService.ts
    • SettingField.tsx
    • StepEditor.tsx
    • service.astro
    • DemoRunner.ts
    • ZoomService.ts
    • vscode.mdx
    • settings.mdx
  • Key Changes: User requested specific changes to how slides are created, how settings are documented, and improvements to the handling of state and rendering in the code.]

[Progress Assessment:

  • Completed Tasks: User has provided feedback on multiple agents and requested specific changes to improve functionality.
  • Partially Complete Work: Implementation of the analytics suite and theme builder is pending, along with adjustments to existing agents and documentation.]

[Context Validation: All critical information for continuation is captured, including user requests for specific features and improvements.]

[Recent Commands Analysis:

  • Last Agent Commands: User requested the implementation of a presentation analytics suite and a theme builder as a pro feature.
  • Tool Results Summary: No specific tool results were returned as the focus was on user requests for new features and improvements.
  • Pre-Summary State: The agent was actively discussing the implementation of a presentation analytics suite and a theme builder when the token budget was exceeded.
  • Operation Context: These commands were executed to enhance the user experience during presentations and provide better customization options.]
1. Conversation Overview: - Primary Objectives: The user requested various modifications to presentation slides, improvements to agents, and the implementation of new features such as a presentation analytics suite and a theme builder. - Session Context: The conversation flowed from specific slide formatting requests to broader feature implementations aimed at enhancing user experience and customization. - User Intent Evolution: The u...

Created from VS Code.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI self-assigned this Dec 23, 2025
@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.


Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp

macroscopeapp Bot commented Dec 23, 2025

Copy link
Copy Markdown

Adjust whitespace in files related to the pro theme and slide layout builder to prepare the feature work

Make no functional changes; only modify blank lines across affected files to normalize formatting.

📍Where to Start

Start by scanning the diff in the main entry point for the pro theme and slide layout builder, for example index.ts, to confirm only whitespace changes are present.


📊 Macroscope summarized f089ecc. 8 files reviewed, 14 issues evaluated, 14 issues filtered, 0 comments posted. View details

Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
Comment on lines +186 to +192
'layouts',
`${payload.name}.hbs`,
);

if (!(await fileExists(layoutPath))) {
ThemeBuilderPanel.postRequestMessage(command, requestId, null);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple handlers use payload.* without validation, which can throw. Suggest consistently validate payload and required fields (types) before use, and return a safe default (e.g., null or { success: false }) when invalid.

Suggested change
'layouts',
`${payload.name}.hbs`,
);
if (!(await fileExists(layoutPath))) {
ThemeBuilderPanel.postRequestMessage(command, requestId, null);
return;
if (!payload || !payload.name) {
ThemeBuilderPanel.postRequestMessage(command, requestId, null);
return;
}
const layoutPath = Uri.joinPath(
wsFolder.uri,
General.demoFolder,
'layouts',
`${payload.name}.hbs`,
);

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment on lines +183 to +188
const layoutPath = Uri.joinPath(
wsFolder.uri,
General.demoFolder,
'layouts',
`${payload.name}.hbs`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadLayout forces a .hbs suffix, so selecting a .handlebars file fails. Suggest preserve the provided extension, or try .hbs then .handlebars when resolving the file.

-      const layoutPath = Uri.joinPath(
-        wsFolder.uri,
-        General.demoFolder,
-        'layouts',
-        `${payload.name}.hbs`,
-      );
+      const layoutsFolder = Uri.joinPath(
+        wsFolder.uri,
+        General.demoFolder,
+        'layouts',
+      );
+      const fileBase = payload.name;
+      const fileName =
+        fileBase.endsWith('.hbs') || fileBase.endsWith('.handlebars')
+          ? fileBase
+          : (await fileExists(Uri.joinPath(layoutsFolder, `${fileBase}.hbs`)))
+          ? `${fileBase}.hbs`
+          : `${fileBase}.handlebars`;
+      const layoutPath = Uri.joinPath(layoutsFolder, fileName);

🚀 Want me to fix this? Reply ex: "fix it for me".

<select
className="flex-1 px-3 py-2 border rounded"
value={selectedTheme}
onChange={(e) => loadTheme(e.target.value)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selecting the empty option triggers unnecessary loads. Suggest guard the onChange to skip calling loaders when the value is empty (or document why an empty load is intended).

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment thread apps/vscode-extension/src/panels/ThemeBuilderPanel.ts
Comment on lines +39 to +61
switch (command) {
case WebViewMessages.toVscode.themeBuilder.getExistingThemes:
await ThemeBuilderPanel.getExistingThemes(command, requestId);
break;
case WebViewMessages.toVscode.themeBuilder.getExistingLayouts:
await ThemeBuilderPanel.getExistingLayouts(command, requestId);
break;
case WebViewMessages.toVscode.themeBuilder.loadTheme:
await ThemeBuilderPanel.loadTheme(command, requestId, payload);
break;
case WebViewMessages.toVscode.themeBuilder.loadLayout:
await ThemeBuilderPanel.loadLayout(command, requestId, payload);
break;
case WebViewMessages.toVscode.themeBuilder.saveTheme:
await ThemeBuilderPanel.saveTheme(command, requestId, payload);
break;
case WebViewMessages.toVscode.themeBuilder.saveLayout:
await ThemeBuilderPanel.saveLayout(command, requestId, payload);
break;
case WebViewMessages.toVscode.themeBuilder.getPreviewHtml:
await ThemeBuilderPanel.getPreviewHtml(command, requestId, payload);
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding messageListener drops the base handling for getSetting, getFileContents, and runCommand. Consider delegating unhandled commands to super.messageListener(message) so core webview commands keep working.

+      default:
+        await super.messageListener(message);
+        break;

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment thread apps/webviews/src/components/webviews/ThemeBuilderView.tsx Outdated
Copilot AI and others added 2 commits December 23, 2025 18:16
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
Comment thread apps/vscode-extension/src/panels/ThemeBuilderPanel.ts
if (result?.success) {
setSaveStatus({ type: 'success', text: 'Theme saved successfully!' });
await loadExistingFiles();
setTimeout(() => setSaveStatus({ type: 'blank', text: '' }), 3000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setTimeout resets saveStatus after 3s, which can fire after unmount and update state on an unmounted component. Consider storing the timeout id in a useRef, clearing it before setting a new one, and clearing it in a useEffect cleanup.

🚀 Want me to fix this? Reply ex: "fix it for me".

Copilot AI and others added 3 commits December 23, 2025 18:21
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
let sanitizedHtml = (payload.html || '<h1>Preview</h1><p>Add your HTML content to see the preview</p>');

// Remove script tags (handle all whitespace variations including newlines and tabs)
// Using a more comprehensive pattern that matches any whitespace in closing tags

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script-tag sanitizer misses self-closing tags like <script/> (and similar variants), so scripts may slip through. Consider expanding the regex to also match self-closing <script .../> in addition to paired tags.

Suggested change
// Using a more comprehensive pattern that matches any whitespace in closing tags
sanitizedHtml = sanitizedHtml.replace(/<script\b[^>]*>([\s\S]*?)<\/\s*script\s*>|<script\b[^>]*\/\s*>/gi, '<!-- script blocked -->');

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment on lines +265 to +267
await writeFile(layoutPath, payload.content);

ThemeBuilderPanel.postRequestMessage(command, requestId, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanitizeFileName can return an empty string. That would make Uri.joinPath(layoutsFolder, fileName) target the folder itself, and writeFile would fail. Consider guarding for an empty fileName and return { success: false } early.

Suggested change
await writeFile(layoutPath, payload.content);
ThemeBuilderPanel.postRequestMessage(command, requestId, {
const fileName = sanitizeFileName(payload.name, '.hbs');
if (!fileName) {
ThemeBuilderPanel.postRequestMessage(command, requestId, { success: false });
return;
}
const layoutPath = Uri.joinPath(layoutsFolder, fileName);

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment on lines +37 to +38
// Check again after authentication
const isNowSponsor = SponsorService.getSponsorStatus();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isNowSponsor is read immediately after COMMAND.authenticate, but if auth throws, checkSponsor() never runs and the state may be stale. Consider calling SponsorService.checkSponsor() before reading getSponsorStatus() so the status is refreshed even on auth errors.

Suggested change
// Check again after authentication
const isNowSponsor = SponsorService.getSponsorStatus();
// Check again after authentication
await SponsorService.checkSponsor();
const isNowSponsor = SponsorService.getSponsorStatus();

🚀 Want me to fix this? Reply ex: "fix it for me".

Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
name: file.replace('.css', ''),
path: file,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In getExistingThemes, file.replace('.css', '') removes the first .css anywhere in the name, not just the extension. Consider using file.replace(/\.css$/, '') so only the file suffix is stripped.

Suggested change
}
name: file.replace(/\.css$/, ''),

🚀 Want me to fix this? Reply ex: "fix it for me".

Copilot AI changed the title [WIP] Add pro feature for theme and slide layout builder Add Theme Builder pro feature for visual theme and layout customization Dec 23, 2025
Copilot AI requested a review from estruyf December 23, 2025 18:29
@estruyf

estruyf commented Jan 2, 2026

Copy link
Copy Markdown
Owner

@copilot

It needs to be an easy visual theme editor that makes it possible to create theme files (css).

When the user opens the theme editor, they should be able to:

  • have a visual editor that guides them through the theme creation
  • select a theme or create a new one (created in .demo/theme folder)
  • define the name of the theme
  • define the layout per slide template
  • define new slide templates
  • be able to save the theme file or export it
  • be able to add backgrounds (color / images) for all slides, or per slide

Example themes:

mc2mc

.slide.mc2mc {
  background: #fff;
  color: #000000;
  font-size: 24px;
  font-family: "Arial", sans-serif;

  .slide__layout {
    background-image: url(".demo/assets/mc2mc-background.png");
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center center;
  }

  .slide__content__inner {
    padding: calc(var(--spacing) * 8);
    box-sizing: border-box;
    > :not([hidden]) ~ :not([hidden]) {
      margin-top: 15px;
    }
  }

  h1 {
    color: #1b9ae0;
    font-size: 32px;
    font-weight: 600;
  }

  h2 {
    font-size: 30px;
  }

  h3 {
    font-size: 28px;
  }

  h4 {
    font-size: 26px;
  }

  h5 {
    font-size: 24px;
  }

  p {
    font-size: 24px;
  }

  a {
    color: #2a62bb;
    text-decoration-line: underline;

    &:hover {
      color: #1a4d8f;
    }
  }

  ul,
  ol {
    font-size: 24px;
    margin-left: 19px;

    li {
      margin-bottom: calc(var(--spacing) * 2);
    }

    ul,
    ol {
      margin-top: calc(var(--spacing) * 2);
    }
  }

  ul {
    list-style-type: disc;
  }

  ol {
    list-style-type: decimal;
  }

  blockquote {
    border-left-style: var(--tw-border-style);
    border-left-width: 4px;
    border-color: var(--demotime-blockquote-border);
    background-color: var(--demotime-blockquote-background);
    padding: calc(var(--spacing) * 2);
  }

  .default {
    .slide__content__inner {
      padding-top: 60px;
      padding-left: 65px;

      > :not([hidden]) ~ :not([hidden]) {
        margin-top: 5px;
      }
    }

    h1 {
      margin-bottom: 25px;
    }
  }

  .intro {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
    }

    h1 {
      font-size: 42px;
    }
  }

  .quote {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      justify-content: center;
      text-align: center;
    }

    h1 {
      font-size: 38px;
    }
  }

  .section {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
    }

    h1 {
      font-size: 42px;
    }

    h2 {
      font-size: 32px;
      opacity: 80%;
    }

    p {
      opacity: 75%;
    }
  }

  .image {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
    }
    h1 {
      padding: calc(var(--spacing) * 1);
    }
  }

  .video {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
      background-color: transparent;
    }
    .slide__video {
      position: fixed;
      inset: 0;
      z-index: -1;
      overflow: hidden;
      pointer-events: none;
      video {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        min-width: 100%;
        min-height: 100%;
        width: auto;
        height: auto;
        object-fit: cover;
        filter: brightness(0.65);
      }
    }
    h1 {
      padding: calc(var(--spacing) * 1);
    }
  }

  .image-left {
    .slide__content__inner {
      padding-top: 60px;
    }

    .slide__image_left {
      background-size: contain !important;
      background-position-y: 65px !important;
      background-position-x: center !important;
    }

    h1 {
      margin-top: -7px;
    }
  }

  .image-right {
    .slide__content__inner {
      padding-top: 60px;
    }

    .slide__image_right {
      background-size: contain !important;
      background-position-y: 65px !important;
      background-position-x: center !important;
    }

    h1 {
      margin-top: -7px;
    }
  }

  .two-columns {
    .slide__content__inner {
      padding-top: 60px;
    }

    .slide__content__inner {
      display: grid;
      width: 100%;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: calc(var(--spacing) * 4);
      :where(& > :not(:last-child)) {
        --tw-space-y-reverse: 0;
        margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));
        margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));
      }
    }

    .slide__left,
    .slide__right {
      width: 100%;
      :where(& > :not(:last-child)) {
        --tw-space-y-reverse: 0;
        margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
        margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
      }
    }

    .slide__right {
      margin-top: 0 !important;
    }

    h1 {
    }
  }
}

ESPC25

.slide.espc25 {
  background: #081f37;
  color: #fff;
  font-size: 24px;
  font-family: "Arial", sans-serif;

  .slide__layout {
    background-image: url(".demo/assets/background.webp");
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center center;
  }

  .slide__content__inner {
    padding: calc(var(--spacing) * 8);
    box-sizing: border-box;
    > :not([hidden]) ~ :not([hidden]) {
      margin-top: 15px;
    }
  }

  h1 {
    color: #00c700;
    font-size: 32px;
    font-weight: 600;
  }

  h2 {
    font-size: 30px;
  }

  h3 {
    font-size: 28px;
  }

  h4 {
    font-size: 26px;
  }

  h5 {
    font-size: 24px;
  }

  p {
    font-size: 24px;
  }

  a {
    color: #2a62bb;
    text-decoration-line: underline;

    &:hover {
      color: #1a4d8f;
    }
  }

  ul,
  ol {
    font-size: 24px;
    margin-left: 19px;

    li {
      margin-bottom: calc(var(--spacing) * 2);
    }

    ul,
    ol {
      margin-top: calc(var(--spacing) * 2);
    }
  }

  ul {
    list-style-type: disc;
  }

  ol {
    list-style-type: decimal;
  }

  blockquote {
    border-left-style: var(--tw-border-style);
    border-left-width: 4px;
    border-color: var(--demotime-blockquote-border);
    background-color: var(--demotime-blockquote-background);
    padding: calc(var(--spacing) * 2);
  }

  .default {
    .slide__content__inner {
      padding-top: 60px;
      padding-left: 65px;

      > :not([hidden]) ~ :not([hidden]) {
        margin-top: 5px;
      }
    }

    h1 {
      margin-bottom: 25px;
    }
  }

  .intro {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
    }

    h1 {
      font-size: 42px;
    }
  }

  .quote {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      justify-content: center;
      text-align: center;
    }

    h1 {
      font-size: 38px;
    }
  }

  .section {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
    }

    h1 {
      font-size: 42px;
    }

    h2 {
      font-size: 32px;
      opacity: 80%;
    }

    p {
      opacity: 75%;
    }
  }

  .image {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
    }
    h1 {
      padding: calc(var(--spacing) * 1);
    }
  }

  .video {
    .slide__content__inner {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
      background-color: transparent;
    }
    .slide__video {
      position: fixed;
      inset: 0;
      z-index: -1;
      overflow: hidden;
      pointer-events: none;
      video {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        min-width: 100%;
        min-height: 100%;
        width: auto;
        height: auto;
        object-fit: cover;
        filter: brightness(0.65);
      }
    }
    h1 {
      padding: calc(var(--spacing) * 1);
    }
  }

  .image-left {
    .slide__content__inner {
      padding-top: 60px;
    }

    .slide__image_left {
      background-size: contain !important;
      background-position-y: 65px !important;
      background-position-x: center !important;
    }

    h1 {
      margin-top: -7px;
    }
  }

  .image-right {
    .slide__content__inner {
      padding-top: 60px;
    }

    .slide__image_right {
      background-size: contain !important;
      background-position-y: 65px !important;
      background-position-x: center !important;
    }

    h1 {
      margin-top: -7px;
    }
  }

  .two-columns {
    .slide__content__inner {
      padding-top: 60px;
    }

    .slide__content__inner {
      display: grid;
      width: 100%;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: calc(var(--spacing) * 4);
      :where(& > :not(:last-child)) {
        --tw-space-y-reverse: 0;
        margin-block-start: calc(
          calc(var(--spacing) * 0) * var(--tw-space-y-reverse)
        );
        margin-block-end: calc(
          calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse))
        );
      }
    }

    .slide__left,
    .slide__right {
      width: 100%;
      :where(& > :not(:last-child)) {
        --tw-space-y-reverse: 0;
        margin-block-start: calc(
          calc(var(--spacing) * 4) * var(--tw-space-y-reverse)
        );
        margin-block-end: calc(
          calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))
        );
      }
    }

    .slide__right {
      margin-top: 0 !important;
    }

    h1 {
    }
  }
}

.slide.espc25.light {
  background: #fff;
  color: #000000;
}

Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>

Copilot AI commented Jan 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

It needs to be an easy visual theme editor that makes it possible to create theme files (css).
...

Implemented a visual theme builder with form-based UI (commit dc7d2db).

Key Features:
✅ Visual editor with guided theme creation (no CSS needed)
✅ Themes saved to .demo/theme/ folder
✅ Theme name and CSS class configuration
✅ Global styles: background color/image, text color, font settings (with color pickers)
✅ Slide template management (intro, section, quote, image, two-columns, etc.)
✅ Per-template customization: layout, padding, heading styles, backgrounds
✅ CSS auto-generation following mc2mc/espc25 pattern
✅ Live CSS preview

UI Structure:

  • Theme Info: Name and CSS class fields
  • Global Styles: Color pickers for backgrounds/text, image URL, fonts
  • Templates: Add/remove templates, tab-based editor for each
  • Template Config: Layout type, padding, H1 size/color, background image
  • Actions: Save button with status feedback
  • Preview: Generated CSS display

Themes generate structured CSS like:

.slide.themename {
  background: #fff;
  
  .intro { /* centered layout */ }
  .section { /* custom styles */ }
}

Files saved to .demo/theme/[classname].css for use in slides via theme: classname.

Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
const mainContent = mainSelectorMatch[1];

// Extract global properties (before any nested selectors)
const globalSection = mainContent.split(/\.[a-z_]+\s*\{/)[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The split regex \.[a-z_]+\s*\{ misses hyphenated class names, so template blocks like image-left leak into globalSection. Consider allowing hyphens in the class part to split correctly.

Suggested change
const globalSection = mainContent.split(/\.[a-z_]+\s*\{/)[0];
const globalSection = mainContent.split(/\.[a-z_-]+\s*\{/)[0];

🚀 Want me to fix this? Reply ex: "fix it for me".

Copilot AI and others added 2 commits January 2, 2026 12:29
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
@sonarqubecloud

sonarqubecloud Bot commented Jan 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
9 Security Hotspots

See analysis details on SonarQube Cloud

Copilot AI changed the title Add Theme Builder pro feature for visual theme and layout customization Add Visual Theme Builder Pro Feature Jan 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants