diff --git a/README.md b/README.md index d7be2b6..909755b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ This addon comes with a default config, allowing you to get started immediately ### Display Logic -By default, all tags are always displayed on the toolbar, but they're only displayed for component entries in the sidebar. +By default, all tags are always displayed on the toolbar, but they're only displayed in the sidebar for component entries, and for docs or story entries that appear at the top-level. They are not displayed in docs or story entries inside a component or group entry. Besides, the addon is limited to one badge per entry in the sidebar. Badges placed first in the configuration will be displayed in priority. For example, the `new` badge will be displayed before the `code-only` badge. @@ -221,17 +221,36 @@ A tag pattern can be: ### Display -The `display` property controls where and for what type of content the badges are rendered. It has two sub-properties: `sidebar` and `toolbar`. In the sidebar, tags may be displayed for component, docs or story entries. In the toolbar, they may be set for docs or story entries (as other entry types aren't displayable outside the sidebar). +The `display` property controls where and for what type of content the badges are rendered. It has two sub-properties: `sidebar` and `toolbar`. In the sidebar, tags may be displayed for component, group, docs or story entries. In the toolbar, they may be set for docs or story entries (as other entry types aren't displayable outside the sidebar). -Each of these sub-properties can be set to: +The following entry types are rendered by Storybook: -| Type | Description | Example | Sidebar outcome | Toolbar outcome | -| --------------- | ---------------------------------- | ---------- | -------------------------------- | ------------------- | -| `ø` _(not set)_ | Use default behaviour | | `['component']` | `['docs', 'story']` | -| `false` | Never display tag | `false` | `[]` | `[]` | -| `true` | Always display tag | `true` | `['component', 'docs', 'story']` | `['docs', 'story']` | -| `string` | Display only for one type of entry | `'docs'` | `['docs']` | `['docs']` | -| `string[]` | Display for a list of entry types | `['docs']` | `['docs']` | `['docs']` | +| Icon | Name | Description | +| --------------------------------- | --------- | -------------------------------------------------------------------------- | +| ![](./static/entry-story.svg) | story | One of the component stories written in your CSF files. | +| ![](./static/entry-docs.svg) | docs | A documentation page generated through MDX files or autodocs. | +| ![](./static/entry-component.svg) | component | The grouping of a component's stories and autodocs page. | +| ![](./static/entry-group.svg) | group | A generic group containing unattached MDX docs, stories and/or components. | + +To control where badges are shown, you pass conditions to the `sidebar` and `toolbar` keys. You can either specify a single condition, or an array of conditions (in which case matching any condition causes the badge to display). + +Conditions can either specify the type of entry you want to display badges for, or, in the sidebar, the depth until which you'll stop displaying badges. A condition takes three properties in its full form: + +| Property | Description | Type | Example value | +| ------------ | ------------------------------------------------------------------------------------------------------------ | --------- | ------------- | +| `type` | The type of entry to match | `string` | `'docs'` | +| `depth` | How far down the sidebar to display entries | `number` | `1` | +| `exactDepth` | When false, match all items of depth lesser or equal to `depth`
When true, do not match shallower items | `boolean` | `true` | + +Syntax shortcuts are supported, and summarised in the table below: + +| Type | Description | Example | Sidebar outcome | Toolbar outcome | +| --------------- | ----------------------------------- | -------- | ---------------------------------------------------------- | --------------------------------------- | +| `ø` _(not set)_ | Use default behaviour | | `[{ type: 'component' }, { type: 'group' }, { depth: 1 }]` | `[{ type: 'docs' }, { type: 'story' }]` | +| `false` | Never display badge | `false` | `[]` | `[]` | +| `true` | Always display badge | `true` | `[{ depth: Infinity }]` | `[{ depth: Infinity }]` | +| `string` | Display only for one type of entry | `'docs'` | `[{ type: 'docs' }]` | `[{ type: 'docs' }]` | +| `number` | Display until that depth is reached | `2` | `[{ depth: 2 }]` | `[{ depth: 2 }]` | --- diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 8505502..36246ad 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -35,6 +35,7 @@ export const Sidebar: FC = ({ children, item }) => { const badgesToDisplay = useBadgesToDisplay({ context: 'sidebar', + depth: item.depth, parameters, tags: item.tags, type: item.type, diff --git a/src/defaultConfig.ts b/src/defaultConfig.ts index 36d9a46..d95ef0c 100644 --- a/src/defaultConfig.ts +++ b/src/defaultConfig.ts @@ -2,10 +2,6 @@ import type { TagBadgeParameters } from './types/TagBadgeParameters' export const defaultConfig: TagBadgeParameters = [ { - display: { - sidebar: ['component'], - toolbar: ['story', 'docs'], - }, tags: 'new', badge: { text: 'New', diff --git a/src/types/DisplayOption.ts b/src/types/DisplayOption.ts index a79e047..2ce6e57 100644 --- a/src/types/DisplayOption.ts +++ b/src/types/DisplayOption.ts @@ -5,7 +5,19 @@ import { API_HashEntry } from '@storybook/types' * If `false`, never displays. If a string or string array, each string is a type of HashEntry * for which the badge will be shown (e.g. 'docs' or 'story'). */ -export type DisplayOption = boolean | T | T[] +export type DisplayOptionItem = + | boolean + | number + | { depth: number; exactDepth?: boolean; type?: T } + | { depth?: number; exactDepth?: boolean; type: T } + | T + +/** + * Display options for badges in a part of the UI. If `true`, displays for any type of item. + * If `false`, never displays. If a string or string array, each string is a type of HashEntry + * for which the badge will be shown (e.g. 'docs' or 'story'). + */ +export type DisplayOption = DisplayOptionItem | DisplayOptionItem[] /** * The types of HashEntries for which badges will be displayed in different parts of the Storybook UI. diff --git a/src/useBadgesToDisplay.ts b/src/useBadgesToDisplay.ts index c16f85e..da895fc 100644 --- a/src/useBadgesToDisplay.ts +++ b/src/useBadgesToDisplay.ts @@ -8,6 +8,7 @@ import { BadgeOrBadgeFn } from './types/Badge' interface UseBadgesToDisplayOptions { context: 'sidebar' | 'toolbar' + depth?: number parameters: TagBadgeParameters tags: string[] type: API_ComponentEntry['type'] | API_LeafEntry['type'] @@ -17,13 +18,14 @@ type BadgesToDisplay = { badge: BadgeOrBadgeFn; tag: string }[] export function useBadgesToDisplay({ context, + depth, parameters, tags, type, }: UseBadgesToDisplayOptions): BadgesToDisplay { return useMemo(() => { return (parameters || []) - .filter((config) => shouldDisplay({ context, config, type })) + .filter((config) => shouldDisplay({ context, config, depth, type })) .flatMap((config) => matchTags(tags, config.tags).map((tag) => ({ badge: config.badge, @@ -36,5 +38,5 @@ export function useBadgesToDisplay({ } return acc }, []) - }, [parameters, tags, type]) + }, [parameters, depth, tags, type]) } diff --git a/src/utils/display.ts b/src/utils/display.ts index d724f87..b3de761 100644 --- a/src/utils/display.ts +++ b/src/utils/display.ts @@ -1,41 +1,79 @@ import { HashEntry } from '@storybook/manager-api' import type { ArrayElement } from '../types/ArrayElement' import type { TagBadgeParameters } from '../types/TagBadgeParameters' -import type { Display } from '../types/DisplayOption' +import type { + Display, + DisplayOption, + DisplayOptionItem, +} from '../types/DisplayOption' +import { API_HashEntry } from '@storybook/types' export interface ShouldDisplayOptions { config: Partial> + depth?: number context: 'sidebar' | 'toolbar' type: HashEntry['type'] } -export const DISPLAY_DEFAULTS = { - sidebar: ['component'], - toolbar: ['docs', 'story'], -} satisfies Display +type NormalisedDisplayOption = + | { depth: number; exactDepth?: boolean; type?: API_HashEntry['type'] } + | { depth?: number; exactDepth?: boolean; type: API_HashEntry['type'] } + +type NormalisedDisplayOptions = NormalisedDisplayOption[] + +export const DISPLAY_DEFAULTS: { + sidebar: NormalisedDisplayOptions + toolbar: NormalisedDisplayOptions +} = { + sidebar: [{ type: 'component' }, { type: 'group' }, { depth: 1 }], + toolbar: [{ type: 'docs' }, { type: 'story' }], +} + +function normaliseAtomicValue( + v: DisplayOptionItem, +): NormalisedDisplayOption { + if (typeof v === 'number') { + return { depth: v, exactDepth: false } + } + if (typeof v === 'string') { + return { type: v } + } + if (v === true) { + // Will match every entry, always. + return { depth: Infinity, exactDepth: false } + } + if (v === false) { + // Will never be called in practice, only for TypeScript. + // Will never match any entry. + return { depth: -1, exactDepth: true } + } + return v +} function normaliseDisplayProperty( - value: boolean | HashEntry['type'] | HashEntry['type'][] | undefined, - defaultValue: HashEntry['type'][], -): HashEntry['type'][] { + value: DisplayOption | undefined, + defaultValue: NormalisedDisplayOptions, +): NormalisedDisplayOptions { if (value === undefined) { return [...defaultValue] - } - if (value === true) { - return ['component', 'docs', 'story', 'group'] - } - if (value === false) { + } else if (value === false) { return [] + } else if (!Array.isArray(value)) { + return [normaliseAtomicValue(value)] + } else { + return ( + value + // Remove false items in the array. + .filter(Boolean) + // Then normalise the items into fully-defined objects. + .map(normaliseAtomicValue) + ) } - if (typeof value === 'string') { - return [value] - } - return [...value] } export function normaliseDisplay(display?: Display): { - sidebar: HashEntry['type'][] - toolbar: HashEntry['type'][] + sidebar: NormalisedDisplayOptions + toolbar: NormalisedDisplayOptions } { return { sidebar: normaliseDisplayProperty( @@ -55,15 +93,43 @@ export function normaliseDisplay(display?: Display): { * * @param options The options to determine display. * @param options.config The configuration for the badge. + * @param options.depth An optional sidebar depth for the 'sidebar' context. * @param options.context The context where the badge might be displayed. * @param options.type The type of the current entry. * * @returns {boolean} `true` if the badge should be displayed, `false` otherwise. */ -export function shouldDisplay({ config, context, type }: ShouldDisplayOptions) { +export function shouldDisplay({ + config, + context, + depth, + type, +}: ShouldDisplayOptions) { if (type === 'root') { return false } - return normaliseDisplay(config.display)[context].includes(type) + const normalised = normaliseDisplay(config.display)[context] + + return normalised.some((condition) => { + // When a type is defined, it must always match the type of the HashEntry. + if (condition.type !== undefined && condition.type !== type) { + return false + } + // When a type is defined... + if (condition.depth !== undefined && depth !== undefined) { + // Only match exact depths when asked to. + if (condition.exactDepth && condition.depth !== depth) { + return false + } + + // Or match any depth smaller or equal to the condition. + if (condition.depth < depth) { + return false + } + } + + // If we haven't been filtered out yet, the HashEntry should be displayed. + return true + }) } diff --git a/static/entry-component.svg b/static/entry-component.svg new file mode 100644 index 0000000..8b4c272 --- /dev/null +++ b/static/entry-component.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/entry-docs.svg b/static/entry-docs.svg new file mode 100644 index 0000000..c7d422c --- /dev/null +++ b/static/entry-docs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/entry-group.svg b/static/entry-group.svg new file mode 100644 index 0000000..4334fa1 --- /dev/null +++ b/static/entry-group.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/entry-story.svg b/static/entry-story.svg new file mode 100644 index 0000000..d38374d --- /dev/null +++ b/static/entry-story.svg @@ -0,0 +1 @@ + \ No newline at end of file