diff --git a/.github/workflows/deployment_tests.yml b/.github/workflows/api-first.yml similarity index 56% rename from .github/workflows/deployment_tests.yml rename to .github/workflows/api-first.yml index 9f6edc6e22..8edf0f2f8e 100644 --- a/.github/workflows/deployment_tests.yml +++ b/.github/workflows/api-first.yml @@ -1,12 +1,49 @@ -name: Deployment Tests +name: API-first story Tests on: [push, pull_request] env: node-version: 22.x jobs: - vitessr: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + config: + runs-on: ubuntu-latest + outputs: + ENVIRONMENT: ${{ steps.vars.outputs.ENVIRONMENT }} + STACK_NAME: ${{ steps.vars.outputs.STACK_NAME }} + nextjs: ${{ steps.filter.outputs.nextjs }} + remix: ${{ steps.filter.outputs.remix }} + rr7: ${{ steps.filter.outputs.rr7 }} + tss: ${{ steps.filter.outputs.tss }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set Env Vars + id: vars + run: | + ENVIRONMENT=${{ vars.LIVE_ENV }} + echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_OUTPUT + echo "STACK_NAME=${ENVIRONMENT//./-}" >> $GITHUB_OUTPUT + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + nextjs: + - 'apps/nextjs/**' + - '.github/workflows/api-first.yml' + remix: + - 'apps/remix/**' + - '.github/workflows/api-first.yml' + rr7: + - 'apps/rr7/**' + - '.github/workflows/api-first.yml' + tss: + - 'apps/vite-ssr/**' + - '.github/workflows/api-first.yml' + + tss: + if: ${{ needs.config.outputs.tss == 'true' }} && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) runs-on: ubuntu-latest name: Vite SSR timeout-minutes: 5 @@ -68,7 +105,8 @@ jobs: run: make backend-docker-detached-stop nextjs: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + # if: ${{ needs.config.outputs.nextjs == 'true' }} && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) + if: false runs-on: ubuntu-latest name: Next.JS timeout-minutes: 5 @@ -130,7 +168,7 @@ jobs: run: make backend-docker-detached-stop remix: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + if: ${{ needs.config.outputs.remix == 'true' }} && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) runs-on: ubuntu-latest name: Remix timeout-minutes: 5 @@ -174,7 +212,63 @@ jobs: run: pnpm --filter plone-remix build - name: Start server - run: nohup pnpm --filter plone-remix start:prod & + run: PLONE_API_PATH=http://localhost:8080/Plone nohup pnpm --filter plone-remix start:prod & + + - name: Wait + run: packages/scripts/node_modules/.bin/wait-on --httpTimeout 20000 http-get://127.0.0.1:8080/Plone + + - name: Run tests + run: node packages/scripts/check_deployment.js + + - name: Stop backend + run: make backend-docker-detached-stop + + rr7: + if: ${{ needs.config.outputs.rr7 == 'true' }} && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) + runs-on: ubuntu-latest + name: ReactRouter 7 + timeout-minutes: 5 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + # node setup + - name: Use Node.js ${{ env.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.node-version }} + + - name: Enable corepack + run: corepack enable + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build dependencies + run: pnpm build:all + + - name: Start backend + run: make backend-docker-detached-start + + - name: Build + run: pnpm --filter plone-rr7 build + + - name: Start server + run: PLONE_API_PATH=http://localhost:8080/Plone nohup pnpm --filter plone-rr7 start:prod & - name: Wait run: packages/scripts/node_modules/.bin/wait-on --httpTimeout 20000 http-get://127.0.0.1:8080/Plone diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 418c2c4f18..b9616ac57d 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -20,7 +20,7 @@ jobs: - name: Install towncrier run: pip install towncrier - - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v3 id: filter with: filters: | @@ -42,6 +42,12 @@ jobs: - 'packages/scripts/**' voltoSlate: - 'packages/volto-slate/**' + blocks: + - 'packages/blocks/**' + slots: + - 'packages/slots/**' + theming: + - 'packages/theming/**' wrongNews: - added|modified: 'news/**' @@ -120,6 +126,30 @@ jobs: env: BASE_BRANCH: ${{ github.base_ref }} + - name: Blocks changelog check + if: steps.filter.outputs.blocks == 'true' + run: | + git fetch --no-tags origin main + towncrier check --compare-with origin/main --dir packages/blocks + env: + BASE_BRANCH: ${{ github.base_ref }} + + - name: Slots changelog check + if: steps.filter.outputs.slots == 'true' + run: | + git fetch --no-tags origin main + towncrier check --compare-with origin/main --dir packages/slots + env: + BASE_BRANCH: ${{ github.base_ref }} + + - name: Theming changelog check + if: steps.filter.outputs.theming == 'true' + run: | + git fetch --no-tags origin main + towncrier check --compare-with origin/main --dir packages/theming + env: + BASE_BRANCH: ${{ github.base_ref }} + - name: Wrong location of news changelog check if: steps.filter.outputs.wrongNews == 'true' run: echo "News items should be moved from the repository root to the appropriate package root in `packages/package-name`." && exit 1 diff --git a/.stylelintignore b/.stylelintignore index 1521c8b765..4b2be235a0 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1 +1,4 @@ dist +docs +node_modules +packages/registry/lib diff --git a/apps/remix/.gitignore b/apps/remix/.gitignore index 3f7bf98da3..8bf74e0cf1 100644 --- a/apps/remix/.gitignore +++ b/apps/remix/.gitignore @@ -4,3 +4,4 @@ node_modules /build /public/build .env +.registry.loader.js diff --git a/apps/remix/app/client.ts b/apps/remix/app/client.ts index 0eec9cd62e..db178fa7f7 100644 --- a/apps/remix/app/client.ts +++ b/apps/remix/app/client.ts @@ -2,7 +2,7 @@ import ploneClient from '@plone/client'; import config from '@plone/registry'; const cli = ploneClient.initialize({ - apiPath: config.settings.apiPath, + apiPath: config.settings.internalApiPath || config.settings.apiPath, }); export { cli as ploneClient }; diff --git a/apps/remix/app/config.ts b/apps/remix/app/config.ts index ae8881a358..88d1de718f 100644 --- a/apps/remix/app/config.ts +++ b/apps/remix/app/config.ts @@ -1,15 +1,15 @@ import config from '@plone/registry'; -import { blocksConfig, slate } from '@plone/blocks'; - -const settings = { - apiPath: 'http://localhost:8080/Plone', - slate, -}; - -// @ts-expect-error We need to fix typing -config.set('settings', settings); - -// @ts-expect-error We need to fix typing -config.set('blocks', { blocksConfig }); - -export default config; +import applyAddonConfiguration from '@plone/registry/addons-loader'; + +export default function install() { + applyAddonConfiguration(config); + config.settings.apiPath = + process.env.PLONE_API_PATH || 'http://localhost:3000'; + config.settings.internalApiPath = process.env.PLONE_INTERNAL_API_PATH || ''; + console.log('API_PATH is:', config.settings.apiPath); + console.log( + 'INTERNAL_API_PATH is:', + config.settings.internalApiPath || 'not set', + ); + return config; +} diff --git a/apps/remix/app/routes/_index.tsx b/apps/remix/app/content.tsx similarity index 61% rename from apps/remix/app/routes/_index.tsx rename to apps/remix/app/content.tsx index 2ad6a19bbe..3e8a562784 100644 --- a/apps/remix/app/routes/_index.tsx +++ b/apps/remix/app/content.tsx @@ -7,14 +7,16 @@ import { dehydrate, QueryClient, HydrationBoundary, + useQueryClient, useQuery, } from '@tanstack/react-query'; -import { flattenToAppURL } from '../utils'; +import { flattenToAppURL } from './utils'; import { useLoaderData, useLocation } from '@remix-run/react'; import { usePloneClient } from '@plone/providers'; -import { Breadcrumbs, RenderBlocks } from '@plone/components'; -import config from '@plone/registry'; -import { ploneClient } from '../client'; +// import { Breadcrumbs, RenderBlocks } from '@plone/components'; +// import config from '@plone/registry'; +import { ploneClient } from './client'; +import App from '@plone/slots/components/App'; export const meta: MetaFunction = () => { return [ @@ -23,7 +25,7 @@ export const meta: MetaFunction = () => { ]; }; -const expand = ['breadcrumbs', 'navigation']; +const expand = ['navroot', 'breadcrumbs', 'navigation']; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const loader = async ({ params, request }: LoaderFunctionArgs) => { @@ -40,7 +42,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { const { getContentQuery } = ploneClient; await queryClient.prefetchQuery( - getContentQuery({ path: flattenToAppURL(request.url), expand }), + getContentQuery({ path: flattenToAppURL(request.url || '/'), expand }), ); return json({ dehydratedState: dehydrate(queryClient) }); @@ -48,31 +50,23 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { function Page() { const { getContentQuery } = usePloneClient(); - const pathname = useLocation().pathname; - const { data } = useQuery(getContentQuery({ path: pathname, expand })); + const { pathname } = useLocation(); + const { data } = useQuery(getContentQuery({ path: pathname || '/', expand })); if (!data) return null; return ( <> - - + ); } -export default function Index() { +export default function Content() { const { dehydratedState } = useLoaderData(); + const queryClient = useQueryClient(); return ( - + ); diff --git a/apps/remix/app/entry.client.tsx b/apps/remix/app/entry.client.tsx index 4680b84a57..b1f2b5cf0b 100644 --- a/apps/remix/app/entry.client.tsx +++ b/apps/remix/app/entry.client.tsx @@ -3,11 +3,12 @@ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ * For more information, see https://remix.run/file-conventions/entry.client */ - +import install from './config'; import { RemixBrowser } from '@remix-run/react'; import { startTransition, StrictMode } from 'react'; import { hydrateRoot } from 'react-dom/client'; -import './config'; + +install(); startTransition(() => { hydrateRoot( diff --git a/apps/remix/app/entry.server.tsx b/apps/remix/app/entry.server.tsx index 9af132970a..64f8029dd8 100644 --- a/apps/remix/app/entry.server.tsx +++ b/apps/remix/app/entry.server.tsx @@ -11,10 +11,12 @@ import { createReadableStreamFromReadable } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; import { isbot } from 'isbot'; import { renderToPipeableStream } from 'react-dom/server'; -import './config'; +import install from './config'; const ABORT_DELAY = 5_000; +install(); + export default function handleRequest( request: Request, responseStatusCode: number, diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index ecfb28c8be..5d49a06f99 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -17,18 +17,16 @@ import PloneClient from '@plone/client'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import '@plone/components/dist/basic.css'; +import '@plone/slots/main.css'; import { flattenToAppURL } from './utils'; import { PloneProvider } from '@plone/providers'; + import config from '@plone/registry'; export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : []), ]; -function useHrefLocal(to: string) { - return useHref(flattenToAppURL(to)); -} - export default function App() { const [queryClient] = useState( () => @@ -55,6 +53,10 @@ export default function App() { return RRNavigate(flattenToAppURL(to)); }; + function useHrefLocal(to: string) { + return useHref(flattenToAppURL(to)); + } + return ( @@ -71,6 +73,7 @@ export default function App() { useParams={useParams} useHref={useHrefLocal} navigate={navigate} + flattenToAppURL={flattenToAppURL} > diff --git a/apps/remix/app/routes/$.tsx b/apps/remix/app/routes/$.tsx deleted file mode 100644 index abd1fd4bbd..0000000000 --- a/apps/remix/app/routes/$.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { - json, - type LoaderFunctionArgs, - type MetaFunction, -} from '@remix-run/node'; -import { - dehydrate, - QueryClient, - HydrationBoundary, - useQuery, -} from '@tanstack/react-query'; -import { flattenToAppURL } from '../utils'; -import { useLoaderData, useLocation } from '@remix-run/react'; -import { usePloneClient } from '@plone/providers'; -import { Breadcrumbs, RenderBlocks } from '@plone/components'; -import config from '@plone/registry'; -import { ploneClient } from '../client'; - -export const meta: MetaFunction = () => { - return [ - { title: 'Plone on Remix' }, - { name: 'description', content: 'Welcome to Plone on Remix!' }, - ]; -}; - -const expand = ['breadcrumbs', 'navigation']; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const loader = async ({ params, request }: LoaderFunctionArgs) => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // With SSR, we usually want to set some default staleTime - // above 0 to avoid refetching immediately on the client - staleTime: 60 * 1000, - }, - }, - }); - - const { getContentQuery } = ploneClient; - - await queryClient.prefetchQuery( - getContentQuery({ path: flattenToAppURL(request.url), expand }), - ); - - return json({ dehydratedState: dehydrate(queryClient) }); -}; - -function Page() { - const { getContentQuery } = usePloneClient(); - const pathname = useLocation().pathname; - const { data } = useQuery(getContentQuery({ path: pathname, expand })); - - if (!data) return null; - return ( - <> - - - - ); -} - -export default function Index() { - const { dehydratedState } = useLoaderData(); - - return ( - - - - ); -} diff --git a/apps/remix/app/utils.ts b/apps/remix/app/utils.ts index c297613f90..186833cc1d 100644 --- a/apps/remix/app/utils.ts +++ b/apps/remix/app/utils.ts @@ -1,4 +1,4 @@ -import config from './config'; +import config from '@plone/registry'; /** * Flatten to app server URL - Given a URL if it starts with the API server URL @@ -9,9 +9,11 @@ import config from './config'; */ export function flattenToAppURL(url: string) { const { settings } = config; - - return ( + const result = url && - url.replace(settings.apiPath, '').replace('http://localhost:3000', '') - ); + url + .replace(settings.apiPath, '') + .replace(settings.internalApiPath || '', '') + .replace('http://localhost:3000', ''); + return result; } diff --git a/apps/remix/package.json b/apps/remix/package.json index 31d7ec65e8..4af4ff9935 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@plone/blocks": "workspace: *", + "@plone/slots": "workspace: *", "@plone/client": "workspace: *", "@plone/components": "workspace: *", "@plone/providers": "workspace: *", diff --git a/apps/remix/registry.config.ts b/apps/remix/registry.config.ts new file mode 100644 index 0000000000..15b9c3f5d2 --- /dev/null +++ b/apps/remix/registry.config.ts @@ -0,0 +1,4 @@ +const addons = ['@plone/blocks', '@plone/slots']; +const theme = ''; + +export { addons, theme }; diff --git a/apps/remix/remix.config.js b/apps/remix/remix.config.js deleted file mode 100644 index 812df9d051..0000000000 --- a/apps/remix/remix.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('@remix-run/dev').AppConfig} */ -export default { - ignoredRouteFiles: ['**/.*'], - // appDirectory: "app", - // assetsBuildDirectory: "public/build", - // publicPath: "/build/", - // serverBuildPath: "build/index.js", -}; diff --git a/apps/remix/vite.config.ts b/apps/remix/vite.config.ts index 03bfa30a99..d8694b0580 100644 --- a/apps/remix/vite.config.ts +++ b/apps/remix/vite.config.ts @@ -4,8 +4,41 @@ import tsconfigPaths from 'vite-tsconfig-paths'; import { PloneRegistryVitePlugin } from '@plone/registry/vite-plugin'; export default defineConfig({ + server: { + port: 3000, + proxy: { + '^/\\+\\+api\\+\\+($$|/.*)': { + target: + 'http://localhost:8080/VirtualHostBase/http/localhost:3000/Plone/++api++/VirtualHostRoot', + rewrite: (path) => { + console.log(path); + return path.replace('/++api++', ''); + }, + }, + }, + }, plugins: [ remix({ + routes: async (defineRoutes) => { + // If you need to do async work, do it before calling `defineRoutes`, we use + // the call stack of `route` inside to set nesting. + + return defineRoutes((route) => { + route('/', 'content.tsx', { id: 'index' }); + route('*', 'content.tsx', { id: 'splat' }); + // A common use for this is catchall routes. + // - The first argument is the React Router path to match against + // - The second is the relative filename of the route handler + // route("/some/path/*", "catchall.tsx"); + + // if you want to nest routes, use the optional callback argument + // route("some/:path", "some/route/file.js", () => { + // // - path is relative to parent path + // // - filenames are still relative to the app directory + // route("relative/path", "some/other/file"); + // }); + }); + }, future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, @@ -15,7 +48,4 @@ export default defineConfig({ tsconfigPaths(), PloneRegistryVitePlugin(), ], - server: { - port: 3000, - }, }); diff --git a/apps/rr7/.eslintrc.cjs b/apps/rr7/.eslintrc.cjs index b4a6a65b4d..d1480fca9f 100644 --- a/apps/rr7/.eslintrc.cjs +++ b/apps/rr7/.eslintrc.cjs @@ -38,6 +38,7 @@ module.exports = { react: { version: 'detect', }, + 'import/core-modules': ['@plone/registry/addons-loader'], formComponents: ['Form'], linkComponents: [ { name: 'Link', linkAttribute: 'to' }, diff --git a/apps/rr7/app/client.ts b/apps/rr7/app/client.ts deleted file mode 100644 index 0eec9cd62e..0000000000 --- a/apps/rr7/app/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import ploneClient from '@plone/client'; -import config from '@plone/registry'; - -const cli = ploneClient.initialize({ - apiPath: config.settings.apiPath, -}); - -export { cli as ploneClient }; diff --git a/apps/rr7/app/config.server.ts b/apps/rr7/app/config.server.ts new file mode 100644 index 0000000000..2d7c951f7d --- /dev/null +++ b/apps/rr7/app/config.server.ts @@ -0,0 +1,32 @@ +/** + * This is the server side config entry point + */ +import config from '@plone/registry'; +import ploneClient from '@plone/client'; +import applyAddonConfiguration from '@plone/registry/addons-loader'; + +export default function install() { + applyAddonConfiguration(config); + + config.settings.apiPath = + process.env.PLONE_API_PATH || 'http://localhost:3000'; + config.settings.internalApiPath = + process.env.PLONE_INTERNAL_API_PATH || undefined; + + const cli = ploneClient.initialize({ + apiPath: config.settings.internalApiPath || config.settings.apiPath, + }); + + config.registerUtility({ + name: 'ploneClient', + type: 'client', + method: () => cli, + }); + + console.log('API_PATH is:', config.settings.apiPath); + console.log( + 'INTERNAL_API_PATH is:', + config.settings.internalApiPath || 'not set', + ); + return config; +} diff --git a/apps/rr7/app/config.ts b/apps/rr7/app/config.ts index e7133efdce..7f2026e538 100644 --- a/apps/rr7/app/config.ts +++ b/apps/rr7/app/config.ts @@ -1,15 +1,11 @@ +/** + * This is the client side config entry point + */ import config from '@plone/registry'; -import { blocksConfig, slate } from '@plone/blocks'; +import applyAddonConfiguration from '@plone/registry/addons-loader'; -const settings = { - apiPath: 'http://localhost:3000', - slate, -}; - -// @ts-expect-error We need to fix typing -config.set('settings', settings); - -// @ts-expect-error We need to fix typing -config.set('blocks', { blocksConfig }); - -export default config; +export default function install() { + applyAddonConfiguration(config); + config.settings.apiPath = 'http://localhost:3000'; + return config; +} diff --git a/apps/rr7/app/routes/home.tsx b/apps/rr7/app/content.tsx similarity index 63% rename from apps/rr7/app/routes/home.tsx rename to apps/rr7/app/content.tsx index c470ad9f42..5433ba9fda 100644 --- a/apps/rr7/app/routes/home.tsx +++ b/apps/rr7/app/content.tsx @@ -1,4 +1,4 @@ -import type { LoaderArgs } from '../routes/+types.home'; +import type { LoaderArgs } from './routes/+types.home'; import { dehydrate, QueryClient, @@ -6,14 +6,13 @@ import { useQuery, useQueryClient, } from '@tanstack/react-query'; -import { flattenToAppURL } from '../utils'; import { useLoaderData, useLocation } from 'react-router'; +import type { MetaFunction } from 'react-router'; import { usePloneClient } from '@plone/providers'; -import { Breadcrumbs, RenderBlocks } from '@plone/components'; +import PloneClient from '@plone/client'; +import App from '@plone/slots/components/App'; +import { flattenToAppURL } from './utils'; import config from '@plone/registry'; -import { ploneClient } from '../client'; - -import type { MetaFunction } from 'react-router'; export const meta: MetaFunction = () => { return [ @@ -22,7 +21,7 @@ export const meta: MetaFunction = () => { ]; }; -const expand = ['breadcrumbs', 'navigation']; +const expand = ['navroot', 'breadcrumbs', 'navigation']; // eslint-disable-next-line @typescript-eslint/no-unused-vars export async function loader({ params, request }: LoaderArgs) { @@ -36,11 +35,28 @@ export async function loader({ params, request }: LoaderArgs) { }, }); - const { getContentQuery } = ploneClient; + const ploneClient = config + .getUtility({ + name: 'ploneClient', + type: 'client', + }) + .method(); - await queryClient.prefetchQuery( - getContentQuery({ path: flattenToAppURL(request.url), expand }), - ); + const { getContentQuery } = ploneClient as PloneClient; + + const path = flattenToAppURL(request.url); + if ( + !( + /^https?:\/\//.test(path) || + /^favicon.ico\/\//.test(path) || + /expand/.test(path) || + /^\/@@images/.test(path) || + /^\/@@download/.test(path) || + /^\/assets/.test(path) + ) + ) { + await queryClient.prefetchQuery(getContentQuery({ path, expand })); + } return { dehydratedState: dehydrate(queryClient) }; } @@ -51,20 +67,7 @@ function Page() { const { data } = useQuery(getContentQuery({ path: pathname, expand })); if (!data) return 'Loading...'; - return ( - <> - - - - ); + return ; } export default function Content() { diff --git a/apps/rr7/app/okroute.tsx b/apps/rr7/app/okroute.tsx new file mode 100644 index 0000000000..56472e1bb6 --- /dev/null +++ b/apps/rr7/app/okroute.tsx @@ -0,0 +1,5 @@ +export async function loader() { + return new Response(null, { + status: 200, + }); +} diff --git a/apps/rr7/app/root.tsx b/apps/rr7/app/root.tsx index 50808c5b17..d1c4bb5bae 100644 --- a/apps/rr7/app/root.tsx +++ b/apps/rr7/app/root.tsx @@ -7,8 +7,9 @@ import { ScrollRestoration, useHref, useLocation, - useNavigate, + useNavigate as useRRNavigate, useParams, + useLoaderData, } from 'react-router'; import type { LinksFunction } from 'react-router'; @@ -18,9 +19,18 @@ import PloneClient from '@plone/client'; import { PloneProvider } from '@plone/providers'; import { flattenToAppURL } from './utils'; import config from '@plone/registry'; -import './config'; +import install from './config'; +import installSSR from './config.server'; -import '@plone/components/dist/basic.css'; +install(); + +import '@plone/theming/styles/main.css'; +import '@plone/slots/main.css'; + +function useNavigate() { + const navigate = useRRNavigate(); + return (to: string) => navigate(flattenToAppURL(to)); +} function useHrefLocal(to: string) { return useHref(flattenToAppURL(to)); @@ -35,11 +45,24 @@ export const links: LinksFunction = () => [ }, { rel: 'stylesheet', - href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', + href: 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap', }, ]; +export async function loader() { + const ssrConfig = installSSR(); + + return { + env: { + PLONE_API_PATH: ssrConfig.settings.apiPath, + PLONE_INTERNAL_API_PATH: ssrConfig.settings.internalApiPath, + }, + }; +} + export function Layout({ children }: { children: React.ReactNode }) { + const data = useLoaderData(); + return ( @@ -51,6 +74,11 @@ export function Layout({ children }: { children: React.ReactNode }) { {children} + \ No newline at end of file diff --git a/packages/slots/.storybook/preview.ts b/packages/slots/.storybook/preview.ts new file mode 100644 index 0000000000..362843c9bc --- /dev/null +++ b/packages/slots/.storybook/preview.ts @@ -0,0 +1,24 @@ +import './storybook-base.css'; +import '@plone/components/dist/basic.css'; +import '../main.css'; +import config from '@plone/registry'; +import installSlots from '../config'; +import installBlocks from '@plone/blocks'; + +config.set('slots', {}); +config.set('utilities', {}); +installSlots(config); +installBlocks(config); + +export const parameters = { + backgrounds: { + default: 'light', + }, + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; diff --git a/packages/slots/.storybook/storybook-base.css b/packages/slots/.storybook/storybook-base.css new file mode 100644 index 0000000000..f55ba4bf27 --- /dev/null +++ b/packages/slots/.storybook/storybook-base.css @@ -0,0 +1,19 @@ +/* Base styles */ +:root { + --basic-font-family: system-ui; + --basic-font-size: 16px; + background: var(--background-color); + font-family: var(--basic-font-family); + font-size: var(--basic-font-size); + line-height: 1.5; +} + +.sbdocs.sbdocs-content { + p { + font-size: 16px; + } +} + +#storybook-root { + width: 100vw; +} diff --git a/packages/slots/.storybook/theme.ts b/packages/slots/.storybook/theme.ts new file mode 100644 index 0000000000..3262e1f700 --- /dev/null +++ b/packages/slots/.storybook/theme.ts @@ -0,0 +1,10 @@ +import { create } from '@storybook/theming/create'; +import logo from './Logo.svg'; + +export default create({ + base: 'light', + brandTitle: '@plone/components StoryBook', + brandUrl: 'https://plone-components.netlify.app/', + brandImage: logo, + brandTarget: '_self', +}); diff --git a/packages/slots/.stylelintrc b/packages/slots/.stylelintrc new file mode 100644 index 0000000000..8ac62f8d0f --- /dev/null +++ b/packages/slots/.stylelintrc @@ -0,0 +1,14 @@ +{ + "extends": ["stylelint-config-idiomatic-order"], + "plugins": ["stylelint-prettier"], + "overrides": [ + { + "files": ["**/*.scss"], + "customSyntax": "postcss-scss" + } + ], + "rules": { + "prettier/prettier": true, + "order/properties-alphabetical-order": null + } +} diff --git a/packages/slots/CHANGELOG.md b/packages/slots/CHANGELOG.md new file mode 100644 index 0000000000..1eb8ee8211 --- /dev/null +++ b/packages/slots/CHANGELOG.md @@ -0,0 +1,11 @@ +# @plone/slots Release Notes + + + + + +## 1.0.0 (unreleased) diff --git a/packages/slots/README.md b/packages/slots/README.md index c77e2bb2de..8dae40cad3 100644 --- a/packages/slots/README.md +++ b/packages/slots/README.md @@ -1,6 +1,6 @@ -# `@plone/blocks` +# `@plone/slots` -This package contains the default blocks provided by Plone. +This package provides default structural slots for Plone 7 and the API-first story. > [!WARNING] > This package or app is experimental. diff --git a/packages/slots/SlotRenderer.tsx b/packages/slots/SlotRenderer.tsx new file mode 100644 index 0000000000..df2501d721 --- /dev/null +++ b/packages/slots/SlotRenderer.tsx @@ -0,0 +1,58 @@ +import config from '@plone/registry'; +import type { GetSlotArgs } from '@plone/types'; + +export type SlotComponentProps = { + content: GetSlotArgs['content']; + location: GetSlotArgs['location']; + navRoot?: GetSlotArgs['navRoot']; +}; + +/* + * The SlotRenderer component is used to render the components registered for a + * given slot. + * + * Usage: + * + */ +const SlotRenderer = ({ + name, + content, + location, + navRoot, +}: { + name: string; + content: GetSlotArgs['content']; + location: GetSlotArgs['location']; + navRoot?: GetSlotArgs['navRoot']; +}) => { + let slots = config.getSlot(name, { + content, + location, + // This is to cover the use case while adding a new content and we don't have + // the navRoot information in the initial content. This will be + // useful for SlotRenderers rendered in the `Add` route. + navRoot: content?.['@components']?.navroot?.navroot || navRoot, + }); + + if (!slots) { + return null; + } + + return ( + <> + {slots.map(({ component, name }) => { + const SlotComponent = component; + return ( + + ); + })} + + ); +}; + +export default SlotRenderer; diff --git a/packages/slots/components/App.stories.tsx b/packages/slots/components/App.stories.tsx new file mode 100644 index 0000000000..6dce553dfb --- /dev/null +++ b/packages/slots/components/App.stories.tsx @@ -0,0 +1,61 @@ +import App from './App'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { storyData } from '../stories'; + +const meta = { + title: 'App', + component: App, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args: any) => , + args: { + content: { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + description: 'Welcome to Plone', + items: [], + blocks: { + ...storyData.blocks, + }, + blocks_layout: { + ...storyData.blocks_layout, + }, + '@components': { + navigation: { + items: [ + { + '@id': 'http://localhost:3000/Plone', + title: 'Home', + }, + { + '@id': 'http://localhost:3000/Plone/news', + title: 'News', + }, + { + '@id': 'http://localhost:3000/Plone/about', + title: 'About', + }, + ], + }, + navroot: { + // @ts-expect-error This is a test object, missing all content properties + navroot: { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + }, + }, + site: { + 'plone.site_title': 'Plone site', + }, + }, + }, + }, +}; diff --git a/packages/slots/components/App.tsx b/packages/slots/components/App.tsx new file mode 100644 index 0000000000..9f703a2be2 --- /dev/null +++ b/packages/slots/components/App.tsx @@ -0,0 +1,25 @@ +import type { GetSlotArgs } from '@plone/types'; +import SlotRenderer from '../SlotRenderer'; + +type AppProps = { + content: GetSlotArgs['content']; + location: GetSlotArgs['location']; +}; + +const App = (props: AppProps) => { + const { content, location } = props; + + return ( +
+
+ +
+ +
+ +
+
+ ); +}; + +export default App; diff --git a/packages/slots/components/ContentArea.tsx b/packages/slots/components/ContentArea.tsx new file mode 100644 index 0000000000..b4cd5dfc4a --- /dev/null +++ b/packages/slots/components/ContentArea.tsx @@ -0,0 +1,19 @@ +import RenderBlocks from '@plone/blocks/RenderBlocks/RenderBlocks'; +import { SlotComponentProps } from '../SlotRenderer'; +import config from '@plone/registry'; + +const ContentArea = (props: SlotComponentProps) => { + const { content } = props; + + return ( + <> + + + ); +}; + +export default ContentArea; diff --git a/packages/slots/components/Footer.tsx b/packages/slots/components/Footer.tsx new file mode 100644 index 0000000000..626340a642 --- /dev/null +++ b/packages/slots/components/Footer.tsx @@ -0,0 +1,21 @@ +import type { GetSlotArgs } from '@plone/types'; +import SlotRenderer from '../SlotRenderer'; + +type FooterProps = { + content: GetSlotArgs['content']; + location: GetSlotArgs['location']; +}; + +const Footer = (props: FooterProps) => { + const { content, location } = props; + + return ( + <> + + + + + ); +}; + +export default Footer; diff --git a/packages/slots/components/Header.stories.tsx b/packages/slots/components/Header.stories.tsx new file mode 100644 index 0000000000..d90e0f251f --- /dev/null +++ b/packages/slots/components/Header.stories.tsx @@ -0,0 +1,55 @@ +import Header from './Header'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Header', + component: Header, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args: any) =>
, + args: { + content: { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + description: 'Welcome to Plone', + items: [], + '@components': { + navigation: { + items: [ + { + '@id': 'http://localhost:3000/Plone', + title: 'Home', + }, + { + '@id': 'http://localhost:3000/Plone/news', + title: 'News', + }, + { + '@id': 'http://localhost:3000/Plone/about', + title: 'About', + }, + ], + }, + navroot: { + // @ts-expect-error This is a test object, missing all content properties + navroot: { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + }, + }, + site: { + 'plone.site_title': 'Plone site', + }, + }, + }, + }, +}; diff --git a/packages/slots/components/Header.tsx b/packages/slots/components/Header.tsx new file mode 100644 index 0000000000..1b2d743082 --- /dev/null +++ b/packages/slots/components/Header.tsx @@ -0,0 +1,28 @@ +import type { GetSlotArgs } from '@plone/types'; +import SlotRenderer from '../SlotRenderer'; +import { Container } from '@plone/components'; + +type HeaderProps = { + content: GetSlotArgs['content']; + location: GetSlotArgs['location']; +}; + +const Header = (props: HeaderProps) => { + const { content, location } = props; + + return ( + + + +
+ +
+
+ ); +}; + +export default Header; diff --git a/packages/slots/components/Logo.stories.tsx b/packages/slots/components/Logo.stories.tsx new file mode 100644 index 0000000000..93d2623f27 --- /dev/null +++ b/packages/slots/components/Logo.stories.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import Logo from './Logo'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Logo', + component: Logo, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args: any) => , + args: { + content: { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + description: 'Welcome to Plone', + items: [], + '@components': { + navroot: { + // @ts-expect-error This is a test object, missing all content properties + navroot: { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + }, + }, + site: { + 'plone.site_title': 'Plone site', + }, + }, + }, + }, +}; diff --git a/packages/slots/components/Logo.svg b/packages/slots/components/Logo.svg new file mode 100644 index 0000000000..5a7ba56902 --- /dev/null +++ b/packages/slots/components/Logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/slots/components/Logo.tsx b/packages/slots/components/Logo.tsx new file mode 100644 index 0000000000..d543931753 --- /dev/null +++ b/packages/slots/components/Logo.tsx @@ -0,0 +1,27 @@ +import { SlotComponentProps } from '../SlotRenderer'; +import { Link } from '@plone/components'; +import LogoImage from './Logo.svg'; +import config from '@plone/registry'; +import { messages } from './messages'; + +const Logo = (props: SlotComponentProps) => { + const { content } = props; + const intl: (id: string) => string = config.getUtility({ + name: 'translation', + type: 'factory', + }).method; + const navRootPath = content['@components'].navroot?.navroot?.['@id'] || '/'; + const site = content['@components'].site; + const siteTitle = site?.['plone.site_title'] || ''; + const logoUrl = site?.['plone.site_logo'] + ? site['plone.site_logo'] + : LogoImage; + + return ( + + {intl(messages.logoOf) + + ); +}; + +export default Logo; diff --git a/packages/slots/components/Main.tsx b/packages/slots/components/Main.tsx new file mode 100644 index 0000000000..454ed36d78 --- /dev/null +++ b/packages/slots/components/Main.tsx @@ -0,0 +1,22 @@ +import type { GetSlotArgs } from '@plone/types'; +import SlotRenderer from '../SlotRenderer'; +import { Container } from '@plone/components'; + +type MainProps = { + content: GetSlotArgs['content']; + location: GetSlotArgs['location']; +}; + +const Main = (props: MainProps) => { + const { content, location } = props; + + return ( + + + + + + ); +}; + +export default Main; diff --git a/packages/slots/components/MainFooter.tsx b/packages/slots/components/MainFooter.tsx new file mode 100644 index 0000000000..d97347f71e --- /dev/null +++ b/packages/slots/components/MainFooter.tsx @@ -0,0 +1,38 @@ +import { SlotComponentProps } from '../SlotRenderer'; +import { Container, Link } from '@plone/components'; +import Logo from './Logo'; + +const Footer = (props: SlotComponentProps) => { + const { content, location } = props; + const siteActions = content?.['@components']?.actions?.site_actions || []; + + return ( + + {/* TODO: i18n properly */} +
+ The Plone® Open Source CMS/WCM is © 2000-2024 by the Plone Foundation + and friends.
+ Distributed under the GNU GPL license. +
+
    + {siteActions?.length + ? siteActions.map((item) => ( +
  • + + {item?.title} + +
  • + )) + : null} +
+ + + + + Powered by Plone & Python + +
+ ); +}; + +export default Footer; diff --git a/packages/slots/components/Navigation.stories.tsx b/packages/slots/components/Navigation.stories.tsx new file mode 100644 index 0000000000..3aee92ac77 --- /dev/null +++ b/packages/slots/components/Navigation.stories.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import Navigation from './Navigation'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Navigation', + component: Navigation, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args: any) => , + args: { + content: { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + description: 'Welcome to Plone', + items: [], + '@components': { + navigation: { + items: [ + { + '@id': 'http://localhost:3000/Plone', + title: 'Plone site', + }, + { + '@id': 'http://localhost:3000/Plone/news', + title: 'News', + }, + { + '@id': 'http://localhost:3000/Plone/about', + title: 'About', + }, + ], + }, + }, + }, + }, +}; diff --git a/packages/slots/components/Navigation.tsx b/packages/slots/components/Navigation.tsx new file mode 100644 index 0000000000..afccd4ae93 --- /dev/null +++ b/packages/slots/components/Navigation.tsx @@ -0,0 +1,26 @@ +import type { SlotComponentProps } from '../SlotRenderer'; +import { Link } from '@plone/components'; + +type NavItem = { + '@id': string; + title: string; +}; + +const Navigation = (props: SlotComponentProps) => { + const { content } = props; + const navItems = content['@components'].navigation?.items || []; + + return ( + + ); +}; + +export default Navigation; diff --git a/packages/slots/components/Tools.tsx b/packages/slots/components/Tools.tsx new file mode 100644 index 0000000000..6718f028e1 --- /dev/null +++ b/packages/slots/components/Tools.tsx @@ -0,0 +1,6 @@ +const HeaderTools = (props) => { + const { content, location } = props; + return 'The tools'; +}; + +export default HeaderTools; diff --git a/packages/slots/components/messages.ts b/packages/slots/components/messages.ts new file mode 100644 index 0000000000..2e2e95fd7d --- /dev/null +++ b/packages/slots/components/messages.ts @@ -0,0 +1,5 @@ +export const messages = { + home: 'Home', + siteLogo: 'Site logo', + logoOf: 'Logo of', +}; diff --git a/packages/slots/index.ts b/packages/slots/index.ts new file mode 100644 index 0000000000..885e8d2778 --- /dev/null +++ b/packages/slots/index.ts @@ -0,0 +1,78 @@ +import type { ConfigType } from '@plone/registry'; +import App from './components/App'; +import Header from './components/Header'; +import Main from './components/Main'; +import Footer from './components/Footer'; +import Logo from './components/Logo'; +import Navigation from './components/Navigation'; +import HeaderTools from './components/Tools'; +import ContentArea from './components/ContentArea'; +import MainFooter from './components/MainFooter'; + +export default function install(config: ConfigType) { + // Translation factory + config.registerUtility({ + name: 'translation', + type: 'factory', + method: (id: string) => id, + }); + + // Main App Slot + config.registerSlotComponent({ name: 'App', slot: 'App', component: App }); + + // Header Slot + config.registerSlotComponent({ + name: 'Header', + slot: 'header', + component: Header, + }); + + // Logo + config.registerSlotComponent({ + name: 'Logo', + slot: 'logo', + component: Logo, + }); + + // Navigation + config.registerSlotComponent({ + name: 'Navigation', + slot: 'navigation', + component: Navigation, + }); + + // Tools + config.registerSlotComponent({ + name: 'Tools', + slot: 'headertools', + component: HeaderTools, + }); + + // Main Slot + config.registerSlotComponent({ + name: 'Main', + slot: 'main', + component: Main, + }); + + config.registerSlotComponent({ + name: 'contentArea', + slot: 'contentArea', + component: ContentArea, + }); + + // Footer Slot + config.registerSlotComponent({ + name: 'Footer', + slot: 'footer', + component: Footer, + }); + + config.registerSlotComponent({ + name: 'mainFooter', + slot: 'mainFooter', + component: MainFooter, + }); + + return config; +} diff --git a/packages/slots/main.css b/packages/slots/main.css new file mode 100644 index 0000000000..32062cf557 --- /dev/null +++ b/packages/slots/main.css @@ -0,0 +1,100 @@ +:root { + /* These should come from @plone/components by default */ + --layout-container-width: 1440px; + --default-container-width: 940px; + --narrow-container-width: 620px; + + --align-right: end; + --align-left: start; + --align-center: center; +} + +body { + /* This needs a proper reset */ + margin: 0; +} + +.app-slot { + display: grid; + height: 100vh; + margin: 0; + grid-template-areas: + 'header' + 'content' + 'footer'; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; +} + +.header-logo-nav-tools-wrapper { + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: space-between; + padding-top: 25px; + padding-bottom: 35px; +} + +.logo { + flex: 1 1 0; + align-self: center; +} + +.navigation { + display: flex; + flex: 3.5 1 0; + align-self: center; + justify-content: center; + + ul { + display: flex; + padding: 0; + margin: 0; + list-style: none; + + li a { + padding: 1rem 0; + margin-left: 1rem; + } + + li:first-child a { + margin-left: 0; + } + } +} + +.header-tools { + display: flex; + flex: 1 1 0; + flex-direction: row-reverse; +} + +main { + grid-area: 'content'; +} + +.content-area > * { + max-width: var(--default-container-width); + margin-right: auto; + margin-left: auto; +} + +figure img { + width: 100%; +} + +footer { + margin-top: 100px; +} + +.footer { + display: flex; + flex-direction: column; + padding: 2rem 0; + text-align: center; + + .logo { + margin: 20px 0; + place-self: center; + } +} diff --git a/packages/slots/news/.gitkeep b/packages/slots/news/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/slots/news/6409.internal b/packages/slots/news/6409.internal new file mode 100644 index 0000000000..5712bbc003 --- /dev/null +++ b/packages/slots/news/6409.internal @@ -0,0 +1 @@ +Initial commit, first steps. @sneridagh diff --git a/packages/slots/package.json b/packages/slots/package.json index 9766de2823..e6a8658cc0 100644 --- a/packages/slots/package.json +++ b/packages/slots/package.json @@ -28,13 +28,15 @@ "publishConfig": { "access": "public" }, - "main": "src/index.ts", + "main": "index.ts", "scripts": { "test": "vitest", "dry-release": "release-it --dry-run", "release": "release-it", "release-major-alpha": "release-it major --preRelease=alpha", - "release-alpha": "release-it --preRelease=alpha" + "release-alpha": "release-it --preRelease=alpha", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", @@ -46,16 +48,33 @@ } }, "dependencies": { - "@plone/providers": "workspace:*", - "@plone/registry": "workspace:*" + "@plone/blocks": "workspace:*", + "@plone/client": "workspace:*", + "@plone/components": "workspace:*", + "@plone/registry": "workspace:*", + "react-aria-components": "^1.4.0" }, "devDependencies": { "@plone/types": "workspace:*", + "@storybook/addon-essentials": "^8.0.4", + "@storybook/addon-interactions": "^8.0.4", + "@storybook/addon-links": "^8.0.4", + "@storybook/addon-mdx-gfm": "^8.0.4", + "@storybook/blocks": "^8.0.4", + "@storybook/manager-api": "^8.0.4", + "@storybook/react": "^8.0.4", + "@storybook/react-vite": "^8.0.4", + "@storybook/theming": "^8.0.4", "@types/react": "^18", "@types/react-dom": "^18", + "eslint-plugin-storybook": "^0.8.0", + "jest-axe": "^8.0.0", "release-it": "17.1.1", + "storybook": "^8.0.4", "tsconfig": "workspace:*", "typescript": "^5.6.3", - "vitest": "^2.1.3" + "vite": "^5.4.8", + "vitest": "^2.1.3", + "vitest-axe": "^0.1.0" } } diff --git a/packages/slots/setupTesting.ts b/packages/slots/setupTesting.ts new file mode 100644 index 0000000000..8bc87fa36e --- /dev/null +++ b/packages/slots/setupTesting.ts @@ -0,0 +1,3 @@ +import '@testing-library/jest-dom'; +import { toHaveNoViolations } from 'jest-axe'; +expect.extend(toHaveNoViolations); diff --git a/packages/slots/src/SlotRenderer.tsx b/packages/slots/src/SlotRenderer.tsx deleted file mode 100644 index a2567dee6f..0000000000 --- a/packages/slots/src/SlotRenderer.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { useRouterLocation } from '@plone/providers'; -import config from '@plone/registry'; - -import type { Content } from '@plone/types'; - -/* -Usage: - -*/ - -const SlotRenderer = ({ - name, - content, - navRoot, -}: { - name: string; - content: Content; - navRoot?: Content; -}) => { - const pathname = useRouterLocation().pathname; - - let slots = config.getSlot(name, { - content, - pathname, - // This is to cover the use case while adding a new content and we don't have - // have the navRoot information in the initial content. This will be - // useful for SlotRenderers rendered in the `Add` route. - navRoot: content?.['@components']?.navroot?.navroot || navRoot, - }); - - if (!slots) { - return null; - } - - return ( - <> - {slots.map( - ({ - component, - name, - }: { - component: React.ComponentType; - name: string; - }) => { - // ^^ Weird compilation issue for Jest tests, that forced to re-declare the type above - const SlotComponent = component; - return ( - - ); - }, - )} - - ); -}; - -export default SlotRenderer; diff --git a/packages/slots/src/index.ts b/packages/slots/src/index.ts deleted file mode 100644 index 6fe8d63809..0000000000 --- a/packages/slots/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SlotRenderer'; diff --git a/packages/slots/stories.ts b/packages/slots/stories.ts new file mode 100644 index 0000000000..6a668df163 --- /dev/null +++ b/packages/slots/stories.ts @@ -0,0 +1,22 @@ +export const storyData = { + blocks: { + '7ab29abe-b38c-406b-94d7-b270e544a998': { + '@type': 'slate', + value: [ + { + type: 'p', + children: [ + { + text: 'Lorem ipsum dolor sit amet eu tempus ornare elit. Curabitur egestas quisque molestie pellentesque nunc imperdiet posuere morbi nunc eleifend. Volutpat enim augue blandit aliquam interdum pulvinar eu mattis congue. Eleifend mauris ut fermentum egestas mi faucibus adipiscing arcu nibh scelerisque justo habitasse. Mi consectetur hac maecenas leo dictumst vitae phasellus quam praesent vivamus nullam imperdiet integer mauris.', + }, + ], + }, + ], + plaintext: + 'Lorem ipsum dolor sit amet eu tempus ornare elit. Curabitur egestas quisque molestie pellentesque nunc imperdiet posuere morbi nunc eleifend. Volutpat enim augue blandit aliquam interdum pulvinar eu mattis congue. Eleifend mauris ut fermentum egestas mi faucibus adipiscing arcu nibh scelerisque justo habitasse. Mi consectetur hac maecenas leo dictumst vitae phasellus quam praesent vivamus nullam imperdiet integer mauris.', + }, + }, + blocks_layout: { + items: ['7ab29abe-b38c-406b-94d7-b270e544a998'], + }, +}; diff --git a/packages/slots/towncrier.toml b/packages/slots/towncrier.toml new file mode 100644 index 0000000000..3ef721f378 --- /dev/null +++ b/packages/slots/towncrier.toml @@ -0,0 +1,33 @@ +[tool.towncrier] +filename = "CHANGELOG.md" +directory = "news/" +title_format = "## {version} ({project_date})" +underlines = ["", "", ""] +template = "../scripts/templates/towncrier_template.jinja" +start_string = "\n" +issue_format = "[#{issue}](https://github.com/plone/volto/issues/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Feature" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfix" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true diff --git a/packages/slots/tsconfig.json b/packages/slots/tsconfig.json index 27e48db56e..2965402840 100644 --- a/packages/slots/tsconfig.json +++ b/packages/slots/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "tsconfig/react-library.json", - "include": ["src", "src/**/*.js", "../providers/src/RouterLocation.tsx"], + "include": ["**/*.ts", "**/*.tsx"], "exclude": [ "node_modules", "build", diff --git a/packages/slots/vitest.config.ts b/packages/slots/vitest.config.ts new file mode 100644 index 0000000000..fddf5f61f7 --- /dev/null +++ b/packages/slots/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +// https://vitejs.dev/config/ +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: './setupTesting.ts', + // you might want to disable it, if you don't have tests that rely on CSS + // since parsing CSS is slow + css: true, + exclude: ['**/node_modules/**', '**/lib/**'], + }, +}); diff --git a/packages/theming/.release-it.json b/packages/theming/.release-it.json new file mode 100644 index 0000000000..708ed7fca5 --- /dev/null +++ b/packages/theming/.release-it.json @@ -0,0 +1,29 @@ +{ + "plugins": { + "../scripts/prepublish.js": {} + }, + "hooks": { + "after:bump": [ + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft", + "pipx run towncrier build --yes --version ${version}" + ], + "after:release": "rm .changelog.draft" + }, + "npm": { + "publish": false + }, + "git": { + "commitArgs": ["--no-verify"], + "changelog": "pipx run towncrier build --draft --yes --version 0.0.0", + "requireUpstream": false, + "requireCleanWorkingDir": false, + "commitMessage": "Release @plone/theming ${version}", + "tagName": "plone-theming-${version}", + "tagAnnotation": "Release @plone/theming ${version}" + }, + "github": { + "release": true, + "releaseName": "@plone/theming ${version}", + "releaseNotes": "cat .changelog.draft" + } +} diff --git a/packages/theming/.stylelintrc b/packages/theming/.stylelintrc new file mode 100644 index 0000000000..8ac62f8d0f --- /dev/null +++ b/packages/theming/.stylelintrc @@ -0,0 +1,14 @@ +{ + "extends": ["stylelint-config-idiomatic-order"], + "plugins": ["stylelint-prettier"], + "overrides": [ + { + "files": ["**/*.scss"], + "customSyntax": "postcss-scss" + } + ], + "rules": { + "prettier/prettier": true, + "order/properties-alphabetical-order": null + } +} diff --git a/packages/theming/CHANGELOG.md b/packages/theming/CHANGELOG.md new file mode 100644 index 0000000000..d3ab18563b --- /dev/null +++ b/packages/theming/CHANGELOG.md @@ -0,0 +1,11 @@ +# @plone/theming Release Notes + + + + + +## 1.0.0 (unreleased) diff --git a/packages/theming/README.md b/packages/theming/README.md new file mode 100644 index 0000000000..6d4db8dc8e --- /dev/null +++ b/packages/theming/README.md @@ -0,0 +1,8 @@ +# @plone/theming + +Base fundamental theming infrastructure for Plone 7 and the API-first story. + +> [!WARNING] +> This package or app is experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. diff --git a/packages/theming/index.ts b/packages/theming/index.ts new file mode 100644 index 0000000000..7258290153 --- /dev/null +++ b/packages/theming/index.ts @@ -0,0 +1,5 @@ +import type { ConfigType } from '@plone/registry'; + +export default function install(config: ConfigType) { + return config; +} diff --git a/packages/theming/news/.gitkeep b/packages/theming/news/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/theming/news/6409.internal b/packages/theming/news/6409.internal new file mode 100644 index 0000000000..5712bbc003 --- /dev/null +++ b/packages/theming/news/6409.internal @@ -0,0 +1 @@ +Initial commit, first steps. @sneridagh diff --git a/packages/theming/package.json b/packages/theming/package.json new file mode 100644 index 0000000000..688bd0231c --- /dev/null +++ b/packages/theming/package.json @@ -0,0 +1,79 @@ +{ + "name": "@plone/theming", + "description": "Plone base foundamental theming", + "maintainers": [ + { + "name": "Plone Foundation", + "url": "https://plone.org" + } + ], + "funding": "https://github.com/sponsors/plone", + "license": "MIT", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/plone/volto.git" + }, + "bugs": { + "url": "https://github.com/plone/volto/issues" + }, + "homepage": "https://plone.org", + "keywords": [ + "volto", + "plone", + "plone6", + "react", + "helpers" + ], + "publishConfig": { + "access": "public" + }, + "main": "index.ts", + "scripts": { + "test": "vitest", + "dry-release": "release-it --dry-run", + "release": "release-it", + "release-major-alpha": "release-it major --preRelease=alpha", + "release-alpha": "release-it --preRelease=alpha", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + }, + "dependencies": { + "@plone/components": "workspace:*", + "@plone/registry": "workspace:*", + "lightningcss": "1.28.1", + "lightningcss-cli": "1.28.1" + }, + "devDependencies": { + "@plone/types": "workspace:*", + "@storybook/addon-essentials": "^8.0.4", + "@storybook/addon-interactions": "^8.0.4", + "@storybook/addon-links": "^8.0.4", + "@storybook/addon-mdx-gfm": "^8.0.4", + "@storybook/blocks": "^8.0.4", + "@storybook/manager-api": "^8.0.4", + "@storybook/react": "^8.0.4", + "@storybook/react-vite": "^8.0.4", + "@storybook/theming": "^8.0.4", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint-plugin-storybook": "^0.8.0", + "jest-axe": "^8.0.0", + "release-it": "17.1.1", + "storybook": "^8.0.4", + "tsconfig": "workspace:*", + "typescript": "^5.6.3", + "vite": "^5.4.8", + "vitest": "^2.1.3", + "vitest-axe": "^0.1.0" + } +} diff --git a/packages/theming/styles/base.css b/packages/theming/styles/base.css new file mode 100644 index 0000000000..66bee507e3 --- /dev/null +++ b/packages/theming/styles/base.css @@ -0,0 +1,8 @@ +body { + font-family: var(--basic-font-family, system-ui); + font-size: var(--basic-font-size, 16px); +} + +figure { + margin: 0; +} diff --git a/packages/theming/styles/layout.css b/packages/theming/styles/layout.css new file mode 100644 index 0000000000..630985a3f3 --- /dev/null +++ b/packages/theming/styles/layout.css @@ -0,0 +1,3 @@ +/* body { + background-color: red; +} */ diff --git a/packages/theming/styles/main.css b/packages/theming/styles/main.css new file mode 100644 index 0000000000..c3db9e90b1 --- /dev/null +++ b/packages/theming/styles/main.css @@ -0,0 +1,5 @@ +@layer reset, plone-components, layout, addons, theme; +@import 'reset.css'; +@import 'base.css'; +@import '@plone/components/dist/basic.css'; +@import 'layout.css'; diff --git a/packages/theming/styles/reset.css b/packages/theming/styles/reset.css new file mode 100644 index 0000000000..31cdac9449 --- /dev/null +++ b/packages/theming/styles/reset.css @@ -0,0 +1,224 @@ +/* Borrowed from https://www.miriamsuzanne.com */ +@layer reset { + /* @docs + label: Core Remedies + version: 0.1.0-beta.2 + + note: | + These remedies are recommended + as a starter for any project. + + category: file + */ + /* @docs + label: Box Sizing + + note: | + Use border-box by default, globally. + + category: global + */ + *, + ::before, + ::after { + box-sizing: border-box; + } + /* @docs + label: Line Sizing + + note: | + Consistent line-spacing, + even when inline elements have different line-heights. + + links: + - https://drafts.csswg.org/css-inline-3/#line-sizing-property + + category: global + */ + html { + line-sizing: normal; + } + /* @docs + label: Body Margins + + note: | + Remove the tiny space around the edge of the page. + + category: global + */ + body { + margin: 0; + } + /* @docs + label: Heading Sizes + + note: | + Switch to rem units for headings + + category: typography + */ + h1 { + font-size: 2rem; + } + h2 { + font-size: 1.5rem; + } + h3 { + font-size: 1.17rem; + } + h4 { + font-size: 1rem; + } + h5 { + font-size: 0.83rem; + } + h6 { + font-size: 0.67rem; + } + /* @docs + label: H1 Margins + + note: | + Keep h1 margins consistent, even when nested. + + category: typography + */ + h1 { + margin: 0.67em 0; + } + /* @docs + label: Pre Wrapping + + note: | + Overflow by default is bad... + + category: typography + */ + pre { + white-space: pre-wrap; + } + /* @docs + label: Horizontal Rule + + note: | + 1. Solid, thin horizontal rules + 2. Remove Firefox `color: gray` + 3. Remove default `1px` height, and common `overflow: hidden` + + category: typography + */ + hr { + overflow: visible; + height: 0; + border-width: 1px 0 0; + border-style: solid; + color: inherit; + } + /* @docs + label: Responsive Embeds + + note: | + 1. Block display is usually what we want + 2. Remove strange space-below when inline + 3. Responsive by default + + category: embedded elements + */ + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; + max-width: 100%; + vertical-align: middle; + } + /* @docs + label: Aspect Ratios + + note: | + Maintain intrinsic aspect ratios when `max-width` is applied. + `iframe`, `embed`, and `object` are also embedded, + but have no intrinsic ratio, + so their `height` needs to be set explicitly. + + category: embedded elements + */ + img, + svg, + video, + canvas { + height: auto; + } + /* @docs + label: Audio Width + + note: | + There is no good reason elements default to 300px, + and audio files are unlikely to come with a width attribute. + + category: embedded elements + */ + audio { + width: 100%; + } + /* @docs + label: Image Borders + + note: | + Remove the border on images inside links in IE 10 and earlier. + + category: legacy browsers + */ + img { + border-style: none; + } + /* @docs + label: SVG Overflow + + note: | + Hide the overflow in IE 10 and earlier. + + category: legacy browsers + */ + svg { + overflow: hidden; + } + /* @docs + label: HTML5 Elements + + note: | + Default block display on HTML5 elements + + category: legacy browsers + */ + article, + aside, + figcaption, + figure, + footer, + header, + hgroup, + main, + nav, + section { + display: block; + } + /* @docs + label: Checkbox & Radio Inputs + + note: | + 1. Add the correct box sizing in IE 10 + 2. Remove the padding in IE 10 + + category: legacy browsers + */ + [type='checkbox'], + [type='radio'] { + box-sizing: border-box; + padding: 0; + } +} diff --git a/packages/theming/towncrier.toml b/packages/theming/towncrier.toml new file mode 100644 index 0000000000..3ef721f378 --- /dev/null +++ b/packages/theming/towncrier.toml @@ -0,0 +1,33 @@ +[tool.towncrier] +filename = "CHANGELOG.md" +directory = "news/" +title_format = "## {version} ({project_date})" +underlines = ["", "", ""] +template = "../scripts/templates/towncrier_template.jinja" +start_string = "\n" +issue_format = "[#{issue}](https://github.com/plone/volto/issues/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Feature" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfix" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true diff --git a/packages/theming/tsconfig.json b/packages/theming/tsconfig.json new file mode 100644 index 0000000000..4df9070106 --- /dev/null +++ b/packages/theming/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + + "strict": true, + "noImplicitOverride": true, + + "lib": ["es2022", "dom", "dom.iterable"], + "module": "preserve", + "noEmit": true, + + "jsx": "react-jsx", + + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + + "paths": {} + }, + "include": ["src/index.ts"], + "exclude": [ + "node_modules", + "build", + "public", + "coverage", + "src/**/*.test.{js,jsx,ts,tsx}", + "src/**/*.spec.{js,jsx,ts,tsx}", + "src/**/*.stories.{js,jsx,ts,tsx}" + ] +} diff --git a/packages/types/.release-it.json b/packages/types/.release-it.json index 11f1e08e9e..3db3e87c46 100644 --- a/packages/types/.release-it.json +++ b/packages/types/.release-it.json @@ -4,7 +4,8 @@ }, "hooks": { "after:bump": [ - "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft && pipx run towncrier build --yes --version ${version}" + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft", + "pipx run towncrier build --yes --version ${version}" ], "after:release": "rm .changelog.draft" }, diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 7ff1b32ec2..114cb17adf 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,4 +1,4 @@ -# @plone/volto-types Release Notes +# @plone/types Release Notes