@skipleague/design 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/AppBadge.d.ts +8 -0
- package/dist/AppBadge.js +21 -0
- package/dist/AppLogo.d.ts +13 -0
- package/dist/AppLogo.js +33 -0
- package/dist/BottomNav.d.ts +41 -0
- package/dist/BottomNav.js +145 -0
- package/dist/Button.d.ts +7 -0
- package/dist/Button.js +22 -0
- package/dist/Card.d.ts +10 -0
- package/dist/Card.js +13 -0
- package/dist/Field.d.ts +9 -0
- package/dist/Field.js +14 -0
- package/dist/ProfileMenu.d.ts +72 -0
- package/dist/ProfileMenu.js +106 -0
- package/dist/Select.d.ts +3 -0
- package/dist/Select.js +6 -0
- package/dist/ShareMenu.d.ts +16 -0
- package/dist/ShareMenu.js +83 -0
- package/dist/TopBar.d.ts +30 -0
- package/dist/TopBar.js +57 -0
- package/dist/apps.d.ts +19 -0
- package/dist/apps.js +17 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +11 -0
- package/dist/styles.d.ts +3 -0
- package/dist/styles.js +12 -0
- package/package.json +38 -0
- package/src/AppBadge.tsx +26 -0
- package/src/AppLogo.tsx +81 -0
- package/src/BottomNav.tsx +276 -0
- package/src/Button.tsx +28 -0
- package/src/Card.tsx +33 -0
- package/src/Field.tsx +40 -0
- package/src/ProfileMenu.tsx +275 -0
- package/src/Select.tsx +11 -0
- package/src/ShareMenu.tsx +146 -0
- package/src/TopBar.tsx +105 -0
- package/src/apps.ts +28 -0
- package/src/index.ts +16 -0
- package/src/styles.ts +14 -0
- package/src/tokens.css +43 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Link2, Mail, MessageCircle, MoreHorizontal } from "lucide-react";
|
|
4
|
+
import { TopBarIconButton } from "./TopBar.js";
|
|
5
|
+
/**
|
|
6
|
+
* The top-bar Share dropdown (design_handoff_top_bar). A self-contained trigger
|
|
7
|
+
* (the upload glyph, styled like a {@link TopBarIconButton}) + an anchored menu —
|
|
8
|
+
* same open/dismiss model as ProfileMenu (click, outside-click, Escape).
|
|
9
|
+
*
|
|
10
|
+
* `noun` names the thing being shared ("players" → "Share players"). Pass the
|
|
11
|
+
* handlers you support; omitted items are hidden.
|
|
12
|
+
*/
|
|
13
|
+
export function ShareMenu({ noun, compact = false, onCopyLink, onMessages, onEmail, onMore, }) {
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const ref = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!open)
|
|
18
|
+
return;
|
|
19
|
+
const onDoc = (e) => {
|
|
20
|
+
if (!ref.current?.contains(e.target))
|
|
21
|
+
setOpen(false);
|
|
22
|
+
};
|
|
23
|
+
const onKey = (e) => {
|
|
24
|
+
if (e.key === "Escape")
|
|
25
|
+
setOpen(false);
|
|
26
|
+
};
|
|
27
|
+
window.addEventListener("mousedown", onDoc);
|
|
28
|
+
window.addEventListener("keydown", onKey);
|
|
29
|
+
return () => {
|
|
30
|
+
window.removeEventListener("mousedown", onDoc);
|
|
31
|
+
window.removeEventListener("keydown", onKey);
|
|
32
|
+
};
|
|
33
|
+
}, [open]);
|
|
34
|
+
const run = (fn) => () => {
|
|
35
|
+
setOpen(false);
|
|
36
|
+
fn?.();
|
|
37
|
+
};
|
|
38
|
+
const items = [
|
|
39
|
+
{ key: "copy", icon: _jsx(Link2, { size: 18 }), label: "Copy link", fn: onCopyLink },
|
|
40
|
+
{ key: "messages", icon: _jsx(MessageCircle, { size: 18 }), label: "Send in Messages", fn: onMessages },
|
|
41
|
+
{ key: "email", icon: _jsx(Mail, { size: 18 }), label: "Email", fn: onEmail },
|
|
42
|
+
].filter((i) => i.fn);
|
|
43
|
+
return (_jsxs("div", { ref: ref, style: { position: "relative", display: "inline-flex" }, children: [_jsx(TopBarIconButton, { compact: compact, "aria-label": `Share ${noun}`, "aria-expanded": open, onClick: () => setOpen((v) => !v), children: _jsxs("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 14.5V3.5" }), _jsx("path", { d: "M8 7.2l4-3.7 4 3.7" }), _jsx("path", { d: "M5.5 12.5v6a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-6" })] }) }), open && (_jsxs(_Fragment, { children: [_jsx("style", { children: "@keyframes sklShareMenuPop{from{opacity:0;transform:translateY(-6px) scale(.97)}to{opacity:1;transform:none}}" }), _jsxs("div", { role: "menu", style: panelStyle, children: [_jsxs("div", { style: sectionLabelStyle, children: ["Share ", noun] }), items.map((it) => (_jsx(MenuItem, { icon: it.icon, label: it.label, onClick: run(it.fn) }, it.key))), onMore && (_jsxs(_Fragment, { children: [_jsx("div", { style: dividerStyle }), _jsx(MenuItem, { icon: _jsx(MoreHorizontal, { size: 18 }), label: "More options\u2026", onClick: run(onMore), muted: true })] }))] })] }))] }));
|
|
44
|
+
}
|
|
45
|
+
function MenuItem({ icon, label, onClick, muted }) {
|
|
46
|
+
const [hover, setHover] = useState(false);
|
|
47
|
+
return (_jsxs("button", { type: "button", role: "menuitem", onClick: onClick, onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), style: {
|
|
48
|
+
display: "flex",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
gap: 11,
|
|
51
|
+
width: "100%",
|
|
52
|
+
padding: "9px 10px",
|
|
53
|
+
border: "none",
|
|
54
|
+
background: hover ? "var(--skl-color-surface-muted)" : "transparent",
|
|
55
|
+
borderRadius: 8,
|
|
56
|
+
font: "600 14px var(--skl-font-sans)",
|
|
57
|
+
color: muted ? "var(--skl-color-text-muted)" : "var(--skl-color-text)",
|
|
58
|
+
textAlign: "left",
|
|
59
|
+
cursor: "pointer",
|
|
60
|
+
}, children: [_jsx("span", { style: { display: "inline-flex", color: muted ? "var(--skl-color-text-faint)" : "var(--skl-color-text-muted)" }, children: icon }), label] }));
|
|
61
|
+
}
|
|
62
|
+
const panelStyle = {
|
|
63
|
+
position: "absolute",
|
|
64
|
+
top: "calc(100% + 8px)",
|
|
65
|
+
right: 0,
|
|
66
|
+
width: 226,
|
|
67
|
+
background: "var(--skl-color-surface)",
|
|
68
|
+
border: "1px solid var(--skl-color-border)",
|
|
69
|
+
borderRadius: "var(--skl-radius-panel)",
|
|
70
|
+
boxShadow: "var(--skl-shadow-menu)",
|
|
71
|
+
padding: 6,
|
|
72
|
+
zIndex: 70,
|
|
73
|
+
transformOrigin: "top right",
|
|
74
|
+
animation: "sklShareMenuPop .16s ease-out",
|
|
75
|
+
};
|
|
76
|
+
const sectionLabelStyle = {
|
|
77
|
+
padding: "8px 10px 7px",
|
|
78
|
+
font: "700 10.5px var(--skl-font-sans)",
|
|
79
|
+
textTransform: "uppercase",
|
|
80
|
+
letterSpacing: "0.6px",
|
|
81
|
+
color: "var(--skl-color-text-faint)",
|
|
82
|
+
};
|
|
83
|
+
const dividerStyle = { height: 1, background: "var(--skl-color-border)", margin: "6px 8px" };
|
package/dist/TopBar.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { type AppGlyph } from "./AppLogo.js";
|
|
3
|
+
import { type ProfileMenuProps } from "./ProfileMenu.js";
|
|
4
|
+
/**
|
|
5
|
+
* The shared SkipLeague top bar (design_handoff_top_bar, Option A — dark).
|
|
6
|
+
*
|
|
7
|
+
* Layout is identical across apps; only the logo glyph, wordmark, and the
|
|
8
|
+
* contextual action slot change:
|
|
9
|
+
* [AppLogo + wordmark] ........ [ ...actions ][ ProfileMenu ]
|
|
10
|
+
*
|
|
11
|
+
* - ProfileMenu is pinned to the far right on every page (the persistent slot).
|
|
12
|
+
* - `actions` is the contextual slot (Print / Share / …) — render zero or more
|
|
13
|
+
* {@link TopBarIconButton}s; they appear left of the ProfileMenu only when the
|
|
14
|
+
* current page offers them.
|
|
15
|
+
*/
|
|
16
|
+
export declare function TopBar({ app, appName, actions, compact, ...profile }: {
|
|
17
|
+
app: AppGlyph;
|
|
18
|
+
appName: string;
|
|
19
|
+
/** Contextual page actions (Print/Share buttons) rendered left of ProfileMenu. */
|
|
20
|
+
actions?: ReactNode;
|
|
21
|
+
/** Mobile sizing (48px bar) vs the default 54px. */
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
} & Pick<ProfileMenuProps, "user" | "currentSlug" | "apps" | "enabledSlugs" | "accountUrl" | "onSignOut" | "renderLink" | "onSignIn">): import("react").JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* A contextual top-bar action button (Print, Share, …) styled for the dark bar.
|
|
26
|
+
* Pass a 17–18px line icon (stroke 2, currentColor) as children.
|
|
27
|
+
*/
|
|
28
|
+
export declare function TopBarIconButton({ compact, style, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
29
|
+
compact?: boolean;
|
|
30
|
+
}): import("react").JSX.Element;
|
package/dist/TopBar.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { AppLogo } from "./AppLogo.js";
|
|
4
|
+
import { ProfileMenu } from "./ProfileMenu.js";
|
|
5
|
+
/**
|
|
6
|
+
* The shared SkipLeague top bar (design_handoff_top_bar, Option A — dark).
|
|
7
|
+
*
|
|
8
|
+
* Layout is identical across apps; only the logo glyph, wordmark, and the
|
|
9
|
+
* contextual action slot change:
|
|
10
|
+
* [AppLogo + wordmark] ........ [ ...actions ][ ProfileMenu ]
|
|
11
|
+
*
|
|
12
|
+
* - ProfileMenu is pinned to the far right on every page (the persistent slot).
|
|
13
|
+
* - `actions` is the contextual slot (Print / Share / …) — render zero or more
|
|
14
|
+
* {@link TopBarIconButton}s; they appear left of the ProfileMenu only when the
|
|
15
|
+
* current page offers them.
|
|
16
|
+
*/
|
|
17
|
+
export function TopBar({ app, appName, actions, compact = false, ...profile }) {
|
|
18
|
+
const bar = {
|
|
19
|
+
display: "flex",
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
justifyContent: "space-between",
|
|
22
|
+
height: compact ? 48 : 54,
|
|
23
|
+
padding: compact ? "0 12px" : "0 14px",
|
|
24
|
+
background: "var(--skl-color-header-dark)",
|
|
25
|
+
};
|
|
26
|
+
return (_jsxs("div", { style: bar, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10, minWidth: 0 }, children: [_jsx(AppLogo, { app: app, size: compact ? 26 : 27 }), _jsx("span", { style: {
|
|
27
|
+
fontFamily: "var(--skl-font-sans)",
|
|
28
|
+
fontWeight: 700,
|
|
29
|
+
fontSize: compact ? 14 : 15,
|
|
30
|
+
color: "var(--skl-color-on-dark)",
|
|
31
|
+
whiteSpace: "nowrap",
|
|
32
|
+
overflow: "hidden",
|
|
33
|
+
textOverflow: "ellipsis",
|
|
34
|
+
}, children: appName })] }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [actions, _jsx(ProfileMenu, { tone: "dark", ...profile })] })] }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A contextual top-bar action button (Print, Share, …) styled for the dark bar.
|
|
38
|
+
* Pass a 17–18px line icon (stroke 2, currentColor) as children.
|
|
39
|
+
*/
|
|
40
|
+
export function TopBarIconButton({ compact = false, style, children, ...props }) {
|
|
41
|
+
const [hover, setHover] = useState(false);
|
|
42
|
+
const dim = compact ? 36 : 38;
|
|
43
|
+
return (_jsx("button", { type: "button", onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), style: {
|
|
44
|
+
width: dim,
|
|
45
|
+
height: dim,
|
|
46
|
+
borderRadius: 9,
|
|
47
|
+
display: "inline-flex",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
justifyContent: "center",
|
|
50
|
+
cursor: "pointer",
|
|
51
|
+
background: hover ? "rgba(255,255,255,0.08)" : "transparent",
|
|
52
|
+
border: `1px solid ${hover ? "rgba(94,234,212,0.5)" : "rgba(255,255,255,0.16)"}`,
|
|
53
|
+
color: hover ? "var(--skl-color-brand-bright)" : "var(--skl-color-on-dark-muted)",
|
|
54
|
+
transition: "background .12s, border-color .12s, color .12s",
|
|
55
|
+
...style,
|
|
56
|
+
}, ...props, children: children }));
|
|
57
|
+
}
|
package/dist/apps.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A SkipLeague app the ProfileMenu can switch between. */
|
|
2
|
+
export interface AppLink {
|
|
3
|
+
/** Platform app slug, e.g. "skiplists". Matches the registry + introspect. */
|
|
4
|
+
slug: string;
|
|
5
|
+
/** Display name, e.g. "SkipLists". */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Absolute URL to the app. */
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Canonical list of LIVE SkipLeague apps shown in every app's switcher.
|
|
12
|
+
* Add an app here (one place) when it goes live and every app's menu updates.
|
|
13
|
+
* What each user actually sees is this list filtered by their enabled apps —
|
|
14
|
+
* pass `enabledSlugs={user.app_slugs}` to ProfileMenu. SkipEvolve is omitted
|
|
15
|
+
* until it launches.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SKIPLEAGUE_APPS: AppLink[];
|
|
18
|
+
/** Default target for the menu's "Manage account" link (the platform account page). */
|
|
19
|
+
export declare const SKIPLEAGUE_ACCOUNT_URL = "https://skipleague.com/account";
|
package/dist/apps.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical list of LIVE SkipLeague apps shown in every app's switcher.
|
|
3
|
+
* Add an app here (one place) when it goes live and every app's menu updates.
|
|
4
|
+
* What each user actually sees is this list filtered by their enabled apps —
|
|
5
|
+
* pass `enabledSlugs={user.app_slugs}` to ProfileMenu. SkipEvolve is omitted
|
|
6
|
+
* until it launches.
|
|
7
|
+
*/
|
|
8
|
+
export const SKIPLEAGUE_APPS = [
|
|
9
|
+
{ slug: "skiplists", name: "SkipLists", url: "https://lists.skipleague.com" },
|
|
10
|
+
{ slug: "skipracquetball", name: "SkipRacquetball", url: "https://racquetball.skipleague.com" },
|
|
11
|
+
{ slug: "skiptrips", name: "SkipTrips", url: "https://trips.skipleague.com" },
|
|
12
|
+
{ slug: "skipreading", name: "SkipReading", url: "https://reading.skipleague.com" },
|
|
13
|
+
{ slug: "skipgifts", name: "SkipGifts", url: "https://gifts.skipleague.com" },
|
|
14
|
+
{ slug: "skiptoday", name: "SkipToday", url: "https://today.skipleague.com" },
|
|
15
|
+
];
|
|
16
|
+
/** Default target for the menu's "Manage account" link (the platform account page). */
|
|
17
|
+
export const SKIPLEAGUE_ACCOUNT_URL = "https://skipleague.com/account";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { ProfileMenu } from "./ProfileMenu.js";
|
|
2
|
+
export type { ProfileMenuProps, ProfileMenuUser, ProfileMenuLinkArgs } from "./ProfileMenu.js";
|
|
3
|
+
export { AppBadge } from "./AppBadge.js";
|
|
4
|
+
export { AppLogo } from "./AppLogo.js";
|
|
5
|
+
export type { AppGlyph } from "./AppLogo.js";
|
|
6
|
+
export { TopBar, TopBarIconButton } from "./TopBar.js";
|
|
7
|
+
export { BottomNav } from "./BottomNav.js";
|
|
8
|
+
export type { BottomNavProps, BottomNavTab, BottomNavAction } from "./BottomNav.js";
|
|
9
|
+
export { ShareMenu } from "./ShareMenu.js";
|
|
10
|
+
export { Button } from "./Button.js";
|
|
11
|
+
export type { ButtonProps } from "./Button.js";
|
|
12
|
+
export { Field, Input } from "./Field.js";
|
|
13
|
+
export { Select } from "./Select.js";
|
|
14
|
+
export { Card } from "./Card.js";
|
|
15
|
+
export { SKIPLEAGUE_APPS, SKIPLEAGUE_ACCOUNT_URL } from "./apps.js";
|
|
16
|
+
export type { AppLink } from "./apps.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { ProfileMenu } from "./ProfileMenu.js";
|
|
2
|
+
export { AppBadge } from "./AppBadge.js";
|
|
3
|
+
export { AppLogo } from "./AppLogo.js";
|
|
4
|
+
export { TopBar, TopBarIconButton } from "./TopBar.js";
|
|
5
|
+
export { BottomNav } from "./BottomNav.js";
|
|
6
|
+
export { ShareMenu } from "./ShareMenu.js";
|
|
7
|
+
export { Button } from "./Button.js";
|
|
8
|
+
export { Field, Input } from "./Field.js";
|
|
9
|
+
export { Select } from "./Select.js";
|
|
10
|
+
export { Card } from "./Card.js";
|
|
11
|
+
export { SKIPLEAGUE_APPS, SKIPLEAGUE_ACCOUNT_URL } from "./apps.js";
|
package/dist/styles.d.ts
ADDED
package/dist/styles.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Base style shared by text inputs and selects. */
|
|
2
|
+
export const controlStyle = {
|
|
3
|
+
width: "100%",
|
|
4
|
+
boxSizing: "border-box",
|
|
5
|
+
padding: "0.625rem 0.75rem",
|
|
6
|
+
fontFamily: "var(--skl-font-sans)",
|
|
7
|
+
fontSize: "var(--skl-text-sm)",
|
|
8
|
+
color: "var(--skl-color-text)",
|
|
9
|
+
background: "var(--skl-color-surface)",
|
|
10
|
+
border: "1px solid var(--skl-color-border)",
|
|
11
|
+
borderRadius: "var(--skl-radius-md)",
|
|
12
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skipleague/design",
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "SkipUI — the SkipLeague design system: shared tokens and React components for every SkipLeague app.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/SkipLeague/design.git"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./tokens.css": "./src/tokens.css"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json"
|
|
26
|
+
},
|
|
27
|
+
"license": "UNLICENSED",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"lucide-react": ">=0.400.0",
|
|
30
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^18.3.12",
|
|
34
|
+
"lucide-react": "^0.468.0",
|
|
35
|
+
"react": "^18.3.1",
|
|
36
|
+
"typescript": "^5.7.2"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/AppBadge.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The square letter-logo for a SkipLeague app — the first letter of the name
|
|
5
|
+
* with the "Skip" prefix dropped (e.g. "SkipLists" → "L"). Brand-green.
|
|
6
|
+
*/
|
|
7
|
+
export function AppBadge({ name, size = 20 }: { name: string; size?: number }) {
|
|
8
|
+
const style: CSSProperties = {
|
|
9
|
+
display: "inline-flex",
|
|
10
|
+
alignItems: "center",
|
|
11
|
+
justifyContent: "center",
|
|
12
|
+
width: size,
|
|
13
|
+
height: size,
|
|
14
|
+
borderRadius: "var(--skl-radius-badge)",
|
|
15
|
+
background: "var(--skl-color-brand)",
|
|
16
|
+
color: "#fff",
|
|
17
|
+
fontSize: "var(--skl-text-2xs)",
|
|
18
|
+
fontWeight: 700,
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
};
|
|
21
|
+
return (
|
|
22
|
+
<span style={style} aria-hidden>
|
|
23
|
+
{name.replace(/^Skip/, "").charAt(0)}
|
|
24
|
+
</span>
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/AppLogo.tsx
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/** A SkipLeague app's logo glyph (monoline, drawn on the brand tile). */
|
|
4
|
+
export type AppGlyph = "lists" | "racquetball" | "trips";
|
|
5
|
+
|
|
6
|
+
// Exact 24×24 glyph paths (design_handoff_top_bar). `color` only matters for the
|
|
7
|
+
// racquetball ball, which is filled rather than stroked.
|
|
8
|
+
function glyphPaths(app: AppGlyph, color: string): ReactNode {
|
|
9
|
+
switch (app) {
|
|
10
|
+
case "lists":
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<path d="M3.5 7l1.8 1.8L8.5 5.6" />
|
|
14
|
+
<path d="M12.5 7H20.5" />
|
|
15
|
+
<path d="M3.5 16l1.8 1.8L8.5 14.6" />
|
|
16
|
+
<path d="M12.5 16.5H20.5" />
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
case "racquetball":
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<ellipse cx="10" cy="9" rx="5.4" ry="6.6" transform="rotate(-34 10 9)" />
|
|
23
|
+
<path d="M6.6 14.2L3.4 19.4" />
|
|
24
|
+
<circle cx="18" cy="17.4" r="2.3" fill={color} stroke="none" />
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
case "trips":
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<path d="M12 21.5s6.6-5.8 6.6-10.5a6.6 6.6 0 1 0-13.2 0c0 4.7 6.6 10.5 6.6 10.5z" />
|
|
31
|
+
<circle cx="12" cy="10.8" r="2.4" />
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The per-app logo mark — a brand-green rounded tile holding a white monoline
|
|
39
|
+
* glyph. Replaces the single-letter {@link AppBadge}; gives each SkipLeague app a
|
|
40
|
+
* distinct mark for the top bar, app switcher, and home screens.
|
|
41
|
+
*/
|
|
42
|
+
export function AppLogo({
|
|
43
|
+
app,
|
|
44
|
+
size = 28,
|
|
45
|
+
bg = "var(--skl-color-brand)",
|
|
46
|
+
glyph = "#ffffff",
|
|
47
|
+
}: {
|
|
48
|
+
app: AppGlyph;
|
|
49
|
+
size?: number;
|
|
50
|
+
bg?: string;
|
|
51
|
+
glyph?: string;
|
|
52
|
+
}) {
|
|
53
|
+
const inner = Math.round(size * 0.84);
|
|
54
|
+
const style: CSSProperties = {
|
|
55
|
+
display: "inline-flex",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
justifyContent: "center",
|
|
58
|
+
width: size,
|
|
59
|
+
height: size,
|
|
60
|
+
borderRadius: Math.round(size * 0.32),
|
|
61
|
+
background: bg,
|
|
62
|
+
boxShadow: "0 1px 2px rgba(2,6,23,0.18)",
|
|
63
|
+
flexShrink: 0,
|
|
64
|
+
};
|
|
65
|
+
return (
|
|
66
|
+
<span style={style} aria-hidden>
|
|
67
|
+
<svg
|
|
68
|
+
width={inner}
|
|
69
|
+
height={inner}
|
|
70
|
+
viewBox="0 0 24 24"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke={glyph}
|
|
73
|
+
strokeWidth={2.2}
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
>
|
|
77
|
+
{glyphPaths(app, glyph)}
|
|
78
|
+
</svg>
|
|
79
|
+
</span>
|
|
80
|
+
);
|
|
81
|
+
}
|