Compare commits

...

15 Commits

Author SHA1 Message Date
ae89ac642e due date working 2025-11-07 09:00:58 -05:00
dfffa59094 estimation started 2025-11-06 22:27:09 -05:00
ec85ab6a34 misc tweaks 2025-11-06 21:58:13 -05:00
76d4c4be73 progress... 2025-11-05 14:15:40 -05:00
ef1fb55057 add typings/parsing 2025-11-05 08:18:24 -05:00
697f8a2d7d complex editor 2025-11-04 11:29:19 -05:00
11bc2e1d99 style tweaks 2025-11-03 15:58:48 -05:00
66f0d22970 need colors finished 2025-11-03 15:32:46 -05:00
2ca31bb21d fixed make 2025-11-03 15:22:18 -05:00
cbcf559bac better truth table 2025-11-03 14:02:40 -05:00
a573835219 colors improved, still not quite right 2025-11-03 13:24:16 -05:00
e682b8a619 desk status started 2025-11-02 21:21:40 -05:00
f47ab32d68 input controls 2025-11-02 13:47:12 -05:00
396ad5b3c9 table layout started 2025-11-01 15:55:24 -04:00
1d527de26f ui tweaks 2025-11-01 15:03:57 -04:00
20 changed files with 669 additions and 241 deletions

461
app.js
View File

