Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 36 additions & 21 deletions src/components/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,19 @@ function tooltipIdFor(rowIndex: number, actionIndex: number): string {
return `datatable-action-tooltip-${rowIndex}-${actionIndex}`
}

function cleanActionText(value: unknown): string {
return DOMPurify.sanitize(String(value ?? ''), { ALLOWED_TAGS: [], ALLOWED_ATTR: [] })
.replace(/\s+/g, ' ')
.trim()
}

function getActionAriaLabel(action: NonNullable<TableColumn['actions']>[number], elem: any, col: TableColumn): string {
const title = getActionTitle(action, elem)
const isDeleteAction = title.toLocaleLowerCase() === t('delete').toLocaleLowerCase()
const actionLabel = isDeleteAction ? t('remove') : title
return cleanActionText(actionLabel || col.label || t('actions'))
}

const displayElemRange = computed(() => {
const begin = (props.currentPage - 1) * props.elementList.length
const end = begin + props.elementList.length
Expand Down Expand Up @@ -455,37 +468,37 @@ const RenderCell = defineComponent<{
const isReloading = computed(() => props.isLoading || pendingReset.value)
const isAdding = computed(() => props.isLoading || pendingAdd.value)
const paginationClass = computed(() => props.mobileFixedPagination
? 'fixed bottom-0 left-0 z-40 flex items-center justify-between w-full p-4 bg-white md:relative md:pt-4 md:bg-transparent dark:bg-gray-900 dark:md:bg-transparent'
: 'flex items-center justify-between w-full p-4 bg-white md:relative md:pt-4 md:bg-transparent dark:bg-gray-900 dark:md:bg-transparent')
? 'fixed bottom-0 left-0 z-40 flex items-center justify-between w-full gap-3 border-t border-gray-200 bg-white p-4 md:relative md:border-t-0 md:pt-4 md:bg-transparent dark:border-gray-700 dark:bg-gray-900 dark:md:bg-transparent'
: 'flex items-center justify-between w-full gap-3 p-4 bg-white md:relative md:pt-4 md:bg-transparent dark:bg-gray-900 dark:md:bg-transparent')
</script>

<template>
<div class="pb-4 overflow-x-auto md:pb-0">
<div class="flex items-start justify-between p-3 pb-4 md:items-center">
<div class="flex h-10 md:mb-0">
<div class="pb-4 md:pb-0">
<div class="flex flex-col gap-3 p-3 pb-4 md:flex-row md:items-center md:justify-between">
<div class="flex flex-wrap items-center gap-2 md:mb-0">
<button
class="inline-flex items-center py-1.5 px-3 mr-2 text-sm font-medium text-gray-500 bg-white rounded-md border border-gray-300 cursor-pointer dark:text-white dark:bg-gray-800 dark:border-gray-600 hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 focus:outline-hidden"
class="inline-flex min-h-11 items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 touch-manipulation cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
type="button" @click="handleResetClick"
>
<IconReload v-if="!isReloading" class="m-1 md:mr-2" />
<Spinner v-else size="w-[16.8px] h-[16.8px] m-1 mr-2" />
<IconReload v-if="!isReloading" class="size-5" />
<Spinner v-else size="w-[16.8px] h-[16.8px]" />
<span class="hidden text-sm md:block">{{ t("reload") }}</span>
</button>
<div v-if="showAdd" class="p-px mr-2 rounded-lg from-cyan-500 to-purple-500 bg-linear-to-r">
<div v-if="showAdd" class="rounded-lg bg-linear-to-r from-cyan-500 to-purple-500 p-px">
<button
:data-test="addButtonTestId"
class="inline-flex items-center py-1.5 px-3 text-sm font-medium text-gray-500 bg-white rounded-md cursor-pointer dark:text-white dark:bg-gray-800 hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-700 dark:focus:ring-gray-700 focus:outline-hidden"
class="inline-flex min-h-11 items-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-gray-500 touch-manipulation cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 dark:focus:ring-gray-700"
type="button" @click="handleAddClick"
>
<plusOutline v-if="!isAdding" class="m-1 md:mr-2" />
<Spinner v-else size="w-[16.8px] h-[16.8px] m-1 mr-2" />
<plusOutline v-if="!isAdding" class="size-5" />
<Spinner v-else size="w-[16.8px] h-[16.8px]" />
<span class="hidden text-sm md:block">{{ t("add-one") }}</span>
</button>
</div>
<div v-if="filterText && filterList.length" class="h-10 d-dropdown">
<div v-if="filterText && filterList.length" class="d-dropdown">
<button
tabindex="0"
class="inline-flex items-center py-1.5 px-3 mr-2 h-full text-sm font-medium text-gray-500 bg-white rounded-md border border-gray-300 cursor-pointer dark:text-white dark:bg-gray-800 dark:border-gray-600 hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 focus:outline-hidden"
class="inline-flex min-h-11 items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 touch-manipulation cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
>
<div
v-if="filterActivated"
Expand Down Expand Up @@ -522,7 +535,7 @@ const paginationClass = computed(() => props.mobileFixedPagination
</div>
<button
v-if="isSelectAllEnabled"
class="inline-flex items-center self-end px-3 py-2 ml-auto mr-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg cursor-pointer dark:text-white dark:bg-gray-800 dark:border-gray-600 hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 focus:outline-hidden"
class="inline-flex min-h-11 items-center self-start rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 touch-manipulation cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 md:ml-auto md:self-end"
type="button" @click="
selectedRows = selectedRows.map(() => true);
emit('selectRow', selectedRows);
Expand All @@ -532,21 +545,21 @@ const paginationClass = computed(() => props.mobileFixedPagination
</button>
<button
v-if="isSelectAllEnabled"
class="inline-flex items-center self-end py-1.5 px-3 mr-2 text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-300 cursor-pointer dark:text-white dark:bg-gray-800 dark:border-gray-600 hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 focus:outline-hidden"
class="inline-flex min-h-11 items-center self-start rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 touch-manipulation cursor-pointer hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 md:self-end"
type="button" @click="emit('massDelete')"
>
<IconTrash class="h-6 text-red-500" />
</button>
<div class="flex overflow-hidden md:w-auto">
<div class="flex w-full min-w-0 overflow-hidden md:w-auto">
<FormKit
v-model="searchVal" :placeholder="searchPlaceholder" :prefix-icon="IconSearch"
:disabled="isLoading" enterkeyhint="send" :classes="{
outer: 'mb-0! md:w-96',
outer: 'mb-0! w-full md:w-96',
}"
/>
</div>
</div>
<div class="block">
<div class="block overflow-x-auto">
<table id="custom_table" class="w-full text-sm text-left text-gray-500 pb-14 md:pb-0 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:text-gray-400 dark:bg-gray-700">
<tr>
Expand Down Expand Up @@ -611,7 +624,8 @@ const paginationClass = computed(() => props.mobileFixedPagination
:disabled="isActionDisabled(action, elem)"
:aria-describedby="getActionTitle(action, elem) ? tooltipIdFor(i, actionIndex) : undefined"
:data-test="action.testId ? (typeof action.testId === 'function' ? action.testId(elem) : action.testId) : undefined"
class="p-2 text-gray-500 rounded-md cursor-pointer dark:text-gray-400 hover:text-gray-600 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:disabled:hover:text-gray-400 disabled:hover:bg-transparent disabled:hover:text-gray-500"
class="min-h-11 min-w-11 rounded-md p-2.5 text-gray-500 touch-manipulation cursor-pointer hover:bg-gray-200 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-gray-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:disabled:hover:text-gray-400"
:aria-label="getActionAriaLabel(action, elem, col)"
@click.stop="action.onClick(elem)"
>
<component :is="action.icon" />
Expand All @@ -629,7 +643,8 @@ const paginationClass = computed(() => props.mobileFixedPagination
</template>
<template v-else-if="col.icon">
<button
class="p-2 text-gray-500 rounded-md cursor-pointer dark:text-gray-400 hover:text-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700 dark:hover:text-gray-300"
class="min-h-11 min-w-11 rounded-md p-2.5 text-gray-500 touch-manipulation cursor-pointer hover:bg-gray-200 hover:text-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300"
:aria-label="col.label"
@click.stop="col.onClick ? col.onClick(elem) : () => { }"
>
<component :is="col.icon" />
Expand Down
6 changes: 3 additions & 3 deletions src/components/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ const { t } = useI18n()
</script>

<template>
<header class="bg-slate-100 backdrop-blur-xl dark:bg-slate-900">
<header class="border-b border-slate-200/80 bg-slate-100/95 backdrop-blur-xl dark:border-slate-800 dark:bg-slate-900/95">
<div class="px-2 sm:px-4 lg:px-6">
<div class="relative flex items-center justify-between h-16 -mb-px">
<!-- Header: Left side -->
<div class="flex items-center space-x-4">
<div v-if="displayStore.NavTitle && isMobile" class="pr-2">
<button
class="flex p-2 rounded-sm dark:text-white focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none text-slate-500 dark:hover:bg-slate-600 hover:bg-slate-300"
class="flex min-h-11 min-w-11 items-center justify-center rounded-md p-2 text-slate-500 touch-manipulation hover:bg-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:text-white dark:hover:bg-slate-600"
:aria-label="t('button-back')"
@click="back()"
>
Expand All @@ -50,7 +50,7 @@ const { t } = useI18n()
</div>
<!-- Hamburger button -->
<button
class="p-1 rounded-md lg:hidden dark:text-white focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none text-slate-500 dark:hover:text-slate-50 hover:text-slate-600"
class="min-h-11 min-w-11 rounded-md p-2 text-slate-500 touch-manipulation hover:bg-slate-200 hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:text-white dark:hover:bg-slate-800 dark:hover:text-slate-50 lg:hidden"
aria-controls="sidebar"
:aria-expanded="props.sidebarOpen"
:aria-label="props.sidebarOpen ? t('close-sidebar') : t('open-sidebar')"
Expand Down
2 changes: 1 addition & 1 deletion src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const tabs = computed<Tab[]>(() => {
<ul class="space-y-1 lg:space-y-2">
<li v-for="tab, i in tabs" :key="i">
<button
class="flex items-center p-3 w-full rounded-md transition duration-150 cursor-pointer lg:p-3 lg:rounded-lg focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none text-slate-200 min-h-[44px] lg:text-slate-200 lg:hover:bg-slate-700/50 hover:bg-slate-700/50 focus:ring-offset-slate-800"
class="group flex min-h-11 w-full cursor-pointer items-center rounded-md p-3 text-slate-200 touch-manipulation transition duration-150 hover:bg-slate-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-slate-800 lg:rounded-lg lg:p-3 lg:text-slate-200 lg:hover:bg-slate-700/50"
:class="{
'hover:bg-slate-700/50 lg:hover:bg-slate-700/50': !isTabActive(tab.key),
'bg-slate-700 text-white lg:bg-slate-700 lg:text-white': isTabActive(tab.key),
Expand Down
8 changes: 7 additions & 1 deletion src/components/TabSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ onMounted(() => {
<div class="w-full">
<ul class="flex flex-nowrap mr-3 md:block md:mr-0">
<li v-for="(m, i) in tabs" :key="i" class="mr-0.5 w-full cursor-pointer md:mr-0 md:mb-0.5" @click="openLink(m.key)">
<button :id="`tab-${m.label}`" class="flex items-center py-2 px-2.5 w-full whitespace-nowrap rounded-sm cursor-pointer hover:bg-gray-400 first-letter:uppercase" :class="{ 'text-blue-600 hover:text-blue-800': isActive(m.key), 'text-slate-400 hover:text-slate-100': !isActive(m.key) }">
<button
:id="`tab-${m.label}`"
type="button"
class="flex min-h-11 w-full cursor-pointer items-center rounded-md px-2.5 py-2 text-left whitespace-nowrap touch-manipulation hover:bg-gray-400 first-letter:uppercase focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-800"
:class="{ 'text-blue-600 hover:text-blue-800': isActive(m.key), 'text-slate-400 hover:text-slate-100': !isActive(m.key) }"
:aria-current="isActive(m.key) ? 'page' : undefined"
>
<component :is="m.icon" class="mr-2 w-4 h-4 fill-current shrink-0" />
<span class="hidden text-sm font-medium md:block first-letter:uppercase">{{ t(m.label) }}</span>
</button>
Expand Down
26 changes: 20 additions & 6 deletions src/components/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,42 @@ function activeTabColor(tab: string, isSecondary = false) {

const ulPrimaryClass = 'flex text-xs md:text-sm font-medium text-center text-gray-500 dark:text-gray-300 gap-1 pt-1 px-1'
const ulSecondaryClass = 'flex text-sm font-medium text-center text-gray-600 dark:text-gray-200 gap-2 py-2'
const buttonPrimaryClass = 'inline-flex items-center gap-2 px-3 py-2 min-w-[42px] min-h-[38px] rounded-t-md cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-900 transition-all group relative'
const buttonSecondaryClass = 'inline-flex items-center gap-2 px-3 py-1.5 rounded-md cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-900 transition-colors group'
const buttonPrimaryClass = 'inline-flex items-center justify-center gap-2 px-3 py-2 min-w-11 min-h-11 rounded-t-md cursor-pointer touch-manipulation focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-900 transition-colors group relative'
const buttonSecondaryClass = 'inline-flex items-center justify-center gap-2 px-3 py-2 min-h-11 min-w-11 rounded-md cursor-pointer touch-manipulation focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-900 transition-colors group'
const iconClass = 'w-5 h-5 transition-colors'
const labelClass = 'hidden md:block text-xs md:text-sm font-medium transition-colors first-letter:uppercase'
</script>

<template>
<div>
<div class="pb-0">
<ul :class="[ulPrimaryClass, noWrap ? 'flex-nowrap overflow-x-scroll no-scrollbar px-1' : 'flex-wrap']">
<ul :class="[ulPrimaryClass, noWrap ? 'flex-nowrap overflow-x-auto overscroll-x-contain no-scrollbar px-1' : 'flex-wrap']">
<li v-for="(tab, i) in tabs" :key="i" class="relative mr-2" :class="{ 'z-20': activeTab === tab.key }">
<button :class="[buttonPrimaryClass, activeTabColor(tab.key)]" @click="emit('update:activeTab', tab.key)">
<button
type="button"
:class="[buttonPrimaryClass, activeTabColor(tab.key)]"
:aria-current="activeTab === tab.key ? 'page' : undefined"
:aria-label="t(tab.label)"
:title="t(tab.label)"
@click="emit('update:activeTab', tab.key)"
>
<component :is="tab.icon" :class="iconClass" />
<span :class="labelClass">{{ t(tab.label) }}</span>
</button>
</li>
</ul>
</div>
<div class="relative -mt-px border-t bg-blue-50 dark:bg-slate-800/40 border-blue-200/60 dark:border-blue-800/70" :class="secondaryTabs?.length ? 'z-10' : 'z-0'">
<ul v-if="secondaryTabs?.length" :class="[ulSecondaryClass, noWrap ? 'flex-nowrap overflow-x-scroll no-scrollbar px-1' : 'flex-wrap']">
<ul v-if="secondaryTabs?.length" :class="[ulSecondaryClass, noWrap ? 'flex-nowrap overflow-x-auto overscroll-x-contain no-scrollbar px-1' : 'flex-wrap']">
<li v-for="(tab, i) in secondaryTabs" :key="i" class="mr-2">
<button :class="[buttonSecondaryClass, activeTabColor(tab.key, true)]" @click="emit('update:secondaryActiveTab', tab.key)">
<button
type="button"
:class="[buttonSecondaryClass, activeTabColor(tab.key, true)]"
:aria-current="secondaryActiveTab === tab.key ? 'page' : undefined"
:aria-label="t(tab.label)"
:title="t(tab.label)"
@click="emit('update:secondaryActiveTab', tab.key)"
>
<component :is="tab.icon" :class="iconClass" />
<span :class="labelClass">{{ t(tab.label) }}</span>
</button>
Expand Down
Loading
Loading