@sevenfold/setto-client
v0.5.0
Published
React client library for Setto — git-based inline CMS for Vite + i18next apps.
Readme
@setto/client
React client library for Setto — git-based inline CMS for Vite + i18next apps.
Published on npm as @sevenfold/setto-client (Sevenfold org). Import as @setto/client in app code.
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.
Install
Sevenfold sites pin a git tag, not an npm version, so changes ship without an npm round-trip:
"@setto/client": "github:nitech/setto-client#v0.5.0"Bun clones the repo on bun install, runs the package's prepare script to build dist/, and links it as @setto/client. Vercel does the same automatically when it installs build deps.
The package is also still published to npm (@sevenfold/setto-client) for external consumers:
bun add @setto/client@npm:@sevenfold/setto-clientPeer deps: react, react-dom, react-i18next, i18next.
For local monorepo development the consumer's vite.config.ts aliases @setto/client to setto-client source — see Local development.
Releasing a new version
- Bump
versioninpackage.jsonfollowing semver (patch / minor / major). - Commit and tag:
git tag v0.5.1 && git push origin main --follow-tags. - In each consumer (
carryon.no,setto-site):- Update the tag in
package.json(e.g.#v0.5.0→#v0.5.1). - Run
bun installto refresh the lockfile. - Push — Vercel pulls the new tag on the next deploy.
- Update the tag in
GitHub Actions still publishes to npm automatically when package.json lands on main (requires NPM_TOKEN secret), but Sevenfold consumers no longer wait for it.
Quick start
1. Wrap the app
// main.tsx
import { SettoProvider } from '@setto/client';
import sectionsTheme from './theme/sections.json';
import { brandColors } from './theme/brand-colors';
import { sectionSchemas } from './theme/section-schemas';
createRoot(document.getElementById('root')!).render(
<SettoProvider
config={{
siteId: 'my-site', // slug in setto-server `sites` table
apiUrl: import.meta.env.VITE_SETTO_API_URL,
supabase: {
url: import.meta.env.VITE_SUPABASE_URL,
anonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
},
theme: sectionsTheme, // bundled defaults for section colours
themePath: 'src/theme/sections.json', // GitHub path — must be in content_paths
brandColors, // palette for the colour toolbar
sectionSchemas, // which fields each section exposes
}}
>
<BrowserRouter>
<App />
</BrowserRouter>
</SettoProvider>,
);2. Mount the Setto route
Editors sign in once at sitenavn.no/setto. After that, every visit to the public site auto-enters edit mode for as long as the Supabase session persists.
// App.tsx
import { SettoAdminApp } from '@setto/client';
<Routes>
<Route path="/" element={<Home />} />
<Route path="/setto/*" element={<SettoAdminApp />} />
</Routes>3. Replace copy with <T>
import { T } from '@setto/client';
<h1><T k="hero.headline" /></h1>
<p><T k="hero.subheadline" /></p>
{/* Dynamic keys work too */}
<T k={`faq.items.${item.key}.q`} />Keep using t() for non-visible strings (placeholders, aria-label, alt) — those are not inline-editable in v0.
4. Wire section colours (optional)
import { SettoSection, useSectionTheme } from '@setto/client';
function ValuesSection() {
const colors = useSectionTheme('values');
return (
<SettoSection sectionId="values" id="verdier" className="py-20">
<span style={{ color: colors.label }}><T k="values.label" /></span>
<h2 style={{ color: colors.heading }}><T k="values.headline" /></h2>
</SettoSection>
);
}Environment variables (host app)
| Variable | Purpose |
|----------|---------|
| VITE_SUPABASE_URL | Supabase project URL |
| VITE_SUPABASE_ANON_KEY | Supabase anon key (public) |
| VITE_SETTO_API_URL | setto-server base URL, e.g. http://localhost:3001 |
Edit mode
Edit mode activates whenever all of these are true:
- The user has an authenticated Supabase session.
- The user has access to
config.siteId(a row in thesitestable that they can read). - The current path is not
/setto(the dashboard is rendered without inline editing).
Sign-in at /setto persists via Supabase, so subsequent visits drop straight into edit mode — no URL flag, no extra step.
What editors see
A small round Setto button floats in the bottom-right corner. While there are no unpublished changes it stays as a circle showing only the Setto mark. As soon as you edit something it grows into a pill containing Publiser and a ⋯ menu with:
| Item | Effect |
|------|--------|
| Avbryt | Discards every unsaved draft (text, section colours, image uploads) |
| Logg ut | Signs out via Supabase and exits edit mode |
| Historikk | Opens the /setto dashboard with deployment history |
| Action | How |
|--------|-----|
| Edit text | Click any <T> element — it becomes contentEditable |
| Edit section colours | Click a section or block background (not text) |
| Follow a link | Ctrl/Cmd + click (desktop) · Naviger ↗ chip when focused (touch) |
| Publish | FAB → Publiser (visible once you have drafts) |
When you click a section or block, a compact colour toolbar appears above it. Click again or press Escape to dismiss.
Text editing (<T>)
<T k="dotted.key" /> renders the i18next string for the active language.
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.
On publish, the full locale bundles are serialised to JSON and committed to GitHub:
src/i18n/locales/no.json
src/i18n/locales/en.jsonWhen edit mode starts, setto-server loads the current files from GitHub as the baseline (GET /sites/:id/content).
Section colours
Section colours live in a separate JSON file (not in i18n):
src/theme/sections.jsonExample:
{
"values": {
"background": "#640AFF",
"label": "#C9C0DA",
"heading": "#FFFFFF",
"icon": "#FFFFFF",
"cardTitle": "#FFFFFF",
"cardDesc": "#C9C0DA"
}
}Brand palette (brandColors)
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:
// theme/brand-colors.ts
import type { BrandColor } from '@setto/client';
export const brandColors: BrandColor[] = [
{ label: 'Beige', value: '#E6DCCF' },
{ label: 'Oliven', value: '#362F00' },
{ label: 'Lilla', value: '#640AFF' },
// …
];Rules for integrators:
- List every colour token editors should be able to pick.
- Values must match the palette used in Tailwind/CSS on the site (same hex values).
- Use solid hex colours in
sections.json— rgba values won't match swatches. - When you add a new brand token to Tailwind, add it to
brandColorstoo.
Section schemas (sectionSchemas)
Define which colour fields each section exposes in the toolbar:
// theme/section-schemas.ts
import type { SectionSchema } from '@setto/client';
export const sectionSchemas: Record<string, SectionSchema> = {
values: {
label: 'Verdier',
fields: [
{ key: 'background', label: 'Bakgrunn' },
{ key: 'label', label: 'Etikett' },
{ key: 'heading', label: 'Overskrift' },
// keys must match properties used in useSectionTheme('values')
],
},
};The sectionId prop on <SettoSection> must match a key in both sectionSchemas and sections.json.
Applying colours in components
Read tokens with useSectionTheme(sectionId) and apply via inline style (or CSS variables you control):
const colors = useSectionTheme('hero');
<SettoSection sectionId="hero" style={{ /* background applied automatically */ }}>
<h1 style={{ color: colors.heading }}><T k="hero.headline" /></h1>
</SettoSection><SettoSection> always applies colors.background as backgroundColor. Other tokens are your responsibility.
Nested blocks (<SettoBlock>)
For cards or panels inside a section, wrap each in <SettoBlock> with its own theme key:
import { SettoSection, SettoBlock, useSectionTheme } from '@setto/client';
function InnovationSection() {
const card = useSectionTheme('innovationCard');
return (
<SettoSection sectionId="innovation" className="py-20">
<SettoBlock blockId="innovationCard" className="p-8">
<span style={{ color: card.label }}><T k="innovation.label" /></span>
<h3 style={{ color: card.heading }}><T k="innovation.headline" /></h3>
</SettoBlock>
</SettoSection>
);
}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.
Server setup (setto-server)
Site configuration is split in two:
1. Supabase sites row — control plane (where + routing). Set once when the site is registered (platform admin → Ny side):
| Column | Example |
|--------|---------|
| id | carryon-no (must match siteId in SettoProvider) |
| repo_owner / repo_name / branch | GitHub target |
| vercel_project_id | prj_… (used to route Vercel webhooks) |
2. setto.config.json in the site repo root — content shape. setto-server reads this from GitHub (cached, with a DB fallback):
{
"displayName": "Carry On",
"contentPaths": [
"src/i18n/locales/no.json",
"src/i18n/locales/en.json",
"src/theme/sections.json",
"public/images/setto/"
],
"allowedOrigins": ["https://carryon.no", "http://localhost:3000"]
}contentPathsis the publish whitelist — only these paths can be committed. Add new content files here before publishing them.setto.config.jsonitself is intentionally not in the list, so editors can never widen their own access.allowedOriginsis the CORS allow-list; the first entry is also used as the editor-invite activation domain.
Keeping this in the repo means content shape lives with the code that defines it, and no DB change is needed when you add a content file — just commit the config. The legacy content_paths / allowed_origins / display_name DB columns remain as a fallback for sites without the file.
Publish flow
- Editor clicks Publiser in the floating FAB.
- Client serialises changed locale bundles +
sections.json(if theme drafts exist). POST /sites/:siteId/publishwith{ files: [{ path, content }] }.- setto-server validates paths against
content_paths, commits to GitHub. - A
deploymentsrow is inserted; the toolbar shows build status via Supabase Realtime + Vercel webhook.
Drafts are cleared after a successful publish.
Admin app (SettoAdminApp)
Route: /setto/*
Provides Supabase email/password login (invite-only — no self-service sign-up), password reset, and a dashboard. After sign-in the dashboard redirects to the site home, where edit mode auto-activates. Invite links from Supabase land on /setto to set a password. Does not render the inline editor itself.
Local development (Sevenfold monorepo)
When consumed by carryon.no, Vite aliases @setto/client to src/index.ts so changes hot-reload without a separate watch build.
Typical stack:
# Terminal 1 — Supabase
cd setto-server && supabase start
# Terminal 2 — API
cd setto-server && bun run dev # :3001
# Terminal 3 — Site
cd carryon.no && bun run dev # :3000Build (library)
bun install
bun run buildProduces dist/setto-client.js and .d.ts via Vite library mode. Only needed before publishing to npm.
API surface
| Export | Purpose |
|--------|---------|
| SettoProvider / useSetto | Context, auth, stores, edit mode flag |
| T | Inline-editable translation |
| SettoSection | Section wrapper + edit selection |
| SettoBlock | Nested card/panel with its own colour toolbar |
| useSectionTheme | Read section colour tokens |
| SettoAdminApp | /setto login + dashboard |
| AuthGate | Standalone login wrapper |
| BrandColor, SectionSchema, SettoConfig | Types for host config |
Limitations (v0)
<T>supports text content only — not HTML attributes (placeholder,alt,aria-label).- No list/repeater UI (cannot add a new FAQ row from the editor).
- Drafts are in-memory — refresh discards unpublished changes.
- Section colour toolbar only offers
brandColors— no custom hex/rgba input. - CTA card colours and nested component colours are not section-themeable yet.
- Single editor per site (no concurrent-edit conflict handling).
