├── public
└── fonts
│ ├── BioRhyme-Bold.ttf
│ ├── Caveat-Bold.ttf
│ ├── Rasa-SemiBold.otf
│ ├── Roboto-Bold.ttf
│ ├── FiraCode-Bold.woff2
│ ├── Inter-Regular.woff2
│ ├── Lato-Semibold.woff2
│ ├── Inter-SemiBold.woff2
│ ├── Merriweather-Bold.ttf
│ ├── OpenSans-SemiBold.ttf
│ ├── EBGaramond-SemiBold.ttf
│ ├── IBMPlexSans-SemiBold.woff2
│ ├── Raleway-v4020-SemiBold.otf
│ └── SourceSansPro-Semibold.otf.woff2
├── src
├── components
│ ├── code
│ │ ├── search
│ │ │ ├── base.ts
│ │ │ ├── server.ts
│ │ │ └── client.ts
│ │ ├── text
│ │ │ ├── label.tsx
│ │ │ ├── box-server.tsx
│ │ │ ├── input.tsx
│ │ │ ├── box-base.tsx
│ │ │ └── box-client.tsx
│ │ ├── desc
│ │ │ ├── fonts
│ │ │ │ ├── server.tsx
│ │ │ │ ├── client.tsx
│ │ │ │ └── base.tsx
│ │ │ ├── texts
│ │ │ │ ├── server.tsx
│ │ │ │ ├── client.tsx
│ │ │ │ └── base.tsx
│ │ │ ├── related.tsx
│ │ │ ├── usage.tsx
│ │ │ ├── list.tsx
│ │ │ ├── hrefs.tsx
│ │ │ └── box.tsx
│ │ ├── overview
│ │ │ ├── font
│ │ │ │ ├── server.tsx
│ │ │ │ ├── clien.tsx
│ │ │ │ └── base.tsx
│ │ │ ├── box.tsx
│ │ │ └── code.tsx
│ │ ├── source.tsx
│ │ └── box.tsx
│ ├── dropdown
│ │ ├── arrow.tsx
│ │ ├── server.tsx
│ │ ├── button.tsx
│ │ ├── menu.tsx
│ │ └── box.tsx
│ ├── side
│ │ ├── feature
│ │ │ ├── list.tsx
│ │ │ └── item.tsx
│ │ └── box.tsx
│ └── main
│ │ ├── box.tsx
│ │ └── header
│ │ ├── box.tsx
│ │ └── link.tsx
├── styles
│ ├── index.css
│ ├── global.css
│ └── fonts.css
├── fonts.ts
├── app
│ ├── layout.tsx
│ ├── page.tsx
│ ├── [code]
│ │ └── page.tsx
│ ├── icon.svg
│ └── about
│ │ └── page.tsx
└── features.ts
├── .vscode
└── settings.json
├── postcss.config.mjs
├── next-env.d.ts
├── .gitignore
├── tsconfig.json
├── package.json
├── README.md
├── tailwind.config.ts
└── pnpm-lock.yaml
/public/fonts/BioRhyme-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/BioRhyme-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/Caveat-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Caveat-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/Rasa-SemiBold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Rasa-SemiBold.otf
--------------------------------------------------------------------------------
/public/fonts/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/src/components/code/search/base.ts:
--------------------------------------------------------------------------------
1 | export type GetCodeSearch = (name: string, value: string) => string;
2 |
--------------------------------------------------------------------------------
/public/fonts/FiraCode-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/FiraCode-Bold.woff2
--------------------------------------------------------------------------------
/public/fonts/Inter-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Inter-Regular.woff2
--------------------------------------------------------------------------------
/public/fonts/Lato-Semibold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Lato-Semibold.woff2
--------------------------------------------------------------------------------
/public/fonts/Inter-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Inter-SemiBold.woff2
--------------------------------------------------------------------------------
/public/fonts/Merriweather-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Merriweather-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/OpenSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/OpenSans-SemiBold.ttf
--------------------------------------------------------------------------------
/public/fonts/EBGaramond-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/EBGaramond-SemiBold.ttf
--------------------------------------------------------------------------------
/public/fonts/IBMPlexSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/IBMPlexSans-SemiBold.woff2
--------------------------------------------------------------------------------
/public/fonts/Raleway-v4020-SemiBold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/Raleway-v4020-SemiBold.otf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Semibold.otf.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thien-do/otf/HEAD/public/fonts/SourceSansPro-Semibold.otf.woff2
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import "./fonts.css";
6 | @import "./global.css";
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "[typescript]": {
4 | "editor.defaultFormatter": "esbenp.prettier-vscode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/src/fonts.ts:
--------------------------------------------------------------------------------
1 | const fonts = [
2 | "BioRhyme",
3 | "Caveat",
4 | "EB Garamond",
5 | "Fira Code",
6 | "IBM Plex Sans",
7 | "Inter",
8 | "Lato",
9 | "Merriweather",
10 | "Open Sans",
11 | "Raleway",
12 | "Rasa",
13 | "Roboto",
14 | "Source Sans Pro",
15 | ];
16 |
17 | export default fonts;
18 |
--------------------------------------------------------------------------------
/src/components/dropdown/arrow.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 |
3 | export const DropdownArrow = (): ReactElement => {
4 | return (
5 |
6 |
7 | ▼
8 |
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/dropdown/server.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 | import { DropdownArrow } from "./arrow";
3 |
4 | export const DropdownServer = (props: { text: string }): ReactElement => {
5 | const { text } = props;
6 |
7 | return (
8 |
9 | {text}
10 |
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/code/text/label.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement, ReactNode } from "react";
2 |
3 | export const CodeTextLabel = (props: {
4 | id: string;
5 | children: ReactNode;
6 | }): ReactElement => {
7 | const { id, children } = props;
8 |
9 | return (
10 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/code/text/box-server.tsx:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { ReactElement } from "react";
3 | import { CodeTextBoxBase } from "./box-base";
4 |
5 | export function CodeTextBoxServer(props: { feature: Feature }): ReactElement {
6 | const { feature } = props;
7 |
8 | return (
9 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/code/search/server.ts:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { GetCodeSearch } from "./base";
3 |
4 | export const useCodeSearchServer = (props: {
5 | feature: Feature;
6 | }): GetCodeSearch => {
7 | const { feature } = props;
8 |
9 | const get: GetCodeSearch = (name, value) => {
10 | const next = new URLSearchParams();
11 | next.set(name, value);
12 | const href = `/${feature.code}?${next.toString()}`;
13 | return href;
14 | };
15 |
16 | return get;
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/side/feature/list.tsx:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { ReactElement } from "react";
3 | import { SideFeatureItem } from "./item";
4 |
5 | export const SideFeatureList = (props: {
6 | features: Feature[];
7 | }): ReactElement => {
8 | const { features } = props;
9 |
10 | return (
11 |
12 | {features.map((feature) => (
13 |
14 | ))}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/code/desc/fonts/server.tsx:
--------------------------------------------------------------------------------
1 | import { useCodeSearchServer } from "components/code/search/server";
2 | import { Feature } from "features";
3 | import { ReactElement } from "react";
4 | import { CodeDescFontsBase } from "./base";
5 |
6 | export const CodeDescFontsServer = (props: {
7 | feature: Feature;
8 | }): ReactElement => {
9 | const { feature } = props;
10 |
11 | return (
12 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/code/desc/texts/server.tsx:
--------------------------------------------------------------------------------
1 | import { useCodeSearchServer } from "components/code/search/server";
2 | import { Feature } from "features";
3 | import { ReactElement } from "react";
4 | import { CodeDescTextsBase } from "./base";
5 |
6 | export const CodeDescTextsServer = (props: {
7 | feature: Feature;
8 | }): ReactElement => {
9 | const { feature } = props;
10 |
11 | return (
12 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/src/components/code/desc/fonts/client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCodeSearchClient } from "components/code/search/client";
4 | import { Feature } from "features";
5 | import { ReactElement } from "react";
6 | import { CodeDescFontsBase } from "./base";
7 |
8 | export const CodeDescFontsClient = (props: {
9 | // Same API with CodeDescFontsServer
10 | feature: Feature;
11 | }): ReactElement => {
12 | const { feature } = props;
13 |
14 | return (
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/code/desc/texts/client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCodeSearchClient } from "components/code/search/client";
4 | import { Feature } from "features";
5 | import { ReactElement } from "react";
6 | import { CodeDescTextsBase } from "./base";
7 |
8 | export const CodeDescTextsClient = (props: {
9 | // Same API with CodeDescTextsServer
10 | feature: Feature;
11 | }): ReactElement => {
12 | const { feature } = props;
13 |
14 | return (
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/code/search/client.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname, useSearchParams } from "next/navigation";
4 | import { GetCodeSearch } from "./base";
5 |
6 | export const useCodeSearchClient = (): GetCodeSearch => {
7 | const prev = useSearchParams();
8 | const code = usePathname();
9 |
10 | const get: GetCodeSearch = (name, value) => {
11 | const next = new URLSearchParams(prev.toString());
12 | next.set(name, value);
13 | const href = `${code}?${next.toString()}`;
14 | return href;
15 | };
16 |
17 | return get;
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/main/box.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement, ReactNode } from "react";
2 | import { MainHeaderBox } from "./header/box";
3 |
4 | export function MainBox(props: { children: ReactNode }): ReactElement {
5 | const { children } = props;
6 |
7 | return (
8 |
14 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/code/overview/font/server.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCodeSearchServer } from "components/code/search/server";
4 | import { Feature } from "features";
5 | import { ReactElement } from "react";
6 | import { CodeOverviewFontBase } from "./base";
7 |
8 | export const CodeOverviewFontServer = (props: {
9 | feature: Feature;
10 | }): ReactElement => {
11 | const { feature } = props;
12 |
13 | return (
14 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/dropdown/button.tsx:
--------------------------------------------------------------------------------
1 | import { UseSelectReturnValue } from "downshift";
2 | import { ReactElement } from "react";
3 | import { DropdownArrow } from "./arrow";
4 |
5 | export const DropdownButton = (props: {
6 | select: UseSelectReturnValue;
7 | value: string;
8 | itemToString: (item: string) => string;
9 | }): ReactElement => {
10 | const { select, value, itemToString } = props;
11 |
12 | return (
13 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/code/overview/font/clien.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCodeSearchClient } from "components/code/search/client";
4 | import { Feature } from "features";
5 | import { useSearchParams } from "next/navigation";
6 | import { ReactElement } from "react";
7 | import { CodeOverviewFontBase } from "./base";
8 |
9 | export const CodeOverviewFontClient = (props: {
10 | feature: Feature;
11 | }): ReactElement => {
12 | const { feature } = props;
13 |
14 | return (
15 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/code/desc/related.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ReactElement } from "react";
3 | import { CodeDescList } from "./list";
4 |
5 | export const CodeDescRelated = (props: { codes: string[] }): ReactElement => {
6 | const { codes } = props;
7 |
8 | if (codes.length === 0) throw new Error("codes.length must be > 0");
9 |
10 | const links = codes.map((code) => (
11 |
12 | {code}
13 |
14 | ));
15 |
16 | const prefix = "You may also be interested in";
17 | return (
18 |
19 | {prefix} .
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/code/source.tsx:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { ReactElement } from "react";
3 |
4 | export function CodeSource(props: { feature: Feature }): ReactElement {
5 | const { feature } = props;
6 |
7 | const { property, value } = feature.css;
8 |
9 | return (
10 |
11 |
12 | To enable in css:
13 |
14 |
15 |
16 | {property}:
17 | {value};
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/code/text/input.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 |
3 | export function CodeTextInput(props: {
4 | id: string;
5 | font: string;
6 | text: string;
7 | setText: null | ((next: string) => void);
8 | }): ReactElement {
9 | const { id, text, setText, font } = props;
10 |
11 | return (
12 | void setText(event.target.value)
24 | }
25 | />
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/code/desc/usage.tsx:
--------------------------------------------------------------------------------
1 | import { marked } from "marked";
2 | import { ReactElement } from "react";
3 |
4 | import { Feature } from "features";
5 |
6 | const getName = (feature: Feature) => {
7 | const { family_code, family_name, name, code } = feature;
8 |
9 | return family_code && family_name
10 | ? `${family_name} (${family_code})`
11 | : `${name} (${code})`;
12 | };
13 |
14 | export const CodeDescUsage = (props: { feature: Feature }): ReactElement => {
15 | const { feature } = props;
16 |
17 | const raw = `**${getName(feature)}** ${feature.description}`;
18 | const html = { __html: marked(raw) };
19 | return ;
20 | };
21 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { MainBox } from "components/main/box";
2 | import { SideBox } from "components/side/box";
3 | import { Metadata } from "next";
4 | import { ReactElement, ReactNode } from "react";
5 | import "styles/index.css";
6 |
7 | export const metadata: Metadata = {
8 | title: "OTF Show",
9 | description: "An interactive showcase of OpenType features",
10 | };
11 |
12 | function RootLayout(props: { children: ReactNode }): ReactElement {
13 | const { children } = props;
14 |
15 | return (
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default RootLayout;
26 |
--------------------------------------------------------------------------------
/src/components/main/header/box.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 | import { MainHeaderLink } from "./link";
3 |
4 | const links = [
5 | { href: "/", text: "otf.thien.do" },
6 | { href: "/about", text: "what's this" },
7 | { href: "https://github.com/thien-do/otf", text: "github" },
8 | ];
9 |
10 | export const MainHeaderBox = (): ReactElement => (
11 |
12 |
13 | {links.map((link) => (
14 | -
15 | {link.text}
16 |
17 | ))}
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { featureArr } from "features";
3 | import { useRouter } from "next/navigation";
4 | import { useEffect } from "react";
5 |
6 | function Page() {
7 | // Cannot use `redirect` as it will be cached,
8 | // meaning repeatedly clicking on the home link will show the same feature,
9 | // while we want to show a different feature each time.
10 | const { replace } = useRouter();
11 |
12 | useEffect(() => {
13 | const index = Math.floor(Math.random() * featureArr.length);
14 | const feature = featureArr.at(index);
15 | if (feature === undefined) throw new Error("Feature not found"); // Coding error
16 | replace(`/${feature.code}`);
17 | }, []);
18 | }
19 |
20 | export default Page;
21 |
--------------------------------------------------------------------------------
/src/components/code/desc/list.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement, ReactNode } from "react";
2 |
3 | const addSep: (prev: ReactNode[], link: ReactElement) => ReactNode[] = (
4 | prev,
5 | link
6 | ) => [...prev, link, ", "];
7 |
8 | export const CodeDescList = (props: {
9 | items: ReactElement[];
10 | }): ReactElement => {
11 | const { items } = props;
12 |
13 | switch (items.length) {
14 | case 0:
15 | throw new Error("Length must be > 0");
16 | case 1:
17 | return items[0];
18 | default: {
19 | const init = items.slice(0, items.length - 1).reduce(addSep, []);
20 | init.pop(); // remove last ","
21 | const last = items[items.length - 1];
22 | return (
23 |
24 | {init} and {last}
25 |
26 | );
27 | }
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/code/desc/texts/base.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ReactElement } from "react";
3 | import { GetCodeSearch } from "../../search/base";
4 | import { CodeDescList } from "../list";
5 |
6 | export const CodeDescTextsBase = (props: {
7 | texts: string[];
8 | getHref: GetCodeSearch;
9 | }): ReactElement => {
10 | const { texts, getHref } = props;
11 |
12 | if (texts.length === 0) throw new Error("texts length must be > 0");
13 |
14 | const links = texts.map((text) => {
15 | const link = (
16 |
17 | {text}
18 |
19 | );
20 |
21 | return “{link}”;
22 | });
23 |
24 | return (
25 |
26 | Try them with text like .
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/code/desc/fonts/base.tsx:
--------------------------------------------------------------------------------
1 | import { GetCodeSearch } from "components/code/search/base";
2 | import Link from "next/link";
3 | import { ReactElement } from "react";
4 | import { CodeDescList } from "../list";
5 |
6 | export const CodeDescFontsBase = (props: {
7 | fonts: string[];
8 | getHref: GetCodeSearch;
9 | }): ReactElement => {
10 | const { fonts, getHref } = props;
11 |
12 | if (fonts.length === 0) throw new Error("fonts.length must be > 0");
13 |
14 | const links = fonts.map((font) => (
15 |
16 | {font}
17 |
18 | ));
19 |
20 | const prefix =
21 | fonts.length === 1
22 | ? "A font that have this feature is"
23 | : "Fonts that have this feature includes";
24 | return (
25 |
26 | {prefix} .
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "bundler",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "baseUrl": "./src",
20 | "incremental": true,
21 | "paths": {
22 | "@/*": [
23 | "./src/*"
24 | ]
25 | },
26 | "plugins": [
27 | {
28 | "name": "next"
29 | }
30 | ]
31 | },
32 | "exclude": [
33 | "node_modules"
34 | ],
35 | "include": [
36 | "next-env.d.ts",
37 | "**/*.ts",
38 | "**/*.tsx",
39 | ".next/types/**/*.ts"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/code/overview/box.tsx:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { ReactElement, Suspense } from "react";
3 | import { CodeOverviewCode } from "./code";
4 | import { CodeOverviewFontClient } from "./font/clien";
5 | import { CodeOverviewFontServer } from "./font/server";
6 |
7 | export function CodeOverviewBox(props: { feature: Feature }): ReactElement {
8 | const { feature } = props;
9 |
10 | return (
11 |
12 | This is how
13 |
14 | {" "}
15 |
16 | in
17 |
18 | }>
19 |
20 | {" "}
21 |
22 | brings better typography:
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "otf",
3 | "version": "1.0.0",
4 | "description": "An interactive showcase of OpenType's typographic features",
5 | "repository": "http://github.com/thien-do/otf",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "keywords": [
13 | "OTF",
14 | "OpenType",
15 | "Typographic feature"
16 | ],
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "downshift": "^9.0.6",
21 | "marked": "^13.0.2",
22 | "next": "14.2.4",
23 | "react": "^18.3.1",
24 | "react-dom": "^18.3.1"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20.14.10",
28 | "@types/react": "^18.3.3",
29 | "@types/react-dom": "^18.3.0",
30 | "postcss": "^8.4.39",
31 | "tailwindcss": "^3.4.4",
32 | "typescript": "^5.5.3"
33 | },
34 | "packageManager": "pnpm@9.4.0"
35 | }
--------------------------------------------------------------------------------
/src/components/side/box.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 |
3 | import { featureArr, featureGroups } from "features";
4 | import { SideFeatureList } from "./feature/list";
5 |
6 | const Group = (props: {
7 | type: string;
8 | label: string;
9 | index: number;
10 | }): ReactElement => {
11 | const { type, label, index } = props;
12 |
13 | return (
14 |
15 |
{label}
16 |
f.type === type)} />
17 |
18 | );
19 | };
20 |
21 | export const SideBox = (): ReactElement => {
22 | return (
23 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/code/overview/code.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DropdownBox, DropdownOption } from "components/dropdown/box";
4 | import { Feature, featureArr, featureGroups } from "features";
5 | import { useRouter } from "next/navigation";
6 | import { ReactElement } from "react";
7 |
8 | const toOption = (ft: Feature): DropdownOption => ({
9 | value: ft.code,
10 | label: `${ft.name} (${ft.code})`,
11 | });
12 |
13 | export function CodeOverviewCode(props: { feature: Feature }): ReactElement {
14 | const { feature } = props;
15 |
16 | const router = useRouter();
17 |
18 | const options: DropdownOption[] = [];
19 | featureGroups.forEach(({ label, type }) => {
20 | options.push({ label, value: label, isHeading: true });
21 | options.push(...featureArr.filter((f) => f.type === type).map(toOption));
22 | });
23 |
24 | return (
25 | void router.push(`/${value}`)}
28 | options={options}
29 | />
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/[code]/page.tsx:
--------------------------------------------------------------------------------
1 | import { CodeBox } from "components/code/box";
2 | import { featureArr, getFeature } from "features";
3 | import { Metadata } from "next";
4 | import { ReactElement } from "react";
5 |
6 | type Props = {
7 | params: { code: string };
8 | };
9 |
10 | export async function generateMetadata(props: Props): Promise {
11 | const { params } = props;
12 |
13 | const feature = getFeature(params.code);
14 | const fullName = `${feature.name} (${feature.code})`;
15 | const description = feature.description.split("\n\n")[0];
16 |
17 | return {
18 | title: `${fullName} - otf.show`,
19 | description: `${fullName} ${description}`,
20 | };
21 | }
22 |
23 | export async function generateStaticParams() {
24 | return featureArr.map((feature) => ({
25 | code: feature.code,
26 | }));
27 | }
28 |
29 | export default function Page(props: Props): ReactElement {
30 | const { params } = props;
31 |
32 | const feature = getFeature(params.code);
33 |
34 | return ;
35 | }
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # OpenType Feature Showcase — [otf.thien.do](https://otf.thien.do)
4 |
5 | > _“OpenType features are like secret compartments in fonts. Unlock them and you’ll
6 | > find ways to make fonts look and behave differently in subtle and dramatic ways.”_ —
7 | > [Tim Brown](https://helpx.adobe.com/fonts/using/use-open-type-features.html), Head of Typography at Adobe
8 |
9 | I recently discovered [OpenType Features](https://en.wikipedia.org/wiki/OpenType#Advanced_typography) and quickly impressed by the enormous potential they could bring to web typography. However, the current state of their usage on the web is still quite limited. Many don't know about them, even those who would have benefit from.
10 |
11 | Therefore, I decided to build **[otf](https://otf.thien.do)** as an interactive showcase of these typographic features. I hope it could help others explore OpenType Features easily so we can achieve better typography on the web.
12 |
13 | Cheer!
14 |
--------------------------------------------------------------------------------
/src/components/code/desc/hrefs.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 |
3 | function Item(props: { href: string }): ReactElement {
4 | const { href } = props;
5 |
6 | const url = new URL(href);
7 |
8 | const last = url.pathname.split("/").at(-1);
9 | if (!last) throw new Error(`No last path: ${href}`);
10 | const title = last
11 | .replaceAll("-", " ")
12 | .replaceAll(".html", "")
13 | .replaceAll("_", " ")
14 | .toLowerCase();
15 |
16 | const host = url.hostname.replaceAll("www.", "").replaceAll("en.", "");
17 |
18 | return (
19 |
20 |
21 | {title}
22 |
23 | {` on ${host}`}
24 |
25 | );
26 | }
27 |
28 | export function CodeDescHrefs(props: { hrefs: string[] }): ReactElement {
29 | const { hrefs } = props;
30 |
31 | return (
32 | <>
33 |
34 | To learn more, see:
35 |
36 |
37 | {hrefs.map((href) => (
38 |
39 | ))}
40 |
41 | >
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/code/box.tsx:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { ReactElement, Suspense } from "react";
3 | import { CodeDescBox } from "./desc/box";
4 | import { CodeOverviewBox } from "./overview/box";
5 | import { CodeTextBoxClient } from "./text/box-client";
6 | import { CodeTextBoxServer } from "./text/box-server";
7 | import { CodeSource } from "./source";
8 |
9 | const column = "flex-1 lt960:w-full lt960:flex-none";
10 | const desc = "pl-72 lt1280:pl-36 lt960:pl-0 lt960:pt-36";
11 |
12 | export function CodeBox(props: { feature: Feature }): ReactElement {
13 | const { feature } = props;
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
}>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | *, *:after, *:before {
2 | border: none; border-width: 0;
3 | margin: 0; padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | ul { list-style: none; }
8 | a { text-decoration: none; color: inherit; }
9 | strong { font: inherit; }
10 |
11 | input, button {
12 | appearance: none; font: inherit;
13 | background: none; color: inherit;
14 | text-align: left;
15 | }
16 |
17 | html { font-family: 'Inter', sans-serif; }
18 |
19 | /* https://github.com/WICG/focus-visible#backwards-compatibility */
20 | .js-focus-visible :focus:not(.focus-visible) {
21 | outline: none;
22 | }
23 |
24 | input, button, a, select {
25 | transition: box-shadow 0.2s;
26 | box-shadow: 0 0 0 12px transparent;
27 | }
28 |
29 | .js-focus-visible .focus-visible {
30 | box-shadow: 0 0 0 2px #4299E1;
31 | }
32 |
33 | .hanging-quote { position: relative; }
34 |
35 | .hanging-quote::before {
36 | content: "“"; position: absolute;
37 | right: 100%; top: 0;
38 | }
39 |
40 | .uppercase { font-feature-settings: "cpsp" }
41 |
42 | /* for marked.js. this is simpler than rewrite the renderers */
43 | .marked strong { font-weight: 600; }
44 | .marked p + p { margin-top: 24px; }
45 | .marked a { text-decoration: underline; }
46 |
--------------------------------------------------------------------------------
/src/components/code/overview/font/base.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { GetCodeSearch } from "components/code/search/base";
4 | import { DropdownBox, DropdownOption } from "components/dropdown/box";
5 | import { Feature } from "features";
6 | import fonts from "fonts";
7 | import { useRouter } from "next/navigation";
8 | import { ReactElement } from "react";
9 |
10 | const toOption = (font: string): DropdownOption => ({
11 | value: font,
12 | label: font,
13 | });
14 |
15 | export function CodeOverviewFontBase(props: {
16 | feature: Feature;
17 | getHref: GetCodeSearch;
18 | fontParams: null | string;
19 | }): ReactElement {
20 | const { feature, getHref, fontParams } = props;
21 |
22 | const router = useRouter();
23 |
24 | const font = fontParams ?? feature.fonts[0];
25 |
26 | const supported = feature.fonts;
27 | const options: DropdownOption[] = [
28 | { label: "supported typefaces", value: "0", isHeading: true },
29 | ...supported.map(toOption),
30 | { label: "other typefaces", value: "1", isHeading: true },
31 | ...fonts.filter((f) => !supported.includes(f)).map(toOption),
32 | ];
33 |
34 | return (
35 | {
38 | const href = getHref("font", value);
39 | router.push(href);
40 | }}
41 | options={options}
42 | />
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/main/header/link.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Link, { LinkProps } from "next/link";
3 | import { useParams, usePathname } from "next/navigation";
4 | import { AnchorHTMLAttributes, ReactElement } from "react";
5 |
6 | function useIsActive(props: { href: string }): boolean {
7 | const { href: target } = props;
8 | const pathname = usePathname();
9 | const params = useParams();
10 |
11 | switch (target) {
12 | // Home ("/") is actually the same as code ("/[code]") due to a redirect.
13 | // Technically there are more cases,
14 | // such as "/calt" target should match "/[code]" as well.
15 | // However, we don't have that cases in the header links.
16 | case "/":
17 | return "code" in params;
18 | default:
19 | return pathname === target;
20 | }
21 | }
22 |
23 | export function MainHeaderLink(props: {
24 | href: string;
25 | children: string;
26 | }): ReactElement {
27 | const { href, children } = props;
28 |
29 | const className = useIsActive({ href }) ? "text-2D3" : "text-A0A";
30 | type Common = LinkProps & AnchorHTMLAttributes;
31 | const common: Common = { href, className };
32 |
33 | // Internal link
34 | if (href.startsWith("/")) return {children};
35 |
36 | // External link
37 | return (
38 |
39 | {`${children}\u00a0→`}
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/code/desc/box.tsx:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { ReactElement, Suspense } from "react";
3 | import { CodeDescFontsClient } from "./fonts/client";
4 | import { CodeDescFontsServer } from "./fonts/server";
5 | import { CodeDescHrefs } from "./hrefs";
6 | import { CodeDescRelated } from "./related";
7 | import { CodeDescTextsClient } from "./texts/client";
8 | import { CodeDescTextsServer } from "./texts/server";
9 | import { CodeDescUsage } from "./usage";
10 |
11 | export function CodeDescBox(props: { feature: Feature }): ReactElement {
12 | const { feature } = props;
13 |
14 | return (
15 |
16 |
17 |
18 | }>
19 |
20 |
21 |
22 | }>
23 |
24 |
25 |
26 | {feature.related.length > 0 && (
27 |
28 |
29 |
30 | )}
31 | {feature.references.length > 0 && (
32 |
33 |
34 |
35 | )}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/side/feature/item.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Feature } from "features";
3 | import Link from "next/link";
4 | import { useParams } from "next/navigation";
5 | import { ReactElement } from "react";
6 |
7 | function Code(props: { code: string }): ReactElement {
8 | const { code } = props;
9 |
10 | if (code.length !== 4) throw new Error("code length must be 4");
11 |
12 | return (
13 |
14 | {code.slice(0, 2)}
15 | {code.slice(2, 4)}
16 |
17 | );
18 | }
19 |
20 | function Name(props: { feature: Feature }): ReactElement {
21 | const { feature } = props;
22 |
23 | return (
24 |
25 | {feature.name}
26 |
27 | {feature.fonts.join(", ")}
28 |
29 |
30 | );
31 | }
32 |
33 | export function SideFeatureItem(props: { feature: Feature }): ReactElement {
34 | const { feature } = props;
35 |
36 | const params = useParams();
37 |
38 | return (
39 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/code/text/box-base.tsx:
--------------------------------------------------------------------------------
1 | import { Feature } from "features";
2 | import { ReactElement } from "react";
3 | import { CodeTextInput } from "./input";
4 | import { CodeTextLabel } from "./label";
5 |
6 | const getSetting = (feature: Feature): string => {
7 | const arr = [];
8 | arr.push(`"${feature.patchedCode ?? feature.code}"`);
9 | arr.push(feature.default ? "0" : "1"); // enabled or disabled
10 | if (feature.required) {
11 | arr.push(`, ${feature.required}`);
12 | }
13 | return arr.join(" ");
14 | };
15 |
16 | export function CodeTextBoxBase(props: {
17 | text: string;
18 | setText: null | ((next: string) => void);
19 | //
20 | feature: Feature;
21 | font: string;
22 | }): ReactElement {
23 | const { feature, font, text, setText } = props;
24 |
25 | const input = { font, text, setText, feature };
26 |
27 | return (
28 |
29 |
30 |
31 | with “{feature.name}”
32 | {feature.default ? "disabled" : "enabled"}:
33 |
34 |
35 |
36 |
37 |
38 |
39 |
typeface default:
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/dropdown/menu.tsx:
--------------------------------------------------------------------------------
1 | import { UseSelectReturnValue } from "downshift";
2 | import { ReactElement } from "react";
3 | import { DropdownOption } from "./box";
4 |
5 | const Option = (props: {
6 | select: UseSelectReturnValue;
7 | option: DropdownOption;
8 | index: number;
9 | }): ReactElement => {
10 | const { select, option, index } = props;
11 |
12 | const isSelected = select.selectedItem === option.value;
13 | const isHighlighted = select.highlightedIndex === index;
14 |
15 | return (
16 |
26 | {option.label}
27 |
28 | );
29 | };
30 |
31 | export const DropdownMenu = (props: {
32 | select: UseSelectReturnValue;
33 | options: DropdownOption[];
34 | }): ReactElement => {
35 | const { select, options } = props;
36 |
37 | return (
38 |
46 | {options.map((option, index) => (
47 |
53 | ))}
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/app/icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
5 | theme: {
6 | textColor: {
7 | "2D3": "#2D3748",
8 | "718": "#718096",
9 | CBD: "#CBD5E0",
10 | A0A: "#A0AEC0",
11 | FFF: "#FFF",
12 | },
13 | backgroundColor: {
14 | "718": "#718096",
15 | "4A5": "#4A5568",
16 | "2D3": "#2D3748",
17 | EDF: "#EDF2F7",
18 | F7F: "#F7FAFC",
19 | FFF: "#FFF",
20 | },
21 | boxShadow: {
22 | E2E: "0px 0px 8px #E2E8F0",
23 | A0A: "0px 2px 8px #A0AEC0",
24 | },
25 | borderColor: {
26 | E2E: "#E2E8F0",
27 | CBD: "#CBD5E0",
28 | },
29 | width: {
30 | 320: "320px",
31 | 640: "640px",
32 | full: "100%",
33 | },
34 | maxWidth: {
35 | 640: "640px",
36 | none: "none",
37 | },
38 | padding: {
39 | 0: "0",
40 | 9: "9px",
41 | 18: "18px",
42 | 24: "24px",
43 | 36: "36px",
44 | 72: "72px",
45 | },
46 | gap: {
47 | 0: "0",
48 | 9: "9px",
49 | 18: "18px",
50 | 24: "24px",
51 | 36: "36px",
52 | 72: "72px",
53 | },
54 | margin: {
55 | 18: "18px",
56 | 24: "24px",
57 | 36: "36px",
58 | 48: "48px",
59 | },
60 | fontFamily: {
61 | mono: "Roboto Mono, monospace",
62 | },
63 | fontSize: {
64 | 15: "15px",
65 | 18: "18px",
66 | 24: "24px",
67 | 72: "72px",
68 | },
69 | fontWeight: {
70 | semibold: "600",
71 | normal: "400",
72 | },
73 | lineHeight: {
74 | 18: "18px",
75 | 24: "24px",
76 | 30: "30px",
77 | 36: "36px",
78 | 96: "96px",
79 | none: "1",
80 | },
81 | zIndex: {
82 | 0: "0",
83 | 1: "1",
84 | },
85 | screens: {
86 | lt1280: { max: "1279px" },
87 | lt960: { max: "959px" },
88 | },
89 | },
90 | };
91 |
92 | export default config;
93 |
--------------------------------------------------------------------------------
/src/components/dropdown/box.tsx:
--------------------------------------------------------------------------------
1 | import { useSelect, UseSelectState } from "downshift";
2 | import { ReactElement } from "react";
3 | import { DropdownButton } from "./button";
4 | import { DropdownMenu } from "./menu";
5 |
6 | export interface DropdownOption {
7 | value: string;
8 | label: string;
9 | isHeading?: boolean;
10 | }
11 |
12 | type SetValue = (value: string) => void;
13 |
14 | const makeItemToString =
15 | (options: DropdownOption[]) =>
16 | (value: string | null): string => {
17 | if (value === null) {
18 | return "";
19 | }
20 | const option = options.find((o) => o.value === value);
21 | if (option === undefined) {
22 | return "";
23 | }
24 | return option.label;
25 | };
26 |
27 | type Changes = Partial>;
28 |
29 | const makeOnSelectedItemChange = (setValue: SetValue) => (changes: Changes) => {
30 | if (changes.selectedItem === undefined) {
31 | throw new Error("dropdown: selected item must be defined");
32 | }
33 | setValue(changes.selectedItem ?? "");
34 | };
35 |
36 | const makeIsItemDisable =
37 | (options: DropdownOption[]) => (_item: string, index: number) => {
38 | return options[index].isHeading ?? false;
39 | };
40 |
41 | export const DropdownBox = (props: {
42 | value: string;
43 | setValue: SetValue;
44 | options: DropdownOption[];
45 | }): ReactElement => {
46 | const { value, setValue, options } = props;
47 |
48 | const itemToString = makeItemToString(options);
49 | const onSelectedItemChange = makeOnSelectedItemChange(setValue);
50 | const isItemDisabled = makeIsItemDisable(options);
51 | const select = useSelect({
52 | items: options.map((o) => o.value),
53 | selectedItem: value,
54 | itemToString,
55 | onSelectedItemChange,
56 | isItemDisabled,
57 | });
58 |
59 | return (
60 |
61 |
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/styles/fonts.css:
--------------------------------------------------------------------------------
1 | /* app fonts */
2 | @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap");
3 | @font-face {
4 | font-family: "Inter"; font-style: normal; font-weight: 400;
5 | src: url("/fonts/Inter-Regular.woff2?v=3.12") format("woff2");
6 | }
7 | @font-face {
8 | font-family: "Inter"; font-style: normal; font-weight: 600;
9 | src: url("/fonts/Inter-SemiBold.woff2?v=3.12") format("woff2");
10 | }
11 | /* libraries */
12 | @font-face{
13 | font-family: "Source Sans Pro"; font-weight: 600; font-style: normal;
14 | src: url("/fonts/SourceSansPro-Semibold.otf.woff2") format("woff2");
15 | }
16 | @font-face{
17 | font-family: "Lato"; font-weight: 600; font-style: normal;
18 | src: url("/fonts/Lato-Semibold.woff2") format("woff2");
19 | }
20 | @font-face{
21 | font-family: "Roboto"; font-weight: 700; font-style: normal;
22 | src: url("/fonts/Roboto-Bold.ttf") format("truetype");
23 | }
24 | @font-face{
25 | font-family: "Rasa"; font-weight: 600; font-style: normal;
26 | src: url("/fonts/Rasa-SemiBold.otf") format("opentype");
27 | }
28 | @font-face{
29 | font-family: "Open Sans"; font-weight: 600; font-style: normal;
30 | src: url("/fonts/OpenSans-SemiBold.ttf") format("truetype");
31 | }
32 | @font-face{
33 | font-family: "Raleway"; font-weight: 600; font-style: normal;
34 | src: url("/fonts/Raleway-v4020-SemiBold.otf") format("opentype");
35 | }
36 | @font-face{
37 | font-family: "Merriweather"; font-weight: 700; font-style: normal;
38 | src: url("/fonts/Merriweather-Bold.ttf") format("truetype");
39 | }
40 | @font-face{
41 | font-family: "IBM Plex Sans"; font-weight: 600; font-style: normal;
42 | src: url("/fonts/IBMPlexSans-SemiBold.woff2") format("woff2");
43 | }
44 | @font-face{
45 | font-family: "Fira Code"; font-weight: 700; font-style: normal;
46 | src: url("/fonts/FiraCode-Bold.woff2") format("woff2");
47 | }
48 | @font-face{
49 | font-family: "EB Garamond"; font-weight: 600; font-style: normal;
50 | src: url("/fonts/EBGaramond-SemiBold.ttf") format("truetype");
51 | }
52 | @font-face{
53 | font-family: "BioRhyme"; font-weight: 700; font-style: normal;
54 | src: url("/fonts/BioRhyme-Bold.ttf") format("truetype");
55 | }
56 | @font-face{
57 | font-family: "Caveat"; font-weight: 700; font-style: normal;
58 | src: url("/fonts/Caveat-Bold.ttf") format("truetype");
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/code/text/box-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Feature } from "features";
4 | import { useRouter, useSearchParams } from "next/navigation";
5 | import { ReactElement, useEffect, useState } from "react";
6 | import { useCodeSearchClient } from "../search/client";
7 | import { CodeTextBoxBase } from "./box-base";
8 |
9 | /**
10 | * This is surprisingly tricky to get right.
11 | *
12 | * If we use the URL as the source of truth, then we don't need both state and effect.
13 | * However, this would cause caret jumping, because it takes steps
14 | * to go from user event to URL and back to render.
15 | * These steps decouple React's rendering, so it puts the caret at the end,
16 | * because it lost track of why the change happened.
17 | *
18 | * If we use the state as the source of truth,
19 | * and passively update the URL,
20 | * then the text is not updated if the URL is changed from elsewhere,
21 | * such as the suggestions in the description area.
22 | *
23 | * Our current solution is a compromise of both.
24 | * In practice, the URL is the source of truth, so all states are updated correctly.
25 | * However, in React's perspective, the URL is actually a side effect of the state.
26 | * The state is the source of truth for React, so it can maintain the caret position.
27 | */
28 | function useText(props: { feature: Feature }): {
29 | text: string;
30 | setText: (next: string) => void;
31 | } {
32 | const { feature } = props;
33 |
34 | const param = useSearchParams().get("text") ?? feature.texts[0];
35 | const { push } = useRouter();
36 | const getHref = useCodeSearchClient();
37 |
38 | const [state, setState] = useState(param);
39 |
40 | const setText = (next: string): void => {
41 | setState(next);
42 | const href = getHref("text", next);
43 | push(href, { scroll: false });
44 | };
45 |
46 | useEffect(() => {
47 | if (state !== param) void setState(param);
48 | }, [param]);
49 |
50 | return { text: state, setText };
51 | }
52 |
53 | export function CodeTextBoxClient(props: { feature: Feature }): ReactElement {
54 | const { feature } = props;
55 |
56 | const font = useSearchParams().get("font") ?? feature.fonts[0];
57 | const { text, setText } = useText({ feature });
58 |
59 | return (
60 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/about/page.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 |
3 | const Link = (props: { href: string; children: string }): ReactElement => {
4 | const { href, children } = props;
5 |
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
13 | const L_REACT = "https://reactjs.org/";
14 | const L_NEXT = "https://nextjs.org/";
15 | const L_DOWNSHIFT = "https://github.com/downshift-js/downshift/";
16 | const L_OTFSHOW = "https://github.com/thien-do/otf/";
17 | const L_FIGMA = "https://www.figma.com/blog/opentype-font-features/";
18 | const L_ADOBE =
19 | "https://helpx.adobe.com/fonts/using/use-open-type-features.html";
20 | const L_MICROSOFT =
21 | "https://docs.microsoft.com/en-us/typography/opentype/spec/featuretags";
22 | const L_CONTRIBUTE = "https://github.com/thien-do/otf/issues/1";
23 | const L_WIKIPEDIA =
24 | "https://en.wikipedia.org/wiki/List_of_typographic_features";
25 |
26 | const About = () => (
27 |
28 |
29 | OpenType features are like secret compartments in fonts. Unlock them and
30 | you’ll find ways to make fonts look and behave differently in subtle and
31 | dramatic ways.”
32 |
33 | {" "}
34 | — Tim Brown, Head of Typography at Adobe
35 |
36 |
37 |
38 |
39 | "otf" is an interactive showcase of OpenType features.
40 | {" "}
41 | We hope it helps designers and developers to explore and achieve better
42 | typography on the web.
43 |
44 |
45 | This is an Open Source project. Contributions are
46 | more than welcome
47 | . The entire development takes place on
48 | GitHub.
49 |
50 |
51 | The project is built with
52 |
53 | React,{" "}
54 |
55 |
56 | Next.js, and{" "}
57 |
58 |
59 | Downshift.{" "}
60 |
61 | The majority of information originally comes from
62 |
63 | {" "}
64 | Figma,{" "}
65 |
66 |
67 | Adobe,{" "}
68 |
69 |
70 | Microsoft and{" "}
71 |
72 |
73 | Wikipedia.
74 |
75 |
76 |
77 | );
78 |
79 | export default About;
80 |
--------------------------------------------------------------------------------
/src/features.ts:
--------------------------------------------------------------------------------
1 | export interface Feature {
2 | /**
3 | * The standard code, i.e., in communication
4 | */
5 | code: string;
6 | /**
7 | * The real code that get applied, like "calt" for "rand"
8 | */
9 | patchedCode?: string;
10 | /**
11 | * The recommended CSS usage,
12 | * e.g., with dedicated properties like "font-variant-numeric"
13 | */
14 | css: {
15 | property: string;
16 | value: string;
17 | };
18 | name: string;
19 | family_code?: string;
20 | family_name?: string;
21 | description: string;
22 | fonts: string[];
23 | texts: string[];
24 | related: string[];
25 | references: string[];
26 | type: "digit" | "letter";
27 | default?: boolean;
28 | required?: string;
29 | }
30 |
31 | export const featureMap: { [code: string]: Feature | undefined } = {
32 | tnum: {
33 | code: "tnum",
34 | name: "Tabular Figures",
35 | description:
36 | "displays numerical digits (0–9) in the same width, like in monospace typefaces. For example, the digit 1 would take the same space as 2, 3, 4 or 0, so the number 1111 would take the same space as 2340.\n\nTabular figures are useful in aligned columns, like in tables or price lists, as they allow users to easily compare values vertically.",
37 | fonts: ["Inter", "Rasa"],
38 | texts: ["11,150,110", "11110000"],
39 | related: ["pnum", "onum", "lnum"],
40 | references: [
41 | "https://www.fonts.com/content/learning/fontology/level-3/numbers/proportional-vs-tabular-figures",
42 | ],
43 | type: "digit",
44 | css: {
45 | property: "font-variant-numeric",
46 | value: "tabular-nums",
47 | },
48 | },
49 | pnum: {
50 | code: "pnum",
51 | name: "Proportional Figures",
52 | description:
53 | "displays numerical digits (0–9) in varying widths, similar to letters. For example, the digit 1 would likely take less space than others, so the number 1111 would take a smaller space than 2340.\n\nProportional figures look good in horizontal text, like in an address, as they maintain the balance and produce a consistent appearance with the rest of the alphabet.",
54 | fonts: ["Lato", "Source Sans Pro", "Roboto", "Open Sans"],
55 | texts: ["11,150,110", "11110000"],
56 | related: ["tnum", "onum", "lnum"],
57 | references: [
58 | "https://www.fonts.com/content/learning/fontology/level-3/numbers/proportional-vs-tabular-figures",
59 | ],
60 | type: "digit",
61 | css: {
62 | property: "font-variant-numeric",
63 | value: "proportional-nums",
64 | },
65 | },
66 | onum: {
67 | code: "onum",
68 | name: "Oldstyle Figures",
69 | description:
70 | "displays numbers in varying heights and alignments. These numbers blend in with lowercase letters, as they share the same x-height, and also have ascenders (usually 6 and 8) and descenders (3, 4, 5, 7 and 9).\n\nOldstyle figures are often considered more pleasing and less intrusive than lining figures. They are preferred in running text and also pair nicely with small caps.",
71 | fonts: ["EB Garamond", "Lato", "Source Sans Pro", "Roboto", "Open Sans"],
72 | texts: ["10Broad36", "123456789"],
73 | related: ["lnum", "tnum", "pnum"],
74 | references: [
75 | "https://en.wikipedia.org/wiki/Text_figures",
76 | "https://www.fonts.com/content/learning/fontology/level-3/numbers/oldstyle-figures",
77 | "https://practicaltypography.com/alternate-figures.html#oldstyle-figures",
78 | ],
79 | type: "digit",
80 | css: {
81 | property: "font-variant-numeric",
82 | value: "oldstyle-nums",
83 | },
84 | },
85 | lnum: {
86 | code: "lnum",
87 | name: "Lining Figures",
88 | description:
89 | "displays numbers in a uniform height, which usually aligned with uppercase letters (baseline and cap height).\n\nLining figures is the default style in most fonts. In all-cap settings, they are almost always preferred over oldstyle ones.",
90 | fonts: ["Merriweather", "Raleway"],
91 | texts: ["10Broad36", "123456789"],
92 | related: ["onum", "tnum", "pnum"],
93 | references: [
94 | "https://www.fonts.com/content/learning/fontology/level-3/numbers/lining-figures",
95 | ],
96 | type: "digit",
97 | css: {
98 | property: "font-variant-numeric",
99 | value: "lining-nums",
100 | },
101 | },
102 | ordn: {
103 | code: "ordn",
104 | name: "Ordinals",
105 | description:
106 | "set the letters following a number superscripted, to denote that number is an ordinal one (represent position in a sequential order), like 1st or 2nd.",
107 | fonts: ["Source Sans Pro", "Lato"],
108 | texts: ["1st 2nd 3rd", "1o 1a", "Nº No"],
109 | related: [],
110 | references: [
111 | "https://en.wikipedia.org/wiki/Ordinal_indicator",
112 | "https://practicaltypography.com/ordinals.html",
113 | ],
114 | type: "digit",
115 | css: {
116 | property: "font-variant-numeric",
117 | value: "ordinal",
118 | },
119 | },
120 | frac: {
121 | code: "frac",
122 | name: "Fractions",
123 | description:
124 | "applies the diagonal (slashed) fraction style to numbers separated by a slash, as in 1/2 or 3/4. These are called fractions and are usually used in dimensions, recipes and mathematics.\n\nThe diagonal style is not the only way to represent fractions, but usually considered the most common one. They look natural, use space effectively and are easier to read.",
125 | fonts: ["Roboto", "Inter", "Source Sans Pro", "Lato"],
126 | texts: ["1/2 1/4 3/4", "123/45678"],
127 | related: [],
128 | references: [
129 | "https://www.fonts.com/content/learning/fontology/level-3/numbers/fractions",
130 | ],
131 | type: "digit",
132 | css: {
133 | property: "font-variant-numeric",
134 | value: "diagonal-fractions",
135 | },
136 | },
137 | zero: {
138 | code: "zero",
139 | name: "Slashed Zero",
140 | description:
141 | "adds a diagonal slash to the zero digit to make it more distinguishable from the capital O.",
142 | fonts: ["IBM Plex Sans", "Inter", "Source Sans Pro"],
143 | texts: ["0O", "ZERO0"],
144 | related: [],
145 | references: [],
146 | type: "digit",
147 | css: {
148 | property: "font-variant-numeric",
149 | value: "slashed-zero",
150 | },
151 | },
152 | liga: {
153 | code: "liga",
154 | name: "Standard Ligatures",
155 | description:
156 | "combines two (or sometimes three) characters into a single character to prevent character collision, like the one between the hook of “f” and the dot of “i”.\n\nStandard Ligatures is enabled by default. It usually has ligatures for “f-” pairs, such as “fi”, “fl”, “ff” or “ffi”. Others ligatures might be found via “dlig”.",
157 | fonts: ["Lato", "EB Garamond", "Roboto"],
158 | texts: ["Clifftop", "flying fish"],
159 | related: ["calt", "dlig", "hlig"],
160 | references: [
161 | "https://en.wikipedia.org/wiki/Orthographic_ligature",
162 | "https://www.fonts.com/content/learning/fontology/level-3/signs-and-symbols/ligatures-1",
163 | ],
164 | type: "letter",
165 | default: true,
166 | css: {
167 | property: "font-variant-ligatures",
168 | value: "common-ligatures",
169 | },
170 | },
171 | calt: {
172 | code: "calt",
173 | name: "Contextual Alternates",
174 | description:
175 | 'uses alternate forms when some specific characters are used together. This usually improves the spacing and/or connection between these characters.\n\nContextual Alternates is enabled by default. It can be disabled if separated, distinct characters is preferred. In some typefaces designed for code, this is also referred as "programming ligatures".',
176 | fonts: ["Fira Code", "Inter"],
177 | texts: ["<- -> <->", "1*2 3×4"],
178 | related: ["liga"],
179 | references: [],
180 | type: "letter",
181 | default: true,
182 | css: {
183 | property: "font-variant-ligatures",
184 | value: "contextual",
185 | },
186 | },
187 | hist: {
188 | code: "hist",
189 | name: "Historical Forms",
190 | description:
191 | "brings your text back to the distant past, like in 1800s. It replaces some letters with their archaic alternatives, such as the long form “s”.\n\nIt's worth to note that some historical characters have quite complex rules that may not be implemented completely. For example, EB Garamond doesn't reserve the round “s” at the end of a word (try “sinfulness”).\n\nTechnical-wise, Historical Forms only deals with single characters. For completeness, consider applying the [Historical Ligatures](/hlig) and [Discretionary Ligatures](/dlig).",
192 | fonts: ["EB Garamond"],
193 | texts: ["substitute", "Joiner"],
194 | related: ["hlig", "dlig"],
195 | references: [
196 | "https://en.wikipedia.org/wiki/J#History",
197 | "https://en.wikipedia.org/wiki/Long_s",
198 | ],
199 | type: "letter",
200 | css: {
201 | property: "font-feature-settings",
202 | value: "hist",
203 | },
204 | },
205 | hlig: {
206 | code: "hlig",
207 | name: "Historical Ligatures",
208 | description:
209 | "also connects characters like Standard Ligatures but work on historical ones, like “ſt” (instead of “st”).\n\nHistorical Ligatures may require [Historical Forms](/hist) to be enabled or not depend on the font. When the latter is not enabled, one should you the historical alternates manually (e.g. “ſ” instead of “s”).\n\nIt may be useful to also enable [Discretionary Ligatures](/dlig) as some of them (like “ct” and “st”) are often common in the past as well.",
210 | fonts: ["EB Garamond"],
211 | texts: ["lost", "short"],
212 | related: ["liga", "dlig", "hist"],
213 | references: [],
214 | type: "letter",
215 | required: '"hist"',
216 | css: {
217 | property: "font-variant-ligatures",
218 | value: "historical-ligatures",
219 | },
220 | },
221 | dlig: {
222 | code: "dlig",
223 | name: "Discretionary Ligatures",
224 | description:
225 | "– sometimes called Rare Ligatures – also connects characters like [Standard Ligatures](/liga) but work on not-so-common ones, like “ct”, “st” or “Th”.\n\nDiscretionary Ligatures are usually more decorative in nature, and should be used at one's discretion, thus the name. Some of these ligatures may also be common in the distant past, and could be used together with [Historical Forms](/hist).",
226 | fonts: ["EB Garamond"],
227 | texts: ["Extract", "Chest", "Thedore"],
228 | related: ["liga", "hlig", "hist"],
229 | references: [
230 | "https://www.fonts.com/content/learning/fontology/level-3/signs-and-symbols/ligatures-2",
231 | ],
232 | type: "letter",
233 | css: {
234 | property: "font-variant-ligatures",
235 | value: "discretionary-ligatures",
236 | },
237 | },
238 | salt: {
239 | code: "salt",
240 | name: "Stylistic Alternates",
241 | description:
242 | "replaces some characters with their stylistic alternates. For example, the single-story “g” and “a” could be replaced with their double-story forms, and vice versa.\n\nStylistic Alternates are used mostly for their aesthetic effect. However, sometimes they can also help improve readability as in the case of the finial of lowercase “l” and the serif of uppercase “I”.",
243 | fonts: ["Inter", "Source Sans Pro", "Open Sans"],
244 | texts: ["Illustrating", "Illegal"],
245 | related: ["swsh", "hist"],
246 | references: [
247 | "https://en.wikipedia.org/wiki/G#Typographic_variants",
248 | "https://en.wikipedia.org/wiki/A#Typographic_variants",
249 | "https://en.wikipedia.org/wiki/I#Forms_and_variants",
250 | ],
251 | type: "letter",
252 | css: {
253 | property: "font-feature-settings",
254 | value: "salt",
255 | },
256 | },
257 | swsh: {
258 | code: "swsh",
259 | name: "Swashes",
260 | description:
261 | "decorates some letters with typographical flourish, such as an extended serif or terminal. They add an elegant touch to an otherwise straightforward letterform.\n\nSwashes should be used judiciously. A single swash character can make an eye-catching initial letter to start an article, but many of them can quickly reduce readability.",
262 | fonts: ["BioRhyme", "EB Garamond"],
263 | texts: ["Rose", "Quality"],
264 | related: ["salt", "hist"],
265 | references: [
266 | "https://www.fonts.com/content/learning/fontology/level-4/fine-typography/using-swash-characters",
267 | "https://en.wikipedia.org/wiki/Swash_(typography)",
268 | ],
269 | type: "letter",
270 | css: {
271 | property: "font-feature-settings",
272 | value: "swsh",
273 | },
274 | },
275 | smcp: {
276 | code: "smcp",
277 | name: "Small Capitals",
278 | description:
279 | "turns lowercase letters into glyphs that resemble uppercase letterforms but reduced in size, close to the surrounding lowercase letters.\n\nSmall Capitals are not simply scaled-down versions of normal capitals. They normally retain the same stroke weight and have a wider aspect ratio to harmonize with surrounding letters, both uppercase and lowercase ones.\n\nSmall Capitals are used in running text as a form of emphasis alongside italic and bold, for example to draw attention to the openning phrase of an article. They are also used where uppercase letters would appear jarring, like in long acronyms.",
280 | fonts: ["Roboto", "EB Garamond", "Source Sans Pro"],
281 | texts: ["Signal & Noise", "Time Roman"],
282 | related: [],
283 | references: [
284 | "https://en.wikipedia.org/wiki/Small_caps",
285 | "https://www.fonts.com/content/learning/fontology/level-1/type-anatomy/small-caps",
286 | ],
287 | type: "letter",
288 | css: {
289 | property: "font-variant-caps",
290 | value: "small-caps",
291 | },
292 | },
293 | rand: {
294 | code: "rand",
295 | patchedCode: "calt",
296 | name: "Randomize",
297 | description:
298 | "makes your text looks like handwritten by randomly rendering different forms of the same letter. This is easy to observe in words that have consecutive repeated letters, like “Raccoon” or “Bookkeeper”.\n\nHowever, true support for Randomize in today client applications is somewhat limited, so many fonts (including Caveat in this demo) implement them via [Contextual Alternates](/calt) instead.",
299 | fonts: ["Caveat"],
300 | texts: ["Red Racoon", "Bookkeeper"],
301 | related: [],
302 | references: [],
303 | type: "letter",
304 | default: true,
305 | css: {
306 | property: "font-feature-settings",
307 | value: "calt",
308 | },
309 | },
310 | };
311 |
312 | export const featureArr: Feature[] = [
313 | "onum",
314 | "lnum",
315 | "tnum",
316 | "pnum",
317 | "ordn",
318 | "frac",
319 | "zero", // digit
320 | "salt",
321 | "swsh",
322 | "hist",
323 | "smcp",
324 | "cswh",
325 | "rand", // letter
326 | "liga",
327 | "hlig",
328 | "dlig",
329 | "calt", // ligature
330 | "kern",
331 | "sups",
332 | "subs", // position
333 | ]
334 | .map((key) => featureMap[key])
335 | // https://codereview.stackexchange.com/a/138289
336 | .filter((a) => a !== undefined) as Feature[];
337 |
338 | export const featureGroups = [
339 | { label: "Digits, Numbers & Math", type: "digit" },
340 | { label: "Letters & Ligatures", type: "letter" },
341 | ];
342 |
343 | export function getFeature(code: string): Feature {
344 | if (typeof code != "string") {
345 | throw new Error("invalid code from query");
346 | }
347 | const feature = featureMap[code];
348 | if (feature === undefined) {
349 | throw new Error("not found code");
350 | }
351 | return feature;
352 | }
353 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | downshift:
12 | specifier: ^9.0.6
13 | version: 9.0.6(react@18.3.1)
14 | marked:
15 | specifier: ^13.0.2
16 | version: 13.0.2
17 | next:
18 | specifier: 14.2.4
19 | version: 14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
20 | react:
21 | specifier: ^18.3.1
22 | version: 18.3.1
23 | react-dom:
24 | specifier: ^18.3.1
25 | version: 18.3.1(react@18.3.1)
26 | devDependencies:
27 | '@types/node':
28 | specifier: ^20.14.10
29 | version: 20.14.10
30 | '@types/react':
31 | specifier: ^18.3.3
32 | version: 18.3.3
33 | '@types/react-dom':
34 | specifier: ^18.3.0
35 | version: 18.3.0
36 | postcss:
37 | specifier: ^8.4.39
38 | version: 8.4.39
39 | tailwindcss:
40 | specifier: ^3.4.4
41 | version: 3.4.4
42 | typescript:
43 | specifier: ^5.5.3
44 | version: 5.5.3
45 |
46 | packages:
47 |
48 | '@alloc/quick-lru@5.2.0':
49 | resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
50 | engines: {node: '>=10'}
51 |
52 | '@babel/runtime@7.24.7':
53 | resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
54 | engines: {node: '>=6.9.0'}
55 |
56 | '@isaacs/cliui@8.0.2':
57 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
58 | engines: {node: '>=12'}
59 |
60 | '@jridgewell/gen-mapping@0.3.5':
61 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
62 | engines: {node: '>=6.0.0'}
63 |
64 | '@jridgewell/resolve-uri@3.1.2':
65 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
66 | engines: {node: '>=6.0.0'}
67 |
68 | '@jridgewell/set-array@1.2.1':
69 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
70 | engines: {node: '>=6.0.0'}
71 |
72 | '@jridgewell/sourcemap-codec@1.4.15':
73 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
74 |
75 | '@jridgewell/trace-mapping@0.3.25':
76 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
77 |
78 | '@next/env@14.2.4':
79 | resolution: {integrity: sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==}
80 |
81 | '@next/swc-darwin-arm64@14.2.4':
82 | resolution: {integrity: sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==}
83 | engines: {node: '>= 10'}
84 | cpu: [arm64]
85 | os: [darwin]
86 |
87 | '@next/swc-darwin-x64@14.2.4':
88 | resolution: {integrity: sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==}
89 | engines: {node: '>= 10'}
90 | cpu: [x64]
91 | os: [darwin]
92 |
93 | '@next/swc-linux-arm64-gnu@14.2.4':
94 | resolution: {integrity: sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==}
95 | engines: {node: '>= 10'}
96 | cpu: [arm64]
97 | os: [linux]
98 |
99 | '@next/swc-linux-arm64-musl@14.2.4':
100 | resolution: {integrity: sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==}
101 | engines: {node: '>= 10'}
102 | cpu: [arm64]
103 | os: [linux]
104 |
105 | '@next/swc-linux-x64-gnu@14.2.4':
106 | resolution: {integrity: sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==}
107 | engines: {node: '>= 10'}
108 | cpu: [x64]
109 | os: [linux]
110 |
111 | '@next/swc-linux-x64-musl@14.2.4':
112 | resolution: {integrity: sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==}
113 | engines: {node: '>= 10'}
114 | cpu: [x64]
115 | os: [linux]
116 |
117 | '@next/swc-win32-arm64-msvc@14.2.4':
118 | resolution: {integrity: sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==}
119 | engines: {node: '>= 10'}
120 | cpu: [arm64]
121 | os: [win32]
122 |
123 | '@next/swc-win32-ia32-msvc@14.2.4':
124 | resolution: {integrity: sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==}
125 | engines: {node: '>= 10'}
126 | cpu: [ia32]
127 | os: [win32]
128 |
129 | '@next/swc-win32-x64-msvc@14.2.4':
130 | resolution: {integrity: sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==}
131 | engines: {node: '>= 10'}
132 | cpu: [x64]
133 | os: [win32]
134 |
135 | '@nodelib/fs.scandir@2.1.5':
136 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
137 | engines: {node: '>= 8'}
138 |
139 | '@nodelib/fs.stat@2.0.5':
140 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
141 | engines: {node: '>= 8'}
142 |
143 | '@nodelib/fs.walk@1.2.8':
144 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
145 | engines: {node: '>= 8'}
146 |
147 | '@pkgjs/parseargs@0.11.0':
148 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
149 | engines: {node: '>=14'}
150 |
151 | '@swc/counter@0.1.3':
152 | resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
153 |
154 | '@swc/helpers@0.5.5':
155 | resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
156 |
157 | '@types/node@20.14.10':
158 | resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
159 |
160 | '@types/prop-types@15.7.12':
161 | resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
162 |
163 | '@types/react-dom@18.3.0':
164 | resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
165 |
166 | '@types/react@18.3.3':
167 | resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
168 |
169 | ansi-regex@5.0.1:
170 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
171 | engines: {node: '>=8'}
172 |
173 | ansi-regex@6.0.1:
174 | resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
175 | engines: {node: '>=12'}
176 |
177 | ansi-styles@4.3.0:
178 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
179 | engines: {node: '>=8'}
180 |
181 | ansi-styles@6.2.1:
182 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
183 | engines: {node: '>=12'}
184 |
185 | any-promise@1.3.0:
186 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
187 |
188 | anymatch@3.1.3:
189 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
190 | engines: {node: '>= 8'}
191 |
192 | arg@5.0.2:
193 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
194 |
195 | balanced-match@1.0.2:
196 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
197 |
198 | binary-extensions@2.3.0:
199 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
200 | engines: {node: '>=8'}
201 |
202 | brace-expansion@2.0.1:
203 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
204 |
205 | braces@3.0.3:
206 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
207 | engines: {node: '>=8'}
208 |
209 | busboy@1.6.0:
210 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
211 | engines: {node: '>=10.16.0'}
212 |
213 | camelcase-css@2.0.1:
214 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
215 | engines: {node: '>= 6'}
216 |
217 | caniuse-lite@1.0.30001640:
218 | resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==}
219 |
220 | chokidar@3.6.0:
221 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
222 | engines: {node: '>= 8.10.0'}
223 |
224 | client-only@0.0.1:
225 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
226 |
227 | color-convert@2.0.1:
228 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
229 | engines: {node: '>=7.0.0'}
230 |
231 | color-name@1.1.4:
232 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
233 |
234 | commander@4.1.1:
235 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
236 | engines: {node: '>= 6'}
237 |
238 | compute-scroll-into-view@3.1.0:
239 | resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==}
240 |
241 | cross-spawn@7.0.3:
242 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
243 | engines: {node: '>= 8'}
244 |
245 | cssesc@3.0.0:
246 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
247 | engines: {node: '>=4'}
248 | hasBin: true
249 |
250 | csstype@3.1.3:
251 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
252 |
253 | didyoumean@1.2.2:
254 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
255 |
256 | dlv@1.1.3:
257 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
258 |
259 | downshift@9.0.6:
260 | resolution: {integrity: sha512-lkqWh0eb34XuH+3z3/BH/LGVRV7ur0rielSlxtlQKsjAFF/wc/c0wsM9phUGXyzK2g1QWHoNHQyc+vVAheI17Q==}
261 | peerDependencies:
262 | react: '>=16.12.0'
263 |
264 | eastasianwidth@0.2.0:
265 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
266 |
267 | emoji-regex@8.0.0:
268 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
269 |
270 | emoji-regex@9.2.2:
271 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
272 |
273 | fast-glob@3.3.2:
274 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
275 | engines: {node: '>=8.6.0'}
276 |
277 | fastq@1.17.1:
278 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
279 |
280 | fill-range@7.1.1:
281 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
282 | engines: {node: '>=8'}
283 |
284 | foreground-child@3.2.1:
285 | resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==}
286 | engines: {node: '>=14'}
287 |
288 | fsevents@2.3.3:
289 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
290 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
291 | os: [darwin]
292 |
293 | function-bind@1.1.2:
294 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
295 |
296 | glob-parent@5.1.2:
297 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
298 | engines: {node: '>= 6'}
299 |
300 | glob-parent@6.0.2:
301 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
302 | engines: {node: '>=10.13.0'}
303 |
304 | glob@10.4.3:
305 | resolution: {integrity: sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==}
306 | engines: {node: '>=18'}
307 | hasBin: true
308 |
309 | graceful-fs@4.2.11:
310 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
311 |
312 | hasown@2.0.2:
313 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
314 | engines: {node: '>= 0.4'}
315 |
316 | is-binary-path@2.1.0:
317 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
318 | engines: {node: '>=8'}
319 |
320 | is-core-module@2.14.0:
321 | resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==}
322 | engines: {node: '>= 0.4'}
323 |
324 | is-extglob@2.1.1:
325 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
326 | engines: {node: '>=0.10.0'}
327 |
328 | is-fullwidth-code-point@3.0.0:
329 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
330 | engines: {node: '>=8'}
331 |
332 | is-glob@4.0.3:
333 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
334 | engines: {node: '>=0.10.0'}
335 |
336 | is-number@7.0.0:
337 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
338 | engines: {node: '>=0.12.0'}
339 |
340 | isexe@2.0.0:
341 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
342 |
343 | jackspeak@3.4.1:
344 | resolution: {integrity: sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==}
345 | engines: {node: '>=18'}
346 |
347 | jiti@1.21.6:
348 | resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
349 | hasBin: true
350 |
351 | js-tokens@4.0.0:
352 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
353 |
354 | lilconfig@2.1.0:
355 | resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
356 | engines: {node: '>=10'}
357 |
358 | lilconfig@3.1.2:
359 | resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
360 | engines: {node: '>=14'}
361 |
362 | lines-and-columns@1.2.4:
363 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
364 |
365 | loose-envify@1.4.0:
366 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
367 | hasBin: true
368 |
369 | lru-cache@10.3.1:
370 | resolution: {integrity: sha512-9/8QXrtbGeMB6LxwQd4x1tIMnsmUxMvIH/qWGsccz6bt9Uln3S+sgAaqfQNhbGA8ufzs2fHuP/yqapGgP9Hh2g==}
371 | engines: {node: '>=18'}
372 |
373 | marked@13.0.2:
374 | resolution: {integrity: sha512-J6CPjP8pS5sgrRqxVRvkCIkZ6MFdRIjDkwUwgJ9nL2fbmM6qGQeB2C16hi8Cc9BOzj6xXzy0jyi0iPIfnMHYzA==}
375 | engines: {node: '>= 18'}
376 | hasBin: true
377 |
378 | merge2@1.4.1:
379 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
380 | engines: {node: '>= 8'}
381 |
382 | micromatch@4.0.7:
383 | resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
384 | engines: {node: '>=8.6'}
385 |
386 | minimatch@9.0.5:
387 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
388 | engines: {node: '>=16 || 14 >=14.17'}
389 |
390 | minipass@7.1.2:
391 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
392 | engines: {node: '>=16 || 14 >=14.17'}
393 |
394 | mz@2.7.0:
395 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
396 |
397 | nanoid@3.3.7:
398 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
399 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
400 | hasBin: true
401 |
402 | next@14.2.4:
403 | resolution: {integrity: sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==}
404 | engines: {node: '>=18.17.0'}
405 | hasBin: true
406 | peerDependencies:
407 | '@opentelemetry/api': ^1.1.0
408 | '@playwright/test': ^1.41.2
409 | react: ^18.2.0
410 | react-dom: ^18.2.0
411 | sass: ^1.3.0
412 | peerDependenciesMeta:
413 | '@opentelemetry/api':
414 | optional: true
415 | '@playwright/test':
416 | optional: true
417 | sass:
418 | optional: true
419 |
420 | normalize-path@3.0.0:
421 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
422 | engines: {node: '>=0.10.0'}
423 |
424 | object-assign@4.1.1:
425 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
426 | engines: {node: '>=0.10.0'}
427 |
428 | object-hash@3.0.0:
429 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
430 | engines: {node: '>= 6'}
431 |
432 | package-json-from-dist@1.0.0:
433 | resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
434 |
435 | path-key@3.1.1:
436 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
437 | engines: {node: '>=8'}
438 |
439 | path-parse@1.0.7:
440 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
441 |
442 | path-scurry@1.11.1:
443 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
444 | engines: {node: '>=16 || 14 >=14.18'}
445 |
446 | picocolors@1.0.1:
447 | resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
448 |
449 | picomatch@2.3.1:
450 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
451 | engines: {node: '>=8.6'}
452 |
453 | pify@2.3.0:
454 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
455 | engines: {node: '>=0.10.0'}
456 |
457 | pirates@4.0.6:
458 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
459 | engines: {node: '>= 6'}
460 |
461 | postcss-import@15.1.0:
462 | resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
463 | engines: {node: '>=14.0.0'}
464 | peerDependencies:
465 | postcss: ^8.0.0
466 |
467 | postcss-js@4.0.1:
468 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
469 | engines: {node: ^12 || ^14 || >= 16}
470 | peerDependencies:
471 | postcss: ^8.4.21
472 |
473 | postcss-load-config@4.0.2:
474 | resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
475 | engines: {node: '>= 14'}
476 | peerDependencies:
477 | postcss: '>=8.0.9'
478 | ts-node: '>=9.0.0'
479 | peerDependenciesMeta:
480 | postcss:
481 | optional: true
482 | ts-node:
483 | optional: true
484 |
485 | postcss-nested@6.0.1:
486 | resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
487 | engines: {node: '>=12.0'}
488 | peerDependencies:
489 | postcss: ^8.2.14
490 |
491 | postcss-selector-parser@6.1.0:
492 | resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==}
493 | engines: {node: '>=4'}
494 |
495 | postcss-value-parser@4.2.0:
496 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
497 |
498 | postcss@8.4.31:
499 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
500 | engines: {node: ^10 || ^12 || >=14}
501 |
502 | postcss@8.4.39:
503 | resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
504 | engines: {node: ^10 || ^12 || >=14}
505 |
506 | prop-types@15.8.1:
507 | resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
508 |
509 | queue-microtask@1.2.3:
510 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
511 |
512 | react-dom@18.3.1:
513 | resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
514 | peerDependencies:
515 | react: ^18.3.1
516 |
517 | react-is@16.13.1:
518 | resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
519 |
520 | react-is@18.2.0:
521 | resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
522 |
523 | react@18.3.1:
524 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
525 | engines: {node: '>=0.10.0'}
526 |
527 | read-cache@1.0.0:
528 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
529 |
530 | readdirp@3.6.0:
531 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
532 | engines: {node: '>=8.10.0'}
533 |
534 | regenerator-runtime@0.14.1:
535 | resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
536 |
537 | resolve@1.22.8:
538 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
539 | hasBin: true
540 |
541 | reusify@1.0.4:
542 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
543 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
544 |
545 | run-parallel@1.2.0:
546 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
547 |
548 | scheduler@0.23.2:
549 | resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
550 |
551 | shebang-command@2.0.0:
552 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
553 | engines: {node: '>=8'}
554 |
555 | shebang-regex@3.0.0:
556 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
557 | engines: {node: '>=8'}
558 |
559 | signal-exit@4.1.0:
560 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
561 | engines: {node: '>=14'}
562 |
563 | source-map-js@1.2.0:
564 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
565 | engines: {node: '>=0.10.0'}
566 |
567 | streamsearch@1.1.0:
568 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
569 | engines: {node: '>=10.0.0'}
570 |
571 | string-width@4.2.3:
572 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
573 | engines: {node: '>=8'}
574 |
575 | string-width@5.1.2:
576 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
577 | engines: {node: '>=12'}
578 |
579 | strip-ansi@6.0.1:
580 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
581 | engines: {node: '>=8'}
582 |
583 | strip-ansi@7.1.0:
584 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
585 | engines: {node: '>=12'}
586 |
587 | styled-jsx@5.1.1:
588 | resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
589 | engines: {node: '>= 12.0.0'}
590 | peerDependencies:
591 | '@babel/core': '*'
592 | babel-plugin-macros: '*'
593 | react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
594 | peerDependenciesMeta:
595 | '@babel/core':
596 | optional: true
597 | babel-plugin-macros:
598 | optional: true
599 |
600 | sucrase@3.35.0:
601 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
602 | engines: {node: '>=16 || 14 >=14.17'}
603 | hasBin: true
604 |
605 | supports-preserve-symlinks-flag@1.0.0:
606 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
607 | engines: {node: '>= 0.4'}
608 |
609 | tailwindcss@3.4.4:
610 | resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==}
611 | engines: {node: '>=14.0.0'}
612 | hasBin: true
613 |
614 | thenify-all@1.6.0:
615 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
616 | engines: {node: '>=0.8'}
617 |
618 | thenify@3.3.1:
619 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
620 |
621 | to-regex-range@5.0.1:
622 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
623 | engines: {node: '>=8.0'}
624 |
625 | ts-interface-checker@0.1.13:
626 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
627 |
628 | tslib@2.6.3:
629 | resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
630 |
631 | typescript@5.5.3:
632 | resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==}
633 | engines: {node: '>=14.17'}
634 | hasBin: true
635 |
636 | undici-types@5.26.5:
637 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
638 |
639 | util-deprecate@1.0.2:
640 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
641 |
642 | which@2.0.2:
643 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
644 | engines: {node: '>= 8'}
645 | hasBin: true
646 |
647 | wrap-ansi@7.0.0:
648 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
649 | engines: {node: '>=10'}
650 |
651 | wrap-ansi@8.1.0:
652 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
653 | engines: {node: '>=12'}
654 |
655 | yaml@2.4.5:
656 | resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==}
657 | engines: {node: '>= 14'}
658 | hasBin: true
659 |
660 | snapshots:
661 |
662 | '@alloc/quick-lru@5.2.0': {}
663 |
664 | '@babel/runtime@7.24.7':
665 | dependencies:
666 | regenerator-runtime: 0.14.1
667 |
668 | '@isaacs/cliui@8.0.2':
669 | dependencies:
670 | string-width: 5.1.2
671 | string-width-cjs: string-width@4.2.3
672 | strip-ansi: 7.1.0
673 | strip-ansi-cjs: strip-ansi@6.0.1
674 | wrap-ansi: 8.1.0
675 | wrap-ansi-cjs: wrap-ansi@7.0.0
676 |
677 | '@jridgewell/gen-mapping@0.3.5':
678 | dependencies:
679 | '@jridgewell/set-array': 1.2.1
680 | '@jridgewell/sourcemap-codec': 1.4.15
681 | '@jridgewell/trace-mapping': 0.3.25
682 |
683 | '@jridgewell/resolve-uri@3.1.2': {}
684 |
685 | '@jridgewell/set-array@1.2.1': {}
686 |
687 | '@jridgewell/sourcemap-codec@1.4.15': {}
688 |
689 | '@jridgewell/trace-mapping@0.3.25':
690 | dependencies:
691 | '@jridgewell/resolve-uri': 3.1.2
692 | '@jridgewell/sourcemap-codec': 1.4.15
693 |
694 | '@next/env@14.2.4': {}
695 |
696 | '@next/swc-darwin-arm64@14.2.4':
697 | optional: true
698 |
699 | '@next/swc-darwin-x64@14.2.4':
700 | optional: true
701 |
702 | '@next/swc-linux-arm64-gnu@14.2.4':
703 | optional: true
704 |
705 | '@next/swc-linux-arm64-musl@14.2.4':
706 | optional: true
707 |
708 | '@next/swc-linux-x64-gnu@14.2.4':
709 | optional: true
710 |
711 | '@next/swc-linux-x64-musl@14.2.4':
712 | optional: true
713 |
714 | '@next/swc-win32-arm64-msvc@14.2.4':
715 | optional: true
716 |
717 | '@next/swc-win32-ia32-msvc@14.2.4':
718 | optional: true
719 |
720 | '@next/swc-win32-x64-msvc@14.2.4':
721 | optional: true
722 |
723 | '@nodelib/fs.scandir@2.1.5':
724 | dependencies:
725 | '@nodelib/fs.stat': 2.0.5
726 | run-parallel: 1.2.0
727 |
728 | '@nodelib/fs.stat@2.0.5': {}
729 |
730 | '@nodelib/fs.walk@1.2.8':
731 | dependencies:
732 | '@nodelib/fs.scandir': 2.1.5
733 | fastq: 1.17.1
734 |
735 | '@pkgjs/parseargs@0.11.0':
736 | optional: true
737 |
738 | '@swc/counter@0.1.3': {}
739 |
740 | '@swc/helpers@0.5.5':
741 | dependencies:
742 | '@swc/counter': 0.1.3
743 | tslib: 2.6.3
744 |
745 | '@types/node@20.14.10':
746 | dependencies:
747 | undici-types: 5.26.5
748 |
749 | '@types/prop-types@15.7.12': {}
750 |
751 | '@types/react-dom@18.3.0':
752 | dependencies:
753 | '@types/react': 18.3.3
754 |
755 | '@types/react@18.3.3':
756 | dependencies:
757 | '@types/prop-types': 15.7.12
758 | csstype: 3.1.3
759 |
760 | ansi-regex@5.0.1: {}
761 |
762 | ansi-regex@6.0.1: {}
763 |
764 | ansi-styles@4.3.0:
765 | dependencies:
766 | color-convert: 2.0.1
767 |
768 | ansi-styles@6.2.1: {}
769 |
770 | any-promise@1.3.0: {}
771 |
772 | anymatch@3.1.3:
773 | dependencies:
774 | normalize-path: 3.0.0
775 | picomatch: 2.3.1
776 |
777 | arg@5.0.2: {}
778 |
779 | balanced-match@1.0.2: {}
780 |
781 | binary-extensions@2.3.0: {}
782 |
783 | brace-expansion@2.0.1:
784 | dependencies:
785 | balanced-match: 1.0.2
786 |
787 | braces@3.0.3:
788 | dependencies:
789 | fill-range: 7.1.1
790 |
791 | busboy@1.6.0:
792 | dependencies:
793 | streamsearch: 1.1.0
794 |
795 | camelcase-css@2.0.1: {}
796 |
797 | caniuse-lite@1.0.30001640: {}
798 |
799 | chokidar@3.6.0:
800 | dependencies:
801 | anymatch: 3.1.3
802 | braces: 3.0.3
803 | glob-parent: 5.1.2
804 | is-binary-path: 2.1.0
805 | is-glob: 4.0.3
806 | normalize-path: 3.0.0
807 | readdirp: 3.6.0
808 | optionalDependencies:
809 | fsevents: 2.3.3
810 |
811 | client-only@0.0.1: {}
812 |
813 | color-convert@2.0.1:
814 | dependencies:
815 | color-name: 1.1.4
816 |
817 | color-name@1.1.4: {}
818 |
819 | commander@4.1.1: {}
820 |
821 | compute-scroll-into-view@3.1.0: {}
822 |
823 | cross-spawn@7.0.3:
824 | dependencies:
825 | path-key: 3.1.1
826 | shebang-command: 2.0.0
827 | which: 2.0.2
828 |
829 | cssesc@3.0.0: {}
830 |
831 | csstype@3.1.3: {}
832 |
833 | didyoumean@1.2.2: {}
834 |
835 | dlv@1.1.3: {}
836 |
837 | downshift@9.0.6(react@18.3.1):
838 | dependencies:
839 | '@babel/runtime': 7.24.7
840 | compute-scroll-into-view: 3.1.0
841 | prop-types: 15.8.1
842 | react: 18.3.1
843 | react-is: 18.2.0
844 | tslib: 2.6.3
845 |
846 | eastasianwidth@0.2.0: {}
847 |
848 | emoji-regex@8.0.0: {}
849 |
850 | emoji-regex@9.2.2: {}
851 |
852 | fast-glob@3.3.2:
853 | dependencies:
854 | '@nodelib/fs.stat': 2.0.5
855 | '@nodelib/fs.walk': 1.2.8
856 | glob-parent: 5.1.2
857 | merge2: 1.4.1
858 | micromatch: 4.0.7
859 |
860 | fastq@1.17.1:
861 | dependencies:
862 | reusify: 1.0.4
863 |
864 | fill-range@7.1.1:
865 | dependencies:
866 | to-regex-range: 5.0.1
867 |
868 | foreground-child@3.2.1:
869 | dependencies:
870 | cross-spawn: 7.0.3
871 | signal-exit: 4.1.0
872 |
873 | fsevents@2.3.3:
874 | optional: true
875 |
876 | function-bind@1.1.2: {}
877 |
878 | glob-parent@5.1.2:
879 | dependencies:
880 | is-glob: 4.0.3
881 |
882 | glob-parent@6.0.2:
883 | dependencies:
884 | is-glob: 4.0.3
885 |
886 | glob@10.4.3:
887 | dependencies:
888 | foreground-child: 3.2.1
889 | jackspeak: 3.4.1
890 | minimatch: 9.0.5
891 | minipass: 7.1.2
892 | package-json-from-dist: 1.0.0
893 | path-scurry: 1.11.1
894 |
895 | graceful-fs@4.2.11: {}
896 |
897 | hasown@2.0.2:
898 | dependencies:
899 | function-bind: 1.1.2
900 |
901 | is-binary-path@2.1.0:
902 | dependencies:
903 | binary-extensions: 2.3.0
904 |
905 | is-core-module@2.14.0:
906 | dependencies:
907 | hasown: 2.0.2
908 |
909 | is-extglob@2.1.1: {}
910 |
911 | is-fullwidth-code-point@3.0.0: {}
912 |
913 | is-glob@4.0.3:
914 | dependencies:
915 | is-extglob: 2.1.1
916 |
917 | is-number@7.0.0: {}
918 |
919 | isexe@2.0.0: {}
920 |
921 | jackspeak@3.4.1:
922 | dependencies:
923 | '@isaacs/cliui': 8.0.2
924 | optionalDependencies:
925 | '@pkgjs/parseargs': 0.11.0
926 |
927 | jiti@1.21.6: {}
928 |
929 | js-tokens@4.0.0: {}
930 |
931 | lilconfig@2.1.0: {}
932 |
933 | lilconfig@3.1.2: {}
934 |
935 | lines-and-columns@1.2.4: {}
936 |
937 | loose-envify@1.4.0:
938 | dependencies:
939 | js-tokens: 4.0.0
940 |
941 | lru-cache@10.3.1: {}
942 |
943 | marked@13.0.2: {}
944 |
945 | merge2@1.4.1: {}
946 |
947 | micromatch@4.0.7:
948 | dependencies:
949 | braces: 3.0.3
950 | picomatch: 2.3.1
951 |
952 | minimatch@9.0.5:
953 | dependencies:
954 | brace-expansion: 2.0.1
955 |
956 | minipass@7.1.2: {}
957 |
958 | mz@2.7.0:
959 | dependencies:
960 | any-promise: 1.3.0
961 | object-assign: 4.1.1
962 | thenify-all: 1.6.0
963 |
964 | nanoid@3.3.7: {}
965 |
966 | next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
967 | dependencies:
968 | '@next/env': 14.2.4
969 | '@swc/helpers': 0.5.5
970 | busboy: 1.6.0
971 | caniuse-lite: 1.0.30001640
972 | graceful-fs: 4.2.11
973 | postcss: 8.4.31
974 | react: 18.3.1
975 | react-dom: 18.3.1(react@18.3.1)
976 | styled-jsx: 5.1.1(react@18.3.1)
977 | optionalDependencies:
978 | '@next/swc-darwin-arm64': 14.2.4
979 | '@next/swc-darwin-x64': 14.2.4
980 | '@next/swc-linux-arm64-gnu': 14.2.4
981 | '@next/swc-linux-arm64-musl': 14.2.4
982 | '@next/swc-linux-x64-gnu': 14.2.4
983 | '@next/swc-linux-x64-musl': 14.2.4
984 | '@next/swc-win32-arm64-msvc': 14.2.4
985 | '@next/swc-win32-ia32-msvc': 14.2.4
986 | '@next/swc-win32-x64-msvc': 14.2.4
987 | transitivePeerDependencies:
988 | - '@babel/core'
989 | - babel-plugin-macros
990 |
991 | normalize-path@3.0.0: {}
992 |
993 | object-assign@4.1.1: {}
994 |
995 | object-hash@3.0.0: {}
996 |
997 | package-json-from-dist@1.0.0: {}
998 |
999 | path-key@3.1.1: {}
1000 |
1001 | path-parse@1.0.7: {}
1002 |
1003 | path-scurry@1.11.1:
1004 | dependencies:
1005 | lru-cache: 10.3.1
1006 | minipass: 7.1.2
1007 |
1008 | picocolors@1.0.1: {}
1009 |
1010 | picomatch@2.3.1: {}
1011 |
1012 | pify@2.3.0: {}
1013 |
1014 | pirates@4.0.6: {}
1015 |
1016 | postcss-import@15.1.0(postcss@8.4.39):
1017 | dependencies:
1018 | postcss: 8.4.39
1019 | postcss-value-parser: 4.2.0
1020 | read-cache: 1.0.0
1021 | resolve: 1.22.8
1022 |
1023 | postcss-js@4.0.1(postcss@8.4.39):
1024 | dependencies:
1025 | camelcase-css: 2.0.1
1026 | postcss: 8.4.39
1027 |
1028 | postcss-load-config@4.0.2(postcss@8.4.39):
1029 | dependencies:
1030 | lilconfig: 3.1.2
1031 | yaml: 2.4.5
1032 | optionalDependencies:
1033 | postcss: 8.4.39
1034 |
1035 | postcss-nested@6.0.1(postcss@8.4.39):
1036 | dependencies:
1037 | postcss: 8.4.39
1038 | postcss-selector-parser: 6.1.0
1039 |
1040 | postcss-selector-parser@6.1.0:
1041 | dependencies:
1042 | cssesc: 3.0.0
1043 | util-deprecate: 1.0.2
1044 |
1045 | postcss-value-parser@4.2.0: {}
1046 |
1047 | postcss@8.4.31:
1048 | dependencies:
1049 | nanoid: 3.3.7
1050 | picocolors: 1.0.1
1051 | source-map-js: 1.2.0
1052 |
1053 | postcss@8.4.39:
1054 | dependencies:
1055 | nanoid: 3.3.7
1056 | picocolors: 1.0.1
1057 | source-map-js: 1.2.0
1058 |
1059 | prop-types@15.8.1:
1060 | dependencies:
1061 | loose-envify: 1.4.0
1062 | object-assign: 4.1.1
1063 | react-is: 16.13.1
1064 |
1065 | queue-microtask@1.2.3: {}
1066 |
1067 | react-dom@18.3.1(react@18.3.1):
1068 | dependencies:
1069 | loose-envify: 1.4.0
1070 | react: 18.3.1
1071 | scheduler: 0.23.2
1072 |
1073 | react-is@16.13.1: {}
1074 |
1075 | react-is@18.2.0: {}
1076 |
1077 | react@18.3.1:
1078 | dependencies:
1079 | loose-envify: 1.4.0
1080 |
1081 | read-cache@1.0.0:
1082 | dependencies:
1083 | pify: 2.3.0
1084 |
1085 | readdirp@3.6.0:
1086 | dependencies:
1087 | picomatch: 2.3.1
1088 |
1089 | regenerator-runtime@0.14.1: {}
1090 |
1091 | resolve@1.22.8:
1092 | dependencies:
1093 | is-core-module: 2.14.0
1094 | path-parse: 1.0.7
1095 | supports-preserve-symlinks-flag: 1.0.0
1096 |
1097 | reusify@1.0.4: {}
1098 |
1099 | run-parallel@1.2.0:
1100 | dependencies:
1101 | queue-microtask: 1.2.3
1102 |
1103 | scheduler@0.23.2:
1104 | dependencies:
1105 | loose-envify: 1.4.0
1106 |
1107 | shebang-command@2.0.0:
1108 | dependencies:
1109 | shebang-regex: 3.0.0
1110 |
1111 | shebang-regex@3.0.0: {}
1112 |
1113 | signal-exit@4.1.0: {}
1114 |
1115 | source-map-js@1.2.0: {}
1116 |
1117 | streamsearch@1.1.0: {}
1118 |
1119 | string-width@4.2.3:
1120 | dependencies:
1121 | emoji-regex: 8.0.0
1122 | is-fullwidth-code-point: 3.0.0
1123 | strip-ansi: 6.0.1
1124 |
1125 | string-width@5.1.2:
1126 | dependencies:
1127 | eastasianwidth: 0.2.0
1128 | emoji-regex: 9.2.2
1129 | strip-ansi: 7.1.0
1130 |
1131 | strip-ansi@6.0.1:
1132 | dependencies:
1133 | ansi-regex: 5.0.1
1134 |
1135 | strip-ansi@7.1.0:
1136 | dependencies:
1137 | ansi-regex: 6.0.1
1138 |
1139 | styled-jsx@5.1.1(react@18.3.1):
1140 | dependencies:
1141 | client-only: 0.0.1
1142 | react: 18.3.1
1143 |
1144 | sucrase@3.35.0:
1145 | dependencies:
1146 | '@jridgewell/gen-mapping': 0.3.5
1147 | commander: 4.1.1
1148 | glob: 10.4.3
1149 | lines-and-columns: 1.2.4
1150 | mz: 2.7.0
1151 | pirates: 4.0.6
1152 | ts-interface-checker: 0.1.13
1153 |
1154 | supports-preserve-symlinks-flag@1.0.0: {}
1155 |
1156 | tailwindcss@3.4.4:
1157 | dependencies:
1158 | '@alloc/quick-lru': 5.2.0
1159 | arg: 5.0.2
1160 | chokidar: 3.6.0
1161 | didyoumean: 1.2.2
1162 | dlv: 1.1.3
1163 | fast-glob: 3.3.2
1164 | glob-parent: 6.0.2
1165 | is-glob: 4.0.3
1166 | jiti: 1.21.6
1167 | lilconfig: 2.1.0
1168 | micromatch: 4.0.7
1169 | normalize-path: 3.0.0
1170 | object-hash: 3.0.0
1171 | picocolors: 1.0.1
1172 | postcss: 8.4.39
1173 | postcss-import: 15.1.0(postcss@8.4.39)
1174 | postcss-js: 4.0.1(postcss@8.4.39)
1175 | postcss-load-config: 4.0.2(postcss@8.4.39)
1176 | postcss-nested: 6.0.1(postcss@8.4.39)
1177 | postcss-selector-parser: 6.1.0
1178 | resolve: 1.22.8
1179 | sucrase: 3.35.0
1180 | transitivePeerDependencies:
1181 | - ts-node
1182 |
1183 | thenify-all@1.6.0:
1184 | dependencies:
1185 | thenify: 3.3.1
1186 |
1187 | thenify@3.3.1:
1188 | dependencies:
1189 | any-promise: 1.3.0
1190 |
1191 | to-regex-range@5.0.1:
1192 | dependencies:
1193 | is-number: 7.0.0
1194 |
1195 | ts-interface-checker@0.1.13: {}
1196 |
1197 | tslib@2.6.3: {}
1198 |
1199 | typescript@5.5.3: {}
1200 |
1201 | undici-types@5.26.5: {}
1202 |
1203 | util-deprecate@1.0.2: {}
1204 |
1205 | which@2.0.2:
1206 | dependencies:
1207 | isexe: 2.0.0
1208 |
1209 | wrap-ansi@7.0.0:
1210 | dependencies:
1211 | ansi-styles: 4.3.0
1212 | string-width: 4.2.3
1213 | strip-ansi: 6.0.1
1214 |
1215 | wrap-ansi@8.1.0:
1216 | dependencies:
1217 | ansi-styles: 6.2.1
1218 | string-width: 5.1.2
1219 | strip-ansi: 7.1.0
1220 |
1221 | yaml@2.4.5: {}
1222 |
--------------------------------------------------------------------------------