This commit is contained in:
@@ -0,0 +1 @@
|
||||
tailwind.config.cjs
|
||||
@@ -0,0 +1,73 @@
|
||||
/* eslint-env node */
|
||||
const tailwindCssConfig = `${__dirname}/../admin/src/index.css`;
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'plugin:ghost/ts',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
plugins: [
|
||||
'ghost',
|
||||
'react-refresh',
|
||||
'tailwindcss'
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
},
|
||||
tailwindcss: {
|
||||
config: tailwindCssConfig
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false],
|
||||
|
||||
// sort multiple import lines into alphabetical groups
|
||||
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
|
||||
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
|
||||
}],
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@tryghost/shade',
|
||||
message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).'
|
||||
}]
|
||||
}],
|
||||
|
||||
// TODO: re-enable this (maybe fixed fast refresh?)
|
||||
'react-refresh/only-export-components': 'off',
|
||||
|
||||
// suppress errors for missing 'import React' in JSX files, as we don't need it
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// ignore prop-types for now
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// TODO: re-enable these if deemed useful
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
|
||||
// custom react rules
|
||||
'react/jsx-sort-props': ['error', {
|
||||
reservedFirst: true,
|
||||
callbacksLast: true,
|
||||
shorthandLast: true,
|
||||
locale: 'en'
|
||||
}],
|
||||
'react/button-has-type': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/jsx-key': 'off',
|
||||
|
||||
'tailwindcss/classnames-order': 'error',
|
||||
'tailwindcss/enforces-negative-arbitrary-values': 'warn',
|
||||
'tailwindcss/enforces-shorthand': 'warn',
|
||||
'tailwindcss/migration-from-tailwind-2': 'warn',
|
||||
'tailwindcss/no-arbitrary-value': 'off',
|
||||
'tailwindcss/no-custom-classname': 'off',
|
||||
'tailwindcss/no-contradicting-classname': 'error'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
types
|
||||
playwright-report
|
||||
test-results
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AdminX Standalone</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/standalone.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "@tryghost/activitypub",
|
||||
"version": "3.1.13",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TryGhost/Ghost/tree/main/apps/activitypub"
|
||||
},
|
||||
"author": "Ghost Foundation",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"dist/"
|
||||
],
|
||||
"main": "./dist/activitypub.umd.cjs",
|
||||
"module": "./dist/activitypub.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/activitypub.js",
|
||||
"require": "./dist/activitypub.umd.cjs"
|
||||
},
|
||||
"./api": "./src/index.tsx"
|
||||
},
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"dev:start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "pnpm run lint:code && pnpm run lint:test",
|
||||
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src",
|
||||
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test",
|
||||
"test": "pnpm test:unit",
|
||||
"test:unit": "tsc --noEmit && vitest run",
|
||||
"test:acceptance": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test",
|
||||
"test:acceptance:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:acceptance --headed",
|
||||
"test:acceptance:full": "ALL_BROWSERS=1 pnpm test:acceptance",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.59.1",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@types/dompurify": "3.2.0",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/react": "18.3.28",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"jest": "29.7.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"ts-jest": "29.4.9",
|
||||
"vite": "5.4.21",
|
||||
"vitest": "1.6.1"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:unit": {
|
||||
"dependsOn": [
|
||||
"^build",
|
||||
"test:unit"
|
||||
]
|
||||
},
|
||||
"test:acceptance": {
|
||||
"dependsOn": [
|
||||
"^build",
|
||||
"test:acceptance"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@radix-ui/react-form": "0.1.8",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"@tryghost/admin-x-framework": "workspace:*",
|
||||
"@tryghost/shade": "workspace:*",
|
||||
"clsx": "2.1.1",
|
||||
"dompurify": "3.3.1",
|
||||
"html2canvas-objectfit-fix": "1.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "7.72.1",
|
||||
"react-router": "7.14.0",
|
||||
"sonner": "2.0.7",
|
||||
"use-debounce": "10.1.1",
|
||||
"zod": "4.1.12"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright';
|
||||
|
||||
export default adminXPlaywrightConfig();
|
||||
@@ -0,0 +1,29 @@
|
||||
import {FeatureFlagsProvider} from './lib/feature-flags';
|
||||
import {FrameworkProvider, Outlet, RouterProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
|
||||
import {ShadeApp} from '@tryghost/shade/app';
|
||||
import {routes} from '@src/routes';
|
||||
|
||||
interface AppProps {
|
||||
framework: TopLevelFrameworkProps;
|
||||
activityPubEnabled?: boolean;
|
||||
}
|
||||
|
||||
const App: React.FC<AppProps> = ({framework, activityPubEnabled}) => {
|
||||
if (activityPubEnabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkProvider {...framework}>
|
||||
<RouterProvider prefix={'/'} routes={routes}>
|
||||
<FeatureFlagsProvider>
|
||||
<ShadeApp className="shade-activitypub" darkMode={false} fetchKoenigLexical={null}>
|
||||
<Outlet />
|
||||
</ShadeApp>
|
||||
</FeatureFlagsProvider>
|
||||
</RouterProvider>
|
||||
</FrameworkProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,7 @@
|
||||
import './styles/index.css';
|
||||
|
||||
export {default as AdminXApp} from './app';
|
||||
|
||||
export {routes} from './routes';
|
||||
export {FeatureFlagsProvider} from './lib/feature-flags';
|
||||
export {useNotificationsCountForUser} from './hooks/use-activity-pub-queries';
|
||||
@@ -0,0 +1,142 @@
|
||||
import AppError from '@components/layout/error';
|
||||
|
||||
import {Navigate, Outlet, RouteObject, lazyComponent} from '@tryghost/admin-x-framework';
|
||||
|
||||
const basePath = import.meta.env.VITE_TEST ? '' : 'activitypub';
|
||||
|
||||
export type CustomRouteObject = RouteObject & {
|
||||
pageTitle?: string;
|
||||
children?: CustomRouteObject[];
|
||||
showBackButton?: boolean;
|
||||
};
|
||||
|
||||
export const routes: CustomRouteObject[] = [
|
||||
{
|
||||
// Root route that defines the app's base path
|
||||
path: basePath,
|
||||
element: <Outlet />,
|
||||
errorElement: <AppError />, // This will catch all errors in child routes
|
||||
handle: 'activitypub-basepath',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="reader" />
|
||||
},
|
||||
{
|
||||
path: 'inbox',
|
||||
element: <Navigate to="../reader" replace />
|
||||
},
|
||||
{
|
||||
path: 'feed',
|
||||
element: <Navigate to="../notes" replace />
|
||||
},
|
||||
{
|
||||
path: 'reader',
|
||||
lazy: lazyComponent(() => import('./views/inbox')),
|
||||
pageTitle: 'Reader'
|
||||
},
|
||||
{
|
||||
path: 'reader/:postId',
|
||||
lazy: lazyComponent(() => import('./views/inbox')),
|
||||
pageTitle: 'Reader'
|
||||
},
|
||||
{
|
||||
path: 'notes',
|
||||
lazy: lazyComponent(() => import('./views/feed/feed')),
|
||||
pageTitle: 'Notes'
|
||||
},
|
||||
{
|
||||
path: 'notes/:postId',
|
||||
lazy: lazyComponent(() => import('./views/feed/note')),
|
||||
pageTitle: 'Note'
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
lazy: lazyComponent(() => import('./views/notifications')),
|
||||
pageTitle: 'Notifications'
|
||||
},
|
||||
{
|
||||
path: 'explore',
|
||||
lazy: lazyComponent(() => import('./views/explore')),
|
||||
pageTitle: 'Explore'
|
||||
},
|
||||
{
|
||||
path: 'explore/:topic',
|
||||
lazy: lazyComponent(() => import('./views/explore')),
|
||||
pageTitle: 'Explore'
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
lazy: lazyComponent(() => import('./views/profile')),
|
||||
pageTitle: 'Profile'
|
||||
},
|
||||
{
|
||||
path: 'profile/likes',
|
||||
lazy: lazyComponent(() => import('./views/profile')),
|
||||
pageTitle: 'Profile'
|
||||
},
|
||||
{
|
||||
path: 'profile/following',
|
||||
lazy: lazyComponent(() => import('./views/profile')),
|
||||
pageTitle: 'Profile'
|
||||
},
|
||||
{
|
||||
path: 'profile/followers',
|
||||
lazy: lazyComponent(() => import('./views/profile')),
|
||||
pageTitle: 'Profile'
|
||||
},
|
||||
{
|
||||
path: 'profile/:handle/:tab?',
|
||||
lazy: lazyComponent(() => import('./views/profile')),
|
||||
pageTitle: 'Profile'
|
||||
},
|
||||
{
|
||||
path: 'preferences',
|
||||
lazy: lazyComponent(() => import('./views/preferences')),
|
||||
pageTitle: 'Preferences'
|
||||
},
|
||||
{
|
||||
path: 'preferences/moderation',
|
||||
lazy: lazyComponent(() => import('./views/preferences/components/moderation')),
|
||||
pageTitle: 'Moderation',
|
||||
showBackButton: true
|
||||
},
|
||||
{
|
||||
path: 'preferences/bluesky-sharing',
|
||||
lazy: lazyComponent(() => import('./views/preferences/components/bluesky-sharing')),
|
||||
showBackButton: true
|
||||
},
|
||||
{
|
||||
path: 'welcome',
|
||||
lazy: lazyComponent(() => import('./components/layout/onboarding')),
|
||||
pageTitle: 'Welcome',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <Navigate to="1" replace />
|
||||
},
|
||||
{
|
||||
path: '1',
|
||||
lazy: lazyComponent(() => import('./components/layout/onboarding/step-1'))
|
||||
},
|
||||
{
|
||||
path: '2',
|
||||
lazy: lazyComponent(() => import('./components/layout/onboarding/step-2'))
|
||||
},
|
||||
{
|
||||
path: '3',
|
||||
lazy: lazyComponent(() => import('./components/layout/onboarding/step-3'))
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="1" replace />
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: lazyComponent(() => import('./components/layout/error'))
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
import './styles/index.css';
|
||||
import App from './app.tsx';
|
||||
import renderStandaloneApp from '@tryghost/admin-x-framework/test/render';
|
||||
|
||||
renderStandaloneApp(App, {});
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/ts-test'
|
||||
],
|
||||
rules: {
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"declarationDir": "./types",
|
||||
"emitDeclarationOnly": true,
|
||||
"tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client", "jest"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path aliases */
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@src/*": ["*"],
|
||||
"@assets/*": ["assets/*"],
|
||||
"@components/*": ["components/*"],
|
||||
"@hooks/*": ["hooks/*"],
|
||||
"@utils/*": ["utils/*"],
|
||||
"@views/*": ["views/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import adminXViteConfig from '@tryghost/admin-x-framework/vite';
|
||||
import pkg from './package.json';
|
||||
import {resolve} from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const GHOST_CARDS_PATH = resolve(__dirname, '../../ghost/core/core/frontend/src/cards');
|
||||
|
||||
const validateCardsDirectoryPlugin = (cardsPath) => {
|
||||
return {
|
||||
name: 'validate-cards-directory',
|
||||
buildStart() {
|
||||
const jsPath = resolve(cardsPath, 'js');
|
||||
const cssPath = resolve(cardsPath, 'css');
|
||||
|
||||
if (!fs.existsSync(cardsPath)) {
|
||||
throw new Error(`Ghost cards directory not found at: ${cardsPath}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(jsPath)) {
|
||||
throw new Error(`Ghost cards JS directory not found at: ${jsPath}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(cssPath)) {
|
||||
throw new Error(`Ghost cards CSS directory not found at: ${cssPath}`);
|
||||
}
|
||||
|
||||
const jsFiles = fs.readdirSync(jsPath).filter(f => f.endsWith('.js'));
|
||||
const cssFiles = fs.readdirSync(cssPath).filter(f => f.endsWith('.css'));
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
throw new Error(`No JavaScript files found in Ghost cards directory: ${jsPath}`);
|
||||
}
|
||||
|
||||
if (cssFiles.length === 0) {
|
||||
throw new Error(`No CSS files found in Ghost cards directory: ${cssPath}`);
|
||||
}
|
||||
|
||||
console.log(`✓ Found ${jsFiles.length} JS and ${cssFiles.length} CSS card files at: ${cardsPath}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default (function viteConfig() {
|
||||
const config = adminXViteConfig({
|
||||
packageName: pkg.name,
|
||||
entry: resolve(__dirname, 'src/index.tsx'),
|
||||
overrides: {
|
||||
test: {
|
||||
include: [
|
||||
'./test/unit/**/*',
|
||||
'./src/**/*.test.ts'
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@src': resolve(__dirname, './src'),
|
||||
'@assets': resolve(__dirname, './src/assets'),
|
||||
'@components': resolve(__dirname, './src/components'),
|
||||
'@hooks': resolve(__dirname, './src/hooks'),
|
||||
'@utils': resolve(__dirname, './src/utils'),
|
||||
'@views': resolve(__dirname, './src/views'),
|
||||
'@ghost-cards': GHOST_CARDS_PATH
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
validateCardsDirectoryPlugin(GHOST_CARDS_PATH)
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
const tailwindCssConfig = `${__dirname}/../admin/src/index.css`;
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'plugin:ghost/ts',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
plugins: [
|
||||
'ghost',
|
||||
'react-refresh',
|
||||
'tailwindcss'
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
},
|
||||
tailwindcss: {
|
||||
config: tailwindCssConfig
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// suppress errors for missing 'import React' in JSX files, as we don't need it
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// ignore prop-types for now
|
||||
'react/prop-types': 'off',
|
||||
|
||||
'react/jsx-sort-props': ['error', {
|
||||
reservedFirst: true,
|
||||
callbacksLast: true,
|
||||
shorthandLast: true,
|
||||
locale: 'en'
|
||||
}],
|
||||
'react/button-has-type': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/jsx-key': 'off',
|
||||
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false],
|
||||
|
||||
'tailwindcss/classnames-order': 'error',
|
||||
'tailwindcss/enforces-negative-arbitrary-values': 'warn',
|
||||
'tailwindcss/enforces-shorthand': 'warn',
|
||||
'tailwindcss/migration-from-tailwind-2': 'warn',
|
||||
'tailwindcss/no-arbitrary-value': 'off',
|
||||
'tailwindcss/no-custom-classname': 'off',
|
||||
'tailwindcss/no-contradicting-classname': 'error'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
es
|
||||
types
|
||||
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
import {create} from '@storybook/theming/create';
|
||||
|
||||
export default create({
|
||||
base: 'light',
|
||||
// Typography
|
||||
fontBase: '"Inter", sans-serif',
|
||||
fontCode: 'monospace',
|
||||
|
||||
brandTitle: 'AdminX Design System',
|
||||
brandUrl: 'https://ghost.org',
|
||||
brandImage: 'https://github.com/peterzimon/playground/assets/353959/c4358b4e-232f-4dba-8abb-adb3142ccd89',
|
||||
brandTarget: '_self',
|
||||
|
||||
//
|
||||
colorPrimary: '#30CF43',
|
||||
colorSecondary: '#15171A',
|
||||
|
||||
// UI
|
||||
appBg: '#ffffff',
|
||||
appContentBg: '#ffffff',
|
||||
appBorderColor: '#EBEEF0',
|
||||
appBorderRadius: 0,
|
||||
|
||||
// Text colors
|
||||
textColor: '#15171A',
|
||||
textInverseColor: '#ffffff',
|
||||
|
||||
// Toolbar default and active colors
|
||||
barTextColor: '#9E9E9E',
|
||||
barSelectedColor: '#15171A',
|
||||
barBg: '#ffffff',
|
||||
|
||||
// Form colors
|
||||
inputBg: '#ffffff',
|
||||
inputBorder: '#15171A',
|
||||
inputTextColor: '#15171A',
|
||||
inputBorderRadius: 2,
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
{
|
||||
name: '@storybook/addon-styling',
|
||||
},
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
async viteFinal(config, options) {
|
||||
config.resolve!.alias = {
|
||||
crypto: require.resolve('rollup-plugin-node-builtins')
|
||||
}
|
||||
return config;
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,6 @@
|
||||
import {addons} from '@storybook/manager-api';
|
||||
import adminxTheme from './adminx-theme';
|
||||
|
||||
addons.setConfig({
|
||||
theme: adminxTheme
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
|
||||
import '../styles.css';
|
||||
import './storybook.css';
|
||||
|
||||
import type { Preview } from "@storybook/react";
|
||||
import DesignSystemProvider from '../src/providers/design-system-provider';
|
||||
import adminxTheme from './adminx-theme';
|
||||
|
||||
// import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';
|
||||
|
||||
const customViewports = {
|
||||
sm: {
|
||||
name: 'sm',
|
||||
styles: {
|
||||
width: '480px',
|
||||
height: '801px',
|
||||
},
|
||||
},
|
||||
md: {
|
||||
name: 'md',
|
||||
styles: {
|
||||
width: '640px',
|
||||
height: '801px',
|
||||
},
|
||||
},
|
||||
lg: {
|
||||
name: 'lg',
|
||||
styles: {
|
||||
width: '1024px',
|
||||
height: '801px',
|
||||
},
|
||||
},
|
||||
xl: {
|
||||
name: 'xl',
|
||||
styles: {
|
||||
width: '1320px',
|
||||
height: '801px',
|
||||
},
|
||||
},
|
||||
tablet: {
|
||||
name: 'tablet',
|
||||
styles: {
|
||||
width: '860px',
|
||||
height: '801px',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
storySort: {
|
||||
method: 'alphabetical',
|
||||
order: ['Welcome', 'Foundations', ['Style Guide', 'Colors', 'Icons', 'ErrorHandling'], 'Global', ['Form', 'Chrome', 'Modal', 'Layout', ['View Container', 'Page Header', 'Page'], 'List', 'Table', '*'], 'Settings', ['Setting Section', 'Setting Group', '*'], 'Experimental'],
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
theme: adminxTheme,
|
||||
},
|
||||
viewport: {
|
||||
viewports: {
|
||||
...customViewports,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
let {scheme} = context.globals;
|
||||
|
||||
return (
|
||||
<div className={`admin-x-design-system admin-x-base ${scheme === 'dark' ? 'dark' : ''}`} style={{
|
||||
// padding: '24px',
|
||||
// width: 'unset',
|
||||
height: 'unset',
|
||||
// overflow: 'unset',
|
||||
background: (scheme === 'dark' ? '#131416' : '')
|
||||
}}>
|
||||
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
|
||||
<DesignSystemProvider fetchKoenigLexical={async () => {}}>
|
||||
<Story />
|
||||
</DesignSystemProvider>
|
||||
</div>);
|
||||
},
|
||||
],
|
||||
globalTypes: {
|
||||
scheme: {
|
||||
name: "Scheme",
|
||||
description: "Select light or dark mode",
|
||||
defaultValue: "light",
|
||||
toolbar: {
|
||||
icon: "mirror",
|
||||
items: ["light", "dark"],
|
||||
dynamicTitle: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* We load Inter in Ember admin, so loading it explicitly here makes the final rendering
|
||||
* in Storybook match the final rendering when embedded in Ember
|
||||
*/
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("./Inter.ttf") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-size: 62.5%;
|
||||
line-height: 1.5;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
letter-spacing: unset;
|
||||
}
|
||||
|
||||
.sbdocs-wrapper {
|
||||
padding: 3vmin !important;
|
||||
}
|
||||
|
||||
.sbdocs-wrapper .sbdocs-content {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
.sb-doc {
|
||||
max-width: 740px;
|
||||
width: 100%;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.sb-doc,
|
||||
.sb-doc a,
|
||||
.sb-doc h1,
|
||||
.sb-doc h2,
|
||||
.sb-doc h3,
|
||||
.sb-doc h4,
|
||||
.sb-doc h5,
|
||||
.sb-doc h6,
|
||||
.sb-doc p,
|
||||
.sb-doc ul li,
|
||||
.sbdocs-title,
|
||||
.sb-doc ol li {
|
||||
font-family: Inter, sans-serif !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.sb-doc a {
|
||||
color: #30CF43;
|
||||
}
|
||||
|
||||
.sb-doc h1 {
|
||||
font-size: 48px !important;
|
||||
letter-spacing: -0.04em !important;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sb-doc h2 {
|
||||
margin-top: 40px !important;
|
||||
font-size: 27px;
|
||||
border: none;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.sb-doc h3 {
|
||||
margin-top: 40px !important;
|
||||
margin-bottom: 4px !important;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sb-doc h4 {
|
||||
margin: 0 0 4px !important;
|
||||
}
|
||||
|
||||
.sb-doc p,
|
||||
.sb-doc div,
|
||||
.sb-doc ul li,
|
||||
.sb-doc ol li {
|
||||
font-size: 15px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.sb-doc ul li,
|
||||
.sb-doc ol li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sb-doc h2 + p,
|
||||
.sb-doc h3 + p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sb-doc img,
|
||||
.sb-wide img {
|
||||
margin-top: 40px !important;
|
||||
margin-bottom: 40px !important;
|
||||
}
|
||||
|
||||
.sb-doc img.small {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sb-doc p.excerpt {
|
||||
font-size: 19px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sb-doc .highlight {
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
background: #EBEEF0;
|
||||
}
|
||||
|
||||
.sb-doc .highlight.purple {
|
||||
background: #F0E9FA;
|
||||
}
|
||||
|
||||
.sb-doc .highlight.purple a {
|
||||
color: #8E42FF;
|
||||
}
|
||||
|
||||
/* Welcome */
|
||||
.sb-doc img.main-image {
|
||||
margin-top: -2vmin !important;
|
||||
margin-left: -44px;
|
||||
margin-right: -32px;
|
||||
margin-bottom: 0 !important;
|
||||
max-width: unset;
|
||||
width: calc(100% + 64px);
|
||||
}
|
||||
|
||||
.sb-doc .main-structure-container {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
margin: 32px 0 80px;
|
||||
}
|
||||
|
||||
.sb-doc .main-structure-container div {
|
||||
flex-basis: 33%;
|
||||
}
|
||||
|
||||
.sb-doc .main-structure-container div p {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sb-doc .main-structure-container img {
|
||||
margin: 12px 0 !important;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.sb-doc .main-structure-container div h4 {
|
||||
border-bottom: 1px solid #EBEEF0;
|
||||
padding-bottom: 8px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.sb-doc .main-structure-container div p {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
/* Colors */
|
||||
.color-grid {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-grid div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #EBEEF0;
|
||||
}
|
||||
|
||||
.color-grid .swatch {
|
||||
display: block;
|
||||
background: #EFEFEF;
|
||||
border-radius: 100%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.swatch.green {
|
||||
background: #30CF43;
|
||||
}
|
||||
|
||||
.swatch.black {
|
||||
background: #15171A;
|
||||
}
|
||||
|
||||
.swatch.white {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #EBEEF0;
|
||||
}
|
||||
|
||||
.swatch.lime {
|
||||
background: #B5FF18;
|
||||
}
|
||||
.swatch.blue {
|
||||
background: #14B8FF;
|
||||
}
|
||||
.swatch.purple {
|
||||
background: #8E42FF;
|
||||
}
|
||||
.swatch.pink {
|
||||
background: #FB2D8D;
|
||||
}
|
||||
.swatch.yellow {
|
||||
background: #FFB41F;
|
||||
}
|
||||
.swatch.red {
|
||||
background: #F50B23;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
|
||||
.sb-doc .streamline {
|
||||
display: grid;
|
||||
grid-template-columns: auto 240px;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.sbdocs-a {
|
||||
color: #30CF43 !important;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Admin X Design
|
||||
|
||||
Components, design guidelines and documentation for building apps in Ghost Admin
|
||||
|
||||
## Develop
|
||||
|
||||
This is a monorepo package.
|
||||
|
||||
Follow the instructions for the top-level repo.
|
||||
1. `git clone` this repo & `cd` into it as usual
|
||||
2. Run `pnpm` to install top-level dependencies.
|
||||
|
||||
## Test
|
||||
|
||||
- `pnpm lint` run just eslint
|
||||
- `pnpm test` run lint and tests
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"name": "@tryghost/admin-x-design-system",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/admin-x-design-system",
|
||||
"author": "Ghost Foundation",
|
||||
"private": true,
|
||||
"main": "es/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "tsc -p tsconfig.declaration.json && vite build",
|
||||
"test": "pnpm test:unit",
|
||||
"test:unit": "pnpm test:types && vitest run",
|
||||
"test:types": "tsc --noEmit",
|
||||
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
|
||||
"lint": "pnpm lint:code && pnpm lint:test",
|
||||
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"files": [
|
||||
"es",
|
||||
"types"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "8.6.14",
|
||||
"@storybook/addon-styling": "1.3.7",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
"@storybook/preview-api": "^8.6.14",
|
||||
"@storybook/react": "8.6.14",
|
||||
"@storybook/react-vite": "8.6.14",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/react": "18.3.28",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/validator": "13.15.10",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"autoprefixer": "10.4.21",
|
||||
"c8": "10.1.3",
|
||||
"chai": "4.5.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.24",
|
||||
"eslint-plugin-tailwindcss": "4.0.0-beta.0",
|
||||
"glob": "^10.5.0",
|
||||
"jsdom": "28.1.0",
|
||||
"lodash-es": "4.18.1",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-import": "16.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"rollup-plugin-node-builtins": "2.1.2",
|
||||
"sinon": "18.0.1",
|
||||
"storybook": "8.6.15",
|
||||
"tailwindcss": "4.2.1",
|
||||
"typescript": "5.9.3",
|
||||
"validator": "13.12.0",
|
||||
"vite": "5.4.21",
|
||||
"vite-plugin-svgr": "3.3.0",
|
||||
"vitest": "1.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
"@dnd-kit/sortable": "7.0.2",
|
||||
"@ebay/nice-modal-react": "1.2.13",
|
||||
"@radix-ui/react-avatar": "1.1.11",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
"@radix-ui/react-form": "0.1.8",
|
||||
"@radix-ui/react-popover": "1.1.15",
|
||||
"@radix-ui/react-radio-group": "1.3.8",
|
||||
"@radix-ui/react-separator": "1.1.8",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@sentry/react": "7.120.4",
|
||||
"@tryghost/shade": "workspace:*",
|
||||
"@uiw/react-codemirror": "4.25.2",
|
||||
"clsx": "2.1.1",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-hot-toast": "2.6.0",
|
||||
"react-select": "5.10.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:unit": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,381 @@
|
||||
.admin-x-base {
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box; /* 1 */
|
||||
max-width: revert;
|
||||
max-height: revert;
|
||||
min-width: revert;
|
||||
min-height: revert;
|
||||
border-width: 0; /* 2 */
|
||||
border-style: solid; /* 2 */
|
||||
border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-moz-tab-size: 4; /* 3 */
|
||||
tab-size: 4; /* 3 */
|
||||
font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0; /* 1 */
|
||||
line-height: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
border-top-width: 1px; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0; /* 1 */
|
||||
border-color: inherit; /* 2 */
|
||||
border-collapse: collapse; /* 3 */
|
||||
margin: 0;
|
||||
width: auto;
|
||||
max-width: auto;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
padding: unset;
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
line-height: auto;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
font-weight: inherit; /* 1 */
|
||||
line-height: inherit; /* 1 */
|
||||
color: inherit; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
padding: 0; /* 3 */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
letter-spacing: inherit;
|
||||
border-radius: inherit;
|
||||
appearance: auto;
|
||||
-webkit-appearance: auto;
|
||||
background: unset;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
/* [type='button'], */
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
background-color: transparent; /* 2 */
|
||||
background-image: none; /* 2 */
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: unset;
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1; /* 1 */
|
||||
@apply text-grey-500; /* 2 */
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block; /* 1 */
|
||||
vertical-align: middle; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import BoilerPlate from './boilerplate';
|
||||
|
||||
const meta = {
|
||||
title: 'Meta / Boilerplate',
|
||||
component: BoilerPlate,
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof BoilerPlate>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof BoilerPlate>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'This is a boilerplate component. Use as a basis to create new components.'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
interface BoilerPlateProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const BoilerPlate: React.FC<BoilerPlateProps> = ({children}) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoilerPlate;
|
||||
@@ -0,0 +1,26 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import {FetchKoenigLexical} from './global/form/html-editor';
|
||||
import DesignSystemProvider from './providers/design-system-provider';
|
||||
|
||||
export interface DesignSystemAppProps extends React.HTMLProps<HTMLDivElement> {
|
||||
darkMode: boolean;
|
||||
fetchKoenigLexical: FetchKoenigLexical;
|
||||
}
|
||||
|
||||
const DesignSystemApp: React.FC<DesignSystemAppProps> = ({darkMode, fetchKoenigLexical, className, children, ...props}) => {
|
||||
const appClassName = clsx(
|
||||
'admin-x-base',
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={appClassName} {...props}>
|
||||
<DesignSystemProvider darkMode={darkMode} fetchKoenigLexical={fetchKoenigLexical}>
|
||||
{children}
|
||||
</DesignSystemProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesignSystemApp;
|
||||
@@ -0,0 +1,177 @@
|
||||
export {ReactComponent as FacebookLogo} from './assets/images/facebook-logo.svg';
|
||||
export {ReactComponent as GhostLogo} from './assets/images/ghost-logo.svg';
|
||||
export {ReactComponent as GhostOrb} from './assets/images/ghost-orb.svg';
|
||||
export {ReactComponent as GoogleLogo} from './assets/images/google-logo.svg';
|
||||
export {ReactComponent as TwitterLogo} from './assets/images/twitter-logo.svg';
|
||||
export {ReactComponent as XLogo} from './assets/images/x-logo.svg';
|
||||
|
||||
export {default as DesktopChrome} from './global/chrome/desktop-chrome';
|
||||
export type {DesktopChromeProps} from './global/chrome/desktop-chrome';
|
||||
export {default as DesktopChromeHeader} from './global/chrome/desktop-chrome-header';
|
||||
export type {DesktopChromeHeaderProps} from './global/chrome/desktop-chrome-header';
|
||||
export {default as MobileChrome} from './global/chrome/mobile-chrome';
|
||||
export type {MobileChromeProps} from './global/chrome/mobile-chrome';
|
||||
|
||||
export {default as Checkbox} from './global/form/checkbox';
|
||||
export type {CheckboxProps} from './global/form/checkbox';
|
||||
export {default as CheckboxGroup} from './global/form/checkbox-group';
|
||||
export type {CheckboxGroupProps} from './global/form/checkbox-group';
|
||||
export {default as CodeEditor} from './global/form/code-editor';
|
||||
export type {CodeEditorProps, FetchKoenigLexical} from './global/form/code-editor';
|
||||
export {default as ColorIndicator} from './global/form/color-indicator';
|
||||
export type {ColorIndicatorProps} from './global/form/color-indicator';
|
||||
export {default as ColorPicker} from './global/form/color-picker';
|
||||
export type {ColorPickerProps} from './global/form/color-picker';
|
||||
export {default as ColorPickerField} from './global/form/color-picker-field';
|
||||
export type {ColorPickerFieldProps} from './global/form/color-picker-field';
|
||||
export {default as CurrencyField} from './global/form/currency-field';
|
||||
export type {CurrencyFieldProps} from './global/form/currency-field';
|
||||
export {default as FileUpload} from './global/form/file-upload';
|
||||
export type {FileUploadProps} from './global/form/file-upload';
|
||||
export {default as Form} from './global/form/form';
|
||||
export type {FormProps} from './global/form/form';
|
||||
export {default as HtmlEditor} from './global/form/html-editor';
|
||||
export type {HtmlEditorProps} from './global/form/html-editor';
|
||||
export {default as HtmlField} from './global/form/html-field';
|
||||
export type {HtmlFieldProps} from './global/form/html-field';
|
||||
export {default as KoenigEditorBase, loadKoenig} from './global/form/koenig-editor-base';
|
||||
export type {KoenigEditorBaseProps, KoenigInstance, NodeType} from './global/form/koenig-editor-base';
|
||||
export {default as ImageUpload} from './global/form/image-upload';
|
||||
export type {ImageUploadProps} from './global/form/image-upload';
|
||||
export {default as MultiSelect} from './global/form/multi-select';
|
||||
export type {LoadMultiSelectOptions, MultiSelectOption, MultiSelectProps} from './global/form/multi-select';
|
||||
export {default as Radio} from './global/form/radio';
|
||||
export type {RadioProps} from './global/form/radio';
|
||||
export {default as Select} from './global/form/select';
|
||||
export type {LoadSelectOptions, SelectOption, SelectOptionGroup, SelectProps} from './global/form/select';
|
||||
export {default as SelectWithOther} from './global/form/select-with-other';
|
||||
export type {SelectWithOtherProps} from './global/form/select-with-other';
|
||||
export {default as TextArea} from './global/form/text-area';
|
||||
export type {TextAreaProps} from './global/form/text-area';
|
||||
export {default as TextField} from './global/form/text-field';
|
||||
export type {TextFieldProps} from './global/form/text-field';
|
||||
export {default as Toggle} from './global/form/toggle';
|
||||
export type {ToggleProps} from './global/form/toggle';
|
||||
export {default as ToggleGroup} from './global/form/toggle-group';
|
||||
export type {ToggleGroupProps} from './global/form/toggle-group';
|
||||
export {default as URLTextField} from './global/form/url-text-field';
|
||||
export type {URLTextFieldProps} from './global/form/url-text-field';
|
||||
|
||||
export {default as ConfirmationModal, ConfirmationModalContent} from './global/modal/confirmation-modal';
|
||||
export type {ConfirmationModalProps} from './global/modal/confirmation-modal';
|
||||
export {default as LimitModal, LimitModalContent} from './global/modal/limit-modal';
|
||||
export type {LimitModalProps} from './global/modal/limit-modal';
|
||||
export {default as Modal, topLevelBackdropClasses} from './global/modal/modal';
|
||||
export type {ModalProps} from './global/modal/modal';
|
||||
export {default as ModalPage} from './global/modal/modal-page';
|
||||
export type {ModalPageProps} from './global/modal/modal-page';
|
||||
export {default as PreviewModal, PreviewModalContent} from './global/modal/preview-modal';
|
||||
export type {PreviewModalProps} from './global/modal/preview-modal';
|
||||
|
||||
export {default as Avatar} from './global/avatar';
|
||||
export type {AvatarProps} from './global/avatar';
|
||||
export {default as Banner} from './global/banner';
|
||||
export type {BannerProps} from './global/banner';
|
||||
export {default as Breadcrumbs} from './global/breadcrumbs';
|
||||
export type {BreadcrumbItem, BreadcrumbsProps} from './global/breadcrumbs';
|
||||
export {default as Button} from './global/button';
|
||||
export type {ButtonColor, ButtonProps} from './global/button';
|
||||
export {default as ButtonGroup} from './global/button-group';
|
||||
export type {ButtonGroupProps} from './global/button-group';
|
||||
export {default as ErrorBoundary, withErrorBoundary} from './global/error-boundary';
|
||||
export type {ErrorBoundaryProps} from './global/error-boundary';
|
||||
export {default as Heading} from './global/heading';
|
||||
export type {HeadingProps} from './global/heading';
|
||||
export {default as Hint} from './global/hint';
|
||||
export type {HintProps} from './global/hint';
|
||||
export {default as Icon} from './global/icon';
|
||||
export type {IconProps} from './global/icon';
|
||||
export {default as IconLabel} from './global/icon-label';
|
||||
export type {IconLabelProps} from './global/icon-label';
|
||||
export {default as InfiniteScrollListener} from './global/infinite-scroll-listener';
|
||||
export type {InfiniteScrollListenerProps} from './global/infinite-scroll-listener';
|
||||
export {default as Link} from './global/link';
|
||||
export type {LinkProps} from './global/link';
|
||||
export {default as List} from './global/list';
|
||||
export type {ListProps} from './global/list';
|
||||
export {default as ListHeading} from './global/list-heading';
|
||||
export type {ListHeadingProps} from './global/list-heading';
|
||||
export {default as ListItem} from './global/list-item';
|
||||
export type {ListItemProps} from './global/list-item';
|
||||
export {LoadingIndicator} from './global/loading-indicator';
|
||||
export type {LoadingIndicatorProps} from './global/loading-indicator';
|
||||
export {default as Menu} from './global/menu';
|
||||
export type {MenuItem, MenuProps} from './global/menu';
|
||||
export {default as NoValueLabel} from './global/no-value-label';
|
||||
export type {NoValueLabelProps} from './global/no-value-label';
|
||||
export {default as Pagination} from './global/pagination';
|
||||
export type {PaginationProps} from './global/pagination';
|
||||
export {default as Popover} from './global/popover';
|
||||
export type {PopoverProps} from './global/popover';
|
||||
export {default as Separator} from './global/separator';
|
||||
export type {SeparatorProps} from './global/separator';
|
||||
export {DragIndicator, default as SortableList} from './global/sortable-list';
|
||||
export type {DragIndicatorProps, SortableItemContainerProps, SortableListProps} from './global/sortable-list';
|
||||
export {default as SortMenu} from './global/sort-menu';
|
||||
export type {SortMenuProps} from './global/sort-menu';
|
||||
export {default as StickyFooter} from './global/sticky-footer';
|
||||
export type {StickyFooterProps} from './global/sticky-footer';
|
||||
export {default as TabView} from './global/tab-view';
|
||||
export type {Tab, TabViewProps} from './global/tab-view';
|
||||
export {default as Table} from './global/table';
|
||||
export type {ShowMoreData, TableProps} from './global/table';
|
||||
export {default as TableCell} from './global/table-cell';
|
||||
export type {TableCellProps} from './global/table-cell';
|
||||
export {default as TableHead} from './global/table-head';
|
||||
export type {TableHeadProps} from './global/table-head';
|
||||
export {default as TableRow} from './global/table-row';
|
||||
export type {TableRowProps} from './global/table-row';
|
||||
export {default as Toast, dismissAllToasts, showToast} from './global/toast';
|
||||
export type {ToastProps} from './global/toast';
|
||||
export {default as Tooltip} from './global/tooltip';
|
||||
export type {TooltipProps} from './global/tooltip';
|
||||
export {default as PageHeader} from './global/layout/page-header';
|
||||
export type {PageHeaderProps} from './global/layout/page-header';
|
||||
export {default as Page} from './global/layout/page';
|
||||
export type {PageTab} from './global/layout/page';
|
||||
export type {CustomGlobalAction} from './global/layout/page';
|
||||
export {default as ViewContainer} from './global/layout/view-container';
|
||||
export type {View} from './global/layout/view-container';
|
||||
export type {ViewTab} from './global/layout/view-container';
|
||||
export type {PrimaryActionProps} from './global/layout/view-container';
|
||||
export {default as DynamicTable} from './global/table/dynamic-table';
|
||||
export type {DynamicTableProps} from './global/table/dynamic-table';
|
||||
export type {DynamicTableColumn} from './global/table/dynamic-table';
|
||||
export type {DynamicTableRow} from './global/table/dynamic-table';
|
||||
|
||||
export {default as SettingGroup} from './settings/setting-group';
|
||||
export type {SettingGroupProps} from './settings/setting-group';
|
||||
export {default as SettingGroupContent} from './settings/setting-group-content';
|
||||
export type {SettingGroupContentProps} from './settings/setting-group-content';
|
||||
export {default as SettingGroupHeader} from './settings/setting-group-header';
|
||||
export type {SettingGroupHeaderProps} from './settings/setting-group-header';
|
||||
export {default as SettingNavItem} from './settings/setting-nav-item';
|
||||
export type {SettingNavItemProps} from './settings/setting-nav-item';
|
||||
export {default as SettingNavSection} from './settings/setting-nav-section';
|
||||
export type {SettingNavSectionProps} from './settings/setting-nav-section';
|
||||
export {default as SettingSection} from './settings/setting-section';
|
||||
export type {SettingSectionProps} from './settings/setting-section';
|
||||
export {default as SettingSectionHeader} from './settings/setting-section-header';
|
||||
export type {SettingSectionHeaderProps} from './settings/setting-section-header';
|
||||
export {default as SettingValue} from './settings/setting-value';
|
||||
export type {SettingValueProps} from './settings/setting-value';
|
||||
export {default as StripeButton} from './settings/stripe-button';
|
||||
export type {StripeButtonProps} from './settings/stripe-button';
|
||||
|
||||
export {default as useGlobalDirtyState} from './hooks/use-global-dirty-state';
|
||||
export {usePagination} from './hooks/use-pagination';
|
||||
export type {PaginationData} from './hooks/use-pagination';
|
||||
export {default as useSortableIndexedList} from './hooks/use-sortable-indexed-list';
|
||||
|
||||
export {debounce} from './utils/debounce';
|
||||
export {confirmIfDirty} from './utils/modals';
|
||||
export {formatUrl} from './utils/format-url';
|
||||
|
||||
export {default as DesignSystemApp} from './design-system-app';
|
||||
export type {DesignSystemAppProps} from './design-system-app';
|
||||
export {useFocusContext, useDesignSystem} from './providers/design-system-provider';
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
declare module '*.svg' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import React = require('react');
|
||||
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
@import './preflight.css';
|
||||
|
||||
@import 'tailwindcss/theme.css';
|
||||
@import '@tryghost/shade/tailwind.theme.css';
|
||||
|
||||
@import url(https://fonts.bunny.net/css?family=cardo:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=manrope:300,500,700);
|
||||
@import url(https://fonts.bunny.net/css?family=merriweather:300,700);
|
||||
@import url(https://fonts.bunny.net/css?family=nunito:400,600,700);
|
||||
@import url(https://fonts.bunny.net/css?family=old-standard-tt:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=prata:400);
|
||||
@import url(https://fonts.bunny.net/css?family=roboto:400,500,700);
|
||||
@import url(https://fonts.bunny.net/css?family=rufina:400,500,700);
|
||||
@import url(https://fonts.bunny.net/css?family=tenor-sans:400);
|
||||
@import url(https://fonts.bunny.net/css?family=space-grotesk:700);
|
||||
@import url(https://fonts.bunny.net/css?family=chakra-petch:400);
|
||||
@import url(https://fonts.bunny.net/css?family=noto-sans:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=poppins:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=fira-sans:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=inter:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=noto-serif:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=lora:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=ibm-plex-serif:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=space-mono:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=fira-mono:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=jetbrains-mono:400,700);
|
||||
|
||||
/* Defaults */
|
||||
@layer base {
|
||||
/* This just serves as a placeholder; we actually load Inter from a font file in Ember admin */
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: local("Inter") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
||||
.admin-x-base {
|
||||
& {
|
||||
@apply font-sans text-black text-base leading-normal;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
@apply font-bold tracking-tight leading-tighter;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl leading-supertight;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-md leading-supertight;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-md leading-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-x-base {
|
||||
line-height: 1.5;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
letter-spacing: unset;
|
||||
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Used to be for fixed bottom mobile menu bar
|
||||
@media (max-width: 800px) {
|
||||
.admin-x-base {
|
||||
height: calc(100vh - 55px);
|
||||
}
|
||||
} */
|
||||
|
||||
.dark .admin-x-base {
|
||||
color: #FAFAFB;
|
||||
}
|
||||
|
||||
.dark .admin-x-base .gh-loading-orb-container {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.dark .admin-x-base .gh-loading-orb {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.admin-x-base .no-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Chrome */
|
||||
}
|
||||
|
||||
.admin-x-base .no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Prose classes are for formatting arbitrary HTML that comes from the API */
|
||||
.gh-prose-links a {
|
||||
color: #30CF43;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
@import './preflight.css';
|
||||
|
||||
@import 'tailwindcss/theme.css';
|
||||
@import '@tryghost/shade/tailwind.theme.css';
|
||||
@import 'tailwindcss/utilities.css';
|
||||
|
||||
@import url(https://fonts.bunny.net/css?family=cardo:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=manrope:300,500,700);
|
||||
@import url(https://fonts.bunny.net/css?family=merriweather:300,700);
|
||||
@import url(https://fonts.bunny.net/css?family=nunito:400,600,700);
|
||||
@import url(https://fonts.bunny.net/css?family=old-standard-tt:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=prata:400);
|
||||
@import url(https://fonts.bunny.net/css?family=roboto:400,500,700);
|
||||
@import url(https://fonts.bunny.net/css?family=rufina:400,500,700);
|
||||
@import url(https://fonts.bunny.net/css?family=tenor-sans:400);
|
||||
@import url(https://fonts.bunny.net/css?family=space-grotesk:700);
|
||||
@import url(https://fonts.bunny.net/css?family=chakra-petch:400);
|
||||
@import url(https://fonts.bunny.net/css?family=noto-sans:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=poppins:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=fira-sans:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=inter:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=noto-serif:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=lora:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=ibm-plex-serif:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=space-mono:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=fira-mono:400,700);
|
||||
@import url(https://fonts.bunny.net/css?family=jetbrains-mono:400,700);
|
||||
|
||||
/* Defaults */
|
||||
@layer base {
|
||||
/* This just serves as a placeholder; we actually load Inter from a font file in Ember admin */
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: local("Inter") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
||||
.admin-x-base {
|
||||
& {
|
||||
@apply font-sans text-black text-base leading-normal;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
@apply font-bold tracking-tight leading-tighter;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl leading-supertight;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-md leading-supertight;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-md leading-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-x-base {
|
||||
line-height: 1.5;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
letter-spacing: unset;
|
||||
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Used to be for fixed bottom mobile menu bar
|
||||
@media (max-width: 800px) {
|
||||
.admin-x-base {
|
||||
height: calc(100vh - 55px);
|
||||
}
|
||||
} */
|
||||
|
||||
.dark .admin-x-base {
|
||||
color: #FAFAFB;
|
||||
}
|
||||
|
||||
.dark .admin-x-base .gh-loading-orb-container {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.dark .admin-x-base .gh-loading-orb {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.admin-x-base .no-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Chrome */
|
||||
}
|
||||
|
||||
.admin-x-base .no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Prose classes are for formatting arbitrary HTML that comes from the API */
|
||||
.gh-prose-links a {
|
||||
color: #30CF43;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test'
|
||||
],
|
||||
rules: {
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"declarationDir": "./types",
|
||||
"emitDeclarationOnly": true,
|
||||
"tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "package.json"]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import {globSync} from 'glob';
|
||||
import {resolve} from 'path';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default (function viteConfig() {
|
||||
return defineConfig({
|
||||
logLevel: process.env.CI ? 'info' : 'warn',
|
||||
plugins: [
|
||||
svgr(),
|
||||
react()
|
||||
],
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.VITEST_SEGFAULT_RETRY': 3
|
||||
},
|
||||
preview: {
|
||||
port: 4174
|
||||
},
|
||||
build: {
|
||||
reportCompressedSize: false,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
outDir: 'es',
|
||||
lib: {
|
||||
formats: ['es'],
|
||||
entry: globSync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, path) => {
|
||||
if (path.includes('.stories.') || path.endsWith('.d.ts')) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const outPath = path.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, '');
|
||||
entries[outPath] = path;
|
||||
return entries;
|
||||
}, {} as Record<string, string>)
|
||||
},
|
||||
commonjsOptions: {
|
||||
include: [/packages/, /node_modules/]
|
||||
},
|
||||
rollupOptions: {
|
||||
external: (source) => {
|
||||
if (source.startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source.includes('node_modules')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !source.includes(__dirname);
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
globals: true, // required for @testing-library/jest-dom extensions
|
||||
environment: 'jsdom',
|
||||
include: ['./test/unit/**/*'],
|
||||
testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000,
|
||||
...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674
|
||||
minThreads: 1,
|
||||
maxThreads: 2
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'plugin:ghost/ts',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
plugins: [
|
||||
'ghost',
|
||||
'react-refresh',
|
||||
'tailwindcss'
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// suppress errors for missing 'import React' in JSX files, as we don't need it
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// ignore prop-types for now
|
||||
'react/prop-types': 'off',
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@tryghost/shade',
|
||||
message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).'
|
||||
}]
|
||||
}],
|
||||
|
||||
'react/jsx-sort-props': ['error', {
|
||||
reservedFirst: true,
|
||||
callbacksLast: true,
|
||||
shorthandLast: true,
|
||||
locale: 'en'
|
||||
}],
|
||||
'react/button-has-type': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/jsx-key': 'off',
|
||||
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
types
|
||||
@@ -0,0 +1,22 @@
|
||||
# Admin X Framework
|
||||
|
||||
Ghost Shared Framework that is used by all the micro-frontends for common functionality like data fetching and routing.
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
- Run `pnpm` in Ghost monorepo root
|
||||
|
||||
## Develop
|
||||
|
||||
This is a monorepo package.
|
||||
|
||||
Follow the instructions for the top-level repo.
|
||||
1. `git clone` this repo & `cd` into it as usual
|
||||
2. Run `pnpm` to install top-level dependencies.
|
||||
|
||||
## Test
|
||||
|
||||
- `pnpm lint` - run just eslint
|
||||
- `pnpm test` - runs acceptance tests
|
||||
|
||||
In package.json you can find other related running options too.
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"name": "@tryghost/admin-x-framework",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"repository": "https://github.com/TryGhost/Ghost/tree/main/apps/admin-x-framework",
|
||||
"author": "Ghost Foundation",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./types/index.d.ts"
|
||||
},
|
||||
"./errors": {
|
||||
"import": "./dist/errors.js",
|
||||
"require": "./dist/errors.cjs",
|
||||
"types": "./types/errors.d.ts"
|
||||
},
|
||||
"./helpers": {
|
||||
"import": "./dist/helpers.js",
|
||||
"require": "./dist/helpers.cjs",
|
||||
"types": "./types/helpers.d.ts"
|
||||
},
|
||||
"./hooks": {
|
||||
"import": "./dist/hooks.js",
|
||||
"require": "./dist/hooks.cjs",
|
||||
"types": "./types/hooks.d.ts"
|
||||
},
|
||||
"./routing": {
|
||||
"import": "./dist/routing.js",
|
||||
"require": "./dist/routing.cjs",
|
||||
"types": "./types/routing.d.ts"
|
||||
},
|
||||
"./api/*": {
|
||||
"import": "./dist/api/*.js",
|
||||
"require": "./dist/api/*.cjs",
|
||||
"types": "./types/api/*.d.ts"
|
||||
},
|
||||
"./utils/post-utils": {
|
||||
"import": "./dist/utils/post-utils.js",
|
||||
"require": "./dist/utils/post-utils.cjs",
|
||||
"types": "./types/utils/post-utils.d.ts"
|
||||
},
|
||||
"./vite": {
|
||||
"import": "./dist/vite.js",
|
||||
"require": "./dist/vite.cjs",
|
||||
"types": "./types/vite.d.ts"
|
||||
},
|
||||
"./playwright": {
|
||||
"import": "./dist/playwright.js",
|
||||
"require": "./dist/playwright.cjs",
|
||||
"types": "./types/playwright.d.ts"
|
||||
},
|
||||
"./test/*": {
|
||||
"import": "./dist/test/*.js",
|
||||
"require": "./dist/test/*.cjs",
|
||||
"types": "./types/test/*.d.ts"
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "tsc -p tsconfig.declaration.json && vite build",
|
||||
"test": "pnpm test:types && pnpm test:unit",
|
||||
"test:types": "tsc --noEmit",
|
||||
"test:unit": "vitest run --coverage",
|
||||
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
|
||||
"lint": "pnpm lint:code && pnpm lint:test",
|
||||
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"types"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.59.1",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@tryghost/koenig-lexical": "1.7.30",
|
||||
"@types/react": "18.3.28",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"@vitest/coverage-v8": "^1.6.1",
|
||||
"c8": "10.1.3",
|
||||
"eslint": "catalog:",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.24",
|
||||
"glob": "^10.5.0",
|
||||
"jsdom": "28.1.0",
|
||||
"msw": "2.12.14",
|
||||
"sinon": "18.0.1",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "5.4.21",
|
||||
"vite-plugin-css-injected-by-js": "3.5.2",
|
||||
"vite-plugin-svgr": "3.3.0",
|
||||
"vitest": "1.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ebay/nice-modal-react": "1.2.13",
|
||||
"@sentry/react": "7.120.4",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"@tinybirdco/charts": "0.2.4",
|
||||
"@tryghost/admin-x-design-system": "workspace:*",
|
||||
"@tryghost/shade": "workspace:*",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hot-toast": "2.6.0",
|
||||
"react-router": "7.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:unit": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './utils/errors';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './utils/helpers';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export {default as useFilterableApi} from './hooks/use-filterable-api';
|
||||
export {default as useForm} from './hooks/use-form';
|
||||
export type {Dirtyable, ErrorMessages, FormHook, OkProps, SaveHandler, SaveState} from './hooks/use-form';
|
||||
export {default as useHandleError} from './hooks/use-handle-error';
|
||||
export {usePermission} from './hooks/use-permissions';
|
||||
export {useKoenigFileUpload, koenigFileUploadTypes} from './hooks/use-koenig-file-upload';
|
||||
export {useKoenigFetchEmbed} from './hooks/use-koenig-fetch-embed';
|
||||
export type {KoenigFileUploadType} from './hooks/use-koenig-file-upload';
|
||||
export {useKoenigLinkSuggestions} from './hooks/use-koenig-link-suggestions';
|
||||
export {usePinturaConfig} from './hooks/use-pintura-config';
|
||||
@@ -0,0 +1,58 @@
|
||||
// Framework
|
||||
export type {StatsConfig, FrameworkContextType, FrameworkProviderProps, TopLevelFrameworkProps} from './providers/framework-provider';
|
||||
export {FrameworkProvider, useFramework} from './providers/framework-provider';
|
||||
|
||||
// App Context
|
||||
export type {AppSettings, BaseAppProps, AppContextType, AppProviderProps} from './providers/app-provider';
|
||||
export {AppContext, AppProvider, useAppContext} from './providers/app-provider';
|
||||
|
||||
// Hooks
|
||||
export {useActiveVisitors} from './hooks/use-active-visitors';
|
||||
export {default as useForm} from './hooks/use-form';
|
||||
export type {Dirtyable, ErrorMessages, FormHook, OkProps, SaveHandler, SaveState} from './hooks/use-form';
|
||||
export {default as useHandleError} from './hooks/use-handle-error';
|
||||
export {default as useFilterableApi} from './hooks/use-filterable-api';
|
||||
export {useTinybirdToken} from './hooks/use-tinybird-token';
|
||||
export type {UseTinybirdTokenResult} from './hooks/use-tinybird-token';
|
||||
export {useTinybirdQuery} from './hooks/use-tinybird-query';
|
||||
export type {UseTinybirdQueryOptions} from './hooks/use-tinybird-query';
|
||||
export {useKoenigFileUpload, koenigFileUploadTypes} from './hooks/use-koenig-file-upload';
|
||||
export {useKoenigFetchEmbed} from './hooks/use-koenig-fetch-embed';
|
||||
export type {KoenigFileUploadType} from './hooks/use-koenig-file-upload';
|
||||
export {useKoenigLinkSuggestions} from './hooks/use-koenig-link-suggestions';
|
||||
|
||||
// Currency utilities
|
||||
export {getSymbol} from './utils/currency';
|
||||
|
||||
// Stats utilities
|
||||
export {getStatEndpointUrl, getToken} from './utils/stats-config';
|
||||
|
||||
// Post utilities
|
||||
export type {Post} from './api/posts';
|
||||
export {hasBeenEmailed} from './utils/post-utils';
|
||||
export {isEmailOnly, isPublishedOnly, isPublishedAndEmailed, getPostMetricsToDisplay} from './utils/post-helpers';
|
||||
export {focusKoenigEditorOnBottomClick} from './utils/focus-koenig-editor-on-bottom-click';
|
||||
|
||||
// Source utilities
|
||||
export {SOURCE_DOMAIN_MAP, getFaviconDomain, extractDomain, isDomainOrSubdomain, processSources, extendSourcesWithPercentages, normalizeSource} from './utils/source-utils';
|
||||
export type {BaseSourceData, ProcessedSourceData, ExtendSourcesOptions} from './utils/source-utils';
|
||||
|
||||
// Routing
|
||||
export type {RouteObject} from 'react-router';
|
||||
export type {RouterProviderProps, NavigateOptions} from './providers/router-provider';
|
||||
export {RouterProvider, useNavigate, useBaseRoute, useRouteHasParams, resetScrollPosition, ScrollRestoration, Navigate} from './providers/router-provider';
|
||||
export {useNavigationStack} from './providers/navigation-stack-provider';
|
||||
export {Link, NavLink, Outlet, useLocation, useParams, useSearchParams, redirect, matchRoutes, matchPath, useMatch, useMatches} from 'react-router';
|
||||
|
||||
// Lazy component loader
|
||||
export {lazyComponent} from './utils/lazy-component';
|
||||
|
||||
// Data fetching
|
||||
export type {InfiniteData} from '@tanstack/react-query';
|
||||
export {useQueryClient} from '@tanstack/react-query';
|
||||
|
||||
// API
|
||||
export type {TinybirdToken, TinybirdTokenResponseType} from './api/tinybird';
|
||||
export {getTinybirdToken} from './api/tinybird';
|
||||
export type {FeaturebaseToken, FeaturebaseTokenResponseType} from './api/featurebase';
|
||||
export {getFeaturebaseToken} from './api/featurebase';
|
||||
@@ -0,0 +1,62 @@
|
||||
import {defineConfig, devices, PlaywrightTestConfig} from '@playwright/test';
|
||||
|
||||
export const E2E_PORT = 5173;
|
||||
|
||||
export function adminXPlaywrightConfig(overrides: Partial<PlaywrightTestConfig> = {}) {
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
return defineConfig({
|
||||
testDir: './test/acceptance',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Hardcode to use all cores in CI */
|
||||
workers: process.env.CI ? '100%' : (process.env.PLAYWRIGHT_SLOWMO ? 1 : undefined),
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL: `http://localhost:${E2E_PORT}`,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
launchOptions: {
|
||||
slowMo: parseInt(process.env.PLAYWRIGHT_SLOWMO ?? '') || 0,
|
||||
// force GPU hardware acceleration
|
||||
// (even in headless mode)
|
||||
args: ['--use-gl=egl']
|
||||
}
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {...devices['Desktop Chrome']}
|
||||
},
|
||||
|
||||
...(process.env.ALL_BROWSERS ? [{
|
||||
name: 'firefox',
|
||||
use: {...devices['Desktop Firefox']}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {...devices['Desktop Safari']}
|
||||
}] : [])
|
||||
],
|
||||
|
||||
/* Run local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: `pnpm dev:start`,
|
||||
url: `http://localhost:${E2E_PORT}`,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 10000
|
||||
},
|
||||
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export {RoutingProvider, useRouteChangeCallback, useRouting} from './providers/routing-provider';
|
||||
export type {ExternalLink, InternalLink, ModalComponent, RoutingModalProps} from './providers/routing-provider';
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import {PluginOption, UserConfig, mergeConfig} from 'vite';
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
const externalPlugin = ({externals}: { externals: Record<string, string> }): PluginOption => {
|
||||
return {
|
||||
name: 'external-globals',
|
||||
apply: 'build',
|
||||
enforce: 'pre',
|
||||
resolveId(id) {
|
||||
if (Object.keys(externals).includes(id)) {
|
||||
// Naming convention for IDs that will be resolved by a plugin
|
||||
return `\0${id}`;
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
const [originalId, externalName] = Object.entries(externals).find(([key]) => id === `\0${key}`) || [];
|
||||
|
||||
if (originalId) {
|
||||
const module = await import(originalId);
|
||||
|
||||
return Object.keys(module).map(key => (key === 'default' ? `export default ${externalName};` : `export const ${key} = ${externalName}.${key};`)).join('\n');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default function adminXViteConfig({packageName, entry, overrides}: {packageName: string; entry: string; overrides?: UserConfig}) {
|
||||
const outputFileName = packageName[0] === '@' ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
|
||||
|
||||
const defaultConfig = defineConfig({
|
||||
logLevel: process.env.CI ? 'info' : 'warn',
|
||||
plugins: [
|
||||
svgr(),
|
||||
react(),
|
||||
externalPlugin({
|
||||
externals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM'
|
||||
}
|
||||
}),
|
||||
cssInjectedByJsPlugin() as PluginOption // Cast to avoid type conflicts
|
||||
],
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.VITEST_SEGFAULT_RETRY': 3,
|
||||
'import.meta.env.GHOST_BUILD_VERSION': JSON.stringify(process.env.GHOST_BUILD_VERSION || '')
|
||||
},
|
||||
preview: {
|
||||
port: 4174
|
||||
},
|
||||
build: {
|
||||
reportCompressedSize: false,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
formats: ['es'],
|
||||
entry,
|
||||
name: packageName,
|
||||
fileName(format) {
|
||||
if (format === 'umd') {
|
||||
return `${outputFileName}.umd.js`;
|
||||
}
|
||||
|
||||
return `${outputFileName}.js`;
|
||||
}
|
||||
},
|
||||
commonjsOptions: {
|
||||
include: [/packages/, /node_modules/]
|
||||
}
|
||||
},
|
||||
test: {
|
||||
globals: true, // required for @testing-library/jest-dom extensions
|
||||
environment: 'jsdom',
|
||||
include: ['./test/unit/**/*'],
|
||||
testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000,
|
||||
...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674
|
||||
minThreads: 1,
|
||||
maxThreads: 2
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return mergeConfig(defaultConfig, overrides || {});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/ts-test'
|
||||
],
|
||||
rules: {
|
||||
'ghost/mocha/no-mocha-arrows': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
/// <reference types="vitest/globals" />
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// This file ensures TypeScript knows about vitest globals
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"declarationDir": "./types",
|
||||
"emitDeclarationOnly": true,
|
||||
"tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client", "vitest/globals"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "test"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "package.json"]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import path from 'path';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import {globSync} from 'glob';
|
||||
import {resolve} from 'path';
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default (function viteConfig() {
|
||||
return defineConfig({
|
||||
logLevel: process.env.CI ? 'info' : 'warn',
|
||||
plugins: [
|
||||
react()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
port: 4174
|
||||
},
|
||||
build: {
|
||||
reportCompressedSize: false,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
outDir: 'dist',
|
||||
lib: {
|
||||
formats: ['es', 'cjs'],
|
||||
entry: globSync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, libpath) => {
|
||||
if (libpath.endsWith('.d.ts')) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const outPath = libpath.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, '');
|
||||
entries[outPath] = libpath;
|
||||
return entries;
|
||||
}, {} as Record<string, string>)
|
||||
},
|
||||
commonjsOptions: {
|
||||
include: [/packages/, /node_modules/]
|
||||
},
|
||||
rollupOptions: {
|
||||
external: (source) => {
|
||||
if (source.startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source.includes('node_modules')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !source.includes(__dirname);
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
globals: true, // required for @testing-library/jest-dom extensions
|
||||
environment: 'jsdom',
|
||||
include: ['./test/unit/**/*'],
|
||||
setupFiles: ['./test/setup.ts'],
|
||||
testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000,
|
||||
...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674
|
||||
minThreads: 1,
|
||||
maxThreads: 2
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
tailwind.config.cjs
|
||||
@@ -0,0 +1,159 @@
|
||||
/* eslint-env node */
|
||||
const tailwindCssConfig = `${__dirname}/../admin/src/index.css`;
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'plugin:ghost/ts',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
plugins: [
|
||||
'ghost',
|
||||
'react-refresh',
|
||||
'tailwindcss'
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
},
|
||||
tailwindcss: {
|
||||
config: tailwindCssConfig
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// ----------------------
|
||||
// Rules COPIED from base config, remove these when the config is fixed
|
||||
|
||||
// Style Rules
|
||||
// Require 4 spaces
|
||||
indent: ['error', 4],
|
||||
// Require single quotes for strings & properties (allows template literals)
|
||||
quotes: ['error', 'single', {allowTemplateLiterals: true}],
|
||||
'quote-props': ['error', 'as-needed'],
|
||||
// Require semi colons, always at the end of a line
|
||||
semi: ['error', 'always'],
|
||||
'semi-style': ['error', 'last'],
|
||||
// Don't allow dangling commas
|
||||
'comma-dangle': ['error', 'never'],
|
||||
// Always require curly braces, and position them at the end and beginning of lines
|
||||
curly: 'error',
|
||||
'brace-style': ['error', '1tbs'],
|
||||
// Don't allow padding inside of blocks
|
||||
'padded-blocks': ['error', 'never'],
|
||||
// Require objects to be consistently formatted with newlines
|
||||
'object-curly-newline': ['error', {consistent: true}],
|
||||
// Don't allow more than 1 consecutive empty line or an empty 1st line
|
||||
'no-multiple-empty-lines': ['error', {max: 1, maxBOF: 0}],
|
||||
// Variables must be camelcase, but properties are not checked
|
||||
camelcase: ['error', {properties: 'never'}],
|
||||
// Allow newlines before dots, not after e.g. .then goes on a new line
|
||||
'dot-location': ['error', 'property'],
|
||||
// Prefer dot notation over array notation
|
||||
'dot-notation': ['error'],
|
||||
|
||||
// Spacing rules
|
||||
// Don't allow multiple spaces anywhere
|
||||
'no-multi-spaces': 'error',
|
||||
// Anonymous functions have a sape, named functions never do
|
||||
'space-before-function-paren': ['error', {anonymous: 'always', named: 'never'}],
|
||||
// Don't put spaces inside of objects or arrays
|
||||
'object-curly-spacing': ['error', 'never'],
|
||||
'array-bracket-spacing': ['error', 'never'],
|
||||
// Allow a max of one space between colons and values
|
||||
'key-spacing': ['error', {mode: 'strict'}],
|
||||
// Require spaces before and after keywords like if, else, try, catch etc
|
||||
'keyword-spacing': 'error',
|
||||
// No spaces around semis
|
||||
'semi-spacing': 'error',
|
||||
// 1 space around arrows
|
||||
'arrow-spacing': 'error',
|
||||
// Don't allow spaces inside parenthesis
|
||||
'space-in-parens': ['error', 'never'],
|
||||
// Require single spaces either side of operators
|
||||
'space-unary-ops': 'error',
|
||||
'space-infix-ops': 'error',
|
||||
|
||||
// Best practice rules
|
||||
// Require === / !==
|
||||
eqeqeq: ['error', 'always'],
|
||||
// Don't allow ++ and --
|
||||
'no-plusplus': ['error', {allowForLoopAfterthoughts: true}],
|
||||
// Don't allow eval
|
||||
'no-eval': 'error',
|
||||
// Throw errors for unnecessary usage of .call or .apply
|
||||
'no-useless-call': 'error',
|
||||
// Don't allow console.* calls
|
||||
'no-console': 'error',
|
||||
// Prevent [variable shadowing](https://en.wikipedia.org/wiki/Variable_shadowing)
|
||||
'no-shadow': ['error'],
|
||||
|
||||
// Return rules
|
||||
// Prevent missing return statements in array functions like map & reduce
|
||||
'array-callback-return': 'error',
|
||||
'no-constructor-return': 'error',
|
||||
'no-promise-executor-return': 'error',
|
||||
|
||||
// Arrow function styles
|
||||
// Do not enforce single lines when using arrow functions.
|
||||
// https://eslint.org/docs/rules/arrow-body-style
|
||||
'arrow-body-style': 'off',
|
||||
'arrow-parens': ['error', 'as-needed', {requireForBlockBody: true}],
|
||||
'implicit-arrow-linebreak': 'error',
|
||||
'no-confusing-arrow': 'error',
|
||||
|
||||
// ----------------------
|
||||
// Rules NOT COPIED from base config, keep these
|
||||
|
||||
// sort multiple import lines into alphabetical groups
|
||||
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
|
||||
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
|
||||
}],
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@tryghost/shade',
|
||||
message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).'
|
||||
}]
|
||||
}],
|
||||
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false],
|
||||
|
||||
// TODO: enable this when we have the time to retroactively go and fix the issues
|
||||
'prefer-const': 'off',
|
||||
|
||||
// TODO: re-enable this (maybe fixed fast refresh?)
|
||||
'react-refresh/only-export-components': 'off',
|
||||
|
||||
// suppress errors for missing 'import React' in JSX files, as we don't need it
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// ignore prop-types for now
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// TODO: re-enable this because otherwise we're just skirting TS
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
|
||||
// TODO: re-enable these if deemed useful
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
|
||||
// custom react rules
|
||||
'react/jsx-sort-props': ['error', {
|
||||
reservedFirst: true,
|
||||
callbacksLast: true,
|
||||
shorthandLast: true,
|
||||
locale: 'en'
|
||||
}],
|
||||
'react/button-has-type': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/jsx-key': 'off',
|
||||
|
||||
'tailwindcss/classnames-order': 'error',
|
||||
'tailwindcss/enforces-negative-arbitrary-values': 'warn',
|
||||
'tailwindcss/enforces-shorthand': 'warn',
|
||||
'tailwindcss/migration-from-tailwind-2': 'warn',
|
||||
'tailwindcss/no-arbitrary-value': 'off',
|
||||
'tailwindcss/no-custom-classname': 'off',
|
||||
'tailwindcss/no-contradicting-classname': 'error'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
# Admin X Settings
|
||||
|
||||
Ghost Admin Settings micro-frontend.
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
- Run `pnpm` in Ghost monorepo root
|
||||
|
||||
## Running the app
|
||||
|
||||
### Running the development version
|
||||
|
||||
Run `pnpm dev` (in this package folder) to start the development server to test/develop the settings standalone. This will generate a demo site from the `index.html` file which renders the app and makes it available on http://localhost:5173
|
||||
|
||||
### Running inside Admin
|
||||
|
||||
Run `pnpm dev` from the top-level repo. This starts all frontend apps via Docker backend + host dev servers, and AdminX will automatically rebuild when you make changes.
|
||||
|
||||
## Develop
|
||||
|
||||
This is a monorepo package.
|
||||
|
||||
Follow the instructions for the top-level repo.
|
||||
1. `git clone` this repo & `cd` into it as usual
|
||||
2. Run `pnpm` to install top-level dependencies.
|
||||
|
||||
## Test
|
||||
|
||||
- `pnpm lint` - run just eslint
|
||||
- `pnpm test:acceptance` - runs acceptance tests
|
||||
- `pnpm test:unit` - runs unit tests
|
||||
- `pnpm test:acceptance path/to/test` - runs a specific test
|
||||
- `pnpm test:acceptance:slowmo` - runs acceptance tests in slow motion and headed mode, useful for debugging and developing tests
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Settings - Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* This is used by vite to resolve node builtins. See resolve.alias in vite.config.js
|
||||
*/
|
||||
module.exports = {};
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "@tryghost/admin-x-settings",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TryGhost/Ghost/tree/main/packages/admin-x-settings"
|
||||
},
|
||||
"author": "Ghost Foundation",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"dist/"
|
||||
],
|
||||
"main": "./dist/admin-x-settings.umd.cjs",
|
||||
"module": "./dist/admin-x-settings.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/admin-x-settings.js",
|
||||
"require": "./dist/admin-x-settings.umd.cjs",
|
||||
"types": "./src/index.tsx"
|
||||
},
|
||||
"./src/*": {
|
||||
"import": "./src/*.tsx",
|
||||
"require": "./src/*.tsx"
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"dev:start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "pnpm run lint:js",
|
||||
"lint:js": "eslint --ext .js,.ts,.cjs,.tsx --cache src test",
|
||||
"test": "pnpm test:unit",
|
||||
"test:unit": "vitest run --config vitest.config.ts",
|
||||
"test:acceptance": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' playwright test",
|
||||
"test:acceptance:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:acceptance --headed",
|
||||
"test:acceptance:full": "ALL_BROWSERS=1 pnpm test:acceptance",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@dnd-kit/sortable": "7.0.2",
|
||||
"@ebay/nice-modal-react": "1.2.13",
|
||||
"@sentry/react": "7.120.4",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"@tryghost/color-utils": "0.2.16",
|
||||
"@tryghost/i18n": "workspace:*",
|
||||
"@tryghost/kg-unsplash-selector": "0.3.26",
|
||||
"@tryghost/limit-service": "1.5.2",
|
||||
"@tryghost/nql": "0.12.10",
|
||||
"@tryghost/timezone-data": "0.4.18",
|
||||
"@uiw/react-codemirror": "4.25.2",
|
||||
"clsx": "2.1.1",
|
||||
"lucide-react": "0.577.0",
|
||||
"mingo": "2.5.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hot-toast": "2.6.0",
|
||||
"react-select": "5.10.2",
|
||||
"sonner": "2.0.7",
|
||||
"validator": "13.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.59.1",
|
||||
"@testing-library/jest-dom": "^6",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@tryghost/admin-x-design-system": "workspace:*",
|
||||
"@tryghost/admin-x-framework": "workspace:*",
|
||||
"@tryghost/custom-fonts": "1.0.8",
|
||||
"@tryghost/shade": "workspace:*",
|
||||
"@types/node": "22.19.17",
|
||||
"@types/react": "18.3.28",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/validator": "13.15.10",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.24",
|
||||
"eslint-plugin-tailwindcss": "4.0.0-beta.0",
|
||||
"stylelint": "15.11.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vite": "5.4.21",
|
||||
"vite-plugin-css-injected-by-js": "3.5.2",
|
||||
"vite-plugin-svgr": "3.3.0",
|
||||
"vitest": "1.6.1"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"dev": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:acceptance": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright';
|
||||
|
||||
export default adminXPlaywrightConfig();
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@tryghost/admin-x-design-system/postcss.config.cjs');
|
||||
@@ -0,0 +1,38 @@
|
||||
import MainContent from './main-content';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import SettingsAppProvider, {type UpgradeStatusType} from './components/providers/settings-app-provider';
|
||||
import SettingsRouter, {loadModals, modalPaths} from './components/providers/settings-router';
|
||||
import {DesignSystemApp, type DesignSystemAppProps} from '@tryghost/admin-x-design-system';
|
||||
import {FrameworkProvider, type TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
|
||||
import {RoutingProvider} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
interface AppProps {
|
||||
designSystem: DesignSystemAppProps;
|
||||
upgradeStatus?: UpgradeStatusType;
|
||||
}
|
||||
|
||||
export function App({designSystem, upgradeStatus}: AppProps) {
|
||||
return (
|
||||
<SettingsAppProvider upgradeStatus={upgradeStatus}>
|
||||
{/* NOTE: we need to have an extra NiceModal.Provider here because the one inside DesignSystemApp
|
||||
is loaded too late for possible modals in RoutingProvider, and it's quite hard to change it at
|
||||
this point */}
|
||||
<NiceModal.Provider>
|
||||
<RoutingProvider basePath='settings' modals={{paths: modalPaths, load: loadModals}}>
|
||||
<DesignSystemApp className='admin-x-settings' {...designSystem}>
|
||||
<SettingsRouter />
|
||||
<MainContent />
|
||||
</DesignSystemApp>
|
||||
</RoutingProvider>
|
||||
</NiceModal.Provider>
|
||||
</SettingsAppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function StandaloneApp({framework, designSystem, upgradeStatus}: AppProps & {framework: TopLevelFrameworkProps}) {
|
||||
return (
|
||||
<FrameworkProvider {...framework}>
|
||||
<App designSystem={designSystem} upgradeStatus={upgradeStatus} />
|
||||
</FrameworkProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import './styles/index.css';
|
||||
import {StandaloneApp} from './app.tsx';
|
||||
|
||||
export {
|
||||
StandaloneApp as AdminXApp
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import ExitSettingsButton from './components/exit-settings-button';
|
||||
import Settings from './components/settings';
|
||||
import Sidebar from './components/sidebar';
|
||||
import Users from './components/settings/general/users';
|
||||
import {Heading, confirmIfDirty, topLevelBackdropClasses, useGlobalDirtyState} from '@tryghost/admin-x-design-system';
|
||||
import {type ReactNode, useEffect} from 'react';
|
||||
import {canAccessSettings, isEditorUser} from '@tryghost/admin-x-framework/api/users';
|
||||
import {toast} from 'react-hot-toast';
|
||||
import {useGlobalData} from './components/providers/global-data-provider';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
const EMPTY_KEYWORDS: string[] = [];
|
||||
|
||||
const Page: React.FC<{children: ReactNode}> = ({children}) => {
|
||||
return <>
|
||||
<div className='fixed top-2 right-0 z-50 m-8 flex justify-end bg-transparent tablet:fixed tablet:top-0' id="done-button-container">
|
||||
<ExitSettingsButton />
|
||||
</div>
|
||||
<div className="fixed top-0 left-0 flex size-full dark:bg-grey-975" id="admin-x-settings-content">
|
||||
{children}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
const MainContent: React.FC = () => {
|
||||
const {currentUser} = useGlobalData();
|
||||
const {loadingModal} = useRouting();
|
||||
const {isDirty} = useGlobalDirtyState();
|
||||
|
||||
const navigateAway = (escLocation: string) => {
|
||||
window.location.hash = escLocation;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
// Don't navigate away if a modal is open - let the modal handle ESC
|
||||
const modalBackdrop = document.getElementById('modal-backdrop');
|
||||
if (modalBackdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
confirmIfDirty(isDirty, () => {
|
||||
navigateAway('/');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isDirty]);
|
||||
|
||||
useEffect(() => {
|
||||
// resets any toasts that may have been left open on initial load
|
||||
toast.remove();
|
||||
}, []);
|
||||
|
||||
// Contributors/Authors only see their profile modal (rendered via routing)
|
||||
// Don't render the main settings content for them
|
||||
if (!canAccessSettings(currentUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEditorUser(currentUser)) {
|
||||
return (
|
||||
<Page>
|
||||
<div className='flex-1 overflow-y-auto bg-white dark:bg-grey-975' id="admin-x-settings-scroller">
|
||||
<div className='mx-auto max-w-5xl px-[5vmin] tablet:mt-16 xl:mt-10'>
|
||||
<Heading className='mb-[5vmin]'>Settings</Heading>
|
||||
<Users highlight={false} keywords={EMPTY_KEYWORDS} />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{loadingModal && <div className={`fixed inset-0 z-40 h-[calc(100vh-55px)] w-[100vw] tablet:h-[100vh] ${topLevelBackdropClasses}`} />}
|
||||
<div className="fixed inset-x-0 top-0 z-[35] max-w-[calc(100%-16px)] flex-1 basis-[320px] bg-white p-8 tablet:relative tablet:inset-x-auto tablet:top-auto tablet:h-full tablet:overflow-y-scroll tablet:bg-grey-50 tablet:py-0 dark:bg-grey-975 dark:tablet:bg-[#101114]" id="admin-x-settings-sidebar-scroller">
|
||||
<div className="relative w-full">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-full flex-1 overflow-y-scroll bg-white pt-12 tablet:basis-[800px] dark:bg-grey-975 dark:tablet:bg-black" id="admin-x-settings-scroller">
|
||||
<Settings />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainContent;
|
||||
@@ -0,0 +1,6 @@
|
||||
import './styles/index.css';
|
||||
import renderStandaloneApp from '@tryghost/admin-x-framework/test/render';
|
||||
import {StandaloneApp} from './app.tsx';
|
||||
|
||||
renderStandaloneApp(StandaloneApp, {
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
declare module '@tryghost/limit-service'
|
||||
declare module '@tryghost/nql'
|
||||
|
||||
declare module '*.svg' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import React = require('react');
|
||||
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,5 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import {setupShadeMocks} from '@tryghost/admin-x-framework/test/setup';
|
||||
|
||||
// Set up common mocks for shade components
|
||||
setupShadeMocks();
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"declarationDir": "./types",
|
||||
"emitDeclarationOnly": true,
|
||||
"tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["node", "vitest/globals", "@testing-library/jest-dom/vitest"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@src/*": ["./src/*"],
|
||||
"@test/*": ["./test/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import adminXViteConfig from '@tryghost/admin-x-framework/vite';
|
||||
import pkg from './package.json';
|
||||
import {resolve} from 'path';
|
||||
import {createRequire} from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default (function viteConfig() {
|
||||
return adminXViteConfig({
|
||||
packageName: pkg.name,
|
||||
entry: resolve(__dirname, 'src/index.tsx'),
|
||||
overrides: {
|
||||
define: {
|
||||
'process.env.DEBUG': false // Shim env var utilized by the @tryghost/nql package
|
||||
},
|
||||
resolve: {
|
||||
// Shim node modules utilized by the @tryghost/nql package
|
||||
alias: {
|
||||
'@src': resolve(__dirname, 'src'),
|
||||
'@test': resolve(__dirname, 'test'),
|
||||
fs: 'node-shim.cjs',
|
||||
path: 'node-shim.cjs',
|
||||
util: 'node-shim.cjs',
|
||||
// @TODO: Remove this when @tryghost/nql is updated
|
||||
mingo: require.resolve('mingo/dist/mingo.js')
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['@tryghost/kg-unsplash-selector', '@tryghost/custom-fonts']
|
||||
}
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
include: [/ghost\/custom-fonts/]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import {createVitestConfig} from '@tryghost/admin-x-framework/test/vitest-config';
|
||||
|
||||
export default createVitestConfig();
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,27 @@
|
||||
# Ghost Admin (React)
|
||||
|
||||
New React-based Ghost admin interface, gradually replacing the existing Ember admin.
|
||||
|
||||
## Architecture
|
||||
|
||||
Uses an **Ember Bridge** system for smooth migration:
|
||||
- Routes ported to React render React components
|
||||
- Unported routes fall back to the existing Ember admin
|
||||
- Both share the same UI space seamlessly
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Start development server (from monorepo root)
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Building for Production
|
||||
|
||||
```bash
|
||||
# Build production bundle
|
||||
pnpm nx run @tryghost/admin:build
|
||||
```
|
||||
|
||||
This outputs to `apps/admin/dist/` and updates the assets in `ghost/core/core/built/admin/`.
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tailwindcss from 'eslint-plugin-tailwindcss'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths'
|
||||
import ghostPlugin from 'eslint-plugin-ghost';
|
||||
|
||||
const noHardcodedGhostPaths = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow hardcoded /ghost/ paths that break subdirectory installations',
|
||||
},
|
||||
messages: {
|
||||
noHardcodedPath: 'Do not hardcode /ghost/ paths. Use getGhostPaths() from @tryghost/admin-x-framework/helpers to support subdirectory installations.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const pattern = /^\/ghost\//;
|
||||
return {
|
||||
Literal(node) {
|
||||
if (typeof node.value === 'string' && pattern.test(node.value)) {
|
||||
context.report({node, messageId: 'noHardcodedPath'});
|
||||
}
|
||||
},
|
||||
TemplateLiteral(node) {
|
||||
const first = node.quasis[0];
|
||||
if (first && pattern.test(first.value.raw)) {
|
||||
context.report({node, messageId: 'noHardcodedPath'});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const localPlugin = {
|
||||
rules: {
|
||||
'no-hardcoded-ghost-paths': noHardcodedGhostPaths,
|
||||
},
|
||||
};
|
||||
const tailwindCssConfig = `${import.meta.dirname}/src/index.css`;
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
plugins: {
|
||||
'no-relative-import-paths': noRelativeImportPaths,
|
||||
ghost: ghostPlugin,
|
||||
local: localPlugin,
|
||||
tailwindcss,
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
settings: {
|
||||
tailwindcss: {
|
||||
config: tailwindCssConfig,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false],
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@tryghost/shade',
|
||||
message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).',
|
||||
}],
|
||||
}],
|
||||
'tailwindcss/classnames-order': 'error',
|
||||
'tailwindcss/no-contradicting-classname': 'error',
|
||||
},
|
||||
},
|
||||
// Apply no-relative-import-paths rule for src files (auto-fix supported)
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-relative-import-paths/no-relative-import-paths': [
|
||||
'error',
|
||||
{ allowSameFolder: true, rootDir: 'src', prefix: '@' },
|
||||
],
|
||||
},
|
||||
},
|
||||
// Prevent hardcoded /ghost/ paths in production code (not tests, where mocks need fixed paths)
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
ignores: ['src/**/*.test.*'],
|
||||
rules: {
|
||||
'local/no-hardcoded-ghost-paths': 'error',
|
||||
},
|
||||
},
|
||||
// Apply no-relative-import-paths rule for test-utils files
|
||||
// Note: auto-fix may produce incorrect paths for cross-directory imports
|
||||
// Use the correct alias manually: @/* for src/, @test-utils/* for test-utils/
|
||||
{
|
||||
files: ['test-utils/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-relative-import-paths/no-relative-import-paths': [
|
||||
'error',
|
||||
{ allowSameFolder: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<title>Ghost</title>
|
||||
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui, viewport-fit=cover" />
|
||||
<meta name="pinterest" content="nopin" />
|
||||
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="application-name" content="Ghost" />
|
||||
<meta name="apple-mobile-web-app-title" content="Ghost" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</head>
|
||||
<body class="react-admin">
|
||||
<div id="ember-alerts-wormhole"></div>
|
||||
<div id="root"></div>
|
||||
<div id="ember-app">
|
||||
<div class="ember-load-indicator">
|
||||
<div class="gh-loading-content">
|
||||
<video width="100" height="100" loop autoplay muted playsinline preload="metadata" style="width: 100px; height: 100px;">
|
||||
<source src="/src/assets/videos/logo-loader.mp4" type="video/mp4" />
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ember-basic-dropdown-wormhole"></div>
|
||||
<div id="ember-modal-wormhole"></div>
|
||||
<div id="ember-liquid-wormhole"></div>
|
||||
<div id="ember-notifications-wormhole"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"name": "@tryghost/admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "pnpm test:unit",
|
||||
"test:unit": "vitest run",
|
||||
"typecheck": "tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/activitypub": "workspace:*",
|
||||
"@tryghost/admin-x-framework": "workspace:*",
|
||||
"@tryghost/admin-x-settings": "workspace:*",
|
||||
"@tryghost/koenig-lexical": "1.7.30",
|
||||
"@tryghost/posts": "workspace:*",
|
||||
"@tryghost/shade": "workspace:*",
|
||||
"@tryghost/stats": "workspace:*",
|
||||
"mingo": "2.5.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"zod": "4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "catalog:eslint9",
|
||||
"@tailwindcss/vite": "4.2.1",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/react": "18.3.28",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@vitejs/plugin-react-swc": "4.1.0",
|
||||
"eslint": "catalog:eslint9",
|
||||
"eslint-plugin-no-relative-import-paths": "1.6.1",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.24",
|
||||
"eslint-plugin-tailwindcss": "4.0.0-beta.0",
|
||||
"globals": "17.4.0",
|
||||
"jest-extended": "7.0.0",
|
||||
"jsdom": "28.1.0",
|
||||
"msw": "2.12.14",
|
||||
"sirv": "3.0.2",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.58.0",
|
||||
"vite": "7.1.12",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "4.1.2"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"dev": {
|
||||
"dependsOn": [
|
||||
"ghost-admin:dev",
|
||||
"@tryghost/admin-x-framework:dev",
|
||||
"@tryghost/admin-x-design-system:dev",
|
||||
"@tryghost/shade:dev"
|
||||
]
|
||||
},
|
||||
"build:dev": {
|
||||
"dependsOn": [
|
||||
"build",
|
||||
{
|
||||
"projects": [
|
||||
"ghost-admin"
|
||||
],
|
||||
"target": "build:dev"
|
||||
}
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"outputs": [
|
||||
"{projectRoot}/dist",
|
||||
"{workspaceRoot}/ghost/core/core/built/admin"
|
||||
],
|
||||
"dependsOn": [
|
||||
"build",
|
||||
{
|
||||
"projects": [
|
||||
"ghost-admin"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@tryghost/admin-x-framework",
|
||||
"@tryghost/admin-x-design-system",
|
||||
"@tryghost/shade"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Outlet } from "@tryghost/admin-x-framework";
|
||||
import { useCurrentUser } from "@tryghost/admin-x-framework/api/current-user";
|
||||
import { EmberProvider, EmberFallback, EmberRoot } from "./ember-bridge";
|
||||
import { AdminLayout } from "./layout/admin-layout";
|
||||
import { useEmberAuthSync, useEmberDataSync } from "./ember-bridge";
|
||||
|
||||
function App() {
|
||||
const { data: currentUser } = useCurrentUser();
|
||||
useEmberAuthSync();
|
||||
useEmberDataSync();
|
||||
|
||||
return (
|
||||
<EmberProvider>
|
||||
{currentUser ?
|
||||
<AdminLayout>
|
||||
<Outlet />
|
||||
<EmberRoot />
|
||||
</AdminLayout>
|
||||
:
|
||||
<>
|
||||
<EmberFallback />
|
||||
<EmberRoot />
|
||||
</>
|
||||
}
|
||||
</EmberProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,178 @@
|
||||
@source "../../shade/src/**/*.{ts,tsx}";
|
||||
@source "../../posts/src/**/*.{ts,tsx}";
|
||||
@source "../../stats/src/**/*.{ts,tsx}";
|
||||
@source "../../activitypub/src/**/*.{ts,tsx}";
|
||||
@source "../../admin-x-settings/src/**/*.{ts,tsx}";
|
||||
@source "../../admin-x-design-system/src/**/*.{ts,tsx}";
|
||||
@source "../../../node_modules/@tryghost/kg-unsplash-selector/dist/**/*.js";
|
||||
|
||||
@import "@tryghost/shade/styles.css";
|
||||
|
||||
/* Legacy utility compatibility (Spirit/Tachyons-style percentages). */
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Keep admin-x heading line-height consistent with pre-migration settings render. */
|
||||
.admin-x-base h1,
|
||||
.admin-x-base h2,
|
||||
.admin-x-base h3,
|
||||
.admin-x-base h4,
|
||||
.admin-x-base h5 {
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
/* ActivityPub onboarding animations previously defined in admin tailwind config */
|
||||
@keyframes lineExpand {
|
||||
0% {
|
||||
transform: scaleX(0);
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
transform-origin: right;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-onboarding-handle-bg {
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.2s ease-in 0.5s forwards;
|
||||
}
|
||||
|
||||
.animate-onboarding-handle-line {
|
||||
animation: lineExpand 0.2s ease-in-out 0.7s forwards;
|
||||
}
|
||||
|
||||
.before\:animate-onboarding-handle-bg::before {
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.2s ease-in 0.5s forwards;
|
||||
}
|
||||
|
||||
.after\:animate-onboarding-handle-line::after {
|
||||
animation: lineExpand 0.2s ease-in-out 0.7s forwards;
|
||||
transform: scaleX(1) !important;
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.animate-onboarding-handle-label {
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.2s ease-in 1.2s forwards;
|
||||
}
|
||||
|
||||
.animate-onboarding-next-button {
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.2s ease-in 2s forwards;
|
||||
}
|
||||
|
||||
.animate-onboarding-followers {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
animation: fadeIn 0.2s ease-in 0.5s forwards, scale 0.3s ease-in 0.5s forwards;
|
||||
}
|
||||
|
||||
.break-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Base layout - grid structure for alerts and main content */
|
||||
body.react-admin {
|
||||
height: 100svh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: 100%;
|
||||
overflow: hidden; /* Prevent body scroll */
|
||||
}
|
||||
|
||||
/* Alerts show at the top and push content down */
|
||||
body.react-admin #ember-alerts-wormhole {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
z-index: 0; /* Hide alerts when settings app modal is open */
|
||||
}
|
||||
|
||||
/* Main app container - pass through to children (.shade.shade-admin) */
|
||||
body.react-admin #root {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Ensure ShadeApp takes full grid space */
|
||||
body.react-admin #root .shade.shade-admin {
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* iOS safe area handling for mobile navbar */
|
||||
body.react-admin .safe-area-inset-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Ensure the Ember app renders in the correct position and takes full width/height */
|
||||
body.react-admin #ember-app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Temporary overrides until Ember transition is finished */
|
||||
body.react-admin .gh-canvas-header {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
body.react-admin .gh-canvas-breadcrumb+.gh-canvas-title {
|
||||
padding-top: 0px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
body.react-admin .gh-canvas-title,
|
||||
body.react-admin [data-header="header-title"] {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
body.react-admin [data-header="header"] {
|
||||
padding-top: 24px;
|
||||
/* padding-bottom: 18px; */
|
||||
}
|
||||
|
||||
body.react-admin [data-navbar="navbar"] {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
body.react-admin #ember-app .gh-viewport {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
body.react-admin .gh-canvas-header {
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
body.react-admin .members-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.react-admin [data-test-table="members"] thead,
|
||||
body.react-admin [data-test-table="members"] tr {
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./app.tsx";
|
||||
import { FrameworkProvider, RouterProvider } from "@tryghost/admin-x-framework";
|
||||
import { ShadeApp } from "@tryghost/shade/app";
|
||||
|
||||
import { routes } from "./routes.tsx";
|
||||
import { navigateTo } from "./utils/navigation";
|
||||
import { AppProvider } from "./providers/app-provider";
|
||||
|
||||
const framework = {
|
||||
ghostVersion: "",
|
||||
externalNavigate: (link: { route: string; isExternal: boolean }) => {
|
||||
navigateTo(link.route);
|
||||
},
|
||||
unsplashConfig: {
|
||||
Authorization: "Client-ID 8672af113b0a8573edae3aa3713886265d9bb741d707f6c01a486cde8c278980",
|
||||
"Accept-Version": "v1",
|
||||
"Content-Type": "application/json",
|
||||
"App-Pragma": "no-cache",
|
||||
"X-Unsplash-Cache": true,
|
||||
},
|
||||
sentryDSN: null,
|
||||
onUpdate: (dataType: string, response: unknown) => {
|
||||
window.EmberBridge?.state.onUpdate(dataType, response);
|
||||
},
|
||||
onInvalidate: (dataType: string) => {
|
||||
window.EmberBridge?.state.onInvalidate(dataType);
|
||||
},
|
||||
onDelete: (dataType: string, id: string) => {
|
||||
window.EmberBridge?.state.onDelete(dataType, id);
|
||||
},
|
||||
};
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<FrameworkProvider {...framework}>
|
||||
<RouterProvider prefix={"/"} routes={routes}>
|
||||
<AppProvider>
|
||||
<ShadeApp
|
||||
className="shade-admin"
|
||||
darkMode={false}
|
||||
fetchKoenigLexical={null}
|
||||
>
|
||||
<App />
|
||||
</ShadeApp>
|
||||
</AppProvider>
|
||||
</RouterProvider>
|
||||
</FrameworkProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,79 @@
|
||||
import {render, screen} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {MembersRoute} from './members-route';
|
||||
|
||||
const {mockCanManageMembers, mockUseCurrentUser} = vi.hoisted(() => ({
|
||||
mockCanManageMembers: vi.fn(),
|
||||
mockUseCurrentUser: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('@tryghost/admin-x-framework', () => ({
|
||||
Outlet: () => React.createElement('div', {'data-testid': 'outlet'}),
|
||||
Navigate: ({replace, to}: {replace?: boolean; to: string}) => React.createElement('div', {
|
||||
'data-replace': String(Boolean(replace)),
|
||||
'data-testid': 'navigate',
|
||||
'data-to': to
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@tryghost/admin-x-framework/api/current-user', () => ({
|
||||
useCurrentUser: mockUseCurrentUser
|
||||
}));
|
||||
|
||||
vi.mock('@tryghost/admin-x-framework/api/users', () => ({
|
||||
canManageMembers: mockCanManageMembers
|
||||
}));
|
||||
|
||||
describe('MembersRoute', () => {
|
||||
beforeEach(() => {
|
||||
mockCanManageMembers.mockReturnValue(true);
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
data: {
|
||||
id: '1',
|
||||
roles: [{name: 'Administrator'}]
|
||||
},
|
||||
isError: false,
|
||||
isLoading: false
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the nested members routes for authorized users', () => {
|
||||
render(<MembersRoute />);
|
||||
|
||||
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects users without member permissions to home', () => {
|
||||
mockCanManageMembers.mockReturnValue(false);
|
||||
|
||||
render(<MembersRoute />);
|
||||
|
||||
expect(screen.getByTestId('navigate')).toHaveAttribute('data-to', '/');
|
||||
expect(screen.getByTestId('navigate')).toHaveAttribute('data-replace', 'true');
|
||||
});
|
||||
|
||||
it('renders nothing while the current user is still loading', () => {
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
data: undefined,
|
||||
isError: false,
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
const {container} = render(<MembersRoute />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('redirects to home when the current user is unavailable after loading', () => {
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
data: undefined,
|
||||
isError: false,
|
||||
isLoading: false
|
||||
});
|
||||
|
||||
render(<MembersRoute />);
|
||||
|
||||
expect(screen.getByTestId('navigate')).toHaveAttribute('data-to', '/');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {Navigate, Outlet} from "@tryghost/admin-x-framework";
|
||||
import {useCurrentUser} from "@tryghost/admin-x-framework/api/current-user";
|
||||
import {canManageMembers} from "@tryghost/admin-x-framework/api/users";
|
||||
|
||||
export function MembersRoute() {
|
||||
const {data: currentUser, isError, isLoading} = useCurrentUser();
|
||||
|
||||
if (!currentUser) {
|
||||
if (isError || !isLoading) {
|
||||
return <Navigate replace to="/" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!canManageMembers(currentUser)) {
|
||||
return <Navigate replace to="/" />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {Navigate} from "@tryghost/admin-x-framework";
|
||||
import {useCurrentUser} from "@tryghost/admin-x-framework/api/current-user";
|
||||
|
||||
const MyProfileRedirect = () => {
|
||||
const {data: currentUser, isError, isLoading} = useCurrentUser();
|
||||
|
||||
if (!currentUser) {
|
||||
if (isError || !isLoading) {
|
||||
return <Navigate replace to="/" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Navigate replace to={`/settings/staff/${currentUser.slug}`} />;
|
||||
};
|
||||
|
||||
export default MyProfileRedirect;
|
||||
@@ -0,0 +1,11 @@
|
||||
export function NotFound() {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold">404</h1>
|
||||
<span className="text-grey-500" aria-hidden="true">|</span>
|
||||
<h2 className="text-lg text-grey-700">Page not found</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import {type RouteObject, Outlet, lazyComponent, redirect} from "@tryghost/admin-x-framework";
|
||||
|
||||
// ActivityPub
|
||||
import { FeatureFlagsProvider, routes as activityPubRoutes } from "@tryghost/activitypub/api";
|
||||
|
||||
// Posts (aka tags and post analytics)
|
||||
import { PostsAppContextProvider, routes as postRoutes } from "@tryghost/posts/api";
|
||||
|
||||
// Stats (aka analytics)
|
||||
import { GlobalDataProvider, routes as statsRoutes } from "@tryghost/stats/api";
|
||||
import MyProfileRedirect from "./my-profile-redirect";
|
||||
|
||||
// Ember
|
||||
import { EmberFallback, ForceUpgradeGuard } from "./ember-bridge";
|
||||
import type { RouteHandle } from "./ember-bridge";
|
||||
import { MembersRoute } from "./members-route";
|
||||
|
||||
import { NotFound } from "./not-found";
|
||||
|
||||
// Routes handled by the Ember admin app. React delegates these to Ember via
|
||||
// EmberFallback. When migrating a route to React, remove its entry from here.
|
||||
const EMBER_ROUTES: string[] = [
|
||||
"/",
|
||||
"/dashboard",
|
||||
"/site",
|
||||
"/launch",
|
||||
"/setup/*",
|
||||
"/signin/*",
|
||||
"/signout",
|
||||
"/signup/*",
|
||||
"/reset/*",
|
||||
"/pro/*",
|
||||
"/posts",
|
||||
"/posts/analytics/:postId/mentions",
|
||||
"/posts/analytics/:postId/debug",
|
||||
"/restore",
|
||||
"/pages",
|
||||
"/editor/*",
|
||||
"/tags/new",
|
||||
"/explore/*",
|
||||
"/migrate/*",
|
||||
"/members/new",
|
||||
"/members/:member_id",
|
||||
"/members-activity",
|
||||
"/designsandbox",
|
||||
"/mentions",
|
||||
];
|
||||
|
||||
const emberFallbackHandle = { allowInForceUpgrade: true } satisfies RouteHandle;
|
||||
|
||||
const emberFallbackRoutes: RouteObject[] = EMBER_ROUTES.map(path => ({
|
||||
path,
|
||||
Component: EmberFallback,
|
||||
handle: emberFallbackHandle,
|
||||
}));
|
||||
|
||||
const membersRoute: RouteObject = {
|
||||
path: "/members",
|
||||
element: <MembersRoute />,
|
||||
handle: emberFallbackHandle,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: lazyComponent(() => import("@tryghost/posts/members"))
|
||||
},
|
||||
{
|
||||
path: "import",
|
||||
lazy: lazyComponent(() => import("@tryghost/posts/members"))
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
{
|
||||
// ForceUpgradeGuard wraps all routes to redirect to /pro when in force upgrade mode.
|
||||
// Routes with handle.allowInForceUpgrade: true bypass this protection.
|
||||
element: <ForceUpgradeGuard />,
|
||||
children: [
|
||||
{
|
||||
// Override the tag detail route from the posts app to ensure we
|
||||
// correctly delegate to Ember since we can't remove the blank screen in
|
||||
// the posts app. The blank screen needs to be there to prevent the
|
||||
// router error fallback from triggering when navigating from the tag
|
||||
// list to a tag detail page.
|
||||
path: "/tags/:tagSlug",
|
||||
Component: EmberFallback,
|
||||
handle: emberFallbackHandle,
|
||||
},
|
||||
membersRoute,
|
||||
{
|
||||
element: (
|
||||
<PostsAppContextProvider value={{ fromAnalytics: true }}>
|
||||
<Outlet />
|
||||
</PostsAppContextProvider>
|
||||
),
|
||||
// Filter out catch-all routes
|
||||
children: postRoutes[0].children!.filter((route) => route.path !== "*"),
|
||||
},
|
||||
{
|
||||
element: (
|
||||
<GlobalDataProvider>
|
||||
<Outlet />
|
||||
</GlobalDataProvider>
|
||||
),
|
||||
children: statsRoutes,
|
||||
},
|
||||
{
|
||||
path: `network`,
|
||||
loader: () => redirect("/activitypub"),
|
||||
},
|
||||
{
|
||||
path: "my-profile",
|
||||
Component: MyProfileRedirect,
|
||||
handle: { allowInForceUpgrade: true } satisfies RouteHandle,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
element: (
|
||||
<FeatureFlagsProvider>
|
||||
<Outlet />
|
||||
</FeatureFlagsProvider>
|
||||
),
|
||||
children: activityPubRoutes,
|
||||
},
|
||||
{
|
||||
path: `settings/*`,
|
||||
lazy: lazyComponent(() => import("./settings/settings")),
|
||||
handle: { allowInForceUpgrade: true } satisfies RouteHandle,
|
||||
},
|
||||
// Ember-handled routes
|
||||
...emberFallbackRoutes,
|
||||
{
|
||||
// 404 catch-all for routes not handled by React or Ember
|
||||
path: "*",
|
||||
Component: NotFound,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '@tryghost/limit-service'
|
||||
declare module '@tryghost/nql'
|
||||
declare module '@tryghost/koenig-lexical'
|
||||
@@ -0,0 +1,8 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { expect } from "vitest";
|
||||
import matchers from "jest-extended";
|
||||
import { setupShadeMocks } from "@tryghost/admin-x-framework/test/setup";
|
||||
|
||||
expect.extend(matchers);
|
||||
|
||||
setupShadeMocks();
|
||||
@@ -0,0 +1,13 @@
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { expect } from "vitest";
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
|
||||
export async function waitForQuerySettled<T>(result: { current: UseQueryResult<T, unknown> }) {
|
||||
await waitFor(
|
||||
() => {
|
||||
// Query is settled when it has reached a terminal state (success or error)
|
||||
const isSettled = (result.current.isSuccess || result.current.isError) && !result.current.isFetching;
|
||||
expect(isSettled).toBe(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["node", "vitest/globals"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Path aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@test-utils/*": ["./test-utils/*"],
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src", "test-utils"],
|
||||
"references": [
|
||||
{ "path": "../admin-x-framework/tsconfig.declaration.json" },
|
||||
{ "path": "../posts/tsconfig.declaration.json" },
|
||||
{ "path": "../stats/tsconfig.declaration.json" },
|
||||
{ "path": "../activitypub/tsconfig.declaration.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["./vite*.ts",]
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { Plugin, ProxyOptions } from "vite";
|
||||
import type { IncomingMessage } from "http";
|
||||
import { getSubdir, GHOST_URL } from "./vite.config";
|
||||
|
||||
/**
|
||||
* Resolves the configured Ghost site URL by calling the admin api site endpoint
|
||||
* with retries (up to 20 seconds).
|
||||
*/
|
||||
async function resolveGhostSiteUrl() {
|
||||
const MAX_ATTEMPTS = 20;
|
||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
const siteEndpoint = new URL('ghost/api/admin/site/', GHOST_URL);
|
||||
const response = await fetch(siteEndpoint);
|
||||
const data = (await response.json()) as { site: { url: string } };
|
||||
return {
|
||||
url: data.site.url,
|
||||
host: new URL(data.site.url).host,
|
||||
};
|
||||
} catch (error) {
|
||||
if (attempt === MAX_ATTEMPTS) throw error;
|
||||
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Failed to resolve Ghost site URL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates proxy configuration for Ghost Admin API requests. Rewrites cookies
|
||||
* and headers to work with Ghost's security middleware.
|
||||
*/
|
||||
function createAdminApiProxy(site: {
|
||||
url: string;
|
||||
host: string;
|
||||
}): Record<string, ProxyOptions> {
|
||||
// When running the dev server against the backend on HTTPS, we need to
|
||||
// remove the same site and secure flags from the cookie. Otherwise, the
|
||||
// browser won't set it correctly since the dev server is running on HTTP.
|
||||
const rewriteCookies = (proxyRes: IncomingMessage) => {
|
||||
const cookies = proxyRes.headers["set-cookie"];
|
||||
if (Array.isArray(cookies)) {
|
||||
proxyRes.headers["set-cookie"] = cookies.map((cookie) => {
|
||||
return cookie
|
||||
.split(";")
|
||||
.filter((v) => v.trim().toLowerCase() !== "secure")
|
||||
.filter((v) => v.trim().toLowerCase() !== "samesite=none")
|
||||
.join("; ");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const subdir = getSubdir();
|
||||
|
||||
return {
|
||||
[`^${subdir}/ghost/api/.*`]: {
|
||||
target: site.url,
|
||||
changeOrigin: true,
|
||||
followRedirects: true,
|
||||
autoRewrite: true,
|
||||
cookieDomainRewrite: {
|
||||
"*": site.host,
|
||||
},
|
||||
configure(proxy) {
|
||||
proxy.on("proxyRes", rewriteCookies);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates proxy configuration for Ember CLI live reload script.
|
||||
*/
|
||||
function createEmberLiveReloadProxy(): Record<string, ProxyOptions> {
|
||||
return {
|
||||
"^/ember-cli-live-reload.js": {
|
||||
target: "http://localhost:4200",
|
||||
changeOrigin: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin that injects proxy configurations for:
|
||||
* 1. Ghost Admin API - proxies /ghost/api requests to the Ghost backend
|
||||
* 2. Ember Live Reload - proxies ember-cli-live-reload.js to Ember dev server
|
||||
*/
|
||||
export function ghostBackendProxyPlugin(): Plugin {
|
||||
let siteUrl!: { url: string; host: string };
|
||||
|
||||
return {
|
||||
name: "ghost-backend-proxy",
|
||||
|
||||
async configResolved(config) {
|
||||
// Only resolve backend URL for dev/preview, not for builds or tests
|
||||
if (config.command !== 'serve' || config.mode === 'test') return;
|
||||
|
||||
try {
|
||||
// We expect this to succeed immediately, but if the backend
|
||||
// server is getting started, it might need some time.
|
||||
// In that case, this lets the user know in case we're barking
|
||||
// up the wrong tree (aka the GHOST_URL is wrong.)
|
||||
const timeout = setTimeout(() => {
|
||||
config.logger.info(`Trying to reach Ghost Admin API at ${GHOST_URL}...`);
|
||||
}, 1000);
|
||||
|
||||
siteUrl = await resolveGhostSiteUrl();
|
||||
clearTimeout(timeout);
|
||||
|
||||
config.logger.info(`👻 Using backend url: ${siteUrl.url}`);
|
||||
} catch (error) {
|
||||
config.logger
|
||||
.error(`Could not reach Ghost Admin API at: ${GHOST_URL}
|
||||
|
||||
Ensure the Ghost backend is running. If needed, set the GHOST_URL environment variable to the correct URL.
|
||||
`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
configureServer(server) {
|
||||
if (!siteUrl) return;
|
||||
|
||||
server.config.server.proxy = {
|
||||
...server.config.server.proxy,
|
||||
...createAdminApiProxy(siteUrl),
|
||||
...createEmberLiveReloadProxy(),
|
||||
};
|
||||
},
|
||||
|
||||
configurePreviewServer(server) {
|
||||
if (!siteUrl) return;
|
||||
|
||||
server.config.preview.proxy = {
|
||||
...server.config.preview.proxy,
|
||||
...createAdminApiProxy(siteUrl),
|
||||
};
|
||||
},
|
||||
} as const satisfies Plugin;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Plugin, ViteDevServer, PreviewServer } from "vite";
|
||||
|
||||
/**
|
||||
* Vite plugin that redirects admin deep-link URLs to hash-based URLs.
|
||||
*
|
||||
* Mirrors ghost/core/core/server/web/admin/middleware/redirect-admin-urls.js
|
||||
* so that direct navigation to paths like /ghost/posts/123 redirects to /ghost/#/posts/123
|
||||
*
|
||||
* By registering as a post-middleware, static assets and API requests are handled first,
|
||||
* and only unhandled requests trigger the redirect.
|
||||
*/
|
||||
export function deepLinksPlugin(): Plugin {
|
||||
function addRedirectMiddleware(server: ViteDevServer | PreviewServer) {
|
||||
const base = (server.config.base ?? "/ghost").replace(/\/$/, "");
|
||||
const pathRegex = new RegExp(`^${base}/(.+)`);
|
||||
|
||||
return () => {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
const match = req.originalUrl?.match(pathRegex);
|
||||
|
||||
if (match) {
|
||||
res.writeHead(302, { Location: `${base}/#/${match[1]}` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: "deep-links",
|
||||
configureServer: addRedirectMiddleware,
|
||||
configurePreviewServer: addRedirectMiddleware,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import type {PluginOption, HtmlTagDescriptor, ResolvedConfig} from 'vite';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import sirv from 'sirv';
|
||||
|
||||
const GHOST_ADMIN_PATH = path.resolve(__dirname, '../../ghost/core/core/built/admin');
|
||||
const GHOST_ADMIN_DIST = path.resolve(__dirname, '../../ghost/admin/dist');
|
||||
|
||||
function isAbsoluteUrl(url: string): boolean {
|
||||
return url.startsWith('http://') ||
|
||||
url.startsWith('https://') ||
|
||||
url.startsWith('/');
|
||||
}
|
||||
|
||||
function prefixUrl(url: string, base: string): string {
|
||||
if (isAbsoluteUrl(url)) return url;
|
||||
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
||||
return `${normalizedBase}/${url}`;
|
||||
}
|
||||
|
||||
// Vite plugin to extract styles and scripts from Ghost admin index.html
|
||||
export function emberAssetsPlugin() {
|
||||
let config: ResolvedConfig;
|
||||
|
||||
return {
|
||||
name: 'ember-assets',
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
},
|
||||
transformIndexHtml: {
|
||||
order: 'post',
|
||||
handler() {
|
||||
// Read from Ember's own build output (not the combined output
|
||||
// in built/admin which gets overwritten by closeBundle and would
|
||||
// accumulate duplicate path prefixes on repeated builds)
|
||||
const indexPath = path.resolve(GHOST_ADMIN_DIST, 'index.html');
|
||||
try {
|
||||
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
||||
const base = config.base || '/';
|
||||
|
||||
// Extract stylesheets
|
||||
const styleRegex = /<link[^>]*rel="stylesheet"[^>]*href="([^"]*)"[^>]*>/g;
|
||||
const styles: HtmlTagDescriptor[] = [];
|
||||
let styleMatch;
|
||||
while ((styleMatch = styleRegex.exec(indexContent)) !== null) {
|
||||
styles.push({
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'stylesheet',
|
||||
href: prefixUrl(styleMatch[1], base)
|
||||
}
|
||||
});
|
||||
}
|
||||
// Extract scripts
|
||||
const scriptRegex = /<script[^>]*src="([^"]*)"[^>]*><\/script>/g;
|
||||
const scripts: HtmlTagDescriptor[] = [];
|
||||
let scriptMatch;
|
||||
while ((scriptMatch = scriptRegex.exec(indexContent)) !== null) {
|
||||
scripts.push({
|
||||
tag: 'script',
|
||||
injectTo: 'body',
|
||||
attrs: {
|
||||
src: prefixUrl(scriptMatch[1], base)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract meta tags
|
||||
const metaRegex = /<meta name="ghost-admin\/config\/environment" content="([^"]*)"[^>]*>/g;
|
||||
const metaTags: HtmlTagDescriptor[] = [];
|
||||
let metaMatch;
|
||||
while ((metaMatch = metaRegex.exec(indexContent)) !== null) {
|
||||
metaTags.push({
|
||||
tag: 'meta',
|
||||
attrs: {
|
||||
name: 'ghost-admin/config/environment',
|
||||
content: metaMatch[1]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Generate the virtual module content
|
||||
return [...styles, ...scripts, ...metaTags];
|
||||
} catch (error) {
|
||||
console.warn('Failed to read Ghost admin index.html:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
// Serve Ember assets from the filesystem in development
|
||||
const assetsMiddleware = sirv(path.resolve(GHOST_ADMIN_PATH, 'assets'), {
|
||||
dev: true,
|
||||
etag: true
|
||||
});
|
||||
|
||||
const base = (server.config.base ?? '/ghost').replace(/\/$/, '');
|
||||
const assetsPrefix = `${base}/assets/`;
|
||||
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (req.url?.startsWith(assetsPrefix)) {
|
||||
const originalUrl = req.url;
|
||||
req.url = req.url.replace(assetsPrefix, '/');
|
||||
assetsMiddleware(req, res, () => {
|
||||
req.url = originalUrl;
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
closeBundle() {
|
||||
// Only copy assets during production builds
|
||||
if (config.command === 'build') {
|
||||
try {
|
||||
// All legacy admin assets gets copied to the Ghost core
|
||||
// admin assets folder by the Ember build
|
||||
const ghostAssetsDir = path.resolve(GHOST_ADMIN_PATH, 'assets');
|
||||
|
||||
// React admin build output (apps/admin/dist/)
|
||||
const reactAssetsDir = path.resolve(config.build.outDir, 'assets');
|
||||
const reactIndexFile = path.resolve(config.build.outDir, 'index.html');
|
||||
|
||||
// Copy Ember assets to React build output to enable use of
|
||||
// vite preview. This also prevents stale Ember assets from
|
||||
// overwriting fresh ones in the next step.
|
||||
fs.cpSync(ghostAssetsDir, reactAssetsDir, { recursive: true });
|
||||
|
||||
// Copy combined assets back to Ghost core admin assets folder
|
||||
fs.cpSync(reactAssetsDir, ghostAssetsDir, {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
// Copy React index.html, overwriting the existing index.html
|
||||
const forwardIndexFile = path.resolve(GHOST_ADMIN_PATH, 'index.html');
|
||||
fs.copyFileSync(reactIndexFile, forwardIndexFile);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy admin assets: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const satisfies PluginOption;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { resolve } from "path";
|
||||
import { createRequire } from "node:module";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import type { PluginOption } from "vite";
|
||||
const require = createRequire(import.meta.url);
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
import { emberAssetsPlugin } from "./vite-ember-assets";
|
||||
import { ghostBackendProxyPlugin } from "./vite-backend-proxy";
|
||||
import { deepLinksPlugin } from "./vite-deep-links";
|
||||
|
||||
export const GHOST_URL = process.env.GHOST_URL ?? "http://localhost:2368/";
|
||||
const GHOST_CARDS_PATH = resolve(__dirname, "../../ghost/core/core/frontend/src/cards");
|
||||
|
||||
/**
|
||||
* Extracts the subdirectory path from GHOST_URL.
|
||||
* e.g., "http://localhost:2368/blog/" -> "/blog"
|
||||
* "http://localhost:2368/" -> ""
|
||||
*/
|
||||
export function getSubdir(): string {
|
||||
const url = new URL(GHOST_URL);
|
||||
return url.pathname.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the Vite base path.
|
||||
* - If GHOST_CDN_URL is set, use it (for CDN deployments)
|
||||
* - Otherwise, use the subdir + /ghost (e.g., "/ghost" or "/blog/ghost")
|
||||
* - For builds without CDN, use "./" for relative paths in index-forward.html
|
||||
*/
|
||||
function getBase(command: 'build' | 'serve'): string {
|
||||
if (process.env.GHOST_CDN_URL) {
|
||||
return process.env.GHOST_CDN_URL;
|
||||
}
|
||||
// During build, use relative paths so index-forward.html works when served from any subdir
|
||||
if (command === 'build') {
|
||||
return './';
|
||||
}
|
||||
// During dev, use absolute path based on GHOST_URL subdir
|
||||
return `${getSubdir()}/ghost`;
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ command }) => ({
|
||||
base: getBase(command),
|
||||
plugins: [tailwindcss() as PluginOption, react(), emberAssetsPlugin(), ghostBackendProxyPlugin(), deepLinksPlugin(), tsconfigPaths()],
|
||||
define: {
|
||||
"process.env.DEBUG": false, // Shim env var utilized by the @tryghost/nql package
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5174,
|
||||
allowedHosts: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@ghost-cards": GHOST_CARDS_PATH,
|
||||
// TODO: Remove this when @tryghost/nql is updated
|
||||
mingo: require.resolve("mingo/dist/mingo.js"),
|
||||
},
|
||||
// Shim node modules utilized by the @tryghost/nql package
|
||||
external: ["fs", "path", "util"],
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: ["./test-utils/setup.ts"],
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013-2026 Ghost Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Announcement Bar
|
||||
|
||||
## Development
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
- Run `pnpm` in Ghost monorepo root
|
||||
- Run `pnpm` in this directory
|
||||
|
||||
### Running via Ghost `pnpm dev` in root folder
|
||||
|
||||
Announcement Bar runs automatically when using Ghost's development command from the monorepo root:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
This starts all frontend apps (including Announcement Bar.)
|
||||
|
||||
## Release
|
||||
|
||||
A patch release can be rolled out instantly in production, whereas a minor/major release requires the Ghost monorepo to be updated and released.
|
||||
In either case, you need sufficient permissions to release `@tryghost` packages on NPM.
|
||||
|
||||
### Patch release
|
||||
|
||||
1. Run `pnpm ship` and select a patch version when prompted
|
||||
2. Merge the release commit to `main`
|
||||
|
||||
### Minor / major release
|
||||
|
||||
1. Run `pnpm ship` and select a minor or major version when prompted
|
||||
2. Merge the release commit to `main`
|
||||
3. Wait until a new version of Ghost is released
|
||||
|
||||
To use the new version of signup form in Ghost, update the version in Ghost core's default configuration (currently at `core/shared/config/default.json`)
|
||||
|
||||
# Copyright & License
|
||||
|
||||
Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user