Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,155 @@ All notable changes to `@codebar-ag/storybook`.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v1.19.0

Four findings from the app that adopted 1.18.0, three of them acted on and one
declined. One is a rendering bug that had been shipping for the life of the
component: `CodeEditor` had no `yaml` mode and highlighted YAML as JSON without
saying so.

**This release adds a peer dependency.** `@codemirror/lang-yaml` — see
[Upgrade notes](#upgrade-notes). Nothing else here requires a change to a
call site.

### Added

- **`CodeEditor` and `CodePreview` accept `language="yaml"`**, parsed by
`@codemirror/lang-yaml` and highlighted through the same shared theme as
every other code surface in the kit.

The editor previously loaded the JSON grammar for **anything that was not
`markdown`**, so a consuming app's compiled-flow-definition page had been
highlighting YAML through a JSON parser since the day it was written. The
reason nobody noticed is worth recording, because it is what makes this class
of bug expensive: the wrong grammar does not fail loudly on YAML. Run the
JSON parser over a typical flow definition and it still paints nine tokens —
every `:` as punctuation, every bare integer as a number — which reads as
syntax highlighting from across the room. What it cannot paint is `#`, a
comment in YAML and nothing at all in JSON. The `Yaml` story asserts on that
line specifically, and fails if the document is served the JSON grammar; a
"some token is coloured" assertion passes on the broken version.

`CodePreview` gains the mode too. A kit that can edit YAML but only preview
it as plain text is a difference no caller can explain.

- **An unknown `language` now renders unhighlighted and says so**, instead of
silently falling back to JSON. Both components warn once per unknown value in
development, through the same `warnOnce` path as every other dev diagnostic
here. A component that quietly picks the wrong grammar is worse than one that
refuses the mode: the first is indistinguishable from working.

`language` is a typed union, so the only callers who can reach that arm are
untyped ones — which is exactly who has no compiler to tell them.

- **`DataTable` warns in development when `rowKey` is missing.** It has always
been a required prop with no default, and until now nothing said so at
runtime: nine call sites in one consuming app shipped without it, so every
row in those tables was keyed `undefined` and Vue could not tell any row from
any other.

Vue's own "Missing required prop" warning cannot fire for **any** prop in
this package, which is why the years passed quietly. The library is compiled
with `isProd: true`, and `@vue/compiler-sfc` emits `type` and `required` only
for a development build — the published bundle declares the prop as the bare
`rowKey: {}`. Rendering `<DataTable>` with no `rowKey` against a development
build of Vue produces no Vue warning at all. This is the same shape as the
`import.meta.env.DEV` problem documented in `src/helpers/dev.ts`: a
diagnostic that reads as working and is compiled away before it reaches
anyone. The check is therefore written out by hand, like the tone
deprecation, and verified against the built bundle rather than against `src`.

Every other required prop in this kit is unvalidated at runtime for the same
reason. Only `rowKey` is covered here, because only `rowKey` has evidence
attached; a general fix means compiling the library in development mode, and
that is its own release.

### Changed

- **`CodeEditorProps.modelValue` is `string | null`.** The runtime has always
accepted null — `?? ''` guards every read of it, and an empty document is
what it renders — but the type said `string`, so callers holding a nullable
column (a definition that has not been compiled yet, an optional description)
coerced with `?? ''` at the call site for a default the component already
applies. Same shape as `BreadcrumbItem.href` in 1.18.0: the type was narrower
than the behaviour.

Deliberately **not** widened to `string | number | null`, which is what
`Input` takes. `Input` is a native `<input>` whose `value` the DOM stringifies
anyway; `CodeEditor` holds a *document*, and a number would have to be
silently stringified into one on the way in and handed back as a string on
the way out. `Textarea` — the multi-line text sibling, and the closer
analogue — is `string | null` for the same reason.

### Not changed: `DataTableColumn.key`

The fourth finding asked for `key: keyof T & string`, so the `#cell-<key>` slot
could type `value` as `T[K]` instead of `unknown`. Declined, on three grounds.

**It would forbid a column that is not a row property.** An action or computed
column — `{ key: 'actions', label: '' }` rendered through `#cell-actions` — is
a real and supported pattern, and the kit ships no other way to put one
anywhere but the trailing `#row-actions` cell.

**`SortState.key` is a free string on purpose.** It is emitted to the caller
and, in server mode, straight on to an API. Sort keys naming a joined or
computed column that is not in the row DTO are ordinary. Narrowing the column
key without narrowing that leaves the two disagreeing.

**And for most row types it would buy nothing at all.** `DataTable` is
`<T extends Record<string, unknown>>`. An *interface* only satisfies that
constraint by declaring the index signature — which is what this package's own
`DataTable` story does, and what a consuming app's row types have to do — and
once `[key: string]: unknown` is present, `keyof T & string` **is** `string` and
`T[K]` **is** `unknown`. The proposed narrowing is the identity function on
exactly the row shapes it was proposed for. It would only bite for rows
declared as type aliases, which get an implicit index signature and keep their
exact keys.

That last point locates the real obstacle: it is the `Record<string, unknown>`
constraint, not `key`. Relaxing it (to `T extends object`, with the internal
indexing and the `useSort` signature adjusted to match) is what would make an
exact `value` possible, and it is strictly more permissive, so it would break
nobody. It is also a much larger change than a type narrowing, and it is not in
this release.

Eleven consumer bindings moved from `{ value }` to `{ row }` in the meantime,
which is better code regardless: `row.name` is exact today, under both row
shapes, with no change to this package.

### Upgrade notes

**Install `@codemirror/lang-yaml`.**

```bash
npm install --save-dev @codemirror/lang-yaml
```

It is declared optional in `peerDependenciesMeta`, in step with every other
`@codemirror/*` grammar here, but "optional" describes the manifest and not
what a bundler does. `dist/flows.js` is a single file that dynamic-imports
every grammar by bare specifier, so a consuming build resolves all of them
whether or not the app ever renders an editor — which is already true of
`lang-json`, `lang-markdown`, `commands`, `language`, `state` and `view` today,
and is why every existing consumer already has that set installed. This release
adds one more package to it. An app that upgrades without installing it will
fail to resolve the import at build time, not at runtime.

Two smaller things can change behaviour, both only for code that was already
outside the declared types:

- **An unknown `language` no longer highlights as JSON.** If an untyped call
site was passing something the union does not contain and was, by accident,
getting the JSON grammar, it now gets no grammar and a development warning.
A call site that was passing `"yaml"` and getting JSON gets YAML.
- **`CodeEditorProps['modelValue']` includes `null`.** Reading it out into a
`string` needs a fallback. Passing values *in* is strictly freer.

`DataTable`'s new warning is development-only and fires once per page, but an
app with tables missing `rowKey` will start seeing it immediately. It is
reporting a real defect in those tables — the rows have no distinct keys — and
not a new requirement.

## v1.18.0

A types-only release, prompted by a consuming app that stood up a `vue-tsc`
Expand Down
33 changes: 31 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codebar-ag/storybook",
"version": "1.18.0",
"version": "1.19.0",
"description": "codebar-ag DocuHub — shared Vue 3 + Tailwind v4 design-system atoms and tokens, documented in Storybook.",
"license": "MIT",
"author": "codebar Solutions AG",
Expand Down Expand Up @@ -42,15 +42,16 @@
"tailwind-merge": "^3.0.0"
},
"peerDependencies": {
"tailwindcss": "^4.0.0",
"vue": "^3.5.0",
"apexcharts": "^4.5.0 || ^5.0.0 || ^6.0.0",
"@codemirror/commands": "^6.10.0",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-yaml": "^6.1.0",
"@codemirror/language": "^6.12.0",
"@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.43.0"
"@codemirror/view": "^6.43.0",
"apexcharts": "^4.5.0 || ^5.0.0 || ^6.0.0",
"tailwindcss": "^4.0.0",
"vue": "^3.5.0"
},
"peerDependenciesMeta": {
"apexcharts": {
Expand All @@ -68,6 +69,9 @@
"@codemirror/lang-markdown": {
"optional": true
},
"@codemirror/lang-yaml": {
"optional": true
},
"@codemirror/commands": {
"optional": true
},
Expand All @@ -79,6 +83,7 @@
"@codemirror/commands": "^6.10.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.43.4",
Expand Down
90 changes: 90 additions & 0 deletions src/components/organisms/CodeEditor.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,42 @@ async function expectHighlighted(canvasElement: HTMLElement): Promise<void> {
});
}

