chore: unify colors

pull/4722/merge
Johnny 4 weeks ago
parent 2e474d37fd
commit 35928ce5ba

@ -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",

@ -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}

@ -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…
Cancel
Save