Backup
This commit is contained in:
parent
15186e0005
commit
eced006d55
378
examples/kanban_app.php
Normal file
378
examples/kanban_app.php
Normal file
@ -0,0 +1,378 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use PHPNative\Framework\Application;
|
||||
use PHPNative\Ui\Widget\Button;
|
||||
use PHPNative\Ui\Widget\Container;
|
||||
use PHPNative\Ui\Widget\Label;
|
||||
use PHPNative\Ui\Widget\TextInput;
|
||||
use PHPNative\Ui\Window;
|
||||
|
||||
final class KanbanTaskCard extends Container
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $boardId,
|
||||
private readonly array $task,
|
||||
private KanbanBoardView $boardView,
|
||||
) {
|
||||
parent::__construct('bg-white border border-gray-200 rounded shadow px-3 py-2 cursor-pointer');
|
||||
|
||||
$title = new Label($task['title'], 'text-sm text-black');
|
||||
$this->addComponent($title);
|
||||
if (!empty($task['note'] ?? '')) {
|
||||
$this->addComponent(new Label($task['note'], 'text-xs text-gray-500'));
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
|
||||
{
|
||||
if ($button !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->boardView->beginDrag($this->boardId, $this->task);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final class KanbanBoardView extends Container
|
||||
{
|
||||
private array $boards;
|
||||
private Container $columnsContainer;
|
||||
private array $boardViews = [];
|
||||
private ?array $dragState = null;
|
||||
private ?string $hoverBoardId = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $storagePath,
|
||||
array $initialBoards,
|
||||
private readonly Label $statusLabel,
|
||||
) {
|
||||
parent::__construct('flex flex-col flex-1 overflow-hidden');
|
||||
$this->boards = $initialBoards;
|
||||
|
||||
$this->columnsContainer = new Container('flex flex-row gap-4 overflow-auto flex-1 pb-3');
|
||||
$this->addComponent($this->columnsContainer);
|
||||
|
||||
$this->renderBoards();
|
||||
}
|
||||
|
||||
public function addBoard(string $title): void
|
||||
{
|
||||
$title = trim($title);
|
||||
if ($title === '') {
|
||||
$this->statusLabel->setText('Board-Namen eingeben, um es hinzuzufügen.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->boards[] = [
|
||||
'id' => uniqid('board_', true),
|
||||
'title' => $title,
|
||||
'tasks' => [],
|
||||
];
|
||||
|
||||
$this->saveBoards();
|
||||
$this->renderBoards();
|
||||
$this->statusLabel->setText('Board hinzugefügt: ' . $title);
|
||||
}
|
||||
|
||||
public function addTask(string $boardId, string $title): void
|
||||
{
|
||||
$title = trim($title);
|
||||
if ($title === '') {
|
||||
$this->statusLabel->setText('Bitte einen Aufgabentitel eingeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->boards as &$board) {
|
||||
if ($board['id'] === $boardId) {
|
||||
$board['tasks'][] = [
|
||||
'id' => uniqid('task_', true),
|
||||
'title' => $title,
|
||||
'note' => '',
|
||||
];
|
||||
$this->saveBoards();
|
||||
$this->renderBoards();
|
||||
$this->statusLabel->setText(sprintf('Aufgabe "%s" zu "%s" hinzugefügt.', $title, $board['title']));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->statusLabel->setText('Board nicht gefunden.');
|
||||
}
|
||||
|
||||
public function beginDrag(string $boardId, array $task): void
|
||||
{
|
||||
$this->dragState = [
|
||||
'fromBoardId' => $boardId,
|
||||
'taskId' => $task['id'],
|
||||
'taskData' => $task,
|
||||
];
|
||||
$this->statusLabel->setText('Verschiebe: ' . $task['title']);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function handleMouseMove(float $mouseX, float $mouseY): void
|
||||
{
|
||||
parent::handleMouseMove($mouseX, $mouseY);
|
||||
|
||||
if ($this->dragState === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $this->findBoardAt($mouseX, $mouseY);
|
||||
if ($target !== $this->hoverBoardId) {
|
||||
if ($this->hoverBoardId !== null) {
|
||||
$this->setBoardHighlight($this->hoverBoardId, false);
|
||||
}
|
||||
if ($target !== null) {
|
||||
$this->setBoardHighlight($target, true);
|
||||
}
|
||||
$this->hoverBoardId = $target;
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void
|
||||
{
|
||||
parent::handleMouseRelease($mouseX, $mouseY, $button);
|
||||
|
||||
if ($this->dragState === null || $button !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dropTarget = $this->findBoardAt($mouseX, $mouseY);
|
||||
$this->completeDrag($dropTarget);
|
||||
}
|
||||
|
||||
private function renderBoards(): void
|
||||
{
|
||||
$this->columnsContainer->clearChildren();
|
||||
$this->boardViews = [];
|
||||
|
||||
foreach ($this->boards as $board) {
|
||||
$colorHex = substr(md5($board['id']), 0, 6);
|
||||
$defaultStyle = 'flex flex-col w-[260] flex-none border border-gray-400 rounded-lg p-3 gap-3 shadow-sm bg-[#' . $colorHex . ']';
|
||||
$highlightStyle = 'flex flex-col w-[260] flex-none bg-blue-50 border-2 border-blue-500 rounded-lg p-3 gap-3 shadow';
|
||||
|
||||
$debugInfo = new Label(sprintf(
|
||||
"Board %s\nID: %s\nTasks: %d",
|
||||
$board['title'],
|
||||
$board['id'],
|
||||
count($board['tasks']),
|
||||
), 'text-xs text-black whitespace-pre-wrap');
|
||||
|
||||
$column = new Container($defaultStyle);
|
||||
$header = new Container('flex flex-col gap-1 bg-white/80 rounded p-2 shadow-inner');
|
||||
$header->addComponent(new Label($board['title'], 'text-lg font-semibold text-black'));
|
||||
$header->addComponent(new Label(count($board['tasks']) . ' Aufgaben', 'text-xs text-gray-500'));
|
||||
$header->addComponent($debugInfo);
|
||||
$column->addComponent($header);
|
||||
|
||||
$taskList = new Container('flex flex-col gap-2');
|
||||
foreach ($board['tasks'] as $task) {
|
||||
$taskList->addComponent(new KanbanTaskCard($board['id'], $task, $this));
|
||||
}
|
||||
$column->addComponent($taskList);
|
||||
|
||||
$addInput = new TextInput('Neue Aufgabe', 'flex-1 border border-gray-300 rounded px-2 py-1 bg-white text-black text-sm');
|
||||
$addButton = new Button('Hinzufügen', 'px-2 py-1 bg-emerald-500 text-white rounded hover:bg-emerald-600 text-sm');
|
||||
$addButton->setOnClick(function () use ($board, $addInput): void {
|
||||
$this->addTask($board['id'], $addInput->getValue());
|
||||
$addInput->setValue('');
|
||||
});
|
||||
$addRow = new Container('flex flex-row gap-2');
|
||||
$addRow->addComponent($addInput);
|
||||
$addRow->addComponent($addButton);
|
||||
$column->addComponent($addRow);
|
||||
|
||||
$this->columnsContainer->addComponent($column);
|
||||
$this->boardViews[$board['id']] = [
|
||||
'column' => $column,
|
||||
'defaultStyle' => $defaultStyle,
|
||||
'highlightStyle' => $highlightStyle,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function findBoardAt(float $mouseX, float $mouseY): ?string
|
||||
{
|
||||
foreach ($this->boardViews as $boardId => $info) {
|
||||
$viewport = $info['column']->getViewport();
|
||||
if (
|
||||
$mouseX >= $viewport->x &&
|
||||
$mouseX <= ($viewport->x + $viewport->width) &&
|
||||
$mouseY >= $viewport->y &&
|
||||
$mouseY <= ($viewport->y + $viewport->height)
|
||||
) {
|
||||
return $boardId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function setBoardHighlight(string $boardId, bool $active): void
|
||||
{
|
||||
if (!isset($this->boardViews[$boardId])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$column = $this->boardViews[$boardId]['column'];
|
||||
$style = $active ? $this->boardViews[$boardId]['highlightStyle'] : $this->boardViews[$boardId]['defaultStyle'];
|
||||
$column->setStyle($style);
|
||||
}
|
||||
|
||||
private function completeDrag(?string $targetBoardId): void
|
||||
{
|
||||
$state = $this->dragState;
|
||||
$this->dragState = null;
|
||||
|
||||
if ($this->hoverBoardId !== null) {
|
||||
$this->setBoardHighlight($this->hoverBoardId, false);
|
||||
$this->hoverBoardId = null;
|
||||
}
|
||||
|
||||
if (!$state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($targetBoardId === null) {
|
||||
$this->statusLabel->setText('Ziehen abgebrochen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($targetBoardId === $state['fromBoardId']) {
|
||||
$this->statusLabel->setText('Aufgabe blieb im gleichen Board.');
|
||||
return;
|
||||
}
|
||||
|
||||
$movedTask = null;
|
||||
foreach ($this->boards as &$board) {
|
||||
if ($board['id'] === $state['fromBoardId']) {
|
||||
foreach ($board['tasks'] as $index => $task) {
|
||||
if ($task['id'] === $state['taskId']) {
|
||||
$movedTask = $task;
|
||||
array_splice($board['tasks'], $index, 1);
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($movedTask === null) {
|
||||
$this->statusLabel->setText('Aufgabe konnte nicht verschoben werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->boards as &$board) {
|
||||
if ($board['id'] === $targetBoardId) {
|
||||
$board['tasks'][] = $movedTask;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->saveBoards();
|
||||
$this->renderBoards();
|
||||
|
||||
$this->statusLabel->setText(sprintf('"%s" nach "%s" verschoben.', $movedTask['title'], $this->getBoardTitle($targetBoardId)));
|
||||
}
|
||||
|
||||
private function getBoardTitle(string $boardId): string
|
||||
{
|
||||
foreach ($this->boards as $board) {
|
||||
if ($board['id'] === $boardId) {
|
||||
return $board['title'];
|
||||
}
|
||||
}
|
||||
|
||||
return $boardId;
|
||||
}
|
||||
|
||||
private function saveBoards(): void
|
||||
{
|
||||
file_put_contents(
|
||||
$this->storagePath,
|
||||
json_encode($this->boards, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
|
||||
LOCK_EX,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$storagePath = __DIR__ . '/kanban_data.json';
|
||||
|
||||
$boards = [];
|
||||
if (is_file($storagePath)) {
|
||||
$raw = file_get_contents($storagePath);
|
||||
$decoded = json_decode($raw ?: '[]', true);
|
||||
if (is_array($decoded)) {
|
||||
$boards = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($boards)) {
|
||||
$boards = [
|
||||
[
|
||||
'id' => uniqid('board_', true),
|
||||
'title' => 'Backlog',
|
||||
'tasks' => [
|
||||
['id' => uniqid('task_', true), 'title' => 'Idee sammeln', 'note' => ''],
|
||||
['id' => uniqid('task_', true), 'title' => 'Mockups skizzieren', 'note' => ''],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => uniqid('board_', true),
|
||||
'title' => 'In Arbeit',
|
||||
'tasks' => [
|
||||
['id' => uniqid('task_', true), 'title' => 'API anbinden', 'note' => ''],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => uniqid('board_', true),
|
||||
'title' => 'Erledigt',
|
||||
'tasks' => [
|
||||
['id' => uniqid('task_', true), 'title' => 'UI Grundlayout', 'note' => ''],
|
||||
],
|
||||
],
|
||||
];
|
||||
file_put_contents(
|
||||
$storagePath,
|
||||
json_encode($boards, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
|
||||
LOCK_EX,
|
||||
);
|
||||
}
|
||||
|
||||
$app = new Application();
|
||||
$window = new Window('Kanban Beispiel', 1200, 800);
|
||||
|
||||
$root = new Container('flex flex-col h-full w-full bg-gray-100 gap-4 p-4');
|
||||
|
||||
$title = new Label('Kanban Board', 'text-3xl font-bold text-black');
|
||||
$subTitle = new Label('Boards hinzufügen, Aufgaben erstellen und per Drag & Drop verschieben.', 'text-base text-gray-700');
|
||||
$root->addComponent($title);
|
||||
$root->addComponent($subTitle);
|
||||
|
||||
$statusLabel = new Label('Bereit.', 'text-sm text-gray-600');
|
||||
$kanbanView = new KanbanBoardView($storagePath, $boards, $statusLabel);
|
||||
|
||||
$boardInput = new TextInput('Neues Board', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black');
|
||||
$boardAddButton = new Button('Board hinzufügen', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700');
|
||||
$boardAddButton->setOnClick(function () use ($kanbanView, $boardInput, $statusLabel): void {
|
||||
$kanbanView->addBoard($boardInput->getValue());
|
||||
$boardInput->setValue('');
|
||||
});
|
||||
$boardRow = new Container('flex flex-row gap-3 items-center');
|
||||
$boardRow->addComponent($boardInput);
|
||||
$boardRow->addComponent($boardAddButton);
|
||||
$root->addComponent($boardRow);
|
||||
|
||||
$root->addComponent($kanbanView);
|
||||
$root->addComponent($statusLabel);
|
||||
|
||||
$window->setRoot($root);
|
||||
$app->addWindow($window);
|
||||
$app->run();
|
||||
40
examples/kanban_data.json
Normal file
40
examples/kanban_data.json
Normal file
@ -0,0 +1,40 @@
|
||||
[
|
||||
{
|
||||
"id": "board_691661d89de624.46726087",
|
||||
"title": "Backlog",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task_691661d89de735.41071535",
|
||||
"title": "Idee sammeln",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"id": "task_691661d89de7a0.19655479",
|
||||
"title": "Mockups skizzieren",
|
||||
"note": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_691661d89de806.79123800",
|
||||
"title": "In Arbeit",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task_691661d89de858.46479539",
|
||||
"title": "API anbinden",
|
||||
"note": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_691661d89de894.20053237",
|
||||
"title": "Erledigt",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task_691661d89de8e0.86926319",
|
||||
"title": "UI Grundlayout",
|
||||
"note": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
BIN
kanban.png
Normal file
BIN
kanban.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
@ -13,6 +13,11 @@ class Height implements Parser
|
||||
$value = -1;
|
||||
$unit = Unit::Pixel;
|
||||
$found = false;
|
||||
|
||||
if (preg_match('/(min-)?h-\[(\d+)\]/', $style, $output_array)) {
|
||||
$value = (int)$output_array[2];
|
||||
$found = true;
|
||||
}
|
||||
preg_match_all('/h-(\d*)\/(\d*)/', $style, $output_array);
|
||||
if (count($output_array[0]) > 0) {
|
||||
$value1 = (int)$output_array[1][0];
|
||||
|
||||
@ -13,6 +13,11 @@ class Width implements Parser
|
||||
$value = -1;
|
||||
$unit = Unit::Pixel;
|
||||
$found = false;
|
||||
|
||||
if (preg_match('/(min-)?w-\[(\d+)\]/', $style, $output_array)) {
|
||||
$value = (int)$output_array[2];
|
||||
$found = true;
|
||||
}
|
||||
preg_match_all('/w-(\d*)\/(\d*)/', $style, $output_array);
|
||||
if (count($output_array[0]) > 0) {
|
||||
$value1 = (int)$output_array[1][0];
|
||||
|
||||
@ -70,6 +70,16 @@ abstract class Component
|
||||
$this->contentViewport = clone $this->viewport;
|
||||
}
|
||||
|
||||
public function setStyle(string $style): void
|
||||
{
|
||||
if ($this->style === $style) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->style = $style;
|
||||
$this->markDirty(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor - clean up resources
|
||||
*/
|
||||
@ -908,6 +918,10 @@ abstract class Component
|
||||
*/
|
||||
private function createShadowTexture(&$renderer, int $width, int $height, int $blurRadius, int $alpha, int $r = 0, int $g = 0, int $b = 0): mixed
|
||||
{
|
||||
if ($width <= 0 || $height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create alpha map (single channel) - start with transparent
|
||||
$alphaMap = array_fill(0, $width * $height, 0);
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ class Button extends Container
|
||||
|
||||
public function setStyle(string $style): void
|
||||
{
|
||||
$this->style = $style;
|
||||
parent::setStyle($style);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -519,6 +519,8 @@ class Container extends Component
|
||||
|
||||
private function renderScrollbars(&$renderer, array $overflow): void
|
||||
{
|
||||
$scrollbarColor = [120, 120, 120, 230];
|
||||
|
||||
// Vertical scrollbar
|
||||
if ($overflow['y']) {
|
||||
$scrollbarHeight = $this->contentViewport->height;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user