feat: Initial commit
This commit is contained in:
28 files changed
+11034
No files matched your search
@@ -0,0 +1,91 @@
|
||||
name: origin/deploy-production
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
BUILD_PATH: "."
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ ! github.event.release.prerelease }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
||||
build:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Stage
|
||||
run: pnpm build
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy stage
|
||||
steps:
|
||||
- name: Deploy in production
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
name: origin/release-dispatch
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["origin/required"]
|
||||
branches: [main, next]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Stage
|
||||
run: pnpm build
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm exec semantic-release
|
||||
@@ -0,0 +1,97 @@
|
||||
name: origin/required
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, next ]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run ESLint (CI mode)
|
||||
run: pnpm run lint --max-warnings=0
|
||||
|
||||
type-check:
|
||||
name: Typecheck
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run TypeScript compiler
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
build:
|
||||
name: Build stage
|
||||
needs: type-check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Astro build
|
||||
run: pnpm exec astro build
|
||||
@@ -0,0 +1,100 @@
|
||||
name: pr/required
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, next ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run ESLint (CI mode)
|
||||
run: pnpm run lint --max-warnings=0
|
||||
|
||||
type-check:
|
||||
name: Typecheck
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run TypeScript compiler
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
build:
|
||||
name: Build stage
|
||||
needs: type-check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Astro build
|
||||
run: pnpm exec astro build
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false,
|
||||
"plugins": ["prettier-plugin-astro", "prettier-plugin-react", "prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# Astro Starter Kit: Blog
|
||||
|
||||
```sh
|
||||
pnpm create astro@latest -- --template blog
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/blog)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/blog)
|
||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/blog/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||

|
||||
|
||||
Features:
|
||||
|
||||
- ✅ Minimal styling (make it your own!)
|
||||
- ✅ 100/100 Lighthouse performance
|
||||
- ✅ SEO-friendly with canonical URLs and OpenGraph data
|
||||
- ✅ Sitemap support
|
||||
- ✅ RSS Feed support
|
||||
- ✅ Markdown & MDX support
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── content/
|
||||
│ ├── layouts/
|
||||
│ └── pages/
|
||||
├── astro.config.mjs
|
||||
├── README.md
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
The `src/content/` directory contains "collections" of related Markdown and MDX documents. Use `getCollection()` to retrieve posts from `src/content/blog/`, and type-check your frontmatter using an optional schema. See [Astro's Content Collections docs](https://docs.astro.build/en/guides/content-collections/) to learn more.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :--------------------- | :----------------------------------------------- |
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
|
||||
## Credit
|
||||
|
||||
This theme is based off of the lovely [Bear Blog](https://github.com/HermanMartinus/bearblog/).
|
||||
@@ -0,0 +1,20 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
import react from '@astrojs/react';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://www.ermnvldmr.com',
|
||||
output: 'static',
|
||||
srcDir: './src/www',
|
||||
integrations: [mdx(), sitemap(), react()],
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@shadcn/components",
|
||||
"utils": "@shadcn/lib/utils",
|
||||
"ui": "@shadcn/components/ui",
|
||||
"lib": "@shadcn/lib",
|
||||
"hooks": "@shadcn/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginAstro from 'eslint-plugin-astro';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import prettier from 'eslint-config-prettier/flat';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['**/dist', '**/node_modules', '**/.astro', '**/.github', '**/.changeset'],
|
||||
},
|
||||
|
||||
// JavaScript
|
||||
eslint.configs.recommended,
|
||||
|
||||
// TypeScript
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
// Allow triple-slash references in `*.d.ts` files.
|
||||
{
|
||||
files: ['**/*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/triple-slash-reference': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Astro
|
||||
...eslintPluginAstro.configs.recommended,
|
||||
|
||||
// Set globals for Node scripts.
|
||||
{
|
||||
files: ['scripts/**'],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
|
||||
// React
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
...react.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
...react.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
plugins: { 'react-hooks': reactHooks },
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier
|
||||
prettier
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "www.ermnvldmr.com",
|
||||
"version": "0.0.1",
|
||||
"description": "Thoughts and creations of mine!",
|
||||
"author": "Vladimir Eremin <personal@ermnvldmr.com> (https://ermnvldmr.com)",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deytenit/www.ermnvldmr.com.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/deytenit/www.ermnvldmr.com/issues"
|
||||
},
|
||||
"homepage": "https://github.com/deytenit/www.ermnvldmr.com#readme",
|
||||
"files": [
|
||||
"src/",
|
||||
"public/"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"lint": "eslint --fix .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "npm run lint && npm run typecheck",
|
||||
"clean": "rm -rf dist .turbo node_modules",
|
||||
"commit": "git-cz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.2.5",
|
||||
"@astrojs/react": "^4.2.5",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.3.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.10",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"astro": "^5.7.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"git-cz": "^4.9.0",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-react": "0.0.1-alpha.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"semantic-release": "^24.2.3",
|
||||
"tw-animate-css": "^1.2.8",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.1"
|
||||
}
|
||||
}
|
||||
Generated
+9267
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
branches: ["main", { name: "next", prerelease: true }],
|
||||
plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/github']
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { memo } from 'react';
|
||||
import NavigationMenu from './components/NavigationMenu/NavigationMenu';
|
||||
import Breadcrumbs from './components/Breadcrumbs/Breadcrumbs';
|
||||
|
||||
import type { NavItem, Breadcrumb } from './types';
|
||||
|
||||
export type PageHeadProps = {
|
||||
/** The main title displayed in the header */
|
||||
title: string;
|
||||
/** Array of breadcrumbs, displayed above the title */
|
||||
breadcrumbs?: Breadcrumb[];
|
||||
/** Navigation items displayed on the right */
|
||||
navItems?: NavItem[];
|
||||
};
|
||||
|
||||
export const PageHead = memo(function PageHead({ title, breadcrumbs, navItems }: PageHeadProps) {
|
||||
return (
|
||||
<div className="frosted-glass flex items-center justify-between space-x-4 p-4">
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
{breadcrumbs && breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} />}
|
||||
<h1 className="text-foreground font-serif text-2xl font-bold">{title}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{navItems && navItems.length > 0 && <NavigationMenu items={navItems} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default PageHead;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { memo, Fragment } from 'react';
|
||||
|
||||
import {
|
||||
Breadcrumb as BreadcrumbBase,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
} from '@shadcn/components/ui/breadcrumb';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '@shadcn/components/ui/dropdown-menu';
|
||||
|
||||
import type { Breadcrumb } from '../../types';
|
||||
|
||||
const MAX_VISIBLE_BREADCRUMBS = 4;
|
||||
|
||||
export type BreadcrumbsProps = {
|
||||
items: Breadcrumb[];
|
||||
};
|
||||
|
||||
const Breadcrumbs = memo(function Breadcrumbs({ items }: BreadcrumbsProps) {
|
||||
const totalItems = items.length;
|
||||
const hasOverflownBreadcrumbs = totalItems > MAX_VISIBLE_BREADCRUMBS;
|
||||
|
||||
return (
|
||||
<BreadcrumbBase className="text-sm">
|
||||
<BreadcrumbList>
|
||||
{hasOverflownBreadcrumbs ? (
|
||||
<>
|
||||
{/* First crumb */}
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={items[0].href}>{items[0].label}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
|
||||
{/* Ellipsis dropdown for middle crumbs */}
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1">
|
||||
<BreadcrumbEllipsis className="h-4 w-4" />
|
||||
<span className="sr-only">More...</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{items.slice(1, totalItems - 2).map((item, i) => (
|
||||
<DropdownMenuItem key={i}>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
|
||||
{/* Penultimate crumb */}
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={items[totalItems - 2].href}>
|
||||
{items[totalItems - 2].label}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
|
||||
{/* Final “page” crumb */}
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{items[totalItems - 1].label}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
) : (
|
||||
// No overflow: just render them all
|
||||
items.map((item, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<BreadcrumbItem>
|
||||
{idx === totalItems - 1 ? (
|
||||
<BreadcrumbPage>{item.label}</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
{idx < totalItems - 1 && <BreadcrumbSeparator />}
|
||||
</Fragment>
|
||||
))
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</BreadcrumbBase>
|
||||
);
|
||||
});
|
||||
|
||||
export default Breadcrumbs;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import {
|
||||
NavigationMenu as NavigationMenuBase,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
navigationMenuTriggerStyle,
|
||||
} from '@shadcn/components/ui/navigation-menu';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '@shadcn/components/ui/dropdown-menu';
|
||||
import { HiOutlineMenu } from 'react-icons/hi';
|
||||
|
||||
import type { NavItem } from '../../types';
|
||||
|
||||
export type NavigationMenuProps = {
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
const NavigationMenu = memo(function NavigationMenu({ items }: NavigationMenuProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="hidden items-center md:block">
|
||||
<NavigationMenuBase>
|
||||
<NavigationMenuList>
|
||||
{items.map((item, idx) => (
|
||||
<NavigationMenuItem key={idx}>
|
||||
<a href={item.href}>
|
||||
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||
{item.label}
|
||||
</NavigationMenuLink>
|
||||
</a>
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenuBase>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center md:hidden">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button aria-label="Open menu">
|
||||
<HiOutlineMenu className="h-6 w-6" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{items.map((item, i) => (
|
||||
<DropdownMenuItem key={i}>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default NavigationMenu;
|
||||
@@ -0,0 +1,9 @@
|
||||
export type NavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type Breadcrumb = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/*
|
||||
@layer base defaults inspired by shadcn UI Typography
|
||||
Provides opinionated, utility-driven defaults for HTML elements.
|
||||
*/
|
||||
@layer base {
|
||||
/* Headings */
|
||||
h1 {
|
||||
@apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl;
|
||||
}
|
||||
h2 {
|
||||
@apply mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0;
|
||||
}
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply mt-8 scroll-m-20 text-2xl font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
p {
|
||||
@apply leading-7 [&:not(:first-child)]:mt-6;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
@apply text-ring font-medium underline underline-offset-2;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
blockquote {
|
||||
@apply mt-6 border-l-2 pl-6 italic;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
code {
|
||||
@apply bg-muted rounded px-1 font-mono text-sm;
|
||||
}
|
||||
pre {
|
||||
@apply bg-muted overflow-auto rounded-lg p-4 font-mono text-sm;
|
||||
}
|
||||
|
||||
/* Images and Media */
|
||||
img,
|
||||
video {
|
||||
@apply max-w-full rounded-lg;
|
||||
}
|
||||
|
||||
/* Horizontal rules */
|
||||
hr {
|
||||
@apply border-border my-6 border-t;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Lato', sans-serif;
|
||||
--font-serif: 'EB Garamond', serif;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-rainby-red-50: #fef4f2;
|
||||
--color-rainby-red-100: #ffe7e1;
|
||||
--color-rainby-red-200: #ffd2c8;
|
||||
--color-rainby-red-300: #ffb4a3;
|
||||
--color-rainby-red-400: #fd876c;
|
||||
--color-rainby-red-500: #f5603e;
|
||||
--color-rainby-red-600: #e24420;
|
||||
--color-rainby-red-700: #be3617;
|
||||
--color-rainby-red-800: #9d3017;
|
||||
--color-rainby-red-900: #822d1a;
|
||||
--color-rainby-red-950: #471408;
|
||||
|
||||
--color-rainby-orange-50: #fff8ed;
|
||||
--color-rainby-orange-100: #ffefd5;
|
||||
--color-rainby-orange-200: #fedbaa;
|
||||
--color-rainby-orange-300: #fdc175;
|
||||
--color-rainby-orange-400: #fb9b3c;
|
||||
--color-rainby-orange-500: #f97e16;
|
||||
--color-rainby-orange-600: #ea630c;
|
||||
--color-rainby-orange-700: #c24a0c;
|
||||
--color-rainby-orange-800: #9a3b12;
|
||||
--color-rainby-orange-900: #7c3212;
|
||||
--color-rainby-orange-950: #431707;
|
||||
|
||||
--color-rainby-yellow-50: #fefde8;
|
||||
--color-rainby-yellow-100: #fefdc3;
|
||||
--color-rainby-yellow-200: #fef9a3;
|
||||
--color-rainby-yellow-300: #fdec47;
|
||||
--color-rainby-yellow-400: #f9db16;
|
||||
--color-rainby-yellow-500: #e9c209;
|
||||
--color-rainby-yellow-600: #ca9704;
|
||||
--color-rainby-yellow-700: #a16c07;
|
||||
--color-rainby-yellow-800: #85550e;
|
||||
--color-rainby-yellow-900: #714512;
|
||||
--color-rainby-yellow-950: #422406;
|
||||
|
||||
--color-rainby-green-50: #f8faeb;
|
||||
--color-rainby-green-100: #edf4d3;
|
||||
--color-rainby-green-200: #ddebab;
|
||||
--color-rainby-green-300: #c4dc7a;
|
||||
--color-rainby-green-400: #b6d167;
|
||||
--color-rainby-green-500: #8daf33;
|
||||
--color-rainby-green-600: #6e8b25;
|
||||
--color-rainby-green-700: #546b20;
|
||||
--color-rainby-green-800: #44551f;
|
||||
--color-rainby-green-900: #3a491e;
|
||||
--color-rainby-green-950: #1e280b;
|
||||
|
||||
--color-rainby-cyan-50: #effbfc;
|
||||
--color-rainby-cyan-100: #d7f2f6;
|
||||
--color-rainby-cyan-200: #b4e6ed;
|
||||
--color-rainby-cyan-300: #81d3e0;
|
||||
--color-rainby-cyan-400: #45b6cb;
|
||||
--color-rainby-cyan-500: #2a9ab0;
|
||||
--color-rainby-cyan-600: #257d95;
|
||||
--color-rainby-cyan-700: #24657a;
|
||||
--color-rainby-cyan-800: #265464;
|
||||
--color-rainby-cyan-900: #234756;
|
||||
--color-rainby-cyan-950: #122e3a;
|
||||
|
||||
--color-rainby-blue-50: #eff5ff;
|
||||
--color-rainby-blue-100: #dbe8fe;
|
||||
--color-rainby-blue-200: #bfd8fe;
|
||||
--color-rainby-blue-300: #a2c9fe;
|
||||
--color-rainby-blue-400: #5f9efb;
|
||||
--color-rainby-blue-500: #3a79f7;
|
||||
--color-rainby-blue-600: #2358ed;
|
||||
--color-rainby-blue-700: #1c44d9;
|
||||
--color-rainby-blue-800: #1d38b0;
|
||||
--color-rainby-blue-900: #1d358b;
|
||||
--color-rainby-blue-950: #172254;
|
||||
|
||||
--color-rainby-violet-50: #eef0ff;
|
||||
--color-rainby-violet-100: #dfe3ff;
|
||||
--color-rainby-violet-200: #bdc2ff;
|
||||
--color-rainby-violet-300: #a3a8fe;
|
||||
--color-rainby-violet-400: #837ffa;
|
||||
--color-rainby-violet-500: #6e60f4;
|
||||
--color-rainby-violet-600: #6043e8;
|
||||
--color-rainby-violet-700: #5435cd;
|
||||
--color-rainby-violet-800: #442ea5;
|
||||
--color-rainby-violet-900: #3b2c83;
|
||||
--color-rainby-violet-950: #231a4c;
|
||||
|
||||
--color-rainby-neutral-50: #f6f6f6;
|
||||
--color-rainby-neutral-100: #e7e7e7;
|
||||
--color-rainby-neutral-200: #d1d1d1;
|
||||
--color-rainby-neutral-300: #b0b0b0;
|
||||
--color-rainby-neutral-400: #888888;
|
||||
--color-rainby-neutral-500: #737373;
|
||||
--color-rainby-neutral-600: #5d5d5d;
|
||||
--color-rainby-neutral-700: #4f4f4f;
|
||||
--color-rainby-neutral-800: #454545;
|
||||
--color-rainby-neutral-900: #3d3d3d;
|
||||
--color-rainby-neutral-950: #262626;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Core palette */
|
||||
--background: var(--color-rainby-neutral-50);
|
||||
--border: var(--color-rainby-neutral-500);
|
||||
--input: var(--color-rainby-neutral-800);
|
||||
--foreground: var(--color-rainby-neutral-950);
|
||||
--ring: var(--color-rainby-cyan-700);
|
||||
|
||||
/* Surfaces */
|
||||
--card: var(--color-rainby-neutral-50);
|
||||
--card-foreground: var(--color-rainby-neutral-950);
|
||||
--popover: var(--color-rainby-neutral-50);
|
||||
--popover-foreground: var(--color-rainby-neutral-950);
|
||||
|
||||
/* Semantic */
|
||||
--primary: var(--color-rainby-cyan-200);
|
||||
--primary-foreground: var(--color-rainby-cyan-950);
|
||||
--secondary: var(--color-rainby-green-200);
|
||||
--secondary-foreground: var(--color-rainby-green-950);
|
||||
--muted: var(--color-rainby-neutral-300);
|
||||
--muted-foreground: var(--color-rainby-neutral-600);
|
||||
--accent: var(--color-rainby-violet-200);
|
||||
--accent-foreground: var(--color-rainby-violet-950);
|
||||
--destructive: var(--color-rainby-red-200);
|
||||
--destructive-foreground: var(--color-rainby-red-950);
|
||||
|
||||
/* Charts (e.g. Recharts in ShadCN examples) */
|
||||
--chart-1: var(--color-rainby-blue-500);
|
||||
--chart-2: var(--color-rainby-green-500);
|
||||
--chart-3: var(--color-rainby-yellow-500);
|
||||
--chart-4: var(--color-rainby-orange-500);
|
||||
--chart-5: var(--color-rainby-red-500);
|
||||
|
||||
/* Sidebar (if you use ShadCN’s sidebar components) */
|
||||
--sidebar: var(--color-rainby-neutral-50);
|
||||
--sidebar-foreground: var(--color-rainby-neutral-950);
|
||||
--sidebar-primary: var(--color-rainby-cyan-200);
|
||||
--sidebar-primary-foreground: var(--color-rainby-cyan-950);
|
||||
--sidebar-accent: var(--color-rainby-violet-200);
|
||||
--sidebar-accent-foreground: var(--color-rainby-violet-950);
|
||||
--sidebar-border: var(--color-rainby-neutral-500);
|
||||
--sidebar-ring: var(--color-rainby-cyan-700);
|
||||
|
||||
/* Radii */
|
||||
--radius: 0.625rem;
|
||||
}
|
||||
|
||||
/* ──────────────── Dark Mode Override ──────────────── */
|
||||
.dark {
|
||||
/* Core palette flips */
|
||||
--background: var(--color-rainby-neutral-950);
|
||||
--ring: var(--color-rainby-cyan-300);
|
||||
--border: var(--color-rainby-neutral-500);
|
||||
--input: var(--color-rainby-neutral-300);
|
||||
--foreground: var(--color-rainby-neutral-50);
|
||||
|
||||
/* Surfaces */
|
||||
--card: var(--color-rainby-neutral-950);
|
||||
--card-foreground: var(--color-rainby-neutral-50);
|
||||
--popover: var(--color-rainby-neutral-950);
|
||||
--popover-foreground: var(--color-rainby-neutral-50);
|
||||
|
||||
/* Semantic */
|
||||
--primary: var(--color-rainby-cyan-800);
|
||||
--primary-foreground: var(--color-rainby-cyan-50);
|
||||
--secondary: var(--color-rainby-green-800);
|
||||
--secondary-foreground: var(--color-rainby-green-50);
|
||||
--muted: var(--color-rainby-neutral-600);
|
||||
--muted-foreground: var(--color-rainby-neutral-100);
|
||||
--accent: var(--color-rainby-violet-800);
|
||||
--accent-foreground: var(--color-rainby-violet-50);
|
||||
--destructive: var(--color-rainby-red-800);
|
||||
--destructive-foreground: var(--color-rainby-red-50);
|
||||
|
||||
/* Charts */
|
||||
--chart-1: var(--color-rainby-blue-400);
|
||||
--chart-2: var(--color-rainby-green-400);
|
||||
--chart-3: var(--color-rainby-yellow-400);
|
||||
--chart-4: var(--color-rainby-orange-400);
|
||||
--chart-5: var(--color-rainby-red-400);
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: var(--color-rainby-neutral-950);
|
||||
--sidebar-foreground: var(--color-rainby-neutral-50);
|
||||
--sidebar-primary: var(--color-rainby-cyan-800);
|
||||
--sidebar-primary-foreground: var(--color-rainby-cyan-50);
|
||||
--sidebar-accent: var(--color-rainby-violet-800);
|
||||
--sidebar-accent-foreground: var(--color-rainby-violet-50);
|
||||
--sidebar-border: var(--color-rainby-neutral-500);
|
||||
--sidebar-ring: var(--color-rainby-cyan-700);
|
||||
|
||||
/* Radii stays same */
|
||||
--radius: 0.625rem;
|
||||
}
|
||||
|
||||
@utility frosted-glass {
|
||||
@apply bg-background/30 rounded-lg shadow-lg backdrop-blur-md backdrop-opacity-75 backdrop-filter;
|
||||
}
|
||||
|
||||
@utility vstack {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
@utility hstack {
|
||||
@apply flex flex-row;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
|
||||
import { cn } from '@shadcn/lib/utils';
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn('inline-flex items-center gap-1.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'a'> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn('hover:text-foreground transition-colors', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn('text-foreground font-normal', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('[&>svg]:size-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
import * as React from 'react';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@shadcn/lib/utils';
|
||||
|
||||
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: 'default' | 'destructive';
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import * as React from 'react';
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@shadcn/lib/utils';
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn('group flex flex-1 list-none items-center justify-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn('relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1'
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{' '}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
|
||||
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div className={cn('absolute top-full left-0 isolate z-50 flex justify-center')}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatDate(input: string | number): string {
|
||||
const date = new Date(input);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export function absoluteUrl(path: string) {
|
||||
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
import '@global.css';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script is:inline>
|
||||
const getThemePreference = () => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
return localStorage.getItem('theme');
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
};
|
||||
const isDark = getThemePreference() === 'dark';
|
||||
document.documentElement.classList[isDark ? 'add' : 'remove']('dark');
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const observer = new MutationObserver(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="robots" content="index,follow" />
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href="https://www.ermnvldmr.com/" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import IndexLayout from '@www/layouts/IndexLayout/IndexLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '@www/shared/constants';
|
||||
---
|
||||
|
||||
<IndexLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<main class="flex h-full w-full justify-center p-4">
|
||||
<div>
|
||||
<h2>Welcome to my page!</h2>
|
||||
<p>
|
||||
If you see this page, my website is successfuly installed and working.
|
||||
<br />
|
||||
It will be further configured.
|
||||
</p>
|
||||
<p>
|
||||
For the source code and support please refer to <a href="https://github.com/deytenit"
|
||||
>github.com</a
|
||||
>.
|
||||
<br />
|
||||
Pictures of mine is available at <a href="https://instagram.com/deytenit">instagram.com</a>.
|
||||
</p>
|
||||
<p><em>Thank you for visiting!</em></p>
|
||||
</div>
|
||||
</main>
|
||||
</IndexLayout>
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SITE_TITLE = 'Vladimir Eremin';
|
||||
export const SITE_DESCRIPTION = 'Thoughts and creations of mine';
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"],
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@*": ["./src/*"]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user