docs(studio): add current theme author guide and starter

This commit is contained in:
xiexian committed 2026-10-04 00:04:23 +08:00
1 parent 1e6b185c2d
commit 29791a36c6
7 files changed
+332 -18

No files matched your search

+1
View File
@@ -10,6 +10,7 @@
# User-facing guides and references.
/docs/ @SivanCola @esengine
/docs/THEME_AUTHOR_GUIDE.md @esengine @SivanCola
/sdk/ @SivanCola @esengine
/README.md @SivanCola @esengine
/README.zh-CN.md @SivanCola @esengine
+3
View File
@@ -27,6 +27,9 @@ OpenAPI inputs, a reference checker, and a source-linked guide format.
The [stdio MCP example](../examples/mcp-line-counter-kit/README.md) bundles a
local Go server with build, connection, tool-call, and lifecycle checks.
The [Studio theme author guide](THEME_AUTHOR_GUIDE.md) uses a copyable pure-theme
package, the current token vocabulary, and Appearance activation and cleanup.
## CLI Mode
Use `reasonix plugin` when installing or managing plugin packages from a
+135
View File
@@ -0,0 +1,135 @@
---
status: active
owner: @esengine
backup: @SivanCola
reviewed: 2026-10-01
---
# Studio theme author guide
Studio reads a directory containing `theme.json` with `schemaVersion: 1`.
The [Paper Dawn package](themes/reasonix-plugin.json) is a local, token-only
starter with light and dark palettes. Its palette is released as CC0-1.0.
It contributes one theme and no skills, hooks, commands, MCP servers or runtime.
## Install and try the starter
From this repository's root, with the Studio `reasonix` CLI on your PATH:
```bash
reasonix plugin install ./docs/themes --dry-run
reasonix plugin install ./docs/themes --yes
reasonix plugin doctor paper-dawn-kit
reasonix plugin show paper-dawn-kit
```
Review the preview before installing: it should contain one plugin action with
`themeCount: 1` and no execution capabilities. Installation copies the package;
moving the source afterwards does not break the installed theme.
Open **Settings → Appearance** in Studio and choose **Paper Dawn** from the theme
choices. Installing a theme does not select it. Try both light and dark modes,
read ordinary and muted text, and check a dialog and a code block.
The package's theme identity is `plugin:paper-dawn-kit:paper-dawn`: the plugin
name and the contributed theme directory name determine it. The `id` inside
`theme.json` does not override that plugin identity.
## Make your own package
Copy `docs/themes` into a local working folder; keep this layout:
```text
my-palette/
reasonix-plugin.json
paper-dawn/
theme.json
```
Change the plugin `name`, `version` and `description` in
`reasonix-plugin.json`, then edit the theme's `name`, `author`, `description`
and palette. If you rename `paper-dawn`, update `contributes.themes` to match.
The manifest uses an explicit relative path; all contributions stay inside
the package root.
For a live local development copy:
```bash
cd /absolute/path/to/my-palette
reasonix plugin install . --link --yes
```
A link source must be inside the current workspace or your home directory.
A linked package reads your working folder. Keep it in place, and keep the
theme directory name stable.
After edits, reopen Studio's Appearance settings
and reselect the theme to reread its tokens. For a copied installation, preview
and apply the replacement instead:
```bash
reasonix plugin install /absolute/path/to/my-palette --replace --dry-run
reasonix plugin install /absolute/path/to/my-palette --replace --yes
```
Use your package's manifest name in management commands. Share an immutable
repository commit using the [community author guide](MARKET_AUTHOR_GUIDE.md)
after checking the package and its licence; local installation does not publish it.
## Current token vocabulary
Each scheme is a map under `tokens.light` or `tokens.dark`. Both schemes must
contain at least one valid token, or the pack will not load. Omitted tokens
retain Studio defaults or derive from another declared surface.
The authoritative vocabulary is the kernel's [`Tokens` map](../internal/ext/theme/tokens.go), with value validation in `validToken`, `isColour`, `isLength` and `isFontStack` in that same file. Use those definitions when choosing token names and values; this guide does not maintain a second token table.
The frontend maps the vocabulary to CSS variables in [`theme.ts`](../desktop/frontend-next/src/ui/theme.ts). The kernel's [`TestThemeTokenVocabularyMatchesTheFrontend`](../internal/ext/theme/tokens_test.go) checks that both sides agree.
Status colours such as `ok`, `warn` and `err` belong to Studio and cannot be
recoloured by a pack. `sidebar` and `chat` are not current token names.
An unknown token or invalid value is dropped, while valid tokens still load;
the theme choice shows the resulting warnings.
User contrast settings can adjust a theme's text colours. Check readability
with the contrast settings and both colour schemes instead of assuming the
JSON colour is always the final text colour.
The retired desktop's `baseStyle`, `recipes`, `taskBackground` and
`schemaVersion: 2` are not the Studio authoring contract.
[Theme Pack V2](THEME_PACK.md) remains the legacy reference.
## Optional local images
Keep image files next to `theme.json`. Studio recognises an optional
`background.png`, `.jpg`, `.jpeg` or `.webp`, and likewise a `preview` image.
The reader selects those conventional filenames; a `background.image` field
alone does not locate an arbitrary file.
Keep each image at most 8 MiB so the asset endpoint can serve it.
The current `background` block controls `focusX`, `focusY`, `safeArea`
(`left`, `center`, `right`), `homeOpacity`, `taskOpacity` and
`overlayStrength`. Numeric placement and opacity values are bounded to 0–1.
Use a low task opacity so the image does not compete with a transcript.
Check your image licence and avoid including workspace screenshots or secrets.
## Disable and clean up
```bash
reasonix plugin disable paper-dawn-kit
reasonix plugin enable paper-dawn-kit
reasonix plugin remove paper-dawn-kit --yes
```
Restart Studio after external CLI changes, then reopen Appearance settings.
Disabling hides the contributed
theme and retains the copied files; re-enabling restores the same identity.
Removing a copied package deletes its installed copy. Removing a linked
package retains the source working folder.
Studio retains the selected theme identity when its plugin becomes unavailable
and renders its default appearance until the theme is available again. Choose
the default theme in Appearance settings to clear that selection explicitly.
Themes do not enter model prompts or add tools to a conversation.
+3
View File
@@ -8,6 +8,9 @@ archives. What Reasonix Studio reads instead:
- plugin themes are packs of that shape, listed as
`plugin:<plugin>:<directory>` (see `docs/PLUGIN_PACKAGES.md`).
For the current format, a directly installable starter, and its management
lifecycle, use the [Studio theme author guide](THEME_AUTHOR_GUIDE.md).
Native theme packs for the Reasonix desktop app. Packs are controlled skins:
semantic color tokens, density/corner recipes, and optional local images for
the home and task/workspace scenes. They **cannot** run CSS, JavaScript, fonts,
+3 -18
View File
@@ -3,43 +3,28 @@
"id": "paper-dawn",
"name": "Paper Dawn",
"author": "Reasonix",
"description": "Template theme — solid tokens only, no background image. Royalty-free starter.",
"description": "Studio theme template with light and dark palettes, no background image. CC0-1.0 starter.",
"license": "CC0-1.0",
"baseStyle": "graphite",
"tokens": {
"light": {
"bg": "#f7f4ef",
"panel": "#ffffff",
"sidebar": "#f3efe8",
"chat": "#fbfaf7",
"fg": "#1c1917",
"fgDim": "#57534e",
"fgFaint": "#a8a29e",
"border": "#e7e5e4",
"accent": "#c2410c",
"accentFg": "#fff7ed",
"ok": "#15803d",
"warn": "#b45309",
"err": "#b91c1c"
"accentFg": "#fff7ed"
},
"dark": {
"bg": "#0c0b0a",
"panel": "#171412",
"sidebar": "#141210",
"chat": "#0c0b0a",
"fg": "#f5f5f4",
"fgDim": "#a8a29e",
"fgFaint": "#78716c",
"border": "#292524",
"accent": "#fb923c",
"accentFg": "#0c0b0a",
"ok": "#4ade80",
"warn": "#fbbf24",
"err": "#f87171"
"accentFg": "#0c0b0a"
}
},
"recipes": {
"density": "comfortable",
"corners": "soft"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"apiVersion": "reasonix.io/plugin/v2",
"name": "paper-dawn-kit",
"version": "1.0.0",
"description": "A local Studio theme template with light and dark palettes.",
"contributes": {
"themes": ["paper-dawn/theme.json"]
}
}
@@ -0,0 +1,178 @@
package serve
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"reflect"
"testing"
"reasonix/internal/base/testenv"
"reasonix/internal/contract/config"
"reasonix/internal/ext/installsource"
"reasonix/internal/ext/pluginpkg"
"reasonix/internal/ext/theme"
)
func TestStudioThemeAuthorExampleLifecycle(t *testing.T) {
home := testenv.TempDir(t)
reasonixHome := filepath.Join(home, ".reasonix")
t.Setenv("REASONIX_HOME", reasonixHome)
t.Setenv("REASONIX_STATE_HOME", reasonixHome)
source := filepath.Join(testenv.TempDir(t), "theme-example")
if err := os.CopyFS(source, os.DirFS(filepath.Join("..", "..", "..", "docs", "themes"))); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(filepath.Join(source, "paper-dawn", "theme.json"))
if err != nil {
t.Fatal(err)
}
var authored struct {
Name string `json:"name"`
Tokens map[string]map[string]string `json:"tokens"`
}
if err := json.Unmarshal(raw, &authored); err != nil {
t.Fatal(err)
}
installer := installsource.NewTool(installsource.Options{
ProjectRoot: testenv.TempDir(t), HomeDir: home, RequireApprovedPlan: true,
})
type installResult struct {
OK bool `json:"ok"`
Status string `json:"status"`
Applied bool `json:"applied"`
PlanID string `json:"planId"`
Actions []struct {
Target string `json:"target"`
ThemeCount int `json:"themeCount"`
SkillCount int `json:"skillCount"`
AgentCount int `json:"agentCount"`
CommandCount int `json:"commandCount"`
PromptCount int `json:"promptCount"`
HookCount int `json:"hookCount"`
ToolCount int `json:"toolCount"`
Runtime any `json:"runtime"`
} `json:"actions"`
}
run := func(args map[string]any) installResult {
t.Helper()
input, err := json.Marshal(args)
if err != nil {
t.Fatal(err)
}
output, err := installer.Execute(t.Context(), input)
if err != nil {
t.Fatal(err)
}
var result installResult
if err := json.Unmarshal([]byte(output), &result); err != nil {
t.Fatalf("install response %q: %v", output, err)
}
if !result.OK {
t.Fatalf("install response: %s", output)
}
return result
}
const id = "plugin:paper-dawn-kit:paper-dawn"
srv := themeServer(t)
check := func(stage string, present, active bool) {
t.Helper()
t.Run(stage, func(t *testing.T) {
response, err := http.Get(srv.URL + "/themes")
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("theme listing = %d", response.StatusCode)
}
var rows []themeView
if err := json.NewDecoder(response.Body).Decode(&rows); err != nil {
t.Fatal(err)
}
found := false
for _, row := range rows {
if row.ID != id {
continue
}
found = true
if row.Name != authored.Name || row.Active != active || len(row.Warnings) != 0 || !reflect.DeepEqual(row.Tokens, authored.Tokens) {
t.Fatalf("theme row = %+v, want active=%t and all authored tokens without warnings", row, active)
}
if row.Background != nil || row.Sky != nil || row.HasPreview {
t.Fatalf("token-only example has image or sky: %+v", row)
}
}
if found != present {
t.Fatalf("theme present = %t, want %t", found, present)
}
if _, err := theme.Load(id); (err == nil) != present {
t.Fatalf("theme load error = %v, want present=%t", err, present)
}
})
}
check("not-installed", false, false)
preview := run(map[string]any{"source": source, "kind": "plugin", "apply": false})
if preview.Status != "planned" || preview.Applied || preview.PlanID == "" || len(preview.Actions) != 1 {
t.Fatalf("preview = %+v", preview)
}
action := preview.Actions[0]
if action.ThemeCount != 1 || action.SkillCount != 0 || action.AgentCount != 0 || action.CommandCount != 0 || action.PromptCount != 0 || action.HookCount != 0 || action.ToolCount != 0 || action.Runtime != nil {
t.Fatalf("pure-theme preview = %+v", action)
}
check("preview", false, false)
installed := run(map[string]any{"source": source, "kind": "plugin", "apply": true, "planId": preview.PlanID})
if installed.Status != "done" || !installed.Applied || len(installed.Actions) != 1 {
t.Fatalf("installed = %+v", installed)
}
installedRoot := installed.Actions[0].Target
if installedRoot == "" {
t.Fatal("install has no copied target")
}
if err := os.RemoveAll(source); err != nil {
t.Fatal(err)
}
check("copied-source-removed", true, false)
activate := func(themeID string, status int) {
t.Helper()
response := postJSON(t, srv.URL+"/themes", map[string]string{"id": themeID})
defer response.Body.Close()
if response.StatusCode != status {
t.Fatalf("activate %q = %d, want %d", themeID, response.StatusCode, status)
}
}
activate(id, http.StatusNoContent)
check("active", true, true)
if err := pluginpkg.SetEnabled(reasonixHome, "paper-dawn-kit", false); err != nil {
t.Fatal(err)
}
check("disabled", false, false)
activate(id, http.StatusUnprocessableEntity)
if cfg, err := config.Load(); err != nil || cfg.Desktop.ThemePack != id {
t.Fatalf("disabled selection was not retained: config=%+v err=%v", cfg, err)
}
if _, err := os.Stat(filepath.Join(installedRoot, "paper-dawn", "theme.json")); err != nil {
t.Fatalf("disable removed copied content: %v", err)
}
if err := pluginpkg.SetEnabled(reasonixHome, "paper-dawn-kit", true); err != nil {
t.Fatal(err)
}
check("re-enabled-active", true, true)
removed := run(map[string]any{"op": "uninstall", "name": "paper-dawn-kit", "kind": "plugin"})
if removed.Status != "done" || !removed.Applied {
t.Fatalf("uninstall = %+v", removed)
}
check("removed", false, false)
if _, err := os.Stat(installedRoot); !os.IsNotExist(err) {
t.Fatalf("uninstall retained copied content: %v", err)
}
if cfg, err := config.Load(); err != nil || cfg.Desktop.ThemePack != id {
t.Fatalf("removed selection was not retained: config=%+v err=%v", cfg, err)
}
activate("", http.StatusNoContent)
if cfg, err := config.Load(); err != nil || cfg.Desktop.ThemePack != "" {
t.Fatalf("default appearance was not restored: config=%+v err=%v", cfg, err)
}
check("default", false, false)
}