diff --git a/packages/craftcms-cp/.storybook/preview.css b/packages/craftcms-cp/.storybook/preview.css
index 4809e00ea04..790bafc37d9 100644
--- a/packages/craftcms-cp/.storybook/preview.css
+++ b/packages/craftcms-cp/.storybook/preview.css
@@ -70,3 +70,20 @@
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: fixed;
+ text-align: left;
+}
+
+table th,
+table td {
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 0.5em;
+}
+
+table th {
+ background-color: rgba(0, 0, 0, 0.05);
+}
diff --git a/packages/craftcms-cp/scripts/generate-colors.js b/packages/craftcms-cp/scripts/generate-colors.js
index ce8f737df05..98f80432ee6 100644
--- a/packages/craftcms-cp/scripts/generate-colors.js
+++ b/packages/craftcms-cp/scripts/generate-colors.js
@@ -169,7 +169,7 @@ ${buildColorableTokens()}
${buildSemanticTokens()}
}
-${[...availableColors, ...semanticColors].map((c) => buildStyleBlock(c)).join('\n')}
+${[...availableColors, ...Object.values(semanticColors)].map((c) => buildStyleBlock(c)).join('\n')}
`;
}
diff --git a/packages/craftcms-cp/src/actions/index.ts b/packages/craftcms-cp/src/actions/index.ts
index 7f5a8fd29aa..1e90fe236f7 100644
--- a/packages/craftcms-cp/src/actions/index.ts
+++ b/packages/craftcms-cp/src/actions/index.ts
@@ -1,4 +1,4 @@
-import type {VariantKey} from '@src/types';
+import type {VariantKey} from '@src/constants/variants';
export type BaseAction =
| {type: 'clipboard'; value: string}
diff --git a/packages/craftcms-cp/src/components/action-item/action-item.ts b/packages/craftcms-cp/src/components/action-item/action-item.ts
index f381c386510..4a273da2c07 100644
--- a/packages/craftcms-cp/src/components/action-item/action-item.ts
+++ b/packages/craftcms-cp/src/components/action-item/action-item.ts
@@ -1,22 +1,13 @@
import {html, LitElement, nothing} from 'lit';
import {property, state} from 'lit/decorators.js';
import styles from './action-item.styles.js';
-import {
- type AsyncState,
- AsyncStates,
- Variant,
- type VariantKey,
-} from '@src/types';
+import {type AsyncState, AsyncStates} from '@src/types';
import variantsStyles from '@src/styles/variants.styles';
import {classMap} from 'lit/directives/class-map.js';
import '../shortcut/shortcut.js';
-import {
- type ActionFeedback,
- type BaseAction,
- type FeedbackData,
- runAction,
-} from '@src/actions';
+import {type ActionFeedback, type BaseAction, type FeedbackData, runAction,} from '@src/actions';
+import {Variant, type VariantKey} from '@src/constants/variants';
/**
* @summary Either a link or button typically used in a menu.
diff --git a/packages/craftcms-cp/src/components/callout/callout.stories.ts b/packages/craftcms-cp/src/components/callout/callout.stories.ts
index bde500768f6..861647c8eaf 100644
--- a/packages/craftcms-cp/src/components/callout/callout.stories.ts
+++ b/packages/craftcms-cp/src/components/callout/callout.stories.ts
@@ -3,10 +3,8 @@ import type {Meta, StoryObj} from '@storybook/web-components-vite';
import {html} from 'lit';
import './callout.js';
-import {Appearance, Variant} from '@src/types';
-
-const variants = Object.values(Variant);
-const appearances = Object.values(Appearance);
+import {appearances} from '@src/constants/appearances.js';
+import {variants} from '@src/constants/variants.js';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const meta = {
diff --git a/packages/craftcms-cp/src/components/callout/callout.ts b/packages/craftcms-cp/src/components/callout/callout.ts
index 24fe343e05f..c1436886617 100644
--- a/packages/craftcms-cp/src/components/callout/callout.ts
+++ b/packages/craftcms-cp/src/components/callout/callout.ts
@@ -2,12 +2,8 @@ import {type CSSResultGroup, html, LitElement, nothing} from 'lit';
import {property} from 'lit/decorators.js';
import styles from './callout.styles.js';
import '../icon/icon.js';
-import {
- Appearance,
- type AppearanceKey,
- Variant,
- type VariantKey,
-} from '@src/types/index.js';
+import {Appearance, type AppearanceKey} from '@src/constants/appearances';
+import {Variant, type VariantKey} from '@src/constants/variants';
import variantsStyles from '@src/styles/variants.styles.js';
export default class CraftCallout extends LitElement {
diff --git a/packages/craftcms-cp/src/components/indicator/Indicator.mdx b/packages/craftcms-cp/src/components/indicator/Indicator.mdx
new file mode 100644
index 00000000000..3bdf20d3bcf
--- /dev/null
+++ b/packages/craftcms-cp/src/components/indicator/Indicator.mdx
@@ -0,0 +1,186 @@
+import {Canvas, Meta} from '@storybook/addon-docs/blocks';
+import IndicatorMeta, {ArbitraryColor, ArbitrarySize, Default,} from './indicator.stories';
+
+
+
+# Indicator
+
+`` is a small dot used to visually represent the status of an
+object — for example, the live/draft state of an entry or a "has updates" marker
+in a list.
+
+
+
+## Usage
+
+```html
+
+```
+
+The element renders a single `role="img"` dot. It has no slot — all of its
+appearance is driven by attributes.
+
+## Properties
+
+
+
+
+
Property
+
Attribute
+
Type
+
Default
+
Description
+
+
+
+
+
+ fill
+
+
+ fill
+
+
+ string
+
+
+ var(--c-color-fill-loud)
+
+
+ The dot color. Accepts a semantic keyword (see below) or any CSS color.
+
+ md ≈ 0.6em, lg ≈ 1em. Scales with the
+ surrounding font size.
+
+
+
+
+ label
+
+
+ label
+
+
+ string | null
+
+
+ null
+
+
+ Accessible name, exposed as aria-label. Provide it for
+ non-decorative dots.
+
+
+
+
+
+## Fill
+
+`fill` sets the dot's color. A recognized **status variant** (`default`,
+`success`, `warning`, `danger`, `info`) or **palette swatch** (`red`, `orange`,
+`amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`,
+`indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, `gray`, `white`,
+`black`) resolves to the matching `--c-color--fill-loud` design token. Any
+other value — a hex code, `rgb()`, a gradient, or a custom property — is used
+verbatim.
+
+Prefer the variant and swatch names so indicators stay consistent with the rest
+of the palette; reach for an arbitrary value only when a color falls outside it.
+
+
+
+## Appearance
+
+`appearance` controls how the dot is drawn:
+
+
+
+
+
Value
+
Renders
+
+
+
+
+
+ filled-outlined (default)
+
+
Filled dot with a subtle dark outline.
+
+
+
+ filled
+
+
Filled dot with no outline.
+
+
+
+ outlined
+
+
+ Hollow dot — a 2px ring in the fill color over a transparent center.
+
+
+
+
+
+## Sizing
+
+`size` switches between two preset sizes. Because the dimensions are defined in
+`em`, the dot scales with the font size of its container — set `font-size` on a
+wrapper to fine-tune it.
+
+
+
+## Accessibility
+
+- The dot is exposed as `role="img"`, and `label` becomes its `aria-label`.
+- A status dot conveys meaning through color alone, so set `label` whenever the
+indicator isn't purely decorative. If it only repeats adjacent visible text,
+it's fine to leave `label` unset.
+
+## Styling
+
+At render time the resolved color and size are written to the `--fill` and
+`--size` custom properties on the inner dot, so there's nothing to override
+there directly. Because the dot is sized in `em`, set `font-size` on the host to
+scale it:
+
+```css
+craft-indicator {
+ /* the dot is sized in em — change the font size to scale it */
+ font-size: 1.25rem;
+}
+```
diff --git a/packages/craftcms-cp/src/components/indicator/indicator.stories.ts b/packages/craftcms-cp/src/components/indicator/indicator.stories.ts
index d6f105950ec..98492c554f6 100644
--- a/packages/craftcms-cp/src/components/indicator/indicator.stories.ts
+++ b/packages/craftcms-cp/src/components/indicator/indicator.stories.ts
@@ -1,32 +1,101 @@
import type {Meta, StoryObj} from '@storybook/web-components-vite';
-import {html} from 'lit';
+import {html, nothing} from 'lit';
import './indicator.js';
-import {Variant} from '@src/types';
+import type CraftIndicator from './indicator.js';
+import {Variant} from '@src/constants/variants';
+
+const appearances = ['filled-outlined', 'filled', 'outlined'];
+
+/**
+ * Renders the indicator in each appearance, with a label, so every story shares
+ * the same markup.
+ */
+const renderIndicators = (
+ label: string,
+ attrs: {fill?: string; size?: CraftIndicator['size']} = {}
+) => html`
+
+
${label}
+ ${appearances.map(
+ (appearance) => html`
+
+
+
+ `
+ )}
+
+`;
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const meta = {
title: 'Components/Indicator',
component: 'craft-indicator',
args: {},
- render: function () {
- return html`
-