refactor: migrate to M3 SCSS and remove Radix/Tailwind dependencies

Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4.5) <aider@aider.chat>
This commit is contained in:
2026-01-20 16:21:03 +00:00
parent 3962d16017
commit 52fb82a706
7 changed files with 489 additions and 169 deletions

View File

@@ -1,62 +1,12 @@
/* eslint-env node */
/** @type {import('next').NextConfig} */
const isGithubPages = process.env.GITHUB_PAGES === 'true' || process.env.GITHUB_ACTIONS === 'true'
const repoBasePath = process.env.NEXT_PUBLIC_BASE_PATH
|| (isGithubPages && process.env.GITHUB_REPOSITORY
? `/${process.env.GITHUB_REPOSITORY.split('/')[1]}`
: '')
const nextConfig = {
output: process.env.BUILD_STATIC || isGithubPages ? 'export' : 'standalone',
basePath: repoBasePath,
assetPrefix: repoBasePath || undefined,
images: {
unoptimized: true,
sassOptions: {
includePaths: ['./src/styles'],
additionalData: `@use "m3-scss/material" as mat;`,
},
experimental: {
optimizePackageImports: ['@radix-ui/react-icons', '@phosphor-icons/react'],
turbopackScopeHoisting: false,
optimizePackageImports: ['@phosphor-icons/react'],
},
serverExternalPackages: ['pyodide'],
webpack: (config, { isServer, webpack }) => {
// Pyodide contains references to Node.js built-in modules that should be ignored in browser bundles
if (!isServer) {
// Replace node: protocol imports with empty modules
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/^node:/,
(resource) => {
resource.request = resource.request.replace(/^node:/, '');
}
)
);
}
// Set fallbacks for node modules to false (don't polyfill)
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
child_process: false,
crypto: false,
path: false,
url: false,
vm: false,
};
// Exclude pyodide from server-side rendering completely
config.resolve.alias = {
...config.resolve.alias,
pyodide: false,
};
}
// On server, also exclude pyodide
if (isServer) {
config.externals = config.externals || [];
config.externals.push('pyodide');
}
return config;
},
};
export default nextConfig;
module.exports = nextConfig

View File

@@ -1,92 +1,33 @@
{
"name": "spark-template",
"type": "module",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"stylelint": "stylelint \"src/**/*.{scss,css}\"",
"kill": "fuser -k 5000/tcp",
"test:e2e": "playwright test"
},
"dependencies": {
"@babel/standalone": "^7.28.6",
"@hookform/resolvers": "^4.1.3",
"@monaco-editor/react": "^4.7.0",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@reduxjs/toolkit": "^2.11.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.2",
"framer-motion": "^12.6.2",
"lucide-react": "^0.562.0",
"marked": "^15.0.7",
"next": "16.1.3",
"next-themes": "^0.4.6",
"pyodide": "^0.27.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.54.2",
"react-redux": "^9.2.0",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.1",
"sass": "^1.97.2",
"sonner": "^2.0.1",
"uuid": "^11.1.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@playwright/test": "^1.57.0",
"@types/node": "^25.0.9",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.0.4",
"eslint": "^9.28.0",
"eslint-config-next": "^16.1.3",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.0.0",
"stylelint": "^17.0.0",
"stylelint-config-standard-scss": "^17.0.0",
"stylelint-order": "^7.0.1",
"stylelint-scss": "^7.0.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.53.1"
},
"workspaces": {
"packages": [
"packages/*"
]
}
"name": "codesnippet",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@phosphor-icons/react": "^2.1.7",
"@reduxjs/toolkit": "^2.5.0",
"dexie": "^4.0.10",
"framer-motion": "^11.15.0",
"monaco-editor": "^0.52.2",
"next": "15.1.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"sonner": "^1.7.3"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^8",
"eslint-config-next": "15.1.3",
"sass": "^1.70.0",
"typescript": "^5"
}
}

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
autoprefixer: {},
},
}

62
src/app/globals.css Normal file
View File

@@ -0,0 +1,62 @@
@import '../styles/theme.scss';
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
font-family: var(--mat-sys-body-large-font);
background-color: var(--mat-sys-background);
color: var(--mat-sys-on-background);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
color: var(--mat-sys-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
code {
font-family: 'JetBrains Mono', monospace;
background-color: var(--mat-sys-surface-variant);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
pre {
font-family: 'JetBrains Mono', monospace;
background-color: var(--mat-sys-surface-variant);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
}
pre code {
background-color: transparent;
padding: 0;
}
button {
font-family: inherit;
}
input,
textarea,
select {
font-family: inherit;
}
:focus-visible {
outline: 2px solid var(--mat-sys-primary);
outline-offset: 2px;
}

View File

@@ -1,29 +1,42 @@
import type { Metadata } from 'next';
import './globals.scss';
import { Providers } from './providers';
import type { Metadata } from 'next'
import { Inter, Bricolage_Grotesque, JetBrains_Mono } from 'next/font/google'
import '@/styles/theme.scss'
import '@/app/globals.css'
import { Providers } from '@/components/providers/Providers'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
const bricolage = Bricolage_Grotesque({
subsets: ['latin'],
variable: '--font-bricolage',
display: 'swap',
})
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-jetbrains',
display: 'swap',
})
export const metadata: Metadata = {
title: 'CodeSnippet - Share & Run Code (Python, React & More)',
description: 'Save, organize, and share your code snippets with beautiful syntax highlighting and live execution',
};
title: 'CodeSnippet - Material Design 3',
description: 'A modern code snippet manager built with Material Design 3',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="dark" suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
</head>
<html lang="en" className={`${inter.variable} ${bricolage.variable} ${jetbrainsMono.variable}`}>
<body>
<Providers>
{children}
</Providers>
<Providers>{children}</Providers>
</body>
</html>
);
)
}

View File

@@ -1,5 +1,47 @@
import { clsx, type ClassValue } from "clsx"
export function cn(...inputs: ClassValue[]) {
return clsx(inputs)
/**
* Utility function to combine class names
* Replaces tailwind-merge and clsx with a simple implementation
*/
export function cn(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(' ')
}
/**
* Format bytes to human readable string
*/
export function formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
/**
* Debounce function
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null
return function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = null
func(...args)
}
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
}
}
/**
* Sleep utility
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}

307
src/styles/theme.scss Normal file
View File

@@ -0,0 +1,307 @@
@use 'm3-scss/material' as mat;
// Define your M3 theme with violet palette
$theme: mat.define-theme((
color: (
theme-type: light,
primary: mat.$violet-palette,
tertiary: mat.$blue-palette,
),
typography: (
plain-family: (Inter, sans-serif),
brand-family: ('Bricolage Grotesque', sans-serif),
),
density: 0,
));
// Include all M3 component themes
@include mat.all-component-themes($theme);
@include mat.system-classes();
@include mat.typography-hierarchy($theme);
// Dark theme
.dark {
$dark-theme: mat.define-theme((
color: (
theme-type: dark,
primary: mat.$violet-palette,
tertiary: mat.$blue-palette,
),
typography: (
plain-family: (Inter, sans-serif),
brand-family: ('Bricolage Grotesque', sans-serif),
),
density: 0,
));
@include mat.all-component-colors($dark-theme);
}
// Custom utility classes for common patterns
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
.gap-6 {
gap: 1.5rem;
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.border {
border: 1px solid var(--mat-sys-outline);
}
.shadow-sm {
box-shadow: var(--mat-sys-level1);
}
.shadow-lg {
box-shadow: var(--mat-sys-level3);
}
.text-sm {
font-size: 0.875rem;
}
.text-base {
font-size: 1rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-xl {
font-size: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
}
.text-3xl {
font-size: 1.875rem;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.space-y-2 > * + * {
margin-top: 0.5rem;
}
.space-y-4 > * + * {
margin-top: 1rem;
}
.space-y-6 > * + * {
margin-top: 1.5rem;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.max-w-md {
max-width: 28rem;
}
.max-w-2xl {
max-width: 42rem;
}
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (min-width: 768px) {
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.overflow-hidden {
overflow: hidden;
}
.overflow-auto {
overflow: auto;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.cursor-pointer {
cursor: pointer;
}
.transition-colors {
transition-property: color, background-color, border-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.hover\:bg-muted:hover {
background-color: var(--mat-sys-surface-variant);
}
.text-muted-foreground {
color: var(--mat-sys-on-surface-variant);
}
.text-foreground {
color: var(--mat-sys-on-surface);
}
.text-primary {
color: var(--mat-sys-primary);
}
.text-destructive {
color: var(--mat-sys-error);
}
.bg-card {
background-color: var(--mat-sys-surface);
}
.bg-background {
background-color: var(--mat-sys-background);
}
.bg-muted {
background-color: var(--mat-sys-surface-variant);
}
.bg-primary {
background-color: var(--mat-sys-primary);
}
.bg-secondary {
background-color: var(--mat-sys-secondary);
}
.bg-accent {
background-color: var(--mat-sys-tertiary);
}
.bg-destructive {
background-color: var(--mat-sys-error);
}
.text-primary-foreground {
color: var(--mat-sys-on-primary);
}
.text-secondary-foreground {
color: var(--mat-sys-on-secondary);
}
.text-accent-foreground {
color: var(--mat-sys-on-tertiary);
}
.text-destructive-foreground {
color: var(--mat-sys-on-error);
}
.border-border {
border-color: var(--mat-sys-outline);
}
.min-h-screen {
min-height: 100vh;
}
.flex-1 {
flex: 1 1 0%;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}