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
3 changes: 3 additions & 0 deletions libs/core/src/components/pds-button/docs/pds-button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

<Meta of={stories} />

<StatusBadge component="pds-button" />

# 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.
Expand Down
3 changes: 3 additions & 0 deletions libs/core/src/components/pds-modal/docs/pds-modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

<Meta of={stories} />

<StatusBadge component="pds-modal" />

# 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.
Expand Down
3 changes: 3 additions & 0 deletions libs/core/src/components/pds-table/docs/pds-table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

<Meta of={stories} />

<StatusBadge component="pds-table" />

# 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.
Expand Down
56 changes: 56 additions & 0 deletions libs/core/src/stories/_helpers/StatusBadge.css
Original file line number Diff line number Diff line change
@@ -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);
}
142 changes: 142 additions & 0 deletions libs/core/src/stories/_helpers/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -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<ResolvedStatus, string> = {
stable: 'Stable',
beta: 'Beta',
deprecated: 'Deprecated',
unknown: 'Unknown',
};

const COMPONENTS = componentStatus.components as Record<string, ComponentEntry>;

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)) {
Comment thread
cursor[bot] marked this conversation as resolved.
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<StatusBadgeProps> = ({ 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];
Comment thread
cursor[bot] marked this conversation as resolved.
const ariaLabel = buildAriaLabel(label, resolvedNote);

return (
<div
className="pine-status-badge"
data-status={resolvedStatus}
role="note"
aria-label={ariaLabel}
>
<span className="pine-status-badge__dot" aria-hidden="true" />
<span className="pine-status-badge__label">{label}</span>
{resolvedNote !== undefined && resolvedNote !== '' ? (
<span className="pine-status-badge__note">— {resolvedNote}</span>
Comment thread
cursor[bot] marked this conversation as resolved.
) : null}
</div>
);
};
2 changes: 2 additions & 0 deletions libs/core/src/stories/_helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ export const customArgsWithIconControl = ({property}: Omit<IconControlArgs, 'com

export { ChangelogLoader } from './ChangelogLoader';
export { ChangelogRenderer } from './ChangelogRenderer';
export { StatusBadge } from './StatusBadge';
export type { ComponentStatus, StatusBadgeProps } from './StatusBadge';

Loading
Loading