Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 0 additions & 60 deletions app/components/Button/Base.stories.ts

This file was deleted.

7 changes: 7 additions & 0 deletions app/components/Button/Base.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<script setup lang="ts">
import type { IconClass } from '~/types'

/**
* A base button component that supports multiple variants, sizes, and states as well as icons and keyboard shortcuts.
*/
defineOptions({
name: 'ButtonBase',
})

const props = withDefaults(
defineProps<{
disabled?: boolean
Expand Down
116 changes: 116 additions & 0 deletions app/components/Button/ButtonBase.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
import ButtonBase from './Base.vue'

const meta = {
component: ButtonBase,
parameters: {
docs: {
source: {
type: 'dynamic',
transform: (code: string) =>
code.replace(/<Base\b/g, '<ButtonBase').replace(/<\/Base>/g, '</ButtonBase>'),
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof ButtonBase>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
default: 'Button Text',
},
}
Comment on lines +21 to +25
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 6, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default story may not render slot content without a render function.

All other stories use explicit render functions with templates to handle slot content, but Default relies on args.default without a render function. In Vue Storybook, this pattern typically requires either a render function or specific argTypes configuration to pass slot content properly.

Consider adding a render function for consistency and to ensure the button text displays:

Proposed fix
 export const Default: Story = {
-  args: {
-    default: 'Button Text',
-  },
+  render: args => ({
+    components: { ButtonBase },
+    setup() {
+      return { args }
+    },
+    template: `<ButtonBase v-bind="args">Button Text</ButtonBase>`,
+  }),
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const Default: Story = {
args: {
default: 'Button Text',
},
}
export const Default: Story = {
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">Button Text</ButtonBase>`,
}),
}

Copy link
Contributor Author

@cylewaitforit cylewaitforit Mar 6, 2026

Choose a reason for hiding this comment

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

Keeping the default story with the default render allow to someone viewing the storybook to change the story to alternative text in place of Button Text. The other stories use the template render in order for the internationalization to work.

Copy link
Contributor

Choose a reason for hiding this comment

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

@cylewaitforit, that makes perfect sense! Using args.default without a render function is the correct approach here — it exposes the slot content as a Storybook control, allowing users to interactively edit the button text directly from the controls panel. The render function approach in the other stories is needed for i18n to work, but would prevent that interactive editing for the Default story. Thanks for the clarification!

ʕ •`ᴥ´• ʔ


✏️ Learnings added
Learnt from: cylewaitforit
Repo: npmx-dev/npmx.dev PR: 1964
File: app/components/Button/ButtonBase.stories.ts:21-25
Timestamp: 2026-03-06T18:16:31.538Z
Learning: In the npmx.dev project's ButtonBase.stories.ts (app/components/Button/ButtonBase.stories.ts), the `Default` story intentionally uses `args.default` without a render function so that Storybook users can interactively edit the slot content (button text) via the controls panel. Other stories (e.g. Primary, Secondary, etc.) use explicit render functions with templates specifically to support storybook-i18n internationalization. Do not flag the `Default` story's lack of a render function as an issue.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: userquin
Repo: npmx-dev/npmx.dev PR: 1335
File: app/components/Compare/FacetSelector.vue:72-78
Timestamp: 2026-02-10T15:47:40.710Z
Learning: In the npmx.dev project, the `ButtonBase` component (app/components/ButtonBase.vue or similar) provides default `border border-border` styles. When styling ButtonBase instances, avoid adding duplicate `border` classes as this triggers the HTML validator error `no-dup-class` and fails CI.

Learnt from: cylewaitforit
Repo: npmx-dev/npmx.dev PR: 1964
File: .storybook/preview-head.html:1-6
Timestamp: 2026-03-06T17:44:59.085Z
Learning: In the npmx.dev project, the `.storybook/preview-head.html` file sets `background-color` on `.docs-story` via the `--bg` CSS custom property to fix the Autodocs canvas background. This is an intentional workaround for a known Storybook bug (storybookjs/storybook#30928) where `addon-themes` and `parameters.docs.theme` do not apply to the individual story canvas iframes in the Autodocs tab. Removing this file causes the autodocs pages to not display correctly.


export const Primary: Story = {
args: {
variant: 'primary',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
}),
}

export const Secondary: Story = {
args: {
variant: 'secondary',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
}),
}

export const Small: Story = {
args: {
size: 'small',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
}),
}

export const Disabled: Story = {
args: {
disabled: true,
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
}),
}

export const WithIcon: Story = {
args: {
classicon: 'i-lucide:search',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">{{ $t("search.button") }}</ButtonBase>`,
}),
}

