diff --git a/components/helpers.js b/components/helpers.jsx similarity index 100% rename from components/helpers.js rename to components/helpers.jsx diff --git a/components/node-list/index.js b/components/node-list/index.jsx similarity index 83% rename from components/node-list/index.js rename to components/node-list/index.jsx index 99253f2..d04da65 100644 --- a/components/node-list/index.js +++ b/components/node-list/index.jsx @@ -1,11 +1,11 @@ import {NodeList} from './NodeList'; import './node-list.scss' +import { createRoot } from 'react-dom/client'; (function (Drupal, $, once) { const attachNodeList = (element) => { - ReactDOM.render( - , - element + createRoot(element).render( + ); }; diff --git a/components/node-list/node-list.component.yml b/components/node-list/node-list.component.yml new file mode 100644 index 0000000..dd6e792 --- /dev/null +++ b/components/node-list/node-list.component.yml @@ -0,0 +1,43 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json +name: Node List +status: stable +description: 'A React-based component that displays a list of nodes with theme variants.' +group: Content +props: + type: object + properties: + endpoint: + type: string + title: Endpoint + description: 'API endpoint for fetching node data' + default: '/api/node' + examples: + - '/api/node' + variant: + type: string + title: Theme Variant + description: 'Visual theme for the component' + default: 'light' + enum: + - 'light' + - 'dark' + meta:enum: + light: Light + dark: Dark + x-translation-context: Theme variants + examples: + - 'light' + - 'dark' + required: + - endpoint +libraryOverrides: + js: + ../../assets/node-list.js: { } + css: + theme: + ../../assets/node-list.css: { } + dependencies: + - react_scaffold/react + - react_scaffold/react-api-client + - core/drupal.ajax + - core/drupal.dropbutton \ No newline at end of file diff --git a/components/node-list/node-list.story.twig b/components/node-list/node-list.story.twig new file mode 100644 index 0000000..de9642b --- /dev/null +++ b/components/node-list/node-list.story.twig @@ -0,0 +1 @@ +{{ include('react_scaffold:node-list', { variant: 'light' }, with_context = false) }} \ No newline at end of file diff --git a/components/node-list/node-list.twig b/components/node-list/node-list.twig new file mode 100644 index 0000000..48f1242 --- /dev/null +++ b/components/node-list/node-list.twig @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/components/node-list/node-list.ui_patterns.yml b/components/node-list/node-list.ui_patterns.yml deleted file mode 100644 index 3c023be..0000000 --- a/components/node-list/node-list.ui_patterns.yml +++ /dev/null @@ -1,27 +0,0 @@ -node_list: - label: 'Node list' - description: 'Table with list of nodes.' - fields: - endpoint: - type: string - label: 'Endpoint' - description: 'Endpoint for the node list.' - preview: '/api/node' - variants: - light: - label: "Light" - dark: - label: "Dark" - - libraries: - - node_list: - js: - ../../assets/node-list.js: { } - css: - theme: - ../../assets/node-list.css: { } - dependencies: - - react_scaffold/react - - react_scaffold/react-api-client - - core/drupal.ajax - - core/drupal.dropbutton diff --git a/components/node-list/pattern-node-list.html.twig b/components/node-list/pattern-node-list.html.twig deleted file mode 100644 index 2decf49..0000000 --- a/components/node-list/pattern-node-list.html.twig +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/components/react-tooltip/index.js b/components/react-tooltip/index.jsx similarity index 86% rename from components/react-tooltip/index.js rename to components/react-tooltip/index.jsx index 8e380ea..2dee4a4 100644 --- a/components/react-tooltip/index.js +++ b/components/react-tooltip/index.jsx @@ -1,12 +1,13 @@ import {TextWithTooltip} from './TextWithTooltip'; import './react-tooltip.scss' +import { createRoot } from 'react-dom/client'; // Values for this component (e.g. 'text') come from the pattern field. (function (Drupal, $, once) { + const attachTooltip = (element) => { - ReactDOM.render( - , - element + createRoot(element).render( + ); }; diff --git a/components/react-tooltip/pattern-react-tooltip.html.twig b/components/react-tooltip/pattern-react-tooltip.html.twig deleted file mode 100644 index cb33aa7..0000000 --- a/components/react-tooltip/pattern-react-tooltip.html.twig +++ /dev/null @@ -1 +0,0 @@ -{{ content }} diff --git a/components/react-tooltip/react-tooltip.component.yml b/components/react-tooltip/react-tooltip.component.yml new file mode 100644 index 0000000..619387e --- /dev/null +++ b/components/react-tooltip/react-tooltip.component.yml @@ -0,0 +1,33 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json +name: React Tooltip +status: stable +description: 'A React-based tooltip component that displays helpful text on hover.' +group: Interactive +props: + type: object + properties: + text: + type: string + title: Tooltip Text + description: 'The text to display in the tooltip' + default: 'Tooltip text' + examples: + - 'This is the tooltip text' + content: + type: string + title: Content + description: 'The content that triggers the tooltip when hovered' + default: 'Hover me' + examples: + - 'Hover me to see the tooltip' + required: + - text + - content +libraryOverrides: + js: + ../../assets/react-tooltip.js: { } + css: + theme: + ../../assets/react-tooltip.css: { } + dependencies: + - react_scaffold/react \ No newline at end of file diff --git a/components/react-tooltip/react-tooltip.story.twig b/components/react-tooltip/react-tooltip.story.twig new file mode 100644 index 0000000..7b435ad --- /dev/null +++ b/components/react-tooltip/react-tooltip.story.twig @@ -0,0 +1,4 @@ +{{ include('react_scaffold:react-tooltip', { + text: 'This is the tooltip text', + content: 'Hover me to see the tooltip', +}, with_context = false) }} \ No newline at end of file diff --git a/components/react-tooltip/react-tooltip.twig b/components/react-tooltip/react-tooltip.twig new file mode 100644 index 0000000..04f6436 --- /dev/null +++ b/components/react-tooltip/react-tooltip.twig @@ -0,0 +1 @@ +{{ content }} \ No newline at end of file diff --git a/components/react-tooltip/react-tooltip.ui_patterns.yml b/components/react-tooltip/react-tooltip.ui_patterns.yml deleted file mode 100644 index 07b1d49..0000000 --- a/components/react-tooltip/react-tooltip.ui_patterns.yml +++ /dev/null @@ -1,24 +0,0 @@ -react_tooltip: - label: "React tooltip" - description: "A react tooltip." - fields: - text: - type: "string" - label: "Text" - description: "Tooltip text" - preview: "This is the tooltip text" - content: - type: "string" - label: "Content" - description: "Tooltip content" - preview: "Hover me to see the tooltip" - - libraries: - - react_tooltip: - js: - ../../assets/react-tooltip.js: { } - css: - theme: - ../../assets/react-tooltip.css: { } - dependencies: - - react_scaffold/react diff --git a/logo.svg b/logo.svg index d9b0894..45fd9c5 100644 --- a/logo.svg +++ b/logo.svg @@ -1,9 +1,10 @@ - - - - React - - - - Drupal - + + + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index b44ddd5..19b719f 100644 --- a/package.json +++ b/package.json @@ -2,59 +2,42 @@ "name": "drupal-react-scaffold", "version": "0.0.1", "description": "Drupal React Scaffold theme", + "type": "module", "dependencies": { "@babel/runtime": "^7.17.2", - "axios": "^0.25.0", + "@tanstack/react-query": "^5.89.0", + "axios": "^1.12.2", "classnames": "^2.3.1", - "core-js": "^3.21.0", - "flexboxgrid": "6.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-query": "^3.34.14", + "core-js": "^3.45.1", + "flexboxgrid": "^6.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-tippy": "^1.4.0", - "uuid": "^8.3.2", - "whatwg-fetch": "^3.6.2", - "rsuite": "^5.37.4", - "rsuite-table": "^5.11.0" + "rsuite": "^5.83.3", + "rsuite-table": "^5.19.2", + "uuid": "^13.0.0", + "whatwg-fetch": "^3.6.20" }, "devDependencies": { - "@babel/core": "^7.17.2", - "@babel/plugin-proposal-class-properties": "^7.16.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.2", - "babel-loader": "^8.2.3", - "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "^10.2.4", - "css-loader": "^6.6.0", - "css-minimizer-webpack-plugin": "^3.4.1", - "mini-css-extract-plugin": "^2.5.3", - "node-sass": "^7.0.1", - "node-sass-magic-importer": "^5.3.2", + "babel-jest": "^30.1.2", + "jest": "^30.1.3", + "jest-junit": "^16.0.0", + "jest-mock-axios": "^4.9.0", "postcss": "^8.4.6", - "postcss-asset-webpack-plugin": "^1.0.2", - "postcss-loader": "^6.2.1", "react-select-event": "^4.1.4", "sass": "^1.49.7", - "sass-loader": "^12.4.0", - "style-loader": "^3.3.1", - "terser-webpack-plugin": "^5.3.1", - "webpack": "^5.68.0", - "webpack-cli": "^4.9.2", - "webpack-require-from": "^1.8.6", - "@testing-library/jest-dom": "^5.16.2", - "@testing-library/react": "^12.1.2", - "babel-jest": "^27.5.1", - "jest": "^27.5.1", - "jest-junit": "^13.0.0", - "jest-mock-axios": "^4.5.0" + "vite": "^7.1.6", + "vite-plugin-external": "^6.2.2" }, "browserslist": "last 2 versions, not IE 11", "scripts": { - "build": "webpack --mode=none --config=webpack.config.prod.js", - "watch": "webpack --config webpack.config.dev.js", - "dist": "webpack --mode production --config webpack.config.prod.js", + "build": "vite build", + "watch": "vite build --watch --mode development", + "dist": "vite build --mode production", "test:watch": "jest --watchAll", "test": "jest" } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..d9b65e9 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +import autoprefixer from 'autoprefixer'; + +export default { + plugins: [ + autoprefixer, + ], +}; \ No newline at end of file diff --git a/react_scaffold.info.yml b/react_scaffold.info.yml index b60d800..2c8915f 100644 --- a/react_scaffold.info.yml +++ b/react_scaffold.info.yml @@ -1,10 +1,31 @@ name: 'React Scaffold' -description: 'A React Scaffold theme.' +description: 'A React Scaffold theme with Single Directory Components.' type: theme -base theme: stark -core_version_requirement: ^8 || ^9 || ^10 +base theme: umami +core_version_requirement: ^10 || ^11 package: Scaffold libraries: - "react_scaffold/global-styling" - "react_scaffold/global-libraries" + +# Enable component discovery in the components directory +components: + namespaces: + react_scaffold: components + +regions: + pre_header: Pre-header + header: Header + highlighted: Highlighted + tabs: Tabs + banner_top: 'Banner Top' + breadcrumbs: Breadcrumbs + page_title: 'Page Title' + content: Content + sidebar: Sidebar + content_bottom: 'Content Bottom' + footer: Footer + bottom: Bottom + page_top: 'Page top' # Needed by Drupal Core + page_bottom: 'Page bottom' # Needed by Drupal Core \ No newline at end of file diff --git a/react_scaffold.libraries.yml b/react_scaffold.libraries.yml index 0034ae5..cd6e375 100644 --- a/react_scaffold.libraries.yml +++ b/react_scaffold.libraries.yml @@ -17,7 +17,31 @@ react: assets/react/react.js: { minified: true, weight: -4 } assets/react/react-dom.js: { minified: true, weight: -4 } assets/helpers.js: { minified: true, weight: -3 } + dependencies: + - react_scaffold/global-libraries react-api-client: js: assets/apiClient.js: { minified: true, weight: -2 } + +# SDC Component libraries +node-list: + js: + assets/node-list.js: { minified: true } + css: + theme: + assets/node-list.css: {} + dependencies: + - react_scaffold/react + - react_scaffold/react-api-client + - core/drupal.ajax + - core/drupal.dropbutton + +react-tooltip: + js: + assets/react-tooltip.js: { minified: true } + css: + theme: + assets/react-tooltip.css: {} + dependencies: + - react_scaffold/react diff --git a/vite-plugin-copy-react.js b/vite-plugin-copy-react.js new file mode 100644 index 0000000..e50aab1 --- /dev/null +++ b/vite-plugin-copy-react.js @@ -0,0 +1,47 @@ +import { copyFile, mkdir } from 'fs/promises'; +import { resolve, dirname } from 'path'; +import { existsSync } from 'fs'; + +export function vitePluginCopyReact(mode = 'development') { + return { + name: 'copy-react', + writeBundle: { + sequential: true, + async handler() { + const outDir = resolve(__dirname, 'assets'); + const reactDir = resolve(outDir, 'react'); + + // Ensure react directory exists + if (!existsSync(reactDir)) { + await mkdir(reactDir, { recursive: true }); + } + + const isDev = mode === 'development'; + + const reactSource = isDev + ? 'node_modules/react/umd/react.development.js' + : 'node_modules/react/umd/react.production.min.js'; + + const reactDomSource = isDev + ? 'node_modules/react-dom/umd/react-dom.development.js' + : 'node_modules/react-dom/umd/react-dom.production.min.js'; + + try { + await copyFile( + resolve(__dirname, reactSource), + resolve(reactDir, 'react.js') + ); + + await copyFile( + resolve(__dirname, reactDomSource), + resolve(reactDir, 'react-dom.js') + ); + + console.log('React libraries copied successfully'); + } catch (error) { + console.warn('Failed to copy React libraries:', error.message); + } + } + } + }; +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..a94709e --- /dev/null +++ b/vite.config.js @@ -0,0 +1,123 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; +import { readdir, lstat } from 'fs/promises'; +import { vitePluginCopyReact } from './vite-plugin-copy-react.js'; +import vitePluginExternal from 'vite-plugin-external'; + +// Helper function to get dynamic entries from components directory +async function getDynamicEntries() { + const entries = { + // Static entries + common: './components/common.js', + helpers: './components/helpers.jsx', + apiClient: './components/apiClient.js', + }; + + try { + const componentsDir = resolve(__dirname, 'components'); + const items = await readdir(componentsDir); + + for (const item of items) { + const itemPath = resolve(componentsDir, item); + const stats = await lstat(itemPath); + + if (stats.isDirectory()) { + // Check if index.jsx exists, otherwise use index.js + const jsxPath = resolve(itemPath, 'index.jsx'); + const jsPath = resolve(itemPath, 'index.js'); + try { + await lstat(jsxPath); + entries[item] = `./components/${item}/index.jsx`; + } catch { + try { + await lstat(jsPath); + entries[item] = `./components/${item}/index.js`; + } catch { + console.warn(`No index file found for ${item}`); + } + } + } + } + } catch (error) { + console.warn('Error reading components directory:', error); + } + + return entries; +} + +export default defineConfig(async ({ mode }) => { + const entries = await getDynamicEntries(); + + return { + build: { + lib: false, + outDir: 'assets', + emptyOutDir: true, + rollupOptions: { + input: entries, + output: { + entryFileNames: '[name].js', + chunkFileNames: '[name].js', + assetFileNames: (assetInfo) => { + const info = assetInfo.name.split('.'); + const extType = info[info.length - 1]; + if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) { + return `images/[name][extname]`; + } + if (/css/i.test(extType)) { + return `[name][extname]`; + } + return `[name][extname]`; + }, + globals: { + 'react': 'React', + 'react-dom': 'ReactDOM', + }, + }, + external: ['react', 'react-dom'], + }, + sourcemap: mode === 'development', + minify: mode === 'production', + }, + + resolve: { + alias: { + Components: resolve(__dirname, 'components'), + }, + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + includePaths: [resolve(__dirname, 'scss')], + } + } + }, + + define: { + global: 'globalThis', + }, + + esbuild: { + jsxInject: `import React from 'react'`, + loader: 'jsx', + }, + + plugins: [ + react({ + jsxRuntime: 'classic', + }), + vitePluginExternal({ + externals: { + 'react': 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + } + }), + vitePluginCopyReact(mode), + ], + }; +}); \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js deleted file mode 100644 index 40aa31a..0000000 --- a/webpack.common.js +++ /dev/null @@ -1,112 +0,0 @@ -const path = require("path"); -const magicImporter = require("node-sass-magic-importer"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const {CleanWebpackPlugin} = require("clean-webpack-plugin"); -const WebpackRequireFrom = require("webpack-require-from"); -const fs = require("fs"); - -const rootDir = path.resolve(__dirname); -const buildDir = "assets"; - -const config = { - entry: { - // All assets needed on all pages are compiled in a common: - common: "./components/common.js", - // All assets loaded only in some pages will have their own bundle: - helpers: "./components/helpers.js", - apiClient: "./components/apiClient.js", - // Iterate over each directory in "components" and create a bundle for each: - ...Object.fromEntries( - fs - .readdirSync(path.resolve(__dirname, "components")) - .filter((dir) => fs.lstatSync(path.resolve(__dirname, "components", dir)).isDirectory()) - .map((dir) => [dir, `./components/${dir}/index.js`]) - ), - }, - - output: { - path: rootDir + "/" + buildDir, - filename: "[name].js", - assetModuleFilename: "[name][ext]", - }, - - module: { - rules: [ - { - test: /\.(sa|sc|c)ss$/, - exclude: /node_modules/, - use: [ - MiniCssExtractPlugin.loader, - "css-loader", - { - loader: "postcss-loader", - options: { - postcssOptions: { - plugins: ["autoprefixer"], - }, - }, - }, - { - loader: "sass-loader", - options: { - sassOptions: { - // Custom node-sass importer for selector specific imports, - // node importing, module importing, globbing support and - // importing files only once. Check the numerous and - // interesting options here: - // https://www.npmjs.com/package/node-sass-magic-importer - importer: magicImporter(), - }, - }, - }, - ], - }, - { - test: /\.(js|jsx)$/, - type: "javascript/auto", - exclude: /node_modules/, - use: {loader: "babel-loader"}, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.(png|jp(e*)g|svg)$/, - exclude: /node_modules/, - type: "asset", - parser: { - dataUrlCondition: { - maxSize: 8 * 1024, // 8kb - }, - }, - }, - ], - }, - - resolve: { - extensions: [".js", ".jsx"], - alias: { - Components: path.resolve(__dirname, "components/"), - }, - }, - - externals: { - "react": "React", - "react-dom": "ReactDOM", - }, - - plugins: [ - // Clean assets folder before every operation: - new CleanWebpackPlugin({ - cleanStaleWebpackAssets: false, - }), - - new WebpackRequireFrom({ - path: "./react_scaffold/assets/", - }), - // Compile styles no inlined into the JS bundle, but in a separate CSS file: - new MiniCssExtractPlugin(), - ], -}; - -module.exports = config; diff --git a/webpack.config.dev.js b/webpack.config.dev.js deleted file mode 100644 index 7b03755..0000000 --- a/webpack.config.dev.js +++ /dev/null @@ -1,32 +0,0 @@ -const baseConfig = require("./webpack.common.js"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); - -const config = { - ...baseConfig, - mode: "development", - watch: true, - devtool: "eval-source-map", - - plugins: baseConfig.plugins.concat([ - // Copy React libraries: - new CopyWebpackPlugin({ - patterns: [ - { - from: "node_modules/react/umd/react.development.js", - to: "react/react.js", - }, - { - from: "node_modules/react-dom/umd/react-dom.development.js", - to: "react/react-dom.js", - }, - ] - }), - ]), - - watchOptions: { - poll: 500, - ignored: ["node_modules/**", "assets/**"], - }, -}; - -module.exports = config; diff --git a/webpack.config.prod.js b/webpack.config.prod.js deleted file mode 100644 index d7fecb0..0000000 --- a/webpack.config.prod.js +++ /dev/null @@ -1,38 +0,0 @@ -const baseConfig = require("./webpack.common.js"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); -const TerserPlugin = require("terser-webpack-plugin"); - -const config = { - ...baseConfig, - mode: "production", - - plugins: baseConfig.plugins.concat([ - // Copy React libraries: - new CopyWebpackPlugin({ - patterns: [ - { - from: "node_modules/react/umd/react.production.min.js", - to: "react/react.js", - }, - { - from: "node_modules/react-dom/umd/react-dom.production.min.js", - to: "react/react-dom.js", - }, - ], - }), - ]), - - // This optimization block is called only in PRODUCTION mode: - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin({ - parallel: true, - }), - new CssMinimizerPlugin(), - ], - }, -}; - -module.exports = config;