added project pages, updated page for FAM.
This commit is contained in:
10
app/context/ProjectContext.tsx
Normal file
10
app/context/ProjectContext.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const ProjectContext = createContext<any>(null);
|
||||
|
||||
export function ProjectProvider({ children, value }: { children: React.ReactNode, value: any }) {
|
||||
return <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>;
|
||||
}
|
||||
|
||||
export const useSharedData = () => useContext(ProjectContext);
|
||||
@@ -149,6 +149,14 @@ body {
|
||||
height: 28;
|
||||
}
|
||||
|
||||
.expand-on-hover-button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.expand-on-hover-button:hover {
|
||||
scale: 1.05;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 640px) {
|
||||
h1 {
|
||||
font-size: 32px !important;
|
||||
|
||||
@@ -5,6 +5,7 @@ import "./globals.css";
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import ThemeSwitch from "@/components/ThemeSwitch";
|
||||
|
||||
|
||||
const mono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--mono",
|
||||
|
||||
@@ -4,9 +4,7 @@ import { useState } from "react";
|
||||
import { useMountTransition } from "@/hooks/useAnimations";
|
||||
import Nav from "@/components/Nav";
|
||||
import Profile from "@/components/Profile";
|
||||
import Projects from "@/components/Projects";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export default function Home() {
|
||||
const [section, setSection] = useState("profile");
|
||||
const mounted = useMountTransition();
|
||||
@@ -44,11 +42,9 @@ export default function Home() {
|
||||
|
||||
</header>
|
||||
|
||||
<Nav section={section} setSection={setSection} />
|
||||
|
||||
<Nav section={"profile"} />
|
||||
<main>
|
||||
{section === "profile" && <Profile />}
|
||||
{section === "projects" && <Projects />}
|
||||
<Profile />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
54
app/profile/layout.tsx
Normal file
54
app/profile/layout.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import Nav from "@/components/Nav";
|
||||
|
||||
import Footer from "@/components/Footer";
|
||||
export default function Layout({children}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100vh", paddingBottom: 36 }}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 880,
|
||||
margin: "0 auto",
|
||||
padding: "0 24px",
|
||||
transition: "all 0.5s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
}}
|
||||
>
|
||||
<header
|
||||
style={{
|
||||
padding: "24px 0 0",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--mono)",
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: "var(--accent)",
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<Nav section={"profile"} />
|
||||
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/profile/page.tsx
Normal file
12
app/profile/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Profile from "@/components/Profile";
|
||||
export default function ProfilePage() {
|
||||
return (
|
||||
|
||||
<main>
|
||||
<Profile />
|
||||
</main>
|
||||
|
||||
);
|
||||
}
|
||||
18
app/project/[slug]/layout.tsx
Normal file
18
app/project/[slug]/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ProjectProvider } from "../../context/ProjectContext";
|
||||
import { getProjectDetails } from "@/data/content";
|
||||
|
||||
export default async function SlugLayout({children, params}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
|
||||
const {slug} = await params;
|
||||
const projectData = await getProjectDetails(slug)
|
||||
return (
|
||||
<ProjectProvider value={projectData}>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</ProjectProvider>
|
||||
);
|
||||
}
|
||||
105
app/project/[slug]/page.tsx
Normal file
105
app/project/[slug]/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
import { use } from 'react'
|
||||
|
||||
import { redirect, RedirectType } from 'next/navigation'
|
||||
import { useSharedData } from '@/app/context/ProjectContext';
|
||||
import { useMountTransition } from '@/hooks/useAnimations';
|
||||
import "../../../styles/markdown-container.css";
|
||||
import Link from 'next/link';
|
||||
import { PROJECTS } from '@/data/content';
|
||||
|
||||
export default function ProjectPage({
|
||||
params,
|
||||
}: {params: Promise<{ slug: string }>}) {
|
||||
|
||||
const mounted = useMountTransition(50)
|
||||
const { slug } = use(params);
|
||||
|
||||
if (!slug) redirect('project',RedirectType.replace);
|
||||
|
||||
const hasValidProject = PROJECTS.find((v)=>v.slug == slug)
|
||||
const hasValidDetails = hasValidProject && PROJECTS.find((v)=>v.slug == slug)?.details
|
||||
const projectDetails = useSharedData();
|
||||
|
||||
return (
|
||||
<div
|
||||
id="project-markdown-container"
|
||||
className={"markdown-container"}
|
||||
style={{
|
||||
opacity: mounted ? 1 : 0,
|
||||
transition: "all 0.5s ease",
|
||||
translate: mounted ? "0 2em" : "0 0"
|
||||
}}
|
||||
>
|
||||
|
||||
{
|
||||
hasValidDetails ?
|
||||
(
|
||||
<main dangerouslySetInnerHTML={{ __html: projectDetails }}>
|
||||
</main>
|
||||
) : (
|
||||
<p>This project has no details set up for it yet.</p>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "1em",
|
||||
marginBottom: "2em"
|
||||
}}>
|
||||
<Link onClick={(e)=>{
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}}
|
||||
|
||||
className="expand-on-hover-button"
|
||||
style={{
|
||||
fontFamily: "var(--mono)",
|
||||
fontSize: 15,
|
||||
color: "var(--fg)",
|
||||
border: "1px solid var(--border)",
|
||||
backgroundColor: "var(--bg)",
|
||||
height: "3em",
|
||||
display: "block",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
width: "10em",
|
||||
borderRadius: "var(--radius-md)",
|
||||
|
||||
}} href={"#"}>Back To Top</Link>
|
||||
|
||||
<Link onClick={(e)=>{
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}}
|
||||
|
||||
className="expand-on-hover-button"
|
||||
style={{
|
||||
fontFamily: "var(--mono)",
|
||||
fontSize: 15,
|
||||
color: "var(--fg)",
|
||||
border: "1px solid var(--border)",
|
||||
backgroundColor: "var(--bg)",
|
||||
height: "3em",
|
||||
display: "block",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
width: "10em",
|
||||
borderRadius: "var(--radius-md)",
|
||||
|
||||
}} href={"/project"}>More Projects</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
app/project/layout.tsx
Normal file
55
app/project/layout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
import Nav from "@/components/Nav";
|
||||
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export default async function Layout({children, params}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
|
||||
const {slug} = await params;
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100vh", paddingBottom: 36 }}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 880,
|
||||
margin: "0 auto",
|
||||
padding: "0 24px",
|
||||
transition: "all 0.5s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
}}
|
||||
>
|
||||
<header
|
||||
style={{
|
||||
padding: "24px 0 0",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--mono)",
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: "var(--accent)",
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<Nav section={"project"} subtitle={slug} />
|
||||
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
app/project/page.tsx
Normal file
14
app/project/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import ProjectsGrid from "@/components/ProjectsGrid";
|
||||
|
||||
|
||||
export default function Project() {
|
||||
return (
|
||||
|
||||
<main>
|
||||
<ProjectsGrid />
|
||||
</main>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
type Props = {
|
||||
section: string;
|
||||
setSection: (s: string) => void;
|
||||
subtitle?: string;
|
||||
};
|
||||
|
||||
const items = ["profile", "projects"];
|
||||
const items = ["profile", "project"];
|
||||
|
||||
export default function Nav({ section, setSection }: Props) {
|
||||
export default function Nav({ section, subtitle }: Props) {
|
||||
return (
|
||||
<nav
|
||||
style={{
|
||||
@@ -21,10 +23,9 @@ export default function Nav({ section, setSection }: Props) {
|
||||
}}>
|
||||
|
||||
{items.map((item) => (
|
||||
|
||||
<Link key={item} href={"/" + item}>
|
||||
<button
|
||||
key={item}
|
||||
onClick={() => setSection(item)}
|
||||
|
||||
style={{
|
||||
background: section === item ? "var(--surface)" : "transparent",
|
||||
color: section === item ? "var(--accent)" : "var(--muted)",
|
||||
@@ -50,9 +51,11 @@ export default function Nav({ section, setSection }: Props) {
|
||||
|
||||
<span style={{ opacity: 0.4, marginRight: 6 }}></span>
|
||||
{item}
|
||||
</button>
|
||||
</button></Link>
|
||||
))}
|
||||
|
||||
<p>{subtitle}</p>
|
||||
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,22 @@ export default function ProfileMore() {
|
||||
setMorePosition(morePosition + 1);
|
||||
}
|
||||
|
||||
function handleEnter(){
|
||||
if (delayTimer) return;
|
||||
setHovered(true);
|
||||
let timer = window.setTimeout(() => {
|
||||
handleSectionExpand();
|
||||
}, 500);
|
||||
setDelayTimer(timer);
|
||||
}
|
||||
|
||||
function handleLeave() {
|
||||
if (delayTimer){
|
||||
clearTimeout(delayTimer);
|
||||
setDelayTimer(null);
|
||||
}
|
||||
setHovered(false);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="more-section-container"
|
||||
@@ -63,22 +79,19 @@ export default function ProfileMore() {
|
||||
})}
|
||||
|
||||
<div {...handlers} className={`drop-handle ${wiggling ? "wiggle" : ""}`}
|
||||
onMouseEnter={() => {
|
||||
if (delayTimer) return;
|
||||
setHovered(true);
|
||||
|
||||
let timer = window.setTimeout(() => {
|
||||
handleSectionExpand();
|
||||
}, 500);
|
||||
setDelayTimer(timer);
|
||||
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (delayTimer){
|
||||
onMouseEnter={
|
||||
() => handleEnter()
|
||||
}
|
||||
onMouseLeave={() => handleLeave()}
|
||||
onTouchStart={
|
||||
() => handleEnter()
|
||||
}
|
||||
onTouchEnd={()=>handleLeave()}
|
||||
onClick={() => {
|
||||
if (delayTimer){
|
||||
clearTimeout(delayTimer);
|
||||
setDelayTimer(null);
|
||||
}
|
||||
setHovered(false);
|
||||
}
|
||||
handleSectionExpand();
|
||||
}}
|
||||
style={{
|
||||
opacity: morePosition < PROFILE.moreSections.length ? 1.0 : 0.0,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Project } from "@/data/content";
|
||||
import SimpleGallery from "./SimpleGallery";
|
||||
import Link from "next/link";
|
||||
import { relative } from "path";
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -143,10 +145,27 @@ export default function ProjectCard({ project, visible, selected = false, setSel
|
||||
</p>
|
||||
|
||||
{contentRef.current && project.images && project.images.length > 0 && (
|
||||
<SimpleGallery images={project.images} videos={project.videos} title={project.title}
|
||||
/>
|
||||
<SimpleGallery images={project.images} videos={project.videos} title={project.title} />
|
||||
)}
|
||||
|
||||
<Link
|
||||
className="expand-on-hover-button"
|
||||
style={{
|
||||
fontFamily: "var(--mono)",
|
||||
fontSize: 15,
|
||||
color: "var(--fg)",
|
||||
border: `1px solid ${hovered ? "var(--accent)" : "var(--border)"}`,
|
||||
backgroundColor: "var(--bg)",
|
||||
height: "3em",
|
||||
display: "block",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
width: "10em",
|
||||
borderRadius: "var(--radius-md)",
|
||||
position: "absolute",
|
||||
right: "1em",
|
||||
bottom: "1em"
|
||||
}} href={"/project/"+project.slug} >See More</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
123
data/content.ts
123
data/content.ts
@@ -93,20 +93,98 @@ export const PROJECTS: Project[] = [{
|
||||
slug: "flowbased-agent-management",
|
||||
title: "Flow-Based Agent Management platform",
|
||||
description: "The Flow-based Agent Managemment (FAM) platform is a real-world example of an Agent support tool designed to streamline high-volume in-person Agent to Guest interactions in various scenarios.",
|
||||
details: `The source for this project is private, but I am happy to
|
||||
discuss the architecture and design decisions in detail during
|
||||
an interview.
|
||||
details: `# Flow-based Agent Management platform (FAM)
|
||||
|
||||
This application was deployed for a major convention in May 2026, where it achieved a pretty wild amount of success:
|
||||
- Single day registration of over 10,000 guests with an average processing time of about 45 seconds.
|
||||
- No single guest wait time greater than 10 minutes.
|
||||
|
||||
The previous year at the same convention with the same attendance levels experienced major wait times, and physical lines that extended around multiple floors of a large convention space. Some
|
||||
attendees waited in line for hours.
|
||||
|
||||
The platform consists of a Vue 3 frontend and a configurable backend interface.
|
||||
It features a dynamic UX emphasized flow-based interface that allows agents to navigate complex tasks efficiently, and with minimal training.
|
||||
A real-world solution for event staff and volunteer management.
|
||||
The Flow-based Agent Managemment (FAM) platform is designed to streamline high-volume in-person Agent to Guest interactions using the concept of workflows.
|
||||
|
||||
<blockquote>A welcome screen using the default theme.
|
||||
<a target="_blank" href="/img/projects/fam/example-welcome.png">
|
||||
<img src="/img/projects/fam/example-welcome.png" alt="Alt text" width="600"/>
|
||||
</a>
|
||||
</blockquote>
|
||||
|
||||
|
||||
## Private Repository
|
||||
This project was tailored to the needs of a large organization. As such, and naturally so — the source for this project is private.
|
||||
|
||||
I am happy to discuss the architecture and design decisions made for this project in detail during an interview.
|
||||
|
||||
<blockquote>A short video of the platform's operation.
|
||||
<video width="640" height="360" controls>
|
||||
<source src="/img/projects/fam/runthrough.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</blockquote>
|
||||
|
||||
## Summary
|
||||
|
||||
In the context of <b>FAM</b>, an \`interaction\` is the behavior that occurs between a guest and an agent.
|
||||
An \`agent\` is a real-person (and most often a representative of some business) whom is interacting with a \`guest\` in order to exchange information or complete some task.
|
||||
|
||||
A front-desk employee at a hotel checking in a guest is an easy to reach example. The ticketing/admission agents at an event might be another.
|
||||
These agent to guest interactions are what drive an organization on the ground.
|
||||
|
||||
These interactions might occur at high volumes and might be sustained throughout normal business operation. In such cases,
|
||||
the amount of time spent completing tasks and exchanging information might have a very high impact on a business' operations.
|
||||
|
||||
FAM supports breaking these interactions down into streamlined, consistent, efficient, repeatable, and flexible
|
||||
workflows.
|
||||
|
||||
<blockquote>Not every interaction will be the same. Some guests might have additional service needs.
|
||||
<a target="_blank" href="/img/projects/fam/reference-1.png">
|
||||
<img src="/img/projects/fam/reference-1.png" alt="A workflow, and a workload." width="800"/>
|
||||
</a>
|
||||
</blockquote>
|
||||
|
||||
|
||||
|
||||
There is a minimum amount of time that it takes an agent to complete a task in any software, and there is a reasonable maximum amount of time that might be spent on
|
||||
unusual circumstances or perhaps lost to the 'growing pains' of inexperienced staff or imperfect systems.
|
||||
|
||||
FAM tries to shorten the duration of agent to guest interactions without reducing the quality of the interaction through purpose-built UX-first guided workflows. Task identification from business analysis are made into functional workflows that support reducing the time taken to complete tasks while also addressing the variability in
|
||||
how much time that it takes to complete these tasks.
|
||||
|
||||
|
||||
<blockquote>The FAM platform runs on a vue/nuxt stack. Here's a part of the app structure in a diagram, without exposing too much.
|
||||
<a target="_blank" href="/img/projects/fam/reference-1.png">
|
||||
<img src="/img/projects/fam/workflow.png" alt="Workflow architecture" width="800"/>
|
||||
</a>
|
||||
</blockquote>
|
||||
|
||||
|
||||
A step can contain any type of work, and can be used in multiple workflows. An identification step for a check-in process using the default template might look like such:
|
||||
|
||||
<blockquote>The general look and feel of a step in a workflow.
|
||||
<a target="_blank" href="/img/projects/fam/example-step.png">
|
||||
<img src="/img/projects/fam/example-step.png" alt="Example Step" width="800"/>
|
||||
</a>
|
||||
</blockquote>
|
||||
|
||||
In this picture, a guest has had their ID card scanned and their profile information is displayed. The area with a marching border on the right side integrates an external barcode scanner in order to
|
||||
read information on the back of US driver's license. This information is compared with that on the left, and discrepencies are highlighted in yellow. Checking the acknowledgement box at the bottom moves
|
||||
the workflow into the next step.
|
||||
|
||||
## Implementation
|
||||
|
||||
This platform was implemented as a solution for in-person registration, VIP registration, pre-event mailouts, and entitlements fulfillment for an Atlanta-based organization's yearly convention for May 2026, where it achieved significant success:
|
||||
The setup consists of a Vue 3 frontend and a configurable backend interface. This particular implementation utilized a Perl catalyst backend. Standard 2D barcode scanning devices and RFID tag readers were used
|
||||
in the guest identification system and have out-of-the box modular support in flows created on the platform.
|
||||
|
||||
<pre>Some Stats:</pre>
|
||||
|
||||
- Single day registration of over 10,000 guests with an average processing time of about 45 seconds.
|
||||
- Previous year: ~2 minutes.
|
||||
- No single guest wait time greater than 10 minutes.
|
||||
- Previous year had lines that wrapped around the entire building and there were stories of four hour waits.
|
||||
|
||||
<blockquote>A testament
|
||||
<a target="_blank" href="/img/projects/fam/stats-2026-1.png">
|
||||
<img src="/img/projects/fam/stats-2026-1.png" alt="Example Step" width="800"/>
|
||||
</a>
|
||||
</blockquote>
|
||||
|
||||
|
||||
A real-world solution for event staff and volunteer management.
|
||||
`,
|
||||
tags: ["Vue", "Node.JS", "Event-Management", "Real-Time", "Full-Stack"],
|
||||
link: "private",
|
||||
@@ -130,3 +208,22 @@ export const PROJECTS: Project[] = [{
|
||||
link: "https://git.hwilliams.dev/hwilliams/hwilliams-dev",
|
||||
year: "2026",
|
||||
}];
|
||||
|
||||
|
||||
import { remark } from "remark";
|
||||
import remarkParse from "remark-parse";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import remarkRehype from "remark-rehype";
|
||||
|
||||
|
||||
export async function getProjectDetails(slug: string) {
|
||||
const p = PROJECTS.find((v)=>v.slug == slug);
|
||||
if (!p || !p.details) return null;
|
||||
const re = await remark()
|
||||
.use(remarkParse)
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeRaw)
|
||||
.use(rehypeStringify).process(p.details);
|
||||
return re?.toString();
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactCompiler: true,
|
||||
output: "standalone"
|
||||
output: "standalone"
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
1334
package-lock.json
generated
1334
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,14 @@
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-grid-layout" : "^2.2.3",
|
||||
"next-themes": "^0.4.6"
|
||||
"next-themes": "^0.4.6",
|
||||
"remark": "^15.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-html": "^16.0.1",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-rewrite":"^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 179 KiB |
BIN
public/img/projects/fam/example-step.png
Normal file
BIN
public/img/projects/fam/example-step.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
BIN
public/img/projects/fam/reference-1.png
Normal file
BIN
public/img/projects/fam/reference-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
public/img/projects/fam/stats-2026-1.png
Normal file
BIN
public/img/projects/fam/stats-2026-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
BIN
public/img/projects/fam/workflow.png
Normal file
BIN
public/img/projects/fam/workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -1,6 +1,4 @@
|
||||
|
||||
@import '../app/globals.css';
|
||||
|
||||
.gallery-action-button {
|
||||
position: absolute;
|
||||
width: 28;
|
||||
|
||||
70
styles/markdown-container.css
Normal file
70
styles/markdown-container.css
Normal file
@@ -0,0 +1,70 @@
|
||||
|
||||
.markdown-container{
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5em;
|
||||
font-family: var(--sans);
|
||||
font-size: 16;
|
||||
line-height: 1.7;
|
||||
color: var(--fg-secondary);
|
||||
margin: 0 0 40px 0;
|
||||
max-width: 560;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--mono);
|
||||
font-weight: 700;
|
||||
color: var(--fg);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 0.5em;
|
||||
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0 0 40px 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: var(--sans);
|
||||
font-size: 12;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
ul {
|
||||
font-family: var(--sans);
|
||||
list-style: inside;
|
||||
margin: 0 0 40px 0;
|
||||
}
|
||||
|
||||
ul ul {
|
||||
padding-left: 1em;
|
||||
margin-bottom: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
ul > li {
|
||||
font-size: 14;
|
||||
margin: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
ul ul > li {
|
||||
font-size: 12;
|
||||
margin: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: var(--mono);
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
|
||||
.more-section-container {
|
||||
|
||||
}
|
||||
.drop-handle {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
|
||||
@@ -29,6 +29,6 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
, "pages/test.jax" ],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user