@tristan2828/ui-foundation 3.0.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.
Files changed (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/bin/ui-foundation.mjs +249 -0
  4. package/conventions/AGENTS.md +149 -0
  5. package/conventions/agents/deny-impl-read.mjs +36 -0
  6. package/conventions/agents/skills/new-entity/SKILL.md +22 -0
  7. package/conventions/agents/spec-tester.md +35 -0
  8. package/conventions/docs/add-an-entity.md +204 -0
  9. package/conventions/docs/cell-patterns.md +204 -0
  10. package/conventions/docs/column-options.md +122 -0
  11. package/conventions/docs/design-language.md +116 -0
  12. package/conventions/docs/entity-plan-template.md +67 -0
  13. package/dist/api/contracts.d.ts +20 -0
  14. package/dist/api/contracts.js +4 -0
  15. package/dist/api/gateway/auth.d.ts +9 -0
  16. package/dist/api/gateway/auth.js +37 -0
  17. package/dist/api/gateway/errors.d.ts +5 -0
  18. package/dist/api/gateway/errors.js +36 -0
  19. package/dist/api/query-client.d.ts +2 -0
  20. package/dist/api/query-client.js +19 -0
  21. package/dist/api/schema.d.ts +246 -0
  22. package/dist/api/schema.js +5 -0
  23. package/dist/api/transport/index.d.ts +5 -0
  24. package/dist/api/transport/index.js +14 -0
  25. package/dist/auth/auth-context.d.ts +19 -0
  26. package/dist/auth/auth-context.js +12 -0
  27. package/dist/auth/auth-provider.d.ts +4 -0
  28. package/dist/auth/auth-provider.js +101 -0
  29. package/dist/auth/use-auth.d.ts +2 -0
  30. package/dist/auth/use-auth.js +9 -0
  31. package/dist/components/app/app-shell.d.ts +28 -0
  32. package/dist/components/app/app-shell.js +64 -0
  33. package/dist/components/app/data-table.d.ts +56 -0
  34. package/dist/components/app/data-table.js +167 -0
  35. package/dist/components/app/entity-form.d.ts +14 -0
  36. package/dist/components/app/entity-form.js +8 -0
  37. package/dist/components/app/error-state.d.ts +6 -0
  38. package/dist/components/app/error-state.js +20 -0
  39. package/dist/components/app/foundation-providers.d.ts +9 -0
  40. package/dist/components/app/foundation-providers.js +15 -0
  41. package/dist/components/app/mock-mode-banner.d.ts +3 -0
  42. package/dist/components/app/mock-mode-banner.js +16 -0
  43. package/dist/components/app/multi-choice.d.ts +14 -0
  44. package/dist/components/app/multi-choice.js +13 -0
  45. package/dist/components/app/password-input.d.ts +3 -0
  46. package/dist/components/app/password-input.js +30 -0
  47. package/dist/components/app/route-error-boundary.d.ts +1 -0
  48. package/dist/components/app/route-error-boundary.js +13 -0
  49. package/dist/components/theme-provider.d.ts +2 -0
  50. package/dist/components/theme-provider.js +8 -0
  51. package/dist/components/ui/badge.d.ts +7 -0
  52. package/dist/components/ui/badge.js +50 -0
  53. package/dist/components/ui/button.d.ts +8 -0
  54. package/dist/components/ui/button.js +34 -0
  55. package/dist/components/ui/calendar.d.ts +10 -0
  56. package/dist/components/ui/calendar.js +76 -0
  57. package/dist/components/ui/card.d.ts +11 -0
  58. package/dist/components/ui/card.js +25 -0
  59. package/dist/components/ui/combobox.d.ts +24 -0
  60. package/dist/components/ui/combobox.js +62 -0
  61. package/dist/components/ui/dialog.d.ts +17 -0
  62. package/dist/components/ui/dialog.js +37 -0
  63. package/dist/components/ui/empty.d.ts +11 -0
  64. package/dist/components/ui/empty.js +33 -0
  65. package/dist/components/ui/field.d.ts +24 -0
  66. package/dist/components/ui/field.js +67 -0
  67. package/dist/components/ui/input-group.d.ts +18 -0
  68. package/dist/components/ui/input-group.js +57 -0
  69. package/dist/components/ui/input.d.ts +3 -0
  70. package/dist/components/ui/input.js +8 -0
  71. package/dist/components/ui/label.d.ts +3 -0
  72. package/dist/components/ui/label.js +7 -0
  73. package/dist/components/ui/popover.d.ts +9 -0
  74. package/dist/components/ui/popover.js +23 -0
  75. package/dist/components/ui/select.d.ts +15 -0
  76. package/dist/components/ui/select.js +35 -0
  77. package/dist/components/ui/separator.d.ts +3 -0
  78. package/dist/components/ui/separator.js +8 -0
  79. package/dist/components/ui/sheet.d.ts +14 -0
  80. package/dist/components/ui/sheet.js +38 -0
  81. package/dist/components/ui/sidebar.d.ts +63 -0
  82. package/dist/components/ui/sidebar.js +255 -0
  83. package/dist/components/ui/skeleton.d.ts +2 -0
  84. package/dist/components/ui/skeleton.js +6 -0
  85. package/dist/components/ui/sonner.d.ts +3 -0
  86. package/dist/components/ui/sonner.js +24 -0
  87. package/dist/components/ui/spinner.d.ts +2 -0
  88. package/dist/components/ui/spinner.js +7 -0
  89. package/dist/components/ui/table.d.ts +13 -0
  90. package/dist/components/ui/table.js +45 -0
  91. package/dist/components/ui/textarea.d.ts +3 -0
  92. package/dist/components/ui/textarea.js +7 -0
  93. package/dist/components/ui/tooltip.d.ts +6 -0
  94. package/dist/components/ui/tooltip.js +16 -0
  95. package/dist/gateway.d.ts +2 -0
  96. package/dist/gateway.js +5 -0
  97. package/dist/hooks/table-url-changes.d.ts +14 -0
  98. package/dist/hooks/table-url-changes.js +32 -0
  99. package/dist/hooks/use-debounced-value.d.ts +1 -0
  100. package/dist/hooks/use-debounced-value.js +12 -0
  101. package/dist/hooks/use-mobile.d.ts +1 -0
  102. package/dist/hooks/use-mobile.js +14 -0
  103. package/dist/hooks/use-table-url-state.d.ts +14 -0
  104. package/dist/hooks/use-table-url-state.js +43 -0
  105. package/dist/index.d.ts +22 -0
  106. package/dist/index.js +28 -0
  107. package/dist/lib/utils.d.ts +1 -0
  108. package/dist/lib/utils.js +1 -0
  109. package/dist/mocks/auth.d.ts +9 -0
  110. package/dist/mocks/auth.js +79 -0
  111. package/dist/mocks/e2e-hooks.d.ts +3 -0
  112. package/dist/mocks/e2e-hooks.js +28 -0
  113. package/dist/mocks/index.d.ts +3 -0
  114. package/dist/mocks/index.js +5 -0
  115. package/dist/mocks/override.d.ts +23 -0
  116. package/dist/mocks/override.js +1 -0
  117. package/dist/routes/login-schema.d.ts +7 -0
  118. package/dist/routes/login-schema.js +13 -0
  119. package/dist/routes/login.d.ts +8 -0
  120. package/dist/routes/login.js +58 -0
  121. package/dist/routes/register-schema.d.ts +9 -0
  122. package/dist/routes/register-schema.js +23 -0
  123. package/dist/routes/register.d.ts +1 -0
  124. package/dist/routes/register.js +58 -0
  125. package/dist/routes/return-path.d.ts +1 -0
  126. package/dist/routes/return-path.js +9 -0
  127. package/dist/testing/index.d.ts +40 -0
  128. package/dist/testing/index.js +103 -0
  129. package/eslint/index.d.ts +11 -0
  130. package/eslint/index.js +179 -0
  131. package/openapi/foundation.yaml +156 -0
  132. package/package.json +158 -0
  133. package/styles/index.css +88 -0
  134. package/styles/theme.css +209 -0
@@ -0,0 +1,103 @@
1
+ // `@tristan2828/ui-foundation/testing` — Playwright helpers and suites for
2
+ // what every app built on the foundation must keep true. An app's spec file
3
+ // calls a suite with its own routes, instead of carrying a copy of the spec
4
+ // to edit (each of these was forked by the first real app for exactly that).
5
+ //
6
+ // Runs in Node, inside Playwright: @playwright/test and @axe-core/playwright
7
+ // are optional peer dependencies, needed only by apps that import this.
8
+ import AxeBuilder from '@axe-core/playwright';
9
+ import { expect, test } from '@playwright/test';
10
+ /**
11
+ * Forces a response for the page's *first* load: set before navigation, so
12
+ * it wins the race against the app's own first fetch. For states forced
13
+ * after load, use `waitForMswReady` and then `window.__msw.worker.use(...)`.
14
+ */
15
+ export async function forceMswOverride(page, override) {
16
+ await page.addInitScript((spec) => {
17
+ window.__E2E_MSW_OVERRIDE__ = spec;
18
+ }, override);
19
+ }
20
+ /** `window.__msw` is set only once the app's mocking has started — page.goto's load event doesn't wait for it. */
21
+ export async function waitForMswReady(page) {
22
+ await page.waitForFunction(() => window.__msw !== undefined);
23
+ }
24
+ /** Starts the page logged out: GET /auth/me answers 401 on first load. */
25
+ export function forceLoggedOut(page) {
26
+ return forceMswOverride(page, {
27
+ method: 'get',
28
+ path: '*/api/auth/me',
29
+ status: 401,
30
+ body: { detail: 'Not authenticated' },
31
+ });
32
+ }
33
+ export async function expectNoAxeViolations(page) {
34
+ const results = await new AxeBuilder({ page }).analyze();
35
+ expect(results.violations).toEqual([]);
36
+ }
37
+ // The app's own pages, read from the Primary nav. Below the sidebar's
38
+ // mobile breakpoint the nav renders into an off-canvas Sheet *portaled to
39
+ // document.body*, outside the <nav aria-label="Primary"> landmark, so the
40
+ // links are read from the sheet there. Keyed off the `isMobile` fixture,
41
+ // deliberately not a link count: count() doesn't retry, so on a slow first
42
+ // paint it read 0 on desktop too, clicked the trigger (which collapses the
43
+ // desktop sidebar) and then waited for a sheet that never came.
44
+ async function navRoutes(page, isMobile) {
45
+ await page.goto('/');
46
+ const nav = isMobile
47
+ ? (await page.getByRole('button', { name: 'Toggle Sidebar' }).first().click(), page.getByRole('dialog'))
48
+ : page.getByRole('navigation', { name: 'Primary' });
49
+ await expect(nav.getByRole('link').first()).toBeVisible();
50
+ const hrefs = await nav
51
+ .getByRole('link')
52
+ .evaluateAll((links) => links.map((link) => link.href));
53
+ // Only this app's pages: a sidebarExtra group may link to other sites.
54
+ const origin = new URL(page.url()).origin;
55
+ return [...new Set(hrefs.filter((href) => new URL(href).origin === origin).map((href) => new URL(href).pathname))];
56
+ }
57
+ /**
58
+ * Accessibility of the real screens in light and dark mode: every page in
59
+ * the Primary nav (discovered, so a new entity's table is covered with no
60
+ * edit), the given form routes, and the logged-out screens. axe's
61
+ * color-contrast rule against the rendered dark DOM is also what keeps the
62
+ * dark-mode tokens readable. The theme follows the OS setting, so
63
+ * emulateMedia switches it.
64
+ */
65
+ export function defineA11ySuite({ formRoutes, loggedOutRoutes }) {
66
+ for (const colorScheme of ['light', 'dark']) {
67
+ test.describe(`accessibility (${colorScheme})`, () => {
68
+ test.beforeEach(async ({ page }) => {
69
+ await page.emulateMedia({ colorScheme });
70
+ });
71
+ test('every nav page has zero axe violations', async ({ page, isMobile }) => {
72
+ for (const route of await navRoutes(page, isMobile)) {
73
+ await page.goto(route);
74
+ await expect(page.locator('html')).toHaveClass(colorScheme);
75
+ // Wait for data, not the loading skeleton, so the real UI is checked.
76
+ await expect(page.locator('[data-state="loading"]')).toHaveCount(0);
77
+ await test.step(route, () => expectNoAxeViolations(page));
78
+ }
79
+ });
80
+ for (const route of [...formRoutes, ...loggedOutRoutes]) {
81
+ test(`${route} has zero axe violations`, async ({ page }) => {
82
+ await page.goto(route);
83
+ await expect(page.locator('form')).toBeVisible();
84
+ await expectNoAxeViolations(page);
85
+ });
86
+ }
87
+ });
88
+ }
89
+ }
90
+ /**
91
+ * Mock mode is announced on every route, logged-out ones included — /login
92
+ * is where a mock session passing for a real one first cost someone time.
93
+ * The default Playwright run is MSW-backed, so the banner must be there.
94
+ */
95
+ export function defineMockModeBannerSuite({ routes }) {
96
+ test('mock mode is announced on every route, including logged-out ones', async ({ page }) => {
97
+ const banner = page.getByRole('status').filter({ hasText: 'Mock data' });
98
+ for (const route of routes) {
99
+ await page.goto(route);
100
+ await test.step(route, () => expect(banner).toBeVisible());
101
+ }
102
+ });
103
+ }
@@ -0,0 +1,11 @@
1
+ import type { Linter } from 'eslint'
2
+
3
+ export declare const PACKAGE_NAME: string
4
+ export declare const PRIMITIVES: string[]
5
+ export declare const TOKEN_SYNTAX_RULES: { selector: string; message: string }[]
6
+ export declare const NO_BARE_FETCH: { selector: string; message: string }
7
+ export declare function baseConfig(): Linter.Config
8
+ export declare function kebabCaseFilenames(files: string[], ignores?: string[]): Linter.Config
9
+
10
+ /** The lint config for an app built on the foundation. */
11
+ export default function uiFoundation(options?: { ignores?: string[] }): Linter.Config[]
@@ -0,0 +1,179 @@
1
+ // `@tristan2828/ui-foundation/eslint` — the lint half of the Hard Rules
2
+ // (AGENTS.md), shared so every app enforces the same ones and gets new ones
3
+ // by upgrading the package. An app's whole eslint.config.js:
4
+ //
5
+ // import uiFoundation from '@tristan2828/ui-foundation/eslint'
6
+ // export default uiFoundation()
7
+ //
8
+ // Plain JavaScript on purpose: ESLint loads this before any TypeScript
9
+ // tooling runs. The plugins are the app's own devDependencies (optional
10
+ // peer dependencies of the package).
11
+ import { readdirSync } from 'node:fs'
12
+ import js from '@eslint/js'
13
+ import tanstackQuery from '@tanstack/eslint-plugin-query'
14
+ import checkFile from 'eslint-plugin-check-file'
15
+ import reactHooks from 'eslint-plugin-react-hooks'
16
+ import reactRefresh from 'eslint-plugin-react-refresh'
17
+ import globals from 'globals'
18
+ import tseslint from 'typescript-eslint'
19
+ import { defineConfig, globalIgnores } from 'eslint/config'
20
+
21
+ export const PACKAGE_NAME = '@tristan2828/ui-foundation'
22
+
23
+ // The primitives the package ships, read from what was built (dist/) or,
24
+ // inside this repo before a build, from source. An app importing one of
25
+ // these from its own src/components/ui/ has a second, unpatched copy —
26
+ // usually because `shadcn add` pulled it in as another primitive's
27
+ // dependency.
28
+ function shippedPrimitives() {
29
+ for (const dir of ['../dist/components/ui', '../src/components/ui']) {
30
+ try {
31
+ return readdirSync(new URL(dir, import.meta.url))
32
+ .filter((file) => /^[a-z0-9-]+\.(js|tsx)$/.test(file) && !file.includes('.stories.'))
33
+ .map((file) => file.replace(/\.(js|tsx)$/, ''))
34
+ } catch {
35
+ // Not there — try the next location.
36
+ }
37
+ }
38
+ return []
39
+ }
40
+ export const PRIMITIVES = shippedPrimitives()
41
+
42
+ // Raw Tailwind palette classes bypass the semantic token layer — this is
43
+ // what makes "dark mode looks right" a lint error instead of a visual
44
+ // surprise.
45
+ const TAILWIND_PALETTE_CLASS =
46
+ '/\\b(bg|text|border|ring|fill|stroke)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\\d{2,3}\\b/'
47
+ const HEX_COLOR = '/#[0-9a-f]{3,8}/i'
48
+
49
+ export const TOKEN_SYNTAX_RULES = [
50
+ {
51
+ selector: `JSXAttribute[name.name='className'] Literal[value=${TAILWIND_PALETTE_CLASS}]`,
52
+ message:
53
+ 'Use semantic tokens (bg-primary, text-muted-foreground, ...), not raw Tailwind palette classes. See the design-language doc.',
54
+ },
55
+ {
56
+ selector: `JSXAttribute[name.name='style'] Property Literal[value=${HEX_COLOR}]`,
57
+ message: 'No raw hex colors in style props. Use a semantic token.',
58
+ },
59
+ ]
60
+
61
+ export const NO_BARE_FETCH = {
62
+ selector: "CallExpression[callee.name='fetch']",
63
+ message:
64
+ 'Do not call fetch directly. All server state goes through TanStack Query, which calls src/api/gateway/, which builds on the foundation gateway entry point.',
65
+ }
66
+
67
+ // The rules every TypeScript file gets, before any boundary rule.
68
+ export function baseConfig() {
69
+ return {
70
+ files: ['**/*.{ts,tsx}'],
71
+ extends: [
72
+ js.configs.recommended,
73
+ tseslint.configs.recommended,
74
+ reactHooks.configs.flat.recommended,
75
+ reactRefresh.configs.vite,
76
+ ],
77
+ languageOptions: {
78
+ ecmaVersion: 2020,
79
+ globals: globals.browser,
80
+ },
81
+ }
82
+ }
83
+
84
+ export function kebabCaseFilenames(files, ignores = []) {
85
+ return {
86
+ files,
87
+ ignores,
88
+ plugins: { 'check-file': checkFile },
89
+ rules: {
90
+ 'check-file/filename-naming-convention': [
91
+ 'error',
92
+ { '**/*.{ts,tsx}': 'KEBAB_CASE' },
93
+ { ignoreMiddleExtensions: true },
94
+ ],
95
+ },
96
+ }
97
+ }
98
+
99
+ const GATEWAY_ONLY = {
100
+ group: [`${PACKAGE_NAME}/gateway`],
101
+ message:
102
+ 'Only src/api/gateway/ may import the foundation gateway entry point. Components and hooks call a gateway module through TanStack Query.',
103
+ }
104
+
105
+ const PACKAGE_INTERNALS = {
106
+ group: [`${PACKAGE_NAME}/dist/*`, `${PACKAGE_NAME}/src/*`],
107
+ message: `Import from ${PACKAGE_NAME}'s public entry points only. Need something it doesn't export? Raise it for the foundation instead of reaching in.`,
108
+ }
109
+
110
+ function appImportPatterns({ allowGateway }) {
111
+ const patterns = [PACKAGE_INTERNALS]
112
+ if (!allowGateway) patterns.push(GATEWAY_ONLY)
113
+ if (PRIMITIVES.length > 0) {
114
+ patterns.push({
115
+ regex: `^@/components/ui/(${PRIMITIVES.join('|')})$`,
116
+ message: `The foundation ships this primitive: import it from ${PACKAGE_NAME}/ui/<name> and delete the app's copy (shadcn add re-creates one when another primitive depends on it).`,
117
+ })
118
+ }
119
+ return patterns
120
+ }
121
+
122
+ /**
123
+ * The lint config for an app built on the foundation.
124
+ * @param {{ ignores?: string[] }} [options] extra paths to ignore, on top of build output and generated files
125
+ */
126
+ export default function uiFoundation(options = {}) {
127
+ return defineConfig([
128
+ // .claude/worktrees: a session running in a git worktree nested under
129
+ // the project root is normal in an agent-built repo; without this, one
130
+ // session's in-progress files fail another session's verify.
131
+ globalIgnores([
132
+ 'dist',
133
+ 'storybook-static',
134
+ 'test-results',
135
+ 'playwright-report',
136
+ 'public/mockServiceWorker.js',
137
+ '.claude/worktrees',
138
+ 'backend',
139
+ ...(options.ignores ?? []),
140
+ ]),
141
+ {
142
+ ...baseConfig(),
143
+ rules: {
144
+ 'no-restricted-syntax': ['error', ...TOKEN_SYNTAX_RULES, NO_BARE_FETCH],
145
+ 'no-restricted-imports': ['error', { patterns: appImportPatterns({ allowGateway: false }) }],
146
+ },
147
+ },
148
+ {
149
+ // App-added shadcn primitives: CLI-installed, and shadcn's convention
150
+ // exports a component alongside its `*Variants` helper from one file
151
+ // — a false positive for fast-refresh.
152
+ files: ['src/components/ui/**/*.{ts,tsx}'],
153
+ rules: { 'react-refresh/only-export-components': 'off' },
154
+ },
155
+ {
156
+ // The gateway is the one layer allowed onto the network.
157
+ files: ['src/api/gateway/**/*.{ts,tsx}'],
158
+ rules: {
159
+ 'no-restricted-imports': ['error', { patterns: appImportPatterns({ allowGateway: true }) }],
160
+ },
161
+ },
162
+ // Kebab-case for everything hand-authored. App.tsx and main.tsx are
163
+ // Vite's own scaffold names; src/components/ui/ is shadcn's.
164
+ kebabCaseFilenames(['src/**/*.{ts,tsx}', 'e2e/**/*.ts', 'tests/**/*.ts'], [
165
+ 'src/App.tsx',
166
+ 'src/main.tsx',
167
+ 'src/components/ui/**',
168
+ ]),
169
+ {
170
+ // Tests exercise the HTTP boundary directly (gateway tests stub
171
+ // fetch, conformance tests call it against MSW, e2e specs call it in
172
+ // the page) — the no-bare-fetch rule keeps app code on the gateway,
173
+ // not tests off the primitive they're testing.
174
+ files: ['tests/**/*.{ts,tsx}', 'e2e/**/*.{ts,tsx}'],
175
+ rules: { 'no-restricted-syntax': ['error', ...TOKEN_SYNTAX_RULES] },
176
+ },
177
+ ...tanstackQuery.configs['flat/recommended'],
178
+ ])
179
+ }
@@ -0,0 +1,156 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: UI Foundation — the contract every app shares
4
+ version: "3.0.0"
5
+ description: >
6
+ The part of an app's API contract that the foundation package's own code
7
+ calls: session-cookie auth under /auth/*, and the two error envelopes the
8
+ gateway's toAppError() translates (FastAPI's default 422 and
9
+ HTTPException shapes). An app's openapi.yaml must contain every path and
10
+ component here, unchanged — `ui-foundation check-contract` fails verify
11
+ when it drifts. Descriptions, summaries and examples may differ; shapes
12
+ may not.
13
+
14
+ A path marked `x-optional: true` may be left out of an app's spec (an
15
+ app without self-service sign-up omits /auth/register), but if present
16
+ it must match.
17
+
18
+ servers:
19
+ - url: /api
20
+
21
+ paths:
22
+ /auth/login:
23
+ post:
24
+ operationId: login
25
+ summary: Log in with email and password, receiving a session cookie
26
+ tags: [auth]
27
+ requestBody:
28
+ required: true
29
+ content:
30
+ application/json:
31
+ schema: { $ref: "#/components/schemas/LoginRequest" }
32
+ responses:
33
+ "200":
34
+ description: Authenticated. Sets the session_id cookie.
35
+ content:
36
+ application/json:
37
+ schema: { $ref: "#/components/schemas/User" }
38
+ "401": { $ref: "#/components/responses/UnauthorizedError" }
39
+ "422": { $ref: "#/components/responses/ValidationError" }
40
+ "500": { $ref: "#/components/responses/ServerError" }
41
+
42
+ /auth/register:
43
+ x-optional: true
44
+ post:
45
+ operationId: register
46
+ summary: Create an account and immediately log in, receiving a session cookie
47
+ tags: [auth]
48
+ requestBody:
49
+ required: true
50
+ content:
51
+ application/json:
52
+ schema: { $ref: "#/components/schemas/RegisterRequest" }
53
+ responses:
54
+ "200":
55
+ description: Registered and authenticated. Sets the session_id cookie.
56
+ content:
57
+ application/json:
58
+ schema: { $ref: "#/components/schemas/User" }
59
+ "422": { $ref: "#/components/responses/ValidationError" }
60
+ "500": { $ref: "#/components/responses/ServerError" }
61
+
62
+ /auth/logout:
63
+ post:
64
+ operationId: logout
65
+ summary: Clear the current session
66
+ tags: [auth]
67
+ responses:
68
+ "204":
69
+ description: Logged out (always, whether or not a session existed).
70
+ "500": { $ref: "#/components/responses/ServerError" }
71
+
72
+ /auth/me:
73
+ get:
74
+ operationId: getCurrentUser
75
+ summary: The currently authenticated user
76
+ tags: [auth]
77
+ responses:
78
+ "200":
79
+ description: OK.
80
+ content:
81
+ application/json:
82
+ schema: { $ref: "#/components/schemas/User" }
83
+ "401": { $ref: "#/components/responses/UnauthorizedError" }
84
+ "500": { $ref: "#/components/responses/ServerError" }
85
+
86
+ components:
87
+ schemas:
88
+ User:
89
+ type: object
90
+ required: [id, email, name]
91
+ properties:
92
+ id: { type: integer, readOnly: true }
93
+ email: { type: string, format: email }
94
+ name: { type: string, minLength: 1, maxLength: 200 }
95
+
96
+ LoginRequest:
97
+ type: object
98
+ required: [email, password]
99
+ properties:
100
+ email: { type: string, format: email }
101
+ password: { type: string, minLength: 8 }
102
+
103
+ RegisterRequest:
104
+ type: object
105
+ required: [email, name, password]
106
+ properties:
107
+ email: { $ref: "#/components/schemas/User/properties/email" }
108
+ name: { $ref: "#/components/schemas/User/properties/name" }
109
+ password: { $ref: "#/components/schemas/LoginRequest/properties/password" }
110
+
111
+ ValidationErrorBody:
112
+ type: object
113
+ description: FastAPI's default 422 shape (RequestValidationError).
114
+ required: [detail]
115
+ properties:
116
+ detail:
117
+ type: array
118
+ items:
119
+ type: object
120
+ required: [loc, msg, type]
121
+ properties:
122
+ loc:
123
+ type: array
124
+ items:
125
+ type: ["string", "integer"]
126
+ msg: { type: string }
127
+ type: { type: string }
128
+
129
+ HTTPErrorBody:
130
+ type: object
131
+ description: FastAPI's default HTTPException shape.
132
+ required: [detail]
133
+ properties:
134
+ detail: { type: string }
135
+
136
+ responses:
137
+ ValidationError:
138
+ description: Validation failed on one or more fields.
139
+ content:
140
+ application/json:
141
+ schema: { $ref: "#/components/schemas/ValidationErrorBody" }
142
+ NotFoundError:
143
+ description: Resource not found.
144
+ content:
145
+ application/json:
146
+ schema: { $ref: "#/components/schemas/HTTPErrorBody" }
147
+ UnauthorizedError:
148
+ description: Missing or invalid credentials.
149
+ content:
150
+ application/json:
151
+ schema: { $ref: "#/components/schemas/HTTPErrorBody" }
152
+ ServerError:
153
+ description: Unhandled server error.
154
+ content:
155
+ application/json:
156
+ schema: { $ref: "#/components/schemas/HTTPErrorBody" }
package/package.json ADDED
@@ -0,0 +1,158 @@
1
+ {
2
+ "name": "@tristan2828/ui-foundation",
3
+ "version": "3.0.0",
4
+ "description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Tristan2828/ui-foundation.git",
9
+ "directory": "packages/ui-foundation"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": [
13
+ "**/*.css"
14
+ ],
15
+ "files": [
16
+ "dist",
17
+ "styles",
18
+ "eslint",
19
+ "bin",
20
+ "conventions",
21
+ "openapi"
22
+ ],
23
+ "bin": {
24
+ "ui-foundation": "bin/ui-foundation.mjs"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./ui/*": {
32
+ "types": "./dist/components/ui/*.d.ts",
33
+ "default": "./dist/components/ui/*.js"
34
+ },
35
+ "./gateway": {
36
+ "types": "./dist/gateway.d.ts",
37
+ "default": "./dist/gateway.js"
38
+ },
39
+ "./mocks": {
40
+ "types": "./dist/mocks/index.d.ts",
41
+ "default": "./dist/mocks/index.js"
42
+ },
43
+ "./testing": {
44
+ "types": "./dist/testing/index.d.ts",
45
+ "default": "./dist/testing/index.js"
46
+ },
47
+ "./eslint": {
48
+ "types": "./eslint/index.d.ts",
49
+ "default": "./eslint/index.js"
50
+ },
51
+ "./styles.css": {
52
+ "style": "./styles/index.css",
53
+ "default": "./styles/index.css"
54
+ },
55
+ "./openapi.yaml": "./openapi/foundation.yaml",
56
+ "./package.json": "./package.json"
57
+ },
58
+ "scripts": {
59
+ "build": "node scripts/build.mjs",
60
+ "prepack": "npm run build",
61
+ "gen:api": "openapi-typescript ./openapi/foundation.yaml -o ./src/api/schema.ts",
62
+ "lint": "eslint .",
63
+ "storybook": "storybook dev -p 6006",
64
+ "build-storybook": "storybook build",
65
+ "preview-storybook": "vite preview --outDir storybook-static --port 6006 --strictPort",
66
+ "verify:fast": "npm run gen:api && git diff --exit-code -- src/api/schema.ts && tsc -b && eslint . --max-warnings 0 && node bin/ui-foundation.mjs check-deps && npm run build && vitest run",
67
+ "verify": "npm run verify:fast && playwright test -c playwright.storybook.config.ts"
68
+ },
69
+ "peerDependencies": {
70
+ "@axe-core/playwright": "^4.13.0",
71
+ "@base-ui/react": "^1.8.0",
72
+ "@eslint/js": "^10.0.1",
73
+ "@playwright/test": "^1.63.0",
74
+ "@tanstack/eslint-plugin-query": "^5.103.1",
75
+ "@tanstack/react-query": "^5.103.1",
76
+ "@tanstack/react-table": "^9.2.4",
77
+ "eslint": "^10.10.0",
78
+ "eslint-plugin-check-file": "^3.3.2",
79
+ "eslint-plugin-react-hooks": "^7.1.1",
80
+ "eslint-plugin-react-refresh": "^0.5.7",
81
+ "globals": "^17.12.0",
82
+ "msw": "^2.15.0",
83
+ "next-themes": "^0.4.6",
84
+ "react": "^19.3.0",
85
+ "react-dom": "^19.3.0",
86
+ "react-hook-form": "^7.88.0",
87
+ "react-router": "^7.18.4",
88
+ "sonner": "^2.0.8",
89
+ "tailwindcss": "^4.3.3",
90
+ "typescript-eslint": "^8.70.0",
91
+ "zod": "^4.6.5"
92
+ },
93
+ "peerDependenciesMeta": {
94
+ "@axe-core/playwright": { "optional": true },
95
+ "@eslint/js": { "optional": true },
96
+ "@playwright/test": { "optional": true },
97
+ "@tanstack/eslint-plugin-query": { "optional": true },
98
+ "eslint": { "optional": true },
99
+ "eslint-plugin-check-file": { "optional": true },
100
+ "eslint-plugin-react-hooks": { "optional": true },
101
+ "eslint-plugin-react-refresh": { "optional": true },
102
+ "globals": { "optional": true },
103
+ "msw": { "optional": true },
104
+ "typescript-eslint": { "optional": true }
105
+ },
106
+ "dependencies": {
107
+ "@fontsource-variable/geist": "^5.3.0",
108
+ "@hookform/resolvers": "^5.9.1",
109
+ "class-variance-authority": "^0.7.1",
110
+ "cn": "^0.3.0",
111
+ "date-fns": "^4.4.0",
112
+ "js-yaml": "^5.4.2",
113
+ "lucide-react": "^1.47.0",
114
+ "react-day-picker": "^10.0.1",
115
+ "shadcn": "^4.21.0",
116
+ "tw-animate-css": "^1.4.0"
117
+ },
118
+ "devDependencies": {
119
+ "@axe-core/playwright": "^4.13.0",
120
+ "@base-ui/react": "^1.8.0",
121
+ "@eslint/js": "^10.0.1",
122
+ "@playwright/test": "^1.63.0",
123
+ "@storybook/addon-docs": "^10.6.0",
124
+ "@storybook/react-vite": "^10.6.0",
125
+ "@tailwindcss/vite": "^4.3.3",
126
+ "@tanstack/eslint-plugin-query": "^5.103.1",
127
+ "@tanstack/react-query": "^5.103.1",
128
+ "@tanstack/react-table": "^9.2.4",
129
+ "@types/node": "^24.13.5",
130
+ "@types/react": "^19.3.0",
131
+ "@types/react-dom": "^19.3.0",
132
+ "@vitejs/plugin-react": "^6.1.1",
133
+ "eslint": "^10.10.0",
134
+ "eslint-plugin-check-file": "^3.3.2",
135
+ "eslint-plugin-react-hooks": "^7.1.1",
136
+ "eslint-plugin-react-refresh": "^0.5.7",
137
+ "globals": "^17.12.0",
138
+ "msw": "^2.15.0",
139
+ "next-themes": "^0.4.6",
140
+ "openapi-typescript": "^7.13.0",
141
+ "react": "^19.3.0",
142
+ "react-dom": "^19.3.0",
143
+ "react-hook-form": "^7.88.0",
144
+ "react-router": "^7.18.4",
145
+ "sonner": "^2.0.8",
146
+ "storybook": "^10.6.0",
147
+ "tailwindcss": "^4.3.3",
148
+ "typescript": "~6.0.3",
149
+ "typescript-eslint": "^8.70.0",
150
+ "vite": "^8.3.0",
151
+ "vitest": "^5.0.1",
152
+ "zod": "^4.6.5"
153
+ },
154
+ "publishConfig": {
155
+ "access": "public",
156
+ "provenance": true
157
+ }
158
+ }