diff --git a/.gitignore b/.gitignore index c99d9ed..ac1b823 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ .env tailwind.config.js tailwind.config.ts +app src/app src/index.css diff --git a/package-lock.json b/package-lock.json index 372ea70..49e9855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@inquirer/prompts": "^5.1.2", "chalk": "^5.3.0", "commander": "^12.1.0", + "justd-icons": "^1.4.29", "ora": "^8.0.1" }, "bin": { @@ -484,10 +485,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", - "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", - "dev": true, + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -6116,6 +6116,18 @@ "node": "*" } }, + "node_modules/justd-icons": { + "version": "1.4.29", + "resolved": "https://registry.npmjs.org/justd-icons/-/justd-icons-1.4.29.tgz", + "integrity": "sha512-LWWJgmotzsSldAzqrrpdPxnAXRu3gkZgFH0zWYEd1hlm6MjmOd2vsOnf01oSWU31pEk3JFnHxJPAd1HQKlXLbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6350,6 +6362,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -7605,6 +7630,19 @@ "dev": true, "license": "ISC" }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-pkg": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", @@ -7798,7 +7836,6 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/registry-auth-token": { diff --git a/package.json b/package.json index f3848cc..ad6e045 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@inquirer/prompts": "^5.1.2", "chalk": "^5.3.0", "commander": "^12.1.0", + "justd-icons": "^1.4.29", "ora": "^8.0.1" }, "release-it": { diff --git a/src/commands/init.ts b/src/commands/init.ts index adc8e7b..2b3a87e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -14,6 +14,7 @@ const __dirname = path.dirname(__filename) // Adjust the path to reference the correct resource directory relative to the compiled output const resourceDir = path.resolve(__dirname, '../src/resources') +const stubs = path.resolve(__dirname, '../src/resources/stubs') export async function init() { const cssPath = { @@ -44,18 +45,21 @@ export async function init() { { name: 'Other', value: 'Other' }, ], }) - let componentsFolder, uiFolder, cssLocation, configSourcePath + let componentsFolder, uiFolder, cssLocation, configSourcePath, themeProvider, providers if (projectType === 'Laravel') { componentsFolder = 'resources/js/components' uiFolder = path.join(componentsFolder, 'ui') cssLocation = cssPath.laravel - configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.laravel.stub') + configSourcePath = path.join(stubs, 'laravel/tailwind.config.laravel.stub') + themeProvider = path.join(stubs, 'laravel/theme-provider.stub') + providers = path.join(stubs, 'laravel/providers.stub') } else if (projectType === 'Vite') { componentsFolder = 'src/components' uiFolder = path.join(componentsFolder, 'ui') cssLocation = cssPath.vite - configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.vite.stub') + configSourcePath = path.join(stubs, 'vite/tailwind.config.vite.stub') + themeProvider = path.join(stubs, 'vite/theme-provider.stub') } else if (projectType === 'Next.js') { const projectTypeSrc = await select({ message: 'Does this project have a src directory?', @@ -69,7 +73,9 @@ export async function init() { componentsFolder = path.join(hasSrc, 'components') uiFolder = path.join(componentsFolder, 'ui') cssLocation = projectTypeSrc ? cssPath.nextHasSrc : cssPath.nextNoSrc - configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.next.stub') + configSourcePath = path.join(stubs, 'next/tailwind.config.next.stub') + themeProvider = path.join(stubs, 'next/theme-provider.stub') + providers = path.join(stubs, 'next/providers.stub') } else { componentsFolder = await input({ message: 'Enter the path to your components folder:', @@ -80,10 +86,12 @@ export async function init() { message: 'Where would you like to place the CSS file?', default: cssPath.other, }) - configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.next.stub') + configSourcePath = path.join(stubs, 'next/tailwind.config.next.stub') + themeProvider = path.join(stubs, 'next/theme-provider.stub') + providers = path.join(stubs, 'next/providers.stub') } - const spinner = ora(`Initializing D...`).start() + const spinner = ora(`Initializing Justd...`).start() // Ensure the components and UI folders exist if (!fs.existsSync(uiFolder)) { @@ -159,9 +167,22 @@ export async function init() { const fileUrl = 'https://raw.githubusercontent.com/irsyadadl/justd/master/components/ui/primitive.tsx' const response = await fetch(fileUrl) const fileContent = await response.text() - fs.writeFileSync(path.join(uiFolder, 'primitive.tsx'), fileContent) + fs.writeFileSync(path.join(uiFolder, 'primitive.tsx'), fileContent, { flag: 'w' }) spinner.succeed(`primitive.tsx file copied to ${uiFolder}`) + // Copy theme provider and providers files + if (themeProvider) { + const themeProviderContent = fs.readFileSync(themeProvider, 'utf8') + fs.writeFileSync(path.join(componentsFolder, 'theme-provider.tsx'), themeProviderContent, { flag: 'w' }) + + if (providers) { + const providersContent = fs.readFileSync(providers, 'utf8') + fs.writeFileSync(path.join(componentsFolder, 'providers.tsx'), providersContent, { flag: 'w' }) + } + + spinner.succeed(`Theme provider and providers files copied to ${componentsFolder}`) + } + // Save configuration to justd.json with relative path if (fs.existsSync('d.json')) { fs.unlinkSync('d.json') @@ -177,5 +198,16 @@ export async function init() { // Wait for the installation to complete before proceeding spinner.succeed('Installation complete.') + + const continuedToAddComponent = spawn('npx justd-cli@latest add', { + stdio: 'inherit', + shell: true, + }) + await new Promise((resolve) => { + continuedToAddComponent.on('close', () => { + resolve() + }) + }) + spinner.stop() } diff --git a/src/resources/stubs/laravel/providers.stub b/src/resources/stubs/laravel/providers.stub new file mode 100644 index 0000000..373f19a --- /dev/null +++ b/src/resources/stubs/laravel/providers.stub @@ -0,0 +1,14 @@ +import { router } from '@inertiajs/react' +import { ThemeProvider } from './theme-provider' +import React from 'react' +import { RouterProvider } from 'react-aria-components' + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + router.visit(to, options as any)}> + + {children} + + + ) +} diff --git a/src/resources/tailwind-config/tailwind.config.laravel.stub b/src/resources/stubs/laravel/tailwind.config.laravel.stub similarity index 100% rename from src/resources/tailwind-config/tailwind.config.laravel.stub rename to src/resources/stubs/laravel/tailwind.config.laravel.stub diff --git a/src/resources/stubs/laravel/theme-provider.stub b/src/resources/stubs/laravel/theme-provider.stub new file mode 100644 index 0000000..4bd2aa1 --- /dev/null +++ b/src/resources/stubs/laravel/theme-provider.stub @@ -0,0 +1,67 @@ +import { createContext, useContext, useEffect, useState } from 'react' + +type Theme = 'dark' | 'light' | 'system' + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const initialState: ThemeProviderState = { + theme: 'system', + setTheme: () => null +} + +const ThemeProviderContext = createContext(initialState) + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'vite-ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme) + + useEffect(() => { + const root = window.document.documentElement + + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme) + setTheme(theme) + } + } + + return ( + + {children} + + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + + if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider') + + return context +} diff --git a/src/resources/stubs/next/providers.stub b/src/resources/stubs/next/providers.stub new file mode 100644 index 0000000..3e084b7 --- /dev/null +++ b/src/resources/stubs/next/providers.stub @@ -0,0 +1,21 @@ +'use client' + +import { ThemeProvider } from './theme-provider' +import { useRouter } from 'next/navigation' +import { RouterProvider } from 'react-aria-components' + +declare module 'react-aria-components' { + interface RouterConfig { + routerOptions: NonNullable['push']>[1]> + } +} + +export function Providers({ children }: { children: React.ReactNode }) { + const router = useRouter() + + return ( + + {children} + + ) +} diff --git a/src/resources/tailwind-config/tailwind.config.next.stub b/src/resources/stubs/next/tailwind.config.next.stub similarity index 100% rename from src/resources/tailwind-config/tailwind.config.next.stub rename to src/resources/stubs/next/tailwind.config.next.stub diff --git a/src/resources/stubs/next/theme-provider.stub b/src/resources/stubs/next/theme-provider.stub new file mode 100644 index 0000000..f75a629 --- /dev/null +++ b/src/resources/stubs/next/theme-provider.stub @@ -0,0 +1,12 @@ +'use client' + +import * as React from 'react' + +import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes' +import { type ThemeProviderProps } from 'next-themes/dist/types' + +const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => { + return {children} +} + +export { ThemeProvider, useTheme } diff --git a/src/resources/tailwind-config/tailwind.config.vite.stub b/src/resources/stubs/vite/tailwind.config.vite.stub similarity index 100% rename from src/resources/tailwind-config/tailwind.config.vite.stub rename to src/resources/stubs/vite/tailwind.config.vite.stub diff --git a/src/resources/stubs/vite/theme-provider.stub b/src/resources/stubs/vite/theme-provider.stub new file mode 100644 index 0000000..4bd2aa1 --- /dev/null +++ b/src/resources/stubs/vite/theme-provider.stub @@ -0,0 +1,67 @@ +import { createContext, useContext, useEffect, useState } from 'react' + +type Theme = 'dark' | 'light' | 'system' + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const initialState: ThemeProviderState = { + theme: 'system', + setTheme: () => null +} + +const ThemeProviderContext = createContext(initialState) + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'vite-ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme) + + useEffect(() => { + const root = window.document.documentElement + + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme) + setTheme(theme) + } + } + + return ( + + {children} + + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + + if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider') + + return context +}