0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2024-12-30 22:34:01 -05:00

Adding Posts analytics React app (#21878)

ref https://linear.app/ghost/issue/DES-1021/create-posts-app

Part of establishing React patterns in Ghost is to build a well-defined
and fairly self-encapsulated app through which we can test assumptions
and define best practices. Our guinea pig is Post analytics for this
purpose. This PR creates a new React app (posts) using Shade (the new
design system).
This commit is contained in:
Peter Zimon 2024-12-19 12:01:08 +01:00 committed by GitHub
parent ab2c7f18e2
commit 252918b70c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 2017 additions and 473 deletions

View file

@ -74,7 +74,7 @@ const COMMAND_TYPESCRIPT = {
env: {}
};
const adminXApps = '@tryghost/admin-x-demo,@tryghost/admin-x-settings,@tryghost/admin-x-activitypub';
const adminXApps = '@tryghost/admin-x-demo,@tryghost/admin-x-settings,@tryghost/admin-x-activitypub,@tryghost/posts';
const COMMANDS_ADMINX = [{
name: 'adminXDeps',

View file

@ -1,119 +1,119 @@
{
"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"
"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"
},
"./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"
}
},
"./errors": {
"import": "./dist/errors.js",
"require": "./dist/errors.cjs",
"types": "./types/errors.d.ts"
"sideEffects": false,
"scripts": {
"build": "tsc -p tsconfig.declaration.json && vite build",
"prepare": "yarn build",
"test": "yarn test:types && yarn test:unit",
"test:types": "tsc --noEmit",
"test:unit": "vitest run --coverage",
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
"lint": "yarn lint:code && yarn lint:test",
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache"
},
"./helpers": {
"import": "./dist/helpers.js",
"require": "./dist/helpers.cjs",
"types": "./types/helpers.d.ts"
"files": [
"es",
"types"
],
"devDependencies": {
"@testing-library/react": "14.3.1",
"@types/mocha": "10.0.1",
"c8": "8.0.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"jsdom": "24.1.3",
"mocha": "10.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"sinon": "17.0.0",
"ts-node": "10.9.2",
"typescript": "5.4.5"
},
"./hooks": {
"import": "./dist/hooks.js",
"require": "./dist/hooks.cjs",
"types": "./types/hooks.d.ts"
"dependencies": {
"@sentry/react": "7.119.2",
"@tanstack/react-query": "4.36.1",
"@tryghost/admin-x-design-system": "0.0.0",
"@tryghost/shade": "0.0.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "4.2.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"vite": "4.5.3",
"vite-plugin-css-injected-by-js": "^3.3.0",
"vite-plugin-svgr": "3.3.0",
"vitest": "0.34.3"
},
"./routing": {
"import": "./dist/routing.js",
"require": "./dist/routing.cjs",
"types": "./types/routing.d.ts"
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"./api/*": {
"import": "./dist/api/*.js",
"require": "./dist/api/*.cjs",
"types": "./types/api/*.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"
"nx": {
"targets": {
"dev": {
"dependsOn": [
"^build"
]
},
"test:unit": {
"dependsOn": [
"test:unit",
"^build",
"@tryghost/admin-x-design-system:test:unit"
]
}
}
}
},
"sideEffects": false,
"scripts": {
"build": "tsc -p tsconfig.declaration.json && vite build",
"prepare": "yarn build",
"test": "yarn test:types && yarn test:unit",
"test:types": "tsc --noEmit",
"test:unit": "vitest run --coverage",
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
"lint": "yarn lint:code && yarn lint:test",
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache"
},
"files": [
"es",
"types"
],
"devDependencies": {
"@testing-library/react": "14.3.1",
"@types/mocha": "10.0.1",
"c8": "8.0.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"jsdom": "24.1.3",
"mocha": "10.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"sinon": "17.0.0",
"ts-node": "10.9.2",
"typescript": "5.4.5"
},
"dependencies": {
"@sentry/react": "7.119.2",
"@tanstack/react-query": "4.36.1",
"@tryghost/admin-x-design-system": "0.0.0",
"@tryghost/shade": "0.0.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "4.2.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"vite": "4.5.3",
"vite-plugin-css-injected-by-js": "^3.3.0",
"vite-plugin-svgr": "3.3.0",
"vitest": "0.34.3"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"nx": {
"targets": {
"dev": {
"dependsOn": [
"^build"
]
},
"test:unit": {
"dependsOn": [
"test:unit",
"^build",
"@tryghost/admin-x-design-system:test:unit"
]
}
}
}
}

View file

@ -0,0 +1,62 @@
import {ShadeAppProps} from '@tryghost/shade';
import React from 'react';
import ReactDOM from 'react-dom/client';
import {TopLevelFrameworkProps} from '../providers/FrameworkProvider';
export default function renderShadeApp<Props extends object>(
App: React.ComponentType<Props & {
framework: TopLevelFrameworkProps;
designSystem: ShadeAppProps;
}>,
props: Props
) {
const style = document.createElement('style');
style.appendChild(document.createTextNode(`
: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;
}
`));
document.head.appendChild(style);
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App
designSystem={{darkMode: false, fetchKoenigLexical: null}}
framework={{
externalNavigate: (link) => {
// Use the expectExternalNavigate helper to test this dummy external linking
window.location.href = `/external/${encodeURIComponent(JSON.stringify(link))}`;
},
ghostVersion: '5.x',
sentryDSN: null,
unsplashConfig: {
Authorization: '',
'Accept-Version': '',
'Content-Type': '',
'App-Pragma': '',
'X-Unsplash-Cache': false
},
onDelete: () => {},
onInvalidate: () => {},
onUpdate: () => {}
}}
{...props}
/>
</React.StrictMode>
);
}

View file

@ -1,3 +1,4 @@
import path from 'path';
import react from '@vitejs/plugin-react';
import glob from 'glob';
import {resolve} from 'path';
@ -10,6 +11,11 @@ export default (function viteConfig() {
plugins: [
react()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
preview: {
port: 4174
},
@ -20,13 +26,13 @@ export default (function viteConfig() {
outDir: 'dist',
lib: {
formats: ['es', 'cjs'],
entry: glob.sync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, path) => {
if (path.endsWith('.d.ts')) {
entry: glob.sync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, libpath) => {
if (libpath.endsWith('.d.ts')) {
return entries;
}
const outPath = path.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, '');
entries[outPath] = path;
const outPath = libpath.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, '');
entries[outPath] = libpath;
return entries;
}, {} as Record<string, string>)
},

View file

@ -51,6 +51,10 @@ const features = [{
title: 'Comment Improvements',
description: 'Enables new comment features',
flag: 'commentImprovements'
}, {
title: 'Post analytics redesign',
description: 'Enables redesigned Post analytics page',
flag: 'postsX'
}];
const AlphaFeatures: React.FC = () => {

56
apps/posts/.eslintrc.cjs Normal file
View file

@ -0,0 +1,56 @@
/* eslint-env node */
module.exports = {
root: true,
extends: [
'plugin:ghost/ts',
'plugin:react/recommended',
'plugin:react-hooks/recommended'
],
plugins: [
'ghost',
'react-refresh',
'tailwindcss'
],
settings: {
react: {
version: 'detect'
}
},
rules: {
// sort multiple import lines into alphabetical groups
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
}],
// 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', {config: 'tailwind.config.cjs'}],
'tailwindcss/enforces-negative-arbitrary-values': ['warn', {config: 'tailwind.config.cjs'}],
'tailwindcss/enforces-shorthand': ['warn', {config: 'tailwind.config.cjs'}],
'tailwindcss/migration-from-tailwind-2': ['warn', {config: 'tailwind.config.cjs'}],
'tailwindcss/no-arbitrary-value': 'off',
'tailwindcss/no-custom-classname': 'off',
'tailwindcss/no-contradicting-classname': ['error', {config: 'tailwind.config.cjs'}]
}
};

3
apps/posts/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
dist
playwright-report
test-results

16
apps/posts/index.html Normal file
View file

@ -0,0 +1,16 @@
<!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>Posts</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/standalone.tsx"></script>
</body>
</html>

55
apps/posts/package.json Normal file
View file

@ -0,0 +1,55 @@
{
"name": "@tryghost/posts",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/TryGhost/Ghost/tree/main/apps/posts"
},
"author": "Ghost Foundation",
"files": [
"LICENSE",
"README.md",
"dist/"
],
"main": "./dist/posts.umd.cjs",
"module": "./dist/posts.js",
"private": true,
"scripts": {
"dev": "vite build --watch",
"dev:start": "vite",
"build": "tsc && vite build",
"lint": "yarn run lint:code && yarn 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",
"preview": "vite preview"
},
"devDependencies": {
"@testing-library/react": "14.3.1",
"@tryghost/shade": "0.0.0",
"@tryghost/admin-x-framework": "0.0.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"nx": {
"targets": {
"dev": {
"dependsOn": [
"^build"
]
},
"test:unit": {
"dependsOn": [
"^build"
]
},
"test:acceptance": {
"dependsOn": [
"^build"
]
}
}
}
}

View file

@ -0,0 +1,3 @@
import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright';
export default adminXPlaywrightConfig();

View file

@ -0,0 +1 @@
module.exports = require('@tryghost/shade/postcss.config.cjs');

23
apps/posts/src/App.tsx Normal file
View file

@ -0,0 +1,23 @@
import PostAnalytics from './pages/PostAnalytics';
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
import {RoutingProvider} from '@tryghost/admin-x-framework/routing';
import {ShadeApp, ShadeAppProps} from '@tryghost/shade';
interface AppProps {
framework: TopLevelFrameworkProps;
designSystem: ShadeAppProps;
}
const App: React.FC<AppProps> = ({framework, designSystem}) => {
return (
<FrameworkProvider {...framework}>
<RoutingProvider basePath='posts-x'>
<ShadeApp className='posts' {...designSystem}>
<PostAnalytics />
</ShadeApp>
</RoutingProvider>
</FrameworkProvider>
);
};
export default App;

View file

@ -0,0 +1,26 @@
import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, H1} from '@tryghost/shade';
const Header = () => {
return (
<div className="pt-9">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/ghost/posts">
Posts
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
Analytics
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<H1 className='mt-1 max-w-[1024px]'>The Evolution of Basketball: From Pastime to Professional and One of the Most Popular Sports</H1>
</div>
);
};
export default Header;

View file

@ -0,0 +1,17 @@
import ClickPerformance from './overview/ClickPerformance';
import Conversions from './overview/Conversions';
import Feedback from './overview/Feedback';
import NewsletterPerformance from './overview/NewsletterPerformance';
const Overview = () => {
return (
<div className="grid w-full grid-cols-3 gap-6 py-4">
<NewsletterPerformance className='col-span-2' />
<Feedback />
<ClickPerformance className='col-span-2' />
<Conversions />
</div>
);
};
export default Overview;

View file

@ -0,0 +1,22 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface ClickPerformanceProps extends React.ComponentProps<typeof Card> {};
const ClickPerformance: React.FC<ClickPerformanceProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Click performance</CardTitle>
<CardDescription>
Links in this newsletter
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default ClickPerformance;

View file

@ -0,0 +1,22 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface ConversionsProps extends React.ComponentProps<typeof Card> {};
const Conversions: React.FC<ConversionsProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Conversions</CardTitle>
<CardDescription>
3 members signed up on this post
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default Conversions;

View file

@ -0,0 +1,22 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface FeedbackProps extends React.ComponentProps<typeof Card> {};
const Feedback: React.FC<FeedbackProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Feedback</CardTitle>
<CardDescription>
188 reactions
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default Feedback;

View file

@ -0,0 +1,22 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface NewsletterPerformanceProps extends React.ComponentProps<typeof Card> {};
const NewsletterPerformance: React.FC<NewsletterPerformanceProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Newsletter performance</CardTitle>
<CardDescription>
Sent 19 Sept 2024
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default NewsletterPerformance;

6
apps/posts/src/index.tsx Normal file
View file

@ -0,0 +1,6 @@
import './styles/index.css';
import App from './App';
export {
App as AdminXApp
};

View file

@ -0,0 +1,47 @@
import Header from '../components/layout/Header';
import Overview from '../components/post-analytics/Overview';
import {Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, Icon, Page, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade';
const PostAnalytics = () => {
return (
<Page>
<Header />
<Tabs className='mt-7' defaultValue="overview" variant="page">
<div className='flex w-full items-center border-b pb-2'>
<TabsList className='flex w-full items-center justify-start border-none pb-0'>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="newsletter">Newsletter</TabsTrigger>
</TabsList>
<div className='flex items-center gap-1'>
<Button variant='outline'><Icon.Share className='-mt-0.5' />Share</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant='outline'><Icon.Dotdotdot /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-48">
<DropdownMenuItem>
<span>Edit post</span>
<DropdownMenuShortcut>E</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<span>View in browser</span>
<DropdownMenuShortcut>O</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<TabsContent value="overview">
<Overview />
</TabsContent>
<TabsContent value="newsletter">
Newsletter details
</TabsContent>
</Tabs>
</Page>
);
};
export default PostAnalytics;

View file

@ -0,0 +1,5 @@
import './styles/index.css';
import App from './App';
import renderShadeApp from '@tryghost/admin-x-framework/test/render-shade';
renderShadeApp(App, {});

View file

@ -0,0 +1 @@
@import "@tryghost/shade/styles.css";

View file

@ -0,0 +1,6 @@
import shadePreset from '@tryghost/shade/tailwind.cjs';
module.exports = {
presets: [shadePreset('.shade')],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', '../../node_modules/@tryghost/shade/es/**/*.{js,ts,jsx,tsx}']
};

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/ts-test'
]
};

23
apps/posts/tsconfig.json Normal file
View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "test"]
}

