@xzibit/ui 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -2,7 +2,50 @@
2
2
 
3
3
  All notable changes to `@xzibit/ui` are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/) loosely; versioning follows [SemVer](https://semver.org/).
4
4
 
5
- ## [0.1.0] — 2026-05-22
5
+ ## [0.2.0] — 2026-05-24
6
+
7
+ ### Added
8
+
9
+ - **`<ContentContainer tier="...">`** — implements DESIGN-STANDARD v2.4 §Content Density Tiers. Three tiers: `'editorial'` (720px), `'reference'` (1200px, DEFAULT), `'data'` (unconstrained with 32px side padding). Drop-in wrapper for `<main>` content. Per-page overrides via `tier` prop.
10
+ - **`ContentTier`** type exported for app authors wanting to type tier props at their own layer.
11
+
12
+ ### Coming next
13
+
14
+ - `<Toast />` + `useToast()` — wrapper around `sonner` per DESIGN-STANDARD §Toast / Notification (v2.2). Will ship as v0.2.1 once drafted.
15
+ - `<Modal />` — wrapper around `@radix-ui/react-dialog` per DESIGN-STANDARD §Modal / Dialog (v2.2). Will ship as v0.2.2 once drafted.
16
+
17
+ (Joel chose Option A 2026-05-24 — fast-ship v0.2.0 with just ContentContainer to unblock portfolio-wide adoption of the new content tier system. Toast + Modal ship as subsequent patches.)
18
+
19
+ ---
20
+
21
+ ## [0.1.1] — 2026-05-24
22
+
23
+ ### Fixed
24
+ - **Peer dependency widened** to accept React 19 (`react@"^18.0.0 || ^19.0.0"` and `react-dom` same). Surfaced during ERP Overview migration — `--legacy-peer-deps` was needed to install on the React 19 app. Affects every React 19 app planning to adopt; v0.1.0 blocked clean install.
25
+ - **API field-name contract resilience.** `useApps` now normalizes raw Supabase column names (`app_url`, `display_section`, `display_order`) to the canonical `App` shape (`url`, `section`, `section_order`) via internal `normalizeApp()` at the fetch boundary. Apps with existing endpoints returning raw query rows now work without changes; new endpoints can return either shape. Surfaced during ERP Overview migration — package rendered empty dropdown until CC added a manual `.map()` normalization in the route handler.
26
+
27
+ ### Added
28
+ - `RawApp` type exported for apps that want to type their endpoint response explicitly
29
+ - `normalizeApp(raw: RawApp): App` helper exported for apps that want to call it directly (rare; `useApps` calls it automatically)
30
+
31
+ ### Documentation
32
+ - README's `/api/me/apps` section now documents both accepted field-name conventions and the normalization behaviour.
33
+
34
+ ---
35
+
36
+ ## [0.1.0] — 2026-05-22 (drafted) / 2026-05-23 (published)
37
+
38
+ ### Published artifact
39
+ - npm: https://www.npmjs.com/package/@xzibit/ui
40
+ - Provenance attestation: present
41
+ - Install: `npm install @xzibit/ui`
42
+
43
+ ### Bug fixes applied between draft and publish (sync'd back into this draft 2026-05-23)
44
+ - **`package-lock.json` generated** via `npm install` (not present in initial draft — required for `npm ci` in CI).
45
+ - **Removed stale `import React` namespace imports** from 4 source files (`TopBar.tsx`, `XzibitMark.tsx`, `BackToLauncher.tsx`, `AppsDropdown.tsx`). The package's `tsconfig.json` uses `"jsx": "react-jsx"` (modern transform, doesn't need React import) + `noUnusedLocals: true`, so bare `import React from 'react'` fails the DTS build. Fixed by removing the unused namespace import — named imports (`useState`, `useEffect`, etc.) where actually needed are kept.
46
+
47
+ ### Future-package note
48
+ When drafting any future React component package for the `@xzibit/*` scope, omit `import React from 'react'` unless React is actually used directly (e.g. `React.forwardRef`). Just import the hooks / types you need by name.
6
49
 
