@boost33/ui 0.1.0
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 +42 -0
- package/dist/components/NavItem/NavItem.d.ts +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/nav-height-budget.d.ts +9 -0
- package/dist/style.css +1 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @boost33/ui
|
|
2
|
+
|
|
3
|
+
The app shell every Boost33 product renders inside. Eleven components and the
|
|
4
|
+
design tokens they are drawn with, published to public npm.
|
|
5
|
+
|
|
6
|
+
Built because the product switcher and the wallet chip worked in the Wallet and
|
|
7
|
+
nowhere else, and because a sidebar fixed in one app fixed nothing in the other
|
|
8
|
+
three.
|
|
9
|
+
|
|
10
|
+
## Getting oriented
|
|
11
|
+
|
|
12
|
+
| | |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| How we build here | `CLAUDE.md` |
|
|
15
|
+
| What we are building | Linear document **The shared app shell** |
|
|
16
|
+
| Vocabulary | `CONTEXT.md`, and the initiative's **Vocabulary** document |
|
|
17
|
+
| Settled decisions | `docs/adr/` |
|
|
18
|
+
| Tickets | Linear, project **Boost33 Shared UI** |
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run typecheck
|
|
24
|
+
npm run test # behaviour only — screenshots run in CI
|
|
25
|
+
npm run lint
|
|
26
|
+
npm run afk # one Linear issue, unattended, in a container
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Before the first AFK run
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cp ~/.config/toolkit/afk.env .sandcastle/.env
|
|
33
|
+
npx sandcastle docker build-image # once per machine
|
|
34
|
+
npm run afk # watch the whole first iteration
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Watch for real test output in the log, not the agent's summary of it.
|
|
38
|
+
|
|
39
|
+
## Consumers
|
|
40
|
+
|
|
41
|
+
The Wallet migrates first, Score second. **No 1.0 until both render from this
|
|
42
|
+
package** — until then it has one consumer, which is a fork with a version number.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export interface NavItemProps {
|
|
3
|
+
/** The nav label. Truncates with an ellipsis rather than wrapping. */
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Marks this row as the current page. Sets `aria-current="page"`. */
|
|
6
|
+
active?: boolean;
|
|
7
|
+
/** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */
|
|
8
|
+
collapsed?: boolean;
|
|
9
|
+
/** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */
|
|
10
|
+
count?: number;
|
|
11
|
+
/** Renders `count` in the primary color instead of muted. */
|
|
12
|
+
countUrgent?: boolean;
|
|
13
|
+
href?: string;
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function NavItem({ children, active, collapsed, count, countUrgent, href, onClick }: NavItemProps): import("react").JSX.Element;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { NavItem } from './components/NavItem/NavItem';
|
|
2
|
+
export type { NavItemProps } from './components/NavItem/NavItem';
|
|
3
|
+
export { NAV_HEIGHT_BUDGET_PX, checkNavHeightBudget, measureNavHeight } from './nav-height-budget';
|
|
4
|
+
export type { NavHeightBudget } from './nav-height-budget';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as o, jsxs as _, Fragment as f } from "react/jsx-runtime";
|
|
2
|
+
const g = "_row_5f12t_4", m = "_label_5f12t_36", d = "_trailingSlot_5f12t_62", h = "_count_5f12t_76", p = "_countUrgent_5f12t_80", a = {
|
|
3
|
+
row: g,
|
|
4
|
+
label: m,
|
|
5
|
+
trailingSlot: d,
|
|
6
|
+
count: h,
|
|
7
|
+
countUrgent: p
|
|
8
|
+
};
|
|
9
|
+
function w({ children: t, active: n = !1, collapsed: i = !1, count: e, countUrgent: u = !1, href: r, onClick: l }) {
|
|
10
|
+
const c = {
|
|
11
|
+
"data-active": n ? "true" : void 0,
|
|
12
|
+
"data-collapsed": i ? "true" : void 0,
|
|
13
|
+
"aria-current": n ? "page" : void 0
|
|
14
|
+
}, s = /* @__PURE__ */ _(f, { children: [
|
|
15
|
+
/* @__PURE__ */ o("span", { className: a.label, children: t }),
|
|
16
|
+
/* @__PURE__ */ o("span", { className: a.trailingSlot, children: e != null && /* @__PURE__ */ o("span", { className: u ? a.countUrgent : a.count, children: e }) })
|
|
17
|
+
] });
|
|
18
|
+
return r ? /* @__PURE__ */ o("a", { className: a.row, href: r, onClick: l, ...c, children: s }) : /* @__PURE__ */ o("button", { type: "button", className: a.row, onClick: l, ...c, children: s });
|
|
19
|
+
}
|
|
20
|
+
const N = 700;
|
|
21
|
+
function H(t, n = N) {
|
|
22
|
+
return {
|
|
23
|
+
usedPx: t,
|
|
24
|
+
budgetPx: n,
|
|
25
|
+
fits: t <= n,
|
|
26
|
+
overBy: Math.max(0, t - n)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function U(t) {
|
|
30
|
+
return t.scrollHeight;
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
N as NAV_HEIGHT_BUDGET_PX,
|
|
34
|
+
w as NavItem,
|
|
35
|
+
H as checkNavHeightBudget,
|
|
36
|
+
U as measureNavHeight
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/components/NavItem/NavItem.tsx","../src/nav-height-budget.ts"],"sourcesContent":["import type { ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n href?: string\n onClick?: () => void\n}\n\nexport function NavItem({ children, active = false, collapsed = false, count, countUrgent = false, href, onClick }: NavItemProps) {\n const dataProps = {\n 'data-active': active ? ('true' as const) : undefined,\n 'data-collapsed': collapsed ? ('true' as const) : undefined,\n 'aria-current': active ? ('page' as const) : undefined,\n }\n\n const content = (\n <>\n <span className={styles.label}>{children}</span>\n <span className={styles.trailingSlot}>\n {count != null && <span className={countUrgent ? styles.countUrgent : styles.count}>{count}</span>}\n </span>\n </>\n )\n\n if (href) {\n return (\n <a className={styles.row} href={href} onClick={onClick} {...dataProps}>\n {content}\n </a>\n )\n }\n\n return (\n <button type=\"button\" className={styles.row} onClick={onClick} {...dataProps}>\n {content}\n </button>\n )\n}\n","// The spec's rough budget: a sidebar's nav has to fit above the wallet chip\n// and footer without scrolling. Boost Outbound's admin role has already blown\n// past it once, so this is a real check, not a hypothetical.\nexport const NAV_HEIGHT_BUDGET_PX = 700\n\nexport interface NavHeightBudget {\n usedPx: number\n budgetPx: number\n fits: boolean\n overBy: number\n}\n\nexport function checkNavHeightBudget(usedPx: number, budgetPx: number = NAV_HEIGHT_BUDGET_PX): NavHeightBudget {\n return {\n usedPx,\n budgetPx,\n fits: usedPx <= budgetPx,\n overBy: Math.max(0, usedPx - budgetPx),\n }\n}\n\nexport function measureNavHeight(nav: HTMLElement): number {\n return nav.scrollHeight\n}\n"],"names":["NavItem","children","active","collapsed","count","countUrgent","href","onClick","dataProps","content","jsxs","Fragment","jsx","styles","NAV_HEIGHT_BUDGET_PX","checkNavHeightBudget","usedPx","budgetPx","measureNavHeight","nav"],"mappings":";;;;;;;;AAkBO,SAASA,EAAQ,EAAE,UAAAC,GAAU,QAAAC,IAAS,IAAO,WAAAC,IAAY,IAAO,OAAAC,GAAO,aAAAC,IAAc,IAAO,MAAAC,GAAM,SAAAC,EAAA,GAAyB;AAChI,QAAMC,IAAY;AAAA,IAChB,eAAeN,IAAU,SAAmB;AAAA,IAC5C,kBAAkBC,IAAa,SAAmB;AAAA,IAClD,gBAAgBD,IAAU,SAAmB;AAAA,EAAA,GAGzCO,IACJ,gBAAAC,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,WAAWC,EAAO,OAAQ,UAAAZ,GAAS;AAAA,sBACxC,QAAA,EAAK,WAAWY,EAAO,cACrB,eAAS,QAAQ,gBAAAD,EAAC,QAAA,EAAK,WAAWP,IAAcQ,EAAO,cAAcA,EAAO,OAAQ,aAAM,EAAA,CAC7F;AAAA,EAAA,GACF;AAGF,SAAIP,IAEA,gBAAAM,EAAC,OAAE,WAAWC,EAAO,KAAK,MAAAP,GAAY,SAAAC,GAAmB,GAAGC,GACzD,UAAAC,EAAA,CACH,IAKF,gBAAAG,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWC,EAAO,KAAK,SAAAN,GAAmB,GAAGC,GAChE,UAAAC,EAAA,CACH;AAEJ;AC5CO,MAAMK,IAAuB;AAS7B,SAASC,EAAqBC,GAAgBC,IAAmBH,GAAuC;AAC7G,SAAO;AAAA,IACL,QAAAE;AAAA,IACA,UAAAC;AAAA,IACA,MAAMD,KAAUC;AAAA,IAChB,QAAQ,KAAK,IAAI,GAAGD,IAASC,CAAQ;AAAA,EAAA;AAEzC;AAEO,SAASC,EAAiBC,GAA0B;AACzD,SAAOA,EAAI;AACb;"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const NAV_HEIGHT_BUDGET_PX = 700;
|
|
2
|
+
export interface NavHeightBudget {
|
|
3
|
+
usedPx: number;
|
|
4
|
+
budgetPx: number;
|
|
5
|
+
fits: boolean;
|
|
6
|
+
overBy: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function checkNavHeightBudget(usedPx: number, budgetPx?: number): NavHeightBudget;
|
|
9
|
+
export declare function measureNavHeight(nav: HTMLElement): number;
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--color-neutral-0: #FFFFFF;--color-neutral-50: #F7F7F8;--color-neutral-100: #EFEFF1;--color-neutral-200: #E5E5EA;--color-neutral-300: #D9D9D9;--color-neutral-400: #C1C1C2;--color-neutral-500: #8E8E93;--color-neutral-600: #737373;--color-neutral-700: #525252;--color-neutral-800: #3A3A3C;--color-neutral-900: #292929;--color-neutral-950: #1D1D1B;--color-neutral-1000: #000000;--color-orange-50: #FDF3EE;--color-orange-100: #FBE2D5;--color-orange-200: #F7C3A8;--color-orange-300: #F0A173;--color-orange-400: #EB7147;--color-orange-500: #E9521E;--color-orange-600: #C2410C;--color-orange-700: #9A3412;--color-sand-100: #FBF3E4;--color-sand-300: #F3D6AC;--color-sand-600: #8A6510;--color-green-100: #DDF0E4;--color-green-600: #157F3D;--color-crimson-100: #F7DEE1;--color-crimson-600: #A3182B;--color-blue-100: #DEE6FA;--color-blue-600: #2A5BD7;--color-background: var(--color-neutral-50);--color-surface: var(--color-neutral-0);--color-surface-muted: var(--color-neutral-100);--color-border: var(--color-neutral-200);--color-border-strong: var(--color-neutral-300);--color-foreground: var(--color-neutral-950);--color-foreground-secondary: var(--color-neutral-700);--color-foreground-muted: var(--color-neutral-600);--color-foreground-disabled: var(--color-neutral-400);--color-primary: var(--color-orange-500);--color-primary-hover: var(--color-orange-600);--color-primary-foreground: var(--color-neutral-0);--color-primary-subtle: var(--color-orange-50);--color-primary-border: var(--color-orange-200);--color-success: var(--color-green-600);--color-success-subtle: var(--color-green-100);--color-warning: var(--color-sand-600);--color-warning-subtle: var(--color-sand-300);--color-danger: var(--color-crimson-600);--color-danger-subtle: var(--color-crimson-100);--color-info: var(--color-blue-600);--color-info-subtle: var(--color-blue-100);--color-grant: var(--color-sand-600);--color-purchased: var(--color-neutral-950);--font-display: Outfit;--font-sans: Inter;--font-mono: Geist Mono;--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-base: 15px;--text-md: 16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 36px;--text-5xl: 46px;--text-6xl: 60px;--text-7xl: 78px;--text-nav-label: 14px;--font-weight-light: 300;--font-weight-regular: 400;--font-weight-medium: 500;--font-weight-semibold: 600;--font-weight-bold: 700;--tracking-tighter: -.03em;--tracking-tight: -.02em;--tracking-normal: 0em;--tracking-wide: .04em;--tracking-wider: .09em;--leading-none: 100%;--leading-tight: 120%;--leading-snug: 135%;--leading-normal: 150%;--leading-relaxed: 170%;--breakpoint-mobile: 390px;--breakpoint-desktop: 1440px;--container-sidebar: 264px;--container-content: 1440px;--spacing-1: 4px;--spacing-2: 8px;--spacing-3: 12px;--spacing-4: 16px;--spacing-5: 20px;--spacing-6: 24px;--spacing-8: 32px;--spacing-10: 40px;--spacing-12: 48px;--spacing-16: 64px;--spacing-20: 80px;--spacing-2_5: 10px;--spacing-5_5: 22px;--radius-none: 0px;--radius-xs: 2px;--radius-sm: 3px;--radius-md: 4px;--radius-lg: 6px;--radius-xl: 8px;--radius-2xl: 12px;--radius-full: 9999px}:root{--duration-fast: .12s;--easing-standard: cubic-bezier(.2, 0, 0, 1)}._row_5f12t_4{display:flex;align-items:center;width:100%;gap:var(--spacing-2_5);padding:var(--spacing-2) var(--spacing-2_5);border:none;border-radius:var(--radius-lg);background:transparent;font-family:var(--font-sans);text-align:left;text-decoration:none;cursor:pointer;transition:background-color var(--duration-fast) var(--easing-standard)}._row_5f12t_4:hover{background:var(--color-surface-muted)}._row_5f12t_4[data-active=true]{background:var(--color-surface)}._row_5f12t_4:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._label_5f12t_36{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--color-foreground-secondary);font-size:var(--text-nav-label);font-weight:var(--font-weight-regular);text-overflow:ellipsis;white-space:nowrap}._row_5f12t_4[data-active=true] ._label_5f12t_36{color:var(--color-foreground);font-weight:var(--font-weight-semibold)}._row_5f12t_4[data-collapsed=true] ._label_5f12t_36{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._trailingSlot_5f12t_62{display:flex;flex:0 0 var(--spacing-5_5);align-items:center;justify-content:center;width:var(--spacing-5_5);font-family:var(--font-sans);font-size:var(--text-nav-label)}._row_5f12t_4[data-collapsed=true] ._trailingSlot_5f12t_62{display:none}._count_5f12t_76{color:var(--color-foreground-muted)}._countUrgent_5f12t_80{color:var(--color-primary)}@media(prefers-reduced-motion:reduce){._row_5f12t_4{transition:none}}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boost33/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The app shell every Boost33 product renders inside.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"sideEffects": [
|
|
17
|
+
"**/*.css"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./style.css": "./dist/style.css"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"lint": "eslint .",
|
|
33
|
+
"build": "vite build",
|
|
34
|
+
"tokens": "npx tsx scripts/generate-tokens.mts",
|
|
35
|
+
"specimen:dev": "vite --config vite.specimen.config.mts",
|
|
36
|
+
"specimen:build": "vite build --config vite.specimen.config.mts",
|
|
37
|
+
"specimen:preview": "vite preview --config vite.specimen.config.mts",
|
|
38
|
+
"screenshot-diff": "npm run specimen:build && npx tsx scripts/screenshot-diff.mts",
|
|
39
|
+
"afk": "npx tsx .sandcastle/main.mts"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"react-dom": "^19.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@ai-hero/sandcastle": "^0.12.0",
|
|
47
|
+
"@eslint/js": "^10.0.1",
|
|
48
|
+
"@playwright/test": "^1.63.0",
|
|
49
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
50
|
+
"@testing-library/react": "^16.1.0",
|
|
51
|
+
"@testing-library/user-event": "^14.5.2",
|
|
52
|
+
"@types/node": "^24.0.0",
|
|
53
|
+
"@types/pixelmatch": "^5.2.6",
|
|
54
|
+
"@types/pngjs": "^6.0.5",
|
|
55
|
+
"@types/react": "^19.2.0",
|
|
56
|
+
"@types/react-dom": "^19.2.0",
|
|
57
|
+
"eslint": "^10.9.1",
|
|
58
|
+
"jsdom": "^25.0.1",
|
|
59
|
+
"pixelmatch": "^7.2.0",
|
|
60
|
+
"pngjs": "^7.0.0",
|
|
61
|
+
"react": "^19.2.0",
|
|
62
|
+
"react-dom": "^19.2.0",
|
|
63
|
+
"tsx": "^4.19.2",
|
|
64
|
+
"typescript": "^5.7.2",
|
|
65
|
+
"typescript-eslint": "^8.68.0",
|
|
66
|
+
"vite": "^6.0.7",
|
|
67
|
+
"vite-plugin-dts": "^4.4.0",
|
|
68
|
+
"vitest": "^4.1.10"
|
|
69
|
+
},
|
|
70
|
+
"allowScripts": {
|
|
71
|
+
"esbuild@0.28.2": true,
|
|
72
|
+
"esbuild@0.25.12": true,
|
|
73
|
+
"fsevents@2.3.3": true
|
|
74
|
+
}
|
|
75
|
+
}
|