gw-admin 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 (166) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +569 -0
  4. package/dist/account/admin-logout-button.d.ts +21 -0
  5. package/dist/account/admin-logout-button.d.ts.map +1 -0
  6. package/dist/account/admin-logout-button.js +57 -0
  7. package/dist/account/admin-logout-button.js.map +1 -0
  8. package/dist/account/index.d.ts +2 -0
  9. package/dist/account/index.d.ts.map +1 -0
  10. package/dist/account/index.js +3 -0
  11. package/dist/account/index.js.map +1 -0
  12. package/dist/attachments/admin-attachment-list.d.ts +12 -0
  13. package/dist/attachments/admin-attachment-list.d.ts.map +1 -0
  14. package/dist/attachments/admin-attachment-list.js +11 -0
  15. package/dist/attachments/admin-attachment-list.js.map +1 -0
  16. package/dist/attachments/admin-file-attachments.d.ts +33 -0
  17. package/dist/attachments/admin-file-attachments.d.ts.map +1 -0
  18. package/dist/attachments/admin-file-attachments.js +94 -0
  19. package/dist/attachments/admin-file-attachments.js.map +1 -0
  20. package/dist/attachments/index.d.ts +4 -0
  21. package/dist/attachments/index.d.ts.map +1 -0
  22. package/dist/attachments/index.js +3 -0
  23. package/dist/attachments/index.js.map +1 -0
  24. package/dist/attachments/types.d.ts +12 -0
  25. package/dist/attachments/types.d.ts.map +1 -0
  26. package/dist/attachments/types.js +2 -0
  27. package/dist/attachments/types.js.map +1 -0
  28. package/dist/crud/form/admin-crud-form.d.ts +41 -0
  29. package/dist/crud/form/admin-crud-form.d.ts.map +1 -0
  30. package/dist/crud/form/admin-crud-form.js +56 -0
  31. package/dist/crud/form/admin-crud-form.js.map +1 -0
  32. package/dist/crud/form/form-state.d.ts +12 -0
  33. package/dist/crud/form/form-state.d.ts.map +1 -0
  34. package/dist/crud/form/form-state.js +48 -0
  35. package/dist/crud/form/form-state.js.map +1 -0
  36. package/dist/crud/form/index.d.ts +3 -0
  37. package/dist/crud/form/index.d.ts.map +1 -0
  38. package/dist/crud/form/index.js +3 -0
  39. package/dist/crud/form/index.js.map +1 -0
  40. package/dist/crud/index.d.ts +3 -0
  41. package/dist/crud/index.d.ts.map +1 -0
  42. package/dist/crud/index.js +3 -0
  43. package/dist/crud/index.js.map +1 -0
  44. package/dist/crud/pages/admin-crud-detail-page.d.ts +33 -0
  45. package/dist/crud/pages/admin-crud-detail-page.d.ts.map +1 -0
  46. package/dist/crud/pages/admin-crud-detail-page.js +39 -0
  47. package/dist/crud/pages/admin-crud-detail-page.js.map +1 -0
  48. package/dist/crud/pages/admin-crud-form-pages.d.ts +27 -0
  49. package/dist/crud/pages/admin-crud-form-pages.d.ts.map +1 -0
  50. package/dist/crud/pages/admin-crud-form-pages.js +44 -0
  51. package/dist/crud/pages/admin-crud-form-pages.js.map +1 -0
  52. package/dist/crud/pages/admin-crud-list-page.d.ts +27 -0
  53. package/dist/crud/pages/admin-crud-list-page.d.ts.map +1 -0
  54. package/dist/crud/pages/admin-crud-list-page.js +33 -0
  55. package/dist/crud/pages/admin-crud-list-page.js.map +1 -0
  56. package/dist/crud/pages/admin-delete-form.d.ts +12 -0
  57. package/dist/crud/pages/admin-delete-form.d.ts.map +1 -0
  58. package/dist/crud/pages/admin-delete-form.js +15 -0
  59. package/dist/crud/pages/admin-delete-form.js.map +1 -0
  60. package/dist/crud/pages/format-admin-value.d.ts +5 -0
  61. package/dist/crud/pages/format-admin-value.d.ts.map +1 -0
  62. package/dist/crud/pages/format-admin-value.js +38 -0
  63. package/dist/crud/pages/format-admin-value.js.map +1 -0
  64. package/dist/crud/pages/index.d.ts +6 -0
  65. package/dist/crud/pages/index.d.ts.map +1 -0
  66. package/dist/crud/pages/index.js +6 -0
  67. package/dist/crud/pages/index.js.map +1 -0
  68. package/dist/crud/preset/client.d.ts +2 -0
  69. package/dist/crud/preset/client.d.ts.map +1 -0
  70. package/dist/crud/preset/client.js +3 -0
  71. package/dist/crud/preset/client.js.map +1 -0
  72. package/dist/crud/preset/create-admin-crud-form.d.ts +41 -0
  73. package/dist/crud/preset/create-admin-crud-form.d.ts.map +1 -0
  74. package/dist/crud/preset/create-admin-crud-form.js +68 -0
  75. package/dist/crud/preset/create-admin-crud-form.js.map +1 -0
  76. package/dist/crud/preset/create-admin-crud-pages.d.ts +29 -0
  77. package/dist/crud/preset/create-admin-crud-pages.d.ts.map +1 -0
  78. package/dist/crud/preset/create-admin-crud-pages.js +46 -0
  79. package/dist/crud/preset/create-admin-crud-pages.js.map +1 -0
  80. package/dist/crud/preset/create-admin-crud-route-page.d.ts +18 -0
  81. package/dist/crud/preset/create-admin-crud-route-page.d.ts.map +1 -0
  82. package/dist/crud/preset/create-admin-crud-route-page.js +26 -0
  83. package/dist/crud/preset/create-admin-crud-route-page.js.map +1 -0
  84. package/dist/crud/preset/index.d.ts +3 -0
  85. package/dist/crud/preset/index.d.ts.map +1 -0
  86. package/dist/crud/preset/index.js +3 -0
  87. package/dist/crud/preset/index.js.map +1 -0
  88. package/dist/header/admin-page-header.d.ts +15 -0
  89. package/dist/header/admin-page-header.d.ts.map +1 -0
  90. package/dist/header/admin-page-header.js +10 -0
  91. package/dist/header/admin-page-header.js.map +1 -0
  92. package/dist/header/index.d.ts +2 -0
  93. package/dist/header/index.d.ts.map +1 -0
  94. package/dist/header/index.js +2 -0
  95. package/dist/header/index.js.map +1 -0
  96. package/dist/index.d.ts +7 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +7 -0
  99. package/dist/index.js.map +1 -0
  100. package/dist/internal/class-names.d.ts +5 -0
  101. package/dist/internal/class-names.d.ts.map +1 -0
  102. package/dist/internal/class-names.js +7 -0
  103. package/dist/internal/class-names.js.map +1 -0
  104. package/dist/layout/admin-layout.d.ts +16 -0
  105. package/dist/layout/admin-layout.d.ts.map +1 -0
  106. package/dist/layout/admin-layout.js +17 -0
  107. package/dist/layout/admin-layout.js.map +1 -0
  108. package/dist/layout/admin-shell.d.ts +12 -0
  109. package/dist/layout/admin-shell.d.ts.map +1 -0
  110. package/dist/layout/admin-shell.js +10 -0
  111. package/dist/layout/admin-shell.js.map +1 -0
  112. package/dist/layout/admin-sidebar.d.ts +11 -0
  113. package/dist/layout/admin-sidebar.d.ts.map +1 -0
  114. package/dist/layout/admin-sidebar.js +10 -0
  115. package/dist/layout/admin-sidebar.js.map +1 -0
  116. package/dist/layout/index.d.ts +4 -0
  117. package/dist/layout/index.d.ts.map +1 -0
  118. package/dist/layout/index.js +4 -0
  119. package/dist/layout/index.js.map +1 -0
  120. package/dist/layout/mobile.d.ts +15 -0
  121. package/dist/layout/mobile.d.ts.map +1 -0
  122. package/dist/layout/mobile.js +32 -0
  123. package/dist/layout/mobile.js.map +1 -0
  124. package/dist/navigation/admin-navigation.d.ts +21 -0
  125. package/dist/navigation/admin-navigation.d.ts.map +1 -0
  126. package/dist/navigation/admin-navigation.js +22 -0
  127. package/dist/navigation/admin-navigation.js.map +1 -0
  128. package/dist/navigation/index.d.ts +2 -0
  129. package/dist/navigation/index.d.ts.map +1 -0
  130. package/dist/navigation/index.js +2 -0
  131. package/dist/navigation/index.js.map +1 -0
  132. package/dist/pages/admin-account-page.d.ts +13 -0
  133. package/dist/pages/admin-account-page.d.ts.map +1 -0
  134. package/dist/pages/admin-account-page.js +11 -0
  135. package/dist/pages/admin-account-page.js.map +1 -0
  136. package/dist/pages/admin-dashboard-page.d.ts +15 -0
  137. package/dist/pages/admin-dashboard-page.d.ts.map +1 -0
  138. package/dist/pages/admin-dashboard-page.js +8 -0
  139. package/dist/pages/admin-dashboard-page.js.map +1 -0
  140. package/dist/pages/index.d.ts +3 -0
  141. package/dist/pages/index.d.ts.map +1 -0
  142. package/dist/pages/index.js +3 -0
  143. package/dist/pages/index.js.map +1 -0
  144. package/dist/rich-text/admin-rich-text-editor.d.ts +27 -0
  145. package/dist/rich-text/admin-rich-text-editor.d.ts.map +1 -0
  146. package/dist/rich-text/admin-rich-text-editor.js +102 -0
  147. package/dist/rich-text/admin-rich-text-editor.js.map +1 -0
  148. package/dist/rich-text/admin-rich-text-file.d.ts +24 -0
  149. package/dist/rich-text/admin-rich-text-file.d.ts.map +1 -0
  150. package/dist/rich-text/admin-rich-text-file.js +80 -0
  151. package/dist/rich-text/admin-rich-text-file.js.map +1 -0
  152. package/dist/rich-text/admin-rich-text-view.d.ts +9 -0
  153. package/dist/rich-text/admin-rich-text-view.d.ts.map +1 -0
  154. package/dist/rich-text/admin-rich-text-view.js +9 -0
  155. package/dist/rich-text/admin-rich-text-view.js.map +1 -0
  156. package/dist/rich-text/index.d.ts +5 -0
  157. package/dist/rich-text/index.d.ts.map +1 -0
  158. package/dist/rich-text/index.js +5 -0
  159. package/dist/rich-text/index.js.map +1 -0
  160. package/dist/rich-text/plain-text.d.ts +9 -0
  161. package/dist/rich-text/plain-text.d.ts.map +1 -0
  162. package/dist/rich-text/plain-text.js +24 -0
  163. package/dist/rich-text/plain-text.js.map +1 -0
  164. package/package.json +163 -0
  165. package/rich-text.css +1 -0
  166. package/styles.css +901 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## 0.1.0 - 2026-07-31
