@papernote/ui 1.10.9 → 1.10.11

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/styles.css CHANGED
@@ -2308,11 +2308,6 @@ input:checked + .slider:before{
2308
2308
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2309
2309
  }
2310
2310
 
2311
- .-rotate-90{
2312
- --tw-rotate: -90deg;
2313
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2314
- }
2315
-
2316
2311
  .rotate-0{
2317
2312
  --tw-rotate: 0deg;
2318
2313
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -2399,6 +2394,25 @@ input:checked + .slider:before{
2399
2394
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
2400
2395
  }
2401
2396
 
2397
+ @keyframes rowFlash{
2398
+
2399
+ 0%{
2400
+ background-color: rgb(187 247 208);
2401
+ }
2402
+
2403
+ 25%{
2404
+ background-color: rgb(187 247 208);
2405
+ }
2406
+
2407
+ 100%{
2408
+ background-color: transparent;
2409
+ }
2410
+ }
2411
+
2412
+ .animate-row-flash{
2413
+ animation: rowFlash 2s ease-out;
2414
+ }
2415
+
2402
2416
  @keyframes scaleIn{
2403
2417
 
2404
2418
  0%{
@@ -2595,6 +2609,28 @@ input:checked + .slider:before{
2595
2609
  animation: spin 1s linear infinite;
2596
2610
  }
2597
2611
 
2612
+ @keyframes successCheck{
2613
+
2614
+ 0%{
2615
+ transform: scale(0);
2616
+ opacity: 0;
2617
+ }
2618
+
2619
+ 50%{
2620
+ transform: scale(1.2);
2621
+ opacity: 1;
2622
+ }
2623
+
2624
+ 100%{
2625
+ transform: scale(1);
2626
+ opacity: 1;
2627
+ }
2628
+ }
2629
+
2630
+ .animate-success-check{
2631
+ animation: successCheck 0.6s ease-out;
2632
+ }
2633
+
2598
2634
  .cursor-col-resize{
2599
2635
  cursor: col-resize;
2600
2636
  }
@@ -3451,6 +3487,11 @@ input:checked + .slider:before{
3451
3487
  background-color: rgb(87 83 78 / var(--tw-bg-opacity, 1));
3452
3488
  }
3453
3489
 
3490
+ .bg-ink-700{
3491
+ --tw-bg-opacity: 1;
3492
+ background-color: rgb(68 64 60 / var(--tw-bg-opacity, 1));
3493
+ }
3494
+
3454
3495
  .bg-ink-800{
3455
3496
  --tw-bg-opacity: 1;
3456
3497
  background-color: rgb(41 37 36 / var(--tw-bg-opacity, 1));
@@ -3771,6 +3812,14 @@ input:checked + .slider:before{
3771
3812
  fill: currentColor;
3772
3813
  }
3773
3814
 
3815
+ .fill-ink-400{
3816
+ fill: #a8a29e;
3817
+ }
3818
+
3819
+ .fill-ink-700{
3820
+ fill: #44403c;
3821
+ }
3822
+
3774
3823
  .fill-warning-500{
3775
3824
  fill: #f59e0b;
3776
3825
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papernote/ui",
3
- "version": "1.10.9",
3
+ "version": "1.10.11",
4
4
  "type": "module",
5
5
  "description": "A modern React component library with a paper notebook aesthetic - minimal, professional, and expressive",
6
6
  "main": "dist/index.js",
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
2
3
  import Badge from './Badge';
3
4
  import { Check, X, AlertCircle, Info as InfoIcon } from 'lucide-react';
4
5
 
@@ -249,3 +250,58 @@ export const CountBadges: Story = {
249
250
  </div>
250
251
  ),
251
252
  };
253
+
254
+ export const Animated: Story = {
255
+ args: {
256
+ children: 'Animated Badge',
257
+ variant: 'success',
258
+ animate: true,
259
+ },
260
+ };
261
+
262
+ export const AnimatedBadges: Story = {
263
+ render: () => {
264
+ const [badges, setBadges] = useState<string[]>([]);
265
+ const [counter, setCounter] = useState(0);
266
+
267
+ const addBadge = () => {
268
+ const newBadge = `Badge ${counter + 1}`;
269
+ setBadges([...badges, newBadge]);
270
+ setCounter(counter + 1);
271
+ };
272
+
273
+ const removeBadge = (index: number) => {
274
+ setBadges(badges.filter((_, i) => i !== index));
275
+ };
276
+
277
+ return (
278
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
279
+ <button
280
+ onClick={addBadge}
281
+ style={{
282
+ padding: '0.5rem 1rem',
283
+ border: '1px solid #e5e5e5',
284
+ borderRadius: '0.375rem',
285
+ background: 'white',
286
+ cursor: 'pointer',
287
+ width: 'fit-content',
288
+ }}
289
+ >
290
+ Add Badge
291
+ </button>
292
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', minHeight: '2rem' }}>
293
+ {badges.map((badge, index) => (
294
+ <Badge
295
+ key={badge}
296
+ variant="info"
297
+ animate
298
+ onRemove={() => removeBadge(index)}
299
+ >
300
+ {badge}
301
+ </Badge>
302
+ ))}
303
+ </div>
304
+ </div>
305
+ );
306
+ },
307
+ };
@@ -16,6 +16,8 @@ export interface BadgeProps {
16
16
  truncate?: boolean;
17
17
  /** Maximum width for the badge (useful with truncate), e.g. '150px' or '10rem' */
18
18
  maxWidth?: string;
19
+ /** Apply fade-in animation when badge appears */
20
+ animate?: boolean;
19
21
  }
20
22
 
21
23
  export default function Badge({
@@ -29,6 +31,7 @@ export default function Badge({
29
31
  pill = false,
30
32
  truncate = false,
31
33
  maxWidth,
34
+ animate = false,
32
35
  }: BadgeProps) {
33
36
  const variantStyles = {
34
37
  success: 'bg-success-50 text-success-700 border-success-200',
@@ -79,6 +82,7 @@ export default function Badge({
79
82
  inline-block rounded-full
80
83
  ${dotVariantStyles[variant]}
81
84
  ${dotSizeStyles[size]}
85
+ ${animate ? 'animate-fade-in' : ''}
82
86
  ${className}
83
87
  `}
84
88
  aria-label={`${variant} indicator`}
@@ -95,6 +99,7 @@ export default function Badge({
95
99
  ${variantStyles[variant]}
96
100
  ${pill ? pillSizeStyles[size] : sizeStyles[size]}
97
101
  ${truncate ? 'max-w-full overflow-hidden' : ''}
102
+ ${animate ? 'animate-fade-in' : ''}
98
103
  ${className}
99
104
  `}
100
105
  style={maxWidth ? { maxWidth } : undefined}
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
2
3
  import Button from './Button';
3
4
  import { Save, Trash, Plus, Download } from 'lucide-react';
4
5
 
@@ -253,3 +254,100 @@ export const AllSizes: Story = {
253
254
  </div>
254
255
  ),
255
256
  };
257
+
258
+ export const SuccessAnimation: Story = {
259
+ args: {
260
+ variant: 'primary',
261
+ showSuccess: true,
262
+ children: 'Saved!',
263
+ },
264
+ };
265
+
266
+ export const SaveWithSuccess: Story = {
267
+ render: () => {
268
+ const [saving, setSaving] = useState(false);
269
+ const [saved, setSaved] = useState(false);
270
+
271
+ const handleSave = async () => {
272
+ setSaving(true);
273
+ setSaved(false);
274
+
275
+ // Simulate API call
276
+ await new Promise(resolve => setTimeout(resolve, 1000));
277
+
278
+ setSaving(false);
279
+ setSaved(true);
280
+
281
+ // Reset after animation completes
282
+ setTimeout(() => setSaved(false), 1500);
283
+ };
284
+
285
+ return (
286
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
287
+ <Button
288
+ variant="primary"
289
+ icon={<Save />}
290
+ loading={saving}
291
+ showSuccess={saved}
292
+ onClick={handleSave}
293
+ >
294
+ Save Changes
295
+ </Button>
296
+ <p style={{ fontSize: '0.875rem', color: '#64748b' }}>
297
+ Click to simulate save with success animation
298
+ </p>
299
+ </div>
300
+ );
301
+ },
302
+ };
303
+
304
+ export const SuccessAnimationVariants: Story = {
305
+ render: () => {
306
+ const [successStates, setSuccessStates] = useState<Record<string, boolean>>({});
307
+
308
+ const triggerSuccess = (key: string) => {
309
+ setSuccessStates(prev => ({ ...prev, [key]: true }));
310
+ setTimeout(() => {
311
+ setSuccessStates(prev => ({ ...prev, [key]: false }));
312
+ }, 1500);
313
+ };
314
+
315
+ return (
316
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
317
+ <p style={{ fontSize: '0.875rem', color: '#64748b', marginBottom: '0.5rem' }}>
318
+ Click any button to see the success animation
319
+ </p>
320
+ <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
321
+ <Button
322
+ variant="primary"
323
+ showSuccess={successStates['primary']}
324
+ onClick={() => triggerSuccess('primary')}
325
+ >
326
+ Primary
327
+ </Button>
328
+ <Button
329
+ variant="secondary"
330
+ showSuccess={successStates['secondary']}
331
+ onClick={() => triggerSuccess('secondary')}
332
+ >
333
+ Secondary
334
+ </Button>
335
+ <Button
336
+ variant="outline"
337
+ showSuccess={successStates['outline']}
338
+ onClick={() => triggerSuccess('outline')}
339
+ >
340
+ Outline
341
+ </Button>
342
+ <Button
343
+ variant="ghost"
344
+ showSuccess={successStates['ghost']}
345
+ onClick={() => triggerSuccess('ghost')}
346
+ >
347
+ Ghost
348
+ </Button>
349
+ </div>
350
+ </div>
351
+ );
352
+ },
353
+ };
@@ -1,5 +1,5 @@
1
- import React, { forwardRef } from 'react';
2
- import { Loader2 } from 'lucide-react';
1
+ import React, { forwardRef, useState, useEffect, useRef } from 'react';
2
+ import { Loader2, Check } from 'lucide-react';
3
3
 
4
4
  /**
5
5
  * Button component props
@@ -23,6 +23,10 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
23
23
  badge?: number | string;
24
24
  /** Badge color variant */
25
25
  badgeVariant?: 'primary' | 'success' | 'warning' | 'error';
26
+ /** Show success checkmark animation (briefly shows checkmark, then reverts) */
27
+ showSuccess?: boolean;
28
+ /** Duration in ms for success animation (default: 1500) */
29
+ successDuration?: number;
26
30
  }
27
31
 
28
32
  /**
@@ -76,11 +80,40 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
76
80
  iconOnly = false,
77
81
  badge,
78
82
  badgeVariant = 'error',
83
+ showSuccess = false,
84
+ successDuration = 1500,
79
85
  children,
80
86
  disabled,
81
87
  className = '',
82
88
  ...props
83
89
  }, ref) => {
90
+ // Track success animation state
91
+ const [isShowingSuccess, setIsShowingSuccess] = useState(false);
92
+ const successTimeoutRef = useRef<number | null>(null);
93
+
94
+ // Handle showSuccess prop changes
95
+ useEffect(() => {
96
+ if (showSuccess && !isShowingSuccess) {
97
+ setIsShowingSuccess(true);
98
+
99
+ // Clear any existing timeout
100
+ if (successTimeoutRef.current) {
101
+ window.clearTimeout(successTimeoutRef.current);
102
+ }
103
+
104
+ // Set timeout to revert back
105
+ successTimeoutRef.current = window.setTimeout(() => {
106
+ setIsShowingSuccess(false);
107
+ }, successDuration);
108
+ }
109
+
110
+ return () => {
111
+ if (successTimeoutRef.current) {
112
+ window.clearTimeout(successTimeoutRef.current);
113
+ }
114
+ };
115
+ }, [showSuccess, successDuration, isShowingSuccess]);
116
+
84
117
  const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 disabled:opacity-40 disabled:cursor-not-allowed';
85
118
 
86
119
  const variantStyles = {
@@ -117,12 +150,15 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
117
150
  lg: 'min-w-[20px] h-5 text-xs px-1.5',
118
151
  };
119
152
 
153
+ // Determine what to show inside button
154
+ const showSuccessState = isShowingSuccess && !loading;
155
+
120
156
  const buttonElement = (
121
157
  <button
122
158
  ref={ref}
123
159
  className={`
124
160
  ${baseStyles}
125
- ${variantStyles[variant]}
161
+ ${showSuccessState ? 'bg-success-500 border-success-500 text-white' : variantStyles[variant]}
126
162
  ${sizeStyles[size]}
127
163
  ${fullWidth && !iconOnly ? 'w-full' : ''}
128
164
  ${className}
@@ -134,11 +170,14 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
134
170
  {loading && (
135
171
  <Loader2 className={`${iconSize[size]} animate-spin`} />
136
172
  )}
137
- {!loading && icon && iconPosition === 'left' && (
173
+ {showSuccessState && (
174
+ <Check className={`${iconSize[size]} animate-success-check`} />
175
+ )}
176
+ {!loading && !showSuccessState && icon && iconPosition === 'left' && (
138
177
  <span className={iconSize[size]}>{icon}</span>
139
178
  )}
140
- {!iconOnly && children}
141
- {!loading && icon && iconPosition === 'right' && !iconOnly && (
179
+ {!iconOnly && !showSuccessState && children}
180
+ {!loading && !showSuccessState && icon && iconPosition === 'right' && !iconOnly && (
142
181
  <span className={iconSize[size]}>{icon}</span>
143
182
  )}
144
183
  </button>
@@ -1,7 +1,8 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
  import DataTable from './DataTable';
4
4
  import Badge from './Badge';
5
+ import Button from './Button';
5
6
  import { Edit, Trash, Eye } from 'lucide-react';
6
7
 
7
8
  interface User {
@@ -432,3 +433,64 @@ export const FullWidthWithSecondaryRow: Story = {
432
433
  ],
433
434
  },
434
435
  };
436
+
437
+ export const RowHighlighting: Story = {
438
+ render: () => {
439
+ const [highlightedRows, setHighlightedRows] = useState<string[]>([]);
440
+
441
+ const simulateSave = (rowId: string) => {
442
+ setHighlightedRows([rowId]);
443
+ // Clear after animation completes
444
+ setTimeout(() => setHighlightedRows([]), 2000);
445
+ };
446
+
447
+ return (
448
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
449
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
450
+ <Button variant="secondary" size="sm" onClick={() => simulateSave('1')}>
451
+ Flash Row 1
452
+ </Button>
453
+ <Button variant="secondary" size="sm" onClick={() => simulateSave('2')}>
454
+ Flash Row 2
455
+ </Button>
456
+ <Button variant="secondary" size="sm" onClick={() => simulateSave('3')}>
457
+ Flash Row 3
458
+ </Button>
459
+ <Button
460
+ variant="primary"
461
+ size="sm"
462
+ onClick={() => {
463
+ setHighlightedRows(['1', '3', '5']);
464
+ setTimeout(() => setHighlightedRows([]), 2000);
465
+ }}
466
+ >
467
+ Flash Multiple
468
+ </Button>
469
+ </div>
470
+ <DataTable
471
+ data={sampleUsers}
472
+ columns={[
473
+ { key: 'name', header: 'Name', sortable: true },
474
+ { key: 'email', header: 'Email', sortable: true },
475
+ { key: 'role', header: 'Role' },
476
+ {
477
+ key: 'status',
478
+ header: 'Status',
479
+ render: (user: User) => (
480
+ <Badge variant={user.status === 'active' ? 'success' : user.status === 'inactive' ? 'error' : 'warning'}>
481
+ {user.status}
482
+ </Badge>
483
+ ),
484
+ },
485
+ ]}
486
+ highlightedRows={highlightedRows}
487
+ highlightDuration={2000}
488
+ />
489
+ <p style={{ fontSize: '0.875rem', color: '#64748b' }}>
490
+ Click a button to see the row flash green and fade back to normal.
491
+ This is useful for indicating successful saves or updates.
492
+ </p>
493
+ </div>
494
+ );
495
+ },
496
+ };
@@ -207,6 +207,10 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
207
207
  rowHighlight?: (item: T) => string | undefined;
208
208
  /** ID of a single row to highlight */
209
209
  highlightedRowId?: string | number;
210
+ /** Array of row IDs to temporarily highlight (flash animation) */
211
+ highlightedRows?: (string | number)[];
212
+ /** Duration in ms for temporary row highlight (default: 2000) */
213
+ highlightDuration?: number;
210
214
  /** Enable cell borders */
211
215
  bordered?: boolean;
212
216
  /** Custom border color (Tailwind class like 'border-paper-200') */
@@ -513,6 +517,8 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
513
517
  rowClassName,
514
518
  rowHighlight,
515
519
  highlightedRowId,
520
+ highlightedRows = [],
521
+ highlightDuration = 2000,
516
522
  bordered = false,
517
523
  borderColor = 'border-paper-200',
518
524
  disableHover = false,
@@ -561,6 +567,10 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
561
567
  // Row hover state (for coordinating primary + secondary row highlighting)
562
568
  const [hoveredRowKey, setHoveredRowKey] = useState<string | null>(null);
563
569
 
570
+ // Temporary row highlight state (for flash animation)
571
+ const [flashingRows, setFlashingRows] = useState<Set<string>>(new Set());
572
+ const flashTimeoutRef = useRef<number | null>(null);
573
+
564
574
  // Context menu state
565
575
  const [contextMenuState, setContextMenuState] = useState<{
566
576
  isOpen: boolean;
@@ -584,6 +594,31 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
584
594
  }
585
595
  }, [baseVisibleColumns, columnOrder.length]);
586
596
 
597
+ // Handle temporary row highlighting (flash animation)
598
+ useEffect(() => {
599
+ if (highlightedRows.length > 0) {
600
+ // Add new highlighted rows to flashing set
601
+ const newFlashingRows = new Set(highlightedRows.map(id => String(id)));
602
+ setFlashingRows(newFlashingRows);
603
+
604
+ // Clear any existing timeout
605
+ if (flashTimeoutRef.current) {
606
+ window.clearTimeout(flashTimeoutRef.current);
607
+ }
608
+
609
+ // Set timeout to clear the flash
610
+ flashTimeoutRef.current = window.setTimeout(() => {
611
+ setFlashingRows(new Set());
612
+ }, highlightDuration);
613
+ }
614
+
615
+ return () => {
616
+ if (flashTimeoutRef.current) {
617
+ window.clearTimeout(flashTimeoutRef.current);
618
+ }
619
+ };
620
+ }, [highlightedRows, highlightDuration]);
621
+
587
622
  // Apply column order
588
623
  const visibleColumns = reorderable && columnOrder.length > 0
589
624
  ? columnOrder
@@ -618,9 +653,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
618
653
  // Get row background class based on striping and highlighting
619
654
  const getRowBackgroundClass = (item: T, index: number): string => {
620
655
  const classes: string[] = [];
656
+ const rowKey = getRowKey(item);
621
657
 
658
+ // Check for temporary flash highlight (takes priority)
659
+ if (flashingRows.has(rowKey)) {
660
+ classes.push('animate-row-flash');
661
+ }
622
662
  // Check for highlighted row
623
- if (highlightedRowId !== undefined && getRowKey(item) === String(highlightedRowId)) {
663
+ else if (highlightedRowId !== undefined && rowKey === String(highlightedRowId)) {
624
664
  classes.push('bg-accent-100');
625
665
  }
626
666
  // Check for custom row highlight
@@ -410,3 +410,119 @@ export const MultipleProgress: Story = {
410
410
  </div>
411
411
  ),
412
412
  };
413
+
414
+ export const WithMilestones: Story = {
415
+ args: {
416
+ value: 65,
417
+ milestones: [25, 50, 75, 100],
418
+ },
419
+ };
420
+
421
+ export const WithMilestoneLabels: Story = {
422
+ args: {
423
+ value: 75,
424
+ milestones: [25, 50, 75, 100],
425
+ showMilestoneLabels: true,
426
+ },
427
+ };
428
+
429
+ export const CircularWithMilestones: Story = {
430
+ args: {
431
+ value: 60,
432
+ variant: 'circular',
433
+ milestones: [25, 50, 75, 100],
434
+ showLabel: true,
435
+ },
436
+ };
437
+
438
+ export const CircularWithMilestoneLabels: Story = {
439
+ args: {
440
+ value: 80,
441
+ variant: 'circular',
442
+ milestones: [25, 50, 75, 100],
443
+ showMilestoneLabels: true,
444
+ showLabel: true,
445
+ },
446
+ };
447
+
448
+ export const BudgetTracker: Story = {
449
+ render: () => (
450
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem', width: '100%' }}>
451
+ <div>
452
+ <div style={{ fontSize: '0.875rem', marginBottom: '0.5rem', color: '#64748b' }}>Monthly Budget</div>
453
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
454
+ <span style={{ fontWeight: 500 }}>$2,250 / $3,000</span>
455
+ <span style={{ color: '#f59e0b' }}>75% used</span>
456
+ </div>
457
+ <Progress
458
+ value={75}
459
+ color="warning"
460
+ milestones={[25, 50, 75, 100]}
461
+ showMilestoneLabels
462
+ />
463
+ </div>
464
+ <div>
465
+ <div style={{ fontSize: '0.875rem', marginBottom: '0.5rem', color: '#64748b' }}>Savings Goal</div>
466
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
467
+ <span style={{ fontWeight: 500 }}>$4,500 / $10,000</span>
468
+ <span style={{ color: '#3b82f6' }}>45% complete</span>
469
+ </div>
470
+ <Progress
471
+ value={45}
472
+ color="primary"
473
+ milestones={[25, 50, 75, 100]}
474
+ showMilestoneLabels
475
+ />
476
+ </div>
477
+ </div>
478
+ ),
479
+ };
480
+
481
+ export const GoalProgress: Story = {
482
+ render: () => (
483
+ <div style={{ display: 'flex', gap: '3rem', alignItems: 'center' }}>
484
+ <div style={{ textAlign: 'center' }}>
485
+ <Progress
486
+ value={85}
487
+ variant="circular"
488
+ size="lg"
489
+ color="success"
490
+ milestones={[25, 50, 75, 100]}
491
+ showLabel
492
+ />
493
+ <div style={{ fontSize: '0.875rem', color: '#64748b', marginTop: '0.5rem' }}>Sales Target</div>
494
+ </div>
495
+ <div style={{ textAlign: 'center' }}>
496
+ <Progress
497
+ value={60}
498
+ variant="circular"
499
+ size="lg"
500
+ color="primary"
501
+ milestones={[25, 50, 75, 100]}
502
+ showLabel
503
+ />
504
+ <div style={{ fontSize: '0.875rem', color: '#64748b', marginTop: '0.5rem' }}>Project Completion</div>
505
+ </div>
506
+ <div style={{ textAlign: 'center' }}>
507
+ <Progress
508
+ value={30}
509
+ variant="circular"
510
+ size="lg"
511
+ color="warning"
512
+ milestones={[25, 50, 75, 100]}
513
+ showLabel
514
+ />
515
+ <div style={{ fontSize: '0.875rem', color: '#64748b', marginTop: '0.5rem' }}>Sprint Progress</div>
516
+ </div>
517
+ </div>
518
+ ),
519
+ };
520
+
521
+ export const CustomMilestones: Story = {
522
+ args: {
523
+ value: 70,
524
+ milestones: [10, 30, 50, 80, 100],
525
+ showMilestoneLabels: true,
526
+ color: 'success',
527
+ },
528
+ };