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
|
pluginSearchDirs: false
|
||||||
plugins:
|
plugins:
|
||||||
- 'prettier-plugin-astro'
|
- 'prettier-plugin-astro'
|
||||||
- 'prettier-plugin-svelte'
|
|
||||||
- 'prettier-plugin-tailwindcss'
|
- 'prettier-plugin-tailwindcss'
|
||||||
|
|
|
||||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
|
@ -4,7 +4,6 @@
|
||||||
"mgmcdermott.vscode-language-babel",
|
"mgmcdermott.vscode-language-babel",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"svelte.svelte-vscode",
|
|
||||||
"bradlc.vscode-tailwindcss",
|
"bradlc.vscode-tailwindcss",
|
||||||
"redhat.vscode-yaml"
|
"redhat.vscode-yaml"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
|
@ -5,14 +5,9 @@
|
||||||
"emmet.includeLanguages": { "javascript": "javascriptreact", "astro": "javascriptreact" },
|
"emmet.includeLanguages": { "javascript": "javascriptreact", "astro": "javascriptreact" },
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"prettier.documentSelectors": ["**/*.astro"],
|
"prettier.documentSelectors": ["**/*.astro"],
|
||||||
"svelte.enable-ts-plugin": true,
|
|
||||||
"tailwindCSS.classAttributes": ["class", "className", "class:list"],
|
"tailwindCSS.classAttributes": ["class", "className", "class:list"],
|
||||||
"tailwindCSS.experimental.classRegex": [["/\\* tw \\*/ ([^;]*);", "'([^']*)'"]],
|
"tailwindCSS.experimental.classRegex": [["/\\* tw \\*/ ([^;]*);", "'([^']*)'"]],
|
||||||
"tailwindCSS.includeLanguages": {
|
"tailwindCSS.includeLanguages": { "javascript": "javascriptreact", "astro": "javascriptreact" },
|
||||||
"javascript": "javascriptreact",
|
|
||||||
"astro": "javascriptreact",
|
|
||||||
"svelte": "javascriptreact"
|
|
||||||
},
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import image from '@astrojs/image';
|
import image from '@astrojs/image';
|
||||||
import svelte from '@astrojs/svelte';
|
|
||||||
import tailwind from '@astrojs/tailwind';
|
import tailwind from '@astrojs/tailwind';
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import { visualizer } from 'rollup-plugin-visualizer';
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [tailwind(), image(), svelte()],
|
integrations: [tailwind(), image()],
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [visualizer()],
|
plugins: [visualizer()],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -9,7 +9,8 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "1.1.0",
|
"@floating-ui/dom": "1.1.0",
|
||||||
"iconify-icon": "1.0.2"
|
"iconify-icon": "1.0.2",
|
||||||
|
"nanoid": "4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/image": "0.12.1",
|
"@astrojs/image": "0.12.1",
|
||||||
|
|
@ -5206,15 +5207,14 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.4",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz",
|
||||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
"integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^14 || ^16 || >=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/napi-build-utils": {
|
"node_modules/napi-build-utils": {
|
||||||
|
|
@ -5834,6 +5834,18 @@
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
||||||
|
|
@ -12187,10 +12199,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.3.4",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz",
|
||||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
"integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"napi-build-utils": {
|
"napi-build-utils": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
|
@ -12552,6 +12563,14 @@
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"source-map-js": "^1.0.2"
|
"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": {
|
"postcss-import": {
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "1.1.0",
|
"@floating-ui/dom": "1.1.0",
|
||||||
"iconify-icon": "1.0.2"
|
"iconify-icon": "1.0.2",
|
||||||
|
"nanoid": "4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/image": "0.12.1",
|
"@astrojs/image": "0.12.1",
|
||||||
"@astrojs/react": "1.2.2",
|
"@astrojs/react": "1.2.2",
|
||||||
"@astrojs/svelte": "1.0.2",
|
|
||||||
"@astrojs/tailwind": "2.1.3",
|
"@astrojs/tailwind": "2.1.3",
|
||||||
"@types/react": "18.0.26",
|
|
||||||
"@types/react-dom": "18.0.10",
|
|
||||||
"astro": "1.9.2",
|
"astro": "1.9.2",
|
||||||
"concurrently": "7.6.0",
|
"concurrently": "7.6.0",
|
||||||
"iconify-icon-names": "1.1.0",
|
"iconify-icon-names": "1.1.0",
|
||||||
|
|
@ -36,10 +34,8 @@
|
||||||
"postcss": "8.4.21",
|
"postcss": "8.4.21",
|
||||||
"prettier": "2.8.2",
|
"prettier": "2.8.2",
|
||||||
"prettier-plugin-astro": "0.7.2",
|
"prettier-plugin-astro": "0.7.2",
|
||||||
"prettier-plugin-svelte": "2.9.0",
|
|
||||||
"prettier-plugin-tailwindcss": "0.2.1",
|
"prettier-plugin-tailwindcss": "0.2.1",
|
||||||
"rollup-plugin-visualizer": "5.9.0",
|
"rollup-plugin-visualizer": "5.9.0",
|
||||||
"svelte": "3.55.1",
|
|
||||||
"tailwindcss": "3.2.4",
|
"tailwindcss": "3.2.4",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { IconName } from '@/types/icon';
|
import type { IconName } from '@/types/icon';
|
||||||
|
|
||||||
import Icon from './icon.svelte';
|
import Icon from './icon.astro';
|
||||||
|
|
||||||
type IconButtonSize = 'small' | 'large';
|
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}>
|
<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>
|
</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 type { Data } from '@/data';
|
||||||
import isSectionKey from '@/utils/is-section-key';
|
import isSectionKey from '@/utils/is-section-key';
|
||||||
|
|
||||||
import SidebarItem from './sidebar-item.svelte';
|
import SidebarItem from './sidebar-item.astro';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -23,10 +23,31 @@ const sections = Object.keys(data).flatMap((key) => {
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
|
id="sidebar"
|
||||||
class:list={[
|
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',
|
'hidden xl:flex flex-col w-max h-fit p-2 rounded-lg gap-2 bg-white dark:bg-gray-800 shadow-md',
|
||||||
className,
|
className,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{sections.map((section) => <SidebarItem client:load {...section} />)}
|
{sections.map((section) => <SidebarItem {...section} />)}
|
||||||
</nav>
|
</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 type { IconName } from '@/types/icon';
|
||||||
|
|
||||||
import Icon from './icon.svelte';
|
import Icon from './icon.astro';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
name?: IconName;
|
name?: IconName;
|
||||||
|
|
@ -14,6 +14,6 @@ const { name, color } = Astro.props;
|
||||||
<div
|
<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"
|
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 />
|
<slot />
|
||||||
</div>
|
</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">
|
<div class="p-5">
|
||||||
<!-- Available icon names here: https://icon-sets.iconify.design -->
|
<!-- Available icon names here: https://icon-sets.iconify.design -->
|
||||||
<!-- Colors for simple icons here: https://simpleicons.org -->
|
<!-- 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>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import SidebarItem from '@/components/sidebar-item.svelte';
|
import SidebarItem from '@/components/sidebar-item.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<SidebarItem client:load icon="fa6-solid:bars-progress" section="experience" />
|
<SidebarItem icon="fa6-solid:bars-progress" section="experience" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import Sidebar from '@/components/sidebar.astro';
|
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 ExperienceSection from '@/sections/experience/experience-section.astro';
|
||||||
import FavoritesSection from '@/sections/favorites/favorites-section.astro';
|
import FavoritesSection from '@/sections/favorites/favorites-section.astro';
|
||||||
import MainSection from '@/sections/main/main-section.astro';
|
import MainSection from '@/sections/main/main-section.astro';
|
||||||
|
|
@ -44,7 +44,7 @@ const seoImage = seo.image ? seo.image : '/favicon.svg';
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="flex justify-center overflow-x-hidden bg-gray-50 dark:bg-gray-900 xl:relative xl:left-7">
|
<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">
|
<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} />
|
<MainSection {...data.main} />
|
||||||
{data.skills && <SkillsSection {...data.skills} />}
|
{data.skills && <SkillsSection {...data.skills} />}
|
||||||
|
|
@ -54,5 +54,6 @@ const seoImage = seo.image ? seo.image : '/favicon.svg';
|
||||||
{data.favorites && <FavoritesSection {...data.favorites} />}
|
{data.favorites && <FavoritesSection {...data.favorites} />}
|
||||||
</main>
|
</main>
|
||||||
<Sidebar data={data} className="sticky top-8 mt-20" />
|
<Sidebar data={data} className="sticky top-8 mt-20" />
|
||||||
|
<script src="../scripts/initialize-tooltips.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 Typography from '@/components/typography.astro';
|
||||||
import type { IconName } from '@/types/icon';
|
|
||||||
import type { LevelledSkill } from '@/types/skills-section';
|
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';
|
import SkillLevel from './skill-level.astro';
|
||||||
|
|
||||||
export interface Props extends LevelledSkill {}
|
export interface Props extends LevelledSkill {}
|
||||||
|
|
@ -17,21 +15,16 @@ const IconWrapper = url ? 'a' : 'div';
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex h-5 items-center justify-between">
|
<div class="flex h-5 items-center justify-between">
|
||||||
<IconWrapper class="flex gap-2 h-5" {...(url && { href: url, target: '_blank', rel: 'noopener noreferrer' })}>
|
<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">
|
<Typography variant="tile-subtitle">
|
||||||
<span class="text-gray-700 dark:text-gray-300">{name}</span>
|
<span class="text-gray-700 dark:text-gray-300">{name}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
{
|
{
|
||||||
description && (
|
description && (
|
||||||
<IconWithTooltip
|
<div class="flex h-3.5 w-3.5" data-tooltip={description} data-tooltip-placement="top">
|
||||||
client:load
|
<Icon name="fa6-solid:circle-info" color="#D1D5DB" size={14} />
|
||||||
name={'akar-icons:info-fill' as IconName}
|
</div>
|
||||||
color="#D1D5DB"
|
|
||||||
size={14}
|
|
||||||
content={description}
|
|
||||||
placement="top"
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,15 @@ module.exports = {
|
||||||
fluid200: 'repeat(auto-fit, minmax(200px, 1fr))',
|
fluid200: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||||
fluid240: 'repeat(auto-fit, minmax(240px, 1fr))',
|
fluid240: 'repeat(auto-fit, minmax(240px, 1fr))',
|
||||||
},
|
},
|
||||||
|
keyframes: {
|
||||||
|
show: {
|
||||||
|
from: { opacity: '0' },
|
||||||
|
to: { opacity: '1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
show: 'show 225ms ease-in-out',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue