diff --git a/app/features/components-showcase/components-showcase.module.css b/app/features/components-showcase/components-showcase.module.css
index 8d967ead1..77d56a1a6 100644
--- a/app/features/components-showcase/components-showcase.module.css
+++ b/app/features/components-showcase/components-showcase.module.css
@@ -23,3 +23,41 @@
.componentContent {
width: 100%;
}
+
+.sideNav {
+ position: fixed;
+ left: var(--s-4);
+ top: 60px;
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-1);
+ max-height: 90dvh;
+ overflow-y: auto;
+ z-index: 100;
+
+ @media (max-width: 1100px) {
+ display: none;
+ }
+}
+
+.sideNavLink {
+ font-size: var(--fonts-xs);
+ color: var(--color-text-high);
+ text-decoration: none;
+ padding: var(--s-1) var(--s-2);
+ border-radius: var(--rounded-xs);
+ transition:
+ background-color 0.15s,
+ color 0.15s;
+
+ &:hover {
+ color: var(--color-text);
+ background-color: var(--color-bg-high);
+ }
+
+ &[aria-current="page"] {
+ color: var(--color-text);
+ background-color: var(--color-bg-high);
+ font-weight: 600;
+ }
+}
diff --git a/app/features/components-showcase/routes/components.tsx b/app/features/components-showcase/routes/components.tsx
index ccb81c9ac..b3acf8954 100644
--- a/app/features/components-showcase/routes/components.tsx
+++ b/app/features/components-showcase/routes/components.tsx
@@ -1,7 +1,17 @@
-import { useState } from "react";
+import { parseDate } from "@internationalized/date";
+import clsx from "clsx";
+import { useEffect, useState } from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { Ability } from "~/components/Ability";
+import { AddNewButton } from "~/components/AddNewButton";
import { Alert } from "~/components/Alert";
+import { Avatar } from "~/components/Avatar";
+import { Badge } from "~/components/Badge";
+import { CopyToClipboardPopover } from "~/components/CopyToClipboardPopover";
import { Divider } from "~/components/Divider";
import { LinkButton, SendouButton } from "~/components/elements/Button";
+import { SendouCalendar } from "~/components/elements/Calendar";
+import { SendouDatePicker } from "~/components/elements/DatePicker";
import { SendouDialog } from "~/components/elements/Dialog";
import { SendouMenu, SendouMenuItem } from "~/components/elements/Menu";
import { SendouPopover } from "~/components/elements/Popover";
@@ -14,6 +24,18 @@ import {
SendouTabs,
} from "~/components/elements/Tabs";
import { toastQueue } from "~/components/elements/Toast";
+import { Flag } from "~/components/Flag";
+import { FormMessage } from "~/components/FormMessage";
+import { InputFormField } from "~/components/form/InputFormField";
+import { TextAreaFormField } from "~/components/form/TextAreaFormField";
+import {
+ ModeImage,
+ SpecialWeaponImage,
+ StageImage,
+ SubWeaponImage,
+ TierImage,
+ WeaponImage,
+} from "~/components/Image";
import { InfoPopover } from "~/components/InfoPopover";
import { Input } from "~/components/Input";
import { CheckmarkIcon } from "~/components/icons/Checkmark";
@@ -23,32 +45,157 @@ import { SearchIcon } from "~/components/icons/Search";
import { TrashIcon } from "~/components/icons/Trash";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
+import { Pagination } from "~/components/Pagination";
+import { Placeholder } from "~/components/Placeholder";
+import { Placement } from "~/components/Placement";
+import { RelativeTime } from "~/components/RelativeTime";
import { Section } from "~/components/Section";
+import { StageSelect } from "~/components/StageSelect";
import { SubmitButton } from "~/components/SubmitButton";
+import { SubNav, SubNavLink } from "~/components/SubNav";
+import { Table } from "~/components/Table";
+import { WeaponSelect } from "~/components/WeaponSelect";
+import type { MainWeaponId, StageId } from "~/modules/in-game-lists/types";
import styles from "../components-showcase.module.css";
+const SECTIONS = [
+ { title: "Buttons", id: "buttons", component: ButtonsSection },
+ { title: "Alerts", id: "alerts", component: AlertsSection },
+ { title: "Inputs", id: "inputs", component: InputsSection },
+ { title: "Select", id: "select", component: SelectSection },
+ { title: "Switch", id: "switch", component: SwitchSection },
+ { title: "Checkboxes", id: "checkboxes", component: CheckboxSection },
+ {
+ title: "Radio Buttons",
+ id: "radio-buttons",
+ component: RadioButtonSection,
+ },
+ { title: "Fieldsets", id: "fieldsets", component: FieldsetSection },
+ { title: "Details", id: "details", component: DetailsSection },
+ { title: "Tabs", id: "tabs", component: TabsSection },
+ { title: "Dialog", id: "dialog", component: DialogSection },
+ { title: "Popover", id: "popover", component: PopoverSection },
+ { title: "Menu", id: "menu", component: MenuSection },
+ { title: "Toast", id: "toast", component: ToastSection },
+ { title: "Divider", id: "divider", component: DividerSection },
+ { title: "Table", id: "table", component: TableSection },
+ { title: "Pagination", id: "pagination", component: PaginationSection },
+ { title: "Avatar", id: "avatar", component: AvatarSection },
+ {
+ title: "Form Messages",
+ id: "form-messages",
+ component: FormMessageSection,
+ },
+ { title: "Sub Navigation", id: "sub-navigation", component: SubNavSection },
+ { title: "Date Pickers", id: "date-pickers", component: DatePickerSection },
+ {
+ title: "Form Components",
+ id: "form-components",
+ component: FormComponentsSection,
+ },
+ {
+ title: "Splatoon Images",
+ id: "splatoon-images",
+ component: SplatoonImagesSection,
+ },
+ { title: "Abilities", id: "abilities", component: AbilitySection },
+ { title: "Flags", id: "flags", component: FlagSection },
+ { title: "Placements", id: "placements", component: PlacementSection },
+ { title: "Badges", id: "badges", component: BadgeSection },
+ { title: "Game Selects", id: "game-selects", component: GameSelectSection },
+ { title: "Miscellaneous", id: "miscellaneous", component: MiscSection },
+];
+
export default function ComponentsShowcasePage() {
return (
-
- Components
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+ Components
+ {SECTIONS.map(({ id, component: Component }) => (
+
+ ))}
+
+ >
);
}
-function SectionTitle({ children }: { children: React.ReactNode }) {
- return
{children}
;
+function SideNav() {
+ const [activeSection, setActiveSection] = useState(null);
+
+ useEffect(() => {
+ const sectionIds = SECTIONS.map((s) => s.id);
+ const elements = sectionIds
+ .map((id) => document.getElementById(id))
+ .filter(Boolean) as HTMLElement[];
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const visibleEntries = entries.filter((entry) => entry.isIntersecting);
+
+ if (visibleEntries.length > 0) {
+ const topMostEntry = visibleEntries.reduce((prev, curr) =>
+ prev.boundingClientRect.top < curr.boundingClientRect.top
+ ? prev
+ : curr,
+ );
+
+ setActiveSection(topMostEntry.target.id);
+ }
+ },
+ { rootMargin: "-10% 0px -80% 0px", threshold: 0 },
+ );
+
+ for (const element of elements) {
+ observer.observe(element);
+ }
+
+ return () => observer.disconnect();
+ }, []);
+
+ const handleClick = (
+ event: React.MouseEvent,
+ id: string,
+ ) => {
+ event.preventDefault();
+ const element = document.getElementById(id);
+
+ if (element) {
+ element.scrollIntoView({ behavior: "instant" });
+ window.history.replaceState(null, "", `#${id}`);
+ setActiveSection(id);
+ }
+ };
+
+ return (
+
+ );
+}
+
+function SectionTitle({
+ id,
+ children,
+}: {
+ id: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
}
function ComponentRow({
@@ -66,10 +213,10 @@ function ComponentRow({
);
}
-function ButtonsSection() {
+function ButtonsSection({ id }: { id: string }) {
return (
- Buttons
+ Buttons
@@ -176,10 +323,10 @@ function ButtonsSection() {
);
}
-function AlertsSection() {
+function AlertsSection({ id }: { id: string }) {
return (
- Alerts
+ Alerts
@@ -212,10 +359,10 @@ function AlertsSection() {
);
}
-function InputsSection() {
+function InputsSection({ id }: { id: string }) {
return (
- Inputs
+ Inputs
@@ -300,10 +447,10 @@ const SELECT_ITEMS = [
{ id: "5", name: "Option 5" },
];
-function SelectSection() {
+function SelectSection({ id }: { id: string }) {
return (
- Select
+ Select
@@ -409,13 +556,13 @@ function SelectSection() {
);
}
-function SwitchSection() {
+function SwitchSection({ id }: { id: string }) {
const [isOn, setIsOn] = useState(false);
const [isSmallOn, setIsSmallOn] = useState(true);
return (
- Switch
+ Switch
@@ -446,10 +593,489 @@ function SwitchSection() {
);
}
-function TabsSection() {
+function CheckboxSection({ id }: { id: string }) {
+ const [singleChecked, setSingleChecked] = useState(false);
+ const [checkedItems, setCheckedItems] = useState({
+ option1: true,
+ option2: false,
+ option3: true,
+ });
+
return (
- Tabs
+ Checkboxes
+
+
+
+ );
+}
+
+function RadioButtonSection({ id }: { id: string }) {
+ const [selectedOption, setSelectedOption] = useState("option2");
+ const [selectedSize, setSelectedSize] = useState("medium");
+
+ return (
+
+ );
+}
+
+function FieldsetSection({ id }: { id: string }) {
+ const [favoriteColor, setFavoriteColor] = useState("");
+ const [notifications, setNotifications] = useState({
+ email: true,
+ push: false,
+ sms: false,
+ });
+
+ return (
+
+ );
+}
+
+function DetailsSection({ id }: { id: string }) {
+ return (
+
+ Details/Summary
+
+
+
+
+ Click to expand
+
+ This is the hidden content that appears when you expand the
+ details element. It can contain any HTML content.
+
+
+
+
+
+
+ This is open by default
+
+ The details element can be open by default using the "open"
+ attribute.
+
+
+
+
+
+
+ Advanced Settings
+
+
+
+
+
+
+
+
+ {(item) => (
+
+ {item.name}
+
+ )}
+
+
+
+
+
+
+
+
+
+ Section 1
+
+
Content for section 1.
+
+ Subsection 1.1
+
+ Nested content in subsection 1.1
+
+
+
+ Subsection 1.2
+
+ Nested content in subsection 1.2
+
+
+
+
+
+
+
+
+
+ What is this component showcase?
+
+ This is a showcase of various HTML and custom components used in
+ the application.
+
+
+
+ How do I use these components?
+
+ Each component has examples showing different variations and use
+ cases.
+
+
+
+ Can I customize the styles?
+
+ Yes, most components support custom styling through CSS modules
+ and className props.
+
+
+
+
+
+
+ );
+}
+
+function TabsSection({ id }: { id: string }) {
+ return (
+
+ Tabs
@@ -513,12 +1139,12 @@ function TabsSection() {
);
}
-function DialogSection() {
+function DialogSection({ id }: { id: string }) {
const [isOpen, setIsOpen] = useState(false);
return (
- Dialog
+ Dialog
@@ -557,10 +1183,10 @@ function DialogSection() {
);
}
-function PopoverSection() {
+function PopoverSection({ id }: { id: string }) {
return (
- Popover
+ Popover
@@ -591,10 +1217,10 @@ function PopoverSection() {
);
}
-function MenuSection() {
+function MenuSection({ id }: { id: string }) {
return (
- Menu
+ Menu
@@ -646,10 +1272,10 @@ function MenuSection() {
);
}
-function ToastSection() {
+function ToastSection({ id }: { id: string }) {
return (
- Toast
+ Toast
@@ -697,10 +1323,10 @@ function ToastSection() {
);
}
-function DividerSection() {
+function DividerSection({ id }: { id: string }) {
return (
- Divider
+ Divider
@@ -731,10 +1357,723 @@ function DividerSection() {
);
}
-function MiscSection() {
+function TableSection({ id }: { id: string }) {
return (
- Miscellaneous
+ Table
+
+
+
+
+
+
+ | Name |
+ Role |
+ Status |
+
+
+
+
+ | John Doe |
+ Developer |
+ Active |
+
+
+ | Jane Smith |
+ Designer |
+ Active |
+
+
+ | Bob Johnson |
+ Manager |
+ Inactive |
+
+
+
+
+
+
+
+
+
+ | Item |
+ Price |
+ Actions |
+
+
+
+
+ | Product A |
+ $19.99 |
+
+
+ } />
+ }
+ />
+
+ |
+
+
+ | Product B |
+ $29.99 |
+
+
+ } />
+ }
+ />
+
+ |
+
+
+
+
+
+
+ );
+}
+
+function PaginationSection({ id }: { id: string }) {
+ const [currentPage, setCurrentPage] = useState(5);
+ const pagesCount = 50;
+
+ return (
+
+ Pagination
+
+
+
+ setCurrentPage((p) => Math.min(p + 1, pagesCount))}
+ previousPage={() => setCurrentPage((p) => Math.max(p - 1, 1))}
+ setPage={setCurrentPage}
+ />
+
+
+
+ {}}
+ previousPage={() => {}}
+ setPage={() => {}}
+ />
+
+
+
+ {}}
+ previousPage={() => {}}
+ setPage={() => {}}
+ />
+
+
+
+ );
+}
+
+function AvatarSection({ id }: { id: string }) {
+ return (
+
+ Avatar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function FormMessageSection({ id }: { id: string }) {
+ return (
+
+ Form Messages
+
+
+
+
+ This field is required. Please enter a value.
+
+
+
+
+
+ This is an informational message to help you.
+
+
+
+
+
+
+
+
+ Please enter a valid email address.
+
+
+
+
+
+ );
+}
+
+function SubNavSection({ id }: { id: string }) {
+ return (
+
+ Sub Navigation
+
+
+
+
+
+ Overview
+
+
+ Details
+
+
+ Settings
+
+
+
+
+
+
+
+ Tab 1
+
+
+ Tab 2
+
+
+ Tab 3
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function DatePickerSection({ id }: { id: string }) {
+ const [calendarValue, setCalendarValue] = useState(parseDate("2024-12-27"));
+ const [datePickerValue, setDatePickerValue] = useState(
+ parseDate("2024-12-27"),
+ );
+
+ const handleCalendarChange = (value: typeof calendarValue | null) => {
+ if (value) setCalendarValue(value);
+ };
+
+ const handleDatePickerChange = (value: typeof datePickerValue | null) => {
+ if (value) setDatePickerValue(value);
+ };
+
+ return (
+
+ Date Pickers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function FormComponentsSection({ id }: { id: string }) {
+ const methods = useForm();
+
+ return (
+
+ Form Components
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function SplatoonImagesSection({ id }: { id: string }) {
+ return (
+
+ Splatoon Images
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function AbilitySection({ id }: { id: string }) {
+ return (
+
+ Abilities
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function FlagSection({ id }: { id: string }) {
+ return (
+
+ Flags
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function PlacementSection({ id }: { id: string }) {
+ return (
+
+ Placements
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function BadgeSection({ id }: { id: string }) {
+ return (
+
+ Badges
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function GameSelectSection({ id }: { id: string }) {
+ const [selectedWeapon, setSelectedWeapon] = useState(
+ null,
+ );
+ const [selectedStage, setSelectedStage] = useState(null);
+
+ return (
+
+ Game Selects
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function MiscSection({ id }: { id: string }) {
+ const [rangeValue, setRangeValue] = useState(50);
+ const [colorValue, setColorValue] = useState("#3b82f6");
+
+ return (
+
+ Miscellaneous
@@ -743,6 +2082,226 @@ function MiscSection() {
It provides consistent styling for content blocks.
+
+
+
+
+
+
+
+
+
+
+
+
+ setRangeValue(Number(e.target.value))}
+ style={{ width: "100%" }}
+ />
+
+
+
+
+
+ setColorValue(e.target.value)}
+ />
+ {colorValue}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bold text
+
+
+ Italic text
+
+
+ Highlighted text
+
+
+ Deleted text
+
+
+ Inserted text
+
+
+ Inline code
+
+
+ Keyboard input
+
+
+ Sample output
+
+
+ Variable
+
+
+ Small text
+
+
+ H2O (subscript)
+
+
+ E = mc2 (superscript)
+
+
+
+
+
+
+
+
+
Unordered List:
+
+ - Item 1
+ - Item 2
+ -
+ Item 3
+
+ - Nested item 1
+ - Nested item 2
+
+
+
+
+
+
Ordered List:
+
+ - First item
+ - Second item
+ - Third item
+
+
+
+
Description List:
+
+ - Term 1
+ - Definition for term 1
+ - Term 2
+ - Definition for term 2
+
+
+
+
+
+
+
+
+ This is a blockquote. It's used to represent content quoted from
+ another source.
+
+
+
+
+
+
+
+
+ {`function example() {
+ const greeting = "Hello, World!";
+ console.log(greeting);
+ return greeting;
+}`}
+
+
+
+
+
+
+
Content above
+
+
Content below
+
+
+
+
+
+ 1 hour ago
+
+
+
+
+ Share Link}
+ url="https://sendou.ink/example"
+ />
+
);