ruby_ui_admin 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
- data/app/components/ruby_ui_admin/ui/base.rb +26 -0
- data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
- data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
- data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
- data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
- data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
- data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
- data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
- data/app/components/ruby_ui_admin/ui/select.rb +29 -0
- data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
- data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
- data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
- data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
- data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
- data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
- data/app/components/ruby_ui_admin/ui.rb +12 -0
- data/app/components/ruby_ui_admin/views/action.rb +37 -0
- data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
- data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
- data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
- data/app/components/ruby_ui_admin/views/base.rb +301 -0
- data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
- data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
- data/app/components/ruby_ui_admin/views/docs.rb +91 -0
- data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
- data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
- data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
- data/app/components/ruby_ui_admin/views/form.rb +151 -0
- data/app/components/ruby_ui_admin/views/home.rb +35 -0
- data/app/components/ruby_ui_admin/views/index.rb +324 -0
- data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
- data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
- data/app/components/ruby_ui_admin/views/show.rb +77 -0
- data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
- data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
- data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
- data/app/components/ruby_ui_admin/views/translation.rb +12 -0
- data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
- data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
- data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
- data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
- data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
- data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
- data/config/locales/ruby_ui_admin.en.yml +59 -0
- data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
- data/config/routes/dynamic_routes.rb +11 -0
- data/config/routes.rb +23 -0
- data/docs/README.md +53 -0
- data/docs/actions/overview.md +112 -0
- data/docs/authorization/action-policy.md +72 -0
- data/docs/authorization/adapters.md +101 -0
- data/docs/authorization/explicit-mode.md +45 -0
- data/docs/authorization/field-authorization.md +53 -0
- data/docs/authorization/policies.md +62 -0
- data/docs/authorization/scopes.md +48 -0
- data/docs/customization/controllers.md +62 -0
- data/docs/customization/ejecting.md +55 -0
- data/docs/customization/javascript.md +161 -0
- data/docs/customization/theming-rubyui.md +80 -0
- data/docs/dashboards/overview.md +65 -0
- data/docs/fields/overview.md +94 -0
- data/docs/filters/overview.md +73 -0
- data/docs/generators-and-tasks/overview.md +42 -0
- data/docs/getting-started/authentication.md +98 -0
- data/docs/getting-started/configuration.md +174 -0
- data/docs/getting-started/installation.md +201 -0
- data/docs/getting-started/internationalization.md +50 -0
- data/docs/getting-started/practical-guide.md +310 -0
- data/docs/resources/associations.md +66 -0
- data/docs/resources/index-customization.md +68 -0
- data/docs/resources/overview.md +50 -0
- data/docs/resources/scopes.md +54 -0
- data/docs/resources/tabs-panels.md +65 -0
- data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
- data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
- data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
- data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
- data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
- data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
- data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
- data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
- data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
- data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
- data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
- data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
- data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
- data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
- data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
- data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
- data/lib/ruby_ui_admin/authorization.rb +40 -0
- data/lib/ruby_ui_admin/base_action.rb +211 -0
- data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
- data/lib/ruby_ui_admin/base_policy.rb +38 -0
- data/lib/ruby_ui_admin/base_resource.rb +503 -0
- data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
- data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
- data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
- data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
- data/lib/ruby_ui_admin/configuration.rb +177 -0
- data/lib/ruby_ui_admin/current.rb +20 -0
- data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
- data/lib/ruby_ui_admin/engine.rb +80 -0
- data/lib/ruby_ui_admin/execution_context.rb +36 -0
- data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
- data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
- data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
- data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
- data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
- data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
- data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
- data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
- data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
- data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
- data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
- data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
- data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
- data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
- data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
- data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
- data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
- data/lib/ruby_ui_admin/menu/builder.rb +94 -0
- data/lib/ruby_ui_admin/resource_manager.rb +82 -0
- data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
- data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
- data/lib/ruby_ui_admin/structure.rb +40 -0
- data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
- data/lib/ruby_ui_admin/version.rb +5 -0
- data/lib/ruby_ui_admin/view.rb +34 -0
- data/lib/ruby_ui_admin.rb +110 -0
- data/lib/tasks/ruby_ui_admin.rake +71 -0
- data/public/ruby-ui-admin-assets/application.css +2 -0
- data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
- data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
- data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
- data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
- data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
- data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
- data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
- metadata +379 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const VARIANTS = ["default", "success", "error", "warning", "info", "loading"]
|
|
4
|
+
|
|
5
|
+
let streamActionRegistered = false
|
|
6
|
+
|
|
7
|
+
function registerStreamAction() {
|
|
8
|
+
if (streamActionRegistered) return
|
|
9
|
+
if (typeof window === "undefined") return
|
|
10
|
+
const Turbo = window.Turbo
|
|
11
|
+
if (!Turbo?.StreamActions) return
|
|
12
|
+
Turbo.StreamActions.toast = function () {
|
|
13
|
+
const detail = {}
|
|
14
|
+
for (const attr of this.attributes) {
|
|
15
|
+
if (attr.name === "action" || attr.name === "target" || attr.name === "targets") continue
|
|
16
|
+
detail[attr.name] = attr.value
|
|
17
|
+
}
|
|
18
|
+
if (detail.duration != null && detail.duration !== "") detail.duration = Number(detail.duration)
|
|
19
|
+
if (detail.dismissible != null) detail.dismissible = detail.dismissible !== "false"
|
|
20
|
+
window.dispatchEvent(new CustomEvent("ruby-ui:toast", { detail }))
|
|
21
|
+
}
|
|
22
|
+
streamActionRegistered = true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Connects to data-controller="ruby-ui--toaster"
|
|
26
|
+
export default class extends Controller {
|
|
27
|
+
static targets = ["skeleton", "toast", "actionTpl", "cancelTpl", "closeTpl"]
|
|
28
|
+
static values = {
|
|
29
|
+
position: { type: String, default: "bottom-right" },
|
|
30
|
+
expand: { type: Boolean, default: false },
|
|
31
|
+
max: { type: Number, default: 3 },
|
|
32
|
+
duration: { type: Number, default: 4000 },
|
|
33
|
+
gap: { type: Number, default: 14 },
|
|
34
|
+
offset: { type: Number, default: 24 },
|
|
35
|
+
theme: { type: String, default: "system" },
|
|
36
|
+
richColors: { type: Boolean, default: false },
|
|
37
|
+
closeButton: { type: Boolean, default: false },
|
|
38
|
+
hotkey: { type: String, default: "alt+t" },
|
|
39
|
+
dir: { type: String, default: "ltr" },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
connect() {
|
|
43
|
+
// `||=` so state survives a `toastTargetConnected` that fires before `connect()` —
|
|
44
|
+
// happens with server-rendered (flash) toasts present in the initial DOM.
|
|
45
|
+
this._heights ||= new Map()
|
|
46
|
+
this._resizeObservers ||= new WeakMap()
|
|
47
|
+
this._expanded = this.expandValue
|
|
48
|
+
this._listEl = this.element.querySelector("ol") || (this.element.tagName === "OL" ? this.element : null)
|
|
49
|
+
this._registerGlobalApi()
|
|
50
|
+
registerStreamAction()
|
|
51
|
+
if (!this._listEl) return
|
|
52
|
+
|
|
53
|
+
this._onPointerEnter = () => this._setExpanded(true)
|
|
54
|
+
this._onPointerLeave = () => { if (!this.expandValue) this._setExpanded(false) }
|
|
55
|
+
this._onWindowToast = (e) => this._spawn(e.detail || {})
|
|
56
|
+
this._onWindowDismissAll = () => this._dismissById(null)
|
|
57
|
+
this._onKey = this._onKey.bind(this)
|
|
58
|
+
|
|
59
|
+
window.addEventListener("ruby-ui:toast", this._onWindowToast)
|
|
60
|
+
window.addEventListener("ruby-ui:toast:dismiss-all", this._onWindowDismissAll)
|
|
61
|
+
this._listEl.addEventListener("pointerenter", this._onPointerEnter)
|
|
62
|
+
this._listEl.addEventListener("pointerleave", this._onPointerLeave)
|
|
63
|
+
document.addEventListener("keydown", this._onKey)
|
|
64
|
+
|
|
65
|
+
// Position any toasts already in the DOM (server-rendered flash messages).
|
|
66
|
+
this._reflow()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
disconnect() {
|
|
70
|
+
window.removeEventListener("ruby-ui:toast", this._onWindowToast)
|
|
71
|
+
window.removeEventListener("ruby-ui:toast:dismiss-all", this._onWindowDismissAll)
|
|
72
|
+
this._listEl?.removeEventListener("pointerenter", this._onPointerEnter)
|
|
73
|
+
this._listEl?.removeEventListener("pointerleave", this._onPointerLeave)
|
|
74
|
+
document.removeEventListener("keydown", this._onKey)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
toastTargetConnected(el) {
|
|
78
|
+
// May fire before connect() for toasts present in the initial DOM.
|
|
79
|
+
this._heights ||= new Map()
|
|
80
|
+
this._resizeObservers ||= new WeakMap()
|
|
81
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
82
|
+
const ro = new ResizeObserver(() => {
|
|
83
|
+
this._heights.set(el, el.offsetHeight)
|
|
84
|
+
this._reflow()
|
|
85
|
+
})
|
|
86
|
+
ro.observe(el)
|
|
87
|
+
this._resizeObservers.set(el, ro)
|
|
88
|
+
}
|
|
89
|
+
this._heights.set(el, el.offsetHeight || 64)
|
|
90
|
+
this._reflow()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
toastTargetDisconnected(el) {
|
|
94
|
+
this._resizeObservers.get(el)?.disconnect()
|
|
95
|
+
this._resizeObservers.delete(el)
|
|
96
|
+
this._heights.delete(el)
|
|
97
|
+
this._reflow()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_spawn(detail) {
|
|
101
|
+
const variant = VARIANTS.includes(detail.variant) ? detail.variant : "default"
|
|
102
|
+
const tpl = this._skeletonFor(variant)
|
|
103
|
+
if (!tpl) return null
|
|
104
|
+
if (detail.position) {
|
|
105
|
+
this.element.setAttribute("data-position", detail.position)
|
|
106
|
+
this.positionValue = detail.position
|
|
107
|
+
}
|
|
108
|
+
const node = tpl.content.firstElementChild.cloneNode(true)
|
|
109
|
+
|
|
110
|
+
node.id = detail.id || `toast-${this._uuid()}`
|
|
111
|
+
if (detail.duration != null) {
|
|
112
|
+
const dur = detail.duration === Infinity ? 0 : detail.duration
|
|
113
|
+
node.setAttribute("data-ruby-ui--toast-duration-value", String(dur))
|
|
114
|
+
}
|
|
115
|
+
if (detail.dismissible === false) {
|
|
116
|
+
node.setAttribute("data-ruby-ui--toast-dismissible-value", "false")
|
|
117
|
+
}
|
|
118
|
+
if (detail.className) node.className += ` ${detail.className}`
|
|
119
|
+
|
|
120
|
+
const titleEl = node.querySelector('[data-slot="title"]')
|
|
121
|
+
if (titleEl) titleEl.textContent = detail.title || detail.message || ""
|
|
122
|
+
const descEl = node.querySelector('[data-slot="description"]')
|
|
123
|
+
if (descEl) {
|
|
124
|
+
if (detail.description) descEl.textContent = detail.description
|
|
125
|
+
else descEl.remove()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (detail.action && detail.action.label && this.hasActionTplTarget) {
|
|
129
|
+
const btn = this._cloneSlot(this.actionTplTarget)
|
|
130
|
+
btn.textContent = detail.action.label
|
|
131
|
+
btn.addEventListener("click", (ev) => {
|
|
132
|
+
try { detail.action.onClick?.(ev) } finally {
|
|
133
|
+
node.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
node.appendChild(btn)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (detail.cancel && detail.cancel.label && this.hasCancelTplTarget) {
|
|
140
|
+
const btn = this._cloneSlot(this.cancelTplTarget)
|
|
141
|
+
btn.textContent = detail.cancel.label
|
|
142
|
+
node.appendChild(btn)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (detail.closeButton && this.hasCloseTplTarget) {
|
|
146
|
+
const x = this._cloneSlot(this.closeTplTarget)
|
|
147
|
+
node.classList.add("pr-10")
|
|
148
|
+
node.appendChild(x)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this._listEl.appendChild(node)
|
|
152
|
+
return node.id
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_dismissById(id) {
|
|
156
|
+
if (!id) {
|
|
157
|
+
this.toastTargets.forEach((el) =>
|
|
158
|
+
el.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
|
|
163
|
+
if (el) el.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_skeletonFor(variant) {
|
|
167
|
+
return this.skeletonTargets.find((t) => t.dataset.variant === variant)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_cloneSlot(tpl) {
|
|
171
|
+
return tpl.content.firstElementChild.cloneNode(true)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_setExpanded(value) {
|
|
175
|
+
if (this._expanded === value) return
|
|
176
|
+
this._expanded = value
|
|
177
|
+
document.dispatchEvent(new CustomEvent(value ? "ruby-ui:toast:pause" : "ruby-ui:toast:resume"))
|
|
178
|
+
this._reflow()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_reflow() {
|
|
182
|
+
if (!this._listEl) return
|
|
183
|
+
const isBottom = this.positionValue.startsWith("bottom")
|
|
184
|
+
const items = this.toastTargets
|
|
185
|
+
const order = isBottom ? items.slice().reverse() : items.slice()
|
|
186
|
+
const heights = order.map(el => this._heights.get(el) || el.offsetHeight || 64)
|
|
187
|
+
const gap = this.gapValue
|
|
188
|
+
const peekOffset = 16
|
|
189
|
+
const peekScaleStep = 0.05
|
|
190
|
+
const peekOpacityStep = 0.2
|
|
191
|
+
|
|
192
|
+
const expandedHeight = heights.reduce((a, b) => a + b, 0) + gap * Math.max(0, heights.length - 1)
|
|
193
|
+
const collapsedHeight = (heights[0] || 0) + Math.min(2, Math.max(0, heights.length - 1)) * peekOffset
|
|
194
|
+
this._listEl.style.minHeight = `${this._expanded ? expandedHeight : collapsedHeight}px`
|
|
195
|
+
|
|
196
|
+
let acc = 0
|
|
197
|
+
order.forEach((el, i) => {
|
|
198
|
+
const visible = i < this.maxValue
|
|
199
|
+
let yOffset, scale, opacity
|
|
200
|
+
|
|
201
|
+
if (this._expanded) {
|
|
202
|
+
yOffset = acc + i * gap
|
|
203
|
+
scale = 1
|
|
204
|
+
opacity = visible ? 1 : 0
|
|
205
|
+
} else {
|
|
206
|
+
yOffset = i * peekOffset
|
|
207
|
+
scale = Math.max(0.85, 1 - i * peekScaleStep)
|
|
208
|
+
opacity = visible ? Math.max(0, 1 - i * peekOpacityStep) : 0
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sign = isBottom ? -1 : 1
|
|
212
|
+
const ty = sign * yOffset
|
|
213
|
+
|
|
214
|
+
el.style.setProperty("--opacity", String(opacity))
|
|
215
|
+
el.style.setProperty("--scale", String(scale))
|
|
216
|
+
el.style.setProperty("--y-offset", `${ty}px`)
|
|
217
|
+
el.style.transformOrigin = isBottom ? "center bottom" : "center top"
|
|
218
|
+
el.style.top = isBottom ? "auto" : "0"
|
|
219
|
+
el.style.bottom = isBottom ? "0" : "auto"
|
|
220
|
+
el.style.transform = `translate3d(0, ${ty}px, 0) scale(${scale})`
|
|
221
|
+
el.style.zIndex = String(1000 - i)
|
|
222
|
+
el.style.pointerEvents = visible ? "auto" : "none"
|
|
223
|
+
el.tabIndex = visible ? 0 : -1
|
|
224
|
+
|
|
225
|
+
acc += heights[i] || 0
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
this._enforceMax(items)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_enforceMax(items) {
|
|
232
|
+
if (items.length <= this.maxValue) return
|
|
233
|
+
const isBottom = this.positionValue.startsWith("bottom")
|
|
234
|
+
const dropping = items.length - this.maxValue
|
|
235
|
+
const candidates = isBottom ? items.slice(0, dropping) : items.slice(-dropping)
|
|
236
|
+
candidates.forEach(el => {
|
|
237
|
+
if (el.dataset.state !== "closing") {
|
|
238
|
+
el.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_onKey(e) {
|
|
244
|
+
const parts = (this.hotkeyValue || "alt+t").split("+")
|
|
245
|
+
const key = parts.pop()
|
|
246
|
+
const wantAlt = parts.includes("alt")
|
|
247
|
+
const wantCtrl = parts.includes("ctrl")
|
|
248
|
+
const wantMeta = parts.includes("meta")
|
|
249
|
+
if (e.key.toLowerCase() !== key.toLowerCase()) return
|
|
250
|
+
if (wantAlt !== e.altKey) return
|
|
251
|
+
if (wantCtrl !== e.ctrlKey) return
|
|
252
|
+
if (wantMeta !== e.metaKey) return
|
|
253
|
+
e.preventDefault()
|
|
254
|
+
const first = this._listEl.firstElementChild
|
|
255
|
+
first?.focus()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_registerGlobalApi() {
|
|
259
|
+
const fire = (variant, message, opts = {}) =>
|
|
260
|
+
window.dispatchEvent(new CustomEvent("ruby-ui:toast", {
|
|
261
|
+
detail: { ...opts, variant, message: opts.title || message }
|
|
262
|
+
}))
|
|
263
|
+
|
|
264
|
+
const api = (message, opts) => fire("default", message, opts)
|
|
265
|
+
api.success = (m, o) => fire("success", m, o)
|
|
266
|
+
api.error = (m, o) => fire("error", m, o)
|
|
267
|
+
api.warning = (m, o) => fire("warning", m, o)
|
|
268
|
+
api.info = (m, o) => fire("info", m, o)
|
|
269
|
+
api.loading = (m, o = {}) => fire("loading", m, { ...o, duration: o.duration ?? 0 })
|
|
270
|
+
api.dismiss = (id) => {
|
|
271
|
+
if (id) this._dismissById(id)
|
|
272
|
+
else window.dispatchEvent(new CustomEvent("ruby-ui:toast:dismiss-all"))
|
|
273
|
+
}
|
|
274
|
+
api.promise = (p, msgs = {}) => {
|
|
275
|
+
const id = `toast-${this._uuid()}`
|
|
276
|
+
fire("loading", typeof msgs.loading === "function" ? msgs.loading() : (msgs.loading || "Loading..."), { id, duration: 0 })
|
|
277
|
+
Promise.resolve(p).then(
|
|
278
|
+
(val) => this._mutate(id, "success", typeof msgs.success === "function" ? msgs.success(val) : msgs.success),
|
|
279
|
+
(err) => this._mutate(id, "error", typeof msgs.error === "function" ? msgs.error(err) : msgs.error)
|
|
280
|
+
)
|
|
281
|
+
return id
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
window.RubyUI = window.RubyUI || {}
|
|
285
|
+
window.RubyUI.toast = api
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_mutate(id, variant, text) {
|
|
289
|
+
const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
|
|
290
|
+
if (!el) return
|
|
291
|
+
el.dataset.variant = variant
|
|
292
|
+
el.setAttribute("role", variant === "error" ? "alert" : "status")
|
|
293
|
+
this._swapIcon(el, variant)
|
|
294
|
+
const t = el.querySelector('[data-slot="title"]')
|
|
295
|
+
if (t && text) t.textContent = text
|
|
296
|
+
const dur = String(this.durationValue)
|
|
297
|
+
el.setAttribute("data-ruby-ui--toast-duration-value", dur)
|
|
298
|
+
el.dispatchEvent(new CustomEvent("ruby-ui:toast:restart", { bubbles: true }))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_swapIcon(el, variant) {
|
|
302
|
+
const iconHost = el.querySelector('[data-slot="icon"]')
|
|
303
|
+
if (!iconHost) return
|
|
304
|
+
const tpl = this._skeletonFor(variant)
|
|
305
|
+
if (!tpl) return
|
|
306
|
+
const sourceIcon = tpl.content.firstElementChild?.querySelector('[data-slot="icon"]')
|
|
307
|
+
iconHost.innerHTML = sourceIcon ? sourceIcon.innerHTML : ""
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_uuid() {
|
|
311
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID()
|
|
312
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36)
|
|
313
|
+
}
|
|
314
|
+
}
|