feat(web): split the home page into quick share and spaces zones
Two side-by-side zones (stacked on narrow screens) with an icon and a one-line positioning each: quick share is temporary, read-only and needs no account; spaces are long-lived, multi-user and need a login. Shorter copy, my shares listed under the share input, and an empty state for spaces.
This commit is contained in:
1 parent
0d31e3d058
commit
d0aefb3d6e
5 files changed
+46
-64
No files matched your search
@@ -3,6 +3,14 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.quiet {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -11,9 +11,11 @@ interface Props {
|
||||
kind: 'channel' | 'share';
|
||||
title: string;
|
||||
onOpen: (c: ChannelInfo | Share) => void;
|
||||
/** 标题只给屏幕阅读器(所在分区已有标题) */
|
||||
quietTitle?: boolean;
|
||||
}
|
||||
|
||||
export function ChannelList({ kind, title, onOpen }: Props) {
|
||||
export function ChannelList({ kind, title, onOpen, quietTitle = false }: Props) {
|
||||
const channels = useChannels((s) => s.list);
|
||||
const forgetChannel = useChannels((s) => s.forget);
|
||||
const shares = useShares((s) => s.list);
|
||||
@@ -24,7 +26,7 @@ export function ChannelList({ kind, title, onOpen }: Props) {
|
||||
|
||||
return (
|
||||
<section aria-label={title}>
|
||||
<h2 className={styles.h}>{title}</h2>
|
||||
<h2 className={`${styles.h} ${quietTitle ? styles.quiet : ''}`}>{title}</h2>
|
||||
<ul className={styles.list}>
|
||||
{list.map((c) => (
|
||||
<li key={c.id} className={styles.row}>
|
||||
|
||||
@@ -4,10 +4,12 @@ import { buildInfo } from '@/lib/buildInfo';
|
||||
import type { ChannelInfo, Share } from '@/core-bridge/types';
|
||||
import type { ClipboardPayload } from '@/lib/clipboard';
|
||||
import { useGlobalPaste } from '@/lib/useGlobalPaste';
|
||||
import { useChannels } from '@/store/channels';
|
||||
import { useShares } from '@/store/shares';
|
||||
import { solveCreationCaptcha } from '@/core-bridge/captcha';
|
||||
import { errorMessage } from '@/core-bridge/errors';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { ThemeMenu } from '@/ui/ThemeMenu';
|
||||
import { ChannelList } from './ChannelList';
|
||||
import { CreateSheet } from './CreateSheet';
|
||||
@@ -16,10 +18,10 @@ import styles from './Home.module.css';
|
||||
|
||||
const ScanSheet = lazy(() => import('@/features/scan/ScanSheet').then((m) => ({ default: m.ScanSheet })));
|
||||
|
||||
/** 初始状态:什么都没创建时看到的页面。核心是「快速分享」 */
|
||||
/** 首页:左边「便捷分享」(临时、只读、对方无需账号),右边「空间」(长期、多人、登录使用) */
|
||||
export function Home() {
|
||||
const createDraft = useShares((s) => s.createDraft);
|
||||
const shares = useShares((s) => s.list);
|
||||
const channels = useChannels((s) => s.list);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [scanOpen, setScanOpen] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -59,13 +61,21 @@ export function Home() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.appearance}><ThemeMenu /></div>
|
||||
<main className={`${styles.main} ${styles.dashboard} ${shares.length ? styles.hasShares : ''}`}>
|
||||
<main className={`${styles.main} ${styles.dashboard}`}>
|
||||
<header className={`${styles.head} ${styles.dashboardHead}`}>
|
||||
<h1 className={styles.logo}>WebDrop</h1>
|
||||
<p className={styles.sub}>在自己的设备之间快速传文件和文本,也可以分享给别人。</p>
|
||||
<p className={styles.sub}>设备之间传文件,也能分享给别人。</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.primaryColumn}>
|
||||
<section className={styles.zone} aria-labelledby="share-title">
|
||||
<div className={styles.zoneHead}>
|
||||
<span className={styles.zoneIcon}><Icon name="share" size={20} /></span>
|
||||
<div>
|
||||
<h2 id="share-title" className={styles.zoneTitle}>便捷分享</h2>
|
||||
<p className={styles.zoneHint}>临时 · 只读 · 对方无需账号</p>
|
||||
</div>
|
||||
<span className={styles.zoneBadge}><Icon name="lock" size={13} /> 端到端加密</span>
|
||||
</div>
|
||||
<QuickShare
|
||||
busy={busy}
|
||||
text={shareText}
|
||||
@@ -75,24 +85,24 @@ export function Home() {
|
||||
onShare={share}
|
||||
/>
|
||||
{failedMessage ? <p role="alert" className={styles.error}>创建分享失败:{failedMessage}</p> : null}
|
||||
<ChannelList kind="share" title="我的分享" onOpen={open} />
|
||||
</section>
|
||||
|
||||
<section className={styles.channels} aria-labelledby="channels-title">
|
||||
<div className={styles.sectionHead}>
|
||||
<h2 id="channels-title">空间工作区</h2>
|
||||
<p>同一账号跨设备使用,也可邀请其他账号共同使用频道与文件。</p>
|
||||
<section className={styles.zone} aria-labelledby="space-title">
|
||||
<div className={styles.zoneHead}>
|
||||
<span className={styles.zoneIcon}><Icon name="folder" size={20} /></span>
|
||||
<div>
|
||||
<h2 id="space-title" className={styles.zoneTitle}>空间</h2>
|
||||
<p className={styles.zoneHint}>长期 · 多人 · 登录使用</p>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<Button icon="edit" onClick={() => setCreateOpen(true)}>新建空间</Button>
|
||||
<Button icon="scan" onClick={() => setScanOpen(true)}>加入空间</Button>
|
||||
</div>
|
||||
|
||||
<ChannelList kind="channel" title="我的空间" onOpen={open} />
|
||||
<p className={styles.note}>空间密钥按账号加密分发,登录其他设备后可自动恢复。已授权成员可读取空间历史。</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{shares.length ? <aside className={styles.secondaryColumn} aria-label="我的分享"><ChannelList kind="share" title="我的分享" onOpen={open} /></aside> : null}
|
||||
|
||||
</div>
|
||||
<ChannelList kind="channel" title="我的空间" onOpen={open} quietTitle />
|
||||
{channels.length === 0 ? <p className={styles.empty}>还没有空间。</p> : null}
|
||||
<div className={styles.actions}>
|
||||
<Button icon="plus" onClick={() => setCreateOpen(true)}>新建空间</Button>
|
||||
<Button icon="scan" onClick={() => setScanOpen(true)}>加入空间</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside className={styles.buildInfo} aria-label="构建信息">
|
||||
|
||||
@@ -14,36 +14,6 @@
|
||||
background: var(--busy-soft);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: var(--space-1) 0 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--ok-soft);
|
||||
color: var(--ok);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
min-height: 112px;
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
onShare: (payload: ClipboardPayload) => Promise<boolean>;
|
||||
}
|
||||
|
||||
/** 临时分享入口:先收集文本和文件,确认后再建立草稿。 */
|
||||
/** 便捷分享的输入区:先收集文本和文件,确认后再建立草稿。标题与说明由 Home 的分区头提供 */
|
||||
export function QuickShare({ busy, text, files, onTextChange, onFilesChange, onShare }: Props) {
|
||||
const touch = useMediaQuery('(pointer: coarse)');
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
@@ -59,18 +59,10 @@ export function QuickShare({ busy, text, files, onTextChange, onFilesChange, onS
|
||||
<input ref={galleryRef} type="file" accept="image/*,video/*" multiple hidden onChange={(event) => { pick(event.target.files); event.target.value = ''; }} />
|
||||
<input ref={cameraRef} type="file" accept="image/*" capture="environment" hidden onChange={(event) => { pick(event.target.files); event.target.value = ''; }} />
|
||||
|
||||
<header className={styles.head}>
|
||||
<div>
|
||||
<h2 className={styles.title}>快速分享</h2>
|
||||
<p className={styles.hint}>临时文本直接写在这里,也可以拖入文件。内容封存后只读。</p>
|
||||
</div>
|
||||
<span className={styles.tag}><Icon name="lock" size={13} /> 端到端加密</span>
|
||||
</header>
|
||||
|
||||
<textarea
|
||||
className={styles.editor}
|
||||
aria-label="临时分享文本"
|
||||
placeholder="输入或粘贴要分享的文本…"
|
||||
placeholder="输入文字,或拖入文件"
|
||||
value={text}
|
||||
onChange={(event) => onTextChange(event.target.value)}
|
||||
rows={4}
|
||||
@@ -101,7 +93,7 @@ export function QuickShare({ busy, text, files, onTextChange, onFilesChange, onS
|
||||
))}
|
||||
</div>
|
||||
<button type="button" className={styles.submit} disabled={busy || !canShare} onClick={() => void onShare({ files, text })}>
|
||||
{busy ? '正在创建分享…' : '创建临时分享'}
|
||||
{busy ? '创建中…' : '创建分享'}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user