@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.
Files changed (115) hide show
  1. package/README.md +54 -0
  2. package/package.json +42 -0
  3. package/src/components/Accordion/index.astro +31 -0
  4. package/src/components/Alert/AlertDanger.astro +12 -0
  5. package/src/components/Alert/AlertInfo.astro +12 -0
  6. package/src/components/Alert/AlertSuccess.astro +12 -0
  7. package/src/components/Alert/AlertWarning.astro +12 -0
  8. package/src/components/Alert/index.astro +10 -0
  9. package/src/components/Badge/BadgeDanger.astro +15 -0
  10. package/src/components/Badge/BadgeInfo.astro +15 -0
  11. package/src/components/Badge/BadgeSuccess.astro +15 -0
  12. package/src/components/Badge/BadgeWarning.astro +15 -0
  13. package/src/components/Badge/index.astro +9 -0
  14. package/src/components/Box/index.astro +45 -0
  15. package/src/components/BoxPost/index.astro +66 -0
  16. package/src/components/Button/index.astro +35 -0
  17. package/src/components/Caniuse/index.astro +24 -0
  18. package/src/components/Codepen/index.astro +25 -0
  19. package/src/components/DebugGrid/index.astro +100 -0
  20. package/src/components/Demo/Demoui.astro +13 -0
  21. package/src/components/Demo/index.astro +105 -0
  22. package/src/components/Dialog/index.astro +74 -0
  23. package/src/components/FormattedDate/index.astro +17 -0
  24. package/src/components/GithubContributions/index.astro +88 -0
  25. package/src/components/Home/CardsBlog.astro +41 -0
  26. package/src/components/Home/CardsCodepen.astro +53 -0
  27. package/src/components/Home/CardsGithub.astro +44 -0
  28. package/src/components/Img/index.astro +28 -0
  29. package/src/components/Link/index.astro +33 -0
  30. package/src/components/Nav/NavToggle.astro +40 -0
  31. package/src/components/Nav/index.astro +71 -0
  32. package/src/components/Pagination/PaginationItem.astro +51 -0
  33. package/src/components/Pagination/index.astro +39 -0
  34. package/src/components/ScrollWatcher/index.astro +11 -0
  35. package/src/components/Social/index.astro +22 -0
  36. package/src/components/Support/index.astro +24 -0
  37. package/src/components/Svg/index.astro +30 -0
  38. package/src/components/SwitchTheme.astro +45 -0
  39. package/src/components/Table/Table.astro +15 -0
  40. package/src/components/Table/TableBody.astro +21 -0
  41. package/src/components/Table/TableHeader.astro +19 -0
  42. package/src/components/Table/index.astro +21 -0
  43. package/src/components/TableOfContent/index.astro +117 -0
  44. package/src/components/Tabs/TabList.astro +23 -0
  45. package/src/components/Tabs/TabListItem.astro +22 -0
  46. package/src/components/Tabs/TabPanel.astro +19 -0
  47. package/src/components/Tabs/TabsContainer.astro +174 -0
  48. package/src/components/Tabs/index.astro +46 -0
  49. package/src/components/Title/h1.astro +12 -0
  50. package/src/components/Title/h2.astro +12 -0
  51. package/src/components/Title/h3.astro +12 -0
  52. package/src/components/Title/h4.astro +12 -0
  53. package/src/components/Title/h5.astro +12 -0
  54. package/src/components/Title/h6.astro +12 -0
  55. package/src/components/Title/heading.astro +8 -0
  56. package/src/components/Title/index.astro +39 -0
  57. package/src/components/WrapperToHtml/index.astro +8 -0
  58. package/src/components/form/Field/index.astro +43 -0
  59. package/src/components/form/Fieldset/index.astro +14 -0
  60. package/src/components/form/Form/FieldDate.astro +32 -0
  61. package/src/components/form/Form/FieldEmail.astro +32 -0
  62. package/src/components/form/Form/FieldNumber.astro +35 -0
  63. package/src/components/form/Form/FieldPassword.astro +32 -0
  64. package/src/components/form/Form/FieldRadio.astro +32 -0
  65. package/src/components/form/Form/FieldSelect.astro +26 -0
  66. package/src/components/form/Form/FieldText.astro +35 -0
  67. package/src/components/form/Form/FieldTextarea.astro +25 -0
  68. package/src/components/form/Form/index.astro +5 -0
  69. package/src/components/form/Input/index.astro +27 -0
  70. package/src/components/form/InputRadio/index.astro +30 -0
  71. package/src/components/form/MoreInfo/index.astro +14 -0
  72. package/src/components/form/Select/index.astro +25 -0
  73. package/src/components/form/Status/index.astro +34 -0
  74. package/src/components/form/Textarea/index.astro +26 -0
  75. package/src/components/helpers/WrapperOrNot/index.astro +23 -0
  76. package/src/components/layout/Footer.astro +3 -0
  77. package/src/components/layout/Head.astro +154 -0
  78. package/src/components/layout/Header.astro +28 -0
  79. package/src/components/old/Grid.astro +18 -0
  80. package/src/components/old/Section.astro +19 -0
  81. package/src/components/old/SwitchTheme.astro +66 -0
  82. package/src/components/old/index-webco.astro +55 -0
  83. package/src/components/old/send.astro +28 -0
  84. package/src/helpers/dom.ts +19 -0
  85. package/src/helpers/isEmptyOrNull.test.ts +58 -0
  86. package/src/helpers/isEmptyOrNull.ts +6 -0
  87. package/src/helpers/setSlug.test.ts +20 -0
  88. package/src/helpers/setSlug.ts +7 -0
  89. package/src/helpers/setTocTitle.ts +2 -0
  90. package/src/helpers/setVariants.test.ts +26 -0
  91. package/src/helpers/setVariants.ts +18 -0
  92. package/src/icons/Add.astro +18 -0
  93. package/src/icons/Anchor.astro +22 -0
  94. package/src/icons/Arrow.astro +11 -0
  95. package/src/icons/AstroLogo.astro +35 -0
  96. package/src/icons/Check.astro +11 -0
  97. package/src/icons/ChevronDown.astro +11 -0
  98. package/src/icons/ChevronLeft.astro +11 -0
  99. package/src/icons/ChevronRight.astro +11 -0
  100. package/src/icons/Codepen.astro +36 -0
  101. package/src/icons/Cross.astro +11 -0
  102. package/src/icons/ExternalLink.astro +19 -0
  103. package/src/icons/Github.astro +36 -0
  104. package/src/icons/Github2.astro +11 -0
  105. package/src/icons/Grid.astro +19 -0
  106. package/src/icons/Info.astro +11 -0
  107. package/src/icons/Instagram.astro +11 -0
  108. package/src/icons/Linkedin.astro +11 -0
  109. package/src/icons/LogoSG.astro +66 -0
  110. package/src/icons/MoonSun.astro +17 -0
  111. package/src/icons/Send.astro +11 -0
  112. package/src/icons/Slash.astro +14 -0
  113. package/src/icons/Trash.astro +19 -0
  114. package/src/icons/Twitter.astro +14 -0
  115. 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>