6
+
7
+ ### Added
8
+
9
+ - Composable admin shell, sidebar, mobile navigation, page header, and logout
10
+ action.
11
+ - Complete `AdminLayout`, dashboard, and account-page defaults for the common
12
+ path, while preserving the lower-level composition slots.
13
+ - Optional list, detail, create, update, delete, and store-backed CRUD form
14
+ components for `next-headless-crud`.
15
+ - CRUD preset factories that configure fields once, connect attachments and
16
+ rich text by convention, and build a catch-all route from resource/action
17
+ pairs.
18
+ - Application-injected attachment and rich-text upload adapters.
19
+ - Focused package exports so consumers can opt into individual features.
20
+ - Optional default styles, rich-text baseline styles, CSS variables, and
21
+ stable prefixed class names.
22
+ - npm publishing workflow, package verification, and usage documentation.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oh-jinsu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,569 @@
1
+ # gw-admin
2
+
3
+ Composable administration UI for the Next.js App Router and
4
+ `next-headless-crud`.
5
+
6
+ `gw-admin` supplies reusable UI without owning application routes,
7
+ authentication, database access, navigation data, brand assets, file storage,
8
+ or deployment. Every feature is available through a focused package subpath,
9
+ so an application can use the complete default UI, replace one page, or use
10
+ only a few primitives.
11
+
12
+ ## Design goals
13
+
14
+ - Application-owned `app/admin/**` routes
15
+ - Replaceable desktop, mobile, page-header, and page-content slots
16
+ - Separate imports for CRUD pages, CRUD forms, attachments, rich text, and
17
+ account actions
18
+ - Service-owned authentication, Server Actions, repositories, and uploaders
19
+ - Optional default CSS with stable `gw-admin-*` class names and CSS variables
20
+ - Preserved React Server Component and Client Component boundaries
21
+
22
+ ## Requirements
23
+
24
+ - Node.js 20 or newer
25
+ - React 19 or newer
26
+ - Next.js 15 or newer when using the supplied Next.js components
27
+
28
+ Feature entry points have additional optional peers:
29
+
30
+ | Entry point | Additional packages |
31
+ | --- | --- |
32
+ | `gw-admin/crud/pages` | `next-headless-crud` |
33
+ | `gw-admin/crud/form` | `next-headless-crud`, `gw-store`, `react-store-input` |
34
+ | `gw-admin/crud/preset` | Complete CRUD peer set below |
35
+ | `gw-admin/crud/preset/client` | Complete CRUD peer set below |
36
+ | `gw-admin/attachments` | `gw-store`, `gw-react-file-input` |
37
+ | `gw-admin/rich-text` | `gw-store`, `react-store-input`, `gw-rich-text-editor` |
38
+
39
+ ## Install
40
+
41
+ Install the core package:
42
+
43
+ ```bash
44
+ npm install gw-admin
45
+ ```
46
+
47
+ Install only the peers required by the features the application uses. For the
48
+ complete CRUD, attachment, and editor stack:
49
+
50
+ ```bash
51
+ npm install next-headless-crud gw-store react-store-input \
52
+ gw-react-file-input gw-rich-text-editor
53
+ ```
54
+
55
+ The package ships compiled JavaScript and declarations. A consuming Next.js
56
+ application does not need `transpilePackages`.
57
+
58
+ ## Recommended setup
59
+
60
+ The preset API keeps the normal application integration to three small files.
61
+ The application supplies only its brand and navigation, a storage uploader and
62
+ resolver, and its CRUD resources and Server Actions.
63
+
64
+ ### 1. Layout
65
+
66
+ ```tsx
67
+ // app/admin/layout.tsx
68
+ import Image from "next/image";
69
+ import { AdminLayout } from "gw-admin";
70
+ import "gw-admin/styles.css";
71
+ import "gw-admin/rich-text.css";
72
+
73
+ const navigation = [
74
+ { label: "홈", href: "/admin" },
75
+ { label: "새소식", href: "/admin/news" },
76
+ { label: "계정", href: "/admin/account" },
77
+ ] as const;
78
+
79
+ export default async function Layout({ children }: { children: React.ReactNode }) {
80
+ await requireAdmin();
81
+
82
+ return (
83
+ <AdminLayout
84
+ brand={<Image src="/logo.png" alt="서비스" width={100} height={100} />}
85
+ navigation={navigation}
86
+ >
87
+ {children}
88
+ </AdminLayout>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ### 2. One client-side form preset
94
+
95
+ ```tsx
96
+ // admin/admin-form.tsx
97
+ "use client";
98
+
99
+ import { createAdminCrudForm } from "gw-admin/crud/preset/client";
100
+ import { uploadFile } from "@/files/upload-file";
101
+
102
+ export const AdminForm = createAdminCrudForm({
103
+ upload: uploadFile,
104
+ hiddenFields: ["slug", "summary", "publishedAt"],
105
+ hiddenFieldsByPath: {
106
+ "/admin/inquiries": ["status", "repliedAt"],
107
+ },
108
+ createDefaults: { isPrivate: true },
109
+ });
110
+ ```
111
+
112
+ By convention the preset renders `attachmentFileIds` as an attachment field
113
+ and `content` and `replyContent` as rich-text fields. Every name and behavior
114
+ can be changed through the preset options.
115
+
116
+ ### 3. One catch-all CRUD route
117
+
118
+ ```tsx
119
+ // app/admin/[...path]/page.tsx
120
+ import {
121
+ createAdminCrudPages,
122
+ createAdminCrudRoutePage,
123
+ } from "gw-admin/crud/preset";
124
+ import { AdminForm } from "@/admin/admin-form";
125
+ import { resolveFiles } from "@/files/resolve-files";
126
+
127
+ const pages = createAdminCrudPages({
128
+ form: AdminForm,
129
+ resolveFiles,
130
+ });
131
+
132
+ export default createAdminCrudRoutePage({
133
+ pathPrefix: "admin",
134
+ pages,
135
+ routes: [
136
+ {
137
+ resource: newsResource,
138
+ actions: { create: createNews, update: updateNews, delete: deleteNews },
139
+ },
140
+ {
141
+ resource: inquiriesResource,
142
+ actions: {
143
+ create: createInquiry,
144
+ update: updateInquiry,
145
+ delete: deleteInquiry,
146
+ },
147
+ },
148
+ ],
149
+ });
150
+ ```
151
+
152
+ `uploadFile` and `resolveFiles` use the small storage-neutral types exported by
153
+ `gw-admin/attachments`; the package does not require a particular API, CDN, or
154
+ database.
155
+
156
+ The preset is optional. Low-level entry points remain public for applications
157
+ that need only one component or want to own the page composition.
158
+
159
+ To replace one page for one resource, add only that override to the route:
160
+
161
+ ```tsx
162
+ {
163
+ resource: reportsResource,
164
+ actions: reportActions,
165
+ pages: { detail: ReportsDetailPage },
166
+ }
167
+ ```
168
+
169
+ To opt out at the field level, use `renderField` in `createAdminCrudForm` or
170
+ `renderDetailField` in `createAdminCrudPages`. Returning `undefined` delegates
171
+ back to the preset.
172
+
173
+ ## Entry points
174
+
175
+ | Import | Purpose |
176
+ | --- | --- |
177
+ | `gw-admin` | Server-safe shell, sidebar, navigation, and page header |
178
+ | `gw-admin/layout` | Shell and desktop sidebar |
179
+ | `gw-admin/layout/mobile` | Optional client-side mobile header and drawer |
180
+ | `gw-admin/header` | Breadcrumb page header |
181
+ | `gw-admin/navigation` | Application-owned navigation rendering |
182
+ | `gw-admin/account` | Configurable logout button |
183
+ | `gw-admin/pages` | Dashboard and account pages |
184
+ | `gw-admin/crud/pages` | List, detail, create, update, and delete UI |
185
+ | `gw-admin/crud/form` | Store-backed standard CRUD form |
186
+ | `gw-admin/crud/preset` | Shared CRUD pages and catch-all route factory |
187
+ | `gw-admin/crud/preset/client` | Client-side form preset factory |
188
+ | `gw-admin/attachments` | Attachment list and uploader field |
189
+ | `gw-admin/rich-text` | Store-backed editor, view, nodes, and upload adapter |
190
+ | `gw-admin/styles.css` | Optional admin UI theme |
191
+ | `gw-admin/rich-text.css` | Optional rich-editor behavioral baseline |
192
+
193
+ Import focused subpaths in shared libraries and large applications. This keeps
194
+ optional feature dependencies outside bundles that do not use them.
195
+
196
+ ## Application-owned layout
197
+
198
+ The application chooses its own brand and navigation.
199
+
200
+ ```tsx
201
+ // app/admin/layout.tsx
202
+ import Image from "next/image";
203
+ import {
204
+ AdminNavigation,
205
+ AdminShell,
206
+ AdminSidebar,
207
+ type AdminNavigationItem,
208
+ } from "gw-admin";
209
+ import { AdminMobileHeader } from "gw-admin/layout/mobile";
210
+ import "gw-admin/styles.css";
211
+
212
+ const navigation: readonly AdminNavigationItem[] = [
213
+ { label: "홈", href: "/admin" },
214
+ { label: "새소식", href: "/admin/news" },
215
+ { label: "계정", href: "/admin/account" },
216
+ ];
217
+
218
+ const brand = <Image src="/logo.png" alt="서비스 이름" width={100} height={100} />;
219
+
220
+ export default async function AdminLayout({
221
+ children,
222
+ }: {
223
+ children: React.ReactNode;
224
+ }) {
225
+ const session = await requireAdmin();
226
+
227
+ return (
228
+ <AdminShell
229
+ sidebar={
230
+ <AdminSidebar brand={brand}>
231
+ <AdminNavigation items={navigation} />
232
+ </AdminSidebar>
233
+ }
234
+ mobileHeader={
235
+ <AdminMobileHeader brand={brand} navigation={navigation} />
236
+ }
237
+ >
238
+ {children}
239
+ </AdminShell>
240
+ );
241
+ }
242
+ ```
243
+
244
+ `AdminShell` does not create either navigation surface by itself:
245
+
246
+ - Pass no `sidebar` to omit the desktop sidebar.
247
+ - Pass no `mobileHeader` to omit the mobile header.
248
+ - Pass an application component into either slot to replace the default.
249
+ - Skip `AdminShell` entirely and import only `AdminPageHeader` or a CRUD page.
250
+
251
+ Authentication must remain in the application layout or page. Rendering an
252
+ admin component is not an authorization check.
253
+
254
+ ## Logout
255
+
256
+ The default HTTP flow is configurable and performs a full navigation after
257
+ success so cached authenticated UI is discarded:
258
+
259
+ ```tsx
260
+ import { AdminLogoutButton } from "gw-admin/account";
261
+
262
+ <AdminLogoutButton
263
+ endpoint="/api/auth/logout"
264
+ redirectTo="/login"
265
+ />;
266
+ ```
267
+
268
+ For a non-HTTP client flow, wrap the button in an application Client Component
269
+ and supply `request`:
270
+
271
+ ```tsx
272
+ "use client";
273
+
274
+ import { AdminLogoutButton } from "gw-admin/account";
275
+ import { authClient } from "@/auth/client";
276
+
277
+ export function ServiceLogoutButton() {
278
+ return (
279
+ <AdminLogoutButton
280
+ request={() => authClient.logout()}
281
+ redirectTo="/sign-in"
282
+ />
283
+ );
284
+ }
285
+ ```
286
+
287
+ ## CRUD pages
288
+
289
+ The package builds on `next-headless-crud`; resources and Server Actions still
290
+ belong to the application.
291
+
292
+ ```tsx
293
+ // app/admin/[...path]/page.tsx
294
+ import { createCrudRoutePage, defineCrudRoute } from "next-headless-crud/next";
295
+ import {
296
+ AdminCrudCreatePage,
297
+ AdminCrudDetailPage,
298
+ AdminCrudListPage,
299
+ AdminCrudUpdatePage,
300
+ } from "gw-admin/crud/pages";
301
+ import { newsResource } from "@/admin/news/resource";
302
+ import {
303
+ createNews,
304
+ deleteNews,
305
+ updateNews,
306
+ } from "@/admin/news/actions";
307
+
308
+ const newsRoute = defineCrudRoute({
309
+ resource: newsResource,
310
+ actions: {
311
+ create: createNews,
312
+ update: updateNews,
313
+ delete: deleteNews,
314
+ },
315
+ pages: {
316
+ list: AdminCrudListPage,
317
+ create: AdminCrudCreatePage,
318
+ detail: AdminCrudDetailPage,
319
+ update: AdminCrudUpdatePage,
320
+ },
321
+ });
322
+
323
+ export default createCrudRoutePage([newsRoute]);
324
+ ```
325
+
326
+ ### Per-page opt-out
327
+
328
+ Next.js gives an explicit page precedence over a catch-all route. A custom page
329
+ can therefore replace one package page without changing the remaining CRUD
330
+ routes:
331
+
332
+ ```text
333
+ app/admin/[...path]/page.tsx -> shared CRUD pages
334
+ app/admin/account/page.tsx -> application-owned page
335
+ app/admin/reports/page.tsx -> application-owned page
336
+ ```
337
+
338
+ Routes can also mix page implementations in one resource:
339
+
340
+ ```tsx
341
+ pages: {
342
+ list: AdminCrudListPage,
343
+ create: ServiceCreatePage,
344
+ detail: AdminCrudDetailPage,
345
+ update: ServiceUpdatePage,
346
+ }
347
+ ```
348
+
349
+ ### Detail field opt-out
350
+
351
+ Hide fields or replace rendering one field at a time:
352
+
353
+ ```tsx
354
+ import {
355
+ AdminCrudDetailPage,
356
+ type AdminCrudDetailPageProps,
357
+ } from "gw-admin/crud/pages";
358
+ import { AdminRichTextView } from "gw-admin/rich-text";
359
+
360
+ export function NewsDetailPage(props: AdminCrudDetailPageProps) {
361
+ return (
362
+ <AdminCrudDetailPage
363
+ {...props}
364
+ hiddenFields={["description"]}
365
+ longTextFields={["content"]}
366
+ renderField={({ field, value }) => {
367
+ if (field.name === "content" && typeof value === "string") {
368
+ return <AdminRichTextView value={value} />;
369
+ }
370
+
371
+ return undefined;
372
+ }}
373
+ />
374
+ );
375
+ }
376
+ ```
377
+
378
+ `renderActions` can replace every header action, while `formatValue` can
379
+ replace scalar formatting across the page.
380
+
381
+ ## Store-backed form
382
+
383
+ `AdminCrudForm` handles standard input, select, checkbox, and textarea fields.
384
+ It deliberately does not guess that a field name represents an attachment,
385
+ editor, derived value, or server-managed value.
386
+
387
+ ```tsx
388
+ "use client";
389
+
390
+ import {
391
+ AdminCrudForm,
392
+ type AdminCrudFormProps,
393
+ } from "gw-admin/crud/form";
394
+
395
+ export function ServiceAdminForm(props: AdminCrudFormProps) {
396
+ return (
397
+ <AdminCrudForm
398
+ {...props}
399
+ fieldModes={{
400
+ description: "textarea",
401
+ slug: "hidden",
402
+ publishedAt: "hidden",
403
+ }}
404
+ />
405
+ );
406
+ }
407
+ ```
408
+
409
+ The save action is a `type="button"` action. Pressing Enter in a large input
410
+ screen does not implicitly submit the form.
411
+
412
+ ### Composing attachment and editor fields
413
+
414
+ Feature adapters are created in the consuming application, not in the package.
415
+ This example is a Client Component, so uploader functions never cross the
416
+ Server Component serialization boundary:
417
+
418
+ ```tsx
419
+ "use client";
420
+
421
+ import {
422
+ AdminCrudForm,
423
+ type AdminCrudFormProps,
424
+ } from "gw-admin/crud/form";
425
+ import {
426
+ AdminFileAttachments,
427
+ type AdminFileUploader,
428
+ type AdminUploadedFile,
429
+ } from "gw-admin/attachments";
430
+ import {
431
+ AdminRichTextEditor,
432
+ adminRichTextNodes,
433
+ createAdminRichTextUploadAdapter,
434
+ } from "gw-admin/rich-text";
435
+ import "gw-admin/rich-text.css";
436
+
437
+ const uploadFile: AdminFileUploader = async (file, context) => {
438
+ const uploaded = await serviceFileUploader(file, context);
439
+
440
+ return {
441
+ id: uploaded.id,
442
+ name: uploaded.name,
443
+ type: uploaded.type,
444
+ size: uploaded.size,
445
+ key: uploaded.key,
446
+ url: cdn(uploaded.key),
447
+ metadata: uploaded.metadata,
448
+ };
449
+ };
450
+
451
+ const editorUpload = createAdminRichTextUploadAdapter(uploadFile);
452
+
453
+ export function ServiceAdminForm(
454
+ props: AdminCrudFormProps & {
455
+ attachments: readonly AdminUploadedFile[];
456
+ },
457
+ ) {
458
+ return (
459
+ <AdminCrudForm
460
+ {...props}
461
+ fieldModes={{ slug: "hidden" }}
462
+ renderField={({ field, store, error, errorId, setUploading }) => {
463
+ if (field.name === "attachmentFileIds") {
464
+ return (
465
+ <AdminFileAttachments
466
+ store={store}
467
+ name={field.name}
468
+ uploader={uploadFile}
469
+ initialFiles={props.attachments}
470
+ error={error}
471
+ onUploadingChange={setUploading}
472
+ />
473
+ );
474
+ }
475
+
476
+ if (field.name === "content") {
477
+ return (
478
+ <AdminRichTextEditor
479
+ store={store}
480
+ name={field.name}
481
+ upload={editorUpload}
482
+ nodes={adminRichTextNodes}
483
+ invalid={Boolean(error)}
484
+ describedBy={error ? errorId : undefined}
485
+ />
486
+ );
487
+ }
488
+
489
+ return undefined;
490
+ }}
491
+ />
492
+ );
493
+ }
494
+ ```
495
+
496
+ An application that does not need uploads simply does not install or import the
497
+ attachment entry point. Omitting `upload` from `AdminRichTextEditor` also
498
+ removes the attachment toolbar action.
499
+
500
+ ## Styling
501
+
502
+ Default theme:
503
+
504
+ ```tsx
505
+ import "gw-admin/styles.css";
506
+ ```
507
+
508
+ Editor behavioral baseline, only when the rich-text entry point is used:
509
+
510
+ ```tsx
511
+ import "gw-admin/rich-text.css";
512
+ ```
513
+
514
+ The package never imports these files automatically. To own every style, omit
515
+ both imports and target the stable `gw-admin-*` class names yourself.
516
+
517
+ The default theme can also be adjusted without replacing selectors:
518
+
519
+ ```css
520
+ :root {
521
+ --gw-admin-background: #fafafa;
522
+ --gw-admin-surface: #ffffff;
523
+ --gw-admin-text: #111111;
524
+ --gw-admin-primary: #0b4d3b;
525
+ --gw-admin-primary-text: #ffffff;
526
+ --gw-admin-danger: #a61b1b;
527
+ --gw-admin-border: #d8d8d8;
528
+ --gw-admin-sidebar-width: 17rem;
529
+ --gw-admin-content-width: 90rem;
530
+ --gw-admin-radius: 0;
531
+ }
532
+ ```
533
+
534
+ ## Server and client boundaries
535
+
536
+ - Layout shell, sidebar, navigation, header, list page, detail page, and page
537
+ loaders are server-compatible.
538
+ - Mobile navigation, logout, delete, CRUD form, attachment input, and editor
539
+ are Client Components.
540
+ - Application upload functions must be declared in an application Client
541
+ Component before they are passed to attachment or editor components.
542
+ - Authentication, authorization, resource scoping, and Server Actions must
543
+ validate permissions independently of the rendered UI.
544
+
545
+ ## Publishing and updates
546
+
547
+ The repository includes a manual-compatible npm publishing workflow. Configure
548
+ `NPM_PUBLISH_TOKEN` in the GitHub repository, bump the package version, and
549
+ push the package metadata to `main`.
550
+
551
+ ```bash
552
+ npm version patch
553
+ git push --follow-tags
554
+ ```
555
+
556
+ Before publishing:
557
+
558
+ ```bash
559
+ npm run check
560
+ npm run pack:check
561
+ ```
562
+
563
+ Consuming services update manually:
564
+
565
+ ```bash
566
+ npm install gw-admin@0.1.1
567
+ ```
568
+
569
+ No automatic pull request or consumer deployment is performed.
@@ -0,0 +1,21 @@
1
+ export type AdminLogoutRequest = () => Promise<Response | void>;
2
+ export type AdminLogoutButtonLabels = {
3
+ idle: string;
4
+ pending: string;
5
+ confirm: string;
6
+ error: string;
7
+ };
8
+ export type AdminLogoutButtonProps = {
9
+ endpoint?: string;
10
+ redirectTo?: string;
11
+ request?: AdminLogoutRequest;
12
+ confirm?: boolean;
13
+ labels?: Partial<AdminLogoutButtonLabels>;
14
+ className?: string;
15
+ };
16
+ /**
17
+ * Ends an admin session through either a conventional HTTP endpoint or a
18
+ * consumer-supplied client request.
19
+ */
20
+ export declare function AdminLogoutButton({ endpoint, redirectTo, request, confirm, labels: labelOverrides, className, }: AdminLogoutButtonProps): import("react").JSX.Element;
21
+ //# sourceMappingURL=admin-logout-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-logout-button.d.ts","sourceRoot":"","sources":["../../src/account/admin-logout-button.tsx"],"names":[],"mappings":"AAKA,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAEhE,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAoBF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAA6B,EAC7B,UAAqB,EACrB,OAAO,EACP,OAAc,EACd,MAAM,EAAE,cAAc,EACtB,SAAS,GACV,EAAE,sBAAsB,+BAmDxB"}
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { classNames } from "../internal/class-names.js";
5
+ const defaultLabels = {
6
+ idle: "로그아웃",
7
+ pending: "로그아웃 중...",
8
+ confirm: "로그아웃하시겠습니까?",
9
+ error: "로그아웃하지 못했습니다. 잠시 후 다시 시도해 주세요.",
10
+ };
11
+ async function getResponseError(response, fallback) {
12
+ try {
13
+ const payload = (await response.json());
14
+ return typeof payload.message === "string" && payload.message
15
+ ? payload.message
16
+ : fallback;
17
+ }
18
+ catch {
19
+ return fallback;
20
+ }
21
+ }
22
+ /**
23
+ * Ends an admin session through either a conventional HTTP endpoint or a
24
+ * consumer-supplied client request.
25
+ */
26
+ export function AdminLogoutButton({ endpoint = "/api/auth/logout", redirectTo = "/login", request, confirm = true, labels: labelOverrides, className, }) {
27
+ const labels = { ...defaultLabels, ...labelOverrides };
28
+ const [pending, setPending] = useState(false);
29
+ const [errorMessage, setErrorMessage] = useState("");
30
+ const handleLogout = async () => {
31
+ if (confirm && !window.confirm(labels.confirm)) {
32
+ return;
33
+ }
34
+ setPending(true);
35
+ setErrorMessage("");
36
+ try {
37
+ const response = request
38
+ ? await request()
39
+ : await fetch(endpoint, {
40
+ method: "POST",
41
+ credentials: "same-origin",
42
+ });
43
+ if (response instanceof Response && !response.ok) {
44
+ setErrorMessage(await getResponseError(response, labels.error));
45
+ setPending(false);
46
+ return;
47
+ }
48
+ window.location.replace(redirectTo);
49
+ }
50
+ catch (error) {
51
+ setErrorMessage(error instanceof Error && error.message ? error.message : labels.error);
52
+ setPending(false);
53
+ }
54
+ };
55
+ return (_jsxs("div", { className: "gw-admin-logout", children: [_jsx("button", { type: "button", disabled: pending, onClick: handleLogout, className: classNames("gw-admin-button gw-admin-button--primary", className), children: pending ? labels.pending : labels.idle }), errorMessage && (_jsx("p", { role: "alert", className: "gw-admin-field-error", children: errorMessage }))] }));
56
+ }
57
+ //# sourceMappingURL=admin-logout-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-logout-button.js","sourceRoot":"","sources":["../../src/account/admin-logout-button.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { AdminLogoutButton, type AdminLogoutButtonLabels, type AdminLogoutButtonProps, type AdminLogoutRequest, } from "./admin-logout-button.js";
2
+ //# sourceMappingURL=index.d.ts.map