@axzydev/axzy_ui_system 1.4.8 → 1.5.0
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 +2 -11
- package/dist/index.cjs +1826 -1351
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +115 -14
- package/dist/index.d.ts +115 -14
- package/dist/index.js +1481 -1010
- package/dist/index.js.map +1 -1
- package/dist/layered.css +1 -1
- package/package.json +3 -16
- package/src/App.tsx +90 -73
- package/src/components/atoms/badget/badget.tsx +23 -5
- package/src/components/atoms/chip/chip.tsx +3 -1
- package/src/components/atoms/image/image.tsx +3 -1
- package/src/components/atoms/input/input.tsx +8 -5
- package/src/components/atoms/loader/loader.tsx +3 -1
- package/src/components/atoms/slide/slide.props.ts +12 -0
- package/src/components/atoms/slide/slide.tsx +3 -0
- package/src/components/molecules/alert/alert.tsx +3 -1
- package/src/components/molecules/breadcrumbs/breadcrumbs.tsx +3 -1
- package/src/components/molecules/calendar/calendar.tsx +9 -6
- package/src/components/molecules/card/card.tsx +5 -2
- package/src/components/molecules/chip-input/chip-input.tsx +9 -7
- package/src/components/molecules/date-picker/datePicker.tsx +9 -9
- package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +1 -1
- package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +6 -3
- package/src/components/molecules/field/field.tsx +3 -1
- package/src/components/molecules/form-header/form-header.tsx +3 -1
- package/src/components/molecules/line-bar-chart/line-bar-chart.props.ts +2 -2
- package/src/components/molecules/line-bar-chart/line-bar-chart.tsx +20 -15
- package/src/components/molecules/masked-input/masked-input.tsx +3 -1
- package/src/components/molecules/multi-select/multi-select.tsx +12 -9
- package/src/components/molecules/pagination/pagination.tsx +11 -5
- package/src/components/molecules/search-select/search-select.tsx +11 -9
- package/src/components/molecules/select/select.tsx +6 -4
- package/src/components/molecules/stepper/stepper.tsx +6 -4
- package/src/components/molecules/table/table.tsx +30 -28
- package/src/components/molecules/time-picker/timePicker.tsx +8 -6
- package/src/components/molecules/wysiwyg/wysiwyg.tsx +23 -12
- package/src/components/organisms/confirm-dialog/confirm-dialog.props.ts +4 -4
- package/src/components/organisms/confirm-dialog/confirm-dialog.tsx +16 -8
- package/src/components/organisms/data-table/dataTable.tsx +31 -25
- package/src/components/organisms/dialog/dialog.tsx +3 -0
- package/src/components/organisms/drawer/drawer.tsx +3 -0
- package/src/components/organisms/dropfile/dropfile.tsx +19 -17
- package/src/components/organisms/form-builder/fieldRenderer.tsx +4 -2
- package/src/components/organisms/navbar/navbar.tsx +4 -2
- package/src/components/organisms/page-header/page-header.tsx +3 -1
- package/src/components/organisms/searchTable/components/PaginationControls.tsx +8 -6
- package/src/components/organisms/searchTable/components/PaginationInfo.tsx +3 -1
- package/src/components/organisms/searchTable/components/SearchAndSortBar.tsx +7 -5
- package/src/components/organisms/searchTable/components/SearchInput.tsx +4 -2
- package/src/components/organisms/searchTable/components/SortButton.tsx +13 -11
- package/src/components/organisms/searchTable/components/TableEmptyState.tsx +4 -2
- package/src/components/organisms/searchTable/components/TableRow.tsx +6 -4
- package/src/components/organisms/searchTable/searchTable.tsx +4 -2
- package/src/components/organisms/sidebar/sidebar.props.ts +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +9 -4
- package/src/components/organisms/toast/toast.tsx +3 -1
- package/src/components/organisms/topbar/topbar.tsx +4 -2
- package/src/components/templates/page/page.props.ts +4 -4
- package/src/components/templates/page/page.tsx +6 -4
- package/src/components/theme-provider/themeProvider.tsx +28 -17
- package/src/dev.css +77 -2
- package/src/global.d.ts +10 -0
- package/src/i18n/context.ts +9 -0
- package/src/i18n/index.ts +22 -0
- package/src/i18n/locales/en.ts +163 -0
- package/src/i18n/locales/es.ts +165 -0
- package/src/i18n/provider.tsx +44 -0
- package/src/i18n/translator.ts +82 -0
- package/src/i18n/types.ts +54 -0
- package/src/i18n/useITTranslation.ts +20 -0
- package/src/index.css +0 -1
- package/src/index.ts +7 -1
- package/src/sandbox/LandingShowcase.tsx +224 -580
- package/src/sandbox/NotFoundShowcase.tsx +51 -36
- package/src/sandbox/TopbarSearch.tsx +6 -4
- package/src/sandbox/controls.tsx +180 -0
- package/src/sandbox/i18n.ts +1929 -0
- package/src/sandbox/icons.tsx +25 -0
- package/src/sandbox/locale.tsx +72 -0
- package/src/sandbox/ui.tsx +377 -0
- package/src/sandbox/useHashRoute.ts +46 -10
- package/src/showcases/DataShowcases.tsx +103 -87
- package/src/showcases/FeedbackShowcases.tsx +37 -30
- package/src/showcases/FormShowcases.tsx +302 -214
- package/src/showcases/GettingStartedShowcase.tsx +151 -145
- package/src/showcases/HelpShowcase.tsx +126 -128
- package/src/showcases/HomeShowcase.tsx +678 -482
- package/src/showcases/LayoutPrimitivesShowcases.tsx +129 -114
- package/src/showcases/NavigationShowcases.tsx +45 -37
- package/src/showcases/PageShowcases.tsx +171 -152
- package/src/showcases/ProductoDetalle.tsx +272 -129
- package/src/showcases/ShowcaseLayout.tsx +270 -216
- package/src/showcases/SizesShowcase.tsx +123 -64
- package/src/showcases/StructureShowcases.tsx +157 -131
- package/src/showcases/portfolioUi.tsx +153 -0
- package/src/showcases/techIcons.ts +3 -0
- package/src/utils/table.utils.ts +10 -2
- package/src/components/atoms/avatar/avatar.doc.mdx +0 -48
- package/src/components/atoms/avatar/avatar.stories.tsx +0 -64
- package/src/components/atoms/badget/badget.doc.mdx +0 -46
- package/src/components/atoms/badget/badget.stories.tsx +0 -75
- package/src/components/atoms/button/button.doc.mdx +0 -59
- package/src/components/atoms/button/button.stories.tsx +0 -173
- package/src/components/atoms/checkbox/checkbox.doc.mdx +0 -54
- package/src/components/atoms/checkbox/checkbox.stories.tsx +0 -53
- package/src/components/atoms/chip/chip.doc.mdx +0 -47
- package/src/components/atoms/chip/chip.stories.tsx +0 -64
- package/src/components/atoms/divider/divider.doc.mdx +0 -42
- package/src/components/atoms/divider/divider.stories.tsx +0 -33
- package/src/components/atoms/flex/flex.doc.mdx +0 -59
- package/src/components/atoms/flex/flex.stories.tsx +0 -70
- package/src/components/atoms/grid/grid.doc.mdx +0 -54
- package/src/components/atoms/grid/grid.stories.tsx +0 -71
- package/src/components/atoms/image/image.doc.mdx +0 -44
- package/src/components/atoms/image/image.stories.tsx +0 -48
- package/src/components/atoms/input/input.doc.mdx +0 -70
- package/src/components/atoms/input/input.stories.tsx +0 -113
- package/src/components/atoms/input-number/input-number.doc.mdx +0 -74
- package/src/components/atoms/input-number/input-number.stories.tsx +0 -111
- package/src/components/atoms/loader/loader.doc.mdx +0 -43
- package/src/components/atoms/loader/loader.stories.tsx +0 -104
- package/src/components/atoms/popover/popover.doc.mdx +0 -53
- package/src/components/atoms/popover/popover.stories.tsx +0 -24
- package/src/components/atoms/progress/progress.doc.mdx +0 -48
- package/src/components/atoms/progress/progress.stories.tsx +0 -39
- package/src/components/atoms/radio/radio.doc.mdx +0 -62
- package/src/components/atoms/radio/radio.stories.tsx +0 -48
- package/src/components/atoms/segmented-control/segmented-control.doc.mdx +0 -60
- package/src/components/atoms/segmented-control/segmented-control.stories.tsx +0 -62
- package/src/components/atoms/skeleton/skeleton.doc.mdx +0 -43
- package/src/components/atoms/skeleton/skeleton.stories.tsx +0 -29
- package/src/components/atoms/slide/slide.doc.mdx +0 -46
- package/src/components/atoms/slide/slide.stories.tsx +0 -120
- package/src/components/atoms/slider/slider.doc.mdx +0 -49
- package/src/components/atoms/slider/slider.stories.tsx +0 -29
- package/src/components/atoms/stack/stack.doc.mdx +0 -126
- package/src/components/atoms/stack/stack.stories.tsx +0 -283
- package/src/components/atoms/text/text.doc.mdx +0 -41
- package/src/components/atoms/text/text.stories.tsx +0 -66
- package/src/components/atoms/textarea/textarea.doc.mdx +0 -64
- package/src/components/atoms/textarea/textarea.stories.tsx +0 -26
- package/src/components/atoms/tooltip/tooltip.doc.mdx +0 -47
- package/src/components/atoms/tooltip/tooltip.stories.tsx +0 -19
- package/src/components/isolation/isolation.stories.tsx +0 -202
- package/src/components/molecules/accordion/accordion.doc.mdx +0 -47
- package/src/components/molecules/accordion/accordion.stories.tsx +0 -68
- package/src/components/molecules/alert/alert.doc.mdx +0 -48
- package/src/components/molecules/alert/alert.stories.tsx +0 -40
- package/src/components/molecules/breadcrumbs/breadcrumbs.doc.mdx +0 -57
- package/src/components/molecules/breadcrumbs/breadcrumbs.stories.tsx +0 -20
- package/src/components/molecules/calendar/calendar.doc.mdx +0 -76
- package/src/components/molecules/calendar/calendar.stories.tsx +0 -90
- package/src/components/molecules/card/card.doc.mdx +0 -54
- package/src/components/molecules/card/card.stories.tsx +0 -57
- package/src/components/molecules/chip-input/chip-input.doc.mdx +0 -44
- package/src/components/molecules/chip-input/chip-input.stories.tsx +0 -52
- package/src/components/molecules/date-picker/date-picker.doc.mdx +0 -65
- package/src/components/molecules/date-picker/date-picker.stories.tsx +0 -88
- package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +0 -46
- package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +0 -41
- package/src/components/molecules/empty-state/empty-state.doc.mdx +0 -50
- package/src/components/molecules/empty-state/empty-state.stories.tsx +0 -19
- package/src/components/molecules/field/field.doc.mdx +0 -43
- package/src/components/molecules/field/field.stories.tsx +0 -42
- package/src/components/molecules/form-header/form-header.doc.mdx +0 -38
- package/src/components/molecules/form-header/form-header.stories.tsx +0 -52
- package/src/components/molecules/masked-input/masked-input.doc.mdx +0 -78
- package/src/components/molecules/masked-input/masked-input.stories.tsx +0 -126
- package/src/components/molecules/multi-select/multi-select.doc.mdx +0 -55
- package/src/components/molecules/multi-select/multi-select.stories.tsx +0 -59
- package/src/components/molecules/pagination/pagination.doc.mdx +0 -62
- package/src/components/molecules/pagination/pagination.stories.tsx +0 -110
- package/src/components/molecules/search-select/search-select.doc.mdx +0 -77
- package/src/components/molecules/search-select/search-select.stories.tsx +0 -146
- package/src/components/molecules/select/select.doc.mdx +0 -75
- package/src/components/molecules/select/select.stories.tsx +0 -85
- package/src/components/molecules/stat-card/stat-card.doc.mdx +0 -48
- package/src/components/molecules/stat-card/stat-card.stories.tsx +0 -40
- package/src/components/molecules/stepper/stepper.doc.mdx +0 -54
- package/src/components/molecules/stepper/stepper.stories.tsx +0 -154
- package/src/components/molecules/table/table.doc.mdx +0 -140
- package/src/components/molecules/table/table.stories.tsx +0 -402
- package/src/components/molecules/tabs/tabs.doc.mdx +0 -52
- package/src/components/molecules/tabs/tabs.stories.tsx +0 -31
- package/src/components/molecules/time-picker/timePicker.doc.mdx +0 -68
- package/src/components/molecules/time-picker/timePicker.stories.tsx +0 -130
- package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +0 -95
- package/src/components/organisms/confirm-dialog/confirm-dialog.doc.mdx +0 -60
- package/src/components/organisms/confirm-dialog/confirm-dialog.stories.tsx +0 -32
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +0 -132
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +0 -353
- package/src/components/organisms/dialog/dialog.doc.mdx +0 -54
- package/src/components/organisms/dialog/dialog.stories.tsx +0 -126
- package/src/components/organisms/drawer/drawer.doc.mdx +0 -59
- package/src/components/organisms/drawer/drawer.stories.tsx +0 -40
- package/src/components/organisms/dropfile/dropfile.doc.mdx +0 -74
- package/src/components/organisms/dropfile/dropfile.stories.tsx +0 -93
- package/src/components/organisms/form-builder/formBuilder.doc.mdx +0 -251
- package/src/components/organisms/form-builder/formBuilder.stories.tsx +0 -914
- package/src/components/organisms/navbar/navbar.doc.mdx +0 -77
- package/src/components/organisms/navbar/navbar.stories.tsx +0 -83
- package/src/components/organisms/page-header/page-header.doc.mdx +0 -54
- package/src/components/organisms/page-header/page-header.stories.tsx +0 -74
- package/src/components/organisms/searchTable/searchTable.doc.mdx +0 -79
- package/src/components/organisms/searchTable/searchTable.stories.tsx +0 -73
- package/src/components/organisms/sidebar/sidebar.doc.mdx +0 -176
- package/src/components/organisms/sidebar/sidebar.stories.tsx +0 -234
- package/src/components/organisms/toast/toast.doc.mdx +0 -50
- package/src/components/organisms/toast/toast.stories.tsx +0 -137
- package/src/components/organisms/topbar/topbar.doc.mdx +0 -72
- package/src/components/organisms/topbar/topbar.stories.tsx +0 -79
- package/src/components/organisms/triple-filter/tripleFilter.doc.mdx +0 -62
- package/src/components/organisms/triple-filter/tripleFilter.stories.tsx +0 -31
- package/src/components/templates/layout/layout.doc.mdx +0 -53
- package/src/components/templates/layout/layout.stories.tsx +0 -113
- package/src/components/templates/page/page.doc.mdx +0 -86
- package/src/components/templates/page/page.stories.tsx +0 -97
- package/src/components/theme-provider/themeProvider.doc.mdx +0 -85
- package/src/components/theme-provider/themeProvider.stories.tsx +0 -58
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Íconos de las familias del catálogo, compartidos por la barra lateral
|
|
3
|
+
* (`App.tsx`) y la landing (`LandingShowcase`) para que la misma familia se vea
|
|
4
|
+
* igual en los dos sitios.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
import {
|
|
9
|
+
FaBell,
|
|
10
|
+
FaCreditCard,
|
|
11
|
+
FaHome,
|
|
12
|
+
FaKeyboard,
|
|
13
|
+
FaSlidersH,
|
|
14
|
+
FaTable,
|
|
15
|
+
} from "react-icons/fa";
|
|
16
|
+
|
|
17
|
+
/** Ícono por id de grupo (`navigation.ts`). */
|
|
18
|
+
export const GROUP_ICONS: Record<string, ReactNode> = {
|
|
19
|
+
general: <FaHome />,
|
|
20
|
+
struc: <FaCreditCard />,
|
|
21
|
+
forms: <FaKeyboard />,
|
|
22
|
+
data: <FaTable />,
|
|
23
|
+
nav: <FaSlidersH />,
|
|
24
|
+
feed: <FaBell />,
|
|
25
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idioma del sandbox, compartido entre el chrome (ITTopBar) y las páginas del
|
|
3
|
+
* portafolio.
|
|
4
|
+
*
|
|
5
|
+
* La preferencia vivía solo en `App`, así que el portafolio no podía cambiarla:
|
|
6
|
+
* ahora se expone por contexto para que el selector del header del portafolio y
|
|
7
|
+
* el de la barra del sandbox muevan el mismo estado (y persistan en
|
|
8
|
+
* `localStorage`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
12
|
+
import type { ITLocale } from "@/i18n";
|
|
13
|
+
|
|
14
|
+
/** Valor del contexto de idioma del sandbox. */
|
|
15
|
+
export interface SandboxLocaleValue {
|
|
16
|
+
/** Idioma activo. */
|
|
17
|
+
locale: ITLocale;
|
|
18
|
+
/** Cambia el idioma y lo persiste. */
|
|
19
|
+
setLocale: (locale: ITLocale) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Clave de `localStorage` donde se recuerda la preferencia. */
|
|
23
|
+
const STORAGE_KEY = "it-locale";
|
|
24
|
+
|
|
25
|
+
const SandboxLocaleContext = createContext<SandboxLocaleValue | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lee la preferencia guardada. Cualquier valor distinto de `"en"` cae en `"es"`,
|
|
29
|
+
* que es el idioma por defecto del sandbox.
|
|
30
|
+
*/
|
|
31
|
+
function readStoredLocale(): ITLocale {
|
|
32
|
+
if (typeof window === "undefined") return "es";
|
|
33
|
+
return window.localStorage.getItem(STORAGE_KEY) === "en" ? "en" : "es";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Provee el idioma del sandbox a todo el árbol (chrome + páginas).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <SandboxLocaleProvider>
|
|
42
|
+
* <ITI18nProvider locale={locale} messages={sandboxMessages}>…</ITI18nProvider>
|
|
43
|
+
* </SandboxLocaleProvider>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function SandboxLocaleProvider({ children }: { children: ReactNode }) {
|
|
47
|
+
const [locale, setLocale] = useState<ITLocale>(readStoredLocale);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
window.localStorage.setItem(STORAGE_KEY, locale);
|
|
51
|
+
}, [locale]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<SandboxLocaleContext.Provider value={{ locale, setLocale }}>
|
|
55
|
+
{children}
|
|
56
|
+
</SandboxLocaleContext.Provider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Acceso al idioma del sandbox. Fuera del provider devuelve español y un cambio
|
|
62
|
+
* de idioma sin efecto, para que una página se pueda renderizar aislada (tests,
|
|
63
|
+
* Storybook futuro) sin explotar.
|
|
64
|
+
*/
|
|
65
|
+
export function useSandboxLocale(): SandboxLocaleValue {
|
|
66
|
+
return (
|
|
67
|
+
useContext(SandboxLocaleContext) ?? {
|
|
68
|
+
locale: "es",
|
|
69
|
+
setLocale: () => undefined,
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit de UI de las superficies dev (sandbox `#ui-system` + portafolio `/`).
|
|
3
|
+
*
|
|
4
|
+
* Son las piezas que **no** se usan de la librería publicada porque esta no
|
|
5
|
+
* garantiza contraste AA con la paleta por defecto: `ITButton` relleno queda en
|
|
6
|
+
* 2.4:1, `ITButton color="secondary"` en 3.7:1, `ITBadget` outlined en 2.6:1 y
|
|
7
|
+
* `ITSegmentedControl` en 4.34:1. Aquí el acento se usa en `primary-700`
|
|
8
|
+
* (claro) / `primary-500` (oscuro), que sí pasan.
|
|
9
|
+
*
|
|
10
|
+
* Vive en `src/sandbox/` a propósito: es dev-only y nunca entra en `dist/`.
|
|
11
|
+
* Sigue `DESIGN.md` (AXZY Console): superficies planas con borde de 1px,
|
|
12
|
+
* metadatos en mono, filetes para separar y sombra solo al hover.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState, type ReactNode } from "react";
|
|
16
|
+
import { FaCheck, FaCopy } from "react-icons/fa";
|
|
17
|
+
import { useITTranslation } from "@/i18n";
|
|
18
|
+
|
|
19
|
+
/** Eyebrow mono apagado: encabezados de sección y etiquetas de metadato. */
|
|
20
|
+
export const EYEBROW =
|
|
21
|
+
"font-mono !text-[11px] !font-medium uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400";
|
|
22
|
+
|
|
23
|
+
/** Filete de 1px: la separación entre bloques. */
|
|
24
|
+
export const FILETE = "border-slate-200 dark:border-slate-800";
|
|
25
|
+
|
|
26
|
+
/** Superficie de tarjeta: borde 1px, radio 14px, sin sombra en reposo. */
|
|
27
|
+
export const CARD =
|
|
28
|
+
"rounded-[14px] border border-slate-200 bg-white transition-all duration-200 dark:border-slate-800 dark:bg-slate-900";
|
|
29
|
+
|
|
30
|
+
/** Refuerzo al hover de las tarjetas: borde acento, lift de 2px y sombra suave. */
|
|
31
|
+
export const CARD_HOVER =
|
|
32
|
+
"hover:-translate-y-0.5 hover:border-primary-400 hover:shadow-[0_8px_30px_-12px_rgba(15,23,42,0.25)] dark:hover:border-primary-500";
|
|
33
|
+
|
|
34
|
+
/** Chip mono chico para stack y metadatos. */
|
|
35
|
+
export const CHIP =
|
|
36
|
+
"rounded-[6px] border border-slate-200 bg-slate-50 px-2 py-0.5 font-mono text-[10px] text-slate-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Variantes visuales de los CTA. `outlined` es el borde de acento (segundo CTA
|
|
40
|
+
* de un bloque) y `neutral` el ghost de tinta para acciones que no compiten
|
|
41
|
+
* (volver, cerrar).
|
|
42
|
+
*/
|
|
43
|
+
export type VarianteCta = "filled" | "outlined" | "neutral" | "text";
|
|
44
|
+
|
|
45
|
+
/** Tamaños de los CTA (mismos paddings y tipografía que `ITButton`). */
|
|
46
|
+
export type TamanoCta = "sm" | "md" | "lg";
|
|
47
|
+
|
|
48
|
+
/** Padding y tipografía por tamaño (idénticos a `ITButton`). */
|
|
49
|
+
export const TAMANO_CTA: Record<TamanoCta, string> = {
|
|
50
|
+
sm: "text-xs px-2.5 py-1",
|
|
51
|
+
md: "text-[13px] px-3.5 py-1.5",
|
|
52
|
+
lg: "text-sm px-4 py-2",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Color por variante. El relleno usa `primary-700` (5.05:1 con blanco) porque
|
|
57
|
+
* el `primary-600` de la paleta resuelta se queda en 3.3:1.
|
|
58
|
+
*/
|
|
59
|
+
export const VARIANTE_CTA: Record<VarianteCta, string> = {
|
|
60
|
+
filled:
|
|
61
|
+
"border border-transparent bg-primary-700 text-white shadow-sm hover:bg-primary-800",
|
|
62
|
+
outlined:
|
|
63
|
+
"border border-primary-700 text-primary-700 hover:bg-primary-700/10 dark:border-primary-500 dark:text-primary-500",
|
|
64
|
+
neutral:
|
|
65
|
+
"border border-slate-300 text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800",
|
|
66
|
+
text: "border border-transparent text-primary-700 hover:bg-primary-700/10 dark:text-primary-500",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Foco compartido por todos los CTA (anillo del acento, offset del lienzo). */
|
|
70
|
+
export const FOCO_CTA =
|
|
71
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-[#fafafa] dark:focus-visible:ring-offset-[#0b0d10]";
|
|
72
|
+
|
|
73
|
+
/** Props de `EnlaceBoton`. */
|
|
74
|
+
export interface EnlaceBotonProps {
|
|
75
|
+
/** Destino. Interno (`#...`, `/...`) o externo (`https://...`). */
|
|
76
|
+
href: string;
|
|
77
|
+
/** Texto visible del enlace. */
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
/** Estilo visual. @default "filled" */
|
|
80
|
+
variante?: VarianteCta;
|
|
81
|
+
/** Tamaño. @default "md" */
|
|
82
|
+
tamano?: TamanoCta;
|
|
83
|
+
/** Ícono a la izquierda del texto. */
|
|
84
|
+
icono?: ReactNode;
|
|
85
|
+
/** Fuerza abrir en pestaña nueva. Por defecto se deduce de `href`. */
|
|
86
|
+
externo?: boolean;
|
|
87
|
+
/** Marca la descarga (`download`) para archivos propios. */
|
|
88
|
+
descargar?: boolean;
|
|
89
|
+
/** Clases extra. */
|
|
90
|
+
className?: string;
|
|
91
|
+
/** Nombre accesible cuando el texto visible no alcanza. */
|
|
92
|
+
ariaLabel?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* CTA que **es un enlace de verdad**: `ITButton` renderiza un `<button>`, así que
|
|
97
|
+
* envolverlo en `<a>` producía interactivos anidados (HTML inválido y fallo
|
|
98
|
+
* `nested-interactive` en axe). Esta pieza conserva el aspecto del botón, pero
|
|
99
|
+
* mantiene la semántica de enlace: clic central, "abrir en pestaña nueva", menú
|
|
100
|
+
* contextual y URL en la barra de estado.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* <EnlaceBoton href={GITHUB_URL} variante="outlined" icono={<FaGithub size={12} />}>
|
|
105
|
+
* GitHub
|
|
106
|
+
* </EnlaceBoton>
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export const EnlaceBoton = ({
|
|
110
|
+
href,
|
|
111
|
+
children,
|
|
112
|
+
variante = "filled",
|
|
113
|
+
tamano = "md",
|
|
114
|
+
icono,
|
|
115
|
+
externo,
|
|
116
|
+
descargar = false,
|
|
117
|
+
className = "",
|
|
118
|
+
ariaLabel,
|
|
119
|
+
}: EnlaceBotonProps) => {
|
|
120
|
+
const esExterno = externo ?? /^https?:\/\//.test(href);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<a
|
|
124
|
+
href={href}
|
|
125
|
+
{...(esExterno ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
126
|
+
{...(descargar ? { download: "" } : {})}
|
|
127
|
+
aria-label={ariaLabel}
|
|
128
|
+
className={`inline-flex shrink-0 items-center justify-center gap-1.5 rounded-[10px] font-semibold transition-colors duration-200 ${FOCO_CTA} ${TAMANO_CTA[tamano]} ${VARIANTE_CTA[variante]} ${className}`}
|
|
129
|
+
>
|
|
130
|
+
{icono && <span className="flex-shrink-0">{icono}</span>}
|
|
131
|
+
<span>{children}</span>
|
|
132
|
+
</a>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** Props de `BotonAccion`. */
|
|
137
|
+
export interface BotonAccionProps {
|
|
138
|
+
/** Acción al pulsar. */
|
|
139
|
+
onClick: () => void;
|
|
140
|
+
/** Texto visible. */
|
|
141
|
+
children: ReactNode;
|
|
142
|
+
/** Estilo visual. @default "neutral" */
|
|
143
|
+
variante?: VarianteCta;
|
|
144
|
+
/** Tamaño. @default "md" */
|
|
145
|
+
tamano?: TamanoCta;
|
|
146
|
+
/** Ícono a la izquierda. */
|
|
147
|
+
icono?: ReactNode;
|
|
148
|
+
/** Clases extra. */
|
|
149
|
+
className?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Acción que **no** navega (volver, abrir una pestaña interna): mismo aspecto que
|
|
154
|
+
* `EnlaceBoton` pero con semántica de botón.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* <BotonAccion onClick={onBack} icono={<FaArrowLeft size={12} />}>Volver</BotonAccion>
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export const BotonAccion = ({
|
|
162
|
+
onClick,
|
|
163
|
+
children,
|
|
164
|
+
variante = "neutral",
|
|
165
|
+
tamano = "md",
|
|
166
|
+
icono,
|
|
167
|
+
className = "",
|
|
168
|
+
}: BotonAccionProps) => (
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={onClick}
|
|
172
|
+
className={`inline-flex shrink-0 items-center justify-center gap-1.5 rounded-[10px] font-semibold transition-colors duration-200 ${FOCO_CTA} ${TAMANO_CTA[tamano]} ${VARIANTE_CTA[variante]} ${className}`}
|
|
173
|
+
>
|
|
174
|
+
{icono && <span className="flex-shrink-0">{icono}</span>}
|
|
175
|
+
<span>{children}</span>
|
|
176
|
+
</button>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
/** Props de `Etiqueta`. */
|
|
180
|
+
export interface EtiquetaProps {
|
|
181
|
+
/** Texto de la etiqueta (tipo, estado…). */
|
|
182
|
+
children: ReactNode;
|
|
183
|
+
/** Clases extra. */
|
|
184
|
+
className?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Píldora de metadato con el acento del proyecto.
|
|
189
|
+
*
|
|
190
|
+
* Sustituye a `ITBadget`: el badge del tema resuelve su color desde la paleta y
|
|
191
|
+
* con la paleta por defecto queda en 2.6:1 sobre las tarjetas oscuras.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* <Etiqueta>Librería</Etiqueta>
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export const Etiqueta = ({ children, className = "" }: EtiquetaProps) => (
|
|
199
|
+
<span
|
|
200
|
+
className={`shrink-0 rounded-full border border-primary-800/45 px-2 py-[3px] text-[11px] font-semibold text-primary-800 dark:border-primary-500/45 dark:text-primary-500 ${className}`}
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
</span>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Encabezado de sección de la hoja: `NN — ETIQUETA`, título display y apoyo,
|
|
208
|
+
* con acciones opcionales a la derecha.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```tsx
|
|
212
|
+
* <SectionHeading index={2} label="Catálogo" title="Explora por familia" subtitle="…" />
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
export const SectionHeading = ({
|
|
216
|
+
index,
|
|
217
|
+
label,
|
|
218
|
+
title,
|
|
219
|
+
subtitle,
|
|
220
|
+
actions,
|
|
221
|
+
className = "",
|
|
222
|
+
}: {
|
|
223
|
+
/** Índice de sección (0-based; se pinta con dos dígitos). */
|
|
224
|
+
index: number;
|
|
225
|
+
/** Etiqueta mono corta. */
|
|
226
|
+
label: string;
|
|
227
|
+
/** Título display. */
|
|
228
|
+
title: string;
|
|
229
|
+
/** Frase de apoyo. */
|
|
230
|
+
subtitle?: string;
|
|
231
|
+
/** Contenido alineado a la derecha. */
|
|
232
|
+
actions?: ReactNode;
|
|
233
|
+
/** Clases extra. */
|
|
234
|
+
className?: string;
|
|
235
|
+
}) => (
|
|
236
|
+
<div
|
|
237
|
+
className={`flex flex-col gap-5 md:flex-row md:items-end md:justify-between ${className}`}
|
|
238
|
+
>
|
|
239
|
+
<div className="min-w-0">
|
|
240
|
+
<p className={EYEBROW}>
|
|
241
|
+
<span className="tabular-nums">{String(index + 1).padStart(2, "0")}</span>
|
|
242
|
+
{` — ${label}`}
|
|
243
|
+
</p>
|
|
244
|
+
<h2 className="mt-2.5 font-display text-[26px] font-bold tracking-[-0.02em] text-slate-900 dark:text-white sm:text-[32px]">
|
|
245
|
+
{title}
|
|
246
|
+
</h2>
|
|
247
|
+
{subtitle && (
|
|
248
|
+
<p className="mt-2 max-w-[62ch] text-[15px] leading-relaxed text-slate-500 dark:text-slate-400">
|
|
249
|
+
{subtitle}
|
|
250
|
+
</p>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
{actions && <div className="shrink-0">{actions}</div>}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Enlace externo mono en línea (GitHub, npm, docs). Mantiene el acento AA y se
|
|
259
|
+
* subraya al hover.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```tsx
|
|
263
|
+
* <EnlaceMono href={GITHUB_URL} icono={<FaGithub size={11} />}>GitHub</EnlaceMono>
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export const EnlaceMono = ({
|
|
267
|
+
href,
|
|
268
|
+
children,
|
|
269
|
+
icono,
|
|
270
|
+
className = "",
|
|
271
|
+
}: {
|
|
272
|
+
/** Destino (normalmente externo). */
|
|
273
|
+
href: string;
|
|
274
|
+
/** Texto visible. */
|
|
275
|
+
children: ReactNode;
|
|
276
|
+
/** Ícono a la izquierda. */
|
|
277
|
+
icono?: ReactNode;
|
|
278
|
+
/** Clases extra. */
|
|
279
|
+
className?: string;
|
|
280
|
+
}) => (
|
|
281
|
+
<a
|
|
282
|
+
href={href}
|
|
283
|
+
{...(/^https?:\/\//.test(href) ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
284
|
+
className={`inline-flex items-center gap-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] !text-primary-700 transition-colors hover:underline dark:!text-primary-500 ${className}`}
|
|
285
|
+
>
|
|
286
|
+
{icono}
|
|
287
|
+
{children}
|
|
288
|
+
</a>
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Botón mono de «copiar al portapapeles» con confirmación de dos segundos.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```tsx
|
|
296
|
+
* <BotonCopiar texto="pnpm add @axzydev/axzy_ui_system" />
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export const BotonCopiar = ({
|
|
300
|
+
texto,
|
|
301
|
+
className = "",
|
|
302
|
+
}: {
|
|
303
|
+
/** Texto que se copia. */
|
|
304
|
+
texto: string;
|
|
305
|
+
/** Clases extra. */
|
|
306
|
+
className?: string;
|
|
307
|
+
}) => {
|
|
308
|
+
const { t } = useITTranslation();
|
|
309
|
+
const [copiado, setCopiado] = useState(false);
|
|
310
|
+
|
|
311
|
+
const copiar = () => {
|
|
312
|
+
navigator.clipboard?.writeText(texto);
|
|
313
|
+
setCopiado(true);
|
|
314
|
+
window.setTimeout(() => setCopiado(false), 2000);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
onClick={copiar}
|
|
321
|
+
className={`inline-flex items-center gap-1.5 rounded-[8px] px-2.5 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/60 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white ${className}`}
|
|
322
|
+
>
|
|
323
|
+
{copiado ? (
|
|
324
|
+
<FaCheck size={10} className="text-primary-700 dark:text-primary-500" />
|
|
325
|
+
) : (
|
|
326
|
+
<FaCopy size={10} />
|
|
327
|
+
)}
|
|
328
|
+
{copiado ? t("showcase.layout.copied") : t("showcase.layout.copy")}
|
|
329
|
+
</button>
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Bloque de código: fondo tinta, mono, etiqueta del archivo y botón de copiar.
|
|
335
|
+
* El acento de señal (`cyan-400`) solo se usa aquí, como manda el spec.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```tsx
|
|
339
|
+
* <BloqueCodigo filename="vite.config.ts" code={`export default defineConfig({})`} />
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
export const BloqueCodigo = ({
|
|
343
|
+
code,
|
|
344
|
+
filename,
|
|
345
|
+
compact = false,
|
|
346
|
+
className = "",
|
|
347
|
+
}: {
|
|
348
|
+
/** Código a mostrar. */
|
|
349
|
+
code: string;
|
|
350
|
+
/** Etiqueta mono de la esquina (archivo o lenguaje). */
|
|
351
|
+
filename?: string;
|
|
352
|
+
/** Limita la altura con scroll vertical. */
|
|
353
|
+
compact?: boolean;
|
|
354
|
+
/** Clases extra. */
|
|
355
|
+
className?: string;
|
|
356
|
+
}) => (
|
|
357
|
+
<div
|
|
358
|
+
className={`overflow-hidden rounded-[10px] border border-slate-800 bg-slate-950 ${className}`}
|
|
359
|
+
>
|
|
360
|
+
<div className="flex items-center justify-between gap-3 border-b border-slate-800 px-4 py-2.5">
|
|
361
|
+
<span className="font-mono text-[10px] uppercase tracking-[0.14em] text-cyan-400">
|
|
362
|
+
{filename ?? "code"}
|
|
363
|
+
</span>
|
|
364
|
+
<BotonCopiar
|
|
365
|
+
texto={code}
|
|
366
|
+
className="text-slate-400 hover:bg-slate-800 hover:text-white dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
<pre
|
|
370
|
+
className={`overflow-x-auto whitespace-pre-wrap p-4 font-mono text-xs leading-relaxed text-slate-200 ${
|
|
371
|
+
compact ? "max-h-[420px] overflow-y-auto" : ""
|
|
372
|
+
}`}
|
|
373
|
+
>
|
|
374
|
+
<code>{code}</code>
|
|
375
|
+
</pre>
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { GROUP_SLUGS, groupBySlug, isValidItem } from "./navigation";
|
|
3
|
+
import { PRODUCTOS } from "../showcases/productos";
|
|
3
4
|
|
|
4
|
-
/** Top-level sandbox views. `home` is the
|
|
5
|
-
export type SandboxView = "home" | "ui-system";
|
|
5
|
+
/** Top-level sandbox views. `home` is the portfolio, `producto` a project detail page. */
|
|
6
|
+
export type SandboxView = "home" | "ui-system" | "producto";
|
|
6
7
|
|
|
7
8
|
/** Parsed representation of `window.location.hash`. */
|
|
8
9
|
export interface HashRoute {
|
|
@@ -12,20 +13,28 @@ export interface HashRoute {
|
|
|
12
13
|
group: string | null;
|
|
13
14
|
/** Item id (`input`, `table`, …) or `null` for a group-only route. */
|
|
14
15
|
item: string | null;
|
|
16
|
+
/** Product id when `view === "producto"`. */
|
|
17
|
+
producto: string | null;
|
|
15
18
|
/** Raw hash exactly as read from the URL (used by the 404 page). */
|
|
16
19
|
raw: string;
|
|
17
20
|
/** Whether the route resolved to a real page. `false` → render 404. */
|
|
18
21
|
valid: boolean;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/** Whether a product id exists in the portfolio catalog. */
|
|
25
|
+
function isValidProducto(id: string): boolean {
|
|
26
|
+
return PRODUCTOS.some((producto) => producto.id === id);
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* Parse a location hash into a `HashRoute`.
|
|
23
31
|
*
|
|
24
32
|
* Rules:
|
|
25
|
-
* - empty / first segment
|
|
33
|
+
* - empty / first segment not in `{ui-system, proyecto}` → portfolio (`home`).
|
|
26
34
|
* - `#ui-system` → landing.
|
|
27
35
|
* - `#ui-system/<group-slug>` → group landing (valid, `item` null).
|
|
28
36
|
* - `#ui-system/<group-slug>/<item-id>` → page (valid) or 404 when unknown.
|
|
37
|
+
* - `#proyecto/<product-id>` → project detail page (valid) or 404 when unknown.
|
|
29
38
|
* - more than 3 segments → 404.
|
|
30
39
|
*/
|
|
31
40
|
export function parseHash(hash: string): HashRoute {
|
|
@@ -33,20 +42,32 @@ export function parseHash(hash: string): HashRoute {
|
|
|
33
42
|
const clean = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
34
43
|
const parts = clean.split("/").filter(Boolean);
|
|
35
44
|
|
|
45
|
+
if (parts[0] === "proyecto") {
|
|
46
|
+
const producto = parts[1] ?? null;
|
|
47
|
+
return {
|
|
48
|
+
view: "producto",
|
|
49
|
+
group: null,
|
|
50
|
+
item: null,
|
|
51
|
+
producto,
|
|
52
|
+
raw,
|
|
53
|
+
valid: Boolean(producto) && isValidProducto(producto as string),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
if (parts.length === 0 || parts[0] !== "ui-system") {
|
|
37
|
-
return { view: "home", group: null, item: null, raw, valid: true };
|
|
58
|
+
return { view: "home", group: null, item: null, producto: null, raw, valid: true };
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
if (parts.length === 1) {
|
|
41
|
-
return { view: "ui-system", group: null, item: null, raw, valid: true };
|
|
62
|
+
return { view: "ui-system", group: null, item: null, producto: null, raw, valid: true };
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
if (parts.length === 2) {
|
|
45
66
|
const group = groupBySlug(parts[1]);
|
|
46
67
|
if (!group) {
|
|
47
|
-
return { view: "ui-system", group: null, item: null, raw, valid: false };
|
|
68
|
+
return { view: "ui-system", group: null, item: null, producto: null, raw, valid: false };
|
|
48
69
|
}
|
|
49
|
-
return { view: "ui-system", group: group.id, item: null, raw, valid: true };
|
|
70
|
+
return { view: "ui-system", group: group.id, item: null, producto: null, raw, valid: true };
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
if (parts.length === 3) {
|
|
@@ -56,14 +77,15 @@ export function parseHash(hash: string): HashRoute {
|
|
|
56
77
|
view: "ui-system",
|
|
57
78
|
group: group?.id ?? null,
|
|
58
79
|
item: null,
|
|
80
|
+
producto: null,
|
|
59
81
|
raw,
|
|
60
82
|
valid: false,
|
|
61
83
|
};
|
|
62
84
|
}
|
|
63
|
-
return { view: "ui-system", group: group.id, item: parts[2], raw, valid: true };
|
|
85
|
+
return { view: "ui-system", group: group.id, item: parts[2], producto: null, raw, valid: true };
|
|
64
86
|
}
|
|
65
87
|
|
|
66
|
-
return { view: "ui-system", group: null, item: null, raw, valid: false };
|
|
88
|
+
return { view: "ui-system", group: null, item: null, producto: null, raw, valid: false };
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
/** Build a hash for a group/item pair, translating the group id to its slug. */
|
|
@@ -72,6 +94,11 @@ export function buildHash(group: string, item?: string): string {
|
|
|
72
94
|
return item ? `#ui-system/${slug}/${item}` : `#ui-system/${slug}`;
|
|
73
95
|
}
|
|
74
96
|
|
|
97
|
+
/** Build the hash of a project detail page. */
|
|
98
|
+
export function buildProductoHash(id: string): string {
|
|
99
|
+
return `#proyecto/${id}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
/** Navigation options shared by every `goTo*` helper. */
|
|
76
103
|
export interface GoToOptions {
|
|
77
104
|
/** Replace the current history entry instead of pushing a new one. */
|
|
@@ -86,6 +113,8 @@ export interface UseHashRouteResult {
|
|
|
86
113
|
goToItem: (group: string, item: string, options?: GoToOptions) => void;
|
|
87
114
|
/** Navigate to the `#ui-system` landing. */
|
|
88
115
|
goToLanding: (options?: GoToOptions) => void;
|
|
116
|
+
/** Navigate to a project detail page. */
|
|
117
|
+
goToProducto: (id: string, options?: GoToOptions) => void;
|
|
89
118
|
/** Navigate back to the portfolio (strips the hash). */
|
|
90
119
|
goToPortfolio: (options?: GoToOptions) => void;
|
|
91
120
|
}
|
|
@@ -135,6 +164,13 @@ export function useHashRoute(): UseHashRouteResult {
|
|
|
135
164
|
[navigate],
|
|
136
165
|
);
|
|
137
166
|
|
|
167
|
+
const goToProducto = useCallback(
|
|
168
|
+
(id: string, options?: GoToOptions) => {
|
|
169
|
+
navigate(buildProductoHash(id), options);
|
|
170
|
+
},
|
|
171
|
+
[navigate],
|
|
172
|
+
);
|
|
173
|
+
|
|
138
174
|
const goToPortfolio = useCallback((options?: GoToOptions) => {
|
|
139
175
|
const url = `${window.location.pathname}${window.location.search}`;
|
|
140
176
|
if (options?.replace) {
|
|
@@ -145,5 +181,5 @@ export function useHashRoute(): UseHashRouteResult {
|
|
|
145
181
|
setRoute(parseHash(""));
|
|
146
182
|
}, []);
|
|
147
183
|
|
|
148
|
-
return { route, goToItem, goToLanding, goToPortfolio };
|
|
184
|
+
return { route, goToItem, goToLanding, goToProducto, goToPortfolio };
|
|
149
185
|
}
|