diff --git a/package-lock.json b/package-lock.json index dd3db5c..15323f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@codebar-ag/storybook", - "version": "1.8.0", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@codebar-ag/storybook", - "version": "1.8.0", + "version": "1.9.0", "license": "MIT", "dependencies": { "@fontsource/jetbrains-mono": "^5.3.0", @@ -40,6 +40,13 @@ "vue-tsc": "^3.3.7" }, "peerDependencies": { + "@codemirror/commands": "^6.10.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/language": "^6.12.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.43.0", + "apexcharts": "^4.5.0 || ^5.0.0", "tailwindcss": "^4.0.0", "vue": "^3.5.0" }, diff --git a/package.json b/package.json index cd263bd..675fbcc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@codebar-ag/storybook", - "version": "1.8.0", + "version": "1.9.0", "description": "codebar-ag DocuHub — shared Vue 3 + Tailwind v4 design-system atoms and tokens, documented in Storybook.", "license": "MIT", "author": "codebar Solutions AG", @@ -25,7 +25,8 @@ "scripts": { "prepare": "npm run build", "dev": "storybook dev -p 6006", - "build": "vite build && npm run build:tokens", + "build": "vite build && npm run build:tokens && npm run verify:externals", + "verify:externals": "node scripts/verify-externals.mjs", "build:tokens": "node -e \"require('node:fs').copyFileSync('src/tokens.css','dist/tokens.css')\"", "build-storybook": "storybook build", "lint": "eslint \"src/**/*.{ts,vue}\"", @@ -39,7 +40,14 @@ }, "peerDependencies": { "tailwindcss": "^4.0.0", - "vue": "^3.5.0" + "vue": "^3.5.0", + "apexcharts": "^4.5.0 || ^5.0.0", + "@codemirror/commands": "^6.10.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/language": "^6.12.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.43.0" }, "peerDependenciesMeta": { "apexcharts": { diff --git a/scripts/verify-externals.mjs b/scripts/verify-externals.mjs new file mode 100644 index 0000000..f3273b0 --- /dev/null +++ b/scripts/verify-externals.mjs @@ -0,0 +1,39 @@ +// Guards the library build's externalisation contract, which nothing else can. +// +// Storybook and its Playwright suite compile from `src`, so they always see a +// single copy of every dependency — they cannot observe what the PUBLISHED +// bundle does. If a peer package is missing from `rollupOptions.external`, +// Rollup quietly inlines it into an extra chunk and the consumer ends up with +// two instances of it. For `@codemirror/language` that means the parser +// registers its syntax tree against one set of facets while +// `syntaxHighlighting()` reads the other, and every code surface in every +// consuming app renders as flat, unhighlighted text — with no error anywhere. +// +// The observable symptom in `dist` is an extra chunk file plus a relative +// import out of `flows.js`, so both are asserted here. +import { readdirSync, readFileSync } from 'node:fs'; + +const EXPECTED_FILES = ['flows.css', 'flows.js', 'index.d.ts', 'tokens.css']; + +const actual = readdirSync('dist').sort(); +const unexpected = actual.filter((file) => !EXPECTED_FILES.includes(file)); + +if (unexpected.length > 0) { + console.error( + `dist/ has unexpected chunk(s): ${unexpected.join(', ')}\n` + + 'A dependency was bundled instead of externalised. Add it to ' + + "`rollupOptions.external` in vite.config.ts (and to `peerDependencies`).", + ); + process.exit(1); +} + +const bundle = readFileSync('dist/flows.js', 'utf8'); +const relativeImports = [...bundle.matchAll(/(?:from|import\()\s*["'](\.[^"']*)["']/g)].map((match) => match[1]); + +if (relativeImports.length > 0) { + console.error( + `dist/flows.js imports emitted chunk(s): ${[...new Set(relativeImports)].join(', ')}\n` + + 'Every dependency must resolve to a bare specifier so the consuming app supplies one copy.', + ); + process.exit(1); +} diff --git a/src/components/organisms/CodeEditor.stories.ts b/src/components/organisms/CodeEditor.stories.ts index 968cb02..29f1ded 100644 --- a/src/components/organisms/CodeEditor.stories.ts +++ b/src/components/organisms/CodeEditor.stories.ts @@ -1,6 +1,35 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'; +import { expect, waitFor } from 'storybook/test'; import CodeEditor from './CodeEditor.vue'; +/** + * Asserts the document is actually SYNTAX HIGHLIGHTED, not merely rendered. + * + * This guards a failure mode with no error signal: if the library build ever + * bundles `@codemirror/language` instead of externalising it, the consumer + * loads two copies of it, the language's syntax tree registers against one + * set of facets and `syntaxHighlighting()` reads the other, and every editor + * silently renders as flat monochrome text. + * + * The check is "some token is painted a colour other than the body text's", + * not "tokens use more than one colour between them": a short JSON document + * may legitimately contain only one *styled* tag kind (`defaultHighlightStyle` + * leaves plain `propertyName` uncoloured), which would make a colour-diversity + * assertion fail on working code. + */ +async function expectHighlighted(canvasElement: HTMLElement): Promise { + await waitFor(async () => { + const content = canvasElement.querySelector('.cm-content'); + await expect(content).not.toBeNull(); + + const tokens = canvasElement.querySelectorAll('.cm-line span'); + await expect(tokens.length).toBeGreaterThan(0); + + const base = getComputedStyle(content as Element).color; + await expect([...tokens].some((token) => getComputedStyle(token).color !== base)).toBe(true); + }); +} + const meta: Meta = { title: 'Organisms/CodeEditor', component: CodeEditor, @@ -19,13 +48,16 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Json: Story = {}; +export const Json: Story = { + play: ({ canvasElement }) => expectHighlighted(canvasElement), +}; export const Markdown: Story = { args: { - modelValue: '# Extraction prompt\n\nSummarize the invoice fields below.', + modelValue: '# Extraction prompt\n\nSummarize the **invoice** fields below.', language: 'markdown', }, + play: ({ canvasElement }) => expectHighlighted(canvasElement), }; export const ReadOnlyEmpty: Story = { @@ -38,3 +70,7 @@ export const ReadOnlyEmpty: Story = { export const AutoHeight: Story = { args: { autoHeight: true, maxHeight: '12rem' }, }; + +export const Copyable: Story = { + args: { copyable: true, readonly: true }, +}; diff --git a/src/components/organisms/CodeEditor.vue b/src/components/organisms/CodeEditor.vue index 7a8fd4e..2fb8e60 100644 --- a/src/components/organisms/CodeEditor.vue +++ b/src/components/organisms/CodeEditor.vue @@ -1,7 +1,8 @@