From c5fa3c2cf4b71b79752da56b95d1aef0e17af385 Mon Sep 17 00:00:00 2001 From: hfcRed Date: Sat, 27 Dec 2025 14:39:25 +0100 Subject: [PATCH] Update components page --- .../components-showcase.module.css | 38 + .../components-showcase/routes/components.tsx | 1643 ++++++++++++++++- 2 files changed, 1639 insertions(+), 42 deletions(-) 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 ( +
+ Radio Buttons + +
+ +
+ + + +
+
+ + +
+ + + +
+
+ + +
+ + + +
+
+
+
+ ); +} + +function FieldsetSection({ id }: { id: string }) { + const [favoriteColor, setFavoriteColor] = useState(""); + const [notifications, setNotifications] = useState({ + email: true, + push: false, + sms: false, + }); + + return ( +
+ Fieldsets + +
+ +
+ Personal Information +
+
+ + +
+
+ + +
+
+
+
+ + +
+ Favorite Color +
+ + + +
+
+
+ + +
+ Notification Preferences +
+ + + +
+
+
+ + +
+ Disabled Form +
+
+ + +
+ + Button in disabled fieldset +
+
+
+
+
+ ); +} + +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 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRoleStatus
John DoeDeveloperActive
Jane SmithDesignerActive
Bob JohnsonManagerInactive
+
+ + + + + + + + + + + + + + + + + + + + + + +
ItemPriceActions
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) +
+
+ HTML (abbreviation) +
+
+
+ + +
+
+ Unordered List: +
    +
  • Item 1
  • +
  • Item 2
  • +
  • + Item 3 +
      +
    • Nested item 1
    • +
    • Nested item 2
    • +
    +
  • +
+
+
+ Ordered List: +
    +
  1. First item
  2. +
  3. Second item
  4. +
  5. Third item
  6. +
+
+
+ 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. +

+
— Author Name
+
+
+ + +
+						
+							{`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" + /> +
);