implement auto reconnect

This commit is contained in:
marlon-erler
2026-04-01 08:29:39 +00:00
parent c52d7b7685
commit 9d400a9929
82 changed files with 8340 additions and 7641 deletions
+81 -72
View File
@@ -1,8 +1,8 @@
import * as React from "bloatless-react";
import {
PropertyValueList,
createPropertyValueIndexState,
PropertyValueList,
createPropertyValueIndexState,
} from "../Components/propertyValueList";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
@@ -14,84 +14,93 @@ import { allowDrop } from "../utility";
import { translations } from "../translations";
export function BoardKanbanPage(boardViewModel: BoardViewModel) {
return PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>
) => {
const categoryNameConverter: React.StateItemConverter<string> = (
categoryName: string
) => {
const index: React.State<number> = createPropertyValueIndexState(
sortedCategories,
categoryName
);
return KanbanBoard(categoryName, index, boardViewModel);
};
return PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>,
) => {
const categoryNameConverter: React.StateItemConverter<string> = (
categoryName: string,
) => {
const index: React.State<number> =
createPropertyValueIndexState(
sortedCategories,
categoryName,
);
return KanbanBoard(categoryName, index, boardViewModel);
};
return (
<div
class="kanban-board-wrapper"
children:append={[categories, categoryNameConverter]}
></div>
);
}
);
return (
<div
class="kanban-board-wrapper"
children:append={[categories, categoryNameConverter]}
></div>
);
},
);
}
function KanbanBoard(
categoryName: string,
index: React.State<number>,
boardViewModel: BoardViewModel
categoryName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
) {
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName,
);
function drop() {
boardViewModel.handleDropWithinBoard(categoryName);
}
function drop() {
boardViewModel.handleDropWithinBoard(categoryName);
}
const view = (
<div class="flex-column flex-no" on:dragover={allowDrop} on:drop={drop}>
<div class="flex-row width-input">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
const view = (
<div
class="flex-column flex-no"
on:dragover={allowDrop}
on:drop={drop}
>
<div class="flex-row width-input">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<hr></hr>
<div
class="kanban-column"
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
</div>
);
<div
class="kanban-column"
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
</div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
},
);
}
+135 -115
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
import { BoardKanbanPage } from "./boardKanbanPage";
@@ -14,120 +14,140 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations";
export function BoardPage(boardViewModel: BoardViewModel) {
boardViewModel.loadData();
boardViewModel.loadData();
// main content
const mainContent = React.createProxyState(
[boardViewModel.selectedPage],
() => {
switch (boardViewModel.selectedPage.value) {
case BoardPageType.Kanban: {
return BoardKanbanPage(boardViewModel);
}
case BoardPageType.StatusGrid: {
return BoardStatusGridPage(boardViewModel);
}
default: {
return (
<div
class="task-grid"
children:append={[
boardViewModel.filteredTaskViewModels,
// main content
const mainContent = React.createProxyState(
[boardViewModel.selectedPage],
() => {
switch (boardViewModel.selectedPage.value) {
case BoardPageType.Kanban: {
return BoardKanbanPage(boardViewModel);
}
case BoardPageType.StatusGrid: {
return BoardStatusGridPage(boardViewModel);
}
default: {
return (
<div
class="task-grid"
children:append={[
boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry,
]}
></div>
);
}
}
},
);
// task modal
const taskSettingsModal = React.createProxyState(
[boardViewModel.selectedTaskViewModel],
() => {
if (boardViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(
boardViewModel.selectedTaskViewModel.value,
);
}
},
);
return (
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost board-close-button"
aria-label={
translations.chatPage.task
.closeBoardButtonAudioLabel
}
on:click={boardViewModel.close}
>
<span class="icon">arrow_back</span>
</button>
<button
class="ghost board-toggle-button inset-outline"
aria-label={
translations.chatPage.task
.toggleBoardButtonAudioLabel
}
on:click={
boardViewModel.taskPageViewModel.toggleBoardList
}
toggle:selected={
boardViewModel.taskPageViewModel.isShowingBoadList
}
>
<span class="icon">dock_to_right</span>
</button>
<button
class="ghost"
aria-label={
translations.chatPage.task.boardSettingsHeadline
}
on:click={boardViewModel.showSettings}
>
<span class="icon">settings</span>
</button>
</span>
<span class="scroll-h ribbon">
{BoardViewToggleButton(
translations.chatPage.task.listViewButtonAudioLabel,
"view_list",
BoardPageType.List,
boardViewModel,
)}
{BoardViewToggleButton(
translations.chatPage.task.kanbanViewButtonAudioLabel,
"view_kanban",
BoardPageType.Kanban,
boardViewModel,
)}
{BoardViewToggleButton(
translations.chatPage.task.statusViewButtonAudioLabel,
"grid_view",
BoardPageType.StatusGrid,
boardViewModel,
)}
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task
.filterTasksButtonAudioLabel
}
on:click={boardViewModel.showFilterModal}
>
<span class="icon">filter_alt</span>
</button>
<button
class="ghost"
aria-label={
translations.chatPage.task
.createTaskButtonAudioLabel
}
on:click={boardViewModel.createTask}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)}
{SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline,
TaskViewModelToEntry,
]}
></div>
);
}
}
}
);
// task modal
const taskSettingsModal = React.createProxyState(
[boardViewModel.selectedTaskViewModel],
() => {
if (boardViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(boardViewModel.selectedTaskViewModel.value);
}
}
);
return (
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost board-close-button"
aria-label={translations.chatPage.task.closeBoardButtonAudioLabel}
on:click={boardViewModel.close}
>
<span class="icon">arrow_back</span>
</button>
<button
class="ghost board-toggle-button inset-outline"
aria-label={translations.chatPage.task.toggleBoardButtonAudioLabel}
on:click={boardViewModel.taskPageViewModel.toggleBoardList}
toggle:selected={boardViewModel.taskPageViewModel.isShowingBoadList}
>
<span class="icon">dock_to_right</span>
</button>
<button
class="ghost"
aria-label={translations.chatPage.task.boardSettingsHeadline}
on:click={boardViewModel.showSettings}
>
<span class="icon">settings</span>
</button>
</span>
<span class="scroll-h ribbon">
{BoardViewToggleButton(
translations.chatPage.task.listViewButtonAudioLabel,
"view_list",
BoardPageType.List,
boardViewModel
)}
{BoardViewToggleButton(
translations.chatPage.task.kanbanViewButtonAudioLabel,
"view_kanban",
BoardPageType.Kanban,
boardViewModel
)}
{BoardViewToggleButton(
translations.chatPage.task.statusViewButtonAudioLabel,
"grid_view",
BoardPageType.StatusGrid,
boardViewModel
)}
</span>
<span>
<button
class="ghost"
aria-label={translations.chatPage.task.filterTasksButtonAudioLabel}
on:click={boardViewModel.showFilterModal}
>
<span class="icon">filter_alt</span>
</button>
<button
class="ghost"
aria-label={translations.chatPage.task.createTaskButtonAudioLabel}
on:click={boardViewModel.createTask}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)}
{SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline,
TaskViewModelToEntry,
boardViewModel.isPresentingFilterModal
)}
<div children:set={taskSettingsModal}></div>
</div>
);
boardViewModel.isPresentingFilterModal,
)}
<div children:set={taskSettingsModal}></div>
</div>
);
}
+203 -188
View File
@@ -1,14 +1,14 @@
import * as React from "bloatless-react";
import {
PropertyValueList,
collectPropertyValuesToState,
createPropertyValueIndexState,
createSortedPropertyValueState,
PropertyValueList,
collectPropertyValuesToState,
createPropertyValueIndexState,
createSortedPropertyValueState,
} from "../Components/propertyValueList";
import {
TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel,
TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel,
} from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
@@ -19,221 +19,236 @@ import { allowDrop } from "../utility";
import { translations } from "../translations";
export function BoardStatusGridPage(boardViewModel: BoardViewModel) {
const statuses: React.ListState<string> = new React.ListState();
const sortedStatuses: React.State<string[]> =
createSortedPropertyValueState(statuses);
const statuses: React.ListState<string> = new React.ListState();
const sortedStatuses: React.State<string[]> =
createSortedPropertyValueState(statuses);
boardViewModel.taskViewModels.subscribe(() => {
collectPropertyValuesToState(
"status",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
statuses
);
});
boardViewModel.taskViewModels.subscribe(() => {
collectPropertyValuesToState(
"status",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
statuses,
);
});
const statusNameCellConverter: React.StateItemConverter<string> = (
statusName: string
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return StatusNameCell(statusName, index, boardViewModel);
};
const statusNameCellConverter: React.StateItemConverter<string> = (
statusName: string,
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return StatusNameCell(statusName, index, boardViewModel);
};
return (
<div class="status-page-content">
<div
class="status-name-row"
children:append={[statuses, statusNameCellConverter]}
></div>
{PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>
) => {
const categoryRowConverter: React.StateItemConverter<string> = (
categoryName: string
) => {
const index: React.State<number> = createPropertyValueIndexState(
sortedCategories,
categoryName
);
return CategoryRow(
categoryName,
index,
statuses,
sortedStatuses,
boardViewModel
);
};
return (
return (
<div class="status-page-content">
<div
class="status-grid-wrapper"
children:append={[categories, categoryRowConverter]}
class="status-name-row"
children:append={[statuses, statusNameCellConverter]}
></div>
);
}
)}
</div>
);
{PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>,
) => {
const categoryRowConverter: React.StateItemConverter<
string
> = (categoryName: string) => {
const index: React.State<number> =
createPropertyValueIndexState(
sortedCategories,
categoryName,
);
return CategoryRow(
categoryName,
index,
statuses,
sortedStatuses,
boardViewModel,
);
};
return (
<div
class="status-grid-wrapper"
children:append={[categories, categoryRowConverter]}
></div>
);
},
)}
</div>
);
}
function StatusNameCell(
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
) {
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
new React.ListState();
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
new React.ListState();
boardViewModel.filteredTaskViewModels.handleAddition(
(taskViewModel: TaskViewModel) => {
const doesMatchStatus: boolean = taskViewModel.task.status == statusName;
if (doesMatchStatus == false) return;
boardViewModel.filteredTaskViewModels.handleAddition(
(taskViewModel: TaskViewModel) => {
const doesMatchStatus: boolean =
taskViewModel.task.status == statusName;
if (doesMatchStatus == false) return;
taskViewModelsWithMatchingStatus.add(taskViewModel);
boardViewModel.filteredTaskViewModels.handleRemoval(taskViewModel, () => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
});
}
);
const viewModel: TaskStatusBulkChangeViewModel =
new TaskStatusBulkChangeViewModel(
taskViewModelsWithMatchingStatus,
statusName
taskViewModelsWithMatchingStatus.add(taskViewModel);
boardViewModel.filteredTaskViewModels.handleRemoval(
taskViewModel,
() => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
},
);
},
);
const view = (
<div class="flex-row">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
</div>
);
const viewModel: TaskStatusBulkChangeViewModel =
new TaskStatusBulkChangeViewModel(
taskViewModelsWithMatchingStatus,
statusName,
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
const view = (
<div class="flex-row">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
</div>
);
return view;
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
function CategoryRow(
categoryName: string,
index: React.State<number>,
allStatuses: React.ListState<string>,
sortedStatuses: React.State<string[]>,
boardViewModel: BoardViewModel
categoryName: string,
index: React.State<number>,
allStatuses: React.ListState<string>,
sortedStatuses: React.State<string[]>,
boardViewModel: BoardViewModel,
) {
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const statusNameConverter: React.StateItemConverter<string> = (
statusName: string
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return CategoryStatusColumn(
categoryName,
statusName,
index,
boardViewModel,
taskViewModels
);
};
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const statusNameConverter: React.StateItemConverter<string> = (
statusName: string,
) => {
const index = createPropertyValueIndexState(
sortedStatuses,
statusName,
);
return CategoryStatusColumn(
categoryName,
statusName,
index,
boardViewModel,
taskViewModels,
);
};
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName,
);
const view = (
<div class="flex-row flex-no large-gap">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
const view = (
<div class="flex-row flex-no large-gap">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
<div
class="flex-row large-gap padding-right"
children:append={[allStatuses, statusNameConverter]}
></div>
</div>
);
<div
class="flex-row large-gap padding-right"
children:append={[allStatuses, statusNameConverter]}
></div>
</div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
);
return view;
},
);
}
function CategoryStatusColumn(
categoryName: string,
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>
categoryName: string,
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>,
) {
const taskViewModels: React.ListState<TaskViewModel> = new React.ListState();
const taskViewModels: React.ListState<TaskViewModel> =
new React.ListState();
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
const doesMatchStatus: boolean = taskViewModel.status.value == statusName;
if (doesMatchStatus == false) return;
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
const doesMatchStatus: boolean =
taskViewModel.status.value == statusName;
if (doesMatchStatus == false) return;
taskViewModels.add(taskViewModel);
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
taskViewModels.remove(taskViewModel);
taskViewModels.add(taskViewModel);
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
taskViewModels.remove(taskViewModel);
});
});
});
function drop() {
boardViewModel.handleDropWithinBoard(categoryName, statusName);
}
function drop() {
boardViewModel.handleDropWithinBoard(categoryName, statusName);
}
const view = (
<div
class="status-column gap"
on:dragover={allowDrop}
on:drop={drop}
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
);
const view = (
<div
class="status-column gap"
on:dragover={allowDrop}
on:drop={drop}
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
+50 -51
View File
@@ -1,74 +1,73 @@
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper {
display: flex;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper {
display: flex;
padding: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> input {
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> input {
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
-moz-appearance: textfield;
padding-right: 0px;
padding-right: 0px;
}
#calendar-page .input-wrapper > input::-webkit-outer-spin-button,
#calendar-page .input-wrapper > input::-webkit-innner-spin-button {
appearance: none;
-webkit-appearance: none;
appearance: none;
-webkit-appearance: none;
margin: 0px;
margin: 0px;
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .year-input {
width: 4.4rem;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .year-input {
width: 4.4rem;
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .month-input {
width: 3.1rem;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .month-input {
width: 3.1rem;
}
@media (max-width: 1024px) {
#chat-page > div > #main > #calendar-page {
flex-direction: column;
}
#chat-page > div > #main > #calendar-page {
flex-direction: column;
}
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
border-bottom: 1px solid var(--border);
}
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
border-bottom: 1px solid var(--border);
}
}
+195 -160
View File
@@ -10,168 +10,203 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations";
export function CalendarPage(calendarPageViewModel: CalendarPageViewModel) {
calendarPageViewModel.loadData();
calendarPageViewModel.loadData();
const mainContent = React.createProxyState(
[calendarPageViewModel.monthGrid],
() => {
const monthGrid = calendarPageViewModel.monthGrid.value;
const mainContent = React.createProxyState(
[calendarPageViewModel.monthGrid],
() => {
const monthGrid = calendarPageViewModel.monthGrid.value;
if (monthGrid == undefined) {
return <div></div>;
} else {
function drop(date: string) {
calendarPageViewModel.handleDrop(
monthGrid!.year.toString(),
monthGrid!.month.toString(),
date
);
}
return MonthGrid(monthGrid, calendarPageViewModel.selectedDate, drop);
}
}
);
const sidePaneContentWrapper = React.createProxyState(
[
calendarPageViewModel.selectedYear,
calendarPageViewModel.selectedMonth,
calendarPageViewModel.selectedDate,
],
() => {
const listState = calendarPageViewModel.getEventsForDate();
if (listState == undefined) {
return <div></div>;
} else {
const sidePaneContent = React.createProxyState([listState], () => {
if (listState.value.size == 0) {
return (
<div class="width-100 height-100 flex-column justify-center align-center">
<span class="secondary slide-up">
{translations.chatPage.calendar.noEvents}
</span>
</div>
);
} else {
return (
<div
class="flex-column gap slide-up padding-bottom"
children:append={[listState, TaskViewModelToEntry]}
></div>
);
}
});
return (
<div
class="width-100 height-100"
children:set={sidePaneContent}
></div>
);
}
}
);
const taskSettingsModal = React.createProxyState(
[calendarPageViewModel.selectedTaskViewModel],
() => {
if (calendarPageViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(
calendarPageViewModel.selectedTaskViewModel.value
);
}
}
);
return (
<div id="calendar-page">
<div class="pane-wrapper grid-pane-wrapper">
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.todayButtonAudioLabel
if (monthGrid == undefined) {
return <div></div>;
} else {
function drop(date: string) {
calendarPageViewModel.handleDrop(
monthGrid!.year.toString(),
monthGrid!.month.toString(),
date,
);
}
on:click={calendarPageViewModel.showToday}
>
<span class="icon">today</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.previousMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showPreviousMonth}
>
<span class="icon">arrow_back</span>
</button>
<span class="input-wrapper">
<input
class="year-input"
type="number"
aria-label={
translations.chatPage.calendar.yearInputAudioLabel
}
placeholder={
translations.chatPage.calendar.yearInputPlaceholder
}
bind:value={calendarPageViewModel.selectedYear}
></input>
<input
class="month-input"
type="number"
aria-label={
translations.chatPage.calendar.monthInputAudioLabel
}
placeholder={
translations.chatPage.calendar.monthInputPlaceholder
}
bind:value={calendarPageViewModel.selectedMonth}
></input>
</span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.nextMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showNextMonth}
>
<span class="icon">arrow_forward</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task.createTaskButtonAudioLabel
}
on:click={calendarPageViewModel.createEvent}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content padding-0" children:set={mainContent}></div>
return MonthGrid(
monthGrid,
calendarPageViewModel.selectedDate,
drop,
);
}
},
);
const sidePaneContentWrapper = React.createProxyState(
[
calendarPageViewModel.selectedYear,
calendarPageViewModel.selectedMonth,
calendarPageViewModel.selectedDate,
],
() => {
const listState = calendarPageViewModel.getEventsForDate();
if (listState == undefined) {
return <div></div>;
} else {
const sidePaneContent = React.createProxyState(
[listState],
() => {
if (listState.value.size == 0) {
return (
<div class="width-100 height-100 flex-column justify-center align-center">
<span class="secondary slide-up">
{
translations.chatPage.calendar
.noEvents
}
</span>
</div>
);
} else {
return (
<div
class="flex-column gap slide-up padding-bottom"
children:append={[
listState,
TaskViewModelToEntry,
]}
></div>
);
}
},
);
return (
<div
class="width-100 height-100"
children:set={sidePaneContent}
></div>
);
}
},
);
const taskSettingsModal = React.createProxyState(
[calendarPageViewModel.selectedTaskViewModel],
() => {
if (
calendarPageViewModel.selectedTaskViewModel.value == undefined
) {
return <div></div>;
} else {
return TaskSettingsModal(
calendarPageViewModel.selectedTaskViewModel.value,
);
}
},
);
return (
<div id="calendar-page">
<div class="pane-wrapper grid-pane-wrapper">
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.todayButtonAudioLabel
}
on:click={calendarPageViewModel.showToday}
>
<span class="icon">today</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.previousMonthButtonAudioLabel
}
on:click={
calendarPageViewModel.showPreviousMonth
}
>
<span class="icon">arrow_back</span>
</button>
<span class="input-wrapper">
<input
class="year-input"
type="number"
aria-label={
translations.chatPage.calendar
.yearInputAudioLabel
}
placeholder={
translations.chatPage.calendar
.yearInputPlaceholder
}
bind:value={
calendarPageViewModel.selectedYear
}
></input>
<input
class="month-input"
type="number"
aria-label={
translations.chatPage.calendar
.monthInputAudioLabel
}
placeholder={
translations.chatPage.calendar
.monthInputPlaceholder
}
bind:value={
calendarPageViewModel.selectedMonth
}
></input>
</span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.nextMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showNextMonth}
>
<span class="icon">arrow_forward</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task
.createTaskButtonAudioLabel
}
on:click={calendarPageViewModel.createEvent}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div
class="content padding-0"
children:set={mainContent}
></div>
</div>
</div>
<div
class="pane-wrapper side background"
set:color={calendarPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div
class="content"
children:set={sidePaneContentWrapper}
></div>
</div>
</div>
<div children:set={taskSettingsModal}></div>
</div>
</div>
<div
class="pane-wrapper side background"
set:color={calendarPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="content" children:set={sidePaneContentWrapper}></div>
</div>
</div>
<div children:set={taskSettingsModal}></div>
</div>
);
);
}
+78 -78
View File
@@ -1,168 +1,168 @@
/* layout */
#chat-page #main #message-page .content {
padding: 0px;
padding: 0px;
overflow: hidden;
overflow: hidden;
display: grid;
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
display: grid;
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
}
#message-page #message-container {
overflow: auto;
overflow: auto;
}
/* composer */
#message-page #composer {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
}
#message-page #composer input {
margin: 0px;
margin: 0px;
width: 100%;
max-width: unset;
flex: 1;
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
}
@media (max-width: 700px) {
#message-page #composer div {
width: 100%;
max-width: unset;
display: flex;
}
flex: 1;
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
}
@media (max-width: 700px) {
#message-page #composer div {
width: 100%;
max-width: unset;
display: flex;
}
}
@media (min-width: 700px) {
#message-page #composer .content-width-constraint,
#message-page #composer .input-width-constraint {
display: flex;
justify-content: flex-end;
#message-page #composer .content-width-constraint,
#message-page #composer .input-width-constraint {
display: flex;
justify-content: flex-end;
width: 100%;
max-width: var(--max-content-width);
}
width: 100%;
max-width: var(--max-content-width);
}
#message-page #composer .content-width-constraint {
max-width: var(--max-content-width);
}
#message-page #composer .content-width-constraint {
max-width: var(--max-content-width);
}
#message-page #composer .input-width-constraint {
max-width: var(--max-input-width);
}
#message-page #composer .input-width-constraint {
max-width: var(--max-input-width);
}
}
/* message */
#message-page #message-container {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--content-padding);
gap: var(--gap-between-tiles);
padding: var(--content-padding);
gap: var(--gap-between-tiles);
height: auto;
max-height: unset;
height: auto;
max-height: unset;
scroll-behavior: smooth;
scroll-behavior: smooth;
}
.message-bubble {
display: flex;
width: 100%;
max-width: var(--max-content-width);
display: flex;
width: 100%;
max-width: var(--max-content-width);
}
.message-bubble .tile.main {
display: flex;
flex-direction: row;
align-items: start;
display: flex;
flex-direction: row;
align-items: start;
padding: 0px;
padding: 0px;
width: 100%;
max-width: var(--max-input-width);
overflow: hidden;
width: 100%;
max-width: var(--max-input-width);
overflow: hidden;
}
.message-bubble .tile.main .text-container {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
overflow: hidden;
overflow: hidden;
width: 100%;
flex: 1;
width: 100%;
flex: 1;
}
.message-bubble .tile.main .button-container {
display: flex;
display: flex;
width: var(--control-height);
max-width: var(--control-height);
width: var(--control-height);
max-width: var(--control-height);
}
.message-bubble .tile.main button {
justify-content: center;
justify-content: center;
padding: 0px;
width: 100%;
padding: 0px;
width: 100%;
--surface: transparent;
--surface: transparent;
}
.message-bubble .tile.main .text-container {
padding: var(--control-padding);
padding: var(--control-padding);
}
.message-bubble .tile.main .timestamp,
.message-bubble .tile.main .timestamp .icon,
.message-bubble .tile.main .sender-name {
font-size: 0.8rem;
opacity: 0.6;
font-size: 0.8rem;
opacity: 0.6;
}
.message-bubble .tile.main .timestamp {
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
}
.message-bubble .tile.main .timestamp .icon {
display: none;
display: none;
}
.message-bubble .tile.main .sender-name {
font-weight: bold;
font-weight: bold;
}
.message-bubble .tile.main .body {
word-break: break-word;
word-break: break-word;
}
.message-bubble[sentbyuser] {
justify-content: flex-end;
justify-content: flex-end;
}
.message-bubble[sentbyuser] {
align-items: flex-end;
align-items: flex-end;
}
.message-bubble[sentbyuser] .tile.main,
.message-bubble[sentbyuser] .tile.main .timestamp {
flex-direction: row-reverse;
flex-direction: row-reverse;
}
.message-bubble[sentbyuser] .tile.main .timestamp .icon {
display: flex;
display: flex;
}
.message-bubble[sentbyuser] .tile.main span {
text-align: right;
text-align: right;
}
+70 -58
View File
@@ -7,69 +7,81 @@ import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import { translations } from "../translations";
export function MessagePage(messagePageViewModel: MessagePageViewModel) {
messagePageViewModel.loadData();
messagePageViewModel.loadData();
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.chatMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.chatMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
function scrollDown() {
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
function scrollDown() {
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
messagePageViewModel.chatMessageViewModels.subscribeSilent(
scrollDownIfApplicable
);
setTimeout(() => scrollDown(), 100);
scrollDown();
}
messagePageViewModel.chatMessageViewModels.subscribeSilent(
scrollDownIfApplicable,
);
setTimeout(() => scrollDown(), 100);
return (
<div id="message-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">{translations.chatPage.message.messagesHeadline}</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div class="input-width-constraint">
<input
bind:value={messagePageViewModel.composingMessage}
on:enter={messagePageViewModel.sendMessage}
placeholder={
translations.chatPage.message.composerInputPlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.message.sendMessageButtonAudioLabel
}
on:click={messagePageViewModel.sendMessage}
toggle:disabled={messagePageViewModel.cannotSendMessage}
>
<span class="icon">send</span>
</button>
return (
<div id="message-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.message.messagesHeadline}
</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div class="input-width-constraint">
<input
bind:value={
messagePageViewModel.composingMessage
}
on:enter={
messagePageViewModel.sendMessage
}
placeholder={
translations.chatPage.message
.composerInputPlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.message
.sendMessageButtonAudioLabel
}
on:click={
messagePageViewModel.sendMessage
}
toggle:disabled={
messagePageViewModel.cannotSendMessage
}
>
<span class="icon">send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
);
}
+169 -133
View File
@@ -8,141 +8,177 @@ import SettingsPageViewModel from "../../ViewModel/Pages/settingsPageViewModel";
import { translations } from "../translations";
export function SettingsPage(settingsPageViewModel: SettingsPageViewModel) {
settingsPageViewModel.loadData();
settingsPageViewModel.loadData();
const secondaryChannelConverter: React.StateItemConverter<string> = (
secondaryChannel: string
) => {
return DeletableListItem(secondaryChannel, <span></span>, () => {
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
});
};
const secondaryChannelConverter: React.StateItemConverter<string> = (
secondaryChannel: string,
) => {
return DeletableListItem(secondaryChannel, <span></span>, () => {
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
});
};
return (
<div id="settings-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.settings.settingsHeadline}
</span>
</div>
<div class="content">
<label class="tile flex-no">
<span class="icon">forum</span>
<div>
<span>
{translations.chatPage.settings.primaryChannelLabel}
</span>
<input
bind:value={settingsPageViewModel.primaryChannelInput}
on:enter={settingsPageViewModel.setPrimaryChannel}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setPrimaryChannelButtonAudioLabel
}
on:click={settingsPageViewModel.setPrimaryChannel}
toggle:disabled={settingsPageViewModel.cannotSetPrimaryChannel}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
return (
<div id="settings-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.settings.settingsHeadline}
</span>
</div>
<div class="content">
<label class="tile flex-no">
<span class="icon">forum</span>
<div>
<span>
{
translations.chatPage.settings
.primaryChannelLabel
}
</span>
<input
bind:value={
settingsPageViewModel.primaryChannelInput
}
on:enter={
settingsPageViewModel.setPrimaryChannel
}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setPrimaryChannelButtonAudioLabel
}
on:click={
settingsPageViewModel.setPrimaryChannel
}
toggle:disabled={
settingsPageViewModel.cannotSetPrimaryChannel
}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<div class="flex-row width-input margin-bottom">
<input
aria-label={
translations.chatPage.settings
.newSecondaryChannelAudioLabel
}
placeholder={
translations.chatPage.settings
.newSecondaryChannelPlaceholder
}
bind:value={
settingsPageViewModel.newSecondaryChannelInput
}
on:enter={
settingsPageViewModel.addSecondaryChannel
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.settings
.addSecondaryChannelButtonAudioLabel
}
on:click={
settingsPageViewModel.addSecondaryChannel
}
toggle:disabled={
settingsPageViewModel.cannotAddSecondaryChannel
}
>
<span class="icon">add</span>
</button>
</div>
<div
class="flex-column gap width-input"
children:append={[
settingsPageViewModel.secondaryChannels,
secondaryChannelConverter,
]}
></div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">key</span>
<div>
<span>
{
translations.chatPage.settings
.encryptionKeyLabel
}
</span>
<input
bind:value={
settingsPageViewModel.encryptionKeyInput
}
on:enter={
settingsPageViewModel.setEncryptionKey
}
set:type={
settingsPageViewModel.encryptionKeyInputType
}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setEncryptionKeyButtonAudioLabel
}
on:click={
settingsPageViewModel.setEncryptionKey
}
toggle:disabled={
settingsPageViewModel.cannotSetEncryptionKey
}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="inline">
<input
type="checkbox"
bind:checked={
settingsPageViewModel.shouldShowEncryptionKey
}
></input>
{translations.chatPage.settings.showEncryptionKey}
</label>
<hr></hr>
{ColorPicker(settingsPageViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.settings.deleteChatButton,
"chat_error",
settingsPageViewModel.remove,
)}
</div>
</div>
</div>
</div>
<hr></hr>
<div class="flex-row width-input margin-bottom">
<input
aria-label={
translations.chatPage.settings.newSecondaryChannelAudioLabel
}
placeholder={
translations.chatPage.settings.newSecondaryChannelPlaceholder
}
bind:value={settingsPageViewModel.newSecondaryChannelInput}
on:enter={settingsPageViewModel.addSecondaryChannel}
></input>
<button
class="primary"
aria-label={
translations.chatPage.settings
.addSecondaryChannelButtonAudioLabel
}
on:click={settingsPageViewModel.addSecondaryChannel}
toggle:disabled={
settingsPageViewModel.cannotAddSecondaryChannel
}
>
<span class="icon">add</span>
</button>
</div>
<div
class="flex-column gap width-input"
children:append={[
settingsPageViewModel.secondaryChannels,
secondaryChannelConverter,
]}
></div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">key</span>
<div>
<span>{translations.chatPage.settings.encryptionKeyLabel}</span>
<input
bind:value={settingsPageViewModel.encryptionKeyInput}
on:enter={settingsPageViewModel.setEncryptionKey}
set:type={settingsPageViewModel.encryptionKeyInputType}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setEncryptionKeyButtonAudioLabel
}
on:click={settingsPageViewModel.setEncryptionKey}
toggle:disabled={settingsPageViewModel.cannotSetEncryptionKey}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="inline">
<input
type="checkbox"
bind:checked={settingsPageViewModel.shouldShowEncryptionKey}
></input>
{translations.chatPage.settings.showEncryptionKey}
</label>
<hr></hr>
{ColorPicker(settingsPageViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.settings.deleteChatButton,
"chat_error",
settingsPageViewModel.remove
)}
</div>
</div>
</div>
</div>
</div>
);
);
}
+63 -63
View File
@@ -1,53 +1,53 @@
#chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
width: 100%;
max-width: 100%;
width: 100%;
max-width: 100%;
padding: 0px;
padding: 0px;
}
#chat-page
> div
> #main
> #task-page
> .pane-wrapper
> .pane
> .main-content
> div {
animation: slide-up var(--animation-duration);
> div
> #main
> #task-page
> .pane-wrapper
> .pane
> .main-content
> div {
animation: slide-up var(--animation-duration);
}
#chat-page > div > #main > #task-page .task-grid {
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
padding: var(--content-padding);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper,
#chat-page > div > #main > #task-page .status-page-content {
display: flex;
gap: var(--large-gap);
display: flex;
gap: var(--large-gap);
width: 100%;
height: 100%;
width: 100%;
height: 100%;
overflow: auto;
overflow: auto;
padding: var(--content-padding);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
#chat-page > div > #main > #task-page .property-input-wrapper {
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
}
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
@@ -55,72 +55,72 @@
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column,
#chat-page > div > #main > #task-page .status-name-row > *,
#chat-page > div > #main > #task-page .status-name-row::before {
width: 18rem;
min-width: 18rem;
max-width: 18rem;
width: 18rem;
min-width: 18rem;
max-width: 18rem;
}
#chat-page > div > #main > #task-page .status-name-row,
#chat-page > div > #main > #task-page .status-grid-wrapper {
display: flex;
gap: var(--large-gap);
display: flex;
gap: var(--large-gap);
}
#chat-page > div > #main > #task-page .status-page-content,
#chat-page > div > #main > #task-page .status-grid-wrapper {
flex-direction: column;
flex-direction: column;
}
#chat-page > div > #main > #task-page .status-name-row::before {
content: "";
display: flex;
content: "";
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: none;
display: none;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: flex;
display: flex;
}
#chat-page
> div
> #main
> #task-page[isshowingboard][showingboardlist="false"]
#board-list {
display: none;
> div
> #main
> #task-page[isshowingboard][showingboardlist="false"]
#board-list {
display: none;
}
@media (min-width: 640px) {
#chat-page > div > #main > #task-page .task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
#chat-page > div > #main > #task-page .task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
}
@media (max-width: 1024px) {
#chat-page > div > #main > #task-page #board-list {
display: false;
}
#chat-page > div > #main > #task-page #board-list {
display: false;
}
#chat-page > div > #main > #task-page #board-content {
display: none;
}
#chat-page > div > #main > #task-page #board-content {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: flex;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: none;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: none;
}
}
+86 -76
View File
@@ -8,87 +8,97 @@ import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
import { translations } from "../translations";
export function TaskPage(taskPageViewModel: TaskPageViewModel) {
taskPageViewModel.loadData();
taskPageViewModel.loadData();
const isShowingBoard = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => taskPageViewModel.selectedBoardId.value != undefined
);
const isShowingBoard = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => taskPageViewModel.selectedBoardId.value != undefined,
);
const paneContent = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => {
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
if (selectedBoardId == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.noBoardSelected}
</span>
</div>
);
}
const selectedBoard =
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
if (selectedBoard == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.boardNotFound}
</span>
</div>
);
}
const paneContent = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => {
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
if (selectedBoardId == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.noBoardSelected}
</span>
</div>
);
}
const selectedBoard =
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
if (selectedBoard == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.boardNotFound}
</span>
</div>
);
}
return BoardPage(selectedBoard);
}
);
return BoardPage(selectedBoard);
},
);
return (
<div id="task-page" toggle:isshowingboard={isShowingBoard} set:showingboardlist={taskPageViewModel.isShowingBoadList}>
<div
id="board-list"
class="pane-wrapper side background"
set:color={taskPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="toolbar">
<div class="flex-row width-input">
<input
class="no-outline"
bind:value={taskPageViewModel.newBoardNameInput}
on:enter={taskPageViewModel.createBoard}
placeholder={translations.chatPage.task.newBoardNamePlaceholder}
></input>
<button
class="primary"
aria-label={
translations.chatPage.task.createBoardButtonAudioLabel
}
on:click={taskPageViewModel.createBoard}
toggle:disabled={taskPageViewModel.cannotCreateBoard}
>
<span class="icon">add</span>
</button>
</div>
</div>
<div class="content">
return (
<div
id="task-page"
toggle:isshowingboard={isShowingBoard}
set:showingboardlist={taskPageViewModel.isShowingBoadList}
>
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
children:append={[
taskPageViewModel.boardViewModels,
BoardViewModelToEntry,
]}
id="board-list"
class="pane-wrapper side background"
set:color={taskPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="toolbar">
<div class="flex-row width-input">
<input
class="no-outline"
bind:value={taskPageViewModel.newBoardNameInput}
on:enter={taskPageViewModel.createBoard}
placeholder={
translations.chatPage.task
.newBoardNamePlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.task
.createBoardButtonAudioLabel
}
on:click={taskPageViewModel.createBoard}
toggle:disabled={
taskPageViewModel.cannotCreateBoard
}
>
<span class="icon">add</span>
</button>
</div>
</div>
<div class="content">
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
children:append={[
taskPageViewModel.boardViewModels,
BoardViewModelToEntry,
]}
></div>
</div>
</div>
</div>
<div
id="board-content"
class="pane-wrapper"
children:set={paneContent}
></div>
</div>
</div>
</div>
<div
id="board-content"
class="pane-wrapper"
children:set={paneContent}
></div>
</div>
);
);
}