Compare commits

..

25 Commits

Author SHA1 Message Date
0dc21100f8 CSS improvements
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-09 10:57:18 +01:00
a898e5eaa2 fixed broken "add task" buttons in Kanban 2025-12-09 10:21:39 +01:00
85faf31bc6 Merge branch 'main' into dev 2025-12-09 10:11:36 +01:00
a96bcff2df Merge branch 'bugfix/timetrack-buttons' 2025-12-08 15:34:22 +01:00
0a289eb59d Merge branch 'css/stock' 2025-12-08 13:26:40 +01:00
a2f47ac3eb Merge branch 'bugfix/timetrack-buttons' into dev 2025-12-08 13:25:46 +01:00
1de84979ee improving CSS
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-08 13:25:37 +01:00
4e88b2c4de Merge branch 'main' into dev 2025-12-08 13:16:51 +01:00
0d02f3cbda started backend translations
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-05 14:38:46 +01:00
ebf9a83b60 improved css for stock view
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-05 12:45:45 +01:00
9035ca48a7 Merge branch 'bugfix/scroll_dont_click' 2025-12-05 12:24:01 +01:00
1d55325501 Merge branch 'feature/tile_menu' into dev 2025-12-04 22:30:26 +01:00
462d0bb66e increased padding of bottom on tiny screens
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-04 22:30:08 +01:00
fda7f34b99 tried to fix bug: easylist would register clicks when scolling on mobile devices
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-04 21:50:59 +01:00
56b79e1ecf minor CSS improvement
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-04 21:48:13 +01:00
72cb91f865 bugfix
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-04 21:05:36 +01:00
3833f2f978 css improvement, coping to other themes
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-04 20:54:01 +01:00
5e0b59bacf CSS improvement
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-04 20:44:54 +01:00
55f5a663fe improving main menu for mobile devices
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-04 20:18:21 +01:00
8197a0796c implemented live price update in document view
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-03 21:50:57 +01:00
74a1d526ae implementd search in stock items
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-03 18:45:46 +01:00
4cb9c6bd2f preparing search in stock items 2025-12-03 09:34:46 +01:00
fdffad6022 removed statement that alters the log level
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-02 13:24:53 +01:00
a4bcb02459 Merge remote-tracking branch 'origin/feature/items_stock_integration' into feature/items_stock_integration 2025-12-02 11:23:53 +01:00
e887a13bbb added plenty of logging, updateed tools.jdbc
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-02 11:23:04 +01:00
23 changed files with 390 additions and 34 deletions

View File

