@papernote/ui 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/NotificationBell.d.ts +7 -1
- package/dist/components/NotificationBell.d.ts.map +1 -1
- package/dist/components/Tabs.d.ts +112 -1
- package/dist/components/Tabs.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +120 -4
- package/dist/index.esm.js +437 -125
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +439 -123
- package/dist/index.js.map +1 -1
- package/dist/styles.css +23 -0
- package/package.json +1 -1
- package/src/components/NotificationBell.stories.tsx +71 -0
- package/src/components/NotificationBell.tsx +12 -4
- package/src/components/Tabs.stories.tsx +649 -6
- package/src/components/Tabs.tsx +613 -19
- package/src/components/index.ts +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import Tabs from './Tabs';
|
|
4
|
-
import { User, Settings, Bell, Lock } from 'lucide-react';
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
|
+
import Tabs, { Tab, TabsRoot, TabsList, TabsTrigger, TabsContent } from './Tabs';
|
|
4
|
+
import { User, Settings, Bell, Lock, FileText, Mail, Home } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
const meta = {
|
|
7
7
|
title: 'Navigation/Tabs',
|
|
@@ -17,10 +17,19 @@ Tab navigation component for organizing content into separate views with multipl
|
|
|
17
17
|
- **Variants**: underline, pill styling options
|
|
18
18
|
- **Orientation**: horizontal or vertical layout
|
|
19
19
|
- **Icons**: Support for icons alongside tab labels
|
|
20
|
-
- **Badges**: Show notification counts on tabs
|
|
20
|
+
- **Badges**: Show notification counts on tabs with variant colors
|
|
21
21
|
- **Disabled tabs**: Prevent interaction with specific tabs
|
|
22
22
|
- **Controlled state**: Manage active tab externally
|
|
23
|
-
- **
|
|
23
|
+
- **Lazy loading**: Only render active tab content for performance
|
|
24
|
+
- **Closeable tabs**: Editor-like dynamic tab management
|
|
25
|
+
- **Add button**: Create new tabs dynamically
|
|
26
|
+
- **Keyboard navigation**: Full arrow key, Home/End, Enter/Space support
|
|
27
|
+
|
|
28
|
+
## Keyboard Navigation
|
|
29
|
+
- **Arrow Left/Right** (horizontal) or **Up/Down** (vertical): Move focus between tabs
|
|
30
|
+
- **Home/End**: Jump to first/last tab
|
|
31
|
+
- **Enter/Space**: Activate focused tab
|
|
32
|
+
- **Delete/Backspace**: Close focused tab (if closeable)
|
|
24
33
|
|
|
25
34
|
## Usage
|
|
26
35
|
|
|
@@ -29,7 +38,7 @@ import { Tabs } from 'notebook-ui';
|
|
|
29
38
|
|
|
30
39
|
const tabs = [
|
|
31
40
|
{ id: 'profile', label: 'Profile', content: <ProfileContent /> },
|
|
32
|
-
{ id: 'settings', label: 'Settings', content: <SettingsContent />, badge: 3 },
|
|
41
|
+
{ id: 'settings', label: 'Settings', content: <SettingsContent />, badge: 3, badgeVariant: 'warning' },
|
|
33
42
|
];
|
|
34
43
|
|
|
35
44
|
<Tabs
|
|
@@ -37,6 +46,9 @@ const tabs = [
|
|
|
37
46
|
activeTab={activeTab}
|
|
38
47
|
onChange={setActiveTab}
|
|
39
48
|
variant="underline"
|
|
49
|
+
lazy
|
|
50
|
+
closeable
|
|
51
|
+
onClose={(id) => removeTab(id)}
|
|
40
52
|
/>
|
|
41
53
|
\`\`\`
|
|
42
54
|
`,
|
|
@@ -280,3 +292,634 @@ export const ControlledMode: Story = {
|
|
|
280
292
|
);
|
|
281
293
|
},
|
|
282
294
|
};
|
|
295
|
+
|
|
296
|
+
export const BadgeVariants: Story = {
|
|
297
|
+
render: () => {
|
|
298
|
+
const [activeTab, setActiveTab] = useState('inbox');
|
|
299
|
+
const tabs: Tab[] = [
|
|
300
|
+
{ id: 'inbox', label: 'Inbox', badge: 12, badgeVariant: 'info', content: <div style={{ padding: '1rem' }}>Inbox messages (12 new)</div> },
|
|
301
|
+
{ id: 'alerts', label: 'Alerts', badge: 3, badgeVariant: 'error', content: <div style={{ padding: '1rem' }}>Critical alerts (3)</div> },
|
|
302
|
+
{ id: 'warnings', label: 'Warnings', badge: 7, badgeVariant: 'warning', content: <div style={{ padding: '1rem' }}>Warnings (7)</div> },
|
|
303
|
+
{ id: 'completed', label: 'Completed', badge: 24, badgeVariant: 'success', content: <div style={{ padding: '1rem' }}>Completed tasks (24)</div> },
|
|
304
|
+
{ id: 'all', label: 'All', badge: 46, badgeVariant: 'neutral', content: <div style={{ padding: '1rem' }}>All items (46)</div> },
|
|
305
|
+
];
|
|
306
|
+
return <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />;
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const CloseableTabs: Story = {
|
|
311
|
+
render: () => {
|
|
312
|
+
const [tabs, setTabs] = useState<Tab[]>([
|
|
313
|
+
{ id: 'home', label: 'Home', icon: <Home className="h-4 w-4" />, closeable: false, content: <div style={{ padding: '1rem' }}>Home tab (not closeable)</div> },
|
|
314
|
+
{ id: 'doc1', label: 'Document 1', icon: <FileText className="h-4 w-4" />, content: <div style={{ padding: '1rem' }}>Document 1 content</div> },
|
|
315
|
+
{ id: 'doc2', label: 'Document 2', icon: <FileText className="h-4 w-4" />, content: <div style={{ padding: '1rem' }}>Document 2 content</div> },
|
|
316
|
+
{ id: 'mail', label: 'Mail', icon: <Mail className="h-4 w-4" />, badge: 5, content: <div style={{ padding: '1rem' }}>Mail content</div> },
|
|
317
|
+
]);
|
|
318
|
+
const [activeTab, setActiveTab] = useState('home');
|
|
319
|
+
|
|
320
|
+
const handleClose = (tabId: string) => {
|
|
321
|
+
const tabIndex = tabs.findIndex(t => t.id === tabId);
|
|
322
|
+
const newTabs = tabs.filter(t => t.id !== tabId);
|
|
323
|
+
setTabs(newTabs);
|
|
324
|
+
|
|
325
|
+
// If closing active tab, switch to adjacent tab
|
|
326
|
+
if (activeTab === tabId && newTabs.length > 0) {
|
|
327
|
+
const newActiveIndex = Math.min(tabIndex, newTabs.length - 1);
|
|
328
|
+
setActiveTab(newTabs[newActiveIndex].id);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<div>
|
|
334
|
+
<p style={{ marginBottom: '1rem', color: '#64748b', fontSize: '0.875rem' }}>
|
|
335
|
+
Hover over tabs to see close buttons. Home tab is not closeable.
|
|
336
|
+
</p>
|
|
337
|
+
<Tabs
|
|
338
|
+
tabs={tabs}
|
|
339
|
+
activeTab={activeTab}
|
|
340
|
+
onChange={setActiveTab}
|
|
341
|
+
closeable
|
|
342
|
+
onClose={handleClose}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const DynamicTabs: Story = {
|
|
350
|
+
render: () => {
|
|
351
|
+
const [tabs, setTabs] = useState<Tab[]>([
|
|
352
|
+
{ id: 'tab-1', label: 'Tab 1', content: <div style={{ padding: '1rem' }}>Content for Tab 1</div> },
|
|
353
|
+
{ id: 'tab-2', label: 'Tab 2', content: <div style={{ padding: '1rem' }}>Content for Tab 2</div> },
|
|
354
|
+
]);
|
|
355
|
+
const [activeTab, setActiveTab] = useState('tab-1');
|
|
356
|
+
const nextIdRef = useRef(3);
|
|
357
|
+
|
|
358
|
+
const handleAdd = () => {
|
|
359
|
+
const newId = `tab-${nextIdRef.current++}`;
|
|
360
|
+
const newTab: Tab = {
|
|
361
|
+
id: newId,
|
|
362
|
+
label: `Tab ${nextIdRef.current - 1}`,
|
|
363
|
+
content: <div style={{ padding: '1rem' }}>Content for Tab {nextIdRef.current - 1}</div>,
|
|
364
|
+
};
|
|
365
|
+
setTabs([...tabs, newTab]);
|
|
366
|
+
setActiveTab(newId);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const handleClose = (tabId: string) => {
|
|
370
|
+
const tabIndex = tabs.findIndex(t => t.id === tabId);
|
|
371
|
+
const newTabs = tabs.filter(t => t.id !== tabId);
|
|
372
|
+
setTabs(newTabs);
|
|
373
|
+
|
|
374
|
+
if (activeTab === tabId && newTabs.length > 0) {
|
|
375
|
+
const newActiveIndex = Math.min(tabIndex, newTabs.length - 1);
|
|
376
|
+
setActiveTab(newTabs[newActiveIndex].id);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div>
|
|
382
|
+
<p style={{ marginBottom: '1rem', color: '#64748b', fontSize: '0.875rem' }}>
|
|
383
|
+
Click the + button to add new tabs. Tabs can be closed by clicking the X.
|
|
384
|
+
</p>
|
|
385
|
+
<Tabs
|
|
386
|
+
tabs={tabs}
|
|
387
|
+
activeTab={activeTab}
|
|
388
|
+
onChange={setActiveTab}
|
|
389
|
+
closeable
|
|
390
|
+
onClose={handleClose}
|
|
391
|
+
showAddButton
|
|
392
|
+
onAdd={handleAdd}
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export const LazyLoading: Story = {
|
|
400
|
+
render: () => {
|
|
401
|
+
const [activeTab, setActiveTab] = useState('tab1');
|
|
402
|
+
const [renderLog, setRenderLog] = useState<string[]>(['tab1']);
|
|
403
|
+
|
|
404
|
+
// Each tab logs when it renders
|
|
405
|
+
const createContent = (id: string, name: string) => {
|
|
406
|
+
// Track render
|
|
407
|
+
if (!renderLog.includes(id)) {
|
|
408
|
+
setRenderLog(prev => [...prev, id]);
|
|
409
|
+
}
|
|
410
|
+
return (
|
|
411
|
+
<div style={{ padding: '1rem' }}>
|
|
412
|
+
<h3>{name} Content</h3>
|
|
413
|
+
<p>This tab was rendered when you first visited it.</p>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const tabs: Tab[] = [
|
|
419
|
+
{ id: 'tab1', label: 'Tab 1', content: createContent('tab1', 'Tab 1') },
|
|
420
|
+
{ id: 'tab2', label: 'Tab 2', content: createContent('tab2', 'Tab 2') },
|
|
421
|
+
{ id: 'tab3', label: 'Tab 3', content: createContent('tab3', 'Tab 3') },
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div>
|
|
426
|
+
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8fafc', borderRadius: '0.375rem' }}>
|
|
427
|
+
<strong>Rendered tabs:</strong> {renderLog.join(', ')}
|
|
428
|
+
<p style={{ fontSize: '0.75rem', color: '#64748b', marginTop: '0.25rem' }}>
|
|
429
|
+
With lazy loading, only the active tab is rendered. Visit other tabs to see them added to the list.
|
|
430
|
+
</p>
|
|
431
|
+
</div>
|
|
432
|
+
<Tabs
|
|
433
|
+
tabs={tabs}
|
|
434
|
+
activeTab={activeTab}
|
|
435
|
+
onChange={setActiveTab}
|
|
436
|
+
lazy
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export const LazyWithPreserveState: Story = {
|
|
444
|
+
render: () => {
|
|
445
|
+
const [activeTab, setActiveTab] = useState('counter');
|
|
446
|
+
const [count, setCount] = useState(0);
|
|
447
|
+
|
|
448
|
+
const tabs: Tab[] = [
|
|
449
|
+
{
|
|
450
|
+
id: 'counter',
|
|
451
|
+
label: 'Counter',
|
|
452
|
+
content: (
|
|
453
|
+
<div style={{ padding: '1rem' }}>
|
|
454
|
+
<h3>Counter Tab</h3>
|
|
455
|
+
<p>Count: {count}</p>
|
|
456
|
+
<button
|
|
457
|
+
onClick={() => setCount(c => c + 1)}
|
|
458
|
+
style={{ padding: '0.5rem 1rem', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '0.375rem', cursor: 'pointer' }}
|
|
459
|
+
>
|
|
460
|
+
Increment
|
|
461
|
+
</button>
|
|
462
|
+
</div>
|
|
463
|
+
)
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
id: 'other',
|
|
467
|
+
label: 'Other Tab',
|
|
468
|
+
content: (
|
|
469
|
+
<div style={{ padding: '1rem' }}>
|
|
470
|
+
<h3>Other Tab</h3>
|
|
471
|
+
<p>Switch back to Counter tab - the count should be preserved!</p>
|
|
472
|
+
</div>
|
|
473
|
+
)
|
|
474
|
+
},
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<div>
|
|
479
|
+
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8fafc', borderRadius: '0.375rem' }}>
|
|
480
|
+
<p style={{ fontSize: '0.875rem', color: '#64748b' }}>
|
|
481
|
+
With <code>preserveState</code>, tabs stay mounted after being visited. Increment the counter, switch tabs, then return - the state is preserved.
|
|
482
|
+
</p>
|
|
483
|
+
</div>
|
|
484
|
+
<Tabs
|
|
485
|
+
tabs={tabs}
|
|
486
|
+
activeTab={activeTab}
|
|
487
|
+
onChange={setActiveTab}
|
|
488
|
+
lazy
|
|
489
|
+
preserveState
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
export const KeyboardNavigation: Story = {
|
|
497
|
+
render: () => {
|
|
498
|
+
const [activeTab, setActiveTab] = useState('profile');
|
|
499
|
+
return (
|
|
500
|
+
<div>
|
|
501
|
+
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8fafc', borderRadius: '0.375rem' }}>
|
|
502
|
+
<p style={{ fontSize: '0.875rem', color: '#334155', marginBottom: '0.5rem' }}>
|
|
503
|
+
<strong>Test keyboard navigation:</strong>
|
|
504
|
+
</p>
|
|
505
|
+
<ul style={{ fontSize: '0.75rem', color: '#64748b', marginLeft: '1rem' }}>
|
|
506
|
+
<li>Click a tab to focus, then use <strong>Arrow Left/Right</strong> to navigate</li>
|
|
507
|
+
<li><strong>Home/End</strong> to jump to first/last tab</li>
|
|
508
|
+
<li><strong>Enter/Space</strong> to activate the focused tab</li>
|
|
509
|
+
<li>The disabled "Admin" tab is skipped during navigation</li>
|
|
510
|
+
</ul>
|
|
511
|
+
</div>
|
|
512
|
+
<Tabs
|
|
513
|
+
tabs={[
|
|
514
|
+
{ id: 'profile', label: 'Profile', icon: <User className="h-4 w-4" />, content: <div style={{ padding: '1rem' }}>Profile content</div> },
|
|
515
|
+
{ id: 'settings', label: 'Settings', icon: <Settings className="h-4 w-4" />, content: <div style={{ padding: '1rem' }}>Settings content</div> },
|
|
516
|
+
{ id: 'admin', label: 'Admin', icon: <Lock className="h-4 w-4" />, disabled: true, content: <div style={{ padding: '1rem' }}>Admin content</div> },
|
|
517
|
+
{ id: 'notifications', label: 'Notifications', icon: <Bell className="h-4 w-4" />, badge: 5, content: <div style={{ padding: '1rem' }}>Notifications content</div> },
|
|
518
|
+
]}
|
|
519
|
+
activeTab={activeTab}
|
|
520
|
+
onChange={setActiveTab}
|
|
521
|
+
/>
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export const Sizes: Story = {
|
|
528
|
+
render: () => {
|
|
529
|
+
const [activeTab, setActiveTab] = useState('tab1');
|
|
530
|
+
const tabs: Tab[] = [
|
|
531
|
+
{ id: 'tab1', label: 'Tab 1', badge: 3, content: <div style={{ padding: '1rem' }}>Tab 1 content</div> },
|
|
532
|
+
{ id: 'tab2', label: 'Tab 2', content: <div style={{ padding: '1rem' }}>Tab 2 content</div> },
|
|
533
|
+
{ id: 'tab3', label: 'Tab 3', content: <div style={{ padding: '1rem' }}>Tab 3 content</div> },
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
|
538
|
+
<div>
|
|
539
|
+
<p style={{ marginBottom: '0.5rem', fontSize: '0.875rem', color: '#64748b' }}>Small</p>
|
|
540
|
+
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} size="sm" />
|
|
541
|
+
</div>
|
|
542
|
+
<div>
|
|
543
|
+
<p style={{ marginBottom: '0.5rem', fontSize: '0.875rem', color: '#64748b' }}>Medium (default)</p>
|
|
544
|
+
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} size="md" />
|
|
545
|
+
</div>
|
|
546
|
+
<div>
|
|
547
|
+
<p style={{ marginBottom: '0.5rem', fontSize: '0.875rem', color: '#64748b' }}>Large</p>
|
|
548
|
+
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} size="lg" />
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
export const EditorStyleTabs: Story = {
|
|
556
|
+
render: () => {
|
|
557
|
+
const [tabs, setTabs] = useState<Tab[]>([
|
|
558
|
+
{ id: 'index', label: 'index.tsx', icon: <FileText className="h-4 w-4" />, content: <div style={{ padding: '1rem', fontFamily: 'monospace', background: '#1e293b', color: '#e2e8f0', borderRadius: '0 0 0.375rem 0.375rem' }}>{'// index.tsx\nexport default function App() {\n return <div>Hello World</div>;\n}'}</div> },
|
|
559
|
+
{ id: 'styles', label: 'styles.css', icon: <FileText className="h-4 w-4" />, badge: '•', badgeVariant: 'warning', content: <div style={{ padding: '1rem', fontFamily: 'monospace', background: '#1e293b', color: '#e2e8f0', borderRadius: '0 0 0.375rem 0.375rem' }}>{'/* styles.css - unsaved */\n.container {\n display: flex;\n}'}</div> },
|
|
560
|
+
{ id: 'config', label: 'config.json', icon: <FileText className="h-4 w-4" />, content: <div style={{ padding: '1rem', fontFamily: 'monospace', background: '#1e293b', color: '#e2e8f0', borderRadius: '0 0 0.375rem 0.375rem' }}>{'{\n "name": "my-app",\n "version": "1.0.0"\n}'}</div> },
|
|
561
|
+
]);
|
|
562
|
+
const [activeTab, setActiveTab] = useState('index');
|
|
563
|
+
const nextIdRef = useRef(1);
|
|
564
|
+
|
|
565
|
+
const handleAdd = () => {
|
|
566
|
+
const newId = `new-${nextIdRef.current++}`;
|
|
567
|
+
const newTab: Tab = {
|
|
568
|
+
id: newId,
|
|
569
|
+
label: `untitled-${nextIdRef.current - 1}.txt`,
|
|
570
|
+
icon: <FileText className="h-4 w-4" />,
|
|
571
|
+
content: <div style={{ padding: '1rem', fontFamily: 'monospace', background: '#1e293b', color: '#e2e8f0', borderRadius: '0 0 0.375rem 0.375rem' }}>{'// New file'}</div>,
|
|
572
|
+
};
|
|
573
|
+
setTabs([...tabs, newTab]);
|
|
574
|
+
setActiveTab(newId);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const handleClose = (tabId: string) => {
|
|
578
|
+
const tabIndex = tabs.findIndex(t => t.id === tabId);
|
|
579
|
+
const newTabs = tabs.filter(t => t.id !== tabId);
|
|
580
|
+
setTabs(newTabs);
|
|
581
|
+
|
|
582
|
+
if (activeTab === tabId && newTabs.length > 0) {
|
|
583
|
+
const newActiveIndex = Math.min(tabIndex, newTabs.length - 1);
|
|
584
|
+
setActiveTab(newTabs[newActiveIndex].id);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
<div style={{ border: '1px solid #334155', borderRadius: '0.375rem', overflow: 'hidden' }}>
|
|
590
|
+
<Tabs
|
|
591
|
+
tabs={tabs}
|
|
592
|
+
activeTab={activeTab}
|
|
593
|
+
onChange={setActiveTab}
|
|
594
|
+
variant="pill"
|
|
595
|
+
size="sm"
|
|
596
|
+
closeable
|
|
597
|
+
onClose={handleClose}
|
|
598
|
+
showAddButton
|
|
599
|
+
onAdd={handleAdd}
|
|
600
|
+
/>
|
|
601
|
+
</div>
|
|
602
|
+
);
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// ============================================
|
|
607
|
+
// COMPOUND COMPONENT PATTERN STORIES
|
|
608
|
+
// ============================================
|
|
609
|
+
|
|
610
|
+
export const CompoundBasic: Story = {
|
|
611
|
+
render: () => (
|
|
612
|
+
<TabsRoot defaultValue="account">
|
|
613
|
+
<TabsList>
|
|
614
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
615
|
+
<TabsTrigger value="password">Password</TabsTrigger>
|
|
616
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
617
|
+
</TabsList>
|
|
618
|
+
<TabsContent value="account">
|
|
619
|
+
<div style={{ padding: '1rem' }}>
|
|
620
|
+
<h3>Account Settings</h3>
|
|
621
|
+
<p>Manage your account information and preferences.</p>
|
|
622
|
+
</div>
|
|
623
|
+
</TabsContent>
|
|
624
|
+
<TabsContent value="password">
|
|
625
|
+
<div style={{ padding: '1rem' }}>
|
|
626
|
+
<h3>Password Settings</h3>
|
|
627
|
+
<p>Change your password and security settings.</p>
|
|
628
|
+
</div>
|
|
629
|
+
</TabsContent>
|
|
630
|
+
<TabsContent value="settings">
|
|
631
|
+
<div style={{ padding: '1rem' }}>
|
|
632
|
+
<h3>Application Settings</h3>
|
|
633
|
+
<p>Configure application preferences.</p>
|
|
634
|
+
</div>
|
|
635
|
+
</TabsContent>
|
|
636
|
+
</TabsRoot>
|
|
637
|
+
),
|
|
638
|
+
parameters: {
|
|
639
|
+
docs: {
|
|
640
|
+
description: {
|
|
641
|
+
story: `
|
|
642
|
+
The compound component pattern provides more flexibility over the layout and structure of tabs.
|
|
643
|
+
This is similar to Radix UI's Tabs API.
|
|
644
|
+
|
|
645
|
+
\`\`\`tsx
|
|
646
|
+
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from 'notebook-ui';
|
|
647
|
+
|
|
648
|
+
<TabsRoot defaultValue="account">
|
|
649
|
+
<TabsList>
|
|
650
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
651
|
+
<TabsTrigger value="password">Password</TabsTrigger>
|
|
652
|
+
</TabsList>
|
|
653
|
+
<TabsContent value="account">Account content</TabsContent>
|
|
654
|
+
<TabsContent value="password">Password content</TabsContent>
|
|
655
|
+
</TabsRoot>
|
|
656
|
+
\`\`\`
|
|
657
|
+
`,
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
export const CompoundWithIcons: Story = {
|
|
664
|
+
render: () => (
|
|
665
|
+
<TabsRoot defaultValue="profile" variant="underline">
|
|
666
|
+
<TabsList>
|
|
667
|
+
<TabsTrigger value="profile" icon={<User className="h-4 w-4" />}>
|
|
668
|
+
Profile
|
|
669
|
+
</TabsTrigger>
|
|
670
|
+
<TabsTrigger value="settings" icon={<Settings className="h-4 w-4" />}>
|
|
671
|
+
Settings
|
|
672
|
+
</TabsTrigger>
|
|
673
|
+
<TabsTrigger value="notifications" icon={<Bell className="h-4 w-4" />}>
|
|
674
|
+
Notifications
|
|
675
|
+
</TabsTrigger>
|
|
676
|
+
<TabsTrigger value="security" icon={<Lock className="h-4 w-4" />} disabled>
|
|
677
|
+
Security
|
|
678
|
+
</TabsTrigger>
|
|
679
|
+
</TabsList>
|
|
680
|
+
<TabsContent value="profile">
|
|
681
|
+
<div style={{ padding: '1rem' }}>
|
|
682
|
+
<h3>Profile Information</h3>
|
|
683
|
+
<p>Update your profile details and avatar.</p>
|
|
684
|
+
</div>
|
|
685
|
+
</TabsContent>
|
|
686
|
+
<TabsContent value="settings">
|
|
687
|
+
<div style={{ padding: '1rem' }}>
|
|
688
|
+
<h3>Application Settings</h3>
|
|
689
|
+
<p>Configure your application preferences.</p>
|
|
690
|
+
</div>
|
|
691
|
+
</TabsContent>
|
|
692
|
+
<TabsContent value="notifications">
|
|
693
|
+
<div style={{ padding: '1rem' }}>
|
|
694
|
+
<h3>Notification Preferences</h3>
|
|
695
|
+
<p>Manage how you receive notifications.</p>
|
|
696
|
+
</div>
|
|
697
|
+
</TabsContent>
|
|
698
|
+
<TabsContent value="security">
|
|
699
|
+
<div style={{ padding: '1rem' }}>
|
|
700
|
+
<h3>Security Settings</h3>
|
|
701
|
+
<p>This tab is disabled.</p>
|
|
702
|
+
</div>
|
|
703
|
+
</TabsContent>
|
|
704
|
+
</TabsRoot>
|
|
705
|
+
),
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
export const CompoundWithBadges: Story = {
|
|
709
|
+
render: () => (
|
|
710
|
+
<TabsRoot defaultValue="inbox" variant="pill">
|
|
711
|
+
<TabsList>
|
|
712
|
+
<TabsTrigger value="inbox" badge={12} badgeVariant="info">
|
|
713
|
+
Inbox
|
|
714
|
+
</TabsTrigger>
|
|
715
|
+
<TabsTrigger value="alerts" badge={3} badgeVariant="error">
|
|
716
|
+
Alerts
|
|
717
|
+
</TabsTrigger>
|
|
718
|
+
<TabsTrigger value="drafts" badge={5} badgeVariant="warning">
|
|
719
|
+
Drafts
|
|
720
|
+
</TabsTrigger>
|
|
721
|
+
<TabsTrigger value="sent">
|
|
722
|
+
Sent
|
|
723
|
+
</TabsTrigger>
|
|
724
|
+
</TabsList>
|
|
725
|
+
<TabsContent value="inbox">
|
|
726
|
+
<div style={{ padding: '1rem' }}>You have 12 new messages.</div>
|
|
727
|
+
</TabsContent>
|
|
728
|
+
<TabsContent value="alerts">
|
|
729
|
+
<div style={{ padding: '1rem' }}>3 critical alerts require attention.</div>
|
|
730
|
+
</TabsContent>
|
|
731
|
+
<TabsContent value="drafts">
|
|
732
|
+
<div style={{ padding: '1rem' }}>5 drafts saved.</div>
|
|
733
|
+
</TabsContent>
|
|
734
|
+
<TabsContent value="sent">
|
|
735
|
+
<div style={{ padding: '1rem' }}>View your sent messages.</div>
|
|
736
|
+
</TabsContent>
|
|
737
|
+
</TabsRoot>
|
|
738
|
+
),
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
export const CompoundControlled: Story = {
|
|
742
|
+
render: () => {
|
|
743
|
+
const [activeTab, setActiveTab] = useState('tab1');
|
|
744
|
+
|
|
745
|
+
return (
|
|
746
|
+
<div>
|
|
747
|
+
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
|
|
748
|
+
<button
|
|
749
|
+
onClick={() => setActiveTab('tab1')}
|
|
750
|
+
style={{ padding: '0.5rem 1rem', background: activeTab === 'tab1' ? '#334155' : '#f1f5f9', color: activeTab === 'tab1' ? 'white' : '#334155', border: 'none', borderRadius: '0.375rem', cursor: 'pointer' }}
|
|
751
|
+
>
|
|
752
|
+
Activate Tab 1
|
|
753
|
+
</button>
|
|
754
|
+
<button
|
|
755
|
+
onClick={() => setActiveTab('tab2')}
|
|
756
|
+
style={{ padding: '0.5rem 1rem', background: activeTab === 'tab2' ? '#334155' : '#f1f5f9', color: activeTab === 'tab2' ? 'white' : '#334155', border: 'none', borderRadius: '0.375rem', cursor: 'pointer' }}
|
|
757
|
+
>
|
|
758
|
+
Activate Tab 2
|
|
759
|
+
</button>
|
|
760
|
+
<button
|
|
761
|
+
onClick={() => setActiveTab('tab3')}
|
|
762
|
+
style={{ padding: '0.5rem 1rem', background: activeTab === 'tab3' ? '#334155' : '#f1f5f9', color: activeTab === 'tab3' ? 'white' : '#334155', border: 'none', borderRadius: '0.375rem', cursor: 'pointer' }}
|
|
763
|
+
>
|
|
764
|
+
Activate Tab 3
|
|
765
|
+
</button>
|
|
766
|
+
</div>
|
|
767
|
+
<TabsRoot value={activeTab} onValueChange={setActiveTab}>
|
|
768
|
+
<TabsList>
|
|
769
|
+
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
770
|
+
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
|
771
|
+
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
|
|
772
|
+
</TabsList>
|
|
773
|
+
<TabsContent value="tab1">
|
|
774
|
+
<div style={{ padding: '1rem' }}>Content for Tab 1</div>
|
|
775
|
+
</TabsContent>
|
|
776
|
+
<TabsContent value="tab2">
|
|
777
|
+
<div style={{ padding: '1rem' }}>Content for Tab 2</div>
|
|
778
|
+
</TabsContent>
|
|
779
|
+
<TabsContent value="tab3">
|
|
780
|
+
<div style={{ padding: '1rem' }}>Content for Tab 3</div>
|
|
781
|
+
</TabsContent>
|
|
782
|
+
</TabsRoot>
|
|
783
|
+
</div>
|
|
784
|
+
);
|
|
785
|
+
},
|
|
786
|
+
parameters: {
|
|
787
|
+
docs: {
|
|
788
|
+
description: {
|
|
789
|
+
story: 'Use `value` and `onValueChange` props for controlled mode.',
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
export const CompoundVertical: Story = {
|
|
796
|
+
render: () => (
|
|
797
|
+
<TabsRoot defaultValue="general" orientation="vertical">
|
|
798
|
+
<TabsList>
|
|
799
|
+
<TabsTrigger value="general" icon={<Settings className="h-4 w-4" />}>
|
|
800
|
+
General
|
|
801
|
+
</TabsTrigger>
|
|
802
|
+
<TabsTrigger value="profile" icon={<User className="h-4 w-4" />}>
|
|
803
|
+
Profile
|
|
804
|
+
</TabsTrigger>
|
|
805
|
+
<TabsTrigger value="notifications" icon={<Bell className="h-4 w-4" />}>
|
|
806
|
+
Notifications
|
|
807
|
+
</TabsTrigger>
|
|
808
|
+
<TabsTrigger value="security" icon={<Lock className="h-4 w-4" />}>
|
|
809
|
+
Security
|
|
810
|
+
</TabsTrigger>
|
|
811
|
+
</TabsList>
|
|
812
|
+
<TabsContent value="general">
|
|
813
|
+
<div style={{ padding: '1rem' }}>
|
|
814
|
+
<h3>General Settings</h3>
|
|
815
|
+
<p>Configure general application settings.</p>
|
|
816
|
+
</div>
|
|
817
|
+
</TabsContent>
|
|
818
|
+
<TabsContent value="profile">
|
|
819
|
+
<div style={{ padding: '1rem' }}>
|
|
820
|
+
<h3>Profile Settings</h3>
|
|
821
|
+
<p>Update your profile information.</p>
|
|
822
|
+
</div>
|
|
823
|
+
</TabsContent>
|
|
824
|
+
<TabsContent value="notifications">
|
|
825
|
+
<div style={{ padding: '1rem' }}>
|
|
826
|
+
<h3>Notification Settings</h3>
|
|
827
|
+
<p>Manage notification preferences.</p>
|
|
828
|
+
</div>
|
|
829
|
+
</TabsContent>
|
|
830
|
+
<TabsContent value="security">
|
|
831
|
+
<div style={{ padding: '1rem' }}>
|
|
832
|
+
<h3>Security Settings</h3>
|
|
833
|
+
<p>Configure security options.</p>
|
|
834
|
+
</div>
|
|
835
|
+
</TabsContent>
|
|
836
|
+
</TabsRoot>
|
|
837
|
+
),
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
export const CompoundLazy: Story = {
|
|
841
|
+
render: () => {
|
|
842
|
+
const [renderLog, setRenderLog] = useState<string[]>(['tab1']);
|
|
843
|
+
|
|
844
|
+
const LoggingContent = ({ id, name }: { id: string; name: string }) => {
|
|
845
|
+
// Track render
|
|
846
|
+
if (!renderLog.includes(id)) {
|
|
847
|
+
setRenderLog(prev => [...prev, id]);
|
|
848
|
+
}
|
|
849
|
+
return (
|
|
850
|
+
<div style={{ padding: '1rem' }}>
|
|
851
|
+
<h3>{name} Content</h3>
|
|
852
|
+
<p>This content was rendered when first visited.</p>
|
|
853
|
+
</div>
|
|
854
|
+
);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
return (
|
|
858
|
+
<div>
|
|
859
|
+
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8fafc', borderRadius: '0.375rem' }}>
|
|
860
|
+
<strong>Rendered tabs:</strong> {renderLog.join(', ')}
|
|
861
|
+
<p style={{ fontSize: '0.75rem', color: '#64748b', marginTop: '0.25rem' }}>
|
|
862
|
+
With lazy loading and preserveState, tabs are only rendered when visited and stay mounted.
|
|
863
|
+
</p>
|
|
864
|
+
</div>
|
|
865
|
+
<TabsRoot defaultValue="tab1" lazy preserveState>
|
|
866
|
+
<TabsList>
|
|
867
|
+
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
868
|
+
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
|
869
|
+
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
|
|
870
|
+
</TabsList>
|
|
871
|
+
<TabsContent value="tab1">
|
|
872
|
+
<LoggingContent id="tab1" name="Tab 1" />
|
|
873
|
+
</TabsContent>
|
|
874
|
+
<TabsContent value="tab2">
|
|
875
|
+
<LoggingContent id="tab2" name="Tab 2" />
|
|
876
|
+
</TabsContent>
|
|
877
|
+
<TabsContent value="tab3">
|
|
878
|
+
<LoggingContent id="tab3" name="Tab 3" />
|
|
879
|
+
</TabsContent>
|
|
880
|
+
</TabsRoot>
|
|
881
|
+
</div>
|
|
882
|
+
);
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
export const CompoundSeparatedLayout: Story = {
|
|
887
|
+
render: () => (
|
|
888
|
+
<TabsRoot defaultValue="overview">
|
|
889
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
|
890
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
891
|
+
<h2 style={{ margin: 0 }}>Dashboard</h2>
|
|
892
|
+
<TabsList>
|
|
893
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
894
|
+
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
895
|
+
<TabsTrigger value="reports">Reports</TabsTrigger>
|
|
896
|
+
</TabsList>
|
|
897
|
+
</div>
|
|
898
|
+
<div style={{ padding: '1.5rem', background: '#f8fafc', borderRadius: '0.5rem', minHeight: '200px' }}>
|
|
899
|
+
<TabsContent value="overview">
|
|
900
|
+
<h3>Overview</h3>
|
|
901
|
+
<p>Dashboard overview with key metrics.</p>
|
|
902
|
+
</TabsContent>
|
|
903
|
+
<TabsContent value="analytics">
|
|
904
|
+
<h3>Analytics</h3>
|
|
905
|
+
<p>Detailed analytics and charts.</p>
|
|
906
|
+
</TabsContent>
|
|
907
|
+
<TabsContent value="reports">
|
|
908
|
+
<h3>Reports</h3>
|
|
909
|
+
<p>Generate and view reports.</p>
|
|
910
|
+
</TabsContent>
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
</TabsRoot>
|
|
914
|
+
),
|
|
915
|
+
parameters: {
|
|
916
|
+
docs: {
|
|
917
|
+
description: {
|
|
918
|
+
story: `
|
|
919
|
+
The compound pattern allows flexible layouts where the tab triggers and content don't need to be adjacent.
|
|
920
|
+
This example places the tabs in a header row separate from the content area.
|
|
921
|
+
`,
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
};
|