teamshare-bridge 0.2.5 → 0.4.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 (140) hide show
  1. package/dist/bridge/daemon.js +36 -0
  2. package/dist/bridge/daemon.js.map +1 -1
  3. package/dist/bridge/local-server.d.ts +1 -1
  4. package/dist/bridge/spawn.d.ts +2 -0
  5. package/dist/bridge/spawn.js +17 -0
  6. package/dist/bridge/spawn.js.map +1 -1
  7. package/dist/cli/commands/audit.d.ts +1 -0
  8. package/dist/cli/commands/audit.js +75 -0
  9. package/dist/cli/commands/audit.js.map +1 -0
  10. package/dist/cli/commands/build.js +24 -8
  11. package/dist/cli/commands/build.js.map +1 -1
  12. package/dist/cli/commands/chat.js +1 -1
  13. package/dist/cli/commands/chat.js.map +1 -1
  14. package/dist/cli/commands/doc.js +2 -2
  15. package/dist/cli/commands/doc.js.map +1 -1
  16. package/dist/cli/commands/harness.js +1 -1
  17. package/dist/cli/commands/harness.js.map +1 -1
  18. package/dist/cli/commands/run.js +19 -4
  19. package/dist/cli/commands/run.js.map +1 -1
  20. package/dist/cli/commands/scaffold.d.ts +1 -0
  21. package/dist/cli/commands/scaffold.js +111 -0
  22. package/dist/cli/commands/scaffold.js.map +1 -0
  23. package/dist/cli/commands/worker.d.ts +2 -0
  24. package/dist/cli/commands/worker.js +110 -0
  25. package/dist/cli/commands/worker.js.map +1 -0
  26. package/dist/cli/index.js +16 -0
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/lib/api.d.ts +35 -4
  29. package/dist/lib/api.js +20 -3
  30. package/dist/lib/api.js.map +1 -1
  31. package/dist/lib/audit/docs.d.ts +5 -0
  32. package/dist/lib/audit/docs.js +52 -0
  33. package/dist/lib/audit/docs.js.map +1 -0
  34. package/dist/lib/audit/errors.d.ts +15 -0
  35. package/dist/lib/audit/errors.js +154 -0
  36. package/dist/lib/audit/errors.js.map +1 -0
  37. package/dist/lib/audit/gates.d.ts +2 -0
  38. package/dist/lib/audit/gates.js +57 -0
  39. package/dist/lib/audit/gates.js.map +1 -0
  40. package/dist/lib/audit/modularity.d.ts +13 -0
  41. package/dist/lib/audit/modularity.js +200 -0
  42. package/dist/lib/audit/modularity.js.map +1 -0
  43. package/dist/lib/audit/quality.d.ts +9 -0
  44. package/dist/lib/audit/quality.js +128 -0
  45. package/dist/lib/audit/quality.js.map +1 -0
  46. package/dist/lib/audit/rbac.d.ts +14 -0
  47. package/dist/lib/audit/rbac.js +139 -0
  48. package/dist/lib/audit/rbac.js.map +1 -0
  49. package/dist/lib/audit/reporter.d.ts +5 -0
  50. package/dist/lib/audit/reporter.js +107 -0
  51. package/dist/lib/audit/reporter.js.map +1 -0
  52. package/dist/lib/audit/runner.d.ts +2 -0
  53. package/dist/lib/audit/runner.js +150 -0
  54. package/dist/lib/audit/runner.js.map +1 -0
  55. package/dist/lib/audit/security.d.ts +9 -0
  56. package/dist/lib/audit/security.js +151 -0
  57. package/dist/lib/audit/security.js.map +1 -0
  58. package/dist/lib/audit/types.d.ts +67 -0
  59. package/dist/lib/audit/types.js +26 -0
  60. package/dist/lib/audit/types.js.map +1 -0
  61. package/dist/lib/audit/ux.d.ts +9 -0
  62. package/dist/lib/audit/ux.js +112 -0
  63. package/dist/lib/audit/ux.js.map +1 -0
  64. package/dist/lib/brief.d.ts +4 -0
  65. package/dist/lib/brief.js +78 -40
  66. package/dist/lib/brief.js.map +1 -1
  67. package/dist/lib/chat-reply.js +2 -2
  68. package/dist/lib/chat-reply.js.map +1 -1
  69. package/dist/lib/config.d.ts +1 -1
  70. package/dist/lib/config.js +1 -1
  71. package/dist/lib/doc-reply.js +1 -1
  72. package/dist/lib/doc-reply.js.map +1 -1
  73. package/dist/lib/harness-adapter.d.ts +3 -1
  74. package/dist/lib/harness-adapter.js +19 -16
  75. package/dist/lib/harness-adapter.js.map +1 -1
  76. package/dist/lib/harness.d.ts +3 -1
  77. package/dist/lib/harness.js +8 -5
  78. package/dist/lib/harness.js.map +1 -1
  79. package/dist/lib/llm.js +87 -9
  80. package/dist/lib/llm.js.map +1 -1
  81. package/dist/lib/lock.d.ts +1 -1
  82. package/dist/lib/scaffold/renderer.d.ts +7 -0
  83. package/dist/lib/scaffold/renderer.js +37 -0
  84. package/dist/lib/scaffold/renderer.js.map +1 -0
  85. package/dist/lib/scaffold/structure.d.ts +3 -0
  86. package/dist/lib/scaffold/structure.js +73 -0
  87. package/dist/lib/scaffold/structure.js.map +1 -0
  88. package/dist/lib/scaffold/types.d.ts +34 -0
  89. package/dist/lib/scaffold/types.js +6 -0
  90. package/dist/lib/scaffold/types.js.map +1 -0
  91. package/dist/lib/scaffold/verify.d.ts +9 -0
  92. package/dist/lib/scaffold/verify.js +48 -0
  93. package/dist/lib/scaffold/verify.js.map +1 -0
  94. package/dist/lib/skills.d.ts +2 -2
  95. package/dist/lib/skills.js +2 -1
  96. package/dist/lib/skills.js.map +1 -1
  97. package/package.json +3 -2
  98. package/skills/teamshare-orchestrator-ask/SKILL.md +14 -0
  99. package/skills/teamshare-scaffold/SKILL.md +133 -0
  100. package/templates/backend/.env.example.tpl +13 -0
  101. package/templates/backend/nest-cli.json.tpl +8 -0
  102. package/templates/backend/package.json.tpl +41 -0
  103. package/templates/backend/prisma/schema.prisma.tpl +39 -0
  104. package/templates/backend/src/app.module.ts.tpl +24 -0
  105. package/templates/backend/src/common/filters/all-exceptions.filter.ts.tpl +59 -0
  106. package/templates/backend/src/common/guards/jwt-auth.guard.ts.tpl +27 -0
  107. package/templates/backend/src/common/guards/rbac.guard.ts.tpl +48 -0
  108. package/templates/backend/src/common/interceptors/transform.interceptor.ts.tpl +44 -0
  109. package/templates/backend/src/common/pipes/zod-validation.pipe.ts.tpl +29 -0
  110. package/templates/backend/src/common/swagger/envelope.schemas.ts.tpl +53 -0
  111. package/templates/backend/src/common/swagger/error.schemas.ts.tpl +62 -0
  112. package/templates/backend/src/main.ts.tpl +27 -0
  113. package/templates/backend/src/modules/example/example.controller.ts.tpl +102 -0
  114. package/templates/backend/src/modules/example/example.dto.ts.tpl +14 -0
  115. package/templates/backend/src/modules/example/example.module.ts.tpl +10 -0
  116. package/templates/backend/src/modules/example/example.service.ts.tpl +45 -0
  117. package/templates/backend/tsconfig.json.tpl +26 -0
  118. package/templates/common/.gitignore.tpl +42 -0
  119. package/templates/common/AGENTS.md.tpl +58 -0
  120. package/templates/common/docs/adr/README.md +25 -0
  121. package/templates/common/docs/api-contract.md.tpl +72 -0
  122. package/templates/web/next.config.ts.tpl +7 -0
  123. package/templates/web/package.json.tpl +37 -0
  124. package/templates/web/src/app/globals.css.tpl +48 -0
  125. package/templates/web/src/app/layout.tsx.tpl +25 -0
  126. package/templates/web/src/components/shared/app-button.tsx.tpl +59 -0
  127. package/templates/web/src/components/shared/app-empty-state.tsx.tpl +39 -0
  128. package/templates/web/src/components/shared/app-list-toolbar.tsx.tpl +63 -0
  129. package/templates/web/src/components/shared/app-skeleton.tsx.tpl +14 -0
  130. package/templates/web/src/components/shared/app-table.tsx.tpl +128 -0
  131. package/templates/web/src/components/shared/index.ts.tpl +5 -0
  132. package/templates/web/src/hooks/use-list-state.ts.tpl +72 -0
  133. package/templates/web/src/hooks/use-url-tab.ts.tpl +25 -0
  134. package/templates/web/src/lib/api/client.ts.tpl +123 -0
  135. package/templates/web/src/lib/providers.tsx.tpl +35 -0
  136. package/templates/web/src/lib/query-keys.ts.tpl +21 -0
  137. package/templates/web/src/lib/utils.ts.tpl +7 -0
  138. package/templates/web/src/lib/validation/schemas.ts.tpl +15 -0
  139. package/templates/web/src/modules/example/views/example-list-view.tsx.tpl +98 -0
  140. package/templates/web/tsconfig.json.tpl +23 -0
