@samuelgomez/astro 0.1.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 +54 -0
- package/package.json +42 -0
- package/src/components/Accordion/index.astro +31 -0
- package/src/components/Alert/AlertDanger.astro +12 -0
- package/src/components/Alert/AlertInfo.astro +12 -0
- package/src/components/Alert/AlertSuccess.astro +12 -0
- package/src/components/Alert/AlertWarning.astro +12 -0
- package/src/components/Alert/index.astro +10 -0
- package/src/components/Badge/BadgeDanger.astro +15 -0
- package/src/components/Badge/BadgeInfo.astro +15 -0
- package/src/components/Badge/BadgeSuccess.astro +15 -0
- package/src/components/Badge/BadgeWarning.astro +15 -0
- package/src/components/Badge/index.astro +9 -0
- package/src/components/Box/index.astro +45 -0
- package/src/components/BoxPost/index.astro +66 -0
- package/src/components/Button/index.astro +35 -0
- package/src/components/Caniuse/index.astro +24 -0
- package/src/components/Codepen/index.astro +25 -0
- package/src/components/DebugGrid/index.astro +100 -0
- package/src/components/Demo/Demoui.astro +13 -0
- package/src/components/Demo/index.astro +105 -0
- package/src/components/Dialog/index.astro +74 -0
- package/src/components/FormattedDate/index.astro +17 -0
- package/src/components/GithubContributions/index.astro +88 -0
- package/src/components/Home/CardsBlog.astro +41 -0
- package/src/components/Home/CardsCodepen.astro +53 -0
- package/src/components/Home/CardsGithub.astro +44 -0
- package/src/components/Img/index.astro +28 -0
- package/src/components/Link/index.astro +33 -0
- package/src/components/Nav/NavToggle.astro +40 -0
- package/src/components/Nav/index.astro +71 -0
- package/src/components/Pagination/PaginationItem.astro +51 -0
- package/src/components/Pagination/index.astro +39 -0
- package/src/components/ScrollWatcher/index.astro +11 -0
- package/src/components/Social/index.astro +22 -0
- package/src/components/Support/index.astro +24 -0
- package/src/components/Svg/index.astro +30 -0
- package/src/components/SwitchTheme.astro +45 -0
- package/src/components/Table/Table.astro +15 -0
- package/src/components/Table/TableBody.astro +21 -0
- package/src/components/Table/TableHeader.astro +19 -0
- package/src/components/Table/index.astro +21 -0
- package/src/components/TableOfContent/index.astro +117 -0
- package/src/components/Tabs/TabList.astro +23 -0
- package/src/components/Tabs/TabListItem.astro +22 -0
- package/src/components/Tabs/TabPanel.astro +19 -0
- package/src/components/Tabs/TabsContainer.astro +174 -0
- package/src/components/Tabs/index.astro +46 -0
- package/src/components/Title/h1.astro +12 -0
- package/src/components/Title/h2.astro +12 -0
- package/src/components/Title/h3.astro +12 -0
- package/src/components/Title/h4.astro +12 -0
- package/src/components/Title/h5.astro +12 -0
- package/src/components/Title/h6.astro +12 -0
- package/src/components/Title/heading.astro +8 -0
- package/src/components/Title/index.astro +39 -0
- package/src/components/WrapperToHtml/index.astro +8 -0
- package/src/components/form/Field/index.astro +43 -0
- package/src/components/form/Fieldset/index.astro +14 -0
- package/src/components/form/Form/FieldDate.astro +32 -0
- package/src/components/form/Form/FieldEmail.astro +32 -0
- package/src/components/form/Form/FieldNumber.astro +35 -0
- package/src/components/form/Form/FieldPassword.astro +32 -0
- package/src/components/form/Form/FieldRadio.astro +32 -0
- package/src/components/form/Form/FieldSelect.astro +26 -0
- package/src/components/form/Form/FieldText.astro +35 -0
- package/src/components/form/Form/FieldTextarea.astro +25 -0
- package/src/components/form/Form/index.astro +5 -0
- package/src/components/form/Input/index.astro +27 -0
- package/src/components/form/InputRadio/index.astro +30 -0
- package/src/components/form/MoreInfo/index.astro +14 -0
- package/src/components/form/Select/index.astro +25 -0
- package/src/components/form/Status/index.astro +34 -0
- package/src/components/form/Textarea/index.astro +26 -0
- package/src/components/helpers/WrapperOrNot/index.astro +23 -0
- package/src/components/layout/Footer.astro +3 -0
- package/src/components/layout/Head.astro +154 -0
- package/src/components/layout/Header.astro +28 -0
- package/src/components/old/Grid.astro +18 -0
- package/src/components/old/Section.astro +19 -0
- package/src/components/old/SwitchTheme.astro +66 -0
- package/src/components/old/index-webco.astro +55 -0
- package/src/components/old/send.astro +28 -0
- package/src/helpers/dom.ts +19 -0
- package/src/helpers/isEmptyOrNull.test.ts +58 -0
- package/src/helpers/isEmptyOrNull.ts +6 -0
- package/src/helpers/setSlug.test.ts +20 -0
- package/src/helpers/setSlug.ts +7 -0
- package/src/helpers/setTocTitle.ts +2 -0
- package/src/helpers/setVariants.test.ts +26 -0
- package/src/helpers/setVariants.ts +18 -0
- package/src/icons/Add.astro +18 -0
- package/src/icons/Anchor.astro +22 -0
- package/src/icons/Arrow.astro +11 -0
- package/src/icons/AstroLogo.astro +35 -0
- package/src/icons/Check.astro +11 -0
- package/src/icons/ChevronDown.astro +11 -0
- package/src/icons/ChevronLeft.astro +11 -0
- package/src/icons/ChevronRight.astro +11 -0
- package/src/icons/Codepen.astro +36 -0
- package/src/icons/Cross.astro +11 -0
- package/src/icons/ExternalLink.astro +19 -0
- package/src/icons/Github.astro +36 -0
- package/src/icons/Github2.astro +11 -0
- package/src/icons/Grid.astro +19 -0
- package/src/icons/Info.astro +11 -0
- package/src/icons/Instagram.astro +11 -0
- package/src/icons/Linkedin.astro +11 -0
- package/src/icons/LogoSG.astro +66 -0
- package/src/icons/MoonSun.astro +17 -0
- package/src/icons/Send.astro +11 -0
- package/src/icons/Slash.astro +14 -0
- package/src/icons/Trash.astro +19 -0
- package/src/icons/Twitter.astro +14 -0
- package/src/types/Permutations.d.ts +3 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Link from '@samuelgomez/astro/components/Link/index.astro';
|
|
3
|
+
import Arrow from '@samuelgomez/astro/icons/Arrow.astro';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<figure>
|
|
7
|
+
<div>
|
|
8
|
+
<Link href="#body"><Arrow slot="after-link" title="Allez en haut" /></Link>
|
|
9
|
+
</div>
|
|
10
|
+
<figcaption></figcaption>
|
|
11
|
+
</figure>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Linkedin from '@samuelgomez/astro/icons/Linkedin.astro';
|
|
3
|
+
import Twitter from '@samuelgomez/astro/icons/Twitter.astro';
|
|
4
|
+
import Github from '@samuelgomez/astro/icons/Github2.astro';
|
|
5
|
+
import Instagram from '@samuelgomez/astro/icons/Instagram.astro';
|
|
6
|
+
import Link from '@samuelgomez/astro/components/Link/index.astro';
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<menu>
|
|
10
|
+
<Link href="https://github.com/samuel-gomez">
|
|
11
|
+
<Github slot="after-link" title="Github de Samuel Gomez" />
|
|
12
|
+
</Link>
|
|
13
|
+
<Link href="https://www.linkedin.com/in/samuel-gomez-developpeur-web/">
|
|
14
|
+
<Linkedin slot="after-link" title="Linkedin de Samuel Gomez" />
|
|
15
|
+
</Link>
|
|
16
|
+
<Link href="https://twitter.com/gamuez">
|
|
17
|
+
<Twitter slot="after-link" title="Twitter de Samuel Gomez" />
|
|
18
|
+
</Link>
|
|
19
|
+
<Link href="https://www.instagram.com/gamuez_art/">
|
|
20
|
+
<Instagram slot="after-link" title="Instagram de Samuel Gomez" />
|
|
21
|
+
</Link>
|
|
22
|
+
</menu>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AlertDanger from '@samuelgomez/astro/components/Alert/AlertDanger.astro';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
condition?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const { condition } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<AlertDanger title="Non supporté">
|
|
12
|
+
<slot>
|
|
13
|
+
Votre navigateur ne supporte pas cette fonctionnalité. Les démos ne seront
|
|
14
|
+
pas visibles.
|
|
15
|
+
</slot>
|
|
16
|
+
</AlertDanger>
|
|
17
|
+
|
|
18
|
+
<style define:vars={{ condition }}>
|
|
19
|
+
@supports (starting-style) {
|
|
20
|
+
.box-alert.box-error {
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
export type SvgProps = HTMLAttributes<'svg'> & {
|
|
5
|
+
size?: number;
|
|
6
|
+
title?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
fill = 'currentColor',
|
|
11
|
+
size = 24,
|
|
12
|
+
title,
|
|
13
|
+
...props
|
|
14
|
+
} = Astro.props as SvgProps;
|
|
15
|
+
|
|
16
|
+
const titleId = Boolean(title) ? `${crypto.randomUUID()}` : null;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<svg
|
|
20
|
+
viewBox={`0 0 ${size} ${size}`}
|
|
21
|
+
width={`${size}`}
|
|
22
|
+
height={`${size}`}
|
|
23
|
+
fill={fill}
|
|
24
|
+
role="img"
|
|
25
|
+
aria-labelledby={titleId}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{Boolean(title) && <title id={titleId}>{title}</title>}
|
|
29
|
+
<slot />
|
|
30
|
+
</svg>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import MoonSun from '@samuelgomez/astro/icons/MoonSun.astro';
|
|
3
|
+
import Button from '@samuelgomez/astro/components/Button/index.astro';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<Button
|
|
7
|
+
variant="ghost small"
|
|
8
|
+
id="switch-theme"
|
|
9
|
+
title="Changer en mode clair ou sombre"
|
|
10
|
+
aria-live="polite"
|
|
11
|
+
>
|
|
12
|
+
<MoonSun />
|
|
13
|
+
</Button>
|
|
14
|
+
|
|
15
|
+
<script is:inline>
|
|
16
|
+
const DARK = 'dark';
|
|
17
|
+
const LIGHT = 'light';
|
|
18
|
+
const themeToggle = document.querySelector('#switch-theme');
|
|
19
|
+
const matchMediaDark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
20
|
+
|
|
21
|
+
// const getMatchMedia = () => (matchMediaDark.matches ? DARK : LIGHT);
|
|
22
|
+
|
|
23
|
+
let theme = localStorage.getItem('theme') || DARK;
|
|
24
|
+
|
|
25
|
+
themeToggle?.setAttribute('aria-label', theme);
|
|
26
|
+
|
|
27
|
+
const setTheme = (isSetToDark) => {
|
|
28
|
+
theme = isSetToDark ? DARK : LIGHT;
|
|
29
|
+
localStorage.setItem('theme', theme);
|
|
30
|
+
themeToggle?.setAttribute('aria-label', theme);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (!Boolean(themeToggle?.getAttribute('aria-label'))) {
|
|
34
|
+
setTheme(getMatchMedia() === DARK);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
window.addEventListener('load', () => {
|
|
38
|
+
themeToggle?.setAttribute('aria-label', theme);
|
|
39
|
+
themeToggle?.addEventListener('click', () => setTheme(theme === LIGHT));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
matchMediaDark.addEventListener('change', ({ matches: isDark }) =>
|
|
43
|
+
setTheme(isDark)
|
|
44
|
+
);
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
export type TableProps = HTMLAttributes<'table'> & {
|
|
5
|
+
caption: string;
|
|
6
|
+
idCaption: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const { caption, idCaption, ...tableProps } = Astro.props as TableProps;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<table {...tableProps}>
|
|
13
|
+
{Boolean(caption) && <caption id={idCaption}>{caption}</caption>}
|
|
14
|
+
<slot />
|
|
15
|
+
</table>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
export type TableBodyProps = {
|
|
3
|
+
items: string[][];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const { items } = Astro.props as TableBodyProps;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
items.length > 0 && (
|
|
11
|
+
<tbody>
|
|
12
|
+
{items.map((line) => (
|
|
13
|
+
<tr>
|
|
14
|
+
{line.map((cell) => (
|
|
15
|
+
<td>{cell}</td>
|
|
16
|
+
))}
|
|
17
|
+
</tr>
|
|
18
|
+
))}
|
|
19
|
+
</tbody>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
export type TableHeaderProps = {
|
|
3
|
+
headers: string[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const { headers } = Astro.props as TableHeaderProps;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
headers.length > 0 && (
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
{headers.map((header) => (
|
|
14
|
+
<th>{header}</th>
|
|
15
|
+
))}
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Table, { type TableProps } from './Table.astro';
|
|
3
|
+
import TableHeader, { type TableHeaderProps } from './TableHeader.astro';
|
|
4
|
+
import TableBody, { type TableBodyProps } from './TableBody.astro';
|
|
5
|
+
|
|
6
|
+
type Props = TableProps & TableHeaderProps & TableBodyProps;
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
items = [],
|
|
10
|
+
headers = [],
|
|
11
|
+
caption,
|
|
12
|
+
id,
|
|
13
|
+
idCaption = `table-title-${id}`,
|
|
14
|
+
...tablesProps
|
|
15
|
+
} = Astro.props;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<Table id={id} caption={caption} idCaption={idCaption} {...tablesProps}>
|
|
19
|
+
<TableHeader headers={headers} />
|
|
20
|
+
<TableBody items={items} />
|
|
21
|
+
</Table>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Nav, { type NavProps } from '@samuelgomez/astro/components/Nav/index.astro';
|
|
3
|
+
import { setSlug } from '@samuelgomez/astro/helpers/setSlug';
|
|
4
|
+
|
|
5
|
+
type TocProps = NavProps & {
|
|
6
|
+
links?: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const { links = [], ...props } = Astro.props as TocProps;
|
|
10
|
+
const LABEL = 'Table des matières';
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
Boolean(links.length) && (
|
|
15
|
+
<Nav
|
|
16
|
+
mode="vertical"
|
|
17
|
+
ordered
|
|
18
|
+
title={LABEL}
|
|
19
|
+
items={links.map((item) => ({
|
|
20
|
+
label: item,
|
|
21
|
+
href: `#${setSlug(item)}`,
|
|
22
|
+
}))}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
if (window.location.hash) {
|
|
30
|
+
const activeAnchor = document.querySelector(
|
|
31
|
+
`a[href="${window.location.hash}"]`
|
|
32
|
+
);
|
|
33
|
+
if (activeAnchor) {
|
|
34
|
+
activeAnchor.setAttribute('aria-current', 'true');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setCurrentAnchor(e: Event) {
|
|
39
|
+
{
|
|
40
|
+
const anchor = e.currentTarget as HTMLAnchorElement;
|
|
41
|
+
const oldCurrent = document.querySelector(
|
|
42
|
+
'nav[aria-labelledby="table-des-matières"] a[aria-current]'
|
|
43
|
+
);
|
|
44
|
+
oldCurrent?.removeAttribute('aria-current');
|
|
45
|
+
anchor?.setAttribute('aria-current', 'true');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const anchors = document.querySelectorAll(
|
|
50
|
+
'nav[aria-labelledby="table-des-matières"] a'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
[...anchors].forEach((anchor) => {
|
|
54
|
+
anchor.addEventListener('click', setCurrentAnchor);
|
|
55
|
+
});
|
|
56
|
+
// other medthod : https://css-tricks.com/table-of-contents-with-intersectionobserver/
|
|
57
|
+
// other method : https://benfrain.com/building-a-table-of-contents-with-active-indicator-using-javascript-intersection-observers/
|
|
58
|
+
function updateActive(id: string, prev?: boolean) {
|
|
59
|
+
const tableOfContent = document.querySelector(
|
|
60
|
+
'[aria-labelledby="table-des-matières"]'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
tableOfContent
|
|
64
|
+
?.querySelector('[aria-current="true"]')
|
|
65
|
+
?.removeAttribute('aria-current');
|
|
66
|
+
|
|
67
|
+
if (prev) {
|
|
68
|
+
tableOfContent
|
|
69
|
+
?.querySelector(`nav li:has(+ li a[href="#${id}"]) a`)
|
|
70
|
+
?.setAttribute('aria-current', 'true');
|
|
71
|
+
} else {
|
|
72
|
+
tableOfContent
|
|
73
|
+
?.querySelector(`nav li a[href="#${id}"]`)
|
|
74
|
+
?.setAttribute('aria-current', 'true');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
79
|
+
let previousYList: Record<string, number> = {};
|
|
80
|
+
let previousRatioList: Record<string, number> = {};
|
|
81
|
+
|
|
82
|
+
const observer = new IntersectionObserver((entries) => {
|
|
83
|
+
entries.forEach((entry) => {
|
|
84
|
+
const {
|
|
85
|
+
isIntersecting,
|
|
86
|
+
boundingClientRect,
|
|
87
|
+
intersectionRatio,
|
|
88
|
+
target,
|
|
89
|
+
} = entry;
|
|
90
|
+
|
|
91
|
+
const id = target?.getAttribute?.('id') ?? '';
|
|
92
|
+
|
|
93
|
+
if (boundingClientRect.y < previousYList[id]) {
|
|
94
|
+
// Scrolling down enter
|
|
95
|
+
if (intersectionRatio > previousRatioList[id] && isIntersecting) {
|
|
96
|
+
updateActive(id);
|
|
97
|
+
}
|
|
98
|
+
} else if (boundingClientRect.y > previousYList[id]) {
|
|
99
|
+
// Scrolling up leave
|
|
100
|
+
if (intersectionRatio < previousRatioList[id]) {
|
|
101
|
+
updateActive(id, true);
|
|
102
|
+
// Scrolling up enter
|
|
103
|
+
} else {
|
|
104
|
+
updateActive(id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
previousYList = { ...previousYList, [id]: boundingClientRect.y };
|
|
108
|
+
previousRatioList = { ...previousRatioList, [id]: intersectionRatio };
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Track all sections that have an `id` applied
|
|
113
|
+
document
|
|
114
|
+
.querySelectorAll('h2[id]')
|
|
115
|
+
.forEach((section) => observer.observe(section));
|
|
116
|
+
});
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
export type TabListProps = HTMLAttributes<'div'> & {
|
|
5
|
+
idTitle: string;
|
|
6
|
+
direction?: 'horizontal' | 'vertical';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
direction = 'horizontal',
|
|
11
|
+
idTitle,
|
|
12
|
+
...props
|
|
13
|
+
} = Astro.props as TabListProps;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
role="tablist"
|
|
18
|
+
aria-labelledby={idTitle}
|
|
19
|
+
aria-orientation={direction}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<slot />
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Button, { type ButtonProps } from '@samuelgomez/astro/components/Button/index.astro';
|
|
3
|
+
|
|
4
|
+
export type TabListItemProps = ButtonProps & {
|
|
5
|
+
index: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const { id, index, ...props } = Astro.props as TabListItemProps;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<Button
|
|
12
|
+
id={`tab-${id}-${index}`}
|
|
13
|
+
type="button"
|
|
14
|
+
role="tab"
|
|
15
|
+
aria-selected={`${index === 0}`}
|
|
16
|
+
aria-controls={`panel-${id}-${index}`}
|
|
17
|
+
tabindex={`${index && -1}`}
|
|
18
|
+
variant="ghost"
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<slot />
|
|
22
|
+
</Button>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
export type TabPanelProps = HTMLAttributes<'div'> & {
|
|
5
|
+
index: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const { id, index } = Astro.props as TabPanelProps;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
id={`panel-${id}-${index}`}
|
|
13
|
+
role="tabpanel"
|
|
14
|
+
tabindex="0"
|
|
15
|
+
aria-labelledby={`tab-${id}-${index}`}
|
|
16
|
+
hidden={index !== 0 ? 'true' : null}
|
|
17
|
+
>
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
export type TabsContainerProps = HTMLAttributes<'div'> & {
|
|
5
|
+
title: string;
|
|
6
|
+
idTitle: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
title,
|
|
11
|
+
id,
|
|
12
|
+
idTitle,
|
|
13
|
+
...tabsProps
|
|
14
|
+
} = Astro.props as TabsContainerProps;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div id={id} {...tabsProps}>
|
|
18
|
+
{Boolean(title) && <h3 id={idTitle}>{title}</h3>}
|
|
19
|
+
<slot />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
class Tabs {
|
|
24
|
+
tabsElements?: HTMLElement[];
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
this.tabsElements = this.getAllTabs();
|
|
28
|
+
if (!this.tabsElements.length) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.initAllTabs();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getAllTabs() {
|
|
35
|
+
const tabsNodeList = document.querySelectorAll<HTMLElement>(":has(> [role='tablist'])");
|
|
36
|
+
return [...tabsNodeList];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
initAllTabs() {
|
|
40
|
+
this.tabsElements?.forEach((tabElement) => {
|
|
41
|
+
const allTab =
|
|
42
|
+
tabElement?.querySelectorAll<HTMLElement>('[role="tab"]');
|
|
43
|
+
if (allTab) {
|
|
44
|
+
this.initFirstTab(tabElement);
|
|
45
|
+
this.addEventTab(tabElement, allTab);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
initFirstTab(tabElement: HTMLElement) {
|
|
51
|
+
const firstTab = tabElement?.querySelector<HTMLElement>(
|
|
52
|
+
'[aria-selected="true"]'
|
|
53
|
+
);
|
|
54
|
+
const isVertical = Boolean(
|
|
55
|
+
tabElement.querySelector('[aria-orientation="vertical"]')
|
|
56
|
+
);
|
|
57
|
+
const tablist =
|
|
58
|
+
tabElement?.querySelector<HTMLElement>('[role="tablist"]');
|
|
59
|
+
const size = isVertical
|
|
60
|
+
? `${(firstTab?.offsetHeight ?? 0) / (tablist?.offsetHeight ?? 1)}`
|
|
61
|
+
: `${(firstTab?.offsetWidth ?? 0) / (tablist?.offsetWidth ?? 1)}`;
|
|
62
|
+
tabElement?.style?.setProperty('--size', size);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
addEventTab(tabElement: HTMLElement, allTab: NodeListOf<HTMLElement>) {
|
|
66
|
+
[...allTab].forEach((tab) => {
|
|
67
|
+
tab.addEventListener('click', (e: Event) =>
|
|
68
|
+
this.switchTab(tabElement, e.target as HTMLElement)
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
tabElement?.addEventListener('keydown', (e) => {
|
|
73
|
+
const currentTab = tabElement?.querySelector<HTMLElement>(
|
|
74
|
+
'[aria-selected="true"]'
|
|
75
|
+
);
|
|
76
|
+
switch (e.key) {
|
|
77
|
+
case 'ArrowLeft':
|
|
78
|
+
case 'ArrowUp':
|
|
79
|
+
this.switchTab(
|
|
80
|
+
tabElement,
|
|
81
|
+
(currentTab?.previousElementSibling as HTMLElement) ||
|
|
82
|
+
allTab[allTab.length - 1]
|
|
83
|
+
);
|
|
84
|
+
break;
|
|
85
|
+
case 'ArrowRight':
|
|
86
|
+
case 'ArrowDown':
|
|
87
|
+
this.switchTab(
|
|
88
|
+
tabElement,
|
|
89
|
+
(currentTab?.nextElementSibling as HTMLElement) || allTab[0]
|
|
90
|
+
);
|
|
91
|
+
break;
|
|
92
|
+
case 'Home':
|
|
93
|
+
this.switchTab(tabElement, allTab[0]);
|
|
94
|
+
break;
|
|
95
|
+
case 'End':
|
|
96
|
+
this.switchTab(tabElement, allTab[allTab.length - 1]);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switchTab(tabElement: HTMLElement, target: HTMLElement) {
|
|
103
|
+
const activeTab = tabElement?.querySelector('[aria-selected="true"]');
|
|
104
|
+
activeTab?.setAttribute('aria-selected', 'false');
|
|
105
|
+
activeTab?.setAttribute('tabindex', '-1');
|
|
106
|
+
|
|
107
|
+
tabElement
|
|
108
|
+
?.querySelector('[role="tabpanel"]:not([hidden])')
|
|
109
|
+
?.setAttribute('hidden', '');
|
|
110
|
+
|
|
111
|
+
target?.setAttribute('aria-selected', 'true');
|
|
112
|
+
target?.setAttribute('tabindex', '0');
|
|
113
|
+
target.focus();
|
|
114
|
+
|
|
115
|
+
tabElement
|
|
116
|
+
?.querySelector(`#${target.getAttribute('aria-controls')}`)
|
|
117
|
+
?.removeAttribute('hidden');
|
|
118
|
+
|
|
119
|
+
this.moveIndicator(tabElement, activeTab as HTMLElement, target);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
moveIndicator(
|
|
123
|
+
tabElement: HTMLElement,
|
|
124
|
+
oldTab: HTMLElement,
|
|
125
|
+
newTab: HTMLElement
|
|
126
|
+
) {
|
|
127
|
+
const tablist =
|
|
128
|
+
tabElement?.querySelector<HTMLElement>('[role="tablist"]');
|
|
129
|
+
const isVertical = Boolean(
|
|
130
|
+
tabElement.querySelector('[aria-orientation="vertical"]')
|
|
131
|
+
);
|
|
132
|
+
const newTabPosition = oldTab.compareDocumentPosition(newTab);
|
|
133
|
+
const { offsetWidth, offsetLeft, offsetHeight, offsetTop } = newTab;
|
|
134
|
+
|
|
135
|
+
const size = isVertical
|
|
136
|
+
? `${(offsetHeight ?? 0) / (tablist?.offsetHeight ?? 1)}`
|
|
137
|
+
: `${(offsetWidth ?? 0) / (tablist?.offsetWidth ?? 1)}`;
|
|
138
|
+
|
|
139
|
+
let transition;
|
|
140
|
+
|
|
141
|
+
if (newTabPosition === 4) {
|
|
142
|
+
transition = isVertical
|
|
143
|
+
? offsetTop + offsetHeight - oldTab.offsetTop
|
|
144
|
+
: offsetLeft + offsetWidth - oldTab.offsetLeft;
|
|
145
|
+
} else {
|
|
146
|
+
transition = isVertical
|
|
147
|
+
? oldTab.offsetTop + oldTab.offsetHeight - offsetTop
|
|
148
|
+
: oldTab.offsetLeft + oldTab.offsetWidth - offsetLeft;
|
|
149
|
+
const offset = `${isVertical ? offsetTop : offsetLeft}px`;
|
|
150
|
+
tabElement?.style.setProperty('--offset', offset);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const transitionSize = isVertical
|
|
154
|
+
? `${transition / (tabElement?.offsetHeight ?? 1)}`
|
|
155
|
+
: `${transition / (tabElement?.offsetWidth ?? 1)}`;
|
|
156
|
+
|
|
157
|
+
tabElement?.style.setProperty('--size', transitionSize);
|
|
158
|
+
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
tabElement?.style.setProperty(
|
|
161
|
+
'--offset',
|
|
162
|
+
`${isVertical ? offsetTop : offsetLeft}px`
|
|
163
|
+
);
|
|
164
|
+
tabElement?.style.setProperty('--size', size);
|
|
165
|
+
}, 200);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tabs = new Tabs();
|
|
170
|
+
|
|
171
|
+
window.addEventListener('load', () => {
|
|
172
|
+
tabs.init();
|
|
173
|
+
});
|
|
174
|
+
</script>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
import TabsContainer, { type TabsContainerProps } from './TabsContainer.astro';
|
|
3
|
+
import TabList, { type TabListProps } from './TabList.astro';
|
|
4
|
+
import TabListItem from './TabListItem.astro';
|
|
5
|
+
import TabPanel from './TabPanel.astro';
|
|
6
|
+
|
|
7
|
+
type Item = {
|
|
8
|
+
tab: string;
|
|
9
|
+
panel: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type TabsProps = TabsContainerProps & {
|
|
13
|
+
items: Item[];
|
|
14
|
+
} & Pick<TabListProps, 'direction'>;
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
items = [],
|
|
18
|
+
direction = 'horizontal',
|
|
19
|
+
title,
|
|
20
|
+
id,
|
|
21
|
+
idTitle = `tablist-title-${id}`,
|
|
22
|
+
...tabsProps
|
|
23
|
+
} = Astro.props as TabsProps;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<TabsContainer id={id} title={title} idTitle={idTitle} {...tabsProps}>
|
|
27
|
+
{
|
|
28
|
+
items.length > 0 && (
|
|
29
|
+
<>
|
|
30
|
+
<TabList idTitle={idTitle} direction={direction}>
|
|
31
|
+
{items.map(({ tab }, index) => (
|
|
32
|
+
<TabListItem id={id} index={index}>
|
|
33
|
+
<Fragment set:html={tab} />
|
|
34
|
+
</TabListItem>
|
|
35
|
+
))}
|
|
36
|
+
</TabList>
|
|
37
|
+
|
|
38
|
+
{items.map(({ panel }, index) => (
|
|
39
|
+
<TabPanel id={id} index={index}>
|
|
40
|
+
<Fragment set:html={panel} />
|
|
41
|
+
</TabPanel>
|
|
42
|
+
))}
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
</TabsContainer>
|