@ -1,32 +1,8 @@
/** @import * as TYPES from "./graph/types.ts" */
import * as FSHandle from "./store-directory-handle.js";
const {Div, DOM} = Gale({
Title:{
padding:"2rem",
background: "blue",
color:"white"
},
Plain:{},
PartGroup:{
display: `flex`,
flexWrap: `wrap`
},
Part:{
border: `1px solid black`,
borderRadius: `5px`,
padding: `1rem`
},
BlockScreen:{
position: "fixed",
zIndex: "9999",
top: "0",
left: "0",
width: "100%",
height: "100%",
background: "rgba(128, 128, 128, 0.5)"
}
});
import Styles from "./styles.js";
const {DOM, Div, Tag} = Styles;
async function PickHandle()
{
@ -44,11 +20,21 @@ async function LoadHandleFiles()
const module = await import("./graph/room.js"+"?bust="+Math.random());
/** @type {Record<string, TYPES.GraphParts>} */
const read = module.default();
for(const roomKey in read)
{
const room = read[roomKey]
for(const pass in room.Pass)
{
await room.Pass[pass].load();
}
}
rooms.val = read;
}
catch(_e)
catch(e)
{
console.log("the handle exists, but the request failed. the service work must not be ready yet.")
console.log("the handle exists, but the request failed. the service work must not be ready yet.", e)
rooms.val = {};
}
}
@ -59,45 +45,281 @@ async function LoadHandleFiles()
}
}
/** @type {Van.State<Record<string, TYPES.GraphParts>>} */
const rooms = van.state({});
let handle = await FSHandle.getDirectoryHandle();
await LoadHandleFiles();
/** @type {(path:string[], part:string, time:number, data:string)=>void} */
async function WRITE(path, part, time, data)
{
const fileHandle = await FSHandle.Dig(handle, path, true);
if(fileHandle)
{
const file = await fileHandle.getFile();
const text = await file.text();
let json = {};
if(text)
{
json = JSON.parse(text);
}
let partProp = json[part];
if(!partProp)
{
partProp = [];
json[part] = partProp;
}
partProp.push([time, data]);
const writeable = await fileHandle.createWritable();
await writeable.write(JSON.stringify(json, null, 2));
await writeable.close();
}
}
/** @type {Van.State<TYPES.User|false>} */
const loggedIn = van.state(false);
const blocking = van.state(false);
const showDesks = van.state(true, "desks");
/** @type {(inParts:Record<string, TYPES.Part>, inPasses:Record<string, TYPES.Pass>)=>HTMLElement} */
function Parts(inParts, inPasses)
{
const rows = [];
const row = [DOM.th()]
for(const pass in inPasses)
{
row.push(DOM.th(inPasses[pass].name));
}
rows.push(DOM.thead(row));
Object.entries(inParts).map(([part_id, part])=>{
const row = [DOM.th(part.name)];
for(const [pass, data] of part.pass)
{
row.push(DOM.td.Part(data.work.map(w=>Div.Plain(w[1]) )))
}
rows.push(DOM.tr(row))
});
return DOM.table.GapVertical(rows);
}
//const deskRender = van.state(0);
/** @type {(part:TYPES.Part, pass:TYPES.Pass, closeHandler:()=>void)=>HTMLElement} */
function PartEditor(part, pass, closeHandler)
{
const partPass = part.pass?.get(pass);
const hist = van.state(false);
const edit = van.state(false);
const upper = ()=>{
if(partPass?.work.length)
{
return DOM.div(
DOM.button(
{
onclick()
{
hist.val=!hist.val;
}
}, ()=>(hist.val ? "hide" : "show")+" changes"
),
hist.val ? partPass.work.map(w=>{
const date = new Date(w[0]);
return DOM.div(`${date.getMonth()}/${date.getDate()}`, DOM.strong(w[1]), w[2].name);
}) : ""
);
}
else
{
return "";
}
}
const lower = ()=>
{
return DOM.div(
()=>{
return loggedIn.rawVal ? DOM.button(
{onclick(){edit.val = !edit.val;}},
()=>(edit.val ? "cancel" : "make") + " changes"
) : DOM.p("log in to make changes")
},
()=>{
const textarea = DOM.textarea()
return edit.val ? DOM.div(
textarea,
DOM.button({
onclick(){
if(loggedIn.rawVal && partPass)
{
blocking.val = true;
partPass.make(loggedIn.rawVal, textarea.value).then(()=>{
blocking.val = false;
//deskRender.val++;
})
}
else
{
return false;
}
}
}, "save changes")
) : ""
}
)
}
const self = DOM.div(
upper,
()=>DOM.button({onclick(e){e.stopPropagation(); closeHandler(this.parentElement)}}, "close"),
DOM.div(
()=>{
if(partPass)
{
return partPass.work.find((w)=>w[0] == partPass.time)?.[1] || ""
}
return "(no data yet)";
}
),
lower,
);
return self;
}
/** @type {(inDesks:Record<string, TYPES.Desk>)=>HTMLElement} */
function Desks(inDesks)
{
return Div.PartGroup(
Object.entries(inDesks).map(([desk_id, desk])=>{
//loggedIn.val;
//deskRender.val;
//console.log("reredering desk", desk.name);
if (loggedIn.val)
{
let userInRole = false;
for(const role of desk.role)
{
if(role.user.includes(loggedIn.val))
{
userInRole = true;
}
}
if(!userInRole)
{
return null;
}
}
const work = [];
for(const [pass, scan] of desk.pass)
{
// at least one but not all need fields are empty
const caution = scan.need_empty.length>0 && scan.need_empty.length<desk.need.length;
work.push(DOM.tr(
DOM.td(pass.name),
desk.need.map((part, index, array)=>
{
const partPass = part.pass.get(pass);
if(!partPass){ return null }
const latest = partPass.work.find(t=>t[0] == partPass.time)?.[1];
const attributes = {};
if(latest)
{
attributes.class = Tag("PartGood")
}
else
{
attributes.class = Tag("PartEmpty")
}
if(scan.need_dirty.includes(index))
{
attributes.class = Tag("PartDirty")
}
return DOM.td(
Div.Part(
attributes,
latest
)
);
}),
DOM.td(
Div.Icon("⇉"),
scan.due_date ? Div.Part(
scan.due_date.toLocaleDateString(),
scan.due_date.toLocaleTimeString()
) : " "
),
desk.make.map((part, index, array)=>
{
const partPass = part.pass.get(pass);
if(!partPass){ return null }
const latest = partPass.work.find(t=>t[0] == partPass.time)?.[1];
const attr = "data-editing"
function close(editor){
editor.remove();
this.setAttribute(attr, "false");
}
const attributes = {
onclick(){
const check = this.getAttribute(attr);
if(check !== "true")
{
this.setAttribute(attr, "true");
this.appendChild(PartEditor(part, pass, close.bind(this)));
}
}
};
if(latest)
{
attributes.class = Tag("PartGood")
}
else
{
attributes.class = Tag("PartEmpty")
}
if( (desk.need.length==0 && !latest) || scan.make_dirty.includes(index))
{
if(!latest && caution)
{
attributes.class = Tag("PartCaution")
}
else
{
attributes.class = Tag("PartDirty");
}
}
return DOM.td(
Div.Part(
attributes,
latest
)
);
}),
))
}
return Div.DeskContainer(
DOM.h3(desk.name),
DOM.table.GapHorizontal(
DOM.thead(
DOM.tr(
DOM.th(),
desk.need.map((part, index)=>DOM.th(part.name)),
DOM.th("→"),
desk.make.map((part, index)=>DOM.th(part.name))
)
),
work
)
)
}),
)
}
/** @type {(room_id:string, graphParts:TYPES.GraphParts)=>HTMLElement} */
function Room(room_id, graphParts)
{
@ -109,10 +331,6 @@ function Room(room_id, graphParts)
Div.Plain("Users:"),
Div.PartGroup(
Object.entries(graphParts.User).map(([user_id, user])=>{
return ()=>{
//rerender.val;
return Div.Part(
DOM.div.Plain(user.name),
()=>{
@ -124,132 +342,21 @@ function Room(room_id, graphParts)
)
},
)
}
})
),
Div.Plain("Passes:"),
Div.PartGroup(
Object.entries(graphParts.Pass).map(([pass_id, pass])=>{
return ()=>{
console.log("rerendering...", rerender.rawVal);
rerender.val;
return Div.Part(
DOM.div.Plain(pass.name),
()=>{
return DOM.button.Plain(
{async onclick(){
await pass[pass.live ? "dump" : "load"]();
rerender.val++
}},
pass.live ? "Dump" : "Load"
)
return DOM.button({onclick(){
showDesks.val = !showDesks.val;
}}, showDesks.val ? "Show Parts" : "Show Desks")
},
)
}
})
),
Div.Plain("Parts"),
Div.PartGroup(
Object.entries(graphParts.Part).map(([part_id, part])=>{
return ()=>{
()=>{
rerender.val;
return showDesks.val ? Desks(graphParts.Desk) : Parts(graphParts.Part, graphParts.Pass);
},
const work = []
for(const [pass, data] of part.pass)
{
work.push(Div.Part(
DOM.h5(pass.name),
DOM.p(data.time),
data.work.map(([time, data, user])=>Div.Plain(
DOM.span(time),
" ",
DOM.strong(data),
)),
Div.Plain(
loggedIn.val ? DOM.button({onclick(){
if(loggedIn.val)
{
blocking.val = true;
data.make(loggedIn.val, "NEW").then(()=>{
blocking.val = false;
rerender.val++;
});
}
}}, "Add work!")
: null
)
))
}
return Div.Part(
DOM.h3(part.name),
...work,
);
}
}),
),
Div.Plain("Desks"),
Div.PartGroup(
Object.entries(graphParts.Desk).map(([desk_id, desk])=>{
return ()=>{
rerender.val;
/** @type {(part:TYPES.Part, index:number, pass:TYPES.Pass, dirty:number[])=>HTMLElement|null} */
const Iterator = (part, index, pass, dirty)=>{
const partPass = part.pass.get(pass);
if(partPass)
{
const time = partPass.time;
const latest = partPass.work.find(t=>t[0] == time);
return Div.Plain(
part.name,
DOM.p( dirty.includes(index) ? "Dirty" : "Good!" ),
Div.Part(
time, " ", latest?.[1] || ""
)
);
}
return null;
}
const work = [];
for(const [pass, dirty] of desk.pass)
{
work.push(Div.Part(
DOM.h5(pass.name),
Div.Part(
DOM.h4("Need:"),
desk.need.map((part, index)=>Iterator(part, index, pass, dirty.need))
),
Div.Part(
DOM.h4("Make:"),
desk.make.map((part, index)=>Iterator(part, index, pass, dirty.make))
)
))
}
return Div.Part(
DOM.h3(desk.name),
work
)
}
}),
),
()=>{
return blocking.val ? Div.BlockScreen() : null

View File

@ -0,0 +1,43 @@
//@ts-check
import CreateAllRooms, {Room} from "../../graph/graph.js";
const user = {
u1:"Seth T",
u4:"Sarah S",
u5:"Adam M",
u6:"Matt Y",
u7:"Seth F",
u8:"Brittany F"
};
export default CreateAllRooms({
room_01:Room({
user,
role:{
dev:["Development", "u1"],
write:["Writing", "u5"],
admin:["Admin", "u4"]
},
part:{
p1:["Page title"],
p2:["Page slug"],
p3:["Page preview"],
p4:["Page Project"],
p5:["Page Corrections", "loop"],
},
desk:{
d1:["Write page metas", ["admin", "write"], { }, "p1", "p2"],
d2:["Build Page preview", ["admin", "dev" ], {p1:1, p2:1, p5:4}, "p3", "p4"],
d3:["Proof Page", ["admin", "write"], {p3:1, }, "p5" ]
},
pass:{
pass_01:["January"],
//pass_02:["February"],
//pass_03:["March"],
//pass_04:["April"],
//pass_05:["May"],
//pass_06:["June"],
//pass_07:["July"],
}
})
});

View File

@ -1,6 +1,8 @@
/** @import * as TYPES from "./types.ts" */
import * as FSAccess from "../store-directory-handle.js";
export const noop = "no-op";
/** @type {TYPES.GraphBuilder} */
export function Room({user, role, part, desk, pass})
{
@ -32,8 +34,9 @@ export function Room({user, role, part, desk, pass})
const PartList = part;
for(let partId in part)
{
const name = part[partId];
PartList[partId] = /** @type {TYPES.Part} */({name, id:partId, need:[], make:[], pass:new Map()});
const [name, loop] = part[partId];
PartList[partId] = /** @type {TYPES.Part} */({name, id:partId, need:[], make:[], pass:new Map(), loop:loop});
}
// mutate desks
@ -42,7 +45,7 @@ export function Room({user, role, part, desk, pass})
const DeskList = desk;
for(let deskId in desk)
{
const [name, roleIDs, mode, needObj, ...makePartIDs] = desk[deskId];
const [name, roleIDs, needObj, ...makePartIDs] = desk[deskId];
/** @type {TYPES.Part[]}*/ const need =[];
/** @type {number[]}*/ const time =[];
@ -50,7 +53,6 @@ export function Room({user, role, part, desk, pass})
const deskObj = {
name,
id:deskId,
mode,
need,
time,
make:[],
@ -160,6 +162,7 @@ export function Room({user, role, part, desk, pass})
{
passCheck.time = latest;
}
//payload.sort()
passCheck.work = /** @type {TYPES.Work[]}*/(payload);
}
@ -219,39 +222,93 @@ const Scan =(desk, pass)=>
const dirtyNeed = [];
const dirtyMake = [];
const emptyNeed = [];
const emptyMake = [];
let makeMin = Infinity;
let needMax = -Infinity;
// added for estimation
let estMin = Infinity;
let estMax = -Infinity;
let estSum = 0;
/*
Loop parts:
- always considered clean when the leading value is a no-op
- as a need, considered clean when empty
*/
/** @type {(part:TYPES.Part)=>[time:number, value:string|undefined, part:TYPES.Part]} */
const lookup =(part)=>
{
const partPass = part.pass.get(pass);
const partPassTime = partPass?.time || 0;
const partPassValue = partPass?.work.find(t=>t[0] == partPassTime)?.[1];
return [partPassTime, partPassValue, part];
}
// update needMax
for(let i=0; i<desk.need.length; i++)
{
const part = desk.need[i];
const partPassTime = part.pass.get(pass)?.time || 0;
if(partPassTime > needMax) needMax = partPassTime;
const [time, value, part] = lookup(desk.need[i]);
if(part.loop)
{
if(!value || value == noop)
{
continue;
}
}
// update makeMin AND dirty check makes
if(time > needMax) needMax = time;
if(!time) emptyNeed.push(i);
}
// update makeMin AND dirtyMakes
for(let i=0; i<desk.make.length; i++)
{
const part = desk.make[i];
const partPassTime = part.pass.get(pass)?.time || 0;
if(partPassTime < makeMin) makeMin = partPassTime;
if(partPassTime < needMax)
{
dirtyMake.push(i);
}
const [time] = lookup(desk.make[i]);
if(time < makeMin) makeMin = time;
if(time < needMax) dirtyMake.push(i);
if(!time) emptyMake.push(i)
}
// dirty check needs
// dirtyNeeds
for(let i=0; i<desk.need.length; i++)
{
const part = desk.need[i];
const partPassTime = part.pass.get(pass)?.time || 0;
if(partPassTime > makeMin)
const [time, value, part] = lookup(desk.need[i]);
if(part.loop)
{
dirtyNeed.push(i);
if(!value || value == noop)
{
continue;
}
}
desk.pass.set(pass, {need:dirtyNeed, make:dirtyMake})
if(time > makeMin)
{
dirtyNeed.push(i);
// estimation
if(time < estMin) estMin = time;
const allottedTime = desk.time[i] * 1000 * 60 * 60;
const projectedTime = time + allottedTime;
estSum += allottedTime;
if(projectedTime > estMax) estMax = projectedTime;
}
}
estSum += estMin;
let stamp = estSum;
if(estMax > estSum)
{
stamp = estMax;
}
desk.pass.set(pass, {need_dirty:dirtyNeed, make_dirty:dirtyMake, need_empty:emptyNeed, make_empty:emptyMake, due_date:isFinite(stamp) ? new Date(stamp) : undefined})
};

View File

@ -1,17 +1,17 @@
export type User = {name:string, id:string, desk:Set<Desk>};
export type Role = {name:string, id:string, user:User[]};
export type Desk = {name:string, id:string, need:Part[], time:number[], make:Part[], pass:Map<Pass, Flag>, mode:string, role:Role[]};
export type Desk = {name:string, id:string, need:Part[], time:number[], make:Part[], pass:Map<Pass, Scan>, role:Role[]};
export type Pass = {name:string, id:string, path:string, live:boolean, load:()=>Promise<void>, dump:()=>void};
export type Part = {name:string, id:string, pass:Map<Pass, {time:number, work:Work[], make:(user:User, data:string)=>Promise<void>}>, need:Desk[], make:Desk[]};
export type Part = {name:string, id:string, pass:Map<Pass, {time:number, work:Work[], make:(user:User, data:string)=>Promise<void>}>, need:Desk[], make:Desk[], loop?:boolean};
export type Work = [time:number, data:string, user:User];
export type Flag = {need:number[], make:number[]}
export type Scan = {need_dirty:number[], make_dirty:number[], need_empty:number[], make_empty:number[], due_date?:Date}
export type GraphBuilder=
<
Users extends Record<string, string>,
Roles extends Record<string, [ name:string, ...users:Array<keyof Users>]>,
Parts extends Record<string, string>,
Desks extends Record<string, [ name:string, roles:Array<keyof Roles>, mode:"all"|"one", need:Partial<Record<keyof Parts, number>>, ...make:Array<keyof Parts>]>,
Parts extends Record<string, [ name:string, loop?:"loop"] >,
Desks extends Record<string, [ name:string, roles:Array<keyof Roles>, need:Partial<Record<keyof Parts, number>>, ...make:Array<keyof Parts>]>,
>
(
params:{

View File

@ -3,6 +3,46 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* Basic CSS Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
a {
text-decoration: none;
color: inherit;
}
ul, ol {
list-style: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
</style>
</head>
<body>
<script>

View File

@ -1,25 +1,43 @@
//@ts-check
import Rooms, {Room} from "../../graph/graph.js";
import User from "./user.js";
import CreateAllRooms, {Room} from "../../graph/graph.js";
export default Rooms({
const user = {
u1:"Seth T",
u4:"Sarah S",
u5:"Adam M",
u6:"Matt Y",
u7:"Seth F",
u8:"Brittany F"
};
export default CreateAllRooms({
room_01:Room({
user:User,
user,
role:{
dev:["Development", "u1"]
dev:["Development", "u1"],
write:["Writing", "u5"],
admin:["Admin", "u4"]
},
part:{
p1:"Page title",
p2:"Page slug",
p3:"Page preview",
p1:["Page title"],
p2:["Page slug"],
p3:["Page preview"],
p4:["Page Project"],
p5:["Page Corrections", "loop"],
},
desk:{
d1:["Write page metas", ["dev"], "all", {}, "p1", "p2"],
d2:["Build Page preview", ["dev"], "all", {p1:1, p2:1}, "p3"]
d1:["Write page metas", ["admin", "write"], { }, "p1", "p2"],
d2:["Build Page preview", ["admin", "dev" ], {p1:1, p2:1, p5:1}, "p3", "p4"],
d3:["Proof Page", ["admin", "write"], {p3:1, }, "p5" ]
},
pass:{
pass_01:["January"],
pass_02:["February"]
//pass_02:["February"],
//pass_03:["March"],
//pass_04:["April"],
//pass_05:["May"],
//pass_06:["June"],
//pass_07:["July"],
}
})
});

View File

@ -1,5 +0,0 @@
export default {
u1:"seth",
u2:"seth2",
u3:"seth3"
}

View File

@ -1,9 +0,0 @@
{
"p1":
[
[123, "data"],
[456, "more data"],
[789, "even more data"],
[101112, "even more data"]
]
}

View File

@ -1,8 +0,0 @@
{
"p2":
[
[123, "data"],
[456, "more data"],
[789, "even more data"]
]
}

View File

@ -0,0 +1,14 @@
{
"p1": [
[
1762196165935,
"normal title"
]
],
"p2": [
[
1762196173135,
"normal slug"
]
]
}

View File

@ -0,0 +1,8 @@
{
"p4": [
[
1762193485093,
"Make made Late"
]
]
}

View File

@ -0,0 +1,8 @@
{
"p1": [
[
1762186057868,
"Need made Early"
]
]
}

View File

@ -0,0 +1,14 @@
{
"p4": [
[
1762196034794,
"Make made Early"
]
],
"p1": [
[
1762196047017,
"Need made Late"
]
]
}

View File

@ -0,0 +1,14 @@
{
"p3": [
[
1762196236384,
"complete 3"
]
],
"p4": [
[
1762196245327,
"complete 4"
]
]
}

View File

@ -0,0 +1,14 @@
{
"p1": [
[
1762196209704,
"complete 1"
]
],
"p2": [
[
1762196217319,
"complete 2"
]
]
}

View File

@ -0,0 +1,18 @@
{
"p3": [
[
1762196341950,
"complete 3"
]
],
"p4": [
[
1762196348950,
"complete 4"
],
[
1762196393702,
"complete Later"
]
]
}

View File

@ -0,0 +1,18 @@
{
"p1": [
[
1762196328127,
"complete 1"
]
],
"p2": [
[
1762196335342,
"complete 2"
],
[
1762196363079,
"complete Late"
]
]
}

View File

@ -0,0 +1,8 @@
{
"p1": [
[
1762201681431,
"underway"
]
]
}

View File

@ -29,8 +29,13 @@ async function Interceptor(event)
const text = await FSAccess.Read(handle, parts);
if(text)
{
console.log("successful intercept:", pathname);
return new Response(text, options);
}
else
{
console.log("failed intercept:", pathname);
}
}

64
styles.js Normal file
View File

@ -0,0 +1,64 @@
export default Gale({
Title:{
padding:"2rem",
background: "blue",
color:"white"
},
Plain:{},
PartGroup:{
display: `flex`,
flexWrap: `wrap`
},
Icon:{
background:"black",
color:"white",
borderRadius:"2rem",
fontWeight:"bolder",
padding:"0 0.8rem",
margin:"0 1rem"
},
Part:{
border: `1px solid black`,
borderRadius: `5px`,
padding: `0.5rem 1rem`,
minHeight: "2rem",
},
PartGood:{
background:"#009b2e",
color:"white",
},
PartEmpty:{
background:"#ddd"
},
PartDirty:{
background:"red",
color:"white",
fontWeight:"bold"
},
PartCaution:{
background:"yellow",
color:"black",
},
BlockScreen:{
position: "fixed",
zIndex: "9999",
top: "0",
left: "0",
width: "100%",
height: "100%",
background: "rgba(128, 128, 128, 0.5)"
},
DeskContainer:{
border: `1px solid black`,
borderRadius: `5px`,
padding: `1rem`
},
GapHorizontal:{
borderCollapse:"separate",
borderSpacing:"0.3rem 2rem",
},
GapVertical:{
borderCollapse:"separate",
borderSpacing:"2rem 0.2rem",
}
});