@mohou/app-view 1.0.20
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 +8 -0
- package/lib/types/app-card-css.d.ts +3 -0
- package/lib/types/app-card-css.d.ts.map +1 -0
- package/lib/types/app-card.d.ts +22 -0
- package/lib/types/app-card.d.ts.map +1 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/library.d.ts +15 -0
- package/lib/types/library.d.ts.map +1 -0
- package/package.json +41 -0
- package/src/app-card-css.ts +83 -0
- package/src/app-card.tsx +281 -0
- package/src/index.ts +4 -0
- package/src/library.tsx +37 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-card-css.d.ts","sourceRoot":"","sources":["../../src/app-card-css.ts"],"names":[],"mappings":"AAGA,wFAAwF;AACxF,eAAO,MAAM,UAAU,QA8EX,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AppListItem } from '@mohou/contract';
|
|
3
|
+
/** Card styles. The same six the panel library offers. */
|
|
4
|
+
export declare const appCardStyles: readonly ["glass", "stamp", "etch", "hero", "pulse", "list"];
|
|
5
|
+
export type AppCardStyle = (typeof appCardStyles)[number];
|
|
6
|
+
/** Fields only some styles read. Glass uses `featured` to span two columns. */
|
|
7
|
+
export interface AppCardExtra {
|
|
8
|
+
readonly featured?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One app card. `onOpen` is the click. The card does not open the app itself.
|
|
12
|
+
* @param props - style, the list item, and the open mark
|
|
13
|
+
*/
|
|
14
|
+
export declare function AppCard(props: {
|
|
15
|
+
readonly type: AppCardStyle;
|
|
16
|
+
readonly app: AppListItem;
|
|
17
|
+
readonly open?: boolean;
|
|
18
|
+
readonly openLabel?: string;
|
|
19
|
+
readonly extra?: AppCardExtra;
|
|
20
|
+
readonly onOpen: () => void;
|
|
21
|
+
}): ReactNode;
|
|
22
|
+
//# sourceMappingURL=app-card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-card.d.ts","sourceRoot":"","sources":["../../src/app-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD,0DAA0D;AAC1D,eAAO,MAAM,aAAa,8DAA+D,CAAA;AAEzF,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAUD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE;IAC7B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAA;CAC5B,GAAG,SAAS,CAqBZ"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { workbenchEntries } from '@mohou/contract';
|
|
2
|
+
export { appCardCss } from './app-card-css.ts';
|
|
3
|
+
export { AppCard, appCardStyles, type AppCardExtra, type AppCardStyle } from './app-card.tsx';
|
|
4
|
+
export { WorkbenchLibrary } from './library.tsx';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AppListItem } from '@mohou/contract';
|
|
3
|
+
import { type AppCardStyle } from './app-card.tsx';
|
|
4
|
+
/**
|
|
5
|
+
* The builtin library body. A workbench app does not render this.
|
|
6
|
+
* `openApp` asks the panel to add a tab. The component does not write policy.
|
|
7
|
+
*/
|
|
8
|
+
export declare function WorkbenchLibrary(props: {
|
|
9
|
+
readonly apps: readonly AppListItem[];
|
|
10
|
+
readonly cardStyle: AppCardStyle;
|
|
11
|
+
readonly openLabel: string;
|
|
12
|
+
readonly openAppIds?: readonly string[];
|
|
13
|
+
openApp(appId: string, title?: string): void;
|
|
14
|
+
}): ReactNode;
|
|
15
|
+
//# sourceMappingURL=library.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library.d.ts","sourceRoot":"","sources":["../../src/library.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE3D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAA;IACrC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAA;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7C,GAAG,SAAS,CAqBZ"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mohou/app-view",
|
|
3
|
+
"version": "1.0.20",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"lib/types",
|
|
15
|
+
"README.md",
|
|
16
|
+
"!**/*.tsbuildinfo"
|
|
17
|
+
],
|
|
18
|
+
"main": "./src/index.ts",
|
|
19
|
+
"types": "./lib/types/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./lib/types/index.d.ts",
|
|
23
|
+
"default": "./src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"./css": {
|
|
26
|
+
"types": "./src/app-card-css.ts",
|
|
27
|
+
"default": "./src/app-card-css.ts"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"react": "^19.2.0",
|
|
33
|
+
"@mohou/contract": "^1.0.20",
|
|
34
|
+
"@mohou/values": "^1.0.20"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^19.2.0",
|
|
38
|
+
"@types/react-dom": "^19.2.0",
|
|
39
|
+
"react-dom": "^19.2.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Mac Avenir Next + PingFang SC. Windows Bahnschrift + DengXian, then YaHei. */
|
|
2
|
+
const glassSans = '"Avenir Next",Bahnschrift,"Segoe UI","PingFang SC",DengXian,"Microsoft YaHei UI",sans-serif'
|
|
3
|
+
|
|
4
|
+
/** Card rules for the panel sheet and every app sheet. Not scoped to the panel host. */
|
|
5
|
+
export const appCardCss = [
|
|
6
|
+
'.mma-slot{display:grid;min-width:0;}',
|
|
7
|
+
`.mma-glass{display:grid;width:100%;height:100%;min-height:100%;box-sizing:border-box;border-radius:24px;border:1px solid color-mix(in oklch,var(--foreground) 8%,transparent);background:color-mix(in oklch,var(--card) 62%,transparent);padding:18px;text-align:left;font-family:${glassSans};backdrop-filter:blur(16px);transition:transform .4s cubic-bezier(.23,1,.32,1),background-color .4s,box-shadow .4s;}`,
|
|
8
|
+
'.mma-glass-body{display:flex;width:100%;height:100%;min-height:100%;flex-direction:column;justify-content:flex-start;gap:12px;}',
|
|
9
|
+
'.mma-feature .mma-glass{padding:24px;}',
|
|
10
|
+
'.mma-feature .mma-glass-body{justify-content:space-between;gap:15px;}',
|
|
11
|
+
'.mma-glass-name{margin:0;font-size:18px;font-weight:700;letter-spacing:-.02em;line-height:1.25;}',
|
|
12
|
+
'.mma-feature .mma-glass-name{font-size:24px;}',
|
|
13
|
+
'.mma-glass-copy{margin:6px 0 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:12px;font-weight:400;line-height:1.45;color:var(--muted-foreground);}',
|
|
14
|
+
'.mma-feature .mma-glass-copy{margin-top:8px;font-size:14px;line-height:1.625;}',
|
|
15
|
+
'button[data-card="stamp"] .mma-copy,button[data-card="etch"] .mma-copy,button[data-card="pulse"] .mma-copy,button[data-card="hero"] .mma-copy{min-height:2.75em;}',
|
|
16
|
+
'.mma-glass:hover{transform:translateY(-4px);background:color-mix(in oklch,var(--card) 88%,transparent);box-shadow:0 20px 40px -16px color-mix(in oklch,var(--foreground) 16%,transparent);}',
|
|
17
|
+
'.mma-feature{grid-column:span 2;}',
|
|
18
|
+
'@container (max-width:680px){.mma-feature{grid-column:span 1;}}',
|
|
19
|
+
'.mma-mark{font-size:30px;font-weight:800;letter-spacing:-1px;line-height:1;background:linear-gradient(to bottom right,hsl(var(--h) 48% 46%),hsl(var(--h) 40% 38% / .35));-webkit-background-clip:text;background-clip:text;color:transparent;}',
|
|
20
|
+
'.mma-feature .mma-mark{font-size:48px;letter-spacing:-2px;}',
|
|
21
|
+
'.mma-tag{border-radius:6px;background:color-mix(in oklch,var(--foreground) 6%,transparent);padding:2px 8px;font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted-foreground);}',
|
|
22
|
+
'[data-mode="dark"] .mma-glass{background:color-mix(in oklch,var(--foreground) 4%,transparent);border-color:color-mix(in oklch,var(--foreground) 12%,transparent);}',
|
|
23
|
+
'[data-mode="dark"] .mma-mark{background:linear-gradient(to bottom right,hsl(var(--h) 42% 74%),hsl(var(--h) 30% 52% / .28));-webkit-background-clip:text;background-clip:text;}',
|
|
24
|
+
`#mma-host[data-card="glass"] .mma-library{font-family:${glassSans};}`,
|
|
25
|
+
'button[data-app-id]{transition:border-color .16s ease,box-shadow .16s ease,transform .16s ease,background-color .22s ease;}',
|
|
26
|
+
'@keyframes mma-hero-breath{0%,100%{transform:translate(-50%,-50%) scale(1);filter:blur(10px) brightness(1)}50%{transform:translate(-50%,-50%) scale(.86);filter:blur(12px) brightness(.75)}}',
|
|
27
|
+
'button[data-app-id][data-card="hero"] .hero-chip{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;min-width:2.15rem;height:1.45rem;padding:0 .42rem;border-radius:.35rem;background:hsl(var(--h) 62% 46%);color:#fff;font-size:11px;font-weight:700;letter-spacing:.06em;line-height:1;}',
|
|
28
|
+
'[data-mode="dark"] button[data-app-id][data-card="hero"] .hero-chip{background:#fff;color:hsl(var(--h) 58% 36%);}',
|
|
29
|
+
'button[data-app-id][data-card="hero"] .hero-mist{position:absolute;border-radius:50%;pointer-events:none;opacity:0;filter:blur(10px);transform:translate(-50%,-50%) scale(.5);transition:opacity 1.2s ease,transform 1.2s ease;}',
|
|
30
|
+
'button[data-app-id][data-card="hero"] .hero-mist-a{left:74%;top:30%;width:84px;height:84px;background:radial-gradient(circle,hsl(var(--h) 72% 56% / .85),transparent 70%);}',
|
|
31
|
+
'button[data-app-id][data-card="hero"] .hero-mist-b{left:30%;top:74%;width:56px;height:56px;background:radial-gradient(circle,hsl(var(--h) 64% 54% / .7),transparent 70%);}',
|
|
32
|
+
'button[data-app-id][data-card="hero"] .hero-mist-c{left:26%;top:28%;width:48px;height:48px;background:radial-gradient(circle,hsl(var(--h) 56% 58% / .55),transparent 72%);}',
|
|
33
|
+
'button[data-app-id][data-card="hero"]:hover{background:var(--card);border-color:hsl(var(--h) 58% 46% / .72);box-shadow:0 0 8px 0 hsl(var(--h) 62% 48% / .55);transform:translateY(-1px);}',
|
|
34
|
+
'button[data-app-id][data-card="hero"]:hover .hero-mist{opacity:.7;transform:translate(-50%,-50%) scale(1);}',
|
|
35
|
+
'[data-mode="dark"] button[data-app-id][data-card="hero"] .hero-mist-a{background:radial-gradient(circle,hsl(var(--h) 58% 72% / .8),transparent 70%);}',
|
|
36
|
+
'[data-mode="dark"] button[data-app-id][data-card="hero"] .hero-mist-b{background:radial-gradient(circle,hsl(var(--h) 48% 70% / .65),transparent 70%);}',
|
|
37
|
+
'[data-mode="dark"] button[data-app-id][data-card="hero"] .hero-mist-c{background:radial-gradient(circle,hsl(var(--h) 42% 74% / .5),transparent 72%);}',
|
|
38
|
+
'[data-mode="dark"] button[data-app-id][data-card="hero"]:hover{border-color:hsl(var(--h) 40% 62% / .45);box-shadow:0 0 8px 0 hsl(var(--h) 50% 68% / .28);}',
|
|
39
|
+
'[data-mode="dark"] button[data-app-id][data-card="hero"]:hover .hero-mist{opacity:.62;}',
|
|
40
|
+
'button[data-app-id][data-card="hero"]:hover .hero-mist-a{animation:mma-hero-breath 4.2s ease-in-out 1.2s infinite;}',
|
|
41
|
+
'button[data-app-id][data-card="hero"]:hover .hero-mist-b{opacity:.55;animation:mma-hero-breath 5.4s ease-in-out 1.2s infinite;}',
|
|
42
|
+
'button[data-app-id][data-card="hero"]:hover .hero-mist-c{opacity:.45;animation:mma-hero-breath 6.6s ease-in-out 1.2s infinite;}',
|
|
43
|
+
'button[data-app-id][data-card="etch"]{border-width:2px;border-style:solid;border-top-color:var(--card);border-left-color:var(--card);border-right-color:hsl(var(--h) 65% 45%);border-bottom-color:hsl(var(--h) 65% 45%);transition:border-color .9s ease,box-shadow .9s ease,background-color .22s ease;}',
|
|
44
|
+
'button[data-app-id][data-card="etch"] .etch-line{transition:opacity .55s ease;}',
|
|
45
|
+
'button[data-app-id][data-card="etch"] .etch-fill{opacity:0;transition:opacity .55s ease;}',
|
|
46
|
+
'button[data-app-id][data-card="etch"]:hover .etch-line{opacity:0;}',
|
|
47
|
+
'button[data-app-id][data-card="etch"]:hover{border-color:hsl(var(--h) 65% 45%);background:var(--card);transform:none;box-shadow:hsl(var(--h) 70% 50% / .40) 5px 5px,hsl(var(--h) 70% 50% / .28) 10px 10px,hsl(var(--h) 70% 50% / .16) 15px 15px;}',
|
|
48
|
+
'button[data-app-id][data-card="etch"]:hover .etch-fill{opacity:1;}',
|
|
49
|
+
'button[data-app-id][data-card="stamp"]:hover{background:linear-gradient(hsl(var(--h) 30% 55% / .07),hsl(var(--h) 30% 55% / .07)),var(--card);border-color:var(--foreground);box-shadow:0 8px 22px var(--shadow);transform:translateY(-1px);}',
|
|
50
|
+
'button[data-app-id][data-card="stamp"]:hover [data-monogram="stamp"]{background:var(--foreground);color:var(--card);transform:scale(1.05);}',
|
|
51
|
+
'@keyframes mma-list-spin{to{transform:rotate(360deg)}}',
|
|
52
|
+
'button[data-app-id][data-card="list"] .list-backlight{position:absolute;top:0;right:0;z-index:0;width:46%;height:100%;background:linear-gradient(to left,hsl(var(--h) 58% 58% / .52),hsl(var(--h) 46% 56% / .2) 55%,transparent);opacity:0;pointer-events:none;transition:opacity .6s ease;}',
|
|
53
|
+
'button[data-app-id][data-card="list"] .list-wash{position:absolute;inset:4px;z-index:1;border-radius:12px 64px 22px 12px;background:hsl(var(--h) 16% 46% / .06);backdrop-filter:blur(10px);pointer-events:none;transition:background-color .28s ease;}',
|
|
54
|
+
'button[data-app-id][data-card="list"] .list-orb{position:absolute;top:50%;right:28px;width:76px;height:76px;margin-top:-38px;border-radius:50%;background:conic-gradient(from 90deg,transparent 0%,hsl(var(--h) 42% 58% / 0) 20%,hsl(var(--h) 48% 50% / .55) 50%,transparent 76%);filter:blur(8px);animation:mma-list-spin 16s linear infinite;pointer-events:none;}',
|
|
55
|
+
'@keyframes mma-list-nudge{0%{transform:translateX(0)}45%{transform:translateX(4px)}100%{transform:translateX(0)}}',
|
|
56
|
+
'@keyframes mma-list-mark{0%{letter-spacing:.04em;opacity:.8}40%{letter-spacing:.18em;opacity:1}100%{letter-spacing:.08em;opacity:1}}',
|
|
57
|
+
'button[data-app-id][data-card="list"] .list-mark{display:block;font-size:13px;font-weight:700;line-height:1;letter-spacing:.04em;white-space:nowrap;color:var(--foreground);opacity:.8;}',
|
|
58
|
+
'button[data-app-id][data-card="list"] .list-go svg{display:block;}',
|
|
59
|
+
'button[data-app-id][data-card="list"]::after{content:"";position:absolute;inset:0;z-index:2;border-radius:inherit;padding:1px;background:linear-gradient(155deg,hsl(var(--h) 58% 46% / .9),hsl(var(--h) 42% 52% / .5) 40%,transparent 72%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;pointer-events:none;transition:opacity .28s ease;}',
|
|
60
|
+
'button[data-app-id][data-card="list"]:hover{border-color:transparent;background:var(--card);}',
|
|
61
|
+
'button[data-app-id][data-card="list"]:hover::after{opacity:1;}',
|
|
62
|
+
'button[data-app-id][data-card="list"]:hover .list-backlight{opacity:1;}',
|
|
63
|
+
'button[data-app-id][data-card="list"]:hover .list-wash{background:hsl(var(--h) 28% 48% / .12);}',
|
|
64
|
+
'[data-mode="dark"] button[data-app-id][data-card="list"] .list-backlight{background:linear-gradient(to left,hsl(var(--h) 28% 64% / .32),transparent 70%);}',
|
|
65
|
+
'[data-mode="dark"] button[data-app-id][data-card="list"] .list-orb{background:conic-gradient(from 90deg,transparent 0%,hsl(var(--h) 22% 62% / 0) 20%,hsl(var(--h) 18% 55% / .42) 50%,transparent 76%);}',
|
|
66
|
+
'[data-mode="dark"] button[data-app-id][data-card="list"]::after{background:linear-gradient(155deg,hsl(0 0% 100% / .9),hsl(var(--h) 35% 62% / .4) 36%,transparent 70%);}',
|
|
67
|
+
'[data-mode="dark"] button[data-app-id][data-card="list"]:hover .list-wash{background:hsl(var(--h) 16% 46% / .1);}',
|
|
68
|
+
'button[data-app-id][data-card="list"]:hover .list-mark{color:hsl(var(--h) 42% 36%);animation:mma-list-mark .55s ease forwards;}',
|
|
69
|
+
'[data-mode="dark"] button[data-app-id][data-card="list"]:hover .list-mark{color:hsl(var(--h) 48% 74%);}',
|
|
70
|
+
'button[data-app-id][data-card="list"]:hover .list-go{background:hsl(var(--h) 42% 48% / .2);}',
|
|
71
|
+
'button[data-app-id][data-card="list"]:hover .list-go svg{animation:mma-list-nudge .45s ease;}',
|
|
72
|
+
'@keyframes mma-pulse-dot{0%,100%{background:hsl(var(--h) 60% 52% / 0)}50%{background:hsl(var(--h) 75% 48%)}}',
|
|
73
|
+
'@keyframes mma-pulse-glow{0%,100%{opacity:0}50%{opacity:1}}',
|
|
74
|
+
'button[data-app-id][data-card="pulse"]{border:none;background:linear-gradient(hsl(0 0% 100% / .35),hsl(0 0% 0% / .03)),var(--card);box-shadow:1px 1px 2px -1px #fff inset,0 2px 1px #00000010,0 8px 8px #00000012;transition:transform .25s cubic-bezier(.25,1.5,.5,1),filter .3s ease;}',
|
|
75
|
+
'button[data-app-id][data-card="pulse"] .pulse-mark{color:hsl(var(--h) 48% 38%);}',
|
|
76
|
+
'[data-mode="dark"] button[data-app-id][data-card="pulse"]{background:linear-gradient(hsl(var(--h) 22% 58% / .1),transparent 42%),var(--card);box-shadow:inset 0 1px 0 hsl(0 0% 100% / .07),0 10px 18px hsl(0 0% 0% / .4);}',
|
|
77
|
+
'[data-mode="dark"] button[data-app-id][data-card="pulse"] .pulse-mark{color:hsl(var(--h) 40% 74%);}',
|
|
78
|
+
'button[data-app-id][data-card="pulse"] .pulse-dot{position:absolute;top:16px;right:14px;width:8px;height:8px;border-radius:50%;border:1px solid hsl(var(--h) 40% 55% / .55);animation:mma-pulse-dot 1.2s ease-in-out infinite;}',
|
|
79
|
+
'button[data-app-id][data-card="pulse"] .pulse-dot::after{content:"";position:absolute;inset:-7px;border-radius:inherit;box-shadow:0 0 10px 2px hsl(var(--h) 80% 55% / .35),0 0 18px 6px hsl(var(--h) 80% 60% / .2);animation:mma-pulse-glow 1.2s ease-in-out infinite;}',
|
|
80
|
+
'button[data-app-id][data-card="pulse"]:hover{transform:translateY(-2px);filter:drop-shadow(6px 0 6px hsl(var(--h) 70% 55% / .45));}',
|
|
81
|
+
'button[data-app-id][data-card="pulse"]:hover .pulse-dot{animation:none;background:hsl(var(--h) 75% 48%);}',
|
|
82
|
+
'button[data-app-id][data-card="pulse"]:hover .pulse-dot::after{animation:none;opacity:1;}',
|
|
83
|
+
].join('\n')
|
package/src/app-card.tsx
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
import { assertNever } from '@mohou/values'
|
|
3
|
+
import type { AppListItem } from '@mohou/contract'
|
|
4
|
+
|
|
5
|
+
/** Card styles. The same six the panel library offers. */
|
|
6
|
+
export const appCardStyles = ['glass', 'stamp', 'etch', 'hero', 'pulse', 'list'] as const
|
|
7
|
+
|
|
8
|
+
export type AppCardStyle = (typeof appCardStyles)[number]
|
|
9
|
+
|
|
10
|
+
/** Fields only some styles read. Glass uses `featured` to span two columns. */
|
|
11
|
+
export interface AppCardExtra {
|
|
12
|
+
readonly featured?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DrawnCard {
|
|
16
|
+
readonly app: AppListItem
|
|
17
|
+
readonly hue: CSSProperties
|
|
18
|
+
readonly open: boolean
|
|
19
|
+
readonly openLabel?: string
|
|
20
|
+
readonly onOpen: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One app card. `onOpen` is the click. The card does not open the app itself.
|
|
25
|
+
* @param props - style, the list item, and the open mark
|
|
26
|
+
*/
|
|
27
|
+
export function AppCard(props: {
|
|
28
|
+
readonly type: AppCardStyle
|
|
29
|
+
readonly app: AppListItem
|
|
30
|
+
readonly open?: boolean
|
|
31
|
+
readonly openLabel?: string
|
|
32
|
+
readonly extra?: AppCardExtra
|
|
33
|
+
readonly onOpen: () => void
|
|
34
|
+
}): ReactNode {
|
|
35
|
+
const hue = { '--h': appCardHue(props.app.id) } as CSSProperties
|
|
36
|
+
const open = props.open === true
|
|
37
|
+
const label = props.openLabel === undefined ? {} : { openLabel: props.openLabel }
|
|
38
|
+
const drawn = { app: props.app, hue, open, ...label, onOpen: () => { props.onOpen() } }
|
|
39
|
+
switch (props.type) {
|
|
40
|
+
case 'pulse':
|
|
41
|
+
return <PulseCard app={props.app} hue={hue} onOpen={drawn.onOpen} />
|
|
42
|
+
case 'list':
|
|
43
|
+
return <ListCard {...drawn} />
|
|
44
|
+
case 'glass':
|
|
45
|
+
return <GlassCard {...drawn} featured={props.extra?.featured === true} />
|
|
46
|
+
case 'hero':
|
|
47
|
+
return <HeroCard {...drawn} />
|
|
48
|
+
case 'etch':
|
|
49
|
+
return <EtchCard {...drawn} />
|
|
50
|
+
case 'stamp':
|
|
51
|
+
return <StampCard {...drawn} />
|
|
52
|
+
default:
|
|
53
|
+
return assertNever(props.type, 'AppCard')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function appCardHue(id: string): number {
|
|
58
|
+
let hue = 0
|
|
59
|
+
for (let index = 0; index < id.length; index += 1) hue = (hue * 31 + id.charCodeAt(index)) % 360
|
|
60
|
+
return hue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function OpenMark(props: { readonly open: boolean; readonly openLabel: string | undefined }): ReactNode {
|
|
64
|
+
if (!props.open || props.openLabel === undefined) return null
|
|
65
|
+
return (
|
|
66
|
+
<span className="inline-flex items-center gap-1 text-[11px] font-semibold text-primary">
|
|
67
|
+
<i />
|
|
68
|
+
{props.openLabel}
|
|
69
|
+
</span>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function PulseCard(props: {
|
|
74
|
+
readonly app: AppListItem
|
|
75
|
+
readonly hue: CSSProperties
|
|
76
|
+
readonly onOpen: () => void
|
|
77
|
+
}): ReactNode {
|
|
78
|
+
return (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
className="relative flex w-full flex-col items-start rounded-[22px] px-[15px] pt-4 pb-[13px] pr-8 text-left"
|
|
82
|
+
data-app-id={props.app.id}
|
|
83
|
+
data-card="pulse"
|
|
84
|
+
style={props.hue}
|
|
85
|
+
onClick={props.onOpen}
|
|
86
|
+
>
|
|
87
|
+
<span className="pulse-dot" aria-hidden />
|
|
88
|
+
<span className="pulse-mark relative z-[1] mb-2 text-[28px] leading-none font-extrabold tracking-wide">
|
|
89
|
+
{props.app.acronym}
|
|
90
|
+
</span>
|
|
91
|
+
<h3 className="relative z-[1] m-0 text-sm font-semibold tracking-tight">{props.app.name}</h3>
|
|
92
|
+
<p className="mma-copy relative z-[1] m-0 mt-1 line-clamp-2 text-xs leading-snug text-muted-foreground">
|
|
93
|
+
{props.app.description}
|
|
94
|
+
</p>
|
|
95
|
+
<span className="relative z-[1] mt-2 text-[11px] text-muted-foreground">{props.app.version}</span>
|
|
96
|
+
</button>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ListCard(props: DrawnCard): ReactNode {
|
|
101
|
+
return (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className="relative flex h-24 w-full items-stretch gap-2 overflow-hidden rounded-2xl border border-foreground/10 bg-card p-2 text-left"
|
|
105
|
+
data-app-id={props.app.id}
|
|
106
|
+
data-card="list"
|
|
107
|
+
style={props.hue}
|
|
108
|
+
onClick={props.onOpen}
|
|
109
|
+
>
|
|
110
|
+
<span aria-hidden className="list-backlight" />
|
|
111
|
+
<span aria-hidden className="list-wash" />
|
|
112
|
+
<span aria-hidden className="list-orb" />
|
|
113
|
+
<span className="relative z-[1] flex min-w-0 flex-1 flex-col justify-between rounded-xl bg-card/50 px-3 py-2 backdrop-blur-md">
|
|
114
|
+
<span className="truncate text-sm font-semibold">{props.app.name}</span>
|
|
115
|
+
<span className="truncate text-xs text-muted-foreground">{props.app.description}</span>
|
|
116
|
+
<span className="text-[11px] text-muted-foreground">
|
|
117
|
+
{props.open ? <OpenMark open={props.open} openLabel={props.openLabel} /> : props.app.version}
|
|
118
|
+
</span>
|
|
119
|
+
</span>
|
|
120
|
+
<span className="list-rail relative z-[2] flex w-11 shrink-0 flex-col items-end justify-between py-0.5">
|
|
121
|
+
<span data-monogram="list" className="list-mark">{props.app.acronym}</span>
|
|
122
|
+
<span className="list-go flex size-7 items-center justify-center overflow-hidden rounded-full bg-foreground/10 text-foreground/70 backdrop-blur-md">
|
|
123
|
+
<svg className="size-3.5" viewBox="0 0 12 12" aria-hidden>
|
|
124
|
+
<path d="M4.646 2.146a.5.5 0 0 0 0 .708L7.793 6 4.646 9.146a.5.5 0 1 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0z" fill="currentColor" />
|
|
125
|
+
</svg>
|
|
126
|
+
</span>
|
|
127
|
+
</span>
|
|
128
|
+
</button>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function GlassCard(props: DrawnCard & { readonly featured: boolean }): ReactNode {
|
|
133
|
+
const slot = props.featured ? 'mma-slot mma-feature' : 'mma-slot'
|
|
134
|
+
const tag = props.open && props.openLabel !== undefined ? props.openLabel : props.app.version
|
|
135
|
+
return (
|
|
136
|
+
<div className={slot}>
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
className="mma-glass"
|
|
140
|
+
data-app-id={props.app.id}
|
|
141
|
+
data-card="glass"
|
|
142
|
+
style={props.hue}
|
|
143
|
+
onClick={props.onOpen}
|
|
144
|
+
>
|
|
145
|
+
<span className="mma-glass-body">
|
|
146
|
+
<span className="flex w-full items-start justify-between gap-3">
|
|
147
|
+
<span data-monogram="glass" className="mma-mark">{props.app.acronym}</span>
|
|
148
|
+
<span className="mma-tag">{tag}</span>
|
|
149
|
+
</span>
|
|
150
|
+
<span className="block">
|
|
151
|
+
<h3 className="mma-glass-name">{props.app.name}</h3>
|
|
152
|
+
<p className="mma-glass-copy">{props.app.description}</p>
|
|
153
|
+
</span>
|
|
154
|
+
</span>
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function HeroCard(props: DrawnCard): ReactNode {
|
|
161
|
+
return (
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
className="relative z-0 flex w-full flex-col items-start overflow-hidden rounded-[13px] border bg-card px-5 pt-4 pb-5 text-left"
|
|
165
|
+
data-app-id={props.app.id}
|
|
166
|
+
data-card="hero"
|
|
167
|
+
style={props.hue}
|
|
168
|
+
onClick={props.onOpen}
|
|
169
|
+
>
|
|
170
|
+
<span aria-hidden className="hero-mist hero-mist-a" />
|
|
171
|
+
<span aria-hidden className="hero-mist hero-mist-b" />
|
|
172
|
+
<span aria-hidden className="hero-mist hero-mist-c" />
|
|
173
|
+
<span className="relative z-[1] flex w-full items-center justify-between gap-3">
|
|
174
|
+
<span data-monogram="hero" className="hero-chip">{props.app.acronym}</span>
|
|
175
|
+
<span className="flex shrink-0 items-center gap-2 text-[13px] text-muted-foreground">
|
|
176
|
+
<span>{props.app.version}</span>
|
|
177
|
+
<OpenMark open={props.open} openLabel={props.openLabel} />
|
|
178
|
+
</span>
|
|
179
|
+
</span>
|
|
180
|
+
<h3 className="relative z-[1] m-0 mt-4 text-lg leading-snug font-semibold tracking-tight">{props.app.name}</h3>
|
|
181
|
+
<p className="mma-copy relative z-[1] m-0 mt-2 line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
182
|
+
{props.app.description}
|
|
183
|
+
</p>
|
|
184
|
+
</button>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function EtchCard(props: DrawnCard): ReactNode {
|
|
189
|
+
return (
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
className="relative flex w-full flex-col items-start overflow-hidden rounded-[13px] border bg-card px-[15px] pt-4 pb-[13px] text-left"
|
|
193
|
+
data-app-id={props.app.id}
|
|
194
|
+
data-card="etch"
|
|
195
|
+
style={props.hue}
|
|
196
|
+
onClick={props.onOpen}
|
|
197
|
+
>
|
|
198
|
+
<EtchMark text={props.app.acronym} />
|
|
199
|
+
<h3 className="relative z-[1] m-0 text-sm font-semibold tracking-tight">{props.app.name}</h3>
|
|
200
|
+
<p className="mma-copy relative z-[1] m-0 mt-1 line-clamp-2 text-xs leading-snug text-muted-foreground">
|
|
201
|
+
{props.app.description}
|
|
202
|
+
</p>
|
|
203
|
+
<span className="relative z-[1] mt-2 flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
204
|
+
<span>{props.app.version}</span>
|
|
205
|
+
<OpenMark open={props.open} openLabel={props.openLabel} />
|
|
206
|
+
</span>
|
|
207
|
+
</button>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function StampCard(props: DrawnCard): ReactNode {
|
|
212
|
+
return (
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
className="relative flex w-full flex-col items-start overflow-hidden rounded-[13px] border bg-card px-[15px] pt-4 pb-[13px] text-left transition duration-200 ease-out hover:-translate-y-px hover:shadow-[0_8px_22px_var(--shadow)]"
|
|
216
|
+
data-app-id={props.app.id}
|
|
217
|
+
data-card="stamp"
|
|
218
|
+
style={props.hue}
|
|
219
|
+
onClick={props.onOpen}
|
|
220
|
+
>
|
|
221
|
+
<span
|
|
222
|
+
data-monogram="stamp"
|
|
223
|
+
className="absolute top-[13px] right-[13px] z-[1] flex size-11 shrink-0 items-center justify-center rounded-[9px] border-2 border-foreground text-sm font-extrabold tracking-wide text-foreground"
|
|
224
|
+
style={props.hue}
|
|
225
|
+
>
|
|
226
|
+
{props.app.acronym}
|
|
227
|
+
</span>
|
|
228
|
+
<h3 className="relative z-[1] m-0 pr-14 text-sm font-semibold tracking-tight">{props.app.name}</h3>
|
|
229
|
+
<p className="mma-copy relative z-[1] m-0 mt-1 line-clamp-2 pr-14 text-xs leading-snug text-muted-foreground">
|
|
230
|
+
{props.app.description}
|
|
231
|
+
</p>
|
|
232
|
+
<span className="relative z-[1] mt-2 flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
233
|
+
<span>{props.app.version}</span>
|
|
234
|
+
<OpenMark open={props.open} openLabel={props.openLabel} />
|
|
235
|
+
</span>
|
|
236
|
+
</button>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function EtchMark(props: { readonly text: string }): ReactNode {
|
|
241
|
+
const width = Math.max(88, props.text.length * 36)
|
|
242
|
+
return (
|
|
243
|
+
<svg
|
|
244
|
+
data-monogram="etch"
|
|
245
|
+
viewBox={`0 0 ${width} 64`}
|
|
246
|
+
className="relative z-[1] mb-2.5 block h-[52px] overflow-visible"
|
|
247
|
+
style={{ width }}
|
|
248
|
+
aria-hidden
|
|
249
|
+
>
|
|
250
|
+
<text
|
|
251
|
+
className="etch-line"
|
|
252
|
+
x="2"
|
|
253
|
+
y="52"
|
|
254
|
+
fill="var(--card)"
|
|
255
|
+
stroke="hsl(var(--h) 65% 42%)"
|
|
256
|
+
strokeWidth="4.5"
|
|
257
|
+
paintOrder="stroke fill"
|
|
258
|
+
strokeLinejoin="round"
|
|
259
|
+
fontSize="46"
|
|
260
|
+
fontWeight="800"
|
|
261
|
+
letterSpacing="2"
|
|
262
|
+
fontFamily="ui-sans-serif, system-ui, sans-serif"
|
|
263
|
+
>
|
|
264
|
+
{props.text}
|
|
265
|
+
</text>
|
|
266
|
+
<text
|
|
267
|
+
className="etch-fill"
|
|
268
|
+
x="2"
|
|
269
|
+
y="52"
|
|
270
|
+
fill="hsl(var(--h) 65% 42%)"
|
|
271
|
+
stroke="none"
|
|
272
|
+
fontSize="46"
|
|
273
|
+
fontWeight="800"
|
|
274
|
+
letterSpacing="2"
|
|
275
|
+
fontFamily="ui-sans-serif, system-ui, sans-serif"
|
|
276
|
+
>
|
|
277
|
+
{props.text}
|
|
278
|
+
</text>
|
|
279
|
+
</svg>
|
|
280
|
+
)
|
|
281
|
+
}
|
package/src/index.ts
ADDED
package/src/library.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { AppListItem } from '@mohou/contract'
|
|
3
|
+
|
|
4
|
+
import { AppCard, type AppCardStyle } from './app-card.tsx'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The builtin library body. A workbench app does not render this.
|
|
8
|
+
* `openApp` asks the panel to add a tab. The component does not write policy.
|
|
9
|
+
*/
|
|
10
|
+
export function WorkbenchLibrary(props: {
|
|
11
|
+
readonly apps: readonly AppListItem[]
|
|
12
|
+
readonly cardStyle: AppCardStyle
|
|
13
|
+
readonly openLabel: string
|
|
14
|
+
readonly openAppIds?: readonly string[]
|
|
15
|
+
openApp(appId: string, title?: string): void
|
|
16
|
+
}): ReactNode {
|
|
17
|
+
const grid = props.cardStyle === 'list' ? 'flex flex-col gap-3' : 'mma-grid'
|
|
18
|
+
return (
|
|
19
|
+
<div className={grid}>
|
|
20
|
+
{props.apps.map((app, index) => {
|
|
21
|
+
const featured = props.cardStyle === 'glass' && index === 0
|
|
22
|
+
return (
|
|
23
|
+
<div key={app.id} className={featured ? 'mma-feature min-w-0' : 'min-w-0'}>
|
|
24
|
+
<AppCard
|
|
25
|
+
type={props.cardStyle}
|
|
26
|
+
app={app}
|
|
27
|
+
open={props.openAppIds?.includes(app.id) === true}
|
|
28
|
+
openLabel={props.openLabel}
|
|
29
|
+
extra={featured ? { featured: true } : {}}
|
|
30
|
+
onOpen={() => { props.openApp(app.id, app.name) }}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
})}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|