Commit cb9a1b78 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Remove pattern-library assets

parent cb3efb8d
import { toChildArray } from 'preact';
import { jsxToString } from '../util/jsx-to-string';
/**
* Components to render patterns and examples. Typical structure of a "Demo"
* page might be:
*
* <PatternPage title="Zoo Animals">
* <Pattern title="Elephant">
* <p>Everyone loves an elephant.</p>
*
* <PatternExamples title="Colored elephants">
* <PatternExample details="Pink elephant">
* <Elephant color="pink" />
* </PatternExample>
* <PatternExample details="Blue elephant">
* <Elephant color="blue" />
* </PatternExample>
* </PatternExamples>
*
* <PatternExamples title="Patterned elephants">
* <PatternExample details="Spotted elephant">
* <Elephant variant="spotted" />
* </PatternExample>
* </PatternExamples>
* </Pattern>
*
* <Pattern title="Monkeys">
* <PatternExamples>
* <PatternExample details="Your basic monkey">
* <Monkey />
* </PatternExample>
* </PatternExamples>
* </Pattern>
* </PatternPage>
*/
/**
* @typedef PatternPageProps
* @prop {import("preact").ComponentChildren} children
* @prop {string} title
*/
/**
* Render a page of patterns
* @param {PatternPageProps} props
*/
export function PatternPage({ children, title }) {
return (
<section className="PatternPage">
<h1>{title}</h1>
<div className="PatternPage__content">{children}</div>
</section>
);
}
/**
* @typedef PatternProps
* @prop {import("preact").ComponentChildren} children
* @prop {string} title
*/
/**
* A pattern and its examples
* @param {PatternProps} props
*/
export function Pattern({ children, title }) {
return (
<section className="Pattern">
<h2>{title}</h2>
{children}
</section>
);
}
/**
* @typedef PatternExamplesProps
* @prop {import("preact").ComponentChildren} children
* @prop {string} [title]
*/
/**
* Tabular layout of one or more `PatternExample` components, with optional
* title.
*
* @param {PatternExamplesProps} props
*/
export function PatternExamples({ children }) {
return (
<table className="PatternExamples">
<tr>
<th>Example</th>
<th>Details</th>
<th>Source</th>
</tr>
{children}
</table>
);
}
/**
* @typedef PatternExampleProps
* @prop {import("preact").ComponentChildren} children - Example source
* @prop {string} details - Details about the pattern example
* @prop {Object} [style] - Optional additional inline styles to apply to the
* table cell with the rendered example
*/
/**
* Tabular layout of a single example. Will render its `children` as the
* example, as well as the source JSX of its `children`
*
* @param {PatternExampleProps} props
*/
export function PatternExample({ children, details, style = {} }) {
const source = toChildArray(children).map((child, idx) => {
return (
<li key={idx}>
<code>
<pre>{jsxToString(child)}</pre>
</code>
</li>
);
});
return (
<tr>
<td style={style}>
<div className="PatternExample__example">{children}</div>
</td>
<td>{details}</td>
<td>
<ul>{source}</ul>
</td>
</tr>
);
}
import { SvgIcon } from '@hypothesis/frontend-shared';
import SharedButtonPatterns from './patterns/SharedButtonPatterns';
import { useRoute } from '../router';
/**
* @typedef PlaygroundRoute - Route "handler" that provides a component (function)
* that should be rendered for the indicated route
* @prop {RegExp|string} route - Pattern or string path relative to
* `baseURL`, e.g. '/my-patterns'
* @prop {string} title
* @prop {import("preact").FunctionComponent<{}>} component
*/
function HomeRoute() {
return (
<>
<h1 className="heading">UI component playground</h1>
<p>Select a component to view examples.</p>
</>
);
}
/** @type {PlaygroundRoute[]} */
const routes = [
{
route: /^\/?$/,
title: 'Home',
component: HomeRoute,
},
{
route: '/shared-buttons',
title: 'Buttons',
component: SharedButtonPatterns,
},
];
const demoRoutes = routes.filter(r => r.component !== HomeRoute);
/**
* @typedef PlaygroundAppProps
* @prop {string} [baseURL]
* @prop {PlaygroundRoute[]} [extraRoutes] - Local-/application-specific routes
* to add to this pattern library in addition to the shared/common routes
*/
/**
* Render web content for the playground application. This includes the "frame"
* around the page and a navigation channel, as well as the content rendered
* by the component handling the current route.
*
* @param {PlaygroundAppProps} props
*/
export default function PlaygroundApp({
baseURL = '/ui-playground',
extraRoutes = [],
}) {
const allRoutes = routes.concat(extraRoutes);
const [route, navigate] = useRoute(baseURL, allRoutes);
const content = route ? (
<route.component />
) : (
<>
<h1 className="heading">:(</h1>
<p>Page not found.</p>
</>
);
return (
<main className="PlaygroundApp">
<div className="PlaygroundApp__sidebar">
<div className="PlaygroundApp__sidebar-home">
<a href={baseURL} onClick={e => navigate(e, '/')}>
<SvgIcon name="logo" />
</a>
</div>
<h2>Common Patterns</h2>
<ul>
{demoRoutes.map(c => (
<li key={c.route}>
<a
className="PlaygroundApp__nav-link"
key={c.route}
href={/** @type string */ (c.route)}
onClick={e => navigate(e, c.route)}
>
{c.title}
</a>
</li>
))}
</ul>
{extraRoutes.length && (
<>
<h2>Application Patterns</h2>
<ul>
{extraRoutes.map(c => (
<li key={c.route}>
<a
className="PlaygroundApp__nav-link"
key={c.route}
href={/** @type string */ (c.route)}
onClick={e => navigate(e, c.route)}
>
{c.title}
</a>
</li>
))}
</ul>
</>
)}
</div>
<div className="PlaygroundApp__content">{content}</div>
</main>
);
}
import { useEffect, useMemo, useState } from 'preact/hooks';
function routeFromCurrentURL(baseURL) {
return location.pathname.slice(baseURL.length);
}
export function useRoute(baseURL, routes) {
const [route, setRoute] = useState(() => routeFromCurrentURL(baseURL));
const routeData = useMemo(() => routes.find(r => route.match(r.route)), [
route,
routes,
]);
const title = `${routeData.title}: Hypothesis UI playground`;
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => {
const popstateListener = () => {
setRoute(routeFromCurrentURL(baseURL));
};
window.addEventListener('popstate', popstateListener);
return () => {
window.removeEventListener('popstate', popstateListener);
};
}, [baseURL]);
const navigate = (event, route) => {
event.preventDefault();
history.pushState({}, title, baseURL + route);
setRoute(route);
};
return [routeData, navigate];
}
/**
* Escape `str` for use in a "-quoted string.
*
* @param {string} str
*/
function escapeQuotes(str) {
return str.replace(/"/g, '\\"');
}
function componentName(type) {
if (typeof type === 'string') {
return type;
} else {
return type.displayName || type.name;
}
}
/**
* Indent a multi-line string by `indent` spaces.
*
* @param {string} str
* @param {number} indent
*/
function indentLines(str, indent) {
const indentStr = ' '.repeat(indent);
const lines = str.split('\n');
return lines.map(line => indentStr + line).join('\n');
}
/**
* Render a JSX expression as a code string.
*
* Currently this only supports serializing props with simple types (strings,
* booleans, numbers).
*
* @example
* jsxToString(<Widget expanded={true} label="Thing"/>) // returns `<Widget expanded label="Thing"/>`
*
* @param {import('preact').ComponentChildren} vnode
* @return {string}
*/
export function jsxToString(vnode) {
if (
typeof vnode === 'string' ||
typeof vnode === 'number' ||
typeof vnode === 'bigint'
) {
return vnode.toString();
} else if (typeof vnode === 'boolean') {
return '';
} else if (vnode && 'type' in vnode) {
const name = componentName(vnode.type);
// nb. The special `key` and `ref` props are not included in `vnode.props`.
// `ref` is not serializable to a string and `key` is generally set dynamically
// (eg. from an index or item ID) so it doesn't make sense to include it either.
let propStr = Object.entries(vnode.props)
.map(([name, value]) => {
if (name === 'children') {
return '';
}
// Boolean props are assumed to default to `false`, in which case they
// can be omitted.
if (typeof value === 'boolean') {
return value ? name : '';
}
const valueStr =
typeof value === 'string' ? `"${escapeQuotes(value)}"` : `{${value}}`;
return `${name}=${valueStr}`;
})
.join(' ')
.trim();
if (propStr.length > 0) {
propStr = ' ' + propStr;
}
const children = vnode.props.children;
if (children) {
let childrenStr = Array.isArray(children)
? children.map(jsxToString).join('\n')
: jsxToString(children);
childrenStr = indentLines(childrenStr, 2);
return `<${name}${propStr}>\n${childrenStr}\n</${name}>`;
} else {
// No children - use a self-closing tag.
return `<${name}${propStr} />`;
}
} else {
return '';
}
}
@use '../reset';
// TODO: Remove dependencies here
@use '../sidebar/elements';
@use '../variables' as var;
// Shared styles from frontend-shared package
@use '@hypothesis/frontend-shared/styles';
// Shared styles from components/styles temporarily still in this repository
@use '../shared';
body {
font-size: 100%;
}
h1 {
font-size: 1.75em;
font-weight: bold;
}
h2 {
font-size: 1.25em;
width: 100%;
border-left: 6px solid var.$color-brand;
font-weight: bold;
padding-left: 0.5em;
}
h3 {
font-size: 1.125em;
font-weight: normal;
font-style: italic;
margin: 1em 0;
}
h4 {
font-size: 1em;
font-weight: 500;
font-style: italic;
}
pre {
margin: 0;
}
// Utilities
.u-center {
align-self: center;
}
// Component styles
.PlaygroundApp {
display: grid;
grid-template-areas:
'navigation'
'content';
&__content {
padding: 1em;
}
&__sidebar {
grid-area: 'navigation';
max-height: 20em;
overflow: scroll;
background-color: var.$grey-2;
}
&__sidebar-home {
text-align: center;
padding: 1em;
}
&__content {
grid-area: 'content';
}
}
.PlaygroundApp__nav-link {
width: 100%;
padding: 1em;
font-size: 16px; // TODO: variable later
border-left: 5px solid transparent;
&:hover {
background-color: var.$grey-3;
}
}
.PatternPage {
font-size: 14px;
&__content {
padding: 2em 0;
}
}
.Pattern {
margin: 0 0 4em 1em;
p {
margin: 1em;
margin-left: 0;
}
& > p:first-of-type {
font-size: 125%;
}
h2 {
margin-left: -1em;
}
}
.PatternExamples {
border: 1px solid var.$grey-3;
border-collapse: collapse;
width: 100%;
margin-top: 1em;
margin-bottom: 2em;
th,
td {
padding: 0.5em;
}
th {
background-color: var.$grey-1;
border-bottom: 1px solid var.$grey-3;
text-align: left;
}
td {
padding-top: 10px;
padding-bottom: 10px;
}
tr:nth-child(even) {
background: var.$grey-0;
}
code {
color: var.$grey-mid;
}
}
.PatternExample {
&__example {
display: flex;
align-items: center;
& > * + * {
margin-left: 1em;
}
}
}
// Element styles
body {
font-family: sans-serif;
}
@media screen and (min-width: 60em) {
.PlaygroundApp {
height: 100vh;
grid-template-columns: 20em auto;
column-gap: 1em;
grid-template-areas: 'navigation content';
&__sidebar {
max-height: none;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment