@openkeyai/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scott Goodwin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # `@openkeyai/ui`
2
+
3
+ The shared design system + mandatory `<HubHeader />` for every [OpenKey AI](https://openkeyai.com) tool.
4
+
5
+ ```bash
6
+ pnpm add @openkeyai/ui
7
+ # or
8
+ npm i @openkeyai/ui
9
+ ```
10
+
11
+ ## Mounting `<HubHeader />`
12
+
13
+ Every tool **must** mount the header in its root layout. The OpenKey AI CI scanner (lands in Phase 9) enforces this — a tool repo's build will fail if the import + mount aren't both present.
14
+
15
+ ### Next.js (App Router)
16
+
17
+ ```tsx
18
+ // app/layout.tsx
19
+ import "@openkeyai/ui/css";
20
+ import { HubHeader } from "@openkeyai/ui";
21
+
22
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
23
+ // `user` comes from your tool's session helper (Phase 8's SDK ships one).
24
+ return (
25
+ <html lang="en">
26
+ <body style={{ paddingTop: "var(--okai-header-h)" }}>
27
+ <HubHeader
28
+ toolName="YouTube Thumbnails"
29
+ user={{
30
+ displayName: "Scott G.",
31
+ avatarUrl: null,
32
+ email: "scott@example.com",
33
+ }}
34
+ />
35
+ <main>{children}</main>
36
+ </body>
37
+ </html>
38
+ );
39
+ }
40
+ ```
41
+
42
+ The header is fixed-position at `z-index: var(--okai-z-header)` and `var(--okai-header-h)` (56px) tall. Leave that much top padding on your root.
43
+
44
+ ### Signed-out state
45
+
46
+ Pass `user={null}` (or omit) and the chip is replaced by a **Sign in** CTA that links back to `https://openkeyai.com/login`. Don't try to hide the chip altogether — it's part of the consistent user experience contract.
47
+
48
+ ## Tokens + Tailwind
49
+
50
+ Two consumption paths besides the CSS sheet:
51
+
52
+ ### Tailwind preset
53
+
54
+ ```ts
55
+ // tailwind.config.ts
56
+ import type { Config } from "tailwindcss";
57
+ import { hubTailwindPreset } from "@openkeyai/ui/tailwind";
58
+
59
+ export default {
60
+ presets: [hubTailwindPreset],
61
+ content: [
62
+ "./app/**/*.{ts,tsx}",
63
+ "./node_modules/@openkeyai/ui/dist/**/*.{js,cjs}",
64
+ ],
65
+ } satisfies Config;
66
+ ```
67
+
68
+ Now `bg-okai-bg`, `text-okai-foreground`, `rounded-okai-xl`, `pt-okai-header` etc. are available alongside Tailwind's defaults.
69
+
70
+ ### Direct token access
71
+
72
+ ```ts
73
+ import { tokens } from "@openkeyai/ui/tokens";
74
+
75
+ const indigo = tokens.color.accent; // "#7c6cf5"
76
+ ```
77
+
78
+ Use this for places utility classes can't reach — SVG gradients, canvas paints, programmatic colour pickups.
79
+
80
+ ## What you CANNOT do with `<HubHeader />`
81
+
82
+ - Hide the OpenKey AI brand mark
83
+ - Hide the user chip / sign-in CTA
84
+ - Replace the brand wordmark
85
+ - Change the header height or position
86
+ - Theme it to match your tool's brand only
87
+
88
+ The header is the shared trust surface — users need to recognise it across every tool. The `rightSlot` prop is the legitimate extension point for tool-specific controls (settings, help, share, …); they render to the left of the user chip.
89
+
90
+ If you have a legitimate need that the current API doesn't cover, open an issue rather than monkey-patching the package. Major version bumps require 60 days notice to tool authors.
91
+
92
+ ## API
93
+
94
+ ### `HubHeader`
95
+
96
+ ```ts
97
+ type HubHeaderProps = {
98
+ toolName: string;
99
+ user?: {
100
+ displayName: string;
101
+ avatarUrl?: string | null;
102
+ email?: string;
103
+ } | null;
104
+ hubUrl?: string; // default: "https://openkeyai.com"
105
+ rightSlot?: React.ReactNode;
106
+ className?: string;
107
+ style?: React.CSSProperties;
108
+ };
109
+ ```
110
+
111
+ ## Versioning
112
+
113
+ This package follows semver. Breaking changes to `<HubHeader />` are major-version bumps and require the 60-day notice flow described above. Token additions are minor; pure-CSS tweaks are patch.
114
+
115
+ `UI_VERSION` is re-exported from the entry so tools can log which build they shipped against:
116
+
117
+ ```ts
118
+ import { UI_VERSION } from "@openkeyai/ui";
119
+ console.log("openkeyai/ui", UI_VERSION); // → "0.1.0"
120
+ ```
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ pnpm install
126
+ pnpm dev # tsup --watch
127
+ pnpm build # tsup
128
+ pnpm typecheck
129
+ pnpm test
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/components/hub-header.tsx
7
+ var DEFAULT_HUB_URL = "https://openkeyai.com";
8
+ function getInitials(name) {
9
+ const parts = name.trim().split(/\s+/).filter(Boolean);
10
+ const first = parts[0];
11
+ if (!first) return "?";
12
+ if (parts.length === 1) return first.slice(0, 2).toUpperCase();
13
+ const last = parts[parts.length - 1] ?? first;
14
+ return ((first[0] ?? "") + (last[0] ?? "")).toUpperCase() || "?";
15
+ }
16
+ function HubHeader({
17
+ toolName,
18
+ user,
19
+ hubUrl = DEFAULT_HUB_URL,
20
+ rightSlot,
21
+ className,
22
+ style
23
+ }) {
24
+ const hubHref = hubUrl;
25
+ const accountHref = `${hubUrl.replace(/\/$/, "")}/settings/account`;
26
+ const signInHref = `${hubUrl.replace(/\/$/, "")}/login`;
27
+ return /* @__PURE__ */ jsxRuntime.jsxs(
28
+ "header",
29
+ {
30
+ className: className ? `okai-header ${className}` : "okai-header",
31
+ style,
32
+ role: "banner",
33
+ "data-okai-component": "hub-header",
34
+ children: [
35
+ /* @__PURE__ */ jsxRuntime.jsxs(
36
+ "a",
37
+ {
38
+ className: "okai-header__brand",
39
+ href: hubHref,
40
+ "aria-label": "Open OpenKey AI hub",
41
+ children: [
42
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "okai-header__brand-mark", "aria-hidden": "true" }),
43
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "okai-header__brand-text", children: "OpenKey AI" })
44
+ ]
45
+ }
46
+ ),
47
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "okai-header__divider", "aria-hidden": "true" }),
48
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "okai-header__tool-name", title: toolName, children: toolName }),
49
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "okai-header__spacer", "aria-hidden": "true" }),
50
+ rightSlot != null ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "okai-header__right", children: rightSlot }) : null,
51
+ user ? /* @__PURE__ */ jsxRuntime.jsxs(
52
+ "a",
53
+ {
54
+ className: "okai-header__user",
55
+ href: accountHref,
56
+ title: user.email ?? user.displayName,
57
+ "aria-label": `Open account for ${user.displayName}`,
58
+ children: [
59
+ user.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx(
60
+ "span",
61
+ {
62
+ className: "okai-header__user-avatar",
63
+ role: "img",
64
+ "aria-label": user.displayName,
65
+ style: { backgroundImage: `url(${user.avatarUrl})` }
66
+ }
67
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
68
+ "span",
69
+ {
70
+ className: "okai-header__user-fallback",
71
+ "aria-hidden": "true",
72
+ children: getInitials(user.displayName)
73
+ }
74
+ ),
75
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "okai-header__user-name", children: user.displayName })
76
+ ]
77
+ }
78
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
79
+ "a",
80
+ {
81
+ className: "okai-header__signin",
82
+ href: signInHref,
83
+ "aria-label": "Sign in to OpenKey AI",
84
+ children: "Sign in"
85
+ }
86
+ )
87
+ ]
88
+ }
89
+ );
90
+ }
91
+
92
+ // src/index.ts
93
+ var UI_VERSION = "0.1.0";
94
+
95
+ exports.HubHeader = HubHeader;
96
+ exports.UI_VERSION = UI_VERSION;
97
+ //# sourceMappingURL=index.cjs.map
98
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/hub-header.tsx","../src/index.ts"],"names":["jsxs","jsx"],"mappings":"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","file":"index.cjs","sourcesContent":["import * as React from \"react\";\n\n/**\n * Mandatory shared header for every OpenKey AI tool.\n *\n * What it gives the user (consistent across every tool):\n * - The hub brand mark — clicking it returns to the hub\n * - The tool's name — so users know which tool they're in\n * - The signed-in user (avatar + display name) — clicking opens the hub\n * account page\n * - A sign-in CTA when no user is provided\n *\n * What it deliberately does NOT do (per docs/TOOL_CONTRACT.md):\n * - Provide props to hide the brand mark, tool name, or user/sign-in chip\n * - Theme the colours away from the hub palette\n * - Allow the tool to swap the brand text\n *\n * The header is fixed to the top of the viewport at `z-index:\n * var(--okai-z-header)` and is `var(--okai-header-h)` tall. Tools should\n * leave that much top padding on their root layout. The styles live in\n * `@openkeyai/ui/css` — import it once in the tool's global CSS.\n *\n * SSR-safe: pure render, no client-only APIs in the default path.\n */\nexport type HubHeaderUser = {\n /** Visible label on the chip. */\n displayName: string;\n /** Optional avatar; falls back to initials when missing. */\n avatarUrl?: string | null;\n /** Used as a tooltip and accessible label. Not displayed. */\n email?: string;\n};\n\nexport type HubHeaderProps = {\n /** Display name of the tool. Goes after the divider. */\n toolName: string;\n /** Signed-in user, or null/undefined to show the Sign in CTA. */\n user?: HubHeaderUser | null;\n /** Override the hub root URL. Defaults to https://openkeyai.com. */\n hubUrl?: string;\n /** Optional right-side slot — tool-specific controls (settings, help, …).\n * Rendered BEFORE the user chip so the chip always ends the row. */\n rightSlot?: React.ReactNode;\n /** Forwarded to the outer <header>. Avoid resetting position/z-index. */\n className?: string;\n /** Optional inline style override for the outer <header>. */\n style?: React.CSSProperties;\n};\n\nconst DEFAULT_HUB_URL = \"https://openkeyai.com\";\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n const first = parts[0];\n if (!first) return \"?\";\n if (parts.length === 1) return first.slice(0, 2).toUpperCase();\n const last = parts[parts.length - 1] ?? first;\n return ((first[0] ?? \"\") + (last[0] ?? \"\")).toUpperCase() || \"?\";\n}\n\n/**\n * `<HubHeader />` — the mandatory header. See module doc above.\n */\nexport function HubHeader({\n toolName,\n user,\n hubUrl = DEFAULT_HUB_URL,\n rightSlot,\n className,\n style,\n}: HubHeaderProps) {\n const hubHref = hubUrl;\n const accountHref = `${hubUrl.replace(/\\/$/, \"\")}/settings/account`;\n const signInHref = `${hubUrl.replace(/\\/$/, \"\")}/login`;\n\n return (\n <header\n className={\n className ? `okai-header ${className}` : \"okai-header\"\n }\n style={style}\n role=\"banner\"\n data-okai-component=\"hub-header\"\n >\n <a\n className=\"okai-header__brand\"\n href={hubHref}\n aria-label=\"Open OpenKey AI hub\"\n >\n <span className=\"okai-header__brand-mark\" aria-hidden=\"true\" />\n <span className=\"okai-header__brand-text\">OpenKey AI</span>\n </a>\n\n <span className=\"okai-header__divider\" aria-hidden=\"true\" />\n\n <span className=\"okai-header__tool-name\" title={toolName}>\n {toolName}\n </span>\n\n <span className=\"okai-header__spacer\" aria-hidden=\"true\" />\n\n {rightSlot != null ? (\n <div className=\"okai-header__right\">{rightSlot}</div>\n ) : null}\n\n {user ? (\n <a\n className=\"okai-header__user\"\n href={accountHref}\n title={user.email ?? user.displayName}\n aria-label={`Open account for ${user.displayName}`}\n >\n {user.avatarUrl ? (\n <span\n className=\"okai-header__user-avatar\"\n role=\"img\"\n aria-label={user.displayName}\n style={{ backgroundImage: `url(${user.avatarUrl})` }}\n />\n ) : (\n <span\n className=\"okai-header__user-fallback\"\n aria-hidden=\"true\"\n >\n {getInitials(user.displayName)}\n </span>\n )}\n <span className=\"okai-header__user-name\">{user.displayName}</span>\n </a>\n ) : (\n <a\n className=\"okai-header__signin\"\n href={signInHref}\n aria-label=\"Sign in to OpenKey AI\"\n >\n Sign in\n </a>\n )}\n </header>\n );\n}\n","/**\n * `@openkeyai/ui` — public entry.\n *\n * Every OpenKey AI tool installs this package and mounts <HubHeader /> in\n * its root layout. CI in `Scott-Builds-AI/.github` enforces the mounting\n * via an AST scanner (lands in Phase 9).\n *\n * Entry paths:\n * - \"@openkeyai/ui\" → React components + their types (this file)\n * - \"@openkeyai/ui/tokens\" → Design tokens as a JS object\n * - \"@openkeyai/ui/tailwind\" → Tailwind preset\n * - \"@openkeyai/ui/css\" → The global stylesheet (CSS custom\n * properties + scoped .okai-* classes).\n * MUST be imported once by the consumer.\n *\n * See README.md for the canonical mounting pattern.\n */\n\nexport { HubHeader } from \"./components/hub-header\";\nexport type {\n HubHeaderProps,\n HubHeaderUser,\n} from \"./components/hub-header\";\n\n/** Bumped on each release. Useful for tools to log which UI version they shipped against. */\nexport const UI_VERSION = \"0.1.0\";\n"]}
@@ -0,0 +1,74 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * Mandatory shared header for every OpenKey AI tool.
5
+ *
6
+ * What it gives the user (consistent across every tool):
7
+ * - The hub brand mark — clicking it returns to the hub
8
+ * - The tool's name — so users know which tool they're in
9
+ * - The signed-in user (avatar + display name) — clicking opens the hub
10
+ * account page
11
+ * - A sign-in CTA when no user is provided
12
+ *
13
+ * What it deliberately does NOT do (per docs/TOOL_CONTRACT.md):
14
+ * - Provide props to hide the brand mark, tool name, or user/sign-in chip
15
+ * - Theme the colours away from the hub palette
16
+ * - Allow the tool to swap the brand text
17
+ *
18
+ * The header is fixed to the top of the viewport at `z-index:
19
+ * var(--okai-z-header)` and is `var(--okai-header-h)` tall. Tools should
20
+ * leave that much top padding on their root layout. The styles live in
21
+ * `@openkeyai/ui/css` — import it once in the tool's global CSS.
22
+ *
23
+ * SSR-safe: pure render, no client-only APIs in the default path.
24
+ */
25
+ type HubHeaderUser = {
26
+ /** Visible label on the chip. */
27
+ displayName: string;
28
+ /** Optional avatar; falls back to initials when missing. */
29
+ avatarUrl?: string | null;
30
+ /** Used as a tooltip and accessible label. Not displayed. */
31
+ email?: string;
32
+ };
33
+ type HubHeaderProps = {
34
+ /** Display name of the tool. Goes after the divider. */
35
+ toolName: string;
36
+ /** Signed-in user, or null/undefined to show the Sign in CTA. */
37
+ user?: HubHeaderUser | null;
38
+ /** Override the hub root URL. Defaults to https://openkeyai.com. */
39
+ hubUrl?: string;
40
+ /** Optional right-side slot — tool-specific controls (settings, help, …).
41
+ * Rendered BEFORE the user chip so the chip always ends the row. */
42
+ rightSlot?: React.ReactNode;
43
+ /** Forwarded to the outer <header>. Avoid resetting position/z-index. */
44
+ className?: string;
45
+ /** Optional inline style override for the outer <header>. */
46
+ style?: React.CSSProperties;
47
+ };
48
+ /**
49
+ * `<HubHeader />` — the mandatory header. See module doc above.
50
+ */
51
+ declare function HubHeader({ toolName, user, hubUrl, rightSlot, className, style, }: HubHeaderProps): React.JSX.Element;
52
+
53
+ /**
54
+ * `@openkeyai/ui` — public entry.
55
+ *
56
+ * Every OpenKey AI tool installs this package and mounts <HubHeader /> in
57
+ * its root layout. CI in `Scott-Builds-AI/.github` enforces the mounting
58
+ * via an AST scanner (lands in Phase 9).
59
+ *
60
+ * Entry paths:
61
+ * - "@openkeyai/ui" → React components + their types (this file)
62
+ * - "@openkeyai/ui/tokens" → Design tokens as a JS object
63
+ * - "@openkeyai/ui/tailwind" → Tailwind preset
64
+ * - "@openkeyai/ui/css" → The global stylesheet (CSS custom
65
+ * properties + scoped .okai-* classes).
66
+ * MUST be imported once by the consumer.
67
+ *
68
+ * See README.md for the canonical mounting pattern.
69
+ */
70
+
71
+ /** Bumped on each release. Useful for tools to log which UI version they shipped against. */
72
+ declare const UI_VERSION = "0.1.0";
73
+
74
+ export { HubHeader, type HubHeaderProps, type HubHeaderUser, UI_VERSION };
@@ -0,0 +1,74 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * Mandatory shared header for every OpenKey AI tool.
5
+ *
6
+ * What it gives the user (consistent across every tool):
7
+ * - The hub brand mark — clicking it returns to the hub
8
+ * - The tool's name — so users know which tool they're in
9
+ * - The signed-in user (avatar + display name) — clicking opens the hub
10
+ * account page
11
+ * - A sign-in CTA when no user is provided
12
+ *
13
+ * What it deliberately does NOT do (per docs/TOOL_CONTRACT.md):
14
+ * - Provide props to hide the brand mark, tool name, or user/sign-in chip
15
+ * - Theme the colours away from the hub palette
16
+ * - Allow the tool to swap the brand text
17
+ *
18
+ * The header is fixed to the top of the viewport at `z-index:
19
+ * var(--okai-z-header)` and is `var(--okai-header-h)` tall. Tools should
20
+ * leave that much top padding on their root layout. The styles live in
21
+ * `@openkeyai/ui/css` — import it once in the tool's global CSS.
22
+ *
23
+ * SSR-safe: pure render, no client-only APIs in the default path.
24
+ */
25
+ type HubHeaderUser = {
26
+ /** Visible label on the chip. */
27
+ displayName: string;
28
+ /** Optional avatar; falls back to initials when missing. */
29
+ avatarUrl?: string | null;
30
+ /** Used as a tooltip and accessible label. Not displayed. */
31
+ email?: string;
32
+ };
33
+ type HubHeaderProps = {
34
+ /** Display name of the tool. Goes after the divider. */
35
+ toolName: string;
36
+ /** Signed-in user, or null/undefined to show the Sign in CTA. */
37
+ user?: HubHeaderUser | null;
38
+ /** Override the hub root URL. Defaults to https://openkeyai.com. */
39
+ hubUrl?: string;
40
+ /** Optional right-side slot — tool-specific controls (settings, help, …).
41
+ * Rendered BEFORE the user chip so the chip always ends the row. */
42
+ rightSlot?: React.ReactNode;
43
+ /** Forwarded to the outer <header>. Avoid resetting position/z-index. */
44
+ className?: string;
45
+ /** Optional inline style override for the outer <header>. */
46
+ style?: React.CSSProperties;
47
+ };
48
+ /**
49
+ * `<HubHeader />` — the mandatory header. See module doc above.
50
+ */
51
+ declare function HubHeader({ toolName, user, hubUrl, rightSlot, className, style, }: HubHeaderProps): React.JSX.Element;
52
+
53
+ /**
54
+ * `@openkeyai/ui` — public entry.
55
+ *
56
+ * Every OpenKey AI tool installs this package and mounts <HubHeader /> in
57
+ * its root layout. CI in `Scott-Builds-AI/.github` enforces the mounting
58
+ * via an AST scanner (lands in Phase 9).
59
+ *
60
+ * Entry paths:
61
+ * - "@openkeyai/ui" → React components + their types (this file)
62
+ * - "@openkeyai/ui/tokens" → Design tokens as a JS object
63
+ * - "@openkeyai/ui/tailwind" → Tailwind preset
64
+ * - "@openkeyai/ui/css" → The global stylesheet (CSS custom
65
+ * properties + scoped .okai-* classes).
66
+ * MUST be imported once by the consumer.
67
+ *
68
+ * See README.md for the canonical mounting pattern.
69
+ */
70
+
71
+ /** Bumped on each release. Useful for tools to log which UI version they shipped against. */
72
+ declare const UI_VERSION = "0.1.0";
73
+
74
+ export { HubHeader, type HubHeaderProps, type HubHeaderUser, UI_VERSION };
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ import 'react';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ // src/components/hub-header.tsx
5
+ var DEFAULT_HUB_URL = "https://openkeyai.com";
6
+ function getInitials(name) {
7
+ const parts = name.trim().split(/\s+/).filter(Boolean);
8
+ const first = parts[0];
9
+ if (!first) return "?";
10
+ if (parts.length === 1) return first.slice(0, 2).toUpperCase();
11
+ const last = parts[parts.length - 1] ?? first;
12
+ return ((first[0] ?? "") + (last[0] ?? "")).toUpperCase() || "?";
13
+ }
14
+ function HubHeader({
15
+ toolName,
16
+ user,
17
+ hubUrl = DEFAULT_HUB_URL,
18
+ rightSlot,
19
+ className,
20
+ style
21
+ }) {
22
+ const hubHref = hubUrl;
23
+ const accountHref = `${hubUrl.replace(/\/$/, "")}/settings/account`;
24
+ const signInHref = `${hubUrl.replace(/\/$/, "")}/login`;
25
+ return /* @__PURE__ */ jsxs(
26
+ "header",
27
+ {
28
+ className: className ? `okai-header ${className}` : "okai-header",
29
+ style,
30
+ role: "banner",
31
+ "data-okai-component": "hub-header",
32
+ children: [
33
+ /* @__PURE__ */ jsxs(
34
+ "a",
35
+ {
36
+ className: "okai-header__brand",
37
+ href: hubHref,
38
+ "aria-label": "Open OpenKey AI hub",
39
+ children: [
40
+ /* @__PURE__ */ jsx("span", { className: "okai-header__brand-mark", "aria-hidden": "true" }),
41
+ /* @__PURE__ */ jsx("span", { className: "okai-header__brand-text", children: "OpenKey AI" })
42
+ ]
43
+ }
44
+ ),
45
+ /* @__PURE__ */ jsx("span", { className: "okai-header__divider", "aria-hidden": "true" }),
46
+ /* @__PURE__ */ jsx("span", { className: "okai-header__tool-name", title: toolName, children: toolName }),
47
+ /* @__PURE__ */ jsx("span", { className: "okai-header__spacer", "aria-hidden": "true" }),
48
+ rightSlot != null ? /* @__PURE__ */ jsx("div", { className: "okai-header__right", children: rightSlot }) : null,
49
+ user ? /* @__PURE__ */ jsxs(
50
+ "a",
51
+ {
52
+ className: "okai-header__user",
53
+ href: accountHref,
54
+ title: user.email ?? user.displayName,
55
+ "aria-label": `Open account for ${user.displayName}`,
56
+ children: [
57
+ user.avatarUrl ? /* @__PURE__ */ jsx(
58
+ "span",
59
+ {
60
+ className: "okai-header__user-avatar",
61
+ role: "img",
62
+ "aria-label": user.displayName,
63
+ style: { backgroundImage: `url(${user.avatarUrl})` }
64
+ }
65
+ ) : /* @__PURE__ */ jsx(
66
+ "span",
67
+ {
68
+ className: "okai-header__user-fallback",
69
+ "aria-hidden": "true",
70
+ children: getInitials(user.displayName)
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsx("span", { className: "okai-header__user-name", children: user.displayName })
74
+ ]
75
+ }
76
+ ) : /* @__PURE__ */ jsx(
77
+ "a",
78
+ {
79
+ className: "okai-header__signin",
80
+ href: signInHref,
81
+ "aria-label": "Sign in to OpenKey AI",
82
+ children: "Sign in"
83
+ }
84
+ )
85
+ ]
86
+ }
87
+ );
88
+ }
89
+
90
+ // src/index.ts
91
+ var UI_VERSION = "0.1.0";
92
+
93
+ export { HubHeader, UI_VERSION };
94
+ //# sourceMappingURL=index.js.map
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/hub-header.tsx","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\n\n/**\n * Mandatory shared header for every OpenKey AI tool.\n *\n * What it gives the user (consistent across every tool):\n * - The hub brand mark — clicking it returns to the hub\n * - The tool's name — so users know which tool they're in\n * - The signed-in user (avatar + display name) — clicking opens the hub\n * account page\n * - A sign-in CTA when no user is provided\n *\n * What it deliberately does NOT do (per docs/TOOL_CONTRACT.md):\n * - Provide props to hide the brand mark, tool name, or user/sign-in chip\n * - Theme the colours away from the hub palette\n * - Allow the tool to swap the brand text\n *\n * The header is fixed to the top of the viewport at `z-index:\n * var(--okai-z-header)` and is `var(--okai-header-h)` tall. Tools should\n * leave that much top padding on their root layout. The styles live in\n * `@openkeyai/ui/css` — import it once in the tool's global CSS.\n *\n * SSR-safe: pure render, no client-only APIs in the default path.\n */\nexport type HubHeaderUser = {\n /** Visible label on the chip. */\n displayName: string;\n /** Optional avatar; falls back to initials when missing. */\n avatarUrl?: string | null;\n /** Used as a tooltip and accessible label. Not displayed. */\n email?: string;\n};\n\nexport type HubHeaderProps = {\n /** Display name of the tool. Goes after the divider. */\n toolName: string;\n /** Signed-in user, or null/undefined to show the Sign in CTA. */\n user?: HubHeaderUser | null;\n /** Override the hub root URL. Defaults to https://openkeyai.com. */\n hubUrl?: string;\n /** Optional right-side slot — tool-specific controls (settings, help, …).\n * Rendered BEFORE the user chip so the chip always ends the row. */\n rightSlot?: React.ReactNode;\n /** Forwarded to the outer <header>. Avoid resetting position/z-index. */\n className?: string;\n /** Optional inline style override for the outer <header>. */\n style?: React.CSSProperties;\n};\n\nconst DEFAULT_HUB_URL = \"https://openkeyai.com\";\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n const first = parts[0];\n if (!first) return \"?\";\n if (parts.length === 1) return first.slice(0, 2).toUpperCase();\n const last = parts[parts.length - 1] ?? first;\n return ((first[0] ?? \"\") + (last[0] ?? \"\")).toUpperCase() || \"?\";\n}\n\n/**\n * `<HubHeader />` — the mandatory header. See module doc above.\n */\nexport function HubHeader({\n toolName,\n user,\n hubUrl = DEFAULT_HUB_URL,\n rightSlot,\n className,\n style,\n}: HubHeaderProps) {\n const hubHref = hubUrl;\n const accountHref = `${hubUrl.replace(/\\/$/, \"\")}/settings/account`;\n const signInHref = `${hubUrl.replace(/\\/$/, \"\")}/login`;\n\n return (\n <header\n className={\n className ? `okai-header ${className}` : \"okai-header\"\n }\n style={style}\n role=\"banner\"\n data-okai-component=\"hub-header\"\n >\n <a\n className=\"okai-header__brand\"\n href={hubHref}\n aria-label=\"Open OpenKey AI hub\"\n >\n <span className=\"okai-header__brand-mark\" aria-hidden=\"true\" />\n <span className=\"okai-header__brand-text\">OpenKey AI</span>\n </a>\n\n <span className=\"okai-header__divider\" aria-hidden=\"true\" />\n\n <span className=\"okai-header__tool-name\" title={toolName}>\n {toolName}\n </span>\n\n <span className=\"okai-header__spacer\" aria-hidden=\"true\" />\n\n {rightSlot != null ? (\n <div className=\"okai-header__right\">{rightSlot}</div>\n ) : null}\n\n {user ? (\n <a\n className=\"okai-header__user\"\n href={accountHref}\n title={user.email ?? user.displayName}\n aria-label={`Open account for ${user.displayName}`}\n >\n {user.avatarUrl ? (\n <span\n className=\"okai-header__user-avatar\"\n role=\"img\"\n aria-label={user.displayName}\n style={{ backgroundImage: `url(${user.avatarUrl})` }}\n />\n ) : (\n <span\n className=\"okai-header__user-fallback\"\n aria-hidden=\"true\"\n >\n {getInitials(user.displayName)}\n </span>\n )}\n <span className=\"okai-header__user-name\">{user.displayName}</span>\n </a>\n ) : (\n <a\n className=\"okai-header__signin\"\n href={signInHref}\n aria-label=\"Sign in to OpenKey AI\"\n >\n Sign in\n </a>\n )}\n </header>\n );\n}\n","/**\n * `@openkeyai/ui` — public entry.\n *\n * Every OpenKey AI tool installs this package and mounts <HubHeader /> in\n * its root layout. CI in `Scott-Builds-AI/.github` enforces the mounting\n * via an AST scanner (lands in Phase 9).\n *\n * Entry paths:\n * - \"@openkeyai/ui\" → React components + their types (this file)\n * - \"@openkeyai/ui/tokens\" → Design tokens as a JS object\n * - \"@openkeyai/ui/tailwind\" → Tailwind preset\n * - \"@openkeyai/ui/css\" → The global stylesheet (CSS custom\n * properties + scoped .okai-* classes).\n * MUST be imported once by the consumer.\n *\n * See README.md for the canonical mounting pattern.\n */\n\nexport { HubHeader } from \"./components/hub-header\";\nexport type {\n HubHeaderProps,\n HubHeaderUser,\n} from \"./components/hub-header\";\n\n/** Bumped on each release. Useful for tools to log which UI version they shipped against. */\nexport const UI_VERSION = \"0.1.0\";\n"]}