@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/LICENSE +21 -0
- package/README.md +45 -0
- package/host-ui.d.ts +145 -0
- package/package.json +44 -0
- package/src/components/AccountAvatar.tsx +169 -0
- package/src/components/AddAttachmentTile.tsx +96 -0
- package/src/components/CameraCapture.tsx +144 -0
- package/src/components/ClientPicker.tsx +358 -0
- package/src/components/ExistingAttachmentTile.tsx +94 -0
- package/src/components/MarkdownNotes.tsx +466 -0
- package/src/components/MentionTextarea.tsx +490 -0
- package/src/components/PaymentAccountPicker.tsx +312 -0
- package/src/components/VoucherPicker.tsx +369 -0
- package/src/index.ts +62 -0
- package/src/lib/account-icons.ts +106 -0
- package/src/lib/account-logo-url.ts +12 -0
- package/src/lib/accounts-index.tsx +105 -0
- package/src/lib/attachments.ts +20 -0
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
|
+
}
|