-
-
-
-
+
)
}
diff --git a/src/components/organisms/MobileMenu/MobileMenu.tsx b/src/components/organisms/MobileMenu/MobileMenu.tsx
new file mode 100644
index 00000000..95d515c7
--- /dev/null
+++ b/src/components/organisms/MobileMenu/MobileMenu.tsx
@@ -0,0 +1,53 @@
+import { memo } from 'react'
+
+import { Details } from '@/components/atoms/Details'
+import { MagicButton } from '@/components/atoms/MagicButton'
+import { plainBlockStyle } from '@/components/atoms/PlainBlock'
+import { MobileMenuBkg } from '@/components/organisms/MobileMenu/MobileMenuBkg'
+import { NAVIGATION_LINKS } from '@/components/organisms/Navigation'
+
+import { tv } from '@/lib/tailwind/variants'
+
+import { useToggleMenuCallback } from './hooks'
+import { Settings } from './Settings'
+
+const styles = tv({
+ slots: {
+ menu: `
+ tw-relative tw-mt-8
+ tw-w-fit before:tw-absolute before:-tw-top-9 before:tw-right-5
+ before:tw-z-[250] before:tw-ml-0 before:tw-border-[0.9rem]
+ before:tw-border-b-[2rem] before:tw-border-transparent
+ before:tw-border-b-window-color before:tw-content-['']
+ sp:tw-mt-2 sp:before:tw-hidden pc:-tw-right-[17.5px]
+ `,
+ window: plainBlockStyle({
+ className:
+ 'tw-flex tw-flex-col tw-gap-3 tw-rounded-lg tw-p-4 sp:tw-w-full',
+ }),
+ links: 'tw-grid tw-grid-cols-2 tw-gap-x-2 tw-gap-y-2.5',
+ },
+})()
+
+export const MobileMenu = memo(function MobileMenu() {
+ const toggleMenu = useToggleMenuCallback()
+
+ return (
+
+
+
+ )
+})
diff --git a/src/components/organisms/MobileMenu/MobileMenuBkg.tsx b/src/components/organisms/MobileMenu/MobileMenuBkg.tsx
new file mode 100644
index 00000000..9046bf79
--- /dev/null
+++ b/src/components/organisms/MobileMenu/MobileMenuBkg.tsx
@@ -0,0 +1,56 @@
+import { ReactNode } from 'react'
+
+import { MainWrapper } from '@/components/atoms/MainWrapper'
+import {
+ useMobileMenuState,
+ useToggleMenuCallback,
+} from '@/components/organisms/MobileMenu/hooks'
+
+import { tv } from '@/lib/tailwind/variants'
+
+const createStyles = tv({
+ slots: {
+ wrapper:
+ 'tw-fixed tw-inset-0 tw-z-[150] tw-flex ' +
+ 'tw-pt-[--header-height] tw-align-bottom',
+ background: 'tw-absolute tw-inset-0 tw-z-[150] tw-bg-black tw-duration-300',
+ menuWrapper:
+ 'tw-z-[151] tw-my-0 tw-flex tw-flex-row-reverse tw-duration-300',
+ },
+ variants: {
+ opened: {
+ true: {
+ background: 'tw-opacity-70',
+ },
+ false: {
+ wrapper: 'tw-pointer-events-none',
+ background: '-tw-z-10 tw-opacity-0',
+ menuWrapper: '-tw-translate-y-[150%]',
+ },
+ },
+ },
+})
+
+interface MobileMenuBkgProps {
+ children: ReactNode
+}
+
+export function MobileMenuBkg(props: MobileMenuBkgProps) {
+ const doNothing = () => {}
+ const toggleMenu = useToggleMenuCallback()
+ const [isOpened] = useMobileMenuState()
+
+ const styles = createStyles({ opened: isOpened })
+
+ return (
+
+
+
+ {props.children}
+
+
+ )
+}
diff --git a/src/components/organisms/MobileMenu/Settings.tsx b/src/components/organisms/MobileMenu/Settings.tsx
index 70dded93..914f0071 100644
--- a/src/components/organisms/MobileMenu/Settings.tsx
+++ b/src/components/organisms/MobileMenu/Settings.tsx
@@ -1,22 +1,35 @@
'use client'
-import { useShouldFollowHeaderAtom } from '@/states/shouldFollowHeaderAtom'
-import { useShouldHideHeaderAtom } from '@/states/shouldHideHeaderAtom'
+import { tv } from 'tailwind-variants'
+
+import { Input } from '@/components/wrappers'
+
+import { useUserSettingStickyHeader } from '@/states/shouldFollowHeaderAtom'
+import { useUserSettingAlwaysVisibleHeader } from '@/states/shouldHideHeaderAtom'
import { useShowSiteCommentsAtom } from '@/states/showSiteCommentsAtom'
+const styles = tv({
+ slots: {
+ settings: 'tw-flex tw-flex-col tw-gap-1',
+ label: 'tw-flex tw-items-center tw-gap-1',
+ },
+})()
+
function FeedbackServiceCheckbox() {
const [shouldShowSiteComments, setShouldShowSiteComments] =
useShowSiteCommentsAtom()
return (
)
@@ -24,14 +37,15 @@ function FeedbackServiceCheckbox() {
function HeaderCheckbox() {
const [shouldFollowHeader, setShouldFollowHeader] =
- useShouldFollowHeaderAtom()
- const [shouldHideHeader, setShouldHideHeader] = useShouldHideHeaderAtom()
+ useUserSettingStickyHeader()
+ const [shouldHideHeader, setShouldHideHeader] =
+ useUserSettingAlwaysVisibleHeader()
return (
<>
-
-
@@ -56,7 +70,7 @@ function HeaderCheckbox() {
export function Settings() {
return (
-
+
diff --git a/src/components/organisms/MobileMenu/hooks.ts b/src/components/organisms/MobileMenu/hooks.ts
new file mode 100644
index 00000000..3411803c
--- /dev/null
+++ b/src/components/organisms/MobileMenu/hooks.ts
@@ -0,0 +1,21 @@
+import { useCallback } from 'react'
+
+import { atom, useAtom } from 'jotai/index'
+
+import { useSetAlwaysShownHeader } from '@/components/organisms/Header'
+
+const mobileMenuAtom = atom(false)
+
+export function useMobileMenuState() {
+ return useAtom(mobileMenuAtom)
+}
+
+export function useToggleMenuCallback() {
+ const [isOpened, setHamburgerState] = useMobileMenuState()
+ const setAlwaysShownHeader = useSetAlwaysShownHeader()
+
+ return useCallback(() => {
+ setHamburgerState(!isOpened)
+ setAlwaysShownHeader(!isOpened)
+ }, [isOpened, setAlwaysShownHeader, setHamburgerState])
+}
diff --git a/src/components/organisms/MobileMenu/index.module.scss b/src/components/organisms/MobileMenu/index.module.scss
deleted file mode 100644
index 4306224a..00000000
--- a/src/components/organisms/MobileMenu/index.module.scss
+++ /dev/null
@@ -1,177 +0,0 @@
-@use '@/styles/mixins';
-@use '@/components/atoms/Button/index.module.scss' as btn;
-
-$background-z-index: 150;
-$menu-z-index: 151;
-
-.menu_background {
- display: block;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: black;
- transition: 0.5s;
-
- &[data-menu-opened='false'] {
- z-index: -1;
- opacity: 0;
- }
-
- &[data-menu-opened='true'] {
- z-index: $background-z-index;
- opacity: 0.7;
- }
-}
-
-@include mixins.mq(sp) {
- .mobile_menu {
- text-align: left;
- }
-
- .side_menu {
- z-index: $menu-z-index;
- overflow-y: auto;
- overflow-x: hidden;
- position: fixed;
- top: 0;
- max-width: 250px;
- width: 100%;
- height: 100%;
- text-align: left;
-
- background: #50861f;
- box-shadow: -3px 0 #538c1d;
-
- transition: 0.5s;
- animation-timing-function: ease-in-out;
-
- &[data-menu-opened='false'] {
- display: block;
- right: -260px;
- }
-
- &[data-menu-opened='true'] {
- display: block;
- right: 0;
- }
- }
-
- .side_header {
- margin: 0;
- padding: 0;
- height: var(--header-height);
- }
-
- .side_links {
- display: inline-block;
- margin: 0;
- padding: 0;
- font-family: var(--font-comfortaa);
- width: 100%;
-
- *[data-current-page='true'] {
- background: rgba(0, 0, 0, 0.28);
- }
-
- a {
- display: inline-block;
- color: white !important;
- text-decoration: none !important;
- width: 100%;
- border-bottom: 1px solid;
- border-bottom-color: rgba(255, 255, 255, 0.151);
-
- cursor: pointer;
- font-size: 1.5em;
- padding: 1em 1em;
- }
- }
-}
-
-@include mixins.mq(pc) {
- .scrollable {
- padding: 1rem;
- height: 100%;
- overflow-y: scroll;
- z-index: 152;
- }
-
- .side_menu {
- position: fixed;
- right: max(calc((100vw - 1000px) / 2 - 20px), 20px);
- width: 300px;
- background: var(--window-bkg-color);
-
- z-index: 151;
-
- max-height: calc(100vh - var(--header-height) - 4em);
- border-radius: 10px;
-
- display: flex;
- flex-direction: column;
- align-items: center;
-
- $outer-glow-for-dark-mode: 0 0 25px 15px #f1f1f155;
- $box-bottom-shadow: 0 5px 0 0 var(--window-bottom-color);
- box-shadow: $box-bottom-shadow;
- @include mixins.mq(dark) {
- box-shadow: $box-bottom-shadow, $outer-glow-for-dark-mode;
- }
-
- transition: 0.3s;
- animation-timing-function: linear;
- top: 0;
-
- &[data-menu-opened='false'] {
- transform: translateY(-100%);
- z-index: -1;
- opacity: 0;
- }
-
- &[data-menu-opened='true'] {
- transform: translateY(6em);
- opacity: 0.9;
- backdrop-filter: blur(15px);
- }
-
- &:before {
- content: '';
- position: absolute;
- right: 1.4em;
- top: -2rem;
- margin-left: -0.7em;
- border: 0.9em solid transparent;
- border-bottom: 2.1em solid var(--window-bkg-color);
- }
- }
-
- .side_links {
- display: grid;
- background: var(--window-bkg-color);
- grid-template-columns: 1fr 1fr;
- width: 100%;
- text-align: center;
- gap: 5px;
-
- a {
- display: block;
- @include btn.button-style;
- }
- }
-}
-
-.settings_wrapper {
- width: 100%;
-}
-
-.settings {
- margin-top: 1em;
- font-size: 0.9em;
- @include mixins.mq(sp) {
- margin: 1em 5px;
- }
- box-sizing: border-box;
- background: var(--window-bkg-color);
-}
diff --git a/src/components/organisms/MobileMenu/index.tsx b/src/components/organisms/MobileMenu/index.tsx
index afb95db0..ab5d5b2c 100644
--- a/src/components/organisms/MobileMenu/index.tsx
+++ b/src/components/organisms/MobileMenu/index.tsx
@@ -1,71 +1,5 @@
'use client'
-import { useCallback, memo } from 'react'
+export { useMobileMenuState, useToggleMenuCallback } from './hooks'
-import { atom, useAtom } from 'jotai'
-import Link from 'next/link'
-import { usePathname } from 'next/navigation'
-
-import { Details } from '@/components/atoms/Details'
-import { useSetAlwaysShownHeader } from '@/components/organisms/Header'
-import { NAVIGATION_LINKS } from '@/components/organisms/Navigation'
-
-import styles from './index.module.scss'
-import { Settings } from './Settings'
-
-const mobileMenuAtom = atom(false)
-
-export function useMobileMenuState() {
- return useAtom(mobileMenuAtom)
-}
-
-export function useToggleMenuCallback() {
- const [isOpened, setHamburgerState] = useMobileMenuState()
- const setAlwaysShownHeader = useSetAlwaysShownHeader()
-
- return useCallback(() => {
- setHamburgerState(!isOpened)
- setAlwaysShownHeader(!isOpened)
- }, [isOpened, setAlwaysShownHeader, setHamburgerState])
-}
-
-export const MobileMenu = memo(function MobileMenu() {
- const doNothing = () => {}
- const toggleMenu = useToggleMenuCallback()
-
- const pathname = usePathname()
- const currentLink = pathname?.split('/').slice(0, 2).join('/')
-
- const [isOpened] = useMobileMenuState()
-
- return (
-
- )
-})
+export { MobileMenu } from './MobileMenu'
diff --git a/src/components/organisms/Navigation.tsx b/src/components/organisms/Navigation.tsx
new file mode 100644
index 00000000..7579fe68
--- /dev/null
+++ b/src/components/organisms/Navigation.tsx
@@ -0,0 +1,84 @@
+'use client'
+import { usePathname } from 'next/navigation'
+import { tv } from 'tailwind-variants'
+
+import { A } from '@/components/wrappers'
+
+type NavigationLinkRecord = {
+ link: string
+ name: string
+ shortName?: string
+ showOnNavBar?: boolean
+}
+
+export const NAVIGATION_LINKS: NavigationLinkRecord[] = [
+ { link: '/', name: 'Home' },
+ { link: '/works', name: 'Works' },
+ { link: '/blog', name: 'Blog' },
+ { link: '/tweets', name: 'Tweets' },
+ { link: '/balloon', name: 'Balloons' },
+ { link: '/environment', name: 'Environment', shortName: 'Env' },
+ { link: '/stickers', name: 'Stickers' },
+ { link: '/icons', name: 'Icons' },
+ { link: '/links', name: 'Links', showOnNavBar: false },
+ { link: '/download', name: 'Downloads', shortName: 'DLC' },
+ {
+ link: '/icon-maker',
+ name: 'Icon Maker',
+ shortName: 'Maker',
+ showOnNavBar: false,
+ },
+ { link: '/walking', name: 'Walking', shortName: 'Walk' },
+]
+
+const styles = {
+ wrapper: tv({
+ base: 'tw-w-full tw-bg-[#81bd4a] tw-py-2 sp:tw-hidden dark:tw-bg-trpfrog-700',
+ }),
+ nav: tv({
+ base: [
+ 'tw-m-auto tw-w-full tw-max-w-[980px]',
+ 'tw-flex tw-items-center tw-justify-between',
+ ],
+ }),
+ link: tv({
+ base: [
+ 'tw-inline-block tw-rounded-full tw-px-4 tw-pt-1',
+ 'tw-duration-1200 tw-font-comfortaa',
+ ],
+ variants: {
+ current: {
+ true: `
+ tw-bg-white tw-text-[#81bd4a] hover:tw-bg-white/80
+ dark:tw-text-trpfrog-700
+ `,
+ false: 'tw-text-white hover:tw-bg-white/20',
+ },
+ },
+ defaultVariants: {
+ current: false,
+ },
+ }),
+}
+
+export function Navigation() {
+ const pathname = usePathname()
+ const currentLink = pathname?.split('/').slice(0, 2).join('/')
+ return (
+
+
+
+ )
+}
diff --git a/src/components/organisms/Navigation/index.module.scss b/src/components/organisms/Navigation/index.module.scss
deleted file mode 100644
index 6f01e292..00000000
--- a/src/components/organisms/Navigation/index.module.scss
+++ /dev/null
@@ -1,70 +0,0 @@
-@use '@/styles/mixins';
-
-#wide_nav {
- background: #66a928d3;
- text-align: center;
- z-index: 50;
-
- //position: absolute;
- display: flex;
- align-items: flex-end;
- width: 100%;
- height: var(--navigation-height);
-
- a {
- display: inline-block;
- font-family: var(--font-comfortaa);
- text-decoration: none;
- color: white;
- padding: 1em;
- }
-
- box-shadow: 0 3px 0 #54a60c;
- @include mixins.mq(dark) {
- box-shadow: 0 3px 0 #27341a;
- }
-}
-
-#wide_nav_wrapper {
- width: 100%;
- vertical-align: bottom;
- margin-bottom: 10px;
-}
-
-@include mixins.mq(dark) {
- #wide_nav {
- background: #2d4b11d3;
- }
-}
-
-@include mixins.mq(pc) {
- .side_menu_link {
- border-radius: 1000px;
- height: 1.3em;
- margin: 0 2px;
- padding: 6px 12px 0 !important;
- transition: 0.1s;
-
- &[data-current-page='true'] {
- background: white;
- color: #66a928d3 !important;
- @include mixins.mq(dark) {
- color: #2d4b11d3 !important;
- }
- cursor: default;
- }
-
- &[data-current-page='false']:hover {
- background: rgba(255, 255, 255, 0.2);
- a:hover {
- color: #90e200;
- }
- }
- }
-}
-
-@include mixins.mq(sp) {
- #wide_nav {
- display: none;
- }
-}
diff --git a/src/components/organisms/Navigation/index.tsx b/src/components/organisms/Navigation/index.tsx
deleted file mode 100644
index 80a0a056..00000000
--- a/src/components/organisms/Navigation/index.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-'use client'
-import Link from 'next/link'
-import { usePathname } from 'next/navigation'
-
-import styles from './index.module.scss'
-
-type NavigationLinkRecord = {
- link: string
- name: string
- shortName?: string
- showOnNavBar?: boolean
-}
-
-export const NAVIGATION_LINKS: NavigationLinkRecord[] = [
- { link: '/', name: 'Home' },
- { link: '/works', name: 'Works' },
- { link: '/blog', name: 'Blog' },
- { link: '/tweets', name: 'Tweets' },
- { link: '/balloon', name: 'Balloons' },
- { link: '/environment', name: 'Environment', shortName: 'Env' },
- { link: '/stickers', name: 'Stickers' },
- { link: '/icons', name: 'Icons' },
- { link: '/links', name: 'Links', showOnNavBar: false },
- { link: '/download', name: 'Downloads' },
- {
- link: '/icon-maker',
- name: 'Icon Maker',
- shortName: 'Maker',
- showOnNavBar: false,
- },
- { link: '/walking', name: 'Walking', shortName: 'Walk' },
-]
-
-export const NavigationLinks = () => {
- const pathname = usePathname()
- const currentLink = pathname?.split('/').slice(0, 2).join('/')
-
- return (
- <>
- {NAVIGATION_LINKS.filter(({ showOnNavBar = true }) => showOnNavBar).map(
- ({ link, name, shortName }) => (
-
- {shortName ?? name}
-
- ),
- )}
- >
- )
-}
-
-export const Navigation = () => {
- return (
- <>
-
- >
- )
-}
diff --git a/src/components/organisms/Title/index.module.scss b/src/components/organisms/Title/index.module.scss
index d1cd5b86..a6844fc8 100644
--- a/src/components/organisms/Title/index.module.scss
+++ b/src/components/organisms/Title/index.module.scss
@@ -1,4 +1,4 @@
-@use '@/styles/mixins';
+@use '../../../styles/mixins';
.title {
h1 {
diff --git a/src/components/organisms/TwitterArchived/__snapshots__/index.test.tsx.snap b/src/components/organisms/TwitterArchived/__snapshots__/index.test.tsx.snap
index a7115ecd..680168fa 100644
--- a/src/components/organisms/TwitterArchived/__snapshots__/index.test.tsx.snap
+++ b/src/components/organisms/TwitterArchived/__snapshots__/index.test.tsx.snap
@@ -31,7 +31,7 @@ exports[`TwitterArchived snapshot test 1`] = `
>
2000-10-17
diff --git a/src/components/organisms/TwitterArchived/index.module.scss b/src/components/organisms/TwitterArchived/index.module.scss
index 1cad7d58..cabcf78c 100644
--- a/src/components/organisms/TwitterArchived/index.module.scss
+++ b/src/components/organisms/TwitterArchived/index.module.scss
@@ -1,4 +1,4 @@
-@use '@/styles/mixins';
+@use '../../../styles/mixins';
.wrapper {
width: 100%;
diff --git a/src/components/organisms/TwitterArchived/index.tsx b/src/components/organisms/TwitterArchived/index.tsx
index 9cf68e99..02ce764b 100644
--- a/src/components/organisms/TwitterArchived/index.tsx
+++ b/src/components/organisms/TwitterArchived/index.tsx
@@ -6,6 +6,7 @@ import {
TwitterImageData,
} from '@/components/atoms/twitter/TwitterImage'
import { TwitterHeader } from '@/components/molecules/TwitterHeader'
+import { A } from '@/components/wrappers'
import styles from './index.module.scss'
@@ -56,7 +57,7 @@ export function TwitterArchived(props: TwitterArchivedProps) {
{hasImage &&
}
{quote &&
}
diff --git a/src/components/organisms/YouTube.tsx b/src/components/organisms/YouTube.tsx
new file mode 100644
index 00000000..050df130
--- /dev/null
+++ b/src/components/organisms/YouTube.tsx
@@ -0,0 +1,16 @@
+import { memo } from 'react'
+
+import { YouTubeEmbed } from '@next/third-parties/google'
+
+export interface YouTubeProps {
+ videoId: string
+}
+
+export const YouTube = memo(function YouTube({ videoId }: YouTubeProps) {
+ return (
+
+ )
+})
diff --git a/src/components/utils/LiteYouTubeEmbedWrapper.tsx b/src/components/utils/LiteYouTubeEmbedWrapper.tsx
index 1b157763..882cc49c 100644
--- a/src/components/utils/LiteYouTubeEmbedWrapper.tsx
+++ b/src/components/utils/LiteYouTubeEmbedWrapper.tsx
@@ -4,9 +4,9 @@
'use client'
-import LiteYouTubeEmbed, { LiteYouTube } from 'react-lite-youtube-embed'
+import LiteYouTubeEmbed, { LiteYouTubeProps } from 'react-lite-youtube-embed'
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'
-export function LiteYouTubeEmbedWrapper(props: LiteYouTube) {
+export function LiteYouTubeEmbedWrapper(props: LiteYouTubeProps) {
return
}
diff --git a/src/components/utils/TooltipWrapper.tsx b/src/components/utils/TooltipWrapper.tsx
new file mode 100644
index 00000000..38f3d31a
--- /dev/null
+++ b/src/components/utils/TooltipWrapper.tsx
@@ -0,0 +1,9 @@
+'use client'
+
+import { ComponentProps } from 'react'
+
+import { Tooltip } from 'react-tooltip'
+
+export function TooltipWrapper(props: ComponentProps
) {
+ return
+}
diff --git a/src/components/wrappers/A.tsx b/src/components/wrappers/A.tsx
new file mode 100644
index 00000000..33454393
--- /dev/null
+++ b/src/components/wrappers/A.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react'
+import { useMemo } from 'react'
+
+import Link from 'next/link'
+
+import { isInternalLink } from '@/lib/isInternalLink'
+
+export interface AProps extends React.ComponentPropsWithRef<'a'> {
+ openInNewTab?: 'always' | 'external' | 'never' | boolean
+}
+
+export const A = React.forwardRef(
+ function A(props, ref) {
+ const { openInNewTab: _openInNewTab, href = '', ...rest } = props
+ const isInternal = isInternalLink(href)
+
+ let openInNewTab: boolean
+ if (_openInNewTab === 'external' || _openInNewTab == null) {
+ openInNewTab = !isInternal
+ } else if (_openInNewTab === 'always') {
+ openInNewTab = true
+ } else if (_openInNewTab === 'never') {
+ openInNewTab = false
+ } else {
+ openInNewTab = _openInNewTab
+ }
+
+ const openInNewTabProps = useMemo(
+ () =>
+ openInNewTab
+ ? {
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ }
+ : {},
+ [openInNewTab],
+ )
+
+ return isInternal ? (
+
+ ) : (
+
+ )
+ },
+)
diff --git a/src/components/atoms/H2/index.tsx b/src/components/wrappers/H2.tsx
similarity index 62%
rename from src/components/atoms/H2/index.tsx
rename to src/components/wrappers/H2.tsx
index f8d443f4..8fc5ca28 100644
--- a/src/components/atoms/H2/index.tsx
+++ b/src/components/wrappers/H2.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
-import styles from './index.module.scss'
+import { tv } from 'tailwind-variants'
export const iconURLs = {
trpfrog: '/images/icons/trpfrog.webp',
@@ -24,13 +24,25 @@ type Props = React.ComponentPropsWithRef<'h2'> & {
icon?: H2Icon
}
+const styles = tv({
+ slots: {
+ h2: [
+ 'tw-font-mplus-rounded tw-text-2xl tw-font-extrabold',
+ 'tw-my-3 tw-flex tw-align-middle',
+ ],
+ icon: 'tw-mr-2 tw-h-8 tw-align-baseline',
+ },
+})()
+
export const H2 = React.forwardRef(
function H2(props, ref) {
- const { icon, className = '', children, ...rest } = props
+ const { icon, className, children, ...rest } = props
return (
-
- {icon && }
+
+ {icon && icon in iconURLs && (
+
+ )}
{children}
)
diff --git a/src/components/wrappers/HorizontalRule.tsx b/src/components/wrappers/HorizontalRule.tsx
new file mode 100644
index 00000000..76028f96
--- /dev/null
+++ b/src/components/wrappers/HorizontalRule.tsx
@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+import { tv } from 'tailwind-variants'
+
+interface Props extends React.ComponentPropsWithRef<'hr'> {}
+
+const style = tv({
+ base: 'tw-my-8 tw-border-t-4 tw-border-dotted tw-border-t-body-color',
+})
+
+export const HorizontalRule = React.forwardRef(
+ function HorizontalRule(props, ref) {
+ const { className, ...rest } = props
+ return
+ },
+)
diff --git a/src/components/wrappers/Input.tsx b/src/components/wrappers/Input.tsx
new file mode 100644
index 00000000..853bf70d
--- /dev/null
+++ b/src/components/wrappers/Input.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react'
+import { HTMLInputTypeAttribute } from 'react'
+
+import { tv } from 'tailwind-variants'
+
+import { addTwModifier } from '@/lib/tailwind/helpers'
+
+interface Props extends React.ComponentPropsWithRef<'input'> {}
+
+export const inputBoxStyle = tv({
+ base: 'tw-rounded-md tw-bg-gray-200 tw-p-1 dark:tw-bg-gray-700',
+})
+
+const style = tv({
+ variants: {
+ type: {
+ text: inputBoxStyle(),
+ number: inputBoxStyle(),
+ submit: [
+ 'tw-p-1 tw-px-2.5 tw-py-1.5 tw-text-white',
+ 'tw-cursor-pointer tw-rounded-md tw-bg-trpfrog-500 tw-align-top',
+ ].join(' '),
+ file: addTwModifier('file:', [
+ 'tw-rounded-full tw-border-none tw-bg-trpfrog-200 tw-px-3',
+ 'tw-text-black hover:tw-bg-trpfrog-400 active:tw-bg-trpfrog-500',
+ ]),
+ checkbox: 'tw-accent-trpfrog-200',
+ } satisfies Partial>,
+ },
+})
+
+function hasVariant(
+ type: string | undefined,
+): type is keyof typeof style.variants.type {
+ return type !== undefined && type in style.variants.type
+}
+
+export const Input = React.forwardRef(
+ function Input(props, ref) {
+ const { className, ...rest } = props
+ const type = hasVariant(props.type) ? props.type : undefined
+
+ return
+ },
+)
diff --git a/src/components/wrappers/Kbd.tsx b/src/components/wrappers/Kbd.tsx
new file mode 100644
index 00000000..f1347e24
--- /dev/null
+++ b/src/components/wrappers/Kbd.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react'
+
+import { tv } from 'tailwind-variants'
+
+interface Props extends React.ComponentPropsWithRef<'kbd'> {}
+
+const style = tv({
+ base: [
+ 'tw-inline-block tw-min-w-6 tw-px-1 tw-align-text-top',
+ 'tw-rounded-md tw-border tw-border-gray-600 tw-bg-gray-800 tw-drop-shadow-sm',
+ 'tw-text-center tw-font-mono tw-text-[0.7em] tw-text-white',
+ ],
+})
+
+export const Kbd = React.forwardRef(
+ function Kbd(props, ref) {
+ const { className, ...rest } = props
+ return
+ },
+)
diff --git a/src/components/wrappers/Table.tsx b/src/components/wrappers/Table.tsx
new file mode 100644
index 00000000..b892b8f4
--- /dev/null
+++ b/src/components/wrappers/Table.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react'
+
+import { tv } from 'tailwind-variants'
+
+interface Props extends React.ComponentPropsWithRef<'table'> {}
+
+const style = tv({
+ base: [
+ // border-radius (角丸にするために clip する)
+ 'tw-overflow-clip tw-rounded-lg',
+ // border
+ 'tw-border-2', // table に border を指定すると内側の border が打ち消される?
+ '[&_th]:tw-border-2 [&_th]:tw-border-window-color',
+ '[&_td]:tw-border-2 [&_td]:tw-border-window-color',
+
+ '[&_th]:tw-px-2 [&_th]:tw-py-1 [&_th]:tw-font-bold',
+ '[&_td]:tw-px-2 [&_td]:tw-py-1',
+ '[&_td]:tw-bg-trpfrog-50 [&_th]:tw-bg-trpfrog-200',
+ ],
+})
+
+export const Table = React.forwardRef(
+ function Table(props, ref) {
+ const { className, ...rest } = props
+
+ return
+ },
+)
diff --git a/src/components/wrappers/headings.tsx b/src/components/wrappers/headings.tsx
new file mode 100644
index 00000000..5a492372
--- /dev/null
+++ b/src/components/wrappers/headings.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react'
+
+import { tv } from 'tailwind-variants'
+
+interface Props extends React.ComponentPropsWithRef<'h3'> {}
+
+const style = tv({
+ base: [
+ // layout
+ 'tw-mb-4 tw-mt-6 tw-block tw-w-fit tw-px-2',
+ // background
+ 'tw-bg-gradient-to-r tw-to-transparent',
+ 'tw-from-trpfrog-25 dark:tw-from-trpfrog-800',
+ // text
+ 'tw-font-bold tw-leading-relaxed',
+ ],
+ variants: {
+ type: {
+ h3: 'tw-border-l-[6px] tw-border-l-trpfrog-500 tw-text-xl dark:tw-border-l-trpfrog-400',
+ h4: 'tw-border-l-[4px] tw-border-l-trpfrog-300 tw-text-lg dark:tw-border-l-trpfrog-600',
+ h5: 'tw-border-l-[2px] tw-border-l-trpfrog-200 tw-text-base dark:tw-border-l-trpfrog-700',
+ },
+ },
+})
+
+export const H3 = React.forwardRef(
+ function H3(props, ref) {
+ const { className, ...rest } = props
+ return (
+
+ )
+ },
+)
+
+export const H4 = React.forwardRef(
+ function H4(props, ref) {
+ const { className, ...rest } = props
+ return (
+
+ )
+ },
+)
+
+export const H5 = React.forwardRef(
+ function H5(props, ref) {
+ const { className, ...rest } = props
+ return (
+
+ )
+ },
+)
diff --git a/src/components/wrappers/index.ts b/src/components/wrappers/index.ts
new file mode 100644
index 00000000..75e72e08
--- /dev/null
+++ b/src/components/wrappers/index.ts
@@ -0,0 +1,8 @@
+export { H2 } from './H2'
+export { H3, H4, H5 } from './headings'
+export { HorizontalRule } from './HorizontalRule'
+export { A } from './A'
+export { Kbd } from './Kbd'
+export { Input } from './Input'
+export { UnorderedList, OrderedList, Li } from './list'
+export { Table } from './Table'
diff --git a/src/components/wrappers/list.tsx b/src/components/wrappers/list.tsx
new file mode 100644
index 00000000..89b2867e
--- /dev/null
+++ b/src/components/wrappers/list.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react'
+
+import { tv } from 'tailwind-variants'
+
+const listStyles = tv({
+ base: [
+ 'tw-pl-7',
+ 'marker:[&]:tw-text-trpfrog-600 dark:marker:[&]:tw-text-trpfrog-100',
+ 'marker:[&_&]:tw-text-trpfrog-500 dark:marker:[&_&]:tw-text-trpfrog-300',
+ 'marker:[&_&_&]:tw-text-trpfrog-300 dark:marker:[&_&_&]:tw-text-trpfrog-500',
+ 'marker:[&_&_&_&]:tw-text-trpfrog-200 dark:marker:[&_&_&_&]:tw-text-trpfrog-600',
+ ],
+ variants: {
+ type: {
+ ul: 'tw-list-outside tw-list-disc',
+ ol: 'tw-list-outside tw-list-decimal',
+ },
+ },
+})
+
+const itemStyle = tv({
+ base: 'tw-my-1',
+})
+
+export const UnorderedList = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<'ul'>
+>(function UnorderedList(props, ref) {
+ const { className, ...rest } = props
+ return (
+
+ )
+})
+
+export const OrderedList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentProps<'ol'>
+>(function OrderedList(props, ref) {
+ const { className, ...rest } = props
+ return (
+
+ )
+})
+
+export const Li = React.forwardRef>(
+ function ListItem(props, ref) {
+ const { className, ...rest } = props
+ return
+ },
+)
diff --git a/src/hooks/useTooltip.tsx b/src/hooks/useTooltip.tsx
new file mode 100644
index 00000000..7eefcf13
--- /dev/null
+++ b/src/hooks/useTooltip.tsx
@@ -0,0 +1,14 @@
+import { ComponentProps, useId } from 'react'
+
+import { TooltipWrapper } from '@/components/utils/TooltipWrapper'
+
+export function useTooltip() {
+ const id = useId()
+ const TooltipButton = (props: ComponentProps<'div'>) => (
+
+ )
+ const TooltipContent = (
+ props: Omit, 'id'>,
+ ) =>
+ return { TooltipButton, TooltipContent }
+}
diff --git a/src/lib/googleFonts.ts b/src/lib/googleFonts.ts
index 0311bcac..97c0b5b2 100644
--- a/src/lib/googleFonts.ts
+++ b/src/lib/googleFonts.ts
@@ -1,12 +1,10 @@
import { NextFontWithVariable } from 'next/dist/compiled/@next/font'
import {
- BIZ_UDPGothic,
Comfortaa,
Inconsolata,
M_PLUS_Rounded_1c,
- Noto_Sans_JP,
Noto_Sans_Mono,
- Roboto,
+ Inter,
} from 'next/font/google'
const mPlusRounded1c = M_PLUS_Rounded_1c({
@@ -25,16 +23,10 @@ const comfortaa = Comfortaa({
variable: '--font-comfortaa',
})
-const roboto = Roboto({
+const inter = Inter({
subsets: ['latin'],
weight: ['400', '700'],
- variable: '--font-roboto',
-})
-
-const notoSansJP = Noto_Sans_JP({
- subsets: ['latin'],
- weight: ['400', '700'],
- variable: '--font-noto-sans-jp',
+ variable: '--font-inter',
})
const notoSansMono = Noto_Sans_Mono({
@@ -43,12 +35,6 @@ const notoSansMono = Noto_Sans_Mono({
variable: '--font-noto-sans-mono',
})
-const bizUdpGothic = BIZ_UDPGothic({
- subsets: ['latin'],
- weight: ['400', '700'],
- variable: '--font-biz-udp-gothic',
-})
-
const inconsolata = Inconsolata({
subsets: ['latin'],
weight: ['200'],
@@ -59,10 +45,8 @@ const inconsolata = Inconsolata({
const fonts: NextFontWithVariable[] = [
mPlusRounded1c,
comfortaa,
- roboto,
- notoSansJP,
+ inter,
notoSansMono,
- bizUdpGothic,
inconsolata,
]
diff --git a/src/lib/isInternalLink.ts b/src/lib/isInternalLink.ts
new file mode 100644
index 00000000..642b1590
--- /dev/null
+++ b/src/lib/isInternalLink.ts
@@ -0,0 +1,7 @@
+import { DEVELOPMENT_HOST, PRODUCTION_HOST } from '@/lib/constants'
+
+export function isInternalLink(href: string): boolean {
+ return ['/', '#', 'mailto:', PRODUCTION_HOST, DEVELOPMENT_HOST].some(prefix =>
+ href.startsWith(prefix),
+ )
+}
diff --git a/src/lib/mdLoader.ts b/src/lib/mdLoader.ts
index b2c8fc6a..64f7acf6 100644
--- a/src/lib/mdLoader.ts
+++ b/src/lib/mdLoader.ts
@@ -2,19 +2,17 @@ import fs from 'fs/promises'
import path from 'path'
import matter from 'gray-matter'
+import { z } from 'zod'
-import { sortWithDates } from './utils'
-
-type DateObject = { date: `${number}/${number}/${number}` }
-
-type MarkdownWithFrontmatter = {
+export type MarkdownWithFrontmatter = {
filename: string
metadata: T
content: string
}
-export async function readMarkdowns(
+export async function readMarkdowns(
dirpath: string,
+ schema: z.Schema,
): Promise[]> {
const files = await fs.readdir(dirpath)
const markdowns = files.filter(file => {
@@ -26,12 +24,12 @@ export async function readMarkdowns(
const matterResult = matter(file)
return {
filename,
- metadata: matterResult.data,
+ metadata: schema.parse(matterResult.data),
content: matterResult.content,
- } as MarkdownWithFrontmatter
+ } as const
}),
)
return markdownsWithFrontmatter.sort((a, b) =>
- sortWithDates(a.metadata.date, b.metadata.date),
+ a.metadata.date < b.metadata.date ? 1 : -1,
)
}
diff --git a/src/lib/tailwind/helpers.ts b/src/lib/tailwind/helpers.ts
new file mode 100644
index 00000000..c11c431c
--- /dev/null
+++ b/src/lib/tailwind/helpers.ts
@@ -0,0 +1,18 @@
+export function addTwModifier(
+ modifier: `${string}:`,
+ className: string | string[],
+): string {
+ if (Array.isArray(className)) {
+ className = className.join(' ')
+ }
+ return className
+ .trim()
+ .split(' ')
+ .filter(Boolean)
+ .map(c => {
+ const mods = c.split(':')
+ mods.splice(-1, 0, modifier.slice(0, -1))
+ return mods.join(':')
+ })
+ .join(' ')
+}
diff --git a/src/lib/tailwind/merge.ts b/src/lib/tailwind/merge.ts
new file mode 100644
index 00000000..3ac6f8ff
--- /dev/null
+++ b/src/lib/tailwind/merge.ts
@@ -0,0 +1,8 @@
+// eslint-disable-next-line
+import { extendTailwindMerge } from 'tailwind-merge'
+
+export const twMergeConfig: Parameters[0] = {
+ prefix: 'tw-',
+}
+
+export const twMerge = extendTailwindMerge(twMergeConfig)
diff --git a/src/lib/tailwind/variants.ts b/src/lib/tailwind/variants.ts
new file mode 100644
index 00000000..96db44ab
--- /dev/null
+++ b/src/lib/tailwind/variants.ts
@@ -0,0 +1,7 @@
+// eslint-disable-next-line
+import { createTV } from 'tailwind-variants'
+import { twMergeConfig } from '@/lib/tailwind/merge'
+
+export const tv = createTV({
+ twMergeConfig,
+})
diff --git a/src/lib/types.ts b/src/lib/types.ts
index cbb8897f..acc7f7ec 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,5 +1,4 @@
import { MDXComponents } from 'mdx/types'
-import ReactMarkdown from 'react-markdown'
type InnerCamelToKebabCase = S extends `${infer T}${infer U}`
? `${T extends Capitalize
@@ -13,18 +12,10 @@ export type CamelToKebabCase = InnerCamelToKebabCase<
export type PropertyIntersect = Pick>
-type ReactMarkdownComponent = Parameters[0]['components']
-
-export type IsomorphicMarkdownComponent = {
- [K in keyof PropertyIntersect<
- MDXComponents,
- ReactMarkdownComponent
- >]: MDXComponents[K] extends ReactMarkdownComponent[K]
- ? ReactMarkdownComponent[K] extends MDXComponents[K]
- ? ReactMarkdownComponent[K]
- : never
- : never
-}
+/**
+ * @deprecated Use `MDXComponents` from `mdx/types` instead
+ */
+export type IsomorphicMarkdownComponent = MDXComponents
export type SelectedRequired = Required> &
Omit
diff --git a/src/lib/wordSplit/index.tsx b/src/lib/wordSplit/index.tsx
index 188f1138..26932bea 100644
--- a/src/lib/wordSplit/index.tsx
+++ b/src/lib/wordSplit/index.tsx
@@ -1,6 +1,6 @@
'use client'
-import { createContext, useContext } from 'react'
+import { createContext, useContext, useLayoutEffect, useState } from 'react'
import dynamic from 'next/dynamic'
@@ -25,7 +25,15 @@ const SSRSafeParseWithBudouX = dynamic(
// due to this is not compatible with SSR, we need to use dynamic import
export function ParseWithBudouX(props: ParseWithBudouXProps) {
- return (
+ const [supportsWordBreakAutoPhrase, setSupportsWordBreakAutoPhrase] =
+ useState(false)
+ useLayoutEffect(() => {
+ setSupportsWordBreakAutoPhrase(CSS.supports('word-break', 'auto-phrase'))
+ }, [])
+ return supportsWordBreakAutoPhrase ? (
+ // @ts-ignore
+ {props.str}
+ ) : (
diff --git a/src/posts/birthday-21.md b/src/posts/birthday-21.md
index 2a010bd3..8665acc5 100644
--- a/src/posts/birthday-21.md
+++ b/src/posts/birthday-21.md
@@ -93,10 +93,9 @@ thumbnail: https://res.cloudinary.com/trpfrog/image/upload/blog/birthday-21/oni.
お金は大切に使って下さい。
-
-
-
-
+```youtube
+brevdPjOiCA
+```
## グリコ 宅飲みスナックアソートセット
diff --git a/src/posts/bocchi-the-cycling.md b/src/posts/bocchi-the-cycling.md
index 4fa57cca..23c63d41 100644
--- a/src/posts/bocchi-the-cycling.md
+++ b/src/posts/bocchi-the-cycling.md
@@ -154,8 +154,8 @@ if (minutesLeft >= 0) {
```centering
-**この記事は「[散歩・徒歩・苦行 Advent Calendar 2023](https://adventar.org/calendars/8696)」
-25 日目の記事です。**
+**この記事は「[散歩・徒歩・苦行 Advent Calendar 2023](https://adventar.org/calendars/8696)」
+25 日目の記事です。**
```
```link-embed
diff --git a/src/posts/code-with-ai.md b/src/posts/code-with-ai.md
index eeef797c..dd60e8fb 100644
--- a/src/posts/code-with-ai.md
+++ b/src/posts/code-with-ai.md
@@ -7,8 +7,8 @@ thumbnail: https://res.cloudinary.com/trpfrog/image/upload/v1671174054/blog/code
---
```centering
-**この記事は「[N予備校プログラミングコース Advent Calendar 2022](https://qiita.com/advent-calendar/2022/nyobi)」
-17 日目の記事です。**
+**この記事は「[N予備校プログラミングコース Advent Calendar 2022](https://qiita.com/advent-calendar/2022/nyobi)」
+17 日目の記事です。**
```
```link-embed
@@ -31,12 +31,8 @@ https://qiita.com/advent-calendar/2022/nyobi
面倒だから登録したくないぜ……!という方も Twitter でバズっている ChatGPT 関連のツイートを見てみてください。
きっと**驚くと思います**。
-
-
-[ChatGPT を使ってみる](https://chat.openai.com/)
-[Twitter で ChatGPT について調べてみる](https://twitter.com/search?q=ChatGPT%20min_retweets%3A100%20lang%3Aja)
-
-
+- [ChatGPT を使ってみる](https://chat.openai.com/)
+- [Twitter で ChatGPT について調べてみる](https://twitter.com/search?q=ChatGPT%20min_retweets%3A100%20lang%3Aja)
@@ -172,11 +168,7 @@ console.log(result); // ["3", "4", "5"]
![](/blog/code-with-ai/tabnine?w=2704&h=1574)
-
-
-[Tabnine (公式サイト)](https://www.tabnine.com/)
-
-
+- [Tabnine (公式サイト)](https://www.tabnine.com/)
Tabnine 社の AI コーディング支援ツールです。独自でトレーニングしたモデルを使っているみたいです。
月額 **$12** ですが、無料版も存在します。(が、機械学習モデルがちょっと古いらしい?)
@@ -194,11 +186,8 @@ Tabnine 社の AI コーディング支援ツールです。独自でトレー
![](/blog/code-with-ai/copilot?w=2410&h=1900)
-
-
-[GitHub Copilot (公式サイト)](https://github.com/features/copilot)
-
+- [GitHub Copilot (公式サイト)](https://github.com/features/copilot)
GitHub 社の AI コーディング支援ツールです。OpenAI Codex (OpenAI GPT-3 をコード生成向けに改良したもの) を使っているみたいです。
Tabnine 同様にエディタにプラグインとしてインストールして使います。
@@ -218,12 +207,9 @@ GitHub Copilot はエディタにそのまま介入してきて入力候補を
![](/blog/code-with-ai/aiprog?w=2994&h=1364)
-
-[AI Programmer](https://aiprogrammer.hashlab.jp/)
-[インタビュー記事 (ITmedia)](https://www.itmedia.co.jp/news/articles/2210/14/news053.html)
-
-
+- [AI Programmer](https://aiprogrammer.hashlab.jp/)
+- [インタビュー記事 (ITmedia)](https://www.itmedia.co.jp/news/articles/2210/14/news053.html)
こちらは Tabnine や GitHub Copilot とは異なって Web アプリケーションです。
@@ -241,12 +227,8 @@ GitHub Copilot はエディタにそのまま介入してきて入力候補を
![](/blog/code-with-ai/chatgpt?w=2368&h=1600)
-
-
-[ChatGPT](https://chat.openai.com/)
-[公式の紹介記事](https://openai.com/blog/chatgpt/)
-
-
+- [ChatGPT](https://chat.openai.com/)
+- [公式の紹介記事](https://openai.com/blog/chatgpt/)
OpenAI が先日公開したばかりのチャットボットです。OpenAI はイーロン・マスクを始めとする IT の強い人たちが出資して作った研究組織です ([ソース](https://japan.zdnet.com/article/35074857/))。
@@ -322,9 +304,9 @@ Deep Learning というのは人間の脳を模倣した計算モデルである
-「AI!」「人間の脳を模倣している!」と聞くとめちゃくちゃ強そうですが、
-完全なる謎技術なんかではなく、
-**ちゃんと人間に理解できる計算の繰り返し**でできているんですよ!
+「AI!」「人間の脳を模倣している!」と聞くとめちゃくちゃ強そうですが、
+完全なる謎技術なんかではなく、
+**ちゃんと人間に理解できる計算の繰り返し**でできているんですよ!
@@ -424,11 +406,6 @@ AI について「シンギュラリティが来る!」とか「エンジニ
-````centering
-```dangerously-set-inner-html
-アドベントカレンダーに戻る
-```
-````
```next-page
付録: AI はどのように文章を生成するか?
```
@@ -570,8 +547,8 @@ function selectNextWord(text) {
分かち書きの例
```centering
-今日は良い天気です。
-→ 今日 / は / 良い / 天気 / です / 。
+今日は良い天気です。
+→ 今日 / は / 良い / 天気 / です / 。
```
````
@@ -581,17 +558,17 @@ function selectNextWord(text) {
トークナイザの辞書
````
-|index|単語|備考|
-|----|-----|----|
-| 0
| [BOS] | 文章の開始 |
-| 1
| [EOS] | 文章の終了 |
-| 2
| [UNK] | 未知の単語 |
-| 3
| 。 |
-| 4
| 良い |
-| 5
| 天気 |
-| 6
| は |
-| 7
| 今日 |
-| 8
| です |
+| index |単語|備考|
+|-------------------------------------|-----|----|
+| 0 | [BOS] | 文章の開始 |
+| 1 | [EOS] | 文章の終了 |
+| 2 | [UNK] | 未知の単語 |
+| 3 | 。 |
+| 4 | 良い |
+| 5 | 天気 |
+| 6 | は |
+| 7 | 今日 |
+| 8 | です |
この場合、トークナイザは
@@ -956,12 +933,4 @@ softmax は
実際にはもっと複雑なアーキテクチャを使っていたり、もっと複雑なサンプリングをしていたり、もっと複雑な微調整をしていたりします。
興味を持った方はぜひ本編で紹介した本や講座などを覗いてみてください!
-````centering
-```dangerously-set-inner-html
-
-前のページに戻る
-アドベントカレンダーに戻る
-
-```
-````
diff --git a/src/posts/half-line.md b/src/posts/half-line.md
index 4f4c82f5..1066ed5a 100644
--- a/src/posts/half-line.md
+++ b/src/posts/half-line.md
@@ -36,13 +36,9 @@ thumbnail: https://res.cloudinary.com/trpfrog/image/upload/v1642314369/blog/half
$xy$ 平面上に点 $A$ から点 $B$ 方向に伸びる半直線 $L$ と、点 $P$ があります。
このとき、点 $P$ から最も近い $L$ 上の点 $Q$ を求めてください。
-
-
-
-
+
+![](/blog/half-line/thumbnail.png)
+
@@ -188,9 +184,7 @@ $$
$xy$ 平面上に点 $A$ から点 $B$ 方向に伸びる半直線 $L$ と、点 $P$ があります。
このとき、点 $P$ から最も近い $L$ 上の点 $Q$ を求めてください。
-
-
-
+![](/blog/half-line/thumbnail.png)
diff --git a/src/posts/skyscraper-walk.md b/src/posts/skyscraper-walk.md
index 14d9b6c4..81c915eb 100644
--- a/src/posts/skyscraper-walk.md
+++ b/src/posts/skyscraper-walk.md
@@ -2011,11 +2011,3 @@ Eys-lWXFnmo
明日 (17日目) の記事はあずきバーさんの「日本語をうまく話したい」です。難しいですからね
-
-````centering
-```dangerously-set-inner-html
-
アドベントカレンダーに戻る
-```
-````
-
-
diff --git a/src/posts/timetable-page.md b/src/posts/timetable-page.md
index 1c2d2609..bc149115 100644
--- a/src/posts/timetable-page.md
+++ b/src/posts/timetable-page.md
@@ -11,7 +11,8 @@ thumbnail: https://res.cloudinary.com/trpfrog/image/upload/v1641545315/blog/time
![thumbnail](/blog/timetable-page/thumbnail?w=1343&h=1059)
-
GitHubで見る
+- [GitHubで見る](https://github.com/TrpFrog/timetable-page)
+
## これは何?
@@ -68,4 +69,4 @@ thumbnail: https://res.cloudinary.com/trpfrog/image/upload/v1641545315/blog/time
ぜひ使ってみてね!さようなら
-
GitHubで見る
+- [GitHubで見る](https://github.com/TrpFrog/timetable-page)
diff --git a/src/posts/twitter-screen.md b/src/posts/twitter-screen.md
index b24fffc8..95d14d8b 100644
--- a/src/posts/twitter-screen.md
+++ b/src/posts/twitter-screen.md
@@ -20,7 +20,7 @@ thumbnail: https://res.cloudinary.com/trpfrog/image/upload/v1641983536/blog/twit
車輪の再発明はしたくありません。とりあえずググってみてもヒットしなかったので作ることにしました。完成品はこちらになります!
-
View Twitter Screen on GitHub
+- [GitHub で見る](https://github.com/TrpFrog/TwitterScreen)
背景を透明化できるので、Twitterのコメントを流しながら配信イベントを見たり
diff --git a/src/states/shouldFollowHeaderAtom.ts b/src/states/shouldFollowHeaderAtom.ts
index 13fbb217..6c989784 100644
--- a/src/states/shouldFollowHeaderAtom.ts
+++ b/src/states/shouldFollowHeaderAtom.ts
@@ -3,6 +3,6 @@ import { atomWithStorage } from 'jotai/utils'
const shouldFollowHeaderAtom = atomWithStorage('shouldFollowHeader', true)
-export function useShouldFollowHeaderAtom() {
+export function useUserSettingStickyHeader() {
return useAtom(shouldFollowHeaderAtom)
}
diff --git a/src/states/shouldHideHeaderAtom.ts b/src/states/shouldHideHeaderAtom.ts
index 4cd8584f..61704702 100644
--- a/src/states/shouldHideHeaderAtom.ts
+++ b/src/states/shouldHideHeaderAtom.ts
@@ -1,8 +1,8 @@
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
-const shouldHideHeaderAtom = atomWithStorage('shouldHideHeader', true)
+const shouldHideHeaderAtom = atomWithStorage('alwaysStickHeader', false)
-export function useShouldHideHeaderAtom() {
+export function useUserSettingAlwaysVisibleHeader() {
return useAtom(shouldHideHeaderAtom)
}
diff --git a/src/styles/colors.scss b/src/styles/colors.scss
deleted file mode 100644
index d418a6e6..00000000
--- a/src/styles/colors.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-@use './mixins';
-
-:root {
- --link-button-color: #77bd36;
- --link-button-color-bottom: #558d21;
- --link-button-color-highlighted: #90e200;
- --link-button-font-color: white;
-}
-
-@include mixins.mq(dark) {
- :root {
- --link-button-color: #558d21;
- --link-button-color-bottom: #406919;
- --link-button-color-highlighted: #7fc900;
- }
-}
diff --git a/src/styles/common/block.scss b/src/styles/common/block.scss
deleted file mode 100644
index 5b6b7fcf..00000000
--- a/src/styles/common/block.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-@use '../mixins';
-
-:root {
- --window-padding-top-bottom: 40px;
- --window-padding-left-right: 40px;
- --window-bottom-color: #9cc535;
- --window-border-radius: 33px;
-}
-
-@include mixins.mq(sp) {
- :root {
- --window-padding-top-bottom: 18px;
- --window-padding-left-right: 12px;
- --window-border-radius: 10px;
- }
-}
-
-@include mixins.mq(dark) {
- :root {
- --window-bottom-color: #35420e;
- }
-}
diff --git a/src/styles/common/grid.scss b/src/styles/common/grid.scss
deleted file mode 100644
index 6a056e8b..00000000
--- a/src/styles/common/grid.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-@use '../mixins';
-
-:root {
- --header-height: 4rem;
- --navigation-height: 3em;
- --footer-height: 3em;
- --main-margin: 2em;
- --window-bkg-color: white;
-}
-
-@include mixins.mq(sp) {
- :root {
- --header-height: 3rem;
- --navigation-height: 0px;
- --footer-height: 3em;
- --main-margin: 8px;
- }
-}
-@include mixins.mq(dark) {
- :root {
- --window-bkg-color: #202020;
- }
-}
-
-@include mixins.mq(dark) {
- .grid {
- background: #0c2400;
- }
-}
diff --git a/src/styles/common/header.scss b/src/styles/common/header.scss
deleted file mode 100644
index 4f6ba4bb..00000000
--- a/src/styles/common/header.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-@use '../mixins';
-
-:root {
- --header-color: #66a928;
- --header-filter: 0 0.2em 0 #5a9623;
- --header-icon-size: 70px;
-
- @include mixins.mq(sp) {
- --header-icon-size: 58px;
- }
-
- @include mixins.mq(dark) {
- --header-color: #4f831f;
- --header-filter: 0 0.2em 0 rgb(49, 80, 15);
- }
-}
diff --git a/src/styles/common/linkbutton.scss b/src/styles/common/linkbutton.scss
deleted file mode 100644
index 9fb932d1..00000000
--- a/src/styles/common/linkbutton.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-@use '../../components/atoms/Button/index.module.scss' as button;
-
-.linkButton {
- @extend .button;
-}
-
-.link-area {
- a {
- @extend .linkButton;
- margin-right: 6px;
- margin-bottom: 8px;
- }
-}
diff --git a/src/styles/common/media.scss b/src/styles/common/media.scss
deleted file mode 100644
index 57345c64..00000000
--- a/src/styles/common/media.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.youtube-outer {
- position: relative;
- width: 100%;
- padding-bottom: 56.25%;
- height: 0;
- overflow: hidden;
- border-radius: 10px;
-}
-
-.youtube-iframe {
- position: absolute;
- top: 0;
- right: 0;
- width: 100%;
- height: 100%;
-}
-
-.rich_image {
- border-radius: 5px;
- background: #90e20055;
-}
diff --git a/src/styles/common/print.scss b/src/styles/common/print.scss
deleted file mode 100644
index 46d9993e..00000000
--- a/src/styles/common/print.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-@media print {
- :root {
- --body-background: none !important;
- --window-bottom-color: none !important;
- --header-filter: none !important;
- }
-
- body {
- color: black;
- zoom: 0.7;
- }
-
- a {
- color: black !important;
- }
-
- #title h1 {
- font-size: 2.5em;
- }
-
- h2 {
- color: black !important;
- }
-
- header {
- position: absolute;
- background: none;
- backdrop-filter: none;
-
- h1 > a {
- color: black !important;
- }
-
- margin: 0;
- z-index: 0;
- }
-
- nav {
- display: none;
- }
-
- footer {
- background: none;
- #copyright {
- color: black !important;
- }
- }
-
- p {
- page-break-inside: avoid;
- }
-
- img {
- page-break-before: auto;
- page-break-after: auto;
- page-break-inside: avoid;
- position: relative;
- }
-}
diff --git a/src/styles/globals.scss b/src/styles/globals.scss
deleted file mode 100644
index 839c8b46..00000000
--- a/src/styles/globals.scss
+++ /dev/null
@@ -1,245 +0,0 @@
-@use '@/styles/common/grid';
-@use '@/styles/common/header';
-@use '@/styles/common/block';
-@use '@/styles/common/media';
-@use '@/styles/common/linkbutton';
-@use '@/styles/mixins';
-@use 'colors';
-@use '@/styles/common/print';
-@use 'table';
-
-:root {
- --h3-marker-color: #90e200;
- --body-background: rgb(192, 225, 121);
- --base-font-color: #151515;
-
- --trpfrog-animation-start-degree: 0rad;
- --anim-height: min(100vw, 400px);
- --trpfrog-icon-size: min(400px, 90vw);
-
- --main-font: -apple-system, BlinkMacSystemFont, Noto Sans JP, Segoe UI, Roboto,
- Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
-
- --footer-color: #163600;
-}
-
-@include mixins.mq(dark) {
- :root {
- --h3-marker-color: #4e8018;
- --body-background: #3d4d22;
- --base-font-color: #e5e5e5;
- }
-}
-
-html,
-body {
- margin: 0;
- padding: 0;
- font-family: var(--main-font);
- scroll-behavior: smooth;
- background: var(--body-background);
- color: var(--base-font-color);
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- & > main {
- display: block;
- flex: 1;
- }
-}
-
-a:link {
- color: forestgreen;
-}
-
-a:visited {
- color: olive;
-}
-
-@include mixins.mq(dark) {
- a:link:not(.linkButton) {
- color: aquamarine;
- }
-
- a:visited {
- color: plum;
- }
-}
-
-h2 {
- font-family: var(--font-m-plus-rounded-1c);
- font-weight: 800;
- color: #172c00;
- line-height: 1.2;
-
- @include mixins.mq(dark) {
- color: #f1ffd0;
- }
-}
-
-h3 {
- display: block;
- width: fit-content;
- padding: 0 7px 0 6px;
- border-left: solid 7px var(--header-color);
- background: rgba(120, 189, 0, 0.09);
- @include mixins.mq(dark) {
- background: var(--body-background);
- }
- border-radius: 1px 5px 5px 1px;
-}
-
-h4 {
- display: block;
- width: fit-content;
- padding: 0 7px 0 6px;
- border-left: solid 3px var(--header-color);
- background: rgba(120, 189, 0, 0.09);
- @include mixins.mq(dark) {
- background: var(--body-background);
- }
- border-radius: 1px 5px 5px 1px;
-}
-
-@media (prefers-color-scheme: dark) {
- .only-on-light-mode {
- display: none;
- }
-}
-
-@media (prefers-color-scheme: light) {
- .only-on-dark-mode {
- display: none;
- }
-}
-
-@include mixins.mq(pc) {
- .only-on-sp {
- display: none;
- }
-}
-
-@include mixins.mq(sp) {
- .only-on-pc {
- display: none;
- }
-}
-
-.ib {
- display: inline-block;
-}
-
-@media screen {
- ol {
- counter-reset: num;
- padding-left: 1em;
-
- li {
- list-style-type: none;
- position: relative;
- padding-left: 2em;
- }
-
- li:before {
- content: counter(num);
- counter-increment: num;
-
- display: inline-block;
- border-radius: 50%;
- text-align: center;
-
- background: #3c7709;
- color: white;
- font-family: var(--font-m-plus-rounded-1c);
- font-weight: bold;
- font-size: 0.9em;
- line-height: 1.66;
-
- position: absolute;
- left: 0;
- width: 1.5em;
- height: 1.5em;
- top: 3px;
- }
- }
-
- ul {
- counter-reset: num;
- padding-left: 2em !important;
-
- li {
- list-style-type: none;
- position: relative;
- padding-left: 0.4em !important;
- }
-
- li:before {
- border-radius: 50%;
- width: 6px;
- height: 6px;
- position: absolute;
- left: -0.5em;
- top: 0.78em;
- content: '';
- background: #3c7709;
- }
- }
-
- ul ul li:before {
- background: #649d31;
- }
-
- ul ul ul li:before {
- background: #66d207;
- }
-}
-
-hr {
- border: none;
- border-top: 5px dotted var(--body-background);
- margin: 2rem 0;
-}
-
-*:focus {
- outline-color: #90e200;
-}
-
-main {
- line-height: 1.8;
- letter-spacing: 0.03em;
-}
-
-input[type='text'] {
- border-radius: 5px;
- border: none;
- background: #dddddd;
- padding: 5px;
- font-size: 1em;
- box-sizing: border-box;
-}
-
-input[type='submit'] {
- border-radius: 5px;
- border: none;
- padding: 5px 10px;
- font-size: 1em;
- box-sizing: border-box;
- background: #3c7709;
- color: white;
- cursor: pointer;
-}
-
-kbd {
- background: #3b3b3b;
- color: white;
- border-radius: 2px;
- padding: 2px 5px;
- font-size: 0.9em;
- vertical-align: middle;
- box-shadow: 0 0 1px 1px rgb(128 128 128 / 30%);
-}
-
-.good-break {
- overflow-wrap: anywhere;
- word-break: keep-all;
-}
diff --git a/src/styles/table.scss b/src/styles/table.scss
deleted file mode 100644
index 0afe0850..00000000
--- a/src/styles/table.scss
+++ /dev/null
@@ -1,64 +0,0 @@
-table {
- * {
- padding: 2px 10px;
- }
- margin: auto;
- border-radius: 5px;
-
- $radius: 8px;
-
- &:not(:has(*[data-table-top-left]))
- *:first-child
- > tr:first-child
- *:first-child {
- border-top-left-radius: $radius;
- }
- &:not(:has(*[data-table-top-right]))
- *:first-child
- > tr:first-child
- *:last-child {
- border-top-right-radius: $radius;
- }
- &:not(:has(*[data-table-bottom-left]))
- *:last-child
- > tr:last-child
- *:first-child {
- border-bottom-left-radius: $radius;
- }
- &:not(:has(*[data-table-bottom-right]))
- *:last-child
- > tr:last-child
- *:last-child {
- border-bottom-right-radius: $radius;
- }
-
- *[data-table-top-left] {
- border-top-left-radius: $radius;
- }
- *[data-table-top-right] {
- border-top-right-radius: $radius;
- }
- *[data-table-bottom-left] {
- border-bottom-left-radius: $radius;
- }
- *[data-table-bottom-right] {
- border-bottom-right-radius: $radius;
- }
-
- thead {
- text-align: center;
- }
-
- th {
- background: var(--header-color);
- color: white;
- }
- td {
- background: rgba(120, 189, 0, 0.3);
- }
-
- .ct {
- text-align: center;
- line-height: 1;
- }
-}
diff --git a/src/styles/variables.css b/src/styles/variables.css
new file mode 100644
index 00000000..8e1d9c35
--- /dev/null
+++ b/src/styles/variables.css
@@ -0,0 +1,84 @@
+:root {
+ --h3-marker-color: #90e200;
+ --body-background: rgb(192, 225, 121);
+ --base-font-color: #151515;
+
+ @media screen and (prefers-color-scheme: dark) {
+ --h3-marker-color: #4e8018;
+ --body-background: #3d4d22;
+ --base-font-color: #e5e5e5;
+ }
+
+ --trpfrog-animation-start-degree: 0rad;
+ --anim-height: min(100vw, 400px);
+ --trpfrog-icon-size: min(400px, 90vw);
+
+ --main-font: -apple-system, BlinkMacSystemFont, Noto Sans JP, Segoe UI, Roboto,
+ Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+
+ --footer-color: #163600;
+
+ /*--------------- from block.scss --------------*/
+
+ --window-padding-top-bottom: 40px;
+ --window-padding-left-right: 40px;
+ --window-bottom-color: #9cc535;
+ --window-border-radius: 33px;
+
+ @media screen and (max-width: 800px) {
+ --window-padding-top-bottom: 18px;
+ --window-padding-left-right: 12px;
+ --window-border-radius: 10px;
+ }
+
+ @media screen and (prefers-color-scheme: dark) {
+ --window-bottom-color: #35420e;
+ }
+
+ /*--------------- from grid.scss ---------------*/
+
+ --header-height: 4rem;
+ --navigation-height: 3em;
+ --footer-height: 3em;
+ --main-margin: 2em;
+ --window-bkg-color: white;
+
+ @media screen and (max-width: 800px) {
+ --header-height: 3rem;
+ --navigation-height: 0px;
+ --footer-height: 3em;
+ --main-margin: 8px;
+ }
+
+ @media screen and (prefers-color-scheme: dark) {
+ --window-bkg-color: #202020;
+ }
+
+ /*-------------- from colors.scss --------------*/
+
+ --link-button-color: #77bd36;
+ --link-button-color-bottom: #558d21;
+ --link-button-color-highlighted: #90e200;
+ --link-button-font-color: white;
+
+ @media screen and (prefers-color-scheme: dark) {
+ --link-button-color: #558d21;
+ --link-button-color-bottom: #406919;
+ --link-button-color-highlighted: #7fc900;
+ }
+
+ /*-------------- from header.scss --------------*/
+
+ --header-color: #66a928;
+ --header-filter: 0 0.2em 0 #5a9623;
+ --header-icon-size: 70px;
+
+ @media screen and (max-width: 800px) {
+ --header-icon-size: 58px;
+ }
+
+ @media screen and (prefers-color-scheme: dark) {
+ --header-color: #4f831f;
+ --header-filter: 0 0.2em 0 rgb(49, 80, 15);
+ }
+}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 00000000..696bf5dc
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,52 @@
+const { withTV } = require('tailwind-variants/transformer')
+const defaultTheme = require('tailwindcss/defaultTheme')
+
+/** @type {import('tailwindcss').Config} */
+module.exports = withTV({
+ content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
+ theme: {
+ extend: {
+ colors: {
+ trpfrog: {
+ 25: '#f5ffe6',
+ 50: '#e5f8c0',
+ 100: '#c7e876',
+ 200: '#b2d558',
+ 300: '#9ACC29',
+ 400: '#8DBA20',
+ 500: '#6EA101',
+ 600: '#617F17',
+ 700: '#456401',
+ 800: '#2A390A',
+ 900: '#32342A',
+ },
+ 'body-color': 'rgb(var(--color-body))',
+ 'text-color': 'rgb(var(--color-text))',
+ 'window-color': 'rgb(var(--color-window))',
+ 'header-color': 'rgb(var(--color-header))',
+ },
+ borderWidth: {
+ 1: '1px',
+ },
+ height: {
+ header: 'var(--header-height)',
+ },
+ },
+ screens: {
+ sp: { max: '799px' },
+ pc: { min: '800px' },
+ wide: { min: '1000px' },
+ },
+ fontFamily: {
+ 'mplus-rounded': [
+ 'var(--font-m-plus-rounded-1c)',
+ ...defaultTheme.fontFamily.sans,
+ ],
+ comfortaa: ['var(--font-comfortaa)', ...defaultTheme.fontFamily.sans],
+ mono: ['var(--font-noto-sans-mono)', ...defaultTheme.fontFamily.mono],
+ inter: ['var(--font-inter)', ...defaultTheme.fontFamily.sans],
+ },
+ },
+ plugins: [require('@tailwindcss/container-queries')],
+ prefix: 'tw-',
+})