mirror of
https://github.com/usememos/memos.git
synced 2025-10-30 08:17:29 +08:00
chore: unify colors
This commit is contained in:
parent
2e474d37fd
commit
35928ce5ba
45 changed files with 897 additions and 339 deletions
309
web/COLOR_GUIDE.md
Normal file
309
web/COLOR_GUIDE.md
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# Color System Guide
|
||||
|
||||
This document explains the color system used in the Memos application, built with OKLCH color space for better perceptual uniformity and accessibility.
|
||||
|
||||
## Overview
|
||||
|
||||
The color system supports both light and dark themes automatically through CSS custom properties. All colors are defined using OKLCH (Oklab LCH) color space, which provides better perceptual uniformity than traditional RGB/HSL.
|
||||
|
||||
## Color Categories
|
||||
|
||||
### 🎨 Primary Brand Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| ---------------------- | ------------- | --------------- | ------------------------------ |
|
||||
| `--primary` | Golden yellow | Brighter golden | Main brand color, primary CTAs |
|
||||
| `--primary-foreground` | White | White | Text on primary backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Call-to-action buttons
|
||||
- Active navigation items
|
||||
- Important links and highlights
|
||||
- Brand elements
|
||||
|
||||
```css
|
||||
/* Example usage */
|
||||
.cta-button {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
```
|
||||
|
||||
### 🔘 Secondary Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| ------------------------ | ----------- | --------------- | ----------------------------- |
|
||||
| `--secondary` | Light gray | Very light gray | Supporting actions |
|
||||
| `--secondary-foreground` | Dark gray | Dark gray | Text on secondary backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Secondary buttons
|
||||
- Less important actions
|
||||
- Alternative navigation items
|
||||
- Subtle highlights
|
||||
|
||||
### 📄 Background & Surface Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| ---------------------- | ----------- | ----------- | --------------------------- |
|
||||
| `--background` | Near white | Dark gray | Main page background |
|
||||
| `--card` | Near white | Dark gray | Card/container backgrounds |
|
||||
| `--card-foreground` | Very dark | Near white | Text on card backgrounds |
|
||||
| `--popover` | Pure white | Darker gray | Overlay backgrounds |
|
||||
| `--popover-foreground` | Dark gray | Light gray | Text on overlay backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Page backgrounds (`--background`)
|
||||
- Content cards and panels (`--card`)
|
||||
- Tooltips, dropdowns, modals (`--popover`)
|
||||
|
||||
### ✏️ Text & Content Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| -------------------- | ----------- | ------------ | ------------------------ |
|
||||
| `--foreground` | Dark gray | Light gray | Primary text color |
|
||||
| `--muted` | Light gray | Very dark | Subtle background areas |
|
||||
| `--muted-foreground` | Medium gray | Medium light | Secondary text, captions |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Main body text (`--foreground`)
|
||||
- Helper text, placeholders (`--muted-foreground`)
|
||||
- Disabled text states
|
||||
- Subtle background sections (`--muted`)
|
||||
|
||||
### 🎯 Interactive Elements
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| --------------------- | ------------ | ----------- | ---------------------------- |
|
||||
| `--accent` | Light gray | Very dark | Hover states, selected items |
|
||||
| `--accent-foreground` | Dark gray | Light gray | Text on accent backgrounds |
|
||||
| `--border` | Medium light | Medium dark | Dividers, input borders |
|
||||
| `--input` | Medium light | Medium dark | Form input backgrounds |
|
||||
| `--ring` | Blue | Blue | Focus outlines |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Hover states (`--accent`)
|
||||
- Form field borders (`--border`)
|
||||
- Input field backgrounds (`--input`)
|
||||
- Focus indicators (`--ring`)
|
||||
|
||||
### ⚠️ Feedback Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| -------------------------- | ----------- | ---------- | ------------------------------- |
|
||||
| `--destructive` | Very dark | Red | Error states, dangerous actions |
|
||||
| `--destructive-foreground` | White | White | Text on destructive backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Error messages
|
||||
- Delete buttons
|
||||
- Warning alerts
|
||||
- Validation failures
|
||||
|
||||
### 📊 Data Visualization
|
||||
|
||||
| Variable | Purpose |
|
||||
| ----------- | --------------------------------------- |
|
||||
| `--chart-1` | Primary data series (golden) |
|
||||
| `--chart-2` | Secondary data series (purple) |
|
||||
| `--chart-3` | Tertiary data series (light) |
|
||||
| `--chart-4` | Quaternary data series (purple variant) |
|
||||
| `--chart-5` | Quinary data series (golden variant) |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Charts and graphs
|
||||
- Data visualization
|
||||
- Progress indicators
|
||||
- Statistical displays
|
||||
|
||||
### 🔧 Sidebar System
|
||||
|
||||
| Variable | Usage |
|
||||
| ------------------------------ | ---------------------------- |
|
||||
| `--sidebar` | Sidebar background |
|
||||
| `--sidebar-foreground` | Sidebar text |
|
||||
| `--sidebar-primary` | Active sidebar items |
|
||||
| `--sidebar-primary-foreground` | Text on active sidebar items |
|
||||
| `--sidebar-accent` | Sidebar hover states |
|
||||
| `--sidebar-accent-foreground` | Text on sidebar hover states |
|
||||
| `--sidebar-border` | Sidebar dividers |
|
||||
| `--sidebar-ring` | Sidebar focus indicators |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Do's
|
||||
|
||||
1. **Always pair colors correctly:**
|
||||
|
||||
```css
|
||||
/* Correct */
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
```
|
||||
|
||||
2. **Use semantic meaning:**
|
||||
|
||||
- Primary = main actions
|
||||
- Secondary = supporting actions
|
||||
- Destructive = dangerous/delete actions
|
||||
- Muted = less important content
|
||||
|
||||
3. **Respect the design system:**
|
||||
- Use existing color tokens instead of custom colors
|
||||
- Maintain consistency across components
|
||||
|
||||
### ❌ Don'ts
|
||||
|
||||
1. **Don't mix incompatible pairs:**
|
||||
|
||||
```css
|
||||
/* Incorrect - poor contrast */
|
||||
background: var(--primary);
|
||||
color: var(--foreground);
|
||||
```
|
||||
|
||||
2. **Don't use colors outside their intended purpose:**
|
||||
|
||||
- Don't use destructive colors for positive actions
|
||||
- Don't use primary colors for secondary elements
|
||||
|
||||
3. **Don't hardcode color values:**
|
||||
|
||||
```css
|
||||
/* Bad */
|
||||
color: #333333;
|
||||
|
||||
/* Good */
|
||||
color: var(--foreground);
|
||||
```
|
||||
|
||||
## Theme Switching
|
||||
|
||||
The color system automatically adapts between light and dark themes when the `.dark` class is applied to a parent element (typically `<html>` or `<body>`):
|
||||
|
||||
```javascript
|
||||
// Toggle dark mode
|
||||
document.documentElement.classList.toggle("dark");
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- All color pairs meet WCAG contrast requirements
|
||||
- Focus indicators use `--ring` for consistency
|
||||
- Color is never the only means of conveying information
|
||||
|
||||
## Implementation Examples
|
||||
|
||||
### Button Variants
|
||||
|
||||
```css
|
||||
/* Primary button */
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
/* Secondary button */
|
||||
.btn-secondary {
|
||||
background: var(--secondary);
|
||||
color: var(--secondary-foreground);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Destructive button */
|
||||
.btn-destructive {
|
||||
background: var(--destructive);
|
||||
color: var(--destructive-foreground);
|
||||
border: 1px solid var(--destructive);
|
||||
}
|
||||
```
|
||||
|
||||
### Form Elements
|
||||
|
||||
```css
|
||||
/* Input field */
|
||||
.input {
|
||||
background: var(--input);
|
||||
color: var(--foreground);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: 2px solid var(--ring);
|
||||
}
|
||||
```
|
||||
|
||||
### Cards and Containers
|
||||
|
||||
```css
|
||||
/* Content card */
|
||||
.card {
|
||||
background: var(--card);
|
||||
color: var(--card-foreground);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Popover/Modal */
|
||||
.popover {
|
||||
background: var(--popover);
|
||||
color: var(--popover-foreground);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
```
|
||||
|
||||
## Color Testing
|
||||
|
||||
To ensure proper contrast and accessibility:
|
||||
|
||||
1. Test both light and dark themes
|
||||
2. Verify readability at different zoom levels
|
||||
3. Check with colorblind simulation tools
|
||||
4. Validate WCAG contrast ratios
|
||||
|
||||
## Z-Index Hierarchy
|
||||
|
||||
The application uses a structured z-index hierarchy to ensure proper layering of UI components:
|
||||
|
||||
| Component Type | Z-Index | Usage |
|
||||
| ----------------- | -------- | ------------------------------------- |
|
||||
| **Base Content** | `z-0` | Normal page content |
|
||||
| **Overlays** | `z-50` | Dialog/Sheet backgrounds |
|
||||
| **Modal Content** | `z-50` | Dialog/Sheet content |
|
||||
| **Dropdowns** | `z-[60]` | Select, DropdownMenu, Popover content |
|
||||
| **Tooltips** | `z-[70]` | Tooltip content (highest priority) |
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Dialog/Sheet**: Use `z-50` for both overlay and content
|
||||
2. **Interactive Elements**: Use `z-[60]` for dropdowns inside dialogs
|
||||
3. **Tooltips**: Use `z-[70]` to appear above all other elements
|
||||
4. **Always test**: Ensure Select/DropdownMenu works inside Dialog/Sheet
|
||||
|
||||
### Example
|
||||
|
||||
```tsx
|
||||
// ✅ Correct: Select inside Dialog will appear above dialog content
|
||||
<Dialog>
|
||||
<DialogContent>
|
||||
<Select>
|
||||
<SelectContent className="z-[60]">
|
||||
{" "}
|
||||
{/* Higher than dialog */}
|
||||
<SelectItem>Option 1</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_This color system is designed to provide a consistent, accessible, and beautiful user experience across all themes and components._
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
"@matejmazur/react-katex": "^3.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
|
|
|
|||
70
web/pnpm-lock.yaml
generated
70
web/pnpm-lock.yaml
generated
|
|
@ -35,6 +35,9 @@ importers:
|
|||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.14
|
||||
version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-dropdown-menu':
|
||||
specifier: ^2.1.15
|
||||
version: 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
|
@ -805,6 +808,19 @@ packages:
|
|||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dropdown-menu@2.1.15':
|
||||
resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-focus-guards@1.1.2':
|
||||
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
|
||||
peerDependencies:
|
||||
|
|
@ -849,6 +865,19 @@ packages:
|
|||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-menu@2.1.15':
|
||||
resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-popover@1.1.14':
|
||||
resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==}
|
||||
peerDependencies:
|
||||
|
|
@ -4052,6 +4081,21 @@ snapshots:
|
|||
'@types/react': 18.3.23
|
||||
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||
|
||||
'@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.2
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.23
|
||||
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||
|
||||
'@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
|
@ -4085,6 +4129,32 @@ snapshots:
|
|||
'@types/react': 18.3.23
|
||||
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||
|
||||
'@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.2
|
||||
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
|
||||
aria-hidden: 1.2.6
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.23
|
||||
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||
|
||||
'@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.2
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const BaseDialog = observer((props: Props) => {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-0 left-0 flex flex-col justify-start items-center w-full h-full pt-16 pb-8 px-4 z-1000 overflow-x-hidden overflow-y-scroll transition-all hide-scrollbar bg-foreground/60",
|
||||
"fixed top-0 left-0 flex flex-col justify-start items-center w-full h-full pt-16 pb-8 px-4 z-50 overflow-x-hidden overflow-y-scroll transition-all hide-scrollbar bg-foreground/60",
|
||||
className,
|
||||
)}
|
||||
onMouseDown={handleSpaceClicked}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,12 @@ const HomeSidebar = observer((props: Props) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<aside className={cn("relative w-full h-full overflow-auto flex flex-col justify-start items-start", props.className)}>
|
||||
<aside
|
||||
className={cn(
|
||||
"relative w-full h-full overflow-auto flex flex-col justify-start items-start bg-sidebar text-sidebar-foreground",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<SearchBar />
|
||||
<div className="mt-1 px-1 w-full">
|
||||
<StatisticsView />
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import memoFilterStore from "@/store/v2/memoFilter";
|
|||
import { Shortcut } from "@/types/proto/api/v1/shortcut_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import showCreateShortcutDialog from "../CreateShortcutDialog";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
|
||||
|
||||
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)$/u;
|
||||
|
||||
|
|
@ -64,35 +64,27 @@ const ShortcutsSection = observer(() => {
|
|||
className="shrink-0 w-full text-sm rounded-md leading-6 flex flex-row justify-between items-center select-none gap-2 text-muted-foreground"
|
||||
>
|
||||
<span
|
||||
className={cn("truncate cursor-pointer opacity-80", selected && "text-primary font-medium")}
|
||||
className={cn("truncate cursor-pointer text-muted-foreground", selected && "text-primary font-medium")}
|
||||
onClick={() => (selected ? memoFilterStore.setShortcut(undefined) : memoFilterStore.setShortcut(shortcutId))}
|
||||
>
|
||||
{emoji && <span className="text-base mr-1">{emoji}</span>}
|
||||
{title.trim()}
|
||||
</span>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<MoreVerticalIcon className="w-4 h-auto shrink-0 opacity-40 cursor-pointer hover:opacity-70" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" alignOffset={-12}>
|
||||
<div className="flex flex-col text-sm gap-0.5">
|
||||
<button
|
||||
onClick={() => showCreateShortcutDialog({ shortcut })}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<Edit3Icon className="w-4 h-auto" />
|
||||
{t("common.edit")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteShortcut(shortcut)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-destructive hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<TrashIcon className="w-4 h-auto" />
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<MoreVerticalIcon className="w-4 h-auto shrink-0 text-muted-foreground cursor-pointer hover:text-foreground" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" alignOffset={-12}>
|
||||
<DropdownMenuItem onClick={() => showCreateShortcutDialog({ shortcut })}>
|
||||
<Edit3Icon className="w-4 h-auto" />
|
||||
{t("common.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDeleteShortcut(shortcut)}>
|
||||
<TrashIcon className="w-4 h-auto" />
|
||||
{t("common.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import memoFilterStore, { MemoFilter } from "@/store/v2/memoFilter";
|
|||
import { useTranslate } from "@/utils/i18n";
|
||||
import showRenameTagDialog from "../RenameTagDialog";
|
||||
import TagTree from "../TagTree";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -53,7 +54,7 @@ const TagsSection = observer((props: Props) => {
|
|||
{tags.length > 0 && (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<MoreVerticalIcon className="w-4 h-auto shrink-0 opacity-60" />
|
||||
<MoreVerticalIcon className="w-4 h-auto shrink-0 text-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" alignOffset={-12}>
|
||||
<div className="w-auto flex flex-row justify-between items-center gap-2 p-1">
|
||||
|
|
@ -74,32 +75,24 @@ const TagsSection = observer((props: Props) => {
|
|||
key={tag}
|
||||
className="shrink-0 w-auto max-w-full text-sm rounded-md leading-6 flex flex-row justify-start items-center select-none hover:opacity-80 text-muted-foreground"
|
||||
>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="shrink-0 group cursor-pointer">
|
||||
<HashIcon className="group-hover:hidden w-4 h-auto shrink-0 opacity-40" />
|
||||
<MoreVerticalIcon className="hidden group-hover:block w-4 h-auto shrink-0 opacity-60" />
|
||||
<HashIcon className="group-hover:hidden w-4 h-auto shrink-0 text-muted-foreground" />
|
||||
<MoreVerticalIcon className="hidden group-hover:block w-4 h-auto shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" sideOffset={2}>
|
||||
<div className="flex flex-col text-sm gap-0.5">
|
||||
<button
|
||||
onClick={() => showRenameTagDialog({ tag: tag })}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<Edit3Icon className="w-4 h-auto" />
|
||||
{t("common.rename")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteTag(tag)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-destructive hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<TrashIcon className="w-4 h-auto" />
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" sideOffset={2}>
|
||||
<DropdownMenuItem onClick={() => showRenameTagDialog({ tag: tag })}>
|
||||
<Edit3Icon className="w-4 h-auto" />
|
||||
{t("common.rename")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDeleteTag(tag)}>
|
||||
<TrashIcon className="w-4 h-auto" />
|
||||
{t("common.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div
|
||||
className={cn("inline-flex flex-nowrap ml-0.5 gap-0.5 cursor-pointer max-w-[calc(100%-16px)]")}
|
||||
onClick={() => handleTagClick(tag)}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ import toast from "react-hot-toast";
|
|||
import { useLocation } from "react-router-dom";
|
||||
import { markdownServiceClient } from "@/grpcweb";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { memoStore, userStore } from "@/store/v2";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { NodeType } from "@/types/proto/api/v1/markdown_service";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { Button } from "./ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
|
|
@ -163,72 +163,55 @@ const MemoActionMenu = observer((props: Props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<span className={cn("flex justify-center items-center rounded-full hover:opacity-70 cursor-pointer", props.className)}>
|
||||
<MoreVerticalIcon className="w-4 h-4 mx-auto text-muted-foreground" />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" sideOffset={2}>
|
||||
<div className="flex flex-col text-sm gap-0.5">
|
||||
{!readonly && !isArchived && (
|
||||
<>
|
||||
{!isComment && (
|
||||
<button
|
||||
onClick={handleTogglePinMemoBtnClick}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded"
|
||||
>
|
||||
{memo.pinned ? <BookmarkMinusIcon className="w-4 h-auto" /> : <BookmarkPlusIcon className="w-4 h-auto" />}
|
||||
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleEditMemoClick}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<Edit3Icon className="w-4 h-auto" />
|
||||
{t("common.edit")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<button onClick={handleCopyLink} className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded">
|
||||
<CopyIcon className="w-4 h-auto" />
|
||||
{t("memo.copy-link")}
|
||||
</button>
|
||||
)}
|
||||
{!readonly && (
|
||||
<>
|
||||
{!isArchived && !isComment && hasCompletedTaskList && (
|
||||
<button
|
||||
onClick={handleRemoveCompletedTaskListItemsClick}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-primary hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<SquareCheckIcon className="w-4 h-auto" />
|
||||
{t("memo.remove-completed-task-list-items")}
|
||||
</button>
|
||||
)}
|
||||
{!isComment && (
|
||||
<button
|
||||
onClick={handleToggleMemoStatusClick}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-primary hover:bg-muted outline-none rounded"
|
||||
>
|
||||
{isArchived ? <ArchiveRestoreIcon className="w-4 h-auto" /> : <ArchiveIcon className="w-4 h-auto" />}
|
||||
{isArchived ? t("common.restore") : t("common.archive")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleDeleteMemoClick}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-destructive hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<TrashIcon className="w-4 h-auto" />
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-4">
|
||||
<MoreVerticalIcon className="text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" sideOffset={2}>
|
||||
{!readonly && !isArchived && (
|
||||
<>
|
||||
{!isComment && (
|
||||
<DropdownMenuItem onClick={handleTogglePinMemoBtnClick}>
|
||||
{memo.pinned ? <BookmarkMinusIcon className="w-4 h-auto" /> : <BookmarkPlusIcon className="w-4 h-auto" />}
|
||||
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={handleEditMemoClick}>
|
||||
<Edit3Icon className="w-4 h-auto" />
|
||||
{t("common.edit")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<DropdownMenuItem onClick={handleCopyLink}>
|
||||
<CopyIcon className="w-4 h-auto" />
|
||||
{t("memo.copy-link")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!readonly && (
|
||||
<>
|
||||
{!isArchived && !isComment && hasCompletedTaskList && (
|
||||
<DropdownMenuItem onClick={handleRemoveCompletedTaskListItemsClick}>
|
||||
<SquareCheckIcon className="w-4 h-auto" />
|
||||
{t("memo.remove-completed-task-list-items")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!isComment && (
|
||||
<DropdownMenuItem onClick={handleToggleMemoStatusClick}>
|
||||
{isArchived ? <ArchiveRestoreIcon className="w-4 h-auto" /> : <ArchiveIcon className="w-4 h-auto" />}
|
||||
{isArchived ? t("common.restore") : t("common.archive")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={handleDeleteMemoClick}>
|
||||
<TrashIcon className="w-4 h-auto" />
|
||||
{t("common.delete")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ const MemoAttachment: React.FC<Props> = (props: Props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={`w-auto flex flex-row justify-start items-center text-muted-foreground hover:opacity-80 ${className}`}>
|
||||
<div
|
||||
className={`w-auto flex flex-row justify-start items-center text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors ${className}`}
|
||||
>
|
||||
{attachment.type.startsWith("audio") ? (
|
||||
<audio src={attachmentUrl} controls></audio>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
|
|||
if (type === "image/*") {
|
||||
return (
|
||||
<img
|
||||
className={cn("cursor-pointer h-full w-auto rounded-lg border border-border object-contain hover:opacity-80", className)}
|
||||
className={cn(
|
||||
"cursor-pointer h-full w-auto rounded-lg border border-border object-contain hover:border-accent transition-colors",
|
||||
className,
|
||||
)}
|
||||
src={attachment.externalLink ? attachmentUrl : attachmentUrl + "?thumbnail=true"}
|
||||
onClick={() => handleImageClick(attachmentUrl)}
|
||||
decoding="async"
|
||||
|
|
@ -44,7 +47,10 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
|
|||
} else if (type === "video/*") {
|
||||
return (
|
||||
<video
|
||||
className={cn("cursor-pointer h-full w-auto rounded-lg border border-border object-contain bg-popover", className)}
|
||||
className={cn(
|
||||
"cursor-pointer h-full w-auto rounded-lg border border-border object-contain bg-popover hover:border-accent transition-colors",
|
||||
className,
|
||||
)}
|
||||
preload="metadata"
|
||||
crossOrigin="anonymous"
|
||||
src={attachmentUrl}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ interface Props extends BaseProps {
|
|||
|
||||
const Blockquote: React.FC<Props> = ({ children }: Props) => {
|
||||
return (
|
||||
<blockquote className="p-2 border-s-4 rounded border-muted bg-muted">
|
||||
<blockquote className="p-2 border-l-4 rounded border-border bg-muted/50 text-muted-foreground">
|
||||
{children.map((child, index) => (
|
||||
<Renderer key={`${child.type}-${index}`} index={String(index)} node={child} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ interface Props {
|
|||
}
|
||||
|
||||
const Code: React.FC<Props> = ({ content }: Props) => {
|
||||
return <code className="inline break-all px-1 font-mono text-sm rounded opacity-80 bg-muted">{content}</code>;
|
||||
return <code className="inline break-all px-1 font-mono text-sm rounded bg-muted text-muted-foreground">{content}</code>;
|
||||
};
|
||||
|
||||
export default Code;
|
||||
|
|
|
|||
|
|
@ -59,16 +59,16 @@ const CodeBlock: React.FC<Props> = ({ language, content }: Props) => {
|
|||
}, [content]);
|
||||
|
||||
return (
|
||||
<div className="w-full my-1 bg-background border-2 border-l-4 border-popover rounded-md relative">
|
||||
<div className="w-full px-2 mt-1 flex flex-row justify-between items-center text-accent-foreground/60">
|
||||
<span className="text-sm font-mono">{formatedLanguage}</span>
|
||||
<CopyIcon className="w-4 h-auto cursor-pointer hover:opacity-80" onClick={handleCopyButtonClick} />
|
||||
<div className="w-full my-1 bg-card border border-border rounded-md relative">
|
||||
<div className="w-full px-2 py-0.5 flex flex-row justify-between items-center text-muted-foreground">
|
||||
<span className="text-xs font-mono">{formatedLanguage}</span>
|
||||
<CopyIcon className="w-3 h-auto cursor-pointer hover:text-foreground" onClick={handleCopyButtonClick} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-auto">
|
||||
<pre className={cn("no-wrap overflow-auto", "w-full p-2 bg-accent/5 relative")}>
|
||||
<pre className={cn("no-wrap overflow-auto", "w-full p-2 bg-muted/50 relative")}>
|
||||
<code
|
||||
className={cn(`language-${formatedLanguage}`, "block text-sm leading-5")}
|
||||
className={cn(`language-${formatedLanguage}`, "block text-sm leading-5 text-foreground")}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||
></code>
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -67,19 +67,24 @@ const EmbeddedMemo = observer(({ resourceId: uid, params: paramsStr }: Props) =>
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col justify-start items-start w-full px-3 py-2 bg-popover rounded-lg border border-border hover:shadow">
|
||||
<div className="relative flex flex-col justify-start items-start w-full px-3 py-2 bg-card rounded-lg border border-border hover:shadow-md transition-shadow">
|
||||
<div className="w-full mb-1 flex flex-row justify-between items-center text-muted-foreground">
|
||||
<div className="text-sm leading-5 select-none">
|
||||
<relative-time datetime={memo.displayTime?.toISOString()} format="datetime"></relative-time>
|
||||
</div>
|
||||
<div className="flex justify-end items-center gap-1">
|
||||
<span
|
||||
className="text-xs opacity-60 leading-5 cursor-pointer hover:opacity-80"
|
||||
className="text-xs text-muted-foreground leading-5 cursor-pointer hover:text-foreground"
|
||||
onClick={() => copyMemoUid(extractMemoIdFromName(memo.name))}
|
||||
>
|
||||
{extractMemoIdFromName(memo.name).slice(0, 6)}
|
||||
</span>
|
||||
<Link className="opacity-60 hover:opacity-80" to={`/${memo.name}`} state={{ from: context.parentPage }} viewTransition>
|
||||
<Link
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
to={`/${memo.name}`}
|
||||
state={{ from: context.parentPage }}
|
||||
viewTransition
|
||||
>
|
||||
<ArrowUpRightIcon className="w-5 h-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ interface Props {
|
|||
}
|
||||
|
||||
const Highlight: React.FC<Props> = ({ content }: Props) => {
|
||||
return <mark>{content}</mark>;
|
||||
return <mark className="bg-yellow-200 text-foreground px-1 rounded">{content}</mark>;
|
||||
};
|
||||
|
||||
export default Highlight;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ const MermaidBlock: React.FC<Props> = ({ content }: Props) => {
|
|||
}, [content]);
|
||||
|
||||
return (
|
||||
<pre ref={mermaidDockBlock} className="w-full p-2 whitespace-pre-wrap relative">
|
||||
<pre
|
||||
ref={mermaidDockBlock}
|
||||
className="w-full p-2 whitespace-pre-wrap relative bg-card border border-border rounded text-card-foreground"
|
||||
>
|
||||
{content}
|
||||
</pre>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const ReferencedMemo = observer(({ resourceId: uid, params: paramsStr }: Props)
|
|||
|
||||
return (
|
||||
<span
|
||||
className="text-primary whitespace-nowrap cursor-pointer underline break-all hover:opacity-80 decoration-1"
|
||||
className="text-primary whitespace-nowrap cursor-pointer underline break-all hover:text-primary/80 decoration-1"
|
||||
onClick={handleGotoMemoDetailPage}
|
||||
>
|
||||
{displayContent}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ const Spoiler: React.FC<Props> = ({ content }: Props) => {
|
|||
const [isRevealed, setIsRevealed] = useState(false);
|
||||
|
||||
return (
|
||||
<span className={cn("inline cursor-pointer select-none", isRevealed ? "" : "bg-muted")} onClick={() => setIsRevealed(!isRevealed)}>
|
||||
<span
|
||||
className={cn("inline cursor-pointer select-none", isRevealed ? "" : "bg-muted text-muted")}
|
||||
onClick={() => setIsRevealed(!isRevealed)}
|
||||
>
|
||||
<span className={cn(isRevealed ? "opacity-100" : "opacity-0")}>{content}</span>
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const Tag = observer(({ content }: Props) => {
|
|||
|
||||
return (
|
||||
<span
|
||||
className={cn("inline-block w-auto text-primary", context.disableFilter ? "" : "cursor-pointer hover:opacity-80")}
|
||||
className={cn("inline-block w-auto text-primary", context.disableFilter ? "" : "cursor-pointer hover:text-primary/80")}
|
||||
onClick={handleTagClick}
|
||||
>
|
||||
#{content}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const TaskListItem = observer(({ node, complete, children }: Props) => {
|
|||
onCheckedChange={(checked) => handleCheckboxChange(checked === true)}
|
||||
/>
|
||||
</span>
|
||||
<p className={cn(complete && "line-through opacity-80")}>
|
||||
<p className={cn(complete && "line-through text-muted-foreground")}>
|
||||
{children.map((child, index) => (
|
||||
<Renderer key={`${child.type}-${index}`} index={String(index)} node={child} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const MemoDisplaySettingMenu = observer(({ className }: Props) => {
|
|||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -50,7 +50,7 @@ const MemoDisplaySettingMenu = observer(({ className }: Props) => {
|
|||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ const AddMemoRelationPopover = (props: Props) => {
|
|||
placeholder={t("reference.search-placeholder")}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="h-9 mb-2"
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{filteredMemos.length === 0 ? (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const MemoLocationView: React.FC<Props> = (props: Props) => {
|
|||
return (
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<p className="w-full flex flex-row gap-0.5 items-center text-muted-foreground">
|
||||
<p className="w-full flex flex-row gap-0.5 items-center text-muted-foreground hover:text-foreground cursor-pointer transition-colors">
|
||||
<MapPinIcon className="w-4 h-auto shrink-0" />
|
||||
<span className="text-sm font-normal text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
{location.placeholder ? location.placeholder : `[${location.latitude}, ${location.longitude}]`}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ const MemoRelationListView = (props: Props) => {
|
|||
{referencingMemoList.length > 0 && (
|
||||
<button
|
||||
className={cn(
|
||||
"w-auto flex flex-row justify-start items-center text-xs gap-0.5 text-muted-foreground",
|
||||
selectedTab === "referencing" && "text-foreground",
|
||||
"w-auto flex flex-row justify-start items-center text-xs gap-0.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-1 py-0.5 transition-colors",
|
||||
selectedTab === "referencing" && "text-foreground bg-accent",
|
||||
)}
|
||||
onClick={() => setSelectedTab("referencing")}
|
||||
>
|
||||
|
|
@ -48,8 +48,8 @@ const MemoRelationListView = (props: Props) => {
|
|||
{referencedMemoList.length > 0 && (
|
||||
<button
|
||||
className={cn(
|
||||
"w-auto flex flex-row justify-start items-center text-xs gap-0.5 text-muted-foreground",
|
||||
selectedTab === "referenced" && "text-foreground",
|
||||
"w-auto flex flex-row justify-start items-center text-xs gap-0.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-1 py-0.5 transition-colors",
|
||||
selectedTab === "referenced" && "text-foreground bg-accent",
|
||||
)}
|
||||
onClick={() => setSelectedTab("referenced")}
|
||||
>
|
||||
|
|
@ -65,7 +65,7 @@ const MemoRelationListView = (props: Props) => {
|
|||
return (
|
||||
<Link
|
||||
key={memo.name}
|
||||
className="w-auto max-w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:underline"
|
||||
className="w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors"
|
||||
to={`/${memo.name}`}
|
||||
viewTransition
|
||||
state={{
|
||||
|
|
@ -87,7 +87,7 @@ const MemoRelationListView = (props: Props) => {
|
|||
return (
|
||||
<Link
|
||||
key={memo.name}
|
||||
className="w-auto max-w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:underline"
|
||||
className="w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors"
|
||||
to={`/${memo.name}`}
|
||||
viewTransition
|
||||
state={{
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex flex-col justify-start items-start w-full px-4 py-3 mb-2 gap-2 bg-card rounded-lg border border-border",
|
||||
"group relative flex flex-col justify-start items-start w-full px-4 py-3 mb-2 gap-2 bg-card text-card-foreground rounded-lg border border-border transition-colors",
|
||||
"hover:bg-accent hover:text-accent-foreground hover:border-accent",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
@ -139,19 +140,23 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
||||
{props.showCreator && creator ? (
|
||||
<div className="w-full flex flex-row justify-start items-center">
|
||||
<Link className="w-auto hover:opacity-80" to={`/u/${encodeURIComponent(creator.username)}`} viewTransition>
|
||||
<Link
|
||||
className="w-auto hover:bg-accent hover:text-accent-foreground rounded-md p-1 transition-colors"
|
||||
to={`/u/${encodeURIComponent(creator.username)}`}
|
||||
viewTransition
|
||||
>
|
||||
<UserAvatar className="mr-2 shrink-0" avatarUrl={creator.avatarUrl} />
|
||||
</Link>
|
||||
<div className="w-full flex flex-col justify-center items-start">
|
||||
<Link
|
||||
className="w-full block leading-tight hover:opacity-80 truncate text-muted-foreground"
|
||||
className="w-full block leading-tight hover:bg-accent hover:text-accent-foreground rounded-md px-2 py-1 transition-colors truncate text-muted-foreground"
|
||||
to={`/u/${encodeURIComponent(creator.username)}`}
|
||||
viewTransition
|
||||
>
|
||||
{creator.displayName || creator.username}
|
||||
</Link>
|
||||
<div
|
||||
className="w-auto -mt-0.5 text-xs leading-tight text-muted-foreground select-none cursor-pointer"
|
||||
className="w-auto -mt-0.5 text-xs leading-tight text-muted-foreground select-none cursor-pointer hover:text-foreground transition-colors"
|
||||
onClick={handleGotoMemoDetailPage}
|
||||
>
|
||||
{displayTime}
|
||||
|
|
@ -160,7 +165,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-full text-sm leading-tight text-muted-foreground select-none cursor-pointer"
|
||||
className="w-full text-sm leading-tight text-muted-foreground select-none cursor-pointer hover:text-foreground transition-colors"
|
||||
onClick={handleGotoMemoDetailPage}
|
||||
>
|
||||
{displayTime}
|
||||
|
|
@ -172,7 +177,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="flex justify-center items-center hover:opacity-70">
|
||||
<span className="flex justify-center items-center hover:bg-accent hover:text-accent-foreground rounded-md p-1 transition-colors">
|
||||
<VisibilityIcon visibility={memo.visibility} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -184,7 +189,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
{!isInMemoDetailPage && (workspaceMemoRelatedSetting.enableComment || commentAmount > 0) && (
|
||||
<Link
|
||||
className={cn(
|
||||
"flex flex-row justify-start items-center hover:opacity-70",
|
||||
"flex flex-row justify-start items-center hover:bg-accent hover:text-accent-foreground rounded-md p-1 transition-colors",
|
||||
commentAmount === 0 && "invisible group-hover:visible",
|
||||
)}
|
||||
to={`/${memo.name}#comments`}
|
||||
|
|
@ -216,7 +221,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
<EyeOffIcon className="w-4 h-auto text-primary" onClick={() => setShowNSFWContent(false)} />
|
||||
</span>
|
||||
)}
|
||||
<MemoActionMenu className="-ml-1" memo={memo} readonly={readonly} onEdit={() => setShowEditor(true)} />
|
||||
<MemoActionMenu memo={memo} readonly={readonly} onEdit={() => setShowEditor(true)} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -244,7 +249,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
<>
|
||||
<div className="absolute inset-0 bg-transparent" />
|
||||
<button
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 py-2 px-4 text-sm text-muted-foreground hover:text-foreground border border-border rounded-lg bg-card"
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 py-2 px-4 text-sm text-muted-foreground hover:text-foreground hover:bg-accent hover:border-accent border border-border rounded-lg bg-card transition-colors"
|
||||
onClick={() => setShowNSFWContent(true)}
|
||||
>
|
||||
{t("memo.click-to-show-nsfw-content")}
|
||||
|
|
|
|||
|
|
@ -40,25 +40,25 @@ const Navigation = observer((props: Props) => {
|
|||
id: "header-memos",
|
||||
path: Routes.ROOT,
|
||||
title: t("common.memos"),
|
||||
icon: <LibraryIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
icon: <LibraryIcon className="w-6 h-auto shrink-0" />,
|
||||
};
|
||||
const exploreNavLink: NavLinkItem = {
|
||||
id: "header-explore",
|
||||
path: Routes.EXPLORE,
|
||||
title: t("common.explore"),
|
||||
icon: <EarthIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
icon: <EarthIcon className="w-6 h-auto shrink-0" />,
|
||||
};
|
||||
const attachmentsNavLink: NavLinkItem = {
|
||||
id: "header-attachments",
|
||||
path: Routes.ATTACHMENTS,
|
||||
title: t("common.attachments"),
|
||||
icon: <PaperclipIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
icon: <PaperclipIcon className="w-6 h-auto shrink-0" />,
|
||||
};
|
||||
const signInNavLink: NavLinkItem = {
|
||||
id: "header-auth",
|
||||
path: Routes.AUTH,
|
||||
title: t("common.sign-in"),
|
||||
icon: <UserCircleIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
icon: <UserCircleIcon className="w-6 h-auto shrink-0" />,
|
||||
};
|
||||
|
||||
const navLinks: NavLinkItem[] = currentUser ? [homeNavLink, exploreNavLink, attachmentsNavLink] : [exploreNavLink, signInNavLink];
|
||||
|
|
@ -78,9 +78,11 @@ const Navigation = observer((props: Props) => {
|
|||
<NavLink
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"px-2 py-2 rounded-2xl border flex flex-row items-center text-lg text-sidebar-foreground hover:border-border",
|
||||
"px-2 py-2 rounded-2xl border flex flex-row items-center text-lg text-sidebar-foreground transition-colors",
|
||||
collapsed ? "" : "w-full px-4",
|
||||
isActive ? "bg-sidebar-primary text-sidebar-primary-foreground border-border" : "border-transparent",
|
||||
isActive
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground border-sidebar-border"
|
||||
: "border-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:border-sidebar-border",
|
||||
)
|
||||
}
|
||||
key={navLink.id}
|
||||
|
|
@ -106,7 +108,11 @@ const Navigation = observer((props: Props) => {
|
|||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
{currentUser && <UserBanner collapsed={collapsed} />}
|
||||
{currentUser && (
|
||||
<div className={cn("w-full flex flex-col justify-end", props.collapsed ? "items-center" : "items-start pl-3")}>
|
||||
<UserBanner collapsed={collapsed} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,7 +57,12 @@ const ReactionSelector = observer((props: Props) => {
|
|||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<span className={cn("h-7 w-7 flex justify-center items-center rounded-full border hover:opacity-70 cursor-pointer", className)}>
|
||||
<span
|
||||
className={cn(
|
||||
"h-7 w-7 flex justify-center items-center rounded-full border hover:bg-accent hover:text-accent-foreground cursor-pointer transition-colors",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<SmilePlusIcon className="w-4 h-4 mx-auto text-muted-foreground" />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -69,8 +74,8 @@ const ReactionSelector = observer((props: Props) => {
|
|||
<span
|
||||
key={reactionType}
|
||||
className={cn(
|
||||
"inline-flex w-auto text-base cursor-pointer rounded px-1 text-muted-foreground hover:opacity-80",
|
||||
hasReacted(reactionType) && "bg-primary/10",
|
||||
"inline-flex w-auto text-base cursor-pointer rounded px-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors",
|
||||
hasReacted(reactionType) && "bg-primary text-primary-foreground",
|
||||
)}
|
||||
onClick={() => handleReactionClick(reactionType)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { State } from "@/types/proto/api/v1/common";
|
|||
import { User, User_Role } from "@/types/proto/api/v1/user_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import showCreateUserDialog from "../CreateUserDialog";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
|
||||
|
||||
interface LocalState {
|
||||
creatingUser: User;
|
||||
|
|
@ -225,46 +225,33 @@ const MemberSection = observer(() => {
|
|||
{currentUser?.name === user.name ? (
|
||||
<span>{t("common.yourself")}</span>
|
||||
) : (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="flex items-center justify-center p-1 hover:bg-muted rounded">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<MoreVerticalIcon className="w-4 h-auto" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" sideOffset={2}>
|
||||
<div className="flex flex-col gap-0.5 text-sm">
|
||||
<button
|
||||
onClick={() => showCreateUserDialog(user, () => fetchUsers())}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded"
|
||||
>
|
||||
{t("common.update")}
|
||||
</button>
|
||||
{user.state === State.NORMAL ? (
|
||||
<button
|
||||
onClick={() => handleArchiveUserClick(user)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded"
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" sideOffset={2}>
|
||||
<DropdownMenuItem onClick={() => showCreateUserDialog(user, () => fetchUsers())}>
|
||||
{t("common.update")}
|
||||
</DropdownMenuItem>
|
||||
{user.state === State.NORMAL ? (
|
||||
<DropdownMenuItem onClick={() => handleArchiveUserClick(user)}>
|
||||
{t("setting.member-section.archive-member")}
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuItem onClick={() => handleRestoreUserClick(user)}>{t("common.restore")}</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteUserClick(user)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
{t("setting.member-section.archive-member")}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleRestoreUserClick(user)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left hover:bg-muted outline-none rounded"
|
||||
>
|
||||
{t("common.restore")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteUserClick(user)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-destructive hover:bg-muted outline-none rounded"
|
||||
>
|
||||
{t("setting.member-section.delete-member")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{t("setting.member-section.delete-member")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ const MemoRelatedSettings = observer(() => {
|
|||
<div className="mt-2 w-full flex flex-row flex-wrap gap-1">
|
||||
{memoRelatedSetting.reactions.map((reactionType) => {
|
||||
return (
|
||||
<Badge key={reactionType} variant="outline" className="h-9 flex items-center gap-1">
|
||||
<Badge key={reactionType} variant="outline" className="flex items-center gap-1">
|
||||
{reactionType}
|
||||
<X
|
||||
className="w-3 h-3 cursor-pointer hover:text-destructive"
|
||||
|
|
@ -155,7 +155,7 @@ const MemoRelatedSettings = observer(() => {
|
|||
<div className="mt-2 w-full flex flex-row flex-wrap gap-1">
|
||||
{memoRelatedSetting.nsfwTags.map((nsfwTag) => {
|
||||
return (
|
||||
<Badge key={nsfwTag} variant="outline" className="h-9 flex items-center gap-1">
|
||||
<Badge key={nsfwTag} variant="outline" className="flex items-center gap-1">
|
||||
{nsfwTag}
|
||||
<X
|
||||
className="w-3 h-3 cursor-pointer hover:text-destructive"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useTranslate } from "@/utils/i18n";
|
|||
import showChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog";
|
||||
import showUpdateAccountDialog from "../UpdateAccountDialog";
|
||||
import UserAvatar from "../UserAvatar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
|
||||
import AccessTokenSection from "./AccessTokenSection";
|
||||
import UserSessionsSection from "./UserSessionsSection";
|
||||
|
||||
|
|
@ -31,21 +31,18 @@ const MyAccountSection = () => {
|
|||
<PenLineIcon className="w-4 h-4 mx-auto mr-1" />
|
||||
{t("common.edit")}
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<MoreVerticalIcon className="w-4 h-4 mx-auto" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="text-sm p-1">
|
||||
<button
|
||||
onClick={() => showChangeMemberPasswordDialog(user)}
|
||||
className="w-full flex items-center gap-2 px-2 py-1 text-left text-sm hover:bg-muted rounded-md"
|
||||
>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => showChangeMemberPasswordDialog(user)}>
|
||||
{t("setting.account-section.change-password")}
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<UserSessionsSection />
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { useEffect, useState } from "react";
|
|||
import { toast } from "react-hot-toast";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { identityProviderServiceClient } from "@/grpcweb";
|
||||
import { IdentityProvider } from "@/types/proto/api/v1/idp_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import showCreateIdentityProviderDialog from "../CreateIdentityProviderDialog";
|
||||
import LearnMore from "../LearnMore";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
|
||||
const SSOSection = () => {
|
||||
const t = useTranslate();
|
||||
|
|
@ -57,38 +57,28 @@ const SSOSection = () => {
|
|||
<div className="flex flex-row items-center">
|
||||
<p className="ml-2">
|
||||
{identityProvider.title}
|
||||
<span className="text-sm ml-1 opacity-40">({identityProvider.type})</span>
|
||||
<span className="text-sm ml-1 text-muted-foreground">({identityProvider.type})</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="flex items-center justify-center p-1 hover:bg-popover rounded">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<MoreVerticalIcon className="w-4 h-auto" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" sideOffset={2}>
|
||||
<div className="flex flex-col gap-0.5 text-sm">
|
||||
<button
|
||||
onClick={() => showCreateIdentityProviderDialog(identityProvider, fetchIdentityProviderList)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-foreground hover:bg-popover outline-none rounded"
|
||||
>
|
||||
{t("common.edit")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteIdentityProvider(identityProvider)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-destructive hover:bg-popover outline-none rounded"
|
||||
>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" sideOffset={2}>
|
||||
<DropdownMenuItem onClick={() => showCreateIdentityProviderDialog(identityProvider, fetchIdentityProviderList)}>
|
||||
{t("common.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDeleteIdentityProvider(identityProvider)}>{t("common.delete")}</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{identityProviderList.length === 0 && (
|
||||
<div className="w-full mt-2 text-sm border-border opacity-60 flex flex-row items-center justify-between">
|
||||
<div className="w-full mt-2 text-sm border-border text-muted-foreground flex flex-row items-center justify-between">
|
||||
<p className="">{t("setting.sso-section.no-sso-found")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ interface Props {
|
|||
const UserAvatar = (props: Props) => {
|
||||
const { avatarUrl, className } = props;
|
||||
return (
|
||||
<div className={cn(`w-8 h-8 overflow-clip rounded-xl`, className)}>
|
||||
<div className={cn(`w-8 h-8 overflow-clip rounded-xl border border-border`, className)}>
|
||||
<img
|
||||
className="w-full h-auto shadow min-w-full min-h-full object-cover opacity-80"
|
||||
className="w-full h-auto shadow min-w-full min-h-full object-cover"
|
||||
src={avatarUrl || "/full-logo.webp"}
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
|
|||
import { Routes } from "@/router";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import UserAvatar from "./UserAvatar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
|
||||
|
||||
interface Props {
|
||||
collapsed?: boolean;
|
||||
|
|
@ -24,65 +24,44 @@ const UserBanner = (props: Props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-auto px-1 shrink-0">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild disabled={!currentUser}>
|
||||
<div
|
||||
className={cn("w-auto flex flex-row justify-start items-center cursor-pointer text-foreground", collapsed ? "px-1" : "px-3")}
|
||||
>
|
||||
{currentUser.avatarUrl ? (
|
||||
<UserAvatar className="shrink-0" avatarUrl={currentUser.avatarUrl} />
|
||||
) : (
|
||||
<User2Icon className="w-6 mx-auto h-auto opacity-60" />
|
||||
)}
|
||||
{!collapsed && (
|
||||
<span className="ml-2 text-lg font-medium text-foreground grow truncate">
|
||||
{currentUser.displayName || currentUser.username}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="p-1" style={{ zIndex: "9999" }}>
|
||||
<div className="flex flex-col text-sm gap-0.5">
|
||||
<button
|
||||
onClick={() => navigateTo(`/u/${encodeURIComponent(currentUser.username)}`)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-foreground hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<SquareUserIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.profile")}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateTo(Routes.ARCHIVED)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-foreground hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<ArchiveIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.archived")}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateTo(Routes.INBOX)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-foreground hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<BellIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.inbox")}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateTo(Routes.SETTING)}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-foreground hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.settings")}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
className="flex items-center gap-2 px-2 py-1 text-left text-foreground hover:bg-muted outline-none rounded"
|
||||
>
|
||||
<LogOutIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.sign-out")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={!currentUser}>
|
||||
<div className={cn("w-auto flex flex-row justify-start items-center cursor-pointer text-foreground", collapsed ? "px-1" : "px-3")}>
|
||||
{currentUser.avatarUrl ? (
|
||||
<UserAvatar className="shrink-0" avatarUrl={currentUser.avatarUrl} />
|
||||
) : (
|
||||
<User2Icon className="w-6 mx-auto h-auto text-muted-foreground" />
|
||||
)}
|
||||
{!collapsed && (
|
||||
<span className="ml-2 text-lg font-medium text-foreground grow truncate">
|
||||
{currentUser.displayName || currentUser.username}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => navigateTo(`/u/${encodeURIComponent(currentUser.username)}`)}>
|
||||
<SquareUserIcon className="w-4 h-auto text-muted-foreground" />
|
||||
<span className="truncate">{t("common.profile")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigateTo(Routes.ARCHIVED)}>
|
||||
<ArchiveIcon className="w-4 h-auto text-muted-foreground" />
|
||||
<span className="truncate">{t("common.archived")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigateTo(Routes.INBOX)}>
|
||||
<BellIcon className="w-4 h-auto text-muted-foreground" />
|
||||
<span className="truncate">{t("common.inbox")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigateTo(Routes.SETTING)}>
|
||||
<SettingsIcon className="w-4 h-auto text-muted-foreground" />
|
||||
<span className="truncate">{t("common.settings")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleSignOut}>
|
||||
<LogOutIcon className="w-4 h-auto text-muted-foreground" />
|
||||
<span className="truncate">{t("common.sign-out")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const buttonVariants = cva(
|
|||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-border dark:hover:bg-input/50",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxP
|
|||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer border-border dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
205
web/src/components/ui/dropdown-menu.tsx
Normal file
205
web/src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuContent({ className, sideOffset = 4, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-border flex h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function PopoverContent({ className, align = "center", sideOffset = 4, ...props
|
|||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-auto origin-(--radix-popover-content-transform-origin) rounded-md border p-1 shadow-md outline-hidden",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] w-auto origin-(--radix-popover-content-transform-origin) rounded-md border p-1 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof Rad
|
|||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-border text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function SelectTrigger({
|
|||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"border-border data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-2 py-1 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-8 data-[size=sm]:h-7 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -47,7 +47,7 @@ function SelectContent({ className, children, position = "popper", ...props }: R
|
|||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-[60] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
|
|
@ -72,7 +72,11 @@ function SelectContent({ className, children, position = "popper", ...props }: R
|
|||
|
||||
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label data-slot="select-label" className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} {...props} />
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1 text-xs select-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"border-border placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ function TooltipContent({ className, sideOffset = 0, children, ...props }: React
|
|||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[70] w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-[70] size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const HomeLayout = observer(() => {
|
|||
</MobileHeader>
|
||||
)}
|
||||
{md && (
|
||||
<div className={cn("fixed top-0 left-16 shrink-0 h-svh transition-all", "border-x border-border", lg ? "w-72" : "w-56")}>
|
||||
<div className={cn("fixed top-0 left-16 shrink-0 h-svh transition-all", "border-r border-border", lg ? "w-72" : "w-56")}>
|
||||
<HomeSidebar className={cn("px-3 py-6")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,13 @@ const RootLayout = observer(() => {
|
|||
) : (
|
||||
<div className="w-full min-h-full flex flex-row justify-center items-start sm:pl-16">
|
||||
{sm && (
|
||||
<div className={cn("group flex flex-col justify-start items-start fixed top-0 left-0 select-none h-full bg-sidebar", "w-16 px-2")}>
|
||||
<div
|
||||
className={cn(
|
||||
"group flex flex-col justify-start items-start fixed top-0 left-0 select-none h-full bg-sidebar",
|
||||
"w-16 px-2",
|
||||
"border-r border-border",
|
||||
)}
|
||||
>
|
||||
<Navigation collapsed={true} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -55,23 +55,25 @@ const Home = observer(() => {
|
|||
}, [user, memoFilterStore.filters, viewStore.state.orderByTimeAsc]);
|
||||
|
||||
return (
|
||||
<PagedMemoList
|
||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />}
|
||||
listSort={(memos: Memo[]) =>
|
||||
memos
|
||||
.filter((memo) => memo.state === State.NORMAL)
|
||||
.sort((a, b) =>
|
||||
viewStore.state.orderByTimeAsc
|
||||
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
|
||||
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
|
||||
)
|
||||
.sort((a, b) => Number(b.pinned) - Number(a.pinned))
|
||||
}
|
||||
owner={user.name}
|
||||
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
|
||||
filter={selectedShortcut?.filter || ""}
|
||||
oldFilter={memoListFilter}
|
||||
/>
|
||||
<div className="w-full min-h-full bg-background text-foreground">
|
||||
<PagedMemoList
|
||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />}
|
||||
listSort={(memos: Memo[]) =>
|
||||
memos
|
||||
.filter((memo) => memo.state === State.NORMAL)
|
||||
.sort((a, b) =>
|
||||
viewStore.state.orderByTimeAsc
|
||||
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
|
||||
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
|
||||
)
|
||||
.sort((a, b) => Number(b.pinned) - Number(a.pinned))
|
||||
}
|
||||
owner={user.name}
|
||||
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
|
||||
filter={selectedShortcut?.filter || ""}
|
||||
oldFilter={memoListFilter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue