feat: redo settings and all org pages. (#601)
* feat: new components, deploy storybook * refactor: simplify component field definitions and enhance button loading state - Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook. - Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props. - Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback. - Minor CSS adjustments for better layout and styling consistency. * chore: add field, card, password components * feat: improve page component & update AuthUI path * chore: update bits-ui to version 2.14.4 and all org related UI's - Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4. - Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory. - Removed unused Course components and related files to clean up the codebase. - Enhanced various components to improve consistency and maintainability. * chore: move all setting buttons to the top and update auth ui styles * chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner - Removed all instances of svelte-loading-spinners from the project. - Updated components to use the new Spinner from @cio/ui/base/spinner. - Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners. - Added new CSS animations for rocket launch effects in app.css. - Enhanced email change flow with updated verification messages in translations. * chore: remove supabase file upload from dashboard and move to r2 * chore: update organization API and UI components - Added new API methods for organization plan operations and team management. - Introduced middleware for API key authentication and organization admin checks. - Refactored organization-related components to utilize the new API structure. - Updated various UI components to improve consistency and maintainability. - Enhanced error handling and validation for organization-related actions. - Removed deprecated services and streamlined the codebase.
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
# How to Create Storybook Stories
|
||||
|
||||
This guide documents the patterns and conventions for creating Storybook stories in the ClassroomIO UI component library.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [File Structure](#file-structure)
|
||||
- [Basic Story Structure](#basic-story-structure)
|
||||
- [Import Patterns](#import-patterns)
|
||||
- [Story Definition](#story-definition)
|
||||
- [Template Snippets](#template-snippets)
|
||||
- [Fields File](#fields-file)
|
||||
- [Component Categorization](#component-categorization)
|
||||
- [CSS Class Conventions](#css-class-conventions)
|
||||
- [LMS Content Guidelines](#lms-content-guidelines)
|
||||
- [Examples](#examples)
|
||||
|
||||
## File Structure
|
||||
|
||||
### Directory Organization
|
||||
|
||||
Stories are organized into two main categories:
|
||||
|
||||
- **Atoms** (`src/atoms/`): Basic, indivisible UI components (Button, Input, Badge, etc.)
|
||||
- **Molecules** (`src/molecules/`): Composite components made of multiple atoms (Card, Field, ButtonGroup, etc.)
|
||||
|
||||
### File Naming
|
||||
|
||||
1. Create a directory matching the component name (kebab-case)
|
||||
2. Create a story file: `{component-name}.stories.svelte`
|
||||
3. Optionally create a fields file: `fields.ts` (for controls configuration)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
src/molecules/hover-card/
|
||||
├── hover-card.stories.svelte
|
||||
└── fields.ts
|
||||
```
|
||||
|
||||
## Basic Story Structure
|
||||
|
||||
Every story file follows this structure:
|
||||
|
||||
```svelte
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
// Component imports
|
||||
import * as ComponentName from '@cio/ui/base/component-name';
|
||||
|
||||
// Other imports (icons, utilities, etc.)
|
||||
import IconName from '@lucide/svelte/icons/icon-name';
|
||||
|
||||
// Fields import (if using controls)
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
// Story definition
|
||||
const { Story } = defineMeta({
|
||||
title: 'Category/ComponentName',
|
||||
component: ComponentName.Root, // or ComponentName for single components
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
// State variables (if needed)
|
||||
let someState = $state('');
|
||||
</script>
|
||||
|
||||
<Story name="StoryName">
|
||||
{#snippet template()}
|
||||
<!-- Component usage -->
|
||||
{/snippet}
|
||||
</Story>
|
||||
```
|
||||
|
||||
## Import Patterns
|
||||
|
||||
### Component Imports
|
||||
|
||||
- **Namespace imports** for multi-part components:
|
||||
```svelte
|
||||
import * as HoverCard from '@cio/ui/base/hover-card';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
```
|
||||
|
||||
- **Named imports** for single components:
|
||||
```svelte
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Avatar } from '@cio/ui/base/avatar';
|
||||
```
|
||||
|
||||
### Icon Imports
|
||||
|
||||
Use Lucide Svelte icons:
|
||||
```svelte
|
||||
import CalendarDaysIcon from '@lucide/svelte/icons/calendar-days';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
```
|
||||
|
||||
## Story Definition
|
||||
|
||||
### Title Format
|
||||
|
||||
- **Atoms**: `'Atom/ComponentName'` (e.g., `'Atom/Button'`)
|
||||
- **Molecules**: `'Molecules/ComponentName'` (e.g., `'Molecules/HoverCard'`)
|
||||
|
||||
### Component Reference
|
||||
|
||||
- For multi-part components, reference the `Root` component:
|
||||
```svelte
|
||||
component: HoverCard.Root
|
||||
component: Card.Root
|
||||
```
|
||||
|
||||
- For single components, reference directly:
|
||||
```svelte
|
||||
component: Button
|
||||
component: Input
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Standard parameters configuration:
|
||||
```svelte
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS // Only if using fields.ts
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
Always include `tags: ['autodocs']` for automatic documentation generation.
|
||||
|
||||
## Template Snippets
|
||||
|
||||
### Basic Template
|
||||
|
||||
```svelte
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<ComponentName.Root>
|
||||
<!-- Component content -->
|
||||
</ComponentName.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
```
|
||||
|
||||
### Template with Args (for controls)
|
||||
|
||||
For atoms that need to pass args to controls:
|
||||
```svelte
|
||||
<Story name="Default">
|
||||
{#snippet template(args)}
|
||||
<Button {...args}>Button Text</Button>
|
||||
{/snippet}
|
||||
</Story>
|
||||
```
|
||||
|
||||
### Multiple Stories
|
||||
|
||||
Create multiple stories to showcase different use cases:
|
||||
```svelte
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<!-- Basic usage -->
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Avatar">
|
||||
{#snippet template()}
|
||||
<!-- Avatar variant -->
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Custom Content">
|
||||
{#snippet template()}
|
||||
<!-- Custom content variant -->
|
||||
{/snippet}
|
||||
</Story>
|
||||
```
|
||||
|
||||
## Fields File
|
||||
|
||||
Create a `fields.ts` file to control which props appear in Storybook controls:
|
||||
|
||||
```typescript
|
||||
export const FIELDS = ['class'] as string[];
|
||||
```
|
||||
|
||||
Common fields:
|
||||
- `'class'` - CSS classes
|
||||
- `'variant'` - Component variants
|
||||
- `'size'` - Component sizes
|
||||
- `'disabled'` - Disabled state
|
||||
- Add more as needed based on component props
|
||||
|
||||
## Component Categorization
|
||||
|
||||
### Atoms
|
||||
|
||||
Basic, indivisible components:
|
||||
- Button, Input, Badge, Avatar, Label, Checkbox, Switch, etc.
|
||||
- Single-purpose components
|
||||
- Usually don't compose other components
|
||||
|
||||
### Molecules
|
||||
|
||||
Composite components:
|
||||
- Card, Field, ButtonGroup, HoverCard, Empty, etc.
|
||||
- Made of multiple atoms or other components
|
||||
- More complex functionality
|
||||
|
||||
**Guideline**: If a component uses multiple other components together, it's likely a molecule.
|
||||
|
||||
## CSS Class Conventions
|
||||
|
||||
### UI Prefix
|
||||
|
||||
Use the `ui:` prefix for utility classes to scope them properly:
|
||||
|
||||
```svelte
|
||||
<!-- ✅ Correct -->
|
||||
<div class="ui:flex ui:flex-col ui:gap-6">
|
||||
<Button class="ui:w-full">
|
||||
|
||||
<!-- ❌ Incorrect -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button class="w-full">
|
||||
```
|
||||
|
||||
### When to Use UI Prefix
|
||||
|
||||
- **Always use** for utility classes (flex, grid, gap, padding, margin, etc.)
|
||||
- **Don't use** for component-specific classes or data attributes
|
||||
- **Don't use** for Tailwind classes that are part of component styling
|
||||
|
||||
### Examples
|
||||
|
||||
```svelte
|
||||
<!-- Utility classes - use ui: prefix -->
|
||||
<div class="ui:flex ui:items-center ui:gap-2">
|
||||
<div class="ui:space-y-2">
|
||||
<div class="ui:text-muted-foreground ui:text-sm">
|
||||
|
||||
<!-- Component classes - no prefix needed -->
|
||||
<Card.Root class="w-full max-w-sm">
|
||||
<HoverCard.Content class="w-80">
|
||||
```
|
||||
|
||||
## LMS Content Guidelines
|
||||
|
||||
Since ClassroomIO is an LMS platform, all story content should be **LMS-related**:
|
||||
|
||||
### ✅ Good Examples
|
||||
|
||||
- **Courses**: "Introduction to Web Development", "Advanced React Patterns"
|
||||
- **Users**: "Sarah Johnson (Instructor)", "Alex Martinez (Student)"
|
||||
- **Content**: Course descriptions, assignment types, enrollment information
|
||||
- **Actions**: "Create Course", "Enroll Student", "Submit Assignment"
|
||||
- **States**: "No courses yet", "Student enrolled", "Assignment submitted"
|
||||
|
||||
### ❌ Avoid
|
||||
|
||||
- Generic examples (Svelte, GitHub profiles, etc.)
|
||||
- Non-educational content
|
||||
- Examples that don't relate to learning/teaching
|
||||
|
||||
### Content Patterns
|
||||
|
||||
**Instructor Profiles:**
|
||||
```svelte
|
||||
<h4 class="text-sm font-semibold">Sarah Johnson</h4>
|
||||
<p class="text-sm">Senior Full-Stack Developer & Instructor</p>
|
||||
<span class="text-muted-foreground text-xs">Teaching since January 2020</span>
|
||||
```
|
||||
|
||||
**Course Information:**
|
||||
```svelte
|
||||
<h4 class="text-sm font-semibold">Advanced React Patterns</h4>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Master advanced React concepts including hooks, context, performance optimization...
|
||||
</p>
|
||||
```
|
||||
|
||||
**Student Information:**
|
||||
```svelte
|
||||
<h4 class="text-sm font-semibold">Alex Martinez</h4>
|
||||
<p class="text-muted-foreground text-xs">5 courses enrolled</p>
|
||||
<p class="text-muted-foreground text-xs">Active since March 2024</p>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Atom (Button)
|
||||
|
||||
```svelte
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Atom/Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template(args)}
|
||||
<Button {...args}>Create Course</Button>
|
||||
{/snippet}
|
||||
</Story>
|
||||
```
|
||||
|
||||
### Example 2: Multi-Part Molecule (HoverCard)
|
||||
|
||||
```svelte
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import CalendarDaysIcon from '@lucide/svelte/icons/calendar-days';
|
||||
import * as HoverCard from '@cio/ui/base/hover-card';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/HoverCard',
|
||||
component: HoverCard.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Instructor Profile">
|
||||
{#snippet template()}
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger
|
||||
href="/instructors/sarah-johnson"
|
||||
class="ui:rounded-sm ui:underline-offset-4 ui:hover:underline"
|
||||
>
|
||||
Sarah Johnson
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content class="w-80">
|
||||
<div class="flex justify-between space-x-4">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/shadcn.png" />
|
||||
<Avatar.Fallback>SJ</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="space-y-1">
|
||||
<h4 class="text-sm font-semibold">Sarah Johnson</h4>
|
||||
<p class="text-sm">Senior Full-Stack Developer & Instructor</p>
|
||||
<div class="flex items-center pt-2">
|
||||
<CalendarDaysIcon class="me-2 size-4 opacity-70" />
|
||||
<span class="text-muted-foreground text-xs">Teaching since January 2020</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
```
|
||||
|
||||
### Example 3: Complex Molecule with State (Field)
|
||||
|
||||
```svelte
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/Field',
|
||||
component: Field.Field,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
let email = $state('');
|
||||
</script>
|
||||
|
||||
<Story name="Course Enrollment">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Set>
|
||||
<Field.Legend>Enroll Student</Field.Legend>
|
||||
<Field.Description>Add a new student to your course</Field.Description>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="student-email">Student Email</Field.Label>
|
||||
<Input id="student-email" type="email" bind:value={email} placeholder="[email protected]" />
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When creating a new story, ensure:
|
||||
|
||||
- [ ] File is in the correct directory (`atoms/` or `molecules/`)
|
||||
- [ ] File is named `{component-name}.stories.svelte`
|
||||
- [ ] Imports use `@cio/ui/base/{component-name}` pattern
|
||||
- [ ] Title follows `'Category/ComponentName'` format
|
||||
- [ ] Component reference is correct (Root for multi-part, direct for single)
|
||||
- [ ] `tags: ['autodocs']` is included
|
||||
- [ ] All utility classes use `ui:` prefix
|
||||
- [ ] Content is LMS-related (courses, students, instructors, etc.)
|
||||
- [ ] Multiple stories showcase different use cases
|
||||
- [ ] `fields.ts` is created if controls are needed
|
||||
- [ ] State variables use `$state()` for reactivity
|
||||
- [ ] Icons are imported from `@lucide/svelte/icons/`
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- **State Management**: Use Svelte 5 runes (`$state()`, `$derived()`) for reactive state
|
||||
- **Layout**: Most stories use `layout: 'centered'` in parameters
|
||||
- **Styling**: Prefer Tailwind utility classes with `ui:` prefix
|
||||
- **Accessibility**: Include proper ARIA labels and semantic HTML
|
||||
- **Documentation**: Story names should be descriptive and reflect the use case
|
||||
|
||||
## Resources
|
||||
|
||||
- [Storybook Svelte CSF Addon](https://storybook.js.org/addons/@storybook/addon-svelte-csf)
|
||||
- [Bits UI Documentation](https://bits-ui.com/docs/components)
|
||||
- [Lucide Icons](https://lucide.dev/icons/)
|
||||
|
||||
@@ -16,4 +16,36 @@ To build the Storybook:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
```
|
||||
|
||||
## Build Storybook
|
||||
|
||||
To build the Storybook (alternative command):
|
||||
|
||||
```bash
|
||||
pnpm build-storybook
|
||||
```
|
||||
|
||||
## Upload
|
||||
|
||||
To upload the built Storybook to Cloudflare R2:
|
||||
|
||||
```bash
|
||||
pnpm upload:storybook
|
||||
```
|
||||
|
||||
**Note:** This requires the build to be completed first. Make sure you have the following environment variables set:
|
||||
|
||||
- `CLOUDFLARE_ACCESS_KEY`
|
||||
- `CLOUDFLARE_SECRET_ACCESS_KEY`
|
||||
- `CLOUDFLARE_ACCOUNT_ID`
|
||||
|
||||
## Publish
|
||||
|
||||
To build and upload the Storybook in one command:
|
||||
|
||||
```bash
|
||||
pnpm publish:storybook
|
||||
```
|
||||
|
||||
This will build the Storybook and then upload it to R2.
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
"dev": "storybook dev -p 6006",
|
||||
"build": "storybook build",
|
||||
"preview": "storybook preview",
|
||||
"build-storybook": "storybook build"
|
||||
"build-storybook": "storybook build",
|
||||
"upload:storybook": "tsx scripts/upload-storybook.ts",
|
||||
"publish:storybook": "pnpm build-storybook && pnpm upload:storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cio/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tiptap/pm": "^3.7.1",
|
||||
"@tiptap/core": "^3.7.1",
|
||||
"@aws-sdk/client-s3": "^3.388.0",
|
||||
"@cio/tsconfig": "workspace:*",
|
||||
"@storybook/addon-essentials": "^8.4.2",
|
||||
"@storybook/addon-interactions": "^8.4.2",
|
||||
"@storybook/addon-links": "^8.4.2",
|
||||
@@ -26,10 +28,14 @@
|
||||
"@storybook/test": "^8.4.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@tiptap/core": "^3.7.1",
|
||||
"@tiptap/pm": "^3.7.1",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"storybook": "^8.4.2",
|
||||
"svelte": "^5.39.11",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"@cio/tsconfig": "workspace:*",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript-svelte-plugin": "^0.3.50",
|
||||
"vite": "^7.1.4"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { join, relative } from 'path';
|
||||
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||
|
||||
interface UploadOptions {
|
||||
key: string;
|
||||
content: Buffer | string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
class StorybookUploader {
|
||||
private s3Client: S3Client | null = null;
|
||||
private buildDir: string;
|
||||
private bucket: string;
|
||||
|
||||
constructor() {
|
||||
this.buildDir = join(process.cwd(), 'storybook-static');
|
||||
this.bucket = 'assets';
|
||||
this.initializeS3Client();
|
||||
}
|
||||
|
||||
private initializeS3Client() {
|
||||
const accessKey = process.env.CLOUDFLARE_ACCESS_KEY;
|
||||
const secretKey = process.env.CLOUDFLARE_SECRET_ACCESS_KEY;
|
||||
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
||||
|
||||
if (accessKey && secretKey && accountId) {
|
||||
this.s3Client = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretKey
|
||||
}
|
||||
});
|
||||
console.log('✅ S3 client initialized for Cloudflare R2');
|
||||
} else {
|
||||
console.warn('⚠️ Cloudflare R2 credentials not found. Cannot upload to R2.');
|
||||
throw new Error('Missing Cloudflare R2 credentials');
|
||||
}
|
||||
}
|
||||
|
||||
private getContentType(filePath: string): string {
|
||||
const ext = filePath.split('.').pop()?.toLowerCase();
|
||||
const contentTypes: Record<string, string> = {
|
||||
html: 'text/html',
|
||||
js: 'application/javascript',
|
||||
json: 'application/json',
|
||||
css: 'text/css',
|
||||
png: 'image/png',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
gif: 'image/gif',
|
||||
svg: 'image/svg+xml',
|
||||
ico: 'image/x-icon',
|
||||
woff: 'font/woff',
|
||||
woff2: 'font/woff2',
|
||||
ttf: 'font/ttf',
|
||||
eot: 'application/vnd.ms-fontobject',
|
||||
map: 'application/json',
|
||||
txt: 'text/plain',
|
||||
xml: 'application/xml'
|
||||
};
|
||||
return contentTypes[ext || ''] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
private getCacheControl(filePath: string): string {
|
||||
// Cache static assets longer, HTML files shorter
|
||||
if (filePath.endsWith('.html')) {
|
||||
return 'public, max-age=300'; // 5 minutes for HTML
|
||||
}
|
||||
if (filePath.match(/\.(js|css|woff|woff2|ttf|png|jpg|jpeg|gif|svg|ico)$/)) {
|
||||
return 'public, max-age=31536000, immutable'; // 1 year for assets
|
||||
}
|
||||
return 'public, max-age=3600'; // 1 hour default
|
||||
}
|
||||
|
||||
private getAllFiles(dir: string, baseDir: string = dir): string[] {
|
||||
const files: string[] = [];
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
const relativePath = relative(baseDir, fullPath);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...this.getAllFiles(fullPath, baseDir));
|
||||
} else {
|
||||
files.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async uploadFile(filePath: string): Promise<void> {
|
||||
if (!this.s3Client) {
|
||||
throw new Error('S3 client not initialized. Check your Cloudflare R2 credentials.');
|
||||
}
|
||||
|
||||
try {
|
||||
const fullPath = join(this.buildDir, filePath);
|
||||
const content = readFileSync(fullPath);
|
||||
const contentType = this.getContentType(filePath);
|
||||
const cacheControl = this.getCacheControl(filePath);
|
||||
|
||||
// Upload to storybook/ directory in R2
|
||||
const key = `storybook/${filePath}`;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: this.bucket,
|
||||
Key: key,
|
||||
Body: content,
|
||||
ContentType: contentType,
|
||||
CacheControl: cacheControl
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
console.log(`✅ Uploaded: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error uploading ${filePath}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async uploadAll(): Promise<void> {
|
||||
try {
|
||||
// Check if build directory exists
|
||||
try {
|
||||
statSync(this.buildDir);
|
||||
} catch {
|
||||
throw new Error(`Build directory not found: ${this.buildDir}. Please run 'pnpm build' first.`);
|
||||
}
|
||||
|
||||
console.log('🔄 Starting Storybook upload to R2...');
|
||||
console.log(`📁 Build directory: ${this.buildDir}`);
|
||||
|
||||
const files = this.getAllFiles(this.buildDir);
|
||||
console.log(`📦 Found ${files.length} files to upload`);
|
||||
|
||||
// Upload files in batches to avoid overwhelming the API
|
||||
const batchSize = 10;
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
await Promise.all(batch.map((file) => this.uploadFile(file)));
|
||||
console.log(`📤 Uploaded batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(files.length / batchSize)}`);
|
||||
}
|
||||
|
||||
console.log('🎉 Storybook upload completed successfully!');
|
||||
console.log(`📊 Total files uploaded: ${files.length}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to upload Storybook:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
Usage: tsx scripts/upload-storybook.ts
|
||||
|
||||
Examples:
|
||||
tsx scripts/upload-storybook.ts
|
||||
|
||||
Environment Variables Required for R2 Upload:
|
||||
CLOUDFLARE_ACCESS_KEY
|
||||
CLOUDFLARE_SECRET_ACCESS_KEY
|
||||
CLOUDFLARE_ACCOUNT_ID
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
const uploader = new StorybookUploader();
|
||||
await uploader.uploadAll();
|
||||
}
|
||||
|
||||
// Run the script if called directly
|
||||
if (process.argv[1] && process.argv[1].endsWith('upload-storybook.ts')) {
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('Script completed successfully');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Script failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { StorybookUploader };
|
||||
@@ -1,7 +1,4 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Accordion } from '@cio/ui/base/accordion';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Accordion>)[] = [
|
||||
export const FIELDS = [
|
||||
'type',
|
||||
'value',
|
||||
'onValueChange',
|
||||
@@ -9,4 +6,4 @@ export const FIELDS: (keyof ComponentProps<typeof Accordion>)[] = [
|
||||
'name',
|
||||
'loop',
|
||||
'orientation'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Avatar } from '@cio/ui/base/avatar';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Avatar>)[] = ['loadingStatus', 'onLoadingStatusChange', 'delayMs'];
|
||||
export const FIELDS = ['loadingStatus', 'onLoadingStatusChange', 'delayMs'] as string[];
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<Button>Default Button</Button>
|
||||
{#snippet template(args)}
|
||||
<Button {...args}>Default Button</Button>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
<Story name="Loading">
|
||||
{#snippet template(args)}
|
||||
<Button {...args} isLoading>Loading Button</Button>
|
||||
<Button {...args} loading>Loading Button</Button>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Button>)[] = ['variant', 'size', 'disabled', 'class', 'type', 'href'];
|
||||
export const FIELDS = ['variant', 'size', 'disabled', 'class', 'type', 'href', 'loading'] as string[];
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Checkbox } from '@cio/ui/base/checkbox';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Checkbox>)[] = [
|
||||
export const FIELDS = [
|
||||
'checked',
|
||||
'onCheckedChange',
|
||||
'disabled',
|
||||
'name',
|
||||
'value',
|
||||
'required'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Collapsible } from '@cio/ui/base/collapsible';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Collapsible>)[] = [
|
||||
export const FIELDS = [
|
||||
'open',
|
||||
'onOpenChange',
|
||||
'onOpenChangeComplete',
|
||||
'disabled'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Command } from '@cio/ui/base/command';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Command>)[] = [
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'onValueChange',
|
||||
'label',
|
||||
'filter',
|
||||
'columns',
|
||||
'disablePointerSelection'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Dialog.Root>)[] = ['open', 'onOpenChange'];
|
||||
export const FIELDS = ['open', 'onOpenChange'] as string[];
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Input>)[] = [
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'type',
|
||||
'placeholder',
|
||||
@@ -11,4 +8,4 @@ export const FIELDS: (keyof ComponentProps<typeof Input>)[] = [
|
||||
'name',
|
||||
'required',
|
||||
'readonly'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
|
||||
import BadgeCheckIcon from '@lucide/svelte/icons/badge-check';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
||||
import ShieldAlertIcon from '@lucide/svelte/icons/shield-alert';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Atom/Item',
|
||||
component: Item.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: []
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
const music = [
|
||||
{
|
||||
title: 'Midnight City Lights',
|
||||
artist: 'Neon Dreams',
|
||||
album: 'Electric Nights',
|
||||
duration: '3:45'
|
||||
},
|
||||
{
|
||||
title: 'Coffee Shop Conversations',
|
||||
artist: 'The Morning Brew',
|
||||
album: 'Urban Stories',
|
||||
duration: '4:05'
|
||||
},
|
||||
{
|
||||
title: 'Digital Rain',
|
||||
artist: 'Cyber Symphony',
|
||||
album: 'Binary Beats',
|
||||
duration: '3:30'
|
||||
}
|
||||
];
|
||||
|
||||
const people = [
|
||||
{
|
||||
username: 'shadcn',
|
||||
avatar: 'https://github.com/shadcn.png',
|
||||
email: '[email protected]'
|
||||
},
|
||||
{
|
||||
username: 'maxleiter',
|
||||
avatar: 'https://github.com/maxleiter.png',
|
||||
email: '[email protected]'
|
||||
},
|
||||
{
|
||||
username: 'evilrabbit',
|
||||
avatar: 'https://github.com/evilrabbit.png',
|
||||
email: '[email protected]'
|
||||
}
|
||||
];
|
||||
|
||||
const models = [
|
||||
{
|
||||
name: 'v0-1.5-sm',
|
||||
description: 'Everyday tasks and UI generation.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1650804068570-7fb2e3dbf888?q=80&w=640&auto=format&fit=crop',
|
||||
credit: 'Valeria Reverdo on Unsplash'
|
||||
},
|
||||
{
|
||||
name: 'v0-1.5-lg',
|
||||
description: 'Advanced thinking or reasoning.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1610280777472-54133d004c8c?q=80&w=640&auto=format&fit=crop',
|
||||
credit: 'Michael Oeser on Unsplash'
|
||||
},
|
||||
{
|
||||
name: 'v0-2.0-mini',
|
||||
description: 'Open Source model for everyone.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1602146057681-08560aee8cde?q=80&w=640&auto=format&fit=crop',
|
||||
credit: 'Cherry Laithang on Unsplash'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Story name="Basic Item">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-md flex-col gap-6">
|
||||
<Item.Root variant="outline">
|
||||
<Item.Content>
|
||||
<Item.Title>Basic Item</Item.Title>
|
||||
<Item.Description>A simple item with title and description.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button variant="outline" size="sm">Action</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
<Item.Root variant="outline" size="sm">
|
||||
{#snippet child({ props })}
|
||||
<a href="#/" {...props}>
|
||||
<Item.Media>
|
||||
<BadgeCheckIcon class="size-5" />
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title>Your profile has been verified.</Item.Title>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Variants">
|
||||
{#snippet template()}
|
||||
<div class="flex flex-col gap-6">
|
||||
<Item.Root>
|
||||
<Item.Content>
|
||||
<Item.Title>Default Variant</Item.Title>
|
||||
<Item.Description>Standard styling with subtle background and borders.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button variant="outline" size="sm">Open</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
<Item.Root variant="outline">
|
||||
<Item.Content>
|
||||
<Item.Title>Outline Variant</Item.Title>
|
||||
<Item.Description>Outlined style with clear borders and transparent background.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button variant="outline" size="sm">Open</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
<Item.Root variant="muted">
|
||||
<Item.Content>
|
||||
<Item.Title>Muted Variant</Item.Title>
|
||||
<Item.Description>Subdued appearance with muted colors for secondary content.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button variant="outline" size="sm">Open</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Size">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-md flex-col gap-6">
|
||||
<Item.Root variant="outline">
|
||||
<Item.Content>
|
||||
<Item.Title>Basic Item</Item.Title>
|
||||
<Item.Description>A simple item with title and description.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button variant="outline" size="sm">Action</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
<Item.Root variant="outline" size="sm">
|
||||
{#snippet child({ props })}
|
||||
<a href="#/" {...props}>
|
||||
<Item.Media>
|
||||
<BadgeCheckIcon class="size-5" />
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title>Your profile has been verified.</Item.Title>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Icon">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-lg flex-col gap-6">
|
||||
<Item.Root variant="outline">
|
||||
<Item.Media variant="icon">
|
||||
<ShieldAlertIcon />
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title>Security Alert</Item.Title>
|
||||
<Item.Description>New login detected from unknown device.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button size="sm" variant="outline">Review</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Avatar">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-lg flex-col gap-6">
|
||||
<Item.Root variant="outline">
|
||||
<Item.Media>
|
||||
<Avatar.Root class="size-10">
|
||||
<Avatar.Image src="https://github.com/evilrabbit.png" />
|
||||
<Avatar.Fallback>ER</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title>Evil Rabbit</Item.Title>
|
||||
<Item.Description>Last seen 5 months ago</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button size="icon" variant="outline" class="rounded-full" aria-label="Invite">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
<Item.Root variant="outline">
|
||||
<Item.Media>
|
||||
<div
|
||||
class="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale"
|
||||
>
|
||||
<Avatar.Root class="hidden sm:flex">
|
||||
<Avatar.Image src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<Avatar.Fallback>CN</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<Avatar.Root class="hidden sm:flex">
|
||||
<Avatar.Image src="https://github.com/maxleiter.png" alt="@maxleiter" />
|
||||
<Avatar.Fallback>LR</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
|
||||
<Avatar.Fallback>ER</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</div>
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title>No Team Members</Item.Title>
|
||||
<Item.Description>Invite your team to collaborate on this project.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button size="sm" variant="outline">Invite</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Image">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-md flex-col gap-6">
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
{#each music as song (song)}
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href="#/" {...props}>
|
||||
<Item.Media variant="image">
|
||||
<img
|
||||
src={`https://avatar.vercel.sh/${song.title}`}
|
||||
alt={song.title}
|
||||
width="32"
|
||||
height="32"
|
||||
class="size-8 rounded object-cover grayscale"
|
||||
/>
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title class="line-clamp-1">
|
||||
{song.title} - <span class="text-muted-foreground">{song.album}</span>
|
||||
</Item.Title>
|
||||
<Item.Description>{song.artist}</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Content class="flex-none text-center">
|
||||
<Item.Description>{song.duration}</Item.Description>
|
||||
</Item.Content>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Group">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-md flex-col gap-6">
|
||||
<Item.Group>
|
||||
{#each people as person, index (person.username)}
|
||||
<Item.Root>
|
||||
<Item.Media>
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src={person.avatar} class="grayscale" />
|
||||
<Avatar.Fallback>{person.username.charAt(0)}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</Item.Media>
|
||||
<Item.Content class="gap-1">
|
||||
<Item.Title>{person.username}</Item.Title>
|
||||
<Item.Description>{person.email}</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button variant="ghost" size="icon" class="rounded-full">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
{#if index !== people.length - 1}
|
||||
<Item.Separator />
|
||||
{/if}
|
||||
{/each}
|
||||
</Item.Group>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Header">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-xl flex-col gap-6">
|
||||
<Item.Group class="grid grid-cols-3 gap-4">
|
||||
{#each models as model (model.name)}
|
||||
<Item.Root variant="outline">
|
||||
<Item.Header>
|
||||
<img
|
||||
src={model.image}
|
||||
alt={model.name}
|
||||
width="128"
|
||||
height="128"
|
||||
class="aspect-square w-full rounded-sm object-cover"
|
||||
/>
|
||||
</Item.Header>
|
||||
<Item.Content>
|
||||
<Item.Title>{model.name}</Item.Title>
|
||||
<Item.Description>{model.description}</Item.Description>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
{/each}
|
||||
</Item.Group>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Link">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<Item.Root>
|
||||
{#snippet child({ props })}
|
||||
<a href="#/" {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>Visit our documentation</Item.Title>
|
||||
<Item.Description>Learn how to get started with our components.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href="#/" target="_blank" rel="noopener noreferrer" {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>External resource</Item.Title>
|
||||
<Item.Description>Opens in a new tab with security attributes.</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ExternalLinkIcon class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Dropdown">
|
||||
{#snippet template()}
|
||||
<div class="flex min-h-64 w-full max-w-md flex-col items-center gap-6">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" size="sm" class="w-fit">
|
||||
Select <ChevronDownIcon />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-72 [--radius:0.65rem]" align="end">
|
||||
{#each people as person (person.username)}
|
||||
<DropdownMenu.Item class="p-0">
|
||||
<Item.Root size="sm" class="w-full p-2">
|
||||
<Item.Media>
|
||||
<Avatar.Root class="size-8">
|
||||
<Avatar.Image src={person.avatar} class="grayscale" />
|
||||
<Avatar.Fallback>{person.username.charAt(0)}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</Item.Media>
|
||||
<Item.Content class="gap-0.5">
|
||||
<Item.Title>{person.username}</Item.Title>
|
||||
<Item.Description>{person.email}</Item.Description>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Label>)[] = ['children', 'child'];
|
||||
export const FIELDS = ['children', 'child'] as string[];
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Menubar } from '@cio/ui/base/menubar';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Menubar>)[] = [
|
||||
export const FIELDS = [
|
||||
'onValueChange',
|
||||
'value',
|
||||
'dir',
|
||||
'onOpenChange',
|
||||
'disabled'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Pagination } from '@cio/ui/base/pagination';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Pagination>)[] = [
|
||||
export const FIELDS = [
|
||||
'count',
|
||||
'page',
|
||||
'onPageChange',
|
||||
'perPage',
|
||||
'orientation'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Progress } from '@cio/ui/base/progress';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Progress>)[] = ['max', 'min', 'value'];
|
||||
export const FIELDS = ['max', 'min', 'value'] as string[];
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { RadioGroup } from '@cio/ui/base/radio-group';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof RadioGroup>)[] = [
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'onValueChange',
|
||||
'disabled',
|
||||
@@ -9,4 +6,4 @@ export const FIELDS: (keyof ComponentProps<typeof RadioGroup>)[] = [
|
||||
'name',
|
||||
'loop',
|
||||
'orientation'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Select.Root>)[] = [
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'onValueChange',
|
||||
'disabled',
|
||||
@@ -11,4 +8,4 @@ export const FIELDS: (keyof ComponentProps<typeof Select.Root>)[] = [
|
||||
'required',
|
||||
'autocomplete',
|
||||
'allowDeselect'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Separator } from '@cio/ui/base/separator';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Separator>)[] = ['orientation', 'decorative'];
|
||||
export const FIELDS = ['orientation', 'decorative'] as string[];
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Sheet } from '@cio/ui/base/sheet';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Sheet>)[] = ['open', 'onOpenChange'];
|
||||
export const FIELDS = ['open', 'onOpenChange'] as string[];
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
>Make changes to your profile here. Click save when you're done.</Sheet.SheetDescription
|
||||
>
|
||||
</Sheet.SheetHeader>
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-4 p-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label.Root for="name" class="text-right">Name</Label.Root>
|
||||
<Input.Root id="name" value="Pedro Duarte" class="col-span-3" />
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Skeleton>)[] = ['class'];
|
||||
export const FIELDS = ['class'] as string[];
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Toaster as Sonner } from '@cio/ui/base/sonner';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Sonner>)[] = ['types', 'position', 'expand'];
|
||||
export const FIELDS = ['types', 'position', 'expand'] as string[];
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import * as InputGroup from '@cio/ui/base/input-group';
|
||||
import * as Empty from '@cio/ui/base/empty';
|
||||
import { Progress } from '@cio/ui/base/progress';
|
||||
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Atom/Spinner',
|
||||
@@ -18,34 +25,144 @@
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Small">
|
||||
<Story name="Size">
|
||||
{#snippet template()}
|
||||
<Spinner class="h-4 w-4" />
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Large">
|
||||
{#snippet template()}
|
||||
<Spinner class="h-12 w-12" />
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Custom Color">
|
||||
{#snippet template()}
|
||||
<div class="flex gap-4">
|
||||
<Spinner class="text-blue-500" />
|
||||
<Spinner class="text-green-500" />
|
||||
<Spinner class="text-red-500" />
|
||||
<Spinner class="text-purple-500" />
|
||||
<div class="flex items-center gap-6">
|
||||
<Spinner class="size-3" />
|
||||
<Spinner class="size-4" />
|
||||
<Spinner class="size-6" />
|
||||
<Spinner class="size-8" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Text">
|
||||
<Story name="Color">
|
||||
{#snippet template()}
|
||||
<div class="flex items-center gap-2">
|
||||
<Spinner />
|
||||
<span>Loading...</span>
|
||||
<div class="flex items-center gap-6">
|
||||
<Spinner class="size-6 text-red-500" />
|
||||
<Spinner class="size-6 text-green-500" />
|
||||
<Spinner class="size-6 text-blue-500" />
|
||||
<Spinner class="size-6 text-yellow-500" />
|
||||
<Spinner class="size-6 text-purple-500" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Button">
|
||||
{#snippet template()}
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<Button disabled size="sm">
|
||||
<Spinner />
|
||||
Loading...
|
||||
</Button>
|
||||
<Button variant="outline" disabled size="sm">
|
||||
<Spinner />
|
||||
Please wait
|
||||
</Button>
|
||||
<Button variant="secondary" disabled size="sm">
|
||||
<Spinner />
|
||||
Processing
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Badge">
|
||||
{#snippet template()}
|
||||
<div class="flex items-center gap-4 [--radius:1.2rem]">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Updating
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Processing
|
||||
</Badge>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Input Group">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Send a message..." disabled />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Spinner />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Textarea placeholder="Send a message..." disabled />
|
||||
<InputGroup.Addon align="block-end">
|
||||
<Spinner />
|
||||
Validating...
|
||||
<InputGroup.Button class="ms-auto" variant="default">
|
||||
<ArrowUpIcon />
|
||||
<span class="sr-only">Send</span>
|
||||
</InputGroup.Button>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Empty">
|
||||
{#snippet template()}
|
||||
<Empty.Root class="w-full">
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<Spinner />
|
||||
</Empty.Media>
|
||||
<Empty.Title>Processing your request</Empty.Title>
|
||||
<Empty.Description>Please wait while we process your request. Do not refresh the page.</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button variant="outline" size="sm">Cancel</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Item">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-md flex-col gap-4 [--radius:1rem]">
|
||||
<Item.Root variant="outline">
|
||||
<Item.Media variant="icon">
|
||||
<Spinner />
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title>Downloading...</Item.Title>
|
||||
<Item.Description>129 MB / 1000 MB</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions class="hidden sm:flex">
|
||||
<Button variant="outline" size="sm">Cancel</Button>
|
||||
</Item.Actions>
|
||||
<Item.Footer>
|
||||
<Progress value={75} />
|
||||
</Item.Footer>
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Item with Payment">
|
||||
{#snippet template()}
|
||||
<div class="flex w-full max-w-xs flex-col gap-4 [--radius:1rem]">
|
||||
<Item.Root variant="muted">
|
||||
<Item.Media>
|
||||
<Spinner />
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title class="line-clamp-1">Processing payment...</Item.Title>
|
||||
</Item.Content>
|
||||
<Item.Content class="flex-none justify-end">
|
||||
<span class="text-sm tabular-nums">$100.00</span>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Switch>)[] = [
|
||||
export const FIELDS = [
|
||||
'checked',
|
||||
'onCheckedChange',
|
||||
'disabled',
|
||||
'name',
|
||||
'required',
|
||||
'value'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Tabs } from '@cio/ui/base/tabs';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Tabs>)[] = [
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'onValueChange',
|
||||
'disabled',
|
||||
'activationMode',
|
||||
'orientation'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { Tooltip } from '@cio/ui/base/tooltip';
|
||||
|
||||
export const FIELDS: (keyof ComponentProps<typeof Tooltip>)[] = [
|
||||
export const FIELDS = [
|
||||
'disableHoverableContent',
|
||||
'delayDuration',
|
||||
'ignoreNonKeyboardFocus',
|
||||
'disabled'
|
||||
];
|
||||
] as string[];
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap');
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import '@cio/ui/output.css';
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import Bot from '@lucide/svelte/icons/bot';
|
||||
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
||||
import Copy from '@lucide/svelte/icons/copy';
|
||||
import Share from '@lucide/svelte/icons/share';
|
||||
import Trash from '@lucide/svelte/icons/trash';
|
||||
import UserRoundX from '@lucide/svelte/icons/user-round-x';
|
||||
import VolumeOff from '@lucide/svelte/icons/volume-off';
|
||||
import AudioLines from '@lucide/svelte/icons/audio-lines';
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import Archive from '@lucide/svelte/icons/archive';
|
||||
import CalendarPlus from '@lucide/svelte/icons/calendar-plus';
|
||||
import Clock from '@lucide/svelte/icons/clock';
|
||||
import ListFilter from '@lucide/svelte/icons/list-filter';
|
||||
import MailCheck from '@lucide/svelte/icons/mail-check';
|
||||
import MoreHorizontal from '@lucide/svelte/icons/more-horizontal';
|
||||
import Tag from '@lucide/svelte/icons/tag';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
import Minus from '@lucide/svelte/icons/minus';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Separator } from '@cio/ui/base/separator';
|
||||
import * as Popover from '@cio/ui/base/popover';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import * as InputGroup from '@cio/ui/base/input-group';
|
||||
import * as Tooltip from '@cio/ui/base/tooltip';
|
||||
import * as ButtonGroup from '@cio/ui/base/button-group';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/ButtonGroup',
|
||||
component: ButtonGroup.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
let label = $state('personal');
|
||||
let voiceEnabled = $state(false);
|
||||
|
||||
const CURRENCIES = [
|
||||
{
|
||||
value: '$',
|
||||
label: 'US Dollar'
|
||||
},
|
||||
{
|
||||
value: '€',
|
||||
label: 'Euro'
|
||||
},
|
||||
{
|
||||
value: '£',
|
||||
label: 'British Pound'
|
||||
}
|
||||
];
|
||||
|
||||
let currency = $state('$');
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<ButtonGroup.Root class="hidden sm:flex">
|
||||
<Button variant="outline" size="icon" aria-label="Go Back">
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">Archive</Button>
|
||||
<Button variant="outline">Report</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">Snooze</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" size="icon" aria-label="More Options">
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-52">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<MailCheck />
|
||||
Mark as Read
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Archive />
|
||||
Archive
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<Clock />
|
||||
Snooze
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<CalendarPlus />
|
||||
Add to Calendar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<ListFilter />
|
||||
Add to List
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<Tag />
|
||||
Label As...
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.RadioGroup bind:value={label}>
|
||||
<DropdownMenu.RadioItem value="personal">Personal</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="work">Work</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="other">Other</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item class="text-destructive focus:text-destructive">
|
||||
<Trash2 />
|
||||
Trash
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Orientation">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root orientation="vertical" aria-label="Media controls" class="h-fit">
|
||||
<Button variant="outline" size="icon">
|
||||
<Plus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<Minus />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Sizes">
|
||||
{#snippet template()}
|
||||
<div class="flex flex-col items-start gap-8">
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline" size="sm">Small</Button>
|
||||
<Button variant="outline" size="sm">Button</Button>
|
||||
<Button variant="outline" size="sm">Group</Button>
|
||||
<Button variant="outline" size="icon-sm">
|
||||
<Plus />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">Default</Button>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline">Group</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<Plus />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline" size="lg">Large</Button>
|
||||
<Button variant="outline" size="lg">Button</Button>
|
||||
<Button variant="outline" size="lg">Group</Button>
|
||||
<Button variant="outline" size="icon-lg">
|
||||
<Plus />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Nested">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline" size="sm">1</Button>
|
||||
<Button variant="outline" size="sm">2</Button>
|
||||
<Button variant="outline" size="sm">3</Button>
|
||||
<Button variant="outline" size="sm">4</Button>
|
||||
<Button variant="outline" size="sm">5</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Previous">
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Next">
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Separator">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="secondary" size="sm">Copy</Button>
|
||||
<ButtonGroup.Separator />
|
||||
<Button variant="secondary" size="sm">Paste</Button>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Split">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="secondary">Button</Button>
|
||||
<ButtonGroup.Separator />
|
||||
<Button variant="secondary" size="icon">
|
||||
<Plus />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Input">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<Input placeholder="Search..." />
|
||||
<Button variant="outline" size="icon" aria-label="Search">
|
||||
<Search />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Input Group">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root class="[--radius:9999rem]">
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline" size="icon">
|
||||
<Plus />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input
|
||||
placeholder={voiceEnabled ? 'Record and send audio...' : 'Send a message...'}
|
||||
disabled={voiceEnabled}
|
||||
/>
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<InputGroup.Button
|
||||
{...props}
|
||||
onclick={() => (voiceEnabled = !voiceEnabled)}
|
||||
size="icon-xs"
|
||||
data-active={voiceEnabled}
|
||||
class="ui:data-[active=true]:bg-orange-100 ui:data-[active=true]:text-orange-700 ui:dark:data-[active=true]:bg-orange-800 ui:dark:data-[active=true]:text-orange-100"
|
||||
aria-pressed={voiceEnabled}
|
||||
>
|
||||
<AudioLines />
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Voice Mode</Tooltip.Content>
|
||||
</Tooltip.Root></Tooltip.Provider
|
||||
>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Dropdown Menu">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">Follow</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" class="ps-2!">
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="[--radius:1rem]">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<VolumeOff />
|
||||
Mute Conversation
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Check />
|
||||
Mark as Read
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<AlertTriangle />
|
||||
Report Conversation
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<UserRoundX />
|
||||
Block User
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Share />
|
||||
Share Conversation
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<Copy />
|
||||
Copy Conversation
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item variant="destructive">
|
||||
<Trash />
|
||||
Delete Conversation
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Select">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Select.Root type="single" bind:value={currency}>
|
||||
<Select.Trigger class="font-mono">
|
||||
{currency}
|
||||
</Select.Trigger>
|
||||
<Select.Content class="min-w-24">
|
||||
{#each CURRENCIES as currencyOption (currencyOption.value)}
|
||||
<Select.Item value={currencyOption.value}>
|
||||
{currencyOption.value}
|
||||
<span class="text-muted-foreground">{currencyOption.label}</span>
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Input placeholder="10.00" pattern="[0-9]*" />
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button aria-label="Send" size="icon" variant="outline">
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Popover">
|
||||
{#snippet template()}
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">
|
||||
<Bot />
|
||||
Copilot
|
||||
</Button>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" size="icon" aria-label="Open Popover">
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="end" class="rounded-xl p-0 text-sm">
|
||||
<div class="px-4 py-3">
|
||||
<div class="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="*:[p:not(:last-child)]:mb-2 p-4 text-sm">
|
||||
<Textarea placeholder="Describe your task in natural language." class="mb-4 resize-none" />
|
||||
<p class="font-medium">Start a new task with Copilot</p>
|
||||
<p class="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in the background and open a pull request for
|
||||
your review.
|
||||
</p>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</ButtonGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1 @@
|
||||
export const FIELDS = ['class', 'orientation'] as string[];
|
||||
@@ -0,0 +1,123 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/Card',
|
||||
component: Card.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<Card.Root class="w-full max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Login to your account</Card.Title>
|
||||
<Card.Description>Enter your email below to login to your account</Card.Description>
|
||||
<Card.Action>
|
||||
<Button variant="link">Sign Up</Button>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form>
|
||||
<div class="ui:flex ui:flex-col ui:gap-6">
|
||||
<div class="ui:grid ui:gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input id="email" type="email" bind:value={email} placeholder="[email protected]" required />
|
||||
</div>
|
||||
<div class="ui:grid ui:gap-2">
|
||||
<div class="ui:flex ui:items-center">
|
||||
<Label for="password">Password</Label>
|
||||
<a href="#" class="ui:ms-auto ui:inline-block ui:text-sm ui:underline-offset-4 hover:ui:underline">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" bind:value={password} required />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<Card.Footer class="ui:flex-col ui:gap-2">
|
||||
<Button type="submit" class="ui:w-full">Login</Button>
|
||||
<Button variant="outline" class="ui:w-full">Login with Google</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Simple">
|
||||
{#snippet template()}
|
||||
<Card.Root class="ui:w-full ui:max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Card Title</Card.Title>
|
||||
<Card.Description>Card description goes here.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p>Card content goes here.</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Footer">
|
||||
{#snippet template()}
|
||||
<Card.Root class="ui:w-full ui:max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Card Title</Card.Title>
|
||||
<Card.Description>Card description goes here.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p>Card content goes here.</p>
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<Button size="sm">Action</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Action">
|
||||
{#snippet template()}
|
||||
<Card.Root class="ui:w-full ui:max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Card Title</Card.Title>
|
||||
<Card.Description>Card description goes here.</Card.Description>
|
||||
<Card.Action>
|
||||
<Button variant="ghost" size="sm">More</Button>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p>Card content goes here.</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="No Header">
|
||||
{#snippet template()}
|
||||
<Card.Root class="ui:w-full ui:max-w-sm">
|
||||
<Card.Content>
|
||||
<p>Card content without header.</p>
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<Button size="sm">Action</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
@@ -0,0 +1,102 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import { CopyButton } from '@cio/ui/base/copy-button';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as InputGroup from '@cio/ui/base/input-group';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/CopyButton',
|
||||
component: CopyButton,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
let textToCopy = $state('Hello, World!');
|
||||
let copiedText = $state('');
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<CopyButton text="Copy this text to clipboard" />
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Text">
|
||||
{#snippet template()}
|
||||
<CopyButton text="npm install @cio/ui" variant="outline">Copy Command</CopyButton>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Variants">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:items-start">
|
||||
<CopyButton text="Default variant" variant="default" />
|
||||
<CopyButton text="Outline variant" variant="outline" />
|
||||
<CopyButton text="Secondary variant" variant="secondary" />
|
||||
<CopyButton text="Ghost variant" variant="ghost" />
|
||||
<CopyButton text="Destructive variant" variant="destructive" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Input Group">
|
||||
{#snippet template()}
|
||||
<InputGroup.Root class="ui:w-96">
|
||||
<InputGroup.Input value="https://classroomio.com/courses/123" readonly />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<CopyButton text="https://classroomio.com/courses/123" size="icon-sm" />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Reactive Text">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input bind:value={textToCopy} placeholder="Enter text to copy" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<CopyButton text={textToCopy} size="icon-sm" />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<div class="ui:text-sm ui:text-muted-foreground">
|
||||
<p>Text to copy: <code class="ui:bg-muted ui:px-2 ui:py-1 ui:rounded">{textToCopy}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Code Block">
|
||||
{#snippet template()}
|
||||
<div class="ui:rounded-lg ui:border ui:bg-muted/50 ui:p-4 ui:w-full ui:max-w-2xl">
|
||||
<div class="ui:flex ui:items-center ui:justify-between ui:mb-2">
|
||||
<span class="ui:text-sm ui:font-medium">Installation</span>
|
||||
<CopyButton
|
||||
text="pnpm add @cio/ui"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onCopy={(status) => {
|
||||
if (status === 'success') {
|
||||
copiedText = 'pnpm add @cio/ui';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</CopyButton>
|
||||
</div>
|
||||
<pre class="ui:text-sm"><code>pnpm add @cio/ui</code></pre>
|
||||
{#if copiedText}
|
||||
<p class="ui:mt-2 ui:text-green-600">Last copied: {copiedText}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1,11 @@
|
||||
export const FIELDS = [
|
||||
'ref',
|
||||
'children',
|
||||
'text',
|
||||
'icon',
|
||||
'animationDuration',
|
||||
'variant',
|
||||
'size',
|
||||
'onCopy',
|
||||
'class'
|
||||
] as string[];
|
||||
@@ -0,0 +1,146 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import FolderCodeIcon from '@lucide/svelte/icons/folder-code';
|
||||
import CloudIcon from '@lucide/svelte/icons/cloud';
|
||||
import BellIcon from '@lucide/svelte/icons/bell';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import RefreshCcwIcon from '@lucide/svelte/icons/refresh-ccw';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
|
||||
import * as Empty from '@cio/ui/base/empty';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/Empty',
|
||||
component: Empty.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<FolderCodeIcon />
|
||||
</Empty.Media>
|
||||
<Empty.Title>No Courses Yet</Empty.Title>
|
||||
<Empty.Description>
|
||||
You haven't created any courses yet. Get started by creating your first course.
|
||||
</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<div class="flex gap-2">
|
||||
<Button>Create Course</Button>
|
||||
<Button variant="outline">Import Project</Button>
|
||||
</div>
|
||||
</Empty.Content>
|
||||
<Button variant="link" class="ui:text-muted-foreground" size="sm">
|
||||
<a href="#/">
|
||||
Learn More <ArrowUpRightIcon class="inline" />
|
||||
</a>
|
||||
</Button>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Outline">
|
||||
{#snippet template()}
|
||||
<Empty.Root class="border border-dashed">
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<CloudIcon />
|
||||
</Empty.Media>
|
||||
<Empty.Title>Cloud Storage Empty</Empty.Title>
|
||||
<Empty.Description>Upload files to your cloud storage to access them anywhere.</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button variant="outline" size="sm">Upload Files</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Background">
|
||||
{#snippet template()}
|
||||
<Empty.Root class="ui:from-muted/50 ui:to-background ui:h-full ui:bg-gradient-to-b ui:from-30%">
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<BellIcon />
|
||||
</Empty.Media>
|
||||
<Empty.Title>No Notifications</Empty.Title>
|
||||
<Empty.Description>You're all caught up. New notifications will appear here.</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCcwIcon />
|
||||
Refresh
|
||||
</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Avatar">
|
||||
{#snippet template()}
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="default">
|
||||
<Avatar.Root class="size-12">
|
||||
<Avatar.Image src="https://github.com/shadcn.png" class="grayscale" />
|
||||
<Avatar.Fallback>LR</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</Empty.Media>
|
||||
<Empty.Title>User Offline</Empty.Title>
|
||||
<Empty.Description>
|
||||
This user is currently offline. You can leave a message to notify them or try again later.
|
||||
</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button size="sm">Leave Message</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Avatar Groups">
|
||||
{#snippet template()}
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media>
|
||||
<div class="*:ring-background flex -space-x-2 *:size-12 *:ring-2 *:grayscale">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<Avatar.Fallback>CN</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/maxleiter.png" alt="@maxleiter" />
|
||||
<Avatar.Fallback>LR</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
|
||||
<Avatar.Fallback>ER</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</div>
|
||||
</Empty.Media>
|
||||
<Empty.Title>No Team Members</Empty.Title>
|
||||
<Empty.Description>Invite your team to collaborate on this project.</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button size="sm">
|
||||
<PlusIcon />
|
||||
Invite Members
|
||||
</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
@@ -0,0 +1,508 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Checkbox } from '@cio/ui/base/checkbox';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/Field',
|
||||
component: Field.Field,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
let courseType = $state();
|
||||
let courseVersion = $state();
|
||||
let assignmentType = $state();
|
||||
let notificationPreference = $state('email');
|
||||
let deliveryMethod = $state('live-class');
|
||||
|
||||
const courseTypes = [
|
||||
{ value: 'live-class', label: 'Live Class' },
|
||||
{ value: 'self-paced', label: 'Self-Paced' },
|
||||
{ value: 'hybrid', label: 'Hybrid' }
|
||||
];
|
||||
|
||||
const courseVersions = [
|
||||
{ value: 'v1', label: 'Version 1' },
|
||||
{ value: 'v2', label: 'Version 2' },
|
||||
{ value: 'v3', label: 'Version 3' }
|
||||
];
|
||||
|
||||
const assignmentTypes = [
|
||||
{ value: 'essay', label: 'Essay' },
|
||||
{ value: 'quiz', label: 'Quiz' },
|
||||
{ value: 'project', label: 'Project' },
|
||||
{ value: 'presentation', label: 'Presentation' }
|
||||
];
|
||||
|
||||
const courseTypeLabel = $derived(courseTypes.find((t) => t.value === courseType)?.label ?? 'Select course type');
|
||||
</script>
|
||||
|
||||
<Story name="Payment Form">
|
||||
{#snippet template()}
|
||||
<form class="w-lg">
|
||||
<Field.Group>
|
||||
<Field.Set>
|
||||
<Field.Legend>Course Information</Field.Legend>
|
||||
<Field.Description>Create a new course for your students</Field.Description>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="course-title">Course Title</Field.Label>
|
||||
<Input id="course-title" placeholder="Introduction to Web Development" required />
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="course-description">Description</Field.Label>
|
||||
<Textarea
|
||||
id="course-description"
|
||||
placeholder="A comprehensive course covering HTML, CSS, and JavaScript fundamentals..."
|
||||
rows={4}
|
||||
class="resize-none"
|
||||
/>
|
||||
<Field.Description>Provide a brief overview of what students will learn</Field.Description>
|
||||
</Field.Field>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<Field.Field>
|
||||
<Field.Label for="course-type">Course Type</Field.Label>
|
||||
<Select.Root type="single" bind:value={courseType}>
|
||||
<Select.Trigger id="course-type">
|
||||
<span>{courseTypeLabel}</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each courseTypes as type (type.value)}
|
||||
<Select.Item value={type.value}>{type.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="course-version">Version</Field.Label>
|
||||
<Select.Root type="single" bind:value={courseVersion}>
|
||||
<Select.Trigger id="course-version">
|
||||
<span>{courseVersion || 'Select version'}</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each courseVersions as version (version.value)}
|
||||
<Select.Item value={version.value}>{version.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Field.Field>
|
||||
</div>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
<Field.Separator />
|
||||
<Field.Set>
|
||||
<Field.Legend>Course Settings</Field.Legend>
|
||||
<Field.Description>Configure how your course will be delivered and accessed</Field.Description>
|
||||
<Field.Group>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="course-published" checked={false} />
|
||||
<Field.Label for="course-published" class="font-normal">Publish course immediately</Field.Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="course-certificate" checked={true} />
|
||||
<Field.Label for="course-certificate" class="font-normal">
|
||||
Enable certificate download upon completion
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
<Field.Separator />
|
||||
<Field.Set>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="course-overview">Course Overview</Field.Label>
|
||||
<Textarea
|
||||
id="course-overview"
|
||||
placeholder="Welcome to this amazing course! In this course, you'll learn..."
|
||||
class="resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
<Field.Description
|
||||
>This message will be displayed to students when they first access the course</Field.Description
|
||||
>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Button type="submit">Create Course</Button>
|
||||
<Button variant="outline" type="button">Cancel</Button>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</form>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Basic Usage">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Set>
|
||||
<Field.Legend>Student Profile</Field.Legend>
|
||||
<Field.Description>Update your profile information visible to teachers and classmates</Field.Description>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="student-name">Full name</Field.Label>
|
||||
<Input id="student-name" autoComplete="off" placeholder="John Doe" />
|
||||
<Field.Description
|
||||
>This will be displayed on your certificates and course completion records</Field.Description
|
||||
>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="student-email">Email</Field.Label>
|
||||
<Input id="student-email" type="email" autoComplete="off" aria-invalid />
|
||||
<Field.Error>This email is already registered. Please use a different email.</Field.Error>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="email-notifications" checked={true} />
|
||||
<Field.Label for="email-notifications">Receive email notifications about course updates</Field.Label>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Input">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Set>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="login-email">Email</Field.Label>
|
||||
<Input id="login-email" type="email" placeholder="[email protected]" />
|
||||
<Field.Description>Enter the email address associated with your account</Field.Description>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="login-password">Password</Field.Label>
|
||||
<Field.Description>Must be at least 8 characters long</Field.Description>
|
||||
<Input id="login-password" type="password" placeholder="********" />
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Textarea">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Set>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="assignment-answer">Assignment Answer</Field.Label>
|
||||
<Textarea
|
||||
id="assignment-answer"
|
||||
placeholder="Write your response here. Be sure to address all parts of the question..."
|
||||
rows={8}
|
||||
/>
|
||||
<Field.Description>
|
||||
Submit your completed assignment. You can edit your submission before the deadline.
|
||||
</Field.Description>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Select">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Field>
|
||||
<Field.Label for="assignment-type">Assignment Type</Field.Label>
|
||||
<Select.Root type="single" bind:value={assignmentType}>
|
||||
<Select.Trigger id="assignment-type">
|
||||
<span>{assignmentType || 'Select assignment type'}</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each assignmentTypes as type (type.value)}
|
||||
<Select.Item value={type.value}>{type.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Field.Description>Choose the type of assignment you want to create for your students</Field.Description>
|
||||
</Field.Field>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Field Set">
|
||||
{#snippet template()}
|
||||
<div class="@container w-lg">
|
||||
<!-- <div class="max-w-sm" style="width: 512px;"> -->
|
||||
<Field.Set>
|
||||
<Field.Legend>Enrollment Information</Field.Legend>
|
||||
<Field.Description>Add a new student to your course</Field.Description>
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label for="student-email-enroll">Student Email</Field.Label>
|
||||
<Input id="student-email-enroll" type="email" placeholder="[email protected]" />
|
||||
</Field.Field>
|
||||
<div class="flex justify-between gap-4">
|
||||
<Field.Field>
|
||||
<Field.Label for="enrollment-date">Enrollment Date</Field.Label>
|
||||
<Input id="enrollment-date" type="date" />
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="access-level">Access Level</Field.Label>
|
||||
<Select.Root type="single">
|
||||
<Select.Trigger id="access-level">
|
||||
<span>Full Access</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="full">Full Access</Select.Item>
|
||||
<Select.Item value="limited">Limited Access</Select.Item>
|
||||
<Select.Item value="readonly">Read Only</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Field.Field>
|
||||
</div>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Checkbox">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Group>
|
||||
<Field.Set>
|
||||
<Field.Legend variant="label">Course Features</Field.Legend>
|
||||
<Field.Description>Enable features available to students in this course</Field.Description>
|
||||
<Field.Group class="gap-3">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="course-forum" checked />
|
||||
<Field.Label for="course-forum" class="font-normal">Enable course forum</Field.Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="course-quiz" checked />
|
||||
<Field.Label for="course-quiz" class="font-normal">Enable live quizzes</Field.Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="course-assignments" checked />
|
||||
<Field.Label for="course-assignments" class="font-normal">Enable assignments</Field.Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="course-certificates" />
|
||||
<Field.Label for="course-certificates" class="font-normal">Enable certificate generation</Field.Label>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
<Field.Separator />
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="ai-assistance" checked />
|
||||
<Field.Content>
|
||||
<Field.Label for="ai-assistance">AI-Powered Course Creation</Field.Label>
|
||||
<Field.Description>
|
||||
Use AI to generate course content, lesson outlines, and assignments from your notes. This feature helps
|
||||
you create comprehensive courses faster.
|
||||
</Field.Description>
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Radio">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Set>
|
||||
<Field.Label>Course Delivery Method</Field.Label>
|
||||
<Field.Description>Choose how your course will be delivered to students</Field.Description>
|
||||
<RadioGroup.Root bind:value={deliveryMethod}>
|
||||
<Field.Field orientation="horizontal">
|
||||
<RadioGroup.Item value="live-class" id="type-live" />
|
||||
<Field.Label for="type-live" class="font-normal">
|
||||
Live Class - Scheduled sessions with real-time interaction
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<RadioGroup.Item value="self-paced" id="type-self-paced" />
|
||||
<Field.Label for="type-self-paced" class="font-normal">
|
||||
Self-Paced - Students learn at their own speed
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<RadioGroup.Item value="hybrid" id="type-hybrid" />
|
||||
<Field.Label for="type-hybrid" class="font-normal">
|
||||
Hybrid - Combination of live sessions and self-paced content
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
</RadioGroup.Root>
|
||||
</Field.Set>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Switch">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Field.Content>
|
||||
<Field.Label for="publish-course">Publish Course</Field.Label>
|
||||
<Field.Description>
|
||||
Make this course visible to enrolled students. Unpublished courses are only visible to teachers and admins.
|
||||
</Field.Description>
|
||||
</Field.Content>
|
||||
<Switch id="publish-course" />
|
||||
</Field.Field>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Choice Card">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Group>
|
||||
<Field.Set>
|
||||
<Field.Label for="delivery-method-p8w">Course Delivery Method</Field.Label>
|
||||
<Field.Description>Choose how students will access and interact with your course content</Field.Description>
|
||||
<RadioGroup.Root bind:value={deliveryMethod}>
|
||||
<Field.Label for="live-class-r2h">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Field.Content>
|
||||
<Field.Title>Live Class</Field.Title>
|
||||
<Field.Description>
|
||||
Scheduled sessions with real-time interaction. Perfect for bootcamps and structured learning
|
||||
programs.
|
||||
</Field.Description>
|
||||
</Field.Content>
|
||||
<RadioGroup.Item value="live-class" id="live-class-r2h" />
|
||||
</Field.Field>
|
||||
</Field.Label>
|
||||
<Field.Label for="self-paced-z4k">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Field.Content>
|
||||
<Field.Title>Self-Paced</Field.Title>
|
||||
<Field.Description>
|
||||
Students learn at their own speed. Access all content immediately and complete on their schedule.
|
||||
</Field.Description>
|
||||
</Field.Content>
|
||||
<RadioGroup.Item value="self-paced" id="self-paced-z4k" />
|
||||
</Field.Field>
|
||||
</Field.Label>
|
||||
</RadioGroup.Root>
|
||||
</Field.Set>
|
||||
</Field.Group>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Field Group">
|
||||
{#snippet template()}
|
||||
<div class="w-lg">
|
||||
<Field.Group>
|
||||
<Field.Set>
|
||||
<Field.Label>Course Updates</Field.Label>
|
||||
<Field.Description>
|
||||
Get notified when new lessons, assignments, or announcements are added to your enrolled courses.
|
||||
</Field.Description>
|
||||
<Field.Group data-slot="checkbox-group">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="course-updates-email" checked disabled />
|
||||
<Field.Label for="course-updates-email" class="font-normal">Email notifications</Field.Label>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
<Field.Separator />
|
||||
<Field.Set>
|
||||
<Field.Label>Assignment Reminders</Field.Label>
|
||||
<Field.Description>
|
||||
Get notified about upcoming assignment deadlines and when teachers grade your submissions.
|
||||
<a href="#/">Manage preferences</a>
|
||||
</Field.Description>
|
||||
<Field.Group data-slot="checkbox-group">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="assignment-reminders-email" />
|
||||
<Field.Label for="assignment-reminders-email" class="font-normal">Email notifications</Field.Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="assignment-reminders-push" />
|
||||
<Field.Label for="assignment-reminders-push" class="font-normal">Push notifications</Field.Label>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</Field.Group>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Responsive Layout">
|
||||
{#snippet template()}
|
||||
<div class="w-full max-w-4xl">
|
||||
<form>
|
||||
<Field.Set>
|
||||
<Field.Legend>Teacher Profile</Field.Legend>
|
||||
<Field.Description>Update your teacher profile information</Field.Description>
|
||||
<Field.Separator />
|
||||
<Field.Group>
|
||||
<Field.Field orientation="responsive">
|
||||
<Field.Content>
|
||||
<Field.Label for="teacher-name">Name</Field.Label>
|
||||
<Field.Description>Your name as it will appear to students</Field.Description>
|
||||
</Field.Content>
|
||||
<Input id="teacher-name" placeholder="Dr. Jane Smith" required />
|
||||
</Field.Field>
|
||||
<Field.Separator />
|
||||
<Field.Field orientation="responsive">
|
||||
<Field.Content>
|
||||
<Field.Label for="teacher-bio">Bio</Field.Label>
|
||||
<Field.Description>
|
||||
Write a brief bio about your teaching experience and expertise. This will be visible to students.
|
||||
</Field.Description>
|
||||
</Field.Content>
|
||||
<Textarea
|
||||
id="teacher-bio"
|
||||
placeholder="I'm an experienced educator with 10+ years of teaching web development..."
|
||||
required
|
||||
class="min-h-[100px] resize-none sm:min-w-[300px]"
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Separator />
|
||||
<Field.Field orientation="responsive">
|
||||
<Button type="submit">Save Profile</Button>
|
||||
<Button type="button" variant="outline">Cancel</Button>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</form>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Validation and Errors">
|
||||
{#snippet template()}
|
||||
<div class="w-sm">
|
||||
<Field.Set>
|
||||
<Field.Group>
|
||||
<Field.Field data-invalid>
|
||||
<Field.Label for="student-email-invalid">Student Email</Field.Label>
|
||||
<Input id="student-email-invalid" type="email" aria-invalid placeholder="invalid-email" />
|
||||
<Field.Error>Enter a valid email address to enroll the student.</Field.Error>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="student-email-valid">Student Email</Field.Label>
|
||||
<Input id="student-email-valid" type="email" placeholder="[email protected]" />
|
||||
<Field.Description>This email address is valid and ready for enrollment.</Field.Description>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
@@ -0,0 +1,2 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import CalendarDaysIcon from '@lucide/svelte/icons/calendar-days';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
|
||||
import * as HoverCard from '@cio/ui/base/hover-card';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/HoverCard',
|
||||
component: HoverCard.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>Introduction to Web Development</HoverCard.Trigger>
|
||||
<HoverCard.Content>Learn the fundamentals of modern web development with hands-on projects.</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Instructor Profile">
|
||||
{#snippet template()}
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger
|
||||
href="/instructors/sarah-johnson"
|
||||
class="ui:rounded-sm ui:underline-offset-4 ui:hover:underline ui:focus-visible:outline-2 ui:focus-visible:outline-offset-8 ui:focus-visible:outline-black ui:flex ui:items-center ui:gap-2"
|
||||
>
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/shadcn.png" />
|
||||
<Avatar.Fallback>SJ</Avatar.Fallback>
|
||||
</Avatar.Root> @sarah-johnson
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content class="w-80">
|
||||
<div class="flex justify-between space-x-4">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/shadcn.png" />
|
||||
<Avatar.Fallback>SJ</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="space-y-1">
|
||||
<h4 class="text-sm font-semibold">Sarah Johnson</h4>
|
||||
<p class="text-sm">Senior Full-Stack Developer & Instructor</p>
|
||||
<div class="flex items-center pt-2">
|
||||
<CalendarDaysIcon class="me-2 size-4 opacity-70" />
|
||||
<span class="text-muted-foreground text-xs">Teaching since January 2020</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Course Preview">
|
||||
{#snippet template()}
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger href="/courses/advanced-react" class="text-primary underline-offset-4 hover:underline">
|
||||
Advanced React Patterns
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content class="w-64">
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-sm font-semibold">Advanced React Patterns</h4>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Master advanced React concepts including hooks, context, performance optimization, and modern patterns used
|
||||
in production applications.
|
||||
</p>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Student Profile">
|
||||
{#snippet template()}
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger class="text-primary ui:flex ui:items-center ui:gap-2 underline-offset-4 hover:underline">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/rotimi-best.png" />
|
||||
<Avatar.Fallback>AM</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
@rotimi-best
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content class="w-80">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="https://github.com/rotimi-best.png" />
|
||||
<Avatar.Fallback>AM</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold">Alex Martinez</h4>
|
||||
<p class="text-muted-foreground text-xs">Student</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="text-muted-foreground flex items-center gap-2 text-xs">
|
||||
<BookOpenIcon class="size-4" />
|
||||
<span>5 courses enrolled</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground flex items-center gap-2 text-xs">
|
||||
<UsersIcon class="size-4" />
|
||||
<span>Active since March 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1,2 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
import MailIcon from '@lucide/svelte/icons/mail';
|
||||
import LockIcon from '@lucide/svelte/icons/lock';
|
||||
import CreditCardIcon from '@lucide/svelte/icons/credit-card';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import MessageSquareIcon from '@lucide/svelte/icons/message-square';
|
||||
import GlobeIcon from '@lucide/svelte/icons/globe';
|
||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle';
|
||||
import EyeIcon from '@lucide/svelte/icons/eye';
|
||||
import EyeOffIcon from '@lucide/svelte/icons/eye-off';
|
||||
import KeyIcon from '@lucide/svelte/icons/key';
|
||||
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||
import MoreHorizontalIcon from '@lucide/svelte/icons/more-horizontal';
|
||||
import StarIcon from '@lucide/svelte/icons/star';
|
||||
import InfoIcon from '@lucide/svelte/icons/info';
|
||||
import LoaderIcon from '@lucide/svelte/icons/loader';
|
||||
import Link2Icon from '@lucide/svelte/icons/link-2';
|
||||
|
||||
import { CopyButton } from '@cio/ui/base/copy-button';
|
||||
|
||||
import * as InputGroup from '@cio/ui/base/input-group';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import * as Tooltip from '@cio/ui/base/tooltip';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import * as ButtonGroup from '@cio/ui/base/button-group';
|
||||
import { Kbd } from '@cio/ui/base/kbd';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/InputGroup',
|
||||
component: InputGroup.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let showPassword = $state(false);
|
||||
let apiKey = $state('');
|
||||
let searchQuery = $state('');
|
||||
let fileName = $state('');
|
||||
let username = $state('');
|
||||
let message = $state('');
|
||||
let code = $state("console.log('Hello, world!');");
|
||||
let customInput = $state('Autoresize textarea...');
|
||||
let domain = $state('example.com');
|
||||
let amount = $state('0.00');
|
||||
let cardNumber = $state('');
|
||||
let fileType = $state('txt');
|
||||
</script>
|
||||
|
||||
<Story name="Icon">
|
||||
{#snippet template()}
|
||||
<div class="grid w-full max-w-sm gap-6">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<SearchIcon />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input placeholder="Search..." />
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<MailIcon />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input type="email" placeholder="Enter your email" />
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<CreditCardIcon />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input placeholder="Card number" />
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Card number" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<StarIcon />
|
||||
<InfoIcon />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Text">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<InputGroup.Text>https://</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input placeholder="example.com" />
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<InputGroup.Text>$</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input type="number" placeholder="0.00" />
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<InputGroup.Text>@</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input placeholder="Enter your username" />
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="block-start">
|
||||
<InputGroup.Text>Note</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Textarea placeholder="Enter your message" />
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Button">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="https://x.com/shadcn" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Button>
|
||||
<CopyIcon />
|
||||
</InputGroup.Button>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Type to search..." />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Button>
|
||||
<SearchIcon />
|
||||
</InputGroup.Button>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Tooltip">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<LockIcon />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input type={showPassword ? 'text' : 'password'} placeholder="Enter password" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<InputGroup.Button
|
||||
{...props}
|
||||
onclick={() => (showPassword = !showPassword)}
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{#if showPassword}
|
||||
<EyeOffIcon />
|
||||
{:else}
|
||||
<EyeIcon />
|
||||
{/if}
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{showPassword ? 'Hide password' : 'Show password'}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<MailIcon />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input type="email" placeholder="Your email address" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<InputGroup.Button {...props} aria-label="Help">
|
||||
<MessageSquareIcon />
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>We'll never share your email</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<KeyIcon />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input type="password" placeholder="Enter API key" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<InputGroup.Button {...props} aria-label="Generate API key">
|
||||
<KeyIcon />
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Generate a new API key</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Textarea">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="block-start">
|
||||
<InputGroup.Text>Code</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Textarea placeholder="console.log('Hello, world!');" />
|
||||
</InputGroup.Root>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Addon align="block-end">
|
||||
<InputGroup.Text>Note</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Textarea placeholder="Enter your message" />
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Spinner">
|
||||
{#snippet template()}
|
||||
<div class="grid w-full max-w-sm gap-4">
|
||||
<InputGroup.Root data-disabled>
|
||||
<InputGroup.Input placeholder="Searching..." disabled />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Spinner />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root data-disabled>
|
||||
<InputGroup.Input placeholder="Processing..." disabled />
|
||||
<InputGroup.Addon>
|
||||
<Spinner />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root data-disabled>
|
||||
<InputGroup.Input placeholder="Saving changes..." disabled />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Text>Saving...</InputGroup.Text>
|
||||
<Spinner />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root data-disabled>
|
||||
<InputGroup.Input placeholder="Refreshing data..." disabled />
|
||||
<InputGroup.Addon>
|
||||
<LoaderIcon class="animate-spin" />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Text class="text-muted-foreground">Please wait...</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Label">
|
||||
{#snippet template()}
|
||||
<div class="grid w-full max-w-sm gap-4">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input id="email" placeholder="shadcn" />
|
||||
<InputGroup.Addon>
|
||||
<Label for="email">@</Label>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input id="email-2" placeholder="[email protected]" />
|
||||
<InputGroup.Addon align="block-start">
|
||||
<Label for="email-2" class="ui:text-foreground">Email</Label>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<InputGroup.Button
|
||||
{...props}
|
||||
variant="ghost"
|
||||
aria-label="Help"
|
||||
class="ms-auto rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<InfoIcon />
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>We'll use this to send you notifications</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Dropdown">
|
||||
{#snippet template()}
|
||||
<div class="grid w-full max-w-sm gap-4">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Enter file name" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<InputGroup.Button {...props} variant="ghost" aria-label="More" size="icon-xs">
|
||||
<MoreHorizontalIcon />
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item>Settings</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Copy path</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Open location</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root class="[--radius:1rem]">
|
||||
<InputGroup.Input placeholder="Enter search query" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<InputGroup.Button {...props} variant="ghost" class="!pe-1.5 text-xs">
|
||||
Search In... <ChevronDownIcon class="size-3" />
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="[--radius:0.95rem]">
|
||||
<DropdownMenu.Item>Documentation</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Blog Posts</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Changelog</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Button Group">
|
||||
{#snippet template()}
|
||||
<div class="grid w-full max-w-sm gap-6">
|
||||
<ButtonGroup.Root>
|
||||
<ButtonGroup.Text>
|
||||
<Label for="url">https://</Label>
|
||||
</ButtonGroup.Text>
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input id="url" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Link2Icon />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<ButtonGroup.Text>.com</ButtonGroup.Text>
|
||||
</ButtonGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Custom Input">
|
||||
{#snippet template()}
|
||||
<div class="w-lg grid gap-6">
|
||||
<InputGroup.Root>
|
||||
<textarea
|
||||
data-slot="input-group-control"
|
||||
class="field-sizing-content flex min-h-16 w-full resize-none rounded-md bg-transparent px-3 py-2.5 text-base outline-none transition-[color,box-shadow] md:text-sm"
|
||||
placeholder="Autoresize textarea..."
|
||||
></textarea>
|
||||
<InputGroup.Addon align="block-end">
|
||||
<InputGroup.Button class="ms-auto" size="sm" variant="default">Submit</InputGroup.Button>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1,2 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import BookIcon from '@lucide/svelte/icons/book';
|
||||
import GraduationCapIcon from '@lucide/svelte/icons/graduation-cap';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import FilterIcon from '@lucide/svelte/icons/filter';
|
||||
import DownloadIcon from '@lucide/svelte/icons/download';
|
||||
import MoreHorizontalIcon from '@lucide/svelte/icons/more-horizontal';
|
||||
import FolderCodeIcon from '@lucide/svelte/icons/folder-code';
|
||||
import UserPlusIcon from '@lucide/svelte/icons/user-plus';
|
||||
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import ShareIcon from '@lucide/svelte/icons/share';
|
||||
import ArchiveIcon from '@lucide/svelte/icons/archive';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import * as ButtonGroup from '@cio/ui/base/button-group';
|
||||
import * as Empty from '@cio/ui/base/empty';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import * as InputGroup from '@cio/ui/base/input-group';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/Page',
|
||||
component: Page.Root,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: FIELDS
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<Page.Root class="w-4xl h-4xl">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>Courses</Page.Title>
|
||||
<Page.Subtitle>Manage and organize your course offerings.</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="default">
|
||||
<PlusIcon />
|
||||
New Course
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
Visit Site
|
||||
<ArrowUpRightIcon />
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<Empty.Root class="border border-dashed">
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<FolderCodeIcon />
|
||||
</Empty.Media>
|
||||
<Empty.Title>No Courses Yet</Empty.Title>
|
||||
<Empty.Description>
|
||||
You haven't created any courses yet. Get started by creating your first course.
|
||||
</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<div class="flex gap-2">
|
||||
<Button>
|
||||
<PlusIcon />
|
||||
Create Course
|
||||
</Button>
|
||||
<Button variant="outline">Import Course</Button>
|
||||
</div>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Courses">
|
||||
{#snippet template()}
|
||||
<Page.Root>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>Courses</Page.Title>
|
||||
<Page.Subtitle>Manage and organize your course offerings.</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button>
|
||||
<PlusIcon />
|
||||
New Course
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
<Page.BodyHeader align="right">
|
||||
<ButtonGroup.Root>
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Search courses..." />
|
||||
<InputGroup.Addon>
|
||||
<SearchIcon />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">
|
||||
<FilterIcon />
|
||||
Filter
|
||||
</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" class="ps-2!">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="[--radius:1rem]">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<DownloadIcon />
|
||||
Export Courses
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<ShareIcon />
|
||||
Share Courses
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<ArchiveIcon />
|
||||
Archive Selected
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item variant="destructive">
|
||||
<TrashIcon />
|
||||
Delete Selected
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
</Page.BodyHeader>
|
||||
{#snippet child()}
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<FolderCodeIcon />
|
||||
</Empty.Media>
|
||||
<Empty.Title>No Courses Yet</Empty.Title>
|
||||
<Empty.Description>
|
||||
You haven't created any courses yet. Get started by creating your first course.
|
||||
</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<div class="flex gap-2">
|
||||
<Button>
|
||||
<PlusIcon />
|
||||
Create Course
|
||||
</Button>
|
||||
<Button variant="outline">Import Course</Button>
|
||||
</div>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Classes">
|
||||
{#snippet template()}
|
||||
<Page.Root>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>Classes</Page.Title>
|
||||
<Page.Subtitle>View and manage your class schedules and sessions.</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Input placeholder="Search classes..." />
|
||||
<Button variant="outline" size="icon" aria-label="Search">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">
|
||||
<FilterIcon />
|
||||
Filter
|
||||
</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" class="ps-2!">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="[--radius:1rem]">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<DownloadIcon />
|
||||
Export Schedule
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<SettingsIcon />
|
||||
Class Settings
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
<Button>
|
||||
<PlusIcon />
|
||||
New Class
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<GraduationCapIcon />
|
||||
</Empty.Media>
|
||||
<Empty.Title>No Classes Scheduled</Empty.Title>
|
||||
<Empty.Description>
|
||||
Create your first class session to start teaching. Classes help you organize your teaching schedule.
|
||||
</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button>
|
||||
<PlusIcon />
|
||||
Create Class
|
||||
</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Students">
|
||||
{#snippet template()}
|
||||
<Page.Root>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>Students</Page.Title>
|
||||
<Page.Subtitle>Manage student enrollments and track progress.</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Input placeholder="Search students..." />
|
||||
<Button variant="outline" size="icon" aria-label="Search">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">
|
||||
<FilterIcon />
|
||||
Filter
|
||||
</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" class="ps-2!">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="[--radius:1rem]">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<DownloadIcon />
|
||||
Export Student List
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<FileTextIcon />
|
||||
Generate Report
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<ShareIcon />
|
||||
Share Student List
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
<Button>
|
||||
<UserPlusIcon />
|
||||
Add Student
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
<UsersIcon />
|
||||
</Empty.Media>
|
||||
<Empty.Title>No Students Enrolled</Empty.Title>
|
||||
<Empty.Description>
|
||||
Start by adding students to your courses. You can invite them via email or import from a CSV file.
|
||||
</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<div class="flex gap-2">
|
||||
<Button>
|
||||
<UserPlusIcon />
|
||||
Add Student
|
||||
</Button>
|
||||
<Button variant="outline">Import CSV</Button>
|
||||
</div>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Header Only">
|
||||
{#snippet template()}
|
||||
<Page.Root>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>Analytics Dashboard</Page.Title>
|
||||
<Page.Subtitle>View and analyze your learning management data.</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Input placeholder="Search..." />
|
||||
<Button variant="outline" size="icon" aria-label="Search">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</ButtonGroup.Root>
|
||||
<ButtonGroup.Root>
|
||||
<Button variant="outline">
|
||||
<DownloadIcon />
|
||||
Export
|
||||
</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline" class="ps-2!">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="[--radius:1rem]">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<ShareIcon />
|
||||
Share Dashboard
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<SettingsIcon />
|
||||
Dashboard Settings
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</ButtonGroup.Root>
|
||||
</ButtonGroup.Root>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
</Page.Root>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { Password } from '@cio/ui/custom/password';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Molecules/Password',
|
||||
component: Password,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
include: ['placeholder', 'disabled', 'class']
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
let password = $state('');
|
||||
let passwordWithError = $state('');
|
||||
let passwordDisabled = $state('password123');
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<Password bind:value={password} placeholder="Enter password" />
|
||||
<p class="ui:text-sm ui:text-muted-foreground">Value: {password || '(empty)'}</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Field">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<Field.Field>
|
||||
<Field.Label for="password">Password</Field.Label>
|
||||
<Field.Content>
|
||||
<Password id="password" bind:value={password} placeholder="Enter your password" />
|
||||
</Field.Content>
|
||||
<Field.Description>Password must be more than 8 characters</Field.Description>
|
||||
</Field.Field>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With Error">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<Field.Field>
|
||||
<Field.Label for="password-error">Password</Field.Label>
|
||||
<Field.Content>
|
||||
<Password
|
||||
id="password-error"
|
||||
bind:value={passwordWithError}
|
||||
placeholder="Enter your password"
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</Field.Content>
|
||||
<Field.Error>Password is required</Field.Error>
|
||||
</Field.Field>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Disabled">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<Field.Field>
|
||||
<Field.Label for="password-disabled">Password</Field.Label>
|
||||
<Field.Content>
|
||||
<Password
|
||||
id="password-disabled"
|
||||
bind:value={passwordDisabled}
|
||||
placeholder="Enter your password"
|
||||
disabled={true}
|
||||
/>
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Custom Labels">
|
||||
{#snippet template()}
|
||||
<div class="ui:flex ui:flex-col ui:gap-4 ui:w-96">
|
||||
<Password
|
||||
bind:value={password}
|
||||
placeholder="Enter your password"
|
||||
showPasswordTooltip="Click to show password"
|
||||
hidePasswordTooltip="Click to hide password"
|
||||
showPasswordAriaLabel="Show password"
|
||||
hidePasswordAriaLabel="Hide password"
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"paths": {
|
||||
"@cio/ui": ["../ui/src"],
|
||||
"@cio/ui/*": ["../ui/src/*"],
|
||||
"$src/*": ["../ui/*"]
|
||||
"@cio/ui": ["./node_modules/@cio/ui/src"],
|
||||
"@cio/ui/*": ["./node_modules/@cio/ui/src/*"],
|
||||
"$src/*": ["./node_modules/@cio/ui/*"]
|
||||
},
|
||||
"typeRoots": ["../../node_modules/@types", "./node_modules/@types"],
|
||||
"types": ["node"],
|
||||
@@ -34,5 +34,6 @@
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*", ".storybook/**/*", "vite.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [{ "path": "../ui" }]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user