create-feedbax 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) 2024 Feedbax
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,3 @@
1
+ # create-feedbax
2
+
3
+ Generate an owned, Notion-backed Feedbax portal with `npx create-feedbax@latest`.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { createProject, resolveCreateOptions, resolveInteractiveCreateOptions, } from './index.js';
4
+ try {
5
+ const argv = process.argv.slice(2);
6
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
7
+ const options = interactive
8
+ ? await (async () => {
9
+ const terminal = createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ });
13
+ try {
14
+ return await resolveInteractiveCreateOptions(argv, async (question, defaultValue) => terminal.question(`${question} [${defaultValue}]: `));
15
+ }
16
+ finally {
17
+ terminal.close();
18
+ }
19
+ })()
20
+ : resolveCreateOptions(argv);
21
+ const result = await createProject(options);
22
+ process.stdout.write(`Created Feedbax in ${result.destination}\n\n${result.nextSteps.join('\n')}\n`);
23
+ }
24
+ catch (error) {
25
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
26
+ process.exitCode = 1;
27
+ }
28
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,+BAA+B,GAChC,MAAM,YAAY,CAAA;AAEnB,IAAI,CAAC;IACH,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAClC,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAA;IAC/D,MAAM,OAAO,GAAG,WAAW;QACzB,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;YAChB,MAAM,QAAQ,GAAG,eAAe,CAAC;gBAC/B,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,MAAM,EAAE,OAAO,CAAC,MAAM;aACvB,CAAC,CAAA;YACF,IAAI,CAAC;gBACH,OAAO,MAAM,+BAA+B,CAC1C,IAAI,EACJ,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,CAC/B,QAAQ,CAAC,QAAQ,CAAC,GAAG,QAAQ,KAAK,YAAY,KAAK,CAAC,CACvD,CAAA;YACH,CAAC;oBAAS,CAAC;gBACT,QAAQ,CAAC,KAAK,EAAE,CAAA;YAClB,CAAC;QACH,CAAC,CAAC,EAAE;QACN,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC9B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,sBAAsB,MAAM,CAAC,WAAW,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC/E,CAAA;AACH,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC9D,CAAA;IACD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACtB,CAAC"}
@@ -0,0 +1,21 @@
1
+ type IdentityMode = 'anonymous' | 'email' | 'handoff';
2
+ export type Deployment = 'cloudflare' | 'vercel' | 'node';
3
+ export type PackageManager = 'npm' | 'pnpm';
4
+ export interface CreateOptions {
5
+ readonly directory: string;
6
+ readonly identity: IdentityMode;
7
+ readonly deploy: Deployment;
8
+ readonly packageManager: PackageManager;
9
+ readonly preset: string;
10
+ readonly install: boolean;
11
+ readonly git: boolean;
12
+ }
13
+ export declare function resolveCreateOptions(argv: readonly string[]): CreateOptions;
14
+ export type CreatePrompt = (question: string, defaultValue: string) => Promise<string>;
15
+ export declare function resolveInteractiveCreateOptions(argv: readonly string[], prompt: CreatePrompt): Promise<CreateOptions>;
16
+ export declare function createProject(options: CreateOptions): Promise<{
17
+ destination: string;
18
+ nextSteps: string[];
19
+ }>;
20
+ export {};
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,KAAK,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,CAAA;AAErD,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAA;AACzD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,CAAA;AAC3C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAoBD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CA6C3E;AAED,MAAM,MAAM,YAAY,GAAG,CACzB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,KACjB,OAAO,CAAC,MAAM,CAAC,CAAA;AAEpB,wBAAsB,+BAA+B,CACnD,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,aAAa,CAAC,CAaxB;AAyCD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa;;;GA8DzD"}
package/dist/index.js ADDED
@@ -0,0 +1,173 @@
1
+ import { cp, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const valueFlags = new Set([
6
+ '--connector',
7
+ '--identity',
8
+ '--storage',
9
+ '--deploy',
10
+ '--preset',
11
+ '--package-manager',
12
+ ]);
13
+ const booleanFlags = new Set(['--yes', '--no-install', '--no-git']);
14
+ const allowedDeployments = new Set(['cloudflare', 'vercel', 'node']);
15
+ const allowedPackageManagers = new Set(['npm', 'pnpm']);
16
+ const identityMode = (mode) => {
17
+ if (mode === 'anonymous' || mode === 'email' || mode === 'handoff')
18
+ return mode;
19
+ throw new Error(`Identity mode "${mode}" is not supported in Feedbax 0.1.0.`);
20
+ };
21
+ export function resolveCreateOptions(argv) {
22
+ const values = new Map();
23
+ let directory;
24
+ for (let index = 0; index < argv.length; index += 1) {
25
+ const entry = argv[index];
26
+ if (valueFlags.has(entry)) {
27
+ const next = argv[index + 1];
28
+ if (!next || next.startsWith('-'))
29
+ throw new Error(`Option ${entry} requires a value.`);
30
+ values.set(entry, next);
31
+ index += 1;
32
+ }
33
+ else if (booleanFlags.has(entry))
34
+ continue;
35
+ else if (entry.startsWith('-'))
36
+ throw new Error(`Unknown option ${entry}.`);
37
+ else if (directory)
38
+ throw new Error(`Unexpected argument ${entry}.`);
39
+ else
40
+ directory = entry;
41
+ }
42
+ const value = (name) => values.get(name);
43
+ directory ??= 'my-feedback';
44
+ const connector = value('--connector') ?? 'notion';
45
+ const storage = value('--storage') ?? 'notion';
46
+ if (connector !== 'notion')
47
+ throw new Error(`Connector "${connector}" is not supported in Feedbax 0.1.0.`);
48
+ if (storage !== 'notion')
49
+ throw new Error(`Storage "${storage}" is deferred until after Feedbax 0.1.0.`);
50
+ const identity = identityMode(value('--identity') ?? 'email');
51
+ const deploy = (value('--deploy') ?? 'cloudflare');
52
+ if (!allowedDeployments.has(deploy))
53
+ throw new Error(`Deployment "${deploy}" is not supported.`);
54
+ const packageManager = (value('--package-manager') ??
55
+ 'pnpm');
56
+ if (!allowedPackageManagers.has(packageManager))
57
+ throw new Error(`Package manager "${packageManager}" is not supported.`);
58
+ return {
59
+ directory,
60
+ identity,
61
+ deploy,
62
+ packageManager,
63
+ preset: value('--preset') ?? 'feedbax-default',
64
+ install: !argv.includes('--no-install'),
65
+ git: !argv.includes('--no-git'),
66
+ };
67
+ }
68
+ export async function resolveInteractiveCreateOptions(argv, prompt) {
69
+ if (argv.includes('--yes'))
70
+ return resolveCreateOptions(argv);
71
+ const prompted = [...argv];
72
+ const ask = async (flag, question, defaultValue) => {
73
+ if (argv.includes(flag))
74
+ return;
75
+ const answer = (await prompt(question, defaultValue)).trim() || defaultValue;
76
+ prompted.push(flag, answer);
77
+ };
78
+ await ask('--identity', 'Identity (anonymous, email, handoff)', 'email');
79
+ await ask('--deploy', 'Deployment (cloudflare, vercel, node)', 'cloudflare');
80
+ await ask('--package-manager', 'Package manager (pnpm, npm)', 'pnpm');
81
+ await ask('--preset', 'shadcn preset', 'feedbax-default');
82
+ return resolveCreateOptions(prompted);
83
+ }
84
+ async function assertEmpty(path) {
85
+ try {
86
+ if ((await readdir(path)).length > 0)
87
+ throw new Error(`Target directory is not empty: ${path}`);
88
+ }
89
+ catch (error) {
90
+ if (error.code !== 'ENOENT')
91
+ throw error;
92
+ }
93
+ }
94
+ async function replaceInTree(path, replacements) {
95
+ for (const entry of await readdir(path)) {
96
+ const target = join(path, entry);
97
+ if ((await stat(target)).isDirectory())
98
+ await replaceInTree(target, replacements);
99
+ else {
100
+ let content = await readFile(target, 'utf8');
101
+ for (const [token, replacement] of Object.entries(replacements))
102
+ content = content.replaceAll(token, replacement);
103
+ await writeFile(target, content);
104
+ }
105
+ }
106
+ }
107
+ const run = (command, args, cwd) => new Promise((resolvePromise, reject) => {
108
+ const child = spawn(command, args, { cwd, stdio: 'inherit', shell: false });
109
+ child.once('error', reject);
110
+ child.once('exit', (code) => code === 0
111
+ ? resolvePromise()
112
+ : reject(new Error(`${command} ${args.join(' ')} failed with ${code}.`)));
113
+ });
114
+ export async function createProject(options) {
115
+ const destination = resolve(options.directory);
116
+ await assertEmpty(destination);
117
+ await mkdir(dirname(destination), { recursive: true });
118
+ const packageDirectory = dirname(fileURLToPath(import.meta.url));
119
+ const candidates = [
120
+ resolve(packageDirectory, '../template'),
121
+ resolve(packageDirectory, '../../../templates/default'),
122
+ ];
123
+ let template;
124
+ for (const candidate of candidates) {
125
+ try {
126
+ if ((await stat(candidate)).isDirectory()) {
127
+ template = candidate;
128
+ break;
129
+ }
130
+ }
131
+ catch {
132
+ continue;
133
+ }
134
+ }
135
+ if (!template)
136
+ throw new Error('The Feedbax project template is missing from this package.');
137
+ await cp(template, destination, { recursive: true });
138
+ await replaceInTree(destination, {
139
+ __GENERATOR_VERSION__: '0.1.0',
140
+ __IDENTITY__: options.identity,
141
+ __DEPLOYMENT__: options.deploy,
142
+ __PACKAGE_MANAGER__: options.packageManager,
143
+ __PACKAGE_MANAGER_SPEC__: options.packageManager === 'pnpm' ? 'pnpm@10.29.3' : 'npm@11.4.2',
144
+ __PRESET__: options.preset,
145
+ __NITRO_PRESET__: options.deploy === 'vercel'
146
+ ? 'vercel'
147
+ : options.deploy === 'cloudflare'
148
+ ? 'cloudflare-module'
149
+ : 'node-server',
150
+ });
151
+ try {
152
+ if (options.install) {
153
+ await run(options.packageManager, ['install'], destination);
154
+ await run(options.packageManager, ['run', 'type-check'], destination);
155
+ }
156
+ if (options.git)
157
+ await run('git', ['init'], destination);
158
+ }
159
+ catch (error) {
160
+ await writeFile(join(destination, '.feedbax-recovery'), `Generation stopped: ${error instanceof Error ? error.message : String(error)}\nRerun dependency installation and type checking after resolving the reported issue.\n`);
161
+ throw error;
162
+ }
163
+ return {
164
+ destination,
165
+ nextSteps: [
166
+ `cd ${options.directory}`,
167
+ 'Copy .env.example to .env and configure Notion',
168
+ `${options.packageManager === 'pnpm' ? 'pnpm' : 'npx'} feedbax doctor`,
169
+ `${options.packageManager} run dev`,
170
+ ],
171
+ };
172
+ }
173
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "create-feedbax",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "create-feedbax": "dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "template",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "dependencies": {},
15
+ "devDependencies": {
16
+ "@types/node": "24.10.13"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "type-check": "tsc -p tsconfig.json --noEmit",
21
+ "lint": "eslint src test",
22
+ "test": "vitest run"
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ # Server-only Notion integration token. Never expose this to browser code.
2
+ NOTION_TOKEN=
3
+ # At least 32 random bytes used to sign opaque visitor sessions.
4
+ FEEDBAX_SESSION_SECRET=
@@ -0,0 +1,7 @@
1
+ # Feedbax portal
2
+
3
+ 1. Copy `.env.example` to `.env` and configure the server-only values.
4
+ 2. Run `npx feedbax doctor`.
5
+ 3. Run `pnpm dev` (or `npm run dev`).
6
+
7
+ Email identity is capture-only and does not verify ownership. Notion-backed interaction uniqueness is best effort.
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/styles.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "iconLibrary": "lucide",
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@feedbax/portal",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite dev --port 3000",
7
+ "build": "vite build",
8
+ "type-check": "tsc --noEmit"
9
+ },
10
+ "dependencies": {
11
+ "@feedbax/core": "^0.1.0",
12
+ "@feedbax/notion": "^0.1.0",
13
+ "@tanstack/react-router": "1.170.18",
14
+ "@tanstack/react-start": "1.168.20",
15
+ "class-variance-authority": "0.7.1",
16
+ "clsx": "2.1.1",
17
+ "react": "^19.2.4",
18
+ "react-dom": "^19.2.4",
19
+ "tailwind-merge": "3.5.0"
20
+ },
21
+ "devDependencies": {
22
+ "@tailwindcss/vite": "4.1.18",
23
+ "@tanstack/router-plugin": "1.168.20",
24
+ "@types/react": "^19.2.14",
25
+ "@types/react-dom": "^19.2.3",
26
+ "@vitejs/plugin-react": "5.1.4",
27
+ "nitro": "3.0.260610-beta",
28
+ "tailwindcss": "4.1.18",
29
+ "vite": "7.3.6"
30
+ }
31
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,59 @@
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+ // Generated by TanStack Router. It is refreshed by the Vite router plugin.
4
+
5
+ import { Route as rootRouteImport } from './routes/__root'
6
+ import { Route as IndexRouteImport } from './routes/index'
7
+
8
+ const IndexRoute = IndexRouteImport.update({
9
+ id: '/',
10
+ path: '/',
11
+ getParentRoute: () => rootRouteImport,
12
+ } as any)
13
+
14
+ export interface FileRoutesByFullPath {
15
+ '/': typeof IndexRoute
16
+ }
17
+ export interface FileRoutesByTo {
18
+ '/': typeof IndexRoute
19
+ }
20
+ export interface FileRoutesById {
21
+ __root__: typeof rootRouteImport
22
+ '/': typeof IndexRoute
23
+ }
24
+ export interface FileRouteTypes {
25
+ fileRoutesByFullPath: FileRoutesByFullPath
26
+ fullPaths: '/'
27
+ fileRoutesByTo: FileRoutesByTo
28
+ to: '/'
29
+ id: '__root__' | '/'
30
+ fileRoutesById: FileRoutesById
31
+ }
32
+ export interface RootRouteChildren {
33
+ IndexRoute: typeof IndexRoute
34
+ }
35
+
36
+ declare module '@tanstack/react-router' {
37
+ interface FileRoutesByPath {
38
+ '/': {
39
+ id: '/'
40
+ path: '/'
41
+ fullPath: '/'
42
+ preLoaderRoute: typeof IndexRouteImport
43
+ parentRoute: typeof rootRouteImport
44
+ }
45
+ }
46
+ }
47
+
48
+ const rootRouteChildren: RootRouteChildren = { IndexRoute }
49
+ export const routeTree = rootRouteImport
50
+ ._addFileChildren(rootRouteChildren)
51
+ ._addFileTypes<FileRouteTypes>()
52
+
53
+ import type { getRouter } from './router.tsx'
54
+ declare module '@tanstack/react-start' {
55
+ interface Register {
56
+ ssr: true
57
+ router: Awaited<ReturnType<typeof getRouter>>
58
+ }
59
+ }
@@ -0,0 +1,10 @@
1
+ import { createRouter } from '@tanstack/react-router'
2
+ import { routeTree } from './routeTree.gen'
3
+ export function getRouter() {
4
+ return createRouter({ routeTree, scrollRestoration: true })
5
+ }
6
+ declare module '@tanstack/react-router' {
7
+ interface Register {
8
+ router: ReturnType<typeof getRouter>
9
+ }
10
+ }
@@ -0,0 +1,31 @@
1
+ import {
2
+ createRootRoute,
3
+ HeadContent,
4
+ Outlet,
5
+ Scripts,
6
+ } from '@tanstack/react-router'
7
+ import styles from '../styles.css?url'
8
+ export const Route = createRootRoute({
9
+ head: () => ({
10
+ meta: [
11
+ { charSet: 'utf-8' },
12
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
13
+ { title: 'Feedbax' },
14
+ ],
15
+ links: [{ rel: 'stylesheet', href: styles }],
16
+ }),
17
+ component: Root,
18
+ })
19
+ function Root() {
20
+ return (
21
+ <html lang="en">
22
+ <head>
23
+ <HeadContent />
24
+ </head>
25
+ <body>
26
+ <Outlet />
27
+ <Scripts />
28
+ </body>
29
+ </html>
30
+ )
31
+ }
@@ -0,0 +1,35 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ export const Route = createFileRoute('/')({ component: Portal })
3
+ function Portal() {
4
+ return (
5
+ <main>
6
+ <header>
7
+ <strong>Feedbax</strong>
8
+ <nav>
9
+ <a href="/">Feedback</a>
10
+ <a href="/roadmap">Roadmap</a>
11
+ <a href="/changelog">Changelog</a>
12
+ </nav>
13
+ </header>
14
+ <section>
15
+ <p className="eyebrow">PUBLIC FEEDBACK</p>
16
+ <h1>What should we build next?</h1>
17
+ <p>
18
+ Browse ideas, vote on priorities, and share feedback with the team.
19
+ </p>
20
+ <button type="button">Submit feedback</button>
21
+ </section>
22
+ <section aria-label="Feedback">
23
+ <div className="toolbar">
24
+ <input aria-label="Search feedback" placeholder="Search feedback" />
25
+ <select aria-label="Status">
26
+ <option>All statuses</option>
27
+ </select>
28
+ </div>
29
+ <p className="empty">
30
+ Connect Notion and run <code>feedbax doctor</code> to load your board.
31
+ </p>
32
+ </section>
33
+ </main>
34
+ )
35
+ }
@@ -0,0 +1,91 @@
1
+ @import 'tailwindcss';
2
+
3
+ :root {
4
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
5
+ color-scheme: light dark;
6
+ --background: #f6f5f1;
7
+ --foreground: #171815;
8
+ --primary: #315d42;
9
+ --border: #d7d7cf;
10
+ --radius: 0.65rem;
11
+ }
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+ body {
16
+ margin: 0;
17
+ background: var(--background);
18
+ color: var(--foreground);
19
+ }
20
+ main {
21
+ max-width: 72rem;
22
+ margin: auto;
23
+ padding: 0 2rem;
24
+ }
25
+ header {
26
+ height: 4.5rem;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ border-bottom: 1px solid var(--border);
31
+ }
32
+ nav {
33
+ display: flex;
34
+ gap: 1.5rem;
35
+ }
36
+ a {
37
+ color: inherit;
38
+ text-decoration: none;
39
+ }
40
+ section {
41
+ padding: 5rem 0;
42
+ border-bottom: 1px solid var(--border);
43
+ }
44
+ h1 {
45
+ max-width: 14ch;
46
+ font-size: clamp(2.8rem, 7vw, 6rem);
47
+ line-height: 0.95;
48
+ letter-spacing: -0.06em;
49
+ margin: 0.4rem 0 1.5rem;
50
+ }
51
+ .eyebrow {
52
+ font-size: 0.75rem;
53
+ letter-spacing: 0.18em;
54
+ }
55
+ .toolbar {
56
+ display: flex;
57
+ gap: 1rem;
58
+ }
59
+ input,
60
+ select,
61
+ button {
62
+ font: inherit;
63
+ border: 1px solid var(--border);
64
+ border-radius: var(--radius);
65
+ padding: 0.8rem 1rem;
66
+ background: transparent;
67
+ }
68
+ button {
69
+ background: var(--primary);
70
+ color: white;
71
+ border-color: var(--primary);
72
+ }
73
+ .empty {
74
+ padding: 4rem 0;
75
+ color: #666;
76
+ }
77
+ @media (max-width: 640px) {
78
+ main {
79
+ padding: 0 1.1rem;
80
+ }
81
+ nav {
82
+ gap: 0.75rem;
83
+ font-size: 0.85rem;
84
+ }
85
+ section {
86
+ padding: 3rem 0;
87
+ }
88
+ .toolbar {
89
+ flex-direction: column;
90
+ }
91
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "baseUrl": ".",
11
+ "paths": { "@/*": ["./src/*"] },
12
+ "types": ["vite/client"]
13
+ },
14
+ "include": ["src", "vite.config.ts"]
15
+ }
@@ -0,0 +1,13 @@
1
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import { nitro } from 'nitro/vite'
4
+ import tailwindcss from '@tailwindcss/vite'
5
+ import { defineConfig } from 'vite'
6
+ export default defineConfig({
7
+ plugins: [
8
+ tailwindcss(),
9
+ tanstackStart(),
10
+ nitro({ preset: '__NITRO_PRESET__' }),
11
+ react(),
12
+ ],
13
+ })
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from '@feedbax/core'
2
+ export default defineConfig({
3
+ branding: {
4
+ name: 'My Feedback',
5
+ description: 'Help us decide what to build next',
6
+ },
7
+ features: { comments: true, roadmap: true, changelog: true },
8
+ statuses: [
9
+ { id: 'new', label: 'New' },
10
+ { id: 'planned', label: 'Planned' },
11
+ { id: 'shipped', label: 'Shipped', terminal: true },
12
+ ],
13
+ })
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://feedbax.dev/schema/project.json",
3
+ "version": 1,
4
+ "generatorVersion": "__GENERATOR_VERSION__",
5
+ "connector": "notion",
6
+ "identity": "__IDENTITY__",
7
+ "interactionStore": "notion",
8
+ "deployment": "__DEPLOYMENT__",
9
+ "packageManager": "__PACKAGE_MANAGER__",
10
+ "shadcn": { "preset": "__PRESET__", "workspace": "apps/portal" },
11
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "feedbax-project",
3
+ "private": true,
4
+ "packageManager": "__PACKAGE_MANAGER_SPEC__",
5
+ "workspaces": [
6
+ "apps/*",
7
+ "packages/*"
8
+ ],
9
+ "scripts": {
10
+ "dev": "turbo run dev",
11
+ "build": "turbo run build",
12
+ "type-check": "turbo run type-check",
13
+ "doctor": "feedbax doctor"
14
+ },
15
+ "devDependencies": {
16
+ "feedbax": "^0.1.0",
17
+ "turbo": "2.10.4",
18
+ "typescript": "5.9.3"
19
+ }
20
+ }
@@ -0,0 +1,3 @@
1
+ packages:
2
+ - apps/*
3
+ - packages/*
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "tasks": {
4
+ "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".output/**"] },
5
+ "type-check": { "dependsOn": ["^build"], "outputs": [] },
6
+ "dev": { "cache": false, "persistent": true }
7
+ }
8
+ }