@intlayer/design-system 9.3.1 → 9.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/api/hooks/auth.mjs +2 -0
- package/dist/esm/api/hooks/auth.mjs.map +1 -1
- package/dist/esm/api/index.mjs +2 -2
- package/dist/esm/components/Browser/Browser.mjs +1 -1
- package/dist/esm/components/Browser/Browser.mjs.map +1 -1
- package/dist/esm/components/ContentEditor/ContentEditorTextArea.mjs +1 -1
- package/dist/esm/components/CopyButton/index.mjs +2 -1
- package/dist/esm/components/CopyButton/index.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/ContentEditor.mjs +4 -5
- package/dist/esm/components/DictionaryFieldEditor/ContentEditor.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs +6 -10
- package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/DictionaryCreationForm.mjs +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/useDictionaryFormSchema.mjs +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/useDictionaryFormSchema.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs +6 -6
- package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/useDictionaryDetailsSchema.mjs +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/useDictionaryDetailsSchema.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs +3 -7
- package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs.map +1 -1
- package/dist/esm/components/DictionaryFieldEditor/NavigationView/NavigationViewNode.mjs +1 -1
- package/dist/esm/components/DictionaryFieldEditor/SaveForm/SaveForm.mjs +2 -2
- package/dist/esm/components/DictionaryFieldEditor/StructureView/StructureView.mjs +1 -1
- package/dist/esm/components/ExpandCollapse/ExpandCollapse.mjs +19 -12
- package/dist/esm/components/ExpandCollapse/ExpandCollapse.mjs.map +1 -1
- package/dist/esm/components/Form/FormBase.mjs.map +1 -1
- package/dist/esm/components/Form/elements/FormElementWrapper.mjs +1 -1
- package/dist/esm/components/Form/elements/FormElementWrapper.mjs.map +1 -1
- package/dist/esm/components/HTMLRender/HTMLRender.mjs +2 -2
- package/dist/esm/components/HTMLRender/HTMLRender.mjs.map +1 -1
- package/dist/esm/components/IDE/Code.mjs +3 -3
- package/dist/esm/components/IDE/Code.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockClient.mjs +16 -4
- package/dist/esm/components/IDE/CodeBlockClient.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockHighlight.mjs +14 -5
- package/dist/esm/components/IDE/CodeBlockHighlight.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockServer.mjs +5 -3
- package/dist/esm/components/IDE/CodeBlockServer.mjs.map +1 -1
- package/dist/esm/components/IDE/CodeBlockShiki.mjs +18 -18
- package/dist/esm/components/IDE/CodeBlockShiki.mjs.map +1 -1
- package/dist/esm/components/IDE/IDE.mjs +2 -5
- package/dist/esm/components/IDE/IDE.mjs.map +1 -1
- package/dist/esm/components/IDE/MarkDownRender.mjs +1 -2
- package/dist/esm/components/IDE/MarkDownRender.mjs.map +1 -1
- package/dist/esm/components/IDE/index.mjs +2 -2
- package/dist/esm/components/IDE/shikiThemes.mjs +18 -0
- package/dist/esm/components/IDE/shikiThemes.mjs.map +1 -0
- package/dist/esm/components/MarkDownRender/MarkDownIframe.mjs +105 -4
- package/dist/esm/components/MarkDownRender/MarkDownIframe.mjs.map +1 -1
- package/dist/esm/components/MarkDownRender/MarkDownRender.mjs +16 -16
- package/dist/esm/components/MarkDownRender/MarkDownRender.mjs.map +1 -1
- package/dist/esm/components/MaxHeightSmoother/index.mjs +1 -1
- package/dist/esm/components/MaxHeightSmoother/index.mjs.map +1 -1
- package/dist/esm/components/MaxWidthSmoother/index.mjs +1 -1
- package/dist/esm/components/MaxWidthSmoother/index.mjs.map +1 -1
- package/dist/esm/components/Pattern/GridDistortionPattern.mjs +100 -15
- package/dist/esm/components/Pattern/GridDistortionPattern.mjs.map +1 -1
- package/dist/esm/components/Popover/dynamic.mjs +5 -4
- package/dist/esm/components/Popover/dynamic.mjs.map +1 -1
- package/dist/esm/components/Popover/index.mjs +2 -2
- package/dist/esm/components/Popover/static.mjs +47 -18
- package/dist/esm/components/Popover/static.mjs.map +1 -1
- package/dist/esm/components/Tab/Tab.mjs +37 -16
- package/dist/esm/components/Tab/Tab.mjs.map +1 -1
- package/dist/esm/components/TextArea/AutoSizeTextArea.mjs +3 -2
- package/dist/esm/components/TextArea/AutoSizeTextArea.mjs.map +1 -1
- package/dist/esm/components/index.mjs +3 -3
- package/dist/esm/hooks/useDevice.mjs +16 -6
- package/dist/esm/hooks/useDevice.mjs.map +1 -1
- package/dist/esm/hooks/useItemSelector.mjs +107 -112
- package/dist/esm/hooks/useItemSelector.mjs.map +1 -1
- package/dist/esm/libs/auth.mjs +16 -3
- package/dist/esm/libs/auth.mjs.map +1 -1
- package/dist/esm/structured-data/buildCreativeWorkJsonLd.mjs +1 -1
- package/dist/esm/structured-data/buildCreativeWorkJsonLd.mjs.map +1 -1
- package/dist/types/api/hooks/auth.d.ts.map +1 -1
- package/dist/types/components/Browser/Browser.d.ts +7 -1
- package/dist/types/components/Browser/Browser.d.ts.map +1 -1
- package/dist/types/components/CopyButton/index.d.ts +9 -3
- package/dist/types/components/CopyButton/index.d.ts.map +1 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditor.d.ts +0 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditor.d.ts.map +1 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditorView/TextEditor.d.ts +0 -1
- package/dist/types/components/DictionaryFieldEditor/ContentEditorView/TextEditor.d.ts.map +1 -1
- package/dist/types/components/DictionaryFieldEditor/DictionaryCreationForm/useDictionaryFormSchema.d.ts +1 -1
- package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.d.ts +0 -1
- package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.d.ts.map +1 -1
- package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/useDictionaryDetailsSchema.d.ts +1 -1
- package/dist/types/components/ExpandCollapse/ExpandCollapse.d.ts.map +1 -1
- package/dist/types/components/Form/FormBase.d.ts +1 -1
- package/dist/types/components/Form/elements/FormElementWrapper.d.ts +1 -1
- package/dist/types/components/HTMLRender/HTMLRender.d.ts +1 -2
- package/dist/types/components/HTMLRender/HTMLRender.d.ts.map +1 -1
- package/dist/types/components/IDE/Code.d.ts +6 -1
- package/dist/types/components/IDE/Code.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockClient.d.ts +18 -2
- package/dist/types/components/IDE/CodeBlockClient.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockHighlight.d.ts +3 -2
- package/dist/types/components/IDE/CodeBlockHighlight.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockServer.d.ts +0 -1
- package/dist/types/components/IDE/CodeBlockServer.d.ts.map +1 -1
- package/dist/types/components/IDE/CodeBlockShiki.d.ts +0 -1
- package/dist/types/components/IDE/CodeBlockShiki.d.ts.map +1 -1
- package/dist/types/components/IDE/IDE.d.ts +0 -1
- package/dist/types/components/IDE/IDE.d.ts.map +1 -1
- package/dist/types/components/IDE/MarkDownRender.d.ts +0 -1
- package/dist/types/components/IDE/MarkDownRender.d.ts.map +1 -1
- package/dist/types/components/IDE/index.d.ts +2 -2
- package/dist/types/components/IDE/shikiThemes.d.ts +18 -0
- package/dist/types/components/IDE/shikiThemes.d.ts.map +1 -0
- package/dist/types/components/MarkDownRender/MarkDownIframe.d.ts.map +1 -1
- package/dist/types/components/MarkDownRender/MarkDownRender.d.ts +1 -2
- package/dist/types/components/MarkDownRender/MarkDownRender.d.ts.map +1 -1
- package/dist/types/components/Pattern/GridDistortionPattern.d.ts.map +1 -1
- package/dist/types/components/Popover/dynamic.d.ts.map +1 -1
- package/dist/types/components/Popover/index.d.ts +2 -2
- package/dist/types/components/Popover/static.d.ts +6 -1
- package/dist/types/components/Popover/static.d.ts.map +1 -1
- package/dist/types/components/Tab/Tab.d.ts +2 -2
- package/dist/types/components/Tab/Tab.d.ts.map +1 -1
- package/dist/types/components/index.d.ts +3 -3
- package/dist/types/hooks/useItemSelector.d.ts.map +1 -1
- package/dist/types/libs/auth.d.ts +3 -0
- package/dist/types/libs/auth.d.ts.map +1 -1
- package/dist/types/structured-data/buildCreativeWorkJsonLd.d.ts +9 -3
- package/dist/types/structured-data/buildCreativeWorkJsonLd.d.ts.map +1 -1
- package/package.json +43 -38
- package/tailwind.css +27 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { CodeDefault } from "./CodeBlockClient.mjs";
|
|
3
|
+
import { CodeDefault, shikiWrapperClassName } from "./CodeBlockClient.mjs";
|
|
4
4
|
import { resolveCodeLanguage } from "./shikiLanguages.mjs";
|
|
5
|
+
import { SHIKI_THEMES } from "./shikiThemes.mjs";
|
|
5
6
|
import { useEffect, useState } from "react";
|
|
6
7
|
import { jsx } from "react/jsx-runtime";
|
|
7
8
|
|
|
@@ -28,7 +29,7 @@ const loadLanguage = async ({ id, loadGrammar }) => {
|
|
|
28
29
|
const loadTheme = async (themeName) => {
|
|
29
30
|
const cachedTheme = themeCache.get(themeName);
|
|
30
31
|
if (cachedTheme) return cachedTheme;
|
|
31
|
-
const themePromise = (themeName ===
|
|
32
|
+
const themePromise = (themeName === SHIKI_THEMES.dark ? import("shiki/themes/github-dark.mjs") : import("shiki/themes/github-light.mjs")).then((module) => module.default);
|
|
32
33
|
themeCache.set(themeName, themePromise);
|
|
33
34
|
return themePromise;
|
|
34
35
|
};
|
|
@@ -41,46 +42,45 @@ const getHighlighterInstance = async () => {
|
|
|
41
42
|
return highlighterPromise;
|
|
42
43
|
};
|
|
43
44
|
/**
|
|
44
|
-
* Highlight a snippet, lazily loading only the grammar and
|
|
45
|
+
* Highlight a snippet, lazily loading only the grammar and themes it needs.
|
|
45
46
|
*
|
|
46
47
|
* The language is resolved to a canonical Shiki id first, so that the id passed
|
|
47
48
|
* to `codeToHtml` always matches the grammar that was loaded.
|
|
48
49
|
*/
|
|
49
|
-
const highlightCode = async (code, lang
|
|
50
|
-
const themeName = isDarkMode ? "github-dark" : "github-light";
|
|
50
|
+
const highlightCode = async (code, lang) => {
|
|
51
51
|
const resolvedLanguage = resolveCodeLanguage(lang);
|
|
52
|
-
const [highlighter, languageModule,
|
|
52
|
+
const [highlighter, languageModule, lightTheme, darkTheme] = await Promise.all([
|
|
53
53
|
getHighlighterInstance(),
|
|
54
54
|
loadLanguage(resolvedLanguage),
|
|
55
|
-
loadTheme(
|
|
55
|
+
loadTheme(SHIKI_THEMES.light),
|
|
56
|
+
loadTheme(SHIKI_THEMES.dark)
|
|
56
57
|
]);
|
|
57
58
|
if (languageModule && !highlighter.getLoadedLanguages().includes(resolvedLanguage.id)) await highlighter.loadLanguage(languageModule);
|
|
58
|
-
|
|
59
|
+
const loadedThemes = highlighter.getLoadedThemes();
|
|
60
|
+
if (!loadedThemes.includes(SHIKI_THEMES.light)) await highlighter.loadTheme(lightTheme);
|
|
61
|
+
if (!loadedThemes.includes(SHIKI_THEMES.dark)) await highlighter.loadTheme(darkTheme);
|
|
59
62
|
return highlighter.codeToHtml(String(code), {
|
|
60
63
|
lang: resolvedLanguage.id,
|
|
61
|
-
|
|
64
|
+
themes: SHIKI_THEMES,
|
|
65
|
+
defaultColor: false
|
|
62
66
|
});
|
|
63
67
|
};
|
|
64
|
-
const CodeBlockShiki = ({ children, lang
|
|
68
|
+
const CodeBlockShiki = ({ children, lang }) => {
|
|
65
69
|
const [html, setHtml] = useState(null);
|
|
66
70
|
useEffect(() => {
|
|
67
71
|
let isCancelled = false;
|
|
68
|
-
highlightCode(children, lang
|
|
72
|
+
highlightCode(children, lang).then((result) => {
|
|
69
73
|
if (!isCancelled) setHtml(result);
|
|
70
74
|
}).catch((error) => {
|
|
71
75
|
console.error("Failed to highlight code:", error);
|
|
72
|
-
if (!isCancelled
|
|
76
|
+
if (!isCancelled) setHtml("");
|
|
73
77
|
});
|
|
74
78
|
return () => {
|
|
75
79
|
isCancelled = true;
|
|
76
80
|
};
|
|
77
|
-
}, [
|
|
78
|
-
children,
|
|
79
|
-
lang,
|
|
80
|
-
isDarkMode
|
|
81
|
-
]);
|
|
81
|
+
}, [children, lang]);
|
|
82
82
|
return /* @__PURE__ */ jsx("div", {
|
|
83
|
-
className:
|
|
83
|
+
className: shikiWrapperClassName,
|
|
84
84
|
children: html ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } }) : /* @__PURE__ */ jsx(CodeDefault, { children })
|
|
85
85
|
});
|
|
86
86
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeBlockShiki.mjs","names":[],"sources":["../../../../src/components/IDE/CodeBlockShiki.tsx"],"sourcesContent":["'use client';\n\nimport { type FC, type ReactNode, useEffect, useState } from 'react';\nimport
|
|
1
|
+
{"version":3,"file":"CodeBlockShiki.mjs","names":[],"sources":["../../../../src/components/IDE/CodeBlockShiki.tsx"],"sourcesContent":["'use client';\n\nimport { type FC, type ReactNode, useEffect, useState } from 'react';\nimport { CodeDefault, shikiWrapperClassName } from './CodeBlockClient';\nimport {\n type CodeLanguage,\n type LanguageGrammar,\n type ResolvedCodeLanguage,\n resolveCodeLanguage,\n} from './shikiLanguages';\nimport { SHIKI_THEMES } from './shikiThemes';\n\n/** Theme registration, as default-exported by the `shiki/themes/*` modules. */\ntype ThemeRegistration =\n typeof import('shiki/themes/github-dark.mjs')['default'];\n\n/** Minimal Shiki highlighter surface used here, with unrestricted language ids. */\ntype CodeHighlighter = {\n getLoadedLanguages(): string[];\n getLoadedThemes(): string[];\n loadLanguage(grammar: LanguageGrammar): Promise<void>;\n loadTheme(theme: ThemeRegistration): Promise<void>;\n codeToHtml(\n code: string,\n options: {\n lang: string;\n themes: { light: string; dark: string };\n defaultColor: false;\n }\n ): string;\n};\n\n// Map of in-flight/loaded modules to avoid re-importing\nconst languageCache = new Map<string, Promise<LanguageGrammar>>();\nconst themeCache = new Map<string, Promise<ThemeRegistration>>();\n\n/**\n * Lazy load the grammar of an already resolved language.\n *\n * @returns The grammar registrations, or `null` for languages Shiki renders\n * without a grammar (plain text).\n */\nconst loadLanguage = async ({\n id,\n loadGrammar,\n}: ResolvedCodeLanguage): Promise<LanguageGrammar | null> => {\n if (!loadGrammar) return null;\n\n const cachedLanguage = languageCache.get(id);\n if (cachedLanguage) return cachedLanguage;\n\n const languagePromise = loadGrammar().then((module) => module.default);\n languageCache.set(id, languagePromise);\n\n return languagePromise;\n};\n\n/**\n * Lazy load a theme module.\n */\nconst loadTheme = async (themeName: string): Promise<ThemeRegistration> => {\n const cachedTheme = themeCache.get(themeName);\n if (cachedTheme) return cachedTheme;\n\n const themePromise = (\n themeName === SHIKI_THEMES.dark\n ? import('shiki/themes/github-dark.mjs')\n : import('shiki/themes/github-light.mjs')\n ).then((module) => module.default);\n\n themeCache.set(themeName, themePromise);\n\n return themePromise;\n};\n\n// Singleton Highlighter Instance\nlet highlighterPromise: Promise<CodeHighlighter> | null = null;\n\nconst getHighlighterInstance = async () => {\n if (!highlighterPromise) {\n highlighterPromise = import('shiki/bundle/web').then(\n ({ createHighlighter }) =>\n createHighlighter({\n langs: [],\n themes: [],\n })\n );\n }\n return highlighterPromise;\n};\n\n/**\n * Highlight a snippet, lazily loading only the grammar and themes it needs.\n *\n * The language is resolved to a canonical Shiki id first, so that the id passed\n * to `codeToHtml` always matches the grammar that was loaded.\n */\nconst highlightCode = async (\n code: ReactNode,\n lang: CodeLanguage\n): Promise<string> => {\n const resolvedLanguage = resolveCodeLanguage(lang);\n\n // Load highlighter, language, and themes in parallel\n const [highlighter, languageModule, lightTheme, darkTheme] =\n await Promise.all([\n getHighlighterInstance(),\n loadLanguage(resolvedLanguage),\n loadTheme(SHIKI_THEMES.light),\n loadTheme(SHIKI_THEMES.dark),\n ]);\n\n // Load into the singleton instance if not already loaded\n if (\n languageModule &&\n !highlighter.getLoadedLanguages().includes(resolvedLanguage.id)\n ) {\n await highlighter.loadLanguage(languageModule);\n }\n\n const loadedThemes = highlighter.getLoadedThemes();\n if (!loadedThemes.includes(SHIKI_THEMES.light)) {\n await highlighter.loadTheme(lightTheme);\n }\n if (!loadedThemes.includes(SHIKI_THEMES.dark)) {\n await highlighter.loadTheme(darkTheme);\n }\n\n return highlighter.codeToHtml(String(code), {\n lang: resolvedLanguage.id,\n themes: SHIKI_THEMES,\n defaultColor: false,\n });\n};\n\nexport type CodeBlockShikiProps = {\n children: ReactNode;\n lang: CodeLanguage;\n};\n\nexport const CodeBlockShiki: FC<CodeBlockShikiProps> = ({ children, lang }) => {\n const [html, setHtml] = useState<string | null>(null);\n\n useEffect(() => {\n let isCancelled = false;\n\n highlightCode(children, lang)\n .then((result) => {\n if (!isCancelled) setHtml(result);\n })\n .catch((error) => {\n console.error('Failed to highlight code:', error);\n if (!isCancelled) setHtml('');\n });\n\n return () => {\n isCancelled = true;\n };\n }, [children, lang]);\n\n return (\n <div className={shikiWrapperClassName}>\n {html ? (\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki generates safe HTML for code highlighting\n <div dangerouslySetInnerHTML={{ __html: html }} />\n ) : (\n <CodeDefault>{children}</CodeDefault>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AAiCA,MAAM,gCAAgB,IAAI,IAAsC;AAChE,MAAM,6BAAa,IAAI,IAAwC;;;;;;;AAQ/D,MAAM,eAAe,OAAO,EAC1B,IACA,kBAC2D;CAC3D,IAAI,CAAC,aAAa,OAAO;CAEzB,MAAM,iBAAiB,cAAc,IAAI,EAAE;CAC3C,IAAI,gBAAgB,OAAO;CAE3B,MAAM,kBAAkB,YAAY,CAAC,CAAC,MAAM,WAAW,OAAO,OAAO;CACrE,cAAc,IAAI,IAAI,eAAe;CAErC,OAAO;AACT;;;;AAKA,MAAM,YAAY,OAAO,cAAkD;CACzE,MAAM,cAAc,WAAW,IAAI,SAAS;CAC5C,IAAI,aAAa,OAAO;CAExB,MAAM,gBACJ,cAAc,aAAa,OACvB,OAAO,kCACP,OAAO,iCAAgC,CAC3C,MAAM,WAAW,OAAO,OAAO;CAEjC,WAAW,IAAI,WAAW,YAAY;CAEtC,OAAO;AACT;AAGA,IAAI,qBAAsD;AAE1D,MAAM,yBAAyB,YAAY;CACzC,IAAI,CAAC,oBACH,qBAAqB,OAAO,mBAAmB,CAAC,MAC7C,EAAE,wBACD,kBAAkB;EAChB,OAAO,CAAC;EACR,QAAQ,CAAC;CACX,CAAC,CACL;CAEF,OAAO;AACT;;;;;;;AAQA,MAAM,gBAAgB,OACpB,MACA,SACoB;CACpB,MAAM,mBAAmB,oBAAoB,IAAI;CAGjD,MAAM,CAAC,aAAa,gBAAgB,YAAY,aAC9C,MAAM,QAAQ,IAAI;EAChB,uBAAuB;EACvB,aAAa,gBAAgB;EAC7B,UAAU,aAAa,KAAK;EAC5B,UAAU,aAAa,IAAI;CAC7B,CAAC;CAGH,IACE,kBACA,CAAC,YAAY,mBAAmB,CAAC,CAAC,SAAS,iBAAiB,EAAE,GAE9D,MAAM,YAAY,aAAa,cAAc;CAG/C,MAAM,eAAe,YAAY,gBAAgB;CACjD,IAAI,CAAC,aAAa,SAAS,aAAa,KAAK,GAC3C,MAAM,YAAY,UAAU,UAAU;CAExC,IAAI,CAAC,aAAa,SAAS,aAAa,IAAI,GAC1C,MAAM,YAAY,UAAU,SAAS;CAGvC,OAAO,YAAY,WAAW,OAAO,IAAI,GAAG;EAC1C,MAAM,iBAAiB;EACvB,QAAQ;EACR,cAAc;CAChB,CAAC;AACH;AAOA,MAAa,kBAA2C,EAAE,UAAU,WAAW;CAC7E,MAAM,CAAC,MAAM,WAAW,SAAwB,IAAI;CAEpD,gBAAgB;EACd,IAAI,cAAc;EAElB,cAAc,UAAU,IAAI,CAAC,CAC1B,MAAM,WAAW;GAChB,IAAI,CAAC,aAAa,QAAQ,MAAM;EAClC,CAAC,CAAC,CACD,OAAO,UAAU;GAChB,QAAQ,MAAM,6BAA6B,KAAK;GAChD,IAAI,CAAC,aAAa,QAAQ,EAAE;EAC9B,CAAC;EAEH,aAAa;GACX,cAAc;EAChB;CACF,GAAG,CAAC,UAAU,IAAI,CAAC;CAEnB,OACE,oBAAC,OAAD;EAAK,WAAW;YACb,OAEC,oBAAC,OAAD,EAAK,yBAAyB,EAAE,QAAQ,KAAK,EAAI,KAEjD,oBAAC,aAAD,EAAc,SAAsB;CAEnC;AAET"}
|
|
@@ -9,7 +9,7 @@ import { useEffect, useState } from "react";
|
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
|
|
11
11
|
//#region src/components/IDE/IDE.tsx
|
|
12
|
-
const IDE = ({ pages: initialPages,
|
|
12
|
+
const IDE = ({ pages: initialPages, className, activeTab: defaultActiveTab, ...props }) => {
|
|
13
13
|
const [pages, setPages] = useState(initialPages);
|
|
14
14
|
const tabs = pages.filter(({ isOpen }) => isOpen);
|
|
15
15
|
const firstTabIndex = tabs.findIndex(({ isOpen }) => isOpen);
|
|
@@ -76,10 +76,7 @@ const IDE = ({ pages: initialPages, isDarkMode, className, activeTab: defaultAct
|
|
|
76
76
|
})
|
|
77
77
|
}), /* @__PURE__ */ jsx("div", {
|
|
78
78
|
className: "size-full flex-1 overflow-auto pt-2 text-xs",
|
|
79
|
-
children: /* @__PURE__ */ jsx(MarkdownRenderer, {
|
|
80
|
-
isDarkMode,
|
|
81
|
-
children: content
|
|
82
|
-
})
|
|
79
|
+
children: /* @__PURE__ */ jsx(MarkdownRenderer, { children: content })
|
|
83
80
|
})]
|
|
84
81
|
})
|
|
85
82
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IDE.mjs","names":[],"sources":["../../../../src/components/IDE/IDE.tsx"],"sourcesContent":["'use client';\n\nimport { cn } from '@utils/cn';\nimport { type FC, type HTMLAttributes, useEffect, useState } from 'react';\nimport { Container } from '../Container';\nimport { WithResizer } from '../WithResizer';\nimport { FileTree } from './FileTree';\nimport { MarkdownRenderer } from './MarkDownRender';\n\nexport type IDEProps = {\n pages: {\n path: string;\n content: string;\n isOpen?: boolean;\n }[];\n
|
|
1
|
+
{"version":3,"file":"IDE.mjs","names":[],"sources":["../../../../src/components/IDE/IDE.tsx"],"sourcesContent":["'use client';\n\nimport { cn } from '@utils/cn';\nimport { type FC, type HTMLAttributes, useEffect, useState } from 'react';\nimport { Container } from '../Container';\nimport { WithResizer } from '../WithResizer';\nimport { FileTree } from './FileTree';\nimport { MarkdownRenderer } from './MarkDownRender';\n\nexport type IDEProps = {\n pages: {\n path: string;\n content: string;\n isOpen?: boolean;\n }[];\n activeTab?: number;\n} & HTMLAttributes<HTMLDivElement>;\n\nexport const IDE: FC<IDEProps> = ({\n pages: initialPages,\n className,\n activeTab: defaultActiveTab,\n ...props\n}) => {\n const [pages, setPages] = useState(initialPages);\n const tabs = pages.filter(({ isOpen }) => isOpen);\n\n const firstTabIndex = tabs.findIndex(({ isOpen }) => isOpen);\n const [activeTab, setActiveTab] = useState(defaultActiveTab ?? firstTabIndex);\n\n useEffect(() => {\n setActiveTab(defaultActiveTab ?? firstTabIndex);\n }, [initialPages, defaultActiveTab]);\n\n const { content, path } = pages[activeTab] ?? {};\n const filePaths = initialPages.map(({ path: title }) => title);\n\n const handleClickFile = (title: string) => {\n const page = pages.find(({ path: tabTitle }) => tabTitle === title);\n if (!page) return;\n\n const newPages = pages.map((page) => {\n if (page.path === title) {\n return { ...page, isOpen: true };\n }\n return page;\n });\n\n setPages(newPages);\n\n const newPageIndex = newPages.findIndex(\n ({ path: tabTitle }) => tabTitle === title\n );\n\n setActiveTab(newPageIndex);\n };\n\n return (\n <Container\n className={cn(\n 'flex size-full flex-col justify-start overflow-hidden shadow-lg',\n className\n )}\n roundedSize=\"3xl\"\n transparency=\"none\"\n {...props}\n >\n <div className=\"flex w-auto flex-row items-center justify-start gap-1 bg-neutral-200 text-neutral text-xs dark:bg-neutral-950\">\n <div className=\"mx-2 flex items-center justify-start gap-2 p-1\">\n <div className=\"size-3 rounded-full bg-error\" />\n <div className=\"size-3 rounded-full bg-yellow-500\" />\n <div className=\"size-3 rounded-full bg-green-500\" />\n </div>\n <div className=\"flex size-full overflow-y-auto\">\n {tabs.map(({ path }, index) => {\n const fullPath = path.split('/');\n const title = fullPath[fullPath.length - 1];\n const isActive = index === activeTab;\n\n return (\n <button\n className={cn(\n 'flex h-8 min-w-20 max-w-30 items-center justify-start truncate text-nowrap px-3 py-1 transition',\n isActive\n ? 'bg-card'\n : 'cursor-pointer bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-950'\n )}\n key={title}\n onClick={() => setActiveTab(index)}\n >\n {title}\n </button>\n );\n })}\n </div>\n </div>\n <div className=\"relative flex size-full flex-1 flex-row justify-start\">\n <div className=\"absolute top-0 left-0 size-full\">\n <div className=\"flex size-full\">\n <WithResizer initialWidth={150}>\n <div className=\"max-h-full flex-1 overflow-y-auto\">\n <FileTree\n filesPaths={filePaths}\n activeFile={path}\n onClickFile={handleClickFile}\n />\n </div>\n </WithResizer>\n\n <div className=\"size-full flex-1 overflow-auto pt-2 text-xs\">\n <MarkdownRenderer>{content}</MarkdownRenderer>\n </div>\n </div>\n </div>\n </div>\n </Container>\n );\n};\n"],"mappings":";;;;;;;;;;;AAkBA,MAAa,OAAqB,EAChC,OAAO,cACP,WACA,WAAW,kBACX,GAAG,YACC;CACJ,MAAM,CAAC,OAAO,YAAY,SAAS,YAAY;CAC/C,MAAM,OAAO,MAAM,QAAQ,EAAE,aAAa,MAAM;CAEhD,MAAM,gBAAgB,KAAK,WAAW,EAAE,aAAa,MAAM;CAC3D,MAAM,CAAC,WAAW,gBAAgB,SAAS,oBAAoB,aAAa;CAE5E,gBAAgB;EACd,aAAa,oBAAoB,aAAa;CAChD,GAAG,CAAC,cAAc,gBAAgB,CAAC;CAEnC,MAAM,EAAE,SAAS,SAAS,MAAM,cAAc,CAAC;CAC/C,MAAM,YAAY,aAAa,KAAK,EAAE,MAAM,YAAY,KAAK;CAE7D,MAAM,mBAAmB,UAAkB;EAEzC,IAAI,CADS,MAAM,MAAM,EAAE,MAAM,eAAe,aAAa,KACrD,GAAG;EAEX,MAAM,WAAW,MAAM,KAAK,SAAS;GACnC,IAAI,KAAK,SAAS,OAChB,OAAO;IAAE,GAAG;IAAM,QAAQ;GAAK;GAEjC,OAAO;EACT,CAAC;EAED,SAAS,QAAQ;EAEjB,MAAM,eAAe,SAAS,WAC3B,EAAE,MAAM,eAAe,aAAa,KACvC;EAEA,aAAa,YAAY;CAC3B;CAEA,OACE,qBAAC,WAAD;EACE,WAAW,GACT,mEACA,SACF;EACA,aAAY;EACZ,cAAa;EACb,GAAI;YAPN,CASE,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,qBAAC,OAAD;IAAK,WAAU;cAAf;KACE,oBAAC,OAAD,EAAK,WAAU,+BAAgC;KAC/C,oBAAC,OAAD,EAAK,WAAU,oCAAqC;KACpD,oBAAC,OAAD,EAAK,WAAU,mCAAoC;IAChD;OACL,oBAAC,OAAD;IAAK,WAAU;cACZ,KAAK,KAAK,EAAE,QAAQ,UAAU;KAC7B,MAAM,WAAW,KAAK,MAAM,GAAG;KAC/B,MAAM,QAAQ,SAAS,SAAS,SAAS;KAGzC,OACE,oBAAC,UAAD;MACE,WAAW,GACT,mGALW,UAAU,YAOjB,YACA,wEACN;MAEA,eAAe,aAAa,KAAK;gBAEhC;KACK,GAJD,KAIC;IAEZ,CAAC;GACE,EACF;MACL,oBAAC,OAAD;GAAK,WAAU;aACb,oBAAC,OAAD;IAAK,WAAU;cACb,qBAAC,OAAD;KAAK,WAAU;eAAf,CACE,oBAAC,aAAD;MAAa,cAAc;gBACzB,oBAAC,OAAD;OAAK,WAAU;iBACb,oBAAC,UAAD;QACE,YAAY;QACZ,YAAY;QACZ,aAAa;OACd;MACE;KACM,IAEb,oBAAC,OAAD;MAAK,WAAU;gBACb,oBAAC,kBAAD,YAAmB,QAA0B;KAC1C,EACF;;GACF;EACF,EACI;;AAEf"}
|
|
@@ -3,11 +3,10 @@ import { jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { MarkdownRenderer as MarkdownRenderer$1 } from "react-intlayer";
|
|
4
4
|
|
|
5
5
|
//#region src/components/IDE/MarkDownRender.tsx
|
|
6
|
-
const MarkdownRenderer = ({ children
|
|
6
|
+
const MarkdownRenderer = ({ children }) => /* @__PURE__ */ jsx(MarkdownRenderer$1, {
|
|
7
7
|
components: {
|
|
8
8
|
code: (props) => /* @__PURE__ */ jsx(Code, {
|
|
9
9
|
...props,
|
|
10
|
-
isDarkMode,
|
|
11
10
|
language: props.className?.replace("lang-", ""),
|
|
12
11
|
showHeader: false,
|
|
13
12
|
className: "text-xs leading-5",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkDownRender.mjs","names":["MarkdownRendererIntlayer"],"sources":["../../../../src/components/IDE/MarkDownRender.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { MarkdownRenderer as MarkdownRendererIntlayer } from 'react-intlayer';\nimport { Code } from './Code';\nimport type { CodeLanguage } from './shikiLanguages';\n\ntype MarkdownRendererProps = {\n
|
|
1
|
+
{"version":3,"file":"MarkDownRender.mjs","names":["MarkdownRendererIntlayer"],"sources":["../../../../src/components/IDE/MarkDownRender.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { MarkdownRenderer as MarkdownRendererIntlayer } from 'react-intlayer';\nimport { Code } from './Code';\nimport type { CodeLanguage } from './shikiLanguages';\n\ntype MarkdownRendererProps = {\n children: string;\n};\n\nexport const MarkdownRenderer: FC<MarkdownRendererProps> = ({ children }) => (\n <MarkdownRendererIntlayer\n components={{\n code: (props) => (\n <Code\n {...props}\n language={props.className?.replace('lang-', '') as CodeLanguage}\n showHeader={false}\n className=\"text-xs leading-5\"\n >\n {props.children as string}\n </Code>\n ),\n pre: (props) => props.children,\n }}\n >\n {children ?? ''}\n </MarkdownRendererIntlayer>\n);\n"],"mappings":";;;;;AASA,MAAa,oBAA+C,EAAE,eAC5D,oBAACA,oBAAD;CACE,YAAY;EACV,OAAO,UACL,oBAAC,MAAD;GACE,GAAI;GACJ,UAAU,MAAM,WAAW,QAAQ,SAAS,EAAE;GAC9C,YAAY;GACZ,WAAU;aAET,MAAM;EACH;EAER,MAAM,UAAU,MAAM;CACxB;WAEC,YAAY;AACW"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CodeBlock, CodeDefault } from "./CodeBlockClient.mjs";
|
|
1
|
+
import { CodeBlock, CodeDefault, CodeHighlighted, shikiWrapperClassName } from "./CodeBlockClient.mjs";
|
|
2
2
|
import { Code } from "./Code.mjs";
|
|
3
3
|
import { FileList } from "./FileList.mjs";
|
|
4
4
|
import { IDE } from "./IDE.mjs";
|
|
5
5
|
|
|
6
|
-
export { Code, CodeBlock, CodeDefault, FileList, IDE };
|
|
6
|
+
export { Code, CodeBlock, CodeDefault, CodeHighlighted, FileList, IDE, shikiWrapperClassName };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/components/IDE/shikiThemes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Themes every code block is highlighted with.
|
|
4
|
+
*
|
|
5
|
+
* Shiki is always asked for both, with `defaultColor: false`, so each token
|
|
6
|
+
* carries its light and dark colour as the `--shiki-light` / `--shiki-dark`
|
|
7
|
+
* custom properties rather than a resolved `color`. The markup is therefore
|
|
8
|
+
* theme-agnostic: it can be produced once — at build time — and never has to be
|
|
9
|
+
* recomputed in the browser when the resolved theme changes.
|
|
10
|
+
*/
|
|
11
|
+
const SHIKI_THEMES = {
|
|
12
|
+
light: "github-light",
|
|
13
|
+
dark: "github-dark"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { SHIKI_THEMES };
|
|
18
|
+
//# sourceMappingURL=shikiThemes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shikiThemes.mjs","names":[],"sources":["../../../../src/components/IDE/shikiThemes.ts"],"sourcesContent":["/**\n * Themes every code block is highlighted with.\n *\n * Shiki is always asked for both, with `defaultColor: false`, so each token\n * carries its light and dark colour as the `--shiki-light` / `--shiki-dark`\n * custom properties rather than a resolved `color`. The markup is therefore\n * theme-agnostic: it can be produced once — at build time — and never has to be\n * recomputed in the browser when the resolved theme changes.\n */\nexport const SHIKI_THEMES = {\n light: 'github-light',\n dark: 'github-dark',\n} as const;\n\nexport type ShikiThemes = typeof SHIKI_THEMES;\n"],"mappings":";;;;;;;;;;AASA,MAAa,eAAe;CAC1B,OAAO;CACP,MAAM;AACR"}
|
|
@@ -5,9 +5,9 @@ import { Button } from "../Button/Button.mjs";
|
|
|
5
5
|
import { Link } from "../Link/Link.mjs";
|
|
6
6
|
import { Container } from "../Container/index.mjs";
|
|
7
7
|
import { Modal } from "../Modal/Modal.mjs";
|
|
8
|
-
import { useState } from "react";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import { MoveDiagonal } from "lucide-react";
|
|
10
|
+
import { MoveDiagonal, Play } from "lucide-react";
|
|
11
11
|
|
|
12
12
|
//#region src/components/MarkDownRender/MarkDownIframe.tsx
|
|
13
13
|
function embedLinkMeta(src) {
|
|
@@ -32,6 +32,107 @@ function embedLinkMeta(src) {
|
|
|
32
32
|
label: src
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads the video id out of a YouTube embed address.
|
|
37
|
+
*
|
|
38
|
+
* @returns The video id, or `null` when the address is not a YouTube embed.
|
|
39
|
+
*/
|
|
40
|
+
const getYoutubeVideoId = (src) => {
|
|
41
|
+
if (!src || !/^https?:\/\//i.test(src)) return null;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(src);
|
|
44
|
+
if (!/(^|\.)youtube(-nocookie)?\.com$/.test(url.hostname)) return null;
|
|
45
|
+
return url.pathname.replace(/^\/embed\//, "").split("/")[0] || null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Tracks whether an element has come close enough to the viewport to be worth
|
|
52
|
+
* loading.
|
|
53
|
+
*
|
|
54
|
+
* `loading="lazy"` is not enough for embeds inside tabs or carousels: the
|
|
55
|
+
* browser only defers frames that are far down the page, so a frame parked
|
|
56
|
+
* off-screen by a transform — an inactive tab panel — still loads immediately.
|
|
57
|
+
* A live-app embed costs upwards of a megabyte and seconds of main thread,
|
|
58
|
+
* which is the whole page's performance budget spent on something nobody is
|
|
59
|
+
* looking at.
|
|
60
|
+
*
|
|
61
|
+
* @returns A ref to attach to the placeholder, and whether it became visible.
|
|
62
|
+
*/
|
|
63
|
+
const useHasBecomeVisible = () => {
|
|
64
|
+
const elementRef = useRef(null);
|
|
65
|
+
const [hasBecomeVisible, setHasBecomeVisible] = useState(false);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const element = elementRef.current;
|
|
68
|
+
if (!element || hasBecomeVisible) return;
|
|
69
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
70
|
+
setHasBecomeVisible(true);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const observer = new IntersectionObserver((entries) => {
|
|
74
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
75
|
+
setHasBecomeVisible(true);
|
|
76
|
+
observer.disconnect();
|
|
77
|
+
}
|
|
78
|
+
}, { rootMargin: "200px" });
|
|
79
|
+
observer.observe(element);
|
|
80
|
+
return () => observer.disconnect();
|
|
81
|
+
}, [hasBecomeVisible]);
|
|
82
|
+
return {
|
|
83
|
+
elementRef,
|
|
84
|
+
hasBecomeVisible
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The embed itself: a poster for YouTube, deferred loading for anything else.
|
|
89
|
+
*
|
|
90
|
+
* A YouTube player pulls well over a megabyte of script before it can show a
|
|
91
|
+
* single frame. Standing in for it with its own thumbnail until the reader
|
|
92
|
+
* presses play keeps that cost on the readers who actually watch the video.
|
|
93
|
+
*/
|
|
94
|
+
const EmbeddedFrame = ({ src, title, className, frameProps }) => {
|
|
95
|
+
const [isPlayRequested, setIsPlayRequested] = useState(false);
|
|
96
|
+
const { elementRef, hasBecomeVisible } = useHasBecomeVisible();
|
|
97
|
+
const youtubeVideoId = getYoutubeVideoId(src);
|
|
98
|
+
if (youtubeVideoId && !isPlayRequested) return /* @__PURE__ */ jsxs("button", {
|
|
99
|
+
type: "button",
|
|
100
|
+
"aria-label": title ?? "Play video",
|
|
101
|
+
onClick: () => setIsPlayRequested(true),
|
|
102
|
+
className: cn("group relative block cursor-pointer border-0 bg-card p-0", className),
|
|
103
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
104
|
+
src: `https://i.ytimg.com/vi/${youtubeVideoId}/hqdefault.jpg`,
|
|
105
|
+
alt: "",
|
|
106
|
+
fetchPriority: "high",
|
|
107
|
+
decoding: "async",
|
|
108
|
+
className: "size-full object-cover"
|
|
109
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
110
|
+
className: "absolute inset-0 flex items-center justify-center",
|
|
111
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
112
|
+
className: "flex size-14 items-center justify-center rounded-full bg-text/70 text-text-opposite transition-transform group-hover:scale-110",
|
|
113
|
+
children: /* @__PURE__ */ jsx(Play, { className: "size-6 translate-x-0.5 fill-current" })
|
|
114
|
+
})
|
|
115
|
+
})]
|
|
116
|
+
});
|
|
117
|
+
if (!hasBecomeVisible && !isPlayRequested) return /* @__PURE__ */ jsx("div", {
|
|
118
|
+
ref: elementRef,
|
|
119
|
+
"aria-hidden": true,
|
|
120
|
+
className
|
|
121
|
+
});
|
|
122
|
+
const frameSrc = (() => {
|
|
123
|
+
if (!isPlayRequested || !youtubeVideoId || !src) return src;
|
|
124
|
+
const playbackSrc = new URL(src);
|
|
125
|
+
playbackSrc.searchParams.set("autoplay", "1");
|
|
126
|
+
return playbackSrc.href;
|
|
127
|
+
})();
|
|
128
|
+
return /* @__PURE__ */ jsx("iframe", {
|
|
129
|
+
...frameProps,
|
|
130
|
+
src: frameSrc,
|
|
131
|
+
title,
|
|
132
|
+
loading: "lazy",
|
|
133
|
+
className
|
|
134
|
+
});
|
|
135
|
+
};
|
|
35
136
|
const MarkDownIframe = (props) => {
|
|
36
137
|
const { src, className, title, ...rest } = props;
|
|
37
138
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
@@ -43,10 +144,10 @@ const MarkDownIframe = (props) => {
|
|
|
43
144
|
className: "overflow-hidden p-0",
|
|
44
145
|
gap: "none",
|
|
45
146
|
children: [
|
|
46
|
-
/* @__PURE__ */ jsx(
|
|
47
|
-
...rest,
|
|
147
|
+
/* @__PURE__ */ jsx(EmbeddedFrame, {
|
|
48
148
|
src,
|
|
49
149
|
title,
|
|
150
|
+
frameProps: rest,
|
|
50
151
|
className: cn("block max-h-[80vh] min-h-[12rem] w-full border-0", className)
|
|
51
152
|
}),
|
|
52
153
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkDownIframe.mjs","names":[],"sources":["../../../../src/components/MarkDownRender/MarkDownIframe.tsx"],"sourcesContent":["'use client';\n\nimport { Button } from '@components/Button';\nimport { Container } from '@components/Container';\nimport { Link } from '@components/Link';\nimport { Modal } from '@components/Modal';\nimport { cn } from '@utils/cn';\nimport { MoveDiagonal } from 'lucide-react';\nimport { type ComponentProps, type FC, useState } from 'react';\n\nfunction embedLinkMeta(src: string | undefined): {\n href: string;\n label: string;\n} {\n if (!src) return { href: '', label: '' };\n if (/^https?:\\/\\//i.test(src)) {\n try {\n const url = new URL(src);\n return { href: url.href, label: url.host };\n } catch {\n return { href: src, label: src };\n }\n }\n return { href: src, label: src };\n}\n\nexport const MarkDownIframe: FC<ComponentProps<'iframe'>> = (props) => {\n const { src, className, title, ...rest } = props;\n const [isModalOpen, setIsModalOpen] = useState(false);\n const { href, label } = embedLinkMeta(src);\n\n return (\n <Container\n roundedSize=\"2xl\"\n border\n borderColor=\"neutral\"\n className=\"overflow-hidden p-0\"\n gap=\"none\"\n >\n <iframe\n {...rest}\n src={src}\n title={title}\n className={cn(\n 'block max-h-[80vh] min-h-[12rem] w-full border-0',\n className\n )}\n />\n <div className=\"flex items-center justify-between gap-3 px-3 py-1\">\n {href ? (\n <Link\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n label=\"\"\n color=\"neutral\"\n className=\"inline-flex min-w-0 max-w-[calc(100%-3rem)] items-center gap-2 text-neutral text-xs underline-offset-2 hover:text-text hover:underline\"\n >\n {label}\n </Link>\n ) : (\n <span className=\"text-neutral text-sm\">Embedded frame</span>\n )}\n <Button\n variant=\"hoverable\"\n size=\"icon-md\"\n onClick={() => setIsModalOpen(true)}\n label=\"Open embedded page in fullscreen\"\n Icon={MoveDiagonal}\n />\n </div>\n\n <Modal\n isOpen={isModalOpen}\n onClose={() => setIsModalOpen(false)}\n size=\"unset\"\n hasCloseButton\n isScrollable\n padding=\"sm\"\n >\n {isModalOpen && src ? (\n <Container\n roundedSize=\"2xl\"\n border\n borderColor=\"neutral\"\n className=\"overflow-hidden p-0\"\n gap=\"none\"\n >\n <iframe\n {...rest}\n src={src}\n title={title ?? 'Embedded content'}\n allowFullScreen\n className=\"block min-h-[82vh] w-full border-0\"\n />\n </Container>\n ) : null}\n </Modal>\n </Container>\n );\n};\n"],"mappings":";;;;;;;;;;;;AAUA,SAAS,cAAc,KAGrB;CACA,IAAI,CAAC,KAAK,OAAO;EAAE,MAAM;EAAI,OAAO;CAAG;CACvC,IAAI,gBAAgB,KAAK,GAAG,GAC1B,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,GAAG;EACvB,OAAO;GAAE,MAAM,IAAI;GAAM,OAAO,IAAI;EAAK;CAC3C,QAAQ;EACN,OAAO;GAAE,MAAM;GAAK,OAAO;EAAI;CACjC;CAEF,OAAO;EAAE,MAAM;EAAK,OAAO;CAAI;AACjC;AAEA,MAAa,kBAAgD,UAAU;CACrE,MAAM,EAAE,KAAK,WAAW,OAAO,GAAG,SAAS;CAC3C,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CACpD,MAAM,EAAE,MAAM,UAAU,cAAc,GAAG;CAEzC,OACE,qBAAC,WAAD;EACE,aAAY;EACZ;EACA,aAAY;EACZ,WAAU;EACV,KAAI;YALN;GAOE,oBAAC,UAAD;IACE,GAAI;IACC;IACE;IACP,WAAW,GACT,oDACA,SACF;GACD;GACD,qBAAC,OAAD;IAAK,WAAU;cAAf,CACG,OACC,oBAAC,MAAD;KACQ;KACN,QAAO;KACP,KAAI;KACJ,OAAM;KACN,OAAM;KACN,WAAU;eAET;IACG,KAEN,oBAAC,QAAD;KAAM,WAAU;eAAuB;IAAoB,IAE7D,oBAAC,QAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,eAAe,IAAI;KAClC,OAAM;KACN,MAAM;IACP,EACE;;GAEL,oBAAC,OAAD;IACE,QAAQ;IACR,eAAe,eAAe,KAAK;IACnC,MAAK;IACL;IACA;IACA,SAAQ;cAEP,eAAe,MACd,oBAAC,WAAD;KACE,aAAY;KACZ;KACA,aAAY;KACZ,WAAU;KACV,KAAI;eAEJ,oBAAC,UAAD;MACE,GAAI;MACC;MACL,OAAO,SAAS;MAChB;MACA,WAAU;KACX;IACQ,KACT;GACC;EACE;;AAEf"}
|
|
1
|
+
{"version":3,"file":"MarkDownIframe.mjs","names":[],"sources":["../../../../src/components/MarkDownRender/MarkDownIframe.tsx"],"sourcesContent":["'use client';\n\nimport { Button } from '@components/Button';\nimport { Container } from '@components/Container';\nimport { Link } from '@components/Link';\nimport { Modal } from '@components/Modal';\nimport { cn } from '@utils/cn';\nimport { MoveDiagonal, Play } from 'lucide-react';\nimport {\n type ComponentProps,\n type FC,\n useEffect,\n useRef,\n useState,\n} from 'react';\n\nfunction embedLinkMeta(src: string | undefined): {\n href: string;\n label: string;\n} {\n if (!src) return { href: '', label: '' };\n if (/^https?:\\/\\//i.test(src)) {\n try {\n const url = new URL(src);\n return { href: url.href, label: url.host };\n } catch {\n return { href: src, label: src };\n }\n }\n return { href: src, label: src };\n}\n\n/**\n * Reads the video id out of a YouTube embed address.\n *\n * @returns The video id, or `null` when the address is not a YouTube embed.\n */\nconst getYoutubeVideoId = (src: string | undefined): string | null => {\n if (!src || !/^https?:\\/\\//i.test(src)) return null;\n\n try {\n const url = new URL(src);\n if (!/(^|\\.)youtube(-nocookie)?\\.com$/.test(url.hostname)) return null;\n\n const videoId = url.pathname.replace(/^\\/embed\\//, '').split('/')[0];\n\n return videoId || null;\n } catch {\n return null;\n }\n};\n\n/**\n * Tracks whether an element has come close enough to the viewport to be worth\n * loading.\n *\n * `loading=\"lazy\"` is not enough for embeds inside tabs or carousels: the\n * browser only defers frames that are far down the page, so a frame parked\n * off-screen by a transform — an inactive tab panel — still loads immediately.\n * A live-app embed costs upwards of a megabyte and seconds of main thread,\n * which is the whole page's performance budget spent on something nobody is\n * looking at.\n *\n * @returns A ref to attach to the placeholder, and whether it became visible.\n */\nconst useHasBecomeVisible = <ElementType extends HTMLElement>() => {\n const elementRef = useRef<ElementType>(null);\n const [hasBecomeVisible, setHasBecomeVisible] = useState(false);\n\n useEffect(() => {\n const element = elementRef.current;\n if (!element || hasBecomeVisible) return;\n\n if (typeof IntersectionObserver === 'undefined') {\n setHasBecomeVisible(true);\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (entries.some((entry) => entry.isIntersecting)) {\n setHasBecomeVisible(true);\n observer.disconnect();\n }\n },\n { rootMargin: '200px' }\n );\n\n observer.observe(element);\n\n return () => observer.disconnect();\n }, [hasBecomeVisible]);\n\n return { elementRef, hasBecomeVisible };\n};\n\ntype EmbeddedFrameProps = {\n src: string | undefined;\n title: string | undefined;\n className: string;\n frameProps: Omit<ComponentProps<'iframe'>, 'src' | 'title' | 'className'>;\n};\n\n/**\n * The embed itself: a poster for YouTube, deferred loading for anything else.\n *\n * A YouTube player pulls well over a megabyte of script before it can show a\n * single frame. Standing in for it with its own thumbnail until the reader\n * presses play keeps that cost on the readers who actually watch the video.\n */\nconst EmbeddedFrame: FC<EmbeddedFrameProps> = ({\n src,\n title,\n className,\n frameProps,\n}) => {\n const [isPlayRequested, setIsPlayRequested] = useState(false);\n const { elementRef, hasBecomeVisible } =\n useHasBecomeVisible<HTMLDivElement>();\n const youtubeVideoId = getYoutubeVideoId(src);\n\n if (youtubeVideoId && !isPlayRequested) {\n return (\n <button\n type=\"button\"\n aria-label={title ?? 'Play video'}\n onClick={() => setIsPlayRequested(true)}\n className={cn(\n 'group relative block cursor-pointer border-0 bg-card p-0',\n className\n )}\n >\n {/*\n * The poster stands in for the player, so on a page that opens with a\n * video it is the largest element painted — deferring it would defer\n * the page's LCP. It is the one image here worth fetching eagerly.\n */}\n <img\n src={`https://i.ytimg.com/vi/${youtubeVideoId}/hqdefault.jpg`}\n alt=\"\"\n fetchPriority=\"high\"\n decoding=\"async\"\n className=\"size-full object-cover\"\n />\n <span className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"flex size-14 items-center justify-center rounded-full bg-text/70 text-text-opposite transition-transform group-hover:scale-110\">\n <Play className=\"size-6 translate-x-0.5 fill-current\" />\n </span>\n </span>\n </button>\n );\n }\n\n if (!hasBecomeVisible && !isPlayRequested) {\n // Same box as the frame it stands in for, so nothing shifts once it loads.\n return <div ref={elementRef} aria-hidden className={className} />;\n }\n\n const frameSrc = (() => {\n if (!isPlayRequested || !youtubeVideoId || !src) return src;\n\n const playbackSrc = new URL(src);\n playbackSrc.searchParams.set('autoplay', '1');\n\n return playbackSrc.href;\n })();\n\n return (\n <iframe\n {...frameProps}\n src={frameSrc}\n title={title}\n loading=\"lazy\"\n className={className}\n />\n );\n};\n\nexport const MarkDownIframe: FC<ComponentProps<'iframe'>> = (props) => {\n const { src, className, title, ...rest } = props;\n const [isModalOpen, setIsModalOpen] = useState(false);\n const { href, label } = embedLinkMeta(src);\n\n return (\n <Container\n roundedSize=\"2xl\"\n border\n borderColor=\"neutral\"\n className=\"overflow-hidden p-0\"\n gap=\"none\"\n >\n <EmbeddedFrame\n src={src}\n title={title}\n frameProps={rest}\n className={cn(\n 'block max-h-[80vh] min-h-[12rem] w-full border-0',\n className\n )}\n />\n <div className=\"flex items-center justify-between gap-3 px-3 py-1\">\n {href ? (\n <Link\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n label=\"\"\n color=\"neutral\"\n className=\"inline-flex min-w-0 max-w-[calc(100%-3rem)] items-center gap-2 text-neutral text-xs underline-offset-2 hover:text-text hover:underline\"\n >\n {label}\n </Link>\n ) : (\n <span className=\"text-neutral text-sm\">Embedded frame</span>\n )}\n <Button\n variant=\"hoverable\"\n size=\"icon-md\"\n onClick={() => setIsModalOpen(true)}\n label=\"Open embedded page in fullscreen\"\n Icon={MoveDiagonal}\n />\n </div>\n\n <Modal\n isOpen={isModalOpen}\n onClose={() => setIsModalOpen(false)}\n size=\"unset\"\n hasCloseButton\n isScrollable\n padding=\"sm\"\n >\n {isModalOpen && src ? (\n <Container\n roundedSize=\"2xl\"\n border\n borderColor=\"neutral\"\n className=\"overflow-hidden p-0\"\n gap=\"none\"\n >\n <iframe\n {...rest}\n src={src}\n title={title ?? 'Embedded content'}\n allowFullScreen\n className=\"block min-h-[82vh] w-full border-0\"\n />\n </Container>\n ) : null}\n </Modal>\n </Container>\n );\n};\n"],"mappings":"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"}
|
|
@@ -47,7 +47,7 @@ const StrongRenderer = (props) => /* @__PURE__ */ jsx("strong", {
|
|
|
47
47
|
className: "text-text",
|
|
48
48
|
...props
|
|
49
49
|
});
|
|
50
|
-
const MemoizedCodeBlock = memo(({ className, children,
|
|
50
|
+
const MemoizedCodeBlock = memo(({ className, children, highlightedHtml, lang, ...rest }) => {
|
|
51
51
|
const content = String(children ?? "").replace(/\n$/, "");
|
|
52
52
|
if (!!!className) {
|
|
53
53
|
const decodedContent = content.replace(/&(?:amp;)?#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
|
|
@@ -61,18 +61,18 @@ const MemoizedCodeBlock = memo(({ className, children, isDarkMode, lang, ...rest
|
|
|
61
61
|
...rest,
|
|
62
62
|
language,
|
|
63
63
|
showHeader: true,
|
|
64
|
-
|
|
64
|
+
highlightedHtml,
|
|
65
65
|
children: content
|
|
66
66
|
});
|
|
67
|
-
}, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.className === nextProps.className && prevProps.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
67
|
+
}, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.className === nextProps.className && prevProps.highlightedHtml === nextProps.highlightedHtml);
|
|
68
|
+
/**
|
|
69
|
+
* Renders a fenced code block.
|
|
70
|
+
*
|
|
71
|
+
* `highlightedHtml` reaches this renderer through the code node's attributes:
|
|
72
|
+
* a pipeline that highlights the markdown ahead of time writes it there, and
|
|
73
|
+
* the block then paints without loading the highlighter in the browser.
|
|
74
|
+
*/
|
|
75
|
+
const CodeRenderer = (props) => /* @__PURE__ */ jsx(MemoizedCodeBlock, { ...props });
|
|
76
76
|
const BlockquoteRenderer = ({ className, ...props }) => /* @__PURE__ */ jsx("blockquote", {
|
|
77
77
|
className: cn("mt-5 gap-3 border-card border-l-4 pl-5 text-neutral [&_strong]:text-neutral", className),
|
|
78
78
|
...props
|
|
@@ -146,15 +146,15 @@ const staticMarkdownComponents = {
|
|
|
146
146
|
Column: ColumnRenderer,
|
|
147
147
|
iframe: Iframe
|
|
148
148
|
};
|
|
149
|
-
const createMarkdownComponents = (
|
|
149
|
+
const createMarkdownComponents = (locale) => ({
|
|
150
150
|
...staticMarkdownComponents,
|
|
151
|
-
code:
|
|
151
|
+
code: CodeRenderer,
|
|
152
152
|
a: createLinkRenderer(locale)
|
|
153
153
|
});
|
|
154
154
|
const baseMarkdownComponents = staticMarkdownComponents;
|
|
155
|
-
const getIntlayerMarkdownOptions = (
|
|
156
|
-
const MarkdownRenderer = ({ children,
|
|
157
|
-
const markdownComponents = createMarkdownComponents(
|
|
155
|
+
const getIntlayerMarkdownOptions = () => ({ components: baseMarkdownComponents });
|
|
156
|
+
const MarkdownRenderer = ({ children, locale, forceBlock, preserveFrontmatter, tagfilter, components: componentsProp, wrapper }) => {
|
|
157
|
+
const markdownComponents = createMarkdownComponents(locale);
|
|
158
158
|
const markdownContent = renderMarkdown(children, {
|
|
159
159
|
components: {
|
|
160
160
|
...markdownComponents,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkDownRender.mjs","names":[],"sources":["../../../../src/components/MarkDownRender/MarkDownRender.tsx"],"sourcesContent":["import type { LocalesValues } from '@intlayer/types/module_augmentation';\nimport { cn } from '@utils/cn';\nimport type { ComponentProps, ComponentPropsWithoutRef, FC } from 'react';\nimport { memo } from 'react';\nimport {\n type MarkdownRenderer as MarkdownRendererIntlayer,\n type ParsedMarkdown,\n renderMarkdown,\n} from 'react-intlayer/markdown';\nimport { H1, H2, H3, H4, H5, H6 } from '../Headers';\nimport { Code } from '../IDE/Code';\nimport { CodeProvider } from '../IDE/CodeContext';\nimport type { CodeLanguage } from '../IDE/shikiLanguages';\nimport { Link } from '../Link';\nimport { Tab } from '../Tab';\nimport { TabProvider } from '../Tab/TabContext';\nimport { Hr, SmartTable, Td, Th, Tr } from '../Table';\nimport { MarkDownIframe } from './MarkDownIframe';\n\nexport type { ParsedMarkdown };\n\n// Extracted, stable component renderers\nconst H1Renderer = (props: ComponentProps<'h1'>) => (\n <H1 isClickable className=\"mb-16 mb-8 text-text\" {...props} />\n);\nconst H2Renderer = (props: ComponentProps<'h2'>) => (\n <H2 isClickable className=\"mt-16 text-text\" {...props} />\n);\nconst H3Renderer = (props: ComponentProps<'h3'>) => (\n <H3 isClickable className=\"mt-5 text-text\" {...props} />\n);\nconst H4Renderer = (props: ComponentProps<'h4'>) => (\n <H4 isClickable className=\"mt-3 text-text\" {...props} />\n);\nconst H5Renderer = (props: ComponentProps<'h5'>) => (\n <H5 isClickable className=\"mt-3 text-text\" {...props} />\n);\nconst H6Renderer = (props: ComponentProps<'h6'>) => (\n <H6 isClickable className=\"mt-3 text-text\" {...props} />\n);\nconst StrongRenderer = (props: ComponentProps<'strong'>) => (\n <strong className=\"text-text\" {...props} />\n);\n\nconst MemoizedCodeBlock = memo(\n ({\n className,\n children,\n isDarkMode,\n lang, // Destructure html lang prop to prevent passing invalid BCP-47 language tag to Code component\n ...rest\n }: ComponentProps<'code'> & { isDarkMode?: boolean }) => {\n const content = String(children ?? '').replace(/\\n$/, '');\n const isBlock = !!className;\n\n if (!isBlock) {\n const decodedContent = content.replace(\n /&(?:amp;)?#(\\d+);/g,\n (_, code: string) => String.fromCharCode(parseInt(code, 10))\n );\n return (\n <code className=\"rounded-md border border-neutral/30 bg-card/60 box-decoration-clone px-1.5 py-0.5 font-mono text-[0.875em] leading-[inherit]\">\n {decodedContent}\n </code>\n );\n }\n\n const language = (className?.replace(/lang(?:uage)?-/, '') ||\n 'plaintext') as CodeLanguage;\n\n return (\n <Code {...rest} language={language} showHeader isDarkMode={isDarkMode}>\n {content}\n </Code>\n );\n },\n (prevProps, nextProps) =>\n prevProps.children === nextProps.children &&\n prevProps.className === nextProps.className &&\n prevProps.isDarkMode === nextProps.isDarkMode\n);\n\nconst createCodeRenderer = (isDarkMode?: boolean) => {\n return function CodeWrapper(props: ComponentProps<'code'>) {\n return <MemoizedCodeBlock {...props} isDarkMode={isDarkMode} />;\n };\n};\n\nconst BlockquoteRenderer = ({\n className,\n ...props\n}: ComponentProps<'blockquote'>) => (\n <blockquote\n className={cn(\n 'mt-5 gap-3 border-card border-l-4 pl-5 text-neutral [&_strong]:text-neutral',\n className\n )}\n {...props}\n />\n);\n\nconst UlRenderer = ({ className, ...props }: ComponentProps<'ul'>) => (\n <ul\n className={cn(\n 'mt-5 flex list-disc flex-col gap-3 pl-5 marker:text-neutral/80',\n className\n )}\n {...props}\n />\n);\n\nconst OlRenderer = ({ className, ...props }: ComponentProps<'ol'>) => (\n <ol\n className={cn(\n 'mt-5 flex list-decimal flex-col gap-3 pl-5 marker:text-neutral/80',\n className\n )}\n {...props}\n />\n);\n\nconst ImgRenderer = ({\n className,\n alt,\n src,\n ...props\n}: ComponentProps<'img'>) => (\n <img\n {...props}\n alt={alt ?? ''}\n loading=\"lazy\"\n className={cn('max-h-[80vh] max-w-full rounded-md', className)}\n src={\n src?.includes('github.com')\n ? src\n ?.replace('github.com', 'raw.githubusercontent.com')\n .replace('/blob/', '/') // GitHub raw URLs do not use /blob/\n : src\n }\n />\n);\n\nconst createLinkRenderer = (locale?: LocalesValues) => {\n return (props: ComponentProps<'a'>) => (\n <Link\n isExternalLink={props.href?.startsWith('http')}\n underlined\n locale={locale}\n label=\"\"\n color=\"text\"\n {...(props as any)}\n />\n );\n};\n\nconst PreRenderer = (props: ComponentProps<'pre'>) => <>{props.children}</>;\nconst TableRenderer = (props: ComponentProps<typeof SmartTable>) => (\n <SmartTable isRollable displayModal {...props} />\n);\n\nconst TabsRenderer = (props: ComponentProps<typeof Tab>) => (\n <Tab\n {...props}\n className=\"rounded-xl border border-card\"\n headerClassName=\"sticky rounded-xl top-24 z-5 bg-background/70 backdrop-blur overflow-x-auto\"\n />\n);\nconst ColumnsRenderer = ({\n className,\n ...props\n}: ComponentPropsWithoutRef<'div'>) => (\n <div className={cn('flex gap-4 max-md:flex-col', className)} {...props} />\n);\nconst ColumnRenderer = ({\n className,\n ...props\n}: ComponentPropsWithoutRef<'div'>) => (\n <div className={cn('flex-1', className)} {...props} />\n);\n\nconst Iframe = (props: ComponentProps<'iframe'>) => (\n <MarkDownIframe {...props} />\n);\n\n// Static configuration object for static renderers\nconst staticMarkdownComponents = {\n h1: H1Renderer,\n h2: H2Renderer,\n h3: H3Renderer,\n h4: H4Renderer,\n h5: H5Renderer,\n h6: H6Renderer,\n strong: StrongRenderer,\n blockquote: BlockquoteRenderer,\n ul: UlRenderer,\n ol: OlRenderer,\n img: ImgRenderer,\n pre: PreRenderer,\n table: TableRenderer,\n th: Th,\n tr: Tr,\n td: Td,\n hr: Hr,\n Tabs: TabsRenderer,\n Tab: Tab.Item,\n Columns: ColumnsRenderer,\n Column: ColumnRenderer,\n iframe: Iframe,\n};\n\n// Factory function to create components with dynamic props\nconst createMarkdownComponents = (\n isDarkMode?: boolean,\n locale?: LocalesValues\n) => ({\n ...staticMarkdownComponents,\n code: createCodeRenderer(isDarkMode),\n a: createLinkRenderer(locale),\n});\n\n// Export static renderers for backward compatibility\nexport const baseMarkdownComponents = staticMarkdownComponents;\n\ntype MarkdownRendererProps = {\n children: string | ParsedMarkdown;\n isDarkMode?: boolean;\n locale?: LocalesValues;\n forceBlock?: boolean;\n preserveFrontmatter?: boolean;\n tagfilter?: boolean;\n components?: ComponentProps<typeof MarkdownRendererIntlayer>['components'];\n wrapper?: ComponentProps<typeof MarkdownRendererIntlayer>['wrapper'];\n};\n\nexport const getIntlayerMarkdownOptions = (_isDarkMode?: boolean) => ({\n components: baseMarkdownComponents,\n});\n\nexport const MarkdownRenderer: FC<MarkdownRendererProps> = ({\n children,\n isDarkMode = false,\n locale,\n forceBlock,\n preserveFrontmatter,\n tagfilter,\n components: componentsProp,\n wrapper,\n}) => {\n const markdownComponents = createMarkdownComponents(isDarkMode, locale);\n\n const markdownContent = renderMarkdown(children, {\n components: {\n ...markdownComponents,\n ...componentsProp,\n },\n wrapper,\n forceBlock,\n preserveFrontmatter,\n tagfilter,\n });\n\n return (\n <CodeProvider>\n <TabProvider>{markdownContent}</TabProvider>\n </CodeProvider>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MarkDownRender.mjs","names":[],"sources":["../../../../src/components/MarkDownRender/MarkDownRender.tsx"],"sourcesContent":["import type { LocalesValues } from '@intlayer/types/module_augmentation';\nimport { cn } from '@utils/cn';\nimport type { ComponentProps, ComponentPropsWithoutRef, FC } from 'react';\nimport { memo } from 'react';\nimport {\n type MarkdownRenderer as MarkdownRendererIntlayer,\n type ParsedMarkdown,\n renderMarkdown,\n} from 'react-intlayer/markdown';\nimport { H1, H2, H3, H4, H5, H6 } from '../Headers';\nimport { Code } from '../IDE/Code';\nimport { CodeProvider } from '../IDE/CodeContext';\nimport type { CodeLanguage } from '../IDE/shikiLanguages';\nimport { Link } from '../Link';\nimport { Tab } from '../Tab';\nimport { TabProvider } from '../Tab/TabContext';\nimport { Hr, SmartTable, Td, Th, Tr } from '../Table';\nimport { MarkDownIframe } from './MarkDownIframe';\n\nexport type { ParsedMarkdown };\n\n// Extracted, stable component renderers\nconst H1Renderer = (props: ComponentProps<'h1'>) => (\n <H1 isClickable className=\"mb-16 mb-8 text-text\" {...props} />\n);\nconst H2Renderer = (props: ComponentProps<'h2'>) => (\n <H2 isClickable className=\"mt-16 text-text\" {...props} />\n);\nconst H3Renderer = (props: ComponentProps<'h3'>) => (\n <H3 isClickable className=\"mt-5 text-text\" {...props} />\n);\nconst H4Renderer = (props: ComponentProps<'h4'>) => (\n <H4 isClickable className=\"mt-3 text-text\" {...props} />\n);\nconst H5Renderer = (props: ComponentProps<'h5'>) => (\n <H5 isClickable className=\"mt-3 text-text\" {...props} />\n);\nconst H6Renderer = (props: ComponentProps<'h6'>) => (\n <H6 isClickable className=\"mt-3 text-text\" {...props} />\n);\nconst StrongRenderer = (props: ComponentProps<'strong'>) => (\n <strong className=\"text-text\" {...props} />\n);\n\nconst MemoizedCodeBlock = memo(\n ({\n className,\n children,\n highlightedHtml,\n lang, // Destructure html lang prop to prevent passing invalid BCP-47 language tag to Code component\n ...rest\n }: ComponentProps<'code'> & { highlightedHtml?: string }) => {\n const content = String(children ?? '').replace(/\\n$/, '');\n const isBlock = !!className;\n\n if (!isBlock) {\n const decodedContent = content.replace(\n /&(?:amp;)?#(\\d+);/g,\n (_, code: string) => String.fromCharCode(parseInt(code, 10))\n );\n return (\n <code className=\"rounded-md border border-neutral/30 bg-card/60 box-decoration-clone px-1.5 py-0.5 font-mono text-[0.875em] leading-[inherit]\">\n {decodedContent}\n </code>\n );\n }\n\n const language = (className?.replace(/lang(?:uage)?-/, '') ||\n 'plaintext') as CodeLanguage;\n\n return (\n <Code\n {...rest}\n language={language}\n showHeader\n highlightedHtml={highlightedHtml}\n >\n {content}\n </Code>\n );\n },\n (prevProps, nextProps) =>\n prevProps.children === nextProps.children &&\n prevProps.className === nextProps.className &&\n prevProps.highlightedHtml === nextProps.highlightedHtml\n);\n\n/**\n * Renders a fenced code block.\n *\n * `highlightedHtml` reaches this renderer through the code node's attributes:\n * a pipeline that highlights the markdown ahead of time writes it there, and\n * the block then paints without loading the highlighter in the browser.\n */\nconst CodeRenderer = (props: ComponentProps<'code'>) => (\n <MemoizedCodeBlock {...props} />\n);\n\nconst BlockquoteRenderer = ({\n className,\n ...props\n}: ComponentProps<'blockquote'>) => (\n <blockquote\n className={cn(\n 'mt-5 gap-3 border-card border-l-4 pl-5 text-neutral [&_strong]:text-neutral',\n className\n )}\n {...props}\n />\n);\n\nconst UlRenderer = ({ className, ...props }: ComponentProps<'ul'>) => (\n <ul\n className={cn(\n 'mt-5 flex list-disc flex-col gap-3 pl-5 marker:text-neutral/80',\n className\n )}\n {...props}\n />\n);\n\nconst OlRenderer = ({ className, ...props }: ComponentProps<'ol'>) => (\n <ol\n className={cn(\n 'mt-5 flex list-decimal flex-col gap-3 pl-5 marker:text-neutral/80',\n className\n )}\n {...props}\n />\n);\n\nconst ImgRenderer = ({\n className,\n alt,\n src,\n ...props\n}: ComponentProps<'img'>) => (\n <img\n {...props}\n alt={alt ?? ''}\n loading=\"lazy\"\n className={cn('max-h-[80vh] max-w-full rounded-md', className)}\n src={\n src?.includes('github.com')\n ? src\n ?.replace('github.com', 'raw.githubusercontent.com')\n .replace('/blob/', '/') // GitHub raw URLs do not use /blob/\n : src\n }\n />\n);\n\nconst createLinkRenderer = (locale?: LocalesValues) => {\n return (props: ComponentProps<'a'>) => (\n <Link\n isExternalLink={props.href?.startsWith('http')}\n underlined\n locale={locale}\n label=\"\"\n color=\"text\"\n {...(props as any)}\n />\n );\n};\n\nconst PreRenderer = (props: ComponentProps<'pre'>) => <>{props.children}</>;\nconst TableRenderer = (props: ComponentProps<typeof SmartTable>) => (\n <SmartTable isRollable displayModal {...props} />\n);\n\nconst TabsRenderer = (props: ComponentProps<typeof Tab>) => (\n <Tab\n {...props}\n className=\"rounded-xl border border-card\"\n headerClassName=\"sticky rounded-xl top-24 z-5 bg-background/70 backdrop-blur overflow-x-auto\"\n />\n);\nconst ColumnsRenderer = ({\n className,\n ...props\n}: ComponentPropsWithoutRef<'div'>) => (\n <div className={cn('flex gap-4 max-md:flex-col', className)} {...props} />\n);\nconst ColumnRenderer = ({\n className,\n ...props\n}: ComponentPropsWithoutRef<'div'>) => (\n <div className={cn('flex-1', className)} {...props} />\n);\n\nconst Iframe = (props: ComponentProps<'iframe'>) => (\n <MarkDownIframe {...props} />\n);\n\n// Static configuration object for static renderers\nconst staticMarkdownComponents = {\n h1: H1Renderer,\n h2: H2Renderer,\n h3: H3Renderer,\n h4: H4Renderer,\n h5: H5Renderer,\n h6: H6Renderer,\n strong: StrongRenderer,\n blockquote: BlockquoteRenderer,\n ul: UlRenderer,\n ol: OlRenderer,\n img: ImgRenderer,\n pre: PreRenderer,\n table: TableRenderer,\n th: Th,\n tr: Tr,\n td: Td,\n hr: Hr,\n Tabs: TabsRenderer,\n Tab: Tab.Item,\n Columns: ColumnsRenderer,\n Column: ColumnRenderer,\n iframe: Iframe,\n};\n\n// Factory function to create components with dynamic props\nconst createMarkdownComponents = (locale?: LocalesValues) => ({\n ...staticMarkdownComponents,\n code: CodeRenderer,\n a: createLinkRenderer(locale),\n});\n\n// Export static renderers for backward compatibility\nexport const baseMarkdownComponents = staticMarkdownComponents;\n\ntype MarkdownRendererProps = {\n children: string | ParsedMarkdown;\n locale?: LocalesValues;\n forceBlock?: boolean;\n preserveFrontmatter?: boolean;\n tagfilter?: boolean;\n components?: ComponentProps<typeof MarkdownRendererIntlayer>['components'];\n wrapper?: ComponentProps<typeof MarkdownRendererIntlayer>['wrapper'];\n};\n\nexport const getIntlayerMarkdownOptions = () => ({\n components: baseMarkdownComponents,\n});\n\nexport const MarkdownRenderer: FC<MarkdownRendererProps> = ({\n children,\n locale,\n forceBlock,\n preserveFrontmatter,\n tagfilter,\n components: componentsProp,\n wrapper,\n}) => {\n const markdownComponents = createMarkdownComponents(locale);\n\n const markdownContent = renderMarkdown(children, {\n components: {\n ...markdownComponents,\n ...componentsProp,\n },\n wrapper,\n forceBlock,\n preserveFrontmatter,\n tagfilter,\n });\n\n return (\n <CodeProvider>\n <TabProvider>{markdownContent}</TabProvider>\n </CodeProvider>\n );\n};\n"],"mappings":"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"}
|
|
@@ -130,7 +130,7 @@ const MaxHeightSmoother = ({ children, isHidden, className = "", isOverable = fa
|
|
|
130
130
|
"aria-hidden": isFocusable ? isHidden : void 0,
|
|
131
131
|
tabIndex: isFocusable ? 0 : void 0,
|
|
132
132
|
role: isFocusable ? "button" : "none",
|
|
133
|
-
className: cn("group/height-smoother relative grid w-full grid-rows-[0fr] overflow-hidden transition-
|
|
133
|
+
className: cn("group/height-smoother relative grid w-full grid-rows-[0fr] overflow-hidden transition-[grid-template-rows] duration-700 ease-in-out", typeof isHidden !== "undefined" && !isHidden && "grid-rows-[1fr] overflow-x-auto", isOverable && "hover:grid-rows-[1fr] hover:overflow-x-auto", isFocusable && "focus-within:grid-rows-[1fr] focus-within:overflow-x-auto focus:grid-rows-[1fr] focus:overflow-x-auto", className),
|
|
134
134
|
...props,
|
|
135
135
|
children: /* @__PURE__ */ jsx("div", {
|
|
136
136
|
style: { minHeight: `${minHeight}px` },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/MaxHeightSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { CSSProperties, FC, HTMLAttributes, ReactNode } from 'react';\n\n/**\n * Props for the MaxHeightSmoother component\n */\ninterface MaxHeightSmootherProps extends HTMLAttributes<HTMLDivElement> {\n /** Content to render within the smoother container */\n children: ReactNode;\n /** Controls collapse state. When true, content is collapsed; when false, expanded; when undefined, relies on hover/focus behavior */\n isHidden?: boolean;\n /** Enable expand-on-hover behavior */\n isOverable?: boolean;\n /** Enable expand-on-focus behavior for accessibility and keyboard navigation */\n isFocusable?: boolean;\n /** Minimum height in pixels for the collapsed state */\n minHeight?: number;\n}\n\n/**\n * MaxHeightSmoother Component\n *\n * A sophisticated container component that provides smooth height transitions\n * for collapsible content. Implemented entirely in CSS — no client-side\n * JavaScript, hooks, or event listeners — making it compatible with React\n * Server Components and Next.js App Router without `'use client'`.\n *\n * @component\n * @example\n * Basic controlled usage:\n * ```tsx\n * const [isCollapsed, setIsCollapsed] = useState(true);\n *\n * <MaxHeightSmoother isHidden={isCollapsed}>\n * <div>Your collapsible content here</div>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Hover-triggered expansion:\n * ```tsx\n * <MaxHeightSmoother isOverable>\n * <p>This content expands when you hover over the container.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Focus-triggered expansion (keyboard accessible):\n * ```tsx\n * <MaxHeightSmoother isFocusable>\n * <p>Tab to focus this container to expand the content.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * With minimum height for preview:\n * ```tsx\n * <MaxHeightSmoother isOverable minHeight={100} className=\"border rounded-lg p-4\">\n * <h3>Article Preview</h3>\n * <p>This shows a preview of the content. Hover to see all.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Combined hover and focus:\n * ```tsx\n * <MaxHeightSmoother isOverable isFocusable minHeight={80}>\n * <h4>Interactive Card</h4>\n * <p>Expands on both hover and keyboard focus.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * ## Animation Strategies\n *\n * ### Strategy A — `grid-template-rows` (`minHeight === 0`)\n * Transitions between `grid-rows-[0fr]` and `grid-rows-[1fr]`.\n * Clean and performant; no fixed height ceiling required.\n * Requires `min-h-0` on the inner wrapper so the grid track can fully collapse.\n *\n * ### Strategy B — `max-height` via CSS custom properties (`minHeight > 0`)\n * When a visible preview floor is required, `grid-template-rows` produces a\n * \"dead time\" artifact: the track silently grows from 0 → minHeight before\n * anything visible happens, resulting in a perceived snap.\n *\n * `max-height` sidesteps this entirely — the transition starts from the\n * already-visible floor value, producing a single continuous expansion.\n *\n * The collapsed floor (`--mhs-collapsed`) and expanded ceiling\n * (`--mhs-expanded`) are injected as CSS variables via inline style,\n * NOT as `max-height` directly. This lets Tailwind own `max-height`\n * entirely, so `:hover` and `:focus-within` can override it freely\n * without fighting inline-style specificity.\n *\n * Both variables are consumer-overridable via CSS:\n * ```css\n * .my-container {\n * --mhs-collapsed: 80px;\n * --mhs-expanded: 1200px;\n * }\n * ```\n *\n * Trade-off: easing is applied over [minHeight → 3000px], not the real\n * content height. For typical content this is imperceptible. If pixel-perfect\n * easing is critical, use a JS-measured height with a controlled `isHidden`\n * prop and a `style={{ maxHeight }}` override instead.\n *\n * ## Interaction Modes\n *\n * 1. **Controlled** — Pass `isHidden` for external state control (e.g. accordion)\n * 2. **Hover** — Set `isOverable` for mouse hover expansion\n * 3. **Focus** — Set `isFocusable` for keyboard focus expansion\n * 4. **Combined** — Use `isOverable` and `isFocusable` together\n *\n * ## Accessibility\n *\n * - `role=\"button\"` and `tabIndex={0}` when `isFocusable` is true\n * - `aria-expanded` reflects the current disclosure state\n * - Focus expansion via `:focus-within` CSS pseudo-class\n * - `motion-reduce:transition-none` respects `prefers-reduced-motion`\n *\n * Note: because this component relies on CSS pseudo-classes for interaction,\n * keyboard toggle (Enter / Space) is not supported in uncontrolled mode.\n * For full keyboard toggle behaviour, manage `isHidden` externally and\n * wire an `onClick` / `onKeyDown` handler on the parent.\n *\n * ## Performance\n *\n * - Pure CSS animations — no JavaScript timers or DOM measurements\n * - No `'use client'` directive required\n * - Compatible with React Server Components and Next.js App Router\n * - GPU-accelerated transitions\n *\n * @param props - Component props extending HTML div attributes\n * @param props.children - Content to render within the container\n * @param props.isHidden - Controlled collapse state (`true` = collapsed, `false` = expanded)\n * @param props.isOverable - Enable hover-to-expand behaviour\n * @param props.isFocusable - Enable focus-to-expand behaviour with keyboard navigation\n * @param props.minHeight - Minimum height in pixels for the collapsed state (default: 0)\n * @param props.className - Additional CSS classes\n * @param props.style - Inline styles (CSS variable overrides will be merged)\n *\n * @returns A smooth height-transitioning container with configurable interaction modes\n */\nexport const MaxHeightSmoother: FC<MaxHeightSmootherProps> = ({\n children,\n isHidden,\n className = '',\n isOverable = false,\n isFocusable = false,\n minHeight = 0,\n ...props\n}) => (\n <div\n aria-hidden={isFocusable ? isHidden : undefined}\n tabIndex={isFocusable ? 0 : undefined}\n role={isFocusable ? 'button' : 'none'}\n className={cn(\n 'group/height-smoother relative grid w-full grid-rows-[0fr] overflow-hidden transition-
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/MaxHeightSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { CSSProperties, FC, HTMLAttributes, ReactNode } from 'react';\n\n/**\n * Props for the MaxHeightSmoother component\n */\ninterface MaxHeightSmootherProps extends HTMLAttributes<HTMLDivElement> {\n /** Content to render within the smoother container */\n children: ReactNode;\n /** Controls collapse state. When true, content is collapsed; when false, expanded; when undefined, relies on hover/focus behavior */\n isHidden?: boolean;\n /** Enable expand-on-hover behavior */\n isOverable?: boolean;\n /** Enable expand-on-focus behavior for accessibility and keyboard navigation */\n isFocusable?: boolean;\n /** Minimum height in pixels for the collapsed state */\n minHeight?: number;\n}\n\n/**\n * MaxHeightSmoother Component\n *\n * A sophisticated container component that provides smooth height transitions\n * for collapsible content. Implemented entirely in CSS — no client-side\n * JavaScript, hooks, or event listeners — making it compatible with React\n * Server Components and Next.js App Router without `'use client'`.\n *\n * @component\n * @example\n * Basic controlled usage:\n * ```tsx\n * const [isCollapsed, setIsCollapsed] = useState(true);\n *\n * <MaxHeightSmoother isHidden={isCollapsed}>\n * <div>Your collapsible content here</div>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Hover-triggered expansion:\n * ```tsx\n * <MaxHeightSmoother isOverable>\n * <p>This content expands when you hover over the container.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Focus-triggered expansion (keyboard accessible):\n * ```tsx\n * <MaxHeightSmoother isFocusable>\n * <p>Tab to focus this container to expand the content.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * With minimum height for preview:\n * ```tsx\n * <MaxHeightSmoother isOverable minHeight={100} className=\"border rounded-lg p-4\">\n * <h3>Article Preview</h3>\n * <p>This shows a preview of the content. Hover to see all.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * @example\n * Combined hover and focus:\n * ```tsx\n * <MaxHeightSmoother isOverable isFocusable minHeight={80}>\n * <h4>Interactive Card</h4>\n * <p>Expands on both hover and keyboard focus.</p>\n * </MaxHeightSmoother>\n * ```\n *\n * ## Animation Strategies\n *\n * ### Strategy A — `grid-template-rows` (`minHeight === 0`)\n * Transitions between `grid-rows-[0fr]` and `grid-rows-[1fr]`.\n * Clean and performant; no fixed height ceiling required.\n * Requires `min-h-0` on the inner wrapper so the grid track can fully collapse.\n *\n * ### Strategy B — `max-height` via CSS custom properties (`minHeight > 0`)\n * When a visible preview floor is required, `grid-template-rows` produces a\n * \"dead time\" artifact: the track silently grows from 0 → minHeight before\n * anything visible happens, resulting in a perceived snap.\n *\n * `max-height` sidesteps this entirely — the transition starts from the\n * already-visible floor value, producing a single continuous expansion.\n *\n * The collapsed floor (`--mhs-collapsed`) and expanded ceiling\n * (`--mhs-expanded`) are injected as CSS variables via inline style,\n * NOT as `max-height` directly. This lets Tailwind own `max-height`\n * entirely, so `:hover` and `:focus-within` can override it freely\n * without fighting inline-style specificity.\n *\n * Both variables are consumer-overridable via CSS:\n * ```css\n * .my-container {\n * --mhs-collapsed: 80px;\n * --mhs-expanded: 1200px;\n * }\n * ```\n *\n * Trade-off: easing is applied over [minHeight → 3000px], not the real\n * content height. For typical content this is imperceptible. If pixel-perfect\n * easing is critical, use a JS-measured height with a controlled `isHidden`\n * prop and a `style={{ maxHeight }}` override instead.\n *\n * ## Interaction Modes\n *\n * 1. **Controlled** — Pass `isHidden` for external state control (e.g. accordion)\n * 2. **Hover** — Set `isOverable` for mouse hover expansion\n * 3. **Focus** — Set `isFocusable` for keyboard focus expansion\n * 4. **Combined** — Use `isOverable` and `isFocusable` together\n *\n * ## Accessibility\n *\n * - `role=\"button\"` and `tabIndex={0}` when `isFocusable` is true\n * - `aria-expanded` reflects the current disclosure state\n * - Focus expansion via `:focus-within` CSS pseudo-class\n * - `motion-reduce:transition-none` respects `prefers-reduced-motion`\n *\n * Note: because this component relies on CSS pseudo-classes for interaction,\n * keyboard toggle (Enter / Space) is not supported in uncontrolled mode.\n * For full keyboard toggle behaviour, manage `isHidden` externally and\n * wire an `onClick` / `onKeyDown` handler on the parent.\n *\n * ## Performance\n *\n * - Pure CSS animations — no JavaScript timers or DOM measurements\n * - No `'use client'` directive required\n * - Compatible with React Server Components and Next.js App Router\n * - GPU-accelerated transitions\n *\n * @param props - Component props extending HTML div attributes\n * @param props.children - Content to render within the container\n * @param props.isHidden - Controlled collapse state (`true` = collapsed, `false` = expanded)\n * @param props.isOverable - Enable hover-to-expand behaviour\n * @param props.isFocusable - Enable focus-to-expand behaviour with keyboard navigation\n * @param props.minHeight - Minimum height in pixels for the collapsed state (default: 0)\n * @param props.className - Additional CSS classes\n * @param props.style - Inline styles (CSS variable overrides will be merged)\n *\n * @returns A smooth height-transitioning container with configurable interaction modes\n */\nexport const MaxHeightSmoother: FC<MaxHeightSmootherProps> = ({\n children,\n isHidden,\n className = '',\n isOverable = false,\n isFocusable = false,\n minHeight = 0,\n ...props\n}) => (\n <div\n aria-hidden={isFocusable ? isHidden : undefined}\n tabIndex={isFocusable ? 0 : undefined}\n role={isFocusable ? 'button' : 'none'}\n className={cn(\n 'group/height-smoother relative grid w-full grid-rows-[0fr] overflow-hidden transition-[grid-template-rows] duration-700 ease-in-out',\n typeof isHidden !== 'undefined' &&\n !isHidden &&\n 'grid-rows-[1fr] overflow-x-auto',\n isOverable && 'hover:grid-rows-[1fr] hover:overflow-x-auto',\n isFocusable &&\n 'focus-within:grid-rows-[1fr] focus-within:overflow-x-auto focus:grid-rows-[1fr] focus:overflow-x-auto',\n className\n )}\n {...props}\n >\n <div\n style={{\n minHeight: `${minHeight}px`,\n }}\n className={cn(\n isOverable && 'group-hover/height-smoother:visible',\n isFocusable && 'group-focus/height-smoother:visible',\n className\n )}\n >\n {children}\n </div>\n </div>\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+IA,MAAa,qBAAiD,EAC5D,UACA,UACA,YAAY,IACZ,aAAa,OACb,cAAc,OACd,YAAY,GACZ,GAAG,YAEH,oBAAC,OAAD;CACE,eAAa,cAAc,WAAW;CACtC,UAAU,cAAc,IAAI;CAC5B,MAAM,cAAc,WAAW;CAC/B,WAAW,GACT,uIACA,OAAO,aAAa,eAClB,CAAC,YACD,mCACF,cAAc,+CACd,eACE,yGACF,SACF;CACA,GAAI;WAEJ,oBAAC,OAAD;EACE,OAAO,EACL,WAAW,GAAG,UAAU,IAC1B;EACA,WAAW,GACT,cAAc,uCACd,eAAe,uCACf,SACF;EAEC;CACE;AACF"}
|
|
@@ -5,7 +5,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
5
5
|
const MaxWidthSmoother = ({ children, isHidden, minWidth = 0, align = "left", isDisabled = false, className, ...props }) => {
|
|
6
6
|
const isCurrentlyHidden = isHidden && !isDisabled;
|
|
7
7
|
return /* @__PURE__ */ jsx("div", {
|
|
8
|
-
className: cn("relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-
|
|
8
|
+
className: cn("relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-[grid-template-columns] duration-500 ease-in-out", isCurrentlyHidden ? "" : "grid-cols-[1fr]", className),
|
|
9
9
|
"aria-hidden": isCurrentlyHidden,
|
|
10
10
|
inert: isCurrentlyHidden ? true : void 0,
|
|
11
11
|
...props,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/MaxWidthSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { HTMLAttributes } from 'react';\n\ntype Align = 'left' | 'right';\n\ntype MaxWidthSmootherProps = HTMLAttributes<HTMLDivElement> & {\n isHidden: boolean;\n minWidth?: number;\n align?: Align;\n isDisabled?: boolean;\n};\n\nexport const MaxWidthSmoother = ({\n children,\n isHidden,\n minWidth = 0,\n align = 'left',\n isDisabled = false,\n className,\n ...props\n}: MaxWidthSmootherProps) => {\n const isCurrentlyHidden = isHidden && !isDisabled;\n\n return (\n <div\n className={cn(\n 'relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/MaxWidthSmoother/index.tsx"],"sourcesContent":["import { cn } from '@utils/cn';\nimport type { HTMLAttributes } from 'react';\n\ntype Align = 'left' | 'right';\n\ntype MaxWidthSmootherProps = HTMLAttributes<HTMLDivElement> & {\n isHidden: boolean;\n minWidth?: number;\n align?: Align;\n isDisabled?: boolean;\n};\n\nexport const MaxWidthSmoother = ({\n children,\n isHidden,\n minWidth = 0,\n align = 'left',\n isDisabled = false,\n className,\n ...props\n}: MaxWidthSmootherProps) => {\n const isCurrentlyHidden = isHidden && !isDisabled;\n\n return (\n <div\n className={cn(\n 'relative grid h-full grid-cols-[0fr] overflow-x-hidden overflow-y-hidden transition-[grid-template-columns] duration-500 ease-in-out',\n isCurrentlyHidden ? '' : 'grid-cols-[1fr]',\n className\n )}\n aria-hidden={isCurrentlyHidden}\n inert={isCurrentlyHidden ? true : undefined}\n {...props}\n >\n <div\n style={{\n minWidth: `${minWidth}px`,\n }}\n className={cn(align === 'right' && 'ml-auto')}\n >\n {children}\n </div>\n </div>\n );\n};\n"],"mappings":";;;;AAYA,MAAa,oBAAoB,EAC/B,UACA,UACA,WAAW,GACX,QAAQ,QACR,aAAa,OACb,WACA,GAAG,YACwB;CAC3B,MAAM,oBAAoB,YAAY,CAAC;CAEvC,OACE,oBAAC,OAAD;EACE,WAAW,GACT,wIACA,oBAAoB,KAAK,mBACzB,SACF;EACA,eAAa;EACb,OAAO,oBAAoB,OAAO;EAClC,GAAI;YAEJ,oBAAC,OAAD;GACE,OAAO,EACL,UAAU,GAAG,SAAS,IACxB;GACA,WAAW,GAAG,UAAU,WAAW,SAAS;GAE3C;EACE;CACF;AAET"}
|