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:
Rotimi Best
2025-12-02 09:54:38 +01:00
committed by GitHub
parent 531516a8f7
commit c7854efae3
350 changed files with 13928 additions and 3895 deletions
+458
View File
@@ -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/)
+33 -1
View File
@@ -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.
+10 -4
View File
@@ -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[];
+2 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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[];
+2 -5
View File
@@ -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[];
+2
View File
@@ -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>
+5 -4
View File
@@ -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" }]
}