Move from Svelte to Vanilla JS (#155)
This commit is contained in:
parent
f478ee4972
commit
88ba9d450f
23 changed files with 236 additions and 196 deletions
|
|
@ -7,5 +7,4 @@ endOfLine: 'auto'
|
|||
pluginSearchDirs: false
|
||||
plugins:
|
||||
- 'prettier-plugin-astro'
|
||||
- 'prettier-plugin-svelte'
|
||||
- 'prettier-plugin-tailwindcss'
|
||||
|
|
|
|||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
|
@ -4,7 +4,6 @@
|
|||
"mgmcdermott.vscode-language-babel",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
|
|
|
|||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
|
@ -5,14 +5,9 @@
|
|||
"emmet.includeLanguages": { "javascript": "javascriptreact", "astro": "javascriptreact" },
|
||||
"files.eol": "\n",
|
||||
"prettier.documentSelectors": ["**/*.astro"],
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"tailwindCSS.classAttributes": ["class", "className", "class:list"],
|
||||
"tailwindCSS.experimental.classRegex": [["/\\* tw \\*/ ([^;]*);", "'([^']*)'"]],
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"javascript": "javascriptreact",
|
||||
"astro": "javascriptreact",
|
||||
"svelte": "javascriptreact"
|
||||
},
|
||||
"tailwindCSS.includeLanguages": { "javascript": "javascriptreact", "astro": "javascriptreact" },
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import image from '@astrojs/image';
|
||||
import svelte from '@astrojs/svelte';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind(), image(), svelte()],
|
||||
integrations: [tailwind(), image()],
|
||||
vite: {
|
||||
plugins: [visualizer()],
|
||||
},
|
||||
|
|
|
|||
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -9,7 +9,8 @@
|
|||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.1.0",
|
||||
"iconify-icon": "1.0.2"
|
||||
"iconify-icon": "1.0.2",
|
||||
"nanoid": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/image": "0.12.1",
|
||||
|
|
@ -5206,15 +5207,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"dev": true,
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz",
|
||||
"integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
"node": "^14 || ^16 || >=18"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
|
|
@ -5834,6 +5834,18 @@
|
|||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
||||
|
|
@ -12187,10 +12199,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"dev": true
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz",
|
||||
"integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg=="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
|
|
@ -12552,6 +12563,14 @@
|
|||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"postcss-import": {
|
||||
|
|
|
|||
|
|
@ -20,15 +20,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.1.0",
|
||||
"iconify-icon": "1.0.2"
|
||||
"iconify-icon": "1.0.2",
|
||||
"nanoid": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/image": "0.12.1",
|
||||
"@astrojs/react": "1.2.2",
|
||||
"@astrojs/svelte": "1.0.2",
|
||||
"@astrojs/tailwind": "2.1.3",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"astro": "1.9.2",
|
||||
"concurrently": "7.6.0",
|
||||
"iconify-icon-names": "1.1.0",
|
||||
|
|
@ -36,10 +34,8 @@
|
|||
"postcss": "8.4.21",
|
||||
"prettier": "2.8.2",
|
||||
"prettier-plugin-astro": "0.7.2",
|
||||
"prettier-plugin-svelte": "2.9.0",
|
||||
"prettier-plugin-tailwindcss": "0.2.1",
|
||||
"rollup-plugin-visualizer": "5.9.0",
|
||||
"svelte": "3.55.1",
|
||||
"tailwindcss": "3.2.4",
|
||||
"typescript": "4.9.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import type { IconName } from '@/types/icon';
|
||||
|
||||
import Icon from './icon.svelte';
|
||||
import Icon from './icon.astro';
|
||||
|
||||
type IconButtonSize = 'small' | 'large';
|
||||
|
||||
|
|
@ -28,5 +28,5 @@ const classes = /* tw */ {
|
|||
---
|
||||
|
||||
<a href={href} target={target} class:list={[classes.main, classes.active, classes.focus, sizeMap[size]]} {...rest}>
|
||||
<Icon client:load name={icon} size={16} />
|
||||
<Icon name={icon} size={16} />
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Placement, offset, Padding } from '@floating-ui/dom';
|
||||
import type { IconName } from '@/types/icon';
|
||||
import Icon from './icon.svelte';
|
||||
import Tooltip from './tooltip.svelte';
|
||||
|
||||
export let name: IconName;
|
||||
export let size: number;
|
||||
export let color: string | undefined = undefined;
|
||||
|
||||
export let content: string;
|
||||
export let placement: Placement | undefined = undefined;
|
||||
export let padding: Padding = 8;
|
||||
export let spacing: Parameters<typeof offset>[0] = 8;
|
||||
</script>
|
||||
|
||||
<Tooltip {content} {placement} {padding} {spacing}>
|
||||
<Icon {name} {color} {size} />
|
||||
</Tooltip>
|
||||
22
src/components/icon.astro
Normal file
22
src/components/icon.astro
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
import type { IconName } from '@/types/icon';
|
||||
|
||||
export interface Props {
|
||||
name: IconName;
|
||||
size: number;
|
||||
color?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { name, size, color = 'currentColor', ...props } = Astro.props;
|
||||
---
|
||||
|
||||
<iconify-icon
|
||||
icon={name}
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ color, width: `${size}px`, height: `${size}px` }}
|
||||
{...props}></iconify-icon>
|
||||
<script>
|
||||
import 'iconify-icon';
|
||||
</script>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<script lang="ts">
|
||||
import 'iconify-icon';
|
||||
import type { IconName } from '@/types/icon';
|
||||
import { isServer } from '@/utils/env';
|
||||
|
||||
export let name: IconName;
|
||||
export let size: number;
|
||||
export let color: string | undefined = undefined;
|
||||
|
||||
const dimensions = `width: ${size}px; height: ${size}px`;
|
||||
</script>
|
||||
|
||||
{#if isServer}
|
||||
<div style={dimensions} />
|
||||
{:else}
|
||||
<iconify-icon icon={name} width={size} height={size} style="color: {color}; {dimensions}"> ></iconify-icon>
|
||||
{/if}
|
||||
34
src/components/sidebar-item.astro
Normal file
34
src/components/sidebar-item.astro
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
import type { SectionKey } from '@/types/data';
|
||||
import type { IconName } from '@/types/icon';
|
||||
import Icon from './icon.astro';
|
||||
|
||||
export interface Props {
|
||||
section: SectionKey;
|
||||
icon: IconName;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { section, icon, title = '', ...props } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
href={`#${section}`}
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-lg transition"
|
||||
aria-label={`${section} section`}
|
||||
data-tooltip={`${title || section.charAt(0).toUpperCase() + section.slice(1)}`}
|
||||
data-tooltip-placement="left"
|
||||
{...props}
|
||||
>
|
||||
<Icon name={icon} size={20} />
|
||||
</a>
|
||||
|
||||
<style>
|
||||
[aria-current='page'] {
|
||||
@apply bg-primary-600 text-white;
|
||||
}
|
||||
|
||||
:not([aria-current='page']) {
|
||||
@apply text-gray-400 hover:bg-primary-600 hover:text-white dark:text-gray-200;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { SectionKey } from '@/types/data';
|
||||
import type { IconName } from '@/types/icon';
|
||||
import hashState from '@/utils/hash-state';
|
||||
import Icon from './icon.svelte';
|
||||
import Tooltip from './tooltip.svelte';
|
||||
|
||||
export let section: SectionKey;
|
||||
export let icon: IconName;
|
||||
export let title: string = '';
|
||||
|
||||
let hash = hashState.getHash();
|
||||
|
||||
hashState.subscribe((newHash) => (hash = newHash));
|
||||
|
||||
const href = `#${section}`;
|
||||
|
||||
$: active = hash === href;
|
||||
|
||||
const classes = /* tw */ {
|
||||
main: 'inline-flex h-10 w-10 items-center justify-center rounded-lg transition',
|
||||
active: 'bg-primary-600 text-white',
|
||||
inactive: 'text-gray-400 hover:bg-primary-600 hover:text-white dark:text-gray-200',
|
||||
};
|
||||
</script>
|
||||
|
||||
<Tooltip content={`${title || section.charAt(0).toUpperCase() + section.slice(1)}`} placement="left">
|
||||
<a
|
||||
{href}
|
||||
class={`${classes.main} ${active ? classes.active : classes.inactive}`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
aria-label={`${section} section`}
|
||||
>
|
||||
<Icon name={icon} size={20} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Data } from '@/data';
|
||||
import isSectionKey from '@/utils/is-section-key';
|
||||
|
||||
import SidebarItem from './sidebar-item.svelte';
|
||||
import SidebarItem from './sidebar-item.astro';
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
|
|
@ -23,10 +23,31 @@ const sections = Object.keys(data).flatMap((key) => {
|
|||
---
|
||||
|
||||
<nav
|
||||
id="sidebar"
|
||||
class:list={[
|
||||
'hidden xl:flex flex-col w-max h-fit p-2 rounded-lg gap-2 bg-white dark:bg-gray-800 shadow-md',
|
||||
className,
|
||||
]}
|
||||
>
|
||||
{sections.map((section) => <SidebarItem client:load {...section} />)}
|
||||
{sections.map((section) => <SidebarItem {...section} />)}
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
import hashState from '@/utils/hash-state';
|
||||
|
||||
const sidebarItems = [...document.getElementById('sidebar')!.children] as HTMLAnchorElement[];
|
||||
|
||||
const setActiveItem = (hash: string) => {
|
||||
sidebarItems.forEach((item) => {
|
||||
if (item.href.endsWith(hash)) {
|
||||
item.setAttribute('aria-current', 'page');
|
||||
} else {
|
||||
item.removeAttribute('aria-current');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setActiveItem(hashState.getHash());
|
||||
|
||||
hashState.subscribe(setActiveItem);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import type { IconName } from '@/types/icon';
|
||||
|
||||
import Icon from './icon.svelte';
|
||||
import Icon from './icon.astro';
|
||||
|
||||
export interface Props {
|
||||
name?: IconName;
|
||||
|
|
@ -14,6 +14,6 @@ const { name, color } = Astro.props;
|
|||
<div
|
||||
class="flex h-6 w-fit items-center gap-x-1.5 rounded bg-gray-100 px-2.5 text-sm font-medium tracking-wide text-gray-700 dark:bg-gray-700 dark:text-gray-100"
|
||||
>
|
||||
{name && <Icon client:load name={name} color={color} size={16} />}
|
||||
{name && <Icon name={name} color={color} size={16} />}
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
|||
25
src/components/theme-toggle.astro
Normal file
25
src/components/theme-toggle.astro
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import Icon from './icon.astro';
|
||||
---
|
||||
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="fixed bottom-3 left-3 z-10 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 text-gray-400 shadow-xl transition focus:ring-primary-500 dark:bg-gray-600 dark:text-gray-200"
|
||||
>
|
||||
<Icon class="block dark:hidden" name="ri:moon-fill" size={20} />
|
||||
<Icon class="hidden dark:block" name="ri:sun-line" size={20} />
|
||||
</button>
|
||||
|
||||
<script>
|
||||
const themeToggle = document.getElementById('theme-toggle')!;
|
||||
|
||||
const toggleTheme = () => {
|
||||
const theme = localStorage.getItem('theme') ?? 'light';
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.classList[newTheme === 'dark' ? 'add' : 'remove']('dark');
|
||||
localStorage.setItem('theme', newTheme);
|
||||
};
|
||||
|
||||
themeToggle.addEventListener('click', toggleTheme);
|
||||
</script>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { isClient } from '@/utils/env';
|
||||
import Icon from './icon.svelte';
|
||||
|
||||
let theme = localStorage.getItem('theme') ?? 'light';
|
||||
|
||||
const toggleTheme = () => {
|
||||
theme = theme === 'light' ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
const handleThemeChange = () => {
|
||||
if (isClient) {
|
||||
document.documentElement.classList[theme === 'dark' ? 'add' : 'remove']('dark');
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
};
|
||||
|
||||
$: theme, handleThemeChange();
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={toggleTheme}
|
||||
class="fixed bottom-3 left-3 z-10 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 text-gray-400 shadow-xl transition focus:ring-primary-500 dark:bg-gray-600 dark:text-gray-200"
|
||||
>
|
||||
<Icon name={theme === 'light' ? 'ri:moon-fill' : 'ri:sun-line'} size={20} />
|
||||
</button>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { computePosition, Placement, flip, shift, offset, Padding } from '@floating-ui/dom';
|
||||
|
||||
export let content: string;
|
||||
export let placement: Placement | undefined;
|
||||
export let padding: Padding = 8;
|
||||
export let spacing: Parameters<typeof offset>[0] = 8;
|
||||
|
||||
let button: HTMLElement;
|
||||
let tooltip: HTMLElement;
|
||||
|
||||
const updateTooltip = () => {
|
||||
computePosition(button, tooltip, { placement, middleware: [offset(spacing), flip(), shift({ padding })] }).then(
|
||||
({ x, y }) => {
|
||||
Object.assign(tooltip.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const showTooltip = () => {
|
||||
tooltip.style.display = 'block';
|
||||
updateTooltip();
|
||||
};
|
||||
|
||||
const hideTooltip = () => {
|
||||
tooltip.style.display = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex h-fit w-fit items-center justify-center"
|
||||
bind:this={button}
|
||||
on:mouseenter={showTooltip}
|
||||
on:mouseleave={hideTooltip}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<div
|
||||
bind:this={tooltip}
|
||||
role="tooltip"
|
||||
class="absolute top-0 left-0 hidden max-w-sm rounded-lg bg-gray-700 px-3 py-1 text-white dark:bg-gray-100 dark:text-gray-800 sm:max-w-xs"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
import Icon from '@/components/icon.svelte';
|
||||
import Icon from '@/components/icon.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<!-- Available icon names here: https://icon-sets.iconify.design -->
|
||||
<!-- Colors for simple icons here: https://simpleicons.org -->
|
||||
<Icon client:load name="simple-icons:react" size={24} color="#61DAFB" />
|
||||
<Icon name="simple-icons:react" size={24} color="#61DAFB" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import SidebarItem from '@/components/sidebar-item.svelte';
|
||||
import SidebarItem from '@/components/sidebar-item.astro';
|
||||
---
|
||||
|
||||
<div class="p-5">
|
||||
<SidebarItem client:load icon="fa6-solid:bars-progress" section="experience" />
|
||||
<SidebarItem icon="fa6-solid:bars-progress" section="experience" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import Sidebar from '@/components/sidebar.astro';
|
||||
import ThemeToggle from '@/components/theme-toggle.svelte';
|
||||
import ThemeToggle from '@/components/theme-toggle.astro';
|
||||
import ExperienceSection from '@/sections/experience/experience-section.astro';
|
||||
import FavoritesSection from '@/sections/favorites/favorites-section.astro';
|
||||
import MainSection from '@/sections/main/main-section.astro';
|
||||
|
|
@ -44,7 +44,7 @@ const seoImage = seo.image ? seo.image : '/favicon.svg';
|
|||
</script>
|
||||
</head>
|
||||
<body class="flex justify-center overflow-x-hidden bg-gray-50 dark:bg-gray-900 xl:relative xl:left-7">
|
||||
<ThemeToggle client:load />
|
||||
<ThemeToggle />
|
||||
<main class="w-full max-w-5xl space-y-4 px-2 py-3 sm:space-y-6 sm:px-8 sm:py-12 lg:space-y-8 lg:py-20">
|
||||
<MainSection {...data.main} />
|
||||
{data.skills && <SkillsSection {...data.skills} />}
|
||||
|
|
@ -54,5 +54,6 @@ const seoImage = seo.image ? seo.image : '/favicon.svg';
|
|||
{data.favorites && <FavoritesSection {...data.favorites} />}
|
||||
</main>
|
||||
<Sidebar data={data} className="sticky top-8 mt-20" />
|
||||
<script src="../scripts/initialize-tooltips.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
73
src/scripts/initialize-tooltips.ts
Normal file
73
src/scripts/initialize-tooltips.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { computePosition, flip, shift, offset, autoUpdate, Placement } from '@floating-ui/dom';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
interface UpdateTooltipOptions {
|
||||
element: HTMLElement;
|
||||
tooltip: HTMLElement;
|
||||
placement: Placement;
|
||||
}
|
||||
|
||||
const updateTooltip =
|
||||
({ element, tooltip, placement }: UpdateTooltipOptions) =>
|
||||
() => {
|
||||
computePosition(element, tooltip, { placement, middleware: [offset(8), flip(), shift({ padding: 8 })] }).then(
|
||||
({ x, y }) => {
|
||||
Object.assign(tooltip.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const tooltipClass =
|
||||
/* tw */ 'absolute top-0 left-0 hidden max-w-sm animate-show rounded-lg bg-gray-700 px-3 py-1 text-white dark:bg-gray-100 dark:text-gray-800 sm:max-w-xs';
|
||||
|
||||
const createTooltip = (content: string) => {
|
||||
const tooltip = document.createElement('div');
|
||||
|
||||
tooltip.innerText = content;
|
||||
tooltip.setAttribute('id', `tooltip-${nanoid(8)}`);
|
||||
tooltip.setAttribute('class', tooltipClass);
|
||||
tooltip.setAttribute('role', 'tooltip');
|
||||
|
||||
return tooltip;
|
||||
};
|
||||
|
||||
const addListeners = (element: HTMLElement, tooltip: HTMLElement, updateFn: () => void) => {
|
||||
element.addEventListener('mouseenter', () => {
|
||||
tooltip.style.display = 'block';
|
||||
updateFn();
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', () => {
|
||||
tooltip.style.display = '';
|
||||
});
|
||||
};
|
||||
|
||||
const creteTooltipsForElements = (elements: HTMLElement[]) => {
|
||||
const tooltipsContainer = document.createElement('div');
|
||||
|
||||
const tooltips = elements.map((element) => {
|
||||
const tooltip = createTooltip(element.dataset.tooltip ?? '');
|
||||
tooltipsContainer.appendChild(tooltip);
|
||||
return { tooltip, element };
|
||||
});
|
||||
|
||||
document.body.appendChild(tooltipsContainer);
|
||||
|
||||
return tooltips;
|
||||
};
|
||||
|
||||
const elements = [...document.querySelectorAll('[data-tooltip]')] as HTMLElement[];
|
||||
const elementsWithTooltips = creteTooltipsForElements(elements);
|
||||
|
||||
elementsWithTooltips.forEach(({ element, tooltip }) => {
|
||||
const placement = (element.dataset.tooltipPlacement ?? 'top') as Placement;
|
||||
const updateFn = updateTooltip({ element, tooltip, placement });
|
||||
|
||||
element.setAttribute('aria-describedby', tooltip.id);
|
||||
|
||||
autoUpdate(element, tooltip, updateFn);
|
||||
addListeners(element, tooltip, updateFn);
|
||||
});
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
---
|
||||
import IconWithTooltip from '@/components/icon-with-tooltip.svelte';
|
||||
import Typography from '@/components/typography.astro';
|
||||
import type { IconName } from '@/types/icon';
|
||||
import type { LevelledSkill } from '@/types/skills-section';
|
||||
|
||||
import Icon from '../../components/icon.svelte';
|
||||
import Icon from '../../components/icon.astro';
|
||||
import SkillLevel from './skill-level.astro';
|
||||
|
||||
export interface Props extends LevelledSkill {}
|
||||
|
|
@ -17,21 +15,16 @@ const IconWrapper = url ? 'a' : 'div';
|
|||
<div class="flex flex-col gap-2">
|
||||
<div class="flex h-5 items-center justify-between">
|
||||
<IconWrapper class="flex gap-2 h-5" {...(url && { href: url, target: '_blank', rel: 'noopener noreferrer' })}>
|
||||
{icon && <Icon client:load name={icon} color={iconColor} size={20} />}
|
||||
{icon && <Icon name={icon} color={iconColor} size={20} />}
|
||||
<Typography variant="tile-subtitle">
|
||||
<span class="text-gray-700 dark:text-gray-300">{name}</span>
|
||||
</Typography>
|
||||
</IconWrapper>
|
||||
{
|
||||
description && (
|
||||
<IconWithTooltip
|
||||
client:load
|
||||
name={'akar-icons:info-fill' as IconName}
|
||||
color="#D1D5DB"
|
||||
size={14}
|
||||
content={description}
|
||||
placement="top"
|
||||
/>
|
||||
<div class="flex h-3.5 w-3.5" data-tooltip={description} data-tooltip-placement="top">
|
||||
<Icon name="fa6-solid:circle-info" color="#D1D5DB" size={14} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ module.exports = {
|
|||
fluid200: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
fluid240: 'repeat(auto-fit, minmax(240px, 1fr))',
|
||||
},
|
||||
keyframes: {
|
||||
show: {
|
||||
from: { opacity: '0' },
|
||||
to: { opacity: '1' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
show: 'show 225ms ease-in-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue