@kahitsan/ksui 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahitsan/ksui",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -63,8 +63,12 @@ export interface ResourcePageHost {
63
63
  requestInit?: () => RequestInit;
64
64
  /** Reactive value; when it changes the list resets to page 1 and refetches. */
65
65
  refetchKey?: () => unknown;
66
- /** Extra action elements rendered in the header before the built-in create button. */
67
- headerActions?: JSX.Element;
66
+ /**
67
+ * Render extra header actions before the built-in create button. A function
68
+ * (not a pre-created element) so it runs inside this component's render scope —
69
+ * a pre-created element would re-run outside it if the host object is rebuilt.
70
+ */
71
+ headerActions?: () => JSX.Element;
68
72
  }
69
73
 
70
74
  export interface ResourcePageProps<T extends ResourceRow> {
@@ -76,20 +80,32 @@ export interface ResourcePageProps<T extends ResourceRow> {
76
80
 
77
81
  type RefetchApi = { refetch: () => void; resetAndRefetch: () => void };
78
82
 
79
- export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>) {
83
+ export function ResourcePage<T extends ResourceRow>(
84
+ props: ResourcePageProps<T>
85
+ ) {
80
86
  const spec = props.spec;
81
87
  const ep = endpoints(spec);
82
- const doFetch = props.fetchImpl ?? ((...a: Parameters<typeof fetch>) => fetch(...a));
88
+ const doFetch =
89
+ props.fetchImpl ?? ((...a: Parameters<typeof fetch>) => fetch(...a));
83
90
 
84
- const { PageShell } = props.host;
85
- const can = (key: string) => props.host.can?.(key) ?? true;
91
+ // Read the host config ONCE so the component is robust against a consumer
92
+ // passing an inline `host={{...}}` literal (which Solid re-evaluates on each
93
+ // `props.host.*` access — re-instantiating headerActions outside render scope).
94
+ const {
95
+ PageShell,
96
+ can: hostCan,
97
+ requestInit: hostRequestInit,
98
+ refetchKey,
99
+ headerActions,
100
+ } = props.host;
101
+ const can = (key: string) => hostCan?.(key) ?? true;
86
102
  const canView = () => can(spec.permissions.view);
87
103
  const canEdit = () => spec.permissions.edit.some(can);
88
104
  const canDelete = () => can(spec.permissions.delete);
89
105
 
90
106
  /** Merge the host's per-request init (headers/credentials) with method + body. */
91
107
  function reqInit(extra?: RequestInit): RequestInit {
92
- const base = props.host.requestInit?.() ?? {};
108
+ const base = hostRequestInit?.() ?? {};
93
109
  return {
94
110
  ...base,
95
111
  ...extra,
@@ -100,14 +116,18 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
100
116
  };
101
117
  }
102
118
 
103
- const [filterState, setFilterState] = createStore<Record<string, string>>(initialFilterState(spec));
119
+ const [filterState, setFilterState] = createStore<Record<string, string>>(
120
+ initialFilterState(spec)
121
+ );
104
122
  let refetchFn: RefetchApi | undefined;
105
123
 
106
124
  const [detailRow, setDetailRow] = createSignal<ResourceRow | null>(null);
107
125
  const [editing, setEditing] = createSignal(false);
108
126
  const [createOpen, setCreateOpen] = createSignal(false);
109
127
 
110
- const [form, setForm] = createStore<Record<string, string>>(emptyFormValues(spec));
128
+ const [form, setForm] = createStore<Record<string, string>>(
129
+ emptyFormValues(spec)
130
+ );
111
131
  const [saving, setSaving] = createSignal(false);
112
132
  const [error, setError] = createSignal("");
113
133
  const setValue = (key: string, value: string) => setForm(key, value);
@@ -140,7 +160,12 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
140
160
  setEditing(true);
141
161
  }
142
162
 
143
- async function submit(method: "POST" | "PUT", url: string, fallback: string, onOk: (row: ResourceRow) => void) {
163
+ async function submit(
164
+ method: "POST" | "PUT",
165
+ url: string,
166
+ fallback: string,
167
+ onOk: (row: ResourceRow) => void
168
+ ) {
144
169
  const msg = validateForm(spec, form);
145
170
  if (msg) {
146
171
  setError(msg);
@@ -155,7 +180,7 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
155
180
  method,
156
181
  headers: { "Content-Type": "application/json" },
157
182
  body: JSON.stringify(formToBody(spec, form)),
158
- }),
183
+ })
159
184
  );
