Skip to content

Commit

Permalink
feat: Support depth display parameter and adjust defaults to show top…
Browse files Browse the repository at this point in the history
…-level badges
  • Loading branch information
Sidnioulz committed Nov 23, 2024
1 parent c269be1 commit 595f77c
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 38 deletions.
39 changes: 29 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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`<br />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 }]` |

---

Expand Down
1 change: 1 addition & 0 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const Sidebar: FC<SidebarProps> = ({ children, item }) => {

const badgesToDisplay = useBadgesToDisplay({
context: 'sidebar',
depth: item.depth,
parameters,
tags: item.tags,
type: item.type,
Expand Down
4 changes: 0 additions & 4 deletions src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
14 changes: 13 additions & 1 deletion src/types/DisplayOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = boolean | T | T[]
export type DisplayOptionItem<T> =
| 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<T> = DisplayOptionItem<T> | DisplayOptionItem<T>[]

/**
* The types of HashEntries for which badges will be displayed in different parts of the Storybook UI.
Expand Down
6 changes: 4 additions & 2 deletions src/useBadgesToDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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,
Expand All @@ -36,5 +38,5 @@ export function useBadgesToDisplay({
}
return acc
}, [])
}, [parameters, tags, type])
}, [parameters, depth, tags, type])
}
108 changes: 87 additions & 21 deletions src/utils/display.ts
Original file line number Diff line number Diff line change
@@ -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<ArrayElement<TagBadgeParameters>>
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<API_HashEntry['type']>,
): 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<API_HashEntry['type']> | 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(
Expand All @@ -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
})
}
1 change: 1 addition & 0 deletions static/entry-component.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/entry-docs.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/entry-group.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/entry-story.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 595f77c

Please sign in to comment.