@sevenfold/setto-client 0.1.0

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/README.md ADDED
@@ -0,0 +1,369 @@
1
+ # @setto/client
2
+
3
+ React client library for Setto — git-based inline CMS for Vite + i18next apps.
4
+
5
+ Published on npm as **`@sevenfold/setto-client`** (Sevenfold org). Import as `@setto/client` in app code.
6
+
7
+ Editors authenticate via Supabase, edit text inline on the live page, pick section colours from the site brand palette, and publish changes back to GitHub. setto-server commits whitelisted files and tracks the Vercel deployment.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add @setto/client@npm:@sevenfold/setto-client
13
+ ```
14
+
15
+ Or add directly to `package.json`:
16
+
17
+ ```json
18
+ "@setto/client": "npm:@sevenfold/setto-client@^0.1.0"
19
+ ```
20
+
21
+ Peer deps: `react`, `react-dom`, `react-i18next`, `i18next`.
22
+
23
+ For local monorepo development alongside `carryon.no`, a `file:` link also works — see [Local development](#local-development-sevenfold-monorepo).
24
+
25
+ ### Publishing a new version
26
+
27
+ 1. Bump `version` in `package.json`
28
+ 2. Commit and push a tag: `git tag v0.1.1 && git push origin v0.1.1`
29
+ 3. GitHub Actions publishes to npm automatically
30
+
31
+ Requires an `NPM_TOKEN` secret in the GitHub repo (Granular Access Token from npmjs.com with write access to `@sevenfold`).
32
+
33
+ Or trigger manually via **Actions → Publish to npm → Run workflow**.
34
+
35
+ ---
36
+
37
+ ## Quick start
38
+
39
+ ### 1. Wrap the app
40
+
41
+ ```tsx
42
+ // main.tsx
43
+ import { SettoProvider } from '@setto/client';
44
+ import sectionsTheme from './theme/sections.json';
45
+ import { brandColors } from './theme/brand-colors';
46
+ import { sectionSchemas } from './theme/section-schemas';
47
+
48
+ createRoot(document.getElementById('root')!).render(
49
+ <SettoProvider
50
+ config={{
51
+ siteId: 'my-site', // slug in setto-server `sites` table
52
+ apiUrl: import.meta.env.VITE_SETTO_API_URL,
53
+ supabase: {
54
+ url: import.meta.env.VITE_SUPABASE_URL,
55
+ anonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
56
+ },
57
+ theme: sectionsTheme, // bundled defaults for section colours
58
+ themePath: 'src/theme/sections.json', // GitHub path — must be in content_paths
59
+ brandColors, // palette for the colour toolbar
60
+ sectionSchemas, // which fields each section exposes
61
+ }}
62
+ >
63
+ <BrowserRouter>
64
+ <App />
65
+ </BrowserRouter>
66
+ </SettoProvider>,
67
+ );
68
+ ```
69
+
70
+ ### 2. Mount the admin route
71
+
72
+ ```tsx
73
+ // App.tsx
74
+ import { SettoAdminApp } from '@setto/client';
75
+
76
+ <Routes>
77
+ <Route path="/" element={<Home />} />
78
+ <Route path="/admin/*" element={<SettoAdminApp />} />
79
+ </Routes>
80
+ ```
81
+
82
+ ### 3. Replace copy with `<T>`
83
+
84
+ ```tsx
85
+ import { T } from '@setto/client';
86
+
87
+ <h1><T k="hero.headline" /></h1>
88
+ <p><T k="hero.subheadline" /></p>
89
+
90
+ {/* Dynamic keys work too */}
91
+ <T k={`faq.items.${item.key}.q`} />
92
+ ```
93
+
94
+ Keep using `t()` for non-visible strings (placeholders, `aria-label`, `alt`) — those are not inline-editable in v0.
95
+
96
+ ### 4. Wire section colours (optional)
97
+
98
+ ```tsx
99
+ import { SettoSection, useSectionTheme } from '@setto/client';
100
+
101
+ function ValuesSection() {
102
+ const colors = useSectionTheme('values');
103
+
104
+ return (
105
+ <SettoSection sectionId="values" id="verdier" className="py-20">
106
+ <span style={{ color: colors.label }}><T k="values.label" /></span>
107
+ <h2 style={{ color: colors.heading }}><T k="values.headline" /></h2>
108
+ </SettoSection>
109
+ );
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Environment variables (host app)
116
+
117
+ | Variable | Purpose |
118
+ |----------|---------|
119
+ | `VITE_SUPABASE_URL` | Supabase project URL |
120
+ | `VITE_SUPABASE_ANON_KEY` | Supabase anon key (public) |
121
+ | `VITE_SETTO_API_URL` | setto-server base URL, e.g. `http://localhost:3001` |
122
+
123
+ ---
124
+
125
+ ## Edit mode
126
+
127
+ Edit mode activates when **both** are true:
128
+
129
+ 1. Authenticated Supabase session.
130
+ 2. URL contains `?setto=edit`.
131
+
132
+ The `/admin` dashboard does **not** activate edit mode. Use **Begynn å redigere**, which navigates to `/?setto=edit`.
133
+
134
+ ### What editors see
135
+
136
+ | Action | How |
137
+ |--------|-----|
138
+ | Edit text | Click any `<T>` element — it becomes `contentEditable` |
139
+ | Edit section colours | Click a section or block background (not text) |
140
+ | Follow a link | Ctrl/Cmd + click on nav links |
141
+ | Publish | Top toolbar → **Publiser** |
142
+ | Exit | Top toolbar → **Avslutt** (removes `?setto=edit`) |
143
+
144
+ A fixed toolbar sits at the top of the viewport. When you click a section or block, a compact colour toolbar appears above it, centred and sized to its contents. Click again or press Escape to dismiss.
145
+
146
+ ---
147
+
148
+ ## Text editing (`<T>`)
149
+
150
+ `<T k="dotted.key" />` renders the i18next string for the active language.
151
+
152
+ In edit mode the text is inline-editable. Changes are stored in an in-memory draft layer (`I18nStore`) and applied to the live i18next bundle immediately so the page re-renders.
153
+
154
+ On publish, the full locale bundles are serialised to JSON and committed to GitHub:
155
+
156
+ ```
157
+ src/i18n/locales/no.json
158
+ src/i18n/locales/en.json
159
+ ```
160
+
161
+ When edit mode starts, setto-server loads the current files from GitHub as the baseline (`GET /sites/:id/content`).
162
+
163
+ ---
164
+
165
+ ## Section colours
166
+
167
+ Section colours live in a separate JSON file (not in i18n):
168
+
169
+ ```
170
+ src/theme/sections.json
171
+ ```
172
+
173
+ Example:
174
+
175
+ ```json
176
+ {
177
+ "values": {
178
+ "background": "#640AFF",
179
+ "label": "#C9C0DA",
180
+ "heading": "#FFFFFF",
181
+ "icon": "#FFFFFF",
182
+ "cardTitle": "#FFFFFF",
183
+ "cardDesc": "#C9C0DA"
184
+ }
185
+ }
186
+ ```
187
+
188
+ ### Brand palette (`brandColors`)
189
+
190
+ Editors do **not** get a free-form colour picker. Each colour field shows the **current swatch**; clicking it opens a menu of predefined brand colours:
191
+
192
+ ```ts
193
+ // theme/brand-colors.ts
194
+ import type { BrandColor } from '@setto/client';
195
+
196
+ export const brandColors: BrandColor[] = [
197
+ { label: 'Beige', value: '#E6DCCF' },
198
+ { label: 'Oliven', value: '#362F00' },
199
+ { label: 'Lilla', value: '#640AFF' },
200
+ // …
201
+ ];
202
+ ```
203
+
204
+ **Rules for integrators:**
205
+
206
+ - List every colour token editors should be able to pick.
207
+ - Values must match the palette used in Tailwind/CSS on the site (same hex values).
208
+ - Use solid hex colours in `sections.json` — rgba values won't match swatches.
209
+ - When you add a new brand token to Tailwind, add it to `brandColors` too.
210
+
211
+ ### Section schemas (`sectionSchemas`)
212
+
213
+ Define which colour fields each section exposes in the toolbar:
214
+
215
+ ```ts
216
+ // theme/section-schemas.ts
217
+ import type { SectionSchema } from '@setto/client';
218
+
219
+ export const sectionSchemas: Record<string, SectionSchema> = {
220
+ values: {
221
+ label: 'Verdier',
222
+ fields: [
223
+ { key: 'background', label: 'Bakgrunn' },
224
+ { key: 'label', label: 'Etikett' },
225
+ { key: 'heading', label: 'Overskrift' },
226
+ // keys must match properties used in useSectionTheme('values')
227
+ ],
228
+ },
229
+ };
230
+ ```
231
+
232
+ The `sectionId` prop on `<SettoSection>` must match a key in both `sectionSchemas` and `sections.json`.
233
+
234
+ ### Applying colours in components
235
+
236
+ Read tokens with `useSectionTheme(sectionId)` and apply via inline `style` (or CSS variables you control):
237
+
238
+ ```tsx
239
+ const colors = useSectionTheme('hero');
240
+
241
+ <SettoSection sectionId="hero" style={{ /* background applied automatically */ }}>
242
+ <h1 style={{ color: colors.heading }}><T k="hero.headline" /></h1>
243
+ </SettoSection>
244
+ ```
245
+
246
+ `<SettoSection>` always applies `colors.background` as `backgroundColor`. Other tokens are your responsibility.
247
+
248
+ ### Nested blocks (`<SettoBlock>`)
249
+
250
+ For cards or panels inside a section, wrap each in `<SettoBlock>` with its own theme key:
251
+
252
+ ```tsx
253
+ import { SettoSection, SettoBlock, useSectionTheme } from '@setto/client';
254
+
255
+ function InnovationSection() {
256
+ const card = useSectionTheme('innovationCard');
257
+
258
+ return (
259
+ <SettoSection sectionId="innovation" className="py-20">
260
+ <SettoBlock blockId="innovationCard" className="p-8">
261
+ <span style={{ color: card.label }}><T k="innovation.label" /></span>
262
+ <h3 style={{ color: card.heading }}><T k="innovation.headline" /></h3>
263
+ </SettoBlock>
264
+ </SettoSection>
265
+ );
266
+ }
267
+ ```
268
+
269
+ Click a block's background to edit **that block's** colours. Click the section padding to edit the **section** background. Add matching keys to `sections.json` and `sectionSchemas` for each block.
270
+
271
+ ---
272
+
273
+ ## Server setup (setto-server)
274
+
275
+ Each site row in Supabase needs:
276
+
277
+ | Column | Example |
278
+ |--------|---------|
279
+ | `id` | `carryon-no` |
280
+ | `repo_owner` / `repo_name` / `branch` | GitHub target |
281
+ | `content_paths` | Whitelist of publishable file paths |
282
+
283
+ Example `content_paths`:
284
+
285
+ ```
286
+ src/i18n/locales/no.json
287
+ src/i18n/locales/en.json
288
+ src/theme/sections.json
289
+ ```
290
+
291
+ Only paths in this list can be committed. Add new content files here before publishing them.
292
+
293
+ Register allowed origins for CORS (`allowed_origins`).
294
+
295
+ ---
296
+
297
+ ## Publish flow
298
+
299
+ 1. Editor clicks **Publiser** in the top toolbar.
300
+ 2. Client serialises changed locale bundles + `sections.json` (if theme drafts exist).
301
+ 3. `POST /sites/:siteId/publish` with `{ files: [{ path, content }] }`.
302
+ 4. setto-server validates paths against `content_paths`, commits to GitHub.
303
+ 5. A `deployments` row is inserted; the toolbar shows build status via Supabase Realtime + Vercel webhook.
304
+
305
+ Drafts are cleared after a successful publish.
306
+
307
+ ---
308
+
309
+ ## Admin app (`SettoAdminApp`)
310
+
311
+ Route: `/admin/*`
312
+
313
+ Provides Supabase email/password login and a dashboard with a link to start editing (`/?setto=edit`). Does not render the inline editor itself.
314
+
315
+ ---
316
+
317
+ ## Local development (Sevenfold monorepo)
318
+
319
+ When consumed by `carryon.no`, Vite aliases `@setto/client` to `src/index.ts` so changes hot-reload without a separate watch build.
320
+
321
+ Typical stack:
322
+
323
+ ```bash
324
+ # Terminal 1 — Supabase
325
+ cd setto-server && supabase start
326
+
327
+ # Terminal 2 — API
328
+ cd setto-server && bun run dev # :3001
329
+
330
+ # Terminal 3 — Site
331
+ cd carryon.no && bun run dev # :3000
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Build (library)
337
+
338
+ ```bash
339
+ bun install
340
+ bun run build
341
+ ```
342
+
343
+ Produces `dist/setto-client.js` and `.d.ts` via Vite library mode. Only needed before publishing to npm.
344
+
345
+ ---
346
+
347
+ ## API surface
348
+
349
+ | Export | Purpose |
350
+ |--------|---------|
351
+ | `SettoProvider` / `useSetto` | Context, auth, stores, edit mode flag |
352
+ | `T` | Inline-editable translation |
353
+ | `SettoSection` | Section wrapper + edit selection |
354
+ | `SettoBlock` | Nested card/panel with its own colour toolbar |
355
+ | `useSectionTheme` | Read section colour tokens |
356
+ | `SettoAdminApp` | `/admin` login + dashboard |
357
+ | `AuthGate` | Standalone login wrapper |
358
+ | `BrandColor`, `SectionSchema`, `SettoConfig` | Types for host config |
359
+
360
+ ---
361
+
362
+ ## Limitations (v0)
363
+
364
+ - `<T>` supports text content only — not HTML attributes (`placeholder`, `alt`, `aria-label`).
365
+ - No list/repeater UI (cannot add a new FAQ row from the editor).
366
+ - Drafts are in-memory — refresh discards unpublished changes.
367
+ - Section colour toolbar only offers `brandColors` — no custom hex/rgba input.
368
+ - CTA card colours and nested component colours are not section-themeable yet.
369
+ - Single editor per site (no concurrent-edit conflict handling).
@@ -0,0 +1,10 @@
1
+ import { type ComponentPropsWithoutRef } from 'react';
2
+ export interface SettoBlockProps extends ComponentPropsWithoutRef<'div'> {
3
+ /** Theme key in sections.json, e.g. 'innovationCard'. */
4
+ blockId: string;
5
+ }
6
+ /**
7
+ * Themeable block (card, panel) inside a section. Click the block chrome to
8
+ * edit its colours; click text to edit copy.
9
+ */
10
+ export declare const SettoBlock: import("react").ForwardRefExoticComponent<SettoBlockProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,11 @@
1
+ import { type ComponentPropsWithoutRef } from 'react';
2
+ export interface SettoSectionProps extends ComponentPropsWithoutRef<'section'> {
3
+ /** Theme key, e.g. 'values'. */
4
+ sectionId: string;
5
+ }
6
+ /**
7
+ * Wraps a page section with theme-driven background colour and edit-mode
8
+ * selection. Click the section chrome (not editable text or nested blocks)
9
+ * to open the colour toolbar.
10
+ */
11
+ export declare const SettoSection: import("react").ForwardRefExoticComponent<SettoSectionProps & import("react").RefAttributes<HTMLElement>>;
package/dist/T.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface TProps {
2
+ /** The dotted translation key, e.g. 'hero.headline'. */
3
+ k: string;
4
+ }
5
+ /**
6
+ * Renders a translation key. In edit mode the text is contentEditable inline;
7
+ * changes flow into the draft store for the active language.
8
+ */
9
+ export declare function T({ k }: TProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Drop-in admin SPA. Mount under a route like `<Route path="/admin/*" .../>`.
3
+ *
4
+ * Behaviour after login:
5
+ * - Lists sites the user is a member of (read via Supabase RLS).
6
+ * - Single-site sites: redirect to `/?setto=edit` automatically.
7
+ * - Multi-site: show a picker.
8
+ *
9
+ * While the user is on `/admin`, this component shows the site dashboard.
10
+ * Editing happens on the public site at `/?setto=edit` via "Begynn å redigere".
11
+ */
12
+ export declare function SettoAdminApp(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface AuthGateProps {
3
+ /** Rendered after a session is established. */
4
+ children: ReactNode;
5
+ }
6
+ /**
7
+ * Wraps `children` with a Supabase email/password login screen. Returns the
8
+ * children only when a session exists.
9
+ */
10
+ export declare function AuthGate({ children }: AuthGateProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { DeploymentRow } from '../types';
3
+ interface BuildStatusProps {
4
+ supabase: SupabaseClient;
5
+ deploymentId: string;
6
+ onDone?: (status: DeploymentRow['status']) => void;
7
+ }
8
+ /**
9
+ * Subscribes to a single row in `deployments` via Supabase Realtime.
10
+ * Renders the three-step progress and an error state on failure.
11
+ */
12
+ export declare function BuildStatus({ supabase, deploymentId, onDone }: BuildStatusProps): import("react/jsx-runtime").JSX.Element;
13
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Height of the fixed edit toolbar (px). */
2
+ export declare const TOOLBAR_HEIGHT = 44;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Reserves space at the top of the viewport for the Setto edit toolbar.
3
+ * The host site keeps full width — only vertical offset is applied.
4
+ */
5
+ export declare function useSettoDocumentLayout(active: boolean): void;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tooltip when hovering editable text inside a link or button (except language
3
+ * toggles). Flips to stay within the viewport.
4
+ */
5
+ export declare function EditLinkHint(): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ThemeStore } from '../lib/theme-store';
3
+ interface EditModeShellProps {
4
+ children: ReactNode;
5
+ themeStore: ThemeStore | null;
6
+ }
7
+ /**
8
+ * Edit mode: full-width page with a fixed toolbar at the top. Text is edited
9
+ * inline via contentEditable on `<T>` — no sidebar.
10
+ */
11
+ export declare function EditModeShell({ children, themeStore }: EditModeShellProps): import("react/jsx-runtime").JSX.Element;
12
+ /** @deprecated Use EditModeShell */
13
+ export declare const EditModeMount: typeof EditModeShell;
14
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare function guessLangFromPath(path: string, languages: string[]): string | null;
2
+ export declare function pathForLang(_siteId: string, lng: string, _languages: string[]): string;
3
+ export declare function pathForTheme(_siteId: string, themePath?: string): string;
4
+ export declare function isThemePath(path: string, themePath?: string): boolean;
@@ -0,0 +1 @@
1
+ export declare function SectionToolbar(): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1 @@
1
+ export declare function EditToolbar(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ /** Loads locale and theme files from GitHub once per session to seed the draft baseline. */
2
+ export declare function useEditBaseline(): void;
@@ -0,0 +1,9 @@
1
+ export { SettoProvider, useSetto } from './provider';
2
+ export { T } from './T';
3
+ export { SettoSection } from './SettoSection';
4
+ export { SettoBlock } from './SettoBlock';
5
+ export { useSectionTheme } from './use-section-theme';
6
+ export { SettoAdminApp } from './admin/App';
7
+ export { AuthGate } from './edit-mode/auth-gate';
8
+ export type { SettoConfig, BrandColor, DeploymentRow, SiteRow, ContentFile, PublishResult, } from './types';
9
+ export type { SectionSchema, SectionColorField } from './section-schema';
@@ -0,0 +1,15 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { ContentFile, PublishResult } from '../types';
3
+ export declare function createApi(args: {
4
+ apiUrl: string;
5
+ supabase: SupabaseClient;
6
+ }): {
7
+ getContent(siteId: string): Promise<{
8
+ files: ContentFile[];
9
+ }>;
10
+ publish(siteId: string, files: Array<{
11
+ path: string;
12
+ content: string;
13
+ }>, message?: string): Promise<PublishResult>;
14
+ };
15
+ export type SettoApi = ReturnType<typeof createApi>;
@@ -0,0 +1,4 @@
1
+ /** Returns true when two CSS colour strings represent the same sRGB value. */
2
+ export declare function colorsMatch(a: string, b: string): boolean;
3
+ /** Pick a visible border colour for a swatch on white UI chrome. */
4
+ export declare function swatchBorder(color: string): string;
@@ -0,0 +1,75 @@
1
+ import type { i18n as I18nType } from 'i18next';
2
+ /**
3
+ * Tracks an in-memory layer of edits on top of a loaded i18next instance.
4
+ *
5
+ * On disk, a site has one JSON file per language under `content_paths`. At
6
+ * runtime, i18next holds those JSONs as resource bundles in
7
+ * `i18n.store.data[lng].translation`.
8
+ *
9
+ * The draft store mutates the resource bundles directly so the page re-renders
10
+ * with the editor's text immediately. When the user publishes, we serialise
11
+ * the merged resource bundles back to JSON and POST them to setto-server.
12
+ */
13
+ export interface DraftEntry {
14
+ /** Dotted key path, e.g. 'hero.headline'. */
15
+ key: string;
16
+ /** Language code, e.g. 'no'. */
17
+ lng: string;
18
+ /** Value before any edits in this session — used for diff and revert. */
19
+ original: string;
20
+ /** Current draft value. */
21
+ current: string;
22
+ }
23
+ export interface I18nStoreSnapshot {
24
+ /** All edits made in this session, keyed by `${lng}::${key}`. */
25
+ drafts: Map<string, DraftEntry>;
26
+ /** Bumps every time the store changes; React components subscribe to this. */
27
+ version: number;
28
+ }
29
+ /** Stable empty snapshot for useSyncExternalStore when the store is not ready. */
30
+ export declare const EMPTY_I18N_STORE_SNAPSHOT: I18nStoreSnapshot;
31
+ type Listener = (snap: I18nStoreSnapshot) => void;
32
+ export declare class I18nStore {
33
+ private drafts;
34
+ private version;
35
+ private listeners;
36
+ /** Stable namespace used by react-i18next defaults. */
37
+ private ns;
38
+ private i18n;
39
+ /** Cached reference — useSyncExternalStore requires referential stability. */
40
+ private cachedSnapshot;
41
+ constructor(i18n: I18nType, options?: {
42
+ namespace?: string;
43
+ });
44
+ snapshot(): I18nStoreSnapshot;
45
+ subscribe(listener: Listener): () => void;
46
+ /**
47
+ * Returns the current rendered value for a key in a given language,
48
+ * reading through any draft layer.
49
+ */
50
+ get(key: string, lng: string): string;
51
+ /** Returns the original (pre-edit) value for a key. */
52
+ original(key: string, lng: string): string;
53
+ /**
54
+ * Records an edit. Mutates the live i18next bundle so consumers re-render.
55
+ * If the value is set back to the original, the draft entry is cleared.
56
+ */
57
+ set(key: string, lng: string, value: string): void;
58
+ /** Resets a single draft back to its original value. */
59
+ revert(key: string, lng: string): void;
60
+ /** Resets every draft. */
61
+ revertAll(): void;
62
+ /** Marks the current state as the new baseline (after a successful publish). */
63
+ commit(): void;
64
+ /** Number of pending draft entries across all languages. */
65
+ size(): number;
66
+ /**
67
+ * Serialises the full resource bundle per language as it currently looks in
68
+ * i18next (with drafts applied). Used as the payload for /publish.
69
+ *
70
+ * The caller maps language → file path via `sites.content_paths`.
71
+ */
72
+ serialiseBundles(): Record<string, unknown>;
73
+ private bump;
74
+ }
75
+ export {};
@@ -0,0 +1,9 @@
1
+ import { type SupabaseClient } from '@supabase/supabase-js';
2
+ /**
3
+ * Returns a Supabase client for a given (url, anonKey) pair.
4
+ * Memoised so we never create more than one client per provider mount.
5
+ */
6
+ export declare function getSupabase(config: {
7
+ url: string;
8
+ anonKey: string;
9
+ }): SupabaseClient;
@@ -0,0 +1,35 @@
1
+ /** Section colour theme — keyed by section id, then colour token. */
2
+ export type SectionsTheme = Record<string, Record<string, string>>;
3
+ export interface ThemeDraftEntry {
4
+ sectionId: string;
5
+ field: string;
6
+ original: string;
7
+ current: string;
8
+ }
9
+ export interface ThemeStoreSnapshot {
10
+ drafts: Map<string, ThemeDraftEntry>;
11
+ version: number;
12
+ }
13
+ export declare const EMPTY_THEME_STORE_SNAPSHOT: ThemeStoreSnapshot;
14
+ type Listener = (snap: ThemeStoreSnapshot) => void;
15
+ export declare class ThemeStore {
16
+ private data;
17
+ private baseline;
18
+ private drafts;
19
+ private version;
20
+ private listeners;
21
+ private cachedSnapshot;
22
+ constructor(initial: SectionsTheme);
23
+ snapshot(): ThemeStoreSnapshot;
24
+ subscribe(listener: Listener): () => void;
25
+ getSection(sectionId: string): Record<string, string>;
26
+ get(sectionId: string, field: string): string;
27
+ set(sectionId: string, field: string, value: string): void;
28
+ /** Replace baseline from GitHub without creating drafts. */
29
+ loadBaseline(next: SectionsTheme): void;
30
+ commit(): void;
31
+ size(): number;
32
+ serialise(): SectionsTheme;
33
+ private bump;
34
+ }
35
+ export {};