7
50
  ### Added
8
51
 
package/README.md CHANGED
@@ -58,7 +58,15 @@ That's it. The bar renders:
58
58
 
59
59
  ### `/api/me/apps` endpoint required
60
60
 
61
- `AppsDropdown` calls `GET /api/me/apps` (same-origin) for the cross-app navigation list. Each consuming app must expose this endpoint per CODING-STANDARDS §6.3:
61
+ `AppsDropdown` calls `GET /api/me/apps` (same-origin) for the cross-app navigation list. Each consuming app must expose this endpoint per CODING-STANDARDS §6.3.
62
+
63
+ **Field-name contract (v0.1.1+):** the endpoint can return EITHER:
64
+ - **Canonical shape** (recommended for new endpoints): `{ name, url, description?, section?, section_order? }`
65
+ - **Supabase column-name passthrough** (for endpoints that return raw `public.apps` rows): `{ name, app_url, description?, display_section?, display_order? }`
66
+
67
+ `useApps` normalizes both shapes to the canonical `App` interface at the fetch boundary — components downstream never see the raw form. Apps with existing endpoints that return raw column names work without changes.
68
+
69
+ Example route handler:
62
70
 
63
71
  ```typescript
64
72
  // src/app/api/me/apps/route.ts
@@ -138,6 +146,7 @@ This sets `--xz-charcoal`, `--xz-teal`, `--xz-white`, `--border`, etc. — and `
138
146
  | `<AppsDropdown />` | Sectioned + alphabetical apps dropdown driven by `/api/me/apps` |
139
147
  | `<XzibitMark size={28} />` | Xzibit X brand mark as inline SVG (any size, any density) |
140
148
  | `useApps()` | React hook fetching `/api/me/apps` with loading + error + refetch |
149
+ | `<ContentContainer tier="reference">` *(v0.2+)* | Content max-width container per DESIGN-STANDARD v2.4 §Content Density Tiers — tiers: `'editorial'` (720px), `'reference'` (1200px, default), `'data'` (unconstrained). Wraps `<main>` content. |
141
150
 
142
151
  ---
143
152
 
package/dist/index.cjs CHANGED
@@ -22,8 +22,10 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AppsDropdown: () => AppsDropdown,
24
24
  BackToLauncher: () => BackToLauncher,
25
+ ContentContainer: () => ContentContainer,
25
26
  TopBar: () => TopBar,
26
27
  XzibitMark: () => XzibitMark,
28
+ normalizeApp: () => normalizeApp,
27
29
  useApps: () => useApps
28
30
  });
29
31
  module.exports = __toCommonJS(index_exports);
@@ -133,6 +135,19 @@ var import_react3 = require("react");
133
135
 
134
136
  // src/useApps.ts
135
137
  var import_react2 = require("react");
138
+
139
+ // src/types.ts
140
+ function normalizeApp(raw) {
141
+ return {
142
+ name: raw.name,
143
+ url: raw.url ?? raw.app_url ?? "",
144
+ description: raw.description,
145
+ section: raw.section ?? raw.display_section ?? null,
146
+ section_order: raw.section_order ?? raw.display_order
147
+ };
148
+ }
149
+
150
+ // src/useApps.ts
136
151
  function useApps(options = {}) {
137
152
  const { endpoint = "/api/me/apps", lazy = false } = options;
138
153
  const [apps, setApps] = (0, import_react2.useState)([]);
@@ -153,7 +168,7 @@ function useApps(options = {}) {
153
168
  if (data.error) {
154
169
  throw new Error(data.error);
155
170
  }
156
- setApps(data.apps ?? []);
171
+ setApps((data.apps ?? []).map(normalizeApp));
157
172
  } catch (err) {
158
173
  console.error("[@xzibit/ui useApps] fetch failed:", err);
159
174
  setError(err instanceof Error ? err.message : "Unknown error");
@@ -545,11 +560,28 @@ function BuildBadge({
545
560
  }
546
561
  );
547
562
  }
563
+
564
+ // src/ContentContainer.tsx
565
+ var import_jsx_runtime5 = require("react/jsx-runtime");
566
+ var TIER_STYLES = {
567
+ editorial: { maxWidth: 720, margin: "0 auto", padding: "3rem 2rem" },
568
+ reference: { maxWidth: 1200, margin: "0 auto", padding: "3rem 2rem" },
569
+ data: { maxWidth: "100%", margin: "0 auto", padding: "1.5rem 2rem" }
570
+ };
571
+ function ContentContainer({
572
+ tier = "reference",
573
+ className,
574
+ children
575
+ }) {
576
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, style: TIER_STYLES[tier], children });
577
+ }
548
578
  // Annotate the CommonJS export names for ESM import in node:
