@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 +21 -0
- package/README.md +134 -0
- package/dist/index.cjs +98 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +308 -0
- package/dist/tailwind.cjs +152 -0
- package/dist/tailwind.cjs.map +1 -0
- package/dist/tailwind.d.cts +83 -0
- package/dist/tailwind.d.ts +83 -0
- package/dist/tailwind.js +147 -0
- package/dist/tailwind.js.map +1 -0
- package/dist/tokens.cjs +109 -0
- package/dist/tokens.cjs.map +1 -0
- package/dist/tokens.d.cts +121 -0
- package/dist/tokens.d.ts +121 -0
- package/dist/tokens.js +107 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +68 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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"]}
|