export const WithKeyboardShortcut: Story = {
args: {
ariaKeyshortcuts: '/',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">{{ $t("search.button") }}</ButtonBase>`,
}),
}

export const Block: Story = {
args: {
block: true,
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: `<ButtonBase v-bind="args">{{ $t("nav.settings") }}</ButtonBase>`,
}),
}
106 changes: 106 additions & 0 deletions app/components/Button/ButtonGroup.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
import ButtonBase from './Base.vue'
import ButtonGroup from './Group.vue'

const meta = {
component: ButtonGroup,
parameters: {
docs: {
source: {
type: 'dynamic',
transform: (code: string) =>
code
.replace(/<Base\b/g, '<ButtonBase')
.replace(/<\/Base>/g, '</ButtonBase>')
.replace(/<Group\b/g, '<ButtonGroup')
.replace(/<\/Group>/g, '</ButtonGroup>'),
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof ButtonGroup>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
parameters: {
docs: {
source: {
code: `
<template>
<ButtonGroup>
<ButtonBase>Back</ButtonBase>
<ButtonBase>Settings</ButtonBase>
<ButtonBase>Compare</ButtonBase>
</ButtonGroup>
</template>
`,
},
},
},
render: () => ({
components: { ButtonBase, ButtonGroup },
template: `
<ButtonGroup>
<ButtonBase>{{ $t('nav.back') }}</ButtonBase>
<ButtonBase>{{ $t('nav.settings') }}</ButtonBase>
<ButtonBase>{{ $t('nav.compare') }}</ButtonBase>
</ButtonGroup>
`,
}),
}

export const WithVariants: Story = {
parameters: {
docs: {
source: {
code: `
<template>
<ButtonGroup>
<ButtonBase variant="primary">Back</ButtonBase>
<ButtonBase variant="primary">Settings</ButtonBase>
<ButtonBase variant="primary">Compare</ButtonBase>
</ButtonGroup>
</template>
`,
},
},
},
render: () => ({
components: { ButtonBase, ButtonGroup },
template: `
<ButtonGroup>
<ButtonBase variant="primary">{{ $t('nav.back') }}</ButtonBase>
<ButtonBase variant="primary">{{ $t('nav.settings') }}</ButtonBase>
<ButtonBase variant="primary">{{ $t('nav.compare') }}</ButtonBase>
</ButtonGroup>
`,
}),
}

export const WithIcons: Story = {
parameters: {
docs: {
source: {
code: `
<template>
<ButtonGroup>
<ButtonBase variant="secondary" classicon="i-lucide:x">Close</ButtonBase>
<ButtonBase variant="secondary" classicon="i-lucide:search">Search</ButtonBase>
</ButtonGroup>
</template>
`,
},
},
},
render: () => ({
components: { ButtonBase, ButtonGroup },
template: `
<ButtonGroup>
<ButtonBase variant="secondary" classicon="i-lucide:x">{{ $t('common.close') }}</ButtonBase>
<ButtonBase variant="secondary" classicon="i-lucide:search">{{ $t('filters.search') }}</ButtonBase>
</ButtonGroup>
`,
}),
}
7 changes: 7 additions & 0 deletions app/components/Button/Group.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<script setup lang="ts">
/**
* Use `ButtonGroup` to group multiple buttons together with connected borders.
*/
defineOptions({
name: 'ButtonGroup',
})
const props = defineProps<{
as?: string | Component
}>()
Expand Down
Loading