@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
package/src/utils/dom.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Selector for elements a Tab key would normally land on. Picker dropdowns
|
|
2
|
+
// in this app render their popups via Portal outside the modal's subtree,
|
|
3
|
+
// so the trap also has to consider those, but options/inputs inside a
|
|
4
|
+
// Portal are still owned by the open modal logically. The trap solves this
|
|
5
|
+
// by also walking matching descendants of the document body that point back
|
|
6
|
+
// to the modal via aria-controls / aria-labelledby. For phase 1, the
|
|
7
|
+
// simpler implementation is sufficient: every modal in this app keeps its
|
|
8
|
+
// own focusable descendants inside the dialog node itself, and pickers
|
|
9
|
+
// close on Escape independently of the trap.
|
|
10
|
+
const TABBABLE_SELECTOR = [
|
|
11
|
+
'a[href]:not([tabindex="-1"])',
|
|
12
|
+
'button:not([disabled]):not([tabindex="-1"])',
|
|
13
|
+
'input:not([type="hidden"]):not([disabled]):not([tabindex="-1"])',
|
|
14
|
+
'select:not([disabled]):not([tabindex="-1"])',
|
|
15
|
+
'textarea:not([disabled]):not([tabindex="-1"])',
|
|
16
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
17
|
+
].join(", ");
|
|
18
|
+
|
|
19
|
+
function tabbablesIn(root: HTMLElement): HTMLElement[] {
|
|
20
|
+
// Walk the modal's subtree and keep visible, non-disabled focusables.
|
|
21
|
+
return Array.from(root.querySelectorAll<HTMLElement>(TABBABLE_SELECTOR)).filter((el) => {
|
|
22
|
+
if (el.hidden) return false;
|
|
23
|
+
// offsetParent is null for `display: none` and detached nodes; visibility
|
|
24
|
+
// hidden elements are excluded too. Don't trust just the selector list.
|
|
25
|
+
if (el.offsetParent === null && getComputedStyle(el).position !== "fixed") return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* While any modal is open, suppress the browser/PWA pull-to-refresh and
|
|
32
|
+
* overscroll-bounce gestures so a stray drag on the backdrop doesn't reload
|
|
33
|
+
* the page out from under a half-filled form. Counter handles nested modals
|
|
34
|
+
* (e.g. a Confirm dialog opening on top of a transactions sheet).
|
|
35
|
+
*
|
|
36
|
+
* Pair `lockPullToRefresh()` with `unlockPullToRefresh()` (e.g. via Solid
|
|
37
|
+
* `onCleanup`) so each lock has exactly one matching unlock.
|
|
38
|
+
*/
|
|
39
|
+
let openModalCount = 0;
|
|
40
|
+
let prevHtmlOverscroll: string | null = null;
|
|
41
|
+
let prevBodyOverscroll: string | null = null;
|
|
42
|
+
|
|
43
|
+
export function lockPullToRefresh() {
|
|
44
|
+
if (typeof document === "undefined") return;
|
|
45
|
+
if (openModalCount === 0) {
|
|
46
|
+
const html = document.documentElement;
|
|
47
|
+
const body = document.body;
|
|
48
|
+
prevHtmlOverscroll = html.style.overscrollBehavior;
|
|
49
|
+
prevBodyOverscroll = body.style.overscrollBehavior;
|
|
50
|
+
html.style.overscrollBehavior = "none";
|
|
51
|
+
body.style.overscrollBehavior = "none";
|
|
52
|
+
}
|
|
53
|
+
openModalCount += 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function unlockPullToRefresh() {
|
|
57
|
+
if (typeof document === "undefined") return;
|
|
58
|
+
openModalCount = Math.max(0, openModalCount - 1);
|
|
59
|
+
if (openModalCount === 0) {
|
|
60
|
+
document.documentElement.style.overscrollBehavior = prevHtmlOverscroll ?? "";
|
|
61
|
+
document.body.style.overscrollBehavior = prevBodyOverscroll ?? "";
|
|
62
|
+
prevHtmlOverscroll = null;
|
|
63
|
+
prevBodyOverscroll = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Trap Tab and Shift+Tab inside `el` and restore focus on cleanup. Returns
|
|
69
|
+
* a teardown function the caller can use to deactivate the trap explicitly
|
|
70
|
+
* (also runs automatically when called inside a Solid `onCleanup`).
|
|
71
|
+
*
|
|
72
|
+
* Use this from a ref callback or Solid effect:
|
|
73
|
+
*
|
|
74
|
+
* <div ref={(el) => { onCleanup(useFocusTrap(el)); }}>
|
|
75
|
+
*
|
|
76
|
+
* Or from an effect:
|
|
77
|
+
*
|
|
78
|
+
* createEffect(() => {
|
|
79
|
+
* if (containerRef) onCleanup(useFocusTrap(containerRef));
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* The trap only intercepts Tab. Escape, click-outside, and submit handling
|
|
83
|
+
* remain the modal's responsibility, the trap is purely a focus-keeper.
|
|
84
|
+
*/
|
|
85
|
+
export function useFocusTrap(el: HTMLElement | undefined): () => void {
|
|
86
|
+
if (!el) return () => {};
|
|
87
|
+
|
|
88
|
+
// Snapshot the element that had focus before the modal mounted so we can
|
|
89
|
+
// hand it back when the trap deactivates. Skip the document body which is
|
|
90
|
+
// a non-actionable default.
|
|
91
|
+
const previouslyFocused =
|
|
92
|
+
document.activeElement instanceof HTMLElement && document.activeElement !== document.body
|
|
93
|
+
? document.activeElement
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
97
|
+
if (e.key !== "Tab") return;
|
|
98
|
+
const list = tabbablesIn(el);
|
|
99
|
+
if (list.length === 0) {
|
|
100
|
+
// No focusable descendants (an empty modal), pin focus on the
|
|
101
|
+
// container itself rather than letting the browser tab away.
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
el.focus();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const first = list[0];
|
|
107
|
+
const last = list[list.length - 1];
|
|
108
|
+
const active = document.activeElement as HTMLElement | null;
|
|
109
|
+
const inTrap = active && el.contains(active);
|
|
110
|
+
if (e.shiftKey) {
|
|
111
|
+
if (!inTrap || active === first) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
last.focus();
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
if (!inTrap || active === last) {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
first.focus();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Capture phase so we see the keydown before any descendant handler can
|
|
124
|
+
// swallow it. Solid's <Show>/<Portal> wrapping doesn't change that.
|
|
125
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
129
|
+
// Restore focus only when the snapshot is still in the DOM. If the
|
|
130
|
+
// caller removed the original button between mount and unmount,
|
|
131
|
+
// dropping focus on a detached element would just clear it instead.
|
|
132
|
+
if (previouslyFocused && document.contains(previouslyFocused)) {
|
|
133
|
+
try {
|
|
134
|
+
previouslyFocused.focus();
|
|
135
|
+
} catch {
|
|
136
|
+
/* element became unfocusable in the meantime, let the browser pick */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Ref callback that moves focus into a freshly mounted container so keyboard
|
|
144
|
+
* users land somewhere meaningful. Used on modal content containers:
|
|
145
|
+
* `ref={autoFocusOnMount}`.
|
|
146
|
+
*
|
|
147
|
+
* Lookup order:
|
|
148
|
+
* 1. An explicit `[data-autofocus]` element (or its first focusable
|
|
149
|
+
* descendant). Lets a parent override the default heuristic when the
|
|
150
|
+
* first visible input is not the right place to land, e.g. a modal
|
|
151
|
+
* whose primary affordance is a search/picker rather than a date field.
|
|
152
|
+
* 2. The first text-entry control (input/textarea/select). Most modals open
|
|
153
|
+
* onto a form, and dropping focus directly into the first field is what
|
|
154
|
+
* sighted keyboard users expect.
|
|
155
|
+
* 3. Any other focusable element (button, link, anything with `tabindex`).
|
|
156
|
+
* A modal that opens onto an action grid (e.g. the Counter cart) has no
|
|
157
|
+
* inputs on first render; without this fallback `el.focus()` would never
|
|
158
|
+
* run and focus would stay on the button that opened the modal.
|
|
159
|
+
*/
|
|
160
|
+
export function autoFocusOnMount(el: HTMLElement | undefined) {
|
|
161
|
+
if (!el) return;
|
|
162
|
+
queueMicrotask(() => {
|
|
163
|
+
const focusableSelector =
|
|
164
|
+
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])';
|
|
165
|
+
const inputSelector =
|
|
166
|
+
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])';
|
|
167
|
+
|
|
168
|
+
const marker = el.querySelector<HTMLElement>("[data-autofocus]");
|
|
169
|
+
if (marker) {
|
|
170
|
+
// The marker itself may be focusable; prefer it. Otherwise pick the
|
|
171
|
+
// first focusable descendant (e.g. the trigger button inside a custom
|
|
172
|
+
// picker component).
|
|
173
|
+
if (marker.matches(focusableSelector)) {
|
|
174
|
+
marker.focus();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const inner = marker.querySelector<HTMLElement>(focusableSelector);
|
|
178
|
+
if (inner) {
|
|
179
|
+
inner.focus();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const input = el.querySelector<HTMLElement>(inputSelector);
|
|
185
|
+
if (input) {
|
|
186
|
+
input.focus();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Fall back to the first interactive element. tabindex="-1" is excluded
|
|
190
|
+
// so a programmatically-focusable container does not steal focus from a
|
|
191
|
+
// child the user can actually act on.
|
|
192
|
+
const focusable = el.querySelector<HTMLElement>(
|
|
193
|
+
'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])',
|
|
194
|
+
);
|
|
195
|
+
focusable?.focus();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Search-match highlighting + tiny string-match helpers. Ported into ksui from
|
|
2
|
+
// the former host kit so the library is self-contained: ComboBox and
|
|
3
|
+
// MarkdownNotes used to import `highlightMatch` from "@kserp/host-ui"; they now
|
|
4
|
+
// import it from here. Pure functions plus a `<mark>` wrapper.
|
|
5
|
+
//
|
|
6
|
+
// The default highlight tint ships as injected CSS (a `.ksui-mark` class) so the
|
|
7
|
+
// library carries no Tailwind dependency — like Button/ProgressBar, the keyframe
|
|
8
|
+
// /helper class is added once per page via a runtime <style> tag. Callers can
|
|
9
|
+
// still pass their own `markClass` to override.
|
|
10
|
+
|
|
11
|
+
import { type JSX } from "solid-js";
|
|
12
|
+
|
|
13
|
+
const MARK_STYLE_ID = "ksui-mark-style";
|
|
14
|
+
|
|
15
|
+
function ensureMarkStyle(): void {
|
|
16
|
+
if (typeof document === "undefined") return;
|
|
17
|
+
if (document.getElementById(MARK_STYLE_ID)) return;
|
|
18
|
+
const style = document.createElement("style");
|
|
19
|
+
style.id = MARK_STYLE_ID;
|
|
20
|
+
style.textContent = `.ksui-mark{background-color:rgba(245,158,11,0.3);color:inherit;border-radius:2px;}`;
|
|
21
|
+
document.head.appendChild(style);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Case-insensitive substring test. Empty query matches everything. */
|
|
25
|
+
export function matchesQuery(text: string | null | undefined, query: string): boolean {
|
|
26
|
+
if (!query) return true;
|
|
27
|
+
if (!text) return false;
|
|
28
|
+
return text.toLowerCase().includes(query.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the query matches any of the given fields. */
|
|
32
|
+
export function matchesAny(query: string, ...fields: (string | null | undefined)[]): boolean {
|
|
33
|
+
if (!query) return true;
|
|
34
|
+
return fields.some((f) => matchesQuery(f, query));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap every case-insensitive occurrence of `query` inside `text` in a `<mark>`.
|
|
39
|
+
* Pass `markClass` to override the default `.ksui-mark` tint.
|
|
40
|
+
*/
|
|
41
|
+
export function highlightMatch(text: string, query: string, markClass?: string): JSX.Element {
|
|
42
|
+
if (!query || !text) return <>{text}</>;
|
|
43
|
+
if (!markClass) ensureMarkStyle();
|
|
44
|
+
const lower = text.toLowerCase();
|
|
45
|
+
const q = query.toLowerCase();
|
|
46
|
+
const out: JSX.Element[] = [];
|
|
47
|
+
let i = 0;
|
|
48
|
+
let idx = lower.indexOf(q, i);
|
|
49
|
+
let key = 0;
|
|
50
|
+
while (idx !== -1) {
|
|
51
|
+
if (idx > i) out.push(<>{text.slice(i, idx)}</>);
|
|
52
|
+
out.push(
|
|
53
|
+
<mark class={markClass ?? "ksui-mark"} data-key={key++}>
|
|
54
|
+
{text.slice(idx, idx + q.length)}
|
|
55
|
+
</mark>,
|
|
56
|
+
);
|
|
57
|
+
i = idx + q.length;
|
|
58
|
+
idx = lower.indexOf(q, i);
|
|
59
|
+
}
|
|
60
|
+
if (i < text.length) out.push(<>{text.slice(i)}</>);
|
|
61
|
+
return <>{out}</>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Component form of {@link highlightMatch}. */
|
|
65
|
+
export function HighlightedText(props: { text: string; query: string; markClass?: string }): JSX.Element {
|
|
66
|
+
return <>{highlightMatch(props.text, props.query, props.markClass)}</>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Optional host integrations.
|
|
2
|
+
//
|
|
3
|
+
// A couple of ksui components can do MORE when the surrounding app feeds them
|
|
4
|
+
// runtime context — a permission check (MarkdownNotes gates its client-mention
|
|
5
|
+
// hover card on "clients.view") and the active workspace id (useAccountsIndex
|
|
6
|
+
// re-keys its fetch per workspace). The library used to pull these from
|
|
7
|
+
// "@kserp/host-ui"; it now owns a tiny opt-in registry instead, so it stays a
|
|
8
|
+
// standalone dependency-free package.
|
|
9
|
+
//
|
|
10
|
+
// Default behavior with nothing configured (any plain SolidJS consumer):
|
|
11
|
+
// - canAccess(code) === false → MarkdownNotes renders a plain, non-hovering
|
|
12
|
+
// mention chip.
|
|
13
|
+
// - getActiveWorkspaceId() === null → useAccountsIndex stays empty (its fetch
|
|
14
|
+
// short-circuits), so accounts resolve to the
|
|
15
|
+
// type-default glyph.
|
|
16
|
+
//
|
|
17
|
+
// A host app wires the real behavior once at startup:
|
|
18
|
+
// import { configurePermissions, configureActiveWorkspace } from "@kahitsan/ksui";
|
|
19
|
+
// configurePermissions((code) => usePermissions().has(code));
|
|
20
|
+
// configureActiveWorkspace(() => useActiveWorkspace().activeWorkspace()?.ws_id ?? null);
|
|
21
|
+
//
|
|
22
|
+
// The resolvers may read reactive sources; ksui calls them inside tracking
|
|
23
|
+
// scopes, so a signal-backed permission/workspace value stays reactive.
|
|
24
|
+
|
|
25
|
+
type PermissionResolver = (code: string) => boolean;
|
|
26
|
+
type WorkspaceResolver = () => number | string | null;
|
|
27
|
+
|
|
28
|
+
let permissionResolver: PermissionResolver | null = null;
|
|
29
|
+
let workspaceResolver: WorkspaceResolver | null = null;
|
|
30
|
+
|
|
31
|
+
/** Register the host's permission check. Pass a function so it can read a reactive source. */
|
|
32
|
+
export function configurePermissions(resolver: PermissionResolver): void {
|
|
33
|
+
permissionResolver = resolver;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when the host granted `code`. False when no resolver is configured. */
|
|
37
|
+
export function canAccess(code: string): boolean {
|
|
38
|
+
return permissionResolver ? !!permissionResolver(code) : false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Register the host's active-workspace id getter. Pass a function so it can read a reactive source. */
|
|
42
|
+
export function configureActiveWorkspace(resolver: WorkspaceResolver): void {
|
|
43
|
+
workspaceResolver = resolver;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The host's active workspace id, or null when no resolver is configured. */
|
|
47
|
+
export function getActiveWorkspaceId(): number | string | null {
|
|
48
|
+
return workspaceResolver ? workspaceResolver() : null;
|
|
49
|
+
}
|
package/host-ui.d.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// CANONICAL SDK type defs for the host UI kit (window.__KSERP_UI__, externalized
|
|
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
|
-
// source) gets it from the installed package via
|
|
5
|
-
// `/// <reference types="@kahitsan/ksui/host-ui" />`; there are no more
|
|
6
|
-
// per-plugin copies to drift. The host owns the runtime: its remote loader
|
|
7
|
-
// (kserp src/lib/remote-loader.ts) populates the global from the host's kit
|
|
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.
|
|
10
|
-
declare module "@kserp/host-ui" {
|
|
11
|
-
import type { JSX, Accessor } from "solid-js";
|
|
12
|
-
|
|
13
|
-
// --- Shared table types (mirror src/components/ui/DataTable/DataTable.tsx) ---
|
|
14
|
-
export interface DataTableRow {
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
-
[key: string]: any;
|
|
17
|
-
}
|
|
18
|
-
export interface DataTableColumn<T extends DataTableRow> {
|
|
19
|
-
data: (keyof T & string) | null;
|
|
20
|
-
title?: string;
|
|
21
|
-
render?: (
|
|
22
|
-
data: T[keyof T] | null,
|
|
23
|
-
type: "display",
|
|
24
|
-
row: T,
|
|
25
|
-
meta: { row: number; col: number; search: string },
|
|
26
|
-
) => JSX.Element | string;
|
|
27
|
-
orderable?: boolean;
|
|
28
|
-
className?: string;
|
|
29
|
-
}
|
|
30
|
-
export interface FetchResult<T> {
|
|
31
|
-
data: T[];
|
|
32
|
-
total: number;
|
|
33
|
-
}
|
|
34
|
-
export interface FetchParams {
|
|
35
|
-
page: number;
|
|
36
|
-
limit: number;
|
|
37
|
-
search: string;
|
|
38
|
-
sortBy: string | null;
|
|
39
|
-
sortDir: "asc" | "desc";
|
|
40
|
-
dateFilter: string | null;
|
|
41
|
-
dateFrom?: string | null;
|
|
42
|
-
dateTo?: string | null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface DataTableProps<T extends DataTableRow> {
|
|
46
|
-
columns?: DataTableColumn<T>[];
|
|
47
|
-
/**
|
|
48
|
-
* Generic data fetcher. When provided, the table runs in server-side mode.
|
|
49
|
-
* Optional: omit it and pass `data` for client-side mode.
|
|
50
|
-
*/
|
|
51
|
-
fetchFn?: (params: FetchParams) => Promise<FetchResult<T>>;
|
|
52
|
-
/** Static data (client-side mode). Ignored when `fetchFn` is set. */
|
|
53
|
-
data?: T[];
|
|
54
|
-
/**
|
|
55
|
-
* Expose the table's refetch handle to the parent. The callback receives
|
|
56
|
-
* `{ refetch, resetAndRefetch }`: `refetch()` re-fetches with the current
|
|
57
|
-
* state; `resetAndRefetch()` resets pagination to page 1 (clearing loadMore
|
|
58
|
-
* accumulators) before fetching.
|
|
59
|
-
*/
|
|
60
|
-
onRefetch?: (api: { refetch: () => void; resetAndRefetch: () => void }) => void;
|
|
61
|
-
// Remaining props are passed through; kept permissive so plugins can use the
|
|
62
|
-
// full surface of the host component without re-declaring it here.
|
|
63
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
-
[key: string]: any;
|
|
65
|
-
}
|
|
66
|
-
export function DataTable<T extends DataTableRow>(props: DataTableProps<T>): JSX.Element;
|
|
67
|
-
|
|
68
|
-
// --- DatePicker ---
|
|
69
|
-
export interface DateRangeValue {
|
|
70
|
-
start: string | null;
|
|
71
|
-
end: string | null;
|
|
72
|
-
}
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
-
export function DatePicker(props: any): JSX.Element;
|
|
75
|
-
|
|
76
|
-
// --- Modal ---
|
|
77
|
-
// The Modal is mounted/unmounted by the caller (wrap it in <Show when={open}>);
|
|
78
|
-
// there is no `open` prop. onClose fires on Escape / backdrop / dismissal.
|
|
79
|
-
export type ModalSize = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "5xl" | "7xl";
|
|
80
|
-
export type ModalTone = "default" | "danger";
|
|
81
|
-
export interface ModalProps {
|
|
82
|
-
onClose: () => void;
|
|
83
|
-
dismissable?: boolean;
|
|
84
|
-
variant?: "default" | "sheet";
|
|
85
|
-
size?: ModalSize;
|
|
86
|
-
tone?: ModalTone;
|
|
87
|
-
ariaLabel?: string;
|
|
88
|
-
children: JSX.Element;
|
|
89
|
-
}
|
|
90
|
-
export function Modal(props: ModalProps): JSX.Element;
|
|
91
|
-
|
|
92
|
-
// --- SearchableSelect ---
|
|
93
|
-
export interface SearchableOption {
|
|
94
|
-
value: string;
|
|
95
|
-
label: string;
|
|
96
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
-
[key: string]: any;
|
|
98
|
-
}
|
|
99
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
-
export function SearchableSelect(props: any): JSX.Element;
|
|
101
|
-
|
|
102
|
-
// --- Other components (permissive: full prop surface lives in the host) ---
|
|
103
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
-
export const Button: (props: any) => JSX.Element;
|
|
105
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
-
export const PageShell: (props: any) => JSX.Element;
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
-
export const PageTitle: (props: any) => JSX.Element;
|
|
109
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
-
export const PageShareButton: (props: any) => JSX.Element;
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
-
export const Avatar: (props: any) => JSX.Element;
|
|
113
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
-
export const PluginPageLoader: (props: any) => JSX.Element;
|
|
115
|
-
|
|
116
|
-
// --- Confirm ---
|
|
117
|
-
export function confirm(opts: {
|
|
118
|
-
title?: string;
|
|
119
|
-
message?: string;
|
|
120
|
-
confirmLabel?: string;
|
|
121
|
-
cancelLabel?: string;
|
|
122
|
-
danger?: boolean;
|
|
123
|
-
}): Promise<boolean>;
|
|
124
|
-
|
|
125
|
-
// --- Host hooks (run on the host's Solid runtime + context providers) ---
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
-
export function useActiveWorkspace(): any;
|
|
128
|
-
export function useCan(code: string): Accessor<boolean>;
|
|
129
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
130
|
-
export function usePermissions(): any;
|
|
131
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
-
export function PermissionGate(props: any): JSX.Element;
|
|
133
|
-
|
|
134
|
-
// --- Helpers ---
|
|
135
|
-
export function highlightMatch(text: string, query: string, markClass?: string): JSX.Element;
|
|
136
|
-
export function HighlightedText(props: {
|
|
137
|
-
text: string;
|
|
138
|
-
query: string;
|
|
139
|
-
markClass?: string;
|
|
140
|
-
}): JSX.Element;
|
|
141
|
-
export function matchesQuery(text: string | null | undefined, query: string): boolean;
|
|
142
|
-
export function matchesAny(query: string, ...fields: (string | null | undefined)[]): boolean;
|
|
143
|
-
export function useFocusTrap(el: HTMLElement | undefined): () => void;
|
|
144
|
-
export function autoFocusOnMount(el: HTMLElement | undefined): void;
|
|
145
|
-
}
|