Refactor tasks UI to use reusable button and select components
Replace inline button markup with `ui.Button` component calls for consistency and maintainability. Add filter menu component with dropdown functionality. Convert roadmap mode toggle from link-based to select dropdown. Include filter counter badge and clear filter actions.
This commit is contained in:
parent
c80a8a875e
commit
d9bf94583b
6 changed files with 1115 additions and 587 deletions
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
.ui-button-md {
|
.ui-button-md {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
padding: 0.75rem 1.1rem;
|
padding: 0.7rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-button-lg {
|
.ui-button-lg {
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,38 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import taskmodel "xtablo-backend/internal/tasks"
|
import taskmodel "xtablo-backend/internal/tasks"
|
||||||
|
import "xtablo-backend/internal/web/ui"
|
||||||
|
|
||||||
templ TasksPageContent(vm TasksPageViewModel) {
|
templ TasksPageContent(vm TasksPageViewModel) {
|
||||||
<div class="min-h-screen" data-current-view={ string(vm.State.View) }>
|
<div class="min-h-screen" data-current-view={ string(vm.State.View) }>
|
||||||
<div class="px-4 md:px-6 pt-6 md:pt-10 pb-5">
|
<div class="px-4 md:px-6 pt-6 md:pt-10 pb-5">
|
||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-4">
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-4">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Mes Tâches</h1>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Mes Tâches</h1>
|
||||||
<button class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 h-9 px-4 py-2 has-[>svg]:px-3 bg-purple-600 hover:bg-purple-700 text-white w-full md:w-auto gap-2" type="button">
|
@ui.Button(ui.ButtonProps{
|
||||||
@TasksIcon("plus", "lucide lucide-plus w-4 h-4")
|
Label: "Nouvelle tâche",
|
||||||
Nouvelle tâche
|
Variant: ui.ButtonVariantDefault,
|
||||||
</button>
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Icon: "plus",
|
||||||
|
})
|
||||||
</div>
|
</div>
|
||||||
@TasksViewTabs(vm.State)
|
@TasksViewTabs(vm.State)
|
||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-end gap-3">
|
<div class="flex flex-col md:flex-row md:items-center md:justify-end gap-3">
|
||||||
<button class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 h-9 px-4 py-2 has-[>svg]:px-3 w-full md:w-auto gap-2 bg-transparent" type="button">
|
@ui.Button(ui.ButtonProps{
|
||||||
@TasksIcon("settings2", "lucide lucide-settings2 w-4 h-4")
|
Label: tasksFilterSummaryLabel(vm.Filters),
|
||||||
Filtrer
|
Variant: ui.ButtonVariantNeutral,
|
||||||
</button>
|
Tone: ui.ButtonToneSolid,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Icon: "filter",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"data-tasks-filter-trigger": "",
|
||||||
|
"aria-haspopup": "menu",
|
||||||
|
"aria-expanded": "false",
|
||||||
|
},
|
||||||
|
})
|
||||||
</div>
|
</div>
|
||||||
|
@TasksFilterMenu(vm)
|
||||||
</div>
|
</div>
|
||||||
<main class="px-4 md:px-6 pb-6">
|
<main class="px-4 md:px-6 pb-6">
|
||||||
if !vm.HasTasks {
|
if !vm.HasTasks {
|
||||||
|
|
@ -36,17 +50,168 @@ templ TasksPageContent(vm TasksPageViewModel) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ TasksFilterMenu(vm TasksPageViewModel) {
|
||||||
|
<div class="relative">
|
||||||
|
<form method="get" action="/tasks" class="hidden absolute right-0 top-2 z-50 w-56 max-h-[28rem] overflow-y-auto overflow-x-hidden rounded-md border bg-white dark:bg-gray-900 p-1 text-gray-900 dark:text-gray-100 shadow-md" data-tasks-filter-menu role="menu" aria-orientation="vertical">
|
||||||
|
<input type="hidden" name="view" value={ string(vm.State.View) }/>
|
||||||
|
if vm.State.View == taskmodel.TaskViewRoadmap {
|
||||||
|
<input type="hidden" name="roadmap_mode" value={ string(vm.State.RoadmapMode) }/>
|
||||||
|
}
|
||||||
|
<div class="px-2 py-1.5 text-sm font-semibold">Projet</div>
|
||||||
|
<div class="-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<a href={ templ.SafeURL(tasksClearTabloFiltersHref(vm.State)) } class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
if tasksFilterGroupAllSelected(vm.Filters.Tablos) {
|
||||||
|
@TasksIcon("check", "h-4 w-4")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
Tous les projets
|
||||||
|
</a>
|
||||||
|
for _, option := range vm.Filters.Tablos {
|
||||||
|
<label class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||||
|
<input class="sr-only" type="checkbox" name="tablo" value={ option.Value } if option.Selected {
|
||||||
|
checked
|
||||||
|
} onchange="this.form.requestSubmit()"/>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
if option.Selected {
|
||||||
|
@TasksIcon("check", "h-4 w-4")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full shrink-0 bg-blue-500"></div>
|
||||||
|
{ option.Label }
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
<div class="-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div class="px-2 py-1.5 text-sm font-semibold">Statut</div>
|
||||||
|
<div class="-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<a href={ templ.SafeURL(tasksClearStatusFiltersHref(vm.State)) } class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
if tasksFilterGroupAllSelected(vm.Filters.Statuses) {
|
||||||
|
@TasksIcon("check", "h-4 w-4")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
Tous
|
||||||
|
</a>
|
||||||
|
for _, option := range vm.Filters.Statuses {
|
||||||
|
<label class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||||
|
<input class="sr-only" type="checkbox" name="status" value={ option.Value } if option.Selected {
|
||||||
|
checked
|
||||||
|
} onchange="this.form.requestSubmit()"/>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
if option.Selected {
|
||||||
|
@TasksIcon("check", "h-4 w-4")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
{ option.Label }
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
<div class="-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div class="px-2 py-1.5 text-sm font-semibold">Assigné</div>
|
||||||
|
<div class="-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<a href={ templ.SafeURL(tasksClearAssigneeFiltersHref(vm.State)) } class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
if tasksFilterGroupAllSelected(vm.Filters.Assignees) {
|
||||||
|
@TasksIcon("check", "h-4 w-4")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
Tous
|
||||||
|
</a>
|
||||||
|
for _, option := range vm.Filters.Assignees {
|
||||||
|
<label class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||||
|
<input class="sr-only" type="checkbox" name="assignee" value={ option.Value } if option.Selected {
|
||||||
|
checked
|
||||||
|
} onchange="this.form.requestSubmit()"/>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
if option.Selected {
|
||||||
|
@TasksIcon("check", "h-4 w-4")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
{ option.Label }
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
<div class="-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
<div class="p-1">
|
||||||
|
<a href={ templ.SafeURL(stateAction("/tasks", taskmodel.TaskPageState{View: vm.State.View, RoadmapMode: vm.State.RoadmapMode})) } class="block rounded-sm px-2 py-1.5 text-sm font-medium text-[#667085] hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100">Réinitialiser</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (window.__tasksFilterMenuInit) {
|
||||||
|
window.__tasksFilterMenuInit(document);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__tasksFilterMenuInit = function (scope) {
|
||||||
|
(scope || document).querySelectorAll("[data-tasks-filter-trigger]").forEach(function (trigger) {
|
||||||
|
if (trigger.dataset.tasksFilterBound === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trigger.dataset.tasksFilterBound = "true";
|
||||||
|
|
||||||
|
var shell = trigger.closest(".px-4, .md\\:px-6") || trigger.parentElement;
|
||||||
|
var menu = shell ? shell.querySelector("[data-tasks-filter-menu]") : null;
|
||||||
|
if (!menu) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var open = !menu.classList.contains("hidden");
|
||||||
|
document.querySelectorAll("[data-tasks-filter-menu]").forEach(function (other) {
|
||||||
|
other.classList.add("hidden");
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-tasks-filter-trigger]").forEach(function (otherTrigger) {
|
||||||
|
otherTrigger.setAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
|
if (!open) {
|
||||||
|
menu.classList.remove("hidden");
|
||||||
|
trigger.setAttribute("aria-expanded", "true");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
if (event.target.closest("[data-tasks-filter-menu]") || event.target.closest("[data-tasks-filter-trigger]")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.querySelectorAll("[data-tasks-filter-menu]").forEach(function (menu) {
|
||||||
|
menu.classList.add("hidden");
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-tasks-filter-trigger]").forEach(function (trigger) {
|
||||||
|
trigger.setAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("htmx:afterSwap", function (event) {
|
||||||
|
window.__tasksFilterMenuInit(event.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__tasksFilterMenuInit(document);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
templ TasksViewTabs(state taskmodel.TaskPageState) {
|
templ TasksViewTabs(state taskmodel.TaskPageState) {
|
||||||
<div class="flex flex-wrap items-center gap-2 md:gap-6 mb-4 border-b border-[#EAECF0] dark:border-gray-700">
|
<div class="flex flex-wrap items-center gap-2 md:gap-6 mb-4 border-b border-[#EAECF0] dark:border-gray-700">
|
||||||
<a href={ templ.SafeURL(taskViewHref(state, taskmodel.TaskViewKanban)) } class={ taskViewTabClass(state, taskmodel.TaskViewKanban) } if state.View == taskmodel.TaskViewKanban { aria-current="page" }>
|
<a href={ templ.SafeURL(taskViewHref(state, taskmodel.TaskViewKanban)) } class={ taskViewTabClass(state, taskmodel.TaskViewKanban) } if state.View == taskmodel.TaskViewKanban {
|
||||||
|
aria-current="page"
|
||||||
|
}>
|
||||||
@TasksIcon("kanban", "w-4 h-4")
|
@TasksIcon("kanban", "w-4 h-4")
|
||||||
<span>Tableau</span>
|
<span>Tableau</span>
|
||||||
</a>
|
</a>
|
||||||
<a href={ templ.SafeURL(taskViewHref(state, taskmodel.TaskViewList)) } class={ taskViewTabClass(state, taskmodel.TaskViewList) } if state.View == taskmodel.TaskViewList { aria-current="page" }>
|
<a href={ templ.SafeURL(taskViewHref(state, taskmodel.TaskViewList)) } class={ taskViewTabClass(state, taskmodel.TaskViewList) } if state.View == taskmodel.TaskViewList {
|
||||||
|
aria-current="page"
|
||||||
|
}>
|
||||||
@TasksIcon("list", "w-4 h-4")
|
@TasksIcon("list", "w-4 h-4")
|
||||||
<span>Liste</span>
|
<span>Liste</span>
|
||||||
</a>
|
</a>
|
||||||
<a href={ templ.SafeURL(taskViewHref(state, taskmodel.TaskViewRoadmap)) } class={ taskViewTabClass(state, taskmodel.TaskViewRoadmap) } if state.View == taskmodel.TaskViewRoadmap { aria-current="page" }>
|
<a href={ templ.SafeURL(taskViewHref(state, taskmodel.TaskViewRoadmap)) } class={ taskViewTabClass(state, taskmodel.TaskViewRoadmap) } if state.View == taskmodel.TaskViewRoadmap {
|
||||||
|
aria-current="page"
|
||||||
|
}>
|
||||||
@TasksIcon("map", "w-4 h-4")
|
@TasksIcon("map", "w-4 h-4")
|
||||||
<span>Roadmap</span>
|
<span>Roadmap</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -57,13 +222,8 @@ templ TasksViewTabs(state taskmodel.TaskPageState) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
if state.View == taskmodel.TaskViewRoadmap {
|
if state.View == taskmodel.TaskViewRoadmap {
|
||||||
<div class="mb-4 flex flex-wrap items-center gap-2">
|
<div class="mb-4 max-w-[220px]">
|
||||||
<a href={ templ.SafeURL(taskRoadmapModeHref(state, taskmodel.TaskRoadmapModeWeek)) } class={ taskRoadmapModeClass(state, taskmodel.TaskRoadmapModeWeek) }>
|
@ui.Select(taskRoadmapModeSelectProps(state))
|
||||||
Semaine
|
|
||||||
</a>
|
|
||||||
<a href={ templ.SafeURL(taskRoadmapModeHref(state, taskmodel.TaskRoadmapModeMonth)) } class={ taskRoadmapModeClass(state, taskmodel.TaskRoadmapModeMonth) }>
|
|
||||||
Mois
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,13 +234,17 @@ templ TasksKanbanLayout(view TasksKanbanView, state taskmodel.TaskPageState) {
|
||||||
<div class="w-full h-fit bg-[#F9FAFB] dark:bg-gray-800/60 rounded-[12px] p-4" data-status-column={ column.ID }>
|
<div class="w-full h-fit bg-[#F9FAFB] dark:bg-gray-800/60 rounded-[12px] p-4" data-status-column={ column.ID }>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
@TasksIcon("circle", "lucide lucide-circle w-5 h-5 " + statusIconClass(column.ID))
|
@TasksIcon("circle", "lucide lucide-circle w-5 h-5 "+statusIconClass(column.ID))
|
||||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100">{ column.Label }</h2>
|
<h2 class="font-semibold text-gray-800 dark:text-gray-100">{ column.Label }</h2>
|
||||||
<span class="bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 text-xs font-medium px-2 py-0.5 rounded-full">{ len(column.Tasks) }</span>
|
<span class="bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 text-xs font-medium px-2 py-0.5 rounded-full">{ len(column.Tasks) }</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 rounded p-2 min-h-[44px] min-w-[44px] flex items-center justify-center transition-colors">
|
@ui.IconButton(ui.IconButtonProps{
|
||||||
@TasksIcon("plus", "lucide lucide-plus w-[18px] h-[18px]")
|
Label: "Nouvelle tâche dans " + column.Label,
|
||||||
</button>
|
Icon: "plus",
|
||||||
|
Variant: ui.IconButtonVariantNeutral,
|
||||||
|
Tone: ui.IconButtonToneGhost,
|
||||||
|
Type: "button",
|
||||||
|
})
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 pr-1 min-h-[80px]">
|
<div class="space-y-3 pr-1 min-h-[80px]">
|
||||||
for _, task := range column.Tasks {
|
for _, task := range column.Tasks {
|
||||||
|
|
@ -98,13 +262,17 @@ templ TasksListLayout(view TasksListView, state taskmodel.TaskPageState) {
|
||||||
<section class="rounded-[12px] bg-[#F9FAFB] dark:bg-gray-800/60 p-4" data-status-group={ group.ID }>
|
<section class="rounded-[12px] bg-[#F9FAFB] dark:bg-gray-800/60 p-4" data-status-group={ group.ID }>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@TasksIcon("circle", "lucide lucide-circle w-5 h-5 " + statusIconClass(group.ID))
|
@TasksIcon("circle", "lucide lucide-circle w-5 h-5 "+statusIconClass(group.ID))
|
||||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100">{ group.Label }</h2>
|
<h2 class="font-semibold text-gray-800 dark:text-gray-100">{ group.Label }</h2>
|
||||||
<span class="bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 text-xs font-medium px-2 py-0.5 rounded-full">{ len(group.Tasks) }</span>
|
<span class="bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 text-xs font-medium px-2 py-0.5 rounded-full">{ len(group.Tasks) }</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 rounded p-2 min-h-[44px] min-w-[44px] flex items-center justify-center transition-colors">
|
@ui.IconButton(ui.IconButtonProps{
|
||||||
@TasksIcon("plus", "lucide lucide-plus w-[18px] h-[18px]")
|
Label: "Nouvelle tâche dans " + group.Label,
|
||||||
</button>
|
Icon: "plus",
|
||||||
|
Variant: ui.IconButtonVariantNeutral,
|
||||||
|
Tone: ui.IconButtonToneGhost,
|
||||||
|
Type: "button",
|
||||||
|
})
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
for _, task := range group.Tasks {
|
for _, task := range group.Tasks {
|
||||||
|
|
@ -125,9 +293,13 @@ templ TasksRoadmapLayout(view TasksRoadmapView, state taskmodel.TaskPageState) {
|
||||||
<h2 class="font-semibold text-gray-800 dark:text-gray-100">{ lane.Label }</h2>
|
<h2 class="font-semibold text-gray-800 dark:text-gray-100">{ lane.Label }</h2>
|
||||||
<p class="text-xs text-[#667085] dark:text-gray-400">Étape comme lane horizontale, avec bucketisation par date d'échéance.</p>
|
<p class="text-xs text-[#667085] dark:text-gray-400">Étape comme lane horizontale, avec bucketisation par date d'échéance.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 rounded p-2 min-h-[44px] min-w-[44px] flex items-center justify-center transition-colors">
|
@ui.IconButton(ui.IconButtonProps{
|
||||||
@TasksIcon("plus", "lucide lucide-plus w-[18px] h-[18px]")
|
Label: "Nouvelle tâche dans " + lane.Label,
|
||||||
</button>
|
Icon: "plus",
|
||||||
|
Variant: ui.IconButtonVariantNeutral,
|
||||||
|
Tone: ui.IconButtonToneGhost,
|
||||||
|
Type: "button",
|
||||||
|
})
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||||
for _, bucket := range lane.Buckets {
|
for _, bucket := range lane.Buckets {
|
||||||
|
|
@ -153,12 +325,30 @@ templ TaskCard(task TaskCardView, state taskmodel.TaskPageState, compact bool) {
|
||||||
<article draggable="true" class={ taskCardClass(compact) } data-task-id={ task.ID }>
|
<article draggable="true" class={ taskCardClass(compact) } data-task-id={ task.ID }>
|
||||||
<div class="flex items-start justify-between gap-2 mb-2">
|
<div class="flex items-start justify-between gap-2 mb-2">
|
||||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 text-sm leading-tight line-clamp-2 flex-1">{ task.Title }</h3>
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 text-sm leading-tight line-clamp-2 flex-1">{ task.Title }</h3>
|
||||||
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0 p-2 -m-1 min-h-[44px] min-w-[44px] flex items-center justify-center" hx-get={ taskEditHref(task, state) } hx-target="#app-main-content" hx-swap="beforeend">
|
@ui.IconButton(ui.IconButtonProps{
|
||||||
@TasksIcon("ellipsis-vertical", "lucide lucide-ellipsis-vertical w-4 h-4")
|
Label: "Modifier la tâche " + task.Title,
|
||||||
</button>
|
Icon: "pencil",
|
||||||
<button type="button" aria-label={ taskDeleteAriaLabel(task) } class="text-gray-400 hover:text-red-500 shrink-0 p-2 -m-1 min-h-[44px] min-w-[44px] flex items-center justify-center" hx-delete={ taskDeleteHref(task, state) } hx-target="#app-main-content" hx-swap="outerHTML">
|
Variant: ui.IconButtonVariantNeutral,
|
||||||
@TasksIcon("trash2", "lucide lucide-trash2 w-4 h-4")
|
Tone: ui.IconButtonToneGhost,
|
||||||
</button>
|
Type: "button",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-get": taskEditHref(task, state),
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "beforeend",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ui.IconButton(ui.IconButtonProps{
|
||||||
|
Label: taskDeleteAriaLabel(task),
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: ui.IconButtonVariantDanger,
|
||||||
|
Tone: ui.IconButtonToneGhost,
|
||||||
|
Type: "button",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-delete": taskDeleteHref(task, state),
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
},
|
||||||
|
})
|
||||||
</div>
|
</div>
|
||||||
if task.DueDate != "" {
|
if task.DueDate != "" {
|
||||||
<div class={ "flex items-center text-xs mb-3 " + dueDateToneClass(task.DueDateValue) }>
|
<div class={ "flex items-center text-xs mb-3 " + dueDateToneClass(task.DueDateValue) }>
|
||||||
|
|
@ -320,6 +510,10 @@ templ TasksIcon(kind string, className string) {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={ className }>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={ className }>
|
||||||
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"></path>
|
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
case "check":
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={ className }>
|
||||||
|
<path d="M20 6 9 17l-5-5"></path>
|
||||||
|
</svg>
|
||||||
default:
|
default:
|
||||||
@TasksIcon("circle", className)
|
@TasksIcon("circle", className)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,12 +3,14 @@ package views
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
tablomodel "xtablo-backend/internal/tablos"
|
tablomodel "xtablo-backend/internal/tablos"
|
||||||
taskmodel "xtablo-backend/internal/tasks"
|
taskmodel "xtablo-backend/internal/tasks"
|
||||||
|
"xtablo-backend/internal/web/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TasksPageViewModel struct {
|
type TasksPageViewModel struct {
|
||||||
|
|
@ -391,6 +393,75 @@ func taskRoadmapModeClass(state taskmodel.TaskPageState, mode taskmodel.TaskRoad
|
||||||
return base + "bg-gray-100 text-gray-600 border border-transparent hover:text-gray-900 dark:bg-gray-800 dark:text-gray-300"
|
return base + "bg-gray-100 text-gray-600 border border-transparent hover:text-gray-900 dark:bg-gray-800 dark:text-gray-300"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func taskRoadmapModeSelectProps(state taskmodel.TaskPageState) ui.SelectProps {
|
||||||
|
return ui.SelectProps{
|
||||||
|
ID: "tasks-roadmap-mode",
|
||||||
|
Name: "roadmap_mode_nav",
|
||||||
|
Value: taskRoadmapModeHref(state, state.RoadmapMode),
|
||||||
|
Options: []ui.SelectOption{
|
||||||
|
{Value: taskRoadmapModeHref(state, taskmodel.TaskRoadmapModeWeek), Label: "Semaine"},
|
||||||
|
{Value: taskRoadmapModeHref(state, taskmodel.TaskRoadmapModeMonth), Label: "Mois"},
|
||||||
|
},
|
||||||
|
Attrs: map[string]any{
|
||||||
|
"onchange": "if (this.value) window.location.href=this.value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tasksFilterSummaryLabel(filters TasksFiltersView) string {
|
||||||
|
count := 0
|
||||||
|
for _, option := range filters.Tablos {
|
||||||
|
if option.Selected {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, option := range filters.Statuses {
|
||||||
|
if option.Selected {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, option := range filters.Assignees {
|
||||||
|
if option.Selected {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return "Filtrer"
|
||||||
|
}
|
||||||
|
return "Filtrer (" + strconv.Itoa(count) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
func tasksFilterGroupAllSelected(options []TasksOptionView) bool {
|
||||||
|
for _, option := range options {
|
||||||
|
if option.Selected {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func tasksFilterGroupHasChoices(options []TasksOptionView) bool {
|
||||||
|
return len(options) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func tasksClearTabloFiltersHref(state taskmodel.TaskPageState) string {
|
||||||
|
nextState := state
|
||||||
|
nextState.TabloIDs = nil
|
||||||
|
return stateAction("/tasks", nextState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tasksClearStatusFiltersHref(state taskmodel.TaskPageState) string {
|
||||||
|
nextState := state
|
||||||
|
nextState.Statuses = nil
|
||||||
|
return stateAction("/tasks", nextState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tasksClearAssigneeFiltersHref(state taskmodel.TaskPageState) string {
|
||||||
|
nextState := state
|
||||||
|
nextState.AssigneeIDs = nil
|
||||||
|
return stateAction("/tasks", nextState)
|
||||||
|
}
|
||||||
|
|
||||||
func taskCardClass(compact bool) string {
|
func taskCardClass(compact bool) string {
|
||||||
if compact {
|
if compact {
|
||||||
return "bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm border border-gray-100 dark:border-gray-700"
|
return "bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm border border-gray-100 dark:border-gray-700"
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ input {
|
||||||
|
|
||||||
.ui-button-md {
|
.ui-button-md {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
padding: 0.75rem 1.1rem;
|
padding: 0.7rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-button-lg {
|
.ui-button-lg {
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
--tracking-wide: 0.025em;
|
--tracking-wide: 0.025em;
|
||||||
--tracking-wider: 0.05em;
|
--tracking-wider: 0.05em;
|
||||||
--leading-tight: 1.25;
|
--leading-tight: 1.25;
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
--radius-md: 0.375rem;
|
--radius-md: 0.375rem;
|
||||||
--radius-lg: 0.5rem;
|
--radius-lg: 0.5rem;
|
||||||
--radius-xl: 0.75rem;
|
--radius-xl: 0.75rem;
|
||||||
|
|
@ -73,6 +74,17 @@
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
.absolute {
|
.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
@ -88,18 +100,33 @@
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1/2 * 100%);
|
top: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
|
.top-2 {
|
||||||
|
top: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.right-0 {
|
||||||
|
right: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.left-2 {
|
||||||
|
left: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.left-3 {
|
.left-3 {
|
||||||
left: calc(var(--spacing) * 3);
|
left: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
.isolate {
|
.isolate {
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
.-m-1 {
|
.z-50 {
|
||||||
margin: calc(var(--spacing) * -1);
|
z-index: 50;
|
||||||
|
}
|
||||||
|
.-mx-1 {
|
||||||
|
margin-inline: calc(var(--spacing) * -1);
|
||||||
}
|
}
|
||||||
.-mx-4 {
|
.-mx-4 {
|
||||||
margin-inline: calc(var(--spacing) * -4);
|
margin-inline: calc(var(--spacing) * -4);
|
||||||
}
|
}
|
||||||
|
.my-1 {
|
||||||
|
margin-block: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.mt-1 {
|
.mt-1 {
|
||||||
margin-top: calc(var(--spacing) * 1);
|
margin-top: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +163,9 @@
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
@ -211,15 +241,15 @@
|
||||||
.h-8 {
|
.h-8 {
|
||||||
height: calc(var(--spacing) * 8);
|
height: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
.h-9 {
|
|
||||||
height: calc(var(--spacing) * 9);
|
|
||||||
}
|
|
||||||
.h-\[18px\] {
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
.h-fit {
|
.h-fit {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
.h-px {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.max-h-\[28rem\] {
|
||||||
|
max-height: 28rem;
|
||||||
|
}
|
||||||
.min-h-\[44px\] {
|
.min-h-\[44px\] {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
@ -232,6 +262,9 @@
|
||||||
.min-h-screen {
|
.min-h-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
.w-2 {
|
||||||
|
width: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.w-3 {
|
.w-3 {
|
||||||
width: calc(var(--spacing) * 3);
|
width: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
|
|
@ -253,14 +286,14 @@
|
||||||
.w-12 {
|
.w-12 {
|
||||||
width: calc(var(--spacing) * 12);
|
width: calc(var(--spacing) * 12);
|
||||||
}
|
}
|
||||||
.w-\[18px\] {
|
.w-56 {
|
||||||
width: 18px;
|
width: calc(var(--spacing) * 56);
|
||||||
}
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.min-w-\[44px\] {
|
.max-w-\[220px\] {
|
||||||
min-width: 44px;
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
.min-w-\[80px\] {
|
.min-w-\[80px\] {
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
|
|
@ -372,8 +405,11 @@
|
||||||
.overflow-x-auto {
|
.overflow-x-auto {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
.rounded {
|
.overflow-x-hidden {
|
||||||
border-radius: 0.25rem;
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.overflow-y-auto {
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.rounded-\[5px\] {
|
.rounded-\[5px\] {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
@ -393,6 +429,9 @@
|
||||||
.rounded-md {
|
.rounded-md {
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
.rounded-sm {
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
.rounded-xl {
|
.rounded-xl {
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
}
|
}
|
||||||
|
|
@ -510,9 +549,6 @@
|
||||||
.bg-teal-500 {
|
.bg-teal-500 {
|
||||||
background-color: var(--color-teal-500);
|
background-color: var(--color-teal-500);
|
||||||
}
|
}
|
||||||
.bg-transparent {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.bg-white {
|
.bg-white {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|
@ -527,8 +563,8 @@
|
||||||
.bg-yellow-500 {
|
.bg-yellow-500 {
|
||||||
background-color: var(--color-yellow-500);
|
background-color: var(--color-yellow-500);
|
||||||
}
|
}
|
||||||
.p-2 {
|
.p-1 {
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
.p-3 {
|
.p-3 {
|
||||||
padding: calc(var(--spacing) * 3);
|
padding: calc(var(--spacing) * 3);
|
||||||
|
|
@ -560,9 +596,6 @@
|
||||||
.py-1\.5 {
|
.py-1\.5 {
|
||||||
padding-block: calc(var(--spacing) * 1.5);
|
padding-block: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
.py-2 {
|
|
||||||
padding-block: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.py-2\.5 {
|
.py-2\.5 {
|
||||||
padding-block: calc(var(--spacing) * 2.5);
|
padding-block: calc(var(--spacing) * 2.5);
|
||||||
}
|
}
|
||||||
|
|
@ -587,6 +620,9 @@
|
||||||
.pr-1 {
|
.pr-1 {
|
||||||
padding-right: calc(var(--spacing) * 1);
|
padding-right: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.pr-2 {
|
||||||
|
padding-right: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.pr-4 {
|
.pr-4 {
|
||||||
padding-right: calc(var(--spacing) * 4);
|
padding-right: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
|
@ -599,6 +635,9 @@
|
||||||
.pb-6 {
|
.pb-6 {
|
||||||
padding-bottom: calc(var(--spacing) * 6);
|
padding-bottom: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.pl-8 {
|
||||||
|
padding-left: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.pl-10 {
|
.pl-10 {
|
||||||
padding-left: calc(var(--spacing) * 10);
|
padding-left: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
|
@ -725,12 +764,12 @@
|
||||||
.opacity-40 {
|
.opacity-40 {
|
||||||
opacity: 40%;
|
opacity: 40%;
|
||||||
}
|
}
|
||||||
.shadow-sm {
|
.shadow-md {
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
.shadow-xs {
|
.shadow-sm {
|
||||||
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
.filter {
|
.filter {
|
||||||
|
|
@ -751,6 +790,14 @@
|
||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
}
|
}
|
||||||
|
.outline-none {
|
||||||
|
--tw-outline-style: none;
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
|
.select-none {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
.hover\:bg-gray-50 {
|
.hover\:bg-gray-50 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
|
|
@ -758,24 +805,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:bg-gray-200 {
|
.hover\:bg-gray-100 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
background-color: var(--color-gray-200);
|
background-color: var(--color-gray-100);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:bg-purple-700 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
background-color: var(--color-purple-700);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:text-gray-600 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
color: var(--color-gray-600);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -793,13 +826,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:text-red-500 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
color: var(--color-red-500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:shadow-md {
|
.hover\:shadow-md {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
|
|
@ -825,39 +851,6 @@
|
||||||
outline-style: none;
|
outline-style: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.focus-visible\:ring-2 {
|
|
||||||
&:focus-visible {
|
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus-visible\:ring-offset-2 {
|
|
||||||
&:focus-visible {
|
|
||||||
--tw-ring-offset-width: 2px;
|
|
||||||
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus-visible\:outline-none {
|
|
||||||
&:focus-visible {
|
|
||||||
--tw-outline-style: none;
|
|
||||||
outline-style: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled\:pointer-events-none {
|
|
||||||
&:disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled\:opacity-50 {
|
|
||||||
&:disabled {
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.has-\[\>svg\]\:px-3 {
|
|
||||||
&:has(>svg) {
|
|
||||||
padding-inline: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sm\:mx-0 {
|
.sm\:mx-0 {
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
margin-inline: calc(var(--spacing) * 0);
|
margin-inline: calc(var(--spacing) * 0);
|
||||||
|
|
@ -878,11 +871,6 @@
|
||||||
width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.md\:w-auto {
|
|
||||||
@media (width >= 48rem) {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.md\:grid-cols-2 {
|
.md\:grid-cols-2 {
|
||||||
@media (width >= 48rem) {
|
@media (width >= 48rem) {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|
@ -1093,15 +1081,6 @@
|
||||||
color: var(--color-purple-400);
|
color: var(--color-purple-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:hover\:bg-gray-700 {
|
|
||||||
&:is(.dark *) {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
background-color: var(--color-gray-700);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dark\:hover\:bg-gray-800 {
|
.dark\:hover\:bg-gray-800 {
|
||||||
&:is(.dark *) {
|
&:is(.dark *) {
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
@ -1129,22 +1108,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.\[\&_svg\]\:pointer-events-none {
|
|
||||||
& svg {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.\[\&_svg\]\:size-4 {
|
|
||||||
& svg {
|
|
||||||
width: calc(var(--spacing) * 4);
|
|
||||||
height: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.\[\&_svg\]\:shrink-0 {
|
|
||||||
& svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.\[\&\>svg\]\:h-4 {
|
.\[\&\>svg\]\:h-4 {
|
||||||
&>svg {
|
&>svg {
|
||||||
height: calc(var(--spacing) * 4);
|
height: calc(var(--spacing) * 4);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue