@papernote/ui 1.10.14 → 1.10.16

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
@@ -2734,6 +2734,10 @@ input:checked + .slider:before{
2734
2734
  cursor: grabbing;
2735
2735
  }
2736
2736
 
2737
+ .cursor-help{
2738
+ cursor: help;
2739
+ }
2740
+
2737
2741
  .cursor-move{
2738
2742
  cursor: move;
2739
2743
  }
@@ -5501,6 +5505,11 @@ input:checked + .slider:before{
5501
5505
  background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
5502
5506
  }
5503
5507
 
5508
+ .hover\:bg-error-100:hover{
5509
+ --tw-bg-opacity: 1;
5510
+ background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
5511
+ }
5512
+
5504
5513
  .hover\:bg-error-200:hover{
5505
5514
  --tw-bg-opacity: 1;
5506
5515
  background-color: rgb(254 202 202 / var(--tw-bg-opacity, 1));
@@ -5576,6 +5585,11 @@ input:checked + .slider:before{
5576
5585
  background-color: rgb(250 250 249 / var(--tw-bg-opacity, 1));
5577
5586
  }
5578
5587
 
5588
+ .hover\:bg-primary-100:hover{
5589
+ --tw-bg-opacity: 1;
5590
+ background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));
5591
+ }
5592
+
5579
5593
  .hover\:bg-primary-200:hover{
5580
5594
  --tw-bg-opacity: 1;
5581
5595
  background-color: rgb(226 232 240 / var(--tw-bg-opacity, 1));
@@ -5636,6 +5650,11 @@ input:checked + .slider:before{
5636
5650
  background-color: rgb(4 120 87 / var(--tw-bg-opacity, 1));
5637
5651
  }
5638
5652
 
5653
+ .hover\:bg-warning-100:hover{
5654
+ --tw-bg-opacity: 1;
5655
+ background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1));
5656
+ }
5657
+
5639
5658
  .hover\:bg-warning-200:hover{
5640
5659
  --tw-bg-opacity: 1;
5641
5660
  background-color: rgb(253 230 138 / var(--tw-bg-opacity, 1));
@@ -5798,6 +5817,11 @@ input:checked + .slider:before{
5798
5817
  color: rgb(127 29 29 / var(--tw-text-opacity, 1));
5799
5818
  }
5800
5819
 
5820
+ .hover\:text-warning-700:hover{
5821
+ --tw-text-opacity: 1;
5822
+ color: rgb(180 83 9 / var(--tw-text-opacity, 1));
5823
+ }
5824
+
5801
5825
  .hover\:underline:hover{
5802
5826
  text-decoration-line: underline;
5803
5827
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papernote/ui",
3
- "version": "1.10.14",
3
+ "version": "1.10.16",
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",
@@ -0,0 +1,219 @@
1
+ import React, { useState, useRef, useEffect, ReactNode } from 'react';
2
+ import { ChevronDown, ExternalLink } from 'lucide-react';
3
+ import { Link } from 'react-router-dom';
4
+ import Badge from './Badge';
5
+
6
+ export interface CollapsibleSectionProps {
7
+ /** Section title displayed in the header */
8
+ title: string;
9
+ /** Optional icon displayed before the title */
10
+ icon?: ReactNode;
11
+ /** Badge content shown next to title (typically a count) */
12
+ badge?: number | string;
13
+ /** Badge color variant */
14
+ badgeVariant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
15
+ /** URL for "View All" link (uses react-router Link) */
16
+ viewAllHref?: string;
17
+ /** Custom label for the view all link */
18
+ viewAllLabel?: string;
19
+ /** Callback when "View All" is clicked (alternative to href) */
20
+ onViewAll?: () => void;
21
+ /** Initial open state (uncontrolled) */
22
+ defaultOpen?: boolean;
23
+ /** Controlled open state */
24
+ open?: boolean;
25
+ /** Callback when open state changes (for persistence) */
26
+ onOpenChange?: (open: boolean) => void;
27
+ /** Section content */
28
+ children: ReactNode;
29
+ /** Additional CSS classes for the container */
30
+ className?: string;
31
+ }
32
+
33
+ /**
34
+ * CollapsibleSection - A card-style collapsible container for dashboard sections
35
+ *
36
+ * Wraps content with a styled header that includes title, optional icon, badge count,
37
+ * and "View All" navigation. Supports both controlled and uncontrolled modes for
38
+ * localStorage persistence via onOpenChange callback.
39
+ *
40
+ * @example Basic usage
41
+ * ```tsx
42
+ * <CollapsibleSection
43
+ * title="Upcoming Bills"
44
+ * badge={5}
45
+ * badgeVariant="warning"
46
+ * viewAllHref="/bills"
47
+ * >
48
+ * <BillsList bills={upcomingBills} />
49
+ * </CollapsibleSection>
50
+ * ```
51
+ *
52
+ * @example With localStorage persistence
53
+ * ```tsx
54
+ * const [isOpen, setIsOpen] = useState(() =>
55
+ * localStorage.getItem('section-open') !== 'false'
56
+ * );
57
+ *
58
+ * <CollapsibleSection
59
+ * title="Pending Items"
60
+ * badge={pendingCount}
61
+ * open={isOpen}
62
+ * onOpenChange={(open) => {
63
+ * setIsOpen(open);
64
+ * localStorage.setItem('section-open', String(open));
65
+ * }}
66
+ * >
67
+ * {children}
68
+ * </CollapsibleSection>
69
+ * ```
70
+ */
71
+ export function CollapsibleSection({
72
+ title,
73
+ icon,
74
+ badge,
75
+ badgeVariant = 'neutral',
76
+ viewAllHref,
77
+ viewAllLabel = 'View All',
78
+ onViewAll,
79
+ defaultOpen = true,
80
+ open: controlledOpen,
81
+ onOpenChange,
82
+ children,
83
+ className = '',
84
+ }: CollapsibleSectionProps) {
85
+ const isControlled = controlledOpen !== undefined;
86
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
87
+ const isOpen = isControlled ? controlledOpen : internalOpen;
88
+
89
+ const contentRef = useRef<HTMLDivElement>(null);
90
+ const [height, setHeight] = useState<number>(0);
91
+
92
+ const handleToggle = () => {
93
+ const newOpen = !isOpen;
94
+
95
+ if (!isControlled) {
96
+ setInternalOpen(newOpen);
97
+ }
98
+
99
+ onOpenChange?.(newOpen);
100
+ };
101
+
102
+ const handleViewAllClick = (e: React.MouseEvent) => {
103
+ if (onViewAll) {
104
+ e.preventDefault();
105
+ onViewAll();
106
+ }
107
+ };
108
+
109
+ // Update height when content changes or open state changes
110
+ useEffect(() => {
111
+ if (!contentRef.current) return;
112
+
113
+ if (isOpen) {
114
+ const contentHeight = contentRef.current.scrollHeight;
115
+ setHeight(contentHeight);
116
+ } else {
117
+ setHeight(0);
118
+ }
119
+ }, [isOpen, children]);
120
+
121
+ // Recalculate height when window resizes
122
+ useEffect(() => {
123
+ if (!isOpen) return;
124
+
125
+ const handleResize = () => {
126
+ if (contentRef.current) {
127
+ setHeight(contentRef.current.scrollHeight);
128
+ }
129
+ };
130
+
131
+ window.addEventListener('resize', handleResize);
132
+ return () => window.removeEventListener('resize', handleResize);
133
+ }, [isOpen]);
134
+
135
+ const ViewAllContent = (
136
+ <>
137
+ <span>{viewAllLabel}</span>
138
+ <ExternalLink className="h-3.5 w-3.5" />
139
+ </>
140
+ );
141
+
142
+ return (
143
+ <div
144
+ className={`
145
+ bg-white bg-subtle-grain border-2 border-paper-300 rounded-xl shadow-sm
146
+ ${className}
147
+ `}
148
+ >
149
+ {/* Header */}
150
+ <div className="flex items-center justify-between px-5 py-4 border-b border-paper-200">
151
+ {/* Left side: Toggle + Icon + Title + Badge */}
152
+ <button
153
+ type="button"
154
+ onClick={handleToggle}
155
+ className="flex items-center gap-3 text-left flex-1 min-w-0 group"
156
+ aria-expanded={isOpen}
157
+ >
158
+ <ChevronDown
159
+ className={`
160
+ h-5 w-5 text-ink-400 transition-transform duration-200 flex-shrink-0
161
+ group-hover:text-ink-600
162
+ ${isOpen ? 'rotate-0' : '-rotate-90'}
163
+ `}
164
+ />
165
+
166
+ {icon && (
167
+ <span className="flex-shrink-0 text-ink-500">
168
+ {icon}
169
+ </span>
170
+ )}
171
+
172
+ <span className="font-medium text-ink-900 truncate">
173
+ {title}
174
+ </span>
175
+
176
+ {badge !== undefined && (
177
+ <Badge variant={badgeVariant} size="sm" pill>
178
+ {badge}
179
+ </Badge>
180
+ )}
181
+ </button>
182
+
183
+ {/* Right side: View All link */}
184
+ {(viewAllHref || onViewAll) && (
185
+ viewAllHref ? (
186
+ <Link
187
+ to={viewAllHref}
188
+ onClick={onViewAll ? handleViewAllClick : undefined}
189
+ className="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700 font-medium flex-shrink-0 ml-4"
190
+ >
191
+ {ViewAllContent}
192
+ </Link>
193
+ ) : (
194
+ <button
195
+ onClick={onViewAll}
196
+ className="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700 font-medium flex-shrink-0 ml-4"
197
+ >
198
+ {ViewAllContent}
199
+ </button>
200
+ )
201
+ )}
202
+ </div>
203
+
204
+ {/* Content */}
205
+ <div
206
+ ref={contentRef}
207
+ className="overflow-hidden transition-all duration-300 ease-in-out"
208
+ style={{ height: `${height}px` }}
209
+ aria-hidden={!isOpen}
210
+ >
211
+ <div className="p-5">
212
+ {children}
213
+ </div>
214
+ </div>
215
+ </div>
216
+ );
217
+ }
218
+
219
+ export default CollapsibleSection;
@@ -0,0 +1,200 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import HelpTooltip from './HelpTooltip';
3
+ import Input from './Input';
4
+ import Stack from './Stack';
5
+ import Text from './Text';
6
+
7
+ const meta = {
8
+ title: 'Feedback/HelpTooltip',
9
+ component: HelpTooltip,
10
+ parameters: {
11
+ layout: 'centered',
12
+ docs: {
13
+ description: {
14
+ component: `
15
+ A convenience component that combines a help icon with a tooltip for providing contextual help.
16
+
17
+ ## Features
18
+ - **Two icon variants**: HelpCircle (?) or Info (i) icon
19
+ - **Three sizes**: sm, md, lg
20
+ - **Accessible**: Includes proper ARIA labels and keyboard focus
21
+ - **Hover states**: Subtle color transition on hover
22
+
23
+ ## Usage
24
+
25
+ \`\`\`tsx
26
+ import { HelpTooltip, Text, Stack } from 'notebook-ui';
27
+
28
+ // Basic usage
29
+ <Stack direction="horizontal" gap="sm" align="center">
30
+ <Text weight="medium">Email</Text>
31
+ <HelpTooltip content="We'll never share your email" />
32
+ </Stack>
33
+
34
+ // With info icon variant
35
+ <HelpTooltip content="Additional information" icon="info" />
36
+
37
+ // Different sizes
38
+ <HelpTooltip content="Small help" size="sm" />
39
+ <HelpTooltip content="Large help" size="lg" />
40
+ \`\`\`
41
+ `,
42
+ },
43
+ },
44
+ },
45
+ tags: ['autodocs'],
46
+ argTypes: {
47
+ content: {
48
+ control: 'text',
49
+ description: 'The help text to display in the tooltip',
50
+ table: {
51
+ type: { summary: 'React.ReactNode' },
52
+ },
53
+ },
54
+ icon: {
55
+ control: 'select',
56
+ options: ['help', 'info'],
57
+ description: 'Icon variant to display',
58
+ table: {
59
+ type: { summary: 'help | info' },
60
+ defaultValue: { summary: 'help' },
61
+ },
62
+ },
63
+ size: {
64
+ control: 'select',
65
+ options: ['sm', 'md', 'lg'],
66
+ description: 'Size of the icon',
67
+ table: {
68
+ type: { summary: 'sm | md | lg' },
69
+ defaultValue: { summary: 'md' },
70
+ },
71
+ },
72
+ position: {
73
+ control: 'select',
74
+ options: ['top', 'bottom', 'left', 'right'],
75
+ description: 'Position of the tooltip relative to the icon',
76
+ table: {
77
+ type: { summary: 'top | bottom | left | right' },
78
+ defaultValue: { summary: 'top' },
79
+ },
80
+ },
81
+ },
82
+ } satisfies Meta<typeof HelpTooltip>;
83
+
84
+ export default meta;
85
+ type Story = StoryObj<typeof meta>;
86
+
87
+ export const Default: Story = {
88
+ args: {
89
+ content: 'This is helpful information',
90
+ },
91
+ };
92
+
93
+ export const HelpIcon: Story = {
94
+ args: {
95
+ content: 'Need help? This explains the feature.',
96
+ icon: 'help',
97
+ },
98
+ };
99
+
100
+ export const InfoIcon: Story = {
101
+ args: {
102
+ content: 'Additional information about this field.',
103
+ icon: 'info',
104
+ },
105
+ };
106
+
107
+ export const Sizes: Story = {
108
+ render: () => (
109
+ <Stack direction="horizontal" gap="lg" align="center">
110
+ <Stack direction="horizontal" gap="xs" align="center">
111
+ <Text size="sm">Small</Text>
112
+ <HelpTooltip content="Small size tooltip" size="sm" />
113
+ </Stack>
114
+ <Stack direction="horizontal" gap="xs" align="center">
115
+ <Text size="sm">Medium</Text>
116
+ <HelpTooltip content="Medium size tooltip" size="md" />
117
+ </Stack>
118
+ <Stack direction="horizontal" gap="xs" align="center">
119
+ <Text size="sm">Large</Text>
120
+ <HelpTooltip content="Large size tooltip" size="lg" />
121
+ </Stack>
122
+ </Stack>
123
+ ),
124
+ };
125
+
126
+ export const Positions: Story = {
127
+ render: () => (
128
+ <Stack direction="horizontal" gap="xl" align="center">
129
+ <Stack direction="horizontal" gap="xs" align="center">
130
+ <Text size="sm">Top</Text>
131
+ <HelpTooltip content="Tooltip on top" position="top" />
132
+ </Stack>
133
+ <Stack direction="horizontal" gap="xs" align="center">
134
+ <Text size="sm">Bottom</Text>
135
+ <HelpTooltip content="Tooltip on bottom" position="bottom" />
136
+ </Stack>
137
+ <Stack direction="horizontal" gap="xs" align="center">
138
+ <Text size="sm">Left</Text>
139
+ <HelpTooltip content="Tooltip on left" position="left" />
140
+ </Stack>
141
+ <Stack direction="horizontal" gap="xs" align="center">
142
+ <Text size="sm">Right</Text>
143
+ <HelpTooltip content="Tooltip on right" position="right" />
144
+ </Stack>
145
+ </Stack>
146
+ ),
147
+ };
148
+
149
+ export const WithFormField: Story = {
150
+ render: () => (
151
+ <Stack gap="md" style={{ width: '300px' }}>
152
+ <Stack gap="xs">
153
+ <Stack direction="horizontal" gap="xs" align="center">
154
+ <Text as="label" size="sm" weight="medium">
155
+ Email Address
156
+ </Text>
157
+ <HelpTooltip
158
+ content="We'll never share your email with anyone else"
159
+ size="sm"
160
+ />
161
+ </Stack>
162
+ <Input placeholder="you@example.com" />
163
+ </Stack>
164
+ <Stack gap="xs">
165
+ <Stack direction="horizontal" gap="xs" align="center">
166
+ <Text as="label" size="sm" weight="medium">
167
+ API Key
168
+ </Text>
169
+ <HelpTooltip
170
+ content="Your API key is used for authentication. Keep it secret!"
171
+ icon="info"
172
+ size="sm"
173
+ />
174
+ </Stack>
175
+ <Input type="password" placeholder="sk-..." />
176
+ </Stack>
177
+ </Stack>
178
+ ),
179
+ };
180
+
181
+ export const InlineWithText: Story = {
182
+ render: () => (
183
+ <Text>
184
+ This feature requires a subscription
185
+ <HelpTooltip
186
+ content="Premium subscriptions include unlimited access to all features"
187
+ size="sm"
188
+ className="ml-1"
189
+ />
190
+ </Text>
191
+ ),
192
+ };
193
+
194
+ export const LongContent: Story = {
195
+ args: {
196
+ content:
197
+ 'This is a longer help message that provides more detailed information about the feature. It can span multiple lines if needed.',
198
+ position: 'right',
199
+ },
200
+ };
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { HelpCircle, Info } from 'lucide-react';
3
+ import Tooltip from './Tooltip';
4
+
5
+ export interface HelpTooltipProps {
6
+ /** The help text to display in the tooltip */
7
+ content: React.ReactNode;
8
+ /** Icon variant to display */
9
+ icon?: 'help' | 'info';
10
+ /** Size of the icon */
11
+ size?: 'sm' | 'md' | 'lg';
12
+ /** Position of the tooltip relative to the icon */
13
+ position?: 'top' | 'bottom' | 'left' | 'right';
14
+ /** Additional CSS classes for the icon container */
15
+ className?: string;
16
+ }
17
+
18
+ const sizeClasses = {
19
+ sm: 'h-3.5 w-3.5',
20
+ md: 'h-4 w-4',
21
+ lg: 'h-5 w-5',
22
+ };
23
+
24
+ export default function HelpTooltip({
25
+ content,
26
+ icon = 'help',
27
+ size = 'md',
28
+ position = 'top',
29
+ className = '',
30
+ }: HelpTooltipProps) {
31
+ const IconComponent = icon === 'info' ? Info : HelpCircle;
32
+
33
+ return (
34
+ <Tooltip content={content} position={position}>
35
+ <span
36
+ className={`inline-flex items-center justify-center text-ink-400 hover:text-ink-600 cursor-help transition-colors ${className}`}
37
+ role="button"
38
+ aria-label="Help"
39
+ tabIndex={0}
40
+ >
41
+ <IconComponent className={sizeClasses[size]} />
42
+ </span>
43
+ </Tooltip>
44
+ );
45
+ }