initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
import { ref, h } from 'vue'
/**
* Global modal visibility state
* Shared across the entire application
*/
const show = ref(false)
/**
* Modal title text
* Can also be `false` to hide the header
* @type {import('vue').Ref<string|boolean>}
*/
const title = ref('')
/**
* Modal body content
* Can be a string, VNode, or Vue component
* @type {import('vue').Ref<any>}
*/
const body = ref(null)
/**
* Modal footer content
* Usually a Vue component, render function, or null
* @type {import('vue').Ref<any>}
*/
const footer = ref(null)
/**
* Global modal composable.
*
* Provides a single shared modal instance
* that can be opened, updated, and closed from anywhere.
*
* Supports string content or Vue components in the body and footer.
*
* Usage:
* ```js
* import { useModal } from '@/composables/useModal'
* import { h } from 'vue'
* import UserDetails from '@/components/UserDetails.vue'
*
* const modal = useModal()
*
* // Open with a string body
* modal.open({ title: 'Notice', body: 'Hello world' })
*
* // Open with a Vue component body
* modal.open({
* title: 'User Details',
* body: h(UserDetails, { userId: 42 })
* })
* ```
*/
export function useModal() {
/** Opens the modal */
function open({ title: t = '', body: b = null, footer: f = null } = {}) {
title.value = t
body.value = b
footer.value = f
show.value = true
}
/** Closes the modal */
function close() {
show.value = false
}
/** Update modal body dynamically */
function setBody(content) {
body.value = content
}
/** Update modal footer dynamically */
function setFooter(content) {
footer.value = content
}
/** Quickly replaces the currently open modal with new content */
function quickDismiss({ title: t = '', body: b = '', footer: f = null, condition = true, onShown = null } = {}) {
if (!condition) return
show.value = false
setTimeout(() => {
open({ title: t, body: b, footer: f })
if (typeof onShown === 'function') onShown()
}, 10)
}
/**
* Displays a modal with navigation buttons for SPA routing.
*
* Each button can pass props to the destination page component.
*
* @param {Object} options
* @param {string} options.title - Modal title
* @param {string|VNode|Component} options.body - Modal body
* @param {Array<Array>} options.buttons - Buttons array:
* [pageName, buttonText, target?, props?]
* Example: ['Users.View', 'View User', 0, { userId: 42 }]
* @param {boolean} [options.condition=true] - Show modal if true
* @param {Function} [options.onShown] - Callback after modal opens
*/
/**
* @param {Object} options
* @param {string} options.title - Modal title
* @param {string|VNode|Component} options.body - Modal body
* @param {Array<Array>} options.buttons - Buttons array:
* [pageName, buttonText, target?, props?]
* @param {Function} options.navigate - The navigate function from useNavigate()
* @param {boolean} [options.condition=true] - Show modal if true
* @param {Function} [options.onShown] - Callback after modal opens
*/
function quickDismissGoto({ title: t = '', body: b = '', buttons = [], navigate = null, condition = true, onShown = null } = {}) {
if (!condition) return
const normalized = Array.isArray(buttons[0]) ? buttons : [buttons]
const FooterComponent = {
render() {
const renderedButtons = normalized.map(btn => {
const page = btn[0]
const text = btn[1] || 'Go to Page'
const props = btn[3] || {}
return h('button', {
class: 'btn btn-primary flex-fill py-2 rounded-3 shadow-sm fw-bold',
onClick: () => {
close()
if (typeof navigate === 'function') navigate({ page, props })
}
}, text)
})
return h('div', { class: 'd-flex w-100 gap-3' }, renderedButtons)
}
}
show.value = false
setTimeout(() => {
open({ title: t, body: b, footer: FooterComponent })
if (typeof onShown === 'function') onShown()
}, 10)
}
/**
* Displays a Yes / No modal with custom callbacks.
*
* @param {Object} options
* @param {string} options.title - Modal title
* @param {string|VNode|Component} options.body - Modal body
* @param {string} options.yesText - Yes button text
* @param {Function} options.onYes - Yes callback
* @param {string} options.noText - No button text
* @param {Function} options.onNo - No callback
* @param {boolean} [options.condition=true] - Show modal if true
* @param {Function} [options.onShown] - Callback after modal opens
*/
function yesNoModal({
title: t = '',
body: b = '',
yesText = 'Yes',
yesClass = 'btn btn-primary w-50 py-2 rounded-3 shadow-sm fw-bold',
onYes = null,
noText = 'No',
noClass = 'btn btn-light w-50 py-2 rounded-3 border fw-bold text-muted',
onNo = null,
condition = true,
onShown = null
} = {}) {
if (!condition) return
const FooterComponent = {
render() {
return h('div', { class: 'd-flex w-100 gap-3' }, [
h('button', { class: noClass, onClick: () => { close(); onNo?.() } }, noText),
h('button', { class: yesClass, onClick: () => { close(); onYes?.() } }, yesText)
])
}
}
show.value = false
setTimeout(() => {
open({ title: t, body: b, footer: FooterComponent })
onShown?.()
}, 10)
}
/**
* Displays a Continue / Cancel modal.
*
* @param {Object} options
* @param {string} options.title - Modal title
* @param {string|VNode|Component} options.body - Modal body content
* @param {Function} options.onContinue - Callback when Continue is clicked
* @param {string} [options.continueText='Continue'] - Continue button text
* @param {string} [options.cancelText='Cancel'] - Cancel button text
* @param {string} [options.continueClass='btn btn-danger'] - Continue button CSS
* @param {string} [options.cancelClass='btn btn-warning'] - Cancel button CSS
* @param {boolean} [options.condition=true] - Only show if true
* @param {Function|null} [options.onShown] - Callback after modal opens
*
* @example
* modal.continueCancelModal({
* title: 'Unsaved Changes',
* body: 'You have unsaved changes. Continue?',
* onContinue: () => saveAndProceed()
* })
*/
function continueCancelModal({
title = '',
body = '',
onContinue = null,
continueText = 'Continue',
cancelText = 'Cancel',
continueClass = 'btn btn-danger w-50 py-2 rounded-3 shadow-sm fw-bold',
cancelClass = 'btn btn-light w-50 py-2 rounded-3 border fw-bold text-muted',
condition = true,
showCancel = true,
onShown = null
} = {}) {
if (condition !== true) return
const FooterComponent = {
render() {
const buttons = []
// Cancel button
if (showCancel) {
buttons.push(
h(
'button',
{
class: cancelClass,
onClick: () => {
close()
}
},
cancelText
)
)
}
// Continue button
buttons.push(
h(
'button',
{
class: continueClass,
onClick: () => {
close()
if (typeof onContinue === 'function') {
onContinue()
}
}
},
continueText
)
)
return h('div', { class: 'd-flex w-100 gap-3' }, buttons)
}
}
// Close any existing modal first
show.value = false
setTimeout(() => {
open({
title,
body,
footer: FooterComponent
})
if (typeof onShown === 'function') {
onShown()
}
}, 10)
}
/**
* Hides the currently open modal.
* Vue replacement for hideallmodals() and hidemodal()
*/
function hideModal() {
close()
}
/**
* Alias for hideModal (migration helper)
*/
function hideAllModals() {
close()
}
return {
show,
title,
body,
footer,
open,
close,
setBody,
setFooter,
quickDismiss,
quickDismissGoto,
yesNoModal,
continueCancelModal,
hideModal,
hideAllModals,
}
}