@spacesync/client 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.
Files changed (100) hide show
  1. package/README.md +39 -0
  2. package/dist/analog-clock-picker.d.ts +10 -0
  3. package/dist/analog-clock-picker.js +408 -0
  4. package/dist/analog-clock-picker.js.map +1 -0
  5. package/dist/api.d.ts +222 -0
  6. package/dist/api.js +167 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/app-modal.d.ts +25 -0
  9. package/dist/app-modal.js +145 -0
  10. package/dist/app-modal.js.map +1 -0
  11. package/dist/app-shell-mobile-nav.d.ts +29 -0
  12. package/dist/app-shell-mobile-nav.js +122 -0
  13. package/dist/app-shell-mobile-nav.js.map +1 -0
  14. package/dist/app-web-shell.d.ts +20 -0
  15. package/dist/app-web-shell.js +45 -0
  16. package/dist/app-web-shell.js.map +1 -0
  17. package/dist/booking-schedule-calendars.d.ts +95 -0
  18. package/dist/booking-schedule-calendars.js +1343 -0
  19. package/dist/booking-schedule-calendars.js.map +1 -0
  20. package/dist/chat-stick-to-bottom.d.ts +22 -0
  21. package/dist/chat-stick-to-bottom.js +190 -0
  22. package/dist/chat-stick-to-bottom.js.map +1 -0
  23. package/dist/detail-page-header.d.ts +16 -0
  24. package/dist/detail-page-header.js +220 -0
  25. package/dist/detail-page-header.js.map +1 -0
  26. package/dist/dialog-CIdvylPK.d.ts +16 -0
  27. package/dist/errors-D1niseaa.d.ts +34 -0
  28. package/dist/file-picker-field.d.ts +23 -0
  29. package/dist/file-picker-field.js +165 -0
  30. package/dist/file-picker-field.js.map +1 -0
  31. package/dist/file-viewer-dialog.d.ts +15 -0
  32. package/dist/file-viewer-dialog.js +387 -0
  33. package/dist/file-viewer-dialog.js.map +1 -0
  34. package/dist/floor-switcher.d.ts +14 -0
  35. package/dist/floor-switcher.js +416 -0
  36. package/dist/floor-switcher.js.map +1 -0
  37. package/dist/forms.d.ts +52 -0
  38. package/dist/forms.js +66 -0
  39. package/dist/forms.js.map +1 -0
  40. package/dist/image-picker-field.d.ts +11 -0
  41. package/dist/image-picker-field.js +135 -0
  42. package/dist/image-picker-field.js.map +1 -0
  43. package/dist/layout-canvas-view.d.ts +57 -0
  44. package/dist/layout-canvas-view.js +974 -0
  45. package/dist/layout-canvas-view.js.map +1 -0
  46. package/dist/lib/dateDisplay.d.ts +24 -0
  47. package/dist/lib/dateDisplay.js +84 -0
  48. package/dist/lib/dateDisplay.js.map +1 -0
  49. package/dist/live-resource-meta.d.ts +11 -0
  50. package/dist/live-resource-meta.js +27 -0
  51. package/dist/live-resource-meta.js.map +1 -0
  52. package/dist/live-space-pulse-dot.d.ts +8 -0
  53. package/dist/live-space-pulse-dot.js +18 -0
  54. package/dist/live-space-pulse-dot.js.map +1 -0
  55. package/dist/notification-sidebar-preview.d.ts +37 -0
  56. package/dist/notification-sidebar-preview.js +121 -0
  57. package/dist/notification-sidebar-preview.js.map +1 -0
  58. package/dist/page-list-chrome.d.ts +124 -0
  59. package/dist/page-list-chrome.js +970 -0
  60. package/dist/page-list-chrome.js.map +1 -0
  61. package/dist/platform-support-message-composer.d.ts +38 -0
  62. package/dist/platform-support-message-composer.js +446 -0
  63. package/dist/platform-support-message-composer.js.map +1 -0
  64. package/dist/shell-sidebar-nav.d.ts +16 -0
  65. package/dist/shell-sidebar-nav.js +69 -0
  66. package/dist/shell-sidebar-nav.js.map +1 -0
  67. package/dist/shell-theme.d.ts +6 -0
  68. package/dist/shell-theme.js +9 -0
  69. package/dist/shell-theme.js.map +1 -0
  70. package/dist/shell-top-bar-chrome.d.ts +39 -0
  71. package/dist/shell-top-bar-chrome.js +105 -0
  72. package/dist/shell-top-bar-chrome.js.map +1 -0
  73. package/dist/shell-top-bar.d.ts +16 -0
  74. package/dist/shell-top-bar.js +71 -0
  75. package/dist/shell-top-bar.js.map +1 -0
  76. package/dist/styles/theme.css +223 -0
  77. package/dist/styles/theme.css.map +1 -0
  78. package/dist/styles/theme.d.ts +2 -0
  79. package/dist/support-presence-status.d.ts +11 -0
  80. package/dist/support-presence-status.js +92 -0
  81. package/dist/support-presence-status.js.map +1 -0
  82. package/dist/theme-appearance-control.d.ts +5 -0
  83. package/dist/theme-appearance-control.js +114 -0
  84. package/dist/theme-appearance-control.js.map +1 -0
  85. package/dist/theme-boot-script.d.ts +4 -0
  86. package/dist/theme-boot-script.js +9 -0
  87. package/dist/theme-boot-script.js.map +1 -0
  88. package/dist/theme-provider.d.ts +9 -0
  89. package/dist/theme-provider.js +25 -0
  90. package/dist/theme-provider.js.map +1 -0
  91. package/dist/ui.d.ts +765 -0
  92. package/dist/ui.js +5637 -0
  93. package/dist/ui.js.map +1 -0
  94. package/dist/use-client-pagination.d.ts +22 -0
  95. package/dist/use-client-pagination.js +51 -0
  96. package/dist/use-client-pagination.js.map +1 -0
  97. package/dist/use-client-sort.d.ts +20 -0
  98. package/dist/use-client-sort.js +41 -0
  99. package/dist/use-client-sort.js.map +1 -0
  100. package/package.json +208 -0
