diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d24d46..07a2931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ Versioning follows [SemVer](https://semver.org/). ## [Unreleased] +### Added (gallery — Theme Builder) + +- Gallery page **`#theme`** — brand lab on top of Softglass languages +- Live preview (Button, Badge, Alert, Input, Card) with scoped CSS variables +- Brand controls: accent family + success / warning / danger +- **Copy CSS** export for `[data-softglass-theme="…"]` overrides +- Reset · Apply to gallery · draft autosave (`localStorage`) +- Welcome CTA card + hero button → `#theme` +- Web-only — no package version bump required for this work + +### Fixed (gallery — App mini product) + +- **`#app`** is a single live product shell (not loose demos): working nav · inventory inside AppShell · settings · command palette +- Taller embed (`.sg-shell-demo-app`) so DataTable + pagination fit + ## [1.7.0] — 2026-07-31 Minor release: **DataTable** (sort · selection · virtualization), Component Studio gallery, product list recipe. diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index c36cb85..c0bc891 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -240,7 +240,8 @@ body { width: 100%; max-width: 52rem; margin: 0 auto; - padding: 1rem 0.85rem 7.5rem; + /* Bottom pager is fixed ~100–120px — leave room so long pages (App/Theme) aren't clipped */ + padding: 1rem 0.85rem 9.5rem; min-width: 0; } @@ -313,6 +314,102 @@ body { align-items: center; } +/* Welcome → Theme Builder CTA */ +.sg-welcome-theme-cta { + border-color: color-mix(in srgb, var(--sg-accent) 28%, var(--sg-border-frost)); + box-shadow: + inset 0 1px 0 var(--sg-gloss-highlight), + 0 0 0 1px color-mix(in srgb, var(--sg-accent) 12%, transparent); +} + +/* Theme Builder (#theme) + Gallery max-width ~52rem — side-by-side brand+preview made color fields overflow. + Stack sections; fields use a safe 2-col grid only when the card is wide enough. */ +.sg-theme-builder-grid { + display: grid; + gap: 1rem; + grid-template-columns: minmax(0, 1fr); + align-items: stretch; +} + +.sg-theme-builder-grid > * { + min-width: 0; + max-width: 100%; +} + +/* Brand colors card body */ +.sg-theme-builder-fields { + display: grid; + gap: 0.85rem 0.75rem; + grid-template-columns: minmax(0, 1fr); + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +/* Full-width brand card → two equal columns that cannot overflow */ +@media (min-width: 560px) { + .sg-theme-builder-fields { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } +} + +.sg-theme-builder-fields > * { + min-width: 0; + max-width: 100%; +} + +/* ColorInput field wrappers */ +.sg-theme-builder-fields .sg-field { + min-width: 0; + max-width: 100%; +} + +.sg-theme-builder-fields .sg-color-input { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.sg-theme-builder-soft { + display: grid; + gap: 0.35rem; + grid-column: 1 / -1; + min-width: 0; +} + +.sg-theme-builder-soft-code { + display: block; + font-size: var(--sg-text-xs); + font-family: var(--sg-font-mono, ui-monospace, monospace); + padding: 0.45rem 0.6rem; + border-radius: var(--sg-radius-md); + background: color-mix(in srgb, var(--sg-fg) 5%, transparent); + border: var(--sg-border-width) solid var(--sg-border-subtle); + overflow-x: auto; + white-space: nowrap; + max-width: 100%; + box-sizing: border-box; +} + +.sg-theme-builder-preview { + display: grid; + gap: 0.85rem; + padding: 0.85rem; + border-radius: var(--sg-radius-lg); + border: var(--sg-border-width) solid var(--sg-border-frost); + background: color-mix(in srgb, var(--sg-fg) 3%, transparent); + min-width: 0; +} + +.sg-theme-builder-export { + max-height: 220px; + overflow: auto; + white-space: pre; + max-width: 100%; + box-sizing: border-box; +} + .sg-gallery-stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/apps/web/src/components/playground/catalog.ts b/apps/web/src/components/playground/catalog.ts index a6b4107..b7b2801 100644 --- a/apps/web/src/components/playground/catalog.ts +++ b/apps/web/src/components/playground/catalog.ts @@ -7,6 +7,7 @@ export type GalleryPageId = | "welcome" | "install" | "languages" + | "theme" | "looks" | "essentials" | "forms" @@ -48,6 +49,13 @@ export const GALLERY_PAGES: GalleryPage[] = [ summary: "3 light + 3 dark (Obsidian, Noir, Ember) — same components, different mood.", takeaway: "Theme is data-softglass-theme; brand is CSS variables.", }, + { + id: "theme", + label: "Theme", + title: "Theme Builder", + summary: "Brand lab: recolor accent + semantics on a language, export CSS overrides.", + takeaway: "Language = dialect; brand = --sg-accent* (and success/warning/danger).", + }, { id: "looks", label: "Looks", @@ -81,8 +89,8 @@ export const GALLERY_PAGES: GalleryPage[] = [ label: "App", title: "Product shell & patterns", summary: - "Inventory list recipe (PageHeader + DataTable + Pagination), AppShell, settings, command palette.", - takeaway: "How a real product list and chrome are assembled.", + "Full mini product: AppShell nav · inventory DataTable · settings · command palette.", + takeaway: "One shell, working sections — not loose demos stacked.", }, { id: "library", diff --git a/apps/web/src/components/playground/pages.tsx b/apps/web/src/components/playground/pages.tsx index 3455f76..f8ec6fb 100644 --- a/apps/web/src/components/playground/pages.tsx +++ b/apps/web/src/components/playground/pages.tsx @@ -15,12 +15,14 @@ import { type SoftglassThemeId, } from "@/lib/themes"; import type { GalleryPageId } from "@/components/playground/catalog"; +import { ThemeBuilderPage } from "@/components/playground/theme-builder"; import { Accordion, Alert, AppShell, AppShellCollapseButton, AppShellMenuButton, + useAppShell, Badge, Button, Card, @@ -53,7 +55,13 @@ import { type DataTableColumn, type DataTableSortState, } from "@softglass/ui"; -import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { + useCallback, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; export function GalleryPageBody({ pageId, @@ -73,6 +81,8 @@ export function GalleryPageBody({ return ; case "languages": return ; + case "theme": + return ; case "looks": return ; case "essentials": @@ -109,13 +119,16 @@ function WelcomePage({ onGo }: { onGo: (id: GalleryPageId) => void }) { MIT, shadcn-style ownership. - + onGo("install")}> Start with install onGo("languages")}> See languages + onGo("theme")}> + Theme Builder + @@ -124,7 +137,7 @@ function WelcomePage({ onGo }: { onGo: (id: GalleryPageId) => void }) { { k: "6", v: "Languages" }, { k: String(COMPONENT_DOCS.length), v: "Documented parts" }, { k: "MIT", v: "License" }, - { k: "9", v: "Tour pages" }, + { k: "10", v: "Tour pages" }, ].map((s) => ( @@ -135,6 +148,27 @@ function WelcomePage({ onGo }: { onGo: (id: GalleryPageId) => void }) { ))} + + + + new · 1.8 + + Theme Builder + + Language is the Softglass dialect. Brand is your accent. Recolor live, + preview components, copy the CSS override — no Storybook, no SaaS. + + + + onGo("theme")}> + Open Theme Builder + + onGo("languages")}> + Pick a language first + + + + How to use this gallery @@ -148,9 +182,10 @@ function WelcomePage({ onGo }: { onGo: (id: GalleryPageId) => void }) { [ ["install", "1 · Install"], ["languages", "2 · Languages"], - ["essentials", "3 · Essentials"], - ["app", "4 · App shell"], - ["library", "5 · Library"], + ["theme", "3 · Theme"], + ["essentials", "4 · Essentials"], + ["app", "5 · App shell"], + ["library", "6 · Library"], ] as const ).map(([id, label]) => ( [] = [ }, ]; -/** 1.7c — product list recipe: PageHeader + filter + DataTable + Pagination */ -function ProductListRecipe() { +type AppSection = "inventory" | "settings"; + +/** Inventory main — lives inside AppShell (PageHeader + filter + DataTable + Pagination) */ +function InventoryPanel({ + onToast, +}: { + onToast?: (message: string) => void; +}) { const [query, setQuery] = useState(""); const [page, setPage] = useState(1); const [selectedIds, setSelectedIds] = useState([]); @@ -721,8 +762,7 @@ function ProductListRecipe() { const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize)); const safePage = Math.min(page, pageCount); const pageRows = useMemo( - () => - filtered.slice((safePage - 1) * pageSize, safePage * pageSize), + () => filtered.slice((safePage - 1) * pageSize, safePage * pageSize), [filtered, safePage, pageSize], ); @@ -731,126 +771,293 @@ function ProductListRecipe() { }, [query]); return ( - - - Product list recipe - - How inventory screens compose Softglass:{" "} - PageHeader · filter · DataTable ·{" "} - Pagination. Sort and multi-select stay on the - current page of rows. - - - - - - Export - - - New product - - > - } - /> + <> + + + onToast?.( + selectedIds.length + ? `Export ${selectedIds.length} selected (demo)` + : "Export all (demo)", + ) + } + > + Export + + onToast?.("New product (demo)")} + > + New product + + > + } + /> - - - - - - page {safePage}/{pageCount} - - {selectedIds.length > 0 ? ( - - {selectedIds.length} selected - - ) : null} + + + + + page {safePage}/{pageCount} + + {selectedIds.length > 0 ? ( + + {selectedIds.length} selected + + ) : null} + - row.name} - sort={sort} - onSortChange={setSort} - clientSort - emptyTitle="No products match" - emptyDescription="Try another filter or clear the search." - aria-label="Inventory products" - maxHeight={360} + row.name} + sort={sort} + onSortChange={setSort} + clientSort + emptyTitle="No products match" + emptyDescription="Try another filter or clear the search." + aria-label="Inventory products" + maxHeight={280} + /> + + + + Showing {(safePage - 1) * pageSize + (pageRows.length ? 1 : 0)}– + {(safePage - 1) * pageSize + pageRows.length} of {filtered.length} + + + + > + ); +} + +function SettingsPanel({ + name, + onNameChange, + onSave, + saved, +}: { + name: string; + onNameChange: (value: string) => void; + onSave: () => void; + saved: boolean; +}) { + return ( + <> + + + {saved ? "Saved" : "Save"} + + } + look="soft" + > + onNameChange(e.target.value)} /> + + Changes stay in this session only (gallery demo). + + + > + ); +} + +/** Closes mobile Sheet when a nav item is chosen (must render under AppShell). */ +function AppNav({ + section, + onSection, +}: { + section: AppSection; + onSection: (next: AppSection) => void; +}) { + const { setMobileNavOpen } = useAppShell(); - + + + Softglass + + + + + { + e.preventDefault(); + go("inventory"); }} > - - Showing {(safePage - 1) * pageSize + (pageRows.length ? 1 : 0)}– - {(safePage - 1) * pageSize + pageRows.length} of {filtered.length} - - - - - + Inventory + + { + e.preventDefault(); + go("settings"); + }} + > + Settings + + + > ); } +/** + * 1.7c + completion — full mini product app: + * AppShell · working nav · inventory DataTable · settings · command palette. + */ function AppPage() { const [collapsed, setCollapsed] = useState(false); + const [section, setSection] = useState("inventory"); const [cmd, setCmd] = useState(false); const [last, setLast] = useState("—"); - const [name, setName] = useState("Studio"); + const [workspaceName, setWorkspaceName] = useState("Softglass Studio"); + const [saved, setSaved] = useState(false); + const [flash, setFlash] = useState(null); + + const notify = useCallback((message: string) => { + setFlash(message); + setLast(message); + window.setTimeout(() => setFlash(null), 2200); + }, []); - const items = useMemo( + const paletteItems = useMemo( () => [ - { id: "home", label: "Go home", group: "Nav", icon: "⌂" }, - { id: "settings", label: "Open settings", group: "Nav", icon: "◎" }, - { id: "theme", label: "Toggle theme", group: "Appearance", icon: "◐" }, + { + id: "inventory", + label: "Go to inventory", + group: "Nav", + icon: "◆", + }, + { + id: "settings", + label: "Open settings", + group: "Nav", + icon: "◎", + }, + { + id: "new", + label: "New product", + group: "Actions", + icon: "+", + }, + { + id: "export", + label: "Export inventory", + group: "Actions", + icon: "↓", + }, ], [], ); + function onPaletteSelect(id: string, label: string) { + setLast(label); + if (id === "inventory") setSection("inventory"); + else if (id === "settings") setSection("settings"); + else if (id === "new") { + setSection("inventory"); + notify("New product (demo)"); + } else if (id === "export") { + setSection("inventory"); + notify("Export all (demo)"); + } + } + return ( - - - + - AppShell (live mini) + + + live product + + + {section === "inventory" ? "Inventory" : "Settings"} + + {flash ? ( + + {flash} + + ) : null} + + Mini product · AppShell - Collapse the rail on desktop. On a narrow screen use the menu - button — same nav in a left Sheet. + Working nav · inventory table · settings · command palette. Collapse + the rail or open the menu on a narrow screen. @@ -860,106 +1067,87 @@ function AppPage() { variant={collapsed ? "primary" : "secondary"} onClick={() => setCollapsed((v) => !v)} > - {collapsed ? "Expand" : "Collapse"} + {collapsed ? "Expand rail" : "Collapse rail"} + + setSection("inventory")} + > + Inventory + + setSection("settings")} + > + Settings + + setCmd(true)}> + ⌘K palette - collapsed: {String(collapsed)} + - Mini product + {workspaceName} - - Softglass - - > - } - sidebar={ - <> - - + + setCmd(true)} + > + Search… + + + Softglass + - - - Home - - - Projects - - - Settings - - > } + sidebar={} > - - New - - } - style={{ marginBottom: "0.75rem" }} - /> - - Main content grows when the rail collapses. Pair with the product - list recipe above for full inventory screens. - + {section === "inventory" ? ( + + ) : ( + { + setWorkspaceName(v); + setSaved(false); + }} + onSave={() => { + setSaved(true); + notify(`Saved “${workspaceName}”`); + }} + saved={saved} + /> + )} - - - - Save - - } - look="soft" - > - setName(e.target.value)} - /> - - - - - Command palette - - Minimal: search + list + keyboard. Wire ⌘K yourself. - - - - setCmd(true)}> - Open palette - - last: {last} setLast(i.label)} + items={paletteItems} + onSelect={(item) => onPaletteSelect(item.id, item.label)} /> + + + Studio deep-dive:{" "} + #library/datatable · brand lab:{" "} + #theme + ); } diff --git a/apps/web/src/components/playground/theme-builder.tsx b/apps/web/src/components/playground/theme-builder.tsx new file mode 100644 index 0000000..2bc5d35 --- /dev/null +++ b/apps/web/src/components/playground/theme-builder.tsx @@ -0,0 +1,547 @@ +"use client"; + +import { + applySoftglassTheme, + readStoredSoftglassTheme, + SOFTGLASS_THEMES, + THEME_CHANGE_EVENT, + type SoftglassThemeId, +} from "@/lib/themes"; +import { + Alert, + Badge, + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + ColorInput, + Input, + Text, +} from "@softglass/ui"; +import { + useCallback, + useEffect, + useMemo, + useState, + type CSSProperties, +} from "react"; + +const BRAND_STORAGE_KEY = "softglass-brand-draft"; + +export type BrandColors = { + accent: string; + accentHover: string; + accentFg: string; + accentSoft: string; + ring: string; + success: string; + warning: string; + danger: string; +}; + +const DEFAULT_BRAND: BrandColors = { + accent: "#7c3aed", + accentHover: "#6d28d9", + accentFg: "#ffffff", + accentSoft: "rgba(124, 58, 237, 0.12)", + ring: "#8b5cf6", + success: "#059669", + warning: "#d97706", + danger: "#dc2626", +}; + +function clampByte(n: number): number { + return Math.max(0, Math.min(255, Math.round(n))); +} + +function parseHex(hex: string): { r: number; g: number; b: number } | null { + const raw = hex.trim().replace(/^#/, ""); + if (!/^[0-9a-fA-F]{6}$/.test(raw)) return null; + return { + r: Number.parseInt(raw.slice(0, 2), 16), + g: Number.parseInt(raw.slice(2, 4), 16), + b: Number.parseInt(raw.slice(4, 6), 16), + }; +} + +function toHex(r: number, g: number, b: number): string { + return `#${[r, g, b] + .map((c) => clampByte(c).toString(16).padStart(2, "0")) + .join("")}`; +} + +/** Darken hex for hover (simple mix toward black). */ +function darkenHex(hex: string, amount = 0.12): string { + const rgb = parseHex(hex); + if (!rgb) return hex; + return toHex( + rgb.r * (1 - amount), + rgb.g * (1 - amount), + rgb.b * (1 - amount), + ); +} + +function softRgba(hex: string, alpha = 0.12): string { + const rgb = parseHex(hex); + if (!rgb) return `rgba(124, 58, 237, ${alpha})`; + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`; +} + +/** Normalize computed colors (browser may return 8-digit hex) to rgba/hex for export. */ +function normalizeCssColor(value: string, fallback: string): string { + const v = value.trim(); + if (!v) return fallback; + // #RRGGBBAA → rgba + const hex8 = v.match(/^#([0-9a-fA-F]{8})$/); + if (hex8) { + const h = hex8[1]!; + const r = Number.parseInt(h.slice(0, 2), 16); + const g = Number.parseInt(h.slice(2, 4), 16); + const b = Number.parseInt(h.slice(4, 6), 16); + const a = Number.parseInt(h.slice(6, 8), 16) / 255; + return `rgba(${r}, ${g}, ${b}, ${Math.round(a * 100) / 100})`; + } + if (parseHex(v) || v.startsWith("rgb") || v.startsWith("hsl")) return v; + return fallback; +} + +function brandToCssVars(brand: BrandColors): CSSProperties { + return { + ["--sg-accent" as string]: brand.accent, + ["--sg-accent-hover" as string]: brand.accentHover, + ["--sg-accent-fg" as string]: brand.accentFg, + ["--sg-accent-soft" as string]: brand.accentSoft, + ["--sg-ring" as string]: brand.ring, + ["--sg-ring-soft" as string]: softRgba(brand.ring, 0.25), + ["--sg-success" as string]: brand.success, + ["--sg-warning" as string]: brand.warning, + ["--sg-danger" as string]: brand.danger, + }; +} + +function brandToExportCss(themeId: SoftglassThemeId, brand: BrandColors): string { + return `[data-softglass-theme="${themeId}"] { + --sg-accent: ${brand.accent}; + --sg-accent-hover: ${brand.accentHover}; + --sg-accent-fg: ${brand.accentFg}; + --sg-accent-soft: ${brand.accentSoft}; + --sg-ring: ${brand.ring}; + --sg-ring-soft: ${softRgba(brand.ring, 0.25)}; + --sg-success: ${brand.success}; + --sg-warning: ${brand.warning}; + --sg-danger: ${brand.danger}; +}`; +} + +function readCssVar(name: string, fallback: string): string { + if (typeof document === "undefined") return fallback; + const value = getComputedStyle(document.documentElement) + .getPropertyValue(name) + .trim(); + return value || fallback; +} + +function readBrandFromDocument(): BrandColors { + const accent = normalizeCssColor( + readCssVar("--sg-accent", DEFAULT_BRAND.accent), + DEFAULT_BRAND.accent, + ); + return { + accent, + accentHover: normalizeCssColor( + readCssVar("--sg-accent-hover", DEFAULT_BRAND.accentHover), + DEFAULT_BRAND.accentHover, + ), + accentFg: normalizeCssColor( + readCssVar("--sg-accent-fg", DEFAULT_BRAND.accentFg), + DEFAULT_BRAND.accentFg, + ), + accentSoft: normalizeCssColor( + readCssVar("--sg-accent-soft", DEFAULT_BRAND.accentSoft), + softRgba(accent), + ), + ring: normalizeCssColor( + readCssVar("--sg-ring", DEFAULT_BRAND.ring), + DEFAULT_BRAND.ring, + ), + success: normalizeCssColor( + readCssVar("--sg-success", DEFAULT_BRAND.success), + DEFAULT_BRAND.success, + ), + warning: normalizeCssColor( + readCssVar("--sg-warning", DEFAULT_BRAND.warning), + DEFAULT_BRAND.warning, + ), + danger: normalizeCssColor( + readCssVar("--sg-danger", DEFAULT_BRAND.danger), + DEFAULT_BRAND.danger, + ), + }; +} + +function applyBrandToDocument(brand: BrandColors | null) { + if (typeof document === "undefined") return; + const root = document.documentElement; + const keys = [ + "--sg-accent", + "--sg-accent-hover", + "--sg-accent-fg", + "--sg-accent-soft", + "--sg-ring", + "--sg-ring-soft", + "--sg-success", + "--sg-warning", + "--sg-danger", + ] as const; + + if (!brand) { + for (const key of keys) root.style.removeProperty(key); + return; + } + + root.style.setProperty("--sg-accent", brand.accent); + root.style.setProperty("--sg-accent-hover", brand.accentHover); + root.style.setProperty("--sg-accent-fg", brand.accentFg); + root.style.setProperty("--sg-accent-soft", brand.accentSoft); + root.style.setProperty("--sg-ring", brand.ring); + root.style.setProperty("--sg-ring-soft", softRgba(brand.ring, 0.25)); + root.style.setProperty("--sg-success", brand.success); + root.style.setProperty("--sg-warning", brand.warning); + root.style.setProperty("--sg-danger", brand.danger); +} + +function loadDraft(): BrandColors | null { + try { + const raw = window.localStorage.getItem(BRAND_STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial; + return { ...DEFAULT_BRAND, ...parsed }; + } catch { + return null; + } +} + +function saveDraft(brand: BrandColors) { + try { + window.localStorage.setItem(BRAND_STORAGE_KEY, JSON.stringify(brand)); + } catch { + /* private mode */ + } +} + +/** + * Softglass Theme Builder — brand lab (language + accent overrides). + * Gallery page body for #theme. + */ +export function ThemeBuilderPage() { + const [themeId, setThemeId] = useState("aurora"); + const [brand, setBrand] = useState(DEFAULT_BRAND); + const [galleryApplied, setGalleryApplied] = useState(false); + const [copyState, setCopyState] = useState<"idle" | "ok" | "err">("idle"); + const [hydrated, setHydrated] = useState(false); + + // Hydrate from storage + current language defaults + useEffect(() => { + const storedTheme = readStoredSoftglassTheme(); + setThemeId(storedTheme); + const draft = loadDraft(); + // Wait a frame so language CSS is applied, then sample defaults + requestAnimationFrame(() => { + const fromDoc = readBrandFromDocument(); + setBrand(draft ?? fromDoc); + setHydrated(true); + }); + }, []); + + useEffect(() => { + function onExternal(e: Event) { + const id = (e as CustomEvent).detail; + if (!id) return; + setThemeId(id); + // Sample language defaults after theme paint (keep draft accents optional) + requestAnimationFrame(() => { + if (!galleryApplied) { + const next = readBrandFromDocument(); + // Only replace if no localStorage draft — user may want to keep brand + const draft = loadDraft(); + if (!draft) setBrand(next); + } + }); + } + window.addEventListener(THEME_CHANGE_EVENT, onExternal); + return () => window.removeEventListener(THEME_CHANGE_EVENT, onExternal); + }, [galleryApplied]); + + useEffect(() => { + if (!hydrated) return; + saveDraft(brand); + }, [brand, hydrated]); + + // Clear document overrides when leaving the page if applied + useEffect(() => { + return () => { + // Keep applied brand if user chose Apply; only clear on explicit reset. + // No-op cleanup to avoid surprising flash when navigating. + }; + }, []); + + const exportCss = useMemo( + () => brandToExportCss(themeId, brand), + [brand, themeId], + ); + const previewStyle = useMemo(() => brandToCssVars(brand), [brand]); + + const setField = useCallback( + (key: K, value: string) => { + setBrand((prev) => { + const next = { ...prev, [key]: value }; + if (key === "accent" && parseHex(value)) { + next.accentHover = darkenHex(value); + next.accentSoft = softRgba(value); + next.ring = value; + } + return next; + }); + }, + [], + ); + + function pickLanguage(id: SoftglassThemeId) { + setThemeId(id); + applySoftglassTheme(id); + requestAnimationFrame(() => { + // Resample language semantics; re-derive soft/hover from current accent + const fromDoc = readBrandFromDocument(); + setBrand((prev) => ({ + ...fromDoc, + accent: prev.accent, + accentHover: darkenHex(prev.accent), + accentSoft: softRgba(prev.accent), + accentFg: prev.accentFg, + ring: prev.accent, + })); + }); + } + + function resetBrand() { + applyBrandToDocument(null); + setGalleryApplied(false); + try { + window.localStorage.removeItem(BRAND_STORAGE_KEY); + } catch { + /* ignore */ + } + requestAnimationFrame(() => { + setBrand(readBrandFromDocument()); + }); + } + + function applyToGallery() { + applyBrandToDocument(brand); + setGalleryApplied(true); + } + + async function copyCss() { + try { + await navigator.clipboard.writeText(exportCss); + setCopyState("ok"); + window.setTimeout(() => setCopyState("idle"), 1600); + } catch { + setCopyState("err"); + window.setTimeout(() => setCopyState("idle"), 2000); + } + } + + const themeMeta = SOFTGLASS_THEMES.find((t) => t.id === themeId); + + return ( + + + Language is the Softglass dialect (surfaces, glass).{" "} + Brand is your accent + semantics — override CSS + variables, keep the engine. + + + + + Base language + + Pick a dialect, then recolor the brand. Active:{" "} + {themeMeta?.name ?? themeId} + + + + {SOFTGLASS_THEMES.map((theme) => ( + pickLanguage(theme.id)} + > + {theme.id} + + ))} + + + + + + + Brand colors + + Accent drives hover/soft/ring automatically. Semantics are + independent. + + + + setField("accent", v)} + /> + setField("accentHover", v)} + /> + setField("accentFg", v)} + /> + setField("ring", v)} + /> + setField("success", v)} + /> + setField("warning", v)} + /> + setField("danger", v)} + /> + + + Soft accent (auto) + + + {brand.accentSoft} + + + + + + Reset + + + Apply to gallery + + {galleryApplied ? ( + + applied + + ) : null} + + + + + + Live preview + + Scoped CSS variables — edit without committing until Apply. + + + + + + Primary + Secondary + + Soft + + + Glass + + + + + Accent + + + Success + + + Warning + + + Danger + + + + Accent and semantics should stay readable on this language’s + surfaces. + + + + + + Glass chrome with brand accent on actions — keep long text on + solid. + + + + + + + + + + + Export CSS + + Paste into your app after the Softglass tokens import. Language id:{" "} + {themeId} + + + + {exportCss} + + void copyCss()}> + {copyState === "ok" + ? "Copied" + : copyState === "err" + ? "Copy failed" + : "Copy CSS"} + + + Draft autosaves in this browser. + + + + + + ); +} diff --git a/docs/claude/todos.md b/docs/claude/todos.md index c0319a2..6065ec2 100644 --- a/docs/claude/todos.md +++ b/docs/claude/todos.md @@ -1,7 +1,7 @@ # Softglass — TODO -> **v1.0–v1.7.0:** shipped (npm + tag) · plan: [v1.7-plan.md](./v1.7-plan.md) -> Arşiv: [v1.6-plan.md](./v1.6-plan.md) · [v1.5-plan.md](./v1.5-plan.md) +> **v1.0–v1.7.0:** shipped (npm + tag) · **Sıradaki:** 1.8 Theme Builder +> Plan: [v1.8-plan.md](./v1.8-plan.md) · arşiv: [v1.7-plan.md](./v1.7-plan.md) > **Hesap:** gh **ArdaDDemir** · npm **ardaddemir** · work/Feedemy **YASAK**. > **Kural:** Aynı anda tek sprint · otomatik push yok. @@ -11,10 +11,9 @@ | Sürüm | Durum | |-------|--------| -| 1.0.0 – 1.5.0 | **shipped** (npm + tag) | -| **1.6** | Component Studio (gallery; shipped with 1.7.0 packages) | -| **1.7.0** | DataTable + virtualization + Studio + App recipe — **shipped** | -| 1.8+ | Theme builder / marketing / Storybook | +| 1.0.0 – 1.7.0 | **shipped** (npm + tag) | +| **1.8** | Theme Builder — branch `feat/v1.8-theme-builder` · **1.8-0 done** | +| 1.9+ | Marketing polish / visual / token JSON | **Repo:** https://github.com/ArdaDDemir/softglass **npm:** `@softglass/tokens@1.7.0` · `@softglass/ui@1.7.0` @@ -22,80 +21,53 @@ --- -## v1.5 — bitti +## v1.7 — bitti -- [x] 1.5a–c + gallery + noir/ember -- [x] review fixes -- [x] 1.5.0 publish + tag -- [x] PR #7 merge → main +- [x] DataTable + virtualization + Studio + App recipe +- [x] 1.7.0 publish + tag + PR #9 → main --- -## v1.6 — Component Studio (Library) — bitti (main) +## v1.8 — Theme Builder (brand lab) -- [x] Hash `#library` / `#library/` -- [x] ComponentStudio chrome -- [x] 56 playgrounds = all `COMPONENT_DOCS.id` -- [x] CHANGELOG Unreleased -- [x] PR #8 merge → main -- [ ] Manual mobile pass in browser *(nice)* - ---- - -## v1.7 — DataTable (+ virtualization) +> Dil + brand accent lab · canlı preview · export CSS. SaaS / Storybook **YAPMA**. -> Ürün listeleri: sort · selection · sticky header · density · loading/empty · Softglass looks. -> Virtualization: planda karar 1.7-0 · ship **1.7b**. - -### 1.7-0 — Plan + skeleton + Studio *(done)* +### 1.8-0 — Plan + Gallery `#theme` *(done)* - [x] Plan + todos -- [x] `DataTable` + `Table*` compounds -- [x] Tokens CSS -- [x] Studio `#library/datatable` -- [x] Tests: sort · select · empty · loading -- [x] Registry `data-table` - -### 1.7a — Polish *(done)* - -- [x] `Checkbox.indeterminate` -- [x] Select-all mixed state -- [x] `getRowLabel` -- [x] Sort `aria-label` -- [x] Skeleton density -- [x] Docs (API / LIMITATIONS / CHANGELOG) +- [x] Catalog page `#theme` +- [x] Brand color controls (accent family + semantics) +- [x] Live preview (Button, Badge, Alert, Input, Card) +- [x] Copy CSS export + Reset +- [x] Apply to gallery (optional documentElement vars) +- [x] CHANGELOG Unreleased -### 1.7b — Virtualization *(done)* +### 1.8a — Polish *(partial in 1.8-0)* -- [x] `virtualized` API + implementation -- [x] 1k-row Studio showcase -- [x] Sticky + selection regression +- [x] Auto-derive soft/hover/ring from accent +- [x] Persist draft in localStorage +- [ ] Shareable hash/query (optional) -### 1.7c — Gallery App (optional) *(done)* +### 1.8b — Marketing glue (optional) *(done)* -- [x] Product list recipe demo (PageHeader + DataTable + Pagination) · `#app` +- [x] Welcome CTA → Theme Builder (hero + card) -### 1.7 YAPMA +### 1.8 YAPMA -- Storybook, theme builder, marketing site -- Excel-grid (edit cell, pivot, freeze N) +- Theme SaaS, Storybook farm, full token surface editor - work hesabı push -### 1.7.0 ship +### 1.8.0 ship -- [x] version bump `@softglass/ui` (+ tokens) → **1.7.0** -- [x] CHANGELOG `[1.7.0]` -- [x] registry build -- [x] npm publish **ardaddemir** only -- [x] tag `v1.7.0` -- [ ] PR merge → main *(open PR; merge when green)* +- [ ] Manual pass `#theme` +- [ ] PR → main (web-only unless packages change) +- [ ] npm only if tokens/ui touched --- -## v1.8+ backlog +## v1.9+ backlog -- [ ] Theme builder -- [ ] Docs marketing site / Storybook +- [ ] Docs marketing site polish - [ ] Visual regression - [ ] Token JSON / Figma bridge - [ ] i18n / RTL @@ -107,9 +79,8 @@ ## Yeni session ```text -Softglass v1.7.0 shipped — DataTable + Component Studio -npm @softglass/tokens@1.7.0 · @softglass/ui@1.7.0 · tag v1.7.0 -Branch feat/v1.7-datatable → PR → main -Next: 1.8+ backlog -work hesabına push yok +Softglass v1.8 Theme Builder — docs/claude/v1.8-plan.md +Branch: feat/v1.8-theme-builder +#theme +web-first; work hesabına push yok ``` diff --git a/docs/claude/v1.8-plan.md b/docs/claude/v1.8-plan.md new file mode 100644 index 0000000..2da3e02 --- /dev/null +++ b/docs/claude/v1.8-plan.md @@ -0,0 +1,119 @@ +# Softglass v1.8 — Theme Builder (brand lab) + +> **Durum:** **1.8-0 + 1.8b done**. Next: 1.8a share URL (optional) · or ship PR. +> **Checkbox:** [todos.md](./todos.md) +> **Base:** `v1.7.0` shipped (npm + tag). +> **Branch:** `feat/v1.8-theme-builder` +> **Hesap:** gh **ArdaDDemir** · npm **ardaddemir** — work/Feedemy **YASAK**. +> **Kural:** Aynı anda **tek sprint**. SaaS multi-tenant theme product **YAPMA**. +> **Publish:** Web-first; npm only if tokens/ui API changes (unlikely in 1.8-0). + +--- + +## 1) Bugün neredeyiz? + +| | | +|--|--| +| **Shipped** | `v1.7.0` — DataTable, virtualization, Component Studio, App recipe | +| **Languages** | 6 dialects via `data-softglass-theme` | +| **Brand** | Docs say override `--sg-accent*` — **no live lab** | +| **Gallery** | Theme chips switch language only | + +**Boşluk:** Adam dil seçer, marka rengini canlı oynar, CSS snippet kopyalar — Softglass “brand vs language” modelini anlar. + +--- + +## 2) Ürün cümlesi + +> **1.8.0** = Softglass **Theme Builder** (Gallery): dil + brand accent lab, canlı preview, export CSS. Storybook / SaaS builder değil. + +| Release | Cümle | +|---------|--------| +| **1.7.0** | DataTable + Studio *(shipped)* | +| **1.8.0** | Theme Builder brand lab in Gallery | +| **1.9+** | Marketing polish / visual regression / token JSON *(ayrı)* | + +--- + +## 3) Bilinçli YAPMA (1.8) + +| Yapma | Neden | +|-------|--------| +| Theme builder **SaaS** (accounts, save cloud themes) | Ürün şişirir; Softglass kit kalır | +| Full Storybook + Chromatic | Gallery + Studio yeter | +| Her dil için yeni CSS dosyası üretimi (codegen farm) | 1.8 export snippet yeter | +| Tüm token surface’lerini (blur, shadow…) UI’da edit | Scope patlar; brand first | +| npm publish her color tweak | Web-only 1.8-0; ship sonunda karar | + +--- + +## 4) UX (kilitli) + +``` +Gallery → Theme (#theme) + ┌─────────────────────────────────────────────┐ + │ Base language: [aurora][mist]… (apply) │ + │ Brand colors: accent · hover · fg · soft │ + │ success · warning · danger │ + │ [Reset] [Copy CSS] [Apply to gallery] │ + │ │ + │ Live preview (scoped vars): │ + │ Button / Badge / Alert / Input / Card │ + │ │ + │ Export: │ + │ [data-softglass-theme="…"] { --sg-… } │ + └─────────────────────────────────────────────┘ +``` + +**Model (PUBLIC-MODEL ile uyumlu):** +- **Language** = dialect (shared) +- **Brand** = override accent (+ optional semantics) + +--- + +## 5) Sprint haritası + +``` +[ 1.8-0 ] Plan + #theme page + brand controls + preview + copy CSS + ↓ +[ 1.8a ] Soft auto-derive · share query/hash · a11y polish + ↓ +[ 1.8b ] Optional Welcome/marketing CTA → Theme Builder + ↓ +[ 1.8.0 ] Ship: web (gallery). npm only if packages touched. +``` + +--- + +## 6) DoD 1.8-0 + +1. Plan + todos 1.8 checklist +2. Gallery page `#theme` in catalog + hash nav +3. Color controls (accent family + success/warning/danger) +4. Live preview with Softglass components +5. Copy export CSS; Reset to language defaults +6. Optional “Apply to gallery” (documentElement CSS vars) +7. `typecheck` (web) green +8. CHANGELOG Unreleased + +--- + +## 7) Teknik notlar + +| | | +|--|--| +| **Scope** | Prefer **scoped preview** (`style` on container) so builder doesn’t break whole app until Apply | +| **Persistence** | `localStorage` key for brand draft (optional 1.8-0) | +| **Derive** | Soft/hover helpers from accent hex (1.8a polish if not in 1.8-0) | +| **Atoms** | Reuse `ColorInput`, `Button`, `Card`, `Badge`, `Alert`, `Input` | + +--- + +## 8) Yeni session + +```text +Softglass v1.8 Theme Builder — docs/claude/v1.8-plan.md +Branch: feat/v1.8-theme-builder +#theme brand lab +web-first; work hesabına push yok +``` diff --git a/packages/tokens/src/base.css b/packages/tokens/src/base.css index 377f84c..5adaaa6 100644 --- a/packages/tokens/src/base.css +++ b/packages/tokens/src/base.css @@ -2790,19 +2790,34 @@ background: var(--sg-surface-solid); } -.sg-shell.sg-shell-demo .sg-shell-sidebar { +/* Full mini-product embed (inventory + settings). + Contain layout — override base .sg-shell min-height: 100dvh. */ +.sg-shell.sg-shell-demo-app { + min-height: 0; + height: min(34rem, 70vh); + max-height: 70vh; + border-radius: var(--sg-radius-lg); + border: var(--sg-border-width) solid var(--sg-border-frost); + overflow: hidden; + background: var(--sg-surface-solid); +} + +.sg-shell.sg-shell-demo .sg-shell-sidebar, +.sg-shell.sg-shell-demo-app .sg-shell-sidebar { height: 100%; position: relative; top: auto; } -.sg-shell.sg-shell-demo .sg-shell-header { +.sg-shell.sg-shell-demo .sg-shell-header, +.sg-shell.sg-shell-demo-app .sg-shell-header { position: relative; top: auto; padding: var(--sg-space-2) var(--sg-space-3); } -.sg-shell.sg-shell-demo .sg-shell-header-inner { +.sg-shell.sg-shell-demo .sg-shell-header-inner, +.sg-shell.sg-shell-demo-app .sg-shell-header-inner { padding: 0.45rem 0.65rem; } @@ -2810,6 +2825,18 @@ padding: var(--sg-space-4); } +.sg-shell.sg-shell-demo-app .sg-shell-main { + min-height: 0; + overflow: auto; +} + +.sg-shell.sg-shell-demo-app .sg-shell-main-inner { + padding: var(--sg-space-4); + display: grid; + gap: 0.85rem; + align-content: start; +} + /* Stronger collapse read: wider gap + rail tint */ .sg-shell-with-sidebar { transition: grid-template-columns var(--sg-duration-normal) var(--sg-ease-out);
+ {brand.accentSoft} +
{themeId}
{exportCss}