@kahitsan/ksui 0.3.0 → 0.4.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 (37) hide show
  1. package/README.md +109 -26
  2. package/host-ui.d.ts +4 -4
  3. package/package.json +7 -3
  4. package/src/components/{AccountAvatar.tsx → base/AccountAvatar.tsx} +10 -10
  5. package/src/components/base/ChartLegend.tsx +34 -0
  6. package/src/components/base/CopyButton.tsx +53 -0
  7. package/src/components/base/DetailRow.tsx +17 -0
  8. package/src/components/{ExistingAttachmentTile.tsx → base/ExistingAttachmentTile.tsx} +2 -2
  9. package/src/components/base/FormErrorBanner.tsx +27 -0
  10. package/src/components/base/FormField.tsx +19 -0
  11. package/src/components/base/ImageCropper.tsx +275 -0
  12. package/src/components/base/KpiCard.tsx +125 -0
  13. package/src/components/base/ProgressBar.tsx +328 -0
  14. package/src/components/base/RadioCardGroup.tsx +146 -0
  15. package/src/components/base/SegmentedFilter.tsx +50 -0
  16. package/src/components/base/StatusPill.tsx +90 -0
  17. package/src/components/base/TagPill.tsx +24 -0
  18. package/src/components/base/Tooltip.tsx +64 -0
  19. package/src/components/{ClientPicker.tsx → composite/ClientPicker.tsx} +1 -1
  20. package/src/components/composite/FormActions.tsx +79 -0
  21. package/src/components/composite/LiveTimer.tsx +434 -0
  22. package/src/components/{MarkdownNotes.tsx → composite/MarkdownNotes.tsx} +1 -1
  23. package/src/components/{PaymentAccountPicker.tsx → composite/PaymentAccountPicker.tsx} +2 -2
  24. package/src/components/composite/SecretReveal.tsx +63 -0
  25. package/src/components/{VoucherPicker.tsx → composite/VoucherPicker.tsx} +1 -1
  26. package/src/index.ts +84 -27
  27. package/src/utils/INPUT_CLASS.ts +7 -0
  28. package/src/{lib → utils}/account-logo-url.ts +1 -1
  29. package/src/{lib → utils}/accounts-index.tsx +2 -2
  30. package/src/{lib → utils}/attachments.ts +3 -3
  31. package/src/utils/formatFullDate.ts +14 -0
  32. package/src/utils/formatPHP.ts +13 -0
  33. package/src/utils/formatShortDate.ts +17 -0
  34. /package/src/components/{AddAttachmentTile.tsx → base/AddAttachmentTile.tsx} +0 -0
  35. /package/src/components/{CameraCapture.tsx → base/CameraCapture.tsx} +0 -0
  36. /package/src/components/{MentionTextarea.tsx → composite/MentionTextarea.tsx} +0 -0
  37. /package/src/{lib → utils}/account-icons.ts +0 -0
package/README.md CHANGED
@@ -1,45 +1,128 @@
1
1
  # @kahitsan/ksui
2
2
 
3
- The single canonical copy of the shared SolidJS UI components that KahitSan/Hilinga
4
- plugins consume — pickers, the mention textarea, markdown notes, the camera +
5
- attachment tiles, and the data-driven account avatar — plus the `@kserp/host-ui`
6
- ambient type contract for the host UI kit.
3
+ A set of UI components for SolidJS apps, built and used by the KahitSan team.
7
4
 
