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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair by claude[bot] · Pull Request #6971 · objectstack-ai/objectui · GitHub
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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair by claude[bot] · Pull Request #6971 · objectstack-ai/objectui · GitHub
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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair by claude[bot] · Pull Request #6971 · objectstack-ai/objectui · GitHub
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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair by claude[bot] · Pull Request #6971 · objectstack-ai/objectui · GitHub
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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair by claude[bot] · Pull Request #6971 · objectstack-ai/objectui · GitHub
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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair by claude[bot] · Pull Request #6971 · objectstack-ai/objectui · GitHub
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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(types,plugin-markdown,plugin-kanban): one authority for `MarkdownSchema` and the in-package Kanban pair by claude[bot] · Pull Request #6971 · objectstack-ai/objectui · GitHub
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
50 changes: 50 additions & 0 deletions .changeset/6172-markdown-kanban-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@object-ui/plugin-markdown': minor
'@object-ui/plugin-kanban': minor
---

One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn`
inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155).

The 2026-08-25 family ruling: every exported schema name has exactly one
authority. Two of this card's names are discharged here.

**`MarkdownSchema` — converged onto `@object-ui/types`.**
`@object-ui/plugin-markdown` declared a second copy of the name. The two
differed on exactly one member — `content`, required in `@object-ui/types` and
optional in the plugin — and that was measured to be drift rather than a real
semantic difference: the plugin's own registration declares the `content` input
`required: true` (pinned by its own test), `MarkdownImplProps.content` is a
non-optional `string`, the Zod mirror spells `z.string()`, and every authored
`type: 'markdown'` node in the repository supplies `content`. The plugin now
re-exports the one authority.

⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown`
consumers**: `MarkdownSchema['content']` goes from optional to **required**. A
value annotated `MarkdownSchema` that omitted `content` no longer type-checks.
Measured against this repository: zero authored markdown nodes omit it, so
nothing in-tree changed. (`type: 'markdown'` literals that carry no `content`
are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.)
The plugin's face also gains the optional `sanitize` and `components` members
the canonical declaration carries; both are additive, and neither is read by
this renderer, which sanitizes unconditionally.

`className` is unaffected — it comes from `BaseSchema`, which both copies
extended, so it was always inherited rather than added by the plugin.

**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to
one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A
TypeScript-AST comparison found them strict-SUBSET copies of `./types` with
nothing typed differently, so their extra members moved onto the one
declaration and both files now re-point at it.

Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and
`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every
value that type-checked before still does. Both modules keep their previous
export surface via re-export, so no import path changes.

The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision
between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here
and is escalated on objectui#6172 — those are two different dialects (`items`
vs `cards`, `labels` vs `badges`), and collapsing them renames a published
name, which needs an authority ruling.
21 changes: 14 additions & 7 deletions content/docs/plugins/plugin-markdown.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,23 +50,30 @@ const schema = {
```plaintext
{
type: 'markdown',
content?: string, // Markdown content
content: string, // Markdown content (required)
className?: string // Tailwind classes
}
```

### MarkdownSchema

Declared by `@object-ui/plugin-markdown`
(`packages/plugin-markdown/src/types.ts`) — the plugin ships its own copy of
this name, so read it there rather than the same-named interface in
`@object-ui/types`. It extends `BaseSchema`, so the shared component properties
are available on a `markdown` node as well as the ones below.
Declared by `@object-ui/types` (`packages/types/src/data-display.ts`) and
re-exported by `@object-ui/plugin-markdown`, so either import spelling resolves
to the same type. The plugin used to ship a second declaration of this name;
the two have been converged onto the one authority (objectui#6172). It extends
`BaseSchema`, so the shared component properties are available on a `markdown`
node as well as the ones below.

`content` is **required**: the plugin registers it as a required input, the
renderer's own props type takes a non-optional `string`, and the Zod mirror
(`packages/types/src/zod/data-display.zod.ts`) validates it as `z.string()`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `content` | string | `''` | Markdown content to render |
| `content` | string | — (required) | Markdown content to render |
| `className` | string | `''` | Additional Tailwind CSS classes |
| `sanitize` | boolean | `true` | Declared on the schema; the renderer sanitizes unconditionally |
| `components` | Record | — | Declared on the schema; not read by this renderer |

## Supported Markdown Features

Expand Down
29 changes: 7 additions & 22 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,32 +29,17 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Butto
import { resolveConditionalFormatting } from "@object-ui/core"
import { usePredicateScope } from "@object-ui/react"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"

const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')

export interface KanbanCard {
id: string
title: string
description?: string
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
collapsed?: boolean
}
// One authority for these two names in this package: `./types` (objectui#6172 / #6155).
// This file's former local copies added `coverImage` (card) and `collapsed`
// (column); both now live on the canonical declaration as optional members, so
// this module sees exactly the shape it declared before. The re-export
// preserves the export surface; it is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
46 changes: 9 additions & 37 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Scrol
import { useHasDndProvider, useDnd, usePredicateScope } from "@object-ui/react"
import { resolveConditionalFormatting } from "@object-ui/core"
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
import type { KanbanCard, KanbanColumn } from './types'
import { createSafeTranslation } from "@object-ui/i18n"
import { Plus } from "lucide-react"

Expand All@@ -48,43 +49,14 @@ const useKanbanT = createSafeTranslation(

const UNCATEGORIZED_LANE = 'Uncategorized'

export interface KanbanCard {
id: string
title: string
description?: string
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*/
cardSubtitle?: string
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>
/**
* `colorStyle` carries the CSS custom properties a hex-derived `colorClass`
* reads (objectui#5183) — see `KanbanCard.badges` in `./types` for how to
* derive the pair.
*/
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline"; colorClass?: string; colorStyle?: React.CSSProperties }>
coverImage?: string
[key: string]: any
}

export interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number
className?: string
}
// `KanbanCard` / `KanbanColumn` have ONE authority in this package: `./types`
// (objectui#6172 / #6155). This file used to redeclare both, and the copies had
// drifted — the local `KanbanCard` carried `cardSubtitle` / `cardFieldCells` /
// `coverImage` that `./types` did not. Those members moved to the canonical
// declaration (all optional, so nothing that type-checked before stopped), and
// the re-export below keeps this module's export surface byte-for-byte what it
// was for any importer. A re-export is not a second declaration.
export type { KanbanCard, KanbanColumn } from './types'

// Card formatting accepts the native `{ field, operator, value }` shape and the
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-kanban/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,29 @@ export interface KanbanCard {
*/
colorStyle?: React.CSSProperties;
}>;
/**
* Synthesized card subtitle (e.g. "Account: Acme · Amount: $150K"). Rendered
* in preference to `description` so we don't have to overwrite the record's
* real `description` field — which would corrupt detail-view and edit-form
* displays once a card is opened.
*
* Read by `KanbanImpl`; absent on a board that renders plain descriptions.
*/
cardSubtitle?: string;
/**
* Structured per-field cells. When provided, the card body renders each
* field via the unified `@object-ui/fields` cell-renderer pipeline (same
* as Grid/Gallery), so lookup/user/email/url/phone/boolean/etc. fields
* keep their semantic styling instead of being flattened to a text join.
*
* Takes precedence over `cardSubtitle` / `description` when present.
*/
cardFieldCells?: Array<{ field: string; label?: string; node: React.ReactNode }>;
/**
* Resolved cover-image URL for the card, derived from the board's
* `coverImageField`. Read by both board implementations.
*/
coverImage?: string;
[key: string]: any;
}

Expand All@@ -52,6 +75,11 @@ export interface KanbanColumn {
cards: KanbanCard[];
limit?: number;
className?: string;
/**
* Whether the lane renders collapsed. Honoured by `KanbanEnhanced` (the
* implementation that ships column collapsing); the plain board ignores it.
*/
collapsed?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-markdown/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const schema: MarkdownSchema = {
```typescript
{
type: 'markdown',
content?: string, // Markdown content (supports GitHub Flavored Markdown)
content: string, // Markdown content (supports GitHub Flavored Markdown)
className?: string // Tailwind classes
}
```
Expand Down
56 changes: 30 additions & 26 deletions packages/plugin-markdown/src/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,31 +6,35 @@
* LICENSE file in the root directory of this source tree.
*/

import type { BaseSchema } from '@object-ui/types';

/**
* Markdown component schema.
* Renders markdown content with GitHub Flavored Markdown support.
* `MarkdownSchema` has ONE authority: `@object-ui/types` (objectui#6172, the
* 2026-08-25 family ruling — every exported schema name has exactly one
* authority). This package used to declare a second copy of the name, and the
* two had drifted on exactly one member: `content` was REQUIRED there and
* optional here.
*
* That divergence was measured, not adjudicated by taste, and it was drift
* rather than a real semantic difference — every other statement this package
* makes about `content` already says required:
*
* - the registration in `./index.tsx` declares the `content` input
* `required: true`, and `./index.test.ts` pins that;
* - `MarkdownImplProps.content` in `./MarkdownImpl.tsx` is `content: string`,
* non-optional — the renderer that actually consumes it;
* - the Zod mirror `packages/types/src/zod/data-display.zod.ts` spells it
* `z.string()`, not `.optional()`, and a parity test pins it;
* - and every authored `type: 'markdown'` NODE in the repository supplies
* `content`. (The `type: 'markdown'` literals that omit it are rich-text
* FIELD metadata — `MarkdownFieldMetadata` in
* `packages/types/src/field-types.ts` — which is a different type.)
*
* The lone `content?: string` here was the outlier, so the copies converge onto
* the required spelling rather than the loose one. `className` is not lost: it
* is declared by `BaseSchema`, which both copies extend, so it was always
* inherited rather than added here.
*
* ⛔ Do not re-add a local `export interface MarkdownSchema`. A re-export is
* one declaration with many export sites; a second declaration is a second
* meaning behind one published name, which is the defect this converged.
*/
export interface MarkdownSchema extends BaseSchema {
type: 'markdown';

/**
* The markdown content to render. Supports GitHub Flavored Markdown including:
* - Headers (H1-H6)
* - Bold, italic, and inline code
* - Links and images
* - Lists (ordered, unordered, and nested)
* - Tables
* - Blockquotes
* - Code blocks
* - Strikethrough
* - Task lists
*/
content?: string;

/**
* Optional CSS class name to apply custom styling to the markdown container.
*/
className?: string;
}
export type { MarkdownSchema } from '@object-ui/types';
27 changes: 23 additions & 4 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,10 +394,29 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
// authority left the tree with the spec-bridge retirement (objectui#6366,
// 2026-08-27 maintainer ruling), so app-shell's `form-spec.ts` is now the
// one authority and the entries would fail the stale-baseline direction.
['KanbanCard', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6155 — the ×4 that card measured
['KanbanColumn', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // the same four files; no family card named it
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172
['MarkdownSchema', ['packages/plugin-markdown/src/types.ts', 'packages/types/src/data-display.ts']], // objectui#6172
// `KanbanCard` / `KanbanColumn` were the ×4 objectui#6155 measured. The THREE
// in-package copies converged (objectui#6172): `KanbanImpl.tsx` and
// `KanbanEnhanced.tsx` were strict-SUBSET copies of `./types` — an AST probe
// found nothing typed differently between them — so their members (`coverImage`,
// `cardSubtitle`, `cardFieldCells`, `collapsed`, all optional) moved onto the one
// in-package declaration and both files now re-point at it. Two sites remain, and
// they are the CROSS-package pair: the `@object-ui/types` copy is a different
// dialect (`items` where the plugin says `cards`, `labels` where it says
// `badges`), so collapsing it is a rename of a published name and needs an
// authority ruling the 2026-08-25 family ruling did not give for this pair — it
// named one only for the cross-package `FormField` clash. Escalated, not guessed.
['KanbanCard', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanColumn', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']],
['KanbanSchema', ['packages/plugin-kanban/src/types.ts', 'packages/types/src/complex.ts']], // objectui#6172 — same cross-package pair, same escalation
// `MarkdownSchema` sat here, colliding between
// `packages/plugin-markdown/src/types.ts` and `packages/types/src/data-display.ts`.
// The copies differed on ONE member — `content`, required there and optional here —
// and that was measured to be DRIFT rather than a semantic difference: the plugin's
// own registration declares the `content` input `required: true` (pinned by its own
// test), its `MarkdownImplProps.content` is non-optional, the Zod mirror spells
// `z.string()`, and every authored `type: 'markdown'` NODE in the repo supplies it.
// So plugin-markdown re-points at the one authority in `@object-ui/types`
// (objectui#6172).
['MenuItem', ['packages/types/src/app.ts', 'packages/types/src/overlay.ts']],
['MetadataTypeStatus', ['packages/app-shell/src/providers/MetadataProvider.tsx', 'packages/react/src/context/AppShellContext.tsx']],
['NamedActionDef', ['packages/plugin-grid/src/resolveBulkActions.ts', 'packages/plugin-grid/src/resolveLegacyRowActions.ts']],
Expand Down
Loading