mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(web): stronger modality section visual hierarchy in tool catalog
This commit is contained in:
@@ -54,17 +54,26 @@ export function ToolPanel() {
|
||||
</div>
|
||||
<div className="px-3 pb-4 flex-1">
|
||||
{MODALITIES.filter((m) => m.id !== "file" && groupedByModality.has(m.id)).map(
|
||||
(modality) => {
|
||||
(modality, idx, arr) => {
|
||||
const ModalityIcon = ICON_MAP[modality.icon] as React.ComponentType<{
|
||||
className?: string;
|
||||
}>;
|
||||
const categoryMap = groupedByModality.get(modality.id);
|
||||
if (!categoryMap) return null;
|
||||
const isLast = idx === arr.length - 1;
|
||||
return (
|
||||
<div key={modality.id} className="mb-5">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
{ModalityIcon && <ModalityIcon className="h-4 w-4 text-foreground/70 shrink-0" />}
|
||||
<h2 className="text-xs font-bold uppercase text-foreground/70 tracking-wider">
|
||||
<div key={modality.id} className={isLast ? "" : "mb-2"}>
|
||||
{idx > 0 && <hr className="border-border mb-4" />}
|
||||
<div
|
||||
className="flex items-center gap-2 mt-4 mb-3 pl-2.5 border-l-[3px] rounded-sm"
|
||||
style={{ borderColor: modality.color }}
|
||||
>
|
||||
{ModalityIcon && (
|
||||
<span className="shrink-0" style={{ color: modality.color }}>
|
||||
<ModalityIcon className="h-4.5 w-4.5" />
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-sm font-semibold text-foreground tracking-wide">
|
||||
{getModalityName(
|
||||
t,
|
||||
modality.id,
|
||||
@@ -73,8 +82,8 @@ export function ToolPanel() {
|
||||
</h2>
|
||||
</div>
|
||||
{CATEGORIES.filter((cat) => categoryMap.has(cat.id)).map((category) => (
|
||||
<div key={category.id} className="mb-4">
|
||||
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-2">
|
||||
<div key={category.id} className="mb-3">
|
||||
<h3 className="text-xs font-medium text-muted-foreground tracking-wider mb-1.5 pl-1">
|
||||
{getCategoryName(t, category.id, category.name)}
|
||||
</h3>
|
||||
<div className="space-y-0.5">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CategoryInfo, Tool } from "@snapotter/shared";
|
||||
import { ANALYTICS_EVENTS, CATEGORIES, TOOLS } from "@snapotter/shared";
|
||||
import type { CategoryInfo, Modality, Tool } from "@snapotter/shared";
|
||||
import { ANALYTICS_EVENTS, CATEGORIES, MODALITIES, TOOLS } from "@snapotter/shared";
|
||||
import {
|
||||
AudioLines,
|
||||
Eye,
|
||||
@@ -23,7 +23,7 @@ import { useMobile } from "@/hooks/use-mobile";
|
||||
import { track } from "@/lib/analytics";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import { ICON_MAP } from "@/lib/icon-map";
|
||||
import { getCategoryName, getToolDescription, getToolName } from "@/lib/tool-i18n";
|
||||
import { getCategoryName, getModalityName, getToolDescription, getToolName } from "@/lib/tool-i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFeaturesStore } from "@/stores/features-store";
|
||||
|
||||
@@ -100,6 +100,25 @@ export function FullscreenGridPage() {
|
||||
|
||||
const activeCategories = CATEGORIES.filter((cat) => groupedTools.has(cat.id));
|
||||
|
||||
/** Map each modality to its list of active categories (for "All" tab grouping). */
|
||||
const modalitySections = useMemo(() => {
|
||||
if (modalityTab !== "all") return null;
|
||||
const catToModality = new Map<string, Modality>();
|
||||
for (const tool of filteredTools) {
|
||||
const key = tool.modality === "file" ? ("document" as Modality) : tool.modality;
|
||||
if (!catToModality.has(tool.category)) {
|
||||
catToModality.set(tool.category, key);
|
||||
}
|
||||
}
|
||||
const sections: { modality: (typeof MODALITIES)[number]; categories: CategoryInfo[] }[] = [];
|
||||
for (const mod of MODALITIES) {
|
||||
if (mod.id === "file") continue;
|
||||
const cats = activeCategories.filter((c) => catToModality.get(c.id) === mod.id);
|
||||
if (cats.length > 0) sections.push({ modality: mod, categories: cats });
|
||||
}
|
||||
return sections;
|
||||
}, [modalityTab, filteredTools, activeCategories]);
|
||||
|
||||
return (
|
||||
<div className={cn("min-h-screen bg-background text-foreground", isMobile && "pb-20")}>
|
||||
{/* Top bar */}
|
||||
@@ -190,16 +209,103 @@ export function FullscreenGridPage() {
|
||||
<p className="text-lg font-medium">{t.fullscreenGrid.noToolsFound}</p>
|
||||
<p className="text-sm mt-1">{t.fullscreenGrid.tryDifferent}</p>
|
||||
</div>
|
||||
) : modalitySections ? (
|
||||
/* "All" tab: render with modality section headers */
|
||||
<div className="space-y-10">
|
||||
{modalitySections.map((section) => {
|
||||
const SectionIcon = ICON_MAP[section.modality.icon] as React.ComponentType<{
|
||||
className?: string;
|
||||
}>;
|
||||
return (
|
||||
<section key={section.modality.id}>
|
||||
<div
|
||||
className="flex items-center gap-3 mb-5 pb-3 border-b-2"
|
||||
style={{ borderColor: section.modality.color }}
|
||||
>
|
||||
{SectionIcon && (
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: `${section.modality.color}15`,
|
||||
color: section.modality.color,
|
||||
}}
|
||||
>
|
||||
<SectionIcon className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{getModalityName(
|
||||
t,
|
||||
section.modality.id,
|
||||
section.modality.id === "document"
|
||||
? "Documents & Files"
|
||||
: section.modality.name,
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{section.categories.map((category) => (
|
||||
<CategoryCard
|
||||
key={category.id}
|
||||
category={category}
|
||||
tools={groupedTools.get(category.id) || []}
|
||||
showDetails={showDetails}
|
||||
accentColor={section.modality.color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{activeCategories.map((category) => (
|
||||
<CategoryCard
|
||||
key={category.id}
|
||||
category={category}
|
||||
tools={groupedTools.get(category.id) || []}
|
||||
showDetails={showDetails}
|
||||
/>
|
||||
))}
|
||||
/* Specific modality tab selected */
|
||||
<div>
|
||||
{(() => {
|
||||
const activeMod = MODALITIES.find((m) => m.id === modalityTab);
|
||||
const ModIcon = activeMod
|
||||
? (ICON_MAP[activeMod.icon] as React.ComponentType<{ className?: string }>)
|
||||
: null;
|
||||
return activeMod ? (
|
||||
<div
|
||||
className="flex items-center gap-3 mb-6 pb-3 border-b-2"
|
||||
style={{ borderColor: activeMod.color }}
|
||||
>
|
||||
{ModIcon && (
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: `${activeMod.color}15`,
|
||||
color: activeMod.color,
|
||||
}}
|
||||
>
|
||||
<ModIcon className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{getModalityName(
|
||||
t,
|
||||
activeMod.id,
|
||||
activeMod.id === "document" ? "Documents & Files" : activeMod.name,
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{activeCategories.map((category) => {
|
||||
const activeMod = MODALITIES.find((m) => m.id === modalityTab);
|
||||
return (
|
||||
<CategoryCard
|
||||
key={category.id}
|
||||
category={category}
|
||||
tools={groupedTools.get(category.id) || []}
|
||||
showDetails={showDetails}
|
||||
accentColor={activeMod?.color}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
@@ -212,17 +318,22 @@ function CategoryCard({
|
||||
category,
|
||||
tools,
|
||||
showDetails,
|
||||
accentColor,
|
||||
}: {
|
||||
category: CategoryInfo;
|
||||
tools: Tool[];
|
||||
showDetails: boolean;
|
||||
accentColor?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const CategoryIcon =
|
||||
(ICON_MAP[category.icon] as React.ComponentType<{ className?: string }>) ?? LayoutGrid;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
||||
<div
|
||||
className="rounded-xl border border-border bg-card overflow-hidden shadow-sm hover:shadow-md transition-shadow"
|
||||
style={accentColor ? { borderLeftWidth: 3, borderLeftColor: accentColor } : undefined}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="px-4 py-3 flex items-center gap-3"
|
||||
|
||||
Reference in New Issue
Block a user