diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts index be966678..3c6fd972 100644 --- a/next-ui/src/components.d.ts +++ b/next-ui/src/components.d.ts @@ -49,6 +49,7 @@ declare module 'vue' { ImportBooksTransientBooksTable: typeof import('./components/import/books/TransientBooksTable.vue')['default'] ImportReadlistTable: typeof import('./components/import/readlist/Table.vue')['default'] ItemCard: typeof import('./components/item/card/ItemCard.vue')['default'] + ItemCardWide: typeof import('./components/item/CardWide/ItemCardWide.vue')['default'] LayoutAppBar: typeof import('./components/layout/app/Bar.vue')['default'] LayoutAppBarHolder: typeof import('./components/layout/app/BarHolder.vue')['default'] LayoutAppDrawer: typeof import('./components/layout/app/drawer/Drawer.vue')['default'] @@ -86,6 +87,7 @@ declare module 'vue' { RouterView: typeof import('vue-router')['RouterView'] SelectionBar: typeof import('./components/selection/Bar.vue')['default'] SeriesCard: typeof import('./components/series/card/SeriesCard.vue')['default'] + SeriesCardWide: typeof import('./components/series/CardWide/SeriesCardWide.vue')['default'] SeriesDeletionWarning: typeof import('./components/series/DeletionWarning.vue')['default'] SeriesFormEditMetadata: typeof import('./components/series/form/EditMetadata.vue')['default'] SeriesMenu: typeof import('./components/series/menu/SeriesMenu.vue')['default'] diff --git a/next-ui/src/components/item/CardWide/ItemCardWide.stories.ts b/next-ui/src/components/item/CardWide/ItemCardWide.stories.ts new file mode 100644 index 00000000..8c07ed74 --- /dev/null +++ b/next-ui/src/components/item/CardWide/ItemCardWide.stories.ts @@ -0,0 +1,166 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import ItemCardWide from './ItemCardWide.vue' +import { seriesThumbnailUrl } from '@/api/images' +import { delay, http } from 'msw' +import { fn } from 'storybook/test' + +const meta = { + component: ItemCardWide, + render: (args: object) => ({ + components: { ItemCardWide }, + setup() { + return { args } + }, + template: '', + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + docs: { + description: { + component: 'A flexible card that serves as the base for entity cards.', + }, + }, + }, + args: { + title: 'Card title', + text: 'Card content', + posterUrl: seriesThumbnailUrl('id'), + width: 150, + onSelection: fn(), + onClickFab: fn(), + onClickQuickAction: fn(), + onClickMenu: fn(), + preSelect: false, + selected: false, + stretchPoster: true, + menuIcon: 'i-mdi:menu', + quickActionIcon: 'i-mdi:pencil', + }, + argTypes: { + posterUrl: { + options: [seriesThumbnailUrl('id'), seriesThumbnailUrl('idL')], + control: { type: 'radio' }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + stretchPoster: true, + }, +} + +export const LongText: Story = { + args: { + title: + 'The Fractured World of Miravara this is a super long title that should overshoot the container, but that should still be alright because it should wrap nicely', + text: `The story unfolds on Miravara, a planet encased within a colossal, translucent barrier known as the Glass Frontier. For centuries, this barrier has protected the populace from a dying sun, filtering its radiation into usable power. Entire cities cling to its surface like glowing barnacles—each one a hybrid of sleek crystal towers and decaying steel foundations. Below the barrier lies the Underglow, a vast labyrinth of machinery and forgotten people who maintain the world’s vital energy grid, unseen by those above. + +Politically, Miravara is divided between the High Dominion—wealthy citizens who live near the surface of the barrier—and the Conduits, a laboring caste living in the Underglow. Above them all looms the enigmatic Council of Luminarchs, a secretive group rumored to communicate directly with the Glass Frontier itself.`, + stretchPoster: true, + }, +} + +export const TopRightCount: Story = { + args: { + topRight: 24, + }, +} + +export const TopRightIcon: Story = { + args: { + topRightIcon: 'i-mdi:check', + }, +} + +export const LandscapeStretched: Story = { + args: { + topRightIcon: 'i-mdi:check', + posterUrl: seriesThumbnailUrl('idL'), + stretchPoster: true, + }, +} + +export const LandscapeNotStretched: Story = { + args: { + topRightIcon: 'i-mdi:check', + posterUrl: seriesThumbnailUrl('idL'), + stretchPoster: false, + }, +} + +export const QuickActionIcon: Story = { + args: { + quickActionIcon: 'i-mdi:pencil', + }, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} + +export const MenuIcon: Story = { + args: { + menuIcon: 'i-mdi:menu', + }, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} + +export const SelectableHover: Story = { + args: {}, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} + +export const DisableSelection: Story = { + args: { + disableSelection: true, + }, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} + +export const Selected: Story = { + args: { + selected: true, + }, +} + +export const PreSelect: Story = { + args: { + preSelect: true, + }, +} + +export const Progress: Story = { + args: { + progressPercent: 33, + }, +} + +export const Big: Story = { + args: { + width: 300, + }, +} + +export const PosterError: Story = { + args: { + posterUrl: '/error', + }, +} + +export const Loading: Story = { + parameters: { + msw: { + handlers: [http.all('*/api/*', async () => await delay(5_000))], + }, + }, +} diff --git a/next-ui/src/components/item/CardWide/ItemCardWide.vue b/next-ui/src/components/item/CardWide/ItemCardWide.vue new file mode 100644 index 00000000..2e6d21aa --- /dev/null +++ b/next-ui/src/components/item/CardWide/ItemCardWide.vue @@ -0,0 +1,235 @@ + + + + + diff --git a/next-ui/src/components/item/card/ItemCard.vue b/next-ui/src/components/item/card/ItemCard.vue index e8c7292a..39e80bc5 100644 --- a/next-ui/src/components/item/card/ItemCard.vue +++ b/next-ui/src/components/item/card/ItemCard.vue @@ -65,7 +65,7 @@ transition="fade-transition" content-class="fill-height w-100" > - + ', + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + docs: { + description: { + component: '', + }, + }, + }, + args: { + series: mockSeries1, + onSelection: fn(), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} +export const Read: Story = { + args: { + series: { + ...mockSeries1, + booksCount: 5, + booksReadCount: 5, + booksUnreadCount: 0, + booksInProgressCount: 0, + }, + }, +} + +export const Oneshot: Story = { + args: { + series: { + ...mockSeries1, + oneshot: true, + }, + }, +} + +export const Deleted: Story = { + args: { + series: { + ...mockSeries1, + deleted: true, + }, + }, +} + +export const Selected: Story = { + args: { + selected: true, + }, +} + +export const Hover: Story = { + args: {}, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} + +export const HoverNonAdmin: Story = { + args: {}, + parameters: { + msw: { + handlers: [ + httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)), + ], + }, + }, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} diff --git a/next-ui/src/components/series/CardWide/SeriesCardWide.vue b/next-ui/src/components/series/CardWide/SeriesCardWide.vue new file mode 100644 index 00000000..c2716a8b --- /dev/null +++ b/next-ui/src/components/series/CardWide/SeriesCardWide.vue @@ -0,0 +1,67 @@ + + + diff --git a/next-ui/src/pages/libraries/[id]/series.vue b/next-ui/src/pages/libraries/[id]/series.vue index 92c1fab5..57217976 100644 --- a/next-ui/src/pages/libraries/[id]/series.vue +++ b/next-ui/src/pages/libraries/[id]/series.vue @@ -3,7 +3,7 @@ @@ -87,15 +100,20 @@ import { useItemsPerPage, usePagination } from '@/composables/pagination' import { useSearchConditionLibraries } from '@/composables/search' import { storeToRefs } from 'pinia' import { useSelectionStore } from '@/stores/selection' +import { useDisplay } from 'vuetify' const route = useRoute('/libraries/[id]/series') const libraryId = route.params.id const { libraries } = useGetLibrariesById(libraryId) const { librariesCondition } = useSearchConditionLibraries(libraries) +const display = useDisplay() const appStore = useAppStore() const { browsingPageSize } = storeToRefs(appStore) const presentationMode = appStore.getPresentationMode(`${libraryId}_series`, 'grid') +const presentationModeEffective = computed(() => + display.xs.value ? 'grid' : presentationMode.value, +) const { itemsPerPage } = useItemsPerPage(browsingPageSize) const { page0, page1, pageCount } = usePagination()