@kahitsan/ksui 0.26.0 → 0.27.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahitsan/ksui",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
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",
package/src/index.ts CHANGED
@@ -194,6 +194,9 @@ export { buildLogoSrc } from "./utils/account-logo-url";
194
194
 
195
195
  export { attachmentUrl, isResolvableAttachment } from "./utils/attachments";
196
196
 
197
+ export { createObjectUrlResource } from "./utils/object-url-resource";
198
+ export type { ObjectUrlOptions } from "./utils/object-url-resource";
199
+
197
200
  export { useAccountsIndex, resolveAccount, resolveAccountName } from "./utils/accounts-index";
198
201
 
199
202
  export { INPUT_CLASS } from "./utils/INPUT_CLASS";
@@ -0,0 +1,53 @@
1
+ import { createResource, createEffect, onCleanup, type Resource } from "solid-js";
2
+
3
+ export interface ObjectUrlOptions {
4
+ /** Extra fetch init, merged after `credentials: "include"` (e.g. tenant headers). */
5
+ init?: RequestInit;
6
+ }
7
+
8
+ /**
9
+ * Fetch a (typically authed, same-origin) resource and expose it as an object
10
+ * URL (`blob:`) for an `<img src>` / `<a href>`. This is the proxy/blob pattern
11
+ * for a privately-stored asset: the bytes come back through an app route that
12
+ * enforces auth/ownership, never a public or signed third-party URL — so the
13
+ * rendered src is a clean same-origin `blob:`, the storage origin is never
14
+ * exposed, and there is no leakable bearer link.
15
+ *
16
+ * The href accessor is the resource source: when it changes, the new blob is
17
+ * fetched and the previous object URL is revoked; the final one is revoked on
18
+ * cleanup (a created object URL leaks until revoked). `url()` is null while
19
+ * loading or on any failure — the consumer gates its own render — and
20
+ * `url.loading` distinguishes the two.
21
+ *
22
+ * Domain-free: the consumer supplies the href and any init (headers/credentials);
23
+ * this primitive assumes nothing about auth, tenancy, or endpoints.
24
+ */
25
+ export function createObjectUrlResource(
26
+ href: () => string | null | undefined,
27
+ options: ObjectUrlOptions = {},
28
+ ): Resource<string | null> {
29
+ const [url] = createResource(
30
+ () => href() || null,
31
+ async (src) => {
32
+ const res = await fetch(src, { credentials: "include", ...options.init });
33
+ if (!res.ok) return null;
34
+ const blob = await res.blob();
35
+ return URL.createObjectURL(blob);
36
+ },
37
+ );
38
+
39
+ // Revoke the previous object URL when the resolved value changes, and the
40
+ // final one on unmount. During a refetch the resource holds its prior value
41
+ // (cur === prev), so an in-flight reload doesn't revoke a URL still on screen.
42
+ let prev: string | null = null;
43
+ createEffect(() => {
44
+ const cur = url() ?? null;
45
+ if (prev && prev !== cur) URL.revokeObjectURL(prev);
46
+ prev = cur;
47
+ });
48
+ onCleanup(() => {
49
+ if (prev) URL.revokeObjectURL(prev);
50
+ });
51
+
52
+ return url;
53
+ }