@@ -0,0 +1,72 @@
1
+ # API Contract - {{projectName}}
2
+
3
+ ## Response envelope
4
+
5
+ Every successful response returns:
6
+
7
+ ```json
8
+ {
9
+ "success": true,
10
+ "data": { ... }
11
+ }
12
+ ```
13
+
14
+ Paginated responses include a `pagination` field:
15
+
16
+ ```json
17
+ {
18
+ "success": true,
19
+ "data": [ ... ],
20
+ "pagination": {
21
+ "page": 1,
22
+ "limit": 20,
23
+ "total": 100,
24
+ "totalPages": 5
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Error envelope
30
+
31
+ ```json
32
+ {
33
+ "success": false,
34
+ "error": {
35
+ "code": "NOT_FOUND",
36
+ "message": "Resource not found",
37
+ "details": null
38
+ }
39
+ }
40
+ ```
41
+
42
+ Common error codes: `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `UNPROCESSABLE_ENTITY`, `TOO_MANY_REQUESTS`, `INTERNAL_ERROR`.
43
+
44
+ ## Authentication
45
+
46
+ - Bearer token in `Authorization` header
47
+ - JWT issued by `/api/auth/login`
48
+ - Public routes marked with `@Public()` decorator
49
+
50
+ ## RBAC
51
+
52
+ Route permissions declared via `@Permissions("resource:action")` decorator.
53
+ Roles resolve against the addressed company scope.
54
+
55
+ | Role | Description |
56
+ |---|---|
57
+ | owner | Full access |
58
+ | admin | Manage members, settings |
59
+ | member | Standard access |
60
+ | viewer | Read-only |
61
+
62
+ ## Pagination
63
+
64
+ Query params: `page` (default 1), `limit` (default 20, max 100).
65
+
66
+ ## Search
67
+
68
+ `search` query param performs case-insensitive text matching.
69
+
70
+ ## Sorting
71
+
72
+ `sort` and `order` query params (e.g. `?sort=createdAt&order=desc`).
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ reactStrictMode: true,
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "{{description}}",
6
+ "scripts": {
7
+ "dev": "next dev --turbopack",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint"
11
+ },
12
+ "dependencies": {
13
+ "next": "^16.0.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "next-auth": "^4.24.0",
17
+ "next-themes": "^0.4.0",
18
+ "@tanstack/react-query": "^5.60.0",
19
+ "@tanstack/react-table": "^8.20.0",
20
+ "react-hook-form": "^7.53.0",
21
+ "@hookform/resolvers": "^3.9.0",
22
+ "zod": "^3.23.0",
23
+ "socket.io-client": "^4.8.0",
24
+ "iconsax-react": "^0.0.8",
25
+ "clsx": "^2.1.0",
26
+ "tailwind-merge": "^2.5.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.0.0",
30
+ "@types/react": "^19.0.0",
31
+ "@types/react-dom": "^19.0.0",
32
+ "typescript": "^5.6.0",
33
+ "tailwindcss": "^4.0.0",
34
+ "@tailwindcss/postcss": "^4.0.0",
35
+ "postcss": "^8.4.0"
36
+ }
37
+ }
@@ -0,0 +1,48 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme inline {
4
+ --app-primary-50: #eff6ff;
5
+ --app-primary-100: #dbeafe;
6
+ --app-primary-200: #bfdbfe;
7
+ --app-primary-300: #93c5fd;
8
+ --app-primary-400: #60a5fa;
9
+ --app-primary-500: #3b82f6;
10
+ --app-primary-600: #2563eb;
11
+ --app-primary-700: #1d4ed8;
12
+ --app-primary-800: #1e40af;
13
+ --app-primary-900: #1e3a8a;
14
+
15
+ --app-bg-primary: #ffffff;
16
+ --app-bg-secondary: #f8fafc;
17
+ --app-bg-tertiary: #f1f5f9;
18
+ --app-bg-inverse: #0f172a;
19
+
20
+ --app-text-primary: #0f172a;
21
+ --app-text-secondary: #475569;
22
+ --app-text-tertiary: #94a3b8;
23
+ --app-text-inverse: #f8fafc;
24
+
25
+ --app-border-default: #e2e8f0;
26
+ --app-border-strong: #cbd5e1;
27
+
28
+ --app-radius: 0.5rem;
29
+
30
+ --color-background: var(--app-bg-primary);
31
+ --color-foreground: var(--app-text-primary);
32
+ --color-muted: var(--app-bg-secondary);
33
+ --color-muted-foreground: var(--app-text-secondary);
34
+ --color-border: var(--app-border-default);
35
+ --color-primary: var(--app-primary-600);
36
+ --color-primary-foreground: var(--app-text-inverse);
37
+ }
38
+
39
+ @custom-variant dark (&:is(.dark *));
40
+
41
+ @layer base {
42
+ * {
43
+ @apply border-border;
44
+ }
45
+ body {
46
+ @apply bg-background text-foreground;
47
+ }
48
+ }
@@ -0,0 +1,25 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import { Providers } from "@/lib/providers";
4
+ import "./globals.css";
5
+
6
+ const inter = Inter({ subsets: ["latin"] });
7
+
8
+ export const metadata: Metadata = {
9
+ title: "{{projectName}}",
10
+ description: "{{description}}",
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ return (
19
+ <html lang="en" suppressHydrationWarning>
20
+ <body className={inter.className}>
21
+ <Providers>{children}</Providers>
22
+ </body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,59 @@
1
+ import { Loader2 } from "lucide-react";
2
+ import { type ButtonHTMLAttributes, forwardRef } from "react";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
12
+ destructive:
13
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14
+ outline:
15
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
+ ghost: "hover:bg-accent hover:text-accent-foreground",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default: "h-10 px-4 py-2",
23
+ sm: "h-9 rounded-md px-3",
24
+ lg: "h-11 rounded-md px-8",
25
+ icon: "h-10 w-10",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ size: "default",
31
+ },
32
+ }
33
+ );
34
+
35
+ export interface AppButtonProps
36
+ extends ButtonHTMLAttributes<HTMLButtonElement>,
37
+ VariantProps<typeof buttonVariants> {
38
+ loading?: boolean;
39
+ }
40
+
41
+ const AppButton = forwardRef<HTMLButtonElement, AppButtonProps>(
42
+ ({ className, variant, size, loading, disabled, children, ...props }, ref) => {
43
+ return (
44
+ <button
45
+ ref={ref}
46
+ data-slot="app-button"
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ disabled={disabled || loading}
49
+ {...props}
50
+ >
51
+ {loading && <Loader2 className="h-4 w-4 animate-spin" variant="Outline" color="currentColor" />}
52
+ {children}
53
+ </button>
54
+ );
55
+ }
56
+ );
57
+ AppButton.displayName = "AppButton";
58
+
59
+ export { AppButton, buttonVariants };
@@ -0,0 +1,39 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface AppEmptyStateProps {
5
+ icon?: ReactNode;
6
+ title: string;
7
+ description?: string;
8
+ action?: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function AppEmptyState({
13
+ icon,
14
+ title,
15
+ description,
16
+ action,
17
+ className,
18
+ }: AppEmptyStateProps) {
19
+ return (
20
+ <div
21
+ data-slot="app-empty-state"
22
+ className={cn(
23
+ "flex flex-col items-center justify-center py-12 text-center",
24
+ className
25
+ )}
26
+ >
27
+ {icon && (
28
+ <div className="mb-4 text-tertiary [&_svg]:h-12 [&_svg]:w-12">
29
+ {icon}
30
+ </div>
31
+ )}
32
+ <h3 className="text-lg font-semibold text-primary">{title}</h3>
33
+ {description && (
34
+ <p className="mt-1 max-w-sm text-sm text-secondary">{description}</p>
35
+ )}
36
+ {action && <div className="mt-4">{action}</div>}
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { Search } from "lucide-react";
4
+ import { type ReactNode } from "react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export interface AppListToolbarProps {
8
+ search?: string;
9
+ onSearchChange?: (value: string) => void;
10
+ searchPlaceholder?: string;
11
+ filters?: ReactNode;
12
+ actions?: ReactNode;
13
+ resultCount?: number;
14
+ className?: string;
15
+ }
16
+
17
+ export function AppListToolbar({
18
+ search,
19
+ onSearchChange,
20
+ searchPlaceholder = "Search...",
21
+ filters,
22
+ actions,
23
+ resultCount,
24
+ className,
25
+ }: AppListToolbarProps) {
26
+ return (
27
+ <div
28
+ data-slot="app-list-toolbar"
29
+ className={cn(
30
+ "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
31
+ className
32
+ )}
33
+ >
34
+ <div className="flex flex-1 items-center gap-3">
35
+ {onSearchChange && (
36
+ <div className="relative flex-1 max-w-sm">
37
+ <Search
38
+ className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-tertiary"
39
+ variant="Outline"
40
+ color="currentColor"
41
+ />
42
+ <input
43
+ type="text"
44
+ placeholder={searchPlaceholder}
45
+ value={search ?? ""}
46
+ onChange={(e) => onSearchChange(e.target.value)}
47
+ className="h-10 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm placeholder:text-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
48
+ />
49
+ </div>
50
+ )}
51
+ {filters}
52
+ </div>
53
+ <div className="flex items-center gap-3">
54
+ {resultCount !== undefined && (
55
+ <span className="text-sm text-secondary">
56
+ {resultCount} result{resultCount !== 1 ? "s" : ""}
57
+ </span>
58
+ )}
59
+ {actions}
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,14 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ export interface AppSkeletonProps {
4
+ className?: string;
5
+ }
6
+
7
+ export function AppSkeleton({ className }: AppSkeletonProps) {
8
+ return (
9
+ <div
10
+ data-slot="app-skeleton"
11
+ className={cn("animate-pulse rounded-md bg-muted", className)}
12
+ />
13
+ );
14
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ColumnDef,
5
+ type PaginationState,
6
+ type SortingState,
7
+ flexRender,
8
+ getCoreRowModel,
9
+ getPaginationRowModel,
10
+ getSortedRowModel,
11
+ useReactTable,
12
+ } from "@tanstack/react-table";
13
+ import { type ReactNode } from "react";
14
+ import { cn } from "@/lib/utils";
15
+ import { AppSkeleton } from "./app-skeleton";
16
+ import { AppEmptyState } from "./app-empty-state";
17
+
18
+ export interface AppTableProps<TData> {
19
+ columns: ColumnDef<TData, unknown>[];
20
+ data: TData[];
21
+ loading?: boolean;
22
+ sorting?: SortingState;
23
+ onSortingChange?: (sorting: SortingState) => void;
24
+ pagination?: PaginationState;
25
+ onPaginationChange?: (pagination: PaginationState) => void;
26
+ pageCount?: number;
27
+ emptyIcon?: ReactNode;
28
+ emptyTitle?: string;
29
+ emptyDescription?: string;
30
+ className?: string;
31
+ }
32
+
33
+ export function AppTable<TData>({
34
+ columns,
35
+ data,
36
+ loading = false,
37
+ sorting,
38
+ onSortingChange,
39
+ pagination,
40
+ onPaginationChange,
41
+ pageCount,
42
+ emptyIcon,
43
+ emptyTitle = "No results",
44
+ emptyDescription,
45
+ className,
46
+ }: AppTableProps<TData>) {
47
+ const table = useReactTable({
48
+ data,
49
+ columns,
50
+ pageCount,
51
+ state: {
52
+ sorting,
53
+ pagination,
54
+ },
55
+ onSortingChange,
56
+ onPaginationChange,
57
+ getCoreRowModel: getCoreRowModel(),
58
+ getSortedRowModel: getSortedRowModel(),
59
+ getPaginationRowModel: getPaginationRowModel(),
60
+ manualPagination: true,
61
+ });
62
+
63
+ if (loading) {
64
+ return (
65
+ <div data-slot="app-table" className={cn("rounded-md border", className)}>
66
+ <div className="space-y-2 p-4">
67
+ {Array.from({ length: 5 }).map((_, i) => (
68
+ <AppSkeleton key={i} className="h-12 w-full" />
69
+ ))}
70
+ </div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ if (data.length === 0) {
76
+ return (
77
+ <div data-slot="app-table" className={cn("rounded-md border", className)}>
78
+ <AppEmptyState
79
+ icon={emptyIcon}
80
+ title={emptyTitle}
81
+ description={emptyDescription}
82
+ />
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <div data-slot="app-table" className={cn("rounded-md border", className)}>
89
+ <div className="overflow-auto">
90
+ <table className="w-full caption-bottom text-sm">
91
+ <thead className="border-b bg-muted/50">
92
+ {table.getHeaderGroups().map((headerGroup) => (
93
+ <tr key={headerGroup.id}>
94
+ {headerGroup.headers.map((header) => (
95
+ <th
96
+ key={header.id}
97
+ className="h-10 px-4 text-left font-medium text-secondary"
98
+ >
99
+ {header.isPlaceholder
100
+ ? null
101
+ : flexRender(
102
+ header.column.columnDef.header,
103
+ header.getContext()
104
+ )}
105
+ </th>
106
+ ))}
107
+ </tr>
108
+ ))}
109
+ </thead>
110
+ <tbody>
111
+ {table.getRowModel().rows.map((row) => (
112
+ <tr
113
+ key={row.id}
114
+ className="border-b transition-colors hover:bg-muted/50"
115
+ >
116
+ {row.getVisibleCells().map((cell) => (
117
+ <td key={cell.id} className="p-4">
118
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
119
+ </td>
120
+ ))}
121
+ </tr>
122
+ ))}
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,5 @@
1
+ export { AppButton, buttonVariants, type AppButtonProps } from "./app-button";
2
+ export { AppEmptyState, type AppEmptyStateProps } from "./app-empty-state";
3
+ export { AppSkeleton, type AppSkeletonProps } from "./app-skeleton";
4
+ export { AppTable, type AppTableProps } from "./app-table";
5
+ export { AppListToolbar, type AppListToolbarProps } from "./app-list-toolbar";
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { useRouter, useSearchParams } from "next/navigation";
4
+ import { useCallback, useTransition } from "react";
5
+
6
+ export interface ListState {
7
+ search: string;
8
+ page: number;
9
+ limit: number;
10
+ sort?: string;
11
+ order?: "asc" | "desc";
12
+ [key: string]: string | number | undefined;
13
+ }
14
+
15
+ export function useListState(defaults: Partial<ListState> = {}) {
16
+ const router = useRouter();
17
+ const searchParams = useSearchParams();
18
+ const [isPending, startTransition] = useTransition();
19
+
20
+ const state: ListState = {
21
+ search: searchParams.get("search") ?? defaults.search ?? "",
22
+ page: Number(searchParams.get("page")) || defaults.page || 1,
23
+ limit: Number(searchParams.get("limit")) || defaults.limit || 20,
24
+ sort: searchParams.get("sort") ?? defaults.sort,
25
+ order: (searchParams.get("order") as "asc" | "desc") ?? defaults.order,
26
+ };
27
+
28
+ const setSearchParams = useCallback(
29
+ (updates: Partial<ListState>) => {
30
+ const params = new URLSearchParams(searchParams.toString());
31
+ Object.entries(updates).forEach(([key, value]) => {
32
+ if (value === undefined || value === null || value === "") {
33
+ params.delete(key);
34
+ } else {
35
+ params.set(key, String(value));
36
+ }
37
+ });
38
+ if (updates.search !== undefined) {
39
+ params.set("page", "1");
40
+ }
41
+ startTransition(() => {
42
+ router.push(`?${params.toString()}`, { scroll: false });
43
+ });
44
+ },
45
+ [router, searchParams, startTransition]
46
+ );
47
+
48
+ const setSearch = useCallback(
49
+ (search: string) => setSearchParams({ search }),
50
+ [setSearchParams]
51
+ );
52
+
53
+ const setPage = useCallback(
54
+ (page: number) => setSearchParams({ page }),
55
+ [setSearchParams]
56
+ );
57
+
58
+ const setSort = useCallback(
59
+ (sort: string, order: "asc" | "desc" = "asc") =>
60
+ setSearchParams({ sort, order }),
61
+ [setSearchParams]
62
+ );
63
+
64
+ return {
65
+ state,
66
+ setSearch,
67
+ setPage,
68
+ setSort,
69
+ setParams: setSearchParams,
70
+ isPending,
71
+ };
72
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { useSearchParams } from "next/navigation";
4
+ import { useCallback } from "react";
5
+
6
+ export function useUrlTab(defaultValue: string = "overview") {
7
+ const searchParams = useSearchParams();
8
+ const tab = searchParams.get("tab") ?? defaultValue;
9
+
10
+ const setTab = useCallback(
11
+ (value: string) => {
12
+ const url = new URL(window.location.href);
13
+ if (value === defaultValue) {
14
+ url.searchParams.delete("tab");
15
+ } else {
16
+ url.searchParams.set("tab", value);
17
+ }
18
+ window.history.replaceState({}, "", url.toString());
19
+ window.dispatchEvent(new Event("popstate"));
20
+ },
21
+ [defaultValue]
22
+ );
23
+
24
+ return { tab, setTab };
25
+ }