summaryrefslogtreecommitdiff
path: root/preact/demo/people
diff options
context:
space:
mode:
Diffstat (limited to 'preact/demo/people')
-rw-r--r--preact/demo/people/Readme.md3
-rw-r--r--preact/demo/people/index.tsx59
-rw-r--r--preact/demo/people/profile.tsx59
-rw-r--r--preact/demo/people/router.tsx153
-rw-r--r--preact/demo/people/store.ts83
-rw-r--r--preact/demo/people/styles/animations.scss34
-rw-r--r--preact/demo/people/styles/app.scss100
-rw-r--r--preact/demo/people/styles/avatar.scss16
-rw-r--r--preact/demo/people/styles/button.scss115
-rw-r--r--preact/demo/people/styles/index.scss168
-rw-r--r--preact/demo/people/styles/profile.scss26
11 files changed, 816 insertions, 0 deletions
diff --git a/preact/demo/people/Readme.md b/preact/demo/people/Readme.md
new file mode 100644
index 0000000..c18a1cb
--- /dev/null
+++ b/preact/demo/people/Readme.md
@@ -0,0 +1,3 @@
+# People demo page
+
+This section of our demo was originally made by [phaux](https://github.com/phaux) in the [web-app-boilerplate](https://github.com/phaux/web-app-boilerplate) repo. It has been slightly modified from it's original to better work inside of our demo app
diff --git a/preact/demo/people/index.tsx b/preact/demo/people/index.tsx
new file mode 100644
index 0000000..0e728ab
--- /dev/null
+++ b/preact/demo/people/index.tsx
@@ -0,0 +1,59 @@
+import { observer } from 'mobx-react';
+import { Component, h } from 'preact';
+import { Profile } from './profile';
+import { Link, Route, Router } from './router';
+import { store } from './store';
+
+import './styles/index.scss';
+
+@observer
+export default class App extends Component {
+ componentDidMount() {
+ store.loadUsers().catch(console.error);
+ }
+
+ render() {
+ return (
+ <Router>
+ <div id="people-app">
+ <nav>
+ <div style={{ margin: 16, textAlign: 'center' }}>
+ Sort by{' '}
+ <select
+ value={store.usersOrder}
+ onChange={(ev: any) => {
+ store.setUsersOrder(ev.target.value);
+ }}
+ >
+ <option value="name">Name</option>
+ <option value="id">ID</option>
+ </select>
+ </div>
+ <ul>
+ {store.getSortedUsers().map((user, i) => (
+ <li
+ key={user.id}
+ style={{
+ animationDelay: `${i * 20}ms`,
+ top: `calc(var(--menu-item-height) * ${i})`,
+ transitionDelay: `${i * 20}ms`
+ }}
+ >
+ <Link href={`people/${user.id}`} active>
+ <img class="avatar" src={user.picture.large} />
+ {user.name.first} {user.name.last}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </nav>
+ <section id="people-main">
+ <Route match="people">
+ <Route match="*" component={Profile} />
+ </Route>
+ </section>
+ </div>
+ </Router>
+ );
+ }
+}
diff --git a/preact/demo/people/profile.tsx b/preact/demo/people/profile.tsx
new file mode 100644
index 0000000..e1f44ea
--- /dev/null
+++ b/preact/demo/people/profile.tsx
@@ -0,0 +1,59 @@
+import { computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Component, h } from 'preact';
+import { RouteChildProps } from './router';
+import { store } from './store';
+
+export type ProfileProps = RouteChildProps;
+@observer
+export class Profile extends Component<ProfileProps> {
+ @observable id = '';
+ @observable busy = false;
+
+ componentDidMount() {
+ this.id = this.props.route;
+ }
+
+ componentWillReceiveProps(props: ProfileProps) {
+ this.id = props.route;
+ }
+
+ render() {
+ const user = this.user;
+ if (user == null) return null;
+ return (
+ <div class="profile">
+ <img class="avatar" src={user.picture.large} />
+ <h2>
+ {user.name.first} {user.name.last}
+ </h2>
+ <div class="details">
+ <p>
+ {user.gender === 'female' ? '👩' : '👨'} {user.id}
+ </p>
+ <p>🖂 {user.email}</p>
+ </div>
+ <p>
+ <button
+ class={this.busy ? 'secondary busy' : 'secondary'}
+ disabled={this.busy}
+ onClick={this.remove}
+ >
+ Remove contact
+ </button>
+ </p>
+ </div>
+ );
+ }
+
+ @computed get user() {
+ return store.users.find(u => u.id === this.id);
+ }
+
+ remove = async () => {
+ this.busy = true;
+ await new Promise<void>(cb => setTimeout(cb, 1500));
+ store.deleteUser(this.id);
+ this.busy = false;
+ };
+}
diff --git a/preact/demo/people/router.tsx b/preact/demo/people/router.tsx
new file mode 100644
index 0000000..56fc420
--- /dev/null
+++ b/preact/demo/people/router.tsx
@@ -0,0 +1,153 @@
+import {
+ ComponentChild,
+ ComponentFactory,
+ createContext,
+ FunctionalComponent,
+ h,
+ JSX
+} from 'preact';
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState
+} from 'preact/hooks';
+
+export type RouterData = {
+ match: string[];
+ path: string[];
+ navigate(path: string): void;
+};
+
+const RouterContext = createContext<RouterData>({
+ match: [],
+ path: [],
+ navigate() {}
+});
+export const useRouter = () => useContext(RouterContext);
+
+const useLocation = (cb: () => void) => {
+ useEffect(() => {
+ window.addEventListener('popstate', cb);
+ return () => {
+ window.removeEventListener('popstate', cb);
+ };
+ }, [cb]);
+};
+
+export const Router: FunctionalComponent = props => {
+ const [path, setPath] = useState(location.pathname);
+
+ const update = useCallback(() => {
+ setPath(location.pathname);
+ }, [setPath]);
+
+ useLocation(update);
+
+ const navigate = useCallback(
+ (path: string) => {
+ history.pushState(null, '', path);
+ update();
+ },
+ [update]
+ );
+
+ const router = useMemo<RouterData>(
+ () => ({
+ match: [],
+ navigate,
+ path: path.split('/').filter(Boolean)
+ }),
+ [navigate, path]
+ );
+
+ return <RouterContext.Provider children={props.children} value={router} />;
+};
+
+export type RouteChildProps = { route: string };
+export type RouteProps = {
+ component?: ComponentFactory<RouteChildProps>;
+ match: string;
+ render?(route: string): ComponentChild;
+};
+export const Route: FunctionalComponent<RouteProps> = props => {
+ const router = useRouter();
+ const [dir, ...subpath] = router.path;
+
+ if (dir == null) return null;
+ if (props.match !== '*' && dir !== props.match) return null;
+
+ const children = useMemo(() => {
+ if (props.component) return <props.component key={dir} route={dir} />;
+ if (props.render) return props.render(dir);
+ return props.children;
+ }, [props.component, props.render, props.children, dir]);
+
+ const innerRouter = useMemo<RouterData>(
+ () => ({
+ ...router,
+ match: [...router.match, dir],
+ path: subpath
+ }),
+ [router.match, dir, subpath.join('/')]
+ );
+
+ return <RouterContext.Provider children={children} value={innerRouter} />;
+};
+
+export type LinkProps = JSX.HTMLAttributes & {
+ active?: boolean | string;
+};
+export const Link: FunctionalComponent<LinkProps> = props => {
+ const router = useRouter();
+
+ const classProps = [props.class, props.className];
+ const originalClasses = useMemo(() => {
+ const classes = [];
+ for (const prop of classProps) if (prop) classes.push(...prop.split(/\s+/));
+ return classes;
+ }, classProps);
+
+ const activeClass = useMemo(() => {
+ if (!props.active || props.href == null) return undefined;
+ const href = props.href.split('/').filter(Boolean);
+ const path =
+ props.href[0] === '/' ? [...router.match, ...router.path] : router.path;
+ const isMatch = href.every((dir, i) => dir === path[i]);
+ if (isMatch) return props.active === true ? 'active' : props.active;
+ }, [originalClasses, props.active, props.href, router.match, router.path]);
+
+ const classes =
+ activeClass == null ? originalClasses : [...originalClasses, activeClass];
+
+ const getHref = useCallback(() => {
+ if (props.href == null || props.href[0] === '/') return props.href;
+ const path = props.href.split('/').filter(Boolean);
+ return '/' + [...router.match, ...path].join('/');
+ }, [router.match, props.href]);
+
+ const handleClick = useCallback(
+ (ev: MouseEvent) => {
+ const href = getHref();
+ if (props.onClick != null) props.onClick(ev);
+ if (ev.defaultPrevented) return;
+ if (href == null) return;
+ if (ev.button !== 0) return;
+ if (props.target != null && props.target !== '_self') return;
+ if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) return;
+ ev.preventDefault();
+ router.navigate(href);
+ },
+ [getHref, router.navigate, props.onClick, props.target]
+ );
+
+ return (
+ <a
+ {...props}
+ class={classes.join(' ')}
+ href={getHref()}
+ onClick={handleClick}
+ />
+ );
+};
diff --git a/preact/demo/people/store.ts b/preact/demo/people/store.ts
new file mode 100644
index 0000000..1967afd
--- /dev/null
+++ b/preact/demo/people/store.ts
@@ -0,0 +1,83 @@
+import { flow, Instance, types } from 'mobx-state-tree';
+
+const cmp = <T, U>(fn: (x: T) => U) => (a: T, b: T): number =>
+ fn(a) > fn(b) ? 1 : -1;
+
+const User = types.model({
+ email: types.string,
+ gender: types.enumeration(['male', 'female']),
+ id: types.identifier,
+ name: types.model({
+ first: types.string,
+ last: types.string
+ }),
+ picture: types.model({
+ large: types.string
+ })
+});
+
+const Store = types
+ .model({
+ users: types.array(User),
+ usersOrder: types.enumeration(['name', 'id'])
+ })
+ .views(self => ({
+ getSortedUsers() {
+ if (self.usersOrder === 'name')
+ return self.users.slice().sort(cmp(x => x.name.first));
+ if (self.usersOrder === 'id')
+ return self.users.slice().sort(cmp(x => x.id));
+ throw Error(`Unknown ordering ${self.usersOrder}`);
+ }
+ }))
+ .actions(self => ({
+ addUser: flow<unknown, []>(function*() {
+ const data = yield fetch('https://randomuser.me/api?results=1')
+ .then(res => res.json())
+ .then(data =>
+ data.results.map((user: any) => ({
+ ...user,
+ id: user.login.username
+ }))
+ );
+ self.users.push(...data);
+ }),
+ loadUsers: flow<unknown, []>(function*() {
+ const data = yield fetch(
+ `https://randomuser.me/api?seed=${12321}&results=12`
+ )
+ .then(res => res.json())
+ .then(data =>
+ data.results.map((user: any) => ({
+ ...user,
+ id: user.login.username
+ }))
+ );
+ self.users.replace(data);
+ }),
+ deleteUser(id: string) {
+ const user = self.users.find(u => u.id === id);
+ if (user != null) self.users.remove(user);
+ },
+ setUsersOrder(order: 'name' | 'id') {
+ self.usersOrder = order;
+ }
+ }));
+
+export type StoreType = Instance<typeof Store>;
+export const store = Store.create({
+ usersOrder: 'name',
+ users: []
+});
+
+// const { Provider, Consumer } = createContext<StoreType>(undefined as any)
+
+// export const StoreProvider: FunctionalComponent = props => {
+// const store = Store.create({})
+// return <Provider value={store} children={props.children} />
+// }
+
+// export type StoreProps = {store: StoreType}
+// export function injectStore<T>(Child: AnyComponent<T & StoreProps>): FunctionalComponent<T> {
+// return props => <Consumer render={store => <Child {...props} store={store}/>}/>
+// }
diff --git a/preact/demo/people/styles/animations.scss b/preact/demo/people/styles/animations.scss
new file mode 100644
index 0000000..c7ce8cb
--- /dev/null
+++ b/preact/demo/people/styles/animations.scss
@@ -0,0 +1,34 @@
+@keyframes popup {
+ from {
+ box-shadow: 0 0 0 black;
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ to {
+ box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
+ opacity: 1;
+ transform: none;
+ }
+}
+
+@keyframes zoom {
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+@keyframes appear-from-left {
+ from {
+ opacity: 0;
+ transform: translateX(-25px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
diff --git a/preact/demo/people/styles/app.scss b/preact/demo/people/styles/app.scss
new file mode 100644
index 0000000..4473f9e
--- /dev/null
+++ b/preact/demo/people/styles/app.scss
@@ -0,0 +1,100 @@
+#people-app {
+ position: relative;
+ overflow: hidden;
+ min-height: 100vh;
+ animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
+ background: var(--app-background);
+ --menu-width: 260px;
+ --menu-item-height: 50px;
+
+ @media (min-width: 1280px) {
+ max-width: 1280px;
+ min-height: calc(100vh - 64px);
+ margin: 32px auto;
+ border-radius: 10px;
+ }
+
+ > nav {
+ position: absolute;
+ display: flow-root;
+ width: var(--menu-width);
+ height: 100%;
+ background-color: var(--app-background-secondary);
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+
+ > nav h4 {
+ padding-left: 16px;
+ font-weight: normal;
+ text-transform: uppercase;
+ }
+
+ > nav ul {
+ position: relative;
+ }
+
+ > nav li {
+ position: absolute;
+ width: 100%;
+ animation: zoom 200ms forwards;
+ opacity: 0;
+ transition: top 200ms;
+ }
+
+ > nav li > a {
+ position: relative;
+ display: flex;
+ overflow: hidden;
+ flex-flow: row;
+ align-items: center;
+ margin-left: 16px;
+ border-right: 2px solid transparent;
+ border-bottom-left-radius: 48px;
+ border-top-left-radius: 48px;
+ text-transform: capitalize;
+ transition: border 500ms;
+ }
+
+ > nav li > a:hover {
+ background-color: var(--app-highlight);
+ }
+
+ > nav li > a::after {
+ position: absolute;
+ top: 0;
+ right: -2px;
+ bottom: 0;
+ left: 0;
+ background-image: radial-gradient(
+ circle,
+ var(--app-ripple) 1%,
+ transparent 1%
+ );
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 10000%;
+ content: '';
+ opacity: 0;
+ transition: opacity 700ms, background 300ms;
+ }
+
+ > nav li > a:active::after {
+ background-size: 100%;
+ opacity: 0.5;
+ transition: none;
+ }
+
+ > nav li > a.active {
+ border-color: var(--app-primary);
+ background-color: var(--app-highlight);
+ }
+
+ > nav li > a > * {
+ margin: 8px;
+ }
+
+ #people-main {
+ padding-left: var(--menu-width);
+ }
+}
diff --git a/preact/demo/people/styles/avatar.scss b/preact/demo/people/styles/avatar.scss
new file mode 100644
index 0000000..437c6c4
--- /dev/null
+++ b/preact/demo/people/styles/avatar.scss
@@ -0,0 +1,16 @@
+#people-app {
+ .avatar {
+ display: inline-block;
+ overflow: hidden;
+ width: var(--avatar-size, 32px);
+ height: var(--avatar-size, 32px);
+ background-color: var(--avatar-color, var(--app-primary));
+ border-radius: 50%;
+ font-size: calc(var(--avatar-size, 32px) * 0.5);
+ line-height: var(--avatar-size, 32px);
+ object-fit: cover;
+ text-align: center;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+}
diff --git a/preact/demo/people/styles/button.scss b/preact/demo/people/styles/button.scss
new file mode 100644
index 0000000..ce43138
--- /dev/null
+++ b/preact/demo/people/styles/button.scss
@@ -0,0 +1,115 @@
+#people-app {
+ button {
+ position: relative;
+ overflow: hidden;
+ min-width: 36px;
+ height: 36px;
+ padding: 0 16px;
+ border: none;
+ background-color: transparent;
+ border-radius: 4px;
+ color: var(--app-text);
+ font-family: 'Montserrat', sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ transition: background 300ms, color 200ms;
+ white-space: nowrap;
+ }
+
+ button::before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: var(--app-ripple);
+ content: '';
+ opacity: 0;
+ transition: opacity 200ms;
+ }
+
+ button:hover:not(:disabled)::before {
+ opacity: 0.3;
+ transition: opacity 100ms;
+ }
+
+ button:active:not(:disabled)::before {
+ opacity: 0.7;
+ transition: none;
+ }
+
+ button::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-image: radial-gradient(
+ circle,
+ var(--app-ripple) 1%,
+ transparent 1%
+ );
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 20000%;
+ content: '';
+ opacity: 0;
+ transition: opacity 700ms, background 400ms;
+ }
+
+ button:active:not(:disabled)::after {
+ background-size: 100%;
+ opacity: 1;
+ transition: none;
+ }
+
+ button.primary {
+ background-color: var(--app-primary);
+ box-shadow: 0 2px 6px var(--app-shadow);
+ }
+
+ button.secondary {
+ background-color: var(--app-secondary);
+ box-shadow: 0 2px 6px var(--app-shadow);
+ }
+
+ button:disabled {
+ color: var(--app-text-secondary);
+ }
+
+ button.busy {
+ animation: stripes 500ms linear infinite;
+ background-image: repeating-linear-gradient(
+ 45deg,
+ var(--app-shadow) 0%,
+ var(--app-shadow) 25%,
+ transparent 25%,
+ transparent 50%,
+ var(--app-shadow) 50%,
+ var(--app-shadow) 75%,
+ transparent 75%,
+ transparent 100%
+ );
+ color: var(--app-text);
+ /* letter-spacing: -.7em; */
+ }
+
+ button:disabled:not(.primary):not(.secondary).busy,
+ button:disabled.primary:not(.busy),
+ button:disabled.secondary:not(.busy) {
+ background-color: var(--app-background-disabled);
+ }
+
+ @keyframes stripes {
+ from {
+ background-position-x: 0;
+ background-size: 16px 16px;
+ }
+ to {
+ background-position-x: 16px;
+ background-size: 16px 16px;
+ }
+ }
+}
diff --git a/preact/demo/people/styles/index.scss b/preact/demo/people/styles/index.scss
new file mode 100644
index 0000000..80b342e
--- /dev/null
+++ b/preact/demo/people/styles/index.scss
@@ -0,0 +1,168 @@
+@import 'app.scss';
+@import 'animations.scss';
+@import 'avatar.scss';
+@import 'profile.scss';
+@import 'button.scss';
+
+// :root {
+#people-app {
+ --app-background: #2f2b43;
+ --app-background-secondary: #353249;
+ --app-background-disabled: #555366;
+ --app-highlight: rgba(255, 255, 255, 0.1);
+ --app-ripple: rgba(255, 255, 255, 0.5);
+ --app-shadow: rgba(0, 0, 0, 0.15);
+ --app-text: #fff;
+ --app-text-secondary: #807e97;
+ --app-primary: #ff0087;
+ --app-secondary: #4d7cfe;
+ --app-tertiary: #00ec97;
+ --app-danger: #f3c835;
+ --spinner-size: 200px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+// body {
+#people-app {
+ // display: flow-root;
+ // overflow: auto;
+ // min-height: 100vh;
+ // margin: 0;
+ // animation: background-light 5s ease-out forwards;
+ // /* very fancy background */
+ // background: radial-gradient(
+ // circle 15px at 150px 90vh,
+ // rgba(255, 255, 255, 0.35),
+ // rgba(255, 255, 255, 0.35) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 9px at 60px 50vh,
+ // rgba(255, 255, 255, 0.55),
+ // rgba(255, 255, 255, 0.55) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 19px at 40vw 70px,
+ // rgba(255, 255, 255, 0.3),
+ // rgba(255, 255, 255, 0.3) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 12px at 80vw 80px,
+ // rgba(255, 255, 255, 0.4),
+ // rgba(255, 255, 255, 0.4) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 7px at 55vw calc(100vh - 95px),
+ // rgba(255, 255, 255, 0.6),
+ // rgba(255, 255, 255, 0.6) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 14px at 25vw calc(100vh - 35px),
+ // rgba(255, 255, 255, 0.4),
+ // rgba(255, 255, 255, 0.4) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 11px at calc(100vw - 95px) 55vh,
+ // rgba(255, 255, 255, 0.45),
+ // rgba(255, 255, 255, 0.45) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 13px at calc(100vw - 35px) 85vh,
+ // rgba(255, 255, 255, 0.4),
+ // rgba(255, 255, 255, 0.4) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 50vw at 0 -25%,
+ // rgba(255, 255, 255, 0.07),
+ // rgba(255, 255, 255, 0.07) 100%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 80vw at top left,
+ // rgba(255, 255, 255, 0.07),
+ // rgba(255, 255, 255, 0.07) 100%,
+ // transparent
+ // ),
+ // radial-gradient(circle at bottom right, #ef2fb8, transparent),
+ // radial-gradient(circle at top right, #c45af3, transparent),
+ // linear-gradient(#ee66ca, #ff47a6);
+ color: var(--app-text);
+ font-family: 'Montserrat', sans-serif;
+}
+
+#people-app {
+ .spinner {
+ position: absolute;
+ top: 200px;
+ left: calc(50% - var(--spinner-size) / 2);
+ width: var(--spinner-size);
+ height: var(--spinner-size);
+ animation: zoom 250ms 500ms forwards ease-out;
+ opacity: 0;
+ transition: opacity 200ms, transform 200ms ease-in;
+ }
+
+ .spinner.exit {
+ opacity: 0;
+ transform: scale(0.5);
+ }
+
+ .spinner::before,
+ .spinner::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: calc(var(--spinner-size) / 3);
+ height: calc(var(--spinner-size) / 3);
+ animation: spinner 2s infinite ease-in-out;
+ background-color: rgba(255, 255, 255, 0.6);
+ content: '';
+ }
+
+ .spinner::after {
+ animation-delay: -1s;
+ }
+
+ @keyframes spinner {
+ 25% {
+ transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px))
+ rotate(-90deg) scale(0.5);
+ }
+ 50% {
+ transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px))
+ translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-179deg);
+ }
+ 50.1% {
+ transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px))
+ translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-180deg);
+ }
+ 75% {
+ transform: translateX(0) translateY(calc(var(--spinner-size) / 3 * 2 - 1px))
+ rotate(-270deg) scale(0.5);
+ }
+ 100% {
+ transform: rotate(-360deg);
+ }
+ }
+
+ ul,
+ ol {
+ padding-left: 0;
+ list-style: none;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+}
diff --git a/preact/demo/people/styles/profile.scss b/preact/demo/people/styles/profile.scss
new file mode 100644
index 0000000..f74b760
--- /dev/null
+++ b/preact/demo/people/styles/profile.scss
@@ -0,0 +1,26 @@
+#people-app {
+ .profile {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ margin: 32px 0;
+ animation: appear-from-left 0.5s forwards;
+ --avatar-size: 80px;
+ }
+
+ .profile h2 {
+ text-transform: capitalize;
+ }
+
+ .profile .details {
+ display: flex;
+ flex-flow: column;
+ align-items: stretch;
+ margin: 16px auto;
+ }
+
+ .profile .details p {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+}