diff --git a/libs/core/src/components/pds-button/docs/pds-button.mdx b/libs/core/src/components/pds-button/docs/pds-button.mdx index 748dc196c..90345501d 100644 --- a/libs/core/src/components/pds-button/docs/pds-button.mdx +++ b/libs/core/src/components/pds-button/docs/pds-button.mdx @@ -3,9 +3,12 @@ import * as stories from '../stories/pds-button.stories.js'; import {DocArgsTable, DocCanvas} from '@pine-ds/doc-components'; import { components } from '../../../../dist/docs.json'; +import { StatusBadge } from '../../../stories/_helpers'; + + # Button A button is an interactive element that can be clicked to perform various actions such as triggering an event, submitting data, or navigating to a different location. diff --git a/libs/core/src/components/pds-modal/docs/pds-modal.mdx b/libs/core/src/components/pds-modal/docs/pds-modal.mdx index 8a9d27db2..4e839cf1d 100644 --- a/libs/core/src/components/pds-modal/docs/pds-modal.mdx +++ b/libs/core/src/components/pds-modal/docs/pds-modal.mdx @@ -3,9 +3,12 @@ import * as stories from '../stories/pds-modal.stories.js'; import {DocArgsTable, DocCanvas} from '@pine-ds/doc-components'; import { components } from '../../../../dist/docs.json'; +import { StatusBadge } from '../../../stories/_helpers'; + + # Modal Modals display content in a dialog that temporarily blocks interaction with the underlying page. Use them for confirmations, focused forms, or detailed information that demands attention without navigating away. diff --git a/libs/core/src/components/pds-table/docs/pds-table.mdx b/libs/core/src/components/pds-table/docs/pds-table.mdx index e31e55511..cb8ee36f1 100644 --- a/libs/core/src/components/pds-table/docs/pds-table.mdx +++ b/libs/core/src/components/pds-table/docs/pds-table.mdx @@ -3,9 +3,12 @@ import * as stories from '../stories/pds-table.stories.js'; import { DocArgsTable, DocCanvas } from '@pine-ds/doc-components'; import { components } from '../../../../dist/docs.json'; +import { StatusBadge } from '../../../stories/_helpers'; + + # Table Tables display tabular data using semantic subcomponents for head, head-cell, body, row, and cell roles. Use pds-table-head, pds-table-head-cell, pds-table-body, pds-table-row, and pds-table-cell to compose rows and columns accessibly. diff --git a/libs/core/src/stories/_helpers/StatusBadge.css b/libs/core/src/stories/_helpers/StatusBadge.css new file mode 100644 index 000000000..fb9ef7cf5 --- /dev/null +++ b/libs/core/src/stories/_helpers/StatusBadge.css @@ -0,0 +1,56 @@ +.pine-status-badge { + --pine-status-badge-bg: var(--pine-color-background-subtle); + --pine-status-badge-fg: var(--pine-color-text-primary); + --pine-status-badge-dot: currentColor; + + align-items: center; + background-color: var(--pine-status-badge-bg); + border-radius: 999px; + color: var(--pine-status-badge-fg); + display: inline-flex; + font-size: 0.8125rem; + font-weight: 500; + gap: 0.5rem; + line-height: 1.2; + margin: 0 0 1rem; + padding: 0.25rem 0.625rem; +} + +.pine-status-badge__dot { + background-color: var(--pine-status-badge-dot); + border-radius: 50%; + display: inline-block; + flex: 0 0 auto; + height: 0.5rem; + width: 0.5rem; +} + +.pine-status-badge__note { + color: inherit; + font-weight: 400; + opacity: 0.8; +} + +.pine-status-badge[data-status='stable'] { + --pine-status-badge-bg: var(--pine-color-success-disabled); + --pine-status-badge-fg: var(--pine-color-text-success); + --pine-status-badge-dot: var(--pine-color-success-hover); +} + +.pine-status-badge[data-status='beta'] { + --pine-status-badge-bg: var(--pine-color-warning-disabled); + --pine-status-badge-fg: var(--pine-color-text-warning); + --pine-status-badge-dot: var(--pine-color-warning-hover); +} + +.pine-status-badge[data-status='deprecated'] { + --pine-status-badge-bg: var(--pine-color-danger-disabled); + --pine-status-badge-fg: var(--pine-color-text-danger); + --pine-status-badge-dot: var(--pine-color-danger-hover); +} + +.pine-status-badge[data-status='unknown'] { + --pine-status-badge-bg: var(--pine-color-warning-disabled); + --pine-status-badge-fg: var(--pine-color-text-warning); + --pine-status-badge-dot: var(--pine-color-warning-hover); +} diff --git a/libs/core/src/stories/_helpers/StatusBadge.tsx b/libs/core/src/stories/_helpers/StatusBadge.tsx new file mode 100644 index 000000000..8cb197bb3 --- /dev/null +++ b/libs/core/src/stories/_helpers/StatusBadge.tsx @@ -0,0 +1,142 @@ +import React from 'react'; + +import componentStatus from '../resources/component-status.json'; +import './StatusBadge.css'; + +export type ComponentStatus = 'stable' | 'beta' | 'deprecated'; + +/** Display-only when manifest lookup fails or status is invalid. */ +type ResolvedStatus = ComponentStatus | 'unknown'; + +export interface StatusBadgeProps { + /** + * Component tag (for example `pds-button`). When provided the badge + * reads the lifecycle label from `component-status.json` so the badge + * and the central status table cannot drift. + */ + component?: string; + + /** + * Lifecycle label override. Takes priority over the JSON manifest when + * both are set — use to force a status that differs from the central + * table (rare; prefer updating the JSON manifest). + */ + status?: ComponentStatus; + + /** Optional inline note (for example: 'Replaced by `pds-property`'). */ + note?: string; +} + +interface ComponentEntry { + status: ComponentStatus; + note?: string; +} + +const VALID_STATUSES: readonly ComponentStatus[] = ['stable', 'beta', 'deprecated']; + +const STATUS_LABELS: Record = { + stable: 'Stable', + beta: 'Beta', + deprecated: 'Deprecated', + unknown: 'Unknown', +}; + +const COMPONENTS = componentStatus.components as Record; + +function isComponentStatus(value: unknown): value is ComponentStatus { + return typeof value === 'string' && (VALID_STATUSES as readonly string[]).includes(value); +} + +function isComponentEntry(value: unknown): value is ComponentEntry { + return value != null && typeof value === 'object'; +} + +function warnDev(message: string): void { + if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn(`[StatusBadge] ${message}`); + } +} + +function resolveStatus( + component: string | undefined, + status: ComponentStatus | undefined, + entry: ComponentEntry | undefined, +): ResolvedStatus { + if (status !== undefined) { + if (!isComponentStatus(status)) { + warnDev(`Invalid status prop "${status}".`); + return 'unknown'; + } + return status; + } + + if (isComponentEntry(entry)) { + if (!isComponentStatus(entry.status)) { + warnDev( + `Invalid status "${String(entry.status)}" for "${component ?? 'component'}" in component-status.json.`, + ); + return 'unknown'; + } + return entry.status; + } + + if (component !== undefined) { + warnDev(`"${component}" is not listed in component-status.json.`); + return 'unknown'; + } + + return 'stable'; +} + +function buildAriaLabel(label: string, note: string | undefined): string { + if (note !== undefined && note !== '') { + return `Component status: ${label}. ${note}`; + } + return `Component status: ${label}`; +} + +/** + * Renders the per-component lifecycle label that mirrors the central + * Resources/Component status table. Apply once at the top of each + * component's MDX page so consumers see stability state without + * navigating elsewhere. + * + * Source of truth: `libs/core/src/stories/resources/component-status.json`. + * Pass the component tag (`component="pds-button"`) so the badge stays + * in sync automatically. + */ +export const StatusBadge: React.FC = ({ component, status, note }) => { + const rawEntry = component !== undefined ? COMPONENTS[component] : undefined; + let entry: ComponentEntry | undefined; + + if (rawEntry === undefined) { + entry = undefined; + } else if (isComponentEntry(rawEntry)) { + entry = rawEntry; + } else { + warnDev(`Invalid entry for "${component}" in component-status.json.`); + entry = undefined; + } + + const resolvedStatus = resolveStatus(component, status, entry); + const resolvedNote = + note ?? (status === undefined && entry !== undefined ? entry.note : undefined); + const label = STATUS_LABELS[resolvedStatus]; + const ariaLabel = buildAriaLabel(label, resolvedNote); + + return ( +
+
+ ); +}; diff --git a/libs/core/src/stories/_helpers/index.ts b/libs/core/src/stories/_helpers/index.ts index 28e78405e..aa1587b32 100644 --- a/libs/core/src/stories/_helpers/index.ts +++ b/libs/core/src/stories/_helpers/index.ts @@ -32,4 +32,6 @@ export const customArgsWithIconControl = ({property}: Omit + +# Component status + +Lifecycle labels describe **API and visual stability** for product teams—not Storybook polish. They are updated when a component’s contract or usage is intentionally changing. + +> **Source of truth:** `libs/core/src/stories/resources/component-status.json` drives both this table and the `StatusBadge` rendered on each component MDX page. When you promote or deprecate a component, edit the JSON manifest **and** the table below in the same PR. + +## Definitions + + + + + Label + Meaning + + + + Stable + Safe for broad production use; breaking changes follow semver and migration notes. + + + Beta + API or visuals may still shift; prefer new work here only when you accept occasional churn. + + + Deprecated + Do not use in new UI; plan migration; removal is scheduled with notice in the changelog. + + + + + +## Pine web components + +All components below are **Stable** unless noted. Subcomponents (for example `pds-table-row`) follow the parent unless called out separately. + + + + + Component + Status + Notes + + + + pds-accordion + Stable + + + + pds-alert + Stable + + + + pds-avatar + Stable + + + + pds-box + Stable + + + + pds-button + Stable + + + + pds-checkbox + Stable + + + + pds-chip + Stable + + + + pds-combobox + Stable + + + + pds-container + Stable + + + + pds-copytext + Stable + + + + pds-divider + Stable + + + + pds-dropdown-menu + Stable + + + + pds-filters / pds-filter + Stable + + + + pds-icon + Stable + Icons ship from @pine-ds/icons. + + + pds-image + Stable + + + + pds-input + Stable + + + + pds-link + Stable + + + + pds-loader + Stable + + + + pds-modal + Stable + + + + pds-multiselect + Stable + + + + pds-popover + Stable + + + + pds-progress + Stable + + + + pds-property + Stable + + + + pds-radio / pds-radio-group + Stable + + + + pds-row + Stable + + + + pds-select + Stable + + + + pds-sortable + Stable + + + + pds-switch + Stable + + + + pds-table + Stable + + + + pds-tabs + Stable + + + + pds-text + Stable + + + + pds-textarea + Stable + + + + pds-toast + Stable + + + + pds-tooltip + Stable + + + + + + +When a component moves to **Beta** or **Deprecated**, update this table in the same pull request as the changelog entry and any Storybook migration callouts. diff --git a/libs/core/src/stories/resources/component-status.json b/libs/core/src/stories/resources/component-status.json new file mode 100644 index 000000000..7a13fd676 --- /dev/null +++ b/libs/core/src/stories/resources/component-status.json @@ -0,0 +1,56 @@ +{ + "$schema": "./component-status.schema.json", + "description": "Lifecycle status for every Pine component. Source of truth for StatusBadge and the central Component status table. Update both when a component is promoted to Beta or Deprecated.", + "statuses": { + "stable": { + "label": "Stable", + "summary": "Safe for broad production use; breaking changes follow semver and migration notes." + }, + "beta": { + "label": "Beta", + "summary": "API or visuals may still shift; prefer new work here only when you accept occasional churn." + }, + "deprecated": { + "label": "Deprecated", + "summary": "Do not use in new UI; plan migration; removal is scheduled with notice in the changelog." + } + }, + "components": { + "pds-accordion": { "status": "stable" }, + "pds-alert": { "status": "stable" }, + "pds-avatar": { "status": "stable" }, + "pds-box": { "status": "stable" }, + "pds-button": { "status": "stable" }, + "pds-checkbox": { "status": "stable" }, + "pds-chip": { "status": "stable" }, + "pds-combobox": { "status": "stable" }, + "pds-container": { "status": "stable" }, + "pds-copytext": { "status": "stable" }, + "pds-divider": { "status": "stable" }, + "pds-dropdown-menu": { "status": "stable" }, + "pds-filters": { "status": "stable" }, + "pds-filter": { "status": "stable", "note": "Subcomponent of pds-filters." }, + "pds-icon": { "status": "stable", "note": "Icons ship from `@pine-ds/icons`." }, + "pds-image": { "status": "stable" }, + "pds-input": { "status": "stable" }, + "pds-link": { "status": "stable" }, + "pds-loader": { "status": "stable" }, + "pds-modal": { "status": "stable" }, + "pds-multiselect": { "status": "stable" }, + "pds-popover": { "status": "stable" }, + "pds-progress": { "status": "stable" }, + "pds-property": { "status": "stable" }, + "pds-radio": { "status": "stable" }, + "pds-radio-group": { "status": "stable" }, + "pds-row": { "status": "stable" }, + "pds-select": { "status": "stable" }, + "pds-sortable": { "status": "stable" }, + "pds-switch": { "status": "stable" }, + "pds-table": { "status": "stable" }, + "pds-tabs": { "status": "stable" }, + "pds-text": { "status": "stable" }, + "pds-textarea": { "status": "stable" }, + "pds-toast": { "status": "stable" }, + "pds-tooltip": { "status": "stable" } + } +}