mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
Feat: Add custom landing page with theme switcher and auto-versioned SPM badge
- Next.js static site in docs/ with SN Pro font and WarText headline - 4 phosphor themes (Green, Amber, White, Red) with localStorage persistence - Animated cloud background tinted to active theme - SF Symbols icons via sf-symbols-lib/hierarchical - PackageBadge with copy-to-clipboard, version from CI tag - CI workflow builds landing page + DocC, deploys to GitHub Pages - Version auto-resolved from git tags on deploy
This commit is contained in:
+36
-14
@@ -3,6 +3,7 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["*"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
@@ -61,15 +62,46 @@ jobs:
|
||||
DISABLE_SWIFTLINT: "1"
|
||||
run: swift test
|
||||
|
||||
# ── DocC Documentation (macOS only, main branch only) ──────────
|
||||
# ── Landing Page + DocC (macOS only, main/tag only) ────────────
|
||||
build-docs:
|
||||
name: Build DocC
|
||||
name: Build Landing Page & DocC
|
||||
needs: build
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
# Use tag name if triggered by a tag push, otherwise latest tag
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
else
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.1.0")
|
||||
fi
|
||||
# Strip leading 'v' if present (v1.2.3 -> 1.2.3)
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved version: $VERSION"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: docs/package-lock.json
|
||||
|
||||
- name: Build landing page
|
||||
working-directory: docs
|
||||
env:
|
||||
TUIKIT_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Build DocC documentation
|
||||
env:
|
||||
@@ -81,19 +113,9 @@ jobs:
|
||||
--output-path docs-output \
|
||||
--transform-for-static-hosting
|
||||
|
||||
- name: Add root redirect to documentation
|
||||
- name: Assemble site
|
||||
run: |
|
||||
cat > docs-output/index.html << 'HTMLEOF'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; url=/documentation/tuikit" />
|
||||
</head>
|
||||
<body>
|
||||
<a href="/documentation/tuikit">Redirecting to TUIkit Documentation...</a>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
cp -r docs/out/* docs-output/
|
||||
|
||||
- name: Add CNAME for custom domain
|
||||
run: echo "tuikit.layered.work" > docs-output/CNAME
|
||||
|
||||
+3
-2
@@ -10,9 +10,10 @@ DerivedData/
|
||||
# DocC generated output
|
||||
/docs-output/
|
||||
|
||||
# VitePress and dependencies
|
||||
# Next.js landing page
|
||||
docs/node_modules/
|
||||
docs/.vitepress/dist/
|
||||
docs/.next/
|
||||
docs/out/
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Animated cloudy background tinted to the active theme color.
|
||||
*
|
||||
* Uses CSS custom properties (--accent-glow, --accent-glow-secondary,
|
||||
* --accent-glow-tertiary) so the clouds automatically recolor on theme switch.
|
||||
* Each blob has its own animation timing, position, and intensity.
|
||||
*/
|
||||
export default function CloudBackground() {
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
|
||||
{/* Large cloud — top left */}
|
||||
<div
|
||||
className="absolute -left-32 -top-32 h-[800px] w-[800px] rounded-full blur-[150px] transition-[background] duration-700"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(var(--accent-glow),0.50) 0%, rgba(var(--accent-glow-secondary),0.20) 40%, transparent 70%)",
|
||||
animation: "cloud-drift 90s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Medium cloud — top right */}
|
||||
<div
|
||||
className="absolute -right-20 top-1/4 h-[700px] w-[700px] rounded-full blur-[130px] transition-[background] duration-700"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(var(--accent-glow-secondary),0.40) 0%, rgba(var(--accent-glow-tertiary),0.15) 40%, transparent 70%)",
|
||||
animation: "cloud-drift-reverse 110s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Accent cloud — center */}
|
||||
<div
|
||||
className="absolute left-1/3 top-1/2 h-[600px] w-[600px] rounded-full blur-[120px] transition-[background] duration-700"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(var(--accent-glow),0.35) 0%, rgba(var(--accent-glow-secondary),0.12) 40%, transparent 70%)",
|
||||
animation: "cloud-drift-slow 120s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom-left glow */}
|
||||
<div
|
||||
className="absolute -bottom-40 -left-20 h-[700px] w-[700px] rounded-full blur-[140px] transition-[background] duration-700"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(var(--accent-glow-tertiary),0.38) 0%, rgba(var(--accent-glow-secondary),0.14) 40%, transparent 70%)",
|
||||
animation: "cloud-drift-reverse 100s ease-in-out infinite",
|
||||
animationDelay: "-30s",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Wide upper cloud for depth */}
|
||||
<div
|
||||
className="absolute left-1/2 -top-20 h-[500px] w-[900px] -translate-x-1/2 rounded-full blur-[150px] transition-[background] duration-700"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse, rgba(var(--accent-glow),0.30) 0%, transparent 60%)",
|
||||
animation: "cloud-drift-slow 130s ease-in-out infinite",
|
||||
animationDelay: "-50s",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Extra cloud — bottom right */}
|
||||
<div
|
||||
className="absolute -bottom-20 -right-32 h-[600px] w-[600px] rounded-full blur-[130px] transition-[background] duration-700"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(var(--accent-glow-secondary),0.32) 0%, rgba(var(--accent-glow),0.10) 40%, transparent 70%)",
|
||||
animation: "cloud-drift 105s ease-in-out infinite",
|
||||
animationDelay: "-40s",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
/** A Swift code block with copy-to-clipboard and minimal syntax highlighting. */
|
||||
export default function CodePreview() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const code = `import TUIkit
|
||||
|
||||
@main
|
||||
struct MyApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
VStack {
|
||||
Text("Hello, TUIkit!")
|
||||
.bold()
|
||||
.foregroundColor(.cyan)
|
||||
Button("Press me") {
|
||||
// handle action
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative w-full overflow-hidden rounded-xl border border-border bg-[#0c0c0e]">
|
||||
{/* Header bar */}
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-3 w-3 rounded-full bg-[#ff5f57]" />
|
||||
<div className="h-3 w-3 rounded-full bg-[#febc2e]" />
|
||||
<div className="h-3 w-3 rounded-full bg-[#28c840]" />
|
||||
</div>
|
||||
<span className="ml-3 text-xs text-muted">MyApp.swift</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="rounded-md px-2.5 py-1 text-xs text-muted transition-colors hover:bg-white/5 hover:text-foreground"
|
||||
>
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Code content */}
|
||||
<pre className="overflow-x-auto p-5 text-base leading-relaxed">
|
||||
<code>
|
||||
<Highlight code={code} />
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Minimal Swift syntax highlighter — no external dependency. */
|
||||
function Highlight({ code }: { code: string }) {
|
||||
const keywords =
|
||||
/\b(struct|var|some|func|import|let|return|if|else|for|in|while|switch|case|default|class|protocol|enum|init|self|true|false|nil|private|public|internal)\b/g;
|
||||
const decorators = /(@\w+)/g;
|
||||
const types = /\b(App|Scene|WindowGroup|VStack|HStack|Text|Button|View|String|Int|Bool|Never)\b/g;
|
||||
const strings = /("(?:[^"\\]|\\.)*")/g;
|
||||
const comments = /(\/\/.*$)/gm;
|
||||
const modifiers = /(\.\w+\()/g;
|
||||
|
||||
const lines = code.split("\n");
|
||||
|
||||
return (
|
||||
<>
|
||||
{lines.map((line, lineIndex) => (
|
||||
<span key={lineIndex}>
|
||||
{tokenizeLine(line, {
|
||||
keywords,
|
||||
decorators,
|
||||
types,
|
||||
strings,
|
||||
comments,
|
||||
modifiers,
|
||||
})}
|
||||
{lineIndex < lines.length - 1 && "\n"}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface Patterns {
|
||||
keywords: RegExp;
|
||||
decorators: RegExp;
|
||||
types: RegExp;
|
||||
strings: RegExp;
|
||||
comments: RegExp;
|
||||
modifiers: RegExp;
|
||||
}
|
||||
|
||||
function tokenizeLine(line: string, patterns: Patterns) {
|
||||
// Comments take precedence
|
||||
const commentMatch = line.match(/^(.*?)(\/\/.*)$/);
|
||||
if (commentMatch) {
|
||||
const [, before, comment] = commentMatch;
|
||||
return (
|
||||
<>
|
||||
{tokenizeSegment(before, patterns)}
|
||||
<span className="text-[#6a737d]">{comment}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return tokenizeSegment(line, patterns);
|
||||
}
|
||||
|
||||
function tokenizeSegment(segment: string, patterns: Patterns) {
|
||||
// Build a combined regex for all token types
|
||||
const combined =
|
||||
/(@\w+)|("(?:[^"\\]|\\.)*")|(\.\w+)\(|\b(struct|var|some|func|import|let|return|if|else|for|in|while|switch|case|default|class|protocol|enum|init|self|true|false|nil|private|public|internal)\b|\b(App|Scene|WindowGroup|VStack|HStack|Text|Button|View|String|Int|Bool|Never)\b/g;
|
||||
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// Reset pattern state
|
||||
combined.lastIndex = 0;
|
||||
|
||||
while ((match = combined.exec(segment)) !== null) {
|
||||
// Add text before match
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(segment.slice(lastIndex, match.index));
|
||||
}
|
||||
|
||||
if (match[1]) {
|
||||
// Decorator (@main, @State)
|
||||
parts.push(
|
||||
<span key={match.index} className="text-[#d19a66]">
|
||||
{match[1]}
|
||||
</span>
|
||||
);
|
||||
} else if (match[2]) {
|
||||
// String literal
|
||||
parts.push(
|
||||
<span key={match.index} className="text-[#98c379]">
|
||||
{match[2]}
|
||||
</span>
|
||||
);
|
||||
} else if (match[3]) {
|
||||
// Modifier (.bold, .foregroundColor) — add dot+name, then the ( back
|
||||
parts.push(
|
||||
<span key={match.index} className="text-[#61afef]">
|
||||
{match[3]}
|
||||
</span>
|
||||
);
|
||||
parts.push("(");
|
||||
} else if (match[4]) {
|
||||
// Keyword
|
||||
parts.push(
|
||||
<span key={match.index} className="text-[#c678dd]">
|
||||
{match[4]}
|
||||
</span>
|
||||
);
|
||||
} else if (match[5]) {
|
||||
// Type
|
||||
parts.push(
|
||||
<span key={match.index} className="text-[#e5c07b]">
|
||||
{match[5]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
lastIndex = combined.lastIndex;
|
||||
}
|
||||
|
||||
// Remaining text
|
||||
if (lastIndex < segment.length) {
|
||||
parts.push(segment.slice(lastIndex));
|
||||
}
|
||||
|
||||
return <>{parts}</>;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface FeatureCardProps {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/** A feature highlight card with icon, title, and description. */
|
||||
export default function FeatureCard({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
}: FeatureCardProps) {
|
||||
return (
|
||||
<div className="group rounded-xl border border-border bg-card p-6 transition-all duration-300 hover:border-accent/30 hover:bg-card-hover">
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-accent transition-colors group-hover:bg-accent/15">
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-foreground">{title}</h3>
|
||||
</div>
|
||||
<p className="text-xl leading-relaxed text-muted">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SFSymbol,
|
||||
SFAppleTerminalFill,
|
||||
SFPaintbrushFill,
|
||||
SFKeyboardFill,
|
||||
SFSquareStack3dUpFill,
|
||||
SFBoltFill,
|
||||
SFDocumentFill,
|
||||
SFEyeFill,
|
||||
SFArrowLeftArrowRight,
|
||||
SFSwift,
|
||||
SFCheckmarkCircleFill,
|
||||
} from "sf-symbols-lib/hierarchical";
|
||||
|
||||
/** Maps of icon names to SF Symbol constants for type-safe usage. */
|
||||
const icons = {
|
||||
terminal: SFAppleTerminalFill,
|
||||
paintbrush: SFPaintbrushFill,
|
||||
keyboard: SFKeyboardFill,
|
||||
stack: SFSquareStack3dUpFill,
|
||||
bolt: SFBoltFill,
|
||||
document: SFDocumentFill,
|
||||
eye: SFEyeFill,
|
||||
arrows: SFArrowLeftArrowRight,
|
||||
swift: SFSwift,
|
||||
checkmark: SFCheckmarkCircleFill,
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
interface IconProps {
|
||||
name: IconName;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Wrapper around SFSymbol that works in both server and client contexts. */
|
||||
export default function Icon({ name, size = 20, className }: IconProps) {
|
||||
return <SFSymbol name={icons[name]} size={size} className={className} />;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const VERSION = process.env.TUIKIT_VERSION ?? "0.1.0";
|
||||
const PACKAGE_LINE = `.package(url: "https://github.com/phranck/TUIkit.git", from: "${VERSION}")`;
|
||||
|
||||
/** SPM package dependency badge with copy-to-clipboard. */
|
||||
export default function PackageBadge() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(PACKAGE_LINE);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-3 rounded-full border border-border bg-card/50 px-6 py-3 text-muted backdrop-blur-sm">
|
||||
<Icon name="swift" size={20} className="text-accent" />
|
||||
<code className="font-mono text-xl">
|
||||
{PACKAGE_LINE}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy to clipboard"
|
||||
className="ml-1 rounded-md p-1.5 text-muted transition-colors hover:bg-white/10 hover:text-foreground"
|
||||
>
|
||||
{copied ? (
|
||||
<Icon name="checkmark" size={18} className="text-accent" />
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
/** Available phosphor themes matching TUIkit's built-in palettes. */
|
||||
export const themes = ["green", "amber", "white", "red"] as const;
|
||||
export type Theme = (typeof themes)[number];
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>({
|
||||
theme: "amber",
|
||||
setTheme: () => {},
|
||||
});
|
||||
|
||||
const STORAGE_KEY = "tuikit-theme";
|
||||
|
||||
/** Provides theme state and applies it to the document root via data-theme attribute. */
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>("amber");
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
||||
if (stored && themes.includes(stored)) {
|
||||
setThemeState(stored);
|
||||
document.documentElement.setAttribute("data-theme", stored);
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "amber");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setTheme = useCallback((next: Theme) => {
|
||||
setThemeState(next);
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
localStorage.setItem(STORAGE_KEY, next);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/** Hook to access the current theme and setter. */
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme, themes, type Theme } from "./ThemeProvider";
|
||||
|
||||
/** Color dot for each theme used in the switcher. */
|
||||
const themeColors: Record<Theme, string> = {
|
||||
green: "#33ff33",
|
||||
amber: "#ffbf00",
|
||||
white: "#e4e4e7",
|
||||
red: "#ff3333",
|
||||
};
|
||||
|
||||
/** Theme labels for accessibility. */
|
||||
const themeLabels: Record<Theme, string> = {
|
||||
green: "Green",
|
||||
amber: "Amber",
|
||||
white: "White",
|
||||
red: "Red",
|
||||
};
|
||||
|
||||
/** Compact theme switcher with colored dots and active indicator. */
|
||||
export default function ThemeSwitcher() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{themes.map((themeOption) => (
|
||||
<button
|
||||
key={themeOption}
|
||||
onClick={() => setTheme(themeOption)}
|
||||
aria-label={`Switch to ${themeLabels[themeOption]} theme`}
|
||||
className="group relative flex h-7 w-7 items-center justify-center rounded-full transition-transform hover:scale-110"
|
||||
>
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full transition-all"
|
||||
style={{
|
||||
backgroundColor: themeColors[themeOption],
|
||||
boxShadow:
|
||||
theme === themeOption
|
||||
? `0 0 6px ${themeColors[themeOption]}, 0 0 14px ${themeColors[themeOption]}60`
|
||||
: "none",
|
||||
opacity: theme === themeOption ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,203 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=SN+Pro:wght@400;500;600;700&display=swap");
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: "WarText";
|
||||
src: url("/fonts/WarText.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Theme: Amber (default) */
|
||||
:root,
|
||||
[data-theme="amber"] {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #e8dcc2;
|
||||
--accent: #fbbf24;
|
||||
--accent-secondary: #f59e0b;
|
||||
--accent-glow: 251, 191, 36;
|
||||
--accent-glow-secondary: 245, 158, 11;
|
||||
--accent-glow-tertiary: 217, 119, 6;
|
||||
--headline-color: #ffbf00;
|
||||
--headline-glow: 255, 191, 0;
|
||||
--muted: #ad9a6e;
|
||||
--border: #27272a;
|
||||
--card: #111113;
|
||||
--card-hover: #18181b;
|
||||
}
|
||||
|
||||
/* Theme: Green */
|
||||
[data-theme="green"] {
|
||||
--foreground: #c2e8d4;
|
||||
--accent: #6ee7b7;
|
||||
--accent-secondary: #34d399;
|
||||
--accent-glow: 110, 231, 183;
|
||||
--accent-glow-secondary: 52, 211, 153;
|
||||
--accent-glow-tertiary: 16, 185, 129;
|
||||
--headline-color: #33ff33;
|
||||
--headline-glow: 51, 255, 51;
|
||||
--muted: #7aad94;
|
||||
}
|
||||
|
||||
/* Theme: White */
|
||||
[data-theme="white"] {
|
||||
--foreground: #d8d8da;
|
||||
--accent: #e4e4e7;
|
||||
--accent-secondary: #a1a1aa;
|
||||
--accent-glow: 228, 228, 231;
|
||||
--accent-glow-secondary: 161, 161, 170;
|
||||
--accent-glow-tertiary: 113, 113, 122;
|
||||
--headline-color: #f0f0f0;
|
||||
--headline-glow: 240, 240, 240;
|
||||
--muted: #8a8a90;
|
||||
}
|
||||
|
||||
/* Theme: Red */
|
||||
[data-theme="red"] {
|
||||
--foreground: #e8c8c8;
|
||||
--accent: #f87171;
|
||||
--accent-secondary: #ef4444;
|
||||
--accent-glow: 248, 113, 113;
|
||||
--accent-glow-secondary: 239, 68, 68;
|
||||
--accent-glow-tertiary: 220, 38, 38;
|
||||
--headline-color: #ff3333;
|
||||
--headline-glow: 255, 51, 51;
|
||||
--muted: #ad7a7a;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-secondary: var(--accent-secondary);
|
||||
--color-muted: var(--muted);
|
||||
--color-border: var(--border);
|
||||
--color-card: var(--card);
|
||||
--color-card-hover: var(--card-hover);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: "SN Pro", system-ui, sans-serif;
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
/* Smooth scroll */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
code {
|
||||
font-family: var(--font-geist-mono), ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* Cloud animation keyframes — slow, massive wandering paths */
|
||||
@keyframes cloud-drift {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
10% {
|
||||
transform: translate(900px, -500px) scale(1.15);
|
||||
opacity: 0.6;
|
||||
}
|
||||
22% {
|
||||
transform: translate(-600px, 800px) scale(0.85);
|
||||
opacity: 0.4;
|
||||
}
|
||||
38% {
|
||||
transform: translate(1100px, 300px) scale(1.1);
|
||||
opacity: 0.55;
|
||||
}
|
||||
52% {
|
||||
transform: translate(-900px, -700px) scale(0.9);
|
||||
opacity: 0.45;
|
||||
}
|
||||
68% {
|
||||
transform: translate(500px, -1000px) scale(1.08);
|
||||
opacity: 0.5;
|
||||
}
|
||||
82% {
|
||||
transform: translate(-400px, 600px) scale(0.92);
|
||||
opacity: 0.48;
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloud-drift-reverse {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
12% {
|
||||
transform: translate(-1000px, 600px) scale(1.12);
|
||||
opacity: 0.55;
|
||||
}
|
||||
25% {
|
||||
transform: translate(700px, -900px) scale(0.88);
|
||||
opacity: 0.35;
|
||||
}
|
||||
40% {
|
||||
transform: translate(-500px, -800px) scale(1.08);
|
||||
opacity: 0.5;
|
||||
}
|
||||
55% {
|
||||
transform: translate(1100px, 400px) scale(0.92);
|
||||
opacity: 0.42;
|
||||
}
|
||||
70% {
|
||||
transform: translate(-800px, 700px) scale(1.06);
|
||||
opacity: 0.48;
|
||||
}
|
||||
85% {
|
||||
transform: translate(400px, -500px) scale(0.95);
|
||||
opacity: 0.44;
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloud-drift-slow {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.35;
|
||||
}
|
||||
14% {
|
||||
transform: translate(800px, 900px) scale(1.1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
28% {
|
||||
transform: translate(-1000px, -400px) scale(0.88);
|
||||
opacity: 0.3;
|
||||
}
|
||||
42% {
|
||||
transform: translate(600px, -1100px) scale(1.06);
|
||||
opacity: 0.45;
|
||||
}
|
||||
57% {
|
||||
transform: translate(-700px, 500px) scale(0.92);
|
||||
opacity: 0.38;
|
||||
}
|
||||
72% {
|
||||
transform: translate(1000px, -300px) scale(1.04);
|
||||
opacity: 0.42;
|
||||
}
|
||||
86% {
|
||||
transform: translate(-500px, 800px) scale(0.96);
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist_Mono } from "next/font/google";
|
||||
import { ThemeProvider } from "./components/ThemeProvider";
|
||||
import "./globals.css";
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "TUIkit — Terminal UI Framework for Swift",
|
||||
description:
|
||||
"A declarative, SwiftUI-like framework for building Terminal User Interfaces in Swift. No ncurses, no C dependencies — pure Swift.",
|
||||
openGraph: {
|
||||
title: "TUIkit — Terminal UI Framework for Swift",
|
||||
description:
|
||||
"Build terminal apps with SwiftUI-like syntax. Pure Swift, no ncurses.",
|
||||
url: "https://tuikit.layered.work",
|
||||
siteName: "TUIkit",
|
||||
type: "website",
|
||||
},
|
||||
icons: {
|
||||
icon: "/tuikit-logo.png",
|
||||
apple: "/tuikit-logo.png",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import Image from "next/image";
|
||||
import CloudBackground from "./components/CloudBackground";
|
||||
import CodePreview from "./components/CodePreview";
|
||||
import FeatureCard from "./components/FeatureCard";
|
||||
import Icon from "./components/Icon";
|
||||
import PackageBadge from "./components/PackageBadge";
|
||||
import ThemeSwitcher from "./components/ThemeSwitcher";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="relative min-h-screen">
|
||||
<CloudBackground />
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="fixed top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-xl">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src="/tuikit-logo.png"
|
||||
alt="TUIkit Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<span className="text-2xl font-semibold text-foreground">
|
||||
TUIkit
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<a
|
||||
href="/documentation/tuikit"
|
||||
className="text-lg text-muted transition-colors hover:text-foreground"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phranck/TUIkit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-lg text-muted transition-colors hover:text-foreground"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<div className="ml-2 border-l border-border pl-4">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative mx-auto flex max-w-6xl flex-col items-center px-6 pt-40 pb-24 text-center">
|
||||
<Image
|
||||
src="/tuikit-logo.png"
|
||||
alt="TUIkit Logo"
|
||||
width={200}
|
||||
height={200}
|
||||
className="mb-10 rounded-3xl"
|
||||
priority
|
||||
/>
|
||||
|
||||
<h1
|
||||
className="mb-6 max-w-4xl text-5xl leading-tight tracking-wide transition-all duration-500 md:text-7xl"
|
||||
style={{
|
||||
fontFamily: "WarText, monospace",
|
||||
color: "var(--headline-color)",
|
||||
textShadow:
|
||||
"0 0 7px rgba(var(--headline-glow),0.6), 0 0 20px rgba(var(--headline-glow),0.35), 0 0 42px rgba(var(--headline-glow),0.15)",
|
||||
}}
|
||||
>
|
||||
> Terminal UIs, the Swift way_
|
||||
</h1>
|
||||
|
||||
<p className="mb-10 max-w-2xl text-2xl leading-relaxed text-muted">
|
||||
A declarative, SwiftUI-like framework for building Terminal User
|
||||
Interfaces. No ncurses, no C dependencies — pure Swift on macOS and
|
||||
Linux.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<a
|
||||
href="/documentation/tuikit"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-accent px-7 py-2.5 text-xl font-semibold text-background transition-all hover:bg-accent-secondary hover:shadow-lg hover:shadow-accent/20"
|
||||
>
|
||||
<Icon name="document" size={22} />
|
||||
Read the Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phranck/TUIkit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full border border-border px-7 py-2.5 text-xl font-semibold text-foreground transition-all hover:border-accent/40 hover:bg-white/5"
|
||||
>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Swift Package badge */}
|
||||
<p className="mt-20 mb-4 max-w-2xl text-xl leading-relaxed text-muted">
|
||||
Getting started is simple. Add TUIkit as a dependency to your
|
||||
Swift package — no extra configuration, no system libraries to install:
|
||||
</p>
|
||||
<PackageBadge />
|
||||
</section>
|
||||
|
||||
{/* Code Preview Section */}
|
||||
<section className="mx-auto max-w-3xl px-6 pb-28">
|
||||
<CodePreview />
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
<section className="mx-auto max-w-6xl px-6 pb-28">
|
||||
<div className="mb-12 text-center">
|
||||
<h2 className="mb-4 text-4xl font-bold text-foreground">
|
||||
Everything you need
|
||||
</h2>
|
||||
<p className="mx-auto max-w-2xl text-2xl text-muted">
|
||||
Built from the ground up for the terminal, with APIs you already
|
||||
know from SwiftUI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={<Icon name="terminal" size={28} />}
|
||||
title="Declarative Syntax"
|
||||
description="Build UIs with VStack, HStack, Text, Button, and more — the same patterns you know from SwiftUI."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<Icon name="paintbrush" size={28} />}
|
||||
title="Theming System"
|
||||
description="Multiple built-in phosphor themes with full RGB color support. Cycle at runtime or create custom palettes."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<Icon name="keyboard" size={28} />}
|
||||
title="Keyboard-Driven"
|
||||
description="Focus management, key event handlers, customizable status bar with shortcut display."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<Icon name="stack" size={28} />}
|
||||
title="Rich Components"
|
||||
description="Panel, Card, Dialog, Alert, Menu, Button, ForEach — container and interactive views out of the box."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<Icon name="bolt" size={28} />}
|
||||
title="Zero Dependencies"
|
||||
description="Pure Swift. No ncurses, no C libraries. Just add the Swift package and go."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={<Icon name="arrows" size={28} />}
|
||||
title="Cross-Platform"
|
||||
description="Runs on macOS and Linux. Same code, same API, same results."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Architecture highlights */}
|
||||
<section className="mx-auto max-w-6xl px-6 pb-28">
|
||||
<div className="rounded-2xl border border-border bg-card/50 p-8 backdrop-blur-sm md:p-12">
|
||||
<h2 className="mb-8 text-center text-4xl font-bold text-foreground">
|
||||
Built right
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-accent">
|
||||
<Icon name="swift" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 text-xl font-semibold text-foreground">
|
||||
Swift 6.0 + Strict Concurrency
|
||||
</h3>
|
||||
<p className="text-xl leading-relaxed text-muted">
|
||||
Full Sendable compliance. No data races, no unsafe globals.
|
||||
Modern Swift from top to bottom.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-accent">
|
||||
<Icon name="eye" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 text-xl font-semibold text-foreground">
|
||||
5 Border Appearances
|
||||
</h3>
|
||||
<p className="text-xl leading-relaxed text-muted">
|
||||
Line, rounded, double-line, heavy, and block style. Cycle with
|
||||
a single keystroke.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-accent">
|
||||
<Icon name="checkmark" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 text-xl font-semibold text-foreground">
|
||||
498 Tests
|
||||
</h3>
|
||||
<p className="text-xl leading-relaxed text-muted">
|
||||
Comprehensive test suite covering views, modifiers, rendering,
|
||||
state management, and more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-accent">
|
||||
<Icon name="document" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 text-xl font-semibold text-foreground">
|
||||
13 DocC Articles
|
||||
</h3>
|
||||
<p className="text-xl leading-relaxed text-muted">
|
||||
Architecture guides, API references, theming, focus system,
|
||||
keyboard shortcuts, and palette documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="mx-auto max-w-6xl px-6 pb-20">
|
||||
<div className="text-center">
|
||||
<h2 className="mb-4 text-4xl font-bold text-foreground">
|
||||
Ready to build?
|
||||
</h2>
|
||||
<p className="mb-8 text-2xl text-muted">
|
||||
Get started with TUIkit in minutes. Add the package, write your
|
||||
first view, run it.
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<a
|
||||
href="/documentation/tuikit/gettingstarted"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-accent px-7 py-2.5 text-xl font-semibold text-background transition-all hover:bg-accent-secondary hover:shadow-lg hover:shadow-accent/20"
|
||||
>
|
||||
Getting Started Guide
|
||||
</a>
|
||||
<a
|
||||
href="/documentation/tuikit"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full border border-border px-7 py-2.5 text-xl font-semibold text-foreground transition-all hover:border-accent/40 hover:bg-white/5"
|
||||
>
|
||||
Browse Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-border bg-card/30 backdrop-blur-sm">
|
||||
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-4 px-6 py-8 sm:flex-row">
|
||||
<span className="text-base text-muted">
|
||||
Made with ❤️ in Bregenz at Lake Constance
|
||||
</span>
|
||||
<div className="flex items-center gap-6 text-base text-muted">
|
||||
<a
|
||||
href="/documentation/tuikit"
|
||||
className="transition-colors hover:text-foreground"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phranck/TUIkit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-colors hover:text-foreground"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://creativecommons.org/licenses/by-nc-sa/4.0/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-colors hover:text-foreground"
|
||||
>
|
||||
CC BY-NC-SA 4.0
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
env: {
|
||||
TUIKIT_VERSION: process.env.TUIKIT_VERSION ?? "0.1.0",
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+6563
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"sf-symbols-lib": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 908 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user