@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.
- package/README.md +109 -26
- package/host-ui.d.ts +4 -4
- package/package.json +7 -3
- package/src/components/{AccountAvatar.tsx → base/AccountAvatar.tsx} +10 -10
- package/src/components/base/ChartLegend.tsx +34 -0
- package/src/components/base/CopyButton.tsx +53 -0
- package/src/components/base/DetailRow.tsx +17 -0
- package/src/components/{ExistingAttachmentTile.tsx → base/ExistingAttachmentTile.tsx} +2 -2
- package/src/components/base/FormErrorBanner.tsx +27 -0
- package/src/components/base/FormField.tsx +19 -0
- package/src/components/base/ImageCropper.tsx +275 -0
- package/src/components/base/KpiCard.tsx +125 -0
- package/src/components/base/ProgressBar.tsx +328 -0
- package/src/components/base/RadioCardGroup.tsx +146 -0
- package/src/components/base/SegmentedFilter.tsx +50 -0
- package/src/components/base/StatusPill.tsx +90 -0
- package/src/components/base/TagPill.tsx +24 -0
- package/src/components/base/Tooltip.tsx +64 -0
- package/src/components/{ClientPicker.tsx → composite/ClientPicker.tsx} +1 -1
- package/src/components/composite/FormActions.tsx +79 -0
- package/src/components/composite/LiveTimer.tsx +434 -0
- package/src/components/{MarkdownNotes.tsx → composite/MarkdownNotes.tsx} +1 -1
- package/src/components/{PaymentAccountPicker.tsx → composite/PaymentAccountPicker.tsx} +2 -2
- package/src/components/composite/SecretReveal.tsx +63 -0
- package/src/components/{VoucherPicker.tsx → composite/VoucherPicker.tsx} +1 -1
- package/src/index.ts +84 -27
- package/src/utils/INPUT_CLASS.ts +7 -0
- package/src/{lib → utils}/account-logo-url.ts +1 -1
- package/src/{lib → utils}/accounts-index.tsx +2 -2
- package/src/{lib → utils}/attachments.ts +3 -3
- package/src/utils/formatFullDate.ts +14 -0
- package/src/utils/formatPHP.ts +13 -0
- package/src/utils/formatShortDate.ts +17 -0
- /package/src/components/{AddAttachmentTile.tsx → base/AddAttachmentTile.tsx} +0 -0
- /package/src/components/{CameraCapture.tsx → base/CameraCapture.tsx} +0 -0
- /package/src/components/{MentionTextarea.tsx → composite/MentionTextarea.tsx} +0 -0
- /package/src/{lib → utils}/account-icons.ts +0 -0
package/README.md
CHANGED
|
@@ -1,45 +1,128 @@
|
|
|
1
1
|
# @kahitsan/ksui
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
```tsx
|
|
67
|
+
import { createSignal } from "solid-js";
|
|
68
|
+
import { MentionTextarea } from "@kahitsan/ksui";
|
|
32
69
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
##
|
|
124
|
+
## License
|
|
44
125
|
|
|
45
|
-
|
|
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/
|
|
3
|
-
// source of truth
|
|
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/
|
|
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
|
|
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.
|
|
4
|
-
"description": "ksui
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
25
|
-
import { buildLogoSrc } from "
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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 "
|
|
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}
|
|
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
|
+
}
|