@deernova/style 0.1.0 → 0.3.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.
Files changed (215) hide show
  1. package/README.md +8 -6
  2. package/bin/cli.mjs +212 -29
  3. package/package.json +3 -4
  4. package/skill/SKILL.md +91 -21
  5. package/skill/assets/DeernovaTokens.swift +376 -0
  6. package/skill/assets/screens/iphone-button-sizes-dark.png +0 -0
  7. package/skill/assets/screens/iphone-button-sizes-light.png +0 -0
  8. package/skill/assets/screens/iphone-screens-dark.png +0 -0
  9. package/skill/assets/screens/iphone-screens-light.png +0 -0
  10. package/skill/assets/screens/mac-window-dark.png +0 -0
  11. package/skill/assets/screens/mac-window-light.png +0 -0
  12. package/skill/assets/tailwind.css +106 -0
  13. package/skill/assets/tokens.css +533 -0
  14. package/skill/assets/tokens.json +1686 -0
  15. package/skill/manifest.json +2 -22
  16. package/skill/references/components/banner.md +95 -0
  17. package/skill/references/components/button.md +181 -0
  18. package/skill/references/components/card.md +89 -0
  19. package/skill/references/components/code-block.md +77 -0
  20. package/skill/references/components/composer.md +206 -0
  21. package/skill/references/components/empty-state.md +63 -0
  22. package/skill/references/components/glass-controls.md +127 -0
  23. package/skill/references/components/list.md +130 -0
  24. package/skill/references/components/message.md +94 -0
  25. package/skill/references/components/segmented.md +79 -0
  26. package/skill/references/components/sheet.md +84 -0
  27. package/skill/references/components/sidebar.md +139 -0
  28. package/skill/references/components/step-row.md +135 -0
  29. package/skill/references/components/tag.md +86 -0
  30. package/skill/references/components/task-list.md +83 -0
  31. package/skill/references/components/text-field.md +86 -0
  32. package/skill/references/components/toolbar.md +139 -0
  33. package/skill/references/foundations/accessibility.md +27 -0
  34. package/skill/references/foundations/color.md +58 -0
  35. package/skill/references/foundations/elevation.md +17 -0
  36. package/skill/references/foundations/glass.md +36 -0
  37. package/skill/references/foundations/iconography.md +44 -0
  38. package/skill/references/foundations/motion.md +22 -0
  39. package/skill/references/foundations/radius.md +31 -0
  40. package/skill/references/foundations/spacing.md +38 -0
  41. package/skill/references/foundations/stroke.md +35 -0
  42. package/skill/references/foundations/typography.md +54 -0
  43. package/skill/references/foundations/writing.md +10 -0
  44. package/skill/references/review.md +26 -13
  45. package/skill/references/scenes/ios.md +61 -0
  46. package/skill/references/scenes/ipados.md +42 -0
  47. package/skill/references/scenes/macos.md +65 -0
  48. package/skill/references/scenes/swiftui.md +94 -0
  49. package/skill/references/scenes/web-marketing.md +32 -0
  50. package/skill/references/scenes/web.md +83 -0
  51. package/skill/references/tokens.md +441 -74
  52. package/skill/assets/design/README.md +0 -170
  53. package/skill/assets/design/components/Banner/README.md +0 -33
  54. package/skill/assets/design/components/Banner/preview.html +0 -26
  55. package/skill/assets/design/components/Button/README.md +0 -59
  56. package/skill/assets/design/components/Button/preview.html +0 -27
  57. package/skill/assets/design/components/Card/README.md +0 -31
  58. package/skill/assets/design/components/Card/preview.html +0 -24
  59. package/skill/assets/design/components/CodeBlock/README.md +0 -34
  60. package/skill/assets/design/components/CodeBlock/preview.html +0 -16
  61. package/skill/assets/design/components/Composer/README.md +0 -42
  62. package/skill/assets/design/components/Composer/preview.html +0 -28
  63. package/skill/assets/design/components/Cover/preview.html +0 -53
  64. package/skill/assets/design/components/EmptyState/README.md +0 -28
  65. package/skill/assets/design/components/EmptyState/preview.html +0 -16
  66. package/skill/assets/design/components/GlassControls/README.md +0 -43
  67. package/skill/assets/design/components/GlassControls/preview.html +0 -40
  68. package/skill/assets/design/components/ListGroup/README.md +0 -39
  69. package/skill/assets/design/components/ListGroup/preview.html +0 -22
  70. package/skill/assets/design/components/MacWindow/README.md +0 -27
  71. package/skill/assets/design/components/MacWindow/preview.html +0 -70
  72. package/skill/assets/design/components/Message/README.md +0 -31
  73. package/skill/assets/design/components/Message/preview.html +0 -21
  74. package/skill/assets/design/components/PhoneScreen/README.md +0 -25
  75. package/skill/assets/design/components/PhoneScreen/preview.html +0 -74
  76. package/skill/assets/design/components/Segmented/README.md +0 -21
  77. package/skill/assets/design/components/Segmented/preview.html +0 -21
  78. package/skill/assets/design/components/Sheet/README.md +0 -29
  79. package/skill/assets/design/components/Sheet/preview.html +0 -31
  80. package/skill/assets/design/components/Sidebar/README.md +0 -36
  81. package/skill/assets/design/components/Sidebar/preview.html +0 -20
  82. package/skill/assets/design/components/StepRow/README.md +0 -35
  83. package/skill/assets/design/components/StepRow/preview.html +0 -17
  84. package/skill/assets/design/components/Tag/README.md +0 -36
  85. package/skill/assets/design/components/Tag/preview.html +0 -22
  86. package/skill/assets/design/components/TextField/README.md +0 -29
  87. package/skill/assets/design/components/TextField/preview.html +0 -28
  88. package/skill/assets/design/components/bundle.css +0 -189
  89. package/skill/assets/design/design-system.json +0 -25
  90. package/skill/assets/design/images/Banner-dark.png +0 -0
  91. package/skill/assets/design/images/Banner-light.png +0 -0
  92. package/skill/assets/design/images/Button-dark.png +0 -0
  93. package/skill/assets/design/images/Button-light.png +0 -0
  94. package/skill/assets/design/images/Card-dark.png +0 -0
  95. package/skill/assets/design/images/Card-light.png +0 -0
  96. package/skill/assets/design/images/CodeBlock-dark.png +0 -0
  97. package/skill/assets/design/images/CodeBlock-light.png +0 -0
  98. package/skill/assets/design/images/Composer-dark.png +0 -0
  99. package/skill/assets/design/images/Composer-light.png +0 -0
  100. package/skill/assets/design/images/Cover-dark.png +0 -0
  101. package/skill/assets/design/images/Cover-light.png +0 -0
  102. package/skill/assets/design/images/EmptyState-dark.png +0 -0
  103. package/skill/assets/design/images/EmptyState-light.png +0 -0
  104. package/skill/assets/design/images/Example-iOSButtonsInPhone-dark.png +0 -0
  105. package/skill/assets/design/images/Example-iOSButtonsInPhone-light.png +0 -0
  106. package/skill/assets/design/images/GlassControls-dark.png +0 -0
  107. package/skill/assets/design/images/GlassControls-light.png +0 -0
  108. package/skill/assets/design/images/ListGroup-dark.png +0 -0
  109. package/skill/assets/design/images/ListGroup-light.png +0 -0
  110. package/skill/assets/design/images/MacWindow-dark.png +0 -0
  111. package/skill/assets/design/images/MacWindow-light.png +0 -0
  112. package/skill/assets/design/images/Message-dark.png +0 -0
  113. package/skill/assets/design/images/Message-light.png +0 -0
  114. package/skill/assets/design/images/PhoneScreen-dark.png +0 -0
  115. package/skill/assets/design/images/PhoneScreen-light.png +0 -0
  116. package/skill/assets/design/images/Segmented-dark.png +0 -0
  117. package/skill/assets/design/images/Segmented-light.png +0 -0
  118. package/skill/assets/design/images/Sheet-dark.png +0 -0
  119. package/skill/assets/design/images/Sheet-light.png +0 -0
  120. package/skill/assets/design/images/Sidebar-dark.png +0 -0
  121. package/skill/assets/design/images/Sidebar-light.png +0 -0
  122. package/skill/assets/design/images/StepRow-dark.png +0 -0
  123. package/skill/assets/design/images/StepRow-light.png +0 -0
  124. package/skill/assets/design/images/Tag-dark.png +0 -0
  125. package/skill/assets/design/images/Tag-light.png +0 -0
  126. package/skill/assets/design/images/TextField-dark.png +0 -0
  127. package/skill/assets/design/images/TextField-light.png +0 -0
  128. package/skill/assets/design/tokens.css +0 -187
  129. package/skill/assets/design/tokens.json +0 -142
  130. package/skill/references/components.md +0 -648
  131. package/skill/references/foundations.md +0 -170
  132. package/skill/references/native.md +0 -7
  133. package/skill/references/web.md +0 -11
  134. package/template/app.js +0 -7
  135. package/template/index.html +0 -1290
  136. package/template/manifest.json +0 -24
  137. package/template/project/README.md +0 -170
  138. package/template/project/components/Banner/README.md +0 -33
  139. package/template/project/components/Banner/preview.html +0 -26
  140. package/template/project/components/Button/README.md +0 -59
  141. package/template/project/components/Button/preview.html +0 -27
  142. package/template/project/components/Card/README.md +0 -31
  143. package/template/project/components/Card/preview.html +0 -24
  144. package/template/project/components/CodeBlock/README.md +0 -34
  145. package/template/project/components/CodeBlock/preview.html +0 -16
  146. package/template/project/components/Composer/README.md +0 -42
  147. package/template/project/components/Composer/preview.html +0 -28
  148. package/template/project/components/Cover/preview.html +0 -53
  149. package/template/project/components/EmptyState/README.md +0 -28
  150. package/template/project/components/EmptyState/preview.html +0 -16
  151. package/template/project/components/GlassControls/README.md +0 -43
  152. package/template/project/components/GlassControls/preview.html +0 -40
  153. package/template/project/components/ListGroup/README.md +0 -39
  154. package/template/project/components/ListGroup/preview.html +0 -22
  155. package/template/project/components/MacWindow/README.md +0 -27
  156. package/template/project/components/MacWindow/preview.html +0 -70
  157. package/template/project/components/Message/README.md +0 -31
  158. package/template/project/components/Message/preview.html +0 -21
  159. package/template/project/components/PhoneScreen/README.md +0 -25
  160. package/template/project/components/PhoneScreen/preview.html +0 -74
  161. package/template/project/components/Segmented/README.md +0 -21
  162. package/template/project/components/Segmented/preview.html +0 -21
  163. package/template/project/components/Sheet/README.md +0 -29
  164. package/template/project/components/Sheet/preview.html +0 -31
  165. package/template/project/components/Sidebar/README.md +0 -36
  166. package/template/project/components/Sidebar/preview.html +0 -20
  167. package/template/project/components/StepRow/README.md +0 -35
  168. package/template/project/components/StepRow/preview.html +0 -17
  169. package/template/project/components/Tag/README.md +0 -36
  170. package/template/project/components/Tag/preview.html +0 -22
  171. package/template/project/components/TextField/README.md +0 -29
  172. package/template/project/components/TextField/preview.html +0 -28
  173. package/template/project/components/bundle.css +0 -189
  174. package/template/project/design-system.json +0 -25
  175. package/template/project/images/Banner-dark.png +0 -0
  176. package/template/project/images/Banner-light.png +0 -0
  177. package/template/project/images/Button-dark.png +0 -0
  178. package/template/project/images/Button-light.png +0 -0
  179. package/template/project/images/Card-dark.png +0 -0
  180. package/template/project/images/Card-light.png +0 -0
  181. package/template/project/images/CodeBlock-dark.png +0 -0
  182. package/template/project/images/CodeBlock-light.png +0 -0
  183. package/template/project/images/Composer-dark.png +0 -0
  184. package/template/project/images/Composer-light.png +0 -0
  185. package/template/project/images/Cover-dark.png +0 -0
  186. package/template/project/images/Cover-light.png +0 -0
  187. package/template/project/images/EmptyState-dark.png +0 -0
  188. package/template/project/images/EmptyState-light.png +0 -0
  189. package/template/project/images/Example-iOSButtonsInPhone-dark.png +0 -0
  190. package/template/project/images/Example-iOSButtonsInPhone-light.png +0 -0
  191. package/template/project/images/GlassControls-dark.png +0 -0
  192. package/template/project/images/GlassControls-light.png +0 -0
  193. package/template/project/images/ListGroup-dark.png +0 -0
  194. package/template/project/images/ListGroup-light.png +0 -0
  195. package/template/project/images/MacWindow-dark.png +0 -0
  196. package/template/project/images/MacWindow-light.png +0 -0
  197. package/template/project/images/Message-dark.png +0 -0
  198. package/template/project/images/Message-light.png +0 -0
  199. package/template/project/images/PhoneScreen-dark.png +0 -0
  200. package/template/project/images/PhoneScreen-light.png +0 -0
  201. package/template/project/images/Segmented-dark.png +0 -0
  202. package/template/project/images/Segmented-light.png +0 -0
  203. package/template/project/images/Sheet-dark.png +0 -0
  204. package/template/project/images/Sheet-light.png +0 -0
  205. package/template/project/images/Sidebar-dark.png +0 -0
  206. package/template/project/images/Sidebar-light.png +0 -0
  207. package/template/project/images/StepRow-dark.png +0 -0
  208. package/template/project/images/StepRow-light.png +0 -0
  209. package/template/project/images/Tag-dark.png +0 -0
  210. package/template/project/images/Tag-light.png +0 -0
  211. package/template/project/images/TextField-dark.png +0 -0
  212. package/template/project/images/TextField-light.png +0 -0
  213. package/template/project/tokens.css +0 -187
  214. package/template/project/tokens.json +0 -142
  215. package/template/style.css +0 -1