@@ -0,0 +1,12 @@
1
+ ---
2
+ import Title from './index.astro';
3
+
4
+ type Props = {
5
+ label: string;
6
+ noLink?: boolean;
7
+ };
8
+
9
+ const props = Astro.props;
10
+ ---
11
+
12
+ <Title {...props}><slot /></Title>
@@ -0,0 +1,12 @@
1
+ ---
2
+ import Title from './index.astro';
3
+
4
+ type Props = {
5
+ label: string;
6
+ noLink?: boolean;
7
+ };
8
+
9
+ const props = Astro.props;
10
+ ---
11
+
12
+ <Title level={2} {...props}><slot /></Title>
@@ -0,0 +1,12 @@
1
+ ---
2
+ import Title from './index.astro';
3
+
4
+ type Props = {
5
+ label: string;
6
+ noLink?: boolean;
7
+ };
8
+
9
+ const props = Astro.props;
10
+ ---
11
+
12
+ <Title level={3} {...props}><slot /></Title>
@@ -0,0 +1,12 @@
1
+ ---
2
+ import Title from './index.astro';
3
+
4
+ type Props = {
5
+ label: string;
6
+ noLink?: boolean;
7
+ };
8
+
9
+ const props = Astro.props;
10
+ ---
11
+
12
+ <Title level={4} {...props}><slot /></Title>
@@ -0,0 +1,12 @@
1
+ ---
2
+ import Title from './index.astro';
3
+
4
+ type Props = {
5
+ label: string;
6
+ noLink?: boolean;
7
+ };
8
+
9
+ const props = Astro.props;
10
+ ---
11
+
12
+ <Title level={5} {...props}><slot /></Title>