@xyhp915/slack-base-ui 0.0.1 → 0.0.3
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/README.md +220 -4
- package/agents/slack-base-ui/SKILL.md +137 -0
- package/agents/slack-base-ui/checklists/style-review.md +56 -0
- package/agents/slack-base-ui/templates/consumer-setup.md +109 -0
- package/agents/slack-base-ui/templates/slack-theme.css +152 -0
- package/libs/Dialog.d.ts +73 -0
- package/libs/Dialog.d.ts.map +1 -1
- package/libs/Popover.d.ts +69 -0
- package/libs/Popover.d.ts.map +1 -1
- package/libs/index.d.ts +4 -4
- package/libs/index.d.ts.map +1 -1
- package/libs/index.js +2885 -2718
- package/package.json +1 -1
- package/src/App.css +7 -0
- package/src/App.tsx +18 -0
- package/src/assets/react.svg +1 -0
- package/src/components/AlertDialog.tsx +185 -0
- package/src/components/AutoComplete.tsx +311 -0
- package/src/components/Avatar.tsx +70 -0
- package/src/components/Badge.tsx +48 -0
- package/src/components/Button.tsx +53 -0
- package/src/components/Checkbox.tsx +109 -0
- package/src/components/ContextMenu.tsx +393 -0
- package/src/components/Dialog.tsx +371 -0
- package/src/components/Form.tsx +409 -0
- package/src/components/IconButton.tsx +49 -0
- package/src/components/Input.tsx +56 -0
- package/src/components/Loading.tsx +123 -0
- package/src/components/Menu.tsx +368 -0
- package/src/components/Popover.tsx +367 -0
- package/src/components/Progress.tsx +89 -0
- package/src/components/Radio.tsx +137 -0
- package/src/components/Select.tsx +177 -0
- package/src/components/Switch.tsx +116 -0
- package/src/components/Tabs.tsx +128 -0
- package/src/components/Toast.tsx +149 -0
- package/src/components/Tooltip.tsx +46 -0
- package/src/components/index.ts +186 -0
- package/src/context/ThemeContext.tsx +53 -0
- package/src/context/useTheme.ts +11 -0
- package/src/examples/slack-clone/SlackApp.tsx +94 -0
- package/src/examples/slack-clone/components/ChannelHeader.tsx +34 -0
- package/src/examples/slack-clone/components/Composer.tsx +42 -0
- package/src/examples/slack-clone/components/Message.tsx +97 -0
- package/src/examples/slack-clone/components/UserProfile.tsx +78 -0
- package/src/examples/slack-clone/layout/Layout.tsx +27 -0
- package/src/examples/slack-clone/layout/Sidebar.tsx +67 -0
- package/src/examples/slack-clone/layout/SidebarItem.tsx +57 -0
- package/src/examples/slack-clone/layout/TopBar.tsx +30 -0
- package/src/index.css +240 -0
- package/src/main.tsx +22 -0
- package/src/pages/ComponentShowcase.tsx +1964 -0
- package/src/pages/Dashboard.tsx +87 -0
- package/src/pages/QuickStartDemo.tsx +262 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// UI Components Export
|
|
2
|
+
// 统一导出所有组件,方便使用
|
|
3
|
+
|
|
4
|
+
// Basic Components
|
|
5
|
+
export { Button } from './Button'
|
|
6
|
+
export type { ButtonProps } from './Button'
|
|
7
|
+
|
|
8
|
+
export { Avatar } from './Avatar'
|
|
9
|
+
export type { AvatarProps } from './Avatar'
|
|
10
|
+
|
|
11
|
+
export { Badge } from './Badge'
|
|
12
|
+
export type { BadgeProps } from './Badge'
|
|
13
|
+
|
|
14
|
+
export { Input } from './Input'
|
|
15
|
+
export type { InputProps } from './Input'
|
|
16
|
+
|
|
17
|
+
export { IconButton } from './IconButton'
|
|
18
|
+
export type { IconButtonProps } from './IconButton'
|
|
19
|
+
|
|
20
|
+
export { Tooltip } from './Tooltip'
|
|
21
|
+
export type { TooltipProps } from './Tooltip'
|
|
22
|
+
|
|
23
|
+
// Popover Components
|
|
24
|
+
export {
|
|
25
|
+
Popover,
|
|
26
|
+
PopoverTrigger,
|
|
27
|
+
PopoverContent,
|
|
28
|
+
PopoverClose,
|
|
29
|
+
PopoverHeader,
|
|
30
|
+
PopoverBody,
|
|
31
|
+
PopoverFooter,
|
|
32
|
+
ImperativePopoverProvider,
|
|
33
|
+
useImperativePopover,
|
|
34
|
+
} from './Popover'
|
|
35
|
+
export type {
|
|
36
|
+
PopoverProps,
|
|
37
|
+
PopoverTriggerProps,
|
|
38
|
+
PopoverContentProps,
|
|
39
|
+
PopoverCloseProps,
|
|
40
|
+
ImperativePopoverOptions,
|
|
41
|
+
UseImperativePopoverReturn,
|
|
42
|
+
} from './Popover'
|
|
43
|
+
|
|
44
|
+
// Menu Components
|
|
45
|
+
export {
|
|
46
|
+
Menu,
|
|
47
|
+
MenuTrigger,
|
|
48
|
+
MenuContent,
|
|
49
|
+
MenuItem,
|
|
50
|
+
MenuCheckboxItem,
|
|
51
|
+
MenuRadioGroup,
|
|
52
|
+
MenuRadioItem,
|
|
53
|
+
MenuLabel,
|
|
54
|
+
MenuSeparator,
|
|
55
|
+
MenuSub,
|
|
56
|
+
MenuSubTrigger,
|
|
57
|
+
MenuSubContent,
|
|
58
|
+
MenuItemWithIcon
|
|
59
|
+
} from './Menu'
|
|
60
|
+
export type {
|
|
61
|
+
MenuProps,
|
|
62
|
+
MenuTriggerProps,
|
|
63
|
+
MenuContentProps,
|
|
64
|
+
MenuItemProps,
|
|
65
|
+
MenuCheckboxItemProps,
|
|
66
|
+
MenuRadioGroupProps,
|
|
67
|
+
MenuRadioItemProps,
|
|
68
|
+
MenuLabelProps,
|
|
69
|
+
MenuSeparatorProps,
|
|
70
|
+
MenuSubProps,
|
|
71
|
+
MenuSubTriggerProps,
|
|
72
|
+
MenuSubContentProps
|
|
73
|
+
} from './Menu'
|
|
74
|
+
|
|
75
|
+
// Context Menu Components
|
|
76
|
+
export {
|
|
77
|
+
ContextMenu,
|
|
78
|
+
ContextMenuTrigger,
|
|
79
|
+
ContextMenuContent,
|
|
80
|
+
ContextMenuItem,
|
|
81
|
+
ContextMenuCheckboxItem,
|
|
82
|
+
ContextMenuRadioGroup,
|
|
83
|
+
ContextMenuRadioItem,
|
|
84
|
+
ContextMenuLabel,
|
|
85
|
+
ContextMenuSeparator,
|
|
86
|
+
ContextMenuSub,
|
|
87
|
+
ContextMenuSubTrigger,
|
|
88
|
+
ContextMenuSubContent,
|
|
89
|
+
ContextMenuItemWithIcon
|
|
90
|
+
} from './ContextMenu'
|
|
91
|
+
export type {
|
|
92
|
+
ContextMenuProps,
|
|
93
|
+
ContextMenuTriggerProps,
|
|
94
|
+
ContextMenuContentProps,
|
|
95
|
+
ContextMenuItemProps,
|
|
96
|
+
ContextMenuCheckboxItemProps,
|
|
97
|
+
ContextMenuRadioGroupProps,
|
|
98
|
+
ContextMenuRadioItemProps,
|
|
99
|
+
ContextMenuLabelProps,
|
|
100
|
+
ContextMenuSeparatorProps,
|
|
101
|
+
ContextMenuSubProps,
|
|
102
|
+
ContextMenuSubTriggerProps,
|
|
103
|
+
ContextMenuSubContentProps
|
|
104
|
+
} from './ContextMenu'
|
|
105
|
+
|
|
106
|
+
// Dialog Components
|
|
107
|
+
export {
|
|
108
|
+
Dialog,
|
|
109
|
+
DialogHeader,
|
|
110
|
+
DialogBody,
|
|
111
|
+
DialogFooter,
|
|
112
|
+
DialogTrigger,
|
|
113
|
+
DialogClose,
|
|
114
|
+
DialogProvider,
|
|
115
|
+
useDialog,
|
|
116
|
+
} from './Dialog'
|
|
117
|
+
export type {
|
|
118
|
+
DialogProps,
|
|
119
|
+
DialogTriggerProps,
|
|
120
|
+
DialogSize,
|
|
121
|
+
ShowDialogOptions,
|
|
122
|
+
ConfirmDialogOptions,
|
|
123
|
+
AlertDialogOptions,
|
|
124
|
+
UseDialogReturn,
|
|
125
|
+
} from './Dialog'
|
|
126
|
+
|
|
127
|
+
export { AlertDialog, AlertDialogTrigger } from './AlertDialog'
|
|
128
|
+
export type { AlertDialogProps, AlertDialogTriggerProps } from './AlertDialog'
|
|
129
|
+
|
|
130
|
+
// Form Components
|
|
131
|
+
export {
|
|
132
|
+
Form,
|
|
133
|
+
FormField,
|
|
134
|
+
FormInput,
|
|
135
|
+
FormTextarea,
|
|
136
|
+
FormSelect,
|
|
137
|
+
FormCheckbox,
|
|
138
|
+
FormActions,
|
|
139
|
+
useFormContext
|
|
140
|
+
} from './Form'
|
|
141
|
+
export type {
|
|
142
|
+
FormProps,
|
|
143
|
+
FormFieldProps,
|
|
144
|
+
FormInputProps,
|
|
145
|
+
FormTextareaProps,
|
|
146
|
+
FormSelectProps,
|
|
147
|
+
FormCheckboxProps,
|
|
148
|
+
FormActionsProps
|
|
149
|
+
} from './Form'
|
|
150
|
+
|
|
151
|
+
// Select Component
|
|
152
|
+
export { Select } from './Select'
|
|
153
|
+
export type { SelectProps, SelectOption, SelectGroup } from './Select'
|
|
154
|
+
|
|
155
|
+
// Checkbox Component
|
|
156
|
+
export { Checkbox } from './Checkbox'
|
|
157
|
+
export type { CheckboxProps } from './Checkbox'
|
|
158
|
+
|
|
159
|
+
// Radio Components
|
|
160
|
+
export { Radio, RadioGroup } from './Radio'
|
|
161
|
+
export type { RadioProps, RadioGroupProps } from './Radio'
|
|
162
|
+
|
|
163
|
+
// Switch Component
|
|
164
|
+
export { Switch } from './Switch'
|
|
165
|
+
export type { SwitchProps } from './Switch'
|
|
166
|
+
|
|
167
|
+
// Tabs Components
|
|
168
|
+
export { Tabs, TabList, Tab, TabPanel } from './Tabs'
|
|
169
|
+
export type { TabsProps, TabListProps, TabProps, TabPanelProps } from './Tabs'
|
|
170
|
+
|
|
171
|
+
// Progress Component
|
|
172
|
+
export { Progress } from './Progress'
|
|
173
|
+
export type { ProgressProps } from './Progress'
|
|
174
|
+
|
|
175
|
+
// Toast Components
|
|
176
|
+
export { ToastProvider, useToast } from './Toast'
|
|
177
|
+
export type { ToastProviderProps, ToastOptions, ToastType } from './Toast'
|
|
178
|
+
|
|
179
|
+
// Loading Component
|
|
180
|
+
export { Loading } from './Loading'
|
|
181
|
+
export type { LoadingProps, LoadingVariant, LoadingSize } from './Loading'
|
|
182
|
+
|
|
183
|
+
// AutoComplete Component
|
|
184
|
+
export { AutoComplete } from './AutoComplete'
|
|
185
|
+
export type { AutoCompleteProps, AutoCompleteOption } from './AutoComplete'
|
|
186
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { createContext, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type Theme = 'light' | 'dark';
|
|
4
|
+
type ThemeColor = 'slack' | 'blue' | 'green' | 'aubergine';
|
|
5
|
+
|
|
6
|
+
interface ThemeContextType {
|
|
7
|
+
theme: Theme;
|
|
8
|
+
themeColor: ThemeColor;
|
|
9
|
+
toggleTheme: () => void;
|
|
10
|
+
setThemeColor: (color: ThemeColor) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
16
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
17
|
+
// Check local storage or system preference
|
|
18
|
+
const savedTheme = localStorage.getItem('theme') as Theme;
|
|
19
|
+
if (savedTheme) {
|
|
20
|
+
return savedTheme;
|
|
21
|
+
}
|
|
22
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
23
|
+
return 'dark';
|
|
24
|
+
}
|
|
25
|
+
return 'light';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const [themeColor, setThemeColor] = useState<ThemeColor>(() => {
|
|
29
|
+
const savedThemeColor = localStorage.getItem('themeColor') as ThemeColor;
|
|
30
|
+
return savedThemeColor || 'slack';
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const root = window.document.documentElement;
|
|
35
|
+
root.classList.remove('light', 'dark');
|
|
36
|
+
root.classList.add(theme);
|
|
37
|
+
root.setAttribute('data-theme-color', themeColor);
|
|
38
|
+
localStorage.setItem('theme', theme);
|
|
39
|
+
localStorage.setItem('themeColor', themeColor);
|
|
40
|
+
}, [theme, themeColor]);
|
|
41
|
+
|
|
42
|
+
const toggleTheme = () => {
|
|
43
|
+
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<ThemeContext.Provider value={{ theme, themeColor, toggleTheme, setThemeColor }}>
|
|
48
|
+
{children}
|
|
49
|
+
</ThemeContext.Provider>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { ThemeContext };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ThemeContext } from './ThemeContext';
|
|
3
|
+
|
|
4
|
+
export const useTheme = () => {
|
|
5
|
+
const context = useContext(ThemeContext);
|
|
6
|
+
if (context === undefined) {
|
|
7
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
8
|
+
}
|
|
9
|
+
return context;
|
|
10
|
+
};
|
|
11
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Layout } from './layout/Layout'
|
|
2
|
+
import { Message } from './components/Message'
|
|
3
|
+
import { Composer } from './components/Composer'
|
|
4
|
+
import { ChannelHeader } from './components/ChannelHeader'
|
|
5
|
+
|
|
6
|
+
function SlackApp () {
|
|
7
|
+
return (
|
|
8
|
+
<Layout>
|
|
9
|
+
<ChannelHeader/>
|
|
10
|
+
|
|
11
|
+
{/* Main Scroll Area */}
|
|
12
|
+
<div className="flex-1 overflow-y-auto px-5 custom-scrollbar flex flex-col pt-4">
|
|
13
|
+
|
|
14
|
+
<div className="flex-1"/>
|
|
15
|
+
{/* Spacer to push content down if empty, or just normal flow */}
|
|
16
|
+
|
|
17
|
+
{/* Mock Messages */}
|
|
18
|
+
<div className="pb-4">
|
|
19
|
+
|
|
20
|
+
{/* Welcome Message */}
|
|
21
|
+
<div className="mb-8 mt-4 px-5">
|
|
22
|
+
<h1 className="text-3xl font-black mb-2 flex items-center gap-2">
|
|
23
|
+
👋 Welcome to #design-system!
|
|
24
|
+
</h1>
|
|
25
|
+
<p className="text-(--text-secondary)">
|
|
26
|
+
This channel is for everything related to our new Base UI implementation.
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div className="border-t border-(--border-light) mb-4 flex items-center gap-4 py-2 px-5 text-[13px]">
|
|
31
|
+
<span className="bg-white text-(--text-secondary) -mt-5 px-2">Yesterday</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<Message
|
|
35
|
+
id="1"
|
|
36
|
+
author={{ name: 'Alice Smith', avatar: 'https://i.pravatar.cc/150?u=4', status: 'online' }}
|
|
37
|
+
timestamp="10:23 AM"
|
|
38
|
+
content="Hey everyone! I just pushed the new Button component tokens. Let me know what you think."
|
|
39
|
+
reactions={[{ emoji: '🚀', count: 4, reacted: true }, { emoji: '🔥', count: 2 }]}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<Message
|
|
43
|
+
id="2"
|
|
44
|
+
author={{ name: 'Bob Jones', avatar: 'https://i.pravatar.cc/150?u=5', status: 'away' }}
|
|
45
|
+
timestamp="10:25 AM"
|
|
46
|
+
content="Looks great! I love the new hover states. Are we using CSS variables for the focus rings?"
|
|
47
|
+
isFirst={true}
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<Message
|
|
51
|
+
id="3"
|
|
52
|
+
author={{ name: 'Alice Smith', avatar: 'https://i.pravatar.cc/150?u=4', status: 'online' }}
|
|
53
|
+
timestamp="10:26 AM"
|
|
54
|
+
content="Yes, `var(--slack-blue)` is attached to the `ring` classes in Tailwind."
|
|
55
|
+
isFirst={true}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<div className="border-t border-(--border-light) my-4 flex items-center gap-4 py-2 px-5 text-[13px]">
|
|
59
|
+
<span className="bg-(--bg-primary) text-(--text-secondary) -mt-5 px-2">Today</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<Message
|
|
63
|
+
id="4"
|
|
64
|
+
author={{ name: 'Charlie Day', avatar: 'https://i.pravatar.cc/150?u=8', status: 'dnd' }}
|
|
65
|
+
timestamp="9:01 AM"
|
|
66
|
+
content={
|
|
67
|
+
<div className="space-y-2">
|
|
68
|
+
<p>I've updated the <strong>Composer</strong> component to support rich text (conceptually). Here is
|
|
69
|
+
a list of changes:</p>
|
|
70
|
+
<ul className="list-disc list-inside">
|
|
71
|
+
<li>Added bold/italic support keys</li>
|
|
72
|
+
<li>Integrated the emoji picker icon</li>
|
|
73
|
+
<li>Fixed the resizing issue</li>
|
|
74
|
+
</ul>
|
|
75
|
+
<div
|
|
76
|
+
className="p-3 bg-(--bg-secondary) rounded border border-(--border-light) mt-2 font-mono text-sm text-(--slack-red)">
|
|
77
|
+
npm run build
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
}
|
|
81
|
+
isFirst={true}
|
|
82
|
+
reactions={[{ emoji: '👀', count: 1 }]}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<Composer/>
|
|
90
|
+
</Layout>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default SlackApp
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Hash, ChevronDown, UserPlus, Info } from 'lucide-react';
|
|
2
|
+
import { Avatar } from '../../../components/Avatar';
|
|
3
|
+
|
|
4
|
+
export const ChannelHeader = ({ title = "design-system", memberCount = 24 }) => {
|
|
5
|
+
return (
|
|
6
|
+
<header className="h-[49px] border-b border-(--border-light) flex items-center justify-between px-5 shrink-0 bg-(--bg-primary) shadow-sm z-10">
|
|
7
|
+
<div className="flex items-center gap-1 font-bold text-(--text-primary) cursor-pointer hover:bg-(--bg-hover) py-1 px-2 -ml-2 rounded transition-colors group">
|
|
8
|
+
<Hash className="w-5 h-5 text-(--text-secondary)" />
|
|
9
|
+
<span className="text-lg">{title}</span>
|
|
10
|
+
<ChevronDown className="w-3.5 h-3.5 text-(--text-secondary) group-hover:block hidden" />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div className="flex items-center">
|
|
14
|
+
<div className="flex items-center -space-x-2 mr-4 cursor-pointer hover:opacity-80">
|
|
15
|
+
<Avatar size="xs" src="https://i.pravatar.cc/150?u=1" className="ring-2 ring-(--bg-primary)" />
|
|
16
|
+
<Avatar size="xs" src="https://i.pravatar.cc/150?u=2" className="ring-2 ring-(--bg-primary)" />
|
|
17
|
+
<Avatar size="xs" src="https://i.pravatar.cc/150?u=3" className="ring-2 ring-(--bg-primary)" />
|
|
18
|
+
<div className="w-6 h-6 rounded bg-(--bg-secondary) flex items-center justify-center text-[10px] ring-2 ring-(--bg-primary) font-medium text-(--text-secondary)">
|
|
19
|
+
+{memberCount - 3}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div className="border-l pl-4 flex items-center gap-1 text-(--text-secondary)">
|
|
24
|
+
<button className="p-1 hover:bg-(--bg-hover) rounded" title="Add people">
|
|
25
|
+
<UserPlus className="w-5 h-5 opacity-80" />
|
|
26
|
+
</button>
|
|
27
|
+
<button className="p-1 hover:bg-(--bg-hover) rounded" title="Channel details">
|
|
28
|
+
<Info className="w-5 h-5 opacity-80" />
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</header>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Bold, Italic, Link, List, Smile, Send, Plus, AtSign, Video } from 'lucide-react';
|
|
2
|
+
import { Button } from '../../../components/Button';
|
|
3
|
+
import { IconButton } from '../../../components/IconButton';
|
|
4
|
+
|
|
5
|
+
export const Composer = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="p-5 pb-6">
|
|
8
|
+
<div className="border border-(--border-light) rounded-lg overflow-hidden focus-within:ring-1 focus-within:ring-(--slack-blue) focus-within:border-(--slack-blue) transition-shadow bg-(--bg-primary) shadow-sm">
|
|
9
|
+
{/* Toolbar */}
|
|
10
|
+
<div className="bg-(--bg-hover) px-2 py-1 flex items-center gap-0.5 border-b border-(--border-light) overflow-x-auto">
|
|
11
|
+
<IconButton size="sm" variant="ghost" title="Bold"><Bold className="w-4 h-4" /></IconButton>
|
|
12
|
+
<IconButton size="sm" variant="ghost" title="Italic"><Italic className="w-4 h-4" /></IconButton>
|
|
13
|
+
<IconButton size="sm" variant="ghost" title="Link"><Link className="w-4 h-4" /></IconButton>
|
|
14
|
+
<div className="w-px h-4 bg-(--border-light) mx-1"></div>
|
|
15
|
+
<IconButton size="sm" variant="ghost" title="List"><List className="w-4 h-4" /></IconButton>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{/* Text Area */}
|
|
19
|
+
<textarea
|
|
20
|
+
className="w-full p-3 min-h-[40px] max-h-[40vh] outline-none text-[15px] resize-none font-[lato] bg-(--bg-primary) text-(--text-primary) placeholder:text-(--text-muted)"
|
|
21
|
+
placeholder="Message #design-system"
|
|
22
|
+
rows={1}
|
|
23
|
+
style={{ minHeight: '80px' }}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
{/* Footer Actions */}
|
|
27
|
+
<div className="flex justify-between items-center p-2 bg-(--bg-primary)">
|
|
28
|
+
<div className="flex gap-1">
|
|
29
|
+
<IconButton size="sm" variant="ghost"><Plus className="w-4 h-4" /></IconButton>
|
|
30
|
+
<IconButton size="sm" variant="ghost"><Video className="w-4 h-4" /></IconButton>
|
|
31
|
+
<IconButton size="sm" variant="ghost"><Smile className="w-4 h-4" /></IconButton>
|
|
32
|
+
<IconButton size="sm" variant="ghost"><AtSign className="w-4 h-4" /></IconButton>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex items-center gap-2">
|
|
35
|
+
<span className="text-[11px] text-(--text-muted) hidden sm:inline-block">Press <strong>Enter</strong> to send</span>
|
|
36
|
+
<Button variant="primary" size="sm" className="px-4 h-8"><Send className="w-3.5 h-3.5" /></Button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Avatar } from '../../../components/Avatar';
|
|
3
|
+
import { Smile, MessageSquare, Share, MoreHorizontal } from 'lucide-react';
|
|
4
|
+
import { IconButton } from '../../../components/IconButton';
|
|
5
|
+
import { UserProfile } from './UserProfile';
|
|
6
|
+
import clsx from 'clsx';
|
|
7
|
+
|
|
8
|
+
export interface MessageProps {
|
|
9
|
+
id: string;
|
|
10
|
+
author: {
|
|
11
|
+
name: string;
|
|
12
|
+
avatar: string;
|
|
13
|
+
status?: 'online' | 'away' | 'dnd' | 'offline';
|
|
14
|
+
};
|
|
15
|
+
content: React.ReactNode;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
isFirst?: boolean; // If false, squashes the avatar/header
|
|
18
|
+
reactions?: Array<{ emoji: string; count: number; reacted?: boolean }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Message = ({ author, content, timestamp, isFirst = true, reactions }: MessageProps) => {
|
|
22
|
+
return (
|
|
23
|
+
<div className={clsx(
|
|
24
|
+
"group flex gap-2 py-0.5 px-5 hover:bg-(--bg-hover) -mx-5 relative",
|
|
25
|
+
isFirst ? "mt-2 pt-2" : ""
|
|
26
|
+
)}>
|
|
27
|
+
|
|
28
|
+
{/* Gutter / Avatar */}
|
|
29
|
+
<div className="w-9 shrink-0">
|
|
30
|
+
{isFirst ? (
|
|
31
|
+
<UserProfile user={{ ...author, title: "Product Designer", localTime: "4:43 PM local time", email: "user@example.com" }}>
|
|
32
|
+
<button className="block">
|
|
33
|
+
<Avatar
|
|
34
|
+
src={author.avatar}
|
|
35
|
+
alt={author.name}
|
|
36
|
+
size="md"
|
|
37
|
+
status={author.status}
|
|
38
|
+
className="cursor-pointer"
|
|
39
|
+
/>
|
|
40
|
+
</button>
|
|
41
|
+
</UserProfile>
|
|
42
|
+
) : (
|
|
43
|
+
<div className="w-[36px] text-right text-[11px] text-(--text-muted) opacity-0 group-hover:opacity-100 mt-1 select-none">
|
|
44
|
+
{timestamp.split(' ')[0]}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{/* Content */}
|
|
50
|
+
<div className="flex-1 min-w-0">
|
|
51
|
+
{isFirst && (
|
|
52
|
+
<div className="flex items-baseline gap-2 mb-0.5">
|
|
53
|
+
<UserProfile user={{ ...author, title: "Product Designer", localTime: "4:43 PM local time", email: "user@example.com" }}>
|
|
54
|
+
<button className="font-bold text-[15px] cursor-pointer hover:underline text-(--text-primary) hover:text-(--text-primary) bg-transparent border-none p-0">
|
|
55
|
+
{author.name}
|
|
56
|
+
</button>
|
|
57
|
+
</UserProfile>
|
|
58
|
+
<span className="text-[12px] text-(--text-muted) cursor-pointer hover:underline">{timestamp}</span>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<div className="text-[15px] leading-relaxed text-(--text-primary) wrap-break-word">
|
|
63
|
+
{content}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Reactions */}
|
|
67
|
+
{reactions && reactions.length > 0 && (
|
|
68
|
+
<div className="flex flex-wrap gap-1 mt-1">
|
|
69
|
+
{reactions.map((r, i) => (
|
|
70
|
+
<button
|
|
71
|
+
key={i}
|
|
72
|
+
className={clsx(
|
|
73
|
+
"flex items-center gap-1.5 px-1.5 py-0.5 rounded-full border text-[13px] hover:bg-white transition-colors",
|
|
74
|
+
r.reacted
|
|
75
|
+
? "bg-[rgba(29,28,29,0.05)] border-(--slack-blue) text-(--slack-blue)"
|
|
76
|
+
: "bg-[rgba(29,28,29,0.05)] border-transparent text-(--text-secondary)"
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<span>{r.emoji}</span>
|
|
80
|
+
<span className="font-medium text-[11px]">{r.count}</span>
|
|
81
|
+
</button>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Hover Actions (Floating Toolbar) */}
|
|
88
|
+
<div className="absolute -top-4 right-4 border border-(--border-light) shadow-sm bg-(--bg-primary) rounded-md p-1 opacity-0 group-hover:opacity-100 transition-opacity z-10 hidden group-hover:flex">
|
|
89
|
+
<IconButton size="sm" variant="ghost" title="Add reaction"><Smile className="w-4 h-4" /></IconButton>
|
|
90
|
+
<IconButton size="sm" variant="ghost" title="Reply"><MessageSquare className="w-4 h-4" /></IconButton>
|
|
91
|
+
<IconButton size="sm" variant="ghost" title="Share"><Share className="w-4 h-4" /></IconButton>
|
|
92
|
+
<IconButton size="sm" variant="ghost" title="More"><MoreHorizontal className="w-4 h-4" /></IconButton>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Popover as BasePopover } from '@base-ui/react';
|
|
3
|
+
import { Avatar } from '../../../components/Avatar';
|
|
4
|
+
import { Button } from '../../../components/Button';
|
|
5
|
+
import { Mail, Clock } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export interface UserProfileProps {
|
|
8
|
+
user: {
|
|
9
|
+
name: string;
|
|
10
|
+
avatar: string;
|
|
11
|
+
status?: 'online' | 'away' | 'dnd' | 'offline';
|
|
12
|
+
title?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
localTime?: string;
|
|
15
|
+
};
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const UserProfile = ({ user, children }: UserProfileProps) => {
|
|
20
|
+
return (
|
|
21
|
+
<BasePopoverProvider>
|
|
22
|
+
<BasePopover.Trigger className="cursor-pointer appearance-none bg-transparent border-none p-0 text-left">
|
|
23
|
+
{children}
|
|
24
|
+
</BasePopover.Trigger>
|
|
25
|
+
<BasePopover.Portal>
|
|
26
|
+
<BasePopover.Positioner side="right" align="start" sideOffset={10}>
|
|
27
|
+
<BasePopover.Popup className="w-[300px] bg-white rounded-lg shadow-xl border border-(--border-light) overflow-hidden z-50 animate-in fade-in zoom-in-95 duration-200 focus:outline-none">
|
|
28
|
+
|
|
29
|
+
{/* Header Image / Avatar */}
|
|
30
|
+
<div className="h-24 bg-(--slack-aubergine) relative">
|
|
31
|
+
<div className="absolute -bottom-8 left-6">
|
|
32
|
+
<Avatar
|
|
33
|
+
src={user.avatar}
|
|
34
|
+
size="xl"
|
|
35
|
+
status={user.status}
|
|
36
|
+
className="ring-4 ring-white rounded-md"
|
|
37
|
+
rounded={true}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div className="pt-10 px-6 pb-6">
|
|
43
|
+
<h2 className="text-xl font-bold text-(--text-primary)">{user.name}</h2>
|
|
44
|
+
<p className="text-(--text-secondary) text-[15px]">{user.title || "Member"}</p>
|
|
45
|
+
|
|
46
|
+
<div className="mt-4 flex gap-2">
|
|
47
|
+
<Button size="sm" variant="secondary" fullWidth>Message</Button>
|
|
48
|
+
<Button size="sm" variant="secondary" fullWidth>Huddle</Button>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="mt-6 space-y-3">
|
|
52
|
+
{user.localTime && (
|
|
53
|
+
<div className="flex items-center gap-3 text-[15px] text-(--text-primary)">
|
|
54
|
+
<Clock className="w-4 h-4 text-(--text-secondary)" />
|
|
55
|
+
<span>{user.localTime}</span>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
{user.email && (
|
|
59
|
+
<div className="flex items-center gap-3 text-[15px] text-(--text-primary)">
|
|
60
|
+
<Mail className="w-4 h-4 text-(--text-secondary)" />
|
|
61
|
+
<a href={`mailto:${user.email}`} className="text-[#1264a3] hover:underline decoration-1">{user.email}</a>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
</BasePopover.Popup>
|
|
68
|
+
</BasePopover.Positioner>
|
|
69
|
+
</BasePopover.Portal>
|
|
70
|
+
</BasePopoverProvider>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Wrapper for Base UI Popover Structure (similar to Tooltip assumption)
|
|
75
|
+
function BasePopoverProvider({ children }: { children: React.ReactNode }) {
|
|
76
|
+
// Creating a safer flexible wrapper in case exports differ
|
|
77
|
+
return <BasePopover.Root>{children}</BasePopover.Root>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Sidebar } from './Sidebar';
|
|
3
|
+
import { TopBar } from './TopBar';
|
|
4
|
+
|
|
5
|
+
interface LayoutProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Layout = ({ children }: LayoutProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex flex-col h-screen w-full bg-(--bg-primary) overflow-hidden">
|
|
12
|
+
{/* Top Navigation */}
|
|
13
|
+
<TopBar />
|
|
14
|
+
|
|
15
|
+
{/* Main Workspace Area */}
|
|
16
|
+
<div className="flex flex-1 overflow-hidden">
|
|
17
|
+
{/* Sidebar (expandable/collapsible logic could go here) */}
|
|
18
|
+
<Sidebar />
|
|
19
|
+
|
|
20
|
+
{/* Main Content View */}
|
|
21
|
+
<main className="flex-1 flex flex-col bg-(--bg-primary) min-w-0 relative">
|
|
22
|
+
{children}
|
|
23
|
+
</main>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|