frontend: iterate on CSS / UI

This commit is contained in:
0x1eef 2024-01-09 00:58:33 -03:00
parent 7b600c2e82
commit 6bd3e3ad65
16 changed files with 316 additions and 189 deletions

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true
class Twenty::Model < ActiveRecord::Base
require "fileutils"
extend FileUtils
require_relative "model/project"
require_relative "model/task"
end

View file

@ -1,5 +1,3 @@
$black: lighten(#000, 20%);
$gray1: #f4f0ec;
$gray2: lighten($gray1, 5%);
$gray3: #cfcfc4;
$blue: #008cff;
$primary-color: #FFF;
$secondary-color: #333;
$accent-color: #0066CC;

View file

@ -0,0 +1,17 @@
body .wrapper {
a {
@import "colors";
color: $accent-color;
&:hover,
&:active,
&:visited {
color: $accent-color;
text-decoration: none;
}
}
ul {
margin: 0;
padding: 0;
}
.w-85 { width: 85%; }
}

View file

@ -0,0 +1,20 @@
.two-columns {
display: flex;
flex-direction: space-between;
.column-1 {
width: 20%;
}
.column-2 {
width: 100%;
}
}
.max-width {
width: 75%;
max-width: 1024px;
}
.align-center {
margin: 0 auto;
}

View file

@ -1,66 +1,108 @@
/**
-----------------
| ul.collection |
-----------------
**/
ul.collection {
h1 {
display: flex;
align-items: center;
margin: 0;
padding: 0;
background: $primary-color;
color: $secondary-color;
height: 25px;
width: 100%;
font-size: medium;
}
}
ul.collection li.item {
display: flex;
flex-wrap: wrap;
padding-bottom: 5px;
a {
display: flex;
flex-direction: column;
height: 50px;
justify-content: space-between;
padding: 10px 10px 5px 0px;
.title {
display: flex;
padding-bottom: 5px;
font-size: smaller;
}
.subtitle {
font-size: small;
color: $secondary-color;
}
}
ul.actions {
list-style-type: none;
width: 5%;
display: flex;
place-content: flex-end;
li {
display: flex;
align-items: center;
}
}
}
ul.collection li.item.removed {
animation: bounceOutDown;
animation-duration: 0.5s;
}
ul.collection li.item.completed {
animation: bounceOutUp;
animation-duration: 0.5s;
}
ul.items {
@import "colors";
margin: 0;
padding: 0;
list-style-type: none;
li.item:first-child {
a {
border-top: 1px solid #FFF;
}
}
ul.items.nav {
border: 1px solid $secondary-color;
border-radius: 5px;
background: $secondary-color;
color: $primary-color;
width: 85%;
padding: 10px;
h1 {
height: unset;
font-size: smaller;
align-items: center;
background: $secondary-color;
color: $primary-color;
margin-top: 10px;
}
h1:first-child {
margin-top: unset;
}
li.item {
padding: 5px;
justify-content: space-between;
background: $primary-color;
color: $secondary-color;
display: flex;
flex-wrap: wrap;
align-items: center;
font-size: smaller;
height: 25px;
a {
display: flex;
flex-direction: column;
height: 60px;
justify-content: space-between;
$blue: #008cff;
color: $blue;
padding: 10px 10px 5px 10px;
border-radius: 10px;
border: 1px solid #FFF;
border-top: 1px solid $gray1;
&:hover {
background: $gray1;
border: 1px solid $gray3;
cursor: pointer;
text-decoration: none;
color: $blue;
}
span {
display: flex;
flex-wrap: wrap;
width: 100%;
}
span.date, span.path {
font-size: small;
color: $black;
}
display: inline;
height: unset;
padding: unset;
}
ul.actions {
list-style-type: none;
width: 5%;
display: flex;
place-content: flex-end;
li {
display: flex;
align-items: center;
}
}
}
li.item.removed {
animation: bounceOutDown;
animation-duration: 0.5s;
}
li.item.completed {
animation: bounceOutUp;
animation-duration: 0.5s;
}
}

View file

@ -0,0 +1,73 @@
.panel h1 {
@import "colors";
display: flex;
align-items: center;
margin: 0;
padding: 0;
background: $primary-color;
color: $secondary-color;
height: 25px;
width: 100%;
font-size: medium;
}
.panel .panel-header.panel-tabs {
@import "colors";
padding: 5px 5px 0 0px;
ul.tabs {
list-style-type: none;
display: flex;
height: 100%;
margin: 0;
padding: 0;
li:first-child {
border-left: none;
}
li {
font-size: small;
height: 100%;
margin-right: 5px;
border: 1px solid $accent-color;
border-bottom: none;
padding: 10px 5px 5px 10px;
border-radius: 5px;
min-width: 120px;
text-align: center;
background: $secondary-color;
color: lighten($secondary-color, 15%);
cursor: pointer;
opacity: 0.5;
}
li.active, li:hover {
font-weight: bold;
color: $secondary-color;
opacity: 1;
}
}
}
.panel .panel-body {
@import "colors";
width: 100%;
.task.content {
padding: 10px;
ul li {
line-height: 1.7em;
}
h3,h4,h5 {
&:first-child {
margin: 0 0 5px 0;
}
margin: 15px 0 5px 0;
}
code {
padding: 0px 5px 0px 5px;
font-family: "Noto Sans Mono Regular";
font-size: smaller;
font-weight: bold;
color: lighten(#FF0000, 25%);
border-radius: 5px;
border: 1px solid $accent-color;
}
}
}

View file

@ -7,7 +7,7 @@
width: 100%;
color: #FFF;
background: $gray1;
color: $black;
color: $secondary-color;
border-radius: 2px;
border: #cfcfc4 1px solid;
border-bottom: none;
@ -37,14 +37,14 @@
min-width: 120px;
text-align: center;
background: darken($gray1, 10%);
color: lighten($black, 15%);
color: lighten($secondary-color, 15%);
cursor: pointer;
opacity: 0.5;
}
li.active, li:hover {
font-weight: bold;
background: #FFF;
color: $black;
color: $secondary-color;
opacity: 1;
}
}

View file

@ -1,7 +1,10 @@
@import "fonts";
@import "colors";
@import "tables";
@import "global";
@import "layout";
@import "panels";
@import "lists";
@import "vendor/forms";
@import "vendor/animations";
@ -9,39 +12,13 @@
box-sizing: border-box;
}
html, html body, .root {
html, html body, .wrapper {
height: 100%;
}
body {
font-family: 'Courier new', serif;
font-weight: normal;
font-family: "Noto Sans Regular";
}
body header {
width: 100%;
background: $gray1;
min-height: 40px;
border-bottom: 1px solid $gray3;
.wrapper {
display: flex;
flex-direction: row;
justify-content: space-between;
ul.nav {
display: flex;
flex-direction: row;
list-style-type: none;
column-gap: 10px;
align-items: center;
li a {
&:hover {
color: $blue;
text-decoration: none;
font-weight: bold;
}
}
}
}
}
.pt-20 { padding-top: 20px; }
@ -50,24 +27,11 @@ body header {
.mb-50 { margin-bottom: 50px; }
.w-100 { width: 100%; }
.max-width {
width: 75%;
max-width: 1024px;
}
.wrapper {
color: $secondary-color;
.align-center {
margin: 0 auto;
}
.root {
color: $black;
.task {
.content {
padding: 10px;
}
.content, textarea {
min-height: 350px;
}
.react-mount {
padding-top: 25px;
}
.trash.icon, .done.icon {

View file

@ -1 +1 @@
<svg id="Layer_2_00000088828019791583343630000013885780770304495538_" enable-background="new 0 0 512 512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g id="Icon"><g id="_29"><path d="m362.7 414.8c-37.4 0-68.4-13.9-92.2-41.4-23.4-27.1-35.2-66.6-35.2-117.6s11.9-90.4 35.5-117c23.9-27 55.1-40.7 92.7-40.7s68.6 13.7 92 40.6c23 26.5 34.6 66.4 34.6 118.4s-11.9 91.8-35.4 117.9c-23.7 26.4-54.7 39.8-92 39.8zm0-217.5c-4.4 0-11.1 1.4-18.3 13.1-6.3 10.3-9.5 25.7-9.5 45.8 0 20 3.1 35.4 9.1 45.4 6.8 11.4 13.9 12.7 18.7 12.7s11.8-1.3 18.6-12.9c6-10.2 9.1-25.6 9.1-45.7s-3.2-35.4-9.5-45.5c-7.9-12.9-15.7-12.9-18.2-12.9zm-306.9 213.4c-15 0-25.1-12.1-30-36.1v-.1c-2.1-10.9-3.1-19-3.1-25 0-5.1.5-12.5 5.3-17.3 77.9-80.4 89.6-111.3 89.6-122.6 0-4.9-1-8.3-2.9-10.1-2-1.9-6.3-3-12-3-7.4 0-14.9 1.4-22.4 4.3-7.7 2.9-13.7 6-17.9 9.3-1.8 1.4-4 2.1-6.1 2.1-2.9 0-5.7-1.2-7.7-3.6-11.6-14-19.8-29-24.3-44.6-1.6-5.5-2.5-10.8-2.5-15.8s.9-10 2.6-14.7c3.4-10.5 15.2-18.8 36.9-26.4 19.3-6.7 39.9-10 61.3-10 84.7 0 102.5 50.1 102.5 92.1 0 26.1-8.9 52.8-26.4 79.2-11.6 17.5-24.7 33.6-39.1 48l56-2.2h.4c5.1 0 9.5 3.9 9.9 9.1 1.4 14.9 2.1 28 2.1 38.9 0 12.5-2.7 23.4-8 32.3-6.2 10.5-15.6 16-27.2 16z" fill="#003c72"/><path d="m192.8 400.7h-137c-9.6 0-16.4-9.3-20.2-28.1-1.9-10.2-2.9-17.9-2.9-23.1s.8-8.7 2.5-10.3c61.6-63.6 92.5-106.8 92.5-129.6 0-7.7-2-13.5-6-17.3s-10.3-5.8-19-5.8-17.3 1.7-26 5-15.5 6.9-20.4 10.7c-10.7-12.9-18.2-26.6-22.3-40.9-1.4-4.7-2.1-9-2.1-13s.7-7.8 2.1-11.4c2.2-7.1 12.5-13.9 30.8-20.2s37.6-9.5 58-9.5c61.6 0 92.5 27.4 92.5 82.1 0 24.2-8.3 48.8-24.8 73.7s-35.8 46.4-57.8 64.6l83.4-3.3c1.4 14.6 2.1 27.2 2.1 38 0 10.7-2.2 19.8-6.6 27.2-4.6 7.5-10.8 11.2-18.8 11.2zm169.9 4.1c-34.5 0-62.7-12.7-84.6-38s-32.8-62.3-32.8-111 11-85.5 33-110.4 50.4-37.4 85.2-37.4 63 12.4 84.4 37.1c21.5 24.8 32.2 62.1 32.2 111.9s-10.9 86.9-32.8 111.2c-21.9 24.4-50.1 36.6-84.6 36.6zm0-217.5c-10.6 0-19.5 6-26.8 17.9-7.3 12-10.9 29-10.9 51s3.5 38.9 10.5 50.6 16.1 17.5 27.2 17.5 20.2-5.9 27.2-17.8c7-11.8 10.5-28.8 10.5-50.8s-3.6-38.9-10.9-50.8c-7.3-11.7-16.2-17.6-26.8-17.6z" fill="#008cff"/></g></g></svg>
<svg id="Layer_2_00000088828019791583343630000013885780770304495538_" enable-background="new 0 0 512 512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g id="Icon"><g id="_29"><path d="m362.7 414.8c-37.4 0-68.4-13.9-92.2-41.4-23.4-27.1-35.2-66.6-35.2-117.6s11.9-90.4 35.5-117c23.9-27 55.1-40.7 92.7-40.7s68.6 13.7 92 40.6c23 26.5 34.6 66.4 34.6 118.4s-11.9 91.8-35.4 117.9c-23.7 26.4-54.7 39.8-92 39.8zm0-217.5c-4.4 0-11.1 1.4-18.3 13.1-6.3 10.3-9.5 25.7-9.5 45.8 0 20 3.1 35.4 9.1 45.4 6.8 11.4 13.9 12.7 18.7 12.7s11.8-1.3 18.6-12.9c6-10.2 9.1-25.6 9.1-45.7s-3.2-35.4-9.5-45.5c-7.9-12.9-15.7-12.9-18.2-12.9zm-306.9 213.4c-15 0-25.1-12.1-30-36.1v-.1c-2.1-10.9-3.1-19-3.1-25 0-5.1.5-12.5 5.3-17.3 77.9-80.4 89.6-111.3 89.6-122.6 0-4.9-1-8.3-2.9-10.1-2-1.9-6.3-3-12-3-7.4 0-14.9 1.4-22.4 4.3-7.7 2.9-13.7 6-17.9 9.3-1.8 1.4-4 2.1-6.1 2.1-2.9 0-5.7-1.2-7.7-3.6-11.6-14-19.8-29-24.3-44.6-1.6-5.5-2.5-10.8-2.5-15.8s.9-10 2.6-14.7c3.4-10.5 15.2-18.8 36.9-26.4 19.3-6.7 39.9-10 61.3-10 84.7 0 102.5 50.1 102.5 92.1 0 26.1-8.9 52.8-26.4 79.2-11.6 17.5-24.7 33.6-39.1 48l56-2.2h.4c5.1 0 9.5 3.9 9.9 9.1 1.4 14.9 2.1 28 2.1 38.9 0 12.5-2.7 23.4-8 32.3-6.2 10.5-15.6 16-27.2 16z" fill="#003c72"/><path d="m192.8 400.7h-137c-9.6 0-16.4-9.3-20.2-28.1-1.9-10.2-2.9-17.9-2.9-23.1s.8-8.7 2.5-10.3c61.6-63.6 92.5-106.8 92.5-129.6 0-7.7-2-13.5-6-17.3s-10.3-5.8-19-5.8-17.3 1.7-26 5-15.5 6.9-20.4 10.7c-10.7-12.9-18.2-26.6-22.3-40.9-1.4-4.7-2.1-9-2.1-13s.7-7.8 2.1-11.4c2.2-7.1 12.5-13.9 30.8-20.2s37.6-9.5 58-9.5c61.6 0 92.5 27.4 92.5 82.1 0 24.2-8.3 48.8-24.8 73.7s-35.8 46.4-57.8 64.6l83.4-3.3c1.4 14.6 2.1 27.2 2.1 38 0 10.7-2.2 19.8-6.6 27.2-4.6 7.5-10.8 11.2-18.8 11.2zm169.9 4.1c-34.5 0-62.7-12.7-84.6-38s-32.8-62.3-32.8-111 11-85.5 33-110.4 50.4-37.4 85.2-37.4 63 12.4 84.4 37.1c21.5 24.8 32.2 62.1 32.2 111.9s-10.9 86.9-32.8 111.2c-21.9 24.4-50.1 36.6-84.6 36.6zm0-217.5c-10.6 0-19.5 6-26.8 17.9-7.3 12-10.9 29-10.9 51s3.5 38.9 10.5 50.6 16.1 17.5 27.2 17.5 20.2-5.9 27.2-17.8c7-11.8 10.5-28.8 10.5-50.8s-3.6-38.9-10.9-50.8c-7.3-11.7-16.2-17.6-26.8-17.6z" fill="#FFF"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,4 +1,4 @@
<div class='root align-center max-width'>
<div class='wrapper align-center max-width'>
<div class="react-mount projects"></div>
<script src="/js/main/projects.js"></script>
</div>

View file

@ -1,4 +1,4 @@
<div class="root align-center max-width">
<div class="wrapper align-center max-width">
<div class="react-mount tasks"></div>
<script src="/js/main/tasks.js"></script>
</div>

View file

@ -0,0 +1,19 @@
import React from "react";
export function NavBar() {
return (
<ul className="items nav">
<h1>Tasks</h1>
<li className="item">
<a href="/tasks/new">New</a>&nbsp;task
</li>
<li className="item">
<a href="/tasks">Show</a>&nbsp;tasks
</li>
<h1>Projects</h1>
<li className="item">
<a href="/projects">Show</a>&nbsp;projects
</li>
</ul>
);
}

View file

@ -1,4 +1,5 @@
import React, { useEffect } from "react";
import { NavBar } from "/components/NavBar";
import { useProjects } from "/hooks/useProjects";
export function Projects() {
@ -9,23 +10,28 @@ export function Projects() {
}, []);
return (
<div className="table">
<div className="table div">
<span>Projects</span>
<div className="two-columns">
<div className="column-1">
<NavBar/>
</div>
<div className="table content">
<ul className="items projects">
{projects.map((project, i) => {
return (
<li className="item" key={i}>
<a href={`/tasks#project_id=${project.id}`}>
<span>{project.name}</span>
<span className="path">{project.path}</span>
</a>
</li>
);
})}
</ul>
<div className="column-2">
<div className="panel">
<h1>Projects</h1>
<div className="panel-body">
<ul className="collection">
{projects.map((project, i) => {
return (
<li className="item" key={i}>
<a href={`/tasks#project_id=${project.id}`}>
<span className="title">{project.name}</span>
<span className="subtitle">{project.path}</span>
</a>
</li>
);
})}
</ul>
</div>
</div>
</div>
</div>
);

View file

@ -47,8 +47,8 @@ export function Task({ task }: { task?: Task }) {
return (
<form className="task" onSubmit={handleSubmit(onSave)}>
<input type="hidden" value={task?.id} {...register("id")} />
<div className="table">
<div className="table tabbed div">
<div className="panel">
<div className="panel-header panel-tabs">
<ul className="tabs">
<li
className={classnames({ active: isEditable })}
@ -64,7 +64,7 @@ export function Task({ task }: { task?: Task }) {
</li>
</ul>
</div>
<div className="table content">
<div className="panel-body">
<div>
<Select {...register("projectId")} className="form">
{projects.map((project, key) => {

View file

@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { useTasks } from "/hooks/useTasks";
import { useDestroyTask } from "/hooks/useDestroyTask";
import { TrashIcon, DoneIcon } from "/components/Icons";
import { NavBar } from "/components/NavBar";
import { DateTime } from "luxon";
import { Task, TASK_COMPLETE } from "/types/schema";
import { useUpsertTask } from "/hooks/useUpsertTask";
@ -49,53 +50,57 @@ export function Tasks() {
}, []);
return (
<div className="table">
<div className="table div">
<span>Tasks</span>
<a href="/tasks/new">New task</a>
<div className="two-columns">
<div className="column-1">
<NavBar/>
</div>
<div className="table content">
<ul className="items tasks">
{tasks.map((task: Task, key: number) => {
const { updated_at: updatedAt } = task;
const datetime = DateTime.fromISO(updatedAt);
const wasDestroyed = task === destroyedTask;
const wasCompleted = task === completedTask;
const classes = { completed: wasCompleted, removed: wasDestroyed };
const editHref = `/tasks/edit#id=${task.id}`;
return (
<li className={classnames("item", classes)} key={key}>
<a href={editHref}>
<span className="item title">{task.title}</span>
<span className="date">
{datetime.toFormat("dd LLL, yyyy")} at{" "}
{datetime.toFormat("HH:mm")}
</span>
</a>
<ul className="actions">
<li>
<DoneIcon
title="Complete task"
onClick={(e: React.MouseEvent) => [
e.stopPropagation(),
onComplete(task),
]}
/>
<div className="column-2">
<div className="panel">
<h1>Tasks</h1>
<div className="panel-body">
<ul className="collection">
{tasks.map((task: Task, key: number) => {
const { updated_at: updatedAt } = task;
const datetime = DateTime.fromISO(updatedAt);
const wasDestroyed = task === destroyedTask;
const wasCompleted = task === completedTask;
const classes = { completed: wasCompleted, removed: wasDestroyed };
const editHref = `/tasks/edit#id=${task.id}`;
return (
<li className={classnames("item", classes)} key={key}>
<a className="w-85" href={editHref}>
<span className="title">{task.title}</span>
<span className="subtitle">
{datetime.toFormat("dd LLL, yyyy")} at{" "}
{datetime.toFormat("HH:mm")}
</span>
</a>
<ul className="actions">
<li>
<DoneIcon
title="Complete task"
onClick={(e: React.MouseEvent) => [
e.stopPropagation(),
onComplete(task),
]}
/>
</li>
<li>
<TrashIcon
title="Delete task"
onClick={(e: React.MouseEvent) => [
e.stopPropagation(),
onDestroy(task),
]}
/>
</li>
</ul>
</li>
<li>
<TrashIcon
title="Delete task"
onClick={(e: React.MouseEvent) => [
e.stopPropagation(),
onDestroy(task),
]}
/>
</li>
</ul>
</li>
);
})}
</ul>
);
})}
</ul>
</div>
</div>
</div>
</div>
);

View file

@ -7,20 +7,6 @@
<link rel="icon" href="/favicon.svg"/>
</head>
<body>
<header class="mb-50">
<div class="align-center max-width wrapper">
<a href="/">
<img width=48 height=48 src="/favicon.svg"/>
</a>
<ul class="nav">
<li>
<a href="/projects">Projects</a>
<li>
<a href="/tasks">Tasks</a>
</li>
</ul>
</div>
</header>
<%= yield %>
</body>
</html>