@axzydev/axzy_ui_system 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/index.cjs +2342 -1715
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +286 -9
- package/dist/index.d.ts +286 -9
- package/dist/index.js +2164 -1538
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +153 -153
- package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
- package/src/components/molecules/calendar/calendar.tsx +102 -24
- package/src/components/molecules/masked-input/masked-input.doc.mdx +78 -0
- package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
- package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
- package/src/components/molecules/masked-input/masked-input.tsx +244 -0
- package/src/components/molecules/table/table.doc.mdx +64 -21
- package/src/components/molecules/table/table.props.ts +81 -2
- package/src/components/molecules/table/table.stories.tsx +155 -0
- package/src/components/molecules/table/table.tsx +177 -36
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
- package/src/components/organisms/data-table/dataTable.props.ts +64 -0
- package/src/components/organisms/data-table/dataTable.tsx +240 -82
- package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
- package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
- package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +27 -7
- package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
- package/src/components/organisms/topbar/topbar.props.ts +16 -0
- package/src/components/organisms/topbar/topbar.tsx +133 -98
- package/src/components/templates/layout/layout.doc.mdx +2 -0
- package/src/components/templates/layout/layout.tsx +7 -1
- package/src/components/templates/page/page.doc.mdx +8 -2
- package/src/components/templates/page/page.props.ts +11 -1
- package/src/components/templates/page/page.tsx +11 -1
- package/src/dev.css +58 -1
- package/src/hooks/useElementSize.ts +46 -0
- package/src/hooks/useVirtualRows.ts +123 -0
- package/src/index.ts +6 -2
- package/src/sandbox/LandingShowcase.tsx +465 -0
- package/src/sandbox/NotFoundShowcase.tsx +70 -0
- package/src/sandbox/TopbarSearch.tsx +85 -0
- package/src/sandbox/navigation.ts +148 -0
- package/src/sandbox/useHashRoute.ts +149 -0
- package/src/showcases/DataShowcases.tsx +321 -24
- package/src/showcases/FormShowcases.tsx +178 -0
- package/src/showcases/PageShowcases.tsx +31 -0
- package/src/showcases/StructureShowcases.tsx +42 -4
- package/src/types/table.types.ts +48 -0
- package/src/utils/styles.ts +9 -4
- package/src/utils/table.utils.ts +31 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { FaHome, FaLayerGroup } from "react-icons/fa";
|
|
2
|
+
import ITButton from "@/components/atoms/button/button";
|
|
3
|
+
import ITFlex from "@/components/atoms/flex/flex";
|
|
4
|
+
import ITStack from "@/components/atoms/stack/stack";
|
|
5
|
+
import ITText from "@/components/atoms/text/text";
|
|
6
|
+
|
|
7
|
+
/** Props for the sandbox 404 page. */
|
|
8
|
+
export interface NotFoundShowcaseProps {
|
|
9
|
+
/** Raw hash that failed to resolve (shown inside a `<code>` block). */
|
|
10
|
+
attempted: string;
|
|
11
|
+
/** Navigate back to the `#ui-system` landing. */
|
|
12
|
+
onGoLanding: () => void;
|
|
13
|
+
/** Navigate back to the portfolio. */
|
|
14
|
+
onGoPortfolio: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sandbox 404 page rendered when a hash route does not resolve to a page.
|
|
19
|
+
*
|
|
20
|
+
* Shows the attempted path, a friendly message, and buttons to recover.
|
|
21
|
+
*/
|
|
22
|
+
export default function NotFoundShowcase({
|
|
23
|
+
attempted,
|
|
24
|
+
onGoLanding,
|
|
25
|
+
onGoPortfolio,
|
|
26
|
+
}: NotFoundShowcaseProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="animate-fadeIn flex min-h-[60vh] items-center justify-center px-4">
|
|
29
|
+
<ITStack spacing={5} className="w-full max-w-xl text-center items-center">
|
|
30
|
+
<ITText
|
|
31
|
+
as="div"
|
|
32
|
+
className="!text-7xl !font-extrabold !tracking-tight bg-gradient-to-br from-primary-500 via-primary-600 to-purple-600 bg-clip-text text-transparent"
|
|
33
|
+
>
|
|
34
|
+
404
|
|
35
|
+
</ITText>
|
|
36
|
+
|
|
37
|
+
<ITStack spacing={2} className="items-center">
|
|
38
|
+
<ITText as="h1" className="!text-2xl !font-bold text-slate-900 dark:text-white">
|
|
39
|
+
Ruta no encontrada
|
|
40
|
+
</ITText>
|
|
41
|
+
<ITText as="p" className="!text-sm text-slate-500 dark:text-slate-400">
|
|
42
|
+
No existe una página para este hash. Revisa el enlace o vuelve al inicio.
|
|
43
|
+
</ITText>
|
|
44
|
+
<code className="mt-1 max-w-full truncate rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 font-mono text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
|
|
45
|
+
{attempted || "#"}
|
|
46
|
+
</code>
|
|
47
|
+
</ITStack>
|
|
48
|
+
|
|
49
|
+
<ITFlex gap={3} wrap="wrap" justify="center" align="center">
|
|
50
|
+
<ITButton
|
|
51
|
+
variant="filled"
|
|
52
|
+
color="primary"
|
|
53
|
+
icon={<FaHome size={13} />}
|
|
54
|
+
onClick={onGoLanding}
|
|
55
|
+
>
|
|
56
|
+
Volver al inicio
|
|
57
|
+
</ITButton>
|
|
58
|
+
<ITButton
|
|
59
|
+
variant="outlined"
|
|
60
|
+
color="secondary"
|
|
61
|
+
icon={<FaLayerGroup size={13} />}
|
|
62
|
+
onClick={onGoPortfolio}
|
|
63
|
+
>
|
|
64
|
+
Ver portafolio
|
|
65
|
+
</ITButton>
|
|
66
|
+
</ITFlex>
|
|
67
|
+
</ITStack>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ChangeEvent } from "react";
|
|
2
|
+
import { FaSearch, FaTimes } from "react-icons/fa";
|
|
3
|
+
import ITButton from "@/components/atoms/button/button";
|
|
4
|
+
import ITInput from "@/components/atoms/input/input";
|
|
5
|
+
|
|
6
|
+
/** Props for the sandbox topbar search filter. */
|
|
7
|
+
export interface TopbarSearchProps {
|
|
8
|
+
/** Current (live) search term shown in the input. */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Called with the new term on every keystroke. */
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
/** Called when the user clears the search. */
|
|
13
|
+
onClear: () => void;
|
|
14
|
+
/** Number of matching navigation entries for the current term. */
|
|
15
|
+
resultCount: number;
|
|
16
|
+
/**
|
|
17
|
+
* When true, the debounced filter has not caught up with the live term yet.
|
|
18
|
+
* The result count is hidden so it never announces a stale number.
|
|
19
|
+
*/
|
|
20
|
+
pending?: boolean;
|
|
21
|
+
/** Additional CSS classes on the form wrapper. Defaults to `"w-full"`. */
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Topbar search box that filters every sandbox component live.
|
|
27
|
+
*
|
|
28
|
+
* Renders a search input with a clear button and an accessible live-region
|
|
29
|
+
* announcement of the result count. It lives in the topbar center slot
|
|
30
|
+
* (`topBar.centerContent`), so it stays visible while the sidebar collapses.
|
|
31
|
+
*/
|
|
32
|
+
export default function TopbarSearch({
|
|
33
|
+
value,
|
|
34
|
+
onChange,
|
|
35
|
+
onClear,
|
|
36
|
+
resultCount,
|
|
37
|
+
pending = false,
|
|
38
|
+
className = "w-full",
|
|
39
|
+
}: TopbarSearchProps) {
|
|
40
|
+
const isActive = value.trim().length > 0;
|
|
41
|
+
const showCount = isActive && !pending;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<form
|
|
45
|
+
role="search"
|
|
46
|
+
onSubmit={(event) => event.preventDefault()}
|
|
47
|
+
className={className}
|
|
48
|
+
>
|
|
49
|
+
<div className="flex items-center gap-1.5">
|
|
50
|
+
<div className="flex-1 min-w-0">
|
|
51
|
+
<ITInput
|
|
52
|
+
name="sandbox-topbar-search"
|
|
53
|
+
label="Buscar componentes"
|
|
54
|
+
labelClassName="sr-only"
|
|
55
|
+
placeholder="Buscar componente…"
|
|
56
|
+
value={value}
|
|
57
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
|
58
|
+
onChange(event.target.value)
|
|
59
|
+
}
|
|
60
|
+
iconLeft={<FaSearch size={12} />}
|
|
61
|
+
size="sm"
|
|
62
|
+
containerClassName="w-full"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{isActive && (
|
|
67
|
+
<ITButton
|
|
68
|
+
variant="text"
|
|
69
|
+
color="danger"
|
|
70
|
+
size="sm"
|
|
71
|
+
ariaLabel="Limpiar búsqueda"
|
|
72
|
+
onClick={onClear}
|
|
73
|
+
className="flex-shrink-0"
|
|
74
|
+
>
|
|
75
|
+
<FaTimes size={11} />
|
|
76
|
+
</ITButton>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<span role="status" aria-live="polite" className="sr-only">
|
|
81
|
+
{showCount ? `${resultCount} resultados` : ""}
|
|
82
|
+
</span>
|
|
83
|
+
</form>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox navigation catalog (dev-only, NOT part of the published package).
|
|
3
|
+
*
|
|
4
|
+
* Plain data only — no JSX, no icons. Icons live in the consumer (`App.tsx`)
|
|
5
|
+
* keyed by group id so this module stays serializable and router-friendly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A single component page inside a sandbox group. */
|
|
9
|
+
export interface SandboxSubItem {
|
|
10
|
+
/** Stable item id used in the hash route (`#ui-system/<group>/<id>`). */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Human-readable label shown in the sidebar and landing. */
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A family of component pages (e.g. forms, data visualization). */
|
|
17
|
+
export interface SandboxGroup {
|
|
18
|
+
/** Stable group id used internally and by `GROUP_SLUGS`. */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Human-readable group label. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Pages belonging to this group. */
|
|
23
|
+
subitems: SandboxSubItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** All sandbox groups and their pages, transcribed from the legacy App catalog. */
|
|
27
|
+
export const SANDBOX_GROUPS: SandboxGroup[] = [
|
|
28
|
+
{
|
|
29
|
+
id: "general",
|
|
30
|
+
label: "General",
|
|
31
|
+
subitems: [
|
|
32
|
+
{ id: "getting-started", label: "Getting Started" },
|
|
33
|
+
{ id: "sizes", label: "Medidas · sm / md / lg" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "struc",
|
|
38
|
+
label: "Estructura & Layout",
|
|
39
|
+
subitems: [
|
|
40
|
+
{ id: "layout", label: "ITLayout & ITNavbar" },
|
|
41
|
+
{ id: "stack", label: "ITStack" },
|
|
42
|
+
{ id: "flex", label: "ITFlex" },
|
|
43
|
+
{ id: "grid", label: "ITGrid" },
|
|
44
|
+
{ id: "card", label: "ITCard" },
|
|
45
|
+
{ id: "text", label: "ITText" },
|
|
46
|
+
{ id: "accordion", label: "ITAccordion" },
|
|
47
|
+
{ id: "pageheader", label: "ITPageHeader" },
|
|
48
|
+
{ id: "page", label: "ITPage" },
|
|
49
|
+
{ id: "screen-dashboard", label: "Dashboard Ejemplo" },
|
|
50
|
+
{ id: "screen-form", label: "Formulario Ejemplo" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "forms",
|
|
55
|
+
label: "Formularios & Inputs",
|
|
56
|
+
subitems: [
|
|
57
|
+
{ id: "button", label: "ITButton" },
|
|
58
|
+
{ id: "input", label: "ITInput" },
|
|
59
|
+
{ id: "select", label: "ITSelect" },
|
|
60
|
+
{ id: "searchselect", label: "ITSearchSelect" },
|
|
61
|
+
{ id: "multiselect", label: "ITMultiSelect" },
|
|
62
|
+
{ id: "chipinput", label: "ITChipInput" },
|
|
63
|
+
{ id: "field", label: "ITField" },
|
|
64
|
+
{ id: "datepicker", label: "ITDatePicker" },
|
|
65
|
+
{ id: "timepicker", label: "ITTimePicker" },
|
|
66
|
+
{ id: "maskedinput", label: "ITMaskedInput" },
|
|
67
|
+
{ id: "calendar", label: "ITCalendar" },
|
|
68
|
+
{ id: "slidetoggle", label: "ITSlideToggle" },
|
|
69
|
+
{ id: "dropfile", label: "ITDropfile" },
|
|
70
|
+
{ id: "wysiwyg", label: "ITWysiwyg" },
|
|
71
|
+
{ id: "formbuilder", label: "ITFormBuilder" },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "data",
|
|
76
|
+
label: "Visualización Datos",
|
|
77
|
+
subitems: [
|
|
78
|
+
{ id: "table", label: "ITTable" },
|
|
79
|
+
{ id: "datatable", label: "ITDataTable" },
|
|
80
|
+
{ id: "badget", label: "ITBadget" },
|
|
81
|
+
{ id: "chip", label: "ITChip" },
|
|
82
|
+
{ id: "image", label: "ITImage" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "nav",
|
|
87
|
+
label: "Navegación & Control",
|
|
88
|
+
subitems: [
|
|
89
|
+
{ id: "tabs", label: "ITTabs" },
|
|
90
|
+
{ id: "stepper", label: "ITStepper" },
|
|
91
|
+
{ id: "pagination", label: "ITPagination" },
|
|
92
|
+
{ id: "triplefilter", label: "ITTripleFilter" },
|
|
93
|
+
{ id: "dropdownmenu", label: "ITDropdownMenu" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "feed",
|
|
98
|
+
label: "Feedback & Sistema",
|
|
99
|
+
subitems: [
|
|
100
|
+
{ id: "dialog", label: "ITDialog" },
|
|
101
|
+
{ id: "toast", label: "ITToast" },
|
|
102
|
+
{ id: "loader", label: "ITLoader" },
|
|
103
|
+
{ id: "themeprovider", label: "ITThemeProvider" },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/** Readable group-id → URL-slug map. */
|
|
109
|
+
export const GROUP_SLUGS: Record<string, string> = {
|
|
110
|
+
general: "general",
|
|
111
|
+
struc: "structure",
|
|
112
|
+
forms: "forms",
|
|
113
|
+
data: "viewdata",
|
|
114
|
+
nav: "navigation",
|
|
115
|
+
feed: "feedback",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Inverse of `GROUP_SLUGS`: URL-slug → group-id. */
|
|
119
|
+
export const SLUG_TO_GROUP: Record<string, string> = Object.fromEntries(
|
|
120
|
+
Object.entries(GROUP_SLUGS).map(([id, slug]) => [slug, id]),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
/** Look up a group by its internal id. Returns `null` when unknown. */
|
|
124
|
+
export function groupById(id: string): SandboxGroup | null {
|
|
125
|
+
return SANDBOX_GROUPS.find((group) => group.id === id) ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Look up a group by its URL slug. Returns `null` when unknown. */
|
|
129
|
+
export function groupBySlug(slug: string): SandboxGroup | null {
|
|
130
|
+
const id = SLUG_TO_GROUP[slug];
|
|
131
|
+
return id ? groupById(id) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** First item id of a group, or `null` when the group is unknown/empty. */
|
|
135
|
+
export function firstItemOf(groupId: string): string | null {
|
|
136
|
+
return groupById(groupId)?.subitems[0]?.id ?? null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether `itemId` exists inside `groupId`. */
|
|
140
|
+
export function isValidItem(groupId: string, itemId: string): boolean {
|
|
141
|
+
return groupById(groupId)?.subitems.some((sub) => sub.id === itemId) ?? false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Total number of pages across every group (derived — never hardcoded). */
|
|
145
|
+
export const TOTAL_ITEMS = SANDBOX_GROUPS.reduce(
|
|
146
|
+
(total, group) => total + group.subitems.length,
|
|
147
|
+
0,
|
|
148
|
+
);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { GROUP_SLUGS, groupBySlug, isValidItem } from "./navigation";
|
|
3
|
+
|
|
4
|
+
/** Top-level sandbox views. `home` is the untouched portfolio. */
|
|
5
|
+
export type SandboxView = "home" | "ui-system";
|
|
6
|
+
|
|
7
|
+
/** Parsed representation of `window.location.hash`. */
|
|
8
|
+
export interface HashRoute {
|
|
9
|
+
/** Which top-level view the hash points at. */
|
|
10
|
+
view: SandboxView;
|
|
11
|
+
/** Internal group id (`forms`, `data`, …) or `null` on the landing. */
|
|
12
|
+
group: string | null;
|
|
13
|
+
/** Item id (`input`, `table`, …) or `null` for a group-only route. */
|
|
14
|
+
item: string | null;
|
|
15
|
+
/** Raw hash exactly as read from the URL (used by the 404 page). */
|
|
16
|
+
raw: string;
|
|
17
|
+
/** Whether the route resolved to a real page. `false` → render 404. */
|
|
18
|
+
valid: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a location hash into a `HashRoute`.
|
|
23
|
+
*
|
|
24
|
+
* Rules:
|
|
25
|
+
* - empty / first segment !== `ui-system` → portfolio (`home`).
|
|
26
|
+
* - `#ui-system` → landing.
|
|
27
|
+
* - `#ui-system/<group-slug>` → group landing (valid, `item` null).
|
|
28
|
+
* - `#ui-system/<group-slug>/<item-id>` → page (valid) or 404 when unknown.
|
|
29
|
+
* - more than 3 segments → 404.
|
|
30
|
+
*/
|
|
31
|
+
export function parseHash(hash: string): HashRoute {
|
|
32
|
+
const raw = hash;
|
|
33
|
+
const clean = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
34
|
+
const parts = clean.split("/").filter(Boolean);
|
|
35
|
+
|
|
36
|
+
if (parts.length === 0 || parts[0] !== "ui-system") {
|
|
37
|
+
return { view: "home", group: null, item: null, raw, valid: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (parts.length === 1) {
|
|
41
|
+
return { view: "ui-system", group: null, item: null, raw, valid: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (parts.length === 2) {
|
|
45
|
+
const group = groupBySlug(parts[1]);
|
|
46
|
+
if (!group) {
|
|
47
|
+
return { view: "ui-system", group: null, item: null, raw, valid: false };
|
|
48
|
+
}
|
|
49
|
+
return { view: "ui-system", group: group.id, item: null, raw, valid: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (parts.length === 3) {
|
|
53
|
+
const group = groupBySlug(parts[1]);
|
|
54
|
+
if (!group || !isValidItem(group.id, parts[2])) {
|
|
55
|
+
return {
|
|
56
|
+
view: "ui-system",
|
|
57
|
+
group: group?.id ?? null,
|
|
58
|
+
item: null,
|
|
59
|
+
raw,
|
|
60
|
+
valid: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { view: "ui-system", group: group.id, item: parts[2], raw, valid: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { view: "ui-system", group: null, item: null, raw, valid: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build a hash for a group/item pair, translating the group id to its slug. */
|
|
70
|
+
export function buildHash(group: string, item?: string): string {
|
|
71
|
+
const slug = GROUP_SLUGS[group] ?? group;
|
|
72
|
+
return item ? `#ui-system/${slug}/${item}` : `#ui-system/${slug}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Navigation options shared by every `goTo*` helper. */
|
|
76
|
+
export interface GoToOptions {
|
|
77
|
+
/** Replace the current history entry instead of pushing a new one. */
|
|
78
|
+
replace?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Result of `useHashRoute`. */
|
|
82
|
+
export interface UseHashRouteResult {
|
|
83
|
+
/** Current parsed route. */
|
|
84
|
+
route: HashRoute;
|
|
85
|
+
/** Navigate to a component page. */
|
|
86
|
+
goToItem: (group: string, item: string, options?: GoToOptions) => void;
|
|
87
|
+
/** Navigate to the `#ui-system` landing. */
|
|
88
|
+
goToLanding: (options?: GoToOptions) => void;
|
|
89
|
+
/** Navigate back to the portfolio (strips the hash). */
|
|
90
|
+
goToPortfolio: (options?: GoToOptions) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Zero-dependency hash router for the dev sandbox.
|
|
95
|
+
*
|
|
96
|
+
* Reads the initial hash on mount (deep links + refresh work), listens to both
|
|
97
|
+
* `hashchange` and `popstate` (so back/forward work no matter how the entry was
|
|
98
|
+
* created) and cleans both listeners up on unmount (StrictMode-safe).
|
|
99
|
+
*/
|
|
100
|
+
export function useHashRoute(): UseHashRouteResult {
|
|
101
|
+
const [route, setRoute] = useState<HashRoute>(() =>
|
|
102
|
+
parseHash(typeof window === "undefined" ? "" : window.location.hash),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const sync = () => setRoute(parseHash(window.location.hash));
|
|
107
|
+
window.addEventListener("hashchange", sync);
|
|
108
|
+
window.addEventListener("popstate", sync);
|
|
109
|
+
return () => {
|
|
110
|
+
window.removeEventListener("hashchange", sync);
|
|
111
|
+
window.removeEventListener("popstate", sync);
|
|
112
|
+
};
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const navigate = useCallback((hash: string, options?: GoToOptions) => {
|
|
116
|
+
if (options?.replace) {
|
|
117
|
+
window.history.replaceState(null, "", hash);
|
|
118
|
+
setRoute(parseHash(hash));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
window.location.hash = hash;
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const goToItem = useCallback(
|
|
125
|
+
(group: string, item: string, options?: GoToOptions) => {
|
|
126
|
+
navigate(buildHash(group, item), options);
|
|
127
|
+
},
|
|
128
|
+
[navigate],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const goToLanding = useCallback(
|
|
132
|
+
(options?: GoToOptions) => {
|
|
133
|
+
navigate("#ui-system", options);
|
|
134
|
+
},
|
|
135
|
+
[navigate],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const goToPortfolio = useCallback((options?: GoToOptions) => {
|
|
139
|
+
const url = `${window.location.pathname}${window.location.search}`;
|
|
140
|
+
if (options?.replace) {
|
|
141
|
+
window.history.replaceState(null, "", url);
|
|
142
|
+
} else {
|
|
143
|
+
window.history.pushState(null, "", url);
|
|
144
|
+
}
|
|
145
|
+
setRoute(parseHash(""));
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
return { route, goToItem, goToLanding, goToPortfolio };
|
|
149
|
+
}
|