create-nextblock 0.14.2 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +375 -117
  3. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +2 -2
  4. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +17 -22
  5. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +13 -19
  6. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +45 -34
  7. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -0
  8. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -0
  9. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -24
  10. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -0
  11. package/templates/nextblock-template/app/layout.tsx +49 -0
  12. package/templates/nextblock-template/app/providers.tsx +16 -4
  13. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +7 -10
  14. package/templates/nextblock-template/components/theme-icon.tsx +78 -0
  15. package/templates/nextblock-template/components/theme-switcher.tsx +85 -90
  16. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -0
  17. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -0
  18. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +114 -0
  19. package/templates/nextblock-template/lib/blocks/blockColors.ts +132 -0
  20. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +17 -1
  21. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +92 -87
  22. package/templates/nextblock-template/lib/themes/buildThemeCss.test.ts +163 -0
  23. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -0
  24. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -0
  25. package/templates/nextblock-template/lib/themes/tokens.ts +143 -0
  26. package/templates/nextblock-template/next-env.d.ts +2 -2
  27. package/templates/nextblock-template/package.json +1 -1
  28. package/templates/nextblock-template/scripts/verify-site-themes.ts +63 -0
  29. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,37 +1,37 @@
1
- // AUTO-GENERATED FILE. Do not edit this file directly.
2
- // Regenerate it from repo root with: npm run generate:sandbox
3
- // Source of truth: tools/scripts/generate-sandbox-reset.ts and libs/db/src/supabase/migrations/*.sql
4
-
5
- export const SANDBOX_RESET_SQL = `
6
- -- AUTO-GENERATED: Sandbox Reset Script. Do Not Edit Manually.
7
- -- Generated by tools/scripts/generate-sandbox-reset.ts
8
-
9
- -- Step A: Erase all non-demo users
10
- DELETE FROM auth.users
11
- WHERE id NOT IN (
12
- '63a8aa07-ef6e-4cc7-bafa-ece023c74366'::uuid,
13
- 'e3a2c121-70a4-4239-a9df-f70ce717f5a1'::uuid
14
- );
15
-
16
- -- Step B: Wipe public schema and truncate migrations
17
- DROP SCHEMA public CASCADE;
18
- CREATE SCHEMA public;
19
-
20
- -- Restore default public schema privileges so the standard migrations succeed
21
- GRANT USAGE ON SCHEMA public TO postgres, anon, authenticated, service_role;
22
- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres, anon, authenticated, service_role;
23
- GRANT ALL PRIVILEGES ON ALL ROUTINES IN SCHEMA public TO postgres, anon, authenticated, service_role;
24
- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO postgres, anon, authenticated, service_role;
25
- ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
26
- ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON ROUTINES TO postgres, anon, authenticated, service_role;
27
- ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
28
-
29
- TRUNCATE TABLE supabase_migrations.schema_migrations;
30
-
31
- -- Step C: Execute full schema & seed from production migrations
32
-
33
-
34
- -- >>> FROM: 00000000000000_baseline_schema.sql <<<
1
+ // AUTO-GENERATED FILE. Do not edit this file directly.
2
+ // Regenerate it from repo root with: npm run generate:sandbox
3
+ // Source of truth: tools/scripts/generate-sandbox-reset.ts and libs/db/src/supabase/migrations/*.sql
4
+
5
+ export const SANDBOX_RESET_SQL = `
6
+ -- AUTO-GENERATED: Sandbox Reset Script. Do Not Edit Manually.
7
+ -- Generated by tools/scripts/generate-sandbox-reset.ts
8
+
9
+ -- Step A: Erase all non-demo users
10
+ DELETE FROM auth.users
11
+ WHERE id NOT IN (
12
+ '63a8aa07-ef6e-4cc7-bafa-ece023c74366'::uuid,
13
+ 'e3a2c121-70a4-4239-a9df-f70ce717f5a1'::uuid
14
+ );
15
+
16
+ -- Step B: Wipe public schema and truncate migrations
17
+ DROP SCHEMA public CASCADE;
18
+ CREATE SCHEMA public;
19
+
20
+ -- Restore default public schema privileges so the standard migrations succeed
21
+ GRANT USAGE ON SCHEMA public TO postgres, anon, authenticated, service_role;
22
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres, anon, authenticated, service_role;
23
+ GRANT ALL PRIVILEGES ON ALL ROUTINES IN SCHEMA public TO postgres, anon, authenticated, service_role;
24
+ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO postgres, anon, authenticated, service_role;
25
+ ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
26
+ ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON ROUTINES TO postgres, anon, authenticated, service_role;
27
+ ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
28
+
29
+ TRUNCATE TABLE supabase_migrations.schema_migrations;
30
+
31
+ -- Step C: Execute full schema & seed from production migrations
32
+
33
+
34
+ -- >>> FROM: 00000000000000_baseline_schema.sql <<<
35
35
  -- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.
