diff --git a/examples/vue3/cypress/e2e/layout.cy.js b/examples/vue3/cypress/e2e/layout.cy.js new file mode 100644 index 00000000..4866cb14 --- /dev/null +++ b/examples/vue3/cypress/e2e/layout.cy.js @@ -0,0 +1,77 @@ +/// + +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') + }) +}) diff --git a/examples/vue3/cypress/e2e/search.cy.js b/examples/vue3/cypress/e2e/search.cy.js index 7ec5a989..fe64d32d 100644 --- a/examples/vue3/cypress/e2e/search.cy.js +++ b/examples/vue3/cypress/e2e/search.cy.js @@ -7,12 +7,12 @@ describe('Search', () => { cy.get('[data-test-id="search-modal"] input').type('Demo') cy.get('[data-test-id="search-item"]').should('have.length.greaterThan', 3) cy.get('[data-test-id="search-item"]').contains('untitled').click() - cy.get('[data-test-id="story-variant-single-view"]').contains('untitled') + cy.get('.histoire-toolbar-title').contains('untitled') cy.get('[data-test-id="search-btn"]').click() cy.get('[data-test-id="search-modal"] input').type('variant 2') cy.contains('[data-test-id="search-item"][data-selected]', 'Variant 2') cy.get('[data-test-id="search-modal"] input').type('{enter}') - cy.get('[data-test-id="story-variant-single-view"]').contains('Variant 2') + cy.get('.histoire-toolbar-title').contains('Variant 2') }) it('should handle keyboard navigation', () => { @@ -21,13 +21,13 @@ describe('Search', () => { cy.get('[data-test-id="search-modal"] input').type('Demo') cy.contains('[data-test-id="search-item"][data-selected]', 'Demo') cy.get('[data-test-id="search-modal"] input').type('{downArrow}{enter}') - cy.get('[data-test-id="story-variant-single-view"]').contains('untitled') + cy.get('.histoire-toolbar-title').contains('untitled') cy.get('[data-test-id="search-btn"]').click() cy.get('[data-test-id="search-modal"] input').clear() cy.get('[data-test-id="search-modal"] input').type('Demo') cy.contains('[data-test-id="search-item"][data-selected]', 'Demo') cy.get('[data-test-id="search-modal"] input').type('{downArrow}{downArrow}{enter}') - cy.get('[data-test-id="story-variant-single-view"]').contains('Variant 2') + cy.get('.histoire-toolbar-title').contains('Variant 2') }) it('should close', () => { diff --git a/examples/vue3/cypress/e2e/stories-list.cy.js b/examples/vue3/cypress/e2e/stories-list.cy.js index 7a284eb1..654cd9ad 100644 --- a/examples/vue3/cypress/e2e/stories-list.cy.js +++ b/examples/vue3/cypress/e2e/stories-list.cy.js @@ -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') diff --git a/examples/vue3/cypress/e2e/story-preview.cy.js b/examples/vue3/cypress/e2e/story-preview.cy.js index 766db5c6..a8e1b53e 100644 --- a/examples/vue3/cypress/e2e/story-preview.cy.js +++ b/examples/vue3/cypress/e2e/story-preview.cy.js @@ -11,7 +11,7 @@ describe('Story preview', () => { cy.get('[data-test-id="story-list-item"]').contains('Demo').click() cy.get('[data-test-id="story-variant-list-item"]').should('have.length', 2) cy.get('[data-test-id="story-variant-list-item"]').contains('untitled').click() - cy.get('[data-test-id="story-variant-single-view"]').contains('untitled') + cy.get('.histoire-toolbar-title').contains('untitled') getIframeBody().contains('Hello world!') cy.get('[data-test-id="story-source-code"]').should('have.text', '') }) @@ -21,7 +21,7 @@ describe('Story preview', () => { cy.get('[data-test-id="story-list-item"]').contains('Demo').click() cy.get('[data-test-id="story-variant-list-item"]').should('have.length', 2) cy.get('[data-test-id="story-variant-list-item"]').contains('Variant 2').click() - cy.get('[data-test-id="story-variant-single-view"]').contains('Variant 2') + cy.get('.histoire-toolbar-title').contains('Variant 2') getIframeBody().contains('Meow!') cy.get('[data-test-id="story-source-code"]').should('have.text', '') }) diff --git a/examples/vue3/src/components/StoryOptionsOverride.story.vue b/examples/vue3/src/components/StoryOptionsOverride.story.vue new file mode 100644 index 00000000..07c26695 --- /dev/null +++ b/examples/vue3/src/components/StoryOptionsOverride.story.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/histoire-app/src/app/App.vue b/packages/histoire-app/src/app/App.vue index ab4c95ed..7c28bc0c 100644 --- a/packages/histoire-app/src/app/App.vue +++ b/packages/histoire-app/src/app/App.vue @@ -9,17 +9,22 @@ 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 { toggleDark } from './util/dark' import { onKeyboardShortcut } from './util/keyboard' import { mapFile } from './util/mapping' import { isMobile } from './util/responsive' @@ -61,10 +66,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) { @@ -77,6 +81,16 @@ onKeyboardShortcut(['ctrl+k', 'meta+k'], (event) => { event.preventDefault() }) +onKeyboardShortcut(['ctrl+shift+l', 'meta+shift+l'], (event) => { + isLayoutOpen.value = !isLayoutOpen.value + event.preventDefault() +}) + +onKeyboardShortcut(['ctrl+shift+d', 'meta+shift+d'], (event) => { + event.preventDefault() + toggleDark() +}) + const loading = ref(false) if (import.meta.hot && !rawFiles.length) { @@ -92,6 +106,7 @@ onMounted(() => { }) const commandStore = useCommandStore() +const layoutStore = useLayoutStore()