@@ -41,7 +41,7 @@ subprojects {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
implementation("de.srsoftware:configuration.api:1.0.2")
implementation("de.srsoftware:tools.jdbc:2.0.4")
implementation("de.srsoftware:tools.jdbc:2.0.7")
implementation("de.srsoftware:tools.http:6.0.5")
implementation("de.srsoftware:tools.mime:1.1.3")
implementation("de.srsoftware:tools.logging:1.3.2")

View File

@@ -2,9 +2,11 @@
package de.srsoftware.umbrella.core;
import static de.srsoftware.tools.Optionals.nullable;
import static de.srsoftware.umbrella.core.ModuleRegistry.translator;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.HttpURLConnection.*;
import static java.text.MessageFormat.format;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.tools.Path;
@@ -72,7 +74,9 @@ public abstract class BaseHandler extends PathHandler {
}
public boolean send(HttpExchange ex, UmbrellaException e) throws IOException {
return sendContent(ex,e.statusCode(),e.getMessage());
String lang = languages(ex).stream().findFirst().orElse(null);
var translatedMessage = translator().translate(lang,e.getMessage());
return sendContent(ex,e.statusCode(),format(translatedMessage,e.fills()));
}
public boolean unauthorized(HttpExchange ex) throws IOException {

View File

@@ -13,13 +13,15 @@ import static java.text.MessageFormat.format;
public class UmbrellaException extends RuntimeException{
private final int statusCode;
private Object[] fills;
public UmbrellaException(String message, Object ... fills){
this(HTTP_SERVER_ERROR,message,fills);
}
public UmbrellaException(int statusCode, String message, Object ... fills){
super(fills == null || fills.length<1 ? message : format(message,fills));
super(message);
this.fills = fills;
this.statusCode = statusCode;
}
@@ -34,6 +36,10 @@ public class UmbrellaException extends RuntimeException{
return new UmbrellaException(message,fills);
}
public Object[] fills(){
return fills;
}
public static UmbrellaException forbidden(String message, Object... fills) {
return new UmbrellaException(HTTP_FORBIDDEN,message,fills);
}

View File

@@ -22,7 +22,11 @@
async function applyEdit(){
let success = await onSet(editValue);
if (success) {
if (success == 'reset'){
editValue = value;
} else {
value = editValue;
}
editing = false;
} else {
editValue = value;

View File

@@ -10,6 +10,7 @@ import TimeRecorder from './TimeRecorder.svelte';
let key = $state(null);
const router = useTinyRouter();
const modules = $state([]);
let expand = $state(false);
async function fetchModules(){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/legacy/user/modules`;
@@ -27,6 +28,7 @@ async function fetchModules(){
function onclick(e){
e.preventDefault();
expand = false;
let href = e.target.getAttribute('href');
if (href) router.navigate(href);
return false;
@@ -34,6 +36,7 @@ function onclick(e){
async function search(e){
e.preventDefault();
expand = false;
router.navigate(`/search?key=${key}`);
return false;
}
@@ -47,11 +50,12 @@ onMount(fetchModules);
}
</style>
<nav>
<nav class={expand?"expanded":"collapsed"}>
<form onsubmit={search}>
<input type="text" bind:value={key} />
<button type="submit">{t('search')}</button>
</form>
<button class="symbol" onclick={e => expand = !expand}></button>
<a href="/user" {onclick} class="user">{t('users')}</a>
<a href="/company" {onclick} class="company">{t('companies')}</a>
<a href="/project" {onclick} class="project">{t('projects')}</a>
@@ -72,7 +76,7 @@ onMount(fetchModules);
{#if module.name.trim()}<a href={module.url}>{module.name}</a>{/if}
{/each}
{#if user.name }
<a onclick={logout}>{t('logout_user',{user:user.name})}</a>
<a class="logout" onclick={logout}>{t('logout_user',{user:user.name})}</a>
{/if}
<TimeRecorder />
</nav>

View File

@@ -14,6 +14,23 @@
} = $props();
let editable = $derived(document.state == 1);
let sums = $derived.by(calcSums);
function calcSums(){
let data = {}
let net = 0;
let gross = 0;
for (let pos of Object.values(document.positions)){
let net_price = pos.unit_price * pos.amount;
let tax = +pos.tax;
data[tax] = net_price + (data[tax] ? data[tax] : 0);
net += net_price;
}
for (let [tax, price] of Object.entries(data)) gross += price * (+tax+100)/100;
data['net'] = net/100;
data['gross'] = (gross/100).toFixed(2);
return data;
}
async function updatePositions(resp){
let json = await resp.json();
@@ -73,9 +90,9 @@
<tr class="sums">
<td colspan="2"></td>
<td>{t('net_sum')}</td>
<td>{document.net_sum/100}&nbsp;{document.currency}</td>
<td>{sums['net']}&nbsp;{document.currency}</td>
<td colspan="2">{t('gross_sum')}</td>
<td>{document.gross_sum/100}&nbsp;{document.currency}</td>
<td>{sums['gross']}&nbsp;{document.currency}</td>
<td></td>
</tr>
</tbody>

View File

@@ -78,8 +78,10 @@
if (!tasks[user_id][state]) tasks[user_id][state] = {};
tasks[user_id][state][task.id] = task;
yikes();
return 'reset';
} else {
error(resp);
return false;
}
}
@@ -255,7 +257,7 @@
{/if}
{/each}
<div class="add_task">
<LineEditor value={t('add_object',{object:t('task')})} editable={true} onSet={(name) => create(name,uid,state)}/>
<LineEditor value={t('add_object',{object:t('task')})} editable={true} onSet={(name) => create(name,u.id,state)}/>
</div>
</div>
{/each}

View File

@@ -1,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, target } from '../../urls.svelte.js';
import { api, post, target } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js';
import { display } from '../../time.svelte';
@@ -18,6 +18,7 @@
let notes = $state(null);
let pages = $state(null);
let projects = $state(null);
let stock = $state(null);
let tasks = $state(null);
let times = $state(null);
@@ -38,19 +39,16 @@
window.history.replaceState(history.state, '', url);
const data = { key : key, fulltext : fulltext };
const options = {
credentials:'include',
method: 'POST',
body: JSON.stringify(data)
};
fetch(api('bookmark/search'),options).then(handleBookmarks);
fetch(api('company/search'),options).then(handleCompanies);
fetch(api('document/search'),options).then(handleDocuments);
fetch(api('notes/search'),options).then(handleNotes);
fetch(api('project/search'),options).then(handleProjects);
fetch(api('task/search'),options).then(handleTasks);
fetch(api('time/search'),options).then(handleTimes);
fetch(api('wiki/search'),options).then(handleWikiPages);
post(api('bookmark/search'),data).then(handleBookmarks);
post(api('company/search '),data).then(handleCompanies);
post(api('document/search'),data).then(handleDocuments);
post(api('notes/search' ),data).then(handleNotes);
post(api('project/search' ),data).then(handleProjects);
post(api('task/search' ),data).then(handleTasks);
post(api('stock/search' ),data).then(handleStock);
post(api('time/search' ),data).then(handleTimes);
post(api('wiki/search' ),data).then(handleWikiPages);
}
function onclick(e){
@@ -107,6 +105,15 @@
}
}
async function handleStock(resp){
if (resp.ok){
const res = await resp.json();
stock = Object.keys(res).length ? res : null;
} else {
error(resp);
}
}
async function handleTasks(resp){
if (resp.ok){
const res = await resp.json();
@@ -212,6 +219,23 @@
</ul>
</fieldset>
{/if}
{#if stock}
<fieldset>
<legend>
{t('stock')}
</legend>
<ul>
{#each Object.values(stock) as item}
<li>
<a href="/stock/{item.owner.type}/{item.owner.id}/item/{item.owner_number}" {onclick} >
{item.name} [{t('code')}: <span class="code">{item.code}</span>]
</a>
{@html item.description.rendered}
</li>
{/each}
</ul>
</fieldset>
{/if}
{#if notes}
<fieldset>
<legend>

View File

@@ -73,7 +73,7 @@
}
function measured(evt,duration,d){
if (d > 100) return;
if (d > 10) return;
if (duration < 500){
onclick(evt);
} else {

View File

@@ -137,7 +137,7 @@ CREATE TABLE IF NOT EXISTS {0} (
if (rs.next()) project = Project.of(rs);
rs.close();
if (project == null) throw UmbrellaException.notFound("No project found for id {0}",projectId);
if (project == null) throw UmbrellaException.notFound("no_project_for_id",projectId);
rs = select(ALL).from(TABLE_CUSTOM_STATES).where(PROJECT_ID,equal(projectId)).exec(db);
var states = project.allowedStates();

View File

@@ -11,7 +11,9 @@ import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
import static de.srsoftware.umbrella.core.ModuleRegistry.translator;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.stock.Constants.TABLE_ITEMS;
import static java.lang.System.Logger.Level.DEBUG;
import de.srsoftware.tools.ColorLogger;
import de.srsoftware.tools.Tuple;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.model.*;
@@ -33,6 +35,7 @@ public class ItemDb {
try {
var companyLocations = new HashMap<Long,Location>();
var companyInfo = new HashMap<Long, Tuple<Company,String>>(); // map from companyId → (company, language)
LOG.log(DEBUG,"Reading items:\nid: code / name / unit / price / tax rate");
var rs = select(ALL).from(TABLE_ITEMS).exec(db);
while (rs.next()){
var id = rs.getLong(ID);
@@ -44,13 +47,17 @@ public class ItemDb {
var unitPrice = rs.getLong(UNIT_PRICE);
var tax = rs.getLong(TAX);
var tuple = companyInfo.get(companyId);
LOG.log(DEBUG," - read item {0}: {1} / {2} / {3} / {4} / {5} %",id,companyId,code,name,unit,unitPrice,tax);
String lang = null;
Company company;
if (tuple == null){
LOG.log(DEBUG, " loading company {0}:",companyId);
company = companyService().get(companyId);
LOG.log(DEBUG, " → {0}",company.name());
for (var member : companyService().getMembers(companyId)){
lang = member.language();
if (lang != null){
LOG.log(DEBUG, " → language = {0}",lang);
tuple = Tuple.of(company,lang);
companyInfo.put(companyId,tuple);
break;
@@ -59,12 +66,15 @@ public class ItemDb {
} else {
company = tuple.a;
lang = tuple.b;
LOG.log(DEBUG, " using company: {0} ({1})",company.name(),lang);
}
var location = companyLocations.get(companyId);
if (location == null) {
location = stockDb.save(new DbLocation(0,company,null,"virtual items",null));
companyLocations.put(companyId,location);
}
LOG.log(DEBUG, " using location: {0}",location.resolve().name());
var stockItem = new Item(0,company,0,location,code,name,description);
var props = stockItem.properties();
var keyUnitPrice = translator().translate(lang,UNIT_PRICE);
@@ -75,12 +85,12 @@ public class ItemDb {
props.add(new Property(0,keyUnit,unit,null));
props.add(new Property(0,keyTax,tax,"%"));
props.add(new Property(0,keyLegacyId,id,null));
LOG.log(DEBUG," saving item {0}:",stockItem);
stockDb.save(stockItem);
}
rs.close();
} catch (SQLException e) {
throw databaseException("Failed to migrate items from itemDB to stockDB!");
}
LOG.log(System.Logger.Level.WARNING,"migrateTo({0}) not implemented", stockDb);
}
}

View File

@@ -3,8 +3,8 @@ package de.srsoftware.umbrella.stock;
import static de.srsoftware.tools.Optionals.is0;
import static de.srsoftware.tools.Optionals.nullIfEmpty;
import static de.srsoftware.tools.jdbc.Condition.equal;
import static de.srsoftware.tools.jdbc.Condition.isNull;
import static de.srsoftware.tools.jdbc.Condition.*;
import static de.srsoftware.tools.jdbc.Condition.like;
import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.core.Constants.*;
@@ -17,6 +17,7 @@ import static java.text.MessageFormat.format;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Location;
import java.sql.Connection;
@@ -189,6 +190,30 @@ public class SqliteDb extends BaseDb implements StockDb {
}
}
@Override
public Map<Long, Item> find(Collection<Owner> owners, Collection<String> keys, boolean fulltext) {
try {
var items = new HashMap<Long,Item>();
var ownerCodes = owners.stream().map(Owner::dbCode).toArray();
var query = select(ALL).from(TABLE_ITEMS).where(OWNER, in(ownerCodes));
if (fulltext) {
query.leftJoin(ID,TABLE_ITEM_PROPERTIES,ITEM_ID);
for (var key : keys) query.where(format("CONCAT({0},\" \",{1},\" \",{2})",NAME,DESCRIPTION,VALUE),like("%"+key+"%"));
} else {
for (var key : keys) query.where(NAME,like("%"+key+"%"));
}
var rs = query.exec(db);
while (rs.next()){
var item = Item.of(rs);
items.put(item.id(),item);
}
rs.close();
return items;
} catch (SQLException e) {
throw new UmbrellaException("Failed to load items from database");
}
}
@Override
public Collection<DbLocation> listChildLocations(long parentId) {
try {

View File

@@ -6,10 +6,12 @@ import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Location;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public interface StockDb {
Property addNewProperty(long itemId, String name, Object value, String unit);
Location delete(DbLocation location);
Map<Long, Item> find(Collection<Owner> owners, Collection<String> keys, boolean fulltext);
Collection<DbLocation> listChildLocations(long parentId);
Collection<DbLocation> listCompanyLocations(Company company);
Collection<Item> listItemsAt(Location location);
@@ -24,5 +26,6 @@ public interface StockDb {
Map<String,Object> pathToLocation(Location location);
DbLocation save(DbLocation location);
Item save(Item item);
Property setProperty(long itemId, long existingPropId, Object value);
}

View File

@@ -9,6 +9,8 @@ import static de.srsoftware.umbrella.core.Field.ITEM;
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.Paths.SEARCH;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.WARNING;
@@ -174,6 +176,7 @@ public class StockModule extends BaseHandler implements StockService {
case LIST -> postItemList(user.get(), path, ex);
case LOCATION -> postLocation(user.get(),ex);
case PROPERTY -> postProperty(user.get(),ex);
case SEARCH -> postSearch(user.get(),ex);
case null, default -> super.doPost(path,ex);
};
} catch (UmbrellaException e){
@@ -389,6 +392,17 @@ public class StockModule extends BaseHandler implements StockService {
return sendContent(ex,property);
}
private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingFieldException(KEY);
var keys = Arrays.asList(key.split(" "));
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
Set<Owner> owners = new HashSet<>(companyService().listCompaniesOf(user).values());
owners.add(user);
var items = stockDb.find(owners,keys,fulltext);
return sendContent(ex,mapValues(items));
}
@Override
public Collection<Object> redefineMe(long company_id) {
// TODO

View File

@@ -270,7 +270,7 @@ CREATE TABLE IF NOT EXISTS {0} (
public Task load(long taskId) throws UmbrellaException {
var map = load(List.of(taskId));
var task = map.get(taskId);
if (task == null) throw UmbrellaException.notFound("No task found for id {0}",taskId);
if (task == null) throw UmbrellaException.notFound("no_task_for_id",taskId);
return task;
}

View File

@@ -183,6 +183,8 @@
"new_password": "neues Passwort",
"new_property": "neue Eigenschaft",
"no_company": "keine Firma",
"no_project_for_id": "Kein Projekt mit ID {0} gefunden!",
"no_task_for_id": "Keine Aufgabe mit ID {0} gefunden!",
"note": "Notiz",
"notes": "Notizen",
"not_recent_version": "Die ist nicht die neuste Version dieser Seite!",

View File

@@ -183,6 +183,8 @@
"new_password": "new password",
"new_property": "new property",
"no_company": "no company",
"no_project_for_id": "No project found for id {0}",
"no_task_for_id": "No task found for id {0}",
"note": "note",
"notes": "notes",
"not_recent_version": "This is not the current version of this page!",

View File

@@ -308,3 +308,10 @@ tr:hover .taglist .tag button {
.easylist .filter{
background: black;
}
@media screen and (max-width: 900px) {
#app nav a{
background: black;
color: red;
}
}

View File

@@ -426,6 +426,58 @@ a.wikilink{
float: right;
}
@media screen and (min-width: 900px) {
#app nav > button.symbol{
display: none;
}
}
@media screen and (max-width: 900px) {
body{
padding-top: 30px;
}
#app nav{
grid-template-columns: 33% 34% 33%;
display: grid;
padding: 0 10px;
position: absolute;
left: 0;
right: 0;
}
#app nav form{
grid-column-end: span 2;
}
#app nav .logout{
grid-column-end: 4;
}
#app nav.collapsed a{
display: none;
}
#app nav a {
font-size: 19px !important;
display: grid;
text-align: center;
border: 1px solid;
margin: 5px;
border-radius: 7px;
}
#app nav a::before {
font-size: 30px;
}
#app nav .timetracking{
grid-column-end: span 3;
font-size: 19px;
text-align: center;
}
#app nav.expanded .timetracking{
display: none;
}
}
@media screen and (max-width: 600px) {
.grid2{
display: grid;
@@ -455,6 +507,18 @@ a.wikilink{
.easylist input{
font-size: 20px;
}
#app nav{
grid-template-columns: auto auto;
}
#app nav form{
grid-column-end: span 1;
}
#app nav .logout{
grid-column-end: 3;
}
#app nav.expanded .timetracking{
grid-column-end: span 2;
}
}
fieldset.vcard{

View File

@@ -298,3 +298,10 @@ tr:hover .taglist .tag button {
.easylist .filter{
background: black;
}
@media screen and (max-width: 900px) {
#app nav a{
background: black;
color: orange;
}
}

View File

@@ -504,8 +504,83 @@ a.wikilink{
float: right;
}
@media screen and (min-width: 900px) {
#app nav > button.symbol{
display: none;
}
}
@media screen and (max-width: 900px) {
body{
padding-top: 30px;
}
#app nav{
grid-template-columns: 33% 34% 33%;
display: grid;
padding: 0 10px;
position: absolute;
left: 0;
right: 0;
}
#app nav form{
grid-column-end: span 2;
}
#app nav .logout{
grid-column-end: 4;
}
#app nav.collapsed a{
display: none;
}
#app nav a {
font-size: 19px !important;
display: grid;
text-align: center;
border: 1px solid;
margin: 5px;
border-radius: 7px;
}
#app nav a::before {
font-size: 30px;
}
#app nav .timetracking{
grid-column-end: span 3;
font-size: 19px;
text-align: center;
}
#app nav.expanded .timetracking{
display: none;
}
.grid3 {
grid-template-columns: auto auto;
}
.grid3 .properties{
grid-column-end: span 2;
order: 1;
}
.grid3 .tags{
order: 2;
}
.grid3 .notes{
grid-column-end: span 2;
order: 3;
}
.grid3 .locations{
order: 5;
}
.grid3 .items{
order: 6;
}
}
@media screen and (max-width: 600px) {
.grid2{
.grid2,
.grid3{
display: grid;
grid-template-columns: auto;
}
@@ -533,6 +608,21 @@ a.wikilink{
.easylist input{
font-size: 20px;
}
#app nav{
grid-template-columns: auto auto;
}
#app nav form,
.grid3 .notes,
.grid3 .tags,
.grid3 .properties{
grid-column-end: span 1;
}
#app nav .logout{
grid-column-end: 3;
}
#app nav.expanded .timetracking{
grid-column-end: span 2;
}
}
fieldset.vcard{

View File

@@ -283,3 +283,10 @@ tr:hover .taglist .tag button {
border-color: blue;
color: blue;
}
@media screen and (max-width: 900px) {
#app nav a{
background: white;
color: blue;
}
}

View File

@@ -426,6 +426,58 @@ a.wikilink{
float: right;
}
@media screen and (min-width: 900px) {
#app nav > button.symbol{
display: none;
}
}
@media screen and (max-width: 900px) {
body{
padding-top: 30px;
}
#app nav{
grid-template-columns: 33% 34% 33%;
display: grid;
padding: 0 10px;
position: absolute;
left: 0;
right: 0;
}
#app nav form{
grid-column-end: span 2;
}
#app nav .logout{
grid-column-end: 4;
}
#app nav.collapsed a{
display: none;
}
#app nav a {
font-size: 19px !important;
display: grid;
text-align: center;
border: 1px solid;
margin: 5px;
border-radius: 7px;
}
#app nav a::before {
font-size: 30px;
}
#app nav .timetracking{
grid-column-end: span 3;
font-size: 19px;
text-align: center;
}
#app nav.expanded .timetracking{
display: none;
}
}
@media screen and (max-width: 600px) {
.grid2{
display: grid;
@@ -455,6 +507,18 @@ a.wikilink{
.easylist input{
font-size: 20px;
}
#app nav{
grid-template-columns: auto auto;
}
#app nav form{
grid-column-end: span 1;
}
#app nav .logout{
grid-column-end: 3;
}
#app nav.expanded .timetracking{
grid-column-end: span 2;
}
}
fieldset.vcard{