@featurefoundry/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,18 @@
1
+ # Feature Foundry UI
2
+
3
+ Shared brand assets, base CSS, and React primitives for Feature Foundry apps.
4
+
5
+ This package is intentionally small: it keeps future apps visually aligned without forcing every feature into a heavy design system too early.
6
+
7
+ ## Usage
8
+
9
+ ```ts
10
+ import { BrandLink } from "@featurefoundry/ui";
11
+ import "@featurefoundry/ui/styles.css";
12
+ ```
13
+
14
+ For local apps, install it as a file dependency while the package is private:
15
+
16
+ ```sh
17
+ npm install ../featurefoundry-ui
18
+ ```
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@featurefoundry/ui",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Shared Feature Foundry brand assets, base styles, and React UI primitives.",
6
+ "peerDependencies": {
7
+ "react": "^19.1.0"
8
+ },
9
+ "devDependencies": {
10
+ "@types/react": "^19.1.0",
11
+ "typescript": "^5.8.0"
12
+ },
13
+ "scripts": {
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "main": "./src/index.tsx",
17
+ "types": "./src/index.tsx",
18
+ "style": "./styles.css",
19
+ "license": "MIT",
20
+ "files": [
21
+ "src",
22
+ "styles.css"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./src/index.tsx",
27
+ "import": "./src/index.tsx"
28
+ },
29
+ "./styles.css": "./styles.css",
30
+ "./favicon.svg": "./src/assets/favicon.svg"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Feature Foundry">
2
+ <rect width="64" height="64" rx="14" fill="#17201a" />
3
+ <path fill="#fffdf8" d="M15 16h35v9H25v8h20v9H25v16H15V16Z" />
4
+ <path fill="#fffdf8" d="M34 33h17v9h-7v16H34V33Z" />
5
+ <rect x="47" y="48" width="7" height="7" fill="#e05a3c" />
6
+ </svg>
@@ -0,0 +1,2 @@
1
+ declare module "*.png";
2
+ declare module "*.svg";
package/src/index.tsx ADDED
@@ -0,0 +1,113 @@
1
+ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import brandMarkUrl from "./assets/feature-foundry-app-icon.png";
4
+ import brandWordmarkUrl from "./assets/feature-foundry-wordmark.png";
5
+ import faviconUrl from "./assets/favicon.svg";
6
+
7
+ export { brandMarkUrl, brandWordmarkUrl, faviconUrl };
8
+
9
+ type BrandLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
10
+ label?: string;
11
+ };
12
+
13
+ export function BrandLink({ label = "Feature Foundry", className = "", ...props }: BrandLinkProps) {
14
+ return (
15
+ <a className={["ffBrand", className].filter(Boolean).join(" ")} {...props}>
16
+ <img className="ffBrandMark" src={brandMarkUrl} alt="" aria-hidden="true" />
17
+ <span>{label}</span>
18
+ </a>
19
+ );
20
+ }
21
+
22
+ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
23
+ active?: boolean;
24
+ };
25
+
26
+ export function ControlButton({ active = false, className = "", ...props }: ButtonProps) {
27
+ return (
28
+ <button
29
+ className={["ffControlButton", active ? "active" : "", className].filter(Boolean).join(" ")}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ export function Card({ children, className = "" }: { children: ReactNode; className?: string }) {
36
+ return <div className={["ffCard", className].filter(Boolean).join(" ")}>{children}</div>;
37
+ }
38
+
39
+ export type MenuItem = {
40
+ label: string;
41
+ href: string;
42
+ emphasis?: boolean;
43
+ };
44
+
45
+ type HamburgerMenuProps = {
46
+ items: MenuItem[];
47
+ label?: string;
48
+ };
49
+
50
+ export function HamburgerMenu({ items, label = "Primary navigation" }: HamburgerMenuProps) {
51
+ const [isOpen, setIsOpen] = useState(false);
52
+ const rootRef = useRef<HTMLDivElement>(null);
53
+
54
+ useEffect(() => {
55
+ function handlePointerDown(event: PointerEvent) {
56
+ if (!rootRef.current?.contains(event.target as Node)) {
57
+ setIsOpen(false);
58
+ }
59
+ }
60
+
61
+ function handleKeyDown(event: KeyboardEvent) {
62
+ if (event.key === "Escape") {
63
+ setIsOpen(false);
64
+ }
65
+ }
66
+
67
+ document.addEventListener("pointerdown", handlePointerDown);
68
+ document.addEventListener("keydown", handleKeyDown);
69
+
70
+ return () => {
71
+ document.removeEventListener("pointerdown", handlePointerDown);
72
+ document.removeEventListener("keydown", handleKeyDown);
73
+ };
74
+ }, []);
75
+
76
+ return (
77
+ <div className="ffNavControls" ref={rootRef}>
78
+ <button
79
+ className="ffMenuButton"
80
+ type="button"
81
+ aria-controls="featurefoundry-menu"
82
+ aria-expanded={isOpen}
83
+ aria-label={isOpen ? "Close primary navigation" : "Open primary navigation"}
84
+ onClick={() => setIsOpen((open) => !open)}
85
+ >
86
+ <span className="ffMenuGlyph" aria-hidden="true">
87
+ <span />
88
+ <span />
89
+ <span />
90
+ <span />
91
+ </span>
92
+ </button>
93
+ <nav
94
+ className={isOpen ? "ffNavMenu open" : "ffNavMenu"}
95
+ id="featurefoundry-menu"
96
+ aria-label={label}
97
+ >
98
+ <div className="ffNavMenuPanel">
99
+ {items.map((item) => (
100
+ <a
101
+ className={item.emphasis ? "ffNavLink emphasis" : "ffNavLink"}
102
+ href={item.href}
103
+ key={item.href}
104
+ onClick={() => setIsOpen(false)}
105
+ >
106
+ {item.label}
107
+ </a>
108
+ ))}
109
+ </div>
110
+ </nav>
111
+ </div>
112
+ );
113
+ }
package/src/styles.css ADDED
@@ -0,0 +1,228 @@
1
+ :root {
2
+ color: #17201a;
3
+ background: #f7f5ef;
4
+ font-family:
5
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
6
+ font-synthesis: none;
7
+ text-rendering: optimizeLegibility;
8
+ -webkit-font-smoothing: antialiased;
9
+ }
10
+
11
+ * {
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ margin: 0;
17
+ min-width: 320px;
18
+ min-height: 100vh;
19
+ }
20
+
21
+ a {
22
+ color: inherit;
23
+ text-decoration: none;
24
+ }
25
+
26
+ button,
27
+ input,
28
+ textarea,
29
+ select {
30
+ font: inherit;
31
+ }
32
+
33
+ h1,
34
+ h2,
35
+ h3,
36
+ p {
37
+ margin-top: 0;
38
+ }
39
+
40
+ h1 {
41
+ max-width: 920px;
42
+ margin-bottom: 24px;
43
+ font-size: clamp(2.7rem, 7vw, 6.3rem);
44
+ line-height: 0.98;
45
+ letter-spacing: 0;
46
+ }
47
+
48
+ h2 {
49
+ margin-bottom: 0;
50
+ max-width: 760px;
51
+ font-size: clamp(2rem, 4vw, 3.2rem);
52
+ line-height: 1.03;
53
+ letter-spacing: 0;
54
+ }
55
+
56
+ p {
57
+ color: #536057;
58
+ line-height: 1.65;
59
+ }
60
+
61
+ .ffPageWidth {
62
+ width: min(1120px, calc(100% - 40px));
63
+ margin-right: auto;
64
+ margin-left: auto;
65
+ }
66
+
67
+ .ffTopBar {
68
+ position: relative;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ gap: 24px;
73
+ min-height: 78px;
74
+ }
75
+
76
+ .ffBrand {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ gap: 10px;
80
+ font-weight: 850;
81
+ }
82
+
83
+ .ffBrandMark {
84
+ width: 34px;
85
+ height: 34px;
86
+ border: 1px solid rgba(23, 32, 26, 0.2);
87
+ border-radius: 8px;
88
+ background: #17201a;
89
+ object-fit: cover;
90
+ }
91
+
92
+ .ffEyebrow {
93
+ margin: 0 0 14px;
94
+ color: #b4432f;
95
+ font-size: 0.76rem;
96
+ font-weight: 850;
97
+ letter-spacing: 0.08em;
98
+ text-transform: uppercase;
99
+ }
100
+
101
+ .ffCard {
102
+ border: 1px solid rgba(23, 32, 26, 0.14);
103
+ border-radius: 8px;
104
+ background: #fffdf8;
105
+ }
106
+
107
+ .ffBackLink,
108
+ .ffControlButton {
109
+ display: inline-flex;
110
+ min-height: 42px;
111
+ align-items: center;
112
+ justify-content: center;
113
+ border: 1px solid rgba(23, 32, 26, 0.18);
114
+ border-radius: 8px;
115
+ padding: 0 14px;
116
+ background: #fffdf8;
117
+ color: #536057;
118
+ cursor: pointer;
119
+ font-weight: 820;
120
+ }
121
+
122
+ .ffBackLink:hover,
123
+ .ffControlButton:hover {
124
+ border-color: rgba(180, 67, 47, 0.55);
125
+ color: #17201a;
126
+ }
127
+
128
+ .ffControlButton.active {
129
+ border-color: #17201a;
130
+ background: #17201a;
131
+ color: #fffdf8;
132
+ }
133
+
134
+ .ffControlButton:disabled {
135
+ cursor: not-allowed;
136
+ opacity: 0.46;
137
+ }
138
+
139
+ @media (max-width: 860px) {
140
+ .ffPageWidth {
141
+ width: min(100% - 24px, 1120px);
142
+ }
143
+ }
144
+
145
+ .ffNavControls {
146
+ position: relative;
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 10px;
150
+ }
151
+
152
+ .ffMenuButton {
153
+ display: inline-grid;
154
+ width: 46px;
155
+ height: 46px;
156
+ place-items: center;
157
+ border: 1px solid rgba(23, 32, 26, 0.18);
158
+ border-radius: 8px;
159
+ padding: 0;
160
+ background: #fffdf8;
161
+ color: #17201a;
162
+ cursor: pointer;
163
+ }
164
+
165
+ .ffMenuGlyph {
166
+ display: grid;
167
+ grid-template-columns: repeat(2, 6px);
168
+ gap: 5px;
169
+ }
170
+
171
+ .ffMenuGlyph span {
172
+ display: block;
173
+ width: 6px;
174
+ height: 6px;
175
+ border-radius: 2px;
176
+ background: currentColor;
177
+ }
178
+
179
+ .ffMenuGlyph span:last-child {
180
+ background: #e05a3c;
181
+ }
182
+
183
+ .ffMenuButton:hover,
184
+ .ffMenuButton[aria-expanded="true"] {
185
+ background: rgba(23, 32, 26, 0.08);
186
+ }
187
+
188
+ .ffNavMenu {
189
+ position: absolute;
190
+ top: calc(100% + 8px);
191
+ right: 0;
192
+ z-index: 10;
193
+ display: none;
194
+ min-width: 220px;
195
+ }
196
+
197
+ .ffNavMenu.open {
198
+ display: block;
199
+ }
200
+
201
+ .ffNavMenuPanel {
202
+ display: grid;
203
+ gap: 4px;
204
+ border: 1px solid rgba(23, 32, 26, 0.14);
205
+ border-radius: 8px;
206
+ padding: 8px;
207
+ background: #fffdf8;
208
+ box-shadow: 0 18px 42px rgba(23, 32, 26, 0.12);
209
+ color: #536057;
210
+ font-size: 0.94rem;
211
+ }
212
+
213
+ .ffNavLink {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ border-radius: 8px;
217
+ padding: 9px 11px;
218
+ }
219
+
220
+ .ffNavLink:hover {
221
+ background: rgba(23, 32, 26, 0.08);
222
+ color: #17201a;
223
+ }
224
+
225
+ .ffNavLink.emphasis {
226
+ color: #b4432f;
227
+ font-weight: 760;
228
+ }
package/styles.css ADDED
@@ -0,0 +1 @@
1
+ @import "./src/styles.css";