poveste 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @deprecated
2
+ * @deprecated Use {@link logEvent} instead. This forwards to it and warns.
3
3
  */
4
4
  export function hstEvent(name: string, argument): void
5
5
 
@@ -15,5 +15,13 @@ export function logEvent(name: string, argument): void
15
15
  */
16
16
  export function isCollecting(): boolean
17
17
 
18
+ /**
19
+ * Switches the book between light and dark, or to `value` when one is given.
20
+ * Returns the mode it ended up in.
21
+ */
18
22
  export function toggleDark(value?: boolean): boolean
23
+
24
+ /**
25
+ * Whether the book is currently in dark mode.
26
+ */
19
27
  export function isDark(): boolean
@@ -0,0 +1,26 @@
1
+ import type { ServerStoryFile } from '@poveste/shared';
2
+ export interface BuildCounts {
3
+ /** Story files that produced a story with variants. */
4
+ stories: number;
5
+ variants: number;
6
+ /** Standalone `.story.md` pages, which the book's home page calls Documents. */
7
+ docs: number;
8
+ /** Relative paths of the files that produced neither, so the warning can name them. */
9
+ empty: string[];
10
+ }
11
+ /**
12
+ * Splits the collected story files three ways, the way the book's own home page
13
+ * does: stories, documents, and files that produced nothing.
14
+ *
15
+ * "No variants" is not the question. A standalone `.story.md` is collected into a
16
+ * story with `docsOnly: true` and `variants: []` by construction (`markdown.ts`),
17
+ * so counting empty by variants alone reported every documentation page in the
18
+ * book as a problem — three of the four `examples/vue3` warned about are
19
+ * `SHARED_STORY_TITLES` entries that a required spec fails if you delete them,
20
+ * which left the one real empty file hidden inside them (#670).
21
+ *
22
+ * Documents are counted rather than folded in with stories because `HomeView.vue`
23
+ * already makes exactly this split, and a build that reports 60 stories over a
24
+ * home page reading 57 Stories and 3 Documents disagrees with itself.
25
+ */
26
+ export declare function buildCounts(storyFiles: ServerStoryFile[]): BuildCounts;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Splits the collected story files three ways, the way the book's own home page
3
+ * does: stories, documents, and files that produced nothing.
4
+ *
5
+ * "No variants" is not the question. A standalone `.story.md` is collected into a
6
+ * story with `docsOnly: true` and `variants: []` by construction (`markdown.ts`),
7
+ * so counting empty by variants alone reported every documentation page in the
8
+ * book as a problem — three of the four `examples/vue3` warned about are
9
+ * `SHARED_STORY_TITLES` entries that a required spec fails if you delete them,
10
+ * which left the one real empty file hidden inside them (#670).
11
+ *
12
+ * Documents are counted rather than folded in with stories because `HomeView.vue`
13
+ * already makes exactly this split, and a build that reports 60 stories over a
14
+ * home page reading 57 Stories and 3 Documents disagrees with itself.
15
+ */
16
+ export function buildCounts(storyFiles) {
17
+ const counts = { stories: 0, variants: 0, docs: 0, empty: [] };
18
+ for (const file of storyFiles) {
19
+ if (file.story?.docsOnly) {
20
+ counts.docs++;
21
+ }
22
+ else if (file.story?.variants.length) {
23
+ counts.stories++;
24
+ counts.variants += file.story.variants.length;
25
+ }
26
+ else {
27
+ counts.empty.push(file.relativePath);
28
+ }
29
+ }
30
+ return counts;
31
+ }
@@ -5,6 +5,7 @@ import { join } from 'pathe';
5
5
  import pc from 'picocolors';
6
6
  import { createServer as createViteServer, mergeConfig as mergeViteConfig, build as viteBuild, } from 'vite';
7
7
  import { APP_PATH } from './alias.js';
8
+ import { buildCounts } from './build-counts.js';
8
9
  import { getSerializedStoryData } from './build-serialize.js';
9
10
  import { useCollectStories } from './collect/index.js';
10
11
  import { useModuleLoader } from './load.js';
@@ -105,9 +106,7 @@ export async function build(ctx) {
105
106
  // Same wrapper the dev path uses for these exact calls (server.ts).
106
107
  await wrapLogError('destroyCollectStories', () => destroyCollectStories());
107
108
  }
108
- const storyCount = ctx.storyFiles.reduce((sum, file) => sum + (file.story?.variants.length ? 1 : 0), 0);
109
- const variantCount = ctx.storyFiles.reduce((sum, file) => sum + (file.story?.variants.length ?? 0), 0);
110
- const emptyStoryCount = ctx.storyFiles.length - storyCount;
109
+ const { stories: storyCount, variants: variantCount, docs: docsCount, empty } = buildCounts(ctx.storyFiles);
111
110
  const { viteConfig: buildViteConfigRaw } = await getViteConfigWithPlugins(false, ctx);
112
111
  const buildViteConfig = mergeViteConfig(buildViteConfigRaw, {
113
112
  // Vite picks the `.env` file from `mode`, not from NODE_ENV — this said
@@ -261,10 +260,11 @@ export async function build(ctx) {
261
260
  await writeFile('__sandbox.html', sandboxHtml, ctx);
262
261
  await writeFile('poveste.json', JSON.stringify(getSerializedStoryData(ctx), null, 2), ctx);
263
262
  const duration = performance.now() - startTime;
264
- if (emptyStoryCount) {
265
- console.warn(pc.yellow(`⚠️ ${emptyStoryCount} empty story file${emptyStoryCount === 1 ? '' : 's'}`));
263
+ if (empty.length) {
264
+ console.warn(pc.yellow(`⚠️ ${empty.length} empty story file${empty.length === 1 ? '' : 's'}: ${empty.join(', ')}`));
266
265
  }
267
- console.log(pc.green(`✅ Built ${storyCount} stor${storyCount === 1 ? 'y' : 'ies'} (${variantCount} variant${variantCount === 1 ? '' : 's'}) in ${Math.round(duration / 1000 * 100) / 100}s`));
266
+ const docsNote = docsCount ? ` and ${docsCount} document${docsCount === 1 ? '' : 's'}` : '';
267
+ console.log(pc.green(`✅ Built ${storyCount} stor${storyCount === 1 ? 'y' : 'ies'} (${variantCount} variant${variantCount === 1 ? '' : 's'})${docsNote} in ${Math.round(duration / 1000 * 100) / 100}s`));
268
268
  // Render
269
269
  if (previewStoryCallbacks.length) {
270
270
  const { baseUrl, close } = await startPreview({}, ctx);
@@ -1,6 +1,7 @@
1
1
  import { cpus } from 'node:os';
2
2
  import { MessageChannel } from 'node:worker_threads';
3
3
  import Tinypool from '@akryum/tinypool';
4
+ import { escapeRegExp } from '@poveste/shared';
4
5
  import { createBirpc } from 'birpc';
5
6
  import path, { relative } from 'pathe';
6
7
  import pc from 'picocolors';
@@ -29,7 +30,7 @@ export function useCollectStories(options, ctx) {
29
30
  // @TODO temporary fix for https://github.com/histoire-dev/histoire/issues/409
30
31
  /vite\w*\/dist\/client\/(client|env).mjs/,
31
32
  ...ctx.config.viteNodeInlineDeps ?? [],
32
- new RegExp(path.resolve(TEMP_PATH, 'plugins')),
33
+ new RegExp(escapeRegExp(path.resolve(TEMP_PATH, 'plugins'))),
33
34
  ],
34
35
  fallbackCJS: true,
35
36
  },
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Emitting values into generated modules.
3
+ *
4
+ * Every generator in this directory builds JavaScript source by interpolation,
5
+ * and most of the values come from somewhere the author of that source did not
6
+ * control — a plugin's id, a user's config key, the project's own path. Getting
7
+ * it wrong produces a syntax error inside a module nobody wrote and nobody can
8
+ * open, which is the symptom rather than a crash at the input (#609).
9
+ *
10
+ * There are two positions and they need different things.
11
+ */
12
+ /** A value in a string or key position. Valid JSON is valid JavaScript here. */
13
+ export declare function jsString(value: string): string;
14
+ /**
15
+ * A value in an *identifier* position — a binding name, an import name.
16
+ *
17
+ * Quoting cannot help here, so the only honest options are to reject the value
18
+ * or to rename it. These names are read back by consumers (`logos.square`,
19
+ * `generatedSetup.setupVue3`), so renaming would move the problem to whoever
20
+ * looks them up. It rejects, naming the value and where it came from.
21
+ */
22
+ export declare function jsIdentifier(value: string, what: string): string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Emitting values into generated modules.
3
+ *
4
+ * Every generator in this directory builds JavaScript source by interpolation,
5
+ * and most of the values come from somewhere the author of that source did not
6
+ * control — a plugin's id, a user's config key, the project's own path. Getting
7
+ * it wrong produces a syntax error inside a module nobody wrote and nobody can
8
+ * open, which is the symptom rather than a crash at the input (#609).
9
+ *
10
+ * There are two positions and they need different things.
11
+ */
12
+ /** A value in a string or key position. Valid JSON is valid JavaScript here. */
13
+ export function jsString(value) {
14
+ return JSON.stringify(value);
15
+ }
16
+ /**
17
+ * A value in an *identifier* position — a binding name, an import name.
18
+ *
19
+ * Quoting cannot help here, so the only honest options are to reject the value
20
+ * or to rename it. These names are read back by consumers (`logos.square`,
21
+ * `generatedSetup.setupVue3`), so renaming would move the problem to whoever
22
+ * looks them up. It rejects, naming the value and where it came from.
23
+ */
24
+ export function jsIdentifier(value, what) {
25
+ if (!/^[A-Z_$][\w$]*$/i.test(value)) {
26
+ throw new Error(`${what} must be usable as a JavaScript identifier, and ${JSON.stringify(value)} is not. `
27
+ + 'It is emitted into a generated module as a binding name, where quoting cannot rescue it.');
28
+ }
29
+ return value;
30
+ }
@@ -1,3 +1,4 @@
1
+ import { jsIdentifier, jsString } from './codegen.js';
1
2
  const serializedFields = ['id', 'label', 'prompts', 'icon', 'searchText'];
2
3
  export function resolvedCommands(ctx) {
3
4
  const imports = [];
@@ -21,10 +22,10 @@ function getCommandFields(command, imports) {
21
22
  if (command.clientSetupFile) {
22
23
  const importedVar = `__setup${imports.length}__`;
23
24
  if (typeof command.clientSetupFile === 'string') {
24
- imports.push(`import ${importedVar} from '${command.clientSetupFile}'`);
25
+ imports.push(`import ${importedVar} from ${jsString(command.clientSetupFile)}`);
25
26
  }
26
27
  else {
27
- imports.push(`import { ${command.clientSetupFile.importName} as ${importedVar} } from '${command.clientSetupFile.file}'`);
28
+ imports.push(`import { ${jsIdentifier(command.clientSetupFile.importName, `clientSetupFile.importName '${command.clientSetupFile.importName}'`)} as ${importedVar} } from ${jsString(command.clientSetupFile.file)}`);
28
29
  }
29
30
  fields.push(`...${importedVar}`);
30
31
  }
@@ -1,10 +1,11 @@
1
+ import { jsIdentifier, jsString } from './codegen.js';
1
2
  export function resolvedConfig(ctx) {
2
3
  let js = `export const config = ${JSON.stringify(ctx.config)}\n`;
3
4
  if (ctx.config.theme?.logo) {
4
5
  for (const key in ctx.config.theme.logo) {
5
- js += `import Logo_${key} from '${ctx.config.theme.logo[key]}'\n`;
6
+ js += `import Logo_${jsIdentifier(key, `theme.logo key '${key}'`)} from ${jsString(ctx.config.theme.logo[key])}\n`;
6
7
  }
7
8
  }
8
- js += `export const logos = {${Object.keys(ctx.config.theme?.logo ?? {}).map(key => `${key}: Logo_${key}`).join(', ')}}\n`;
9
+ js += `export const logos = {${Object.keys(ctx.config.theme?.logo ?? {}).map(key => `${jsString(key)}: Logo_${key}`).join(', ')}}\n`;
9
10
  return js;
10
11
  }
@@ -1,4 +1,5 @@
1
1
  import { makeTree } from '../tree.js';
2
+ import { jsString } from './codegen.js';
2
3
  export function resolvedStories(ctx) {
3
4
  const resolvedStories = ctx.storyFiles.filter(s => !!s.story);
4
5
  const files = resolvedStories.map((file, index) => {
@@ -23,7 +24,7 @@ ${resolvedStories.map((file, index) => {
23
24
  }
24
25
  return supportPlugin.importStoryComponent(file, index);
25
26
  }).filter(Boolean).join('\n')}
26
- export let files = [${files.map(file => `{${JSON.stringify(file).slice(1, -1)}, component: Comp${file.index}, source: () => import('virtual:story-source:${file.story.id}')}`).join(',\n')}]
27
+ export let files = [${files.map(file => `{${JSON.stringify(file).slice(1, -1)}, component: Comp${file.index}, source: () => import(${jsString(`virtual:story-source:${file.story.id}`)})}`).join(',\n')}]
27
28
  export let tree = ${JSON.stringify(makeTree(ctx.config, resolvedStories))}
28
29
  const handlers = []
29
30
  export function onUpdate (cb) {
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { jsString } from './codegen.js';
2
3
  import { PLUGINS_HAVE_DEV } from './util.js';
3
4
  const require = createRequire(import.meta.url);
4
5
  export function resolvedSupportPluginsClient(ctx) {
5
- const plugins = ctx.supportPlugins.map(p => `'${p.id}': () => import(${JSON.stringify(require.resolve(`${p.moduleName}/client${process.env.POVESTE_DEV && PLUGINS_HAVE_DEV.includes(p.moduleName) ? '-dev' : ''}`, {
6
+ const plugins = ctx.supportPlugins.map(p => `${jsString(p.id)}: () => import(${JSON.stringify(require.resolve(`${p.moduleName}/client${process.env.POVESTE_DEV && PLUGINS_HAVE_DEV.includes(p.moduleName) ? '-dev' : ''}`, {
6
7
  paths: [ctx.root, import.meta.url],
7
8
  }))})`);
8
9
  return `export const clientSupportPlugins = {
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { jsString } from './codegen.js';
2
3
  import { PLUGINS_HAVE_DEV } from './util.js';
3
4
  const require = createRequire(import.meta.url);
4
5
  export function resolvedSupportPluginsCollect(ctx) {
5
- const plugins = ctx.supportPlugins.map(p => `'${p.id}': () => import(${JSON.stringify(require.resolve(`${p.moduleName}/collect${process.env.POVESTE_DEV && PLUGINS_HAVE_DEV.includes(p.moduleName) ? '-dev' : ''}`, {
6
+ const plugins = ctx.supportPlugins.map(p => `${jsString(p.id)}: () => import(${JSON.stringify(require.resolve(`${p.moduleName}/collect${process.env.POVESTE_DEV && PLUGINS_HAVE_DEV.includes(p.moduleName) ? '-dev' : ''}`, {
6
7
  paths: [ctx.root, import.meta.url],
7
8
  }))})`);
8
9
  return `export const collectSupportPlugins = {
@@ -1,3 +1,4 @@
1
+ import { jsIdentifier } from './codegen.js';
1
2
  export const ID_SEPARATOR = '__-__';
2
3
  /**
3
4
  * Names of every setup hook contributed by the enabled support plugins, e.g.
@@ -28,7 +29,7 @@ export function getSetupFnGroups(ctx) {
28
29
  * in every consumer build.
29
30
  */
30
31
  export function declareEmptySetupFns(ctx) {
31
- return getSetupFnNames(ctx).map(fnName => `export const ${fnName} = undefined`).join('\n');
32
+ return getSetupFnNames(ctx).map(fnName => `export const ${jsIdentifier(fnName, `support plugin setupFn '${fnName}'`)} = undefined`).join('\n');
32
33
  }
33
34
  export const PLUGINS_HAVE_DEV = [
34
35
  '@poveste/plugin-vue',
package/dist/node/vite.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
+ import { escapeRegExp } from '@poveste/shared';
3
4
  import { lookup as lookupMime } from 'mrmime';
4
5
  import { dirname, join, relative } from 'pathe';
5
6
  import { loadConfigFromFile as loadViteConfigFromFile, mergeConfig as mergeViteConfig, searchForWorkspaceRoot, } from 'vite';
@@ -299,7 +300,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
299
300
  if (/\.(?:vue|js)(?:$|\?)/.test(id)) {
300
301
  const original = code;
301
302
  for (const flag in flags) {
302
- code = code.replace(new RegExp(flag, 'g'), flags[flag]);
303
+ code = code.replace(new RegExp(escapeRegExp(flag), 'g'), flags[flag]);
303
304
  }
304
305
  if (original !== code)
305
306
  return code;
@@ -360,11 +361,11 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
360
361
  ['vue', 'vue'],
361
362
  ].reduce((acc, [name, entry]) => {
362
363
  acc.push({
363
- find: new RegExp(`^${name.replace(/\//g, '\\/')}$`),
364
+ find: new RegExp(`^${escapeRegExp(name)}$`),
364
365
  replacement: `@poveste/vendors/${entry}`,
365
366
  });
366
367
  acc.push({
367
- find: new RegExp(`^${name.replace(/\//g, '\\/')}\\/`),
368
+ find: new RegExp(`^${escapeRegExp(name)}/`),
368
369
  replacement: `@poveste/vendors/${entry}/`,
369
370
  });
370
371
  return acc;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.14.0",
5
5
  "description": "Interactive component playgrounds for Vue, Nuxt, Svelte, SvelteKit and Quasar — a drop-in fork of histoire, with Quasar support histoire does not ship.",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan",
@@ -73,10 +73,10 @@
73
73
  },
74
74
  "dependencies": {
75
75
  "@akryum/tinypool": "^0.3.1",
76
- "@poveste/app": "^0.13.0",
77
- "@poveste/controls": "^0.13.0",
78
- "@poveste/shared": "^0.13.0",
79
- "@poveste/vendors": "^0.13.0",
76
+ "@poveste/app": "^0.14.0",
77
+ "@poveste/controls": "^0.14.0",
78
+ "@poveste/shared": "^0.14.0",
79
+ "@poveste/vendors": "^0.14.0",
80
80
  "@types/markdown-it": "^14.1.2",
81
81
  "birpc": "^0.2.19",
82
82
  "change-case": "^5.4.4",
@@ -109,7 +109,7 @@
109
109
  "@types/node": "^22.10.1",
110
110
  "typescript": "5.6.3",
111
111
  "vite": "^8.2.0",
112
- "vitest": "^4.1.10"
112
+ "vitest": "^5.0.0"
113
113
  },
114
114
  "scripts": {
115
115
  "build": "rimraf dist && tsc",
package/plugin.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Sends a custom event from the book UI to the plugin's `onDevEvent` hook, and
3
+ * resolves with whatever that hook returns.
4
+ *
5
+ * Development only: the API it calls is installed by the dev server, so in a
6
+ * built book this resolves to `undefined` without erroring.
7
+ */
1
8
  export function sendEvent(event: string, payload?: any): Promise<any>
2
9
 
10
+ /**
11
+ * Navigates the book to a story by its `id` — the same field `getStories()`
12
+ * returns on the plugin API. Development only, and a no-op in a built book.
13
+ */
3
14
  export function openStory(storyId: string): void
package/plugin.js CHANGED
@@ -2,6 +2,11 @@ export function sendEvent(event, payload) {
2
2
  if (window.__HST_PLUGIN_API__) {
3
3
  return window.__HST_PLUGIN_API__.sendEvent(event, payload)
4
4
  }
5
+ // The dev server installs that API and a built book has no plugin host, so
6
+ // this is the ordinary path there. Resolve rather than fall off the end: the
7
+ // signature promises a `Promise`, and returning `undefined` meant
8
+ // `sendEvent(...).then(...)` threw in a built book while `await` was fine.
9
+ return Promise.resolve(undefined)
5
10
  }
6
11
 
7
12
  export function openStory(storyId) {
@@ -1 +0,0 @@
1
- export declare const css = ".__pvt-shade {\n height: 80px;\n border-radius: 4px;\n}\n\n.__pvt-text {\n font-size: 4rem;\n display: flex;\n align-items: flex-end;\n}\n\n.__pvt-border {\n border-style: solid;\n border-width: 2px;\n}\n\n.__pvt-spaces {\n background-color: rgb(113 113 122 / 0.1);\n width: min-content;\n}\n\n.__pvt-margin {\n border: dashed 1px rgb(113 113 122 / 0.5);\n width: min-content;\n}\n\n.__pvt-spaces-box,\n.__pvt-margin-box {\n width: 5rem;\n height: 5rem;\n background-color: rgb(113 113 122 / 0.5);\n}\n\n.__pvt-spaces,\n.__pvt-spaces-box,\n.__pvt-margin,\n.__pvt-margin-box,\n.__pvt-drop-shadow {\n border-radius: 4px;\n}\n\n.__pvt-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.__pvt-drop-shadow {\n width: 8rem;\n height: 8rem;\n background: white;\n margin-bottom: 0.5rem;\n}\n\n.__pvt-drop-shadow {\n background: #4e4e57;\n}\n\n.__pvt-border-radius {\n width: 8rem;\n height: 8rem;\n background-color: rgb(113 113 122 / 0.5);\n}\n\n.__pvt-border-width {\n width: 8rem;\n height: 8rem;\n border-color: rgb(113 113 122 / 0.5);\n background-color: rgb(113 113 122 / 0.1);\n}\n\n.__pvt-width {\n background-color: rgb(113 113 122 / 0.1);\n}\n\n.__pvt-width-box,\n.__pvt-height {\n background-color: rgb(113 113 122 / 0.5);\n}\n\n.__pvt-width-box {\n height: 5rem;\n}";
@@ -1,85 +0,0 @@
1
- // Injected as `style.css` next to the generated story file, so it styles
2
- // the Pinceau token stories without reaching the book's own stylesheet.
3
- export const css = `.__pvt-shade {
4
- height: 80px;
5
- border-radius: 4px;
6
- }
7
-
8
- .__pvt-text {
9
- font-size: 4rem;
10
- display: flex;
11
- align-items: flex-end;
12
- }
13
-
14
- .__pvt-border {
15
- border-style: solid;
16
- border-width: 2px;
17
- }
18
-
19
- .__pvt-spaces {
20
- background-color: rgb(113 113 122 / 0.1);
21
- width: min-content;
22
- }
23
-
24
- .__pvt-margin {
25
- border: dashed 1px rgb(113 113 122 / 0.5);
26
- width: min-content;
27
- }
28
-
29
- .__pvt-spaces-box,
30
- .__pvt-margin-box {
31
- width: 5rem;
32
- height: 5rem;
33
- background-color: rgb(113 113 122 / 0.5);
34
- }
35
-
36
- .__pvt-spaces,
37
- .__pvt-spaces-box,
38
- .__pvt-margin,
39
- .__pvt-margin-box,
40
- .__pvt-drop-shadow {
41
- border-radius: 4px;
42
- }
43
-
44
- .__pvt-truncate {
45
- overflow: hidden;
46
- text-overflow: ellipsis;
47
- white-space: nowrap;
48
- }
49
-
50
- .__pvt-drop-shadow {
51
- width: 8rem;
52
- height: 8rem;
53
- background: white;
54
- margin-bottom: 0.5rem;
55
- }
56
-
57
- .__pvt-drop-shadow {
58
- background: #4e4e57;
59
- }
60
-
61
- .__pvt-border-radius {
62
- width: 8rem;
63
- height: 8rem;
64
- background-color: rgb(113 113 122 / 0.5);
65
- }
66
-
67
- .__pvt-border-width {
68
- width: 8rem;
69
- height: 8rem;
70
- border-color: rgb(113 113 122 / 0.5);
71
- background-color: rgb(113 113 122 / 0.1);
72
- }
73
-
74
- .__pvt-width {
75
- background-color: rgb(113 113 122 / 0.1);
76
- }
77
-
78
- .__pvt-width-box,
79
- .__pvt-height {
80
- background-color: rgb(113 113 122 / 0.5);
81
- }
82
-
83
- .__pvt-width-box {
84
- height: 5rem;
85
- }`;
@@ -1,6 +0,0 @@
1
- import type { Plugin } from '@poveste/shared';
2
- export interface PinceauTokensOptions {
3
- configOrPaths?: string | string[];
4
- configFileName?: string;
5
- }
6
- export declare function pinceauTokens(_options?: PinceauTokensOptions): Plugin;
@@ -1,424 +0,0 @@
1
- import { join } from 'pathe';
2
- import { getInjectedImport } from '../util/vendors.js';
3
- import { css } from './pinceau-tokens-style.js';
4
- export function pinceauTokens(_options = {}) {
5
- const themePath = join(process.cwd(), 'node_modules/.vite/pinceau/index.js');
6
- async function generate(api) {
7
- try {
8
- await api.fs.ensureDir(api.pluginTempDir);
9
- await api.fs.emptyDir(api.pluginTempDir);
10
- api.moduleLoader.clearCache();
11
- await api.fs.writeFile(api.path.resolve(api.pluginTempDir, 'style.css'), css);
12
- const theme = await api.moduleLoader.loadModule(themePath);
13
- const storyFile = api.path.resolve(api.pluginTempDir, 'Pinceau.story.js');
14
- await api.fs.writeFile(storyFile, storyTemplate(theme));
15
- api.addStoryFile(storyFile);
16
- }
17
- catch (e) {
18
- api.error(e.stack ?? e.message);
19
- }
20
- }
21
- return {
22
- name: 'builtin:pinceau-tokens',
23
- config(config) {
24
- // Add 'design-system' group
25
- if (!config.tree) {
26
- config.tree = {};
27
- }
28
- if (!config.tree.groups) {
29
- config.tree.groups = [];
30
- }
31
- if (!config.tree.groups.some(g => g.id === 'design-system')) {
32
- let index = 0;
33
- // After 'top' group
34
- const topIndex = config.tree.groups.findIndex(g => g.id === 'top');
35
- if (topIndex > -1) {
36
- index = topIndex + 1;
37
- }
38
- // Insert group
39
- config.tree.groups.splice(index, 0, {
40
- id: 'design-system',
41
- title: 'Design System',
42
- });
43
- }
44
- },
45
- async onDev(api, onCleanup) {
46
- await generate(api);
47
- const watcher = api.watcher.watch(themePath)
48
- .on('change', () => generate(api))
49
- .on('add', () => generate(api));
50
- onCleanup(() => {
51
- watcher.close();
52
- });
53
- },
54
- async onBuild(api) {
55
- await generate(api);
56
- },
57
- };
58
- }
59
- function storyTemplate(pinceauConfig) {
60
- pinceauConfig = pinceauConfig?.theme || pinceauConfig?.default?.theme || {};
61
- // No `import 'poveste-style'` — see the tailwind plugin's template (#219).
62
- return `import './style.css'
63
- import { createApp, h, markRaw, ref } from ${getInjectedImport('@poveste/vendors/vue')}
64
- import {
65
- HstColorShades,
66
- HstTokenList,
67
- HstTokenGrid,
68
- HstText,
69
- HstTextarea,
70
- HstNumber,
71
- } from ${getInjectedImport('@poveste/controls')}
72
-
73
- const config = markRaw(${JSON.stringify(pinceauConfig, null, 2)})
74
- const search = ref('')
75
- const sampleText = ref('Cat sit like bread eat prawns daintily with a claw then lick paws clean wash down prawns with a lap of carnation milk then retire to the warmest spot on the couch to claw at the fabric before taking a catnap mrow cat cat moo moo lick ears lick paws')
76
- const fontSize = ref(16)
77
- const flattenTokens = (data, transformValue = (value) => (value?.value || value)) => {
78
- const output = {}
79
- function step(obj, prev) {
80
- Object.keys(obj).forEach((key) => {
81
- const value = obj[key]
82
- const isarray = Array.isArray(value)
83
- const type = Object.prototype.toString.call(value)
84
- const isobject
85
- = type === '[object Object]'
86
- || type === '[object Array]'
87
-
88
- const newKey = prev ? prev + '.' + key : key
89
-
90
- if (value.value && !output[newKey]) {
91
- output[newKey] = transformValue(value)
92
- }
93
-
94
- if (!isarray && isobject && Object.keys(value).length) { return step(value, newKey) }
95
- })
96
- }
97
- step(data)
98
- return output
99
- }
100
-
101
- function mountApp ({ el, state, onUnmount }, render) {
102
- Object.assign(state, {
103
- search,
104
- sampleText,
105
- fontSize,
106
- })
107
-
108
- const app = createApp({
109
- render,
110
- })
111
- app.mount(el)
112
-
113
- onUnmount(() => {
114
- app.unmount()
115
- })
116
- }
117
-
118
- /*
119
- media?: PinceauTokens
120
- colors?: ScaleTokens
121
- fonts?: PinceauTokens
122
- fontWeights?: FontWeightTokens
123
- fontSizes?: BreakpointsTokens
124
- size?: BreakpointsTokens
125
- space?: BreakpointsTokens
126
- radii?: BreakpointsTokens
127
- borders?: BreakpointsTokens
128
- borderWidths?: BreakpointsTokens
129
- borderStyles?: BreakpointsTokens
130
- shadows?: BreakpointsTokens
131
- opacity?: BreakpointsTokens
132
- leads?: BreakpointsTokens
133
- letterSpacings?: BreakpointsTokens
134
- transitions?: PinceauTokens
135
- zIndices?: PinceauTokens
136
- */
137
-
138
- export default {
139
- id: 'pinceau',
140
- title: 'Pinceau',
141
- group: 'design-system',
142
- icon: 'noto:paintbrush',
143
- responsiveDisabled: true,
144
- layout: { type: 'single', iframe: false },
145
- variants: [
146
- {
147
- id: 'media-queries',
148
- title: 'Media Queries',
149
- icon: 'carbon:screen',
150
- onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
151
- tokens: flattenTokens(config.media),
152
- getName: key => '@mq.' + key,
153
- colSize: 180,
154
- }, ({ token }) => h('div', {
155
- class: '__pvt-drop-shadow',
156
- }))),
157
- },
158
- {
159
- id: 'colors',
160
- title: 'Colors',
161
- icon: 'carbon:color-palette',
162
- onMount: (api) => mountApp(api, () => Object.entries(config.colors).map(([key, shades]) => h(HstColorShades, {
163
- key,
164
- shades: shades.value ? { DEFAULT: shades.value } : flattenTokens(shades),
165
- getName: shade => '{colors.' + key + '.' + shade + '}',
166
- search: search.value,
167
- }, ({ color}) => h('div', {
168
- class: '__pvt-shade',
169
- style: {
170
- backgroundColor: color,
171
- },
172
- })))),
173
- onMountControls: (api) => mountApp(api, () => [
174
- h(HstText, {
175
- title: 'Filter...',
176
- modelValue: search.value,
177
- 'onUpdate:modelValue': value => { search.value = value },
178
- }),
179
- ]),
180
- },
181
- // SPACES
182
- {
183
- id: 'Spaces',
184
- title: 'Spaces',
185
- icon: 'carbon:area',
186
- onMount: (api) => mountApp(api, () => h(HstTokenList, {
187
- tokens: flattenTokens(config.space),
188
- getName: key => '{space.' + key + '}',
189
- }, ({ token }) => h('div', {
190
- class: '__pvt-spaces',
191
- style: {
192
- padding: token.value,
193
- },
194
- }, [
195
- h('div', {
196
- class: '__pvt-spaces-box',
197
- }),
198
- ]))),
199
- },
200
- // SIZES
201
- {
202
- id: 'sizes',
203
- title: 'Sizes',
204
- icon: 'carbon:pan-horizontal',
205
- onMount: (api) => mountApp(api, () => h(HstTokenList, {
206
- tokens: flattenTokens(config.size),
207
- getName: key => '{size.' + key + '}',
208
- }, ({ token }) => h('div', {
209
- class: '__pvt-width',
210
- }, [
211
- h('div', {
212
- class: '__pvt-width-box',
213
- style: {
214
- width: token.value,
215
- },
216
- }),
217
- ]))),
218
- },
219
- // FONTSIZES
220
- {
221
- id: 'font-size',
222
- title: 'Font Size',
223
- icon: 'carbon:text-font',
224
- onMount: (api) => mountApp(api, () => h(HstTokenList, {
225
- tokens: flattenTokens(config.fontSizes),
226
- getName: key => '{fontSizes.' + key + '}',
227
- }, ({ token }) => h('div', {
228
- class: '__pvt-truncate',
229
- style: {
230
- fontSize: token.value,
231
- },
232
- }, sampleText.value))),
233
- onMountControls: (api) => mountApp(api, () => [
234
- h(HstTextarea, {
235
- title: 'Sample text',
236
- modelValue: sampleText.value,
237
- 'onUpdate:modelValue': value => { sampleText.value = value },
238
- rows: 5,
239
- }),
240
- ]),
241
- },
242
- // FONTWEIGHTS
243
- {
244
- id: 'font-weight',
245
- title: 'Font Weight',
246
- icon: 'carbon:text-font',
247
- onMount: (api) => mountApp(api, () => h(HstTokenList, {
248
- tokens: flattenTokens(config.fontWeights),
249
- getName: key => '{fontWeights.' + key + '}',
250
- }, ({ token }) => h('div', {
251
- class: '__pvt-truncate',
252
- style: {
253
- fontWeight: token.value,
254
- fontSize: \`\${fontSize.value}px\`,
255
- },
256
- }, sampleText.value))),
257
- onMountControls: (api) => mountApp(api, () => [
258
- h(HstTextarea, {
259
- title: 'Sample text',
260
- modelValue: sampleText.value,
261
- 'onUpdate:modelValue': value => { sampleText.value = value },
262
- rows: 5,
263
- }),
264
- h(HstNumber, {
265
- title: 'Font size',
266
- modelValue: fontSize.value,
267
- 'onUpdate:modelValue': value => { fontSize.value = value },
268
- min: 1,
269
- }),
270
- ]),
271
- },
272
- // FONTS
273
- {
274
- id: 'fonts',
275
- title: 'Fonts',
276
- icon: 'carbon:text-font',
277
- onMount: (api) => mountApp(api, () => h(HstTokenList, {
278
- tokens: flattenTokens(config.fonts),
279
- getName: key => '{fonts.' + key + '}',
280
- }, ({ token }) => h('div', {
281
- class: '__pvt-truncate',
282
- style: {
283
- fontFamily: token.value,
284
- fontSize: \`\${fontSize.value}px\`,
285
- },
286
- }, sampleText.value))),
287
- onMountControls: (api) => mountApp(api, () => [
288
- h(HstTextarea, {
289
- title: 'Sample text',
290
- modelValue: sampleText.value,
291
- 'onUpdate:modelValue': value => { sampleText.value = value },
292
- rows: 5,
293
- }),
294
- h(HstNumber, {
295
- title: 'Font size',
296
- modelValue: fontSize.value,
297
- 'onUpdate:modelValue': value => { fontSize.value = value },
298
- min: 1,
299
- }),
300
- ]),
301
- },
302
- // LETTERSPACINGS
303
- {
304
- id: 'letter-spacing',
305
- title: 'Letter Spacing',
306
- icon: 'carbon:text-font',
307
- onMount: (api) => mountApp(api, () => h(HstTokenList, {
308
- tokens: flattenTokens(config.letterSpacings),
309
- getName: key => '{letterSpacings.' + key + '}',
310
- }, ({ token }) => h('div', {
311
- class: '__pvt-truncate',
312
- style: {
313
- letterSpacing: token.value,
314
- fontSize: \`\${fontSize.value}px\`,
315
- },
316
- }, sampleText.value))),
317
- onMountControls: (api) => mountApp(api, () => [
318
- h(HstTextarea, {
319
- title: 'Sample text',
320
- modelValue: sampleText.value,
321
- 'onUpdate:modelValue': value => { sampleText.value = value },
322
- rows: 5,
323
- }),
324
- h(HstNumber, {
325
- title: 'Font size',
326
- modelValue: fontSize.value,
327
- 'onUpdate:modelValue': value => { fontSize.value = value },
328
- min: 1,
329
- }),
330
- ]),
331
- },
332
- // LEADS
333
- {
334
- id: 'leads',
335
- title: 'Leads',
336
- icon: 'carbon:text-font',
337
- onMount: (api) => mountApp(api, () => h(HstTokenList, {
338
- tokens: flattenTokens(config.leads),
339
- getName: key => '{leads.' + key + '}',
340
- }, ({ token }) => h('div', {
341
- style: {
342
- lineHeight: token.value,
343
- },
344
- }, sampleText.value))),
345
- onMountControls: (api) => mountApp(api, () => [
346
- h(HstTextarea, {
347
- title: 'Sample text',
348
- modelValue: sampleText.value,
349
- 'onUpdate:modelValue': value => { sampleText.value = value },
350
- rows: 5,
351
- }),
352
- // @TODO select font size
353
- ]),
354
- },
355
- // SHADOWS
356
- {
357
- id: 'shadows',
358
- title: 'Shadows',
359
- icon: 'carbon:shape-except',
360
- onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
361
- tokens: flattenTokens(config.shadows),
362
- getName: key => '{shadows.' + key + '}',
363
- colSize: 180,
364
- }, ({ token }) => h('div', {
365
- class: '__pvt-drop-shadow',
366
- style: 'box-shadow:' + token.value,
367
- }))),
368
- },
369
- // OPACITIES
370
- {
371
- id: 'opacities',
372
- title: 'Opacities',
373
- icon: 'carbon:bring-forward',
374
- onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
375
- tokens: flattenTokens(config.opacity),
376
- getName: key => '{opacity.' + key + '}',
377
- colSize: 180,
378
- }, ({ token }) => h('div', {
379
- class: '__pvt-drop-shadow',
380
- style: 'opacity:' + token.value,
381
- }))),
382
- },
383
- // RADII
384
- {
385
- id: 'border-radius',
386
- title: 'Border Radius',
387
- icon: 'carbon:condition-wait-point',
388
- onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
389
- tokens: flattenTokens(config.radii),
390
- getName: key => '{radii.' + key + '}',
391
- colSize: 180,
392
- }, ({ token }) => h('div', {
393
- class: '__pvt-border-radius',
394
- style: {
395
- borderRadius: token.value,
396
- },
397
- }))),
398
- },
399
- // BORDERWIDTHS
400
- {
401
- id: 'border-width',
402
- title: 'Border Width',
403
- icon: 'carbon:checkbox',
404
- onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
405
- tokens: flattenTokens(config.borderWidths),
406
- getName: key => '{borderWidths.' + key + '}',
407
- colSize: 180,
408
- }, ({ token }) => h('div', {
409
- class: '__pvt-border-width',
410
- style: {
411
- borderWidth: token.value,
412
- },
413
- }))),
414
- },
415
- // FULL CONFIG
416
- {
417
- id: 'full-config',
418
- title: 'Full Config',
419
- icon: 'carbon:code',
420
- onMount: (api) => mountApp(api, () => h('pre', JSON.stringify(config, null, 2))),
421
- },
422
- ],
423
- }`;
424
- }