lupine.components 1.1.22 → 1.1.24

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.
Files changed (162) hide show
  1. package/package.json +1 -1
  2. package/src/component-pool/aspect-ratio/aspect-ratio-demo.tsx +103 -0
  3. package/src/component-pool/aspect-ratio/aspect-ratio.tsx +41 -0
  4. package/src/component-pool/aspect-ratio/index.ts +2 -0
  5. package/src/component-pool/avatar/avatar-demo.tsx +127 -0
  6. package/src/component-pool/avatar/avatar.tsx +183 -0
  7. package/src/component-pool/avatar/index.ts +2 -0
  8. package/src/component-pool/badge/badge-demo.tsx +121 -0
  9. package/src/component-pool/badge/badge.tsx +103 -0
  10. package/src/component-pool/badge/index.ts +2 -0
  11. package/src/component-pool/breadcrumbs/breadcrumbs-demo.tsx +91 -0
  12. package/src/component-pool/breadcrumbs/breadcrumbs.tsx +209 -0
  13. package/src/component-pool/breadcrumbs/index.ts +2 -0
  14. package/src/component-pool/card/card-demo.tsx +178 -0
  15. package/src/component-pool/card/card.tsx +150 -0
  16. package/src/component-pool/card/index.ts +2 -0
  17. package/src/component-pool/carousel/carousel-demo.tsx +191 -0
  18. package/src/component-pool/carousel/carousel.tsx +225 -0
  19. package/src/component-pool/carousel/index.ts +2 -0
  20. package/src/component-pool/cascader/cascader-demo.tsx +127 -0
  21. package/src/component-pool/cascader/cascader.tsx +206 -0
  22. package/src/component-pool/cascader/index.ts +2 -0
  23. package/src/component-pool/copy-button/copy-button-demo.tsx +71 -0
  24. package/src/component-pool/copy-button/copy-button.tsx +146 -0
  25. package/src/component-pool/copy-button/index.ts +2 -0
  26. package/src/component-pool/date-picker/date-picker-demo.tsx +84 -0
  27. package/src/component-pool/date-picker/date-picker.tsx +433 -0
  28. package/src/component-pool/date-picker/index.ts +2 -0
  29. package/src/component-pool/floating-icon-menu/floating-icon-menu-demo.tsx +94 -0
  30. package/src/component-pool/floating-icon-menu/floating-icon-menu.tsx +223 -0
  31. package/src/component-pool/floating-icon-menu/index.ts +2 -0
  32. package/src/component-pool/h-editor/h-editor-demo.tsx +112 -0
  33. package/src/{html-editor → component-pool/h-editor}/h-editor.ts +89 -44
  34. package/src/component-pool/h-editor/index.ts +2 -0
  35. package/src/component-pool/i-editor/i-editor-demo.tsx +71 -0
  36. package/src/component-pool/i-editor/i-editor-drawing.ts +292 -0
  37. package/src/component-pool/i-editor/i-editor-frames.ts +42 -0
  38. package/src/component-pool/i-editor/i-editor-geometry.ts +139 -0
  39. package/src/component-pool/i-editor/i-editor-icons.ts +30 -0
  40. package/src/component-pool/i-editor/i-editor-image.ts +110 -0
  41. package/src/component-pool/i-editor/i-editor-styles.ts +22 -0
  42. package/src/component-pool/i-editor/i-editor-types.ts +87 -0
  43. package/src/component-pool/i-editor/i-editor.ts +3431 -0
  44. package/src/component-pool/i-editor/index.ts +2 -0
  45. package/src/component-pool/index.ts +27 -0
  46. package/src/component-pool/map-wrapper/index.ts +2 -0
  47. package/src/component-pool/map-wrapper/map-wrapper-demo.tsx +55 -0
  48. package/src/component-pool/map-wrapper/map-wrapper.tsx +83 -0
  49. package/src/component-pool/p-editor/index.ts +4 -0
  50. package/src/component-pool/p-editor/p-editor-demo.tsx +51 -0
  51. package/src/component-pool/p-editor/p-editor-styles.ts +44 -0
  52. package/src/component-pool/p-editor/p-editor-types.ts +52 -0
  53. package/src/component-pool/p-editor/p-editor-utils.ts +38 -0
  54. package/src/component-pool/p-editor/p-editor.ts +1368 -0
  55. package/src/component-pool/pdf-viewer/index.ts +2 -0
  56. package/src/component-pool/pdf-viewer/pdf-viewer-demo.tsx +116 -0
  57. package/src/component-pool/pdf-viewer/pdf-viewer.tsx +56 -0
  58. package/src/component-pool/picker-helper.tsx +191 -0
  59. package/src/component-pool/pull-to-refresh/index.ts +1 -0
  60. package/src/component-pool/pull-to-refresh/pull-to-refresh-demo.tsx +99 -0
  61. package/src/component-pool/pull-to-refresh/pull-to-refresh.tsx +235 -0
  62. package/src/component-pool/qrcode/index.ts +3 -0
  63. package/src/component-pool/qrcode/qrcode-algorithm.ts +927 -0
  64. package/src/component-pool/qrcode/qrcode-demo.tsx +102 -0
  65. package/src/component-pool/qrcode/qrcode.tsx +169 -0
  66. package/src/component-pool/radial-progress/index.ts +2 -0
  67. package/src/component-pool/radial-progress/radial-progress-demo.tsx +85 -0
  68. package/src/component-pool/radial-progress/radial-progress.tsx +118 -0
  69. package/src/component-pool/range/gauge-demo.tsx +113 -0
  70. package/src/component-pool/range/gauge.tsx +392 -0
  71. package/src/component-pool/range/index.ts +4 -0
  72. package/src/component-pool/range/range-demo.tsx +97 -0
  73. package/src/component-pool/range/range.tsx +432 -0
  74. package/src/component-pool/search-input/index.ts +2 -0
  75. package/src/component-pool/search-input/search-input-demo.tsx +50 -0
  76. package/src/component-pool/search-input/search-input.tsx +123 -0
  77. package/src/component-pool/skeleton/index.ts +3 -0
  78. package/src/component-pool/skeleton/skeleton-card.tsx +114 -0
  79. package/src/component-pool/skeleton/skeleton-demo.tsx +55 -0
  80. package/src/component-pool/skeleton/skeleton.tsx +61 -0
  81. package/src/component-pool/svg-icons.ts +93 -0
  82. package/src/component-pool/svg-props.tsx +144 -0
  83. package/src/component-pool/tag-input/index.ts +2 -0
  84. package/src/component-pool/tag-input/tag-input-demo.tsx +47 -0
  85. package/src/component-pool/tag-input/tag-input.tsx +171 -0
  86. package/src/component-pool/time-picker/index.ts +2 -0
  87. package/src/component-pool/time-picker/time-picker-demo.tsx +80 -0
  88. package/src/component-pool/time-picker/time-picker.tsx +311 -0
  89. package/src/component-pool/timeline/index.ts +2 -0
  90. package/src/component-pool/timeline/timeline-demo.tsx +75 -0
  91. package/src/component-pool/timeline/timeline.tsx +156 -0
  92. package/src/component-pool/tooltip/index.ts +2 -0
  93. package/src/component-pool/tooltip/tooltip-demo.tsx +144 -0
  94. package/src/component-pool/tooltip/tooltip.tsx +241 -0
  95. package/src/component-pool/tour/index.ts +2 -0
  96. package/src/component-pool/tour/tour-demo.tsx +78 -0
  97. package/src/component-pool/tour/tour.tsx +323 -0
  98. package/src/component-pool/youtube-player/index.ts +2 -0
  99. package/src/component-pool/youtube-player/youtube-player-demo.tsx +57 -0
  100. package/src/component-pool/youtube-player/youtube-player.tsx +40 -0
  101. package/src/components/action-sheet-color.tsx +269 -0
  102. package/src/components/action-sheet-date.tsx +186 -0
  103. package/src/components/action-sheet-demo.tsx +80 -0
  104. package/src/components/action-sheet-time.tsx +157 -0
  105. package/src/components/action-sheet.tsx +159 -9
  106. package/src/components/button-demo.tsx +22 -1
  107. package/src/components/button-push-animation-demo.tsx +16 -1
  108. package/src/components/desktop-header.tsx +16 -8
  109. package/src/components/editable-label-demo.tsx +12 -1
  110. package/src/components/float-window.tsx +1 -1
  111. package/src/components/input-number-demo.tsx +54 -0
  112. package/src/components/input-number.tsx +175 -0
  113. package/src/components/input-with-title-demo.tsx +11 -0
  114. package/src/components/message-box-demo.tsx +27 -0
  115. package/src/components/mobile-components/icon-menu-item-props.ts +2 -1
  116. package/src/components/mobile-components/mobile-footer-menu.tsx +25 -15
  117. package/src/components/mobile-components/mobile-header-title-icon.tsx +1 -1
  118. package/src/components/mobile-components/mobile-header-with-back.tsx +1 -1
  119. package/src/components/mobile-components/mobile-side-menu-demo.tsx +57 -0
  120. package/src/components/mobile-components/mobile-side-menu.tsx +110 -31
  121. package/src/components/mobile-components/mobile-top-sys-icon.tsx +8 -0
  122. package/src/components/modal-demo.tsx +12 -0
  123. package/src/components/notice-message-demo.tsx +8 -0
  124. package/src/components/paging-link-demo.tsx +446 -0
  125. package/src/components/popup-menu-demo.tsx +19 -0
  126. package/src/components/progress-demo.tsx +33 -31
  127. package/src/components/radio-label-demo.tsx +14 -0
  128. package/src/components/redirect-demo.tsx +8 -0
  129. package/src/components/resizable-splitter-demo.tsx +8 -0
  130. package/src/components/select-angle-component.tsx +7 -4
  131. package/src/components/select-angle-demo.tsx +8 -0
  132. package/src/components/select-with-title-demo.tsx +17 -0
  133. package/src/components/slide-tab-component-demo.tsx +86 -0
  134. package/src/components/slider-frame-demo.tsx +87 -0
  135. package/src/components/spinner-demo.tsx +11 -0
  136. package/src/components/stars-component-demo.tsx +10 -0
  137. package/src/components/stars-component.tsx +62 -21
  138. package/src/components/switch-option-demo.tsx +13 -3
  139. package/src/components/tabs-demo.tsx +12 -0
  140. package/src/components/text-glow-demo.tsx +10 -0
  141. package/src/components/text-scale-demo.tsx +11 -0
  142. package/src/components/text-wave-demo.tsx +10 -0
  143. package/src/components/toggle-button-demo.tsx +10 -0
  144. package/src/components/toggle-play-button-demo.tsx +12 -0
  145. package/src/components/toggle-switch-demo.tsx +11 -1
  146. package/src/demo/demo-container.tsx +4 -2
  147. package/src/demo/demo-frame-helper.tsx +328 -93
  148. package/src/demo/demo-index.tsx +6 -3
  149. package/src/demo/demo-page.tsx +142 -9
  150. package/src/demo/demo-registry.ts +67 -0
  151. package/src/demo/demo-render-page.tsx +40 -8
  152. package/src/demo/demo-types.ts +2 -1
  153. package/src/demo/index.ts +10 -0
  154. package/src/demo/mock/demo-icons.ts +100 -0
  155. package/src/demo/mock/responsive-demo-mock-pages.tsx +185 -0
  156. package/src/demo/mock/side-menu-mock.tsx +120 -0
  157. package/src/demo/mock/user-settings-mock.tsx +312 -0
  158. package/src/frames/responsive-frame-demo.tsx +120 -0
  159. package/src/frames/responsive-frame.tsx +2 -2
  160. package/src/index.ts +2 -2
  161. package/tsconfig.json +113 -113
  162. package/src/html-editor/buttons_morden.gif +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.components",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -0,0 +1,103 @@
1
+ import { DemoStory } from '../../demo/demo-types';
2
+ import { AspectRatio, AspectRatioProps } from './aspect-ratio';
3
+
4
+ export const aspectRatioDemo: DemoStory<AspectRatioProps> = {
5
+ id: 'aspect-ratio',
6
+ text: 'AspectRatio',
7
+ args: {
8
+ ratio: 16 / 9,
9
+ children: <div />,
10
+ },
11
+ argTypes: {
12
+ ratio: {
13
+ control: 'select',
14
+ options: [16 / 9, 4 / 3, 1, 9 / 16],
15
+ description: 'Aspect ratio (width / height)',
16
+ },
17
+ },
18
+ render: (args) => {
19
+ return (
20
+ <div style={{ padding: '40px', display: 'flex', flexDirection: 'column', gap: '40px', maxWidth: '600px' }}>
21
+ <div>
22
+ <h3 style={{ marginBottom: '8px' }}>16 / 9 (Video)</h3>
23
+ <AspectRatio ratio={16 / 9}>
24
+ <div
25
+ style={{
26
+ width: '100%',
27
+ height: '100%',
28
+ backgroundColor: 'var(--secondary-bg-color, #e5e7eb)',
29
+ borderRadius: '8px',
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ justifyContent: 'center',
33
+ color: 'var(--secondary-color, #6b7280)',
34
+ fontSize: '14px',
35
+ }}
36
+ >
37
+ 16:9 Content
38
+ </div>
39
+ </AspectRatio>
40
+ </div>
41
+
42
+ <div>
43
+ <h3 style={{ marginBottom: '8px' }}>4 / 3 (Classic Photo)</h3>
44
+ <AspectRatio ratio={4 / 3}>
45
+ <div
46
+ style={{
47
+ width: '100%',
48
+ height: '100%',
49
+ backgroundColor: 'var(--secondary-bg-color, #e5e7eb)',
50
+ borderRadius: '8px',
51
+ display: 'flex',
52
+ alignItems: 'center',
53
+ justifyContent: 'center',
54
+ color: 'var(--secondary-color, #6b7280)',
55
+ fontSize: '14px',
56
+ }}
57
+ >
58
+ 4:3 Content
59
+ </div>
60
+ </AspectRatio>
61
+ </div>
62
+
63
+ <div>
64
+ <h3 style={{ marginBottom: '8px' }}>1 / 1 (Square)</h3>
65
+ <AspectRatio ratio={1}>
66
+ <div
67
+ style={{
68
+ width: '100%',
69
+ height: '100%',
70
+ backgroundColor: 'var(--secondary-bg-color, #e5e7eb)',
71
+ borderRadius: '8px',
72
+ display: 'flex',
73
+ alignItems: 'center',
74
+ justifyContent: 'center',
75
+ color: 'var(--secondary-color, #6b7280)',
76
+ fontSize: '14px',
77
+ }}
78
+ >
79
+ 1:1 Content
80
+ </div>
81
+ </AspectRatio>
82
+ </div>
83
+ </div>
84
+ );
85
+ },
86
+ code: `import { AspectRatio } from 'lupine.components/component-pool';
87
+
88
+ // Video embed (16:9)
89
+ <AspectRatio ratio={16 / 9}>
90
+ <iframe src="..." style={{ width: '100%', height: '100%', border: 'none' }} />
91
+ </AspectRatio>
92
+
93
+ // Image (4:3)
94
+ <AspectRatio ratio={4 / 3}>
95
+ <img src="..." style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
96
+ </AspectRatio>
97
+
98
+ // Square
99
+ <AspectRatio ratio={1}>
100
+ <img src="..." style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
101
+ </AspectRatio>
102
+ `,
103
+ };
@@ -0,0 +1,41 @@
1
+ import { bindGlobalStyle, getGlobalStylesId, CssProps, RefProps, VNode } from 'lupine.web';
2
+
3
+ export type AspectRatioProps = {
4
+ class?: string;
5
+ style?: CssProps;
6
+ children: VNode<any>;
7
+ ratio?: number; // width/height ratio, e.g. 16/9, 4/3, 1. Defaults to 16/9
8
+ };
9
+
10
+ export const AspectRatio = (props: AspectRatioProps) => {
11
+ const css: CssProps = {
12
+ position: 'relative',
13
+ width: '100%',
14
+ overflow: 'hidden',
15
+
16
+ '.&-inner': {
17
+ position: 'absolute',
18
+ inset: 0,
19
+ width: '100%',
20
+ height: '100%',
21
+ },
22
+ };
23
+
24
+ const globalCssId = getGlobalStylesId(css);
25
+ bindGlobalStyle(globalCssId, css);
26
+
27
+ const ref: RefProps = {
28
+ globalCssId,
29
+ };
30
+
31
+ const ratio = props.ratio ?? 16 / 9;
32
+ // Use padding-top trick for broad browser support
33
+ // paddingTop = (1 / ratio) * 100%
34
+ const paddingTop = `${(1 / ratio) * 100}%`;
35
+
36
+ return (
37
+ <div class={['&-container', props.class].join(' ').trim()} ref={ref} css={{ paddingTop, ...props.style }}>
38
+ <div class='&-inner'>{props.children}</div>
39
+ </div>
40
+ );
41
+ };
@@ -0,0 +1,2 @@
1
+ export * from './aspect-ratio';
2
+ export * from './aspect-ratio-demo';
@@ -0,0 +1,127 @@
1
+ import { Avatar, AvatarGroup } from './avatar';
2
+ import { DemoStory } from '../../demo/demo-types';
3
+
4
+ export const avatarDemo: DemoStory<any> = {
5
+ id: 'avatarDemo',
6
+ text: 'Avatar',
7
+ args: {
8
+ maxCount: 3,
9
+ size: 'md',
10
+ shape: 'circle',
11
+ },
12
+ argTypes: {
13
+ maxCount: {
14
+ control: 'number',
15
+ description: 'Maximum visible avatars in a group',
16
+ },
17
+ size: {
18
+ control: 'select',
19
+ options: ['xs', 'sm', 'md', 'lg', 'xl'],
20
+ description: 'Size of the avatars',
21
+ },
22
+ shape: {
23
+ control: 'select',
24
+ options: ['circle', 'rounded', 'square'],
25
+ description: 'Shape of the avatars',
26
+ },
27
+ },
28
+ render: (args: any) => {
29
+ const css = {
30
+ display: 'flex',
31
+ flexDirection: 'column',
32
+ gap: '24px',
33
+ padding: '24px',
34
+ '.section-title': {
35
+ fontSize: '18px',
36
+ fontWeight: 'bold',
37
+ marginBottom: '12px',
38
+ color: 'var(--primary-color)',
39
+ },
40
+ '.demo-row': {
41
+ display: 'flex',
42
+ flexWrap: 'wrap',
43
+ gap: '16px',
44
+ alignItems: 'center',
45
+ },
46
+ };
47
+
48
+ return (
49
+ <div css={css}>
50
+ {/* Controllable Standalone */}
51
+ <section>
52
+ <div class='section-title'>
53
+ Controlled by Panel (Size: {args.size}, Shape: {args.shape})
54
+ </div>
55
+ <div class='demo-row'>
56
+ <Avatar size={args.size} shape={args.shape} src='https://i.pravatar.cc/150?u=99' online />
57
+ <Avatar size={args.size} shape={args.shape} initials='AD' />
58
+ </div>
59
+ </section>
60
+
61
+ {/* Sizes */}
62
+ <section>
63
+ <div class='section-title'>Sizes (Static)</div>
64
+ <div class='demo-row'>
65
+ <Avatar size='xs' initials='XS' />
66
+ <Avatar size='sm' initials='SM' />
67
+ <Avatar size='md' initials='MD' />
68
+ <Avatar size='lg' initials='LG' />
69
+ <Avatar size='xl' initials='XL' />
70
+ </div>
71
+ </section>
72
+
73
+ {/* Shapes */}
74
+ <section>
75
+ <div class='section-title'>Shapes (Static)</div>
76
+ <div class='demo-row'>
77
+ <Avatar shape='circle' initials='C' />
78
+ <Avatar shape='rounded' initials='R' />
79
+ <Avatar shape='square' initials='S' />
80
+ </div>
81
+ </section>
82
+
83
+ {/* Status */}
84
+ <section>
85
+ <div class='section-title'>Status Indicators</div>
86
+ <div class='demo-row'>
87
+ <Avatar size='md' src='https://i.pravatar.cc/150?u=3' status='online' />
88
+ <Avatar size='md' src='https://i.pravatar.cc/150?u=4' status='away' />
89
+ <Avatar size='md' src='https://i.pravatar.cc/150?u=5' status='busy' />
90
+ <Avatar size='md' src='https://i.pravatar.cc/150?u=6' status='offline' />
91
+ </div>
92
+ </section>
93
+
94
+ {/* Group */}
95
+ <section>
96
+ <div class='section-title'>
97
+ Avatar Group (maxCount: {args.maxCount}, size: {args.size})
98
+ </div>
99
+ <div class='demo-row'>
100
+ <AvatarGroup maxCount={args.maxCount} size={args.size}>
101
+ <Avatar src='https://i.pravatar.cc/150?u=7' />
102
+ <Avatar src='https://i.pravatar.cc/150?u=8' />
103
+ <Avatar src='https://i.pravatar.cc/150?u=9' />
104
+ <Avatar src='https://i.pravatar.cc/150?u=10' />
105
+ <Avatar src='https://i.pravatar.cc/150?u=11' />
106
+ <Avatar src='https://i.pravatar.cc/150?u=12' />
107
+ </AvatarGroup>
108
+ </div>
109
+ </section>
110
+ </div>
111
+ );
112
+ },
113
+ code: `import { Avatar, AvatarGroup } from 'lupine.components/component-pool';
114
+
115
+ // Standalone
116
+ <Avatar size="md" shape="circle" src="..." status="online" />
117
+ <Avatar size="lg" initials="AD" />
118
+
119
+ // Group
120
+ <AvatarGroup maxCount={3}>
121
+ <Avatar src="..." />
122
+ <Avatar src="..." />
123
+ <Avatar src="..." />
124
+ <Avatar src="..." />
125
+ </AvatarGroup>
126
+ `,
127
+ };
@@ -0,0 +1,183 @@
1
+ import { bindGlobalStyle, getGlobalStylesId, CssProps, RefProps } from 'lupine.web';
2
+ import { HtmlVar } from 'lupine.components';
3
+
4
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
5
+ export type AvatarShape = 'circle' | 'rounded' | 'square';
6
+ export type AvatarStatus = 'online' | 'offline' | 'busy' | 'away';
7
+
8
+ export type AvatarProps = {
9
+ class?: string;
10
+ style?: CssProps;
11
+ src?: string;
12
+ initials?: string;
13
+ size?: AvatarSize;
14
+ shape?: AvatarShape;
15
+ status?: AvatarStatus;
16
+ online?: boolean; // Shortcut for status="online"
17
+ [key: string]: any;
18
+ };
19
+
20
+ const avatarCss: CssProps = {
21
+ display: 'inline-block',
22
+ position: 'relative',
23
+ verticalAlign: 'middle',
24
+
25
+ // Inner wrapper for content clipping/border
26
+ '.&-content': {
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ width: '100%',
31
+ height: '100%',
32
+ backgroundColor: 'var(--secondary-bg-color, #f0f0f0)',
33
+ color: 'var(--secondary-color, #666)',
34
+ overflow: 'hidden',
35
+ border: '1px solid var(--secondary-border-color, #ddd)',
36
+ fontSize: '14px',
37
+ fontWeight: '500',
38
+ transition: 'all 0.2s ease',
39
+ },
40
+
41
+ '&.shape-circle .&-content': { borderRadius: '50%' },
42
+ '&.shape-rounded .&-content': { borderRadius: 'var(--border-radius-m, 8px)' },
43
+ '&.shape-square .&-content': { borderRadius: '0' },
44
+
45
+ '&.size-xs': { width: '24px', height: '24px' },
46
+ '&.size-sm': { width: '32px', height: '32px' },
47
+ '&.size-md': { width: '40px', height: '40px' },
48
+ '&.size-lg': { width: '48px', height: '48px' },
49
+ '&.size-xl': { width: '64px', height: '64px' },
50
+
51
+ '&.size-xs .&-content': { fontSize: '10px' },
52
+ '&.size-xl .&-content': { fontSize: '20px' },
53
+
54
+ '.&-img': {
55
+ width: '100%',
56
+ height: '100%',
57
+ objectFit: 'cover',
58
+ },
59
+
60
+ '.&-status': {
61
+ position: 'absolute',
62
+ bottom: '0',
63
+ right: '0',
64
+ width: '25%',
65
+ height: '25%',
66
+ minWidth: '8px',
67
+ minHeight: '8px',
68
+ borderRadius: '50%',
69
+ border: '2px solid var(--primary-bg-color, #fff)',
70
+ backgroundColor: '#999',
71
+ zIndex: 1,
72
+ },
73
+
74
+ '.&-status.status-online': { backgroundColor: 'var(--success-color, #2ecc71)' },
75
+ '.&-status.status-offline': { backgroundColor: 'var(--secondary-border-color, #999)' },
76
+ '.&-status.status-busy': { backgroundColor: 'var(--error-color, #e74c3c)' },
77
+ '.&-status.status-away': { backgroundColor: 'var(--warning-color, #f39c12)' },
78
+ };
79
+
80
+ const groupCss: CssProps = {
81
+ display: 'flex',
82
+ flexDirection: 'row',
83
+ alignItems: 'center',
84
+
85
+ '.&-item': {
86
+ marginLeft: '-12px',
87
+ border: '2px solid var(--primary-bg-color, #fff)',
88
+ borderRadius: '50%',
89
+ zIndex: 0,
90
+ },
91
+ '.&-item:first-child': {
92
+ marginLeft: '0',
93
+ },
94
+ '.&-item:hover': {
95
+ zIndex: 1,
96
+ },
97
+ };
98
+
99
+ export const Avatar = (props: AvatarProps) => {
100
+ const size = props.size || 'md';
101
+ const shape = props.shape || 'circle';
102
+ const status = props.online ? 'online' : props.status;
103
+
104
+ const globalCssId = getGlobalStylesId(avatarCss);
105
+ bindGlobalStyle(globalCssId, avatarCss);
106
+
107
+ const renderInitials = () => <span>{props.initials || '?'}</span>;
108
+
109
+ const renderContent = () => {
110
+ if (props.src) {
111
+ return (
112
+ <img
113
+ class='&-img'
114
+ src={props.src}
115
+ onError={() => {
116
+ avatarContent.value = renderInitials();
117
+ }}
118
+ />
119
+ );
120
+ }
121
+ return renderInitials();
122
+ };
123
+
124
+ // Use HtmlVar to hold the actual renderable content
125
+ const avatarContent = new HtmlVar(renderContent());
126
+
127
+ const ref: RefProps = {
128
+ globalCssId,
129
+ };
130
+
131
+ return (
132
+ <div class={[`size-${size}`, `shape-${shape}`, props.class].join(' ').trim()} ref={ref} css={props.style}>
133
+ <div class='&-content'>{avatarContent.node}</div>
134
+ {status && <div class={['&-status', `status-${status}`].join(' ')}></div>}
135
+ </div>
136
+ );
137
+ };
138
+
139
+ export type AvatarGroupProps = {
140
+ class?: string;
141
+ style?: CssProps;
142
+ children?: any;
143
+ maxCount?: number;
144
+ size?: AvatarSize;
145
+ };
146
+
147
+ export const AvatarGroup = (props: AvatarGroupProps) => {
148
+ const globalCssId = getGlobalStylesId(groupCss);
149
+ bindGlobalStyle(globalCssId, groupCss);
150
+
151
+ const ref: RefProps = {
152
+ globalCssId,
153
+ };
154
+
155
+ const children = Array.isArray(props.children) ? props.children : [props.children];
156
+ const max = props.maxCount || children.length;
157
+ const visibleItems = children.slice(0, max);
158
+ const remaining = children.length - max;
159
+
160
+ const items = visibleItems.map((child: any) => {
161
+ if (child && typeof child === 'object' && child.props) {
162
+ if (props.size && !child.props.size) child.props.size = props.size;
163
+ child.props.class = [child.props.class, '&-item'].join(' ').trim();
164
+ }
165
+ return child;
166
+ });
167
+
168
+ if (remaining > 0) {
169
+ items.push(
170
+ <Avatar
171
+ class='&-item'
172
+ size={props.size || (visibleItems[0]?.props?.size as AvatarSize) || 'md'}
173
+ initials={`+${remaining}`}
174
+ />
175
+ );
176
+ }
177
+
178
+ return (
179
+ <div ref={ref} class={['&-group', props.class].join(' ').trim()} css={props.style}>
180
+ {items}
181
+ </div>
182
+ );
183
+ };
@@ -0,0 +1,2 @@
1
+ export * from './avatar';
2
+ export * from './avatar-demo';
@@ -0,0 +1,121 @@
1
+ import { DemoStory } from '../../demo/demo-types';
2
+ import { Badge, BadgeProps } from './badge';
3
+ import { SvgSvg, SvgPath } from '../svg-props';
4
+
5
+ const BellIcon = () => (
6
+ <SvgSvg
7
+ width='24'
8
+ height='24'
9
+ viewBox='0 0 24 24'
10
+ fill='none'
11
+ stroke='currentColor'
12
+ stroke-width='2'
13
+ stroke-linecap='round'
14
+ stroke-linejoin='round'
15
+ >
16
+ <SvgPath d='M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9' />
17
+ <SvgPath d='M13.73 21a2 2 0 0 1-3.46 0' />
18
+ </SvgSvg>
19
+ );
20
+
21
+ const CheckIcon = () => (
22
+ <SvgSvg
23
+ width='12'
24
+ height='12'
25
+ viewBox='0 0 24 24'
26
+ fill='none'
27
+ stroke='currentColor'
28
+ stroke-width='3'
29
+ stroke-linecap='round'
30
+ stroke-linejoin='round'
31
+ >
32
+ <SvgPath d='M20 6L9 17l-5-5' />
33
+ </SvgSvg>
34
+ );
35
+
36
+ export const badgeDemo: DemoStory<BadgeProps> = {
37
+ id: 'badge',
38
+ text: 'Badge',
39
+ args: {
40
+ content: 5,
41
+ max: 99,
42
+ dot: false,
43
+ color: '#ffffff',
44
+ bgColor: '#e74c3c',
45
+ },
46
+ argTypes: {
47
+ content: { control: 'number', description: 'Number or text to display' },
48
+ max: { control: 'number', description: 'Max number before showing +' },
49
+ dot: { control: 'boolean', description: 'Show as dot instead of value' },
50
+ color: { control: 'color', description: 'Text color' },
51
+ bgColor: { control: 'color', description: 'Background color' },
52
+ },
53
+ render: (args) => {
54
+ return (
55
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '30px', padding: '20px' }}>
56
+ <div style={{ display: 'flex', gap: '40px', alignItems: 'center', flexWrap: 'wrap' }}>
57
+ <div style={{ textAlign: 'center' }}>
58
+ <div style={{ marginBottom: '16px' }}>Interactive Config</div>
59
+ <Badge
60
+ content={args.content}
61
+ max={args.max}
62
+ dot={args.dot}
63
+ color={args.color}
64
+ bgColor={args.bgColor}
65
+ onClick={() => alert(`Badge clicked: ${args.content}`)}
66
+ >
67
+ <div style={{ padding: '8px 16px', backgroundColor: '#f0f0f0', borderRadius: '4px' }}>Messages</div>
68
+ </Badge>
69
+ </div>
70
+
71
+ <div style={{ textAlign: 'center' }}>
72
+ <div style={{ marginBottom: '16px' }}>Max Value (99+)</div>
73
+ <Badge content={150} max={99}>
74
+ <div style={{ padding: '8px 16px', backgroundColor: '#f0f0f0', borderRadius: '4px' }}>Notifications</div>
75
+ </Badge>
76
+ </div>
77
+
78
+ <div style={{ textAlign: 'center' }}>
79
+ <div style={{ marginBottom: '16px' }}>Dot Mode</div>
80
+ <Badge dot={true}>
81
+ <BellIcon />
82
+ </Badge>
83
+ </div>
84
+
85
+ <div style={{ textAlign: 'center' }}>
86
+ <div style={{ marginBottom: '16px' }}>Custom Node Content</div>
87
+ <Badge content={<CheckIcon />} bgColor='var(--success-color, #2ecc71)'>
88
+ <div style={{ width: '40px', height: '40px', backgroundColor: '#eee', borderRadius: '4px' }} />
89
+ </Badge>
90
+ </div>
91
+
92
+ <div style={{ textAlign: 'center' }}>
93
+ <div style={{ marginBottom: '16px' }}>Text Content</div>
94
+ <Badge content='NEW' bgColor='var(--primary-accent-color, #0a74c9)'>
95
+ <div style={{ padding: '8px 16px', backgroundColor: '#f0f0f0', borderRadius: '4px' }}>Features</div>
96
+ </Badge>
97
+ </div>
98
+
99
+ <div style={{ textAlign: 'center' }}>
100
+ <div style={{ marginBottom: '16px' }}>Standalone Badge</div>
101
+ <Badge content='Status: Online' bgColor='var(--success-color, #2ecc71)' />
102
+ </div>
103
+ </div>
104
+ </div>
105
+ );
106
+ },
107
+ code: `import { Badge } from 'lupine.components/component-pool';
108
+
109
+ <Badge content={5} max={99} bgColor="red" color="white" onClick={...}>
110
+ <div>Anchor Element</div>
111
+ </Badge>
112
+
113
+ <Badge dot={true}>
114
+ <Icon />
115
+ </Badge>
116
+
117
+ <Badge content={<CheckIcon />} bgColor="green">
118
+ <div>Task</div>
119
+ </Badge>
120
+ `,
121
+ };
@@ -0,0 +1,103 @@
1
+ import { bindGlobalStyle, getGlobalStylesId, CssProps, RefProps, VNode } from 'lupine.web';
2
+
3
+ export type BadgeProps = {
4
+ class?: string;
5
+ style?: CssProps;
6
+ children?: any;
7
+
8
+ content?: number | string | VNode<any>;
9
+ max?: number;
10
+ dot?: boolean;
11
+ color?: string;
12
+ bgColor?: string;
13
+ onClick?: (e: MouseEvent) => void;
14
+ };
15
+
16
+ const badgeCss: CssProps = {
17
+ display: 'inline-flex',
18
+ position: 'relative',
19
+ verticalAlign: 'middle',
20
+
21
+ '.&-bubble': {
22
+ position: 'absolute',
23
+ top: 0,
24
+ right: 0,
25
+ transform: 'translate(50%, -50%)',
26
+ transformOrigin: '100% 0%',
27
+
28
+ display: 'inline-flex',
29
+ alignItems: 'center',
30
+ justifyContent: 'center',
31
+
32
+ padding: '0 6px',
33
+ height: '20px',
34
+ minWidth: '20px',
35
+ borderRadius: '10px',
36
+ fontSize: '12px',
37
+ fontWeight: 'bold',
38
+ lineHeight: '1',
39
+ whiteSpace: 'nowrap',
40
+
41
+ backgroundColor: 'var(--danger-color, #e74c3c)',
42
+ color: '#ffffff',
43
+ boxShadow: '0 0 0 1px var(--primary-bg-color, #ffffff)',
44
+
45
+ zIndex: 10,
46
+ },
47
+
48
+ '.&-bubble.is-dot': {
49
+ padding: 0,
50
+ height: '8px',
51
+ minWidth: '8px',
52
+ width: '8px',
53
+ borderRadius: '50%',
54
+ },
55
+
56
+ '.&-bubble.is-standalone': {
57
+ position: 'relative',
58
+ transform: 'none',
59
+ },
60
+ };
61
+
62
+ export const Badge = (props: BadgeProps) => {
63
+ const globalCssId = getGlobalStylesId(badgeCss);
64
+ bindGlobalStyle(globalCssId, badgeCss);
65
+
66
+ const ref: RefProps = {
67
+ globalCssId,
68
+ };
69
+
70
+ let displayContent = props.content;
71
+
72
+ // Handle max value for numbers
73
+ if (typeof displayContent === 'number' && props.max && displayContent > props.max) {
74
+ displayContent = `${props.max}+`;
75
+ }
76
+
77
+ // Determine bubbling style
78
+ const bubbleStyle: any = {};
79
+ if (props.bgColor) bubbleStyle.backgroundColor = props.bgColor;
80
+ if (props.color) bubbleStyle.color = props.color;
81
+
82
+ const hasChildren = props.children && (Array.isArray(props.children) ? props.children.length > 0 : true);
83
+
84
+ const isSup = !props.dot && displayContent !== undefined && displayContent !== null && displayContent !== '';
85
+ const showBubble = props.dot || isSup;
86
+
87
+ return (
88
+ <div class={['&-container', props.class].join(' ').trim()} ref={ref} css={props.style} onClick={props.onClick}>
89
+ {/* The Anchor Element */}
90
+ {props.children}
91
+
92
+ {/* The floating Badge Bubble */}
93
+ {showBubble && (
94
+ <sup
95
+ class={['&-bubble', props.dot ? 'is-dot' : '', !hasChildren ? 'is-standalone' : ''].join(' ').trim()}
96
+ style={bubbleStyle}
97
+ >
98
+ {!props.dot && displayContent}
99
+ </sup>
100
+ )}
101
+ </div>
102
+ );
103
+ };