36
36
  -- 00 · schema: enums, functions, tables, sequences, defaults
37
37
  -- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.
@@ -2486,9 +2486,9 @@ CREATE TABLE IF NOT EXISTS public.variant_attribute_mapping (
2486
2486
  variant_id uuid NOT NULL,
2487
2487
  attribute_term_id uuid NOT NULL
2488
2488
  );
2489
-
2490
-
2491
- -- >>> FROM: 00000000000001_baseline_constraints_and_indexes.sql <<<
2489
+
2490
+
2491
+ -- >>> FROM: 00000000000001_baseline_constraints_and_indexes.sql <<<
2492
2492
  -- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.
2493
2493
  -- 01 · constraints (PK / unique / check / FK) + indexes
2494
2494
  -- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.
@@ -3560,9 +3560,9 @@ DO $rb$ BEGIN
3560
3560
  ADD CONSTRAINT variant_attribute_mapping_variant_id_fkey FOREIGN KEY (variant_id) REFERENCES public.product_variants(id) ON DELETE CASCADE;
3561
3561
  END IF;
3562
3562
  END $rb$;
3563
-
3564
-
3565
- -- >>> FROM: 00000000000002_baseline_security_and_grants.sql <<<
3563
+
3564
+
3565
+ -- >>> FROM: 00000000000002_baseline_security_and_grants.sql <<<
3566
3566
  -- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.
3567
3567
  -- 02 · row-level security, policies, triggers, grants
3568
3568
  -- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.
@@ -4898,9 +4898,9 @@ CREATE TRIGGER on_auth_user_created
4898
4898
  FOR EACH ROW
4899
4899
  EXECUTE FUNCTION public.handle_new_user();
4900
4900
  REVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;
4901
-
4902
-
4903
- -- >>> FROM: 00000000000003_baseline_seed.sql <<<
4901
+
4902
+
4903
+ -- >>> FROM: 00000000000003_baseline_seed.sql <<<
4904
4904
  -- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.
4905
4905
  -- 03 · seed: canonical NextBlock demo content (no users, no secrets)
4906
4906
  -- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.
