@valentinkolb/cloud 0.4.0 → 0.5.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 +18 -6
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +53 -46
- package/src/api/accounts-entities.ts +4 -0
- package/src/api/admin-core-settings.ts +98 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/auth/schemas.ts +24 -0
- package/src/api/auth.ts +113 -10
- package/src/api/index.ts +7 -2
- package/src/api/me.ts +203 -14
- package/src/api/search/schemas.ts +1 -0
- package/src/api/search.ts +62 -8
- package/src/config/ssr.ts +2 -9
- package/src/contracts/announcements.test.ts +37 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +108 -1
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/server/api/index.ts +1 -1
- package/src/server/api/respond.ts +50 -10
- package/src/server/index.ts +44 -38
- package/src/server/middleware/auth.ts +98 -9
- package/src/server/middleware/index.ts +2 -1
- package/src/server/middleware/settings.ts +26 -0
- package/src/server/services/access.test.ts +197 -0
- package/src/server/services/access.ts +254 -6
- package/src/server/services/index.ts +14 -11
- package/src/server/services/pagination.ts +22 -0
- package/src/server/time.ts +45 -0
- package/src/services/account-lifecycle/index.ts +142 -18
- package/src/services/accounts/app.ts +658 -170
- package/src/services/accounts/authz.test.ts +77 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/entities.ts +84 -5
- package/src/services/accounts/groups.ts +30 -24
- package/src/services/accounts/model.test.ts +30 -0
- package/src/services/accounts/switching.test.ts +14 -0
- package/src/services/accounts/switching.ts +15 -6
- package/src/services/accounts/users.ts +75 -52
- package/src/services/announcements/index.test.ts +32 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/audit/index.test.ts +84 -0
- package/src/services/audit/index.ts +431 -0
- package/src/services/auth-flows/index.ts +9 -2
- package/src/services/auth-flows/ipa.ts +0 -2
- package/src/services/auth-flows/magic-link.ts +3 -2
- package/src/services/auth-flows/password-reset.ts +284 -0
- package/src/services/auth-flows/proxy-return.test.ts +24 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/gateway.ts +162 -0
- package/src/services/index.ts +44 -2
- package/src/services/ipa/effective-groups.test.ts +33 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/profile.ts +45 -3
- package/src/services/ipa/search.ts +3 -5
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.test.ts +32 -0
- package/src/services/ipa/sync-planning.ts +22 -0
- package/src/services/ipa/sync.ts +110 -38
- package/src/services/oauth-tokens.ts +104 -0
- package/src/services/postgres.ts +21 -6
- package/src/services/providers/local/auth.test.ts +22 -0
- package/src/services/providers/local/auth.ts +46 -3
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.test.ts +210 -0
- package/src/services/service-account-credentials.ts +715 -0
- package/src/services/service-accounts.ts +188 -0
- package/src/services/session/index.ts +7 -8
- package/src/services/settings/app.ts +4 -20
- package/src/services/settings/defaults.ts +64 -22
- package/src/services/settings/store.ts +47 -0
- package/src/services/weather/forecast.ts +40 -7
- package/src/services/webauthn.test.ts +36 -0
- package/src/services/webauthn.ts +384 -0
- package/src/shared/icons.ts +391 -100
- package/src/shared/index.ts +7 -0
- package/src/shared/markdown/extensions/code.ts +38 -1
- package/src/shared/markdown/extensions/images.ts +39 -3
- package/src/shared/markdown/extensions/info-blocks.ts +5 -5
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +79 -58
- package/src/shared/markdown/formula.test.ts +1089 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +76 -2
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/redirect.test.ts +49 -0
- package/src/shared/redirect.ts +52 -0
- package/src/shared/theme.test.ts +24 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +7 -3
- package/src/ssr/AdminSidebar.tsx +115 -49
- package/src/ssr/AppLaunchpad.island.tsx +176 -0
- package/src/ssr/Footer.island.tsx +3 -8
- package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
- package/src/ssr/GlobalSearchDialog.tsx +545 -117
- package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
- package/src/ssr/Layout.tsx +74 -66
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutHelp.tsx +266 -0
- package/src/ssr/NavMenu.island.tsx +0 -39
- package/src/ssr/ThemeToggleRail.island.tsx +3 -3
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/islands/index.ts +13 -0
- package/src/styles/base-popover.css +5 -2
- package/src/styles/effects.css +87 -6
- package/src/styles/global.css +146 -9
- package/src/styles/input.css +3 -1
- package/src/styles/utilities-buttons.css +133 -27
- package/src/styles/utilities-code-display.css +67 -0
- package/src/styles/utilities-completion.css +223 -0
- package/src/styles/utilities-detail.css +73 -0
- package/src/styles/utilities-feedback.css +16 -15
- package/src/styles/utilities-layout.css +42 -2
- package/src/styles/utilities-markdown-editor.css +472 -0
- package/src/styles/utilities-navigation.css +63 -8
- package/src/styles/utilities-script.css +84 -0
- package/src/styles/utilities-table-tile.css +229 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/ui/completion/behaviors.test.ts +95 -0
- package/src/ui/completion/behaviors.ts +205 -0
- package/src/ui/completion/engine.ts +368 -0
- package/src/ui/completion/index.ts +40 -0
- package/src/ui/completion/overlay.ts +92 -0
- package/src/ui/dialog-core.ts +173 -45
- package/src/ui/filter/FilterChip.tsx +42 -40
- package/src/ui/index.ts +11 -12
- package/src/ui/input/AutocompleteEditor.tsx +656 -0
- package/src/ui/input/CheckboxCard.tsx +91 -0
- package/src/ui/input/Combobox.tsx +375 -0
- package/src/ui/input/DatePicker.tsx +846 -0
- package/src/ui/input/DateTimeInput.tsx +29 -4
- package/src/ui/input/FileDropzone.tsx +116 -0
- package/src/ui/input/IconInput.tsx +116 -0
- package/src/ui/input/ImageInput.tsx +19 -2
- package/src/ui/input/MultiSelectInput.tsx +448 -0
- package/src/ui/input/NumberInput.tsx +417 -61
- package/src/ui/input/SegmentedControl.tsx +2 -2
- package/src/ui/input/Select.tsx +172 -10
- package/src/ui/input/Slider.tsx +3 -4
- package/src/ui/input/Switch.tsx +3 -2
- package/src/ui/input/TemplateEditor.tsx +212 -0
- package/src/ui/input/TextInput.tsx +144 -13
- package/src/ui/input/index.ts +53 -8
- package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
- package/src/ui/input/markdown/Toolbar.tsx +90 -0
- package/src/ui/input/markdown/actions.ts +233 -0
- package/src/ui/input/markdown/active-formats.ts +94 -0
- package/src/ui/input/markdown/behaviors.ts +193 -0
- package/src/ui/input/markdown/code-zone.ts +23 -0
- package/src/ui/input/markdown/highlight.ts +316 -0
- package/src/ui/layout.ts +22 -0
- package/src/ui/misc/AppOverview.tsx +105 -0
- package/src/ui/misc/AppWorkspace.tsx +607 -0
- package/src/ui/misc/Calendar.tsx +1291 -0
- package/src/ui/misc/Chart.tsx +162 -0
- package/src/ui/misc/CodeDisplay.tsx +54 -0
- package/src/ui/misc/ContextMenu.tsx +2 -2
- package/src/ui/misc/DataTable.tsx +269 -0
- package/src/ui/misc/DockWorkspace.tsx +425 -0
- package/src/ui/misc/Docs.tsx +153 -0
- package/src/ui/misc/Dropdown.tsx +2 -2
- package/src/ui/misc/EntitySearch.tsx +260 -129
- package/src/ui/misc/LinkCard.tsx +14 -2
- package/src/ui/misc/LogEntriesTable.tsx +34 -31
- package/src/ui/misc/Pagination.tsx +31 -12
- package/src/ui/misc/PanelDialog.tsx +109 -0
- package/src/ui/misc/Panes.tsx +873 -0
- package/src/ui/misc/PermissionEditor.tsx +358 -262
- package/src/ui/misc/Placeholder.tsx +40 -0
- package/src/ui/misc/ProgressBar.tsx +1 -1
- package/src/ui/misc/ResourceApiKeys.tsx +260 -0
- package/src/ui/misc/SettingsModal.tsx +150 -0
- package/src/ui/misc/StatCell.tsx +182 -40
- package/src/ui/misc/StatGrid.tsx +149 -0
- package/src/ui/misc/StructuredDataPreview.tsx +107 -0
- package/src/ui/misc/code-highlight.ts +213 -0
- package/src/ui/misc/index.ts +93 -12
- package/src/ui/prompts.tsx +362 -312
- package/src/ui/toast.ts +384 -0
- package/src/ui/widgets/Widget.tsx +12 -4
- package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
- package/src/ui/ipa/GroupView.tsx +0 -36
- package/src/ui/ipa/LoginBtn.tsx +0 -16
- package/src/ui/ipa/UserView.tsx +0 -58
- package/src/ui/ipa/index.ts +0 -4
- package/src/ui/navigation.ts +0 -32
- package/src/ui/sidebar.tsx +0 -468
- /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
package/src/ui/dialog-core.ts
CHANGED
|
@@ -23,13 +23,46 @@ export type DialogCore = {
|
|
|
23
23
|
isOpen: () => boolean;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* One level on the dialog stack. Multiple levels can coexist (a deeper
|
|
28
|
+
* dialog opens on top of a shallower one — e.g. a `prompts.confirm`
|
|
29
|
+
* called from inside a `prompts.dialog`'s view), but only the topmost
|
|
30
|
+
* level is visible. Lower levels stay mounted (`display:none`) so their
|
|
31
|
+
* SolidJS state survives across the round-trip.
|
|
32
|
+
*/
|
|
33
|
+
type DialogStackEntry = {
|
|
34
|
+
/** Container `<div>` inside the shared `<dialog>`; one per level. */
|
|
35
|
+
container: HTMLDivElement;
|
|
36
|
+
/** SolidJS `render` disposer. Called only when this level is popped
|
|
37
|
+
* off the stack — NOT when it's merely hidden by a deeper level. */
|
|
28
38
|
dispose?: () => void;
|
|
39
|
+
/** Promise resolver for this level's `open()` call. */
|
|
29
40
|
resolve?: (value: unknown) => void;
|
|
41
|
+
panelClassName: string;
|
|
42
|
+
cancelBehavior: NonNullable<OpenDialogOptions["cancelBehavior"]>;
|
|
43
|
+
initialFocus: NonNullable<OpenDialogOptions["initialFocus"]>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type DialogState = {
|
|
47
|
+
/** Shared `<dialog>` element. One on the page, regardless of stack
|
|
48
|
+
* depth — only the topmost level's container is visible. */
|
|
49
|
+
element?: HTMLDialogElement;
|
|
50
|
+
/** Active stack of dialog levels. Top of stack is the visible one.
|
|
51
|
+
* Empty means no dialog is shown; `<dialog>.close()` has been called
|
|
52
|
+
* and scroll is unlocked. */
|
|
53
|
+
stack: DialogStackEntry[];
|
|
30
54
|
scrollLocked?: boolean;
|
|
31
55
|
previousBodyOverflow?: string;
|
|
32
56
|
previousHtmlOverflow?: string;
|
|
57
|
+
/** Tracks whether the most recent `mousedown` had `event.target ===
|
|
58
|
+
* dialog` (i.e. on the backdrop itself, not on dialog content).
|
|
59
|
+
* Used by the click handler to distinguish a real backdrop click
|
|
60
|
+
* from a phantom one — e.g. when an option in an open `popover`
|
|
61
|
+
* is mousedown'd, the popover hides synchronously, and the
|
|
62
|
+
* subsequent click event gets retargeted to the dialog because the
|
|
63
|
+
* option is now `display:none`. We only close on a click whose
|
|
64
|
+
* mousedown was ALSO on the backdrop. */
|
|
65
|
+
mouseDownOnDialog?: boolean;
|
|
33
66
|
};
|
|
34
67
|
|
|
35
68
|
const DEFAULT_PANEL_CLASS = "dialog-panel";
|
|
@@ -41,25 +74,49 @@ const resolveInitialFocusTarget = (dialog: HTMLDialogElement, initialFocus: Open
|
|
|
41
74
|
return dialog.querySelector<HTMLElement>("input:not([type='hidden']), textarea, select, button");
|
|
42
75
|
};
|
|
43
76
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
export const createDialogCore = (): DialogCore => {
|
|
78
|
+
const state: DialogState = { stack: [] };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wires up Esc-cancel + backdrop-click behaviour to the dialog. Re-
|
|
82
|
+
* called whenever the topmost level changes (push/pop) so the
|
|
83
|
+
* handlers always reflect the current top's `cancelBehavior` and
|
|
84
|
+
* `close` callback.
|
|
85
|
+
*
|
|
86
|
+
* Backdrop-click detection guards against phantom clicks: it only
|
|
87
|
+
* fires when BOTH `mousedown` AND `click` target the dialog element
|
|
88
|
+
* itself. A click whose `mousedown` was on a child (and got
|
|
89
|
+
* retargeted to the dialog because the child went `display:none`
|
|
90
|
+
* mid-gesture — e.g. a popover hiding from inside its own option's
|
|
91
|
+
* onMouseDown) is rejected as not a real backdrop click.
|
|
92
|
+
*/
|
|
93
|
+
const applyCancelBehavior = (
|
|
94
|
+
dialog: HTMLDialogElement,
|
|
95
|
+
close: () => void,
|
|
96
|
+
behavior: OpenDialogOptions["cancelBehavior"],
|
|
97
|
+
) => {
|
|
98
|
+
dialog.oncancel = (event) => {
|
|
99
|
+
if (behavior === "ignore") {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
47
103
|
event.preventDefault();
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
event.preventDefault();
|
|
51
|
-
close(undefined);
|
|
52
|
-
};
|
|
104
|
+
close();
|
|
105
|
+
};
|
|
53
106
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
close(undefined);
|
|
58
|
-
};
|
|
59
|
-
};
|
|
107
|
+
dialog.onmousedown = (event) => {
|
|
108
|
+
state.mouseDownOnDialog = event.target === dialog;
|
|
109
|
+
};
|
|
60
110
|
|
|
61
|
-
|
|
62
|
-
|
|
111
|
+
dialog.onclick = (event) => {
|
|
112
|
+
const wasRealBackdropClick = state.mouseDownOnDialog === true;
|
|
113
|
+
state.mouseDownOnDialog = false;
|
|
114
|
+
if (event.target !== dialog) return;
|
|
115
|
+
if (!wasRealBackdropClick) return;
|
|
116
|
+
if (behavior === "ignore") return;
|
|
117
|
+
close();
|
|
118
|
+
};
|
|
119
|
+
};
|
|
63
120
|
|
|
64
121
|
const ensureDialogElement = () => {
|
|
65
122
|
if (typeof document === "undefined") throw new Error("Dialog core is browser-only");
|
|
@@ -71,11 +128,6 @@ export const createDialogCore = (): DialogCore => {
|
|
|
71
128
|
return element;
|
|
72
129
|
};
|
|
73
130
|
|
|
74
|
-
const clearRenderedContent = () => {
|
|
75
|
-
state.dispose?.();
|
|
76
|
-
state.dispose = undefined;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
131
|
const lockPageScroll = () => {
|
|
80
132
|
if (typeof document === "undefined" || state.scrollLocked) return;
|
|
81
133
|
|
|
@@ -96,50 +148,126 @@ export const createDialogCore = (): DialogCore => {
|
|
|
96
148
|
state.previousHtmlOverflow = undefined;
|
|
97
149
|
};
|
|
98
150
|
|
|
99
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Pop the topmost level off the stack. Disposes its SolidJS render,
|
|
153
|
+
* removes its container, resolves its promise. If this empties the
|
|
154
|
+
* stack the underlying `<dialog>` is closed and scroll unlocked;
|
|
155
|
+
* otherwise the previous level is unhidden and its dialog chrome
|
|
156
|
+
* (className, cancel handlers, initial focus) is restored.
|
|
157
|
+
*/
|
|
158
|
+
const popTop = (result?: unknown) => {
|
|
159
|
+
const top = state.stack.pop();
|
|
160
|
+
if (!top) return;
|
|
161
|
+
|
|
162
|
+
top.dispose?.();
|
|
163
|
+
top.container.remove();
|
|
164
|
+
|
|
100
165
|
const dialog = state.element;
|
|
101
|
-
|
|
166
|
+
const previous = state.stack[state.stack.length - 1];
|
|
102
167
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
if (previous && dialog) {
|
|
169
|
+
// Restore the level that was hidden when this one opened — it
|
|
170
|
+
// was never disposed, so its SolidJS state is still live.
|
|
171
|
+
previous.container.style.display = "";
|
|
172
|
+
dialog.className = previous.panelClassName;
|
|
173
|
+
applyCancelBehavior(dialog, () => popTop(undefined), previous.cancelBehavior);
|
|
106
174
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
175
|
+
// Re-run the previous level's initial focus resolver. The
|
|
176
|
+
// browser's modal focus trap would otherwise pick the first
|
|
177
|
+
// focusable in DOM order, which can be wrong if the previous
|
|
178
|
+
// view declared a custom `initialFocus` function.
|
|
179
|
+
requestAnimationFrame(() => {
|
|
180
|
+
resolveInitialFocusTarget(dialog, previous.initialFocus)?.focus();
|
|
181
|
+
});
|
|
182
|
+
} else if (dialog) {
|
|
183
|
+
// Stack is empty — close the underlying dialog for real.
|
|
184
|
+
if (dialog.open) dialog.close();
|
|
185
|
+
unlockPageScroll();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Resolve LAST so the awaiting caller observes the unmounted state.
|
|
189
|
+
top.resolve?.(result);
|
|
110
190
|
};
|
|
111
191
|
|
|
112
192
|
const open = <T>(view: DialogRender<T>, options: OpenDialogOptions = {}): Promise<T | undefined> => {
|
|
113
193
|
const dialog = ensureDialogElement();
|
|
114
|
-
if (dialog.open) close(undefined);
|
|
115
194
|
|
|
116
|
-
|
|
117
|
-
|
|
195
|
+
// Hide the currently-visible level (if any). We don't dispose —
|
|
196
|
+
// its SolidJS render keeps running so its signals, scroll, focus
|
|
197
|
+
// intent, etc. all survive the round-trip.
|
|
198
|
+
const previousTop = state.stack[state.stack.length - 1];
|
|
199
|
+
if (previousTop) {
|
|
200
|
+
previousTop.container.style.display = "none";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const panelClassName = options.panelClassName ?? DEFAULT_PANEL_CLASS;
|
|
204
|
+
const cancelBehavior = options.cancelBehavior ?? "resolve-undefined";
|
|
205
|
+
const initialFocus = options.initialFocus ?? "first-input";
|
|
206
|
+
|
|
207
|
+
dialog.className = panelClassName;
|
|
118
208
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
dialog.appendChild(
|
|
209
|
+
const container = document.createElement("div");
|
|
210
|
+
container.className = options.contentClassName ?? DEFAULT_CONTENT_CLASS;
|
|
211
|
+
dialog.appendChild(container);
|
|
212
|
+
|
|
213
|
+
const entry: DialogStackEntry = {
|
|
214
|
+
container,
|
|
215
|
+
panelClassName,
|
|
216
|
+
cancelBehavior,
|
|
217
|
+
initialFocus,
|
|
218
|
+
};
|
|
122
219
|
|
|
123
220
|
return new Promise((resolve) => {
|
|
124
|
-
|
|
221
|
+
entry.resolve = (value) => resolve(value as T | undefined);
|
|
222
|
+
|
|
223
|
+
// Per-entry close callback. Idempotent and safe to call from a
|
|
224
|
+
// stale closure — if this entry has already been popped (or a
|
|
225
|
+
// deeper level is now on top), the call no-ops instead of
|
|
226
|
+
// accidentally popping someone else's level.
|
|
227
|
+
const closeTyped: DialogClose<T> = (result) => {
|
|
228
|
+
if (state.stack[state.stack.length - 1] !== entry) return;
|
|
229
|
+
popTop(result);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
entry.dispose = render(() => view(closeTyped, { dialog }), container);
|
|
233
|
+
|
|
234
|
+
applyCancelBehavior(dialog, () => closeTyped(undefined), cancelBehavior);
|
|
125
235
|
|
|
126
|
-
|
|
127
|
-
|
|
236
|
+
// Push only after dispose is set so a synchronous close from the
|
|
237
|
+
// view's render path still finds itself on top.
|
|
238
|
+
state.stack.push(entry);
|
|
128
239
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
240
|
+
const wasFirstLevel = state.stack.length === 1;
|
|
241
|
+
if (wasFirstLevel) {
|
|
242
|
+
dialog.showModal();
|
|
243
|
+
lockPageScroll();
|
|
244
|
+
}
|
|
132
245
|
|
|
133
246
|
requestAnimationFrame(() => {
|
|
134
|
-
resolveInitialFocusTarget(dialog,
|
|
247
|
+
resolveInitialFocusTarget(dialog, initialFocus)?.focus();
|
|
135
248
|
});
|
|
136
249
|
});
|
|
137
250
|
};
|
|
138
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Close ALL levels on the stack. The first pop receives `result`,
|
|
254
|
+
* the rest get `undefined`. Used by external callers that want to
|
|
255
|
+
* dismiss the dialog system entirely (e.g. in cleanup / route
|
|
256
|
+
* change). Internal per-level closing goes through the `close`
|
|
257
|
+
* callback passed to each view, which only pops its own level.
|
|
258
|
+
*/
|
|
259
|
+
const close: DialogCore["close"] = (result) => {
|
|
260
|
+
let first = true;
|
|
261
|
+
while (state.stack.length > 0) {
|
|
262
|
+
popTop(first ? result : undefined);
|
|
263
|
+
first = false;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
139
267
|
return {
|
|
140
268
|
open,
|
|
141
269
|
close,
|
|
142
|
-
isOpen: () =>
|
|
270
|
+
isOpen: () => state.stack.length > 0,
|
|
143
271
|
};
|
|
144
272
|
};
|
|
145
273
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, createEffect, Show
|
|
1
|
+
import { createSignal, createEffect, Show } from "solid-js";
|
|
2
2
|
import Dropdown from "../misc/Dropdown";
|
|
3
3
|
import type { DropdownItem } from "../misc/Dropdown";
|
|
4
4
|
|
|
@@ -41,6 +41,8 @@ type FilterChipProps = {
|
|
|
41
41
|
* Also hides the count in the trigger.
|
|
42
42
|
*/
|
|
43
43
|
defaultValue?: string[];
|
|
44
|
+
/** Render only the trigger icon while keeping the label as accessible title text. */
|
|
45
|
+
iconOnly?: boolean;
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
// =============================================================================
|
|
@@ -50,7 +52,7 @@ type FilterChipProps = {
|
|
|
50
52
|
/**
|
|
51
53
|
* Filter chip using the shared Dropdown component.
|
|
52
54
|
* Each section can be single-select or multi-select independently.
|
|
53
|
-
* Changes are
|
|
55
|
+
* Changes are committed immediately so URL-backed filters update without losing focus.
|
|
54
56
|
*/
|
|
55
57
|
export default function FilterChip(props: FilterChipProps) {
|
|
56
58
|
// Local selection state (tracks pending changes)
|
|
@@ -60,12 +62,6 @@ export default function FilterChip(props: FilterChipProps) {
|
|
|
60
62
|
createEffect(() => setLocalValue([...props.value]));
|
|
61
63
|
|
|
62
64
|
// Computed values
|
|
63
|
-
const hasChanges = createMemo(() => {
|
|
64
|
-
const local = localValue();
|
|
65
|
-
const original = props.value;
|
|
66
|
-
return local.length !== original.length || local.some((v) => !original.includes(v));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
65
|
const isActive = () => props.isActive ?? localValue().length > 0;
|
|
70
66
|
const isSelected = (value: string) => localValue().includes(value);
|
|
71
67
|
const selectedCount = () => localValue().length;
|
|
@@ -81,6 +77,11 @@ export default function FilterChip(props: FilterChipProps) {
|
|
|
81
77
|
// Find which section a value belongs to
|
|
82
78
|
const getSectionForValue = (value: string) => props.options.findIndex((s) => s.options.some((o) => o.value === value));
|
|
83
79
|
|
|
80
|
+
const commitValue = (nextValue: string[]) => {
|
|
81
|
+
setLocalValue(nextValue);
|
|
82
|
+
props.onChange(nextValue);
|
|
83
|
+
};
|
|
84
|
+
|
|
84
85
|
// Toggle option selection
|
|
85
86
|
const toggleOption = (value: string) => {
|
|
86
87
|
const sectionIndex = getSectionForValue(value);
|
|
@@ -89,25 +90,20 @@ export default function FilterChip(props: FilterChipProps) {
|
|
|
89
90
|
|
|
90
91
|
const isMultiple = section.multiple ?? false;
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
const prev = localValue();
|
|
94
|
+
const isCurrentlySelected = prev.includes(value);
|
|
95
|
+
if (isMultiple) {
|
|
96
|
+
commitValue(isCurrentlySelected ? prev.filter((v) => v !== value) : [...prev, value]);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
100
|
+
// Single-select: replace any value from this section.
|
|
101
|
+
const sectionValues = new Set(section.options.map((o) => o.value));
|
|
102
|
+
const otherValues = prev.filter((v) => !sectionValues.has(v));
|
|
103
|
+
commitValue(isCurrentlySelected ? otherValues : [...otherValues, value]);
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
const clearOrReset = () =>
|
|
107
|
-
|
|
108
|
-
const handleClose = () => {
|
|
109
|
-
if (hasChanges()) props.onChange(localValue());
|
|
110
|
-
};
|
|
106
|
+
const clearOrReset = () => commitValue(props.defaultValue ? [...props.defaultValue] : []);
|
|
111
107
|
|
|
112
108
|
// Build dropdown elements
|
|
113
109
|
const dropdownElements = (): DropdownItem[] => {
|
|
@@ -128,7 +124,14 @@ export default function FilterChip(props: FilterChipProps) {
|
|
|
128
124
|
}}
|
|
129
125
|
>
|
|
130
126
|
<Show when={isMultiple}>
|
|
131
|
-
<input
|
|
127
|
+
<input
|
|
128
|
+
type="checkbox"
|
|
129
|
+
checked={isSelected(option.value)}
|
|
130
|
+
readOnly
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
tabindex={-1}
|
|
133
|
+
class="shrink-0 pointer-events-none"
|
|
134
|
+
/>
|
|
132
135
|
</Show>
|
|
133
136
|
|
|
134
137
|
<Show when={option.icon && !isMultiple}>
|
|
@@ -174,23 +177,22 @@ export default function FilterChip(props: FilterChipProps) {
|
|
|
174
177
|
};
|
|
175
178
|
|
|
176
179
|
const trigger = (
|
|
177
|
-
<div
|
|
180
|
+
<div
|
|
181
|
+
class={`btn-input btn-input-sm ${props.iconOnly ? "h-8 w-8 justify-center px-0" : ""} ${isActive() ? "btn-input-active" : ""}`}
|
|
182
|
+
role="button"
|
|
183
|
+
aria-label={props.label}
|
|
184
|
+
title={props.iconOnly ? props.label : undefined}
|
|
185
|
+
>
|
|
178
186
|
<i class={`${props.icon} ${isActive() ? "text-blue-600 dark:text-blue-300" : "text-zinc-500 dark:text-zinc-400"}`} />
|
|
179
|
-
<
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
<Show when={!props.iconOnly}>
|
|
188
|
+
<span class={isActive() ? "text-zinc-900 dark:text-zinc-100" : "text-zinc-700 dark:text-zinc-300"}>
|
|
189
|
+
{props.label}
|
|
190
|
+
<Show when={!hasDefaultValue() && selectedCount() > 0}>{` (${selectedCount()})`}</Show>
|
|
191
|
+
</span>
|
|
192
|
+
<i class="ti ti-chevron-down text-zinc-400 text-[10px]" />
|
|
193
|
+
</Show>
|
|
184
194
|
</div>
|
|
185
195
|
);
|
|
186
196
|
|
|
187
|
-
return (
|
|
188
|
-
<Dropdown
|
|
189
|
-
trigger={trigger}
|
|
190
|
-
elements={dropdownElements()}
|
|
191
|
-
position={props.position ?? "bottom-left"}
|
|
192
|
-
onClose={handleClose}
|
|
193
|
-
width="w-52"
|
|
194
|
-
/>
|
|
195
|
-
);
|
|
197
|
+
return <Dropdown trigger={trigger} elements={dropdownElements()} position={props.position ?? "bottom-left"} width="w-52" />;
|
|
196
198
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
1
|
+
export type { SettingsFieldProps, SettingsSaveBarProps } from "./admin-settings";
|
|
2
|
+
export { readSettingsError, SettingsField, SettingsSaveBar, sameSettingValue } from "./admin-settings";
|
|
3
|
+
export type { DialogClose, DialogCore, DialogRender, OpenDialogOptions } from "./dialog-core";
|
|
4
|
+
export { createDialogCore, dialogCore } from "./dialog-core";
|
|
4
5
|
export * from "./filter";
|
|
6
|
+
export * from "./input";
|
|
7
|
+
export { layout, LAYOUT_UPDATE_EVENT, type LayoutBreadcrumb, type LayoutUpdate } from "./layout";
|
|
8
|
+
export * from "./misc";
|
|
9
|
+
export type { PromptSearchInput, PromptSearchItem, PromptSearchOptions } from "./prompts";
|
|
10
|
+
export { createFormState, DialogHeader, prompts } from "./prompts";
|
|
11
|
+
export type { ToastFn, ToastHandle, ToastOptions, ToastVariant } from "./toast";
|
|
12
|
+
export { toast } from "./toast";
|
|
5
13
|
export * from "./widgets";
|
|
6
|
-
export { currentPathWithQuery, refreshCurrentPath, navigateTo } from "./navigation";
|
|
7
|
-
export { SettingsField, SettingsSaveBar, sameSettingValue, readSettingsError } from "./admin-settings";
|
|
8
|
-
export type { SettingsFieldProps, SettingsSaveBarProps } from "./admin-settings";
|
|
9
|
-
export { prompts, DialogHeader, createFormState } from "./prompts";
|
|
10
|
-
export type { PromptSearchItem, PromptSearchInput, PromptSearchOptions } from "./prompts";
|
|
11
|
-
export { dialogCore, createDialogCore } from "./dialog-core";
|
|
12
|
-
export type { DialogClose, OpenDialogOptions, DialogRender, DialogCore } from "./dialog-core";
|
|
13
|
-
export { default as SidebarLayout, SidebarFromSpec } from "./sidebar";
|
|
14
|
-
export type { SidebarSpec, SidebarRow, SidebarSection, SidebarTreeNode, SidebarTreeSpec } from "./sidebar";
|
|
15
14
|
// NOTE: islands (*.island.tsx) belong inside the consuming app's package, not
|
|
16
15
|
// in cloud-lib. The SSR plugin discovers islands by import-path suffix; barrel
|
|
17
16
|
// re-exports strip the `.island` segment and silently break hydration. Apps
|