style: Reorder props in Snippet component
This commit is contained in:
parent
6a5ade483b
commit
8418ca4a74
@ -42,6 +42,6 @@ export function Button(props: ButtonProps) {
|
|||||||
href={props.href}
|
href={props.href}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<button class={className} {...(props as JSX.IntrinsicElements["button"])} />
|
<button {...(props as JSX.IntrinsicElements["button"])} class={className} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import type { ComponentProps, ParentComponent } from "solid-js";
|
import type { ComponentProps, ParentComponent } from "solid-js";
|
||||||
|
|
||||||
import { createEffect, createMemo, mergeProps, on, splitProps } from "solid-js";
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
For,
|
||||||
|
mergeProps,
|
||||||
|
on,
|
||||||
|
splitProps,
|
||||||
|
} from "solid-js";
|
||||||
import * as Prismjs from "prismjs";
|
import * as Prismjs from "prismjs";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
@ -314,6 +321,8 @@ export type Language = (typeof Language)[keyof typeof Language];
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
language: string;
|
language: string;
|
||||||
|
class?: string;
|
||||||
|
withLineNumbers?: boolean;
|
||||||
} & ComponentProps<"code">;
|
} & ComponentProps<"code">;
|
||||||
|
|
||||||
export const Highlight: ParentComponent<Props> = (_props) => {
|
export const Highlight: ParentComponent<Props> = (_props) => {
|
||||||
@ -326,16 +335,16 @@ export const Highlight: ParentComponent<Props> = (_props) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const languageClass = createMemo(() => `language-${props.language}`);
|
const languageClass = createMemo(() => `language-${props.language}`);
|
||||||
|
|
||||||
const highlightedCode = createMemo<string | undefined>(() => {
|
const highlightedCode = createMemo<string | undefined>(() => {
|
||||||
const childrenString = props.children?.toString();
|
const childrenString = props.children?.toString();
|
||||||
if (!childrenString) {
|
if (!childrenString) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const grammar = Prismjs.languages[props.language];
|
const grammar = Prismjs.languages[props.language];
|
||||||
if (!grammar) {
|
if (!grammar) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = Prismjs.highlight(childrenString, grammar, props.language);
|
const result = Prismjs.highlight(childrenString, grammar, props.language);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -346,14 +355,46 @@ export const Highlight: ParentComponent<Props> = (_props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<pre class={clsx("prism-code flex overflow-x-auto pb-6", languageClass())}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"rounded-xl shadow-lg flex items-start px-4 py-2 w-full",
|
||||||
|
props.class,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.withLineNumbers && props.children?.toString() && (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="border-r leading-6 border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each={Array.from({
|
||||||
|
length: props.children.toString().split("\n").length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(_, index) => (
|
||||||
|
<>
|
||||||
|
{(index() + 1).toString().padStart(2, "0")}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<pre
|
||||||
|
class={clsx(
|
||||||
|
"not-prose w-full prism-code flex overflow-x-auto",
|
||||||
|
languageClass(),
|
||||||
|
)}
|
||||||
|
>
|
||||||
<code
|
<code
|
||||||
class={clsx("px-4", props.class)}
|
class={clsx("px-4", "leading-6")}
|
||||||
innerHTML={highlightedCode()}
|
innerHTML={highlightedCode()}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,8 +25,6 @@ export function Prose(props: ProseProps) {
|
|||||||
"prose-a:font-semibold",
|
"prose-a:font-semibold",
|
||||||
// link underline
|
// link underline
|
||||||
"prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,var(--color-violet-300))] prose-a:hover:[--tw-prose-underline-size:6px]",
|
"prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,var(--color-violet-300))] prose-a:hover:[--tw-prose-underline-size:6px]",
|
||||||
// pre
|
|
||||||
"prose-pre:rounded-xl prose-pre:bg-slate-900 prose-pre:shadow-lg",
|
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
93
app/components/Snippet.tsx
Normal file
93
app/components/Snippet.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import type { JSX, Accessor, Setter } from "solid-js";
|
||||||
|
|
||||||
|
import { For, createSignal } from "solid-js";
|
||||||
|
import { Highlight } from "./Highlight";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
|
||||||
|
<circle cx="5" cy="5" r="4.5" class="fill-red-400" />
|
||||||
|
<circle cx="21" cy="5" r="4.5" class="fill-amber-300" />
|
||||||
|
<circle cx="37" cy="5" r="4.5" class="fill-green-500" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnippetTab = {
|
||||||
|
name: string;
|
||||||
|
codeLanguage: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SnippetProps = {
|
||||||
|
children?: JSX.Element;
|
||||||
|
class?: string;
|
||||||
|
snippets: SnippetTab[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Snippet(props: SnippetProps) {
|
||||||
|
const [selectedTab, setSelectedTab] = createSignal<SnippetTab>(
|
||||||
|
props.snippets[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isActive = (tab: SnippetTab) => selectedTab()?.name === tab.name;
|
||||||
|
const selectTab = (name: string) => {
|
||||||
|
const tab = props.snippets.find((tab) => tab.name === name);
|
||||||
|
if (tab) setSelectedTab(tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur-sm",
|
||||||
|
props.class,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="absolute -top-px right-11 left-20 h-px bg-linear-to-r from-violet-300/0 via-violet-300/70 to-violet-300/0" />
|
||||||
|
<div class="absolute right-20 -bottom-px left-11 h-px bg-linear-to-r from-purple-400/0 via-purple-400 to-purple-400/0" />
|
||||||
|
<div class="pt-4 pl-4">
|
||||||
|
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
|
||||||
|
<div class="mt-4 flex space-x-2 text-xs">
|
||||||
|
<For each={props.snippets}>
|
||||||
|
{(tab) => (
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"flex h-6 rounded-full",
|
||||||
|
{ "cursor-pointer": tab.codeLanguage && !isActive(tab) },
|
||||||
|
isActive(tab)
|
||||||
|
? "bg-linear-to-r from-violet-400/30 via-violet-400 to-violet-400/30 p-px font-medium text-violet-300"
|
||||||
|
: "text-slate-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(
|
||||||
|
"flex items-center rounded-full px-2.5",
|
||||||
|
isActive(tab) && "bg-slate-800",
|
||||||
|
)}
|
||||||
|
disabled={!tab.codeLanguage}
|
||||||
|
onClick={() => selectTab(tab.name)}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedTab() && (
|
||||||
|
<div class="mt-6">
|
||||||
|
<Highlight
|
||||||
|
class="dark !pt-0 !px-1"
|
||||||
|
language={selectedTab().codeLanguage}
|
||||||
|
withLineNumbers
|
||||||
|
>
|
||||||
|
{selectedTab().code}
|
||||||
|
</Highlight>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
app/components/Tabs.tsx
Normal file
131
app/components/Tabs.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import type { JSX, Accessor, Setter } from "solid-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
createSignal,
|
||||||
|
onMount,
|
||||||
|
For,
|
||||||
|
} from "solid-js";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type TabType = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TabsContextType = {
|
||||||
|
selectedTab: Accessor<string>;
|
||||||
|
setSelectedTab: Setter<string>;
|
||||||
|
tabs: Accessor<TabType[]>;
|
||||||
|
addTab: (tab: TabType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabsContext = createContext<TabsContextType>({
|
||||||
|
selectedTab: () => "",
|
||||||
|
setSelectedTab: () => {},
|
||||||
|
tabs: () => [],
|
||||||
|
addTab: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Tabs(props: {
|
||||||
|
defaultSelectedTab?: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
}) {
|
||||||
|
const [selectedTab, setSelectedTab] = createSignal(
|
||||||
|
props.defaultSelectedTab || "",
|
||||||
|
);
|
||||||
|
const [tabs, setTabs] = createSignal<TabType[]>([]);
|
||||||
|
|
||||||
|
const addTab = (tab: TabType) => {
|
||||||
|
console.log("Adding tab", tab);
|
||||||
|
|
||||||
|
setTabs((prevTabs) => {
|
||||||
|
// Append to the end of the array and make sure it's unique
|
||||||
|
if (prevTabs.some((t) => t.value === tab.value)) {
|
||||||
|
return prevTabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...prevTabs, tab];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContext.Provider
|
||||||
|
value={{
|
||||||
|
selectedTab,
|
||||||
|
setSelectedTab,
|
||||||
|
tabs,
|
||||||
|
addTab,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="max-w-full overflow-x-auto overflow-y-hidden">
|
||||||
|
<ul
|
||||||
|
class="!p-0 w-max flex items-stretch gap-1 !m-0"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
<For each={tabs()}>
|
||||||
|
{(tab) => (
|
||||||
|
<li class="overflow-hidden">
|
||||||
|
<TabItem
|
||||||
|
tab={tab}
|
||||||
|
isSelected={selectedTab() === tab.value}
|
||||||
|
select={() => setSelectedTab(tab.value)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="-mt-1 p-2">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
</TabsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabItem(props: {
|
||||||
|
tab: TabType;
|
||||||
|
isSelected: boolean;
|
||||||
|
select: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={props.isSelected ? "primary" : "secondary"}
|
||||||
|
class={clsx("!rounded-md", props.isSelected && "cursor-default")}
|
||||||
|
onClick={props.select}
|
||||||
|
>
|
||||||
|
{props.tab.label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tabs.Item = (props: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
}) => {
|
||||||
|
const tabsContext = useContext(TabsContext);
|
||||||
|
if (!tabsContext) {
|
||||||
|
throw new Error("Tabs.Item must be used within Tabs");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log("Mounting tab", props.label);
|
||||||
|
tabsContext.addTab({ label: props.label, value: props.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"first:!mt-0",
|
||||||
|
"last:!mb-0",
|
||||||
|
tabsContext.selectedTab() !== props.value && "hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,46 +1,195 @@
|
|||||||
pre[class*=language-] {
|
code[class*="language-"],
|
||||||
color: var(--color-slate-50)
|
pre[class*="language-"] {
|
||||||
|
color: #383a42;
|
||||||
|
background: none;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
}
|
}
|
||||||
.token.tag,
|
pre[class*="language-"]::selection,
|
||||||
.token.class-name,
|
pre[class*="language-"] ::selection,
|
||||||
.token.selector,
|
code[class*="language-"]::selection,
|
||||||
.token.selector .class,
|
code[class*="language-"] ::selection,
|
||||||
.token.selector.class,
|
pre[class*="language-"]::-moz-selection,
|
||||||
.token.function {
|
pre[class*="language-"] ::-moz-selection,
|
||||||
color: var(--color-pink-400)
|
code[class*="language-"]::-moz-selection,
|
||||||
|
code[class*="language-"] ::-moz-selection {
|
||||||
|
background: #DDE3EA;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
.token.attr-name,
|
@media print {
|
||||||
.token.rule,
|
code[class*="language-"],
|
||||||
.token.pseudo-class,
|
pre[class*="language-"] {
|
||||||
.token.important {
|
text-shadow: none;
|
||||||
color: var(--color-slate-300)
|
|
||||||
}
|
}
|
||||||
.token.keyword,
|
|
||||||
.token.module {
|
|
||||||
color: var(--color-pink-400)
|
|
||||||
}
|
}
|
||||||
.token.attr-value,
|
:not(pre) > code[class*="language-"] {
|
||||||
.token.class,
|
padding: 0.1em;
|
||||||
.token.string,
|
border-radius: 0.3em;
|
||||||
.token.property {
|
white-space: normal;
|
||||||
color: var(--color-sky-300)
|
|
||||||
}
|
|
||||||
.token.punctuation,
|
|
||||||
.token.attr-equals {
|
|
||||||
color: var(--color-slate-500)
|
|
||||||
}
|
|
||||||
.token.unit,
|
|
||||||
.language-css .token.function {
|
|
||||||
color: var(--color-teal-200)
|
|
||||||
}
|
}
|
||||||
.token.comment,
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #a0a1a7;
|
||||||
|
}
|
||||||
|
.token.plain-text,
|
||||||
|
.token.punctuation {
|
||||||
|
color: #383a42;
|
||||||
|
}
|
||||||
|
.token.selector,
|
||||||
|
.token.tag {
|
||||||
|
color: #e45649;
|
||||||
|
}
|
||||||
|
.token.property,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.deleted {
|
||||||
|
color: #e1aa76;
|
||||||
|
}
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #50a14f;
|
||||||
|
}
|
||||||
.token.operator,
|
.token.operator,
|
||||||
.token.combinator {
|
.token.entity,
|
||||||
color: var(--color-slate-400)
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #46a6b2;
|
||||||
}
|
}
|
||||||
.prism-code {
|
.token.function {
|
||||||
margin: 0
|
color: #4078f2;
|
||||||
}
|
}
|
||||||
.prism-code + .prism-code {
|
.token.atrule,
|
||||||
margin-bottom: 1rem
|
.token.keyword,
|
||||||
|
.token.regex,
|
||||||
|
.token.important,
|
||||||
|
.token.variable {
|
||||||
|
color: #a626a4;
|
||||||
|
}
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
pre.line-numbers {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 3.8em;
|
||||||
|
counter-reset: linenumber;
|
||||||
|
}
|
||||||
|
pre.line-numbers > code {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.line-numbers .line-numbers-rows {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
left: -3.8em;
|
||||||
|
width: 3em;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
border-right: 0;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.line-numbers-rows > span {
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
counter-increment: linenumber;
|
||||||
|
}
|
||||||
|
.line-numbers-rows > span:before {
|
||||||
|
content: counter(linenumber);
|
||||||
|
color: #b5b9c2;
|
||||||
|
display: block;
|
||||||
|
padding-right: 0.8em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
code.language-css,
|
||||||
|
pre.languagecss {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-javascript,
|
||||||
|
pre.languagejavascript {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-js,
|
||||||
|
pre.languagejs {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-jsx,
|
||||||
|
pre.languagejsx {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-sass,
|
||||||
|
pre.languagesass {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-scss,
|
||||||
|
pre.languagescss {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-ts,
|
||||||
|
pre.languagets {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-tsx,
|
||||||
|
pre.languagetsx {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
code.language-typescript,
|
||||||
|
pre.languagetypescript {
|
||||||
|
color: #E45649;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark pre[class*="language-"] {
|
||||||
|
color: var(--color-slate-50);
|
||||||
|
}
|
||||||
|
.dark .token.module {
|
||||||
|
color: var(--color-pink-400)!important;
|
||||||
|
}
|
||||||
|
.dark .token.attr-name,
|
||||||
|
.dark .token.keyword,
|
||||||
|
.dark .token.rule,
|
||||||
|
.dark .token.pseudo-class,
|
||||||
|
.dark .token.important {
|
||||||
|
color: var(--color-slate-300);
|
||||||
|
}
|
||||||
|
.dark .token.comment,
|
||||||
|
.dark .token.operator,
|
||||||
|
.dark .token.combinator {
|
||||||
|
color: var(--color-slate-400);
|
||||||
|
}
|
||||||
|
.dark .token.punctuation,
|
||||||
|
.dark .token.attr-equals {
|
||||||
|
color: var(--color-slate-500);
|
||||||
|
}
|
||||||
|
.dark .token.attr-value,
|
||||||
|
.dark .token.class,
|
||||||
|
.dark .token.string,
|
||||||
|
.dark .token.property {
|
||||||
|
color: var(--color-sky-300);
|
||||||
}
|
}
|
||||||
@ -33,9 +33,3 @@ Un grand merci à ces entités qui utilisent le contenu de Memento Dev pour leur
|
|||||||
<Callout type="note" title="Tu utilises mon contenu et tu souhaites apparaître ici ?">
|
<Callout type="note" title="Tu utilises mon contenu et tu souhaites apparaître ici ?">
|
||||||
Pour figurer sur cette page, tu peux tout simplement m'en faire la demande par [email _(gauthier@gauthierdaniels.fr)_](mailto:gauthier@gauthierdaniels?subject=Demande%20d'ajout%20sur%20la%20page%20partages%20et%20r%C3%A9utilisations%20Memento%20Dev).
|
Pour figurer sur cette page, tu peux tout simplement m'en faire la demande par [email _(gauthier@gauthierdaniels.fr)_](mailto:gauthier@gauthierdaniels?subject=Demande%20d'ajout%20sur%20la%20page%20partages%20et%20r%C3%A9utilisations%20Memento%20Dev).
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
{% callout type="note" title="Tu utilises mon contenu et tu souhaites apparaître ici ?" %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|||||||
150
app/pages/docs/react/+Page.mdx
Normal file
150
app/pages/docs/react/+Page.mdx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
title: Introduction à React
|
||||||
|
description: Parlons un peu de React, ce qu'il est, ce qu'il fait et pourquoi il est si populaire.
|
||||||
|
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
|
||||||
|
---
|
||||||
|
|
||||||
|
import Callout from "@/components/Callout";
|
||||||
|
import tabs from "./tabs";
|
||||||
|
|
||||||
|
Parlons peu, parlons bien ! 😄
|
||||||
|
|
||||||
|
React est une **bibliothèque** _(non, pas un **framework** !)_ JavaScript open-source développée par Facebook.
|
||||||
|
Elle est utilisée pour construire des interfaces utilisateur _(UI)_ interactives et dynamiques.
|
||||||
|
|
||||||
|
<Callout type="note" title="Pourquoi React est si populaire ?">
|
||||||
|
- **Facilité d'utilisation** : React est facile à apprendre et à utiliser. Il est basé sur JavaScript, qui est l'un des langages de programmation les plus populaires.
|
||||||
|
- **Réutilisabilité des composants** : React permet de créer des composants réutilisables. Cela signifie que tu peux créer un composant une fois et l'utiliser partout où tu en as besoin.
|
||||||
|
- **Performances** : React utilise un DOM virtuel _(Virtual DOM)_ pour améliorer les performances de l'application.
|
||||||
|
- **Communauté active** : React a une communauté active de développeurs qui contribuent à son développement et partagent des ressources utiles.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
Mais on peut aussi y noter des points faibles bien entendu, car tout n'est pas rose :
|
||||||
|
|
||||||
|
- **Courbe d'apprentissage** : Bien que React soit "facile" à apprendre, les concepts avancés demandent un peu de temps pour être maîtrisés.
|
||||||
|
- **Taille du bundle** : React est relativement lourd en termes de taille de bundle, ce qui peut affecter les performances de l'application en terme de chargement initial.
|
||||||
|
- **GAFAM** : Comme d'autres bibliothèques/frameworks, React n'échappe pas à la critique de la part de certains développeurs qui ne souhaitent pas utiliser des technologies développées par des géants du web.
|
||||||
|
|
||||||
|
## 🤔 Pourquoi une bibliothèque et pas un framework ?
|
||||||
|
|
||||||
|
Très grand débat que voilà ! Vraiment.. il y a des guerres qui se sont déclarées pour moins que ça 😅
|
||||||
|
|
||||||
|
Blague à part, pour pouvoir dire que React n'est pas un framework, il faut comprendre la différence entre les deux :
|
||||||
|
|
||||||
|
- **Framework** : Un framework est un ensemble de bibliothèques et de composants qui sont prédéfinis et structurés pour te permettre de construire une application.
|
||||||
|
En gros, le framework te dit comment faire les choses.
|
||||||
|
- **Bibliothèque** : Une bibliothèque est un ensemble de fonctions et de composants que tu peux utiliser pour construire une application.
|
||||||
|
En gros, c'est toi qui décides comment faire les choses.
|
||||||
|
|
||||||
|
Et si tu connais déjà React, je te vois venir avec tes grands sabots... !
|
||||||
|
|
||||||
|
<Callout type="note" title="React a ses propres règles, on ne peut pas faire n'importe quoi !">
|
||||||
|
C'est vrai ! React a ses propres règles et conventions, mais il te laisse quand même une grande liberté pour organiser ton code comme tu le souhaites.
|
||||||
|
|
||||||
|
Si on se concentre sur la **préoccupation principale** de React, c'est de gérer l'**interface utilisateur** _(UI)_ de ton application.
|
||||||
|
En aucun cas, React _(tel quel et "pour le moment")_ va te dire comment gérer ton état global, comment gérer tes requêtes HTTP, etc.
|
||||||
|
|
||||||
|
Mais tu peux totalement utiliser React **au sein** d'un framework !
|
||||||
|
Tu as notamment des frameworks comme [**Next.js**](https://nextjs.org/) ou [**Gatsby**](https://www.gatsbyjs.com/) qui utilisent React
|
||||||
|
avec des fonctionnalités supplémentaires pour gérer le routage, le rendu côté serveur, etc.
|
||||||
|
|
||||||
|
_(Le meilleur, selon moi, c'est [**Vike**](https://vike.dev/) qui te permet d'utiliser presque n'importe quelle bibliothèque avec une même architecture 😏)_
|
||||||
|
|
||||||
|
Mais concentrons-nous sur React en tant que bibliothèque, et non en tant que framework 😉
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## 📝 JSX
|
||||||
|
|
||||||
|
Ce qui peut être déroutant au premier abord avec React, c'est le **JSX**.
|
||||||
|
On serait tenté de dire que c'est du HTML, mais en fait : **pas du tout** !
|
||||||
|
|
||||||
|
Le JSX est un sucre syntaxique _(syntactic sugar)_ qui permet d'écrire du code JavaScript en se basant sur le système de balisage HTML.
|
||||||
|
|
||||||
|
L'avantage de JSX c'est que le code devient beaucoup plus lisible et plus proche de ce que tu connais déjà avec HTML.
|
||||||
|
Mais il s'agit bien de JavaScript, et non de HTML !
|
||||||
|
|
||||||
|
## 🧩 Composants
|
||||||
|
|
||||||
|
React est basé sur le concept de **composants**. Un composant est une partie réutilisable de l'interface utilisateur _(UI)_ qui peut être affichée à l'écran.
|
||||||
|
|
||||||
|
Dans la majorité des cas, on va chercher à **mutualiser** les composants pour éviter de répéter du code inutilement.
|
||||||
|
L'exemple le plus flagrant sera par exemple tous les boutons de ton application, qui auront probablement tous la même apparence et le même comportement.
|
||||||
|
|
||||||
|
Il est aussi possible de les **imbriquer** les uns dans les autres !
|
||||||
|
En fait, on joue avec des Lego, mais en version code 👷
|
||||||
|
|
||||||
|
Mais si on veut vraiment montrer le potentiel de React, parlons maintenant... 🥁
|
||||||
|
Des **states**, **cycles de vie** et des **props** !
|
||||||
|
|
||||||
|
## 🛠️ States, Cycles de vie et Props
|
||||||
|
|
||||||
|
T'assomer aussi vite avec ces termes qui ne te parlent peut-être pas, c'est pas cool de ma part...
|
||||||
|
Pardon pour les gros mots, je me calme tout de suite ! 🙈
|
||||||
|
|
||||||
|
Si ça te rassure, je vais très rapidement évoquer ce qu'il se cache derrière ces termes barbares,
|
||||||
|
je réserve les détails pour des articles dédiés 😉
|
||||||
|
|
||||||
|
### 🗄️ States
|
||||||
|
|
||||||
|
... ou également appelés **états** en français.
|
||||||
|
|
||||||
|
Le but du state, c'est de stocker des données qui vont être **observées** par React.
|
||||||
|
À chaque fois que le state va être modifié, React va **réagir** et mettre à jour l'interface utilisateur _(UI)_ en conséquence afin d'afficher les nouvelles données.
|
||||||
|
|
||||||
|
### 🔄 Cycles de vie
|
||||||
|
|
||||||
|
Les **cycles de vie** _(lifecycle)_ sont des méthodes qui sont appelées à des moments précis dans le cycle de vie d'un composant React.
|
||||||
|
|
||||||
|
Si tu as lu la section qui parle brièvement des states, tu auras peut-être remarqué cette phrase :
|
||||||
|
|
||||||
|
> À chaque fois que le state va être modifié, React va **réagir** et mettre à jour l'interface utilisateur [...]
|
||||||
|
|
||||||
|
Et bien c'est là que les cycles de vie entrent en jeu !
|
||||||
|
Un composant sur React va avoir un cycle de vie, caractérisé par trois phases :
|
||||||
|
|
||||||
|
1. **Montage du composant** _(Mounting)_ : le composant est créé et inséré dans le DOM.
|
||||||
|
2. **Mise à jour du composant** _(Updating)_ : le composant est mis à jour en fonction des changements de state ou de props.
|
||||||
|
3. **Démontage du composant** _(Unmounting)_ : le composant est retiré du DOM.
|
||||||
|
|
||||||
|
Ces différentes phases vont nous permettre d'interagir avec le composant à des moments précis, et d'effectuer des actions en conséquence.
|
||||||
|
|
||||||
|
### 📦 Props
|
||||||
|
|
||||||
|
Et pour finir, les **props** _(properties ou tout simplement "propriétés" en français)_ !
|
||||||
|
|
||||||
|
Il s'agit ni plus ni moins que des **arguments** que tu vas passer à un composant, comme tu le ferais avec une fonction.
|
||||||
|
|
||||||
|
Cependant il faut noter une chose :
|
||||||
|
|
||||||
|
On transmet les props à un composant précis, qui sera donc un composant **enfant**.
|
||||||
|
Un composant enfant ne pourra pas transmettre des props à un composant parent, c'est unidirectionnel _(mais on verra comment on peut faire autrement 😉)_.
|
||||||
|
|
||||||
|
## 🖥️ Une petite démo ?
|
||||||
|
|
||||||
|
OK, mais vraiment petite !
|
||||||
|
|
||||||
|
Prenons l'exemple d'une application qui servira **uniquement** à afficher une liste de tâches _(une todolist donc !)_.
|
||||||
|
_(Bon... utiliser React uniquement pour ça c'est abusé, mais c'est pour l'exemple 😅)_
|
||||||
|
|
||||||
|
<tabs.reactTodolist />
|
||||||
|
|
||||||
|
On peut très bien imaginer des améliorations à cette application, comme par exemple :
|
||||||
|
|
||||||
|
- Supprimer une tâche
|
||||||
|
- Réinitialiser la liste des tâches
|
||||||
|
- Marquer une tâche comme terminée _(et inversement)_
|
||||||
|
- Ordonner les tâches pour afficher en priorité les tâches non terminées
|
||||||
|
- Enregistrer les tâches dans le navigateur pour les retrouver après un rafraîchissement de la page
|
||||||
|
|
||||||
|
Et si on se gardait ça pour la suite ? 😉
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Tu l'auras compris, React permet de résoudre un certain nombre de problématiques que l'on peut rencontrer lors du développement d'une application web.
|
||||||
|
|
||||||
|
Pas des problématiques majeures, mais ça nous permet tout de même en tant que développeur de gagner du temps et de l'efficacité !
|
||||||
|
|
||||||
|
Dans le cas où le fait que ce soit créé et maintenu par Facebook _(ou GAFAM de manière générale)_ est contre tes valeurs,
|
||||||
|
tu as des solutions très semblables qui existent, comme [**SolidJS**](https://www.solidjs.com/) par exemple.
|
||||||
|
|
||||||
|
Et si tu veux en savoir plus, je t'invite à lire les articles suivants qui vont te permettre de rentrer un peu plus dans le détail de React ! 🚀
|
||||||
94
app/pages/docs/react/tabs.tsx
Normal file
94
app/pages/docs/react/tabs.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { Highlight } from "@/components/Highlight";
|
||||||
|
import Tabs from "@/components/Tabs";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
reactTodolist: () => {
|
||||||
|
return (
|
||||||
|
<Tabs defaultSelectedTab="app">
|
||||||
|
<Tabs.Item value="app" label="App.tsx">
|
||||||
|
<Highlight language="tsx" withLineNumbers>
|
||||||
|
{`import TodoList from "./TodoList";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>TodoList</h1>
|
||||||
|
|
||||||
|
<TodoList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};`}
|
||||||
|
</Highlight>
|
||||||
|
</Tabs.Item>
|
||||||
|
|
||||||
|
<Tabs.Item value="todolist" label="TodoList.tsx">
|
||||||
|
{`
|
||||||
|
\`\`\`tsx showLineNumbers
|
||||||
|
import TodoListItem from "./TodoListItem";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const TodoList = () => {
|
||||||
|
const [items, setItems] = React.useState<string[]>([]);
|
||||||
|
const [inputValue, setInputValue] = React.useState<string>("");
|
||||||
|
|
||||||
|
const handleInputValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
// On empêche le comportement par défaut du formulaire
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// On ajoute un nouvel élément à la liste des tâches
|
||||||
|
setItems([...items, inputValue]);
|
||||||
|
|
||||||
|
// On réinitialise la valeur de l'input
|
||||||
|
setInputValue("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<label htmlFor="todolist-input"></label>
|
||||||
|
|
||||||
|
<input id="todolist-input" type="text" value={inputValue} onChange={handleInputValueChange} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<TodoListItem item={item} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoList;
|
||||||
|
\`\`\`
|
||||||
|
`}
|
||||||
|
</Tabs.Item>
|
||||||
|
|
||||||
|
<Tabs.Item value="todolistitem" label="TodoListItem.tsx">
|
||||||
|
{`
|
||||||
|
\`\`\`tsx showLineNumbers
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface TodoListItemProps {
|
||||||
|
item: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodoListItem = (props: TodoListItemProps) => {
|
||||||
|
return <span>{props.item}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoListItem;
|
||||||
|
\`\`\`
|
||||||
|
`}
|
||||||
|
</Tabs.Item>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -2,26 +2,35 @@ import type { JSX } from "solid-js";
|
|||||||
|
|
||||||
import blurIndigoImage from "@/images/blur-indigo.webp";
|
import blurIndigoImage from "@/images/blur-indigo.webp";
|
||||||
import blurCyanImage from "@/images/blur-cyan.webp";
|
import blurCyanImage from "@/images/blur-cyan.webp";
|
||||||
import { Highlight } from "@/components/Highlight";
|
|
||||||
import { HeroBackground } from "./HeroBackground";
|
import { HeroBackground } from "./HeroBackground";
|
||||||
|
import { Snippet } from "@/components/Snippet";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Image } from "@/components/Image";
|
import { Image } from "@/components/Image";
|
||||||
import { For } from "solid-js";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
const codeLanguage = "javascript";
|
const snippets = [
|
||||||
const code = `export default {
|
{
|
||||||
role: 'developer',
|
name: "memento-dev.config.js",
|
||||||
|
codeLanguage: "javascript",
|
||||||
|
code: `export default {
|
||||||
|
role: "developer",
|
||||||
qualifications: [
|
qualifications: [
|
||||||
'DWWM',
|
"DWWM",
|
||||||
'CDA',
|
"CDA",
|
||||||
'CDUI',
|
"CDUI",
|
||||||
]
|
]
|
||||||
}`;
|
}`,
|
||||||
|
},
|
||||||
const tabs = [
|
{
|
||||||
{ name: "memento-dev.config.js", isActive: true },
|
name: "package.json",
|
||||||
{ name: "package.json", isActive: false },
|
codeLanguage: "json",
|
||||||
|
code: `{
|
||||||
|
"name": "memento-dev",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Memento Dev est une plateforme open-source, soutenue et maintenue par une communauté de contributeurs passionnés.",
|
||||||
|
"main": "index.ts",
|
||||||
|
"license": "MIT"
|
||||||
|
}`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) {
|
function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
@ -87,55 +96,8 @@ export function HeroSection() {
|
|||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10 blur-lg" />
|
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10 blur-lg" />
|
||||||
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10" />
|
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10" />
|
||||||
<div class="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur-sm">
|
|
||||||
<div class="absolute -top-px right-11 left-20 h-px bg-linear-to-r from-violet-300/0 via-violet-300/70 to-violet-300/0" />
|
|
||||||
<div class="absolute right-20 -bottom-px left-11 h-px bg-linear-to-r from-purple-400/0 via-purple-400 to-purple-400/0" />
|
|
||||||
<div class="pt-4 pl-4">
|
|
||||||
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
|
|
||||||
<div class="mt-4 flex space-x-2 text-xs">
|
|
||||||
<For each={tabs}>
|
|
||||||
{(tab) => (
|
|
||||||
<div
|
|
||||||
class={clsx(
|
|
||||||
"flex h-6 rounded-full",
|
|
||||||
tab.isActive
|
|
||||||
? "bg-linear-to-r from-violet-400/30 via-violet-400 to-violet-400/30 p-px font-medium text-violet-300"
|
|
||||||
: "text-slate-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class={clsx(
|
|
||||||
"flex items-center rounded-full px-2.5",
|
|
||||||
tab.isActive && "bg-slate-800",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{tab.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div class="mt-6 flex items-start px-1 text-sm">
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="border-r border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
|
|
||||||
>
|
|
||||||
<For
|
|
||||||
each={Array.from({ length: code.split("\n").length })}
|
|
||||||
>
|
|
||||||
{(_, index) => (
|
|
||||||
<>
|
|
||||||
{(index() + 1).toString().padStart(2, "0")}
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Highlight language={codeLanguage}>{code}</Highlight>
|
<Snippet class="min-h-64" snippets={snippets} />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -58,7 +58,7 @@ function NavigationItem(props: NavigationItemProps) {
|
|||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
{isOpened() && (
|
{isOpened() && (
|
||||||
<ul class="!mt-0 ml-2 space-y-1 border-l-2 border-slate-100 lg:mt-4 lg:space-y-2 lg:border-slate-200 mb-2">
|
<ul class="!mt-0 ml-2 border-l-2 border-slate-100 lg:mt-4 lg:space-y-1 lg:border-slate-200 mb-2">
|
||||||
<For each={props.section.links}>
|
<For each={props.section.links}>
|
||||||
{(link) => (
|
{(link) => (
|
||||||
<li class="relative">
|
<li class="relative">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user