549
579
  0 && (module.exports = {
550
580
  AppsDropdown,
551
581
  BackToLauncher,
582
+ ContentContainer,
552
583
  TopBar,
553
584
  XzibitMark,
585
+ normalizeApp,
554
586
  useApps
555
587
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
2
3
 
3
4
  interface TopBarProps {
4
5
  /** App display name shown in the wordmark slot (e.g. "ERP Overview"). */
@@ -95,11 +96,12 @@ declare function XzibitMark({ size, className, ariaLabel }: XzibitMarkProps): re
95
96
  * Shared types for @xzibit/ui.
96
97
  */
97
98
  /**
98
- * An app in the Xzibit Apps portfolio.
99
+ * Canonical app shape consumed by `@xzibit/ui` components.
99
100
  *
100
- * Shape matches the response from each app's `/api/me/apps` endpoint,
101
- * which queries `public.apps` JOIN `public.role_app_permissions` and returns
102
- * the apps the authenticated user has access to per their role.
101
+ * This is what `useApps` returns after normalization. App authors writing
102
+ * NEW `/api/me/apps` endpoints SHOULD return this shape directly. Apps with
103
+ * existing endpoints that return raw Supabase column names (see `RawApp` below)
104
+ * are also accepted — `useApps` normalizes at the fetch boundary.
103
105
  */
104
106
  interface App {
105
107
  /** Display name, e.g. "Capacity Planner". */
@@ -113,16 +115,46 @@ interface App {
113
115
  /** Sort order for the section itself (matches launcher curation). */
114
116
  section_order?: number;
115
117
  }
118
+ /**
119
+ * Raw shape accepted from `/api/me/apps` endpoints.
120
+ *
121
+ * Supports two field-naming conventions:
122
+ * - **Canonical** (recommended for new endpoints): `url`, `section`, `section_order`
123
+ * - **Supabase column-name passthrough** (for endpoints that return raw query rows):
124
+ * `app_url`, `display_section`, `display_order`
125
+ *
126
+ * `useApps` normalizes to the canonical `App` shape via `normalizeApp()` at the
127
+ * fetch boundary — components downstream only see the canonical shape.
128
+ *
129
+ * Added in v0.1.1 (2026-05-24) after ERP Overview migration surfaced the contract
130
+ * mismatch with raw Supabase column names.
131
+ */
132
+ interface RawApp {
133
+ name: string;
134
+ url?: string;
135
+ app_url?: string;
136
+ description?: string;
137
+ section?: string | null;
138
+ display_section?: string | null;
139
+ section_order?: number;
140
+ display_order?: number;
141
+ }
116
142
  /**
117
143
  * Response shape from `/api/me/apps`.
118
144
  *
119
145
  * Per CODING-STANDARDS §6.4 — successful responses are wrapped in a `data`
120
- * or domain-specific key (in this case `apps`).
146
+ * or domain-specific key (in this case `apps`). The `apps` array contains
147
+ * `RawApp` items; `useApps` normalizes them to `App`.
121
148
  */
122
149
  interface AppsResponse {
123
- apps?: App[];
150
+ apps?: RawApp[];
124
151
  error?: string;
125
152
  }
153
+ /**
154
+ * Internal helper — normalizes a `RawApp` to the canonical `App` shape.
155
+ * Used by `useApps` at the fetch boundary; not typically called by consumers.
156
+ */
157
+ declare function normalizeApp(raw: RawApp): App;
126
158
 
127
159
  interface UseAppsResult {
128
160
  apps: App[];
@@ -150,4 +182,50 @@ interface UseAppsOptions {
150
182
  */
151
183
  declare function useApps(options?: UseAppsOptions): UseAppsResult;
152
184
 
153
- export { type App, AppsDropdown, type AppsDropdownProps, type AppsResponse, BackToLauncher, type BackToLauncherProps, TopBar, type TopBarProps, type UseAppsOptions, type UseAppsResult, XzibitMark, type XzibitMarkProps, useApps };
185
+ /**
186
+ * Content density tier per DESIGN-STANDARD v2.4 §Content Density Tiers.
187
+ *
188
+ * - `'editorial'`: 720px max-width — marketing / public-facing docs / long-form prose
189
+ * - `'reference'` (DEFAULT): 1200px max-width — internal docs / wikis / dashboards / most app pages
190
+ * - `'data'`: unconstrained with 32px min side padding — wide tables / kanban / calendars / heatmaps
191
+ */
192
+ type ContentTier = 'editorial' | 'reference' | 'data';
193
+ interface ContentContainerProps {
194
+ /**
195
+ * Content density tier — picks max-width + padding.
196
+ * Defaults to `'reference'` (the portfolio default per v2.4).
197
+ */
198
+ tier?: ContentTier;
199
+ /** Optional className for additional styling. */
200
+ className?: string;
201
+ /** Children to render inside the container. */
202
+ children: ReactNode;
203
+ }
204
+ /**
205
+ * Content container that applies the right max-width + padding for its content
206
+ * density tier per DESIGN-STANDARD v2.4 §Content Density Tiers.
207
+ *
208
+ * Per the spec:
209
+ * - Component override > page override > app default (most-specific wins)
210
+ * - Cards INHERIT the parent container's max-width — they do NOT set their own
211
+ *
212
+ * Typical app usage in `src/app/layout.tsx`:
213
+ *
214
+ * ```tsx
215
+ * <main id="main" style={{ marginTop: 44 }}>
216
+ * <ContentContainer>{children}</ContentContainer>
217
+ * </main>
218
+ * ```
219
+ *
220
+ * Per-page override (rare):
221
+ *
222
+ * ```tsx
223
+ * // A wide-table page within an otherwise-reference app
224
+ * <ContentContainer tier="data">
225
+ * <DataTable ... />
226
+ * </ContentContainer>
227
+ * ```
228
+ */
229
+ declare function ContentContainer({ tier, className, children, }: ContentContainerProps): react_jsx_runtime.JSX.Element;
230
+
231
+ export { type App, AppsDropdown, type AppsDropdownProps, type AppsResponse, BackToLauncher, type BackToLauncherProps, ContentContainer, type ContentContainerProps, type ContentTier, type RawApp, TopBar, type TopBarProps, type UseAppsOptions, type UseAppsResult, XzibitMark, type XzibitMarkProps, normalizeApp, useApps };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
2
3
 
3
4
  interface TopBarProps {
4
5
  /** App display name shown in the wordmark slot (e.g. "ERP Overview"). */
@@ -95,11 +96,12 @@ declare function XzibitMark({ size, className, ariaLabel }: XzibitMarkProps): re
95
96
  * Shared types for @xzibit/ui.
96
97
  */
97
98
  /**
98
- * An app in the Xzibit Apps portfolio.
99
+ * Canonical app shape consumed by `@xzibit/ui` components.
99
100
  *
100
- * Shape matches the response from each app's `/api/me/apps` endpoint,
101
- * which queries `public.apps` JOIN `public.role_app_permissions` and returns
102
- * the apps the authenticated user has access to per their role.
101
+ * This is what `useApps` returns after normalization. App authors writing
102
+ * NEW `/api/me/apps` endpoints SHOULD return this shape directly. Apps with
103
+ * existing endpoints that return raw Supabase column names (see `RawApp` below)
104
+ * are also accepted — `useApps` normalizes at the fetch boundary.
103
105
  */
104
106
  interface App {
105
107
  /** Display name, e.g. "Capacity Planner". */
@@ -113,16 +115,46 @@ interface App {
113
115
  /** Sort order for the section itself (matches launcher curation). */
114
116
  section_order?: number;
115
117
  }
118
+ /**
119
+ * Raw shape accepted from `/api/me/apps` endpoints.
120
+ *
121
+ * Supports two field-naming conventions:
122
+ * - **Canonical** (recommended for new endpoints): `url`, `section`, `section_order`
123
+ * - **Supabase column-name passthrough** (for endpoints that return raw query rows):
124
+ * `app_url`, `display_section`, `display_order`
125
+ *
126
+ * `useApps` normalizes to the canonical `App` shape via `normalizeApp()` at the
127
+ * fetch boundary — components downstream only see the canonical shape.
128
+ *
129
+ * Added in v0.1.1 (2026-05-24) after ERP Overview migration surfaced the contract
130
+ * mismatch with raw Supabase column names.
131
+ */
132
+ interface RawApp {
133
+ name: string;
134
+ url?: string;
135
+ app_url?: string;
136
+ description?: string;
137
+ section?: string | null;
138
+ display_section?: string | null;
139
+ section_order?: number;
140
+ display_order?: number;
141
+ }
116
142
  /**
117
143
  * Response shape from `/api/me/apps`.
118
144
  *
119
145
  * Per CODING-STANDARDS §6.4 — successful responses are wrapped in a `data`
120
- * or domain-specific key (in this case `apps`).
146
+ * or domain-specific key (in this case `apps`). The `apps` array contains
147
+ * `RawApp` items; `useApps` normalizes them to `App`.
121
148
  */
122
149
  interface AppsResponse {
123
- apps?: App[];
150
+ apps?: RawApp[];
124
151
  error?: string;
125
152
  }
153
+ /**
154
+ * Internal helper — normalizes a `RawApp` to the canonical `App` shape.
155
+ * Used by `useApps` at the fetch boundary; not typically called by consumers.
156
+ */
157
+ declare function normalizeApp(raw: RawApp): App;
126
158
 
127
159
  interface UseAppsResult {
128
160
  apps: App[];
@@ -150,4 +182,50 @@ interface UseAppsOptions {
150
182
  */
151
183
  declare function useApps(options?: UseAppsOptions): UseAppsResult;
152
184
 
153
- export { type App, AppsDropdown, type AppsDropdownProps, type AppsResponse, BackToLauncher, type BackToLauncherProps, TopBar, type TopBarProps, type UseAppsOptions, type UseAppsResult, XzibitMark, type XzibitMarkProps, useApps };
185
+ /**
186
+ * Content density tier per DESIGN-STANDARD v2.4 §Content Density Tiers.
187
+ *
188
+ * - `'editorial'`: 720px max-width — marketing / public-facing docs / long-form prose
189
+ * - `'reference'` (DEFAULT): 1200px max-width — internal docs / wikis / dashboards / most app pages
190
+ * - `'data'`: unconstrained with 32px min side padding — wide tables / kanban / calendars / heatmaps
191
+ */
192
+ type ContentTier = 'editorial' | 'reference' | 'data';
193
+ interface ContentContainerProps {
194
+ /**
195
+ * Content density tier — picks max-width + padding.
196
+ * Defaults to `'reference'` (the portfolio default per v2.4).
197
+ */
198
+ tier?: ContentTier;
199
+ /** Optional className for additional styling. */
200
+ className?: string;
201
+ /** Children to render inside the container. */
202
+ children: ReactNode;
203
+ }
204
+ /**
205
+ * Content container that applies the right max-width + padding for its content
206
+ * density tier per DESIGN-STANDARD v2.4 §Content Density Tiers.
207
+ *
208
+ * Per the spec:
209
+ * - Component override > page override > app default (most-specific wins)
210
+ * - Cards INHERIT the parent container's max-width — they do NOT set their own
211
+ *
212
+ * Typical app usage in `src/app/layout.tsx`:
213
+ *
214
+ * ```tsx
215
+ * <main id="main" style={{ marginTop: 44 }}>
216
+ * <ContentContainer>{children}</ContentContainer>
217
+ * </main>
218
+ * ```
219
+ *
220
+ * Per-page override (rare):
221
+ *
222
+ * ```tsx
223
+ * // A wide-table page within an otherwise-reference app
224
+ * <ContentContainer tier="data">
225
+ * <DataTable ... />
226
+ * </ContentContainer>
227
+ * ```
228
+ */
229
+ declare function ContentContainer({ tier, className, children, }: ContentContainerProps): react_jsx_runtime.JSX.Element;
230
+
231
+ export { type App, AppsDropdown, type AppsDropdownProps, type AppsResponse, BackToLauncher, type BackToLauncherProps, ContentContainer, type ContentContainerProps, type ContentTier, type RawApp, TopBar, type TopBarProps, type UseAppsOptions, type UseAppsResult, XzibitMark, type XzibitMarkProps, normalizeApp, useApps };
package/dist/index.js CHANGED
@@ -103,6 +103,19 @@ import { useState as useState3, useEffect as useEffect2, useRef, Fragment } from
103
103
 
104
104
  // src/useApps.ts
105
105
  import { useState as useState2, useEffect, useCallback } from "react";
106
+
107
+ // src/types.ts
108
+ function normalizeApp(raw) {
109
+ return {
110
+ name: raw.name,
111
+ url: raw.url ?? raw.app_url ?? "",
112
+ description: raw.description,
113
+ section: raw.section ?? raw.display_section ?? null,
114
+ section_order: raw.section_order ?? raw.display_order
115
+ };
116
+ }
117
+
118
+ // src/useApps.ts
106
119
  function useApps(options = {}) {
107
120
  const { endpoint = "/api/me/apps", lazy = false } = options;
108
121
  const [apps, setApps] = useState2([]);
@@ -123,7 +136,7 @@ function useApps(options = {}) {
123
136
  if (data.error) {
124
137
  throw new Error(data.error);
125
138
  }
126
- setApps(data.apps ?? []);
139
+ setApps((data.apps ?? []).map(normalizeApp));
127
140
  } catch (err) {
128
141
  console.error("[@xzibit/ui useApps] fetch failed:", err);
129
142
  setError(err instanceof Error ? err.message : "Unknown error");
@@ -515,10 +528,27 @@ function BuildBadge({
515
528
  }
516
529
  );
517
530
  }
531
+
532
+ // src/ContentContainer.tsx
533
+ import { jsx as jsx5 } from "react/jsx-runtime";
534
+ var TIER_STYLES = {
535
+ editorial: { maxWidth: 720, margin: "0 auto", padding: "3rem 2rem" },
536
+ reference: { maxWidth: 1200, margin: "0 auto", padding: "3rem 2rem" },
537
+ data: { maxWidth: "100%", margin: "0 auto", padding: "1.5rem 2rem" }
538
+ };
539
+ function ContentContainer({
540
+ tier = "reference",
541
+ className,
542
+ children
543
+ }) {
544
+ return /* @__PURE__ */ jsx5("div", { className, style: TIER_STYLES[tier], children });
545
+ }
518
546
  export {
519
547
  AppsDropdown,
520
548
  BackToLauncher,
549
+ ContentContainer,
521
550
  TopBar,
522
551
  XzibitMark,
552
+ normalizeApp,
523
553
  useApps
524
554
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xzibit/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared chrome components for the Xzibit Apps portfolio. v0.1: TopBar + BackToLauncher + AppsDropdown + XzibitMark + useApps hook. Single source of truth for portfolio chrome — tweak once, every app picks it up on next deploy.",
5
5
  "license": "MIT",
6
6
  "author": "Xzibit Apps",
@@ -42,8 +42,8 @@
42
42
  "prepublishOnly": "npm run build"
43
43
  },
44
44
  "peerDependencies": {
45
- "react": "^18.0.0",
46
- "react-dom": "^18.0.0"
45
+ "react": "^18.0.0 || ^19.0.0",
46
+ "react-dom": "^18.0.0 || ^19.0.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "typescript": "^5.0.0",