@kahitsan/ksui 0.10.2 → 0.11.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 +19 -21
- package/package.json +3 -7
- package/src/components/base/CameraCapture.tsx +2 -2
- package/src/components/base/DataTable.tsx +940 -0
- package/src/components/base/ExistingAttachmentTile.tsx +3 -3
- package/src/components/base/ImageCropper.tsx +3 -3
- package/src/components/base/Modal.tsx +198 -0
- package/src/components/composite/ComboBox.tsx +2 -2
- package/src/components/composite/FormActions.tsx +4 -4
- package/src/components/composite/MarkdownNotes.tsx +8 -11
- package/src/index.ts +47 -14
- package/src/utils/accounts-index.tsx +7 -6
- package/src/utils/confirm.tsx +84 -0
- package/src/utils/dom.ts +197 -0
- package/src/utils/highlight.tsx +67 -0
- package/src/utils/integration.ts +49 -0
- package/host-ui.d.ts +0 -145
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Renders one already-uploaded attachment as a 24×24 tile: an image preview or
|
|
2
2
|
// a paperclip/file fallback linking to the s3_link public URL, an "Unavailable"
|
|
3
3
|
// placeholder when the link can't be resolved (see lib/attachments.ts), and an
|
|
4
|
-
// optional remove button. confirm
|
|
5
|
-
// attachment widget set alongside AddAttachmentTile + CameraCapture.
|
|
4
|
+
// optional remove button. confirm is ksui's own self-contained dialog. The third
|
|
5
|
+
// of the attachment widget set alongside AddAttachmentTile + CameraCapture.
|
|
6
6
|
|
|
7
7
|
import { Show, type Component } from "solid-js";
|
|
8
8
|
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
|
-
import { confirm } from "
|
|
11
|
+
import { confirm } from "../../utils/confirm";
|
|
12
12
|
import { attachmentUrl, isResolvableAttachment } from "../../utils/attachments";
|
|
13
13
|
|
|
14
14
|
export interface ExistingAttachment {
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
// corner to resize it. On Apply, the selected region is rendered to a canvas
|
|
5
5
|
// at the requested output size and handed back as a WebP Blob.
|
|
6
6
|
//
|
|
7
|
-
// The widget is
|
|
8
|
-
// Modal + Button (provided by the consumer via "@kserp/host-ui"), solid-js,
|
|
7
|
+
// The widget is self-contained: it uses ksui's own Modal + Button, solid-js,
|
|
9
8
|
// and a single lucide icon.
|
|
10
9
|
|
|
11
10
|
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
12
|
-
import
|
|
11
|
+
import Modal from "./Modal";
|
|
12
|
+
import Button from "./Button";
|
|
13
13
|
import X from "lucide-solid/icons/x";
|
|
14
14
|
|
|
15
15
|
interface ImageCropperProps {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Self-contained modal dialog. Promoted into ksui from the former host kit so
|
|
2
|
+
// the library no longer depends on "@kserp/host-ui": ImageCropper (the only
|
|
3
|
+
// in-library consumer) now imports Modal from here.
|
|
4
|
+
//
|
|
5
|
+
// Like Button/ProgressBar, ksui publishes no sidecar .css — the styles are
|
|
6
|
+
// injected once per page via a runtime <style> tag and referenced with plain,
|
|
7
|
+
// unscoped `ksui-modal-*` class names, so the component carries no Tailwind or
|
|
8
|
+
// host-brand-class dependency. Surface/border colors read from CSS custom
|
|
9
|
+
// properties (`--ksui-modal-bg`, `--ksui-modal-border`) with dark-friendly
|
|
10
|
+
// fallbacks, so a consumer can retint without forking the component.
|
|
11
|
+
//
|
|
12
|
+
// Lifecycle matches the original: mount === open, unmount === closed (wrap in
|
|
13
|
+
// `<Show when={…}>`). The default variant uses the native <dialog> element
|
|
14
|
+
// (top-layer rendering, native ::backdrop, browser Escape + focus trap); the
|
|
15
|
+
// sheet variant uses a <div> overlay so child popovers Portaled into
|
|
16
|
+
// document.body still compose above the backdrop via z-index.
|
|
17
|
+
|
|
18
|
+
import { JSX, onCleanup, onMount, splitProps } from "solid-js";
|
|
19
|
+
import { autoFocusOnMount, lockPullToRefresh, unlockPullToRefresh, useFocusTrap } from "../../utils/dom";
|
|
20
|
+
|
|
21
|
+
export type ModalSize = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "5xl" | "7xl";
|
|
22
|
+
export type ModalTone = "default" | "danger";
|
|
23
|
+
|
|
24
|
+
const SIZE_MAX_WIDTH: Record<ModalSize, string> = {
|
|
25
|
+
sm: "24rem",
|
|
26
|
+
md: "28rem",
|
|
27
|
+
lg: "32rem",
|
|
28
|
+
xl: "36rem",
|
|
29
|
+
"2xl": "42rem",
|
|
30
|
+
"3xl": "48rem",
|
|
31
|
+
"5xl": "64rem",
|
|
32
|
+
"7xl": "80rem",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const STYLE_ID = "ksui-modal-style";
|
|
36
|
+
|
|
37
|
+
function ensureModalStyle(): void {
|
|
38
|
+
if (typeof document === "undefined") return;
|
|
39
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
40
|
+
const style = document.createElement("style");
|
|
41
|
+
style.id = STYLE_ID;
|
|
42
|
+
style.textContent = `
|
|
43
|
+
.ksui-modal-dialog{position:fixed;inset:0;z-index:50;background:transparent;padding:0;margin:0;max-width:none;max-height:none;width:100vw;height:100vh;border:0;}
|
|
44
|
+
.ksui-modal-dialog[open]{display:flex;align-items:center;justify-content:center;padding:1rem;}
|
|
45
|
+
.ksui-modal-dialog::backdrop{background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);}
|
|
46
|
+
.ksui-modal-sheet-overlay{position:fixed;inset:0;z-index:50;display:flex;align-items:flex-end;justify-content:center;}
|
|
47
|
+
@media (min-width:640px){.ksui-modal-sheet-overlay{align-items:center;padding:1rem;}}
|
|
48
|
+
.ksui-modal-sheet-backdrop{position:absolute;inset:0;background:rgba(0,0,0,0.7);backdrop-filter:blur(6px);}
|
|
49
|
+
.ksui-modal-card{position:relative;z-index:10;width:100%;background:var(--ksui-modal-bg,#11161f);color:var(--ksui-modal-fg,inherit);border:1px solid var(--ksui-modal-border,rgba(245,158,11,0.3));border-radius:0.75rem;padding:1.5rem;box-shadow:0 25px 50px -12px rgba(0,0,0,0.6);max-height:90vh;overflow-x:hidden;overflow-y:auto;}
|
|
50
|
+
.ksui-modal-card.danger{border-color:var(--ksui-modal-border-danger,rgba(239,68,68,0.3));}
|
|
51
|
+
.ksui-modal-sheet-card{position:relative;z-index:10;width:100%;background:var(--ksui-modal-bg,#11161f);color:var(--ksui-modal-fg,inherit);border:1px solid var(--ksui-modal-border,rgba(245,158,11,0.3));box-shadow:0 25px 50px -12px rgba(0,0,0,0.6);max-height:92vh;overflow:hidden;overscroll-behavior:contain;}
|
|
52
|
+
.ksui-modal-sheet-card.danger{border-color:var(--ksui-modal-border-danger,rgba(239,68,68,0.3));}
|
|
53
|
+
@media (min-width:640px){.ksui-modal-sheet-card{width:auto;border-radius:0.75rem;max-height:88vh;}}
|
|
54
|
+
`;
|
|
55
|
+
document.head.appendChild(style);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ModalProps {
|
|
59
|
+
/** Fired on Escape, backdrop click, or any other dismissal trigger. */
|
|
60
|
+
onClose: () => void;
|
|
61
|
+
/** When false, Escape and backdrop clicks are ignored. Defaults to true. */
|
|
62
|
+
dismissable?: boolean;
|
|
63
|
+
/** "default" is a centered native-<dialog> card; "sheet" is a bottom-sheet <div> overlay. */
|
|
64
|
+
variant?: "default" | "sheet";
|
|
65
|
+
/** Outer card max width. Defaults to "lg" (default variant) / "3xl" (sheet). */
|
|
66
|
+
size?: ModalSize;
|
|
67
|
+
/** Border tint. "danger" for destructive confirms. Defaults to "default". */
|
|
68
|
+
tone?: ModalTone;
|
|
69
|
+
/** Optional accessible name for the dialog. */
|
|
70
|
+
ariaLabel?: string;
|
|
71
|
+
children: JSX.Element;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type LocalProps = Omit<ModalProps, "variant">;
|
|
75
|
+
|
|
76
|
+
export function Modal(props: ModalProps): JSX.Element {
|
|
77
|
+
ensureModalStyle();
|
|
78
|
+
const [local] = splitProps(props, [
|
|
79
|
+
"onClose",
|
|
80
|
+
"dismissable",
|
|
81
|
+
"size",
|
|
82
|
+
"tone",
|
|
83
|
+
"ariaLabel",
|
|
84
|
+
"children",
|
|
85
|
+
]);
|
|
86
|
+
// Variant is read once at mount; call sites pick it statically.
|
|
87
|
+
if (props.variant === "sheet") return SheetModal(local, props.size);
|
|
88
|
+
return DialogModal(local);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function DialogModal(local: LocalProps): JSX.Element {
|
|
92
|
+
let dialogEl: HTMLDialogElement | undefined;
|
|
93
|
+
|
|
94
|
+
lockPullToRefresh();
|
|
95
|
+
onCleanup(unlockPullToRefresh);
|
|
96
|
+
|
|
97
|
+
onMount(() => {
|
|
98
|
+
if (!dialogEl) return;
|
|
99
|
+
if (!dialogEl.open) {
|
|
100
|
+
try {
|
|
101
|
+
dialogEl.showModal();
|
|
102
|
+
} catch {
|
|
103
|
+
// showModal throws if already open or detached; harmless to ignore.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
onCleanup(() => {
|
|
109
|
+
if (dialogEl?.open) dialogEl.close();
|
|
110
|
+
queueMicrotask(() => {
|
|
111
|
+
for (const el of document.querySelectorAll<HTMLDialogElement>("dialog[open]")) {
|
|
112
|
+
if (el !== dialogEl && el.isConnected && !el.matches(":modal")) {
|
|
113
|
+
try {
|
|
114
|
+
el.close();
|
|
115
|
+
el.showModal();
|
|
116
|
+
} catch {
|
|
117
|
+
// detached, or showModal disallowed; skip it.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const handleCancel = (e: Event) => {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
if (local.dismissable === false) return;
|
|
127
|
+
local.onClose();
|
|
128
|
+
};
|
|
129
|
+
const handleBackdropClick = (e: MouseEvent) => {
|
|
130
|
+
if (local.dismissable === false) return;
|
|
131
|
+
if (e.target === dialogEl) local.onClose();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
|
136
|
+
<dialog
|
|
137
|
+
ref={dialogEl}
|
|
138
|
+
aria-modal="true"
|
|
139
|
+
aria-label={local.ariaLabel}
|
|
140
|
+
onCancel={handleCancel}
|
|
141
|
+
onClick={handleBackdropClick}
|
|
142
|
+
class="ksui-modal-dialog"
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
ref={(el) => {
|
|
146
|
+
autoFocusOnMount(el);
|
|
147
|
+
onCleanup(useFocusTrap(el));
|
|
148
|
+
}}
|
|
149
|
+
class={`ksui-modal-card${local.tone === "danger" ? " danger" : ""}`}
|
|
150
|
+
style={{ "max-width": SIZE_MAX_WIDTH[local.size ?? "lg"] }}
|
|
151
|
+
>
|
|
152
|
+
{local.children}
|
|
153
|
+
</div>
|
|
154
|
+
</dialog>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function SheetModal(local: LocalProps, size?: ModalSize): JSX.Element {
|
|
159
|
+
lockPullToRefresh();
|
|
160
|
+
onCleanup(unlockPullToRefresh);
|
|
161
|
+
|
|
162
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
163
|
+
if (e.key !== "Escape") return;
|
|
164
|
+
if (local.dismissable === false) return;
|
|
165
|
+
local.onClose();
|
|
166
|
+
};
|
|
167
|
+
if (typeof window !== "undefined") {
|
|
168
|
+
window.addEventListener("keydown", handleKey);
|
|
169
|
+
onCleanup(() => window.removeEventListener("keydown", handleKey));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const handleBackdropClick = () => {
|
|
173
|
+
if (local.dismissable === false) return;
|
|
174
|
+
local.onClose();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div class="ksui-modal-sheet-overlay">
|
|
179
|
+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
|
180
|
+
<div class="ksui-modal-sheet-backdrop" onClick={handleBackdropClick} />
|
|
181
|
+
<div
|
|
182
|
+
ref={(el) => {
|
|
183
|
+
autoFocusOnMount(el);
|
|
184
|
+
onCleanup(useFocusTrap(el));
|
|
185
|
+
}}
|
|
186
|
+
role="dialog"
|
|
187
|
+
aria-modal="true"
|
|
188
|
+
aria-label={local.ariaLabel}
|
|
189
|
+
class={`ksui-modal-sheet-card${local.tone === "danger" ? " danger" : ""}`}
|
|
190
|
+
style={{ "max-width": SIZE_MAX_WIDTH[size ?? "3xl"] }}
|
|
191
|
+
>
|
|
192
|
+
{local.children}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default Modal;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { Portal } from "solid-js/web";
|
|
23
23
|
import { createEffect, createSignal, For, onMount, Show, type JSX } from "solid-js";
|
|
24
|
-
import { highlightMatch } from "
|
|
24
|
+
import { highlightMatch } from "../../utils/highlight";
|
|
25
25
|
import UserPlus from "lucide-solid/icons/user-plus";
|
|
26
26
|
import Search from "lucide-solid/icons/search";
|
|
27
27
|
import Star from "lucide-solid/icons/star";
|
|
@@ -98,7 +98,7 @@ export type ComboBoxProps<T> = ComboBoxSingleProps<T> | ComboBoxMultiProps<T>;
|
|
|
98
98
|
|
|
99
99
|
export default function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
|
|
100
100
|
// Mode is read once at setup — call sites pick single/multi statically (same
|
|
101
|
-
// convention as the
|
|
101
|
+
// convention as the Modal variant). Narrowing makes each branch see
|
|
102
102
|
// its concrete prop shape.
|
|
103
103
|
if (props.multiple) return MultiComboBox(props);
|
|
104
104
|
return SingleComboBox(props);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Cancel / Submit modal footer row, composing
|
|
1
|
+
// Cancel / Submit modal footer row, composing ksui's own Button.
|
|
2
2
|
//
|
|
3
3
|
// The same footer is repeated across plugin modals: a
|
|
4
4
|
// `flex flex-col-reverse sm:flex-row gap-2 sm:justify-end` row with a
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
// handlers, saving state, optional icon) as props. No fetch/API call lives
|
|
9
9
|
// here — the caller owns the submit handler.
|
|
10
10
|
//
|
|
11
|
-
// ModalShell is deliberately NOT promoted: the
|
|
11
|
+
// ModalShell is deliberately NOT promoted: the ksui Modal already supplies the
|
|
12
12
|
// backdrop, card, title/close-X path, aria-modal, focus trap, size and tone
|
|
13
|
-
// tokens. Only the body chrome the
|
|
13
|
+
// tokens. Only the body chrome the Modal does not cover (FormErrorBanner +
|
|
14
14
|
// FormActions) is promoted.
|
|
15
15
|
|
|
16
16
|
import type { JSX } from "solid-js";
|
|
17
17
|
import { Show } from "solid-js";
|
|
18
|
-
import
|
|
18
|
+
import Button from "../base/Button";
|
|
19
19
|
|
|
20
20
|
export interface FormActionsProps {
|
|
21
21
|
/** Invoked when the Cancel button is clicked. */
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
// Renders transaction `notes` with a restricted markdown subset and inline
|
|
4
4
|
// client-mention chips (@[Name](client:N)). Adapted for the remote: routing is
|
|
5
5
|
// host-owned so mention chips link via a plain <a href> instead of @solidjs/
|
|
6
|
-
// router's <A>;
|
|
7
|
-
//
|
|
8
|
-
// degrades to
|
|
6
|
+
// router's <A>; highlightMatch is ksui's own helper and the client-view
|
|
7
|
+
// permission is read through ksui's opt-in integration registry (canAccess),
|
|
8
|
+
// which degrades to false when the host hasn't configured one. The hover card
|
|
9
|
+
// fetches the SIBLING clients plugin at /api/clients/:id and degrades to a
|
|
10
|
+
// non-hovering chip when that 404s.
|
|
9
11
|
|
|
10
12
|
import { For, Show, createMemo, createSignal, createUniqueId, onCleanup, type JSX } from "solid-js";
|
|
11
|
-
import {
|
|
13
|
+
import { highlightMatch } from "../../utils/highlight";
|
|
14
|
+
import { canAccess } from "../../utils/integration";
|
|
12
15
|
|
|
13
16
|
const MENTION_RE = /@\[([^\]]+)\](?:\(client:(\d+)\))?/g;
|
|
14
17
|
|
|
@@ -329,13 +332,7 @@ function MentionHoverCard(props: { clientId: number; name: string }): JSX.Elemen
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
function MentionChip(props: { name: string; clientId: number | null }): JSX.Element {
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
const perms = usePermissions();
|
|
335
|
-
canViewClients = () => perms.has("clients.view");
|
|
336
|
-
} catch {
|
|
337
|
-
/* no provider in context, leave canViewClients() returning false */
|
|
338
|
-
}
|
|
335
|
+
const canViewClients = () => canAccess("clients.view");
|
|
339
336
|
|
|
340
337
|
return (
|
|
341
338
|
<Show
|
package/src/index.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
// @kahitsan/ksui:
|
|
1
|
+
// @kahitsan/ksui: a standalone SolidJS UI component library.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// the
|
|
7
|
-
// The
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// Published to the public npm registry and consumed as a normal dependency. The
|
|
4
|
+
// package ships its source under a `solid` export condition (see package.json),
|
|
5
|
+
// so the consumer's vite-plugin-solid compiles these components while solid-js
|
|
6
|
+
// stays EXTERNALIZED to the app's runtime so there is a single Solid instance.
|
|
7
|
+
// The library is self-contained: it depends only on solid-js + lucide-solid and
|
|
8
|
+
// injects its own CSS at runtime — no host UI kit, no Tailwind, no app-provided
|
|
9
|
+
// primitives are required. Components that can integrate with a surrounding app
|
|
10
|
+
// (a permission check, the active workspace) do so through an OPTIONAL opt-in
|
|
11
|
+
// registry (configurePermissions / configureActiveWorkspace) and degrade
|
|
12
|
+
// gracefully when it is not configured.
|
|
13
13
|
//
|
|
14
14
|
// Components live under two folders by category:
|
|
15
|
-
// base/ a primitive that stands on its own. It uses only solid-js
|
|
16
|
-
// lucide-solid
|
|
17
|
-
//
|
|
15
|
+
// base/ a primitive that stands on its own. It uses only solid-js and
|
|
16
|
+
// lucide-solid (plus ksui's own utils). It does not import another
|
|
17
|
+
// ksui component.
|
|
18
18
|
// composite/ a component that wraps a base or composes two or more components
|
|
19
19
|
// into a higher-level widget.
|
|
20
20
|
// See CONTRIBUTING.md for the placement rule when adding a new component.
|
|
@@ -72,6 +72,24 @@ export { default as StatusIndicator, type StatusIndicatorProps, type StatusIndic
|
|
|
72
72
|
export { default as SectionHeading, type SectionHeadingProps, type SectionHeadingAlign, type SectionHeadingLevel } from "./components/base/SectionHeading";
|
|
73
73
|
export { default as EyebrowBadge, type EyebrowBadgeProps, type EyebrowTone, type EyebrowTracking } from "./components/base/EyebrowBadge";
|
|
74
74
|
|
|
75
|
+
// Self-contained modal dialog (promoted from the former host kit). Injects its
|
|
76
|
+
// own CSS; no Tailwind / host-brand classes required.
|
|
77
|
+
export { default as Modal, type ModalProps, type ModalSize, type ModalTone } from "./components/base/Modal";
|
|
78
|
+
|
|
79
|
+
// Server-side / client-side data table with debounced search, column sort,
|
|
80
|
+
// pagination + "Show more" load mode, a filters slot, optional date filter, and
|
|
81
|
+
// an onRefetch handle. Ported from the host kit; injects its own CSS, no Tailwind.
|
|
82
|
+
// The type surface mirrors the kernel's @kserp/host-ui contract exactly, so a
|
|
83
|
+
// caller written against host-ui works unchanged here.
|
|
84
|
+
export {
|
|
85
|
+
default as DataTable,
|
|
86
|
+
type DataTableProps,
|
|
87
|
+
type DataTableColumn,
|
|
88
|
+
type DataTableRow,
|
|
89
|
+
type FetchParams,
|
|
90
|
+
type FetchResult,
|
|
91
|
+
} from "./components/base/DataTable";
|
|
92
|
+
|
|
75
93
|
// ---------------------------------------------------------------------------
|
|
76
94
|
// Composite components
|
|
77
95
|
// ---------------------------------------------------------------------------
|
|
@@ -139,3 +157,18 @@ export { INPUT_CLASS } from "./utils/INPUT_CLASS";
|
|
|
139
157
|
export { formatPHP } from "./utils/formatPHP";
|
|
140
158
|
export { formatShortDate } from "./utils/formatShortDate";
|
|
141
159
|
export { formatFullDate } from "./utils/formatFullDate";
|
|
160
|
+
|
|
161
|
+
// Self-contained helpers promoted from the former host kit so the library has no
|
|
162
|
+
// "@kserp/host-ui" dependency.
|
|
163
|
+
export { highlightMatch, HighlightedText, matchesQuery, matchesAny } from "./utils/highlight";
|
|
164
|
+
export { confirm, type ConfirmOptions } from "./utils/confirm";
|
|
165
|
+
export { useFocusTrap, autoFocusOnMount, lockPullToRefresh, unlockPullToRefresh } from "./utils/dom";
|
|
166
|
+
|
|
167
|
+
// Optional host integrations. Components degrade gracefully when these are not
|
|
168
|
+
// configured; a host app opts in once at startup.
|
|
169
|
+
export {
|
|
170
|
+
configurePermissions,
|
|
171
|
+
configureActiveWorkspace,
|
|
172
|
+
canAccess,
|
|
173
|
+
getActiveWorkspaceId,
|
|
174
|
+
} from "./utils/integration";
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
// The monolith mounts a Provider in the app shell and shares one resource via
|
|
10
10
|
// context. The plugin remote has no such provider, so this version owns a
|
|
11
11
|
// module-level resource created on first use and re-keyed on the active org id
|
|
12
|
-
// (read from
|
|
13
|
-
//
|
|
12
|
+
// (read from ksui's opt-in integration registry, getActiveWorkspaceId(); a host
|
|
13
|
+
// wires it via configureActiveWorkspace, and it degrades to null — empty index —
|
|
14
|
+
// when unconfigured). Because ksui ships as source compiled into each plugin's
|
|
15
|
+
// IIFE, the module-level singleton stays per-plugin.
|
|
14
16
|
// Degrades gracefully: when the financial-accounts plugin isn't deployed the
|
|
15
17
|
// fetch 404s/fails and the index stays empty, so resolveAccount() then falls back
|
|
16
18
|
// to the type-default glyph.
|
|
17
19
|
|
|
18
20
|
import { createResource, type Resource } from "solid-js";
|
|
19
|
-
import {
|
|
21
|
+
import { getActiveWorkspaceId } from "./integration";
|
|
20
22
|
import type { AvatarAccount } from "../components/base/AccountAvatar";
|
|
21
23
|
|
|
22
24
|
interface IndexShape {
|
|
@@ -28,7 +30,7 @@ interface IndexShape {
|
|
|
28
30
|
nameById: Map<number | string, string>;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
async function fetchAccountsIndex(wsId: number | null): Promise<IndexShape> {
|
|
33
|
+
async function fetchAccountsIndex(wsId: number | string | null): Promise<IndexShape> {
|
|
32
34
|
const byId = new Map<number | string, AvatarAccount>();
|
|
33
35
|
const nameById = new Map<number | string, string>();
|
|
34
36
|
if (wsId == null) return { byId, nameById };
|
|
@@ -73,8 +75,7 @@ let sharedResource: Resource<IndexShape> | undefined;
|
|
|
73
75
|
|
|
74
76
|
export function useAccountsIndex(): Resource<IndexShape> {
|
|
75
77
|
if (!sharedResource) {
|
|
76
|
-
const
|
|
77
|
-
const [data] = createResource(() => activeWorkspace()?.ws_id ?? null, fetchAccountsIndex);
|
|
78
|
+
const [data] = createResource(() => getActiveWorkspaceId(), fetchAccountsIndex);
|
|
78
79
|
sharedResource = data;
|
|
79
80
|
}
|
|
80
81
|
return sharedResource;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Promise-based confirm dialog. Ported into ksui from the former host kit so the
|
|
2
|
+
// library is self-contained: ExistingAttachmentTile used to import `confirm`
|
|
3
|
+
// from "@kserp/host-ui"; it now imports it from here.
|
|
4
|
+
//
|
|
5
|
+
// Renders a ksui Modal + Buttons imperatively (outside any component tree) into
|
|
6
|
+
// a transient container appended to <body>, resolves the returned promise when
|
|
7
|
+
// the user confirms/cancels/dismisses, then tears the container down. No host
|
|
8
|
+
// kit, no Tailwind — Modal and Button inject their own styles.
|
|
9
|
+
|
|
10
|
+
import { createSignal, Show } from "solid-js";
|
|
11
|
+
import { render } from "solid-js/web";
|
|
12
|
+
import Modal from "../components/base/Modal";
|
|
13
|
+
import Button from "../components/base/Button";
|
|
14
|
+
|
|
15
|
+
export interface ConfirmOptions {
|
|
16
|
+
title?: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
confirmLabel?: string;
|
|
19
|
+
cancelLabel?: string;
|
|
20
|
+
/** Tints the dialog + confirm button for destructive actions. */
|
|
21
|
+
danger?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function confirm(opts: ConfirmOptions): Promise<boolean> {
|
|
25
|
+
// SSR / non-DOM: no way to ask, so default to "no".
|
|
26
|
+
if (typeof document === "undefined") return Promise.resolve(false);
|
|
27
|
+
|
|
28
|
+
return new Promise<boolean>((resolve) => {
|
|
29
|
+
const host = document.createElement("div");
|
|
30
|
+
document.body.appendChild(host);
|
|
31
|
+
|
|
32
|
+
const [open, setOpen] = createSignal(true);
|
|
33
|
+
let settled = false;
|
|
34
|
+
let dispose = () => {};
|
|
35
|
+
|
|
36
|
+
const finish = (result: boolean) => {
|
|
37
|
+
if (settled) return;
|
|
38
|
+
settled = true;
|
|
39
|
+
setOpen(false);
|
|
40
|
+
// Let the Modal run its unmount/cleanup before yanking the container.
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
dispose();
|
|
43
|
+
host.remove();
|
|
44
|
+
resolve(result);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
dispose = render(
|
|
49
|
+
() => (
|
|
50
|
+
<Show when={open()}>
|
|
51
|
+
<Modal
|
|
52
|
+
onClose={() => finish(false)}
|
|
53
|
+
size="sm"
|
|
54
|
+
tone={opts.danger ? "danger" : "default"}
|
|
55
|
+
ariaLabel={opts.title ?? "Confirm"}
|
|
56
|
+
>
|
|
57
|
+
<div style={{ display: "flex", "flex-direction": "column", gap: "0.75rem" }}>
|
|
58
|
+
<Show when={opts.title}>
|
|
59
|
+
<h2 style={{ margin: 0, "font-size": "1.125rem", "font-weight": 600 }}>{opts.title}</h2>
|
|
60
|
+
</Show>
|
|
61
|
+
<Show when={opts.message}>
|
|
62
|
+
<p style={{ margin: 0, "font-size": "0.875rem", opacity: 0.85 }}>{opts.message}</p>
|
|
63
|
+
</Show>
|
|
64
|
+
<div style={{ display: "flex", "justify-content": "flex-end", gap: "0.5rem", "margin-top": "0.5rem" }}>
|
|
65
|
+
<Button type="button" intent="secondary" variant="ghost" onClick={() => finish(false)}>
|
|
66
|
+
{opts.cancelLabel ?? "Cancel"}
|
|
67
|
+
</Button>
|
|
68
|
+
<Button
|
|
69
|
+
type="button"
|
|
70
|
+
intent={opts.danger ? "danger" : "primary"}
|
|
71
|
+
variant="clip1"
|
|
72
|
+
onClick={() => finish(true)}
|
|
73
|
+
>
|
|
74
|
+
{opts.confirmLabel ?? "Confirm"}
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</Modal>
|
|
79
|
+
</Show>
|
|
80
|
+
),
|
|
81
|
+
host,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}
|