@nextblock-cms/db 0.15.10 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/supabase/server.d.ts +31 -1
- package/lib/supabase/types.d.ts +41 -11
- package/package.json +1 -1
- package/supabase/migrations/00000000000003_baseline_seed.sql +468 -468
- package/supabase/migrations/00000000000006_home_live_demo_promo.sql +67 -67
- package/supabase/migrations/00000000000007_home_live_demo_promo_copy_fix.sql +49 -49
- package/supabase/migrations/00000000000009_home_live_demo_promo_contrast.sql +52 -52
- package/supabase/migrations/00000000000014_site_themes.sql +254 -254
- package/supabase/migrations/00000000000015_scheduled_publishing.sql +43 -43
- package/supabase/migrations/00000000000016_product_revisions_and_revision_baseline.sql +310 -310
- package/supabase/migrations/00000000000017_cortex_ai_mcp_server.sql +91 -91
- package/supabase/migrations/00000000000018_site_scripts.sql +96 -96
- package/supabase/migrations/00000000000019_site_script_revisions.sql +85 -85
- package/supabase/migrations/00000000000020_updating_article.sql +162 -162
- package/supabase/migrations/00000000000021_updating_article_git_merge.sql +91 -91
- package/supabase/migrations/00000000000022_updating_article_accuracy.sql +94 -94
- package/supabase/migrations/00000000000023_updating_article_layout_not_host.sql +87 -87
- package/supabase/migrations/00000000000024_updating_article_newline_fix.sql +70 -70
- package/supabase/migrations/00000000000025_rebrand_nextblock_dev.sql +53 -53
- package/supabase/migrations/00000000000026_product_inquiries.sql +118 -118
- package/supabase/migrations/00000000000027_message_threads.sql +391 -391
- package/supabase/migrations/00000000000028_interaction_replies.sql +86 -86
- package/supabase/migrations/00000000000029_form_endpoints_default_empty.sql +52 -52
- package/supabase/migrations/00000000000030_seo_redirects_and_robots.sql +149 -0
- package/supabase/migrations/00000000000031_seo_robots_settings_admin_only.sql +35 -0
|
@@ -1,254 +1,254 @@
|
|
|
1
|
-
-- Editable site themes.
|
|
2
|
-
--
|
|
3
|
-
-- Themes used to be hardcoded CSS classes in libs/ui/src/styles/theme.css
|
|
4
|
-
-- (:root / .dark / .vibrant) with the switcher list duplicated in
|
|
5
|
-
-- apps/nextblock/app/providers.tsx and components/theme-switcher.tsx. This moves
|
|
6
|
-
-- the palette into the database so an ADMIN can retint the site, add themes and
|
|
7
|
-
-- remove them from /cms/settings/global-css without a redeploy.
|
|
8
|
-
--
|
|
9
|
-
-- Rendering: apps/nextblock/lib/themes/buildThemeCss.ts turns each row into a
|
|
10
|
-
-- `:root.<slug> { --token: value; ... }` rule injected into <head> by
|
|
11
|
-
-- app/layout.tsx. The `:root.x` form is two-class specificity, so generated
|
|
12
|
-
-- themes always beat the (0,1,0) fallback rules still shipped in theme.css for
|
|
13
|
-
-- consumers of the published @nextblock-cms/ui package.
|
|
14
|
-
--
|
|
15
|
-
-- Forward-only and idempotent.
|
|
16
|
-
|
|
17
|
-
CREATE TABLE IF NOT EXISTS public.site_themes (
|
|
18
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
19
|
-
slug text NOT NULL,
|
|
20
|
-
name text NOT NULL,
|
|
21
|
-
description text,
|
|
22
|
-
-- lucide-react icon name rendered by the theme switcher.
|
|
23
|
-
icon text NOT NULL DEFAULT 'Palette',
|
|
24
|
-
-- Drives the CSS `color-scheme` property and decides whether the theme also
|
|
25
|
-
-- carries Tailwind's `.dark` class so `dark:` variants resolve correctly.
|
|
26
|
-
color_scheme text NOT NULL DEFAULT 'light',
|
|
27
|
-
-- Flat map of design token -> raw CSS value, keys WITHOUT the leading `--`,
|
|
28
|
-
-- e.g. {"background": "0 0% 100%", "radius": "0.75rem"}.
|
|
29
|
-
tokens jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
30
|
-
-- Optional per-theme CSS, emitted nested inside the theme rule so it is
|
|
31
|
-
-- automatically scoped. Authors use the `&` nesting selector,
|
|
32
|
-
-- e.g. `& h1 { text-shadow: 0 0 5px hsl(var(--primary)); }`.
|
|
33
|
-
extra_css text,
|
|
34
|
-
-- System themes cannot be deleted: next-themes' `enableSystem` resolves to
|
|
35
|
-
-- 'light' or 'dark', so those two slugs must always exist.
|
|
36
|
-
is_system boolean DEFAULT false NOT NULL,
|
|
37
|
-
is_default boolean DEFAULT false NOT NULL,
|
|
38
|
-
is_active boolean DEFAULT true NOT NULL,
|
|
39
|
-
sort_order integer DEFAULT 0 NOT NULL,
|
|
40
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
41
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
42
|
-
CONSTRAINT site_themes_pkey PRIMARY KEY (id),
|
|
43
|
-
CONSTRAINT site_themes_slug_key UNIQUE (slug),
|
|
44
|
-
CONSTRAINT site_themes_color_scheme_check CHECK ((color_scheme = ANY (ARRAY['light'::text, 'dark'::text]))),
|
|
45
|
-
-- The slug becomes a CSS class and a next-themes value; keep it safe for both.
|
|
46
|
-
CONSTRAINT site_themes_slug_format_check CHECK ((slug ~ '^[a-z][a-z0-9-]{0,38}[a-z0-9]$')),
|
|
47
|
-
CONSTRAINT site_themes_tokens_is_object_check CHECK ((jsonb_typeof(tokens) = 'object'))
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
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.';
|
|
51
|
-
|
|
52
|
-
CREATE INDEX IF NOT EXISTS site_themes_active_sort_idx ON public.site_themes USING btree (is_active, sort_order);
|
|
53
|
-
|
|
54
|
-
-- Exactly one default theme.
|
|
55
|
-
CREATE UNIQUE INDEX IF NOT EXISTS site_themes_single_default_idx ON public.site_themes USING btree (is_default) WHERE (is_default = true);
|
|
56
|
-
|
|
57
|
-
DROP TRIGGER IF EXISTS set_site_themes_updated_at ON public.site_themes;
|
|
58
|
-
CREATE TRIGGER set_site_themes_updated_at
|
|
59
|
-
BEFORE UPDATE ON public.site_themes
|
|
60
|
-
FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();
|
|
61
|
-
|
|
62
|
-
-- A system theme must never be deleted, whoever asks.
|
|
63
|
-
CREATE OR REPLACE FUNCTION public.prevent_system_theme_delete() RETURNS trigger
|
|
64
|
-
LANGUAGE plpgsql
|
|
65
|
-
SET search_path = ''
|
|
66
|
-
AS $$
|
|
67
|
-
BEGIN
|
|
68
|
-
IF OLD.is_system THEN
|
|
69
|
-
RAISE EXCEPTION 'Theme "%" is a system theme and cannot be deleted', OLD.slug
|
|
70
|
-
USING ERRCODE = 'restrict_violation';
|
|
71
|
-
END IF;
|
|
72
|
-
RETURN OLD;
|
|
73
|
-
END;
|
|
74
|
-
$$;
|
|
75
|
-
|
|
76
|
-
DROP TRIGGER IF EXISTS trg_prevent_system_theme_delete ON public.site_themes;
|
|
77
|
-
CREATE TRIGGER trg_prevent_system_theme_delete
|
|
78
|
-
BEFORE DELETE ON public.site_themes
|
|
79
|
-
FOR EACH ROW EXECUTE FUNCTION public.prevent_system_theme_delete();
|
|
80
|
-
|
|
81
|
-
-- Promoting a theme to default demotes the previous one, so the unique partial
|
|
82
|
-
-- index above can never trip on a normal "make this the default" write.
|
|
83
|
-
CREATE OR REPLACE FUNCTION public.handle_default_theme_change() RETURNS trigger
|
|
84
|
-
LANGUAGE plpgsql
|
|
85
|
-
SET search_path = ''
|
|
86
|
-
AS $$
|
|
87
|
-
BEGIN
|
|
88
|
-
IF NEW.is_default THEN
|
|
89
|
-
UPDATE public.site_themes
|
|
90
|
-
SET is_default = false
|
|
91
|
-
WHERE id <> NEW.id AND is_default;
|
|
92
|
-
END IF;
|
|
93
|
-
RETURN NEW;
|
|
94
|
-
END;
|
|
95
|
-
$$;
|
|
96
|
-
|
|
97
|
-
DROP TRIGGER IF EXISTS trg_handle_default_theme_change ON public.site_themes;
|
|
98
|
-
CREATE TRIGGER trg_handle_default_theme_change
|
|
99
|
-
AFTER INSERT OR UPDATE OF is_default ON public.site_themes
|
|
100
|
-
FOR EACH ROW WHEN (NEW.is_default) EXECUTE FUNCTION public.handle_default_theme_change();
|
|
101
|
-
|
|
102
|
-
ALTER TABLE public.site_themes ENABLE ROW LEVEL SECURITY;
|
|
103
|
-
|
|
104
|
-
GRANT ALL ON TABLE public.site_themes TO anon;
|
|
105
|
-
GRANT ALL ON TABLE public.site_themes TO authenticated;
|
|
106
|
-
GRANT ALL ON TABLE public.site_themes TO service_role;
|
|
107
|
-
|
|
108
|
-
DROP POLICY IF EXISTS "Public read active themes" ON public.site_themes;
|
|
109
|
-
CREATE POLICY "Public read active themes" ON public.site_themes
|
|
110
|
-
FOR SELECT TO authenticated, anon USING (true);
|
|
111
|
-
|
|
112
|
-
DROP POLICY IF EXISTS "Admins insert themes" ON public.site_themes;
|
|
113
|
-
CREATE POLICY "Admins insert themes" ON public.site_themes
|
|
114
|
-
FOR INSERT TO authenticated
|
|
115
|
-
WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
116
|
-
|
|
117
|
-
DROP POLICY IF EXISTS "Admins update themes" ON public.site_themes;
|
|
118
|
-
CREATE POLICY "Admins update themes" ON public.site_themes
|
|
119
|
-
FOR UPDATE TO authenticated
|
|
120
|
-
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))
|
|
121
|
-
WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
122
|
-
|
|
123
|
-
DROP POLICY IF EXISTS "Admins delete themes" ON public.site_themes;
|
|
124
|
-
CREATE POLICY "Admins delete themes" ON public.site_themes
|
|
125
|
-
FOR DELETE TO authenticated
|
|
126
|
-
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
127
|
-
|
|
128
|
-
DROP POLICY IF EXISTS "Service role manages themes" ON public.site_themes;
|
|
129
|
-
CREATE POLICY "Service role manages themes" ON public.site_themes
|
|
130
|
-
TO service_role USING (true) WITH CHECK (true);
|
|
131
|
-
|
|
132
|
-
-- Seed the three shipped themes from libs/ui/src/styles/theme.css.
|
|
133
|
-
-- `--warning` / `--warning-foreground` are declared in the Tailwind theme
|
|
134
|
-
-- (libs/ui/tailwind.config.js) but were never defined in CSS, so bg-warning and
|
|
135
|
-
-- text-warning resolved to an invalid colour. They are given real values here.
|
|
136
|
-
INSERT INTO public.site_themes (slug, name, description, icon, color_scheme, is_system, is_default, sort_order, tokens, extra_css)
|
|
137
|
-
VALUES
|
|
138
|
-
(
|
|
139
|
-
'light', 'Light', 'Clean, technical, stark.', 'Sun', 'light', true, true, 10,
|
|
140
|
-
'{
|
|
141
|
-
"background": "0 0% 100%",
|
|
142
|
-
"foreground": "222 47% 11%",
|
|
143
|
-
"card": "0 0% 100%",
|
|
144
|
-
"card-foreground": "222 47% 11%",
|
|
145
|
-
"popover": "0 0% 100%",
|
|
146
|
-
"popover-foreground": "222 47% 11%",
|
|
147
|
-
"primary": "211.55 50.26% 37.84%",
|
|
148
|
-
"primary-foreground": "210 40% 98%",
|
|
149
|
-
"secondary": "210 40% 96.1%",
|
|
150
|
-
"secondary-foreground": "222 47% 11%",
|
|
151
|
-
"muted": "210 40% 96.1%",
|
|
152
|
-
"muted-foreground": "215 16% 47%",
|
|
153
|
-
"accent": "210 40% 96.1%",
|
|
154
|
-
"accent-foreground": "222 47% 11%",
|
|
155
|
-
"destructive": "0 84.2% 60.2%",
|
|
156
|
-
"destructive-foreground": "210 40% 98%",
|
|
157
|
-
"warning": "38 92% 50%",
|
|
158
|
-
"warning-foreground": "222 47% 11%",
|
|
159
|
-
"border": "214.3 31.8% 91.4%",
|
|
160
|
-
"input": "214.3 31.8% 91.4%",
|
|
161
|
-
"ring": "211.55 50.26% 37.84%",
|
|
162
|
-
"radius": "0.75rem",
|
|
163
|
-
"chart-1": "211.55 50.26% 37.84%",
|
|
164
|
-
"chart-2": "215 16% 47%",
|
|
165
|
-
"chart-3": "215 25% 27%",
|
|
166
|
-
"chart-4": "210 40% 96%",
|
|
167
|
-
"chart-5": "214 32% 91%"
|
|
168
|
-
}'::jsonb,
|
|
169
|
-
NULL
|
|
170
|
-
),
|
|
171
|
-
(
|
|
172
|
-
'dark', 'Dark', 'Midnight / neon tech.', 'Moon', 'dark', true, false, 20,
|
|
173
|
-
'{
|
|
174
|
-
"background": "222 47% 2%",
|
|
175
|
-
"foreground": "210 40% 98%",
|
|
176
|
-
"card": "222 47% 11%",
|
|
177
|
-
"card-foreground": "210 40% 98%",
|
|
178
|
-
"popover": "222 47% 11%",
|
|
179
|
-
"popover-foreground": "210 40% 98%",
|
|
180
|
-
"primary": "217 91% 60%",
|
|
181
|
-
"primary-foreground": "222 47% 11%",
|
|
182
|
-
"secondary": "217.2 32.6% 17.5%",
|
|
183
|
-
"secondary-foreground": "210 40% 98%",
|
|
184
|
-
"muted": "217.2 32.6% 17.5%",
|
|
185
|
-
"muted-foreground": "215 20.2% 65.1%",
|
|
186
|
-
"accent": "217.2 32.6% 17.5%",
|
|
187
|
-
"accent-foreground": "210 40% 98%",
|
|
188
|
-
"destructive": "0 62.8% 30.6%",
|
|
189
|
-
"destructive-foreground": "210 40% 98%",
|
|
190
|
-
"warning": "38 92% 50%",
|
|
191
|
-
"warning-foreground": "222 47% 11%",
|
|
192
|
-
"border": "217.2 32.6% 17.5%",
|
|
193
|
-
"input": "217.2 32.6% 17.5%",
|
|
194
|
-
"ring": "224 76% 48%",
|
|
195
|
-
"radius": "0.75rem",
|
|
196
|
-
"chart-1": "220 70% 50%",
|
|
197
|
-
"chart-2": "160 60% 45%",
|
|
198
|
-
"chart-3": "30 80% 55%",
|
|
199
|
-
"chart-4": "280 65% 60%",
|
|
200
|
-
"chart-5": "340 75% 55%"
|
|
201
|
-
}'::jsonb,
|
|
202
|
-
NULL
|
|
203
|
-
),
|
|
204
|
-
(
|
|
205
|
-
'vibrant', 'Vibrant', 'Cyberpunk neon.', 'Zap', 'dark', false, false, 30,
|
|
206
|
-
'{
|
|
207
|
-
"background": "260 50% 5%",
|
|
208
|
-
"foreground": "180 100% 90%",
|
|
209
|
-
"card": "260 50% 8%",
|
|
210
|
-
"card-foreground": "180 100% 90%",
|
|
211
|
-
"popover": "260 50% 8%",
|
|
212
|
-
"popover-foreground": "180 100% 90%",
|
|
213
|
-
"primary": "320 100% 55%",
|
|
214
|
-
"primary-foreground": "0 0% 100%",
|
|
215
|
-
"secondary": "180 100% 50%",
|
|
216
|
-
"secondary-foreground": "260 50% 5%",
|
|
217
|
-
"muted": "260 30% 15%",
|
|
218
|
-
"muted-foreground": "260 20% 65%",
|
|
219
|
-
"accent": "280 100% 50%",
|
|
220
|
-
"accent-foreground": "0 0% 100%",
|
|
221
|
-
"destructive": "0 100% 50%",
|
|
222
|
-
"destructive-foreground": "0 0% 100%",
|
|
223
|
-
"warning": "60 100% 50%",
|
|
224
|
-
"warning-foreground": "260 50% 5%",
|
|
225
|
-
"border": "320 100% 55%",
|
|
226
|
-
"input": "260 30% 15%",
|
|
227
|
-
"ring": "320 100% 55%",
|
|
228
|
-
"radius": "0px",
|
|
229
|
-
"chart-1": "320 100% 55%",
|
|
230
|
-
"chart-2": "180 100% 50%",
|
|
231
|
-
"chart-3": "280 100% 50%",
|
|
232
|
-
"chart-4": "60 100% 50%",
|
|
233
|
-
"chart-5": "120 100% 50%"
|
|
234
|
-
}'::jsonb,
|
|
235
|
-
'& h1, & h2, & h3, & h4, & h5, & h6 {
|
|
236
|
-
text-shadow: 0 0 5px hsl(var(--primary)), 0 0 10px hsl(var(--secondary));
|
|
237
|
-
}
|
|
238
|
-
& button, & [role="button"] {
|
|
239
|
-
box-shadow: 0 0 5px hsl(var(--primary) / 0.5);
|
|
240
|
-
transition: box-shadow 0.3s ease;
|
|
241
|
-
}
|
|
242
|
-
& button:hover, & [role="button"]:hover {
|
|
243
|
-
box-shadow: 0 0 15px hsl(var(--primary));
|
|
244
|
-
}
|
|
245
|
-
& .card, & [class*="card"] {
|
|
246
|
-
border: 1px solid hsl(var(--primary));
|
|
247
|
-
box-shadow: 0 0 10px hsl(var(--primary) / 0.2);
|
|
248
|
-
}
|
|
249
|
-
& .border {
|
|
250
|
-
border-color: hsl(var(--border));
|
|
251
|
-
box-shadow: 0 0 5px hsl(var(--border) / 0.3);
|
|
252
|
-
}'
|
|
253
|
-
)
|
|
254
|
-
ON CONFLICT (slug) DO NOTHING;
|
|
1
|
+
-- Editable site themes.
|
|
2
|
+
--
|
|
3
|
+
-- Themes used to be hardcoded CSS classes in libs/ui/src/styles/theme.css
|
|
4
|
+
-- (:root / .dark / .vibrant) with the switcher list duplicated in
|
|
5
|
+
-- apps/nextblock/app/providers.tsx and components/theme-switcher.tsx. This moves
|
|
6
|
+
-- the palette into the database so an ADMIN can retint the site, add themes and
|
|
7
|
+
-- remove them from /cms/settings/global-css without a redeploy.
|
|
8
|
+
--
|
|
9
|
+
-- Rendering: apps/nextblock/lib/themes/buildThemeCss.ts turns each row into a
|
|
10
|
+
-- `:root.<slug> { --token: value; ... }` rule injected into <head> by
|
|
11
|
+
-- app/layout.tsx. The `:root.x` form is two-class specificity, so generated
|
|
12
|
+
-- themes always beat the (0,1,0) fallback rules still shipped in theme.css for
|
|
13
|
+
-- consumers of the published @nextblock-cms/ui package.
|
|
14
|
+
--
|
|
15
|
+
-- Forward-only and idempotent.
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS public.site_themes (
|
|
18
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
19
|
+
slug text NOT NULL,
|
|
20
|
+
name text NOT NULL,
|
|
21
|
+
description text,
|
|
22
|
+
-- lucide-react icon name rendered by the theme switcher.
|
|
23
|
+
icon text NOT NULL DEFAULT 'Palette',
|
|
24
|
+
-- Drives the CSS `color-scheme` property and decides whether the theme also
|
|
25
|
+
-- carries Tailwind's `.dark` class so `dark:` variants resolve correctly.
|
|
26
|
+
color_scheme text NOT NULL DEFAULT 'light',
|
|
27
|
+
-- Flat map of design token -> raw CSS value, keys WITHOUT the leading `--`,
|
|
28
|
+
-- e.g. {"background": "0 0% 100%", "radius": "0.75rem"}.
|
|
29
|
+
tokens jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
30
|
+
-- Optional per-theme CSS, emitted nested inside the theme rule so it is
|
|
31
|
+
-- automatically scoped. Authors use the `&` nesting selector,
|
|
32
|
+
-- e.g. `& h1 { text-shadow: 0 0 5px hsl(var(--primary)); }`.
|
|
33
|
+
extra_css text,
|
|
34
|
+
-- System themes cannot be deleted: next-themes' `enableSystem` resolves to
|
|
35
|
+
-- 'light' or 'dark', so those two slugs must always exist.
|
|
36
|
+
is_system boolean DEFAULT false NOT NULL,
|
|
37
|
+
is_default boolean DEFAULT false NOT NULL,
|
|
38
|
+
is_active boolean DEFAULT true NOT NULL,
|
|
39
|
+
sort_order integer DEFAULT 0 NOT NULL,
|
|
40
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
41
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
42
|
+
CONSTRAINT site_themes_pkey PRIMARY KEY (id),
|
|
43
|
+
CONSTRAINT site_themes_slug_key UNIQUE (slug),
|
|
44
|
+
CONSTRAINT site_themes_color_scheme_check CHECK ((color_scheme = ANY (ARRAY['light'::text, 'dark'::text]))),
|
|
45
|
+
-- The slug becomes a CSS class and a next-themes value; keep it safe for both.
|
|
46
|
+
CONSTRAINT site_themes_slug_format_check CHECK ((slug ~ '^[a-z][a-z0-9-]{0,38}[a-z0-9]$')),
|
|
47
|
+
CONSTRAINT site_themes_tokens_is_object_check CHECK ((jsonb_typeof(tokens) = 'object'))
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
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.';
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS site_themes_active_sort_idx ON public.site_themes USING btree (is_active, sort_order);
|
|
53
|
+
|
|
54
|
+
-- Exactly one default theme.
|
|
55
|
+
CREATE UNIQUE INDEX IF NOT EXISTS site_themes_single_default_idx ON public.site_themes USING btree (is_default) WHERE (is_default = true);
|
|
56
|
+
|
|
57
|
+
DROP TRIGGER IF EXISTS set_site_themes_updated_at ON public.site_themes;
|
|
58
|
+
CREATE TRIGGER set_site_themes_updated_at
|
|
59
|
+
BEFORE UPDATE ON public.site_themes
|
|
60
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();
|
|
61
|
+
|
|
62
|
+
-- A system theme must never be deleted, whoever asks.
|
|
63
|
+
CREATE OR REPLACE FUNCTION public.prevent_system_theme_delete() RETURNS trigger
|
|
64
|
+
LANGUAGE plpgsql
|
|
65
|
+
SET search_path = ''
|
|
66
|
+
AS $$
|
|
67
|
+
BEGIN
|
|
68
|
+
IF OLD.is_system THEN
|
|
69
|
+
RAISE EXCEPTION 'Theme "%" is a system theme and cannot be deleted', OLD.slug
|
|
70
|
+
USING ERRCODE = 'restrict_violation';
|
|
71
|
+
END IF;
|
|
72
|
+
RETURN OLD;
|
|
73
|
+
END;
|
|
74
|
+
$$;
|
|
75
|
+
|
|
76
|
+
DROP TRIGGER IF EXISTS trg_prevent_system_theme_delete ON public.site_themes;
|
|
77
|
+
CREATE TRIGGER trg_prevent_system_theme_delete
|
|
78
|
+
BEFORE DELETE ON public.site_themes
|
|
79
|
+
FOR EACH ROW EXECUTE FUNCTION public.prevent_system_theme_delete();
|
|
80
|
+
|
|
81
|
+
-- Promoting a theme to default demotes the previous one, so the unique partial
|
|
82
|
+
-- index above can never trip on a normal "make this the default" write.
|
|
83
|
+
CREATE OR REPLACE FUNCTION public.handle_default_theme_change() RETURNS trigger
|
|
84
|
+
LANGUAGE plpgsql
|
|
85
|
+
SET search_path = ''
|
|
86
|
+
AS $$
|
|
87
|
+
BEGIN
|
|
88
|
+
IF NEW.is_default THEN
|
|
89
|
+
UPDATE public.site_themes
|
|
90
|
+
SET is_default = false
|
|
91
|
+
WHERE id <> NEW.id AND is_default;
|
|
92
|
+
END IF;
|
|
93
|
+
RETURN NEW;
|
|
94
|
+
END;
|
|
95
|
+
$$;
|
|
96
|
+
|
|
97
|
+
DROP TRIGGER IF EXISTS trg_handle_default_theme_change ON public.site_themes;
|
|
98
|
+
CREATE TRIGGER trg_handle_default_theme_change
|
|
99
|
+
AFTER INSERT OR UPDATE OF is_default ON public.site_themes
|
|
100
|
+
FOR EACH ROW WHEN (NEW.is_default) EXECUTE FUNCTION public.handle_default_theme_change();
|
|
101
|
+
|
|
102
|
+
ALTER TABLE public.site_themes ENABLE ROW LEVEL SECURITY;
|
|
103
|
+
|
|
104
|
+
GRANT ALL ON TABLE public.site_themes TO anon;
|
|
105
|
+
GRANT ALL ON TABLE public.site_themes TO authenticated;
|
|
106
|
+
GRANT ALL ON TABLE public.site_themes TO service_role;
|
|
107
|
+
|
|
108
|
+
DROP POLICY IF EXISTS "Public read active themes" ON public.site_themes;
|
|
109
|
+
CREATE POLICY "Public read active themes" ON public.site_themes
|
|
110
|
+
FOR SELECT TO authenticated, anon USING (true);
|
|
111
|
+
|
|
112
|
+
DROP POLICY IF EXISTS "Admins insert themes" ON public.site_themes;
|
|
113
|
+
CREATE POLICY "Admins insert themes" ON public.site_themes
|
|
114
|
+
FOR INSERT TO authenticated
|
|
115
|
+
WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
116
|
+
|
|
117
|
+
DROP POLICY IF EXISTS "Admins update themes" ON public.site_themes;
|
|
118
|
+
CREATE POLICY "Admins update themes" ON public.site_themes
|
|
119
|
+
FOR UPDATE TO authenticated
|
|
120
|
+
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))
|
|
121
|
+
WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
122
|
+
|
|
123
|
+
DROP POLICY IF EXISTS "Admins delete themes" ON public.site_themes;
|
|
124
|
+
CREATE POLICY "Admins delete themes" ON public.site_themes
|
|
125
|
+
FOR DELETE TO authenticated
|
|
126
|
+
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
127
|
+
|
|
128
|
+
DROP POLICY IF EXISTS "Service role manages themes" ON public.site_themes;
|
|
129
|
+
CREATE POLICY "Service role manages themes" ON public.site_themes
|
|
130
|
+
TO service_role USING (true) WITH CHECK (true);
|
|
131
|
+
|
|
132
|
+
-- Seed the three shipped themes from libs/ui/src/styles/theme.css.
|
|
133
|
+
-- `--warning` / `--warning-foreground` are declared in the Tailwind theme
|
|
134
|
+
-- (libs/ui/tailwind.config.js) but were never defined in CSS, so bg-warning and
|
|
135
|
+
-- text-warning resolved to an invalid colour. They are given real values here.
|
|
136
|
+
INSERT INTO public.site_themes (slug, name, description, icon, color_scheme, is_system, is_default, sort_order, tokens, extra_css)
|
|
137
|
+
VALUES
|
|
138
|
+
(
|
|
139
|
+
'light', 'Light', 'Clean, technical, stark.', 'Sun', 'light', true, true, 10,
|
|
140
|
+
'{
|
|
141
|
+
"background": "0 0% 100%",
|
|
142
|
+
"foreground": "222 47% 11%",
|
|
143
|
+
"card": "0 0% 100%",
|
|
144
|
+
"card-foreground": "222 47% 11%",
|
|
145
|
+
"popover": "0 0% 100%",
|
|
146
|
+
"popover-foreground": "222 47% 11%",
|
|
147
|
+
"primary": "211.55 50.26% 37.84%",
|
|
148
|
+
"primary-foreground": "210 40% 98%",
|
|
149
|
+
"secondary": "210 40% 96.1%",
|
|
150
|
+
"secondary-foreground": "222 47% 11%",
|
|
151
|
+
"muted": "210 40% 96.1%",
|
|
152
|
+
"muted-foreground": "215 16% 47%",
|
|
153
|
+
"accent": "210 40% 96.1%",
|
|
154
|
+
"accent-foreground": "222 47% 11%",
|
|
155
|
+
"destructive": "0 84.2% 60.2%",
|
|
156
|
+
"destructive-foreground": "210 40% 98%",
|
|
157
|
+
"warning": "38 92% 50%",
|
|
158
|
+
"warning-foreground": "222 47% 11%",
|
|
159
|
+
"border": "214.3 31.8% 91.4%",
|
|
160
|
+
"input": "214.3 31.8% 91.4%",
|
|
161
|
+
"ring": "211.55 50.26% 37.84%",
|
|
162
|
+
"radius": "0.75rem",
|
|
163
|
+
"chart-1": "211.55 50.26% 37.84%",
|
|
164
|
+
"chart-2": "215 16% 47%",
|
|
165
|
+
"chart-3": "215 25% 27%",
|
|
166
|
+
"chart-4": "210 40% 96%",
|
|
167
|
+
"chart-5": "214 32% 91%"
|
|
168
|
+
}'::jsonb,
|
|
169
|
+
NULL
|
|
170
|
+
),
|
|
171
|
+
(
|
|
172
|
+
'dark', 'Dark', 'Midnight / neon tech.', 'Moon', 'dark', true, false, 20,
|
|
173
|
+
'{
|
|
174
|
+
"background": "222 47% 2%",
|
|
175
|
+
"foreground": "210 40% 98%",
|
|
176
|
+
"card": "222 47% 11%",
|
|
177
|
+
"card-foreground": "210 40% 98%",
|
|
178
|
+
"popover": "222 47% 11%",
|
|
179
|
+
"popover-foreground": "210 40% 98%",
|
|
180
|
+
"primary": "217 91% 60%",
|
|
181
|
+
"primary-foreground": "222 47% 11%",
|
|
182
|
+
"secondary": "217.2 32.6% 17.5%",
|
|
183
|
+
"secondary-foreground": "210 40% 98%",
|
|
184
|
+
"muted": "217.2 32.6% 17.5%",
|
|
185
|
+
"muted-foreground": "215 20.2% 65.1%",
|
|
186
|
+
"accent": "217.2 32.6% 17.5%",
|
|
187
|
+
"accent-foreground": "210 40% 98%",
|
|
188
|
+
"destructive": "0 62.8% 30.6%",
|
|
189
|
+
"destructive-foreground": "210 40% 98%",
|
|
190
|
+
"warning": "38 92% 50%",
|
|
191
|
+
"warning-foreground": "222 47% 11%",
|
|
192
|
+
"border": "217.2 32.6% 17.5%",
|
|
193
|
+
"input": "217.2 32.6% 17.5%",
|
|
194
|
+
"ring": "224 76% 48%",
|
|
195
|
+
"radius": "0.75rem",
|
|
196
|
+
"chart-1": "220 70% 50%",
|
|
197
|
+
"chart-2": "160 60% 45%",
|
|
198
|
+
"chart-3": "30 80% 55%",
|
|
199
|
+
"chart-4": "280 65% 60%",
|
|
200
|
+
"chart-5": "340 75% 55%"
|
|
201
|
+
}'::jsonb,
|
|
202
|
+
NULL
|
|
203
|
+
),
|
|
204
|
+
(
|
|
205
|
+
'vibrant', 'Vibrant', 'Cyberpunk neon.', 'Zap', 'dark', false, false, 30,
|
|
206
|
+
'{
|
|
207
|
+
"background": "260 50% 5%",
|
|
208
|
+
"foreground": "180 100% 90%",
|
|
209
|
+
"card": "260 50% 8%",
|
|
210
|
+
"card-foreground": "180 100% 90%",
|
|
211
|
+
"popover": "260 50% 8%",
|
|
212
|
+
"popover-foreground": "180 100% 90%",
|
|
213
|
+
"primary": "320 100% 55%",
|
|
214
|
+
"primary-foreground": "0 0% 100%",
|
|
215
|
+
"secondary": "180 100% 50%",
|
|
216
|
+
"secondary-foreground": "260 50% 5%",
|
|
217
|
+
"muted": "260 30% 15%",
|
|
218
|
+
"muted-foreground": "260 20% 65%",
|
|
219
|
+
"accent": "280 100% 50%",
|
|
220
|
+
"accent-foreground": "0 0% 100%",
|
|
221
|
+
"destructive": "0 100% 50%",
|
|
222
|
+
"destructive-foreground": "0 0% 100%",
|
|
223
|
+
"warning": "60 100% 50%",
|
|
224
|
+
"warning-foreground": "260 50% 5%",
|
|
225
|
+
"border": "320 100% 55%",
|
|
226
|
+
"input": "260 30% 15%",
|
|
227
|
+
"ring": "320 100% 55%",
|
|
228
|
+
"radius": "0px",
|
|
229
|
+
"chart-1": "320 100% 55%",
|
|
230
|
+
"chart-2": "180 100% 50%",
|
|
231
|
+
"chart-3": "280 100% 50%",
|
|
232
|
+
"chart-4": "60 100% 50%",
|
|
233
|
+
"chart-5": "120 100% 50%"
|
|
234
|
+
}'::jsonb,
|
|
235
|
+
'& h1, & h2, & h3, & h4, & h5, & h6 {
|
|
236
|
+
text-shadow: 0 0 5px hsl(var(--primary)), 0 0 10px hsl(var(--secondary));
|
|
237
|
+
}
|
|
238
|
+
& button, & [role="button"] {
|
|
239
|
+
box-shadow: 0 0 5px hsl(var(--primary) / 0.5);
|
|
240
|
+
transition: box-shadow 0.3s ease;
|
|
241
|
+
}
|
|
242
|
+
& button:hover, & [role="button"]:hover {
|
|
243
|
+
box-shadow: 0 0 15px hsl(var(--primary));
|
|
244
|
+
}
|
|
245
|
+
& .card, & [class*="card"] {
|
|
246
|
+
border: 1px solid hsl(var(--primary));
|
|
247
|
+
box-shadow: 0 0 10px hsl(var(--primary) / 0.2);
|
|
248
|
+
}
|
|
249
|
+
& .border {
|
|
250
|
+
border-color: hsl(var(--border));
|
|
251
|
+
box-shadow: 0 0 5px hsl(var(--border) / 0.3);
|
|
252
|
+
}'
|
|
253
|
+
)
|
|
254
|
+
ON CONFLICT (slug) DO NOTHING;
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
-- Scheduled publishing for pages and products.
|
|
2
|
-
--
|
|
3
|
-
-- Posts already support scheduling: `posts.published_at` exists and every public
|
|
4
|
-
-- read gates on `published_at IS NULL OR published_at <= now()`, so a row with
|
|
5
|
-
-- status='published' and a future date is withheld until the date passes. Pages
|
|
6
|
-
-- and products had no equivalent column, so "go live on Tuesday" was impossible
|
|
7
|
-
-- for them. This adds the same column with the same semantics.
|
|
8
|
-
--
|
|
9
|
-
-- Visibility is derived from the (status, published_at) PAIR — no new enum value:
|
|
10
|
-
--
|
|
11
|
-
-- status = draft/archived -> not public, whatever the date
|
|
12
|
-
-- status = published|active, published_at NULL -> public now
|
|
13
|
-
-- status = published|active, date <= now() -> public now
|
|
14
|
-
-- status = published|active, date > now() -> SCHEDULED (withheld)
|
|
15
|
-
--
|
|
16
|
-
-- Deriving "scheduled" instead of storing it keeps `page_status` unchanged (adding
|
|
17
|
-
-- an enum value can't be done inside a transaction with other DDL in Postgres) and
|
|
18
|
-
-- matches what posts have always done, so one code path covers all three types.
|
|
19
|
-
--
|
|
20
|
-
-- NULL is the safe default: every existing published row keeps rendering exactly as
|
|
21
|
-
-- before, so this migration needs no backfill and changes no current behavior.
|
|
22
|
-
--
|
|
23
|
-
-- Forward-only and idempotent.
|
|
24
|
-
|
|
25
|
-
ALTER TABLE public.pages
|
|
26
|
-
ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;
|
|
27
|
-
|
|
28
|
-
COMMENT ON COLUMN public.pages.published_at IS
|
|
29
|
-
'Optional go-live moment. NULL = live as soon as status is published. A future value withholds the page from public reads until it passes (status stays "published"; the CMS renders that pair as "Scheduled").';
|
|
30
|
-
|
|
31
|
-
ALTER TABLE public.products
|
|
32
|
-
ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;
|
|
33
|
-
|
|
34
|
-
COMMENT ON COLUMN public.products.published_at IS
|
|
35
|
-
'Optional go-live moment. NULL = live as soon as status is active. A future value withholds the product from public reads until it passes (status stays "active"; the CMS renders that pair as "Scheduled").';
|
|
36
|
-
|
|
37
|
-
-- Public listing/index queries filter on the (status, published_at) pair together
|
|
38
|
-
-- (catalog, sitemap, page lookups), so a composite index serves them in one pass.
|
|
39
|
-
CREATE INDEX IF NOT EXISTS pages_status_published_at_idx
|
|
40
|
-
ON public.pages (status, published_at);
|
|
41
|
-
|
|
42
|
-
CREATE INDEX IF NOT EXISTS products_status_published_at_idx
|
|
43
|
-
ON public.products (status, published_at);
|
|
1
|
+
-- Scheduled publishing for pages and products.
|
|
2
|
+
--
|
|
3
|
+
-- Posts already support scheduling: `posts.published_at` exists and every public
|
|
4
|
+
-- read gates on `published_at IS NULL OR published_at <= now()`, so a row with
|
|
5
|
+
-- status='published' and a future date is withheld until the date passes. Pages
|
|
6
|
+
-- and products had no equivalent column, so "go live on Tuesday" was impossible
|
|
7
|
+
-- for them. This adds the same column with the same semantics.
|
|
8
|
+
--
|
|
9
|
+
-- Visibility is derived from the (status, published_at) PAIR — no new enum value:
|
|
10
|
+
--
|
|
11
|
+
-- status = draft/archived -> not public, whatever the date
|
|
12
|
+
-- status = published|active, published_at NULL -> public now
|
|
13
|
+
-- status = published|active, date <= now() -> public now
|
|
14
|
+
-- status = published|active, date > now() -> SCHEDULED (withheld)
|
|
15
|
+
--
|
|
16
|
+
-- Deriving "scheduled" instead of storing it keeps `page_status` unchanged (adding
|
|
17
|
+
-- an enum value can't be done inside a transaction with other DDL in Postgres) and
|
|
18
|
+
-- matches what posts have always done, so one code path covers all three types.
|
|
19
|
+
--
|
|
20
|
+
-- NULL is the safe default: every existing published row keeps rendering exactly as
|
|
21
|
+
-- before, so this migration needs no backfill and changes no current behavior.
|
|
22
|
+
--
|
|
23
|
+
-- Forward-only and idempotent.
|
|
24
|
+
|
|
25
|
+
ALTER TABLE public.pages
|
|
26
|
+
ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;
|
|
27
|
+
|
|
28
|
+
COMMENT ON COLUMN public.pages.published_at IS
|
|
29
|
+
'Optional go-live moment. NULL = live as soon as status is published. A future value withholds the page from public reads until it passes (status stays "published"; the CMS renders that pair as "Scheduled").';
|
|
30
|
+
|
|
31
|
+
ALTER TABLE public.products
|
|
32
|
+
ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;
|
|
33
|
+
|
|
34
|
+
COMMENT ON COLUMN public.products.published_at IS
|
|
35
|
+
'Optional go-live moment. NULL = live as soon as status is active. A future value withholds the product from public reads until it passes (status stays "active"; the CMS renders that pair as "Scheduled").';
|
|
36
|
+
|
|
37
|
+
-- Public listing/index queries filter on the (status, published_at) pair together
|
|
38
|
+
-- (catalog, sitemap, page lookups), so a composite index serves them in one pass.
|
|
39
|
+
CREATE INDEX IF NOT EXISTS pages_status_published_at_idx
|
|
40
|
+
ON public.pages (status, published_at);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS products_status_published_at_idx
|
|
43
|
+
ON public.products (status, published_at);
|