Local metadata link commit at 2025-06-06 15:40:57 — file:///home/mrhavens/git-local-repos/git-sigil.git
This commit is contained in:
commit
b548318725
134 files changed
+15789
No files matched your search
@@ -0,0 +1,75 @@
|
||||
---
|
||||
import "@fontsource/fira-sans";
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
import { SITE } from "$/config";
|
||||
import "../styles/global.css";
|
||||
|
||||
export type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
permalink: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
const { title = SITE.title, description, permalink, image } = Astro.props;
|
||||
---
|
||||
|
||||
<!-- Use Google Fonts, if you don't wanna prefer a self-hosted version --><!-- <link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap" rel="stylesheet"> -->
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
{description && <meta name="description" content={description} />}
|
||||
<ViewTransitions />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="msapplication-config" content="/browserconfig.xml" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<!-- Open Graph Tags (Facebook) -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
{permalink && <meta property="og:url" content={permalink} />}
|
||||
{description && <meta property="og:description" content={description} />}
|
||||
{image && <meta property="og:image" content={image} />}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:title" content={title} />
|
||||
{permalink && <meta property="twitter:url" content={permalink} />}
|
||||
{description && <meta property="twitter:description" content={description} />}
|
||||
{image && <meta property="twitter:image" content={image} />}
|
||||
|
||||
<script is:inline>
|
||||
const theme = (() => {
|
||||
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
|
||||
return localStorage.getItem("theme");
|
||||
}
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark";
|
||||
}
|
||||
return "light";
|
||||
})();
|
||||
|
||||
if (theme === "light") {
|
||||
document.documentElement.classList.remove("dark");
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<body class="font-sans antialiased min-h-screen bg-gray-100 dark:bg-gray-800">
|
||||
<svg class="absolute w-full fill-theme-primary dark:fill-theme-dark-primary opacity-10 -z-10" viewBox="0 0 960 540" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><path d="M0 81L26.7 86.5C53.3 92 106.7 103 160 99.3C213.3 95.7 266.7 77.3 320 66.3C373.3 55.3 426.7 51.7 480 49.7C533.3 47.7 586.7 47.3 640 45.2C693.3 43 746.7 39 800 51C853.3 63 906.7 91 933.3 105L960 119L960 0L933.3 0C906.7 0 853.3 0 800 0C746.7 0 693.3 0 640 0C586.7 0 533.3 0 480 0C426.7 0 373.3 0 320 0C266.7 0 213.3 0 160 0C106.7 0 53.3 0 26.7 0L0 0Z" stroke-linecap="round" stroke-linejoin="miter"></path></svg>
|
||||
<div class="transition-colors">
|
||||
<main class="mx-auto max-w-4xl px-4 md:px-0">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon'
|
||||
export type CalloutType = 'check' | 'error' | 'note' | 'warning'
|
||||
interface Props {
|
||||
title: string
|
||||
type: CalloutType
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<CalloutType, string> = {
|
||||
'check': 'check-circle',
|
||||
'error': 'close-circle',
|
||||
'note': 'note',
|
||||
'warning': 'warning-circle'
|
||||
}
|
||||
|
||||
const COLOR_MAP: Record<CalloutType, string> = {
|
||||
'check': 'text-green-700',
|
||||
'error': 'text-red-700',
|
||||
'note': ' text-gray-700',
|
||||
'warning': 'text-orange-700'
|
||||
}
|
||||
|
||||
const { title, type = 'note' } = Astro.props
|
||||
---
|
||||
<div class="callout flex gap-2 w-full bg-gray-50 my-1 px-5 py-2 rounded-sm shadow-sm">
|
||||
<Icon class={`w-8 h-8 inline-block ${COLOR_MAP[type]}`} pack="mdi" name={ICON_MAP[type]} />
|
||||
<div class="copy flex flex-col">
|
||||
<h3 class={`title m-0 ${COLOR_MAP[type]}`}>{title}</h3>
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import CodeCopy from './CodeCopy.svelte'
|
||||
---
|
||||
<CodeCopy client:load/>
|
||||
<code class="astro-ink__code">
|
||||
<slot/>
|
||||
</code>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { fade, blur } from "svelte/transition";
|
||||
import { onMount } from "svelte";
|
||||
const COPIED_TIMEOUT = 2 * 1000
|
||||
|
||||
export let stayCopied = COPIED_TIMEOUT
|
||||
|
||||
let copied = false
|
||||
let selfElm: HTMLButtonElement;
|
||||
let isCodeBlock = true;
|
||||
|
||||
const copy = async () => {
|
||||
if(selfElm) {
|
||||
const preElm = selfElm.parentElement?.parentElement
|
||||
const codeElm = preElm?.querySelector('code')
|
||||
if(preElm?.tagName === 'PRE' && codeElm) {
|
||||
await navigator.clipboard.writeText(codeElm.innerText);
|
||||
copied = true
|
||||
|
||||
setTimeout(() => {
|
||||
copied = false
|
||||
}, stayCopied);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const preElm = selfElm.parentElement?.parentElement
|
||||
if(preElm && preElm.tagName === 'PRE') {
|
||||
isCodeBlock = true
|
||||
} else {
|
||||
isCodeBlock = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<button
|
||||
bind:this={selfElm}
|
||||
on:click={copy}
|
||||
class="absolute px-2 text-theme-primary dark:text-theme-dark-primary border-1 rounded-lg"
|
||||
style="top: 6px; right: 8px;"
|
||||
style:display={isCodeBlock ? 'inline-block' : 'none'}
|
||||
disabled={copied}
|
||||
>
|
||||
{#if copied}
|
||||
<span transition:blur={{ amount: 50, opacity: 50 }}>✓Copied</span>
|
||||
{:else}
|
||||
<span transition:fade>Copy</span>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
interface Props {
|
||||
value: Date;
|
||||
title?: string;
|
||||
lang?: Intl.LocalesArgument,
|
||||
dateStyle?: "full" | "long" | "medium" | "short"
|
||||
}
|
||||
const { value, title, lang = 'en-US', dateStyle = 'full' } = Astro.props
|
||||
---
|
||||
<time title={title} datetime={value.toISOString()}>
|
||||
{value.toLocaleDateString(lang, { dateStyle, timeZone: 'UTC' })}
|
||||
</time>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon'
|
||||
|
||||
interface Props {
|
||||
label?: string,
|
||||
editUrl: string
|
||||
}
|
||||
|
||||
const { editUrl, label = 'Edit this page' } = Astro.props
|
||||
|
||||
---
|
||||
{
|
||||
editUrl && (
|
||||
<a href={editUrl} title={label} class=" font-thin text-theme-primary dark:text-theme-dark-primary text-sm">
|
||||
<Icon class="w-4 h-4 inline-block" pack="mdi" name={'pencil'} />
|
||||
{label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import ModeLabel from './ModeLabel.svelte'
|
||||
---
|
||||
<footer class="footer">
|
||||
<nav class="nav">
|
||||
<div>2021 © Copyright notice | <a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}>{ SITE.name }</a>
|
||||
<ModeLabel client:load/> theme on <a href="https://astro.build/">Astro</a></div>
|
||||
</nav>
|
||||
</footer>
|
||||
<style>
|
||||
.footer {
|
||||
@apply py-6 border-t
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import SvgIcon from './SvgIcon.astro'
|
||||
import ModeSwitcherBtn from './ModeSwitcherBtn.svelte'
|
||||
import SearchBtn from './SearchBtn.svelte'
|
||||
|
||||
---
|
||||
|
||||
<header class="header">
|
||||
<div class="header__logo">
|
||||
<a href="/" class="avatar">
|
||||
<img class="header__logo-img" src="/assets/logo.svg" alt="Astro logo" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="header__meta flex-1">
|
||||
<h3 class="header__title dark:text-theme-dark-secondary">
|
||||
<a href="/">{ SITE.name }</a>
|
||||
</h3>
|
||||
<div class="header__meta-more flex">
|
||||
<p class="header__desc">
|
||||
{ SITE.description }
|
||||
</p>
|
||||
<nav class="header__nav flex">
|
||||
<ul class="header__ref-list">
|
||||
<li>
|
||||
<SearchBtn client:visible />
|
||||
</li>
|
||||
<li>
|
||||
<a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}>
|
||||
<SvgIcon>
|
||||
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
||||
</SvgIcon>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/rss.xml" title="RSS">
|
||||
<SvgIcon>
|
||||
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
||||
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
||||
<circle cx="5" cy="19" r="1"></circle>
|
||||
</SvgIcon>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<ModeSwitcherBtn client:visible />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
@apply flex gap-4 border-b py-3 /* border-gray-200 dark:border-gray-700 - check styles/global.css */
|
||||
}
|
||||
.header__logo-img {
|
||||
@apply w-16 h-16 rounded-full overflow-hidden
|
||||
}
|
||||
.header__title {
|
||||
@apply text-4xl font-extrabold md:text-5xl text-theme-secondary dark:text-theme-dark-secondary
|
||||
}
|
||||
.header__desc {
|
||||
@apply text-xl flex-1 dark:text-gray-200
|
||||
}
|
||||
.header__ref-list {
|
||||
@apply flex gap-3 text-gray-400
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<img src="/assets/yay.svg" alt="Yay!" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
@apply mx-auto w-2/3 mt-6
|
||||
}
|
||||
h1 {
|
||||
@apply w-full justify-center text-center text-3xl font-bold text-purple-600 py-10
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from './Header.astro';
|
||||
import Footer from './Footer.astro';
|
||||
import Nav from './Nav.astro';
|
||||
import Portal from './Portal.astro';
|
||||
import SearchModal from './SearchModal.svelte'
|
||||
|
||||
---
|
||||
<BaseLayout>
|
||||
<br class="my-4"/>
|
||||
<Header/>
|
||||
<Nav/>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<br class="my-4"/>
|
||||
<Footer/>
|
||||
<Portal>
|
||||
<SearchModal client:load/>
|
||||
</Portal>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
min-height: 580px
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
import { getMonthName } from '$/utils'
|
||||
import { USE_MEDIA_THUMBNAIL } from '$/config'
|
||||
const { post } = Astro.props
|
||||
---
|
||||
<div class="post-preview">
|
||||
<div class="sm:w-20 md:w-32">
|
||||
<div class="post-preview__date">
|
||||
<span class="post-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class={`flex-1 ${USE_MEDIA_THUMBNAIL && post.thumbnail ? 'flex flex-row gap-4' : ''}`}>
|
||||
{ USE_MEDIA_THUMBNAIL && post.thumbnail && <img class="post-preview__media" src= {post.thumbnail} alt="media thumbnail" />}
|
||||
<div class="flex flex-col mb-2">
|
||||
<h4 class="post-preview__title dark:text-theme-dark-primary">
|
||||
<a href={post.url} title={post.title} target="_blank">{post.title}</a>
|
||||
</h4>
|
||||
<div>
|
||||
<strong>{post.host}</strong>
|
||||
{
|
||||
post.participants.length > 0 && <em>with</em>
|
||||
}
|
||||
{
|
||||
post.participants.length > 0 && `${post.participants.join(', ')}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-preview {
|
||||
@apply flex gap-6
|
||||
}
|
||||
.post-preview__date {
|
||||
@apply flex flex-col w-full text-center
|
||||
}
|
||||
.post-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300
|
||||
}
|
||||
.post-preview__date__month-n-year {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary /* this doesn't works here */ hover:underline
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-lg leading-6 dark:text-white line-clamp-2 hyphens-auto
|
||||
}
|
||||
.post-preview__media {
|
||||
@apply w-48 rounded-md shadow-lg shadow-theme-accent-gray-light dark:shadow-theme-accent-gray-dark
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import MediaPreview from './MediaPreview.astro'
|
||||
const { posts } = Astro.props
|
||||
---
|
||||
<section class="media-preview__list">
|
||||
{posts.map((post) => (
|
||||
<MediaPreview post={post}/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.media-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ModeSensitive from './ModeSensitive.svelte'
|
||||
</script>
|
||||
<ModeSensitive>
|
||||
<span slot="dark">(dark)</span>
|
||||
<span slot="light">(light)</span>
|
||||
</ModeSensitive>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { theme } from '../store/theme'
|
||||
</script>
|
||||
{#if $theme === 'dark'}
|
||||
<slot name="dark"/>
|
||||
{:else}
|
||||
<slot name="light"/>
|
||||
{/if}
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { theme } from '../store/theme'
|
||||
|
||||
type ThemeType = 'dark' | 'light'
|
||||
|
||||
const THEME_DARK: ThemeType = 'dark'
|
||||
const THEME_LIGHT: ThemeType = 'light'
|
||||
let currTheme: ThemeType = THEME_DARK
|
||||
|
||||
|
||||
function toggleTheme() {
|
||||
window.document.documentElement.classList.toggle(THEME_DARK)
|
||||
currTheme = localStorage.getItem('theme') === THEME_DARK ? THEME_LIGHT : THEME_DARK
|
||||
// Update Storage
|
||||
localStorage.setItem('theme', currTheme)
|
||||
// Update Store
|
||||
theme.set(currTheme)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem('theme') === THEME_DARK || (!('theme' in localStorage) && window.matchMedia(`(prefers-color-scheme: ${THEME_DARK})`).matches)) {
|
||||
window.document.documentElement.classList.add(THEME_DARK)
|
||||
currTheme = THEME_DARK
|
||||
} else {
|
||||
window.document.documentElement.classList.remove(THEME_DARK)
|
||||
currTheme = THEME_LIGHT
|
||||
}
|
||||
// Update Store
|
||||
theme.set(currTheme)
|
||||
})
|
||||
</script>
|
||||
<button on:click={toggleTheme}>
|
||||
<slot theme={currTheme}/>
|
||||
</button>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
|
||||
import { draw } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import ModeSwitcher from './ModeSwitcher.svelte'
|
||||
import SvgIcon from './SvgIcon.svelte'
|
||||
</script>
|
||||
<ModeSwitcher let:theme>
|
||||
<SvgIcon>
|
||||
{#if theme === 'dark'}
|
||||
<circle cx="12" cy="12" r="5" transition:draw={{ duration: 1000, delay: 200, easing: quintOut }}></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3" transition:draw={{ duration: 100, delay: 30, easing: quintOut }}></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23" transition:draw={{ duration: 100, delay: 40, easing: quintOut }}></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" transition:draw={{ duration: 100, delay: 50, easing: quintOut }}></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" transition:draw={{ duration: 100, delay: 60, easing: quintOut }}></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12" transition:draw={{ duration: 100, delay: 70, easing: quintOut }}></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12" transition:draw={{ duration: 100, delay: 80, easing: quintOut }}></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" transition:draw={{ duration: 100, delay: 90, easing: quintOut }}></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" transition:draw={{ duration: 100, delay: 100, easing: quintOut }}></line>
|
||||
{:else}
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" transition:draw={{ duration: 500, delay: 100, easing: quintOut }}></path>
|
||||
{/if}
|
||||
</SvgIcon>
|
||||
</ModeSwitcher>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import { toTitleCase } from '$/utils'
|
||||
import { NAV_ITEMS } from '$/config'
|
||||
---
|
||||
<nav class="nav py-3">
|
||||
<ul class="nav-list dark:text-theme-dark-secondary" transition:animate="fade">
|
||||
{
|
||||
Object.keys(NAV_ITEMS).map(navItemKey => <li>
|
||||
<a class:list={[
|
||||
`pb-1 border-b-2 hover:border-gray-400 hover:dark:border-gray-700 `,
|
||||
Astro.url.pathname !== NAV_ITEMS[navItemKey].path ? 'border-gray-100 dark:border-gray-800': '',
|
||||
Astro.url.pathname === NAV_ITEMS[navItemKey].path ? 'border-theme-primary' : ''
|
||||
]} href={NAV_ITEMS[navItemKey].path} title={NAV_ITEMS[navItemKey].title}>{toTitleCase(NAV_ITEMS[navItemKey].title)}</a>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<style>
|
||||
.nav-list {
|
||||
@apply inline-flex list-none gap-8 text-xl font-semibold text-theme-secondary dark:text-theme-dark-secondary py-2 flex-wrap mb-8
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
const { page } = Astro.props
|
||||
---
|
||||
<div class="page__actions">
|
||||
{page.url.prev && <a class="action__go-to-x" href={page.url.prev} title="Go to Previous">← Prev</a>}
|
||||
{page.url.next && <a class="action__go-to-x" href={page.url.next} title="Go to Next">Next →</a>}
|
||||
</div>
|
||||
<style>
|
||||
.page__actions {
|
||||
@apply flex justify-center md:justify-end py-6 gap-2
|
||||
}
|
||||
.action__go-to-x {
|
||||
@apply text-base uppercase text-gray-500 dark:text-gray-400 hover:underline
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="portal-root">
|
||||
<slot/>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import { getMonthName, getSlugFromPathname } from '$/utils'
|
||||
const { frontmatter: post, file } = Astro.props.post
|
||||
---
|
||||
<div class="post-draft-preview">
|
||||
<div class="sm:w-20 md:w-32">
|
||||
<div class="post-draft-preview__date">
|
||||
<span class="post-draft-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-draft-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="post-draft-preview__title dark:text-theme-dark-primary">
|
||||
<a href={`/drafts/${getSlugFromPathname(file)}`} title={post.title}>{post.title}</a>
|
||||
</h4>
|
||||
<p class="post-draft-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-draft-preview {
|
||||
@apply flex gap-6
|
||||
}
|
||||
.post-draft-preview__date {
|
||||
@apply flex flex-col w-full text-center
|
||||
}
|
||||
.post-draft-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300
|
||||
}
|
||||
.post-draft-preview__date__month-n-year {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post-draft-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary /* this doesn't works here */ hover:underline mb-2
|
||||
}
|
||||
.post-draft-preview__desc {
|
||||
@apply text-lg leading-6 dark:text-white line-clamp-2 hyphens-auto
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import PostDraftPreview from './PostDraftPreview.astro'
|
||||
const { posts } = Astro.props
|
||||
---
|
||||
<section class="post-draft-preview__list">
|
||||
{posts.map((post) => (
|
||||
<PostDraftPreview post={post}/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.post-draft-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import { getMonthName } from '$/utils'
|
||||
|
||||
interface Props {
|
||||
post: CollectionEntry<'blog'>,
|
||||
asCard?: boolean
|
||||
}
|
||||
|
||||
const { post: { data: post, slug }, asCard = false } = Astro.props
|
||||
---
|
||||
<div class={`post-preview ${asCard && 'post-preview--card'}`}>
|
||||
<div class="post-preview__date-box">
|
||||
<div class="post-preview__date">
|
||||
<span class="post-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="post-preview__title dark:text-theme-dark-primary">
|
||||
<a href={`/blog/${slug}`} title={post.title}>{post.title}</a>
|
||||
</h4>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-preview {
|
||||
@apply flex gap-6;
|
||||
}
|
||||
.post-preview--card {
|
||||
@apply flex flex-col-reverse gap-4 sm:w-72 md:w-60 lg:w-64;
|
||||
}
|
||||
.post-preview__date-box {
|
||||
@apply sm:w-20 md:w-32
|
||||
}
|
||||
.post-preview--card .post-preview__date-box {
|
||||
@apply w-full
|
||||
}
|
||||
.post-preview__date {
|
||||
@apply flex flex-col w-full text-center;
|
||||
}
|
||||
.post-preview--card .post-preview__date {
|
||||
@apply text-left flex flex-row gap-1
|
||||
}
|
||||
.post-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300;
|
||||
}
|
||||
.post-preview--card .post-preview__date__day {
|
||||
@apply text-4xl
|
||||
}
|
||||
.post-preview__date__month-n-year {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary /* this doesn't works here */ hover:underline mb-2;
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-lg leading-6 line-clamp-2 dark:text-white hyphens-auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import PostPreview from './PostPreview.astro'
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry<'blog'>
|
||||
heading?: string
|
||||
mode?: 'row' | 'col'
|
||||
}
|
||||
|
||||
const { posts, heading, mode = 'col' } = Astro.props
|
||||
const sortedPosts = posts.sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
|
||||
---
|
||||
{ heading ? <h5 class={`post-preview__heading post-preview__heading--${mode} ink-h`}>{heading}</h5> : ''}
|
||||
<section class={`post-preview__list post-preview__list--${mode}`}>
|
||||
{sortedPosts.map((post) => (
|
||||
<PostPreview post={post} asCard={mode === 'row' ? true : false }/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.post-preview__heading {
|
||||
@apply pl-0 sm:pl-6
|
||||
}
|
||||
.post-preview__heading--row {
|
||||
@apply pl-0
|
||||
}
|
||||
.post-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
.post-preview__list--row {
|
||||
@apply flex-row flex-wrap md:px-6 lg:px-0 gap-12 md:gap-8 sm:gap-10 lg:gap-12
|
||||
}
|
||||
.post-preview__list--col {
|
||||
@apply flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
type Props = {
|
||||
slug: string
|
||||
title: string
|
||||
description: string
|
||||
category: string,
|
||||
tags: Array<string>
|
||||
}
|
||||
export let post: Props
|
||||
export let isLast: boolean = false
|
||||
</script>
|
||||
<div class="post-preview hover:bg-theme-primary">
|
||||
<div class="flex-1">
|
||||
<h4 class="post-preview__title">
|
||||
<a href={`/${post.category}/${post.slug}`} title={post.title}>{post.title} →</a>
|
||||
</h4>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
<ul class="tag-list">
|
||||
{#each post.tags as tag}
|
||||
<a class="tag" href={`/tags/${tag}`} title={tag}>{tag}</a>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{#if !isLast}
|
||||
<hr class="my-4 text-theme-dark-secondary"/>
|
||||
{/if}
|
||||
<style lang="postcss">
|
||||
.post-preview {
|
||||
@apply flex gap-6 text-left;
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-lg leading-tight font-semibold text-white mb-2;
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-base text-theme-dark-primary leading-5 line-clamp-2;
|
||||
}
|
||||
.tag-list {
|
||||
@apply list-none py-2 flex flex-wrap gap-2;
|
||||
}
|
||||
.tag {
|
||||
@apply inline-block text-xs px-4 py-1 rounded-full text-theme-primary bg-theme-dark-primary;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
|
||||
export let slug: string = ''
|
||||
|
||||
let loading: boolean = false
|
||||
let views: number = 0
|
||||
|
||||
onMount(async () => {
|
||||
if(slug && slug.trim() !== '') {
|
||||
try {
|
||||
loading = true
|
||||
const resp = await fetch(`/api/blog/views/${slug}.json`)
|
||||
const stats = await resp.json()
|
||||
views = stats.views
|
||||
} catch(e) {
|
||||
console.error('PostStats', e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<span class="post-stats__views">{ views } views</span>
|
||||
<style>
|
||||
.post-stats__views {
|
||||
@apply px-1 mx-1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<article class="prose dark:prose-invert">
|
||||
<slot />
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.prose {
|
||||
@apply max-w-none
|
||||
/* Size Modifiers: https://github.com/tailwindlabs/tailwindcss-typography#size-modifiers */
|
||||
/* Color Themes: https://github.com/tailwindlabs/tailwindcss-typography#color-modifiers */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import SearchIcon from './SearchIcon.svelte'
|
||||
import PostSearchPreview from './PostSearchPreview.svelte'
|
||||
|
||||
let searchInput
|
||||
let searchableDocs
|
||||
let searchIndex
|
||||
|
||||
let searchQuery = ''
|
||||
let searchResults = []
|
||||
|
||||
onMount(async() => {
|
||||
const lunr = (await import('lunr')).default
|
||||
const resp = await fetch('/search-index.json')
|
||||
searchableDocs = await resp.json()
|
||||
// Initialize indexing
|
||||
searchIndex = lunr(function(){
|
||||
// the match key...
|
||||
this.ref('slug')
|
||||
|
||||
// indexable properties
|
||||
this.field('title')
|
||||
this.field('description')
|
||||
this.field('tags')
|
||||
|
||||
// Omit, if you don't want to search on `body`
|
||||
this.field('body')
|
||||
|
||||
// Index every document
|
||||
searchableDocs.forEach(doc => {
|
||||
this.add(doc)
|
||||
}, this)
|
||||
})
|
||||
searchInput.focus()
|
||||
})
|
||||
|
||||
$: {
|
||||
if(searchQuery && searchQuery.length >= 3) {
|
||||
const matches = searchIndex.search(searchQuery)
|
||||
searchResults = []
|
||||
matches.map(match => {
|
||||
searchableDocs.filter(doc => {
|
||||
if(match.ref === doc.slug) {
|
||||
searchResults.push(doc)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="search">
|
||||
<div class="search__ctrl">
|
||||
<label for="search"><SearchIcon found={searchResults.length > 0} /></label>
|
||||
<input type="text" name="search" bind:this={searchInput} placeholder="What are you looking for?" bind:value={searchQuery} />
|
||||
</div>
|
||||
<div class="search__results">
|
||||
{#if searchResults.length}
|
||||
{#each searchResults as post, i }
|
||||
<PostSearchPreview post={post} isLast={ i === searchResults.length - 1 } />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="search__results--none">
|
||||
{#if searchQuery.length}
|
||||
No matching items found!
|
||||
{:else}
|
||||
Search something and let me find it for you! :-)
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="note"><small>click anywhere outside to close</small></div>
|
||||
</div>
|
||||
<style>
|
||||
.search {
|
||||
@apply w-full relative bg-theme-primary p-8 rounded-md shadow-lg;
|
||||
}
|
||||
input {
|
||||
@apply w-full px-4 py-2 pl-10 text-xl font-semibold text-gray-600 border-0 shadow-inner rounded-md bg-gray-100 placeholder-theme-dark-secondary;
|
||||
}
|
||||
.search__ctrl {
|
||||
@apply pb-4 relative;
|
||||
}
|
||||
.search__ctrl label {
|
||||
@apply text-theme-primary absolute top-2 left-2;
|
||||
}
|
||||
.search__results {
|
||||
@apply w-96 h-64 py-4 overflow-y-auto;
|
||||
}
|
||||
.search__results--none {
|
||||
@apply text-center text-theme-dark-primary;
|
||||
}
|
||||
.note {
|
||||
@apply w-full text-center text-white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import SearchIcon from './SearchIcon.svelte'
|
||||
import { isSearchVisible } from '../store/search'
|
||||
|
||||
function showSearchDialog() {
|
||||
isSearchVisible.set(true)
|
||||
}
|
||||
</script>
|
||||
<button on:click={showSearchDialog}>
|
||||
<SearchIcon />
|
||||
</button>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import SvgIcon from './SvgIcon.svelte'
|
||||
export let found:boolean = false
|
||||
</script>
|
||||
<SvgIcon>
|
||||
{#if found}
|
||||
<path
|
||||
d="M7.66542 10.2366L9.19751 8.951L10.4831 10.4831L13.5473 7.91194L14.8328 9.44402L10.2366 13.3007L7.66542 10.2366Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{/if}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.2071 4.89344C19.0923 7.77862 19.3131 12.3193 16.8693 15.4578C16.8846 15.4713 16.8996 15.4854 16.9143 15.5L21.1569 19.7427C21.5474 20.1332 21.5474 20.7664 21.1569 21.1569C20.7664 21.5474 20.1332 21.5474 19.7427 21.1569L15.5 16.9143C15.4854 16.8996 15.4713 16.8846 15.4578 16.8693C12.3193 19.3131 7.77862 19.0923 4.89344 16.2071C1.76924 13.083 1.76924 8.01763 4.89344 4.89344C8.01763 1.76924 13.083 1.76924 16.2071 4.89344ZM14.7929 14.7929C17.1361 12.4498 17.1361 8.6508 14.7929 6.30765C12.4498 3.96451 8.6508 3.96451 6.30765 6.30765C3.96451 8.6508 3.96451 12.4498 6.30765 14.7929C8.6508 17.1361 12.4498 17.1361 14.7929 14.7929Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition'
|
||||
import { isSearchVisible } from '../store/search'
|
||||
import Search from './Search.svelte'
|
||||
|
||||
const dismissModal = () => isSearchVisible.set(false)
|
||||
const handleEsc = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
dismissModal()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{#if $isSearchVisible}
|
||||
<div class="modal__backdrop" role="button" tabindex="0" on:click={dismissModal} on:keydown={handleEsc} transition:fade></div>
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal__cnt" transition:fly="{{ y: 200, duration: 300 }}">
|
||||
<Search />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<style>
|
||||
.modal {
|
||||
@apply fixed top-0 left-0 w-full h-full grid justify-center content-center pointer-events-none;
|
||||
}
|
||||
.modal__backdrop {
|
||||
@apply fixed top-0 left-0 w-full h-screen opacity-50 bg-gradient-to-tr from-fuchsia-600 to-fuchsia-900 z-0;
|
||||
}
|
||||
.modal__cnt {
|
||||
@apply w-full z-10 pointer-events-auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<slot />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 202 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<slot/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
@@ -0,0 +1,51 @@
|
||||
---
|
||||
export type BadgeType = 'success' | 'danger' | 'note' | 'warning' | 'tip'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
type: BadgeType
|
||||
outline?: boolean
|
||||
}
|
||||
|
||||
const { text, type = 'note', outline = false } = Astro.props
|
||||
---
|
||||
<span class:list={['ink-badge', `ink-badge__${type}${outline && '--outline'}`, `ink-badge__${type}`]} set:html={text} />
|
||||
|
||||
<style>
|
||||
.ink-badge {
|
||||
@apply inline-block font-xs font-normal text-inherit bg-theme-primary dark:bg-theme-dark-primary px-3 py-1 border-1 rounded-b-md
|
||||
}
|
||||
.ink-badge--outline {
|
||||
@apply bg-transparent
|
||||
}
|
||||
.ink-badge__success {
|
||||
@apply bg-green-300
|
||||
}
|
||||
.ink-badge__danger {
|
||||
@apply bg-red-300
|
||||
}
|
||||
.ink-badge__note {
|
||||
@apply bg-gray-300
|
||||
}
|
||||
.ink-badge__warning {
|
||||
@apply bg-orange-300
|
||||
}
|
||||
.ink-badge__tip {
|
||||
@apply bg-yellow-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__success {
|
||||
@apply border-green-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__danger {
|
||||
@apply border-red-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__note {
|
||||
@apply border-gray-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__warning {
|
||||
@apply border-orange-300
|
||||
}
|
||||
.ink-badge--outline.ink-badge__tip {
|
||||
@apply border-yellow-300
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon'
|
||||
export type CalloutType = 'check' | 'error' | 'note' | 'warning'
|
||||
interface Props {
|
||||
title: string
|
||||
type: CalloutType
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<CalloutType, string> = {
|
||||
'check': 'check-circle',
|
||||
'error': 'close-circle',
|
||||
'note': 'note',
|
||||
'warning': 'warning-circle'
|
||||
}
|
||||
|
||||
const COLOR_MAP: Record<CalloutType, string> = {
|
||||
'check': 'text-green-700',
|
||||
'error': 'text-red-700',
|
||||
'note': ' text-gray-700',
|
||||
'warning': 'text-orange-700'
|
||||
}
|
||||
|
||||
const { title, type = 'note' } = Astro.props
|
||||
---
|
||||
<div class="callout flex gap-2 w-full bg-gray-200/75 dark:bg-gray-600/75 p-4 rounded-sm shadow-sm">
|
||||
<Icon class={`w-6 h-6 inline-block ${COLOR_MAP[type]}`} pack="mdi" name={ICON_MAP[type]} />
|
||||
<div class="copy flex flex-col">
|
||||
<h4 class={`title m-0 ${COLOR_MAP[type]}`}>{title}</h4>
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
interface Props {
|
||||
href: string
|
||||
title: string
|
||||
target: string
|
||||
}
|
||||
|
||||
const { href, title, target } = Astro.props
|
||||
---
|
||||
<a class="site-link" href={href} title={title} target={target}>
|
||||
<slot/>
|
||||
</a>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { TABS } from './Tabs.svelte';
|
||||
|
||||
const tab = {};
|
||||
const { registerTab, selectTab, selectedTab } = getContext(TABS);
|
||||
|
||||
registerTab(tab);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
button {
|
||||
@apply bg-none border-b-2 border-solid border-white m-0 text-gray-400 px-4 py-1;
|
||||
}
|
||||
|
||||
.selected {
|
||||
@apply border-b-2 border-solid border-gray-700 text-gray-700;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class:selected="{$selectedTab === tab}" on:click="{() => selectTab(tab)}">
|
||||
<slot></slot>
|
||||
</button>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="tab-list">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tab-list {
|
||||
@apply border-b border-solid border-gray-500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { TABS } from './Tabs.svelte';
|
||||
|
||||
const panel = {};
|
||||
const { registerPanel, selectedPanel } = getContext(TABS);
|
||||
|
||||
registerPanel(panel);
|
||||
</script>
|
||||
|
||||
{#if $selectedPanel === panel}
|
||||
<slot></slot>
|
||||
{/if}
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Tabs from './index.svelte'
|
||||
import type { TabItem } from './tabs'
|
||||
|
||||
interface Props {
|
||||
tabs: TabItem[]
|
||||
}
|
||||
|
||||
const { tabs } = Astro.props
|
||||
---
|
||||
<Tabs tabs={tabs} client:visible/>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script context="module">
|
||||
export const TABS = {};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { setContext, onDestroy } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const tabs = [];
|
||||
const panels = [];
|
||||
const selectedTab = writable(null);
|
||||
const selectedPanel = writable(null);
|
||||
|
||||
setContext(TABS, {
|
||||
registerTab: tab => {
|
||||
tabs.push(tab);
|
||||
selectedTab.update(current => current || tab);
|
||||
|
||||
onDestroy(() => {
|
||||
const i = tabs.indexOf(tab);
|
||||
tabs.splice(i, 1);
|
||||
selectedTab.update(current => current === tab ? (tabs[i] || tabs[tabs.length - 1]) : current);
|
||||
});
|
||||
},
|
||||
|
||||
registerPanel: panel => {
|
||||
panels.push(panel);
|
||||
selectedPanel.update(current => current || panel);
|
||||
|
||||
onDestroy(() => {
|
||||
const i = panels.indexOf(panel);
|
||||
panels.splice(i, 1);
|
||||
selectedPanel.update(current => current === panel ? (panels[i] || panels[panels.length - 1]) : current);
|
||||
});
|
||||
},
|
||||
|
||||
selectTab: tab => {
|
||||
const i = tabs.indexOf(tab);
|
||||
selectedTab.set(tab);
|
||||
selectedPanel.set(panels[i]);
|
||||
},
|
||||
|
||||
selectedTab,
|
||||
selectedPanel
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="tabs">
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Tabs, TabList, TabPanel, Tab, type TabItem } from './tabs';
|
||||
export let tabs: TabItem[] = []
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
{#each tabs as tab}
|
||||
<Tab>{tab.title}</Tab>
|
||||
{/each}
|
||||
</TabList>
|
||||
|
||||
{#each tabs as tab}
|
||||
<TabPanel>
|
||||
<div class="body">{tab.body}</div>
|
||||
</TabPanel>
|
||||
{/each}
|
||||
</Tabs>
|
||||
|
||||
<style>
|
||||
.body {
|
||||
@apply px-4 py-1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as Tabs } from "./Tabs.svelte";
|
||||
export { default as TabList } from "./TabList.svelte";
|
||||
export { default as TabPanel } from "./TabPanel.svelte";
|
||||
export { default as Tab } from "./Tab.svelte";
|
||||
|
||||
export interface TabItem {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
interface Props {
|
||||
url: string
|
||||
}
|
||||
|
||||
const { url } = Astro.props
|
||||
---
|
||||
<div
|
||||
class="twitter-embed flex flex-col items-center justify-center relative"
|
||||
>
|
||||
<blockquote
|
||||
class="twitter-tweet"
|
||||
data-conversation="none"
|
||||
data-theme="light"
|
||||
data-lang="en"
|
||||
data-dnt="true"
|
||||
>
|
||||
<a class="unset no-underline text-current absolute top-0 left-0" href={url}>Loading embedded tweet...</a>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<script
|
||||
async
|
||||
defer
|
||||
src="https://platform.twitter.com/widgets.js"
|
||||
charset="utf-8"
|
||||
></script>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const { url, title } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
<iframe
|
||||
class="yt-iframe"
|
||||
width="560"
|
||||
height="315"
|
||||
src={url}
|
||||
title={title}
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.yt-iframe {
|
||||
@apply w-full aspect-[16/9]
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user