Skip to content
Open
77 changes: 77 additions & 0 deletions examples/vue3/cypress/e2e/layout.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/// <reference types="cypress" />

describe('Layout customization', () => {
beforeEach(() => {
cy.clearLocalStorage()
cy.visit('/')
})

it('opens the modal via the header button', () => {
cy.get('[data-test-id="layout-btn"]').click()
cy.get('[data-test-id="layout-modal"]').should('be.visible')
cy.contains('[data-test-id="layout-modal"]', 'Customize Layout')
})

it('toggles Story List visibility', () => {
cy.get('.histoire-base-split-pane').should('exist')
cy.get('[data-test-id="layout-btn"]').click()
cy.get('[data-test-id="layout-toggle-story-list"]').click()
cy.get('[data-test-id="layout-modal-close"]').click()
cy.get('.histoire-base-split-pane').should('not.exist')
})

it('toggles Story Options visibility on a story', () => {
cy.get('[data-test-id="story-list-item"]').contains('Controls').click()
cy.get('[data-test-id="story-side-panel"]').should('exist')
cy.get('[data-test-id="layout-btn"]').click()
cy.get('[data-test-id="layout-toggle-story-options"]').click()
cy.get('[data-test-id="layout-modal-close"]').click()
cy.get('[data-test-id="story-side-panel"]').should('not.exist')
})

it('switches Story Options placement to Bottom', () => {
cy.get('[data-test-id="story-list-item"]').contains('Controls').click()
cy.get('[data-test-id="story-side-panel"]').should('exist')
cy.get('[data-test-id="layout-btn"]').click()
cy.get('[data-test-id="layout-placement-bottom"]').click()
cy.get('[data-test-id="layout-modal-close"]').click()
cy.get('.histoire-base-split-pane.portrait').should('exist')
})

it('persists settings across reload', () => {
cy.get('[data-test-id="layout-btn"]').click()
cy.get('[data-test-id="layout-toggle-story-list"]').click()
cy.get('[data-test-id="layout-modal-close"]').click()
cy.reload()
cy.get('.histoire-base-split-pane').should('not.exist')
})

it('hides the Layout button on mobile', () => {
cy.viewport(375, 667)
cy.get('[data-test-id="layout-btn"]').should('not.exist')
})

it('opens and toggles via keyboard shortcut', () => {
cy.get('body').type('{meta}{shift}l')
cy.get('[data-test-id="layout-modal"]').should('be.visible')
cy.get('body').type('{meta}{shift}l')
cy.get('[data-test-id="layout-modal"]').should('not.be.visible')
})

it('respects per-story meta.storyOptions override', () => {
cy.get('[data-test-id="story-list-item"]').contains('StoryOptions Override').click()
cy.get('[data-test-id="story-side-panel"]').should('not.exist')
cy.get('[data-test-id="story-list-item"]').contains('Controls').click()
cy.get('[data-test-id="story-side-panel"]').should('exist')
})

it('closes the modal via backdrop and Esc', () => {
cy.get('[data-test-id="layout-btn"]').click()
cy.get('[data-test-id="layout-modal"]').should('be.visible')
cy.get('body').click(10, 10)
cy.get('[data-test-id="layout-modal"]').should('not.be.visible')
cy.get('[data-test-id="layout-btn"]').click()
cy.get('body').type('{esc}')
cy.get('[data-test-id="layout-modal"]').should('not.be.visible')
})
})
2 changes: 1 addition & 1 deletion examples/vue3/cypress/e2e/stories-list.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe('Stories list', () => {
it('should display the stories', () => {
cy.clearLocalStorage()
cy.visit('/')
cy.get('[data-test-id="story-list-item"]').should('have.length', 35)
cy.get('[data-test-id="story-list-item"]').should('have.length', 36)
cy.get('[data-test-id="story-list-item"]').contains('🐱 Meow')
cy.get('[data-test-id="story-list-item"]').contains('BaseButton').contains('3') // Variants count
cy.get('[data-test-id="story-list-item"]').contains('Demo')
Expand Down
15 changes: 15 additions & 0 deletions examples/vue3/src/components/StoryOptionsOverride.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts" setup>
const meta = { storyOptions: false }
</script>

<template>
<Story
title="StoryOptions Override"
icon="carbon:no-image"
:meta="meta"
>
<Variant>
<p>This story sets <code>meta.storyOptions: false</code>, which forces the right pane hidden regardless of the global Layout setting.</p>
</Variant>
</Story>
</template>
55 changes: 45 additions & 10 deletions packages/histoire-app/src/app/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ import type { StoryFile, Tree } from './types'
import { useTitle } from '@vueuse/core'
import { onUpdate, files as rawFiles, tree as rawTree } from 'virtual:$histoire-stories'
import { computed, onMounted, ref, watch } from 'vue'
import AppActions from './components/app/AppActions.vue'
import AppHeader from './components/app/AppHeader.vue'
import Breadcrumb from './components/app/Breadcrumb.vue'
import InitialLoading from './components/app/InitialLoading.vue'
import TopBar from './components/app/TopBar.vue'
import BaseSplitPane from './components/base/BaseSplitPane.vue'
import CommandPromptsModal from './components/command/CommandPromptsModal.vue'
import LayoutModal from './components/layout/LayoutModal.vue'
import SearchModal from './components/search/SearchModal.vue'
import GenericMountStory from './components/story/GenericMountStory.vue'
import StoryList from './components/tree/StoryList.vue'
import { useCommandStore } from './stores/command'
import { useLayoutStore } from './stores/layout'
import { useStoryStore } from './stores/story'
import { histoireConfig } from './util/config'
import { onKeyboardShortcut } from './util/keyboard'
Expand Down Expand Up @@ -61,10 +65,9 @@ useTitle(computed(() => {
return histoireConfig.theme.title
}))

// Search

const loadSearch = ref(false)
const isSearchOpen = ref(false)
const isLayoutOpen = ref(false)

watch(isSearchOpen, (value) => {
if (value) {
Expand All @@ -77,6 +80,11 @@ onKeyboardShortcut(['ctrl+k', 'meta+k'], (event) => {
event.preventDefault()
})

onKeyboardShortcut(['ctrl+shift+l', 'meta+shift+l'], (event) => {
isLayoutOpen.value = !isLayoutOpen.value
event.preventDefault()
})
Comment on lines +84 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The dark mode keyboard shortcut (ctrl+shift+d / meta+shift+d) is currently registered in AppHeader.vue. However, with the new layout customization, AppHeader is not rendered when the story list is hidden on desktop (see the v-else block at line 172). This causes the shortcut to stop working when the story list is toggled off.

Please move the dark mode shortcut registration to App.vue alongside the search and layout shortcuts to ensure it remains active globally. You will also need to import toggleDark from ./util/dark.


const loading = ref(false)

if (import.meta.hot && !rawFiles.length) {
Expand All @@ -92,6 +100,7 @@ onMounted(() => {
})

const commandStore = useCommandStore()
const layoutStore = useLayoutStore()
</script>

<template>
Expand All @@ -106,7 +115,7 @@ const commandStore = useCommandStore()
</div>

<div
class="htw-h-screen htw-bg-white dark:htw-bg-gray-700 dark:htw-text-gray-100"
class="htw-h-screen htw-bg-gray-100 dark:htw-bg-gray-750 dark:htw-text-gray-100"
:style="{
// Prevent flash of content
opacity: mounted ? 1 : 0,
Expand All @@ -116,7 +125,14 @@ const commandStore = useCommandStore()
v-if="isMobile"
class="htw-h-full htw-flex htw-flex-col htw-divide-y htw-divide-gray-100 dark:htw-divide-gray-800"
>
<AppHeader @search="isSearchOpen = true" />
<div class="htw-flex htw-items-center htw-gap-2 htw-pr-4">
<AppHeader class="htw-flex-1" />
<AppActions
class="htw-flex-none"
@layout="isLayoutOpen = true"
@search="isSearchOpen = true"
/>
</div>
<Breadcrumb
:tree="tree"
:stories="stories"
Expand All @@ -125,7 +141,7 @@ const commandStore = useCommandStore()
</div>

<BaseSplitPane
v-else
v-else-if="layoutStore.settings.storyListVisible"
save-id="main-horiz"
:min="5"
:max="50"
Expand All @@ -134,10 +150,7 @@ const commandStore = useCommandStore()
>
<template #first>
<div class="htw-flex htw-flex-col htw-h-full htw-bg-gray-100 dark:htw-bg-gray-750 __histoire-pane-shadow-from-right">
<AppHeader
class="htw-flex-none"
@search="isSearchOpen = true"
/>
<AppHeader class="htw-flex-none" />
<StoryList
:tree="tree"
:stories="stories"
Expand All @@ -147,9 +160,31 @@ const commandStore = useCommandStore()
</template>

<template #last>
<RouterView />
<div class="htw-flex htw-flex-col htw-h-full">
<TopBar
@layout="isLayoutOpen = true"
@search="isSearchOpen = true"
/>
<RouterView class="htw-flex-1 htw-min-h-0" />
</div>
</template>
</BaseSplitPane>
<div
v-else
class="htw-h-full htw-flex htw-flex-col"
>
<TopBar
@layout="isLayoutOpen = true"
@search="isSearchOpen = true"
/>
<RouterView class="htw-flex-1 htw-min-h-0" />
</div>

<LayoutModal
v-if="!isMobile"
:shown="isLayoutOpen"
@close="isLayoutOpen = false"
/>

<SearchModal
v-if="loadSearch"
Expand Down
58 changes: 58 additions & 0 deletions packages/histoire-app/src/app/components/app/AppActions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { computed } from 'vue'
import { histoireConfig } from '../../util/config'
import { isDark, toggleDark } from '../../util/dark'
import { isMobile } from '../../util/responsive'
import { makeTooltip } from '../../util/tooltip'
import TopBarChip from './TopBarChip.vue'
import TopBarChipItem from './TopBarChipItem.vue'

defineEmits({
search: () => true,
layout: () => true,
})

const themeIcon = computed(() => {
return isDark.value ? 'carbon:moon' : 'carbon:sun'
})
</script>

<template>
<TopBarChip>
<TopBarChipItem
v-if="!isMobile"
v-tooltip="makeTooltip('Layout', ({ isMac }) => isMac ? 'meta+shift+l' : 'ctrl+shift+l')"
aria-label="Open layout customization"
data-test-id="layout-btn"
@click="$emit('layout')"
>
<Icon
icon="carbon:panel-expansion"
class="htw-w-4 htw-h-4"
/>
</TopBarChipItem>

<TopBarChipItem
v-tooltip="makeTooltip('Search', ({ isMac }) => isMac ? 'meta+k' : 'ctrl+k')"
data-test-id="search-btn"
@click="$emit('search')"
>
<Icon
icon="carbon:search"
class="htw-w-4 htw-h-4"
/>
</TopBarChipItem>

<TopBarChipItem
v-if="!histoireConfig.theme.hideColorSchemeSwitch"
v-tooltip="makeTooltip('Toggle dark mode', ({ isMac }) => isMac ? 'meta+shift+d' : 'ctrl+shift+d')"
@click="toggleDark()"
>
<Icon
:icon="themeIcon"
class="htw-w-4 htw-h-4"
/>
</TopBarChipItem>
</TopBarChip>
</template>
40 changes: 2 additions & 38 deletions packages/histoire-app/src/app/components/app/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { computed } from 'vue'
import { histoireConfig } from '../../util/config'
import { isDark, toggleDark } from '../../util/dark'
import { toggleDark } from '../../util/dark'
import { onKeyboardShortcut } from '../../util/keyboard'
import { makeTooltip } from '../../util/tooltip'
import AppLogo from './AppLogo.vue'

defineEmits({
search: () => true,
})

const themeIcon = computed(() => {
return isDark.value ? 'carbon:moon' : 'carbon:sun'
})

onKeyboardShortcut(['ctrl+shift+d', 'meta+shift+d'], (event) => {
event.preventDefault()
toggleDark()
Expand All @@ -23,7 +12,7 @@ onKeyboardShortcut(['ctrl+shift+d', 'meta+shift+d'], (event) => {

<template>
<div
class="histoire-app-header htw-px-4 htw-h-16 htw-flex htw-items-center htw-gap-2"
class="histoire-app-header htw-px-4 htw-h-14 htw-flex htw-items-center htw-gap-2"
>
<div class="htw-py-3 sm:htw-py-4 htw-flex-1 htw-h-full htw-flex htw-items-center htw-pr-2">
<a
Expand All @@ -36,30 +25,5 @@ onKeyboardShortcut(['ctrl+shift+d', 'meta+shift+d'], (event) => {
/>
</a>
</div>
<div class="htw-ml-auto htw-flex-none htw-flex">
<a
v-tooltip="makeTooltip('Search', ({ isMac }) => isMac ? 'meta+k' : 'ctrl+k')"
class="htw-p-2 sm:htw-p-1 hover:htw-text-primary-500 dark:hover:htw-text-primary-400 htw-cursor-pointer htw-text-gray-900 dark:htw-text-gray-100"
data-test-id="search-btn"
@click="$emit('search')"
>
<Icon
icon="carbon:search"
class="htw-w-6 htw-h-6 sm:htw-w-4 sm:htw-h-4"
/>
</a>

<a
v-if="!histoireConfig.theme.hideColorSchemeSwitch"
v-tooltip="makeTooltip('Toggle dark mode', ({ isMac }) => isMac ? 'meta+shift+d' : 'ctrl+shift+d')"
class="htw-p-2 sm:htw-p-1 hover:htw-text-primary-500 dark:hover:htw-text-primary-400 htw-cursor-pointer htw-text-gray-900 dark:htw-text-gray-100"
@click="toggleDark()"
>
<Icon
:icon="themeIcon"
class="htw-w-6 htw-h-6 sm:htw-w-4 sm:htw-h-4"
/>
</a>
</div>
</div>
</template>
Loading
Loading