View file

@ -0,0 +1,10 @@
import adminXViteConfig from '@tryghost/admin-x-framework/vite';
import pkg from './package.json';
import {resolve} from 'path';
export default (function viteConfig() {
return adminXViteConfig({
packageName: pkg.name,
entry: resolve(__dirname, 'src/index.tsx')
});
});

View file

@ -25,6 +25,6 @@ const config: StorybookConfig = {
crypto: require.resolve('rollup-plugin-node-builtins')
}
return config;
}
},
};
export default config;

View file

@ -3,4 +3,4 @@ import shadeTheme from './shade-theme';
addons.setConfig({
theme: shadeTheme
});
});

View file

@ -59,7 +59,9 @@ const preview: Preview = {
options: {
storySort: {
method: 'alphabetical',
order: ['Welcome', 'Adding components', 'Component usage', 'Conventions', 'Icons', 'Components', 'Meta', 'Experimental'],
order: [
'Welcome', 'Adding components', 'Component usage', 'Conventions', 'Icons',
'Components', 'Layout', 'Experimental', 'Meta'],
},
},
docs: {
@ -76,7 +78,7 @@ const preview: Preview = {
let {scheme} = context.globals;
return (
<div className={`shade shade-base ${scheme === 'dark' ? 'dark' : ''}`} style={{
<div className={`shade ${scheme === 'dark' ? 'dark' : ''}`} style={{
// padding: '24px',
// width: 'unset',
height: 'unset',

View file

@ -34,4 +34,4 @@ export default create({
inputBorder: '#15171A',
inputTextColor: '#15171A',
inputBorderRadius: 2,
});
});

View file

@ -318,3 +318,7 @@ body,
.sbdocs hr {
margin: 40px 0;
}
.docs-story .shade {
overflow: unset;
}

View file

@ -1,99 +1,100 @@
{
"name": "@tryghost/shade",
"type": "module",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/shade",
"author": "Ghost Foundation",
"private": true,
"main": "es/index.js",
"types": "types/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "tsc -p tsconfig.declaration.json && vite build",
"prepare": "yarn build",
"test": "yarn test:unit",
"test:unit": "yarn test:types && yarn nx build && vitest run",
"test:types": "tsc --noEmit",
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
"lint": "yarn lint:code && yarn 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",
"tailwind.cjs",
"tailwind.config.cjs"
],
"devDependencies": {
"@codemirror/lang-html": "6.4.9",
"@radix-ui/react-tooltip": "1.1.2",
"@storybook/addon-essentials": "7.6.20",
"@storybook/addon-interactions": "7.6.20",
"@storybook/addon-links": "7.6.20",
"@storybook/addon-styling": "1.3.7",
"@storybook/blocks": "7.6.20",
"@storybook/react": "7.6.20",
"@storybook/react-vite": "7.6.4",
"@storybook/testing-library": "0.2.2",
"@testing-library/react": "14.3.1",
"@testing-library/react-hooks": "8.0.1",
"@types/lodash-es": "4.17.12",
"@types/node": "^22.10.1",
"@vitejs/plugin-react": "4.2.1",
"c8": "8.0.1",
"chai": "4.3.8",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"eslint-plugin-tailwindcss": "3.13.0",
"jsdom": "24.1.3",
"lodash-es": "4.17.21",
"mocha": "10.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"rollup-plugin-node-builtins": "2.1.2",
"sinon": "17.0.0",
"storybook": "7.6.20",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"validator": "13.12.0",
"vite": "4.5.3",
"vite-plugin-svgr": "3.3.0"
},
"dependencies": {
"@dnd-kit/core": "6.1.0",
"@dnd-kit/sortable": "7.0.2",
"@ebay/nice-modal-react": "1.2.13",
"@radix-ui/react-avatar": "1.1.0",
"@radix-ui/react-checkbox": "1.1.1",
"@radix-ui/react-form": "0.0.3",
"@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-radio-group": "1.2.0",
"@radix-ui/react-separator": "1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tabs": "1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
"@sentry/react": "7.119.2",
"@tailwindcss/forms": "0.5.9",
"@tailwindcss/line-clamp": "0.4.4",
"@uiw/react-codemirror": "4.23.7",
"autoprefixer": "10.4.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"postcss": "8.4.39",
"postcss-import": "16.1.0",
"react-colorful": "5.6.1",
"react-hot-toast": "2.4.1",
"react-select": "5.8.2",
"tailwind-merge": "^2.5.5",
"tailwindcss": "3.4.14",
"tailwindcss-animate": "^1.0.7"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
"name": "@tryghost/shade",
"type": "module",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/shade",
"author": "Ghost Foundation",
"private": true,
"main": "es/index.js",
"types": "types/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "tsc -p tsconfig.declaration.json && vite build",
"prepare": "yarn build",
"test": "yarn test:unit",
"test:unit": "yarn test:types && yarn nx build && vitest run",
"test:types": "tsc --noEmit",
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
"lint": "yarn lint:code && yarn 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",
"tailwind.cjs",
"tailwind.config.cjs"
],
"devDependencies": {
"@codemirror/lang-html": "6.4.9",
"@radix-ui/react-tooltip": "1.1.2",
"@storybook/addon-essentials": "7.6.20",
"@storybook/addon-interactions": "7.6.20",
"@storybook/addon-links": "7.6.20",
"@storybook/addon-styling": "1.3.7",
"@storybook/blocks": "7.6.20",
"@storybook/react": "7.6.20",
"@storybook/react-vite": "7.6.4",
"@storybook/testing-library": "0.2.2",
"@testing-library/react": "14.3.1",
"@testing-library/react-hooks": "8.0.1",
"@types/lodash-es": "4.17.12",
"@types/node": "^22.10.1",
"@vitejs/plugin-react": "4.2.1",
"c8": "8.0.1",
"chai": "4.3.8",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"eslint-plugin-tailwindcss": "3.13.0",
"jsdom": "24.1.3",
"lodash-es": "4.17.21",
"mocha": "10.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"rollup-plugin-node-builtins": "2.1.2",
"sinon": "17.0.0",
"storybook": "7.6.20",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"validator": "13.12.0",
"vite": "4.5.3",
"vite-plugin-svgr": "3.3.0"
},
"dependencies": {
"@dnd-kit/core": "6.1.0",
"@dnd-kit/sortable": "7.0.2",
"@ebay/nice-modal-react": "1.2.13",
"@radix-ui/react-avatar": "1.1.0",
"@radix-ui/react-checkbox": "1.1.1",
"@radix-ui/react-dropdown-menu": "2.1.3",
"@radix-ui/react-form": "0.0.3",
"@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-radio-group": "1.2.0",
"@radix-ui/react-separator": "1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-tooltip": "1.1.2",
"@sentry/react": "7.119.2",
"@tailwindcss/forms": "0.5.9",
"@tailwindcss/line-clamp": "0.4.4",
"@uiw/react-codemirror": "4.23.7",
"autoprefixer": "10.4.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"postcss": "8.4.39",
"postcss-import": "16.1.0",
"react-colorful": "5.6.1",
"react-hot-toast": "2.4.1",
"react-select": "5.8.2",
"tailwind-merge": "^2.5.5",
"tailwindcss": "3.4.14",
"tailwindcss-animate": "^1.0.7"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View file

@ -1,4 +1,4 @@
.shade-base {
.shade {
/*
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)

View file

@ -5,12 +5,13 @@ import ShadeProvider from './providers/ShadeProvider';
export interface ShadeAppProps extends React.HTMLProps<HTMLDivElement> {
darkMode: boolean;
// fetchKoenigLexical: FetchKoenigLexical;
fetchKoenigLexical: null;
}
const ShadeApp: React.FC<ShadeAppProps> = ({darkMode, className, children, ...props}) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ShadeApp: React.FC<ShadeAppProps> = ({darkMode, fetchKoenigLexical, className, children, ...props}) => {
const appClassName = clsx(
'shade-base',
'shade',
darkMode && 'dark',
className
);

View file

@ -12,4 +12,4 @@ const BoilerPlate: React.FC<BoilerPlateProps> = ({children}) => {
);
};
export default BoilerPlate;
export default BoilerPlate;

View file

@ -0,0 +1,42 @@
import type {Meta, StoryObj} from '@storybook/react';
import {H1, H2, H3, H4, HeadingProps} from './heading';
const meta = {
title: 'Layout / Heading',
tags: ['autodocs']
} satisfies Meta<HeadingProps>;
export default meta;
type Story = StoryObj<HeadingProps>;
export const HeadingOne = {
render: (args: Story['args']) => {
return (
<H1 {...args}>The Joke Tax Chronicles</H1>
);
}
};
export const HeadingTwo = {
render: (args: Story['args']) => {
return (
<H2 {...args}>The Plan</H2>
);
}
};
export const HeadingThree = {
render: (args: Story['args']) => {
return (
<H3 {...args}>The Joke Tax</H3>
);
}
};
export const HeadingFour = {
render: (args: Story['args']) => {
return (
<H4 {...args}>Jokester Revolt</H4>
);
}
};

View file

@ -0,0 +1,60 @@
import {cn} from '@/lib/utils';
import * as React from 'react';
export interface HeadingProps
extends React.HTMLAttributes<HTMLHeadingElement> {}
const H1 = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({className, ...props}, ref) => {
return (
<h1
ref={ref}
className={cn('scroll-m-20 text-3xl leading-supertight tracking-tight font-bold', className)}
{...props} />
);
}
);
H1.displayName = 'H1';
const H2 = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({className, ...props}, ref) => {
return (
<h2
ref={ref}
className={cn('scroll-m-20 text-2xl font-bold tracking-tight first:mt-0', className)}
{...props} />
);
}
);
H2.displayName = 'H2';
const H3 = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({className, ...props}, ref) => {
return (
<h3
ref={ref}
className={cn('scroll-m-20 text-xl font-semibold tracking-tight', className)}
{...props} />
);
}
);
H3.displayName = 'H3';
const H4 = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({className, ...props}, ref) => {
return (
<h4
ref={ref}
className={cn('scroll-m-20 text-lg font-semibold tracking-tight', className)}
{...props} />
);
}
);
H4.displayName = 'H4';
export {
H1,
H2,
H3,
H4
};

View file

@ -0,0 +1,21 @@
import type {Meta, StoryObj} from '@storybook/react';
import {Page} from './page';
const meta = {
title: 'Layout / Page',
component: Page,
tags: ['autodocs']
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof Page>;
export const Default: Story = {
args: {
children: (
<>
Page container with a max width of <code>max-w-content</code>
</>
)
}
};

View file

@ -0,0 +1,21 @@
import {cn} from '@/lib/utils';
import * as React from 'react';
export interface PageProps
extends React.HTMLAttributes<HTMLDivElement> {}
const Page = React.forwardRef<HTMLDivElement, PageProps>(
({className, ...props}, ref) => {
return (
<div
ref={ref}
className={cn('max-w-page mx-auto w-full px-12', className)}
{...props}
/>
);
}
);
Page.displayName = 'Page';
export {Page};

View file

@ -0,0 +1,31 @@
import type {Meta, StoryObj} from '@storybook/react';
import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator} from './breadcrumb';
const meta = {
title: 'Components / Breadcrumb',
component: Breadcrumb,
tags: ['autodocs']
} satisfies Meta<typeof Breadcrumb>;
export default meta;
type Story = StoryObj<typeof Breadcrumb>;
export const Default: Story = {
args: {
children: (
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/components">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
)
}
};

View file

@ -0,0 +1,115 @@
import * as React from 'react';
import {Slot} from '@radix-ui/react-slot';
import {ChevronRight, MoreHorizontal} from 'lucide-react';
import {cn} from '@/lib/utils';
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<'nav'> & {
separator?: React.ReactNode
}
>(({...props}, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = 'Breadcrumb';
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<'ol'>
>(({className, ...props}, ref) => (
<ol
ref={ref}
className={cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className
)}
{...props}
/>
));
BreadcrumbList.displayName = 'BreadcrumbList';
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<'li'>
>(({className, ...props}, ref) => (
<li
ref={ref}
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
));
BreadcrumbItem.displayName = 'BreadcrumbItem';
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<'a'> & {
asChild?: boolean
}
>(({asChild, className, ...props}, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
className={cn('transition-colors hover:text-foreground', className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = 'BreadcrumbLink';
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<'span'>
>(({className, ...props}, ref) => (
<span
ref={ref}
aria-current="page"
aria-disabled="true"
className={cn('font-normal text-foreground', className)}
role="link"
{...props}
/>
));
BreadcrumbPage.displayName = 'BreadcrumbPage';
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<'li'>) => (
<li
aria-hidden="true"
className={cn('[&>svg]:w-3.5 [&>svg]:h-3.5', className)}
role="presentation"
{...props}
>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
aria-hidden="true"
className={cn('flex h-9 w-9 items-center justify-center', className)}
role="presentation"
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis
};

View file

@ -21,6 +21,7 @@ export const Default: Story = {
export const IconOnly: Story = {
args: {
size: 'icon',
children: (
<Icon.ArrowUp />
)
@ -47,4 +48,4 @@ export const LucideIcon: Story = {
</>
)
}
};
};

View file

@ -9,16 +9,16 @@ const buttonVariants = cva(
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
default: 'h-[34px] px-3 py-2',
sm: 'h-7 rounded-md px-3 text-xs [&_svg]:size-3',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9'
}

View file

@ -0,0 +1,33 @@
import type {Meta, StoryObj} from '@storybook/react';
import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from './card';
import {Button} from './button';
const meta = {
title: 'Components / Card',
component: Card,
tags: ['autodocs']
} satisfies Meta<typeof Card>;
export default meta;
type Story = StoryObj<typeof Card>;
export const Default: Story = {
args: {
className: 'w-[350px]',
children: [
<CardHeader key="header">
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>,
<CardContent key="content">
Card contents
</CardContent>,
<CardFooter key="footer" className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
]
}
};

View file

@ -0,0 +1,76 @@
import * as React from 'react';
import {cn} from '@/lib/utils';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border bg-card text-card-foreground',
className
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('font-semibold leading-none', className)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export {Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent};

View file

@ -0,0 +1,71 @@
import type {Meta, StoryObj} from '@storybook/react';
import {DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger} from './dropdown-menu';
import {Button} from './button';
const meta = {
title: 'Components / Dropdown menu',
component: DropdownMenu,
tags: ['autodocs']
} satisfies Meta<typeof DropdownMenu>;
export default meta;
type Story = StoryObj<typeof DropdownMenu>;
export const Default: Story = {
args: {
children: [
<DropdownMenuTrigger key="trigger" asChild>
<Button variant="outline">Open</Button>
</DropdownMenuTrigger>,
<DropdownMenuContent key="content" className="w-56">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Keyboard shortcuts
<DropdownMenuShortcut>K</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Invite users</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>Email</DropdownMenuItem>
<DropdownMenuItem>Message</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>More...</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem>New Team
<DropdownMenuShortcut>+T</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>GitHub</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuItem disabled>API</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Log out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
]
}
};

View file

@ -0,0 +1,203 @@
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import {Check, ChevronRight, Circle} from 'lucide-react';
import {cn} from '@/lib/utils';
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({className, inset, children, ...props}, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({className, ...props}, ref) => (
<div className='shade'>
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</div>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({className, sideOffset = 4, ...props}, ref) => (
<DropdownMenuPrimitive.Portal>
<div className='shade'>
<DropdownMenuPrimitive.Content
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
sideOffset={sideOffset}
{...props}
/>
</div>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({className, inset, ...props}, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({className, children, checked, ...props}, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
checked={checked}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({className, children, ...props}, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({className, inset, ...props}, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({className, ...props}, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup
};

View file

@ -50,4 +50,4 @@ const Icon = Object.entries(iconModules).reduce((acc, [path, module]) => {
export type IconName = keyof typeof Icon;
export const IconComponents = Icon;
export default Icon;
export default Icon;

View file

@ -0,0 +1,31 @@
import type {Meta, StoryObj} from '@storybook/react';
import {Tabs, TabsContent, TabsList, TabsTrigger} from './tabs';
const meta = {
title: 'Components / Tabs',
component: Tabs,
tags: ['autodocs']
} satisfies Meta<typeof Tabs>;
export default meta;
type Story = StoryObj<typeof Tabs>;
export const Default: Story = {
args: {
defaultValue: 'account',
children: [
<TabsList key="list" className="grid w-full grid-cols-2">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>,
<TabsContent key="account" value="account">
Account contents
</TabsContent>,
<TabsContent key="password" value="password">
Password contents
</TabsContent>
]
}
};

View file

@ -0,0 +1,130 @@
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import {cn} from '@/lib/utils';
import {cva} from 'class-variance-authority';
type TabsVariant = 'segmented' | 'page';
const TabsVariantContext = React.createContext<TabsVariant>('segmented');
export interface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root> {
variant?: TabsVariant;
}
const tabsVariants = cva(
'',
{
variants: {
variant: {
segmented: '',
page: ''
}
},
defaultVariants: {
variant: 'segmented'
}
}
);
const Tabs = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
TabsProps
>(({variant = 'segmented', ...props}, ref) => (
<TabsVariantContext.Provider value={variant}>
<TabsPrimitive.Root ref={ref} {...props} />
</TabsVariantContext.Provider>
));
Tabs.displayName = TabsPrimitive.Root.displayName;
const tabsListVariants = cva(
'inline-flex items-center justify-center text-muted-foreground',
{
variants: {
variant: {
segmented: 'h-[34px] rounded-lg bg-muted px-[3px]',
page: 'gap-2 border-b pb-2'
}
},
defaultVariants: {
variant: 'segmented'
}
}
);
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({className, ...props}, ref) => {
const variant = React.useContext(TabsVariantContext);
return (
<TabsPrimitive.List
ref={ref}
className={cn(tabsListVariants({variant, className}))}
{...props}
/>
);
});
TabsList.displayName = TabsPrimitive.List.displayName;
const tabsTriggerVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground',
{
variants: {
variant: {
segmented: 'h-7 data-[state=active]:shadow-md',
page: 'h-[34px] border data-[state=active]:bg-muted/70'
}
},
defaultVariants: {
variant: 'segmented'
}
}
);
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({className, ...props}, ref) => {
const variant = React.useContext(TabsVariantContext);
return (
<TabsPrimitive.Trigger
ref={ref}
className={cn(tabsTriggerVariants({variant, className}))}
{...props}
/>
);
});
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const tabsContentVariants = cva(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
{
variants: {
variant: {
segmented: '',
page: ''
}
},
defaultVariants: {
variant: 'segmented'
}
}
);
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({className, ...props}, ref) => {
const variant = React.useContext(TabsVariantContext);
return (
<TabsPrimitive.Content
ref={ref}
className={cn(tabsContentVariants({variant, className}))}
{...props}
/>
);
});
TabsContent.displayName = TabsPrimitive.Content.displayName;
export {Tabs, TabsList, TabsTrigger, TabsContent, tabsVariants};

View file

@ -83,7 +83,15 @@ export interface ButtonProps
}
```
That's it — you've just added a new component to Shade.
**Step 5:** Export the component
In order to be able to import the new component in apps, you'll need to export it in the `index.ts` file of Shade, like this:
```
export * from './components/ui/button';
```
That's it — you've just added a new component to Shade and made it ready to use in other React apps.
## Stories

View file

@ -1,3 +1,15 @@
// UI components
export * from './components/ui/button';
export {IconComponents as Icon} from './components/ui/icon';
export * from './components/ui/breadcrumb';
export * from './components/ui/dropdown-menu';
export * from './components/ui/card';
export * from './components/ui/tabs';
export * from './components/layout/page';
export * from './components/layout/heading';
// Assets
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';
@ -7,9 +19,11 @@ export {ReactComponent as XLogo} from './assets/images/x-logo.svg';
export {default as useGlobalDirtyState} from './hooks/useGlobalDirtyState';
// Utils
export * from '@/lib/utils';
export {debounce} from './utils/debounce';
export {formatUrl} from './utils/formatUrl';
export {default as ShadeApp} from './ShadeApp';
export type {ShadeAppProps} from './ShadeApp';
export {useFocusContext} from './providers/ShadeProvider';
export {useFocusContext} from './providers/ShadeProvider';

View file

@ -3,4 +3,4 @@ export const kebabToPascalCase = (str: string): string => {
const processed = str
.replace(/[-_]([a-z0-9])/gi, (_, char) => char.toUpperCase());
return processed.charAt(0).toUpperCase() + processed.slice(1);
};
};

View file

@ -80,42 +80,10 @@
font-weight: 100 900;
}
.shade-base {
.shade {
& {
@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;
}
}
}
@ -128,7 +96,7 @@
}
}
.shade-base {
.shade {
line-height: 1.5;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
@ -146,28 +114,28 @@
}
@media (max-width: 800px) {
.shade-base {
.shade {
height: calc(100vh - 55px);
}
}
.shade-base.dark {
.shade.dark {
color: #fafafb;
}
.shade-base.dark .gh-loading-orb-container {
.shade.dark .gh-loading-orb-container {
background-color: #000000;
}
.shade-base.dark .gh-loading-orb {
.shade.dark .gh-loading-orb {
filter: invert(100%);
}
.shade-base .no-scrollbar::-webkit-scrollbar {
.shade .no-scrollbar::-webkit-scrollbar {
display: none; /* Chrome */
}
.shade-base .no-scrollbar {
.shade .no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

View file

@ -287,7 +287,8 @@ module.exports = {
min: 'min-content',
max: 'max-content',
fit: 'fit-content',
prose: '65ch'
prose: '65ch',
page: '128rem'
},
borderRadius: {
sm: 'calc(var(--radius) - 4px)',

View file

@ -1,11 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./types"
},
"include": ["src"],
"exclude": ["src/**/*.stories.tsx"]
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./types"
},
"include": ["src"],
"exclude": ["src/**/*.stories.tsx"]
}

View file

@ -1,28 +1,28 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["vite/client"],
"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",
/* Bundler mode */
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
}

View file

@ -1,10 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "package.json"]
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "package.json"]
}

View file

@ -47,6 +47,10 @@ export default (function viteConfig() {
},
rollupOptions: {
external: (source) => {
if (source.startsWith('@/')) {
return false;
}
if (source.startsWith('.')) {
return false;
}

View file

@ -193,3 +193,11 @@ add|ember-template-lint|no-action|5|14|5|14|a90edd9a99596008f60bfcdbc6befe7fe8d2
remove|ember-template-lint|no-action|5|14|5|14|88b11bf43be33d97824ebac071a563affac0b97d|1730678400000|1741046400000|1746230400000|app/components/gh-psm-tags-input.hbs
add|ember-template-lint|no-action|80|92|80|92|f30d469e4ae668f05aca2f92a124a6b4748847a3|1730678400000|1741046400000|1746230400000|app/components/gh-post-settings-menu.hbs
remove|ember-template-lint|no-action|8|50|8|50|de589665046a78748832e8f93d2e495d1d259265|1728345600000|1738717200000|1743897600000|app/components/gh-mobile-nav-bar.hbs
add|ember-template-lint|no-action|12|16|12|16|3696846a8a04d429559abebaaf5dab2c6387c21f|1734307200000|1744671600000|1749855600000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-action|34|26|34|26|3b76c38861ddcdfaa277e272a1d27293c2659524|1734307200000|1744671600000|1749855600000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-invalid-interactive|34|26|34|26|0c04fbca90264398b6b5033632f3170c73d1769b|1734307200000|1744671600000|1749855600000|app/components/gh-nav-menu/main.hbs
add|ember-template-lint|no-invalid-link-title|29|20|29|20|593cda92786c7440169712eadac45a452b967dd5|1734307200000|1744671600000|1749855600000|app/components/gh-nav-menu/main.hbs
remove|ember-template-lint|no-action|10|110|10|110|4bbb6ad1f623335866ac715f34f2ce9829b1d55b|1728345600000|1738717200000|1743897600000|app/components/gh-nav-menu/main.hbs
remove|ember-template-lint|no-action|31|30|31|30|8eee88dbd40609f8ddc00330f4a47b1d30c483f0|1728345600000|1738717200000|1743897600000|app/components/gh-nav-menu/main.hbs
remove|ember-template-lint|no-invalid-interactive|31|30|31|30|929269f70336deb9f640bc0b5d54a0bd5336d27a|1728345600000|1738717200000|1743897600000|app/components/gh-nav-menu/main.hbs
remove|ember-template-lint|no-invalid-link-title|27|24|27|24|17a357b69040eb9e19a79fe08468c698eab84939|1728345600000|1738717200000|1743897600000|app/components/gh-nav-menu/main.hbs

View file

@ -0,0 +1 @@
<div {{react-render this.ReactComponent}}></div>

View file

@ -0,0 +1,8 @@
import AdminXComponent from './admin-x-component';
import {inject as service} from '@ember/service';
export default class Posts extends AdminXComponent {
@service upgradeStatus;
static packageName = '@tryghost/posts';
}

View file

@ -1,171 +1,200 @@
<div class="flex flex-column h-100" {{css-transition (unless @firstRender "gh-nav-main")}} data-test-nav-menu="main" ...attributes>
<div class="flex flex-column h-100" {{css-transition (unless @firstRender "gh-nav-main" )}} data-test-nav-menu="main"
...attributes>
{{#unless this.session.user.isContributor}}
<header class="gh-nav-menu">
<div class="gh-nav-menu-details">
<div class="gh-nav-menu-icon {{this.iconClass}}" style={{this.iconStyle}}></div>
<div class="gh-nav-menu-details-sitetitle">{{this.config.blogTitle}}</div>
</div>
<div class="gh-nav-menu-search">
<button class="gh-nav-btn-search" title="Search site (Ctrl/⌘ + K)" type="button" {{on "click" (action "openSearchModal")}} data-test-button="search"><span>{{svg-jar "search"}}</span></button>
</div>
</header>
<header class="gh-nav-menu">
<div class="gh-nav-menu-details">
<div class="gh-nav-menu-icon {{this.iconClass}}" style={{this.iconStyle}}></div>
<div class="gh-nav-menu-details-sitetitle">{{this.config.blogTitle}}</div>
</div>
<div class="gh-nav-menu-search">
<button class="gh-nav-btn-search" title="Search site (Ctrl/⌘ + K)" type="button" {{on "click"
(action "openSearchModal" )}} data-test-button="search"><span>{{svg-jar "search"}}</span></button>
</div>
</header>
{{/unless}}
<section class="gh-nav-body">
{{#unless this.session.user.isContributor}}
<div class="gh-nav-top">
<ul class="gh-nav-list gh-nav-main">
{{#if (feature "ActivityPub")}}
<li>
<LinkTo @route="activitypub-x" @current-when="activitypub-x">{{svg-jar "star"}}ActivityPub</LinkTo>
</li>
{{/if}}
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative gh-nav-list-home">
<LinkTo @route="dashboard" @alt="Dashboard" title="Dashboard" data-test-nav="dashboard">{{svg-jar "house"}} Dashboard</LinkTo>
</li>
{{/if}}
<li class="relative">
<span {{action "transitionToOrRefreshSite" on="click"}}>
<LinkTo @route="site" data-test-nav="site" @current-when={{this.isOnSite}} @preventDefault={{false}}>
{{svg-jar "view-site"}} View site
</LinkTo>
</span>
<a href="{{this.config.blogUrl}}/" class="gh-secondary-action" title="Open site in new tab" target="_blank" rel="noopener noreferrer">
<span>{{svg-jar "external"}}</span>
</a>
</li>
{{#if (and (gh-user-can-admin this.session.user) this.config.stats)}}
<li class="relative">
<LinkTo @route="stats">{{svg-jar "stats-outline"}}Stats</LinkTo>
</li>
{{/if}}
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative">
<a href="javascript:void(0)" class={{if this.explore.exploreWindowOpen "active"}} {{on "click" (fn this.toggleExploreWindow "")}} data-test-nav="explore">
{{svg-jar "globe-simple"}} Explore
</a>
</li>
{{/if}}
</ul>
<ul class="gh-nav-list gh-nav-manage">
<li class="gh-nav-list-new relative">
<GhLinkToCustomViewsIndex @route="posts" @query={{reset-query-params "posts"}} data-test-nav="posts">{{svg-jar "posts"}}Posts</GhLinkToCustomViewsIndex>
<LinkTo @route="lexical-editor.new" @model="post" class="gh-secondary-action gh-nav-new-post" @alt="New post" title="New post" data-test-nav="new-story"><span>{{svg-jar "plus"}}</span></LinkTo>
{{#if this.session.user.isAuthorOrContributor}}
{{#if this.customViews.forPosts}}
<ul class="gh-nav-view-list">
{{#each this.customViews.forPosts as |view|}}
<li>
<LinkTo @route="posts" @query={{reset-query-params "posts" view.filter}} data-test-nav-custom="{{view.route}}-{{view.name}}" title="{{view.name}}">
<span class="gh-nav-viewname">{{view.name}}</span>
<span class="flex items-center svg-{{view.color}}">
{{#unless view.icon}}
<span class="absolute circle"></span>
{{/unless}}
</span>
</LinkTo>
</li>
{{/each}}
</ul>
{{/if}}
{{else}}
{{#if this.customViews.forPosts}}
<button type="button" class="gh-nav-button-expand {{if this.navigation.settings.expanded.posts "expanded"}}" {{on "click" (fn this.navigation.toggleExpansion "posts")}} aria-label="{{if this.navigation.settings.expanded.posts "Collapse custom post types" "Expand custom post types"}}">
{{svg-jar (if this.navigation.settings.expanded.posts "arrow-down-stroke" "arrow-right-stroke")}}
</button>
{{#liquid-if this.navigation.settings.expanded.posts}}
<ul class="gh-nav-view-list">
{{#each this.customViews.forPosts as |view|}}
<li>
<LinkTo @route="posts" @query={{reset-query-params "posts" view.filter}} data-test-nav-custom="{{view.route}}-{{view.name}}" title="{{view.name}}">
<span class="gh-nav-viewname">{{view.name}}</span>
<span class="flex items-center svg-{{view.color}}">
{{#unless view.icon}}
<span class="absolute circle"></span>
{{/unless}}
</span>
</LinkTo>
</li>
{{/each}}
</ul>
{{/liquid-if}}
{{/if}}
{{/if}}
</li>
<li>
{{!-- clicking the Content link whilst on the content screen should reset the filter --}}
{{#if (eq this.router.currentRouteName "pages")}}
<LinkTo @route="pages" @query={{reset-query-params "pages"}} class="active" data-test-nav="pages">{{svg-jar "page"}}Pages</LinkTo>
{{else}}
<LinkTo @route="pages" data-test-nav="pages">{{svg-jar "page"}}Pages</LinkTo>
{{/if}}
</li>
{{#if this.showTagsNavigation}}
<li><LinkTo @route="tags" @current-when="tags tag tag.new" data-test-nav="tags">{{svg-jar "tag"}}Tags</LinkTo></li>
{{/if}}
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative">
{{#if (eq this.router.currentRouteName "members.index")}}
<LinkTo @route="members" @current-when="members member member.new" @query={{reset-query-params "members.index"}} data-test-nav="members">{{svg-jar "members"}}Members
{{#let (members-count-fetcher) as |count|}}
{{#unless count.isLoading}}
<span class="gh-nav-member-count">{{format-number count.count}}</span>
{{/unless}}
{{/let}}
</LinkTo>
{{else}}
<LinkTo @route="members" @current-when="members member member.new" data-test-nav="members">{{svg-jar "members"}}Members
{{#let (members-count-fetcher) as |count|}}
{{#unless count.isLoading}}
<span class="gh-nav-member-count">{{format-number count.count}}</span>
{{/unless}}
{{/let}}
</LinkTo>
{{/if}}
</li>
{{/if}}
{{#if (feature "adminXDemo")}}
<li>
<LinkTo @route="demo-x" @current-when="demo-x">{{svg-jar "star"}}AdminX Demo</LinkTo>
</li>
{{/if}}
</ul>
{{#if this.session.user.isOwnerOnly}}
<ul class="gh-nav-list">
{{#if this.showBilling}}
<li class="relative">
<a href="javascript:void(0)" class={{if this.billing.billingWindowOpen "active"}} {{action "toggleBillingModal" }} data-test-nav="billing">
{{svg-jar "credit-card"}} Ghost(Pro)
</a>
</li>
<li class="relative gh-nav-pro">
<GhBillingUpdateButton />
</li>
{{/if}}
</ul>
<div class="gh-nav-top">
<ul class="gh-nav-list gh-nav-main">
{{#if (feature "ActivityPub")}}
<li>
<LinkTo @route="activitypub-x" @current-when="activitypub-x">{{svg-jar "star"}}ActivityPub</LinkTo>
</li>
{{/if}}
{{#if this.showMenuExtension}}
<ul class="gh-nav-list gh-nav-settings">
{{#if this.config.clientExtensions.menu.title}}
<li class="gh-nav-list-h">{{this.config.clientExtensions.menu.title}}</li>
{{/if}}
{{#each this.config.clientExtensions.menu.items as |menuItem| }}
<li>
<a href="{{menuItem.href}}" target="_blank" rel="noopener noreferrer">{{svg-jar menuItem.icon}}{{menuItem.text}}</a>
</li>
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative gh-nav-list-home">
<LinkTo @route="dashboard" @alt="Dashboard" title="Dashboard" data-test-nav="dashboard">{{svg-jar
"house"}} Dashboard</LinkTo>
</li>
{{/if}}
<li class="relative">
<span {{action "transitionToOrRefreshSite" on="click" }}>
<LinkTo @route="site" data-test-nav="site" @current-when={{this.isOnSite}}
@preventDefault={{false}}>
{{svg-jar "view-site"}} View site
</LinkTo>
</span>
<a href="{{this.config.blogUrl}}/" class="gh-secondary-action" title="Open site in new tab"
target="_blank" rel="noopener noreferrer">
<span>{{svg-jar "external"}}</span>
</a>
</li>
{{#if (and (gh-user-can-admin this.session.user) this.config.stats)}}
<li class="relative">
<LinkTo @route="stats">{{svg-jar "stats-outline"}}Stats</LinkTo>
</li>
{{/if}}
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative">
<a href="javascript:void(0)" class={{if this.explore.exploreWindowOpen "active" }} {{on "click" (fn
this.toggleExploreWindow "" )}} data-test-nav="explore">
{{svg-jar "globe-simple"}} Explore
</a>
</li>
{{/if}}
</ul>
<ul class="gh-nav-list gh-nav-manage">
<li class="gh-nav-list-new relative">
<GhLinkToCustomViewsIndex @route="posts" @query={{reset-query-params "posts" }}
data-test-nav="posts">{{svg-jar "posts"}}Posts</GhLinkToCustomViewsIndex>
<LinkTo @route="lexical-editor.new" @model="post" class="gh-secondary-action gh-nav-new-post"
@alt="New post" title="New post" data-test-nav="new-story"><span>{{svg-jar "plus"}}</span>
</LinkTo>
{{#if this.session.user.isAuthorOrContributor}}
{{#if this.customViews.forPosts}}
<ul class="gh-nav-view-list">
{{#each this.customViews.forPosts as |view|}}
<li>
<LinkTo @route="posts" @query={{reset-query-params "posts" view.filter}}
data-test-nav-custom="{{view.route}}-{{view.name}}" title="{{view.name}}">
<span class="gh-nav-viewname">{{view.name}}</span>
<span class="flex items-center svg-{{view.color}}">
{{#unless view.icon}}
<span class="absolute circle"></span>
{{/unless}}
</span>
</LinkTo>
</li>
{{/each}}
</ul>
{{/if}}
{{else}}
{{#if this.customViews.forPosts}}
<button type="button" class="gh-nav-button-expand {{if this.navigation.settings.expanded.posts "
expanded"}}" {{on "click" (fn this.navigation.toggleExpansion "posts" )}}
aria-label="{{if this.navigation.settings.expanded.posts " Collapse custom post
types" "Expand custom post types" }}">
{{svg-jar (if this.navigation.settings.expanded.posts "arrow-down-stroke"
"arrow-right-stroke")}}
</button>
{{#liquid-if this.navigation.settings.expanded.posts}}
<ul class="gh-nav-view-list">
{{#each this.customViews.forPosts as |view|}}
<li>
<LinkTo @route="posts" @query={{reset-query-params "posts" view.filter}}
data-test-nav-custom="{{view.route}}-{{view.name}}" title="{{view.name}}">
<span class="gh-nav-viewname">{{view.name}}</span>
<span class="flex items-center svg-{{view.color}}">
{{#unless view.icon}}
<span class="absolute circle"></span>
{{/unless}}
</span>
</LinkTo>
</li>
{{/each}}
</ul>
{{/liquid-if}}
{{/if}}
{{/if}}
</li>
<li>
{{!-- clicking the Content link whilst on the content screen should reset the filter --}}
{{#if (eq this.router.currentRouteName "pages")}}
<LinkTo @route="pages" @query={{reset-query-params "pages" }} class="active" data-test-nav="pages">
{{svg-jar "page"}}Pages</LinkTo>
{{else}}
<LinkTo @route="pages" data-test-nav="pages">{{svg-jar "page"}}Pages</LinkTo>
{{/if}}
</li>
{{#if this.showTagsNavigation}}
<li>
<LinkTo @route="tags" @current-when="tags tag tag.new" data-test-nav="tags">{{svg-jar "tag"}}Tags
</LinkTo>
</li>
{{/if}}
</div>
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative">
{{#if (eq this.router.currentRouteName "members.index")}}
<LinkTo @route="members" @current-when="members member member.new"
@query={{reset-query-params "members.index" }} data-test-nav="members">{{svg-jar
"members"}}Members
{{#let (members-count-fetcher) as |count|}}
{{#unless count.isLoading}}
<span class="gh-nav-member-count">{{format-number count.count}}</span>
{{/unless}}
{{/let}}
</LinkTo>
{{else}}
<LinkTo @route="members" @current-when="members member member.new" data-test-nav="members">{{svg-jar
"members"}}Members
{{#let (members-count-fetcher) as |count|}}
{{#unless count.isLoading}}
<span class="gh-nav-member-count">{{format-number count.count}}</span>
{{/unless}}
{{/let}}
</LinkTo>
{{/if}}
</li>
{{/if}}
{{#if (feature "adminXDemo")}}
<li>
<LinkTo @route="demo-x" @current-when="demo-x">{{svg-jar "star"}}AdminX Demo</LinkTo>
</li>
{{/if}}
{{#if (feature "postsX")}}
<li>
<LinkTo @route="posts-x" @current-when="posts-x">{{svg-jar "chart"}}Post analytics</LinkTo>
</li>
{{/if}}
</ul>
{{#if this.session.user.isOwnerOnly}}
<ul class="gh-nav-list">
{{#if this.showBilling}}
<li class="relative">
<a href="javascript:void(0)" class={{if this.billing.billingWindowOpen "active" }}
{{action "toggleBillingModal" }} data-test-nav="billing">
{{svg-jar "credit-card"}} Ghost(Pro)
</a>
</li>
<li class="relative gh-nav-pro">
<GhBillingUpdateButton />
</li>
{{/if}}
</ul>
{{/if}}
{{#if this.showMenuExtension}}
<ul class="gh-nav-list gh-nav-settings">
{{#if this.config.clientExtensions.menu.title}}
<li class="gh-nav-list-h">{{this.config.clientExtensions.menu.title}}</li>
{{/if}}
{{#each this.config.clientExtensions.menu.items as |menuItem| }}
<li>
<a href="{{menuItem.href}}" target="_blank" rel="noopener noreferrer">{{svg-jar
menuItem.icon}}{{menuItem.text}}</a>
</li>
{{/each}}
</ul>
{{/if}}
</div>
{{/unless}}
<GhNavMenu::Footer />
</section>
</div>
</div>

View file

@ -0,0 +1,3 @@
import Controller from '@ember/controller';
export default class PostsXController extends Controller {}

View file

@ -52,6 +52,10 @@ Router.map(function () {
this.route('demo-x', {path: '/*sub'});
});
this.route('posts-x', function () {
this.route('posts-x', {path: '/*sub'});
});
this.route('settings-x', {path: '/settings'}, function () {
this.route('settings-x', {path: '/*sub'});
});

View file

@ -0,0 +1,3 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default class PostsXRoute extends AuthenticatedRoute {}

View file

@ -76,6 +76,7 @@ export default class FeatureService extends Service {
@feature('editorExcerpt') editorExcerpt;
@feature('contentVisibility') contentVisibility;
@feature('commentImprovements') commentImprovements;
@feature('postsX') postsX;
_user = null;

View file

@ -0,0 +1 @@
<AdminX::Posts />

View file

@ -6,7 +6,7 @@ const fs = require('fs');
const path = require('path');
const camelCase = require('lodash/camelCase');
const adminXApps = ['admin-x-demo', 'admin-x-settings', 'admin-x-activitypub'];
const adminXApps = ['admin-x-demo', 'admin-x-settings', 'admin-x-activitypub', 'posts'];
function generateHash(filePath) {
const fileContents = fs.readFileSync(filePath, 'utf8');

View file

@ -190,7 +190,8 @@
"projects": [
"@tryghost/admin-x-demo",
"@tryghost/admin-x-settings",
"@tryghost/admin-x-activitypub"
"@tryghost/admin-x-activitypub",
"@tryghost/posts"
],
"target": "build"
}
@ -207,7 +208,8 @@
"projects": [
"@tryghost/admin-x-demo",
"@tryghost/admin-x-settings",
"@tryghost/admin-x-activitypub"
"@tryghost/admin-x-activitypub",
"@tryghost/posts"
],
"target": "build"
}

View file

@ -52,7 +52,8 @@ const ALPHA_FEATURES = [
'collectionsCard',
'lexicalIndicators',
'adminXDemo',
'commentImprovements'
'commentImprovements',
'postsX'
];
module.exports.GA_KEYS = [...GA_FEATURES];

View file

@ -29,6 +29,7 @@ Object {
"members": true,
"newEmailAddresses": true,
"outboundLinkTagging": true,
"postsX": true,
"stripeAutomaticTax": true,
"themeErrorsNotification": true,
"urlCache": true,

182
yarn.lock
View file

@ -4230,6 +4230,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
"@radix-ui/primitive@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==
"@radix-ui/react-arrow@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d"
@ -4245,6 +4250,13 @@
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-arrow@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz#2103721933a8bfc6e53bbfbdc1aaad5fc8ba0dd7"
integrity sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-avatar@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz#457c81334c93f4608df15f081e7baa286558d6a2"
@ -4290,6 +4302,16 @@
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-collection@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0"
integrity sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-compose-refs@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
@ -4302,6 +4324,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
"@radix-ui/react-compose-refs@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
"@radix-ui/react-context@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
@ -4314,6 +4341,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8"
integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==
"@radix-ui/react-context@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
"@radix-ui/react-direction@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b"
@ -4349,6 +4381,30 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dismissable-layer@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz#771594b202f32bc8ffeb278c565f10c513814aee"
integrity sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dropdown-menu@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.3.tgz#02665f99bfdcefc33a8a15dc130e9b98ebdf7671"
integrity sha512-eKyAfA9e4HOavzyGJC6kiDIlHMPzAU0zqSqTg+VwS0Okvb9nkTo7L4TugkCUqM3I06ciSpdtYQ73cgB7tyUgVw==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-menu" "2.1.3"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-focus-guards@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
@ -4361,6 +4417,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13"
integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==
"@radix-ui/react-focus-guards@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe"
integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==
"@radix-ui/react-focus-scope@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz#9c2e8d4ed1189a1d419ee61edd5c1828726472f9"
@ -4380,6 +4441,15 @@
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-focus-scope@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb"
integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-form@0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.0.3.tgz#328e7163e723ccc748459d66a2d685d7b4f85d5a"
@ -4416,6 +4486,30 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-menu@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.3.tgz#5a3330cf5dc5d48666da31ba0e83fef99288e367"
integrity sha512-wY5SY6yCiJYP+DMIy7RrjF4shoFpB9LJltliVwejBm8T2yepWDJgKBhIFYOGWYR/lFHOCtbstN9duZFu6gmveQ==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-collection" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.2"
"@radix-ui/react-focus-guards" "1.1.1"
"@radix-ui/react-focus-scope" "1.1.1"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-popper" "1.2.1"
"@radix-ui/react-portal" "1.1.3"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-roving-focus" "1.1.1"
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.6.0"
"@radix-ui/react-popover@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.1.tgz#604b783cdb3494ed4f16a58c17f0e81e61ab7775"
@ -4470,6 +4564,22 @@
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-popper@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz#2fc66cfc34f95f00d858924e3bee54beae2dff0a"
integrity sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==
dependencies:
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-rect" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-portal@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1"
@ -4486,6 +4596,14 @@
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-portal@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440"
integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478"
@ -4494,6 +4612,14 @@
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-primitive@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
@ -4509,6 +4635,13 @@
dependencies:
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-primitive@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e"
integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==
dependencies:
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-radio-group@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.2.0.tgz#f937dd6b9436ded80c4bebdf3901c20cb8bcbb5a"
@ -4556,6 +4689,21 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-roving-focus@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520"
integrity sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-collection" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-select@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-1.2.2.tgz#caa981fa0d672cf3c1b2a5240135524e69b32181"
@ -4614,6 +4762,13 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-slot@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3"
integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-switch@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.0.tgz#fcf8e778500f1d60d4b2bec2fc3fad77a7c118e3"
@ -4641,6 +4796,20 @@
"@radix-ui/react-roving-focus" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-tabs@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz#a72da059593cba30fccb30a226d63af686b32854"
integrity sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-roving-focus" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-toggle-group@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec"
@ -27118,7 +27287,7 @@ react-refresh@^0.14.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4:
react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c"
integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==
@ -27148,6 +27317,17 @@ react-remove-scroll@2.5.7:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-remove-scroll@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07"
integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==
dependencies:
react-remove-scroll-bar "^2.3.6"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-select@5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.2.tgz#0d7ccb1895d61aafcd090fbf65aa9e506225a854"