import type { Meta, StoryObj } from '@storybook/vue3-vite' import { computed, ref } from 'vue' import CpButton from '@/components/CpButton.vue' import CpTextMorph from '@/components/CpTextMorph.vue' const meta = { title: 'Transitions/CpTextMorph', component: CpTextMorph, argTypes: { text: { control: 'text', description: 'Text to display. Changing it morphs from the previous value.', }, duration: { control: 'number', description: 'Duration of the morph in milliseconds.', }, disabled: { control: 'boolean', description: 'Whether the morph is disabled.', }, as: { control: 'text', description: 'HTML tag to render the component as.', }, }, args: { duration: 400, disabled: false, as: 'span', }, } satisfies Meta export default meta type Story = StoryObj const wrapperStyle = 'display: flex; flex-direction: column; align-items: flex-start; gap: 24px;' const singleLineStyle = 'font-size: 2rem; font-weight: 600; line-height: 1.2;' const multiLineStyle = 'font-size: 1.25rem; line-height: 1.6;' const singleLineTexts = ['Search for flights', 'Choose your seat'] /** * Single line text. Use the button to swap the value and watch the * characters morph from one label to the other. */ export const Default: Story = { args: { text: singleLineTexts[0], }, render: (args) => ({ components: { CpTextMorph, CpButton }, setup() { const isMorphed = ref(false) const text = computed(() => (isMorphed.value ? singleLineTexts[1] : args.text)) const toggle = () => { isMorphed.value = !isMorphed.value } const buttonLabel = computed(() => (isMorphed.value ? 'Unmorph text' : 'Morph text')) return { args, text, toggle, buttonLabel } }, template: `
`, }), } /** Lines are explicit: torph keeps its content on a single line unless the value contains newlines. */ const multiLineTexts = [ [ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,', 'sed do eiusmod tempor incididunt ut labore et dolore', 'magna aliqua veniam quis nostrud.', ].join('\n'), [ 'Lorem ipsum dolor sit amet, quis nostrud exercitation', 'ullamco laboris nisi ut aliquip ex ea commodo dolore', 'magna aliqua consectetur adipiscing elit.', ].join('\n'), ] /** * Multi line paragraph. Both values share most of their words, so the * morph mostly moves existing words to their new position across lines * instead of fading everything in and out. */ export const MultiLine: Story = { args: { text: multiLineTexts[0], }, render: (args) => ({ components: { CpTextMorph, CpButton }, setup() { const isMorphed = ref(false) const text = computed(() => (isMorphed.value ? multiLineTexts[1] : args.text)) const toggle = () => { isMorphed.value = !isMorphed.value } return { args, text, toggle } }, template: `
Morph text
`, }), } const bookingSteps = [ { title: 'Select your flight', fields: ['Paris (CDG) → London (LHR)', 'Wednesday 19 August, 09:45', 'Economy, 1 passenger'], }, { title: 'Select your seat', fields: ['Seat 14A, window', 'Extra legroom', 'Front of the cabin'], }, { title: 'Confirm your booking', fields: ['Paris (CDG) → London (LHR)', 'Seat 14A, window', 'Total: 189,00 €'], }, ] const dialogFooterStyle = 'display: flex; justify-content: space-between; gap: 8px; width: 100%;' const dialogFieldsStyle = 'display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none;' /** * Use case — a multi step dialog whose title morphs on every step * instead of being swapped instantly. The step titles share words on * purpose ("Select your …"), so only the changing part animates while * the rest stays in place. * * The title is wrapped in an `h2` inside the `title` slot to keep the * dialog heading semantics. Around it, several transitions run together: * `CpTransitionCounter` rolls the step number, `CpTransitionSize` * animates the width of the whole "Step 1 of 3" subtitle and the body * height, and the footer keeps a single action button whose label morphs * from "Next" to "Confirm" while its width follows. */ export const MultiStepDialog: Story = { args: { text: bookingSteps[0].title, }, render: (args) => ({ components: { CpTextMorph }, setup() { const isOpen = ref(false) const stepIndex = ref(0) const step = computed(() => bookingSteps[stepIndex.value]) const stepLabel = computed(() => `Step ${stepIndex.value + 1} of ${bookingSteps.length}`) const isLastStep = computed(() => stepIndex.value === bookingSteps.length - 1) const nextLabel = computed(() => (isLastStep.value ? 'Confirm' : 'Next')) const open = () => { stepIndex.value = 0 isOpen.value = true } const close = () => { isOpen.value = false } const goToNextStep = () => { if (isLastStep.value) { close() return } stepIndex.value++ } return { args, isOpen, stepIndex, step, stepLabel, nextLabel, open, close, goToNextStep, } }, template: ` Book a flight
  • {{ field }}
`, }), }