/**
* The exact inverse, for the unknown-language fallback: the document renders,
* and nothing in it is painted.
*
* Measured in colour rather than by counting `<span>`s on purpose — CodeMirror
* emits spans of its own for line decorations, so a zero-span assertion would
* be testing something else. Colour is what a grammar buys, so colour is what
* its absence has to be read from.
*/
async function expectNotHighlighted(canvasElement: HTMLElement): Promise<void> {
// Wait for the document first: an editor that has not mounted yet has no
// coloured tokens either, and would pass this vacuously.
await waitFor(async () => {
await expect(canvasElement.querySelector('.cm-line')?.textContent ?? '').not.toBe('');
});

const content = canvasElement.querySelector('.cm-content') as Element;
const base = getComputedStyle(content).color;
const tokens = [...canvasElement.querySelectorAll('.cm-line span')];

await expect(tokens.every((token) => getComputedStyle(token).color === base)).toBe(true);
}

const sampleYaml = `# Compiled flow definition
name: invoice-intake
version: 3
steps:
- id: ocr
uses: mistral-document-ai
with:
max_page_count: 50
- id: extract
uses: llm
prompt: "Extract vendor, invoice number and total."
`;

const meta: Meta<typeof CodeEditor> = {
title: 'Organisms/CodeEditor',
component: CodeEditor,
Expand Down Expand Up @@ -60,6 +96,60 @@ export const Markdown: Story = {
play: ({ canvasElement }) => expectHighlighted(canvasElement),
};

/**
* YAML is the mode this editor spent its whole life without. Anything that was
* not `markdown` loaded the JSON grammar, so a consuming app's compiled
* flow-definition page highlighted YAML as JSON from the day it was written.
*
* The assertion is the comment line specifically, and `expectHighlighted` would
* not have done. Run the JSON grammar over the document below and it still
* paints nine tokens — every `:` as punctuation, `3` and `50` as numbers —
* which is both why "some token is coloured" passes on the broken version and
* why nobody noticed: the wrong grammar produced plausible colour rather than
* visible breakage. `#` opens a comment in YAML and nothing at all in JSON, so
* a painted first line is reachable only through `@codemirror/lang-yaml`.
*/
async function expectYamlGrammar(canvasElement: HTMLElement): Promise<void> {
await waitFor(async () => {
const content = canvasElement.querySelector('.cm-content');
await expect(content).not.toBeNull();

const commentLine = [...canvasElement.querySelectorAll('.cm-line')].find((line) =>
line.textContent?.startsWith('#'),
);
await expect(commentLine).not.toBeUndefined();

const base = getComputedStyle(content as Element).color;
const painted = [...(commentLine as Element).querySelectorAll('span')].some(
(token) => getComputedStyle(token).color !== base,
);
await expect(painted).toBe(true);
});
}

export const Yaml: Story = {
args: { modelValue: sampleYaml, language: 'yaml' },
play: ({ canvasElement }) => expectYamlGrammar(canvasElement),
};

/**
* A language the editor does not know now renders unhighlighted and warns in
* development, instead of silently reaching for JSON.
*
* The value is passed from a template string, which is how it reaches a
* component in practice: `language` is a typed union, so the only callers who
* can get here are untyped ones — and they are exactly the ones with no
* compiler to tell them.
*/
export const UnknownLanguage: Story = {
render: () => ({
components: { CodeEditor },
setup: () => ({ sampleYaml }),
template: '<div class="h-64"><CodeEditor :model-value="sampleYaml" language="toml" /></div>',
}),
play: ({ canvasElement }) => expectNotHighlighted(canvasElement),
};

export const ReadOnlyEmpty: Story = {
render: () => ({
components: { CodeEditor },
Expand Down
Loading
Loading