@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 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;
@@ -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
+ }