@squeditor/squeditor-framework 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ /* Swiper framework overrides */
2
+ .swiper {
3
+ --swiper-theme-color: var(--sq-color-primary, #000);
4
+ --swiper-navigation-size: 20px;
5
+
6
+ &-pagination-bullet {
7
+ background: var(--sq-color-primary, #000);
8
+ opacity: 0.3;
9
+ transition: all 0.3s ease;
10
+
11
+ &-active {
12
+ opacity: 1;
13
+ width: 24px;
14
+ border-radius: 4px;
15
+ }
16
+ }
17
+
18
+ &-button-next,
19
+ &-button-prev {
20
+ transition: all 0.3s ease;
21
+ &:hover {
22
+ transform: translateY(-50%) scale(1.05);
23
+ }
24
+
25
+ &.swiper-button-disabled {
26
+ opacity: 0;
27
+ pointer-events: none;
28
+ }
29
+ }
30
+ }
@@ -7,5 +7,6 @@
7
7
  @import 'utilities';
8
8
  @import 'base';
9
9
  @import 'components';
10
+ @import 'swiper';
10
11
  @import 'custom';
11
-
12
+ @import 'themes/two';
@@ -0,0 +1,95 @@
1
+ // two.scss
2
+ $sq-color-primary: #21169a;
3
+ $sq-color-secondary: #f5590b;
4
+ $sq-color-accent: #f59e0b;
5
+ $sq-color-muted-text: #272164;
6
+ $sq-color-muted-bg: #e4e2de;
7
+ $sq-color-light: #e4e2de;
8
+ $sq-color-body-bg: #f4f1eb;
9
+ $sq-color-body-text: #201a63;
10
+ $sq-color-heading-text: #272164;
11
+
12
+ .theme-two {
13
+ --sq-color-primary: #{$sq-color-primary};
14
+ --sq-color-primary-contrast: #{sq-color-contrast($sq-color-primary, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
15
+ --sq-color-secondary: #{$sq-color-secondary};
16
+ --sq-color-secondary-contrast: #{sq-color-contrast($sq-color-secondary, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
17
+ --sq-color-accent: #{$sq-color-accent};
18
+ --sq-color-accent-contrast: #{sq-color-contrast($sq-color-accent, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
19
+ --sq-color-muted-text: #{$sq-color-muted-text};
20
+ --sq-color-muted-bg: #{$sq-color-muted-bg};
21
+ --sq-color-light: #{$sq-color-light};
22
+ --sq-color-body-bg: #{$sq-color-body-bg};
23
+ --sq-color-body-text: #{$sq-color-body-text};
24
+ --sq-color-heading-text: #{$sq-color-heading-text};
25
+ --sq-font-sans: 'TASA Orbiter', sans-serif;
26
+ --sq-font-heading: 'TASA Orbiter', sans-serif;
27
+ --sq-radius-lg: 32px;
28
+
29
+ // Page Transitions
30
+ --sq-page-transition-from: var(--sq-color-secondary);
31
+ --sq-page-transition-via: var(--sq-color-accent);
32
+ --sq-page-transition-to: var(--sq-color-accent);
33
+
34
+ // Explicit overrides
35
+ --sq-btn-primary-bg: var(--sq-color-primary);
36
+ --sq-btn-primary-text: var(--sq-color-primary-contrast);
37
+ --sq-btn-secondary-bg: var(--sq-color-secondary);
38
+ --sq-btn-secondary-text: var(--sq-color-secondary-contrast);
39
+ --sq-btn-accent-bg: var(--sq-color-accent);
40
+ --sq-btn-accent-text: var(--sq-color-accent-contrast);
41
+ --sq-form-focus-border: var(--sq-color-secondary);
42
+
43
+ h1,
44
+ h2,
45
+ h3,
46
+ h4,
47
+ h5,
48
+ h6 {
49
+ font-family: var(--sq-font-heading);
50
+ letter-spacing: -0.04em;
51
+ }
52
+
53
+ .btn {
54
+ --sq-btn-radius: 50rem;
55
+ }
56
+
57
+ @apply selection:bg-secondary selection:text-black;
58
+ }
59
+
60
+ .theme-two.sq-theme-dark {
61
+ $sq-color-primary-dark: #16239a;
62
+ $sq-color-secondary-dark: #ffc156;
63
+ $sq-color-accent-dark: #f59e0b;
64
+ $sq-color-muted-text: #c4d3d3;
65
+ $sq-color-muted-bg: #202052;
66
+ $sq-color-body-bg: #181843;
67
+ $sq-color-body-text: #c4d3d3;
68
+ $sq-color-heading-text: #ffffff;
69
+
70
+ --sq-color-primary: #{$sq-color-primary-dark};
71
+ --sq-color-primary-contrast: #{sq-color-contrast($sq-color-primary-dark, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
72
+ --sq-color-secondary: #{$sq-color-secondary-dark};
73
+ --sq-color-secondary-contrast: #{sq-color-contrast($sq-color-secondary-dark, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
74
+ --sq-color-accent: #{$sq-color-accent-dark};
75
+ --sq-color-accent-contrast: #{sq-color-contrast($sq-color-accent-dark, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
76
+
77
+ --sq-color-primary: #{$sq-color-primary-dark};
78
+ --sq-color-primary-contrast: #{sq-color-contrast($sq-color-primary-dark, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
79
+ --sq-color-secondary: #{$sq-color-secondary-dark};
80
+ --sq-color-secondary-contrast: #{sq-color-contrast($sq-color-secondary-dark, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
81
+ --sq-color-accent: #{$sq-color-accent-dark};
82
+ --sq-color-accent-contrast: #{sq-color-contrast($sq-color-accent-dark, $sq-contrast-light-text, $sq-contrast-dark-text, $sq-min-contrast-ratio)};
83
+
84
+ --sq-color-muted-text: #{$sq-color-muted-text};
85
+ --sq-color-muted-bg: #{$sq-color-muted-bg};
86
+
87
+ --sq-color-body-bg: #{$sq-color-body-bg};
88
+ --sq-color-body-text: #{$sq-color-body-text};
89
+ --sq-color-heading-text: #{$sq-color-heading-text};
90
+
91
+ --sq-sticky-bg: #{$sq-color-muted-bg};
92
+ --sq-sticky-text: #{$sq-color-body-text};
93
+
94
+ --sq-card-bg: #{$sq-color-body-bg};
95
+ }
@@ -30,17 +30,17 @@ ob_start();
30
30
  ?>
31
31
 
32
32
  <section class="py-32 md:py-48 flex items-center justify-center bg-gradient-to-b from-zinc-100 to-transparent" data-uk-height-viewport="offset-top: true">
33
- <div class="max-w-4xl mx-auto px-6 text-center">
33
+ <div class="max-w-4xl mx-auto px-6 text-center" data-gsap-timeline="{defaults: {duration: 1.2, ease: 'power4.out', delay: 0.8}}">
34
34
 
35
- <h2 class="text-6xl font-bold tracking-tight mb-8 lg:px-8">
36
- Start building<span class="opacity-30"> creative websites</span> with Squeditor
35
+ <h2 class="text-6xl font-bold tracking-tight mb-8 lg:px-8" data-gsap="from: {y: 60, opacity: 0, filter: 'blur(20px)'}">
36
+ Start building <span class="bg-gradient-to-r from-zinc-500 via-zinc-400 to-zinc-200 text-transparent bg-clip-text">creative websites</span> with Squeditor
37
37
  </h2>
38
38
 
39
- <p class="text-xl text-muted max-w-2xl mx-auto font-light leading-relaxed mb-12">
39
+ <p class="text-xl text-muted max-w-2xl mx-auto font-light leading-relaxed mb-12" data-gsap="position: '<-0.5'; from: {y: 40, opacity: 0, filter: 'blur(10px)'}">
40
40
  You can now begin creating interactive GSAP experiences, modifying the UIKit configuration, and adding content to your template files.
41
41
  </p>
42
42
 
43
- <div class="flex items-center justify-center gap-4">
43
+ <div class="flex items-center justify-center gap-4" data-gsap="position: '<-0.5'; from: {y: 20, opacity: 0, filter: 'blur(5px)'}">
44
44
  <a href="https://squeditor.com/" class="btn btn-lg btn-secondary !px-8 !py-3 !rounded-full text-white font-medium hover:scale-105 transition-transform">
45
45
  Try Squeditor
46
46
  </a>
@@ -11,9 +11,45 @@ require_once __DIR__ . '/config/site-config.php';
11
11
  define('SRC_PATH', __DIR__);
12
12
 
13
13
  // Global Theme & Schema Detection
14
- $active_theme = $_GET['theme'] ?? 'default';
14
+ $active_theme = $_GET['theme'] ?? null;
15
15
  $active_schema = $_GET['schema'] ?? 'light';
16
16
 
17
+ if (!$active_theme) {
18
+ if (file_exists(__DIR__ . '/config/active-themes.php')) {
19
+ require_once __DIR__ . '/config/active-themes.php';
20
+
21
+ $current_uri = $_SERVER['REQUEST_URI'] ?? '/';
22
+ $path_only = parse_url($current_uri, PHP_URL_PATH);
23
+
24
+ // Strip trailing slash except for root
25
+ if ($path_only !== '/' && substr($path_only, -1) === '/') {
26
+ $path_only = rtrim($path_only, '/');
27
+ }
28
+
29
+ // Normalize .html requests to .php for config matching
30
+ if (substr($path_only, -5) === '.html') {
31
+ $path_only = substr($path_only, 0, -5) . '.php';
32
+ }
33
+ // Normalize extensionless requests to .php for config matching
34
+ elseif ($path_only !== '/' && !pathinfo($path_only, PATHINFO_EXTENSION)) {
35
+ $path_only .= '.php';
36
+ }
37
+
38
+ // Check exact match, also check matching without leading slash
39
+ $path_no_slash = ltrim($path_only, '/');
40
+
41
+ if (isset($active_themes[$path_only])) {
42
+ $active_theme = $active_themes[$path_only];
43
+ } elseif (isset($active_themes[$path_no_slash])) {
44
+ $active_theme = $active_themes[$path_no_slash];
45
+ } else {
46
+ $active_theme = 'default';
47
+ }
48
+ } else {
49
+ $active_theme = 'default';
50
+ }
51
+ }
52
+
17
53
  // Add to site config for template access
18
54
  $site['theme'] = $active_theme;
19
- $site['schema'] = $active_schema;
55
+ $site['schema'] = $active_schema;
@@ -13,6 +13,12 @@ if (!$is_snapshot) {
13
13
  fclose($fp);
14
14
  }
15
15
  }
16
+
17
+ // Load slider config for conditional CSS loading
18
+ $active_slider = false;
19
+ if (file_exists(__DIR__ . '/../config/active-slider.php')) {
20
+ require_once __DIR__ . '/../config/active-slider.php';
21
+ }
16
22
  ?>
17
23
  <meta charset="UTF-8">
18
24
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -36,6 +42,7 @@ if (!$is_snapshot) {
36
42
  <script type="module" src="<?= $vite_server ?>/@vite/client"></script>
37
43
  <link rel="stylesheet" href="<?= $vite_server ?>/assets/css/tailwind.css">
38
44
  <link rel="stylesheet" href="<?= $vite_server ?>/assets/css/squeditor-icons.css">
45
+ <?php if ($active_slider): ?><link rel="stylesheet" href="<?= $vite_server ?>/assets/css/slider.min.css"><?php endif; ?>
39
46
  <script type="module" src="<?= $vite_server ?>/assets/scss/main.scss"></script>
40
47
  <style>html.js-fouc { opacity: 0; transition: opacity 0.15s ease-out; }</style>
41
48
  <script>document.documentElement.classList.add('js-fouc');</script>
@@ -43,5 +50,6 @@ if (!$is_snapshot) {
43
50
  <!-- Production Static Assets -->
44
51
  <link rel="stylesheet" href="assets/css/squeditor-icons.css">
45
52
  <link rel="stylesheet" href="assets/css/tailwind.css">
46
- <link rel="stylesheet" href="assets/css/main_css.css">
53
+ <?php if ($active_slider): ?><link rel="stylesheet" href="assets/css/slider.min.css"><?php endif; ?>
54
+ <link rel="stylesheet" href="assets/css/main.min.css">
47
55
  <?php endif; ?>
@@ -0,0 +1,87 @@
1
+ <?php
2
+ // src/pages/swiper-test.php
3
+ require_once __DIR__ . '/init.php';
4
+
5
+ // Content
6
+ ob_start();
7
+ ?>
8
+
9
+ <main id="main-content">
10
+ <section class="py-24">
11
+ <div class="max-w-7xl mx-auto px-6">
12
+ <h1 class="text-4xl md:text-5xl font-bold mb-4 text-center">Multi-Slider Architecture</h1>
13
+ <p class="text-lg text-muted text-center max-w-2xl mx-auto mb-16">
14
+ Test the dynamic library injection framework. Switch between <code>swiper</code> and <code>splide</code> in your <code>squeditor.config.js</code> to see Vite dynamically bundle the correct dependencies or set <code>false</code> to disable it!
15
+ </p>
16
+
17
+ <!-- Splide Test -->
18
+ <div class="mb-16 border-t pt-16">
19
+ <div class="flex items-center justify-between">
20
+ <h2 class="text-2xl font-bold mb-6">Splide Slider</h2>
21
+ <p class="text-xs font-mono uppercase tracking-widest text-muted mb-4">Requires <code class="text-xs">slider: { library: 'splide' }</code> -> squeditor.config.js</p>
22
+ </div>
23
+ <div class="splide" data-splide='{
24
+ "type" : "blurfade",
25
+ "perMove" : 1,
26
+ "gap" : 16,
27
+ "autoplay" : true,
28
+ "interval" : 3000,
29
+ "pagination" : true,
30
+ "breakpoints": {
31
+ "1024": { "perPage": 1, "gap": "1rem" },
32
+ "768" : { "perPage": 1, "gap": "0.75rem" },
33
+ "480" : { "perPage": 1, "arrows": false }
34
+ }
35
+ }'>
36
+ <div class="splide__track">
37
+ <ul class="splide__list">
38
+ <?php for($i=1; $i<=6; $i++): ?>
39
+ <li class="splide__slide">
40
+ <div class="bg-muted text-dark h-full min-h-[24rem] rounded-2xl flex items-center justify-center text-2xl font-bold">Slide <?= $i ?></div>
41
+ </li>
42
+ <?php endfor; ?>
43
+ </ul>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <style>
49
+ .splide__pagination__page.is-active {
50
+ background: var(--sq-color-primary);
51
+ }
52
+ </style>
53
+
54
+ <!-- Demo 1: Basic Responsive Grid -->
55
+ <div class="mb-8 border-t pt-16 mt-16">
56
+ <div class="flex items-center justify-between">
57
+ <h2 class="text-2xl font-bold mb-6">Swiper Slider</h2>
58
+ <p class="text-xs font-mono uppercase tracking-widest text-muted mb-4">Requires <code class="text-xs">slider: { library: 'swiper' }</code> -> squeditor.config.js</p>
59
+ </div>
60
+ <div class="swiper rounded-2xl overflow-hidden shadow-sm"
61
+ data-sq-swiper="
62
+ slidesPerView: 1;
63
+ spaceBetween: 20;
64
+ breakpoints: {
65
+ sm: { slidesPerView: 2 },
66
+ md: { slidesPerView: 3 },
67
+ lg: { slidesPerView: 4 }
68
+ }"
69
+ >
70
+ <div class="swiper-wrapper">
71
+ <?php for($i=1; $i<=6; $i++): ?>
72
+ <div class="swiper-slide">
73
+ <div class="bg-muted text-dark aspect-square rounded-2xl flex items-center justify-center text-2xl font-bold">Slide <?= $i ?></div>
74
+ </div>
75
+ <?php endfor; ?>
76
+ </div>
77
+ <div class="swiper-pagination mt-6 relative !bottom-0 pb-4"></div>
78
+ </div>
79
+ </div>
80
+
81
+ </div>
82
+ </section>
83
+ </main>
84
+
85
+ <?php
86
+ $content = ob_get_clean();
87
+ require SRC_PATH . '/page-templates/base.php';
@@ -11,15 +11,15 @@ if ($layout !== 'default') {
11
11
  }
12
12
  ?>
13
13
  <!-- Site Header -->
14
- <header id="sq_header" class="sq--header absolute w-full top-0 z-[1000]" data-uk-sticky="top: 80; cls-active: sq-header--sticky; animation: uk-animation-slide-top">
14
+ <header id="sq_header" class="sq--header absolute w-full top-0 z-[1000] bg-white dark:bg-zinc-900" data-gsap="from: {y: -80, opacity: 0}; to: {y: 0, opacity: 1}; duration: 1.2; ease: expo.inOut;">
15
15
  <div class="sq--header--inner max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
16
16
 
17
17
  <!-- Logo -->
18
18
  <a href="index.html" class="sq--logo flex items-center gap-2 group">
19
19
  <div class="w-8 h-8 sq-bg-secondary rounded-lg flex items-center justify-center transition-transform group-hover:scale-110">
20
- <i class="sq-icon-zap"></i>
20
+ <i class="sq-icon-waves"></i>
21
21
  </div>
22
- <h1 class="font-bold text-xl m-0 tracking-tight hidden md:inline-flex"><?= htmlspecialchars($site['name']) ?></h1>
22
+ <h1 class="font-bold text-xl m-0 tracking-tight"><?= htmlspecialchars($site['name']) ?></h1>
23
23
  </a>
24
24
 
25
25
  <!-- Desktop Nav (Centered) -->
@@ -30,11 +30,11 @@ if ($layout !== 'default') {
30
30
  <!-- Right Side / CTA -->
31
31
  <div class="sq--header--cta flex items-center gap-6">
32
32
  <a href="#" class="sq-nav-link text-base font-medium text-zinc-900 dark:text-white !text-opacity-70 hover:text-secondary hover:!text-opacity-100 transition-colors hidden md:inline-block">Log in</a>
33
- <a href="https://squeditor.com/" class="btn btn-secondary">Try Squeditor</a>
33
+ <a href="https://squeditor.com/" class="btn btn-secondary !hidden md:!inline-flex">Try Squeditor</a>
34
34
 
35
35
  <!-- Mobile Toggle -->
36
- <button class="sq--header--mobile--toggle md:hidden" data-uk-toggle="target: #sq_mobile_offcanvas" type="button" aria-label="Menu">
37
- <span data-uk-icon="icon: menu; ratio: 1"></span>
36
+ <button class="sq--header--mobile--toggle md:hidden" data-gsap-toggle="#sq_offcanvas_creative" type="button" aria-label="Menu">
37
+ <span data-uk-icon="icon: menu; ratio: 1.5"></span>
38
38
  </button>
39
39
  </div>
40
40
  </div>
@@ -48,18 +48,18 @@ if ($layout !== 'default') {
48
48
  </div>
49
49
 
50
50
  <!-- Creative Offcanvas -->
51
- <div id="sq-offcanvas-creative" class="sq-offcanvas-creative fixed inset-0 z-[9999] pointer-events-none text-zinc-600 shadow-2xl" data-gsap-timeline="{paused: true}">
51
+ <div id="sq_offcanvas_creative" class="sq-offcanvas-creative fixed inset-0 z-[9999] pointer-events-none text-zinc-600 shadow-2xl" style="visibility: hidden" data-gsap-timeline="{paused: true}">
52
52
 
53
53
  <!-- Backdrop Overlay -->
54
- <div class="sq-offcanvas-creative--close fixed inset-0 z-10 pointer-events-auto bg-black/50 backdrop-blur-md" data-gsap-reverse="#sq-offcanvas-creative" data-gsap="position: '<'; from: {autoAlpha: 0, duration: 0.25, ease: 'power3.out'}"></div>
54
+ <div class="sq-offcanvas-creative--close fixed inset-0 z-10 pointer-events-auto bg-black/50 backdrop-blur-md" data-gsap-reverse="#sq_offcanvas_creative" data-gsap="position: '<'; from: {autoAlpha: 0, duration: 0.25, ease: 'power3.out'}"></div>
55
55
 
56
56
  <!-- Offcanvas Background Panel -->
57
- <div class="absolute top-0 left-0 w-full md:w-1/3 h-full bg-white shadow-2xl flex flex-col items-start justify-start z-20 pointer-events-auto" data-gsap="from: {xPercent: -100, duration: 0.6, ease: 'power4.inOut'}" data-sq-cursor-color="#4f46e5">
57
+ <div class="absolute top-0 left-0 w-full md:w-1/3 h-full bg-white shadow-2xl flex flex-col items-start justify-start z-20 pointer-events-auto" data-gsap="position: '-=0.3'; from: {xPercent: -100, duration: 0.6, ease: 'power4.inOut'}" data-sq-cursor-color="#4f46e5">
58
58
 
59
59
  <!-- Header -->
60
60
  <div class="flex items-center justify-between w-full p-6 lg:p-12" data-gsap="from: {yPercent: -20, opacity: 0, duration: 0.4, ease: 'power3.out'}">
61
61
  <a href="#" class="sq-logo text-xl font-bold text-zinc-900" data-sq-cursor-stick>Squeditor</a>
62
- <button data-gsap-reverse="#sq-offcanvas-creative" class="opacity-50 hover:opacity-100 transition-opacity" data-sq-cursor-stick>
62
+ <button data-gsap-reverse="#sq_offcanvas_creative" class="opacity-50 hover:opacity-100 transition-opacity" data-sq-cursor-stick>
63
63
  <span data-uk-icon="icon: close; ratio: 1.5"></span>
64
64
  </button>
65
65
  </div>
@@ -117,4 +117,4 @@ if ($layout !== 'default') {
117
117
  </a>
118
118
  </div>
119
119
  </div>
120
- </div>
120
+ </div>
@@ -6,6 +6,7 @@ const projectRoot = process.cwd();
6
6
  const config = require(path.join(projectRoot, 'squeditor.config.js'));
7
7
  const fwRoot = path.resolve(projectRoot, config.framework); // resolves ..
8
8
  const manifest = require(path.join(fwRoot, 'uikit-manifest.json'));
9
+ const resolvePages = require('./utils/resolve-pages');
9
10
 
10
11
 
11
12
  const selectedComponents = config.components || [];
@@ -82,12 +83,34 @@ console.log(` Components included: _core, ${selectedComponents.join(', ')}`);
82
83
  // Generate src/config/active-components.php for the style-guide page
83
84
  const phpConfigDir = path.join(projectRoot, 'src/config');
84
85
  fs.mkdirSync(phpConfigDir, { recursive: true });
85
- const phpConfig = `<?php\n// Auto-generated by build-components.js DO NOT EDIT\n$active_components = ${JSON.stringify(selectedComponents)};\n`;
86
+ const phpComponentsArray = `[${selectedComponents.map(c => `'${c}'`).join(', ')}]`;
87
+ const phpConfig = `<?php\n// Auto-generated by build-components.js — DO NOT EDIT\n$active_components = ${phpComponentsArray};\n`;
86
88
  fs.writeFileSync(
87
89
  path.join(phpConfigDir, 'active-components.php'),
88
90
  phpConfig
89
91
  );
90
92
 
93
+ // Generate src/config/active-themes.php for automatic PHP dev server theme detection
94
+ // Map each page to its corresponding theme identifier
95
+ const themePageMapping = {};
96
+ if (config.themes) {
97
+ Object.keys(config.themes).forEach(themeKey => {
98
+ const rawPages = config.themes[themeKey].pages || [];
99
+ const absolutePages = resolvePages(rawPages, projectRoot);
100
+ absolutePages.forEach(page => {
101
+ // Clean leading slashes for normalization if desired, but here we just map exact values
102
+ themePageMapping[page] = themeKey;
103
+ });
104
+ });
105
+ }
106
+ const phpThemesArrayParts = Object.entries(themePageMapping).map(([page, theme]) => `'${page}' => '${theme}'`);
107
+ const phpThemesConfig = `<?php\n// Auto-generated by build-components.js — DO NOT EDIT\n$active_themes = [${phpThemesArrayParts.join(', ')}];\n`;
108
+ fs.writeFileSync(
109
+ path.join(phpConfigDir, 'active-themes.php'),
110
+ phpThemesConfig
111
+ );
112
+
113
+
91
114
  // Inject all themes into main.scss so live-switching works in the Style Guide
92
115
  const mainScssPath = path.join(projectRoot, 'src/assets/scss/main.scss');
93
116
  if (fs.existsSync(mainScssPath) && config.themes) {
@@ -107,3 +130,57 @@ if (fs.existsSync(mainScssPath) && config.themes) {
107
130
  fs.writeFileSync(mainScssPath, mainScss);
108
131
  console.log(`[Squeditor] 🎨 Injected themes: ${Object.keys(config.themes).join(', ')}`);
109
132
  }
133
+
134
+ // Generate Dynamic Slider Config Import
135
+ const sliderConfig = config.slider || { library: false };
136
+ const dynamicSliderPath = path.join(outputJsDir, '_slider_dynamic.js');
137
+ let sliderImportCode = '// Auto-generated by build-components.js — DO NOT EDIT\n';
138
+ if (sliderConfig.library === 'swiper') {
139
+ sliderImportCode += 'import \'./modules/swiper-init.js\';\n';
140
+ } else if (sliderConfig.library === 'splide') {
141
+ sliderImportCode += 'import \'./modules/splide-init.js\';\n';
142
+ }
143
+ fs.writeFileSync(dynamicSliderPath, sliderImportCode);
144
+ console.log(`[Squeditor] 🎠 Injected slider library: ${sliderConfig.library || 'none'}`);
145
+
146
+ // Copy slider library CSS to src/assets/css/slider.min.css
147
+ // This keeps the CSS separate from main.js and gives it a clear, descriptive filename
148
+ const sliderCssDest = path.join(projectRoot, 'src/assets/css/slider.min.css');
149
+ if (sliderConfig.library === 'splide') {
150
+ const splideCssPath = path.join(projectRoot, 'node_modules/@splidejs/splide/dist/css/splide.min.css');
151
+ if (fs.existsSync(splideCssPath)) {
152
+ fs.copyFileSync(splideCssPath, sliderCssDest);
153
+ console.log('[Squeditor] 📎 Copied Splide CSS → src/assets/css/slider.min.css');
154
+ } else {
155
+ console.warn('[Squeditor] ⚠️ Splide CSS not found at expected path.');
156
+ fs.writeFileSync(sliderCssDest, '/* Splide CSS not found */\n');
157
+ }
158
+ } else if (sliderConfig.library === 'swiper') {
159
+ // Concatenate all Swiper CSS modules into a single file
160
+ const swiperCssParts = [
161
+ 'node_modules/swiper/swiper.min.css',
162
+ 'node_modules/swiper/modules/navigation.min.css',
163
+ 'node_modules/swiper/modules/pagination.min.css',
164
+ 'node_modules/swiper/modules/effect-fade.min.css',
165
+ 'node_modules/swiper/modules/free-mode.min.css',
166
+ ];
167
+ let combinedCss = '/* Swiper CSS - auto-generated by build-components.js */\n';
168
+ for (const part of swiperCssParts) {
169
+ const fullPath = path.join(projectRoot, part);
170
+ if (fs.existsSync(fullPath)) {
171
+ combinedCss += fs.readFileSync(fullPath, 'utf8') + '\n';
172
+ }
173
+ }
174
+ fs.writeFileSync(sliderCssDest, combinedCss);
175
+ console.log('[Squeditor] 📎 Copied Swiper CSS → src/assets/css/slider.min.css');
176
+ } else {
177
+ // No slider configured — write an empty placeholder so head.php link doesn't 404
178
+ fs.writeFileSync(sliderCssDest, '/* No slider library configured */\n');
179
+ }
180
+
181
+ // Generate src/config/active-slider.php for conditional CSS loading in head.php
182
+ const phpSliderConfig = `<?php\n// Auto-generated by build-components.js — DO NOT EDIT\n$active_slider = ${sliderConfig.library ? `'${sliderConfig.library}'` : 'false'};\n`;
183
+ fs.writeFileSync(
184
+ path.join(phpConfigDir, 'active-slider.php'),
185
+ phpSliderConfig
186
+ );
@@ -145,6 +145,32 @@ async function run() {
145
145
  } else {
146
146
  console.warn(`[Squeditor] ⚠️ Source directory for static assets not found: ${srcDir}`);
147
147
  }
148
+
149
+ // Post-Vite-build CSS renaming for clearer dist output
150
+ const distCssDir = path.join(projectRoot, config.snapshot.outputDir, 'assets/css');
151
+
152
+ // Rename main_css.css → main.min.css (SCSS entry output)
153
+ const mainCssSrc = path.join(distCssDir, 'main_css.css');
154
+ const mainCssDest = path.join(distCssDir, 'main.min.css');
155
+ if (fs.existsSync(mainCssSrc)) {
156
+ fs.renameSync(mainCssSrc, mainCssDest);
157
+ console.log('[Squeditor] 📎 Renamed main_css.css → main.min.css');
158
+ }
159
+
160
+ // Remove stale main.css (previously contained CSS-in-JS extracted by Vite, now handled separately)
161
+ const staleMainCss = path.join(distCssDir, 'main.css');
162
+ if (fs.existsSync(staleMainCss)) {
163
+ fs.unlinkSync(staleMainCss);
164
+ }
165
+
166
+ // Copy slider.min.css to dist (generated by build-components.js)
167
+ const sliderCssSrc = path.join(projectRoot, 'src/assets/css/slider.min.css');
168
+ const sliderCssDest = path.join(distCssDir, 'slider.min.css');
169
+ if (fs.existsSync(sliderCssSrc)) {
170
+ fs.mkdirSync(distCssDir, { recursive: true });
171
+ fs.copyFileSync(sliderCssSrc, sliderCssDest);
172
+ console.log('[Squeditor] 📎 Copied slider.min.css → dist/assets/css/');
173
+ }
148
174
  }
149
175
 
150
176
  run();
@@ -19,5 +19,14 @@ if (preg_match('/\.html$/', $uri)) {
19
19
  }
20
20
  }
21
21
 
22
- // 3. Let PHP's core handle index.php fallback for directories or 404s
22
+ // 3. If it is an extensionless request, route it to the equivalent PHP file
23
+ if ($uri !== '/' && !pathinfo($uri, PATHINFO_EXTENSION)) {
24
+ $php_file = $uri . '.php';
25
+ if (file_exists($doc_root . $php_file)) {
26
+ require $doc_root . $php_file;
27
+ return true;
28
+ }
29
+ }
30
+
31
+ // 4. Let PHP's core handle index.php fallback for directories or 404s
23
32
  return false;
package/scripts/dev.js CHANGED
@@ -23,6 +23,18 @@ async function startDev() {
23
23
  fwRoot = config.framework;
24
24
  }
25
25
  }
26
+
27
+ // Run build-components.js BEFORE starting servers to ensure
28
+ // active-themes.php, _uikit_dynamic.scss, _slider_dynamic.js etc. exist on first request
29
+ const buildComponentsPath = path.join(fwRoot, 'scripts/build-components.js');
30
+ console.log('[Squeditor] 🔧 Building dynamic components...');
31
+ const { execSync } = require('child_process');
32
+ try {
33
+ execSync(`node "${buildComponentsPath}"`, { stdio: 'inherit', cwd: projectRoot });
34
+ } catch (e) {
35
+ console.error('[Squeditor] ❌ Failed to build dynamic components:', e.message);
36
+ }
37
+
26
38
  const devRouterPath = path.join(fwRoot, 'scripts/dev-router.php');
27
39
 
28
40
  // Start PHP Server
@@ -35,7 +47,6 @@ async function startDev() {
35
47
  env: { ...process.env, SQUEDITOR_PHP_PORT: phpPort, SQUEDITOR_VITE_PORT: vitePort }
36
48
  });
37
49
 
38
- // Start Vite
39
50
  const vite = spawn('npx', [
40
51
  'vite',
41
52
  '--port', vitePort.toString(),
@@ -45,6 +56,24 @@ async function startDev() {
45
56
  env: { ...process.env, SQUEDITOR_PHP_PORT: phpPort, SQUEDITOR_VITE_PORT: vitePort }
46
57
  });
47
58
 
59
+ // Watch squeditor.config.js for changes and rebuild dynamic components
60
+ if (fs.existsSync(configPath)) {
61
+ let rebuildTimeout;
62
+ fs.watch(configPath, (eventType) => {
63
+ if (eventType === 'change') {
64
+ // Debounce to prevent multiple triggers from IDE saves
65
+ clearTimeout(rebuildTimeout);
66
+ rebuildTimeout = setTimeout(() => {
67
+ console.log(`\n[Squeditor] 🔄 Config changed. Rebuilding dynamic components...`);
68
+ const buildScript = spawn('node', [path.join(fwRoot, 'scripts/build-components.js')], { stdio: 'inherit' });
69
+ buildScript.on('close', (code) => {
70
+ if (code === 0) console.log(`[Squeditor] ✨ Rebuild complete! (Vite will hot-reload automatically)`);
71
+ });
72
+ }, 300);
73
+ }
74
+ });
75
+ }
76
+
48
77
  process.on('SIGINT', () => {
49
78
  php.kill();
50
79
  vite.kill();