) {
+ return (
+
+ )
+}
+
+function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
+ return (
+ a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+}
diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx
new file mode 100644
index 0000000..ce22b98
--- /dev/null
+++ b/src/components/ui/input-group.tsx
@@ -0,0 +1,158 @@
+'use client'
+
+import { cva, type VariantProps } from 'class-variance-authority'
+import { cn } from 'cn'
+import type * as React from 'react'
+
+import { Button } from '#/components/ui/button.tsx'
+import { Input } from '#/components/ui/input.tsx'
+import { Textarea } from '#/components/ui/textarea.tsx'
+
+function InputGroup({ className, ...props }: React.ComponentProps<'fieldset'>) {
+ return (
+
{connection.platform === 'mastodon' && (
-
+
{connection.status !== 'disconnected' && (
-
+
)}
)}
@@ -139,30 +147,35 @@ export function ConnectionManager({
) : instances.isError ? (
接続先を取得できませんでした。
-
) : instances.data.origins.length === 0 ? (
利用するMastodonサーバーがまだ設定されていません。
) : (
<>
-
-
-
+
+
Mastodonで認可する
-
+
>
)}
{hasTemporaryDecks && (
diff --git a/src/features/decks/column-editor.test.tsx b/src/features/decks/column-editor.test.tsx
index 0e3e4b0..8b38f20 100644
--- a/src/features/decks/column-editor.test.tsx
+++ b/src/features/decks/column-editor.test.tsx
@@ -162,7 +162,7 @@ it('preserves title and Twitter search options after a rejected save', async ()
fireEvent.change(screen.getByLabelText('表示順'), {
target: { value: 'Top' },
})
- fireEvent.click(screen.getByLabelText('フォロー中のみ'))
+ fireEvent.click(screen.getByRole('checkbox', { name: 'フォロー中のみ' }))
fireEvent.submit(screen.getByRole('form', { name: 'カラムを追加' }))
await waitFor(() => expect(save).toHaveBeenCalledOnce())
expect(screen.getByLabelText('カラム名')).toHaveValue('調査')
@@ -170,7 +170,7 @@ it('preserves title and Twitter search options after a rejected save', async ()
'from:owner WebMCP',
)
expect(screen.getByLabelText('表示順')).toHaveValue('Top')
- expect(screen.getByLabelText('フォロー中のみ')).toBeChecked()
+ expect(screen.getByRole('checkbox', { name: 'フォロー中のみ' })).toBeChecked()
fireEvent.submit(screen.getByRole('form', { name: 'カラムを追加' }))
await waitFor(() => expect(save).toHaveBeenCalledTimes(2))
expect(save.mock.calls[1]?.[0].source).toEqual(save.mock.calls[0]?.[0].source)
diff --git a/src/features/decks/column-editor.tsx b/src/features/decks/column-editor.tsx
index 23b4022..11ac31b 100644
--- a/src/features/decks/column-editor.tsx
+++ b/src/features/decks/column-editor.tsx
@@ -1,4 +1,8 @@
import { useId, useState } from 'react'
+import { Button } from '#/components/ui/button'
+import { Input } from '#/components/ui/input'
+import { Label } from '#/components/ui/label'
+import { NativeSelect, NativeSelectOption } from '#/components/ui/native-select'
import type { Connection } from '#/features/connections/model'
import {
ColumnSourceEditor,
@@ -60,9 +64,9 @@ export function ColumnEditor({
}}
>
-
)}
-
+
{column ? '変更を保存' : '追加して検索'}
-
-
+
+
キャンセル
-
+
)
diff --git a/src/features/decks/deck-column.tsx b/src/features/decks/deck-column.tsx
index 17f717f..fb6019c 100644
--- a/src/features/decks/deck-column.tsx
+++ b/src/features/decks/deck-column.tsx
@@ -1,4 +1,13 @@
import { Icon } from '#/components/icon'
+import { Badge } from '#/components/ui/badge'
+import { Button } from '#/components/ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '#/components/ui/dropdown-menu'
import {
flattenResearchPages,
useResearchFeed,
@@ -53,8 +62,9 @@ export function ResearchColumn({
{column.title}
-
-
-
-
+
+
+ }
>
-
-
-
- 編集
-
- {
- onMove(-1)
- event.currentTarget
- .closest('details')
- ?.removeAttribute('open')
- }}
+
+
+ 編集
+ onMove(-1)}
disabled={first}
aria-label={`${column.title}を左へ`}
>
← 左へ移動
-
- {
- onMove(1)
- event.currentTarget
- .closest('details')
- ?.removeAttribute('open')
- }}
+
+ onMove(1)}
disabled={last}
aria-label={`${column.title}を右へ`}
>
右へ移動 →
-
-
+
+
カラムを削除
-
-
-
+
+
+
{connectionLabel ?? '接続が見つかりません'}
-
+
{sourceDescription.label}
-
+
{sourceDescription.query}
@@ -152,7 +153,8 @@ export function ResearchColumn({
{error.message}
{error.retryable ? (
-
@@ -162,7 +164,7 @@ export function ResearchColumn({
}
>
再試行
-
+
) : null}
) : null}
@@ -171,13 +173,14 @@ export function ResearchColumn({
) : null}
{query.hasNextPage && !query.isFetchNextPageError ? (
- void query.fetchNextPage({ cancelRefetch: false })}
>
続きを読み込む
-
+
) : null}
{!query.isPending &&
diff --git a/src/features/decks/deck-page.tsx b/src/features/decks/deck-page.tsx
index f35141a..6865159 100644
--- a/src/features/decks/deck-page.tsx
+++ b/src/features/decks/deck-page.tsx
@@ -5,6 +5,16 @@ import { useRef, useState } from 'react'
import { AppShell } from '#/components/app-shell'
import { Dialog } from '#/components/dialog'
import { Icon } from '#/components/icon'
+import { Button } from '#/components/ui/button'
+import {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '#/components/ui/empty'
+import { Input } from '#/components/ui/input'
import { ConnectionManager } from '#/features/connections/connection-manager'
import { loadConnections } from '#/features/connections/server-functions'
import { syncResearchDeck } from '#/features/research/research-deck-sync'
@@ -32,7 +42,9 @@ export function DeckPage() {
const [editing, setEditing] = useState<{ id: string } | 'new' | null>(null)
const [renaming, setRenaming] = useState(false)
const [managingConnections, setManagingConnections] = useState(false)
- const [switcherEditing, setSwitcherEditing] = useState(false)
+ const [deckDialog, setDeckDialog] = useState<'create' | 'delete' | null>(null)
+ const [newDeckTitle, setNewDeckTitle] = useState('')
+ const switcherEditing = deckDialog !== null
const {
deck,
workspace,
@@ -83,6 +95,8 @@ export function DeckPage() {
})
function clearEditors() {
+ setDeckDialog(null)
+ setNewDeckTitle('')
setEditing(null)
setRenaming(false)
setRemoved(undefined)
@@ -102,312 +116,357 @@ export function DeckPage() {
return saved
}
+ const storageErrorNotice = storageError ? (
+
+ {storageError}
+ {
+ clearEditors()
+ void refresh(true)
+ }}
+ >
+ 最新のデッキを再読み込み
+
+
+ ) : null
+
return (
- {
- await syncResearchDeck(plan, activate, researchDeckBindings, {
- getWorkspace,
- createTemporary,
- save,
- select,
- })
- if (activate) clearEditors()
- }}
- />
- }
- sidebar={
- <>
- {
- select(id)
- clearEditors()
- }}
- onCreate={async (title) => {
- const saved = await create(title)
- if (saved) clearEditors()
- return saved
- }}
- onDelete={async () => {
- const saved = await remove(deck.id)
- if (saved) clearEditors()
- return saved
- }}
- />
- = MAX_COLUMNS}
- onClick={() => setEditing('new')}
- >
-
- カラムを追加
-
-
- {deck.persisted
- ? 'サーバーに保存・端末間で共有'
- : '一時ビュー · このタブのみ'}
-
- setManagingConnections(true)}
- >
- 接続アカウントを管理
-
- >
- }
+ {
+ await syncResearchDeck(plan, activate, researchDeckBindings, {
+ getWorkspace,
+ createTemporary,
+ save,
+ select,
+ })
+ if (activate) clearEditors()
+ }}
>
-
-
-
{deck.title}
-
- {deck.columns.length} / {MAX_COLUMNS} カラム
-
-
-
- {deck.persisted ? (
- {
- createTemporary(`${deck.title}のコピー`, deck.columns)
- clearEditors()
- }}
- >
- 一時コピーを作成
-
- ) : (
- void persist(deck.id)}
- >
- デッキとして保存
-
- )}
- setRenaming(!renaming)}
- >
-
-
-
-
- {saving ? (
-
- 保存しています…
-
- ) : null}
- {legacyAvailable ? (
-
- このブラウザに旧デッキがあります。
- void importLegacy()}
- >
- 旧デッキを取り込む
-
-
- ) : null}
- {!ready ? (
-
- デッキを読み込んでいます…
-
- ) : null}
- {profiles.isError ? (
-
- 接続プロファイルを取得できませんでした。
- void profiles.refetch()}>
- 再試行
-
-
- ) : null}
- {oauthResult === 'failed' ? (
-
- Mastodonの認可を完了できませんでした。接続アカウントからやり直してください。
-
- ) : null}
- {oauthResult === 'connected' ? (
-
- Mastodonアカウントを接続しました。カラムの接続先に選べます。
-
- ) : null}
- {profiles.data?.relayError ? (
-
- {profiles.data.relayError}
-
- ) : null}
- {managingConnections ? (
-
- ) : null}
- {profiles.isPending ? (
- 接続プロファイルを取得しています…
- ) : null}
- {storageError ? (
-
- {storageError}
- {
- clearEditors()
- void refresh(true)
- }}
- >
- 最新のデッキを再読み込み
-
-
- ) : null}
- {renaming ? (
-
- ) : null}
- {editing !== null ? (
-
- ) : null}
- {removed ? (
-
- 「{removed.column.title}」を削除しました。
- = MAX_COLUMNS || editing === 'new'}
- onClick={async () => {
- const columns = [...deck.columns]
- columns.splice(removed.index, 0, removed.column)
- if (await save({ ...deck, columns })) setRemoved(undefined)
- }}
- >
- 元に戻す
-
-
- ) : null}
- {ready && deck.columns.length === 0 && editing === null ? (
-
-
-
調べたいことから、デッキを作りましょう
-
- チャットでAIに調べたいことを伝えると、調査に合ったカラムを作成します。
- 検索・ユーザー投稿・リストのカラムを手動で追加することもできます。
-
-
setEditing('new')}
- >
- 手動でカラムを追加
-
-
- ) : null}
- {ready && deck.columns.length > 0 ? (
- // biome-ignore lint/a11y/noNoninteractiveTabindex: Keyboard users must be able to scroll the column board.
-
- {deck.columns.map((column, index) => (
- connection.id === column.connectionId,
- )?.displayName
- }
- first={index === 0}
- last={index === deck.columns.length - 1}
- onEdit={() => setEditing({ id: column.id })}
- onMove={(direction) =>
- save(moveColumn(deck, column.id, direction))
- }
- onRemove={async () => {
- const saved = await save({
- ...deck,
- columns: deck.columns.filter(
- (current) => current.id !== column.id,
- ),
- })
- if (saved) setRemoved({ column, index })
- if (typeof editing === 'object' && editing?.id === column.id)
- setEditing(null)
- }}
- />
- ))}
-
- ) : null}
-
+
+
+
{deck.title}
+
+ {deck.columns.length} / {MAX_COLUMNS} カラム
+
+
+
+ {deck.persisted ? (
+ {
+ createTemporary(`${deck.title}のコピー`, deck.columns)
+ clearEditors()
+ }}
+ >
+ 一時コピーを作成
+
+ ) : (
+ void persist(deck.id)}
+ >
+ デッキとして保存
+
+ )}
+ setRenaming(!renaming)}
+ >
+
+
+
+
+ {saving ? (
+
+ 保存しています…
+
+ ) : null}
+ {legacyAvailable ? (
+
+ このブラウザに旧デッキがあります。
+ void importLegacy()}
+ >
+ 旧デッキを取り込む
+
+
+ ) : null}
+ {!ready ? (
+
+ デッキを読み込んでいます…
+
+ ) : null}
+ {profiles.isError ? (
+
+ 接続プロファイルを取得できませんでした。
+ void profiles.refetch()}
+ >
+ 再試行
+
+
+ ) : null}
+ {oauthResult === 'failed' ? (
+
+ Mastodonの認可を完了できませんでした。接続アカウントからやり直してください。
+
+ ) : null}
+ {oauthResult === 'connected' ? (
+
+ Mastodonアカウントを接続しました。カラムの接続先に選べます。
+
+ ) : null}
+ {profiles.data?.relayError ? (
+
+ {profiles.data.relayError}
+
+ ) : null}
+ {managingConnections ? (
+
+ ) : null}
+ {profiles.isPending ? (
+ 接続プロファイルを取得しています…
+ ) : null}
+ {!renaming && editing === null && !switcherEditing
+ ? storageErrorNotice
+ : null}
+ {renaming ? (
+
+ ) : null}
+ {editing !== null ? (
+
+ ) : null}
+ {removed ? (
+
+ 「{removed.column.title}」を削除しました。
+ = MAX_COLUMNS || editing === 'new'
+ }
+ onClick={async () => {
+ const columns = [...deck.columns]
+ columns.splice(removed.index, 0, removed.column)
+ if (await save({ ...deck, columns })) setRemoved(undefined)
+ }}
+ >
+ 元に戻す
+
+
+ ) : null}
+ {ready && deck.columns.length === 0 && editing === null ? (
+
+
+
+
+
+
+ 調べたいことから、デッキを作りましょう
+
+
+ チャットでAIに調べたいことを伝えると、調査に合ったカラムを作成します。
+ 検索・ユーザー投稿・リストのカラムを手動で追加することもできます。
+
+
+
+ setEditing('new')}>
+ 手動でカラムを追加
+
+
+
+ ) : null}
+ {ready && deck.columns.length > 0 ? (
+
+ {deck.columns.map((column, index) => (
+ connection.id === column.connectionId,
+ )?.displayName
+ }
+ first={index === 0}
+ last={index === deck.columns.length - 1}
+ onEdit={() => setEditing({ id: column.id })}
+ onMove={(direction) =>
+ save(moveColumn(deck, column.id, direction))
+ }
+ onRemove={async () => {
+ const saved = await save({
+ ...deck,
+ columns: deck.columns.filter(
+ (current) => current.id !== column.id,
+ ),
+ })
+ if (saved) setRemoved({ column, index })
+ if (
+ typeof editing === 'object' &&
+ editing?.id === column.id
+ )
+ setEditing(null)
+ }}
+ />
+ ))}
+
+ ) : null}
+
+ )}
+
)
}
diff --git a/src/features/decks/deck-switcher.tsx b/src/features/decks/deck-switcher.tsx
index f524735..1d569ea 100644
--- a/src/features/decks/deck-switcher.tsx
+++ b/src/features/decks/deck-switcher.tsx
@@ -1,6 +1,10 @@
-import { useEffect, useState } from 'react'
+import type { ReactNode } from 'react'
import { Dialog } from '#/components/dialog'
import { Icon } from '#/components/icon'
+import { Button } from '#/components/ui/button'
+import { Input } from '#/components/ui/input'
+import { Label } from '#/components/ui/label'
+import { NativeSelect, NativeSelectOption } from '#/components/ui/native-select'
import type { DeckView } from './model'
export function DeckSwitcher({
@@ -10,7 +14,11 @@ export function DeckSwitcher({
onSelect,
onCreate,
onDelete,
- onEditingChange,
+ dialog,
+ onDialogChange,
+ newTitle,
+ onNewTitleChange,
+ errorNotice,
}: {
decks: DeckView[]
activeId: string
@@ -18,109 +26,118 @@ export function DeckSwitcher({
onSelect: (id: string) => void
onCreate: (title: string) => Promise
onDelete: () => Promise
- onEditingChange: (editing: boolean) => void
+ dialog: 'create' | 'delete' | null
+ onDialogChange: (dialog: 'create' | 'delete' | null) => void
+ newTitle: string
+ onNewTitleChange: (title: string) => void
+ errorNotice?: ReactNode
}) {
- const [creating, setCreating] = useState(false)
- const [deleting, setDeleting] = useState(false)
- useEffect(() => {
- onEditingChange(creating || deleting)
- return () => onEditingChange(false)
- }, [creating, deleting, onEditingChange])
return (
- デッキプロファイル
-
-
+ {
- setCreating(!creating)
- setDeleting(false)
+ onDialogChange('create')
}}
>
デッキを作成
-
-
+ {
- setDeleting(!deleting)
- setCreating(false)
+ onDialogChange('delete')
}}
>
デッキを削除
-
+
- {creating ? (
-
diff --git a/src/features/decks/decks.css b/src/features/decks/decks.css
index b7cc157..a895e19 100644
--- a/src/features/decks/decks.css
+++ b/src/features/decks/decks.css
@@ -1,47 +1,3 @@
-/* Hallmark · macrostructure: Workbench · tone: compact · anchor hue: Twitter blue */
-/* Hallmark · pre-emit critique: P5 H5 E4 S5 R5 V4 */
-.app-deck button {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: var(--space-xs);
- min-height: 36px;
- padding: var(--space-xs) var(--space-sm);
- border: var(--rule-hairline) solid var(--control-border);
- border-radius: var(--radius-sm);
- color: var(--ink);
- background: var(--surface);
- cursor: pointer;
- white-space: nowrap;
-}
-.app-deck button:hover:not(:disabled) {
- background: var(--color-paper-3);
-}
-.app-deck button:active:not(:disabled) {
- color: var(--accent);
-}
-.app-deck button:disabled {
- opacity: 0.45;
- cursor: default;
-}
-.app-deck .icon-button {
- width: 36px;
- height: 36px;
- padding: var(--space-xs);
- border-color: transparent;
- background: transparent;
-}
-.app-deck .deck-primary {
- color: var(--color-accent-ink);
- background: var(--accent);
- border-color: var(--accent);
- font-weight: 650;
-}
-.app-deck .deck-primary:hover:not(:disabled),
-.app-deck .deck-primary:active:not(:disabled) {
- background: var(--accent-hover);
- color: var(--color-accent-ink);
-}
.deck-heading {
display: flex;
align-items: center;
@@ -61,7 +17,7 @@
}
.deck-heading p {
margin: var(--space-2xs) 0 0;
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-xs);
}
.deck-actions {
@@ -73,7 +29,7 @@
.deck-help,
.deck-save-status,
.deck-column-status {
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-xs);
line-height: 1.5;
}
@@ -130,7 +86,7 @@
align-items: center;
flex-wrap: wrap;
gap: var(--space-xs);
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-xs);
}
.deck-column-label span {
@@ -149,7 +105,7 @@
.deck-query {
margin: var(--space-2xs) 0;
font-size: var(--text-xs);
- color: var(--secondary);
+ color: var(--muted-foreground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -166,47 +122,9 @@
scrollbar-width: thin;
scrollbar-color: var(--control-border) var(--paper);
}
-.column-menu {
- position: relative;
-}
-.column-menu summary {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border-radius: var(--radius-sm);
- list-style: none;
- cursor: pointer;
-}
-.column-menu summary::-webkit-details-marker {
- display: none;
-}
-.column-menu summary:hover,
-.column-menu[open] summary {
- background: var(--color-paper-3);
-}
-.column-menu-panel {
- position: absolute;
- top: 100%;
- right: 0;
- z-index: 5;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- min-width: 158px;
- padding: var(--space-2xs);
- border: var(--rule-hairline) solid var(--control-border);
- background: var(--surface);
- box-shadow: 0 var(--space-xs) var(--space-lg) var(--color-shadow);
-}
-.column-menu-panel button {
- justify-content: flex-start;
- border: 0;
-}
.deck-message {
padding: var(--space-md);
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-sm);
line-height: 1.6;
}
@@ -223,11 +141,6 @@
.research-post header > div {
min-width: 0;
}
-.research-post header img {
- flex-shrink: 0;
- border-radius: var(--radius-sm);
- object-fit: cover;
-}
.research-post strong {
font-size: var(--text-sm);
}
@@ -250,11 +163,11 @@
justify-content: space-between;
gap: var(--space-xs);
margin-top: var(--space-sm);
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-xs);
}
.research-post a {
- color: var(--accent);
+ color: var(--primary);
text-underline-offset: 0.2em;
}
.research-media {
@@ -265,34 +178,6 @@
margin-block: var(--space-xs);
border-radius: var(--radius-sm);
}
-.deck-dialog {
- width: min(480px, calc(100vw - 32px));
- max-height: calc(100dvh - 32px);
- padding: var(--space-lg);
- overflow-y: auto;
- border: var(--rule-hairline) solid var(--control-border);
- border-radius: var(--radius-md);
- background: var(--paper);
- color: var(--ink);
- box-shadow: 0 var(--space-md) var(--space-xl) var(--color-shadow);
-}
-.deck-dialog::backdrop {
- background: var(--color-overlay);
-}
-.dialog-heading {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-xs);
- margin-bottom: var(--space-md);
-}
-.dialog-heading h2,
-.deck-editor h2,
-.deck-empty h2 {
- margin: 0;
- font-size: var(--text-md);
- overflow-wrap: anywhere;
-}
.deck-editor {
margin: 0;
padding: 0;
@@ -312,30 +197,12 @@
font-size: var(--text-sm);
min-width: 0;
}
-.deck-editor input:not([type="checkbox"]),
-.deck-editor select,
-.deck-rename input,
-.deck-workspaces select {
- min-width: 0;
- width: 100%;
- min-height: 40px;
- padding: var(--space-xs);
- border: var(--rule-hairline) solid var(--control-border);
- border-radius: var(--radius-sm);
- background: var(--canvas);
- color: var(--ink);
-}
.deck-editor .deck-checkbox {
display: flex;
align-items: center;
min-height: 40px;
margin-bottom: var(--space-sm);
}
-.deck-checkbox input {
- accent-color: var(--accent);
- width: 1.15rem;
- height: 1.15rem;
-}
.deck-rename {
display: flex;
flex-wrap: wrap;
@@ -347,7 +214,7 @@
flex: 1 1 100%;
}
.deck-error {
- color: var(--error);
+ color: var(--destructive);
font-size: var(--text-sm);
line-height: 1.6;
overflow-wrap: anywhere;
@@ -374,7 +241,7 @@
padding: var(--space-lg);
}
.deck-empty p {
- color: var(--secondary);
+ color: var(--muted-foreground);
line-height: 1.7;
margin-bottom: var(--space-lg);
}
@@ -386,10 +253,10 @@
}
.deck-workspaces > .deck-actions > label {
width: 100%;
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-xs);
}
-.deck-workspaces > .deck-actions > select {
+.deck-workspaces > .deck-actions > [data-slot="native-select-wrapper"] {
flex: 1 1 100%;
}
.deck-workspaces > .deck-actions > button {
@@ -404,35 +271,6 @@
width: 100%;
}
@media (max-width: 767px) {
- .deck-workspaces {
- flex: 1;
- }
- .deck-workspaces > .deck-actions {
- flex-wrap: nowrap;
- gap: var(--space-2xs);
- }
- .deck-workspaces > .deck-actions > label {
- position: absolute;
- width: 1px;
- height: 1px;
- overflow: hidden;
- clip-path: inset(50%);
- }
- .deck-workspaces > .deck-actions > select {
- flex: 1;
- width: 0;
- }
- .deck-workspaces > .deck-actions > button {
- flex: none;
- }
- .app-deck .sidebar-add {
- width: 36px;
- flex-shrink: 0;
- padding: var(--space-xs);
- }
- .sidebar-add span {
- display: none;
- }
.deck-heading {
min-height: 52px;
padding-inline: var(--space-sm);
@@ -444,37 +282,19 @@
grid-auto-columns: min(328px, 100%);
}
}
-@media (pointer: coarse) {
- .app-deck .icon-button,
- .column-menu summary {
- min-width: 40px;
- min-height: 40px;
- }
-}
.deck-count {
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-xs);
}
.research-post header time {
display: block;
- color: var(--secondary);
+ color: var(--muted-foreground);
font-size: var(--text-xs);
}
.deck-column-status {
text-align: right;
}
-@media (max-width: 767px) {
- .workspace-create span,
- .workspace-delete span {
- display: none;
- }
- .app-deck .workspace-create,
- .app-deck .workspace-delete {
- width: 36px;
- padding: var(--space-xs);
- }
-}
-.research-post header > div {
+.research-post header > div:not([data-slot="avatar"]) {
flex: 1;
}
.research-post header time {
@@ -490,6 +310,3 @@
width: 14px;
height: 14px;
}
-.column-menu-panel .danger {
- color: var(--error);
-}
diff --git a/src/features/decks/mastodon-source-editor.tsx b/src/features/decks/mastodon-source-editor.tsx
index 538ab36..089e8b6 100644
--- a/src/features/decks/mastodon-source-editor.tsx
+++ b/src/features/decks/mastodon-source-editor.tsx
@@ -1,6 +1,10 @@
import { useQuery } from '@tanstack/react-query'
import { useServerFn } from '@tanstack/react-start'
import { type ReactElement, useId } from 'react'
+import { Button } from '#/components/ui/button'
+import { Input } from '#/components/ui/input'
+import { Label } from '#/components/ui/label'
+import { NativeSelect, NativeSelectOption } from '#/components/ui/native-select'
import { loadMastodonLists } from '../platforms/mastodon-server-functions'
import type { MastodonSource } from '../platforms/mastodon-source'
@@ -32,9 +36,10 @@ export function MastodonSourceEditor({
const id = useId()
return (
<>
-
+
カラムの種類
-
-
+ 検索
+ ユーザー投稿
+ リスト
+ ハッシュタグ
+
+
-
+
Mastodonの検索条件
-
@@ -102,7 +107,7 @@ function MastodonSearchEditor({
placeholder="例:WebMCP"
required
/>
-
+
全文検索の対象はサーバーの設定によって異なります。結果が0件でも、検索に対応しているとは限りません。
@@ -117,9 +122,9 @@ function MastodonUserEditor({
const id = useId()
return (
-
+
ユーザー
-
@@ -129,7 +134,7 @@ function MastodonUserEditor({
maxLength={256}
required
/>
-
+
)
}
@@ -141,9 +146,9 @@ function MastodonHashtagEditor({
const id = useId()
return (
-
+
ハッシュタグ
-
@@ -153,7 +158,7 @@ function MastodonHashtagEditor({
maxLength={128}
required
/>
-
+
)
}
@@ -168,9 +173,9 @@ function MastodonListEditor({
const id = useId()
return (
-
+
リスト
-
@@ -180,7 +185,7 @@ function MastodonListEditor({
maxLength={32}
required
/>
-
+
{connectionId && (
リストを取得できませんでした。
- void lists.refetch()}>
+ void lists.refetch()}
+ >
再試行
-
+
)
return (
@@ -220,9 +229,14 @@ function MastodonListChoices({
このアカウントのリストはありません。
) : (
lists.data.lists.map((list) => (
- onSelect(list.id)}>
+ onSelect(list.id)}
+ >
{list.name}
-
+
))
)}
diff --git a/src/features/decks/research-post-card.tsx b/src/features/decks/research-post-card.tsx
index 7fe4f05..6ab4a63 100644
--- a/src/features/decks/research-post-card.tsx
+++ b/src/features/decks/research-post-card.tsx
@@ -1,6 +1,9 @@
import { useEffect, useRef, useState } from 'react'
import { Icon } from '#/components/icon'
import { PostText } from '#/components/post-text'
+import { Avatar, AvatarFallback, AvatarImage } from '#/components/ui/avatar'
+import { Badge } from '#/components/ui/badge'
+import { Button } from '#/components/ui/button'
import type { ResearchPost } from '#/features/platforms/types'
const dateFormatter = new Intl.DateTimeFormat('ja-JP', {
@@ -49,20 +52,17 @@ export function ResearchPostCard({
className={`research-post${highlighted ? ' citation-highlight' : ''}`}
data-post-url={post.url}
>
- {citationSnapshot ? 調査時に取得した投稿
: null}
+ {citationSnapshot ? (
+ 調査時に取得した投稿
+ ) : null}
{post.boostedBy ? (
{post.boostedBy.name} がブースト
) : null}