Skip to content

Commit

Permalink
feat(tree): groups
Browse files Browse the repository at this point in the history
  • Loading branch information
Akryum committed Apr 19, 2022
1 parent 67cb60d commit aec7673
Show file tree
Hide file tree
Showing 16 changed files with 226 additions and 38 deletions.
2 changes: 1 addition & 1 deletion examples/vue3/src/components/Introduction.story.vue
@@ -1,5 +1,5 @@
<template>
<Story>
<Story group="top">
This is a demo book using Vue 3.
</Story>
</template>
17 changes: 17 additions & 0 deletions examples/vue3/vite.config.ts
Expand Up @@ -15,5 +15,22 @@ export default defineConfig({
theme: {
logoHref: 'http://histoire.dev',
},

tree: {
groups: [
{
id: 'top',
title: '',
},
{
title: 'My Group',
include: file => /Code gen|Controls|Docs/.test(file.title),
},
{
title: 'Components',
include: file => !file.title.includes('Serialize'),
},
],
},
},
})
2 changes: 1 addition & 1 deletion packages/histoire/src/client/app/App.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
// @ts-expect-error virtual module
import { files as rawFiles, tree as rawTree, onUpdate } from '$histoire-stories'
import StoryList from './components/story/StoryList.vue'
import StoryList from './components/tree/StoryList.vue'
import BaseSplitPane from './components/base/BaseSplitPane.vue'
import { computed, ref, watch, defineAsyncComponent } from 'vue'
import AppHeader from './components/app/AppHeader.vue'
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { Icon } from '@iconify/vue'
import { useStoryStore } from '../../stores/story'
import { Story, Tree } from '../../types'
import MobileOverlay from './MobileOverlay.vue'
import StoryList from '../story/StoryList.vue'
import StoryList from '../tree/StoryList.vue'
const props = defineProps<{
tree: Tree
Expand Down
75 changes: 75 additions & 0 deletions packages/histoire/src/client/app/components/tree/StoryGroup.vue
@@ -0,0 +1,75 @@
<script lang="ts" setup>
import type { Story, TreeGroup, TreeFolder, TreeLeaf } from '../../types'
import StoryListItem from './StoryListItem.vue'
import StoryListFolder from './StoryListFolder.vue'
import { Icon } from '@iconify/vue'
import { computed, withDefaults } from 'vue'
import { useStoryStore } from '../../stores/story'
const props = withDefaults(defineProps<{
path?: Array<string>
group: TreeGroup
stories: Story[]
}>(), {
path: () => [],
})
const storyStore = useStoryStore()
const folderPath = computed(() => [...props.path, props.group.title])
const isFolderOpen = computed(() => storyStore.isFolderOpened(folderPath.value, true))
function toggleOpen () {
storyStore.toggleFolder(folderPath.value, false)
}
</script>

<template>
<div
data-test-id="story-group"
class="htw-my-2 first:htw-mt-0 last:htw-mb-0"
>
<div
v-if="group.title"
role="button"
tabindex="0"
class="htw-px-0.5 htw-py-2 md:htw-py-1.5 htw-mx-1 htw-rounded-sm hover:htw-bg-primary-100 dark:hover:htw-bg-primary-900 htw-cursor-pointer htw-select-none htw-flex htw-items-center htw-gap-1 htw-min-w-0"
@click="toggleOpen"
@keyup.enter="toggleOpen"
@keyup.space="toggleOpen"
>
<Icon
icon="carbon:caret-right"
class="htw-w-4 htw-h-4 htw-transition-transform htw-duration-150 htw-opacity-30 htw-flex-none"
:class="{
'htw-rotate-90': isFolderOpen,
}"
/>
<span class="htw-truncate htw-opacity-50">{{ group.title }}</span>
<span class="htw-h-[1px] htw-flex-1 htw-bg-gray-500/10 htw-mx-2" />
</div>

<!-- Children -->
<div
v-if="isFolderOpen"
>
<template
v-for="element of group.children"
:key="element.title"
>
<StoryListFolder
v-if="(element as TreeFolder).children"
:path="folderPath"
:folder="(element as TreeFolder)"
:stories="stories"
:depth="0"
/>
<StoryListItem
v-else
:story="stories[(element as TreeLeaf).index]"
:depth="0"
/>
</template>
</div>
</div>
</template>
@@ -1,7 +1,8 @@
<script lang="ts" setup>
import type { Story, Tree, TreeFolder, TreeLeaf } from '../../types'
import type { Story, Tree, TreeGroup, TreeFolder, TreeLeaf } from '../../types'
import StoryListItem from './StoryListItem.vue'
import StoryListFolder from './StoryListFolder.vue'
import StoryGroup from './StoryGroup.vue'
const props = defineProps<{
tree: Tree
Expand All @@ -15,8 +16,13 @@ const props = defineProps<{
v-for="element of tree"
:key="element.title"
>
<StoryGroup
v-if="(element as TreeGroup).group"
:group="(element as TreeGroup)"
:stories="stories"
/>
<StoryListFolder
v-if="(element as TreeFolder).children"
v-else-if="(element as TreeFolder).children"
:folder="(element as TreeFolder)"
:stories="stories"
/>
Expand Down
Expand Up @@ -25,7 +25,7 @@ function toggleOpen () {
}
const folderPadding = computed(() => {
return (props.depth * 16) + 'px'
return (props.depth * 12) + 'px'
})
</script>

Expand All @@ -34,13 +34,13 @@ const folderPadding = computed(() => {
<div
role="button"
tabindex="0"
class="htw-px-0.5 htw-py-2 md:htw-py-1.5 htw-m-1 htw-rounded-sm hover:htw-bg-primary-100 dark:hover:htw-bg-primary-900 htw-cursor-pointer htw-select-none htw-flex"
class="htw-px-0.5 htw-py-2 md:htw-py-1.5 htw-mx-1 htw-rounded-sm hover:htw-bg-primary-100 dark:hover:htw-bg-primary-900 htw-cursor-pointer htw-select-none htw-flex"
@click="toggleOpen"
@keyup.enter="toggleOpen"
@keyup.space="toggleOpen"
>
<span class="bind-tree-padding htw-flex htw-items-center htw-gap-2 htw-min-w-0">
<span class="htw-flex htw-gap-1 htw-flex-none htw-items-center">
<span class="htw-flex htw-flex-none htw-items-center">
<Icon
icon="carbon:caret-right"
class="htw-w-4 htw-h-4 htw-transition-transform htw-duration-150 htw-opacity-30"
Expand All @@ -56,6 +56,8 @@ const folderPadding = computed(() => {
<span class="htw-truncate">{{ folder.title }}</span>
</span>
</div>

<!-- Children -->
<div
v-if="isFolderOpen"
>
Expand All @@ -66,7 +68,7 @@ const folderPadding = computed(() => {
<StoryListFolder
v-if="(element as TreeFolder).children"
:path="folderPath"
:folder="element"
:folder="(element as TreeFolder)"
:stories="stories"
:depth="depth + 1"
/>
Expand Down
Expand Up @@ -14,7 +14,7 @@ const props = withDefaults(defineProps<{
})
const filePadding = computed(() => {
return (props.depth * 16) + 'px'
return (props.depth * 12) + 'px'
})
const route = useRoute()
Expand All @@ -36,9 +36,9 @@ useScrollOnActive(active, el)
storyId: story.id,
},
}"
class="htw-pl-0.5 htw-pr-2 htw-py-2 md:htw-py-1.5 htw-m-1 htw-rounded-sm"
class="htw-pl-0.5 htw-pr-2 htw-py-2 md:htw-py-1.5 htw-mx-1 htw-rounded-sm"
>
<span class="bind-tree-margin htw-flex htw-items-center htw-gap-2 htw-pl-5 htw-min-w-0">
<span class="bind-tree-margin htw-flex htw-items-center htw-gap-2 htw-pl-4 htw-min-w-0">
<Icon
:icon="story.icon ?? 'carbon:cube'"
class="base-list-item-link-icon htw-w-5 htw-h-5 sm:htw-w-4 sm:htw-h-4 htw-flex-none"
Expand Down
22 changes: 13 additions & 9 deletions packages/histoire/src/client/app/stores/story.ts
Expand Up @@ -23,22 +23,26 @@ export const useStoryStore = defineStore('story', () => {
return path.join('␜')
}

function toggleFolder (path: Array<string>, force?: boolean) {
function toggleFolder (path: Array<string>, defaultToggleValue = true) {
const stringPath = getStringPath(path)

if (force === undefined) {
force = !openedFolders.value.get(stringPath)
}
const currentValue = openedFolders.value.get(stringPath)

if (force) {
openedFolders.value.set(stringPath, true)
if (currentValue == null) {
openedFolders.value.set(stringPath, defaultToggleValue)
} else if (currentValue) {
openedFolders.value.set(stringPath, false)
} else {
openedFolders.value.delete(stringPath)
openedFolders.value.set(stringPath, true)
}
}

function isFolderOpened (path: Array<string>) {
return openedFolders.value.get(getStringPath(path))
function isFolderOpened (path: Array<string>, defaultValue = false) {
const value = openedFolders.value.get(getStringPath(path))
if (value == null) {
return defaultValue
}
return value
}

function openFileFolders (path: Array<string>) {
Expand Down
10 changes: 9 additions & 1 deletion packages/histoire/src/client/app/types.ts
Expand Up @@ -11,6 +11,7 @@ export interface StoryFile {
export interface Story {
id: string
title: string
group?: string
variants: Variant[]
layout?: {
type: 'single'
Expand Down Expand Up @@ -48,7 +49,14 @@ export type TreeFolder = {
children: (TreeFolder | TreeLeaf)[]
}

export type Tree = (TreeFolder | TreeLeaf)[]
export interface TreeGroup {
group: true
id: string
title: string
children: (TreeFolder | TreeLeaf)[]
}

export type Tree = (TreeGroup | TreeFolder | TreeLeaf)[]

export interface SearchResult {
kind: 'story' | 'variant'
Expand Down
6 changes: 6 additions & 0 deletions packages/histoire/src/client/server/Story.server.vue
Expand Up @@ -11,6 +11,11 @@ export default defineComponent({
default: null,
},
group: {
type: String,
default: null,
},
layout: {
type: Object as PropType<Story['layout']>,
default: () => ({ type: 'single' }),
Expand All @@ -37,6 +42,7 @@ export default defineComponent({
const story: Story = {
id: attrs.data.id,
title: props.title ?? attrs.data.fileName,
group: props.group,
layout: props.layout,
icon: props.icon,
iconColor: props.iconColor,
Expand Down
6 changes: 3 additions & 3 deletions packages/histoire/src/node/collect/index.ts
Expand Up @@ -7,7 +7,7 @@ import pc from 'picocolors'
import Tinypool from 'tinypool'
import { createBirpc } from 'birpc'
import type { StoryFile } from '../types.js'
import { createPath, TreeFile } from '../tree.js'
import { createPath } from '../tree.js'
import type { Context } from '../context.js'
import type { Payload, ReturnData } from './worker.js'

Expand Down Expand Up @@ -79,11 +79,11 @@ export function useCollectStories (options: UseCollectStoriesOptions, ctx: Conte
console.warn(pc.yellow(`⚠️ Multiple stories not supported: ${storyFile.path}`))
}
storyFile.story = storyData[0]
const file: TreeFile = {
storyFile.treeFile = {
title: storyData[0].title,
path: relative(server.config.root, storyFile.path),
}
storyFile.treePath = createPath(ctx.config, file)
storyFile.treePath = createPath(ctx.config, storyFile.treeFile)
storyFile.story.title = storyFile.treePath[storyFile.treePath.length - 1]
} catch (e) {
console.error(pc.red(`Error while collecting story ${storyFile.path}:\n${e.frame ? `${pc.bold(e.message)}\n${e.frame}` : e.stack}`))
Expand Down
11 changes: 9 additions & 2 deletions packages/histoire/src/node/config.ts
Expand Up @@ -13,17 +13,23 @@ type CustomizableColors = 'primary' | 'gray'
type ColorKeys = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
type GrayColorKeys = ColorKeys | '750' | '850' | '950'

interface ResponsivePreset {
export interface ResponsivePreset {
label: string
width: number
height: number
}

interface BackgroundPreset {
export interface BackgroundPreset {
label: string
color: string
}

export interface TreeGroupConfig {
title: string
id?: string
include?: (file: TreeFile) => boolean
}

export interface HistoireConfig {
/**
* Output directory.
Expand All @@ -48,6 +54,7 @@ export interface HistoireConfig {
*/
file?: 'title' | 'path' | ((file: TreeFile) => string[])
order?: 'asc' | ((a: string, b: string) => number)
groups?: TreeGroupConfig[]
}
/**
* Customize the look of the histoire book.
Expand Down
2 changes: 1 addition & 1 deletion packages/histoire/src/node/plugin.ts
Expand Up @@ -99,7 +99,7 @@ export async function createVitePlugins (ctx: Context): Promise<VitePlugin[]> {
return `import { defineAsyncComponent } from 'vue'
${resolvedStories.map((file, index) => `const Comp${index} = defineAsyncComponent(() => import('${file.path}'))`).join('\n')}
export let files = [${files.map((file) => `{${JSON.stringify(file).slice(1, -1)}, component: Comp${file.index}}`).join(',\n')}]
export let tree = ${JSON.stringify(makeTree(ctx.config, files))}
export let tree = ${JSON.stringify(makeTree(ctx.config, resolvedStories))}
const handlers = []
export function onUpdate (cb) {
handlers.push(cb)
Expand Down

0 comments on commit aec7673

Please sign in to comment.