package/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # @deernova/style
2
2
 
3
- Versioned Deernova design skill installer and static website template. Requires Node.js 22+. No runtime dependencies.
3
+ Install the Deernova design skill into an existing repository, so AI agents follow Deernova when they build or review interfaces there. Requires Node.js 22+. No runtime dependencies.
4
4
 
5
5
  ```sh
6
- npx @deernova/style@latest install
6
+ npx @deernova/style@latest install # writes .agents/skills/deernova-style/
7
+ npx @deernova/style@latest install --agent claude # writes .claude/skills/deernova-style/
7
8
  npx @deernova/style@latest update
8
9
  npx @deernova/style@latest doctor
9
- npx @deernova/style@latest init ./my-site
10
10
  ```
11
11
 
12
- Default installation: `.agents/skills/deernova-style` in the current project. Use `--global` for Codex user skills (`$CODEX_HOME/skills` or `~/.codex/skills`), `--agent claude|codex|agents` to choose the agent directory, or `--dir <skills-parent>` for a custom destination. Repeat the same options when updating.
12
+ Run these inside the target project, or pass `--repo <path>`. The CLI writes only the skill directory. It does not add application code, CSS imports or package dependencies.
13
13
 
14
- The skill bundles full foundations, component usage and SwiftUI examples, token reference, web/native integration guidance, review criteria, CSS/HTML assets and images. The CLI and website ship the same version and source hash.
14
+ The skill is guidance, not a component library: `SKILL.md` picks the scene (macOS, iOS, iPadOS, web app, website) and routes to `references/` (`foundations/`, `components/` with size tables and reference CSS/SwiftUI, `scenes/`, the token table and a review checklist). `assets/` holds `tokens.json` and the files generated from it — `tokens.css` (themes × densities), `tailwind.css` (Tailwind v4) and `DeernovaTokens.swift` — plus reference screenshots. Agents apply it through the project's existing framework and components.
15
15
 
