@valentinkolb/cloud 0.1.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/package.json +69 -0
- package/public/logo.svg +1 -0
- package/scripts/build.ts +113 -0
- package/scripts/preload.ts +73 -0
- package/src/_internal/define-app.ts +399 -0
- package/src/_internal/heartbeat.ts +33 -0
- package/src/_internal/registry.ts +100 -0
- package/src/_internal/runtime-context.ts +38 -0
- package/src/api/accounts-entities.ts +134 -0
- package/src/api/admin-lifecycle.ts +210 -0
- package/src/api/auth/schemas.ts +28 -0
- package/src/api/auth.ts +230 -0
- package/src/api/index.ts +66 -0
- package/src/api/me.ts +206 -0
- package/src/api/search/schemas.ts +43 -0
- package/src/api/search.ts +130 -0
- package/src/clients/core.ts +19 -0
- package/src/config/env.ts +23 -0
- package/src/config/index.ts +6 -0
- package/src/config/ssr.ts +58 -0
- package/src/contracts/app.ts +140 -0
- package/src/contracts/index.ts +5 -0
- package/src/contracts/profile.ts +67 -0
- package/src/contracts/registry.ts +50 -0
- package/src/contracts/settings-types.ts +84 -0
- package/src/contracts/shared.ts +258 -0
- package/src/contracts/widgets.ts +121 -0
- package/src/index.ts +6 -0
- package/src/server/api/index.ts +1 -0
- package/src/server/api/respond.ts +55 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/index.ts +62 -0
- package/src/server/middleware/auth.ts +168 -0
- package/src/server/middleware/index.ts +7 -0
- package/src/server/middleware/middleware.ts +47 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +126 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/validator.ts +35 -0
- package/src/server/services/access.ts +294 -0
- package/src/server/services/freeipa/client.ts +100 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +78 -0
- package/src/server/services/freeipa/tls.ts +48 -0
- package/src/server/services/freeipa/util.ts +60 -0
- package/src/server/services/geo.ts +154 -0
- package/src/server/services/index.ts +28 -0
- package/src/server/services/services.ts +13 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +907 -0
- package/src/services/account-lifecycle/scheduler.ts +347 -0
- package/src/services/account-model.ts +21 -0
- package/src/services/accounts/app.ts +966 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +45 -0
- package/src/services/accounts/entities.ts +529 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +246 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +64 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +491 -0
- package/src/services/accounts/model.ts +135 -0
- package/src/services/accounts/switching.ts +117 -0
- package/src/services/accounts/users.ts +714 -0
- package/src/services/auth-flows/index.ts +6 -0
- package/src/services/auth-flows/ipa.ts +128 -0
- package/src/services/auth-flows/magic-link.ts +119 -0
- package/src/services/freeipa-config.ts +89 -0
- package/src/services/index.ts +46 -0
- package/src/services/ipa/auth.ts +122 -0
- package/src/services/ipa/groups.ts +684 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/profile.ts +90 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/sync.ts +740 -0
- package/src/services/ipa/users.ts +794 -0
- package/src/services/logging/index.ts +294 -0
- package/src/services/notifications/email.ts +123 -0
- package/src/services/notifications/index.ts +413 -0
- package/src/services/postgres.ts +51 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +13 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +255 -0
- package/src/services/session/index.ts +137 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +101 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +824 -0
- package/src/services/settings/index.ts +203 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +179 -0
- package/src/services/settings/templates.ts +10 -0
- package/src/services/weather/forecast.ts +287 -0
- package/src/services/weather/geo.ts +110 -0
- package/src/services/weather/index.ts +99 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +125 -0
- package/src/services/weather/migrate.ts +22 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/shared/account-display.ts +17 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/icons.ts +109 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +58 -0
- package/src/shared/markdown/extensions/images.ts +43 -0
- package/src/shared/markdown/extensions/info-blocks.ts +93 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +34 -0
- package/src/shared/markdown/extensions/tables.ts +88 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/index.ts +97 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/ssr/AdminLayout.tsx +42 -0
- package/src/ssr/AdminSidebar.tsx +95 -0
- package/src/ssr/Footer.island.tsx +62 -0
- package/src/ssr/GlobalSearchDialog.tsx +389 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
- package/src/ssr/Layout.tsx +326 -0
- package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
- package/src/ssr/NavMenu.island.tsx +108 -0
- package/src/ssr/ThemeToggleRail.island.tsx +27 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/SearchBar.island.tsx +77 -0
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/runtime.ts +22 -0
- package/src/styles/base-popover.css +28 -0
- package/src/styles/effects.css +65 -0
- package/src/styles/global.css +133 -0
- package/src/styles/input.css +54 -0
- package/src/styles/tokens.css +35 -0
- package/src/styles/utilities-buttons.css +125 -0
- package/src/styles/utilities-feedback.css +65 -0
- package/src/styles/utilities-layout.css +122 -0
- package/src/styles/utilities-navigation.css +196 -0
- package/src/types/ambient.d.ts +8 -0
- package/src/ui/admin-settings.tsx +148 -0
- package/src/ui/dialog-core.ts +146 -0
- package/src/ui/filter/FilterChip.tsx +196 -0
- package/src/ui/filter/index.ts +2 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/input/Checkbox.tsx +55 -0
- package/src/ui/input/ColorInput.tsx +122 -0
- package/src/ui/input/DateTimeInput.tsx +86 -0
- package/src/ui/input/ImageInput.tsx +170 -0
- package/src/ui/input/NumberInput.tsx +113 -0
- package/src/ui/input/PinInput.tsx +169 -0
- package/src/ui/input/SegmentedControl.tsx +99 -0
- package/src/ui/input/Select.tsx +288 -0
- package/src/ui/input/SelectChip.tsx +61 -0
- package/src/ui/input/Slider.tsx +118 -0
- package/src/ui/input/Switch.tsx +62 -0
- package/src/ui/input/TagsInput.tsx +115 -0
- package/src/ui/input/TextInput.tsx +160 -0
- package/src/ui/input/index.ts +13 -0
- package/src/ui/input/types.ts +42 -0
- package/src/ui/input/util.tsx +105 -0
- package/src/ui/ipa/Avatar.tsx +28 -0
- package/src/ui/ipa/GroupView.tsx +36 -0
- package/src/ui/ipa/LoginBtn.tsx +16 -0
- package/src/ui/ipa/UserView.tsx +58 -0
- package/src/ui/ipa/index.ts +4 -0
- package/src/ui/misc/ContextMenu.tsx +211 -0
- package/src/ui/misc/CopyButton.tsx +28 -0
- package/src/ui/misc/Dropdown.tsx +194 -0
- package/src/ui/misc/EntitySearch.tsx +213 -0
- package/src/ui/misc/Lightbox.tsx +194 -0
- package/src/ui/misc/LinkCard.tsx +34 -0
- package/src/ui/misc/LogEntriesTable.tsx +61 -0
- package/src/ui/misc/MarkdownView.tsx +65 -0
- package/src/ui/misc/Pagination.tsx +51 -0
- package/src/ui/misc/PermissionEditor.tsx +379 -0
- package/src/ui/misc/ProgressBar.tsx +47 -0
- package/src/ui/misc/RemoveBtn.tsx +27 -0
- package/src/ui/misc/StatCell.tsx +90 -0
- package/src/ui/misc/index.ts +18 -0
- package/src/ui/navigation.ts +32 -0
- package/src/ui/prompts.tsx +854 -0
- package/src/ui/sidebar.tsx +468 -0
- package/src/ui/widgets/Widget.tsx +62 -0
- package/src/ui/widgets/WidgetCard.tsx +19 -0
- package/src/ui/widgets/WidgetHero.tsx +39 -0
- package/src/ui/widgets/WidgetList.tsx +84 -0
- package/src/ui/widgets/WidgetPills.tsx +68 -0
- package/src/ui/widgets/WidgetStat.tsx +67 -0
- package/src/ui/widgets/WidgetStatus.tsx +62 -0
- package/src/ui/widgets/index.ts +9 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal dialog library for alert, confirm, prompt, and custom dialogs
|
|
3
|
+
* @module prompt-lib
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import CheckboxInput from "./input/Checkbox";
|
|
7
|
+
import DateTimeInput from "./input/DateTimeInput";
|
|
8
|
+
import ImageInput from "./input/ImageInput";
|
|
9
|
+
import NumberInput from "./input/NumberInput";
|
|
10
|
+
import PinInput from "./input/PinInput";
|
|
11
|
+
import SelectInput from "./input/Select";
|
|
12
|
+
import TagsInput from "./input/TagsInput";
|
|
13
|
+
import TextInput from "./input/TextInput";
|
|
14
|
+
import { mutation, timed } from "@valentinkolb/stdlib/solid";
|
|
15
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type JSX } from "solid-js";
|
|
16
|
+
import { createStore } from "solid-js/store";
|
|
17
|
+
import { dialogCore } from "./dialog-core";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration options for dialog appearance and behavior
|
|
21
|
+
*/
|
|
22
|
+
export interface DialogOptions {
|
|
23
|
+
/** Optional title displayed in the dialog header */
|
|
24
|
+
title?: string;
|
|
25
|
+
/** Optional icon class for header (e.g., "ti ti-trash") */
|
|
26
|
+
icon?: string;
|
|
27
|
+
/** Custom text for the confirm/OK button*/
|
|
28
|
+
confirmText?: string;
|
|
29
|
+
/** Custom text for the cancel button, or false to hide it*/
|
|
30
|
+
cancelText?: string | false;
|
|
31
|
+
/** Visual variant affecting button and outline colors */
|
|
32
|
+
variant?: "danger" | "primary" | "success";
|
|
33
|
+
/** Dialog size preset (default: "medium") */
|
|
34
|
+
size?: "small" | "medium" | "large";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PromptSearchItem<T = unknown> = {
|
|
38
|
+
label: string;
|
|
39
|
+
desc?: string;
|
|
40
|
+
icon?: string;
|
|
41
|
+
previewUrl?: string;
|
|
42
|
+
value?: T;
|
|
43
|
+
onClick?: (item: PromptSearchItem<T>) => void | Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type PromptSearchInput = {
|
|
47
|
+
query: string;
|
|
48
|
+
abortSignal: AbortSignal;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type PromptSearchOptions = DialogOptions & {
|
|
52
|
+
placeholder?: string;
|
|
53
|
+
icon?: string;
|
|
54
|
+
initialQuery?: string;
|
|
55
|
+
minQueryLength?: number;
|
|
56
|
+
debounceMs?: number;
|
|
57
|
+
emptyText?: string;
|
|
58
|
+
noResultsText?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Base field configuration shared by all field types
|
|
63
|
+
*/
|
|
64
|
+
type BaseField<T = any> = {
|
|
65
|
+
label?: string | false;
|
|
66
|
+
description?: string;
|
|
67
|
+
placeholder?: string;
|
|
68
|
+
required?: boolean;
|
|
69
|
+
default?: T;
|
|
70
|
+
validate?: (value: T | undefined) => string | null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Field schema for form inputs - discriminated union of all field types
|
|
75
|
+
*/
|
|
76
|
+
export type FieldSchema =
|
|
77
|
+
| (BaseField<string> & {
|
|
78
|
+
type: "text";
|
|
79
|
+
multiline?: boolean;
|
|
80
|
+
maxLength?: number;
|
|
81
|
+
minLength?: number;
|
|
82
|
+
icon?: string;
|
|
83
|
+
activeIcon?: string;
|
|
84
|
+
password?: boolean;
|
|
85
|
+
})
|
|
86
|
+
| (BaseField<number> & {
|
|
87
|
+
type: "number";
|
|
88
|
+
min?: number;
|
|
89
|
+
max?: number;
|
|
90
|
+
step?: number;
|
|
91
|
+
})
|
|
92
|
+
| (BaseField<string> & {
|
|
93
|
+
type: "image";
|
|
94
|
+
round?: boolean;
|
|
95
|
+
ariaLabel?: string;
|
|
96
|
+
})
|
|
97
|
+
| (BaseField<string> & {
|
|
98
|
+
type: "pin";
|
|
99
|
+
length?: number;
|
|
100
|
+
stretch?: boolean;
|
|
101
|
+
})
|
|
102
|
+
| (BaseField<string> & {
|
|
103
|
+
type: "select";
|
|
104
|
+
options: string[] | { id: string; label?: string; description?: string; icon?: string }[];
|
|
105
|
+
icon?: string;
|
|
106
|
+
activeIcon?: string;
|
|
107
|
+
clearable?: boolean;
|
|
108
|
+
})
|
|
109
|
+
| (BaseField<string[]> & {
|
|
110
|
+
type: "tags";
|
|
111
|
+
maxTags?: number;
|
|
112
|
+
minTags?: number;
|
|
113
|
+
icon?: string;
|
|
114
|
+
activeIcon?: string;
|
|
115
|
+
})
|
|
116
|
+
| (BaseField<boolean> & {
|
|
117
|
+
type: "boolean";
|
|
118
|
+
})
|
|
119
|
+
| (BaseField<string> & {
|
|
120
|
+
type: "datetime";
|
|
121
|
+
/** Use date-only input instead of datetime-local */
|
|
122
|
+
dateOnly?: boolean;
|
|
123
|
+
})
|
|
124
|
+
| {
|
|
125
|
+
type: "info";
|
|
126
|
+
content: string | JSX.Element | (() => JSX.Element);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract value type from field schema
|
|
131
|
+
*/
|
|
132
|
+
type InferFieldType<T extends FieldSchema> = T extends { type: "text" }
|
|
133
|
+
? string
|
|
134
|
+
: T extends { type: "number" }
|
|
135
|
+
? number
|
|
136
|
+
: T extends { type: "image" }
|
|
137
|
+
? string
|
|
138
|
+
: T extends { type: "pin" }
|
|
139
|
+
? string
|
|
140
|
+
: T extends { type: "select" }
|
|
141
|
+
? string
|
|
142
|
+
: T extends { type: "tags" }
|
|
143
|
+
? string[]
|
|
144
|
+
: T extends { type: "boolean" }
|
|
145
|
+
? boolean
|
|
146
|
+
: T extends { type: "datetime" }
|
|
147
|
+
? string
|
|
148
|
+
: T extends { type: "currency" }
|
|
149
|
+
? number
|
|
150
|
+
: T extends { type: "info" }
|
|
151
|
+
? never
|
|
152
|
+
: never;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Infer form values type from schema, excluding info fields
|
|
156
|
+
*/
|
|
157
|
+
type InferFormValues<T extends Record<string, FieldSchema>> = {
|
|
158
|
+
[K in keyof T as T[K] extends { type: "info" } ? never : K]: T[K] extends {
|
|
159
|
+
required: true;
|
|
160
|
+
}
|
|
161
|
+
? InferFieldType<T[K]>
|
|
162
|
+
: InferFieldType<T[K]> | undefined;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reusable form state management hook
|
|
167
|
+
* @param schema - Form field schema
|
|
168
|
+
* @returns Form state utilities
|
|
169
|
+
*/
|
|
170
|
+
export const createFormState = <T extends Record<string, FieldSchema>>(schema: T) => {
|
|
171
|
+
const [values, setValues] = createStore<any>({});
|
|
172
|
+
const [errors, setErrors] = createStore<Record<string, string>>({});
|
|
173
|
+
|
|
174
|
+
// Initialize with default values
|
|
175
|
+
Object.entries(schema).forEach(([key, field]) => {
|
|
176
|
+
if (field.type !== "info" && "default" in field) {
|
|
177
|
+
setValues(key, field.default);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Validate single field
|
|
182
|
+
const validateField = (key: string, value: any): string | null => {
|
|
183
|
+
const field = schema[key];
|
|
184
|
+
if (!field || field.type === "info") return null;
|
|
185
|
+
|
|
186
|
+
// Required check
|
|
187
|
+
if (field.required && (value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0))) {
|
|
188
|
+
return "required";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Custom validator
|
|
192
|
+
if ("validate" in field && field.validate) {
|
|
193
|
+
return field.validate(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Update field value and validation
|
|
200
|
+
const updateField = (key: string, value: any) => {
|
|
201
|
+
setValues(key, value);
|
|
202
|
+
const error = validateField(key, value);
|
|
203
|
+
setErrors(key, error || (undefined as any));
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Validate all fields
|
|
207
|
+
const validateAll = (): boolean => {
|
|
208
|
+
let isValid = true;
|
|
209
|
+
Object.entries(schema).forEach(([key, field]) => {
|
|
210
|
+
if (field.type !== "info") {
|
|
211
|
+
const error = validateField(key, values[key]);
|
|
212
|
+
if (error) {
|
|
213
|
+
setErrors(key, error);
|
|
214
|
+
isValid = false;
|
|
215
|
+
} else {
|
|
216
|
+
setErrors(key, undefined as any);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return isValid;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Reset to initial state
|
|
224
|
+
const reset = () => {
|
|
225
|
+
Object.entries(schema).forEach(([key, field]) => {
|
|
226
|
+
if (field.type !== "info") {
|
|
227
|
+
setValues(key, "default" in field ? field.default : undefined);
|
|
228
|
+
setErrors(key, undefined as any);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
values,
|
|
235
|
+
errors,
|
|
236
|
+
updateField,
|
|
237
|
+
validateAll,
|
|
238
|
+
reset,
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const DialogHeader = (props: { close: () => void; title?: string; icon?: string }) => {
|
|
243
|
+
const { title, icon, close } = props || {};
|
|
244
|
+
return (
|
|
245
|
+
<div class="flex flex-row items-center justify-start gap-4 border-b border-zinc-200 pb-2 dark:border-zinc-700">
|
|
246
|
+
{icon && <i class={`${icon}`} />}
|
|
247
|
+
{title && <p class="truncate font-semibold">{title}</p>}
|
|
248
|
+
<button type="button" onClick={() => close()} class="ti ti-x ml-auto" aria-label="close dialog" />
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const getSizeClassName = (size: DialogOptions["size"] = "medium") => {
|
|
254
|
+
if (size === "small") return "w-[min(90vw,22rem)] max-h-[72vh]";
|
|
255
|
+
if (size === "large") return "w-[min(96vw,48rem)] max-h-[86vh]";
|
|
256
|
+
return "w-[min(94vw,28rem)] max-h-[90vh]";
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const getVariantClassName = (variant?: DialogOptions["variant"]) => {
|
|
260
|
+
if (variant === "danger") return "ring-red-500/45 dark:ring-red-500/35";
|
|
261
|
+
if (variant === "success") return "ring-green-500/45 dark:ring-green-500/35";
|
|
262
|
+
return "ring-zinc-300/60 dark:ring-zinc-700/60";
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const getPanelClassName = (options?: Pick<DialogOptions, "variant" | "size">) => {
|
|
266
|
+
const sizeClass = getSizeClassName(options?.size);
|
|
267
|
+
const variantClass = getVariantClassName(options?.variant);
|
|
268
|
+
return `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 m-0 ${sizeClass} overflow-x-hidden overflow-y-auto rounded-2xl border-0 bg-white/95 p-4 text-zinc-900 shadow-none ring-1 ring-inset ${variantClass} backdrop:bg-black/45 dark:backdrop:bg-black/35 backdrop:backdrop-blur-sm dark:bg-zinc-950/95 dark:text-zinc-100`;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const getSearchPanelClassName = () =>
|
|
272
|
+
"fixed left-1/2 top-[25vh] -translate-x-1/2 m-0 w-[min(96vw,46rem)] h-[50vh] border-0 bg-transparent p-0 text-zinc-900 shadow-none backdrop:bg-black/45 dark:backdrop:bg-black/35 backdrop:backdrop-blur-sm dark:text-zinc-100 [@media(min-height:1100px)]:top-[33vh] [@media(min-height:1100px)]:h-[33vh]";
|
|
273
|
+
|
|
274
|
+
const isPreviewUrl = (value?: string) => typeof value === "string" && value.startsWith("/");
|
|
275
|
+
|
|
276
|
+
const openSearchPrompt = <T = unknown>(
|
|
277
|
+
resolver: (input: PromptSearchInput) => Promise<PromptSearchItem<T>[]> | PromptSearchItem<T>[],
|
|
278
|
+
options?: PromptSearchOptions,
|
|
279
|
+
) =>
|
|
280
|
+
dialogCore.open<PromptSearchItem<T>>((close) => {
|
|
281
|
+
const [query, setQuery] = createSignal(options?.initialQuery ?? "");
|
|
282
|
+
const [items, setItems] = createSignal<PromptSearchItem<T>[]>([]);
|
|
283
|
+
const [activeIndex, setActiveIndex] = createSignal(0);
|
|
284
|
+
const [hasLoaded, setHasLoaded] = createSignal(false);
|
|
285
|
+
const [failedPreviews, setFailedPreviews] = createStore<Record<number, true>>({});
|
|
286
|
+
const [activeSearchQuery, setActiveSearchQuery] = createSignal("");
|
|
287
|
+
|
|
288
|
+
const rowRefs = new Map<number, HTMLButtonElement>();
|
|
289
|
+
let inputRef: HTMLInputElement | undefined;
|
|
290
|
+
|
|
291
|
+
const minQueryLength = options?.minQueryLength ?? 0;
|
|
292
|
+
const debounceMs = options?.debounceMs ?? 180;
|
|
293
|
+
const searchMutation = mutation.create<
|
|
294
|
+
{
|
|
295
|
+
query: string;
|
|
296
|
+
items: PromptSearchItem<T>[];
|
|
297
|
+
},
|
|
298
|
+
string,
|
|
299
|
+
{ requestQuery: string }
|
|
300
|
+
>({
|
|
301
|
+
onBefore: (requestQuery) => ({ requestQuery }),
|
|
302
|
+
mutation: async (requestQuery, ctx) => {
|
|
303
|
+
const result = await resolver({
|
|
304
|
+
query: requestQuery,
|
|
305
|
+
abortSignal: ctx.abortSignal,
|
|
306
|
+
});
|
|
307
|
+
return { query: requestQuery, items: (result ?? []).slice() };
|
|
308
|
+
},
|
|
309
|
+
onSuccess: (result, ctx) => {
|
|
310
|
+
if (!ctx || ctx.requestQuery !== activeSearchQuery()) return;
|
|
311
|
+
setItems(result.items);
|
|
312
|
+
setActiveIndex(0);
|
|
313
|
+
setHasLoaded(true);
|
|
314
|
+
},
|
|
315
|
+
onError: (err, ctx) => {
|
|
316
|
+
if (!ctx || ctx.requestQuery !== activeSearchQuery()) return;
|
|
317
|
+
if (err.name === "AbortError") return;
|
|
318
|
+
setItems([]);
|
|
319
|
+
setActiveIndex(0);
|
|
320
|
+
setHasLoaded(true);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const searchError = createMemo(() => {
|
|
324
|
+
const err = searchMutation.error();
|
|
325
|
+
if (!err || err.name === "AbortError") return null;
|
|
326
|
+
return err.message || "Search failed.";
|
|
327
|
+
});
|
|
328
|
+
const shouldShowResults = createMemo(() => {
|
|
329
|
+
if (query().trim().length < minQueryLength) return false;
|
|
330
|
+
return hasLoaded() || searchError() !== null || items().length > 0;
|
|
331
|
+
});
|
|
332
|
+
const emptyStateText = createMemo(() => {
|
|
333
|
+
if (!hasLoaded()) return options?.emptyText ?? "Type to search.";
|
|
334
|
+
return options?.noResultsText ?? "No results.";
|
|
335
|
+
});
|
|
336
|
+
const getItemClassName = (isActive: boolean) =>
|
|
337
|
+
`flex w-full items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors ${
|
|
338
|
+
isActive
|
|
339
|
+
? "bg-blue-50/80 text-blue-900 dark:bg-blue-950/45 dark:text-blue-100"
|
|
340
|
+
: "hover:bg-zinc-200/65 dark:hover:bg-zinc-800/70"
|
|
341
|
+
}`;
|
|
342
|
+
const { debouncedFn: debounceSearch, cancel: cancelDebounce } = timed.debounce((nextQuery: string) => {
|
|
343
|
+
setActiveSearchQuery(nextQuery);
|
|
344
|
+
searchMutation.abort();
|
|
345
|
+
void searchMutation.mutate(nextQuery);
|
|
346
|
+
}, debounceMs);
|
|
347
|
+
|
|
348
|
+
const execute = async (item?: PromptSearchItem<T>) => {
|
|
349
|
+
if (!item) return;
|
|
350
|
+
if (item.onClick) await item.onClick(item);
|
|
351
|
+
close(item);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const moveSelection = (delta: -1 | 1) => {
|
|
355
|
+
const list = items();
|
|
356
|
+
if (list.length === 0) return;
|
|
357
|
+
const next = (activeIndex() + delta + list.length) % list.length;
|
|
358
|
+
setActiveIndex(next);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
createEffect(() => {
|
|
362
|
+
const list = items();
|
|
363
|
+
const maxIndex = list.length - 1;
|
|
364
|
+
if (maxIndex < 0) {
|
|
365
|
+
setActiveIndex(0);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (activeIndex() > maxIndex) setActiveIndex(maxIndex);
|
|
369
|
+
rowRefs.get(activeIndex())?.scrollIntoView({ block: "nearest" });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
createEffect(() => {
|
|
373
|
+
const nextQuery = query().trim();
|
|
374
|
+
setFailedPreviews({});
|
|
375
|
+
|
|
376
|
+
if (nextQuery.length < minQueryLength) {
|
|
377
|
+
cancelDebounce();
|
|
378
|
+
searchMutation.abort();
|
|
379
|
+
setItems([]);
|
|
380
|
+
setActiveIndex(0);
|
|
381
|
+
setHasLoaded(false);
|
|
382
|
+
setActiveSearchQuery("");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
debounceSearch(nextQuery);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
onCleanup(() => {
|
|
390
|
+
cancelDebounce();
|
|
391
|
+
searchMutation.abort();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div class="flex h-full min-h-0 flex-col gap-2 pb-1 [--search-body-max:calc(50vh-3.5rem)] [@media(min-height:1100px)]:[--search-body-max:calc(33vh-3.5rem)]">
|
|
396
|
+
<Show when={options?.title}>
|
|
397
|
+
{(title) => (
|
|
398
|
+
<p class="px-1 text-base font-semibold text-white dark:text-zinc-100">
|
|
399
|
+
{title()}
|
|
400
|
+
</p>
|
|
401
|
+
)}
|
|
402
|
+
</Show>
|
|
403
|
+
|
|
404
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl bg-white/95 text-zinc-900 shadow-none ring-1 ring-inset ring-zinc-300/60 dark:bg-zinc-950/95 dark:text-zinc-100 dark:ring-zinc-700/60">
|
|
405
|
+
<label class="flex items-center gap-2 px-3 py-2.5">
|
|
406
|
+
<i class={`${options?.icon ?? "ti ti-search"} text-dimmed`} />
|
|
407
|
+
<input
|
|
408
|
+
ref={inputRef}
|
|
409
|
+
type="search"
|
|
410
|
+
value={query()}
|
|
411
|
+
onInput={(event) => setQuery(event.currentTarget.value)}
|
|
412
|
+
onKeyDown={(event) => {
|
|
413
|
+
if (event.key === "ArrowDown") {
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
moveSelection(1);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (event.key === "ArrowUp") {
|
|
419
|
+
event.preventDefault();
|
|
420
|
+
moveSelection(-1);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (event.key === "Enter") {
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
void execute(items()[activeIndex()]);
|
|
426
|
+
}
|
|
427
|
+
}}
|
|
428
|
+
placeholder={options?.placeholder ?? "Search..."}
|
|
429
|
+
class="w-full border-0 bg-transparent text-sm outline-none placeholder:text-dimmed"
|
|
430
|
+
spellcheck={false}
|
|
431
|
+
autocapitalize="off"
|
|
432
|
+
autocomplete="off"
|
|
433
|
+
autocorrect="off"
|
|
434
|
+
/>
|
|
435
|
+
<Show when={searchMutation.loading()}>
|
|
436
|
+
<i class="ti ti-loader-2 animate-spin text-dimmed" />
|
|
437
|
+
</Show>
|
|
438
|
+
</label>
|
|
439
|
+
|
|
440
|
+
<div
|
|
441
|
+
class="overflow-hidden transition-[height,opacity] duration-200 ease-out"
|
|
442
|
+
style={{
|
|
443
|
+
height: shouldShowResults() ? "var(--search-body-max)" : "0px",
|
|
444
|
+
opacity: shouldShowResults() ? "1" : "0",
|
|
445
|
+
}}
|
|
446
|
+
>
|
|
447
|
+
<div class="h-full min-h-0 overflow-y-auto overscroll-y-contain px-2 pb-2" onWheel={(event) => event.stopPropagation()}>
|
|
448
|
+
<Show when={searchError()}>{(message) => <div class="info-block-danger mb-2 text-xs">{message()}</div>}</Show>
|
|
449
|
+
|
|
450
|
+
<Show when={items().length > 0} fallback={<p class="px-1.5 py-2 text-xs text-dimmed">{emptyStateText()}</p>}>
|
|
451
|
+
<div class="flex flex-col gap-1">
|
|
452
|
+
<For each={items()}>
|
|
453
|
+
{(item, index) => (
|
|
454
|
+
<button
|
|
455
|
+
ref={(element) => {
|
|
456
|
+
if (!element) {
|
|
457
|
+
rowRefs.delete(index());
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
rowRefs.set(index(), element);
|
|
461
|
+
}}
|
|
462
|
+
type="button"
|
|
463
|
+
onMouseEnter={() => setActiveIndex(index())}
|
|
464
|
+
onClick={() => void execute(item)}
|
|
465
|
+
class={getItemClassName(activeIndex() === index())}
|
|
466
|
+
>
|
|
467
|
+
<Show when={isPreviewUrl(item.previewUrl) || item.icon}>
|
|
468
|
+
<span class="mt-0.5 grid h-7 w-7 shrink-0 place-items-center overflow-hidden rounded-md bg-zinc-200/80 dark:bg-zinc-800/80">
|
|
469
|
+
<Show
|
|
470
|
+
when={isPreviewUrl(item.previewUrl) && !failedPreviews[index()]}
|
|
471
|
+
fallback={<i class={`${item.icon ?? "ti ti-file"} text-xs text-dimmed`} />}
|
|
472
|
+
>
|
|
473
|
+
<img
|
|
474
|
+
src={item.previewUrl}
|
|
475
|
+
alt={item.label}
|
|
476
|
+
class="h-full w-full object-cover"
|
|
477
|
+
onError={() => setFailedPreviews(index(), true)}
|
|
478
|
+
/>
|
|
479
|
+
</Show>
|
|
480
|
+
</span>
|
|
481
|
+
</Show>
|
|
482
|
+
|
|
483
|
+
<div class="min-w-0 flex-1">
|
|
484
|
+
<p class="truncate text-sm leading-5">{item.label}</p>
|
|
485
|
+
<Show when={item.desc}>
|
|
486
|
+
<p class="mt-0.5 truncate text-xs leading-4 text-dimmed">{item.desc}</p>
|
|
487
|
+
</Show>
|
|
488
|
+
</div>
|
|
489
|
+
</button>
|
|
490
|
+
)}
|
|
491
|
+
</For>
|
|
492
|
+
</div>
|
|
493
|
+
</Show>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
);
|
|
499
|
+
}, {
|
|
500
|
+
panelClassName: getSearchPanelClassName(),
|
|
501
|
+
contentClassName: "h-full min-h-0 p-0",
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Simple dialog utilities for user interactions
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```typescript
|
|
509
|
+
* // Simple alert
|
|
510
|
+
* await prompts.alert("File saved!");
|
|
511
|
+
*
|
|
512
|
+
* // Confirmation dialog
|
|
513
|
+
* const confirmed = await prompts.confirm("Delete this item?");
|
|
514
|
+
*
|
|
515
|
+
* // Text input
|
|
516
|
+
* const name = await prompts.prompt("Enter your name:");
|
|
517
|
+
*
|
|
518
|
+
* // Number input
|
|
519
|
+
* const age = await prompts.promptNumber("Enter your age:", 25);
|
|
520
|
+
*
|
|
521
|
+
* // Dynamic form with schema
|
|
522
|
+
* const values = await prompts.form({
|
|
523
|
+
* title: 'User Registration',
|
|
524
|
+
* icon: 'ti ti-user-plus',
|
|
525
|
+
* fields: {
|
|
526
|
+
* name: { type: 'text', required: true },
|
|
527
|
+
* age: { type: 'number', min: 18 },
|
|
528
|
+
* country: { type: 'select', options: ['DE', 'AT', 'CH'] },
|
|
529
|
+
* interests: { type: 'tags' },
|
|
530
|
+
* avatar: { type: 'image', round: true },
|
|
531
|
+
* price: { type: 'currency', min: 100 },
|
|
532
|
+
* pin: { type: 'pin', length: 4 },
|
|
533
|
+
* agree: { type: 'boolean', label: 'I agree to terms', required: true }
|
|
534
|
+
* }
|
|
535
|
+
* });
|
|
536
|
+
*
|
|
537
|
+
* // Custom dialog with SolidJS component
|
|
538
|
+
* const result = await prompts.dialog<boolean>((close) => (
|
|
539
|
+
* <div>
|
|
540
|
+
* <p>Custom content here</p>
|
|
541
|
+
* <button onClick={() => close(true)}>OK</button>
|
|
542
|
+
* </div>
|
|
543
|
+
* ));
|
|
544
|
+
*
|
|
545
|
+
* // Error dialog with danger variant
|
|
546
|
+
* await prompts.error("Something went wrong!");
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
export const prompts = {
|
|
550
|
+
/**
|
|
551
|
+
* Display an alert dialog with a single OK button
|
|
552
|
+
* @param content - Message to display (supports HTML)
|
|
553
|
+
* @param options - Optional styling and text configuration
|
|
554
|
+
* @returns Promise that resolves when dialog is closed
|
|
555
|
+
*/
|
|
556
|
+
alert: (content: string | HTMLElement | JSX.Element, options?: DialogOptions) =>
|
|
557
|
+
dialogCore.open(
|
|
558
|
+
(close) => (
|
|
559
|
+
<div>
|
|
560
|
+
<DialogHeader title={options?.title || "Info"} icon={options?.icon} close={close} />
|
|
561
|
+
|
|
562
|
+
<div class="font-xs py-4 text-sm whitespace-pre-wrap">{content}</div>
|
|
563
|
+
|
|
564
|
+
<div class="flex justify-end gap-3">
|
|
565
|
+
<button
|
|
566
|
+
onClick={() => close()}
|
|
567
|
+
class={`${
|
|
568
|
+
options?.variant === "danger" ? "btn-danger" : options?.variant === "success" ? "btn-success" : "btn-primary"
|
|
569
|
+
} btn-sm`}
|
|
570
|
+
>
|
|
571
|
+
{options?.confirmText || "OK"}
|
|
572
|
+
</button>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
),
|
|
576
|
+
{
|
|
577
|
+
panelClassName: getPanelClassName(options),
|
|
578
|
+
},
|
|
579
|
+
),
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Display a success dialog with a single OK button.
|
|
583
|
+
*/
|
|
584
|
+
success: (content: string | HTMLElement | JSX.Element, options?: Omit<DialogOptions, "variant">) =>
|
|
585
|
+
prompts.alert(content, {
|
|
586
|
+
...options,
|
|
587
|
+
variant: "success",
|
|
588
|
+
title: options?.title ?? "Success",
|
|
589
|
+
icon: options?.icon ?? "ti ti-check",
|
|
590
|
+
}),
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Display a confirmation dialog with OK and Cancel buttons
|
|
594
|
+
* @param content - Question/message to display
|
|
595
|
+
* @param options - Optional styling and text configuration
|
|
596
|
+
* @returns Promise resolving to true if confirmed, false if cancelled
|
|
597
|
+
*/
|
|
598
|
+
confirm: (content: string | HTMLElement | JSX.Element, options?: DialogOptions) =>
|
|
599
|
+
dialogCore.open<boolean>(
|
|
600
|
+
(close) => (
|
|
601
|
+
<div>
|
|
602
|
+
<DialogHeader title={options?.title} icon={options?.icon} close={() => close(false)} />
|
|
603
|
+
|
|
604
|
+
<div class="font-xs py-4 text-sm whitespace-pre-wrap">{content}</div>
|
|
605
|
+
|
|
606
|
+
<div class="flex justify-end gap-3">
|
|
607
|
+
<button type="button" onClick={() => close(false)} class="btn-secondary btn-sm">
|
|
608
|
+
{options?.cancelText || "Nope"}
|
|
609
|
+
</button>
|
|
610
|
+
<button
|
|
611
|
+
type="button"
|
|
612
|
+
onClick={() => close(true)}
|
|
613
|
+
class={`${
|
|
614
|
+
options?.variant === "danger" ? "btn-danger" : options?.variant === "success" ? "btn-success" : "btn-primary"
|
|
615
|
+
} btn-sm`}
|
|
616
|
+
>
|
|
617
|
+
{options?.confirmText || "Yees"}
|
|
618
|
+
</button>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
),
|
|
622
|
+
{
|
|
623
|
+
panelClassName: getPanelClassName(options),
|
|
624
|
+
},
|
|
625
|
+
),
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Display a prompt dialog with text input
|
|
629
|
+
* @param content - Prompt message
|
|
630
|
+
* @param defaultValue - Initial value for the input field
|
|
631
|
+
* @param options - Optional styling and text configuration
|
|
632
|
+
* @returns Promise resolving to entered text (empty string is possible), or null if dialog was cancelled
|
|
633
|
+
*/
|
|
634
|
+
prompt: (content: string, defaultValue?: string, options?: DialogOptions) =>
|
|
635
|
+
prompts
|
|
636
|
+
.form({
|
|
637
|
+
...options,
|
|
638
|
+
fields: {
|
|
639
|
+
message: {
|
|
640
|
+
type: "info",
|
|
641
|
+
content: () => <div class="font-xs text-sm">{content}</div>,
|
|
642
|
+
},
|
|
643
|
+
value: {
|
|
644
|
+
type: "text",
|
|
645
|
+
label: false,
|
|
646
|
+
default: defaultValue || "",
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
})
|
|
650
|
+
.then((result) => result?.value ?? null),
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Display a prompt dialog with number input
|
|
654
|
+
* @param content - Prompt message
|
|
655
|
+
* @param defaultValue - Initial value for the input field
|
|
656
|
+
* @param options - Optional styling and text configuration
|
|
657
|
+
* @returns Promise resolving to entered number, or null if cancelled/empty
|
|
658
|
+
*/
|
|
659
|
+
promptNumber: async (
|
|
660
|
+
content: string,
|
|
661
|
+
defaultValue?: number,
|
|
662
|
+
options?: DialogOptions & {
|
|
663
|
+
min?: number;
|
|
664
|
+
max?: number;
|
|
665
|
+
},
|
|
666
|
+
) =>
|
|
667
|
+
prompts
|
|
668
|
+
.form({
|
|
669
|
+
...options,
|
|
670
|
+
fields: {
|
|
671
|
+
message: {
|
|
672
|
+
type: "info",
|
|
673
|
+
content: () => <div class="font-xs text-sm">{content}</div>,
|
|
674
|
+
},
|
|
675
|
+
value: {
|
|
676
|
+
type: "number",
|
|
677
|
+
label: false,
|
|
678
|
+
default: defaultValue || 0,
|
|
679
|
+
min: options?.min,
|
|
680
|
+
max: options?.max,
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
})
|
|
684
|
+
.then((result) => result?.value ?? null),
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Build and display a dynamic form from schema
|
|
688
|
+
* @param config - Form configuration with title, icon, and fields
|
|
689
|
+
* @returns Promise resolving to form values or null if cancelled
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```typescript
|
|
693
|
+
* const values = await prompts.form({
|
|
694
|
+
* title: 'User Form',
|
|
695
|
+
* icon: 'ti ti-user',
|
|
696
|
+
* fields: {
|
|
697
|
+
* name: { type: 'text', required: true },
|
|
698
|
+
* age: { type: 'number', min: 18 }
|
|
699
|
+
* }
|
|
700
|
+
* });
|
|
701
|
+
* ```
|
|
702
|
+
*/
|
|
703
|
+
form: <T extends Record<string, FieldSchema>>(config: {
|
|
704
|
+
title?: string;
|
|
705
|
+
icon?: string;
|
|
706
|
+
fields: T;
|
|
707
|
+
confirmText?: string;
|
|
708
|
+
cancelText?: string | false;
|
|
709
|
+
variant?: "danger" | "primary" | "success";
|
|
710
|
+
size?: "small" | "medium" | "large";
|
|
711
|
+
}): Promise<InferFormValues<T> | null> => {
|
|
712
|
+
return dialogCore.open<InferFormValues<T> | null>((close) => {
|
|
713
|
+
const state = createFormState(config.fields);
|
|
714
|
+
|
|
715
|
+
// Field renderer map
|
|
716
|
+
const fieldRenderers: Record<string, (props: any, field: any) => JSX.Element> = {
|
|
717
|
+
text: (props, field) => (
|
|
718
|
+
<TextInput {...props} multiline={field.multiline} icon={field.icon} activeIcon={field.activeIcon} password={field.password} />
|
|
719
|
+
),
|
|
720
|
+
number: (props, field) => <NumberInput {...props} min={field.min} max={field.max} step={field.step} />,
|
|
721
|
+
image: (props, field) => <ImageInput {...props} round={field.round} ariaLabel={field.ariaLabel} />,
|
|
722
|
+
pin: (props, field) => <PinInput {...props} length={field.length} stretch={field.stretch} />,
|
|
723
|
+
select: (props, field) => (
|
|
724
|
+
<SelectInput {...props} options={field.options} icon={field.icon} activeIcon={field.activeIcon} clearable={field.clearable} />
|
|
725
|
+
),
|
|
726
|
+
tags: (props, field) => <TagsInput {...props} icon={field.icon} activeIcon={field.activeIcon} />,
|
|
727
|
+
boolean: (props) => <CheckboxInput {...props} />,
|
|
728
|
+
datetime: (props, field) => <DateTimeInput {...props} dateOnly={field.dateOnly} />,
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Handle form submission
|
|
732
|
+
const handleSubmit = (e: Event) => {
|
|
733
|
+
e.preventDefault();
|
|
734
|
+
if (state.validateAll()) {
|
|
735
|
+
close(state.values as InferFormValues<T>);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// Determine button variant class
|
|
740
|
+
const submitButtonClass = config.variant === "danger" ? "btn-danger" : config.variant === "success" ? "btn-success" : "btn-primary";
|
|
741
|
+
|
|
742
|
+
return (
|
|
743
|
+
<form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
|
744
|
+
<DialogHeader title={config.title} icon={config.icon} close={() => close(null)} />
|
|
745
|
+
|
|
746
|
+
<div class="flex flex-col gap-4">
|
|
747
|
+
<For each={Object.entries(config.fields)}>
|
|
748
|
+
{([key, field]) => {
|
|
749
|
+
// Info field - just display content
|
|
750
|
+
if (field.type === "info") {
|
|
751
|
+
return (
|
|
752
|
+
<div>
|
|
753
|
+
{typeof field.content === "string" ? (
|
|
754
|
+
<p class="text-sm text-zinc-600 dark:text-zinc-400">{field.content}</p>
|
|
755
|
+
) : typeof field.content === "function" ? (
|
|
756
|
+
field.content()
|
|
757
|
+
) : (
|
|
758
|
+
field.content
|
|
759
|
+
)}
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Regular input fields
|
|
765
|
+
// Handle label: false or undefined means no label, otherwise use provided label
|
|
766
|
+
const label = field.label || undefined;
|
|
767
|
+
const commonProps = {
|
|
768
|
+
label,
|
|
769
|
+
description: field.description,
|
|
770
|
+
placeholder: field.placeholder,
|
|
771
|
+
required: field.required,
|
|
772
|
+
value: () => state.values[key],
|
|
773
|
+
onChange: (v: any) => state.updateField(key, v),
|
|
774
|
+
error: () => state.errors[key],
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
return fieldRenderers[field.type]?.(commonProps, field);
|
|
778
|
+
}}
|
|
779
|
+
</For>
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
<div class="flex justify-end gap-3">
|
|
783
|
+
<Show when={config.cancelText !== false}>
|
|
784
|
+
<button type="button" onClick={() => close(null)} class="btn-secondary btn-sm">
|
|
785
|
+
{config.cancelText || "ESC"}
|
|
786
|
+
</button>
|
|
787
|
+
</Show>
|
|
788
|
+
<button type="submit" class={`${submitButtonClass} btn-sm`}>
|
|
789
|
+
{config.confirmText || "ENTER"}
|
|
790
|
+
</button>
|
|
791
|
+
</div>
|
|
792
|
+
</form>
|
|
793
|
+
);
|
|
794
|
+
}, {
|
|
795
|
+
panelClassName: getPanelClassName(config),
|
|
796
|
+
}) as Promise<InferFormValues<T> | null>;
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Display a custom dialog with a SolidJS component
|
|
801
|
+
* @param componentFactory - Function that receives close callback and returns JSX
|
|
802
|
+
* @param options - Optional dialog options for header (title, icon, variant)
|
|
803
|
+
* @returns Promise resolving to the result passed to close, or undefined if cancelled
|
|
804
|
+
*
|
|
805
|
+
* @example
|
|
806
|
+
* ```typescript
|
|
807
|
+
* const confirmed = await prompts.dialog<boolean>((close) => (
|
|
808
|
+
* <p class="mb-4">Custom content here</p>
|
|
809
|
+
* ), { title: "My Dialog", icon: "ti ti-info-circle" });
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
dialog: <T = any>(component: (close: (result?: T) => void) => JSX.Element, options?: DialogOptions) =>
|
|
813
|
+
dialogCore.open<T>(
|
|
814
|
+
(close: (result?: T) => void) => (
|
|
815
|
+
<div class="flex flex-col gap-4">
|
|
816
|
+
<DialogHeader title={options?.title} icon={options?.icon} close={() => close(undefined)} />
|
|
817
|
+
{component(close)}
|
|
818
|
+
</div>
|
|
819
|
+
),
|
|
820
|
+
{
|
|
821
|
+
panelClassName: getPanelClassName(options),
|
|
822
|
+
},
|
|
823
|
+
),
|
|
824
|
+
|
|
825
|
+
search: openSearchPrompt,
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Wrapper around the alert dialog with error styling and icon
|
|
829
|
+
* @param content - Error message to display
|
|
830
|
+
* @param options - Optional styling and text configuration
|
|
831
|
+
* @returns Promise that resolves when dialog is closed
|
|
832
|
+
*/
|
|
833
|
+
error: (content: string | HTMLElement, options?: DialogOptions) =>
|
|
834
|
+
dialogCore.open(
|
|
835
|
+
(close) => (
|
|
836
|
+
<div>
|
|
837
|
+
<DialogHeader title={options?.title ?? "Uuups"} icon={options?.icon ?? "ti ti-alert-circle"} close={close} />
|
|
838
|
+
|
|
839
|
+
<div class="font-xs p-4 text-sm">{content}</div>
|
|
840
|
+
|
|
841
|
+
<div class="flex justify-end gap-3">
|
|
842
|
+
<button onClick={() => close()} class="btn-primary btn-sm">
|
|
843
|
+
{options?.confirmText || "Ok .. me sad now"}
|
|
844
|
+
</button>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
),
|
|
848
|
+
{
|
|
849
|
+
panelClassName: getPanelClassName({ ...options, variant: "danger" }),
|
|
850
|
+
},
|
|
851
|
+
),
|
|
852
|
+
|
|
853
|
+
getDialogElement: () => (typeof document === "undefined" ? undefined : document.querySelector<HTMLDialogElement>("dialog")),
|
|
854
|
+
};
|