@@ -5369,9 +5369,9 @@ SELECT pg_catalog.setval('public.languages_id_seq', COALESCE((SELECT MAX(id) FRO
5369
5369
  SELECT pg_catalog.setval('public.navigation_items_id_seq', COALESCE((SELECT MAX(id) FROM public.navigation_items), 1), true);
5370
5370
  SELECT pg_catalog.setval('public.pages_id_seq', COALESCE((SELECT MAX(id) FROM public.pages), 1), true);
5371
5371
  SELECT pg_catalog.setval('public.posts_id_seq', COALESCE((SELECT MAX(id) FROM public.posts), 1), true);
5372
-
5373
-
5374
- -- >>> FROM: 00000000000004_default_logo_email_safe.sql <<<
5372
+
5373
+
5374
+ -- >>> FROM: 00000000000004_default_logo_email_safe.sql <<<
5375
5375
  -- Swap the seeded DEFAULT site logo from the WebP asset to a bundled PNG.
5376
5376
  --
5377
5377
  -- WebP (and AVIF) don't render in Outlook and several other email clients, so the default
@@ -5396,9 +5396,9 @@ UPDATE public.logos
5396
5396
  SET media_id = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
5397
5397
  WHERE id = 'd45ef03d-27fc-4099-8b46-1cdf82d658d2'
5398
5398
  AND media_id = 'ea6fdaf5-f8de-416c-9f2b-b689b7a4ed38';
5399
-
5400
-
5401
- -- >>> FROM: 00000000000005_add_custom_canonical.sql <<<
5399
+
5400
+
5401
+ -- >>> FROM: 00000000000005_add_custom_canonical.sql <<<
5402
5402
  -- Add a nullable \`custom_canonical\` column to pages, posts and products.
5403
5403
  --
5404
5404
  -- This is the per-content manual canonical override used by each route's
@@ -5412,9 +5412,9 @@ WHERE id = 'd45ef03d-27fc-4099-8b46-1cdf82d658d2'
5412
5412
  ALTER TABLE public.pages ADD COLUMN IF NOT EXISTS custom_canonical text;
5413
5413
  ALTER TABLE public.posts ADD COLUMN IF NOT EXISTS custom_canonical text;
5414
5414
  ALTER TABLE public.products ADD COLUMN IF NOT EXISTS custom_canonical text;
5415
-
5416
-
5417
- -- >>> FROM: 00000000000006_home_live_demo_promo.sql <<<
5415
+
5416
+
5417
+ -- >>> FROM: 00000000000006_home_live_demo_promo.sql <<<
5418
5418
  -- 00000000000006_home_sandbox_promo.sql
5419
5419
  -- Adds a "Live Demo" promo section to the English (slug 'home') and French
5420
5420
  -- (slug 'accueil') home pages that drives visitors to the public sandbox at
@@ -5482,9 +5482,9 @@ $fr$::jsonb, 1);
5482
5482
  END IF;
5483
5483
  END
5484
5484
  $body$;
5485
-
5486
-
5487
- -- >>> FROM: 00000000000007_home_live_demo_promo_copy_fix.sql <<<
5485
+
5486
+
5487
+ -- >>> FROM: 00000000000007_home_live_demo_promo_copy_fix.sql <<<
5488
5488
  -- 00000000000007_home_live_demo_promo_copy_fix.sql
5489
5489
  -- Corrects the "Live Demo" promo that migration 006 seeded on the EN (slug 'home')
5490
5490
  -- and FR (slug 'accueil') home pages. 006 was already applied to production and the
@@ -5534,9 +5534,9 @@ $fr$::jsonb,
5534
5534
  AND content::text LIKE '%nb-sandbox-promo%';
5535
5535
  END
5536
5536
  $body$;
5537
-
5538
-
5539
- -- >>> FROM: 00000000000008_setup_article_reorder.sql <<<
5537
+
5538
+
5539
+ -- >>> FROM: 00000000000008_setup_article_reorder.sql <<<
5540
5540
  -- 00000000000008_setup_article_reorder.sql
5541
5541
  -- Reorders and rewrites the "How to Install NextBlock" setup guide on BOTH language
5542
5542
  -- twins: EN (post slug 'how-to-setup-nextblock', post_id=3 / block id=40) and FR
@@ -5567,9 +5567,9 @@ $nbfr$::jsonb,
5567
5567
  updated_at = now()
5568
5568
  WHERE post_id = 4
5569
5569
  AND block_type = 'text';
5570
-
5571
-
5572
- -- >>> FROM: 00000000000009_home_live_demo_promo_contrast.sql <<<
5570
+
5571
+
5572
+ -- >>> FROM: 00000000000009_home_live_demo_promo_contrast.sql <<<
5573
5573
  -- 00000000000009_home_live_demo_promo_contrast.sql
5574
5574
  -- Restyles the "Live Demo" promo that migrations 006/007 seeded on the EN (slug 'home')
5575
5575
  -- and FR (slug 'accueil') home pages. 006/007 gave it a fixed dark-navy GRADIENT section
@@ -5622,9 +5622,9 @@ $fr$::jsonb,
5622
5622
  AND content::text LIKE '%nb-sandbox-promo%';
5623
5623
  END
5624
5624
  $body$;
5625
-
5626
-
5627
- -- >>> FROM: 00000000000010_drop_github_username.sql <<<
5625
+
5626
+
5627
+ -- >>> FROM: 00000000000010_drop_github_username.sql <<<
5628
5628
  -- Drop the github_username profile column and remove the GitHub-auth artifacts.
5629
5629
  --
5630
5630
  -- GitHub OAuth sign-in / account-linking was removed from the app: it cannot be
@@ -5704,9 +5704,9 @@ WHERE key IN (
5704
5704
  'github_link_failed',
5705
5705
  'github_connected'
5706
5706
  );
5707
-
5708
-
5709
- -- >>> FROM: 00000000000011_language_detection_admin_only.sql <<<
5707
+
5708
+
5709
+ -- >>> FROM: 00000000000011_language_detection_admin_only.sql <<<
5710
5710
  -- Restrict writes to the language-detection settings row to ADMIN only.
5711
5711
  --
5712
5712
  -- \`site_settings.language_detection_settings\` is a non-sensitive, anon-READABLE
@@ -5728,9 +5728,9 @@ CREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO
5728
5728
 
5729
5729
  DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
5730
5730
  CREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
5731
-
5732
-
5733
- -- >>> FROM: 00000000000012_cortex_ai_stock_photo_settings.sql <<<
5731
+
5732
+
5733
+ -- >>> FROM: 00000000000012_cortex_ai_stock_photo_settings.sql <<<
5734
5734
  -- Protect the Cortex AI stock-photo provider API keys stored in site_settings.
5735
5735
  --
5736
5736
  -- The stock-photo search tool (search_stock_photos) can read a Pexels or Unsplash
@@ -5756,9 +5756,9 @@ CREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO
5756
5756
 
5757
5757
  DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
5758
5758
  CREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
5759
-
5760
-
5761
- -- >>> FROM: 00000000000013_youtube_nocookie_embeds.sql <<<
5759
+
5760
+
5761
+ -- >>> FROM: 00000000000013_youtube_nocookie_embeds.sql <<<
5762
5762
  -- 00000000000013_youtube_nocookie_embeds.sql
5763
5763
  --
5764
5764
  -- Lighthouse Best Practices scored 96 instead of 100: the \`inspector-issues\` audit
@@ -5895,47 +5895,305 @@ UPDATE public.custom_block_definitions
5895
5895
  )::jsonb
5896
5896
  WHERE layout_schema::text LIKE '%www.youtube.com/embed/%'
5897
5897
  OR layout_schema::text LIKE '%accelerated-motion%';
5898
-
5899
-
5900
- -- Step D: Record the applied migrations in history (truncated in Step B) so
5901
- -- \`npm run db:migrate:check\` reports up to date instead of listing every file as pending.
5902
- INSERT INTO supabase_migrations.schema_migrations (version, name) VALUES
5903
- ('00000000000000', 'baseline_schema'),
5904
- ('00000000000001', 'baseline_constraints_and_indexes'),
5905
- ('00000000000002', 'baseline_security_and_grants'),
5906
- ('00000000000003', 'baseline_seed'),
5907
- ('00000000000004', 'default_logo_email_safe'),
5908
- ('00000000000005', 'add_custom_canonical'),
5909
- ('00000000000006', 'home_live_demo_promo'),
5910
- ('00000000000007', 'home_live_demo_promo_copy_fix'),
5911
- ('00000000000008', 'setup_article_reorder'),
5912
- ('00000000000009', 'home_live_demo_promo_contrast'),
5913
- ('00000000000010', 'drop_github_username'),
5914
- ('00000000000011', 'language_detection_admin_only'),
5915
- ('00000000000012', 'cortex_ai_stock_photo_settings'),
5916
- ('00000000000013', 'youtube_nocookie_embeds')
5917
- ON CONFLICT (version) DO NOTHING;
5918
-
5919
- -- Step E: Anchor preserved profiles
5920
- INSERT INTO public.profiles (id, updated_at, full_name, avatar_url, website, role)
5921
- SELECT preserved_user.id, NULL, NULL, NULL, NULL, 'ADMIN'
5922
- FROM (
5923
- VALUES
5924
- ('63a8aa07-ef6e-4cc7-bafa-ece023c74366'::uuid),
5925
- ('e3a2c121-70a4-4239-a9df-f70ce717f5a1'::uuid)
5926
- ) AS preserved_user(id)
5927
- WHERE EXISTS (SELECT 1 FROM auth.users WHERE id = preserved_user.id)
5928
- AND NOT EXISTS (SELECT 1 FROM public.profiles WHERE id = preserved_user.id);
5929
-
5930
- -- Step F: Re-assert the "first admin exists" flag. The sandbox preserves its demo
5931
- -- ADMIN users across the wipe (Steps A & E), but the reseeded baseline resets
5932
- -- is_admin_created to 'false' and handle_new_user() only flips it on a NEW auth.users
5933
- -- INSERT -- which never fires for preserved users. Without this the proxy setup-gate
5934
- -- funnels ALL sandbox traffic (including this reset cron on its next run) to /setup even
5935
- -- though admins already exist. Guarded on an ADMIN profile actually being present.
5936
- UPDATE public.site_settings
5937
- SET value = 'true'::jsonb
5938
- WHERE key = 'is_admin_created'
5939
- AND EXISTS (SELECT 1 FROM public.profiles WHERE role = 'ADMIN'::public.user_role);
5940
-
5941
- `;
5898
+
5899
+
5900
+ -- >>> FROM: 00000000000014_site_themes.sql <<<
5901
+ -- Editable site themes.
5902
+ --
5903
+ -- Themes used to be hardcoded CSS classes in libs/ui/src/styles/theme.css
5904
+ -- (:root / .dark / .vibrant) with the switcher list duplicated in
5905
+ -- apps/nextblock/app/providers.tsx and components/theme-switcher.tsx. This moves
5906
+ -- the palette into the database so an ADMIN can retint the site, add themes and
5907
+ -- remove them from /cms/settings/global-css without a redeploy.
5908
+ --
5909
+ -- Rendering: apps/nextblock/lib/themes/buildThemeCss.ts turns each row into a
5910
+ -- \`:root.<slug> { --token: value; ... }\` rule injected into <head> by
5911
+ -- app/layout.tsx. The \`:root.x\` form is two-class specificity, so generated
5912
+ -- themes always beat the (0,1,0) fallback rules still shipped in theme.css for
5913
+ -- consumers of the published @nextblock-cms/ui package.
5914
+ --
5915
+ -- Forward-only and idempotent.
5916
+
5917
+ CREATE TABLE IF NOT EXISTS public.site_themes (
5918
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
5919
+ slug text NOT NULL,
5920
+ name text NOT NULL,
5921
+ description text,
5922
+ -- lucide-react icon name rendered by the theme switcher.
5923
+ icon text NOT NULL DEFAULT 'Palette',
5924
+ -- Drives the CSS \`color-scheme\` property and decides whether the theme also
5925
+ -- carries Tailwind's \`.dark\` class so \`dark:\` variants resolve correctly.
5926
+ color_scheme text NOT NULL DEFAULT 'light',
5927
+ -- Flat map of design token -> raw CSS value, keys WITHOUT the leading \`--\`,
5928
+ -- e.g. {"background": "0 0% 100%", "radius": "0.75rem"}.
5929
+ tokens jsonb NOT NULL DEFAULT '{}'::jsonb,
5930
+ -- Optional per-theme CSS, emitted nested inside the theme rule so it is
5931
+ -- automatically scoped. Authors use the \`&\` nesting selector,
5932
+ -- e.g. \`& h1 { text-shadow: 0 0 5px hsl(var(--primary)); }\`.
5933
+ extra_css text,
5934
+ -- System themes cannot be deleted: next-themes' \`enableSystem\` resolves to
5935
+ -- 'light' or 'dark', so those two slugs must always exist.
5936
+ is_system boolean DEFAULT false NOT NULL,
5937
+ is_default boolean DEFAULT false NOT NULL,
5938
+ is_active boolean DEFAULT true NOT NULL,
5939
+ sort_order integer DEFAULT 0 NOT NULL,
5940
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
5941
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
5942
+ CONSTRAINT site_themes_pkey PRIMARY KEY (id),
5943
+ CONSTRAINT site_themes_slug_key UNIQUE (slug),
5944
+ CONSTRAINT site_themes_color_scheme_check CHECK ((color_scheme = ANY (ARRAY['light'::text, 'dark'::text]))),
5945
+ -- The slug becomes a CSS class and a next-themes value; keep it safe for both.
5946
+ CONSTRAINT site_themes_slug_format_check CHECK ((slug ~ '^[a-z][a-z0-9-]{0,38}[a-z0-9]$')),
5947
+ CONSTRAINT site_themes_tokens_is_object_check CHECK ((jsonb_typeof(tokens) = 'object'))
5948
+ );
5949
+
5950
+ COMMENT ON TABLE public.site_themes IS 'Editable colour themes. Each row renders to a \`:root.<slug>\` CSS rule injected by the root layout. Publicly readable (anonymous visitors need the palette); only ADMIN may write.';
5951
+
5952
+ CREATE INDEX IF NOT EXISTS site_themes_active_sort_idx ON public.site_themes USING btree (is_active, sort_order);
5953
+
5954
+ -- Exactly one default theme.
5955
+ CREATE UNIQUE INDEX IF NOT EXISTS site_themes_single_default_idx ON public.site_themes USING btree (is_default) WHERE (is_default = true);
5956
+
5957
+ DROP TRIGGER IF EXISTS set_site_themes_updated_at ON public.site_themes;
5958
+ CREATE TRIGGER set_site_themes_updated_at
5959
+ BEFORE UPDATE ON public.site_themes
5960
+ FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();
5961
+
5962
+ -- A system theme must never be deleted, whoever asks.
5963
+ CREATE OR REPLACE FUNCTION public.prevent_system_theme_delete() RETURNS trigger
5964
+ LANGUAGE plpgsql
5965
+ SET search_path = ''
5966
+ AS $$
5967
+ BEGIN
5968
+ IF OLD.is_system THEN
5969
+ RAISE EXCEPTION 'Theme "%" is a system theme and cannot be deleted', OLD.slug
5970
+ USING ERRCODE = 'restrict_violation';
5971
+ END IF;
5972
+ RETURN OLD;
5973
+ END;
5974
+ $$;
5975
+
5976
+ DROP TRIGGER IF EXISTS trg_prevent_system_theme_delete ON public.site_themes;
5977
+ CREATE TRIGGER trg_prevent_system_theme_delete
5978
+ BEFORE DELETE ON public.site_themes
5979
+ FOR EACH ROW EXECUTE FUNCTION public.prevent_system_theme_delete();
5980
+
5981
+ -- Promoting a theme to default demotes the previous one, so the unique partial
5982
+ -- index above can never trip on a normal "make this the default" write.
5983
+ CREATE OR REPLACE FUNCTION public.handle_default_theme_change() RETURNS trigger
5984
+ LANGUAGE plpgsql
5985
+ SET search_path = ''
5986
+ AS $$
5987
+ BEGIN
5988
+ IF NEW.is_default THEN
5989
+ UPDATE public.site_themes
5990
+ SET is_default = false
5991
+ WHERE id <> NEW.id AND is_default;
5992
+ END IF;
5993
+ RETURN NEW;
5994
+ END;
5995
+ $$;
5996
+
5997
+ DROP TRIGGER IF EXISTS trg_handle_default_theme_change ON public.site_themes;
5998
+ CREATE TRIGGER trg_handle_default_theme_change
5999
+ AFTER INSERT OR UPDATE OF is_default ON public.site_themes
6000
+ FOR EACH ROW WHEN (NEW.is_default) EXECUTE FUNCTION public.handle_default_theme_change();
6001
+
6002
+ ALTER TABLE public.site_themes ENABLE ROW LEVEL SECURITY;
6003
+
6004
+ GRANT ALL ON TABLE public.site_themes TO anon;
6005
+ GRANT ALL ON TABLE public.site_themes TO authenticated;
6006
+ GRANT ALL ON TABLE public.site_themes TO service_role;
6007
+
6008
+ DROP POLICY IF EXISTS "Public read active themes" ON public.site_themes;
6009
+ CREATE POLICY "Public read active themes" ON public.site_themes
6010
+ FOR SELECT TO authenticated, anon USING (true);
6011
+
6012
+ DROP POLICY IF EXISTS "Admins insert themes" ON public.site_themes;
6013
+ CREATE POLICY "Admins insert themes" ON public.site_themes
6014
+ FOR INSERT TO authenticated
6015
+ WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
6016
+
6017
+ DROP POLICY IF EXISTS "Admins update themes" ON public.site_themes;
6018
+ CREATE POLICY "Admins update themes" ON public.site_themes
6019
+ FOR UPDATE TO authenticated
6020
+ USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))
6021
+ WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
6022
+
6023
+ DROP POLICY IF EXISTS "Admins delete themes" ON public.site_themes;
6024
+ CREATE POLICY "Admins delete themes" ON public.site_themes
6025
+ FOR DELETE TO authenticated
6026
+ USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
6027
+
6028
+ DROP POLICY IF EXISTS "Service role manages themes" ON public.site_themes;
6029
+ CREATE POLICY "Service role manages themes" ON public.site_themes
6030
+ TO service_role USING (true) WITH CHECK (true);
6031
+
6032
+ -- Seed the three shipped themes from libs/ui/src/styles/theme.css.
6033
+ -- \`--warning\` / \`--warning-foreground\` are declared in the Tailwind theme
6034
+ -- (libs/ui/tailwind.config.js) but were never defined in CSS, so bg-warning and
6035
+ -- text-warning resolved to an invalid colour. They are given real values here.
6036
+ INSERT INTO public.site_themes (slug, name, description, icon, color_scheme, is_system, is_default, sort_order, tokens, extra_css)
6037
+ VALUES
6038
+ (
6039
+ 'light', 'Light', 'Clean, technical, stark.', 'Sun', 'light', true, true, 10,
6040
+ '{
6041
+ "background": "0 0% 100%",
6042
+ "foreground": "222 47% 11%",
6043
+ "card": "0 0% 100%",
6044
+ "card-foreground": "222 47% 11%",
6045
+ "popover": "0 0% 100%",
6046
+ "popover-foreground": "222 47% 11%",
6047
+ "primary": "211.55 50.26% 37.84%",
6048
+ "primary-foreground": "210 40% 98%",
6049
+ "secondary": "210 40% 96.1%",
6050
+ "secondary-foreground": "222 47% 11%",
6051
+ "muted": "210 40% 96.1%",
6052
+ "muted-foreground": "215 16% 47%",
6053
+ "accent": "210 40% 96.1%",
6054
+ "accent-foreground": "222 47% 11%",
6055
+ "destructive": "0 84.2% 60.2%",
6056
+ "destructive-foreground": "210 40% 98%",
6057
+ "warning": "38 92% 50%",
6058
+ "warning-foreground": "222 47% 11%",
6059
+ "border": "214.3 31.8% 91.4%",
6060
+ "input": "214.3 31.8% 91.4%",
6061
+ "ring": "211.55 50.26% 37.84%",
6062
+ "radius": "0.75rem",
6063
+ "chart-1": "211.55 50.26% 37.84%",
6064
+ "chart-2": "215 16% 47%",
6065
+ "chart-3": "215 25% 27%",
6066
+ "chart-4": "210 40% 96%",
6067
+ "chart-5": "214 32% 91%"
6068
+ }'::jsonb,
6069
+ NULL
6070
+ ),
6071
+ (
6072
+ 'dark', 'Dark', 'Midnight / neon tech.', 'Moon', 'dark', true, false, 20,
6073
+ '{
6074
+ "background": "222 47% 2%",
6075
+ "foreground": "210 40% 98%",
6076
+ "card": "222 47% 11%",
6077
+ "card-foreground": "210 40% 98%",
6078
+ "popover": "222 47% 11%",
6079
+ "popover-foreground": "210 40% 98%",
6080
+ "primary": "217 91% 60%",
6081
+ "primary-foreground": "222 47% 11%",
6082
+ "secondary": "217.2 32.6% 17.5%",
6083
+ "secondary-foreground": "210 40% 98%",
6084
+ "muted": "217.2 32.6% 17.5%",
6085
+ "muted-foreground": "215 20.2% 65.1%",
6086
+ "accent": "217.2 32.6% 17.5%",
6087
+ "accent-foreground": "210 40% 98%",
6088
+ "destructive": "0 62.8% 30.6%",
6089
+ "destructive-foreground": "210 40% 98%",
6090
+ "warning": "38 92% 50%",
6091
+ "warning-foreground": "222 47% 11%",
6092
+ "border": "217.2 32.6% 17.5%",
6093
+ "input": "217.2 32.6% 17.5%",
6094
+ "ring": "224 76% 48%",
6095
+ "radius": "0.75rem",
6096
+ "chart-1": "220 70% 50%",
6097
+ "chart-2": "160 60% 45%",
6098
+ "chart-3": "30 80% 55%",
6099
+ "chart-4": "280 65% 60%",
6100
+ "chart-5": "340 75% 55%"
6101
+ }'::jsonb,
6102
+ NULL
6103
+ ),
6104
+ (
6105
+ 'vibrant', 'Vibrant', 'Cyberpunk neon.', 'Zap', 'dark', false, false, 30,
6106
+ '{
6107
+ "background": "260 50% 5%",
6108
+ "foreground": "180 100% 90%",
6109
+ "card": "260 50% 8%",
6110
+ "card-foreground": "180 100% 90%",
6111
+ "popover": "260 50% 8%",
6112
+ "popover-foreground": "180 100% 90%",
6113
+ "primary": "320 100% 55%",
6114
+ "primary-foreground": "0 0% 100%",
6115
+ "secondary": "180 100% 50%",
6116
+ "secondary-foreground": "260 50% 5%",
6117
+ "muted": "260 30% 15%",
6118
+ "muted-foreground": "260 20% 65%",
6119
+ "accent": "280 100% 50%",
6120
+ "accent-foreground": "0 0% 100%",
6121
+ "destructive": "0 100% 50%",
6122
+ "destructive-foreground": "0 0% 100%",
6123
+ "warning": "60 100% 50%",
6124
+ "warning-foreground": "260 50% 5%",
6125
+ "border": "320 100% 55%",
6126
+ "input": "260 30% 15%",
6127
+ "ring": "320 100% 55%",
6128
+ "radius": "0px",
6129
+ "chart-1": "320 100% 55%",
6130
+ "chart-2": "180 100% 50%",
6131
+ "chart-3": "280 100% 50%",
6132
+ "chart-4": "60 100% 50%",
6133
+ "chart-5": "120 100% 50%"
6134
+ }'::jsonb,
6135
+ '& h1, & h2, & h3, & h4, & h5, & h6 {
6136
+ text-shadow: 0 0 5px hsl(var(--primary)), 0 0 10px hsl(var(--secondary));
6137
+ }
6138
+ & button, & [role="button"] {
6139
+ box-shadow: 0 0 5px hsl(var(--primary) / 0.5);
6140
+ transition: box-shadow 0.3s ease;
6141
+ }
6142
+ & button:hover, & [role="button"]:hover {
6143
+ box-shadow: 0 0 15px hsl(var(--primary));
6144
+ }
6145
+ & .card, & [class*="card"] {
6146
+ border: 1px solid hsl(var(--primary));
6147
+ box-shadow: 0 0 10px hsl(var(--primary) / 0.2);
6148
+ }
6149
+ & .border {
6150
+ border-color: hsl(var(--border));
6151
+ box-shadow: 0 0 5px hsl(var(--border) / 0.3);
6152
+ }'
6153
+ )
6154
+ ON CONFLICT (slug) DO NOTHING;
6155
+
6156
+
6157
+ -- Step D: Record the applied migrations in history (truncated in Step B) so
6158
+ -- \`npm run db:migrate:check\` reports up to date instead of listing every file as pending.
6159
+ INSERT INTO supabase_migrations.schema_migrations (version, name) VALUES
6160
+ ('00000000000000', 'baseline_schema'),
6161
+ ('00000000000001', 'baseline_constraints_and_indexes'),
6162
+ ('00000000000002', 'baseline_security_and_grants'),
6163
+ ('00000000000003', 'baseline_seed'),
6164
+ ('00000000000004', 'default_logo_email_safe'),
6165
+ ('00000000000005', 'add_custom_canonical'),
6166
+ ('00000000000006', 'home_live_demo_promo'),
6167
+ ('00000000000007', 'home_live_demo_promo_copy_fix'),
6168
+ ('00000000000008', 'setup_article_reorder'),
6169
+ ('00000000000009', 'home_live_demo_promo_contrast'),
6170
+ ('00000000000010', 'drop_github_username'),
6171
+ ('00000000000011', 'language_detection_admin_only'),
6172
+ ('00000000000012', 'cortex_ai_stock_photo_settings'),
6173
+ ('00000000000013', 'youtube_nocookie_embeds'),
6174
+ ('00000000000014', 'site_themes')
6175
+ ON CONFLICT (version) DO NOTHING;
6176
+
6177
+ -- Step E: Anchor preserved profiles
6178
+ INSERT INTO public.profiles (id, updated_at, full_name, avatar_url, website, role)
6179
+ SELECT preserved_user.id, NULL, NULL, NULL, NULL, 'ADMIN'
6180
+ FROM (
6181
+ VALUES
6182
+ ('63a8aa07-ef6e-4cc7-bafa-ece023c74366'::uuid),
6183
+ ('e3a2c121-70a4-4239-a9df-f70ce717f5a1'::uuid)
6184
+ ) AS preserved_user(id)
6185
+ WHERE EXISTS (SELECT 1 FROM auth.users WHERE id = preserved_user.id)
6186
+ AND NOT EXISTS (SELECT 1 FROM public.profiles WHERE id = preserved_user.id);
6187
+
6188
+ -- Step F: Re-assert the "first admin exists" flag. The sandbox preserves its demo
6189
+ -- ADMIN users across the wipe (Steps A & E), but the reseeded baseline resets
6190
+ -- is_admin_created to 'false' and handle_new_user() only flips it on a NEW auth.users
6191
+ -- INSERT -- which never fires for preserved users. Without this the proxy setup-gate
6192
+ -- funnels ALL sandbox traffic (including this reset cron on its next run) to /setup even
6193
+ -- though admins already exist. Guarded on an ADMIN profile actually being present.
6194
+ UPDATE public.site_settings
6195
+ SET value = 'true'::jsonb
6196
+ WHERE key = 'is_admin_created'
6197
+ AND EXISTS (SELECT 1 FROM public.profiles WHERE role = 'ADMIN'::public.user_role);
6198
+
6199
+ `;
@@ -228,7 +228,7 @@ export default function CmsClientLayout({
228
228
  // Fallback for general /cms/settings if no more specific language path matches
229
229
  else if (pathname.startsWith("/cms/settings/logos")) pageTitle = "Branding";
230
230
  else if (pathname.startsWith("/cms/settings/copyright")) pageTitle = "Copyright Settings";
231
- else if (pathname.startsWith("/cms/settings/global-css")) pageTitle = "Global CSS Settings";
231
+ else if (pathname.startsWith("/cms/settings/global-css")) pageTitle = "Themes & CSS";
232
232
  else if (pathname.startsWith("/cms/settings/extra-translations")) pageTitle = "Extra Translations";
233
233
  else if (pathname.startsWith("/cms/settings/backup-restore")) pageTitle = "Backup And Restore";
234
234
  else if (pathname.startsWith("/cms/settings/currencies")) pageTitle = "Currency Settings";
@@ -422,7 +422,7 @@ export default function CmsClientLayout({
422
422
  Copyright
423
423
  </NavItem>
424
424
  <NavItem href="/cms/settings/global-css" icon={Paintbrush} isActive={pathname.startsWith("/cms/settings/global-css")} adminOnly isAdmin={isAdmin} onClick={closeSidebarOnMobile}>
425
- Global CSS
425
+ Themes &amp; CSS
426
426
  </NavItem>
427
427
  <NavItem href="/cms/settings/privacy" icon={Cookie} isActive={pathname.startsWith("/cms/settings/privacy")} adminOnly isAdmin={isAdmin} onClick={closeSidebarOnMobile}>
428
428
  Privacy &amp; Consent