package/dist/api.d.ts ADDED
@@ -0,0 +1,222 @@
1
+ import { z } from 'zod';
2
+ export { A as ApiClientError, a as ApiClientErrorKind, b as ApiClientErrorOptions, c as ApiFieldError, i as isApiClientError, n as normalizeAxiosError } from './errors-D1niseaa.js';
3
+ import { AxiosInstance, AxiosRequestConfig } from 'axios';
4
+
5
+ /**
6
+ * Mirrors `apps/api/src/lib/ApiResponse.ts` exactly. Every route on the
7
+ * backend responds with this envelope (the only exceptions are documented
8
+ * per-service, e.g. `POST /auth/logout` returns 204 with no body).
9
+ */
10
+ interface ApiResponseMeta {
11
+ page?: number;
12
+ limit?: number;
13
+ total?: number;
14
+ totalPages?: number;
15
+ [key: string]: unknown;
16
+ }
17
+ declare const paginationMetaSchema: z.ZodObject<{
18
+ page: z.ZodOptional<z.ZodNumber>;
19
+ limit: z.ZodOptional<z.ZodNumber>;
20
+ total: z.ZodOptional<z.ZodNumber>;
21
+ totalPages: z.ZodOptional<z.ZodNumber>;
22
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
23
+ page: z.ZodOptional<z.ZodNumber>;
24
+ limit: z.ZodOptional<z.ZodNumber>;
25
+ total: z.ZodOptional<z.ZodNumber>;
26
+ totalPages: z.ZodOptional<z.ZodNumber>;
27
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
28
+ page: z.ZodOptional<z.ZodNumber>;
29
+ limit: z.ZodOptional<z.ZodNumber>;
30
+ total: z.ZodOptional<z.ZodNumber>;
31
+ totalPages: z.ZodOptional<z.ZodNumber>;
32
+ }, z.ZodTypeAny, "passthrough">>;
33
+ declare const apiErrorBodySchema: z.ZodObject<{
34
+ success: z.ZodLiteral<false>;
35
+ data: z.ZodNull;
36
+ error: z.ZodObject<{
37
+ message: z.ZodString;
38
+ details: z.ZodOptional<z.ZodUnknown>;
39
+ }, "strip", z.ZodTypeAny, {
40
+ message: string;
41
+ details?: unknown;
42
+ }, {
43
+ message: string;
44
+ details?: unknown;
45
+ }>;
46
+ meta: z.ZodOptional<z.ZodObject<{
47
+ page: z.ZodOptional<z.ZodNumber>;
48
+ limit: z.ZodOptional<z.ZodNumber>;
49
+ total: z.ZodOptional<z.ZodNumber>;
50
+ totalPages: z.ZodOptional<z.ZodNumber>;
51
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
52
+ page: z.ZodOptional<z.ZodNumber>;
53
+ limit: z.ZodOptional<z.ZodNumber>;
54
+ total: z.ZodOptional<z.ZodNumber>;
55
+ totalPages: z.ZodOptional<z.ZodNumber>;
56
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
57
+ page: z.ZodOptional<z.ZodNumber>;
58
+ limit: z.ZodOptional<z.ZodNumber>;
59
+ total: z.ZodOptional<z.ZodNumber>;
60
+ totalPages: z.ZodOptional<z.ZodNumber>;
61
+ }, z.ZodTypeAny, "passthrough">>>;
62
+ }, "strip", z.ZodTypeAny, {
63
+ success: false;
64
+ data: null;
65
+ error: {
66
+ message: string;
67
+ details?: unknown;
68
+ };
69
+ meta?: z.objectOutputType<{
70
+ page: z.ZodOptional<z.ZodNumber>;
71
+ limit: z.ZodOptional<z.ZodNumber>;
72
+ total: z.ZodOptional<z.ZodNumber>;
73
+ totalPages: z.ZodOptional<z.ZodNumber>;
74
+ }, z.ZodTypeAny, "passthrough"> | undefined;
75
+ }, {
76
+ success: false;
77
+ data: null;
78
+ error: {
79
+ message: string;
80
+ details?: unknown;
81
+ };
82
+ meta?: z.objectInputType<{
83
+ page: z.ZodOptional<z.ZodNumber>;
84
+ limit: z.ZodOptional<z.ZodNumber>;
85
+ total: z.ZodOptional<z.ZodNumber>;
86
+ totalPages: z.ZodOptional<z.ZodNumber>;
87
+ }, z.ZodTypeAny, "passthrough"> | undefined;
88
+ }>;
89
+ type ApiErrorBody = z.infer<typeof apiErrorBodySchema>;
90
+ /**
91
+ * Builds a schema for the success envelope around a given data schema, e.g.
92
+ * `apiSuccessBodySchema(bookingSchema)` or `apiSuccessBodySchema(z.array(bookingSchema))`.
93
+ */
94
+ declare function apiSuccessBodySchema<TData extends z.ZodTypeAny>(dataSchema: TData): z.ZodObject<{
95
+ success: z.ZodLiteral<true>;
96
+ data: TData;
97
+ error: z.ZodNull;
98
+ meta: z.ZodOptional<z.ZodObject<{
99
+ page: z.ZodOptional<z.ZodNumber>;
100
+ limit: z.ZodOptional<z.ZodNumber>;
101
+ total: z.ZodOptional<z.ZodNumber>;
102
+ totalPages: z.ZodOptional<z.ZodNumber>;
103
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
104
+ page: z.ZodOptional<z.ZodNumber>;
105
+ limit: z.ZodOptional<z.ZodNumber>;
106
+ total: z.ZodOptional<z.ZodNumber>;
107
+ totalPages: z.ZodOptional<z.ZodNumber>;
108
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
109
+ page: z.ZodOptional<z.ZodNumber>;
110
+ limit: z.ZodOptional<z.ZodNumber>;
111
+ total: z.ZodOptional<z.ZodNumber>;
112
+ totalPages: z.ZodOptional<z.ZodNumber>;
113
+ }, z.ZodTypeAny, "passthrough">>>;
114
+ }, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<{
115
+ success: z.ZodLiteral<true>;
116
+ data: TData;
117
+ error: z.ZodNull;
118
+ meta: z.ZodOptional<z.ZodObject<{
119
+ page: z.ZodOptional<z.ZodNumber>;
120
+ limit: z.ZodOptional<z.ZodNumber>;
121
+ total: z.ZodOptional<z.ZodNumber>;
122
+ totalPages: z.ZodOptional<z.ZodNumber>;
123
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
124
+ page: z.ZodOptional<z.ZodNumber>;
125
+ limit: z.ZodOptional<z.ZodNumber>;
126
+ total: z.ZodOptional<z.ZodNumber>;
127
+ totalPages: z.ZodOptional<z.ZodNumber>;
128
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
129
+ page: z.ZodOptional<z.ZodNumber>;
130
+ limit: z.ZodOptional<z.ZodNumber>;
131
+ total: z.ZodOptional<z.ZodNumber>;
132
+ totalPages: z.ZodOptional<z.ZodNumber>;
133
+ }, z.ZodTypeAny, "passthrough">>>;
134
+ }>, any> extends infer T ? { [k in keyof T]: T[k]; } : never, z.baseObjectInputType<{
135
+ success: z.ZodLiteral<true>;
136
+ data: TData;
137
+ error: z.ZodNull;
138
+ meta: z.ZodOptional<z.ZodObject<{
139
+ page: z.ZodOptional<z.ZodNumber>;
140
+ limit: z.ZodOptional<z.ZodNumber>;
141
+ total: z.ZodOptional<z.ZodNumber>;
142
+ totalPages: z.ZodOptional<z.ZodNumber>;
143
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
144
+ page: z.ZodOptional<z.ZodNumber>;
145
+ limit: z.ZodOptional<z.ZodNumber>;
146
+ total: z.ZodOptional<z.ZodNumber>;
147
+ totalPages: z.ZodOptional<z.ZodNumber>;
148
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
149
+ page: z.ZodOptional<z.ZodNumber>;
150
+ limit: z.ZodOptional<z.ZodNumber>;
151
+ total: z.ZodOptional<z.ZodNumber>;
152
+ totalPages: z.ZodOptional<z.ZodNumber>;
153
+ }, z.ZodTypeAny, "passthrough">>>;
154
+ }> extends infer T_1 ? { [k_1 in keyof T_1]: T_1[k_1]; } : never>;
155
+ type ApiSuccessBody<TData> = {
156
+ success: true;
157
+ data: TData;
158
+ error: null;
159
+ meta?: ApiResponseMeta;
160
+ };
161
+
162
+ interface AuthTokens {
163
+ accessToken: string;
164
+ refreshToken: string;
165
+ }
166
+ interface TokenStorage {
167
+ getAccessToken(): string | null;
168
+ getRefreshToken(): string | null;
169
+ setTokens(tokens: AuthTokens): void;
170
+ clearTokens(): void;
171
+ }
172
+ /**
173
+ * localStorage-backed token storage, namespaced per app/scope (e.g. "tenant", "platform")
174
+ * so multiple apps on the same origin (or a single app talking to two backends) never
175
+ * collide on the same keys.
176
+ */
177
+ declare function createLocalStorageTokenStorage(namespace: string): TokenStorage;
178
+
179
+ declare module "axios" {
180
+ interface AxiosRequestConfig {
181
+ /** Set on requests (login/register/refresh) that should never get a Bearer header or trigger a refresh-retry. */
182
+ skipAuth?: boolean;
183
+ /** Internal flag set by the response interceptor to avoid retrying the same request twice. */
184
+ _retriedAfterRefresh?: boolean;
185
+ }
186
+ }
187
+ interface CreateApiClientConfig {
188
+ baseURL: string;
189
+ tokenStorage: TokenStorage;
190
+ /** Path (relative to `baseURL`) used to exchange a refresh token for a new token pair, e.g. "/auth/refresh". */
191
+ refreshPath: string;
192
+ /** Called once refreshing the session has failed (bad/expired refresh token) - e.g. navigate to /login. */
193
+ onAuthExpired?: () => void;
194
+ }
195
+ /**
196
+ * Creates an axios instance that:
197
+ * - attaches `Authorization: Bearer <accessToken>` to every request (unless `skipAuth` is set on that request's config)
198
+ * - on a 401, refreshes the session exactly once (concurrent 401s share the same in-flight refresh call via
199
+ * `refreshPromise`, so a burst of requests never fires multiple refreshes) and retries the original request
200
+ * - on refresh failure, clears stored tokens and calls `onAuthExpired` so the app can redirect to login
201
+ *
202
+ * Does NOT unwrap the `{ success, data, error }` envelope itself - see `request.ts` for that - so raw responses
203
+ * (e.g. the 204 from `/auth/logout`) are still inspectable when needed.
204
+ */
205
+ declare function createApiClient(config: CreateApiClientConfig): AxiosInstance;
206
+
207
+ interface ApiRequestResult<TData> {
208
+ data: TData;
209
+ meta?: ApiResponseMeta;
210
+ }
211
+ /**
212
+ * Runs a request through the given client, validates + unwraps the `{ success, data, error, meta }`
213
+ * envelope against `dataSchema`, and normalizes any failure (network, envelope-shape mismatch, or a
214
+ * `{ success: false }` response) into an `ApiClientError`.
215
+ *
216
+ * This is the one place response validation + error normalization happens, so every service call
217
+ * built on top of it gets a runtime-verified response "for free" - if the backend's actual response
218
+ * doesn't match `dataSchema`, this throws instead of silently returning malformed data.
219
+ */
220
+ declare function apiRequest<TData>(client: AxiosInstance, requestConfig: AxiosRequestConfig, dataSchema: z.ZodType<TData>): Promise<ApiRequestResult<TData>>;
221
+
222
+ export { type ApiErrorBody, type ApiRequestResult, type ApiResponseMeta, type ApiSuccessBody, type AuthTokens, type CreateApiClientConfig, type TokenStorage, apiErrorBodySchema, apiRequest, apiSuccessBodySchema, createApiClient, createLocalStorageTokenStorage, paginationMetaSchema };
package/dist/api.js ADDED
@@ -0,0 +1,167 @@
1
+ import { z } from 'zod';
2
+ import axios2 from 'axios';
3
+
4
+ // src/api/envelope.ts
5
+ var paginationMetaSchema = z.object({
6
+ page: z.number().optional(),
7
+ limit: z.number().optional(),
8
+ total: z.number().optional(),
9
+ totalPages: z.number().optional()
10
+ }).passthrough();
11
+ var apiErrorBodySchema = z.object({
12
+ success: z.literal(false),
13
+ data: z.null(),
14
+ error: z.object({
15
+ message: z.string(),
16
+ details: z.unknown().optional()
17
+ }),
18
+ meta: paginationMetaSchema.optional()
19
+ });
20
+ function apiSuccessBodySchema(dataSchema) {
21
+ return z.object({
22
+ success: z.literal(true),
23
+ data: dataSchema,
24
+ error: z.null(),
25
+ meta: paginationMetaSchema.optional()
26
+ });
27
+ }
28
+ var ApiClientError = class _ApiClientError extends Error {
29
+ kind;
30
+ statusCode;
31
+ details;
32
+ fieldErrors;
33
+ constructor(kind, message, options = {}) {
34
+ super(message);
35
+ this.name = "ApiClientError";
36
+ this.kind = kind;
37
+ this.statusCode = options.statusCode;
38
+ this.details = options.details;
39
+ this.fieldErrors = options.fieldErrors;
40
+ Object.setPrototypeOf(this, _ApiClientError.prototype);
41
+ }
42
+ };
43
+ function isApiClientError(err) {
44
+ return err instanceof ApiClientError;
45
+ }
46
+ function isZodIssueArray(details) {
47
+ return Array.isArray(details) && details.length > 0 && details.every(
48
+ (issue) => typeof issue === "object" && issue !== null && Array.isArray(issue.path) && typeof issue.message === "string"
49
+ );
50
+ }
51
+ function toFieldErrors(issues) {
52
+ return issues.map((issue) => ({
53
+ path: issue.path.join("."),
54
+ message: issue.message
55
+ }));
56
+ }
57
+ function normalizeAxiosError(err) {
58
+ if (isApiClientError(err)) return err;
59
+ if (axios2.isAxiosError(err)) {
60
+ if (!err.response) {
61
+ return new ApiClientError("network", "Unable to reach the server. Check your connection and try again.");
62
+ }
63
+ const status = err.response.status;
64
+ const parsedBody = apiErrorBodySchema.safeParse(err.response.data);
65
+ const message2 = parsedBody.success ? parsedBody.data.error.message : `Request failed with status ${status}`;
66
+ const details = parsedBody.success ? parsedBody.data.error.details : void 0;
67
+ if (status === 400 && isZodIssueArray(details)) {
68
+ return new ApiClientError("validation", message2, {
69
+ statusCode: status,
70
+ details,
71
+ fieldErrors: toFieldErrors(details)
72
+ });
73
+ }
74
+ return new ApiClientError("http", message2, { statusCode: status, details });
75
+ }
76
+ if (err instanceof z.ZodError) {
77
+ return new ApiClientError("http", "Unexpected response shape from the server.", {
78
+ details: err.issues
79
+ });
80
+ }
81
+ const message = err instanceof Error ? err.message : "Something went wrong. Please try again.";
82
+ return new ApiClientError("http", message);
83
+ }
84
+
85
+ // src/api/tokenStorage.ts
86
+ function createLocalStorageTokenStorage(namespace) {
87
+ const accessKey = `coworking.${namespace}.accessToken`;
88
+ const refreshKey = `coworking.${namespace}.refreshToken`;
89
+ return {
90
+ getAccessToken() {
91
+ return localStorage.getItem(accessKey);
92
+ },
93
+ getRefreshToken() {
94
+ return localStorage.getItem(refreshKey);
95
+ },
96
+ setTokens({ accessToken, refreshToken }) {
97
+ localStorage.setItem(accessKey, accessToken);
98
+ localStorage.setItem(refreshKey, refreshToken);
99
+ },
100
+ clearTokens() {
101
+ localStorage.removeItem(accessKey);
102
+ localStorage.removeItem(refreshKey);
103
+ }
104
+ };
105
+ }
106
+ function createApiClient(config) {
107
+ const { baseURL, tokenStorage, refreshPath, onAuthExpired } = config;
108
+ const client = axios2.create({ baseURL });
109
+ let refreshPromise = null;
110
+ async function refreshTokens() {
111
+ const refreshToken = tokenStorage.getRefreshToken();
112
+ if (!refreshToken) throw new Error("No refresh token available");
113
+ const response = await axios2.post(`${baseURL}${refreshPath}`, { refreshToken });
114
+ const tokens = response.data.data;
115
+ tokenStorage.setTokens(tokens);
116
+ return tokens;
117
+ }
118
+ client.interceptors.request.use((requestConfig) => {
119
+ if (requestConfig.skipAuth) return requestConfig;
120
+ const accessToken = tokenStorage.getAccessToken();
121
+ if (accessToken) {
122
+ requestConfig.headers.set("Authorization", `Bearer ${accessToken}`);
123
+ }
124
+ return requestConfig;
125
+ });
126
+ client.interceptors.response.use(
127
+ (response) => response,
128
+ async (error) => {
129
+ if (!axios2.isAxiosError(error) || error.response?.status !== 401) {
130
+ throw error;
131
+ }
132
+ const originalRequest = error.config;
133
+ if (!originalRequest || originalRequest.skipAuth || originalRequest._retriedAfterRefresh) {
134
+ throw error;
135
+ }
136
+ try {
137
+ refreshPromise ??= refreshTokens().finally(() => {
138
+ refreshPromise = null;
139
+ });
140
+ const tokens = await refreshPromise;
141
+ originalRequest._retriedAfterRefresh = true;
142
+ originalRequest.headers.set("Authorization", `Bearer ${tokens.accessToken}`);
143
+ return client.request(originalRequest);
144
+ } catch {
145
+ tokenStorage.clearTokens();
146
+ onAuthExpired?.();
147
+ throw error;
148
+ }
149
+ }
150
+ );
151
+ return client;
152
+ }
153
+
154
+ // src/api/request.ts
155
+ async function apiRequest(client, requestConfig, dataSchema) {
156
+ try {
157
+ const response = await client.request(requestConfig);
158
+ const parsed = apiSuccessBodySchema(dataSchema).parse(response.data);
159
+ return { data: parsed.data, meta: parsed.meta };
160
+ } catch (err) {
161
+ throw normalizeAxiosError(err);
162
+ }
163
+ }
164
+
165
+ export { ApiClientError, apiErrorBodySchema, apiRequest, apiSuccessBodySchema, createApiClient, createLocalStorageTokenStorage, isApiClientError, normalizeAxiosError, paginationMetaSchema };
166
+ //# sourceMappingURL=api.js.map
167
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/envelope.ts","../src/api/errors.ts","../src/api/tokenStorage.ts","../src/api/createApiClient.ts","../src/api/request.ts"],"names":["axios","message","z"],"mappings":"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","file":"api.js","sourcesContent":["import { z } from \"zod\";\n\n/**\n * Mirrors `apps/api/src/lib/ApiResponse.ts` exactly. Every route on the\n * backend responds with this envelope (the only exceptions are documented\n * per-service, e.g. `POST /auth/logout` returns 204 with no body).\n */\nexport interface ApiResponseMeta {\n page?: number;\n limit?: number;\n total?: number;\n totalPages?: number;\n [key: string]: unknown;\n}\n\nexport const paginationMetaSchema = z\n .object({\n page: z.number().optional(),\n limit: z.number().optional(),\n total: z.number().optional(),\n totalPages: z.number().optional(),\n })\n .passthrough();\n\nexport const apiErrorBodySchema = z.object({\n success: z.literal(false),\n data: z.null(),\n error: z.object({\n message: z.string(),\n details: z.unknown().optional(),\n }),\n meta: paginationMetaSchema.optional(),\n});\n\nexport type ApiErrorBody = z.infer<typeof apiErrorBodySchema>;\n\n/**\n * Builds a schema for the success envelope around a given data schema, e.g.\n * `apiSuccessBodySchema(bookingSchema)` or `apiSuccessBodySchema(z.array(bookingSchema))`.\n */\nexport function apiSuccessBodySchema<TData extends z.ZodTypeAny>(dataSchema: TData) {\n return z.object({\n success: z.literal(true),\n data: dataSchema,\n error: z.null(),\n meta: paginationMetaSchema.optional(),\n });\n}\n\nexport type ApiSuccessBody<TData> = {\n success: true;\n data: TData;\n error: null;\n meta?: ApiResponseMeta;\n};\n","import axios from \"axios\";\nimport { z } from \"zod\";\nimport { apiErrorBodySchema } from \"./envelope\";\n\n/**\n * - \"network\": the request never got a response at all (server unreachable, timeout, CORS/preflight failure).\n * - \"validation\": HTTP 400 from our API where `error.details` is the Zod-issues shape produced by\n * `validate.middleware.ts` — field-mappable.\n * - \"http\": every other status our API returns (401, 403, 404, 409, 500, ...). Not field-specific by default.\n */\nexport type ApiClientErrorKind = \"network\" | \"validation\" | \"http\";\n\nexport interface ApiFieldError {\n /** Dot-path, e.g. \"email\" — matched against react-hook-form field names. */\n path: string;\n message: string;\n}\n\nexport interface ApiClientErrorOptions {\n statusCode?: number;\n details?: unknown;\n fieldErrors?: ApiFieldError[];\n}\n\nexport class ApiClientError extends Error {\n readonly kind: ApiClientErrorKind;\n readonly statusCode?: number;\n readonly details?: unknown;\n readonly fieldErrors?: ApiFieldError[];\n\n constructor(kind: ApiClientErrorKind, message: string, options: ApiClientErrorOptions = {}) {\n super(message);\n this.name = \"ApiClientError\";\n this.kind = kind;\n this.statusCode = options.statusCode;\n this.details = options.details;\n this.fieldErrors = options.fieldErrors;\n Object.setPrototypeOf(this, ApiClientError.prototype);\n }\n}\n\nexport function isApiClientError(err: unknown): err is ApiClientError {\n return err instanceof ApiClientError;\n}\n\ninterface ZodLikeIssue {\n path: Array<string | number>;\n message: string;\n}\n\nfunction isZodIssueArray(details: unknown): details is ZodLikeIssue[] {\n return (\n Array.isArray(details) &&\n details.length > 0 &&\n details.every(\n (issue) =>\n typeof issue === \"object\" &&\n issue !== null &&\n Array.isArray((issue as ZodLikeIssue).path) &&\n typeof (issue as ZodLikeIssue).message === \"string\",\n )\n );\n}\n\nfunction toFieldErrors(issues: ZodLikeIssue[]): ApiFieldError[] {\n return issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n }));\n}\n\n/**\n * Normalizes anything that can be thrown while making a request (axios error,\n * envelope-shape mismatch, or an unexpected exception) into one `ApiClientError`,\n * classified by `kind` so callers like `useApiForm` can apply a sensible default\n * per failure mode instead of treating a dropped connection the same as a 409.\n */\nexport function normalizeAxiosError(err: unknown): ApiClientError {\n if (isApiClientError(err)) return err;\n\n if (axios.isAxiosError(err)) {\n if (!err.response) {\n return new ApiClientError(\"network\", \"Unable to reach the server. Check your connection and try again.\");\n }\n\n const status = err.response.status;\n const parsedBody = apiErrorBodySchema.safeParse(err.response.data);\n const message = parsedBody.success ? parsedBody.data.error.message : `Request failed with status ${status}`;\n const details = parsedBody.success ? parsedBody.data.error.details : undefined;\n\n if (status === 400 && isZodIssueArray(details)) {\n return new ApiClientError(\"validation\", message, {\n statusCode: status,\n details,\n fieldErrors: toFieldErrors(details),\n });\n }\n\n return new ApiClientError(\"http\", message, { statusCode: status, details });\n }\n\n if (err instanceof z.ZodError) {\n // The server responded 2xx but the payload didn't match the schema we expected -\n // a frontend/backend contract mismatch, not a user-facing form error.\n return new ApiClientError(\"http\", \"Unexpected response shape from the server.\", {\n details: err.issues,\n });\n }\n\n const message = err instanceof Error ? err.message : \"Something went wrong. Please try again.\";\n return new ApiClientError(\"http\", message);\n}\n","export interface AuthTokens {\n accessToken: string;\n refreshToken: string;\n}\n\nexport interface TokenStorage {\n getAccessToken(): string | null;\n getRefreshToken(): string | null;\n setTokens(tokens: AuthTokens): void;\n clearTokens(): void;\n}\n\n/**\n * localStorage-backed token storage, namespaced per app/scope (e.g. \"tenant\", \"platform\")\n * so multiple apps on the same origin (or a single app talking to two backends) never\n * collide on the same keys.\n */\nexport function createLocalStorageTokenStorage(namespace: string): TokenStorage {\n const accessKey = `coworking.${namespace}.accessToken`;\n const refreshKey = `coworking.${namespace}.refreshToken`;\n\n return {\n getAccessToken() {\n return localStorage.getItem(accessKey);\n },\n getRefreshToken() {\n return localStorage.getItem(refreshKey);\n },\n setTokens({ accessToken, refreshToken }) {\n localStorage.setItem(accessKey, accessToken);\n localStorage.setItem(refreshKey, refreshToken);\n },\n clearTokens() {\n localStorage.removeItem(accessKey);\n localStorage.removeItem(refreshKey);\n },\n };\n}\n","import axios, { type AxiosInstance } from \"axios\";\nimport type { AuthTokens, TokenStorage } from \"./tokenStorage\";\n\ndeclare module \"axios\" {\n export interface AxiosRequestConfig {\n /** Set on requests (login/register/refresh) that should never get a Bearer header or trigger a refresh-retry. */\n skipAuth?: boolean;\n /** Internal flag set by the response interceptor to avoid retrying the same request twice. */\n _retriedAfterRefresh?: boolean;\n }\n}\n\nexport interface CreateApiClientConfig {\n baseURL: string;\n tokenStorage: TokenStorage;\n /** Path (relative to `baseURL`) used to exchange a refresh token for a new token pair, e.g. \"/auth/refresh\". */\n refreshPath: string;\n /** Called once refreshing the session has failed (bad/expired refresh token) - e.g. navigate to /login. */\n onAuthExpired?: () => void;\n}\n\n/**\n * Creates an axios instance that:\n * - attaches `Authorization: Bearer <accessToken>` to every request (unless `skipAuth` is set on that request's config)\n * - on a 401, refreshes the session exactly once (concurrent 401s share the same in-flight refresh call via\n * `refreshPromise`, so a burst of requests never fires multiple refreshes) and retries the original request\n * - on refresh failure, clears stored tokens and calls `onAuthExpired` so the app can redirect to login\n *\n * Does NOT unwrap the `{ success, data, error }` envelope itself - see `request.ts` for that - so raw responses\n * (e.g. the 204 from `/auth/logout`) are still inspectable when needed.\n */\nexport function createApiClient(config: CreateApiClientConfig): AxiosInstance {\n const { baseURL, tokenStorage, refreshPath, onAuthExpired } = config;\n const client = axios.create({ baseURL });\n\n let refreshPromise: Promise<AuthTokens> | null = null;\n\n async function refreshTokens(): Promise<AuthTokens> {\n const refreshToken = tokenStorage.getRefreshToken();\n if (!refreshToken) throw new Error(\"No refresh token available\");\n\n // Uses the bare `axios` instance (not `client`) so this call never passes through the\n // interceptors below - it can't trigger its own refresh-retry loop.\n const response = await axios.post<{ data: AuthTokens }>(`${baseURL}${refreshPath}`, { refreshToken });\n const tokens = response.data.data;\n tokenStorage.setTokens(tokens);\n return tokens;\n }\n\n client.interceptors.request.use((requestConfig) => {\n if (requestConfig.skipAuth) return requestConfig;\n\n const accessToken = tokenStorage.getAccessToken();\n if (accessToken) {\n requestConfig.headers.set(\"Authorization\", `Bearer ${accessToken}`);\n }\n return requestConfig;\n });\n\n client.interceptors.response.use(\n (response) => response,\n async (error) => {\n if (!axios.isAxiosError(error) || error.response?.status !== 401) {\n throw error;\n }\n\n const originalRequest = error.config;\n if (!originalRequest || originalRequest.skipAuth || originalRequest._retriedAfterRefresh) {\n throw error;\n }\n\n try {\n refreshPromise ??= refreshTokens().finally(() => {\n refreshPromise = null;\n });\n const tokens = await refreshPromise;\n\n originalRequest._retriedAfterRefresh = true;\n originalRequest.headers.set(\"Authorization\", `Bearer ${tokens.accessToken}`);\n return client.request(originalRequest);\n } catch {\n tokenStorage.clearTokens();\n onAuthExpired?.();\n throw error; // surface the original 401, not the refresh failure\n }\n },\n );\n\n return client;\n}\n","import type { AxiosInstance, AxiosRequestConfig } from \"axios\";\nimport type { z } from \"zod\";\nimport { apiSuccessBodySchema, type ApiResponseMeta } from \"./envelope\";\nimport { normalizeAxiosError } from \"./errors\";\n\nexport interface ApiRequestResult<TData> {\n data: TData;\n meta?: ApiResponseMeta;\n}\n\n/**\n * Runs a request through the given client, validates + unwraps the `{ success, data, error, meta }`\n * envelope against `dataSchema`, and normalizes any failure (network, envelope-shape mismatch, or a\n * `{ success: false }` response) into an `ApiClientError`.\n *\n * This is the one place response validation + error normalization happens, so every service call\n * built on top of it gets a runtime-verified response \"for free\" - if the backend's actual response\n * doesn't match `dataSchema`, this throws instead of silently returning malformed data.\n */\nexport async function apiRequest<TData>(\n client: AxiosInstance,\n requestConfig: AxiosRequestConfig,\n dataSchema: z.ZodType<TData>,\n): Promise<ApiRequestResult<TData>> {\n try {\n const response = await client.request(requestConfig);\n const parsed = apiSuccessBodySchema(dataSchema).parse(response.data);\n // `parsed.data` is structurally `TData` by construction (it came from `dataSchema`), but TS\n // can't prove that through a generic zod composition - narrow it explicitly.\n return { data: parsed.data as TData, meta: parsed.meta };\n } catch (err) {\n throw normalizeAxiosError(err);\n }\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import { D as DialogContent, a as DialogFooter, b as DialogHeader } from './dialog-CIdvylPK.js';
4
+ import '@radix-ui/react-dialog';
5
+
6
+ /** Shared shell for `DialogContent` when header/body/footer supply their own padding (`p-0`). */
7
+ declare const appModalDialogContentChrome = "bg-card border-border rounded-xl text-foreground shadow-xl gap-0 p-0 overflow-hidden";
8
+ declare function AppModalDialogContent({ className, ...props }: React.ComponentProps<typeof DialogContent>): react_jsx_runtime.JSX.Element;
9
+ declare function AppModalDialogHeader({ className, ...props }: React.ComponentProps<typeof DialogHeader>): react_jsx_runtime.JSX.Element;
10
+ declare function AppModalDialogBody({ className, ...props }: React.ComponentProps<"div">): react_jsx_runtime.JSX.Element;
11
+ declare function AppModalDialogFooter({ className, muted, ...props }: React.ComponentProps<typeof DialogFooter> & {
12
+ muted?: boolean;
13
+ }): react_jsx_runtime.JSX.Element;
14
+ type ChromeHeaderVariant = "row" | "stack";
15
+ /** Header region for Radix `Dialog.Content` layouts (custom close in header). */
16
+ declare function AppModalChromeHeader({ variant, className, ...props }: React.ComponentProps<"div"> & {
17
+ variant?: ChromeHeaderVariant;
18
+ }): react_jsx_runtime.JSX.Element;
19
+ type ChromeFooterVariant = "end" | "split";
20
+ /** Footer region for Radix `Dialog.Content` layouts. */
21
+ declare function AppModalChromeFooter({ variant, className, ...props }: React.ComponentProps<"div"> & {
22
+ variant?: ChromeFooterVariant;
23
+ }): react_jsx_runtime.JSX.Element;
24
+
25
+ export { AppModalChromeFooter, AppModalChromeHeader, AppModalDialogBody, AppModalDialogContent, AppModalDialogFooter, AppModalDialogHeader, appModalDialogContentChrome };
@@ -0,0 +1,145 @@
1
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
2
+ import { XIcon } from 'lucide-react';
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+ function DialogPortal({ ...props }) {
11
+ return /* @__PURE__ */ jsx(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
12
+ }
13
+ function DialogOverlay({
14
+ className,
15
+ ...props
16
+ }) {
17
+ return /* @__PURE__ */ jsx(
18
+ DialogPrimitive.Overlay,
19
+ {
20
+ "data-slot": "dialog-overlay",
21
+ className: cn(
22
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
23
+ className
24
+ ),
25
+ ...props
26
+ }
27
+ );
28
+ }
29
+ function DialogContent({
30
+ className,
31
+ children,
32
+ ...props
33
+ }) {
34
+ return /* @__PURE__ */ jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [
35
+ /* @__PURE__ */ jsx(DialogOverlay, {}),
36
+ /* @__PURE__ */ jsxs(
37
+ DialogPrimitive.Content,
38
+ {
39
+ "data-slot": "dialog-content",
40
+ className: cn(
41
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
42
+ className
43
+ ),
44
+ ...props,
45
+ children: [
46
+ children,
47
+ /* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", children: [
48
+ /* @__PURE__ */ jsx(XIcon, {}),
49
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
50
+ ] })
51
+ ]
52
+ }
53
+ )
54
+ ] });
55
+ }
56
+ function DialogHeader({ className, ...props }) {
57
+ return /* @__PURE__ */ jsx(
58
+ "div",
59
+ {
60
+ "data-slot": "dialog-header",
61
+ className: cn("flex flex-col gap-2 text-center sm:text-left", className),
62
+ ...props
63
+ }
64
+ );
65
+ }
66
+ function DialogFooter({ className, ...props }) {
67
+ return /* @__PURE__ */ jsx(
68
+ "div",
69
+ {
70
+ "data-slot": "dialog-footer",
71
+ className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
72
+ ...props
73
+ }
74
+ );
75
+ }
76
+ var appModalDialogContentChrome = "bg-card border-border rounded-xl text-foreground shadow-xl gap-0 p-0 overflow-hidden";
77
+ function AppModalDialogContent({
78
+ className,
79
+ ...props
80
+ }) {
81
+ return /* @__PURE__ */ jsx(DialogContent, { className: cn(appModalDialogContentChrome, className), ...props });
82
+ }
83
+ function AppModalDialogHeader({
84
+ className,
85
+ ...props
86
+ }) {
87
+ return /* @__PURE__ */ jsx(DialogHeader, { className: cn("px-6 pt-6 pb-4", className), ...props });
88
+ }
89
+ function AppModalDialogBody({
90
+ className,
91
+ ...props
92
+ }) {
93
+ return /* @__PURE__ */ jsx("div", { className: cn("px-6 pb-4 flex flex-col gap-4", className), ...props });
94
+ }
95
+ function AppModalDialogFooter({
96
+ className,
97
+ muted = false,
98
+ ...props
99
+ }) {
100
+ return /* @__PURE__ */ jsx(
101
+ DialogFooter,
102
+ {
103
+ className: cn("px-6 py-4 gap-2 sm:gap-2", muted && "bg-muted/80", className),
104
+ ...props
105
+ }
106
+ );
107
+ }
108
+ function AppModalChromeHeader({
109
+ variant = "row",
110
+ className,
111
+ ...props
112
+ }) {
113
+ return /* @__PURE__ */ jsx(
114
+ "div",
115
+ {
116
+ className: cn(
117
+ "shrink-0",
118
+ variant === "stack" ? "flex items-start justify-between gap-4 px-6 py-4" : "flex items-center justify-between px-6 pt-5 pb-4",
119
+ className
120
+ ),
121
+ ...props
122
+ }
123
+ );
124
+ }
125
+ function AppModalChromeFooter({
126
+ variant = "end",
127
+ className,
128
+ ...props
129
+ }) {
130
+ return /* @__PURE__ */ jsx(
131
+ "div",
132
+ {
133
+ className: cn(
134
+ "shrink-0",
135
+ variant === "split" ? "flex items-center justify-between gap-2 px-6 py-4 bg-card" : "flex items-center justify-end gap-2 px-6 py-4",
136
+ className
137
+ ),
138
+ ...props
139
+ }
140
+ );
141
+ }
142
+
143
+ export { AppModalChromeFooter, AppModalChromeHeader, AppModalDialogBody, AppModalDialogContent, AppModalDialogFooter, AppModalDialogHeader, appModalDialogContentChrome };
144
+ //# sourceMappingURL=app-modal.js.map
145
+ //# sourceMappingURL=app-modal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/utils.ts","../src/ui/dialog.tsx","../src/shell/app-modal.tsx"],"names":["jsx"],"mappings":"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","file":"app-modal.js","sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\n\nimport { cn } from \"./utils\";\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content>) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg\",\n className,\n )}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\">\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg leading-none font-semibold\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n","\"use client\";\n\nimport * as React from \"react\";\nimport { DialogContent, DialogFooter, DialogHeader } from \"../ui/dialog\";\nimport { cn } from \"../ui/utils\";\n\n/** Shared shell for `DialogContent` when header/body/footer supply their own padding (`p-0`). */\nexport const appModalDialogContentChrome =\n \"bg-card border-border rounded-xl text-foreground shadow-xl gap-0 p-0 overflow-hidden\";\n\nexport function AppModalDialogContent({\n className,\n ...props\n}: React.ComponentProps<typeof DialogContent>) {\n return <DialogContent className={cn(appModalDialogContentChrome, className)} {...props} />;\n}\n\nexport function AppModalDialogHeader({\n className,\n ...props\n}: React.ComponentProps<typeof DialogHeader>) {\n return <DialogHeader className={cn(\"px-6 pt-6 pb-4\", className)} {...props} />;\n}\n\nexport function AppModalDialogBody({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return <div className={cn(\"px-6 pb-4 flex flex-col gap-4\", className)} {...props} />;\n}\n\nexport function AppModalDialogFooter({\n className,\n muted = false,\n ...props\n}: React.ComponentProps<typeof DialogFooter> & { muted?: boolean }) {\n return (\n <DialogFooter\n className={cn(\"px-6 py-4 gap-2 sm:gap-2\", muted && \"bg-muted/80\", className)}\n {...props}\n />\n );\n}\n\ntype ChromeHeaderVariant = \"row\" | \"stack\";\n\n/** Header region for Radix `Dialog.Content` layouts (custom close in header). */\nexport function AppModalChromeHeader({\n variant = \"row\",\n className,\n ...props\n}: React.ComponentProps<\"div\"> & { variant?: ChromeHeaderVariant }) {\n return (\n <div\n className={cn(\n \"shrink-0\",\n variant === \"stack\"\n ? \"flex items-start justify-between gap-4 px-6 py-4\"\n : \"flex items-center justify-between px-6 pt-5 pb-4\",\n className,\n )}\n {...props}\n />\n );\n}\n\ntype ChromeFooterVariant = \"end\" | \"split\";\n\n/** Footer region for Radix `Dialog.Content` layouts. */\nexport function AppModalChromeFooter({\n variant = \"end\",\n className,\n ...props\n}: React.ComponentProps<\"div\"> & { variant?: ChromeFooterVariant }) {\n return (\n <div\n className={cn(\n \"shrink-0\",\n variant === \"split\"\n ? \"flex items-center justify-between gap-2 px-6 py-4 bg-card\"\n : \"flex items-center justify-end gap-2 px-6 py-4\",\n className,\n )}\n {...props}\n />\n );\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import { ReactNode } from 'react';
4
+
5
+ /**
6
+ * Closes the mobile nav drawer on route change and when crossing the `md` breakpoint upward.
7
+ * Pass `location.pathname` from React Router (or any stable route key).
8
+ */
9
+ declare function useAppShellMobileNav(pathname: string): {
10
+ mobileNavOpen: boolean;
11
+ setMobileNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
12
+ };
13
+ type AppShellMobileNavProps = {
14
+ open: boolean;
15
+ onOpenChange: (open: boolean) => void;
16
+ /** Top block inside the drawer (brand, workspace controls, sign out, etc.). */
17
+ drawerHeader: ReactNode;
18
+ /** Scrollable navigation — usually the same content as `AppWebShell`’s `sidebar`. */
19
+ drawerNav: ReactNode;
20
+ /** Accessible name for the drawer (visually hidden). */
21
+ drawerTitle?: string;
22
+ };
23
+ /**
24
+ * Bottom drawer + FAB used with {@link AppWebShell} on viewports below `md`
25
+ * (sidebar and top bar hidden; main is full-bleed white).
26
+ */
27
+ declare function AppShellMobileNav({ open, onOpenChange, drawerHeader, drawerNav, drawerTitle, }: AppShellMobileNavProps): react_jsx_runtime.JSX.Element;
28
+
29
+ export { AppShellMobileNav, type AppShellMobileNavProps, useAppShellMobileNav };