16
- Updates are explicit: use `@latest update` to fetch the latest release. Locally modified skills are protected; `--force` backs up before replacing them. doctor returns nonzero if missing, modified or out of sync. init refuses nonempty directories. Serve the generated website with any static HTTP server.
16
+ Use `--agent claude|codex|agents` to choose the skill directory, `--dir <skills-parent>` for a custom destination, or `--global` for a user installation; these three destination modes are mutually exclusive. Repeat the same options on update.
17
+
18
+ Updates are explicit. Modified skills are protected; `--force` backs up before replacing them. `doctor` exits non-zero if the skill is missing, modified or out of date.
package/bin/cli.mjs CHANGED
@@ -1,32 +1,215 @@
1
1
  #!/usr/bin/env node
2
- import { cp, mkdir, readFile, readdir, rename, rm, lstat } from 'node:fs/promises';
3
- import { resolve, join, dirname } from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
4
4
  import { homedir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
- import { createHash } from 'node:crypto';
7
- const pkg = fileURLToPath(new URL('../',import.meta.url));
8
- const args=process.argv.slice(2), command=args.shift()||'help';
9
- const options={};let positional;
10
- for(let i=0;i<args.length;i++){const a=args[i];if(['--global','--force'].includes(a))options[a.slice(2)]=true;else if(['--dir','--agent'].includes(a)){if(!args[i+1]||args[i+1].startsWith('--'))throw Error(`${a} needs a value`);options[a.slice(2)]=args[++i];}else if(a.startsWith('-'))throw Error(`Unknown option: ${a}`);else if(positional)throw Error('Too many arguments');else positional=a;}
11
- async function exists(p){try{return await lstat(p);}catch(e){if(e.code==='ENOENT')return null;throw e;}}
12
- async function main(){
13
- if(['help','--help','-h'].includes(command)){console.log(`deernova-style install|update|doctor [--global] [--agent codex|claude|agents] [--dir <skills-parent>] [--force]\ndeernova-style init <empty-directory>\ndeernova-style version\nDefault: project .agents/skills/deernova-style. --global defaults to CODEX_HOME/skills.\nupdate installs the version bundled in this CLI; use npx @deernova/style@latest update to fetch latest.\n--force keeps a timestamped backup before replacing a locally modified skill.`);return;}
14
- const manifest=JSON.parse(await readFile(join(pkg,'skill/manifest.json'),'utf8'));
15
- if(command==='version'){console.log(manifest.version);return;}
16
- if(command==='init'){if(!positional)throw Error('Usage: deernova-style init <empty-directory>');const dest=resolve(positional);const stat=await exists(dest);if(stat&&(!stat.isDirectory()||(await readdir(dest)).length))throw Error('Destination must be an empty directory');await mkdir(dest,{recursive:true});await cp(join(pkg,'template'),dest,{recursive:true});console.log(`Created Deernova website v${manifest.version} at ${dest}\nServe with any static HTTP server.`);return;}
17
- if(!['install','update','doctor'].includes(command))throw Error(`Unknown command: ${command}`);
18
- if(positional)throw Error('Use --dir to specify a skills parent directory');
19
- const agent=options.agent||(options.global?'codex':'agents');if(!['codex','claude','agents'].includes(agent))throw Error('Agent must be codex, claude or agents');
20
- const base=options.dir?resolve(options.dir):options.global?(agent==='codex'?join(process.env.CODEX_HOME||join(homedir(),'.codex'),'skills'):join(homedir(),'.'+agent,'skills')):resolve('.'+agent,'skills');
21
- const dest=join(base,'deernova-style');const state=await exists(dest);
22
- if(state?.isSymbolicLink()||(state&&!state.isDirectory()))throw Error('Destination is not a regular directory');
23
- let installed=null;try{installed=JSON.parse(await readFile(join(dest,'manifest.json'),'utf8'));}catch{}
24
- const clean=state&&installed?.installedHash ? await installedDigest(dest)===installed.installedHash:false;
25
- if(command==='doctor'){console.log(JSON.stringify({path:dest,installedVersion:installed?.version||null,bundledVersion:manifest.version,sourceMatches:installed?.sourceHash===manifest.sourceHash,localChanges:state?!clean:null},null,2));if(!installed||!clean||installed.sourceHash!==manifest.sourceHash)process.exitCode=1;return;}
26
- if(command==='update'&&!installed)throw Error('No managed installation found. Run install first.');
27
- if(state&&!options.force&&(!clean||command==='install'))throw Error('Skill exists or contains local changes. Use update for clean installs, or --force to back up and replace.');
28
- await mkdir(base,{recursive:true});const temp=join(base,`.deernova-style-${process.pid}-${Date.now()}`);let backup;
29
- try{await cp(join(pkg,'skill'),temp,{recursive:true});const hash=await installedDigest(temp);const {writeFile}=await import('node:fs/promises');await writeFile(join(temp,'manifest.json'),JSON.stringify({...manifest,installedHash:hash},null,2)+'\n');if(state){backup=dest+'.backup-'+Date.now();await rename(dest,backup);}try{await rename(temp,dest);}catch(e){if(backup)await rename(backup,dest);throw e;}if(backup&&clean&&!options.force)await rm(backup,{recursive:true});console.log(`Installed Deernova v${manifest.version}: ${dest}`);if(backup&&(!clean||options.force))console.log(`Backup: ${backup}`);}finally{await rm(temp,{recursive:true,force:true});}
30
- }
31
- async function installedDigest(dir){const manifestPath=join(dir,'manifest.json');const original=JSON.parse(await readFile(manifestPath,'utf8'));const hash=createHash('sha256');async function walk(p,relative=''){for(const e of (await readdir(p,{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name))){const name=relative+e.name;if(name==='manifest.json')continue;if(e.isSymbolicLink())throw Error('Symlink in installed skill');hash.update(name);if(e.isDirectory())await walk(join(p,e.name),name+'/');else hash.update(await readFile(join(p,e.name)));}}await walk(dir);hash.update(original.sourceHash);hash.update(original.version);return hash.digest('hex');}
32
- main().catch(e=>{console.error(`deernova-style: ${e.message}`);process.exitCode=1;});
7
+
8
+ const bundledSkill = fileURLToPath(new URL('../skill/', import.meta.url));
9
+ const HELP = `deernova-style install|update|doctor [--repo <existing-repository>] [--global] [--agent codex|claude|agents] [--dir <skills-parent>] [--force]
10
+ deernova-style init [existing-repository] (alias for install --repo)
11
+ deernova-style version
12
+ Installs design reference files only; does not add app code or package dependencies. Default: project .agents/skills/deernova-style. --global defaults to CODEX_HOME/skills.
13
+ update installs the version bundled in this CLI; use npx @deernova/style@latest update to fetch latest.
14
+ --force keeps a timestamped backup before replacing a locally modified skill.`;
15
+
16
+ function parseArgs(args) {
17
+ let [command = 'help', ...flags] = args;
18
+ const options = {};
19
+ let positional;
20
+
21
+ for (let i = 0; i < flags.length; i++) {
22
+ const flag = flags[i];
23
+ if (['--global', '--force'].includes(flag)) {
24
+ options[flag.slice(2)] = true;
25
+ } else if (['--dir', '--agent', '--repo'].includes(flag)) {
26
+ if (!flags[i + 1] || flags[i + 1].startsWith('--')) {
27
+ throw Error(`${flag} needs a value`);
28
+ }
29
+ options[flag.slice(2)] = flags[++i];
30
+ } else if (flag.startsWith('-')) {
31
+ throw Error(`Unknown option: ${flag}`);
32
+ } else if (positional !== undefined) {
33
+ throw Error('Too many arguments');
34
+ } else {
35
+ positional = flag;
36
+ }
37
+ }
38
+
39
+ if (command === 'init') {
40
+ if (options.repo && positional) throw Error('Specify the repository once');
41
+ options.repo = positional || options.repo || process.cwd();
42
+ positional = undefined;
43
+ command = 'install';
44
+ }
45
+
46
+ return { command, options, positional };
47
+ }
48
+
49
+ async function statIfExists(path) {
50
+ try {
51
+ return await lstat(path);
52
+ } catch (error) {
53
+ if (error.code === 'ENOENT') return null;
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async function resolveDestination(options) {
59
+ if ([options.repo, options.global, options.dir].filter(Boolean).length > 1) {
60
+ throw Error('--repo, --global and --dir are mutually exclusive');
61
+ }
62
+
63
+ const repo = resolve(options.repo || process.cwd());
64
+ if (!options.global && !options.dir && !(await statIfExists(repo))?.isDirectory()) {
65
+ throw Error('Repository must be an existing directory');
66
+ }
67
+
68
+ const agent = options.agent || (options.global ? 'codex' : 'agents');
69
+ if (!['codex', 'claude', 'agents'].includes(agent)) {
70
+ throw Error('Agent must be codex, claude or agents');
71
+ }
72
+
73
+ let base;
74
+ if (options.dir) {
75
+ base = resolve(options.dir);
76
+ } else if (options.global) {
77
+ base = agent === 'codex'
78
+ ? join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'skills')
79
+ : join(homedir(), `.${agent}`, 'skills');
80
+ } else {
81
+ base = join(repo, `.${agent}`, 'skills');
82
+ }
83
+
84
+ return { base, dest: join(base, 'deernova-style') };
85
+ }
86
+
87
+ // Keep this digest compatible with existing installations. The manifest is not
88
+ // hashed as a file: only its source hash and version participate in the digest.
89
+ async function installedDigest(dir, manifest) {
90
+ const hash = createHash('sha256');
91
+
92
+ async function walk(path, relative = '') {
93
+ const entries = await readdir(path, { withFileTypes: true });
94
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
95
+ const name = relative + entry.name;
96
+ if (name === 'manifest.json') continue;
97
+ if (entry.isSymbolicLink()) throw Error('Symlink in installed skill');
98
+
99
+ hash.update(name);
100
+ if (entry.isDirectory()) await walk(join(path, entry.name), `${name}/`);
101
+ else hash.update(await readFile(join(path, entry.name)));
102
+ }
103
+ }
104
+
105
+ await walk(dir);
106
+ hash.update(manifest.sourceHash);
107
+ hash.update(manifest.version);
108
+ return hash.digest('hex');
109
+ }
110
+
111
+ async function inspectInstallation(dest) {
112
+ const state = await statIfExists(dest);
113
+ if (state?.isSymbolicLink() || (state && !state.isDirectory())) {
114
+ throw Error('Destination is not a regular directory');
115
+ }
116
+
117
+ let manifest = null;
118
+ try {
119
+ manifest = JSON.parse(await readFile(join(dest, 'manifest.json'), 'utf8'));
120
+ } catch {
121
+ // A folder without a readable manifest is an unmanaged installation.
122
+ }
123
+
124
+ const clean = Boolean(state && manifest?.installedHash)
125
+ && await installedDigest(dest, manifest) === manifest.installedHash;
126
+ return { exists: Boolean(state), manifest, clean };
127
+ }
128
+
129
+ function reportInstallation(dest, installed, bundled) {
130
+ const sourceMatches = installed.manifest?.sourceHash === bundled.sourceHash;
131
+ console.log(JSON.stringify({
132
+ path: dest,
133
+ installedVersion: installed.manifest?.version || null,
134
+ bundledVersion: bundled.version,
135
+ sourceMatches,
136
+ localChanges: installed.exists ? !installed.clean : null
137
+ }, null, 2));
138
+
139
+ if (!installed.manifest || !installed.clean || !sourceMatches) process.exitCode = 1;
140
+ }
141
+
142
+ async function replaceInstallation({ base, dest }, installed, manifest, force) {
143
+ await mkdir(base, { recursive: true });
144
+ const temp = await mkdtemp(join(base, '.deernova-style-'));
145
+ let backup;
146
+
147
+ try {
148
+ // Prepare the complete replacement before touching the installed directory.
149
+ await cp(bundledSkill, temp, { recursive: true });
150
+ const installedHash = await installedDigest(temp, manifest);
151
+ await writeFile(join(temp, 'manifest.json'), JSON.stringify({
152
+ ...manifest,
153
+ installedHash
154
+ }, null, 2) + '\n');
155
+
156
+ if (installed.exists) {
157
+ backup = `${dest}.backup-${Date.now()}`;
158
+ await rename(dest, backup);
159
+ }
160
+
161
+ try {
162
+ await rename(temp, dest);
163
+ } catch (error) {
164
+ if (backup) await rename(backup, dest);
165
+ throw error;
166
+ }
167
+
168
+ const keepBackup = backup && (!installed.clean || force);
169
+ if (backup && !keepBackup) await rm(backup, { recursive: true });
170
+ console.log(`Installed Deernova v${manifest.version}: ${dest}`);
171
+ if (keepBackup) console.log(`Backup: ${backup}`);
172
+ } finally {
173
+ await rm(temp, { recursive: true, force: true });
174
+ }
175
+ }
176
+
177
+ async function main() {
178
+ const { command, options, positional } = parseArgs(process.argv.slice(2));
179
+ if (['help', '--help', '-h'].includes(command)) {
180
+ console.log(HELP);
181
+ return;
182
+ }
183
+ if (!['version', 'install', 'update', 'doctor'].includes(command)) {
184
+ throw Error(`Unknown command: ${command}`);
185
+ }
186
+
187
+ const manifest = JSON.parse(await readFile(join(bundledSkill, 'manifest.json'), 'utf8'));
188
+ if (command === 'version') {
189
+ console.log(manifest.version);
190
+ return;
191
+ }
192
+ if (positional !== undefined) {
193
+ throw Error('Use --repo to specify a repository or --dir to specify a skills parent');
194
+ }
195
+
196
+ const destination = await resolveDestination(options);
197
+ const installed = await inspectInstallation(destination.dest);
198
+ if (command === 'doctor') {
199
+ reportInstallation(destination.dest, installed, manifest);
200
+ return;
201
+ }
202
+ if (command === 'update' && !installed.manifest) {
203
+ throw Error('No managed installation found. Run install first.');
204
+ }
205
+ if (installed.exists && !options.force && (!installed.clean || command === 'install')) {
206
+ throw Error('Skill exists or contains local changes. Use update for clean installs, or --force to back up and replace.');
207
+ }
208
+
209
+ await replaceInstallation(destination, installed, manifest, options.force);
210
+ }
211
+
212
+ main().catch(error => {
213
+ console.error(`deernova-style: ${error.message}`);
214
+ process.exitCode = 1;
215
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deernova/style",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -13,8 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "bin",
16
- "skill",
17
- "template"
16
+ "skill"
18
17
  ],
19
- "description": "Deernova design skill installer and website template CLI"
18
+ "description": "Install the Deernova design reference skill into an existing repository"
20
19
  }
package/skill/SKILL.md CHANGED
@@ -1,36 +1,106 @@
1
1
  ---
2
2
  name: deernova-style
3
- description: Apply Deernova's monochrome design language to Deernova apps, websites and components; implement or review semantic tokens, typography, paper and glass hierarchy, platform density and accessibility. Use when a project requests Deernova styling or adopts this design system.
3
+ description: Deernova 的界面设计语言:只用黑白灰,内容放在不透明纸面上,原生端的操作层用系统 Liquid Glass,Web 应用用不透明平面加 1px 线;界面和 AI 回答用无衬线,衬线只给大标题和长文;按场景(Web 应用、macOS、iOS、iPadOS、官网与文档)切换密度。在 Deernova 的应用或网站里新建、修改或审查界面时使用,包括颜色、字体、间距、圆角、线条、阴影、按钮、输入框、侧边栏、顶栏、列表、卡片、弹窗、空状态、AI 对话和任务清单,以及对应的 CSS、Tailwind 或 SwiftUI 实现。
4
4
  ---
5
5
 
6
- # Deernova Style
6
+ # Deernova 设计语言
7
7
 
8
- Build coherent Deernova interfaces using the versioned resources bundled with this skill. Preserve the user's product, platform, framework and requested scope. The HTML previews demonstrate appearance; they are not production SwiftUI components or proof of functional behavior.
8
+ 只用黑、白、灰;内容放在不透明的纸面上,原生端的操作层交给系统玻璃;界面文字用系统无衬线,衬线只用来点题;整体安静、克制、层次清楚,像一件趁手的工具。
9
9
 
10
- ## Start with the relevant source
10
+ 桌面端的骨架和密度参照 Claude 桌面端(Code):288 宽的侧边栏、44 高的顶栏、居中的对话栏、底部的输入区、可以并排打开的侧面板,正文 15/24。颜色、玻璃和衬线是 Deernova 自己的。
11
11
 
12
- - Read [foundations](references/foundations.md) for principles, writing, layout, platform behavior, motion and accessibility.
13
- - Read [tokens](references/tokens.md) when choosing semantic values. Exact structured values and typography live in `assets/design/tokens.json`; use `assets/design/tokens.css` for web projects.
14
- - Search [components](references/components.md) for the component being implemented, then inspect `assets/design/components/<Name>/preview.html` for its anatomy and variants. Native implementation examples live in that component's README.
15
- - For web implementation and validation, read [web](references/web.md). For native implementation, read [native](references/native.md).
16
- - For design review, read [review](references/review.md). Report concrete mismatches and locations; do not redesign unrelated screens.
12
+ 这份 skill 是规范和数据,不是组件库:不需要安装任何包。用项目现有的技术栈和组件实现这些规则,缺的部分按这里的规格和参考写法补上。
17
13
 
18
- ## Core decisions
14
+ ## 第一步:确定场景
19
15
 
20
- Content lives on opaque paper surfaces; transient actions may use glass. Use neutral grayscale and contrast for emphasis. Color communicates status with supporting text or symbols. Use semantic tokens rather than copying literals. `accent` maps to ink; primary labels use `on-accent`, never hardcoded white.
16
+ 所有尺寸都按场景取值。先看你在做哪一种界面,再去读对应的场景说明。
21
17
 
22
- Serif communicates titles and sustained reading; system sans supports controls; monospace supports code and aligned data. Preserve platform differences: compact macOS controls, Dynamic Type and 44 pt hit regions on iOS, responsive web layouts. Do not impose desktop dimensions on touch screens.
18
+ <!-- tokens:scenes -->
19
+ | 场景 | 密度 | 输入 | 怎么启用 | 读这里 |
20
+ | --- | --- | --- | --- | --- |
21
+ | Web 应用 | 桌面(`desktop`) | 指针 | tokens.css 的默认值 | [scenes/web.md](references/scenes/web.md) |
22
+ | Web 触屏 | 触控(`touch`) | 触摸 | `@media (pointer: coarse)` 自动切换 | [scenes/web.md](references/scenes/web.md) |
23
+ | macOS | 桌面(`desktop`) | 指针 | 系统控件用系统尺寸;内容区用 `DeernovaTokens.swift` 的 `#if os(macOS)` | [scenes/macos.md](references/scenes/macos.md) |
24
+ | iOS | 触控(`touch`) | 触摸 | `DeernovaTokens.swift` 的 `#else` | [scenes/ios.md](references/scenes/ios.md) |
25
+ | iPadOS | 触控(`touch`) | 触摸和指针 | 同 iOS;分栏布局另见场景说明 | [scenes/ipados.md](references/scenes/ipados.md) |
26
+ | 官网与文档 | 舒展(`comfortable`) | 指针 | 根元素加 `data-density="comfortable"` | [scenes/web-marketing.md](references/scenes/web-marketing.md) |
27
+ <!-- /tokens -->
23
28
 
24
- Cards use thin borders without floating shadows. System-native controls and glass take precedence on supported Apple platforms. The supplied CSS glass values are web approximations only. Honor the project's deployment target rather than silently raising it to the source guide's iOS/macOS 26 baseline.
29
+ 后台、工具、编辑器、AI 对话这类应用界面,不论是不是网页,都不要用官网的尺寸。
25
30
 
26
- ## Workflow
31
+ ## 场景速查
27
32
 
28
- 1. Identify target platform, screen purpose, existing components and styling integration.
29
- 2. Select tokens and component patterns from the bundled source. Retain existing product behavior and content.
30
- 3. Implement normal, selected, disabled, loading, empty and error states where the product needs them. Give demonstrations explicit demo behavior; do not invent backend operations.
31
- 4. Verify both themes, keyboard navigation, focus, overflow, status clarity and relevant touch/text scaling. Use the review reference for concrete criteria.
32
- 5. Report implementation and verification limits. Do not claim native parity or accessibility compliance based only on the visual preview.
33
+ <!-- tokens:summary -->
34
+ | | 桌面<br>Web 应用 · macOS 内容区 | 触控<br>iOS · iPadOS · Web 触屏 | 舒展<br>官网与文档 |
35
+ | --- | --- | --- | --- |
36
+ | 正文 | 15 / 24 | 17 / 23 | 15 / 24 |
37
+ | 长文、AI 回答(衬线) | 16 / 26 | 17 / 26 | 17 / 28 |
38
+ | 默认按钮(可见高度) | 28,圆角 6 | 36,胶囊 | 36,胶囊 |
39
+ | 小 / 大按钮 | 24 / 32 | 30 / 46 | 32 / 44 |
40
+ | 最小点击区域 | 24 | 44 | 24 |
41
+ | 输入框 | 28 | 44 | 40 |
42
+ | 列表行 | 32 | 44 | 40 |
43
+ | 卡片:圆角 / 内边距 | 12 / 12 × 16 | 12 / 14 × 16 | 12 / 24 × 24 |
44
+ | 页面边距 | 32 | 16 | 32 |
45
+ | 侧边栏宽度 | 288 | 320 | 260 |
46
+ <!-- /tokens -->
33
47
 
34
- ## Version and updates
48
+ 所有数值都来自 `assets/tokens.json`,按密度生成在 `assets/tokens.css` 里。写代码时引用变量名(`var(--button-height-md)`、`Deernova.buttonHeightMd`),不要抄数字。
35
49
 
36
- `manifest.json` identifies the package version and source hash. Website, CSS and skill references are generated together from the repository's `design/` source. Update through `npx @deernova/style@latest update` in the same project; for global installations append `--global` and the same agent choice. This is an explicit update, not a background network service. Preserve local changes; the CLI refuses to overwrite them unless `--force` is requested, in which case it keeps a backup. Never edit generated references as the upstream source.
50
+ ## 核心规则
51
+
52
+ 1. **纸面承载内容,玻璃交给系统。** 文字、列表、卡片、代码、消息、输入区是内容,放在不透明的纸面上:`canvas` → `surface` → `surface-raised`,代码块和展开的详情用 `surface-sunken`。操作层按平台处理:**SwiftUI 端**(macOS、iOS、iPadOS)的侧边栏、工具栏、标签栏、浮动按钮直接用系统 Liquid Glass,这是原生端的优势,不要盖掉;**Web 应用**不模拟玻璃,操作层是不透明平面加 1px 分隔线;只有官网与文档的页眉可以用轻玻璃。玻璃上不放段落,内容不做成玻璃,玻璃不叠玻璃。
53
+ 2. **黑白灰,强调靠对比。** `accent` 就是墨色 `ink`:主按钮在浅色主题是黑底白字,在深色主题是白底黑字,文字用 `on-accent`,不要写死白色。唯一的彩色是状态色 `success`、`warning`、`danger`、`info`,只用来表达状态,而且必须同时带图标或文字。
54
+ 3. **线条代替阴影。** 区块之间用 `line` 色的 1px 线分隔(`stroke-line`)。卡片不加阴影;只有菜单、弹窗、浮动面板和 Composer 这类真正浮起来的东西才用 `shadow-float`。
55
+ 4. **无衬线做界面,衬线点题。** 界面文字、AI 回答、用户消息、标题栏都用系统无衬线(SF Pro);代码和需要对齐的数字用等宽(SF Mono)。衬线(New York)只用在页面大标题和空状态标题(`display`、`title`)、官网与文档,以及文章类的长文阅读(`prose`)。中文统一落到苹方。
56
+ 5. **层次靠字号和灰度,少用粗体。** 600 只给真正的标题(`display`、`title`、`headline`)。分组标题、会话名、说明、时间戳用常规字重,靠 `ink-secondary`、`ink-tertiary` 和小一号的字区分层级。
57
+ 6. **控件高度由 token 决定,点击区域单独补。** 按钮、输入框、列表行的可见高度直接用 `button-height-*`、`field-height`、`row-height`,上下内边距为 0,不让页面行高把它撑高。可见尺寸小于 `hit-min` 时,用透明的命中区补足,不加大可见的控件。
58
+ 7. **间距成组。** 间距走 4pt 网格。组内用 `gap-stack`,组与组之间用 `gap-group`(至少 2 倍);内容区里段落、工具调用、卡片之间 `space-4`。用细线和少量留白分组,不靠大块空白和外框。
59
+ 8. **原生端用系统控件,Web 端用同一套自绘组件。** SwiftUI 端的导航结构、侧边栏、工具栏、菜单、表单、弹窗直接用系统控件和系统尺寸,只通过颜色和字体让它们融入;自己画的内容区(对话、卡片、输入区、任务清单)用 token。Web 应用没有系统控件可借,按组件说明里的规格画,骨架见 [Web 应用](references/scenes/web.md)。
60
+
61
+ ## 读什么
62
+
63
+ | 你在做 | 先读 |
64
+ | --- | --- |
65
+ | 任何界面 | 上面的核心规则,加上对应的场景说明 `references/scenes/` |
66
+ | 某个组件 | [组件](#组件)里对应的文件:用途、尺寸表、状态、Web 和 SwiftUI 写法 |
67
+ | 需要具体数值 | [设计变量](references/tokens.md);数据在 `assets/tokens.json` |
68
+ | 接入变量 | CSS:`assets/tokens.css`;Tailwind v4:`assets/tailwind.css`;SwiftUI:`assets/DeernovaTokens.swift` |
69
+ | 某条规则为什么这样 | [基础](#基础)里对应的主题 |
70
+ | 审查已有界面 | [审查清单](references/review.md) |
71
+ | 先看原生端的玻璃效果 | `assets/screens/` 里的截图,各有浅色和深色 |
72
+
73
+ ### 基础
74
+
75
+ [颜色](references/foundations/color.md) · [字体](references/foundations/typography.md) · [间距与布局](references/foundations/spacing.md) · [圆角](references/foundations/radius.md) · [线条](references/foundations/stroke.md) · [层级与阴影](references/foundations/elevation.md) · [玻璃](references/foundations/glass.md) · [动效](references/foundations/motion.md) · [图标](references/foundations/iconography.md) · [文案](references/foundations/writing.md) · [无障碍](references/foundations/accessibility.md)
76
+
77
+ ### 组件
78
+
79
+ [按钮](references/components/button.md) · [顶栏与侧面板](references/components/toolbar.md) · [玻璃操作层](references/components/glass-controls.md) · [分段控件](references/components/segmented.md) · [文本框](references/components/text-field.md) · [Composer](references/components/composer.md) · [侧边栏](references/components/sidebar.md) · [分组列表](references/components/list.md) · [卡片](references/components/card.md) · [标签](references/components/tag.md) · [代码块](references/components/code-block.md) · [任务清单](references/components/task-list.md) · [空状态](references/components/empty-state.md) · [横幅与轻提示](references/components/banner.md) · [弹窗](references/components/sheet.md) · [消息](references/components/message.md) · [工具调用与执行步骤](references/components/step-row.md)
80
+
81
+ ### 场景
82
+
83
+ [Web 应用](references/scenes/web.md) · [官网与文档](references/scenes/web-marketing.md) · [macOS](references/scenes/macos.md) · [iOS](references/scenes/ios.md) · [iPadOS](references/scenes/ipados.md) · [SwiftUI 共用](references/scenes/swiftui.md)
84
+
85
+ ## 工作流程
86
+
87
+ 1. **先看项目。** 确认场景(上面的表)、最低系统版本、现有组件,以及颜色和样式怎么管理(CSS 变量、Tailwind 主题、Asset Catalog)。
88
+ 2. **接入变量。** 把对应的生成文件接进项目现有的体系,用语义名引用(`ink`、`surface`、`button-height-md`),不要在代码里散落十六进制颜色和尺寸数字。
89
+ 3. **按组件实现。** 找到对应的组件说明,用项目的组件实现,并补齐需要的状态:默认、悬停或按下、选中、禁用、加载、空、错误。
90
+ 4. **检查。** 浅色和深色两套主题;量一下默认按钮的实际高度是否等于 `button-height-md`;键盘焦点、点击区域、长文本和最大字号、减弱动态效果和降低透明度。审查时按 [审查清单](references/review.md) 逐项看。
91
+ 5. **汇报。** 说明做了什么、哪些没有验证。不要因为这份 skill 就提高项目的最低系统版本、引入新的 UI 库或改动无关的页面。
92
+
93
+ ## 不要做
94
+
95
+ - 加品牌色、渐变、彩色图标,或用 emoji 当图标。
96
+ - 在 SwiftUI 端给系统工具栏、侧边栏加实色背景盖掉玻璃;在 Web 应用里用模糊效果模仿玻璃。
97
+ - 给卡片加阴影;在玻璃上放正文或列表。
98
+ - 给界面文字、AI 回答或按钮用衬线;到处用粗体。
99
+ - 用上下内边距撑出按钮高度;为了凑 44 点击区域把可见的按钮做大。
100
+ - 把官网的大按钮、大字号搬进应用界面;把桌面端的尺寸直接搬到触屏上。
101
+ - 一屏出现两个主按钮;用「确定」「OK」当按钮文字。
102
+ - 只靠颜色表达状态。
103
+
104
+ ## 版本与更新
105
+
106
+ 通过 `npx @deernova/style` 安装时,同一目录下的 `manifest.json` 记录版本和内容哈希;更新运行 `npx @deernova/style@latest update`。不要直接修改安装后的文件,规范的源文件在 deernova-style 仓库的 `skills/deernova-style/`。