plutonium 0.52.0 → 0.53.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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium-resource/SKILL.md +6 -4
- data/.claude/skills/plutonium-tenancy/SKILL.md +9 -4
- data/.claude/skills/plutonium-ui/SKILL.md +29 -5
- data/CHANGELOG.md +16 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +257 -11
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +39 -39
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_resource_header.html.erb +2 -1
- data/docs/.vitepress/config.ts +1 -0
- data/docs/guides/authentication.md +1 -1
- data/docs/guides/custom-actions.md +2 -1
- data/docs/guides/customizing-ui.md +6 -5
- data/docs/guides/multi-tenancy.md +6 -6
- data/docs/guides/theming.md +1 -1
- data/docs/public/images/components/avatar.png +0 -0
- data/docs/reference/auth/accounts.md +1 -1
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/configuration.md +61 -0
- data/docs/reference/resource/actions.md +2 -1
- data/docs/reference/resource/definition.md +4 -3
- data/docs/reference/tenancy/entity-scoping.md +12 -13
- data/docs/reference/ui/components.md +53 -0
- data/docs/reference/ui/forms.md +1 -1
- data/docs/reference/ui/pages.md +6 -5
- data/docs/superpowers/specs/2026-05-29-avatar-component-design.md +153 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +7 -3
- data/lib/plutonium/action/base.rb +43 -63
- data/lib/plutonium/configuration.rb +7 -0
- data/lib/plutonium/definition/actions.rb +10 -11
- data/lib/plutonium/definition/base.rb +29 -0
- data/lib/plutonium/helpers/assets_helper.rb +0 -30
- data/lib/plutonium/helpers/content_helper.rb +0 -44
- data/lib/plutonium/helpers/display_helper.rb +0 -62
- data/lib/plutonium/helpers/turbo_helper.rb +0 -4
- data/lib/plutonium/helpers.rb +0 -2
- data/lib/plutonium/resource/definition.rb +0 -42
- data/lib/plutonium/ui/action_button.rb +4 -3
- data/lib/plutonium/ui/avatar.rb +182 -0
- data/lib/plutonium/ui/component/kit.rb +2 -0
- data/lib/plutonium/ui/form/base.rb +16 -2
- data/lib/plutonium/ui/form/components/secure_association.rb +3 -2
- data/lib/plutonium/ui/form/resource.rb +58 -0
- data/lib/plutonium/ui/form/theme.rb +7 -3
- data/lib/plutonium/ui/grid/card.rb +10 -26
- data/lib/plutonium/ui/modal/base.rb +36 -1
- data/lib/plutonium/ui/modal/centered.rb +24 -6
- data/lib/plutonium/ui/modal/slideover.rb +26 -11
- data/lib/plutonium/ui/nav_user.rb +3 -23
- data/lib/plutonium/ui/page/edit.rb +6 -3
- data/lib/plutonium/ui/page/interactive_action.rb +5 -3
- data/lib/plutonium/ui/page/new.rb +6 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +38 -1
- data/src/css/slim_select.css +3 -2
- data/src/js/controllers/dirty_form_guard_controller.js +165 -0
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/remote_modal_controller.js +53 -19
- data/src/js/turbo/index.js +1 -0
- data/src/js/turbo/turbo_confirm.js +128 -0
- metadata +10 -6
- data/lib/plutonium/helpers/attachment_helper.rb +0 -73
- data/lib/plutonium/helpers/table_helper.rb +0 -35
- /data/lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/{password_changed.text.erb → change_password_notify.text.erb} +0 -0
data/app/assets/plutonium.js
CHANGED
|
@@ -27588,22 +27588,47 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
|
|
|
27588
27588
|
this.originalScrollPosition = window.scrollY;
|
|
27589
27589
|
this.originalOverflow = document.body.style.overflow;
|
|
27590
27590
|
this.bodyStateRestored = false;
|
|
27591
|
+
this._closing = false;
|
|
27591
27592
|
document.body.style.overflow = "hidden";
|
|
27592
27593
|
this.element.showModal();
|
|
27593
|
-
|
|
27594
|
+
requestAnimationFrame(() => {
|
|
27595
|
+
requestAnimationFrame(() => {
|
|
27596
|
+
this.element.setAttribute("data-open", "");
|
|
27597
|
+
});
|
|
27598
|
+
});
|
|
27599
|
+
this.onCancel = this.#onCancel.bind(this);
|
|
27600
|
+
this.onClose = this.#onClose.bind(this);
|
|
27601
|
+
this.onRequestClose = () => this.#animateClose();
|
|
27602
|
+
this.element.addEventListener("cancel", this.onCancel);
|
|
27603
|
+
this.element.addEventListener("close", this.onClose);
|
|
27604
|
+
this.element.addEventListener("modal:request-close", this.onRequestClose);
|
|
27605
|
+
}
|
|
27606
|
+
disconnect() {
|
|
27607
|
+
this.element.removeEventListener("cancel", this.onCancel);
|
|
27608
|
+
this.element.removeEventListener("close", this.onClose);
|
|
27609
|
+
this.element.removeEventListener("modal:request-close", this.onRequestClose);
|
|
27610
|
+
this.#restoreBodyState();
|
|
27594
27611
|
}
|
|
27595
27612
|
close() {
|
|
27596
|
-
this
|
|
27597
|
-
this.restoreBodyState();
|
|
27613
|
+
this.#animateClose();
|
|
27598
27614
|
}
|
|
27599
|
-
|
|
27600
|
-
|
|
27601
|
-
|
|
27615
|
+
#onCancel(event) {
|
|
27616
|
+
if (event.defaultPrevented) return;
|
|
27617
|
+
event.preventDefault();
|
|
27618
|
+
this.#animateClose();
|
|
27602
27619
|
}
|
|
27603
|
-
|
|
27604
|
-
this
|
|
27620
|
+
#onClose() {
|
|
27621
|
+
this.#restoreBodyState();
|
|
27605
27622
|
}
|
|
27606
|
-
|
|
27623
|
+
async #animateClose() {
|
|
27624
|
+
if (this._closing) return;
|
|
27625
|
+
this._closing = true;
|
|
27626
|
+
this.element.removeAttribute("data-open");
|
|
27627
|
+
const animations = this.element.getAnimations({ subtree: true });
|
|
27628
|
+
await Promise.allSettled(animations.map((a4) => a4.finished));
|
|
27629
|
+
this.element.close();
|
|
27630
|
+
}
|
|
27631
|
+
#restoreBodyState() {
|
|
27607
27632
|
if (this.bodyStateRestored) return;
|
|
27608
27633
|
this.bodyStateRestored = true;
|
|
27609
27634
|
document.body.style.overflow = this.originalOverflow || "";
|
|
@@ -28148,6 +28173,129 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
|
|
|
28148
28173
|
}
|
|
28149
28174
|
};
|
|
28150
28175
|
|
|
28176
|
+
// src/js/controllers/dirty_form_guard_controller.js
|
|
28177
|
+
var dirty_form_guard_controller_default = class extends Controller {
|
|
28178
|
+
static targets = ["confirmDialog"];
|
|
28179
|
+
// Set by controllers, not the user — comparing them would flag
|
|
28180
|
+
// every form as dirty on connect (return_to) or on submit (pre_submit).
|
|
28181
|
+
static IGNORED_KEYS = /* @__PURE__ */ new Set(["authenticity_token", "return_to", "pre_submit"]);
|
|
28182
|
+
connect() {
|
|
28183
|
+
this.dialog = this.element.closest("dialog");
|
|
28184
|
+
if (!this.dialog) return;
|
|
28185
|
+
this.snapshot = this.#serialize();
|
|
28186
|
+
this.forceClose = false;
|
|
28187
|
+
this.submitting = false;
|
|
28188
|
+
this.onCancel = this.#onCancel.bind(this);
|
|
28189
|
+
this.onSubmit = this.#onSubmit.bind(this);
|
|
28190
|
+
this.onCloseButtonClick = this.#onCloseButtonClick.bind(this);
|
|
28191
|
+
this.onConfirmCancel = this.#onConfirmCancel.bind(this);
|
|
28192
|
+
this.onKeydown = this.#onKeydown.bind(this);
|
|
28193
|
+
document.addEventListener("keydown", this.onKeydown, true);
|
|
28194
|
+
this.dialog.addEventListener("cancel", this.onCancel, true);
|
|
28195
|
+
this.element.addEventListener("submit", this.onSubmit);
|
|
28196
|
+
this.#closeButtons().forEach(
|
|
28197
|
+
(btn) => btn.addEventListener("click", this.onCloseButtonClick, true)
|
|
28198
|
+
);
|
|
28199
|
+
if (this.hasConfirmDialogTarget) {
|
|
28200
|
+
this.confirmDialogTarget.addEventListener("cancel", this.onConfirmCancel);
|
|
28201
|
+
}
|
|
28202
|
+
}
|
|
28203
|
+
disconnect() {
|
|
28204
|
+
if (!this.dialog) return;
|
|
28205
|
+
document.removeEventListener("keydown", this.onKeydown, true);
|
|
28206
|
+
this.dialog.removeEventListener("cancel", this.onCancel, true);
|
|
28207
|
+
this.element.removeEventListener("submit", this.onSubmit);
|
|
28208
|
+
this.#closeButtons().forEach(
|
|
28209
|
+
(btn) => btn.removeEventListener("click", this.onCloseButtonClick, true)
|
|
28210
|
+
);
|
|
28211
|
+
if (this.hasConfirmDialogTarget) {
|
|
28212
|
+
this.confirmDialogTarget.removeEventListener("cancel", this.onConfirmCancel);
|
|
28213
|
+
}
|
|
28214
|
+
}
|
|
28215
|
+
async discard() {
|
|
28216
|
+
this.forceClose = true;
|
|
28217
|
+
await this.#closeConfirm();
|
|
28218
|
+
this.dialog.dispatchEvent(new CustomEvent("modal:request-close"));
|
|
28219
|
+
}
|
|
28220
|
+
keepEditing() {
|
|
28221
|
+
this.#closeConfirm();
|
|
28222
|
+
}
|
|
28223
|
+
#closeButtons() {
|
|
28224
|
+
if (!this.dialog) return [];
|
|
28225
|
+
return this.dialog.querySelectorAll('[data-action~="remote-modal#close"]');
|
|
28226
|
+
}
|
|
28227
|
+
#serialize() {
|
|
28228
|
+
const data = new FormData(this.element);
|
|
28229
|
+
const enc = encodeURIComponent;
|
|
28230
|
+
return [...data.entries()].filter(([key]) => !this.constructor.IGNORED_KEYS.has(key)).map(([key, value]) => {
|
|
28231
|
+
const v4 = value instanceof File ? value.name : value;
|
|
28232
|
+
return `${enc(key)}=${enc(v4)}`;
|
|
28233
|
+
}).sort().join("&");
|
|
28234
|
+
}
|
|
28235
|
+
#isDirty() {
|
|
28236
|
+
return this.#serialize() !== this.snapshot;
|
|
28237
|
+
}
|
|
28238
|
+
#onSubmit() {
|
|
28239
|
+
this.submitting = true;
|
|
28240
|
+
}
|
|
28241
|
+
#confirmIsOpen() {
|
|
28242
|
+
return this.hasConfirmDialogTarget && this.confirmDialogTarget.open;
|
|
28243
|
+
}
|
|
28244
|
+
#onKeydown(event) {
|
|
28245
|
+
if (event.key !== "Escape") return;
|
|
28246
|
+
if (!this.dialog.open) return;
|
|
28247
|
+
if (this.#confirmIsOpen()) {
|
|
28248
|
+
event.preventDefault();
|
|
28249
|
+
event.stopPropagation();
|
|
28250
|
+
event.stopImmediatePropagation();
|
|
28251
|
+
return;
|
|
28252
|
+
}
|
|
28253
|
+
if (this.forceClose || this.submitting) return;
|
|
28254
|
+
if (!this.#isDirty()) return;
|
|
28255
|
+
event.preventDefault();
|
|
28256
|
+
event.stopPropagation();
|
|
28257
|
+
event.stopImmediatePropagation();
|
|
28258
|
+
this.#promptDiscard();
|
|
28259
|
+
}
|
|
28260
|
+
#onCancel(event) {
|
|
28261
|
+
if (this.forceClose || this.submitting) return;
|
|
28262
|
+
if (!this.#isDirty()) return;
|
|
28263
|
+
event.preventDefault();
|
|
28264
|
+
this.#promptDiscard();
|
|
28265
|
+
}
|
|
28266
|
+
#onCloseButtonClick(event) {
|
|
28267
|
+
if (this.forceClose || this.submitting) return;
|
|
28268
|
+
if (!this.#isDirty()) return;
|
|
28269
|
+
event.preventDefault();
|
|
28270
|
+
event.stopPropagation();
|
|
28271
|
+
this.#promptDiscard();
|
|
28272
|
+
}
|
|
28273
|
+
#onConfirmCancel(event) {
|
|
28274
|
+
event.preventDefault();
|
|
28275
|
+
}
|
|
28276
|
+
#promptDiscard() {
|
|
28277
|
+
if (this.hasConfirmDialogTarget) {
|
|
28278
|
+
const d4 = this.confirmDialogTarget;
|
|
28279
|
+
d4.showModal();
|
|
28280
|
+
requestAnimationFrame(() => {
|
|
28281
|
+
requestAnimationFrame(() => d4.setAttribute("data-open", ""));
|
|
28282
|
+
});
|
|
28283
|
+
} else if (window.confirm("Discard your changes?")) {
|
|
28284
|
+
this.forceClose = true;
|
|
28285
|
+
this.dialog.dispatchEvent(new CustomEvent("modal:request-close"));
|
|
28286
|
+
}
|
|
28287
|
+
}
|
|
28288
|
+
async #closeConfirm() {
|
|
28289
|
+
if (!this.hasConfirmDialogTarget) return;
|
|
28290
|
+
const d4 = this.confirmDialogTarget;
|
|
28291
|
+
if (!d4.open) return;
|
|
28292
|
+
d4.removeAttribute("data-open");
|
|
28293
|
+
const animations = d4.getAnimations({ subtree: true });
|
|
28294
|
+
await Promise.allSettled(animations.map((a4) => a4.finished));
|
|
28295
|
+
d4.close();
|
|
28296
|
+
}
|
|
28297
|
+
};
|
|
28298
|
+
|
|
28151
28299
|
// src/js/controllers/register_controllers.js
|
|
28152
28300
|
function register_controllers_default(application2) {
|
|
28153
28301
|
application2.register("password-visibility", password_visibility_controller_default);
|
|
@@ -28183,6 +28331,7 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
|
|
|
28183
28331
|
application2.register("row-click", row_click_controller_default);
|
|
28184
28332
|
application2.register("view-switcher", view_switcher_controller_default);
|
|
28185
28333
|
application2.register("autosubmit", autosubmit_controller_default);
|
|
28334
|
+
application2.register("dirty-form-guard", dirty_form_guard_controller_default);
|
|
28186
28335
|
}
|
|
28187
28336
|
|
|
28188
28337
|
// src/js/turbo/turbo_actions.js
|
|
@@ -28196,8 +28345,8 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
|
|
|
28196
28345
|
if (!frameId) return;
|
|
28197
28346
|
const frame = document.getElementById(frameId);
|
|
28198
28347
|
if (!frame) return;
|
|
28199
|
-
const
|
|
28200
|
-
if (
|
|
28348
|
+
const dialog2 = frame.querySelector("dialog");
|
|
28349
|
+
if (dialog2 && typeof dialog2.close === "function") dialog2.close();
|
|
28201
28350
|
frame.innerHTML = "";
|
|
28202
28351
|
frame.removeAttribute("src");
|
|
28203
28352
|
};
|
|
@@ -28209,6 +28358,103 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
|
|
|
28209
28358
|
frame.reload();
|
|
28210
28359
|
};
|
|
28211
28360
|
|
|
28361
|
+
// src/js/turbo/turbo_confirm.js
|
|
28362
|
+
var dialog;
|
|
28363
|
+
var messageEl;
|
|
28364
|
+
var confirmButton;
|
|
28365
|
+
var cancelButton;
|
|
28366
|
+
function ensureDialog() {
|
|
28367
|
+
if (dialog) {
|
|
28368
|
+
if (!dialog.isConnected) document.body.appendChild(dialog);
|
|
28369
|
+
return;
|
|
28370
|
+
}
|
|
28371
|
+
dialog = document.createElement("dialog");
|
|
28372
|
+
dialog.className = [
|
|
28373
|
+
"pu-dialog",
|
|
28374
|
+
"top-1/2",
|
|
28375
|
+
"-translate-y-1/2",
|
|
28376
|
+
"left-1/2",
|
|
28377
|
+
"-translate-x-1/2",
|
|
28378
|
+
"w-full",
|
|
28379
|
+
"max-w-md",
|
|
28380
|
+
"p-0",
|
|
28381
|
+
"open:flex",
|
|
28382
|
+
"flex-col",
|
|
28383
|
+
"opacity-0",
|
|
28384
|
+
"scale-95",
|
|
28385
|
+
"data-[open]:opacity-100",
|
|
28386
|
+
"data-[open]:scale-100",
|
|
28387
|
+
"transition-[opacity,transform]",
|
|
28388
|
+
"duration-200",
|
|
28389
|
+
"ease-out"
|
|
28390
|
+
].join(" ");
|
|
28391
|
+
dialog.setAttribute("aria-labelledby", "pu-turbo-confirm-message");
|
|
28392
|
+
const header = document.createElement("div");
|
|
28393
|
+
header.className = "px-6 pt-5 pb-4 border-b border-[var(--pu-border)]";
|
|
28394
|
+
messageEl = document.createElement("h2");
|
|
28395
|
+
messageEl.id = "pu-turbo-confirm-message";
|
|
28396
|
+
messageEl.className = "text-lg font-semibold text-[var(--pu-text)]";
|
|
28397
|
+
header.appendChild(messageEl);
|
|
28398
|
+
const footer = document.createElement("div");
|
|
28399
|
+
footer.className = "flex items-center justify-end gap-2 px-6 py-4";
|
|
28400
|
+
cancelButton = document.createElement("button");
|
|
28401
|
+
cancelButton.type = "button";
|
|
28402
|
+
cancelButton.className = "pu-btn pu-btn-md pu-btn-outline";
|
|
28403
|
+
cancelButton.textContent = "Cancel";
|
|
28404
|
+
confirmButton = document.createElement("button");
|
|
28405
|
+
confirmButton.type = "button";
|
|
28406
|
+
confirmButton.className = "pu-btn pu-btn-md pu-btn-primary";
|
|
28407
|
+
confirmButton.textContent = "Confirm";
|
|
28408
|
+
footer.appendChild(cancelButton);
|
|
28409
|
+
footer.appendChild(confirmButton);
|
|
28410
|
+
dialog.appendChild(header);
|
|
28411
|
+
dialog.appendChild(footer);
|
|
28412
|
+
document.body.appendChild(dialog);
|
|
28413
|
+
}
|
|
28414
|
+
async function animateClose() {
|
|
28415
|
+
dialog.removeAttribute("data-open");
|
|
28416
|
+
const animations = dialog.getAnimations({ subtree: true });
|
|
28417
|
+
await Promise.allSettled(animations.map((a4) => a4.finished));
|
|
28418
|
+
if (dialog.open) dialog.close();
|
|
28419
|
+
}
|
|
28420
|
+
function themedConfirm(message) {
|
|
28421
|
+
ensureDialog();
|
|
28422
|
+
messageEl.textContent = message || "Are you sure?";
|
|
28423
|
+
return new Promise((resolve) => {
|
|
28424
|
+
let settled = false;
|
|
28425
|
+
const settle = (value) => {
|
|
28426
|
+
if (settled) return;
|
|
28427
|
+
settled = true;
|
|
28428
|
+
cleanup();
|
|
28429
|
+
resolve(value);
|
|
28430
|
+
animateClose();
|
|
28431
|
+
};
|
|
28432
|
+
const onConfirm = () => settle(true);
|
|
28433
|
+
const onCancel = () => settle(false);
|
|
28434
|
+
const onClose = () => settle(false);
|
|
28435
|
+
const cleanup = () => {
|
|
28436
|
+
confirmButton.removeEventListener("click", onConfirm);
|
|
28437
|
+
cancelButton.removeEventListener("click", onCancel);
|
|
28438
|
+
dialog.removeEventListener("close", onClose);
|
|
28439
|
+
};
|
|
28440
|
+
confirmButton.addEventListener("click", onConfirm);
|
|
28441
|
+
cancelButton.addEventListener("click", onCancel);
|
|
28442
|
+
dialog.addEventListener("close", onClose);
|
|
28443
|
+
dialog.showModal();
|
|
28444
|
+
requestAnimationFrame(() => {
|
|
28445
|
+
requestAnimationFrame(() => dialog.setAttribute("data-open", ""));
|
|
28446
|
+
});
|
|
28447
|
+
confirmButton.focus();
|
|
28448
|
+
});
|
|
28449
|
+
}
|
|
28450
|
+
if (typeof window !== "undefined" && window.Turbo) {
|
|
28451
|
+
if (window.Turbo.config?.forms) {
|
|
28452
|
+
window.Turbo.config.forms.confirm = themedConfirm;
|
|
28453
|
+
} else if (window.Turbo.setConfirmMethod) {
|
|
28454
|
+
window.Turbo.setConfirmMethod(themedConfirm);
|
|
28455
|
+
}
|
|
28456
|
+
}
|
|
28457
|
+
|
|
28212
28458
|
// src/js/plutonium.js
|
|
28213
28459
|
var application = Application.start();
|
|
28214
28460
|
register_controllers_default(application);
|