@kahitsan/ksui 0.3.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/src/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ // @kahitsan/ksui — shared SolidJS UI components for KahitSan/Hilinga plugins.
2
+ //
3
+ // These were copied byte-for-byte across plugins (counter, transactions, ...);
4
+ // this package is the single canonical copy, PUBLISHED to GitHub Packages and
5
+ // consumed as a normal dependency from `node_modules`: a plugin `npm install`s
6
+ // the published version.
7
+ // The package ships its source under a `solid` export condition (see
8
+ // package.json), so the consumer's vite-plugin-solid compiles these components
9
+ // while solid-js and `@kserp/host-ui` stay EXTERNALIZED to the host runtime
10
+ // globals — the plugin IIFE bundles them identically to a local copy: no runtime
11
+ // change, single Solid instance, host UI kit reused. The `@kserp/host-ui` ambient
12
+ // type contract ships alongside (host-ui.d.ts, the `./host-ui` export).
13
+
14
+ export { default as MentionTextarea } from "./components/MentionTextarea";
15
+ export type { MentionTextareaProps } from "./components/MentionTextarea";
16
+
17
+ export { default as MarkdownNotes } from "./components/MarkdownNotes";
18
+ export type { MarkdownNotesProps } from "./components/MarkdownNotes";
19
+
20
+ export { default as ClientPicker } from "./components/ClientPicker";
21
+ export type { ClientOption } from "./components/ClientPicker";
22
+
23
+ export { default as VoucherPicker, calculateDiscount } from "./components/VoucherPicker";
24
+ export type { VoucherOption } from "./components/VoucherPicker";
25
+
26
+ export { default as CameraCapture } from "./components/CameraCapture";
27
+
28
+ export { default as AddAttachmentTile } from "./components/AddAttachmentTile";
29
+
30
+ export { default as PaymentAccountPicker } from "./components/PaymentAccountPicker";
31
+ export type { PaymentAccountOption } from "./components/PaymentAccountPicker";
32
+
33
+ // The canonical account + attachment widget set: AccountAvatar (logo / type-icon
34
+ // glyph / initial-on-color fallback, with its initials + color + SVG helpers)
35
+ // plus the account icon / logo / tone / accounts-index helpers and the
36
+ // ExistingAttachmentTile that completes the attachment trio next to
37
+ // AddAttachmentTile + CameraCapture. Plugins import these instead of keeping
38
+ // their own local copies.
39
+ export {
40
+ default as AccountAvatar,
41
+ getInitials,
42
+ getAvatarColor,
43
+ buildInitialsSvg,
44
+ } from "./components/AccountAvatar";
45
+ export type { AvatarAccount } from "./components/AccountAvatar";
46
+
47
+ export {
48
+ getAccountIcon,
49
+ getAccountTone,
50
+ ACCOUNT_ICON_SLUGS,
51
+ ACCOUNT_ICON_LABELS,
52
+ } from "./lib/account-icons";
53
+ export type { IconComponent, AccountIconSlug } from "./lib/account-icons";
54
+
55
+ export { buildLogoSrc } from "./lib/account-logo-url";
56
+
57
+ export { attachmentUrl, isResolvableAttachment } from "./lib/attachments";
58
+
59
+ export { default as ExistingAttachmentTile } from "./components/ExistingAttachmentTile";
60
+ export type { ExistingAttachment } from "./components/ExistingAttachmentTile";
61
+
62
+ export { useAccountsIndex, resolveAccount, resolveAccountName } from "./lib/accounts-index";
@@ -0,0 +1,106 @@
1
+ // Source: KahitSan/kserp src/lib/account-icons.ts (vendored into the plugin remote).
2
+ // Maps an account-icon slug → lucide glyph, with a type-based fallback for
3
+ // accounts that have no custom icon yet. Used by AccountAvatar.
4
+
5
+ import Banknote from "lucide-solid/icons/banknote";
6
+ import Landmark from "lucide-solid/icons/landmark";
7
+ import Building from "lucide-solid/icons/building";
8
+ import CreditCard from "lucide-solid/icons/credit-card";
9
+ import Smartphone from "lucide-solid/icons/smartphone";
10
+ import Wallet from "lucide-solid/icons/wallet";
11
+ import Coins from "lucide-solid/icons/coins";
12
+ import PiggyBank from "lucide-solid/icons/piggy-bank";
13
+ import DollarSign from "lucide-solid/icons/dollar-sign";
14
+ import Receipt from "lucide-solid/icons/receipt";
15
+ import type { Component, JSX } from "solid-js";
16
+
17
+ export type IconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement> & { size?: number }>;
18
+
19
+ export const ACCOUNT_ICON_SLUGS = [
20
+ "banknote",
21
+ "landmark",
22
+ "building",
23
+ "credit-card",
24
+ "smartphone",
25
+ "wallet",
26
+ "coins",
27
+ "piggy-bank",
28
+ "dollar-sign",
29
+ "receipt",
30
+ ] as const;
31
+
32
+ export type AccountIconSlug = (typeof ACCOUNT_ICON_SLUGS)[number];
33
+
34
+ const ICON_BY_SLUG: Record<AccountIconSlug, IconComponent> = {
35
+ banknote: Banknote,
36
+ landmark: Landmark,
37
+ building: Building,
38
+ "credit-card": CreditCard,
39
+ smartphone: Smartphone,
40
+ wallet: Wallet,
41
+ coins: Coins,
42
+ "piggy-bank": PiggyBank,
43
+ "dollar-sign": DollarSign,
44
+ receipt: Receipt,
45
+ };
46
+
47
+ const DEFAULT_BY_TYPE: Record<string, IconComponent> = {
48
+ bank: Banknote,
49
+ e_wallet: Smartphone,
50
+ cash: Wallet,
51
+ capital: PiggyBank,
52
+ };
53
+
54
+ // Pretty labels for the icon picker (financial-accounts create/edit modal).
55
+ export const ACCOUNT_ICON_LABELS: Record<AccountIconSlug, string> = {
56
+ banknote: "Banknote",
57
+ landmark: "Landmark",
58
+ building: "Building",
59
+ "credit-card": "Credit card",
60
+ smartphone: "Smartphone",
61
+ wallet: "Wallet",
62
+ coins: "Coins",
63
+ "piggy-bank": "Piggy bank",
64
+ "dollar-sign": "Dollar sign",
65
+ receipt: "Receipt",
66
+ };
67
+
68
+ // Returns the lucide icon to render for an account, preferring the account's
69
+ // own icon slug and falling back to the type-based default.
70
+ export function getAccountIcon(account: { icon?: string | null; type: string }): IconComponent {
71
+ if (account.icon && (ACCOUNT_ICON_SLUGS as readonly string[]).includes(account.icon)) {
72
+ return ICON_BY_SLUG[account.icon as AccountIconSlug];
73
+ }
74
+ return DEFAULT_BY_TYPE[account.type] ?? Banknote;
75
+ }
76
+
77
+ const DEFAULT_TONE_BY_TYPE: Record<string, { text: string; bg: string; border: string }> = {
78
+ bank: { text: "text-blue-400", bg: "bg-blue-500/10", border: "border-blue-400/40" },
79
+ e_wallet: { text: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-400/40" },
80
+ cash: { text: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-400/40" },
81
+ capital: { text: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-400/40" },
82
+ };
83
+
84
+ const FALLBACK_TONE = {
85
+ text: "text-zinc-300",
86
+ bg: "bg-zinc-700/30",
87
+ border: "border-zinc-700/60",
88
+ };
89
+
90
+ // Per-type accent tone for an account chip, or the account's own custom color.
91
+ export function getAccountTone(account: { color?: string | null; type: string }): {
92
+ class?: string;
93
+ style?: JSX.CSSProperties;
94
+ } {
95
+ if (account.color) {
96
+ return {
97
+ style: {
98
+ color: account.color,
99
+ "background-color": `${account.color}1a`,
100
+ "border-color": `${account.color}66`,
101
+ },
102
+ };
103
+ }
104
+ const tone = DEFAULT_TONE_BY_TYPE[account.type] ?? FALLBACK_TONE;
105
+ return { class: `${tone.text} ${tone.bg} ${tone.border}` };
106
+ }
@@ -0,0 +1,12 @@
1
+ // Source: KahitSan/kserp src/lib/account-logo-url.ts (vendored into the plugin remote).
2
+ //
3
+ // Build the <img src> for a financial account logo. Logos are object-storage
4
+ // only now: s3_link is the public URL and the sole reference (the legacy
5
+ // logo_path column + kernel /assets/ fallback are gone).
6
+
7
+ export function buildLogoSrc(s3Link: string | null | undefined): string {
8
+ // The value flows straight into <img src>, so only an http(s) URL is allowed
9
+ // through — a stored javascript:/data:/vbscript: scheme would be stored XSS.
10
+ if (s3Link && /^https?:/i.test(s3Link)) return s3Link;
11
+ return "";
12
+ }
@@ -0,0 +1,105 @@
1
+ // Source: KahitSan/kserp src/lib/accounts-index.tsx (promoted into the SDK).
2
+ //
3
+ // Session-scoped index of financial accounts keyed by id. The list rows, the
4
+ // detail payments list, etc. surface accounts by name only (server denormalises
5
+ // source/destination names and drops icon/color/logo_path). This index lets any
6
+ // render site resolve an id → AvatarAccount synchronously so AccountAvatar can
7
+ // show the right glyph/logo.
8
+ //
9
+ // The monolith mounts a Provider in the app shell and shares one resource via
10
+ // context. The plugin remote has no such provider, so this version owns a
11
+ // module-level resource created on first use and re-keyed on the active org id
12
+ // (read from the host's useActiveWorkspace()). Because plugin-ui ships as source
13
+ // compiled into each plugin's IIFE, the module-level singleton stays per-plugin.
14
+ // Degrades gracefully: when the financial-accounts plugin isn't deployed the
15
+ // fetch 404s/fails and the index stays empty — resolveAccount() then falls back
16
+ // to the type-default glyph.
17
+
18
+ import { createResource, type Resource } from "solid-js";
19
+ import { useActiveWorkspace } from "@kserp/host-ui";
20
+ import type { AvatarAccount } from "../components/AccountAvatar";
21
+
22
+ interface IndexShape {
23
+ byId: Map<number | string, AvatarAccount>;
24
+ // Display names keyed by id. The server denormalises source/destination
25
+ // names on most reads but the payment-leg history only carries
26
+ // financial_account_id, so callers resolve the name from here instead of
27
+ // showing the bare "Account #N" placeholder.
28
+ nameById: Map<number | string, string>;
29
+ }
30
+
31
+ async function fetchAccountsIndex(wsId: number | null): Promise<IndexShape> {
32
+ const byId = new Map<number | string, AvatarAccount>();
33
+ const nameById = new Map<number | string, string>();
34
+ if (wsId == null) return { byId, nameById };
35
+
36
+ const [activeResult, archivedResult] = await Promise.all([
37
+ fetch("/api/financial-accounts?status=active&limit=500", { credentials: "include" })
38
+ .then(async (r) => (r.ok ? ((await r.json()).data ?? []) : []))
39
+ .catch(() => []) as Promise<Array<AvatarAccount>>,
40
+ fetch("/api/financial-accounts?status=archived&limit=500", { credentials: "include" })
41
+ .then(async (r) => (r.ok ? ((await r.json()).data ?? []) : []))
42
+ .catch(() => []) as Promise<Array<AvatarAccount>>,
43
+ ]);
44
+
45
+ for (const a of activeResult) {
46
+ byId.set(a.id, {
47
+ id: a.id,
48
+ type: a.type,
49
+ icon: a.icon ?? null,
50
+ color: a.color ?? null,
51
+ s3_link: a.s3_link ?? null,
52
+ });
53
+ if (a.name) nameById.set(a.id, a.name);
54
+ }
55
+ for (const a of archivedResult) {
56
+ if (!byId.has(a.id)) {
57
+ byId.set(a.id, {
58
+ id: a.id,
59
+ type: a.type,
60
+ icon: a.icon ?? null,
61
+ color: a.color ?? null,
62
+ s3_link: a.s3_link ?? null,
63
+ });
64
+ }
65
+ if (a.name && !nameById.has(a.id)) nameById.set(a.id, a.name);
66
+ }
67
+ return { byId, nameById };
68
+ }
69
+
70
+ // Module-level singletons so a page with N rows fires one fetch per org, not
71
+ // 2*N. Created on first useAccountsIndex() call.
72
+ let sharedResource: Resource<IndexShape> | undefined;
73
+
74
+ export function useAccountsIndex(): Resource<IndexShape> {
75
+ if (!sharedResource) {
76
+ const { activeWorkspace } = useActiveWorkspace();
77
+ const [data] = createResource(() => activeWorkspace()?.ws_id ?? null, fetchAccountsIndex);
78
+ sharedResource = data;
79
+ }
80
+ return sharedResource;
81
+ }
82
+
83
+ // Build an AvatarAccount for the given id, falling back to a generic
84
+ // placeholder when the id is missing from the index (deleted/archived edge
85
+ // case, or the index hasn't loaded yet). Always returns something renderable.
86
+ export function resolveAccount(
87
+ index: IndexShape | undefined,
88
+ id: number | string | null | undefined,
89
+ ): AvatarAccount | null {
90
+ if (id == null) return null;
91
+ const hit = index?.byId.get(id);
92
+ if (hit) return hit;
93
+ return { id, type: "external", icon: null, color: null, s3_link: null };
94
+ }
95
+
96
+ // Resolve an account id → its display name. Returns null when the id is
97
+ // missing or the index hasn't loaded yet, so callers can fall back to a
98
+ // server-supplied name or the bare "Account #N" placeholder.
99
+ export function resolveAccountName(
100
+ index: IndexShape | undefined,
101
+ id: number | string | null | undefined,
102
+ ): string | null {
103
+ if (id == null) return null;
104
+ return index?.nameById.get(id) ?? null;
105
+ }
@@ -0,0 +1,20 @@
1
+ // Attachment URL resolver for plugin remotes.
2
+ //
3
+ // Attachments are object-storage only: the upload route stores the public S3
4
+ // URL in s3_link, which is the sole reference — the legacy on-disk file_path
5
+ // column and the kernel /assets/ fallback are gone.
6
+
7
+ export function attachmentUrl(s3Link: string | null | undefined): string {
8
+ // The value flows straight into <a href> / <img src>, so only an http(s) URL
9
+ // is allowed through — a stored javascript:/data:/vbscript: scheme would be
10
+ // stored XSS. Every attachment has a valid https s3_link; anything else
11
+ // yields an empty src and the render site shows an "unavailable" placeholder.
12
+ if (s3Link && /^https?:/i.test(s3Link)) return s3Link;
13
+ return "";
14
+ }
15
+
16
+ // Whether an attachment can actually be fetched — true only for a safe http(s)
17
+ // s3_link. Render sites show an "unavailable" placeholder otherwise.
18
+ export function isResolvableAttachment(s3Link: string | null | undefined): boolean {
19
+ return !!s3Link && /^https?:/i.test(s3Link);
20
+ }