160
185
  // create allows the idempotent-200 path; both treat non-ok as an error
161
186
  if (!res.ok && !(method === "POST" && res.status === 200)) {
@@ -182,10 +207,15 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
182
207
  async function handleUpdate() {
183
208
  const row = detailRow();
184
209
  if (!row) return;
185
- await submit("PUT", ep.one(row.id), spec.labels.updateErrorFallback, (updated) => {
186
- if (updated && typeof updated.id === "number") setDetailRow(updated);
187
- setEditing(false);
188
- });
210
+ await submit(
211
+ "PUT",
212
+ ep.one(row.id),
213
+ spec.labels.updateErrorFallback,
214
+ (updated) => {
215
+ if (updated && typeof updated.id === "number") setDetailRow(updated);
216
+ setEditing(false);
217
+ }
218
+ );
189
219
  }
190
220
 
191
221
  async function handleArchive(id: number) {
@@ -233,7 +263,7 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
233
263
  subtitle={spec.subtitle}
234
264
  actions={
235
265
  <>
236
- {props.host.headerActions}
266
+ {headerActions?.()}
237
267
  <Show when={canEdit()}>
238
268
  <Button
239
269
  intent="primary"
@@ -252,8 +282,10 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
252
282
  }
253
283
  >
254
284
  <DataTable<ResourceRow>
255
- refetchKey={props.host.refetchKey}
256
- fetchFn={async (params: FetchParams): Promise<FetchResult<ResourceRow>> => {
285
+ refetchKey={refetchKey}
286
+ fetchFn={async (
287
+ params: FetchParams
288
+ ): Promise<FetchResult<ResourceRow>> => {
257
289
  const q = buildListQuery(spec, params, filterState);
258
290
  const res = await doFetch(`${ep.list}?${q}`, reqInit());
259
291
  return res.json();
@@ -279,10 +311,14 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
279
311
  ) : (
280
312
  <select
281
313
  value={filterState[f.param]}
282
- onChange={(e) => setFilterState(f.param, e.currentTarget.value)}
314
+ onChange={(e) =>
315
+ setFilterState(f.param, e.currentTarget.value)
316
+ }
283
317
  class="rounded-lg border border-zinc-800/50 bg-zinc-900 px-3 py-1.5 text-xs text-zinc-400 cursor-pointer"
284
318
  >
285
- <For each={f.options}>{(o) => <option value={o.value}>{o.label}</option>}</For>
319
+ <For each={f.options}>
320
+ {(o) => <option value={o.value}>{o.label}</option>}
321
+ </For>
286
322
  </select>
287
323
  )
288
324
  }
@@ -306,7 +342,9 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
306
342
  >
307
343
  <div data-testid={`${spec.testIdPrefix}-create-modal`}>
308
344
  <div class="flex items-center justify-between mb-6">
309
- <h2 class="text-lg font-semibold text-zinc-100">{spec.labels.createTitle}</h2>
345
+ <h2 class="text-lg font-semibold text-zinc-100">
346
+ {spec.labels.createTitle}
347
+ </h2>
310
348
  <button
311
349
  onClick={() => {
312
350
  setCreateOpen(false);
@@ -348,7 +386,9 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
348
386
  <div data-testid={`${spec.testIdPrefix}-detail-modal`}>
349
387
  <div class="flex items-center justify-between mb-6">
350
388
  <h2 class="text-lg font-semibold text-zinc-100">
351
- {editing() ? spec.labels.editTitle : String(row()[spec.labels.titleField] ?? "")}
389
+ {editing()
390
+ ? spec.labels.editTitle
391
+ : String(row()[spec.labels.titleField] ?? "")}
352
392
  </h2>
353
393
  <div class="flex items-center gap-2">
354
394
  <Show when={!editing() && canEdit()}>
@@ -395,7 +435,10 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
395
435
  </div>
396
436
  </div>
397
437
 
398
- <Show when={editing()} fallback={<ResourceDetail rows={spec.detail} row={row()} />}>
438
+ <Show
439
+ when={editing()}
440
+ fallback={<ResourceDetail rows={spec.detail} row={row()} />}
441
+ >
399
442
  <ResourceForm
400
443
  spec={spec}
401
444
  values={form}