8
- Extracted from `kserp/packages/plugin-ui` (`@kahitsan/plugin-ui`) into its own repo
9
- so the UI package versions and publishes independently of the kernel.
5
+ ## What this is
6
+
7
+ `@kahitsan/ksui` is a small library of ready-made user interface pieces for
8
+ [SolidJS](https://www.solidjs.com/). SolidJS is a way to build web pages with
9
+ reusable components, a bit like React but with its own way of working. These
10
+ components are written for SolidJS only. They will not work in React or Vue.
11
+
12
+ Inside you will find small building-block components, ready-made widgets built
13
+ from them, and a few non-visual helpers. The full, always-current list lives in
14
+ the docs (see below), so this README stays short on purpose.
15
+
16
+ ## Where you can use it
17
+
18
+ There are two ways to use ksui.
19
+
20
+ 1. **In your own SolidJS project.** Install it from npm and use the components
21
+ like any other dependency. It is MIT licensed, so you are free to use it in
22
+ personal or commercial work. See the License section below.
23
+ 2. **As a contributor to KahitSan plugins.** If you build or help with our
24
+ plugins, these are the shared components you reuse instead of writing your
25
+ own copy.
26
+
27
+ One honest note so nothing surprises you. Some components are self-contained and
28
+ work right away. Others, like the client, voucher, and payment account pickers,
29
+ expect a backend that answers certain requests, so they fit best inside an app
30
+ built the KahitSan way. A few components also expect a small set of shared UI
31
+ pieces from your app, like a button and a dialog. The setup note below explains
32
+ that part, and each component page says what it needs.
33
+
34
+ ## What is inside
35
+
36
+ The package is organized into three kinds of exports:
37
+
38
+ - **Base components** are the small building blocks, like form fields, status
39
+ pills, tooltips, progress bars, and avatars.
40
+ - **Composite components** combine those building blocks into ready-made widgets,
41
+ like the search-and-pick selectors and the notes editor.
42
+ - **Utils** are non-visual helpers, like the peso and date formatters and the
43
+ safe link builders.
44
+
45
+ We do not list every component here on purpose, so this README does not fall out
46
+ of date as the set grows. The complete, current catalog, with a live example and
47
+ the props for each one, is in the docs:
48
+
49
+ **https://kahitsan.github.io/ksui/**
10
50
 
11
51
  ## Install
12
52
 
13
- Published to the **public npm registry** (npmjs.com) as `@kahitsan/ksui` no
14
- registry config or auth needed:
53
+ `@kahitsan/ksui` is on the public npm registry, so no extra registry setup or
54
+ login is needed.
15
55
 
16
56
  ```
17
57
  npm install @kahitsan/ksui
18
58
  ```
19
59
 
20
- ## How it ships
60
+ You also need a SolidJS app. If you are starting fresh, set one up with
61
+ [`vite-plugin-solid`](https://github.com/solidjs/vite-plugin-solid), which is the
62
+ tool that compiles Solid components.
21
63
 
22
- This is a SolidJS component library shipped as **source** under a `solid` export
23
- condition (see `package.json`). The consumer's `vite-plugin-solid` compiles the
24
- components, while `solid-js` and `@kserp/host-ui` stay **externalized** to the host
25
- runtime globals — so the component source is bundled into the plugin IIFE exactly
26
- as a local copy would be: one Solid instance, the host UI kit reused. `lucide-solid`
27
- is bundled from the consumer's own deps.
64
+ ## A tiny usage example
28
65
 
29
- Consumers must keep `solid-js` + `@kserp/host-ui` externalized in their
30
- `vite.remote.config.ts`, and (until a `.d.ts` bundle ships) reference the host kit
31
- types via the `./host-ui` export:
66
+ ```tsx
67
+ import { createSignal } from "solid-js";
68
+ import { MentionTextarea } from "@kahitsan/ksui";
32
69
 
33
- ```ts
34
- /// <reference types="@kahitsan/ksui/host-ui" />
70
+ function NotesField() {
71
+ const [value, setValue] = createSignal("");
72
+ return (
73
+ <MentionTextarea
74
+ value={value()}
75
+ setValue={setValue}
76
+ placeholder="Add notes, type @ to tag a client"
77
+ rows={3}
78
+ />
79
+ );
80
+ }
35
81
  ```
36
82
 
37
- ## Type-checking
83
+ The avatar chip is also easy and needs no backend:
84
+
85
+ ```tsx
86
+ import { AccountAvatar } from "@kahitsan/ksui";
87
+
88
+ <AccountAvatar account={{ id: 0, type: "user", name: "Maria Cruz" }} size={32} />;
89
+ ```
90
+
91
+ ## The honest setup note
92
+
93
+ Two things this library expects from the app around it:
94
+
95
+ 1. **`solid-js` stays external.** This library ships as source, and your own
96
+ `vite-plugin-solid` compiles it. Keep `solid-js` as a single shared copy in
97
+ your app (not bundled twice), so there is only one Solid instance running.
98
+
99
+ 2. **A host UI kit under the name `@kserp/host-ui`.** Some components import
100
+ shared pieces from your app, like a `Button`, a `confirm` dialog, and a few
101
+ hooks. Your app provides these under the import name `@kserp/host-ui`, and that
102
+ name is kept external at build time too. Components that need it are:
103
+ MarkdownNotes, ClientPicker, CameraCapture, ExistingAttachmentTile, and the
104
+ `useAccountsIndex` hook. The rest do not need it.
105
+
106
+ So a few components work right away with no backend and no host kit
107
+ (MentionTextarea as a plain notes box, AccountAvatar, AddAttachmentTile,
108
+ VoucherPicker, and all the helper functions). Others assume the ERP host and a
109
+ backend that answers calls like `/api/clients`. The docs site walks through how
110
+ to mock the host kit and stub those calls so you can see every component render.
111
+
112
+ ## Docs
113
+
114
+ The full documentation site, with a component-by-component guide and setup steps,
115
+ will be at:
116
+
117
+ https://kahitsan.github.io/ksui/
118
+
119
+ ## Contributing
38
120
 
39
- `npm run typecheck` runs `tsc --noEmit` standalone (`jsxImportSource: solid-js`, the
40
- shipped `host-ui.d.ts` in scope). The authoritative gate remains each consuming
41
- plugin's own `tsc`, where the plugin's `lucide-solid` and host runtime are present.
121
+ We welcome help. Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for how to set
122
+ up the repo, the code style we follow, and how to send a change.
42
123
 
43
- ## Publishing
124
+ ## License
44
125
 
45
- Push a `v*` tag (e.g. `v0.3.0`); the release workflow publishes to GitHub Packages.
126
+ MIT. See [LICENSE](./LICENSE). You are free to use ksui in your own personal or
127
+ commercial SolidJS projects, as long as you keep the MIT license notice. Source
128
+ lives at https://github.com/KahitSan/ksui.
package/host-ui.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  // CANONICAL SDK type defs for the host UI kit (window.__KSERP_UI__, externalized
2
- // as "@kserp/host-ui"). This ships in @kahitsan/plugin-ui and is the single
3
- // source of truth every plugin (ours, and any third-party with no kernel
2
+ // as "@kserp/host-ui"). This ships in @kahitsan/ksui and is the single
3
+ // source of truth. Every plugin (ours, and any third-party with no kernel
4
4
  // source) gets it from the installed package via
5
- // `/// <reference types="@kahitsan/plugin-ui/host-ui" />`; there are no more
5
+ // `/// <reference types="@kahitsan/ksui/host-ui" />`; there are no more
6
6
  // per-plugin copies to drift. The host owns the runtime: its remote loader
7
7
  // (kserp src/lib/remote-loader.ts) populates the global from the host's kit
8
8
  // barrel (kserp src/lib/host-ui.tsx) before loading any remote. Keep this in sync
9
- // with that barrel every member here must be exported there, and vice-versa.
9
+ // with that barrel. Every member here must be exported there, and vice versa.
10
10
  declare module "@kserp/host-ui" {
11
11
  import type { JSX, Accessor } from "solid-js";
12
12
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kahitsan/ksui",
3
- "version": "0.3.0",
4
- "description": "ksui shared SolidJS UI components + the @kserp/host-ui type contract for KahitSan/Hilinga plugins. Published to GitHub Packages and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with solid-js + @kserp/host-ui externalized to the host runtime.",
3
+ "version": "0.4.0",
4
+ "description": "ksui is a set of shared SolidJS UI components plus the @kserp/host-ui type contract for KahitSan/Hilinga plugins. 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 solid-js + @kserp/host-ui externalized to the host runtime.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
7
7
  "types": "./src/index.ts",
@@ -21,7 +21,10 @@
21
21
  "host-ui.d.ts"
22
22
  ],
23
23
  "scripts": {
24
- "typecheck": "tsc --noEmit"
24
+ "typecheck": "tsc --noEmit",
25
+ "changeset": "changeset",
26
+ "version": "changeset version",
27
+ "release": "changeset publish"
25
28
  },
26
29
  "dependencies": {
27
30
  "lucide-solid": "^1.7.0"
@@ -30,6 +33,7 @@
30
33
  "solid-js": "^1.9.0"
31
34
  },
32
35
  "devDependencies": {
36
+ "@changesets/cli": "^2.31.0",
33
37
  "solid-js": "^1.9.0",
34
38
  "typescript": "^5.6.0"
35
39
  },
@@ -1,15 +1,15 @@
1
- // AccountAvatar renders a small visual chip for an account or a user.
1
+ // AccountAvatar renders a small visual chip for an account or a user.
2
2
  //
3
3
  // Two distinct semantics share this chip, and the chip picks by intent:
4
4
  //
5
- // • ACCOUNT (a financial account the default). Shows the account's
5
+ // • ACCOUNT (a financial account, the default). Shows the account's
6
6
  // uploaded logo, else its chosen icon glyph (the slug picked in the
7
7
  // financial-accounts create/edit modal, or the type default). NEVER an
8
8
  // initials circle, and the container is a ROUNDED SQUARE (rounded-md).
9
- // This matches the /financial-accounts page exactly a financial
9
+ // This matches the /financial-accounts page exactly. A financial
10
10
  // account is not a person, so it must not render as an initials avatar.
11
11
  //
12
- // • USER (a person badge calendar entries, mention lists, timesheet
12
+ // • USER (a person badge for calendar entries, mention lists, timesheet
13
13
  // attribution). Shows the profile photo, else an initial-on-color
14
14
  // CIRCLE, in a circular (rounded-full) container. Callers opt in with
15
15
  // `{ id: 0, type: 'user', name: 'Myra Abilay', image }` (the `type:
@@ -21,10 +21,10 @@
21
21
 
22
22
  import { Show } from "solid-js";
23
23
  import { Dynamic } from "solid-js/web";
24
- import { getAccountIcon } from "../lib/account-icons";
25
- import { buildLogoSrc } from "../lib/account-logo-url";
24
+ import { getAccountIcon } from "../../utils/account-icons";
25
+ import { buildLogoSrc } from "../../utils/account-logo-url";
26
26
 
27
- // Shared 16-color palette and initials algorithm keep in lockstep with
27
+ // Shared 16-color palette and initials algorithm. Keep in lockstep with
28
28
  // the kserp's ~/lib/avatar.ts so the host runtime and the plugin fleet
29
29
  // render the same chip for the same user. Exported so any future widget
30
30
  // (or a caller's inline use) can derive a user's color/initials without
@@ -52,7 +52,7 @@ export function getAvatarColor(name: string): string {
52
52
 
53
53
  /** Build a base64 SVG data URL for the initial-on-color circle. Because it
54
54
  * renders as an `<img>`, the chip scales uniformly with every other source
55
- * (photo, s3 logo) no per-plugin/inline text-size drift. The viewBox is
55
+ * (photo, s3 logo), so there is no per-plugin/inline text-size drift. The viewBox is
56
56
  * 100×100 and the font-size is calculated to keep 2-character initials
57
57
  * comfortably inside the circle. */
58
58
  export function buildInitialsSvg(name: string): string {
@@ -81,7 +81,7 @@ export interface AvatarAccount {
81
81
  * account variant, which never renders initials. */
82
82
  name?: string;
83
83
  /** User profile photo URL (e.g. Google identity provider). USER variant
84
- * only higher priority than the initials fallback. */
84
+ * only. Higher priority than the initials fallback. */
85
85
  image?: string | null;
86
86
  }
87
87
 
@@ -136,7 +136,7 @@ export default function AccountAvatar(props: AccountAvatarProps) {
136
136
  when={isUser()}
137
137
  fallback={
138
138
  // ACCOUNT: uploaded logo (rounded square), else the chosen icon
139
- // glyph. No initials a financial account is not a person.
139
+ // glyph. No initials, since a financial account is not a person.
140
140
  <Show when={props.account.s3_link} fallback={iconGlyph()}>
141
141
  <img
142
142
  src={buildLogoSrc(props.account.s3_link)}
@@ -0,0 +1,34 @@
1
+ // ChartLegend renders a single legend entry for a chart: a small colored dot,
2
+ // an uppercase label, and a value beneath it. Used by cashflow / analytics
3
+ // charts to label each series (money in, money out, net) with its current
4
+ // total. Purely presentational; the caller supplies the dot color class, the
5
+ // label, the formatted value, and an optional value color override.
6
+
7
+ interface ChartLegendProps {
8
+ /** Tailwind background class for the legend dot (e.g. "bg-emerald-400"). */
9
+ dot: string;
10
+ /** Uppercase label shown above the value (e.g. "Money in"). */
11
+ label: string;
12
+ /** Pre-formatted value string (e.g. a currency string). */
13
+ value: string;
14
+ /** Optional Tailwind text color class for the value. Defaults to text-zinc-100. */
15
+ valueColor?: string;
16
+ }
17
+
18
+ export default function ChartLegend(props: ChartLegendProps) {
19
+ return (
20
+ <div class="flex items-center gap-2">
21
+ <span class={`w-2 h-2 rounded-sm ${props.dot}`} />
22
+ <div class="flex flex-col">
23
+ <span class="text-[9px] uppercase tracking-widest text-zinc-500 font-medium leading-tight">
24
+ {props.label}
25
+ </span>
26
+ <span
27
+ class={`text-xs font-bold tabular-nums leading-tight ${props.valueColor || "text-zinc-100"}`}
28
+ >
29
+ {props.value}
30
+ </span>
31
+ </div>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,53 @@
1
+ import { createSignal, onCleanup, type JSX } from "solid-js";
2
+ import Copy from "lucide-solid/icons/copy";
3
+ import Check from "lucide-solid/icons/check";
4
+
5
+ interface CopyButtonProps {
6
+ /** The text written to the clipboard on click. Caller owns the value. */
7
+ text: string;
8
+ /** Label shown in the default state. Default "Copy". */
9
+ label?: string;
10
+ /** Label shown for ~1.5s after a successful copy. Default "Copied". */
11
+ copiedLabel?: string;
12
+ /** Accessible label for the button. Default "Copy". */
13
+ ariaLabel?: string;
14
+ /** Icon size in px. Default 12. */
15
+ size?: number;
16
+ /** Extra classes on the button. */
17
+ class?: string;
18
+ }
19
+
20
+ // Self-contained copy-to-clipboard button. Owns its own "copied" signal:
21
+ // on click it writes props.text via navigator.clipboard, flips copied true
22
+ // for ~1500ms, swaps the label, and shows the lucide Check icon while copied
23
+ // (Copy icon otherwise). The pending timer is cleared on cleanup so a copy
24
+ // flash never fires after the button unmounts. No domain coupling — the
25
+ // caller passes the text and any label overrides.
26
+ export default function CopyButton(props: CopyButtonProps): JSX.Element {
27
+ const [copied, setCopied] = createSignal(false);
28
+ let timer: ReturnType<typeof setTimeout> | undefined;
29
+
30
+ const handleClick = () => {
31
+ navigator.clipboard.writeText(props.text).then(() => {
32
+ setCopied(true);
33
+ if (timer) clearTimeout(timer);
34
+ timer = setTimeout(() => setCopied(false), 1500);
35
+ });
36
+ };
37
+
38
+ onCleanup(() => {
39
+ if (timer) clearTimeout(timer);
40
+ });
41
+
42
+ return (
43
+ <button
44
+ type="button"
45
+ onClick={handleClick}
46
+ aria-label={props.ariaLabel ?? "Copy"}
47
+ class={`ks-interactive px-2 py-1.5 rounded bg-zinc-800 border border-zinc-700 text-zinc-300 hover:text-white text-xs flex items-center gap-1 ${props.class ?? ""}`}
48
+ >
49
+ {copied() ? <Check size={props.size ?? 12} /> : <Copy size={props.size ?? 12} />}
50
+ {copied() ? (props.copiedLabel ?? "Copied") : (props.label ?? "Copy")}
51
+ </button>
52
+ );
53
+ }
@@ -0,0 +1,17 @@
1
+ // DetailRow renders a single labeled read-only field: a small muted label
2
+ // stacked above its value, with an em-dash placeholder when the value is
3
+ // empty. Used in detail/view surfaces (e.g. the client detail modal) to lay
4
+ // out a record's fields uniformly.
5
+ //
6
+ // Ported verbatim from kplugin_clients/ui/remote/index.tsx. It is purely
7
+ // presentational with no external dependencies, so the only adaptation is
8
+ // the move into the shared kit.
9
+
10
+ export default function DetailRow(props: { label: string; value: string | null | undefined }) {
11
+ return (
12
+ <div>
13
+ <span class="text-xs text-zinc-500 block">{props.label}</span>
14
+ <span class="text-sm text-zinc-200">{props.value || "—"}</span>
15
+ </div>
16
+ );
17
+ }
@@ -9,7 +9,7 @@ import Paperclip from "lucide-solid/icons/paperclip";
9
9
  import X from "lucide-solid/icons/x";
10
10
  import TriangleAlert from "lucide-solid/icons/triangle-alert";
11
11
  import { confirm } from "@kserp/host-ui";
12
- import { attachmentUrl, isResolvableAttachment } from "../lib/attachments";
12
+ import { attachmentUrl, isResolvableAttachment } from "../../utils/attachments";
13
13
 
14
14
  export interface ExistingAttachment {
15
15
  id: number;
@@ -39,7 +39,7 @@ export default function ExistingAttachmentTile(props: Props) {
39
39
  fallback={
40
40
  <div
41
41
  class="flex w-24 h-24 flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-zinc-700 bg-zinc-900/40 px-2 text-center text-zinc-500"
42
- title={`${props.attachment.file_name} file is no longer available`}
42
+ title={`${props.attachment.file_name} (file is no longer available)`}
43
43
  >
44
44
  <TriangleAlert size={18} class="text-amber-500/70" />
45
45
  <span class="truncate max-w-full text-[10px]">{props.attachment.file_name}</span>
@@ -0,0 +1,27 @@
1
+ import { Show, type JSX } from "solid-js";
2
+
3
+ interface FormErrorBannerProps {
4
+ /** The error message to display. When falsy (null / undefined / empty
5
+ * string) the banner renders nothing, so callers do not need to wrap
6
+ * it in their own <Show>. */
7
+ message: string | null | undefined;
8
+ /** Extra classes on the banner. Use this for margin spacing such as
9
+ * "mt-3" or "mb-3" — the banner ships no margin of its own. */
10
+ class?: string;
11
+ }
12
+
13
+ // Standard form error banner: the red role="alert" box that nearly every
14
+ // create/edit modal hand-rolls. Self-guarding — it renders nothing when the
15
+ // message is falsy, so a surrounding <Show> is unnecessary.
16
+ export default function FormErrorBanner(props: FormErrorBannerProps): JSX.Element {
17
+ return (
18
+ <Show when={props.message}>
19
+ <div
20
+ role="alert"
21
+ class={`rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300 ${props.class ?? ""}`}
22
+ >
23
+ {props.message}
24
+ </div>
25
+ </Show>
26
+ );
27
+ }
@@ -0,0 +1,19 @@
1
+ // FormField is a small presentational wrapper for a labeled form control. It
2
+ // renders a label above the control, the control itself (passed as children),
3
+ // and an optional hint line below. Copied faithfully from the packages plugin
4
+ // remote UI atoms; behavior is unchanged.
5
+
6
+ import { Show } from "solid-js";
7
+ import type { JSX } from "solid-js";
8
+
9
+ export default function FormField(props: { label: string; children: JSX.Element; hint?: string }) {
10
+ return (
11
+ <div>
12
+ <label class="block text-xs text-zinc-500 mb-1">{props.label}</label>
13
+ {props.children}
14
+ <Show when={props.hint}>
15
+ <p class="text-[10px] text-zinc-600 mt-1">{props.hint}</p>
16
+ </Show>
17
+ </div>
18
+ );
19
+ }