You've already forked pages
forked from didirus/AstralRinth
devex: storybook for UI Package (#4984)
* add storybook * clean up stories * small fix * add stories for all components * add vintl * default to dark mode * fix teleport * add theme addon * add new modal story * delete broken stories * move all stories to central stories folder * fix paths * add pnpm run storybook * remove chromatic * add add-stories.md * fix types * fix unncessary args field * cover more addordion states * pt2 * remove old vintl * fix: missing style + ctx --------- Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -68,3 +68,6 @@ app-playground-data/*
|
||||
|
||||
# labrinth demo fixtures
|
||||
apps/labrinth/fixtures/demo
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"prepr:frontend:lib": "turbo run prepr --filter=@modrinth/ui --filter=@modrinth/moderation --filter=@modrinth/assets --filter=@modrinth/blog --filter=@modrinth/api-client --filter=@modrinth/utils --filter=@modrinth/tooling-config",
|
||||
"prepr:frontend:web": "turbo run prepr --filter=@modrinth/frontend",
|
||||
"prepr:frontend:app": "turbo run prepr --filter=@modrinth/app-frontend",
|
||||
"storybook": "pnpm --filter @modrinth/ui storybook",
|
||||
"icons:add": "pnpm --filter @modrinth/assets icons:add",
|
||||
"scripts": "node scripts/run.mjs"
|
||||
},
|
||||
|
||||
17
packages/ui/.storybook/main.ts
Normal file
17
packages/ui/.storybook/main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { StorybookConfig } from '@storybook/vue3-vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
framework: '@storybook/vue3-vite',
|
||||
core: {
|
||||
builder: '@storybook/builder-vite',
|
||||
},
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-themes',
|
||||
'@storybook/addon-vitest',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-onboarding',
|
||||
],
|
||||
}
|
||||
export default config
|
||||
80
packages/ui/.storybook/preview.ts
Normal file
80
packages/ui/.storybook/preview.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import '@modrinth/assets/omorphia.scss'
|
||||
import 'floating-vue/dist/style.css'
|
||||
import '../src/styles/tailwind.css'
|
||||
|
||||
import { withThemeByClassName } from '@storybook/addon-themes'
|
||||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import { setup } from '@storybook/vue3-vite'
|
||||
import FloatingVue from 'floating-vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
buildLocaleMessages,
|
||||
createMessageCompiler,
|
||||
type CrowdinMessages,
|
||||
} from '../src/composables/i18n'
|
||||
|
||||
// Load locale messages from the UI package's locales
|
||||
// @ts-ignore
|
||||
const localeModules = import.meta.glob('../src/locales/*/index.json', {
|
||||
eager: true,
|
||||
}) as Record<string, { default: CrowdinMessages }>
|
||||
|
||||
// Set up vue-i18n for Storybook - provides useVIntl() context for components
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en-US',
|
||||
fallbackLocale: 'en-US',
|
||||
messageCompiler: createMessageCompiler(),
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: buildLocaleMessages(localeModules),
|
||||
})
|
||||
|
||||
setup((app) => {
|
||||
app.use(i18n)
|
||||
app.use(FloatingVue, {
|
||||
themes: {
|
||||
'ribbit-popout': {
|
||||
$extend: 'dropdown',
|
||||
placement: 'bottom-end',
|
||||
instantMove: true,
|
||||
distance: 8,
|
||||
},
|
||||
'dismissable-prompt': {
|
||||
$extend: 'dropdown',
|
||||
placement: 'bottom-start',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Create teleport target for components that use <Teleport to="#teleports">
|
||||
if (typeof document !== 'undefined' && !document.getElementById('teleports')) {
|
||||
const teleportTarget = document.createElement('div')
|
||||
teleportTarget.id = 'teleports'
|
||||
document.body.appendChild(teleportTarget)
|
||||
}
|
||||
})
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
withThemeByClassName({
|
||||
themes: {
|
||||
light: 'light-mode',
|
||||
dark: 'dark-mode',
|
||||
oled: 'oled-mode',
|
||||
},
|
||||
defaultTheme: 'dark',
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
export default preview
|
||||
@@ -1,2 +1,15 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config
|
||||
import baseConfig from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
|
||||
export default baseConfig.append([
|
||||
{
|
||||
name: 'storybook',
|
||||
files: ['**/*.stories.@(js|jsx|ts|tsx)', '**/.storybook/**/*.@(js|ts)'],
|
||||
plugins: {
|
||||
storybook,
|
||||
},
|
||||
rules: {
|
||||
...storybook.configs.recommended.rules,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/ui",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
@@ -18,14 +19,34 @@
|
||||
"scripts": {
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"src/**/*.d.ts\" --out-file src/locales/en-US/index.json --preserve-whitespace"
|
||||
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"src/**/*.d.ts\" --out-file src/locales/en-US/index.json --preserve-whitespace",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"@storybook/addon-a11y": "^10.1.10",
|
||||
"@storybook/addon-docs": "^10.1.10",
|
||||
"@storybook/addon-onboarding": "^10.1.10",
|
||||
"@storybook/addon-themes": "^10.1.10",
|
||||
"@storybook/addon-vitest": "^10.1.10",
|
||||
"@storybook/builder-vite": "^10.1.10",
|
||||
"@storybook/vue3-vite": "^10.1.10",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitest/browser-playwright": "^4.0.16",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"eslint-plugin-storybook": "^10.1.10",
|
||||
"playwright": "^1.57.0",
|
||||
"storybook": "^10.1.10",
|
||||
"stripe": "^18.1.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.4.6",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitest": "^4.0.16",
|
||||
"vue": "^3.5.13",
|
||||
"vue-component-type-helpers": "^3.1.8",
|
||||
"vue-router": "^4.6.0"
|
||||
@@ -47,6 +68,7 @@
|
||||
"@tresjs/post-processing": "^2.4.0",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/how-ago": "^3.0.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"apexcharts": "^4.0.0",
|
||||
"dayjs": "^1.11.10",
|
||||
|
||||
6
packages/ui/postcss.config.js
Normal file
6
packages/ui/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
292
packages/ui/src/stories/add-stories.md
Normal file
292
packages/ui/src/stories/add-stories.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Storybook Story Creation Guide
|
||||
|
||||
This document provides instructions for AI assistants when creating Storybook stories for Vue components in the `@modrinth/ui` package.
|
||||
|
||||
## File Location
|
||||
|
||||
Stories should be placed in a `stories` subdirectory next to the component's directory:
|
||||
|
||||
- Component: `src/components/base/MyComponent.vue`
|
||||
- Story: `src/stories/base/MyComponent.stories.ts`
|
||||
|
||||
Example with modal components:
|
||||
|
||||
- Component: `src/components/modal/MyModal.vue`
|
||||
- Story: `src/stories/modal/MyModal.stories.ts`
|
||||
|
||||
## Basic Story Structure
|
||||
|
||||
```typescript
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import MyComponent from '../../components/base/MyComponent.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/MyComponent', // Category/ComponentName
|
||||
component: MyComponent,
|
||||
} satisfies Meta<typeof MyComponent>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Default prop values
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
### 1. Let Storybook Auto-Infer Props
|
||||
|
||||
**DO NOT** manually define `argTypes`.
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Don't include prop types
|
||||
const meta = {
|
||||
argTypes: {
|
||||
size: { control: 'select', options: ['small', 'medium', 'large'] },
|
||||
},
|
||||
}
|
||||
|
||||
// ✅ GOOD - Let Storybook infer from component
|
||||
const meta = {
|
||||
component: MyComponent,
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Render Functions for Slot Content
|
||||
|
||||
When a component uses slots, provide a render function:
|
||||
|
||||
```typescript
|
||||
const meta = {
|
||||
component: Accordion,
|
||||
render: (args) => ({
|
||||
components: { Accordion },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Accordion v-bind="args">
|
||||
<template #title>Click to expand</template>
|
||||
<p>Accordion content here.</p>
|
||||
</Accordion>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof Accordion>
|
||||
```
|
||||
|
||||
### 3. Keep Stories Concise
|
||||
|
||||
Instead of creating individual stories for every prop variant, use showcase stories:
|
||||
|
||||
Make sure to type it as `StoryObj` when using render functions.
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Too many individual stories
|
||||
export const Small: Story = { args: { size: 'small' } }
|
||||
export const Medium: Story = { args: { size: 'medium' } }
|
||||
export const Large: Story = { args: { size: 'large' } }
|
||||
|
||||
// ✅ GOOD - One showcase story
|
||||
export const AllSizes: StoryObj = {
|
||||
render: () => ({
|
||||
components: { MyComponent },
|
||||
template: /* html */ `
|
||||
<div class="flex gap-4">
|
||||
<MyComponent size="small">Small</MyComponent>
|
||||
<MyComponent size="medium">Medium</MyComponent>
|
||||
<MyComponent size="large">Large</MyComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Required Stories
|
||||
|
||||
Each component should have:
|
||||
|
||||
- `Default` - Basic usage with controls
|
||||
- `All[Variants]` - Showcase stories for major prop variations (e.g., `AllColors`, `AllSizes`, `AllTypes`)
|
||||
|
||||
### 5. Handling Generic Vue Components
|
||||
|
||||
For components with generics like `MyComponent<T>`, add // @ts-ignore
|
||||
|
||||
```typescript
|
||||
const meta = {
|
||||
title: 'Base/Combobox',
|
||||
// @ts-ignore
|
||||
component: Combobox,
|
||||
} satisfies Meta<typeof Combobox>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Components with Icons
|
||||
|
||||
Import icons from `@modrinth/assets`:
|
||||
|
||||
```typescript
|
||||
import { SearchIcon, ChevronDownIcon } from '@modrinth/assets'
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: () => ({
|
||||
components: { MyComponent, SearchIcon },
|
||||
template: /* html */ `
|
||||
<MyComponent>
|
||||
<SearchIcon />
|
||||
Search
|
||||
</MyComponent>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
}
|
||||
```
|
||||
|
||||
### Interactive Components (Modals, Dropdowns)
|
||||
|
||||
For components that need user interaction to show:
|
||||
|
||||
```typescript
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { Modal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof Modal> | null>(null)
|
||||
return { modalRef }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div>
|
||||
<ButtonStyled @click="modalRef?.show()">
|
||||
<button>Open Modal</button>
|
||||
</ButtonStyled>
|
||||
<Modal ref="modalRef" header="Example Modal">
|
||||
<p>Modal content</p>
|
||||
</Modal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
}
|
||||
```
|
||||
|
||||
### Components with v-model
|
||||
|
||||
```typescript
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { Toggle },
|
||||
setup() {
|
||||
const value = ref(false)
|
||||
return { value }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Toggle v-model="value" />
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
}
|
||||
```
|
||||
|
||||
## Things to Avoid
|
||||
|
||||
### 1. Don't Import from `@modrinth/ui` in Components
|
||||
|
||||
Components should use relative imports, not the package alias:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Causes circular dependency in Storybook
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
// ✅ GOOD - Use relative imports
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
```
|
||||
|
||||
### 2. Object/Array Prop Defaults Must Be Factory Functions
|
||||
|
||||
This is a Vue requirement that `vue-docgen-plugin` enforces:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Will cause Storybook build error
|
||||
defineProps<{ icon: Component }>()
|
||||
withDefaults(defineProps<{ icon: Component }>(), {
|
||||
icon: TrashIcon,
|
||||
})
|
||||
|
||||
// ✅ GOOD - Use factory function
|
||||
withDefaults(defineProps<{ icon: Component }>(), {
|
||||
icon: () => TrashIcon,
|
||||
})
|
||||
```
|
||||
|
||||
## Dependencies Available in Storybook
|
||||
|
||||
The following are configured in `.storybook/preview.ts`:
|
||||
|
||||
- **VIntl**: `useVIntl()` and `formatMessage()` work automatically
|
||||
- **Teleports**: `<Teleport to="#teleports">` has a target element
|
||||
- **Tailwind CSS**: All Tailwind classes are available
|
||||
- **Dark Mode**: Use `@storybook/addon-themes` for theme switching
|
||||
|
||||
## Example: Complete Story File
|
||||
|
||||
```typescript
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
import Badge from '../../components/base/Badge.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Badge',
|
||||
component: Badge,
|
||||
render: (args) => ({
|
||||
components: { Badge },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Badge v-bind="args">Badge Text</Badge>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof Badge>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
color: 'green',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllColors: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Badge },
|
||||
template: /* html */ `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge color="green">Green</Badge>
|
||||
<Badge color="red">Red</Badge>
|
||||
<Badge color="orange">Orange</Badge>
|
||||
<Badge color="blue">Blue</Badge>
|
||||
<Badge color="purple">Purple</Badge>
|
||||
<Badge color="gray">Gray</Badge>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllTypes: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Badge },
|
||||
template: /* html */ `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge type="default">Default</Badge>
|
||||
<Badge type="outlined">Outlined</Badge>
|
||||
<Badge type="highlight">Highlight</Badge>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
```
|
||||
188
packages/ui/src/stories/base/Accordion.stories.ts
Normal file
188
packages/ui/src/stories/base/Accordion.stories.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Accordion from '../../components/base/Accordion.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Accordion',
|
||||
component: Accordion,
|
||||
render: (args) => ({
|
||||
components: { Accordion },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<Accordion v-bind="args">
|
||||
<template #title>Click to expand</template>
|
||||
<p>This is the accordion content that shows when expanded.</p>
|
||||
</Accordion>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof Accordion>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const OpenByDefault: Story = {
|
||||
args: {
|
||||
openByDefault: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const ForceOpen: Story = {
|
||||
args: {
|
||||
forceOpen: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithCustomButtonClass: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Accordion },
|
||||
template: /*html*/ `
|
||||
<Accordion
|
||||
open-by-default
|
||||
button-class="text-primary m-0 p-0 border-none bg-transparent active:scale-95"
|
||||
>
|
||||
<template #title>3 servers</template>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<span class="px-2 py-1 bg-bg-raised rounded text-sm">server-001</span>
|
||||
<span class="px-2 py-1 bg-bg-raised rounded text-sm">server-002</span>
|
||||
<span class="px-2 py-1 bg-bg-raised rounded text-sm">server-003</span>
|
||||
</div>
|
||||
</Accordion>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithSummarySlot: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Accordion },
|
||||
template: /*html*/ `
|
||||
<Accordion>
|
||||
<template #title>
|
||||
<span class="text-lg font-bold text-contrast">Advanced Settings</span>
|
||||
</template>
|
||||
<template #summary>
|
||||
<span class="text-sm text-secondary">Configure advanced options for your project</span>
|
||||
</template>
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" />
|
||||
<span>Enable debug mode</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" />
|
||||
<span>Show verbose output</span>
|
||||
</label>
|
||||
</div>
|
||||
</Accordion>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithContentClass: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Accordion },
|
||||
template: /*html*/ `
|
||||
<Accordion open-by-default content-class="p-4 bg-bg-raised rounded-lg mt-2">
|
||||
<template #title>
|
||||
<span class="text-primary font-semibold">Styled Content Area</span>
|
||||
</template>
|
||||
<p>This content has custom padding and background styling applied via contentClass.</p>
|
||||
</Accordion>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const MultipleAccordions: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Accordion },
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-4">
|
||||
<Accordion>
|
||||
<template #title>
|
||||
<span class="text-primary font-semibold">Section 1: Getting Started</span>
|
||||
</template>
|
||||
<div class="mt-2 text-secondary">
|
||||
<p>Welcome! This section covers the basics of getting started with the application.</p>
|
||||
</div>
|
||||
</Accordion>
|
||||
<Accordion>
|
||||
<template #title>
|
||||
<span class="text-primary font-semibold">Section 2: Configuration</span>
|
||||
</template>
|
||||
<div class="mt-2 text-secondary">
|
||||
<p>Learn how to configure your settings and customize the experience.</p>
|
||||
</div>
|
||||
</Accordion>
|
||||
<Accordion open-by-default>
|
||||
<template #title>
|
||||
<span class="text-primary font-semibold">Section 3: FAQ (Open by default)</span>
|
||||
</template>
|
||||
<div class="mt-2 text-secondary">
|
||||
<p>Frequently asked questions and their answers.</p>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const NestedContent: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Accordion },
|
||||
template: /*html*/ `
|
||||
<Accordion open-by-default>
|
||||
<template #title>
|
||||
<span class="text-primary font-semibold">Project Dependencies</span>
|
||||
</template>
|
||||
<div class="mt-2 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between p-2 bg-bg-raised rounded">
|
||||
<span>vue</span>
|
||||
<span class="text-sm text-secondary">^3.5.0</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-2 bg-bg-raised rounded">
|
||||
<span>typescript</span>
|
||||
<span class="text-sm text-secondary">^5.0.0</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-2 bg-bg-raised rounded">
|
||||
<span>vite</span>
|
||||
<span class="text-sm text-secondary">^6.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllStates: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Accordion },
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-secondary mb-2">Default (collapsed)</h3>
|
||||
<Accordion>
|
||||
<template #title><span class="text-primary">Click to expand</span></template>
|
||||
<p class="mt-2">Hidden content revealed on click.</p>
|
||||
</Accordion>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-secondary mb-2">Open by default</h3>
|
||||
<Accordion open-by-default>
|
||||
<template #title><span class="text-primary">Already expanded</span></template>
|
||||
<p class="mt-2">This content is visible by default.</p>
|
||||
</Accordion>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-secondary mb-2">Force open (no toggle)</h3>
|
||||
<Accordion force-open>
|
||||
<template #title><span class="text-primary">Cannot be collapsed</span></template>
|
||||
<p class="mt-2">This accordion is always open and cannot be toggled.</p>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
38
packages/ui/src/stories/base/Admonition.stories.ts
Normal file
38
packages/ui/src/stories/base/Admonition.stories.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Admonition from '../../components/base/Admonition.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Admonition',
|
||||
component: Admonition,
|
||||
} satisfies Meta<typeof Admonition>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
body: 'This is an informational message.',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllTypes: Story = {
|
||||
render: () => ({
|
||||
components: { Admonition },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<Admonition type="info" header="Info" body="This is an informational message." />
|
||||
<Admonition type="warning" header="Warning" body="This is a warning message." />
|
||||
<Admonition type="critical" header="Critical" body="This is a critical message." />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithHeader: Story = {
|
||||
args: {
|
||||
type: 'warning',
|
||||
header: 'Important Notice',
|
||||
body: 'Please read this carefully before proceeding.',
|
||||
},
|
||||
}
|
||||
57
packages/ui/src/stories/base/AppearingProgressBar.stories.ts
Normal file
57
packages/ui/src/stories/base/AppearingProgressBar.stories.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import AppearingProgressBar from '../../components/base/AppearingProgressBar.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/AppearingProgressBar',
|
||||
component: AppearingProgressBar,
|
||||
} satisfies Meta<typeof AppearingProgressBar>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
maxValue: 100,
|
||||
currentValue: 45,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllProgress: Story = {
|
||||
...Default,
|
||||
render: () => ({
|
||||
components: { AppearingProgressBar },
|
||||
template: `
|
||||
<div class="flex flex-col gap-8">
|
||||
<div>
|
||||
<p class="text-secondary mb-2">0%</p>
|
||||
<AppearingProgressBar :maxValue="100" :currentValue="0" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-secondary mb-2">25%</p>
|
||||
<AppearingProgressBar :maxValue="100" :currentValue="25" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-secondary mb-2">50%</p>
|
||||
<AppearingProgressBar :maxValue="100" :currentValue="50" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-secondary mb-2">75%</p>
|
||||
<AppearingProgressBar :maxValue="100" :currentValue="75" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-secondary mb-2">100%</p>
|
||||
<AppearingProgressBar :maxValue="100" :currentValue="100" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const CustomTips: Story = {
|
||||
args: {
|
||||
maxValue: 1000000,
|
||||
currentValue: 450000,
|
||||
tips: ['Loading assets...', 'Processing data...', 'Almost there...'],
|
||||
},
|
||||
}
|
||||
52
packages/ui/src/stories/base/AutoBrandIcon.stories.ts
Normal file
52
packages/ui/src/stories/base/AutoBrandIcon.stories.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import AutoBrandIcon from '../../components/base/AutoBrandIcon.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/AutoBrandIcon',
|
||||
component: AutoBrandIcon,
|
||||
} satisfies Meta<typeof AutoBrandIcon>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
keyword: 'discord',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllBrands: StoryObj = {
|
||||
render: () => ({
|
||||
components: { AutoBrandIcon },
|
||||
template: `
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div v-for="brand in brands" :key="brand" class="flex flex-col items-center gap-2 p-4 bg-bg-raised rounded-lg">
|
||||
<AutoBrandIcon :keyword="brand" class="h-8 w-8" />
|
||||
<span class="text-sm text-secondary">{{ brand }}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
setup() {
|
||||
const brands = [
|
||||
'discord',
|
||||
'github',
|
||||
'twitter',
|
||||
'youtube',
|
||||
'twitch',
|
||||
'reddit',
|
||||
'mastodon',
|
||||
'bluesky',
|
||||
'patreon',
|
||||
'kofi',
|
||||
'paypal',
|
||||
'curseforge',
|
||||
'modrinth',
|
||||
'instagram',
|
||||
'facebook',
|
||||
'tiktok',
|
||||
]
|
||||
return { brands }
|
||||
},
|
||||
}),
|
||||
}
|
||||
32
packages/ui/src/stories/base/AutoLink.stories.ts
Normal file
32
packages/ui/src/stories/base/AutoLink.stories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import AutoLink from '../../components/base/AutoLink.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/AutoLink',
|
||||
component: AutoLink,
|
||||
render: (args) => ({
|
||||
components: { AutoLink },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<AutoLink v-bind="args">Link Text</AutoLink>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof AutoLink>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const ExternalLink: Story = {
|
||||
args: {
|
||||
to: 'https://modrinth.com',
|
||||
},
|
||||
}
|
||||
|
||||
export const InternalPath: Story = {
|
||||
args: {
|
||||
to: '/projects',
|
||||
},
|
||||
}
|
||||
47
packages/ui/src/stories/base/Avatar.stories.ts
Normal file
47
packages/ui/src/stories/base/Avatar.stories.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Avatar from '../../components/base/Avatar.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Avatar',
|
||||
component: Avatar,
|
||||
} satisfies Meta<typeof Avatar>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const WithImage: Story = {
|
||||
args: {
|
||||
src: 'https://cdn.modrinth.com/data/AANobbMI/icon.png',
|
||||
alt: 'Project icon',
|
||||
},
|
||||
}
|
||||
|
||||
export const Circle: Story = {
|
||||
args: {
|
||||
src: 'https://cdn.modrinth.com/data/AANobbMI/icon.png',
|
||||
circle: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => ({
|
||||
components: { Avatar },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<Avatar src="https://cdn.modrinth.com/data/AANobbMI/icon.png" size="1.5rem" />
|
||||
<Avatar src="https://cdn.modrinth.com/data/AANobbMI/icon.png" size="2rem" />
|
||||
<Avatar src="https://cdn.modrinth.com/data/AANobbMI/icon.png" size="3rem" />
|
||||
<Avatar src="https://cdn.modrinth.com/data/AANobbMI/icon.png" size="4rem" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Placeholder: Story = {
|
||||
args: {
|
||||
size: '3rem',
|
||||
},
|
||||
}
|
||||
54
packages/ui/src/stories/base/Badge.stories.ts
Normal file
54
packages/ui/src/stories/base/Badge.stories.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Badge from '../../components/base/Badge.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Badge',
|
||||
component: Badge,
|
||||
} satisfies Meta<typeof Badge>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
type: 'approved',
|
||||
},
|
||||
}
|
||||
|
||||
export const ProjectStatuses: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Badge },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<Badge type="approved" />
|
||||
<Badge type="unlisted" />
|
||||
<Badge type="private" />
|
||||
<Badge type="draft" />
|
||||
<Badge type="archived" />
|
||||
<Badge type="rejected" />
|
||||
<Badge type="processing" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const UserRoles: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Badge },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<Badge type="admin" />
|
||||
<Badge type="moderator" />
|
||||
<Badge type="creator" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithColor: Story = {
|
||||
args: {
|
||||
type: 'release',
|
||||
color: 'green',
|
||||
},
|
||||
}
|
||||
28
packages/ui/src/stories/base/BulletDivider.stories.ts
Normal file
28
packages/ui/src/stories/base/BulletDivider.stories.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import BulletDivider from '../../components/base/BulletDivider.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/BulletDivider',
|
||||
component: BulletDivider,
|
||||
} satisfies Meta<typeof BulletDivider>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const InContext: Story = {
|
||||
render: () => ({
|
||||
components: { BulletDivider },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span>Item 1</span>
|
||||
<BulletDivider />
|
||||
<span>Item 2</span>
|
||||
<BulletDivider />
|
||||
<span>Item 3</span>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
83
packages/ui/src/stories/base/Button.stories.ts
Normal file
83
packages/ui/src/stories/base/Button.stories.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Button from '../../components/base/Button.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Button',
|
||||
component: Button,
|
||||
render: (args) => ({
|
||||
components: { Button },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<Button v-bind="args">Click me</Button>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof Button>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
color: 'primary',
|
||||
},
|
||||
}
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
color: 'danger',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllColors: Story = {
|
||||
render: () => ({
|
||||
components: { Button },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem;">
|
||||
<Button color="default">Default</Button>
|
||||
<Button color="primary">Primary</Button>
|
||||
<Button color="danger">Danger</Button>
|
||||
<Button color="red">Red</Button>
|
||||
<Button color="orange">Orange</Button>
|
||||
<Button color="green">Green</Button>
|
||||
<Button color="blue">Blue</Button>
|
||||
<Button color="purple">Purple</Button>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
large: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
outline: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Transparent: Story = {
|
||||
args: {
|
||||
transparent: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AsLink: Story = {
|
||||
args: {
|
||||
link: 'https://modrinth.com',
|
||||
external: true,
|
||||
},
|
||||
}
|
||||
74
packages/ui/src/stories/base/ButtonStyled.stories.ts
Normal file
74
packages/ui/src/stories/base/ButtonStyled.stories.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ButtonStyled',
|
||||
component: ButtonStyled,
|
||||
render: (args) => ({
|
||||
components: { ButtonStyled },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<ButtonStyled v-bind="args">
|
||||
<button @click="()=>{}">Button</button>
|
||||
</ButtonStyled>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof ButtonStyled>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
// All colors showcase
|
||||
export const AllColors: Story = {
|
||||
render: () => ({
|
||||
components: { ButtonStyled },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: center;">
|
||||
<ButtonStyled color="standard"><button>Standard</button></ButtonStyled>
|
||||
<ButtonStyled color="brand"><button>Brand</button></ButtonStyled>
|
||||
<ButtonStyled color="red"><button>Red</button></ButtonStyled>
|
||||
<ButtonStyled color="orange"><button>Orange</button></ButtonStyled>
|
||||
<ButtonStyled color="green"><button>Green</button></ButtonStyled>
|
||||
<ButtonStyled color="blue"><button>Blue</button></ButtonStyled>
|
||||
<ButtonStyled color="purple"><button>Purple</button></ButtonStyled>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// All sizes showcase
|
||||
export const AllSizes: Story = {
|
||||
render: () => ({
|
||||
components: { ButtonStyled },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: center;">
|
||||
<ButtonStyled size="small" color="standard"><button>Small</button></ButtonStyled>
|
||||
<ButtonStyled size="standard" color="standard"><button>Standard</button></ButtonStyled>
|
||||
<ButtonStyled size="large" color="standard"><button>Large</button></ButtonStyled>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// All types showcase
|
||||
export const AllTypes: Story = {
|
||||
render: () => ({
|
||||
components: { ButtonStyled },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: center;">
|
||||
<ButtonStyled type="standard" color="standard"><button>Standard</button></ButtonStyled>
|
||||
<ButtonStyled type="outlined" color="standard"><button>Outlined</button></ButtonStyled>
|
||||
<ButtonStyled type="transparent" color="standard"><button>Transparent</button></ButtonStyled>
|
||||
<ButtonStyled type="highlight" color="standard"><button>Highlight</button></ButtonStyled>
|
||||
<ButtonStyled type="highlight-colored-text" color="standard"><button>Highlight Colored</button></ButtonStyled>
|
||||
<ButtonStyled type="chip" color="standard"><button>Chip</button></ButtonStyled>
|
||||
<ButtonStyled color="standard"><button disabled>Disabled</button></ButtonStyled>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
38
packages/ui/src/stories/base/Card.stories.ts
Normal file
38
packages/ui/src/stories/base/Card.stories.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Card from '../../components/base/Card.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Card',
|
||||
component: Card,
|
||||
render: (args) => ({
|
||||
components: { Card },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<Card v-bind="args">
|
||||
<template #header><h3>Card Title</h3></template>
|
||||
<p>This is the card content.</p>
|
||||
</Card>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof Card>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Collapsible: Story = {
|
||||
args: {
|
||||
collapsible: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const CollapsedByDefault: Story = {
|
||||
args: {
|
||||
collapsible: true,
|
||||
defaultCollapsed: true,
|
||||
},
|
||||
}
|
||||
56
packages/ui/src/stories/base/Checkbox.stories.ts
Normal file
56
packages/ui/src/stories/base/Checkbox.stories.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Checkbox from '../../components/base/Checkbox.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Checkbox',
|
||||
component: Checkbox,
|
||||
} satisfies Meta<typeof Checkbox>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Accept terms and conditions',
|
||||
modelValue: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Checked: Story = {
|
||||
args: {
|
||||
label: 'Accept terms and conditions',
|
||||
modelValue: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
label: 'Disabled checkbox',
|
||||
modelValue: false,
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Indeterminate: Story = {
|
||||
args: {
|
||||
label: 'Indeterminate state',
|
||||
modelValue: false,
|
||||
indeterminate: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStates: StoryObj = {
|
||||
render: () => ({
|
||||
components: { Checkbox },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<Checkbox label="Unchecked" :model-value="false" />
|
||||
<Checkbox label="Checked" :model-value="true" />
|
||||
<Checkbox label="Indeterminate" :model-value="false" :indeterminate="true" />
|
||||
<Checkbox label="Disabled unchecked" :model-value="false" :disabled="true" />
|
||||
<Checkbox label="Disabled checked" :model-value="true" :disabled="true" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
45
packages/ui/src/stories/base/Chips.stories.ts
Normal file
45
packages/ui/src/stories/base/Chips.stories.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Chips from '../../components/base/Chips.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Chips',
|
||||
// @ts-ignore - error comes from generically typed component
|
||||
component: Chips,
|
||||
} satisfies Meta<typeof Chips>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
size: 'small',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllowEmpty: Story = {
|
||||
args: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
neverEmpty: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoCapitalize: Story = {
|
||||
args: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
capitalize: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const ManyItems: Story = {
|
||||
args: {
|
||||
items: ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta'],
|
||||
},
|
||||
}
|
||||
34
packages/ui/src/stories/base/Collapsible.stories.ts
Normal file
34
packages/ui/src/stories/base/Collapsible.stories.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Collapsible from '../../components/base/Collapsible.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Collapsible',
|
||||
component: Collapsible,
|
||||
render: (args) => ({
|
||||
components: { Collapsible },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<Collapsible v-bind="args">
|
||||
<p>This content can be collapsed or expanded.</p>
|
||||
</Collapsible>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof Collapsible>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
collapsed: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Collapsed: Story = {
|
||||
args: {
|
||||
collapsed: true,
|
||||
},
|
||||
}
|
||||
46
packages/ui/src/stories/base/CollapsibleRegion.stories.ts
Normal file
46
packages/ui/src/stories/base/CollapsibleRegion.stories.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import CollapsibleRegion from '../../components/base/CollapsibleRegion.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/CollapsibleRegion',
|
||||
component: CollapsibleRegion,
|
||||
} satisfies Meta<typeof CollapsibleRegion>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { CollapsibleRegion },
|
||||
template: `
|
||||
<CollapsibleRegion>
|
||||
<div class="space-y-4">
|
||||
<p>This is some content that can be collapsed or expanded.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
||||
<p>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
|
||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.</p>
|
||||
<p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia.</p>
|
||||
</div>
|
||||
</CollapsibleRegion>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const CustomLabels: Story = {
|
||||
render: () => ({
|
||||
components: { CollapsibleRegion },
|
||||
template: `
|
||||
<CollapsibleRegion expandText="Show more" collapseText="Show less" collapsedHeight="6rem">
|
||||
<div class="space-y-4">
|
||||
<p>Custom expand and collapse labels.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
||||
<p>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
|
||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.</p>
|
||||
</div>
|
||||
</CollapsibleRegion>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
46
packages/ui/src/stories/base/Combobox.stories.ts
Normal file
46
packages/ui/src/stories/base/Combobox.stories.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Combobox from '../../components/base/Combobox.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Combobox',
|
||||
// @ts-ignore - generic component
|
||||
component: Combobox,
|
||||
} satisfies Meta<typeof Combobox>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: '1', label: 'Option 1' },
|
||||
{ value: '2', label: 'Option 2' },
|
||||
{ value: '3', label: 'Option 3' },
|
||||
],
|
||||
triggerText: 'Select an option',
|
||||
},
|
||||
}
|
||||
|
||||
export const Searchable: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: '1', label: 'Minecraft' },
|
||||
{ value: '2', label: 'Fabric' },
|
||||
{ value: '3', label: 'Forge' },
|
||||
{ value: '4', label: 'NeoForge' },
|
||||
{ value: '5', label: 'Quilt' },
|
||||
],
|
||||
triggerText: 'Select a loader',
|
||||
searchable: true,
|
||||
searchPlaceholder: 'Search loaders...',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
options: [{ value: '1', label: 'Option 1' }],
|
||||
triggerText: 'Disabled',
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
63
packages/ui/src/stories/base/ContentPageHeader.stories.ts
Normal file
63
packages/ui/src/stories/base/ContentPageHeader.stories.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Avatar from '../../components/base/Avatar.vue'
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
import ContentPageHeader from '../../components/base/ContentPageHeader.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ContentPageHeader',
|
||||
component: ContentPageHeader,
|
||||
} satisfies Meta<typeof ContentPageHeader>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { ContentPageHeader, Avatar, ButtonStyled },
|
||||
template: `
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar size="64px" />
|
||||
</template>
|
||||
<template #title>Project Name</template>
|
||||
<template #summary>A brief description of the project goes here.</template>
|
||||
<template #stats>
|
||||
<span>1.2M downloads</span>
|
||||
<span>50K followers</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button>Follow</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithTitleSuffix: Story = {
|
||||
render: () => ({
|
||||
components: { ContentPageHeader, Avatar, ButtonStyled },
|
||||
template: `
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar size="64px" />
|
||||
</template>
|
||||
<template #title>Featured Project</template>
|
||||
<template #title-suffix>
|
||||
<span class="px-2 py-1 bg-brand-highlight text-brand rounded-full text-sm">Featured</span>
|
||||
</template>
|
||||
<template #summary>This project has been featured by the Modrinth team.</template>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button>Download</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button>Share</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
23
packages/ui/src/stories/base/CopyCode.stories.ts
Normal file
23
packages/ui/src/stories/base/CopyCode.stories.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import CopyCode from '../../components/base/CopyCode.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/CopyCode',
|
||||
component: CopyCode,
|
||||
} satisfies Meta<typeof CopyCode>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
text: 'npm install @modrinth/ui',
|
||||
},
|
||||
}
|
||||
|
||||
export const LongText: Story = {
|
||||
args: {
|
||||
text: 'curl -X GET "https://api.modrinth.com/v2/project/sodium" -H "Accept: application/json"',
|
||||
},
|
||||
}
|
||||
26
packages/ui/src/stories/base/DoubleIcon.stories.ts
Normal file
26
packages/ui/src/stories/base/DoubleIcon.stories.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DownloadIcon, HeartIcon } from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import DoubleIcon from '../../components/base/DoubleIcon.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/DoubleIcon',
|
||||
component: DoubleIcon,
|
||||
render: (args) => ({
|
||||
components: { DoubleIcon, DownloadIcon, HeartIcon },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<DoubleIcon v-bind="args">
|
||||
<template #primary><DownloadIcon style="width: 2rem; height: 2rem;" /></template>
|
||||
<template #secondary><HeartIcon /></template>
|
||||
</DoubleIcon>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof DoubleIcon>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
38
packages/ui/src/stories/base/DropArea.stories.ts
Normal file
38
packages/ui/src/stories/base/DropArea.stories.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import DropArea from '../../components/base/DropArea.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/DropArea',
|
||||
component: DropArea,
|
||||
} satisfies Meta<typeof DropArea>
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default: StoryObj = {
|
||||
render: () => ({
|
||||
components: { DropArea },
|
||||
template: `
|
||||
<DropArea accept="*" @change="(files) => console.log('Files dropped:', files)">
|
||||
<div class="p-8 border-2 border-dashed border-divider rounded-lg text-center">
|
||||
<p class="text-secondary">Drag and drop files anywhere on the page</p>
|
||||
<p class="text-sm text-secondary mt-2">The drop overlay will appear when you drag files over</p>
|
||||
</div>
|
||||
</DropArea>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const ImagesOnly: StoryObj = {
|
||||
render: () => ({
|
||||
components: { DropArea },
|
||||
template: `
|
||||
<DropArea accept="image/*" @change="(files) => console.log('Images dropped:', files)">
|
||||
<div class="p-8 border-2 border-dashed border-divider rounded-lg text-center">
|
||||
<p class="text-secondary">Drop images here</p>
|
||||
<p class="text-sm text-secondary mt-2">Only accepts image files</p>
|
||||
</div>
|
||||
</DropArea>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
36
packages/ui/src/stories/base/DropdownSelect.stories.ts
Normal file
36
packages/ui/src/stories/base/DropdownSelect.stories.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import DropdownSelect from '../../components/base/DropdownSelect.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/DropdownSelect',
|
||||
component: DropdownSelect,
|
||||
} satisfies Meta<typeof DropdownSelect>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: 'dropdown',
|
||||
options: ['Option 1', 'Option 2', 'Option 3'],
|
||||
modelValue: 'Option 1',
|
||||
},
|
||||
}
|
||||
|
||||
export const ManyOptions: Story = {
|
||||
args: {
|
||||
name: 'sort',
|
||||
options: ['Relevance', 'Downloads', 'Follows', 'Newest', 'Updated'],
|
||||
modelValue: 'Relevance',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
name: 'disabled',
|
||||
options: ['Option 1', 'Option 2', 'Option 3'],
|
||||
modelValue: 'Option 1',
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
41
packages/ui/src/stories/base/DropzoneFileInput.stories.ts
Normal file
41
packages/ui/src/stories/base/DropzoneFileInput.stories.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import DropzoneFileInput from '../../components/base/DropzoneFileInput.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/DropzoneFileInput',
|
||||
component: DropzoneFileInput,
|
||||
} satisfies Meta<typeof DropzoneFileInput>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: 'small',
|
||||
},
|
||||
}
|
||||
|
||||
export const MultipleFiles: Story = {
|
||||
args: {
|
||||
multiple: true,
|
||||
primaryPrompt: 'Drag and drop multiple files',
|
||||
secondaryPrompt: 'Select multiple files at once',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomPrompts: Story = {
|
||||
args: {
|
||||
primaryPrompt: 'Drop your mod files here',
|
||||
secondaryPrompt: 'Supports .jar and .zip files up to 25MB',
|
||||
accept: '.jar,.zip',
|
||||
},
|
||||
}
|
||||
53
packages/ui/src/stories/base/EnvironmentIndicator.stories.ts
Normal file
53
packages/ui/src/stories/base/EnvironmentIndicator.stories.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import EnvironmentIndicator from '../../components/base/EnvironmentIndicator.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/EnvironmentIndicator',
|
||||
component: EnvironmentIndicator,
|
||||
} satisfies Meta<typeof EnvironmentIndicator>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
type: 'mod',
|
||||
clientSide: 'required',
|
||||
serverSide: 'optional',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllEnvironments: StoryObj = {
|
||||
render: () => ({
|
||||
components: { EnvironmentIndicator },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvironmentIndicator type="mod" clientSide="required" serverSide="optional" />
|
||||
<span class="text-secondary text-sm">Client (required client, optional server)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvironmentIndicator type="mod" clientSide="optional" serverSide="required" />
|
||||
<span class="text-secondary text-sm">Server (optional client, required server)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvironmentIndicator type="mod" clientSide="required" serverSide="required" />
|
||||
<span class="text-secondary text-sm">Client and server (both required)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvironmentIndicator type="mod" clientSide="optional" serverSide="optional" />
|
||||
<span class="text-secondary text-sm">Client or server (both optional)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvironmentIndicator type="mod" clientSide="unsupported" serverSide="unsupported" />
|
||||
<span class="text-secondary text-sm">Unsupported</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvironmentIndicator type="mod" typeOnly />
|
||||
<span class="text-secondary text-sm">Type only</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
56
packages/ui/src/stories/base/ErrorInformationCard.stories.ts
Normal file
56
packages/ui/src/stories/base/ErrorInformationCard.stories.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { IssuesIcon } from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ErrorInformationCard from '../../components/base/ErrorInformationCard.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ErrorInformationCard',
|
||||
component: ErrorInformationCard,
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div class="flex min-h-[400px] items-center justify-center bg-bg"><story /></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies Meta<typeof ErrorInformationCard>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Something went wrong',
|
||||
description: 'An unexpected error occurred while processing your request.',
|
||||
icon: IssuesIcon,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithErrorDetails: Story = {
|
||||
args: {
|
||||
title: 'Connection Failed',
|
||||
description: 'Unable to connect to the server.',
|
||||
icon: IssuesIcon,
|
||||
errorDetails: [
|
||||
{ label: 'Error Code', value: 'ERR_CONNECTION_REFUSED', type: 'inline' },
|
||||
{ label: 'Timestamp', value: '2024-01-15T10:30:00Z', type: 'inline' },
|
||||
{
|
||||
label: 'Stack Trace',
|
||||
value: 'Error: Connection refused\n at Socket.connect\n at Client.connect',
|
||||
type: 'block',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const WithAction: Story = {
|
||||
args: {
|
||||
title: 'Download Failed',
|
||||
description: 'The file could not be downloaded. Please try again.',
|
||||
icon: IssuesIcon,
|
||||
action: {
|
||||
label: 'Retry Download',
|
||||
onClick: () => console.log('Retry clicked'),
|
||||
color: 'brand',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
packages/ui/src/stories/base/FileInput.stories.ts
Normal file
38
packages/ui/src/stories/base/FileInput.stories.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import FileInput from '../../components/base/FileInput.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/FileInput',
|
||||
component: FileInput,
|
||||
} satisfies Meta<typeof FileInput>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
prompt: 'Select file',
|
||||
},
|
||||
}
|
||||
|
||||
export const Multiple: Story = {
|
||||
args: {
|
||||
prompt: 'Select files',
|
||||
multiple: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const ImagesOnly: Story = {
|
||||
args: {
|
||||
prompt: 'Select image',
|
||||
accept: 'image/*',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
prompt: 'Select file',
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
51
packages/ui/src/stories/base/FilterBar.stories.ts
Normal file
51
packages/ui/src/stories/base/FilterBar.stories.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import FilterBar from '../../components/base/FilterBar.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/FilterBar',
|
||||
component: FilterBar,
|
||||
} satisfies Meta<typeof FilterBar>
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default: StoryObj = {
|
||||
render: () => ({
|
||||
components: { FilterBar },
|
||||
setup() {
|
||||
const selected = ref<string[]>([])
|
||||
const options = [
|
||||
{ id: 'active', message: { id: 'filter.active', defaultMessage: 'Active' } },
|
||||
{ id: 'archived', message: { id: 'filter.archived', defaultMessage: 'Archived' } },
|
||||
{ id: 'draft', message: { id: 'filter.draft', defaultMessage: 'Draft' } },
|
||||
]
|
||||
return { selected, options }
|
||||
},
|
||||
template: `
|
||||
<FilterBar v-model="selected" :options="options" showAllOptions />
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithSelection: StoryObj = {
|
||||
render: () => ({
|
||||
components: { FilterBar },
|
||||
setup() {
|
||||
const selected = ref<string[]>(['mods', 'plugins'])
|
||||
const options = [
|
||||
{ id: 'mods', message: { id: 'filter.mods', defaultMessage: 'Mods' } },
|
||||
{ id: 'plugins', message: { id: 'filter.plugins', defaultMessage: 'Plugins' } },
|
||||
{
|
||||
id: 'resourcepacks',
|
||||
message: { id: 'filter.resourcepacks', defaultMessage: 'Resource Packs' },
|
||||
},
|
||||
{ id: 'modpacks', message: { id: 'filter.modpacks', defaultMessage: 'Modpacks' } },
|
||||
]
|
||||
return { selected, options }
|
||||
},
|
||||
template: `
|
||||
<FilterBar v-model="selected" :options="options" showAllOptions />
|
||||
`,
|
||||
}),
|
||||
}
|
||||
26
packages/ui/src/stories/base/HeadingLink.stories.ts
Normal file
26
packages/ui/src/stories/base/HeadingLink.stories.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import HeadingLink from '../../components/base/HeadingLink.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/HeadingLink',
|
||||
component: HeadingLink,
|
||||
render: (args) => ({
|
||||
components: { HeadingLink },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<HeadingLink v-bind="args">View All Projects</HeadingLink>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof HeadingLink>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
to: '/projects',
|
||||
},
|
||||
}
|
||||
13
packages/ui/src/stories/base/HorizontalRule.stories.ts
Normal file
13
packages/ui/src/stories/base/HorizontalRule.stories.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import HorizontalRule from '../../components/base/HorizontalRule.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/HorizontalRule',
|
||||
component: HorizontalRule,
|
||||
} satisfies Meta<typeof HorizontalRule>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
44
packages/ui/src/stories/base/LargeRadioButton.stories.ts
Normal file
44
packages/ui/src/stories/base/LargeRadioButton.stories.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import LargeRadioButton from '../../components/base/LargeRadioButton.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/LargeRadioButton',
|
||||
// @ts-ignore
|
||||
component: LargeRadioButton,
|
||||
} satisfies Meta<typeof LargeRadioButton>
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default: StoryObj = {
|
||||
render: () => ({
|
||||
components: { LargeRadioButton },
|
||||
template: `
|
||||
<LargeRadioButton :selected="false">
|
||||
Unselected option
|
||||
</LargeRadioButton>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllStates: StoryObj = {
|
||||
render: () => ({
|
||||
components: { LargeRadioButton },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4 max-w-md">
|
||||
<LargeRadioButton :selected="false">
|
||||
Unselected option
|
||||
</LargeRadioButton>
|
||||
<LargeRadioButton :selected="true">
|
||||
Selected option
|
||||
</LargeRadioButton>
|
||||
<LargeRadioButton :selected="false" :disabled="true">
|
||||
Disabled unselected
|
||||
</LargeRadioButton>
|
||||
<LargeRadioButton :selected="true" :disabled="true">
|
||||
Disabled selected
|
||||
</LargeRadioButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
13
packages/ui/src/stories/base/LoadingIndicator.stories.ts
Normal file
13
packages/ui/src/stories/base/LoadingIndicator.stories.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import LoadingIndicator from '../../components/base/LoadingIndicator.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/LoadingIndicator',
|
||||
component: LoadingIndicator,
|
||||
} satisfies Meta<typeof LoadingIndicator>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
41
packages/ui/src/stories/base/MarkdownEditor.stories.ts
Normal file
41
packages/ui/src/stories/base/MarkdownEditor.stories.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MarkdownEditor from '../../components/base/MarkdownEditor.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/MarkdownEditor',
|
||||
component: MarkdownEditor,
|
||||
} satisfies Meta<typeof MarkdownEditor>
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default: StoryObj = {
|
||||
render: () => ({
|
||||
components: { MarkdownEditor },
|
||||
setup() {
|
||||
const content = ref('# Hello World\n\nThis is some **markdown** content.')
|
||||
return { content }
|
||||
},
|
||||
template: `
|
||||
<div class="h-96">
|
||||
<MarkdownEditor v-model="content" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithPlaceholder: StoryObj = {
|
||||
render: () => ({
|
||||
components: { MarkdownEditor },
|
||||
setup() {
|
||||
const content = ref('')
|
||||
return { content }
|
||||
},
|
||||
template: `
|
||||
<div class="h-96">
|
||||
<MarkdownEditor v-model="content" placeholder="Write your description here..." />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
37
packages/ui/src/stories/base/OptionGroup.stories.ts
Normal file
37
packages/ui/src/stories/base/OptionGroup.stories.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import OptionGroup from '../../components/base/OptionGroup.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/OptionGroup',
|
||||
// @ts-ignore - error comes from generically typed component
|
||||
component: OptionGroup,
|
||||
render: (args) => ({
|
||||
components: { OptionGroup },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<OptionGroup v-bind="args" v-model="args.modelValue">
|
||||
<template #default="{ option }">{{ option }}</template>
|
||||
</OptionGroup>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof OptionGroup>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
options: ['Option 1', 'Option 2', 'Option 3'],
|
||||
modelValue: 'Option 1',
|
||||
},
|
||||
}
|
||||
|
||||
export const ManyOptions: Story = {
|
||||
args: {
|
||||
options: ['All', 'Mods', 'Plugins', 'Resource Packs', 'Modpacks', 'Shaders'],
|
||||
modelValue: 'All',
|
||||
},
|
||||
}
|
||||
81
packages/ui/src/stories/base/OverflowMenu.stories.ts
Normal file
81
packages/ui/src/stories/base/OverflowMenu.stories.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { MoreHorizontalIcon } from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
import OverflowMenu from '../../components/base/OverflowMenu.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/OverflowMenu',
|
||||
component: OverflowMenu,
|
||||
render: (args) => ({
|
||||
components: { OverflowMenu, MoreHorizontalIcon, ButtonStyled },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu v-bind="args">
|
||||
<MoreHorizontalIcon class="h-5 w-5" />
|
||||
<template #edit>Edit</template>
|
||||
<template #delete>Delete</template>
|
||||
<template #share>Share</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof OverflowMenu>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ id: 'edit', action: () => console.log('Edit clicked') },
|
||||
{ id: 'share', action: () => console.log('Share clicked') },
|
||||
{ divider: true },
|
||||
{ id: 'delete', action: () => console.log('Delete clicked'), color: 'danger' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const WithDifferentPlacements: StoryObj = {
|
||||
render: () => ({
|
||||
components: { OverflowMenu, MoreHorizontalIcon, ButtonStyled },
|
||||
template: /*html*/ `
|
||||
<div class="flex gap-8 items-start">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-sm text-secondary">bottom-end (default)</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{ id: 'edit', action: () => {} },
|
||||
{ id: 'delete', action: () => {}, color: 'danger' },
|
||||
]"
|
||||
>
|
||||
<MoreHorizontalIcon class="h-5 w-5" />
|
||||
<template #edit>Edit</template>
|
||||
<template #delete>Delete</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-sm text-secondary">bottom-start</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
direction="left"
|
||||
:options="[
|
||||
{ id: 'edit', action: () => {} },
|
||||
{ id: 'delete', action: () => {}, color: 'danger' },
|
||||
]"
|
||||
>
|
||||
<MoreHorizontalIcon class="h-5 w-5" />
|
||||
<template #edit>Edit</template>
|
||||
<template #delete>Delete</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
78
packages/ui/src/stories/base/Page.stories.ts
Normal file
78
packages/ui/src/stories/base/Page.stories.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Page from '../../components/base/Page.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Page',
|
||||
component: Page,
|
||||
} satisfies Meta<typeof Page>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { Page },
|
||||
template: `
|
||||
<Page>
|
||||
<div class="p-4 bg-bg-raised rounded-lg">
|
||||
<h1 class="text-2xl font-bold mb-4">Page Content</h1>
|
||||
<p>This is the main content area of the page.</p>
|
||||
</div>
|
||||
</Page>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithSidebar: Story = {
|
||||
render: () => ({
|
||||
components: { Page },
|
||||
template: `
|
||||
<Page>
|
||||
<template #sidebar>
|
||||
<div class="p-4 bg-bg-raised rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-2">Sidebar</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>Link 1</li>
|
||||
<li>Link 2</li>
|
||||
<li>Link 3</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 bg-bg-raised rounded-lg">
|
||||
<h1 class="text-2xl font-bold mb-4">Page Content</h1>
|
||||
<p>This is the main content area with a sidebar.</p>
|
||||
</div>
|
||||
</Page>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithHeaderAndFooter: Story = {
|
||||
render: () => ({
|
||||
components: { Page },
|
||||
template: `
|
||||
<Page>
|
||||
<template #header>
|
||||
<div class="p-4 bg-brand-highlight rounded-lg mb-4">
|
||||
<h1 class="text-2xl font-bold">Page Header</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #sidebar>
|
||||
<div class="p-4 bg-bg-raised rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-2">Sidebar</h2>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 bg-bg-raised rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Main Content</h2>
|
||||
<p>Content with header, sidebar, and footer.</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="p-4 bg-bg-raised rounded-lg mt-4">
|
||||
<p class="text-secondary">Footer content</p>
|
||||
</div>
|
||||
</template>
|
||||
</Page>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
46
packages/ui/src/stories/base/Pagination.stories.ts
Normal file
46
packages/ui/src/stories/base/Pagination.stories.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Pagination from '../../components/base/Pagination.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Pagination',
|
||||
component: Pagination,
|
||||
} satisfies Meta<typeof Pagination>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
page: 1,
|
||||
count: 10,
|
||||
},
|
||||
}
|
||||
|
||||
export const MiddlePage: Story = {
|
||||
args: {
|
||||
page: 5,
|
||||
count: 10,
|
||||
},
|
||||
}
|
||||
|
||||
export const LastPage: Story = {
|
||||
args: {
|
||||
page: 10,
|
||||
count: 10,
|
||||
},
|
||||
}
|
||||
|
||||
export const FewPages: Story = {
|
||||
args: {
|
||||
page: 1,
|
||||
count: 3,
|
||||
},
|
||||
}
|
||||
|
||||
export const ManyPages: Story = {
|
||||
args: {
|
||||
page: 50,
|
||||
count: 100,
|
||||
},
|
||||
}
|
||||
94
packages/ui/src/stories/base/PopoutMenu.stories.ts
Normal file
94
packages/ui/src/stories/base/PopoutMenu.stories.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { SettingsIcon } from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Button from '../../components/base/Button.vue'
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
import PopoutMenu from '../../components/base/PopoutMenu.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/PopoutMenu',
|
||||
component: PopoutMenu,
|
||||
render: (args) => ({
|
||||
components: { PopoutMenu, Button, ButtonStyled, SettingsIcon },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<ButtonStyled circular type="transparent">
|
||||
<PopoutMenu v-bind="args">
|
||||
<SettingsIcon class="h-5 w-5" />
|
||||
<template #menu>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<Button transparent>Option 1</Button>
|
||||
<Button transparent>Option 2</Button>
|
||||
<Button transparent>Option 3</Button>
|
||||
</div>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
</ButtonStyled>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof PopoutMenu>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const WithTooltip: Story = {
|
||||
args: {
|
||||
tooltip: 'Click for more options',
|
||||
},
|
||||
}
|
||||
|
||||
export const DifferentPlacements: StoryObj = {
|
||||
render: () => ({
|
||||
components: { PopoutMenu, Button, ButtonStyled, SettingsIcon },
|
||||
template: /*html*/ `
|
||||
<div class="flex gap-8 items-start p-8">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-sm text-secondary">bottom-end (default)</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<PopoutMenu placement="bottom-end">
|
||||
<SettingsIcon class="h-5 w-5" />
|
||||
<template #menu>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<Button transparent>Option 1</Button>
|
||||
<Button transparent>Option 2</Button>
|
||||
</div>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-sm text-secondary">bottom-start</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<PopoutMenu placement="bottom-start">
|
||||
<SettingsIcon class="h-5 w-5" />
|
||||
<template #menu>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<Button transparent>Option 1</Button>
|
||||
<Button transparent>Option 2</Button>
|
||||
</div>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-sm text-secondary">top-end</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<PopoutMenu placement="top-end">
|
||||
<SettingsIcon class="h-5 w-5" />
|
||||
<template #menu>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<Button transparent>Option 1</Button>
|
||||
<Button transparent>Option 2</Button>
|
||||
</div>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
133
packages/ui/src/stories/base/PreviewSelectButton.stories.ts
Normal file
133
packages/ui/src/stories/base/PreviewSelectButton.stories.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import PreviewSelectButton from '../../components/base/PreviewSelectButton.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/PreviewSelectButton',
|
||||
component: PreviewSelectButton,
|
||||
} satisfies Meta<typeof PreviewSelectButton>
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default: StoryObj = {
|
||||
render: () => ({
|
||||
components: { PreviewSelectButton },
|
||||
template: `
|
||||
<PreviewSelectButton :checked="false">
|
||||
<template #preview>
|
||||
<div class="w-16 h-16 bg-brand-highlight rounded-lg" />
|
||||
</template>
|
||||
Option Label
|
||||
</PreviewSelectButton>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllStates: StoryObj = {
|
||||
render: () => ({
|
||||
components: { PreviewSelectButton },
|
||||
template: `
|
||||
<div class="flex gap-4">
|
||||
<PreviewSelectButton :checked="false">
|
||||
<template #preview>
|
||||
<div class="w-16 h-16 bg-bg-raised rounded-lg flex items-center justify-center border border-divider">
|
||||
<span class="text-secondary">A</span>
|
||||
</div>
|
||||
</template>
|
||||
Unchecked
|
||||
</PreviewSelectButton>
|
||||
<PreviewSelectButton :checked="true">
|
||||
<template #preview>
|
||||
<div class="w-16 h-16 bg-brand-highlight rounded-lg flex items-center justify-center border border-brand">
|
||||
<span class="text-brand">B</span>
|
||||
</div>
|
||||
</template>
|
||||
Checked
|
||||
</PreviewSelectButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const InteractiveSelection: StoryObj = {
|
||||
render: () => ({
|
||||
components: { PreviewSelectButton },
|
||||
setup() {
|
||||
const selected = ref('dark')
|
||||
return { selected }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<p class="text-sm text-secondary mb-4">Selected: {{ selected }}</p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<PreviewSelectButton
|
||||
:checked="selected === 'light'"
|
||||
@click="selected = 'light'"
|
||||
>
|
||||
<template #preview>
|
||||
<div class="w-16 h-16 bg-white rounded-lg border border-divider" />
|
||||
</template>
|
||||
Light
|
||||
</PreviewSelectButton>
|
||||
<PreviewSelectButton
|
||||
:checked="selected === 'dark'"
|
||||
@click="selected = 'dark'"
|
||||
>
|
||||
<template #preview>
|
||||
<div class="w-16 h-16 bg-gray-900 rounded-lg border border-divider" />
|
||||
</template>
|
||||
Dark
|
||||
</PreviewSelectButton>
|
||||
<PreviewSelectButton
|
||||
:checked="selected === 'oled'"
|
||||
@click="selected = 'oled'"
|
||||
>
|
||||
<template #preview>
|
||||
<div class="w-16 h-16 bg-black rounded-lg border border-divider" />
|
||||
</template>
|
||||
OLED
|
||||
</PreviewSelectButton>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const ColorSelection: StoryObj = {
|
||||
render: () => ({
|
||||
components: { PreviewSelectButton },
|
||||
setup() {
|
||||
const selected = ref('brand')
|
||||
return { selected }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<p class="text-sm text-secondary mb-4">Accent color: {{ selected }}</p>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<PreviewSelectButton
|
||||
v-for="color in ['brand', 'red', 'orange', 'green', 'blue', 'purple']"
|
||||
:key="color"
|
||||
:checked="selected === color"
|
||||
@click="selected = color"
|
||||
>
|
||||
<template #preview>
|
||||
<div
|
||||
class="w-12 h-12 rounded-lg"
|
||||
:class="{
|
||||
'bg-brand': color === 'brand',
|
||||
'bg-red': color === 'red',
|
||||
'bg-orange': color === 'orange',
|
||||
'bg-green': color === 'green',
|
||||
'bg-blue': color === 'blue',
|
||||
'bg-purple': color === 'purple',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
{{ color.charAt(0).toUpperCase() + color.slice(1) }}
|
||||
</PreviewSelectButton>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
73
packages/ui/src/stories/base/ProgressBar.stories.ts
Normal file
73
packages/ui/src/stories/base/ProgressBar.stories.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ProgressBar from '../../components/base/ProgressBar.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ProgressBar',
|
||||
component: ProgressBar,
|
||||
} satisfies Meta<typeof ProgressBar>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
progress: 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithLabel: Story = {
|
||||
args: {
|
||||
progress: 0.75,
|
||||
label: 'Uploading...',
|
||||
showProgress: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllColors: StoryObj = {
|
||||
render: () => ({
|
||||
components: { ProgressBar },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<ProgressBar :progress="0.6" color="brand" label="Brand" />
|
||||
<ProgressBar :progress="0.6" color="green" label="Green" />
|
||||
<ProgressBar :progress="0.6" color="red" label="Red" />
|
||||
<ProgressBar :progress="0.6" color="orange" label="Orange" />
|
||||
<ProgressBar :progress="0.6" color="blue" label="Blue" />
|
||||
<ProgressBar :progress="0.6" color="purple" label="Purple" />
|
||||
<ProgressBar :progress="0.6" color="gray" label="Gray" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Striped: Story = {
|
||||
args: {
|
||||
progress: 0.5,
|
||||
striped: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStripedColors: StoryObj = {
|
||||
render: () => ({
|
||||
components: { ProgressBar },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<ProgressBar :progress="0.6" color="brand" striped label="Brand Striped" />
|
||||
<ProgressBar :progress="0.6" color="green" striped label="Green Striped" />
|
||||
<ProgressBar :progress="0.6" color="red" striped label="Red Striped" />
|
||||
<ProgressBar :progress="0.6" color="orange" striped label="Orange Striped" />
|
||||
<ProgressBar :progress="0.6" color="blue" striped label="Blue Striped" />
|
||||
<ProgressBar :progress="0.6" color="purple" striped label="Purple Striped" />
|
||||
<ProgressBar :progress="0.6" color="gray" striped label="Gray Striped" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const FullWidth: Story = {
|
||||
args: {
|
||||
progress: 0.5,
|
||||
fullWidth: true,
|
||||
},
|
||||
}
|
||||
32
packages/ui/src/stories/base/ProgressSpinner.stories.ts
Normal file
32
packages/ui/src/stories/base/ProgressSpinner.stories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ProgressSpinner from '../../components/base/ProgressSpinner.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ProgressSpinner',
|
||||
component: ProgressSpinner,
|
||||
} satisfies Meta<typeof ProgressSpinner>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
progress: 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllProgress: StoryObj = {
|
||||
render: () => ({
|
||||
components: { ProgressSpinner },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<ProgressSpinner :progress="0" />
|
||||
<ProgressSpinner :progress="0.25" />
|
||||
<ProgressSpinner :progress="0.5" />
|
||||
<ProgressSpinner :progress="0.75" />
|
||||
<ProgressSpinner :progress="1" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
213
packages/ui/src/stories/base/ProjectCard.stories.ts
Normal file
213
packages/ui/src/stories/base/ProjectCard.stories.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ProjectCard from '../../components/base/ProjectCard.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ProjectCard',
|
||||
component: ProjectCard,
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div class="display-mode--grid"><story /></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies Meta<typeof ProjectCard>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
id: 'example-mod',
|
||||
type: 'mod',
|
||||
name: 'Example Mod',
|
||||
author: 'ModAuthor',
|
||||
description:
|
||||
'An example mod that demonstrates the ProjectCard component with a detailed description.',
|
||||
iconUrl: 'https://cdn.modrinth.com/data/AANobbMI/icon.png',
|
||||
downloads: '1234567',
|
||||
follows: '12345',
|
||||
createdAt: '2023-01-15T00:00:00Z',
|
||||
updatedAt: '2024-01-15T00:00:00Z',
|
||||
categories: ['adventure', 'decoration'],
|
||||
projectTypeDisplay: 'Mod',
|
||||
projectTypeUrl: 'mod',
|
||||
clientSide: 'required',
|
||||
serverSide: 'optional',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllTypes: Story = {
|
||||
render: () => ({
|
||||
components: { ProjectCard },
|
||||
template: `
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ProjectCard
|
||||
id="example-mod"
|
||||
type="mod"
|
||||
name="Example Mod"
|
||||
author="ModAuthor"
|
||||
description="A wonderful mod that adds new features to the game."
|
||||
downloads="1000000"
|
||||
follows="50000"
|
||||
createdAt="2023-01-15T00:00:00Z"
|
||||
:categories="['technology', 'magic']"
|
||||
projectTypeDisplay="Mod"
|
||||
projectTypeUrl="mod"
|
||||
clientSide="required"
|
||||
serverSide="optional"
|
||||
/>
|
||||
<ProjectCard
|
||||
id="example-plugin"
|
||||
type="plugin"
|
||||
name="Example Plugin"
|
||||
author="PluginDev"
|
||||
description="A server plugin for managing permissions."
|
||||
downloads="500000"
|
||||
follows="25000"
|
||||
createdAt="2023-06-01T00:00:00Z"
|
||||
:categories="['utility']"
|
||||
projectTypeDisplay="Plugin"
|
||||
projectTypeUrl="plugin"
|
||||
serverSide="required"
|
||||
/>
|
||||
<ProjectCard
|
||||
id="example-modpack"
|
||||
type="modpack"
|
||||
name="Example Modpack"
|
||||
author="PackCreator"
|
||||
description="A curated collection of mods for the best experience."
|
||||
downloads="250000"
|
||||
follows="10000"
|
||||
createdAt="2023-03-20T00:00:00Z"
|
||||
:categories="['adventure']"
|
||||
projectTypeDisplay="Modpack"
|
||||
projectTypeUrl="modpack"
|
||||
/>
|
||||
<ProjectCard
|
||||
id="example-resourcepack"
|
||||
type="resourcepack"
|
||||
name="HD Textures"
|
||||
author="ArtistName"
|
||||
description="High definition textures for a better visual experience."
|
||||
downloads="750000"
|
||||
follows="30000"
|
||||
createdAt="2022-12-01T00:00:00Z"
|
||||
:categories="['realistic']"
|
||||
projectTypeDisplay="Resource Pack"
|
||||
projectTypeUrl="resourcepack"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithStatus: Story = {
|
||||
render: () => ({
|
||||
components: { ProjectCard },
|
||||
template: `
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ProjectCard
|
||||
id="draft-mod"
|
||||
type="mod"
|
||||
name="Draft Project"
|
||||
author="Developer"
|
||||
description="This project is still in draft mode."
|
||||
downloads="0"
|
||||
follows="0"
|
||||
createdAt="2024-01-01T00:00:00Z"
|
||||
:categories="['utility']"
|
||||
projectTypeDisplay="Mod"
|
||||
projectTypeUrl="mod"
|
||||
status="draft"
|
||||
/>
|
||||
<ProjectCard
|
||||
id="pending-mod"
|
||||
type="mod"
|
||||
name="Pending Review"
|
||||
author="Developer"
|
||||
description="This project is pending review."
|
||||
downloads="0"
|
||||
follows="0"
|
||||
createdAt="2024-01-01T00:00:00Z"
|
||||
:categories="['utility']"
|
||||
projectTypeDisplay="Mod"
|
||||
projectTypeUrl="mod"
|
||||
status="processing"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const DisplayModes: StoryObj = {
|
||||
decorators: [], // Remove default decorator for this story
|
||||
render: () => ({
|
||||
components: { ProjectCard },
|
||||
template: `
|
||||
<div class="flex flex-col gap-8">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-4">Grid Mode</h3>
|
||||
<div class="display-mode--grid">
|
||||
<ProjectCard
|
||||
id="grid-mod"
|
||||
type="mod"
|
||||
name="Example Mod"
|
||||
author="ModAuthor"
|
||||
description="A wonderful mod that adds new features to the game."
|
||||
downloads="1000000"
|
||||
follows="50000"
|
||||
createdAt="2023-01-15T00:00:00Z"
|
||||
:categories="['technology', 'magic']"
|
||||
projectTypeDisplay="Mod"
|
||||
projectTypeUrl="mod"
|
||||
clientSide="required"
|
||||
serverSide="optional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-4">List Mode</h3>
|
||||
<div class="display-mode--list">
|
||||
<ProjectCard
|
||||
id="list-mod"
|
||||
type="mod"
|
||||
name="Example Mod"
|
||||
author="ModAuthor"
|
||||
description="A wonderful mod that adds new features to the game."
|
||||
downloads="1000000"
|
||||
follows="50000"
|
||||
createdAt="2023-01-15T00:00:00Z"
|
||||
:categories="['technology', 'magic']"
|
||||
projectTypeDisplay="Mod"
|
||||
projectTypeUrl="mod"
|
||||
clientSide="required"
|
||||
serverSide="optional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-4">Gallery Mode</h3>
|
||||
<div class="display-mode--gallery">
|
||||
<ProjectCard
|
||||
id="gallery-mod"
|
||||
type="mod"
|
||||
name="Example Mod"
|
||||
author="ModAuthor"
|
||||
description="A wonderful mod that adds new features to the game."
|
||||
downloads="1000000"
|
||||
follows="50000"
|
||||
createdAt="2023-01-15T00:00:00Z"
|
||||
:categories="['technology', 'magic']"
|
||||
projectTypeDisplay="Mod"
|
||||
projectTypeUrl="mod"
|
||||
clientSide="required"
|
||||
serverSide="optional"
|
||||
featuredImage="https://cdn.modrinth.com/data/AANobbMI/images/be1cc1abc9cd9c2f52bb6a39be0b4b05af24d813.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
53
packages/ui/src/stories/base/RadialHeader.stories.ts
Normal file
53
packages/ui/src/stories/base/RadialHeader.stories.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import RadialHeader from '../../components/base/RadialHeader.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/RadialHeader',
|
||||
component: RadialHeader,
|
||||
} satisfies Meta<typeof RadialHeader>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { RadialHeader },
|
||||
template: `
|
||||
<RadialHeader class="p-8">
|
||||
<h1 class="text-2xl font-bold text-center">Radial Header Content</h1>
|
||||
</RadialHeader>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllColors: Story = {
|
||||
render: () => ({
|
||||
components: { RadialHeader },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<RadialHeader color="brand" class="p-8">
|
||||
<p class="text-center font-semibold">Brand</p>
|
||||
</RadialHeader>
|
||||
<RadialHeader color="red" class="p-8">
|
||||
<p class="text-center font-semibold">Red</p>
|
||||
</RadialHeader>
|
||||
<RadialHeader color="orange" class="p-8">
|
||||
<p class="text-center font-semibold">Orange</p>
|
||||
</RadialHeader>
|
||||
<RadialHeader color="green" class="p-8">
|
||||
<p class="text-center font-semibold">Green</p>
|
||||
</RadialHeader>
|
||||
<RadialHeader color="blue" class="p-8">
|
||||
<p class="text-center font-semibold">Blue</p>
|
||||
</RadialHeader>
|
||||
<RadialHeader color="purple" class="p-8">
|
||||
<p class="text-center font-semibold">Purple</p>
|
||||
</RadialHeader>
|
||||
<RadialHeader color="gray" class="p-8">
|
||||
<p class="text-center font-semibold">Gray</p>
|
||||
</RadialHeader>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
37
packages/ui/src/stories/base/RadioButtons.stories.ts
Normal file
37
packages/ui/src/stories/base/RadioButtons.stories.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import RadioButtons from '../../components/base/RadioButtons.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/RadioButtons',
|
||||
// @ts-ignore - error comes from generically typed component
|
||||
component: RadioButtons,
|
||||
render: (args) => ({
|
||||
components: { RadioButtons },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<RadioButtons v-bind="args" v-model="args.modelValue">
|
||||
<template #default="{ item }">{{ item }}</template>
|
||||
</RadioButtons>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof RadioButtons>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
modelValue: 'Option 1',
|
||||
},
|
||||
}
|
||||
|
||||
export const ManyOptions: Story = {
|
||||
args: {
|
||||
items: ['Daily', 'Weekly', 'Monthly', 'Yearly'],
|
||||
modelValue: 'Weekly',
|
||||
},
|
||||
}
|
||||
32
packages/ui/src/stories/base/ScrollablePanel.stories.ts
Normal file
32
packages/ui/src/stories/base/ScrollablePanel.stories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ScrollablePanel from '../../components/base/ScrollablePanel.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ScrollablePanel',
|
||||
component: ScrollablePanel,
|
||||
render: (args) => ({
|
||||
components: { ScrollablePanel },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<ScrollablePanel v-bind="args">
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<p v-for="i in 20" :key="i">Item {{ i }}</p>
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof ScrollablePanel>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const DisabledScrolling: Story = {
|
||||
args: {
|
||||
disableScrolling: true,
|
||||
},
|
||||
}
|
||||
64
packages/ui/src/stories/base/ServerNotice.stories.ts
Normal file
64
packages/ui/src/stories/base/ServerNotice.stories.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ServerNotice from '../../components/base/ServerNotice.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/ServerNotice',
|
||||
component: ServerNotice,
|
||||
} satisfies Meta<typeof ServerNotice>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
level: 'info',
|
||||
message: 'This is an informational server notice.',
|
||||
dismissable: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllLevels: StoryObj = {
|
||||
render: () => ({
|
||||
components: { ServerNotice },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<ServerNotice
|
||||
level="info"
|
||||
message="This is an informational notice for users."
|
||||
:dismissable="true"
|
||||
@dismiss="() => console.log('dismissed')"
|
||||
/>
|
||||
<ServerNotice
|
||||
level="warn"
|
||||
message="This is a warning notice that requires attention."
|
||||
:dismissable="true"
|
||||
@dismiss="() => console.log('dismissed')"
|
||||
/>
|
||||
<ServerNotice
|
||||
level="critical"
|
||||
message="This is a critical notice about an important issue."
|
||||
:dismissable="true"
|
||||
@dismiss="() => console.log('dismissed')"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithTitle: Story = {
|
||||
args: {
|
||||
level: 'warn',
|
||||
message: 'Server maintenance is scheduled for tonight at midnight.',
|
||||
dismissable: true,
|
||||
title: 'Scheduled Maintenance',
|
||||
},
|
||||
}
|
||||
|
||||
export const NonDismissable: Story = {
|
||||
args: {
|
||||
level: 'critical',
|
||||
message: 'Your account requires verification before you can upload projects.',
|
||||
dismissable: false,
|
||||
},
|
||||
}
|
||||
32
packages/ui/src/stories/base/SettingsLabel.stories.ts
Normal file
32
packages/ui/src/stories/base/SettingsLabel.stories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import SettingsLabel from '../../components/base/SettingsLabel.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/SettingsLabel',
|
||||
component: SettingsLabel,
|
||||
} satisfies Meta<typeof SettingsLabel>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Setting Name',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithDescription: Story = {
|
||||
args: {
|
||||
title: 'Enable Notifications',
|
||||
description: 'Receive email notifications when someone follows your project.',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithId: Story = {
|
||||
args: {
|
||||
id: 'setting-input',
|
||||
title: 'Username',
|
||||
description: 'Your unique username on the platform.',
|
||||
},
|
||||
}
|
||||
17
packages/ui/src/stories/base/SimpleBadge.stories.ts
Normal file
17
packages/ui/src/stories/base/SimpleBadge.stories.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import SimpleBadge from '../../components/base/SimpleBadge.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/SimpleBadge',
|
||||
component: SimpleBadge,
|
||||
} satisfies Meta<typeof SimpleBadge>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
formattedName: 'Badge Text',
|
||||
},
|
||||
}
|
||||
47
packages/ui/src/stories/base/Slider.stories.ts
Normal file
47
packages/ui/src/stories/base/Slider.stories.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Slider from '../../components/base/Slider.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Slider',
|
||||
component: Slider,
|
||||
} satisfies Meta<typeof Slider>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: 50,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithUnit: Story = {
|
||||
args: {
|
||||
modelValue: 50,
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit: '%',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithSnapPoints: Story = {
|
||||
args: {
|
||||
modelValue: 25,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 25,
|
||||
snapPoints: [0, 25, 50, 75, 100],
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
modelValue: 50,
|
||||
min: 0,
|
||||
max: 100,
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
30
packages/ui/src/stories/base/SmartClickable.stories.ts
Normal file
30
packages/ui/src/stories/base/SmartClickable.stories.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import SmartClickable from '../../components/base/SmartClickable.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/SmartClickable',
|
||||
component: SmartClickable,
|
||||
render: (args) => ({
|
||||
components: { SmartClickable },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<SmartClickable v-bind="args">
|
||||
<template #clickable>
|
||||
<a href="#" style="display: block; width: 100%; height: 100%;"></a>
|
||||
</template>
|
||||
<div style="padding: 1rem; background: var(--color-button-bg); border-radius: 0.5rem;">
|
||||
<h3>Clickable Card</h3>
|
||||
<p>The entire card is clickable</p>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof SmartClickable>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
42
packages/ui/src/stories/base/TagItem.stories.ts
Normal file
42
packages/ui/src/stories/base/TagItem.stories.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import TagItem from '../../components/base/TagItem.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/TagItem',
|
||||
component: TagItem,
|
||||
render: (args) => ({
|
||||
components: { TagItem },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<TagItem v-bind="args">Tag Name</TagItem>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof TagItem>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const WithAction: Story = {
|
||||
args: {
|
||||
action: () => alert('Tag clicked!'),
|
||||
},
|
||||
}
|
||||
|
||||
export const MultipleTags: Story = {
|
||||
render: () => ({
|
||||
components: { TagItem },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<TagItem>Minecraft</TagItem>
|
||||
<TagItem>Fabric</TagItem>
|
||||
<TagItem>Adventure</TagItem>
|
||||
<TagItem>Technology</TagItem>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
47
packages/ui/src/stories/base/Timeline.stories.ts
Normal file
47
packages/ui/src/stories/base/Timeline.stories.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Timeline from '../../components/base/Timeline.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Timeline',
|
||||
component: Timeline,
|
||||
render: (args) => ({
|
||||
components: { Timeline },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<Timeline v-bind="args">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<div style="width: 1rem; height: 1rem; border-radius: 50%; background: var(--color-brand);"></div>
|
||||
<span>Event 1</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<div style="width: 1rem; height: 1rem; border-radius: 50%; background: var(--color-brand);"></div>
|
||||
<span>Event 2</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<div style="width: 1rem; height: 1rem; border-radius: 50%; background: var(--color-brand);"></div>
|
||||
<span>Event 3</span>
|
||||
</div>
|
||||
</Timeline>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof Timeline>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const FadeOutStart: Story = {
|
||||
args: {
|
||||
fadeOutStart: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const FadeOutEnd: Story = {
|
||||
args: {
|
||||
fadeOutEnd: true,
|
||||
},
|
||||
}
|
||||
49
packages/ui/src/stories/base/Toggle.stories.ts
Normal file
49
packages/ui/src/stories/base/Toggle.stories.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Toggle from '../../components/base/Toggle.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Toggle',
|
||||
component: Toggle,
|
||||
} satisfies Meta<typeof Toggle>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Checked: Story = {
|
||||
args: {
|
||||
modelValue: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
modelValue: false,
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStates: Story = {
|
||||
render: () => ({
|
||||
components: { Toggle },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" /> Off
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="true" /> On
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :disabled="true" /> Disabled
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
77
packages/ui/src/stories/base/UnsavedChangesPopup.stories.ts
Normal file
77
packages/ui/src/stories/base/UnsavedChangesPopup.stories.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import UnsavedChangesPopup from '../../components/base/UnsavedChangesPopup.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UnsavedChangesPopup',
|
||||
// @ts-ignore
|
||||
component: UnsavedChangesPopup,
|
||||
} satisfies Meta<typeof UnsavedChangesPopup>
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default: StoryObj = {
|
||||
render: () => ({
|
||||
components: { UnsavedChangesPopup },
|
||||
template: `
|
||||
<div class="relative h-32">
|
||||
<UnsavedChangesPopup
|
||||
:original="{ name: 'Original Name' }"
|
||||
:modified="{ name: 'Modified Name' }"
|
||||
@save="() => console.log('Save clicked')"
|
||||
@reset="() => console.log('Reset clicked')"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Saving: StoryObj = {
|
||||
render: () => ({
|
||||
components: { UnsavedChangesPopup },
|
||||
template: `
|
||||
<div class="relative h-32">
|
||||
<UnsavedChangesPopup
|
||||
:original="{ name: 'Original' }"
|
||||
:modified="{ name: 'Changed' }"
|
||||
:saving="true"
|
||||
@save="() => console.log('Save clicked')"
|
||||
@reset="() => console.log('Reset clicked')"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const NoResetButton: StoryObj = {
|
||||
render: () => ({
|
||||
components: { UnsavedChangesPopup },
|
||||
template: `
|
||||
<div class="relative h-32">
|
||||
<UnsavedChangesPopup
|
||||
:original="{ name: 'Original' }"
|
||||
:modified="{ name: 'Changed' }"
|
||||
:canReset="false"
|
||||
@save="() => console.log('Save clicked')"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Hidden: StoryObj = {
|
||||
render: () => ({
|
||||
components: { UnsavedChangesPopup },
|
||||
template: `
|
||||
<div class="relative h-32">
|
||||
<p class="text-secondary mb-4">No changes detected - popup is hidden</p>
|
||||
<UnsavedChangesPopup
|
||||
:original="{ name: 'Same Value' }"
|
||||
:modified="{ name: 'Same Value' }"
|
||||
@save="() => console.log('Save clicked')"
|
||||
@reset="() => console.log('Reset clicked')"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
213
packages/ui/src/stories/modal/NewModal.stories.ts
Normal file
213
packages/ui/src/stories/modal/NewModal.stories.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type { StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
import NewModal from '../../components/modal/NewModal.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Modal/NewModal',
|
||||
component: NewModal,
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof NewModal>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Open Modal</button>
|
||||
</ButtonStyled>
|
||||
<NewModal ref="modalRef" header="Example Modal">
|
||||
<p>This is the modal content.</p>
|
||||
<p class="text-secondary mt-2">You can put any content here.</p>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithActions: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Open Modal with Actions</button>
|
||||
</ButtonStyled>
|
||||
<NewModal ref="modalRef" header="Confirm Action">
|
||||
<p>Are you sure you want to proceed with this action?</p>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled>
|
||||
<button @click="modalRef?.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="modalRef?.hide()">Confirm</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const DangerFade: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="openModal">Open Danger Modal</button>
|
||||
</ButtonStyled>
|
||||
<NewModal ref="modalRef" header="Delete Item" fade="danger">
|
||||
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled>
|
||||
<button @click="modalRef?.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="modalRef?.hide()">Delete</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WarningFade: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="openModal">Open Warning Modal</button>
|
||||
</ButtonStyled>
|
||||
<NewModal ref="modalRef" header="Warning" fade="warning">
|
||||
<p>This action may have unintended consequences. Please review before proceeding.</p>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled>
|
||||
<button @click="modalRef?.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="modalRef?.hide()">Proceed</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Scrollable: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Open Scrollable Modal</button>
|
||||
</ButtonStyled>
|
||||
<NewModal ref="modalRef" header="Scrollable Content" scrollable max-content-height="300px">
|
||||
<div class="space-y-4">
|
||||
<p v-for="i in 20" :key="i">
|
||||
This is paragraph {{ i }} of scrollable content. The modal will show fade indicators when scrolled.
|
||||
</p>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="modalRef?.hide()">Close</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const MergedHeader: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Open Modal (Merged Header)</button>
|
||||
</ButtonStyled>
|
||||
<NewModal ref="modalRef" hide-header merge-header>
|
||||
<div class="text-center py-8">
|
||||
<h2 class="text-xl font-bold mb-4">Custom Header Area</h2>
|
||||
<p>This modal has the header merged with content and only shows a close button.</p>
|
||||
</div>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const NotClosable: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Open Non-Closable Modal</button>
|
||||
</ButtonStyled>
|
||||
<NewModal ref="modalRef" header="Processing..." :closable="false" :close-on-esc="false" :close-on-click-outside="false">
|
||||
<p>This modal cannot be closed by clicking outside or pressing escape.</p>
|
||||
<p class="text-secondary mt-2">Only the action button can close it.</p>
|
||||
<template #actions>
|
||||
<div class="flex justify-end">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="modalRef?.hide()">I understand, close</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
3
packages/ui/src/styles/tailwind.css
Normal file
3
packages/ui/src/styles/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -2,7 +2,12 @@ import preset from '@modrinth/tooling-config/tailwind/tailwind-preset.ts'
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: ['./src/components/**/*.{js,vue,ts}', './src/pages/**/*.{js,vue,ts}'],
|
||||
content: [
|
||||
'./src/components/**/*.{js,vue,ts}',
|
||||
'./src/pages/**/*.{js,vue,ts}',
|
||||
'./src/stories/**/*.{js,vue,ts,mdx}',
|
||||
'./.storybook/**/*.{ts,js}',
|
||||
],
|
||||
presets: [preset],
|
||||
}
|
||||
|
||||
|
||||
44
packages/ui/vite.config.ts
Normal file
44
packages/ui/vite.config.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
svgLoader({
|
||||
svgoConfig: {
|
||||
plugins: [
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
cacheDir: '.vite',
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'index.ts'),
|
||||
name: 'ModrinthUI',
|
||||
formats: ['es'],
|
||||
fileName: 'index',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue'],
|
||||
},
|
||||
},
|
||||
})
|
||||
2103
pnpm-lock.yaml
generated
2103
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user