Better print style + base tabs
This commit is contained in:
parent
6e0ccd5575
commit
642b555b3d
|
@ -20,7 +20,7 @@ const App: FunctionalComponent = () => {
|
|||
|
||||
return (
|
||||
<div id="app">
|
||||
<Header />
|
||||
<Header class="noPrint" />
|
||||
<Router onChange={handleRoute}>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/p/:projectId" component={Project} />
|
||||
|
|
|
@ -2,9 +2,13 @@ import { FunctionalComponent, h } from "preact";
|
|||
import { Link } from "preact-router/match";
|
||||
import * as style from "./style.css";
|
||||
|
||||
const Header: FunctionalComponent = () => {
|
||||
export interface HeaderProps {
|
||||
class?: string
|
||||
}
|
||||
|
||||
const Header: FunctionalComponent<HeaderProps> = ({ ...props}) => {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class={`container ${props.class ? props.class : ''}`}>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { FunctionalComponent, h, ComponentChild } from "preact";
|
||||
import * as style from "./style.css";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
export interface TabItem {
|
||||
label: string
|
||||
icon?: string
|
||||
render: () => ComponentChild
|
||||
}
|
||||
|
||||
export interface TabsProps {
|
||||
class?: string
|
||||
items: TabItem[]
|
||||
}
|
||||
|
||||
const Tabs: FunctionalComponent<TabsProps> = ({ items, ...props }) => {
|
||||
const [ selectedTabIndex, setSelectedTabIndex ] = useState(0);
|
||||
|
||||
const onTabClick = (tabIndex: number) => {
|
||||
setSelectedTabIndex(tabIndex);
|
||||
};
|
||||
|
||||
const selectedTab = items[selectedTabIndex];
|
||||
|
||||
return (
|
||||
<div class={`${style.tabs} ${props.class}`}>
|
||||
<div class="tabs">
|
||||
<ul class={`noPrint`}>
|
||||
{
|
||||
items.map((tabItem, tabIndex) => (
|
||||
<li key={`tab-${tabIndex}`}
|
||||
onClick={onTabClick.bind(null, tabIndex)}
|
||||
class={`${selectedTabIndex === tabIndex ? 'is-active' : ''}`}>
|
||||
<a>
|
||||
<span class="icon is-small">{tabItem.icon}</span>
|
||||
{tabItem.label}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class={style.tabContent}>
|
||||
{ selectedTab.render() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
|
@ -0,0 +1,8 @@
|
|||
.tabs {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
padding-top: 1em;
|
||||
max-width: 100%;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// This file is automatically generated from your CSS. Any edits will be overwritten.
|
||||
export const tabs: string;
|
||||
export const tabContent: string;
|
|
@ -0,0 +1,41 @@
|
|||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const media = window.matchMedia(query);
|
||||
const [ matches, setMatches ] = useState(media.matches);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MediaQueryListEvent) => {
|
||||
setMatches(evt.matches);
|
||||
};
|
||||
media.addListener(listener);
|
||||
return () => {media.removeListener(listener)}
|
||||
}, []);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function usePrintMediaQuery(): boolean {
|
||||
const isMediaQueryPrint = useMediaQuery("print");
|
||||
const [ isPrint, setIsPrint ] = useState(false);
|
||||
|
||||
// Firefox/IE compatibility layer
|
||||
useEffect(() => {
|
||||
const beforePrint = () => {
|
||||
setIsPrint(true);
|
||||
};
|
||||
const afterPrint = () => {
|
||||
setIsPrint(false);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeprint', beforePrint);
|
||||
window.addEventListener('afterprint', afterPrint);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeprint', beforePrint);
|
||||
window.removeEventListener('afterprint', afterPrint);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isMediaQueryPrint || isPrint;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Project } from "../models/project";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import { getProjectWeightedMean, getProjectStandardDeviation } from "../util/stat";
|
||||
|
||||
export interface Estimation {
|
||||
e: number
|
||||
sd: number
|
||||
}
|
||||
|
||||
export interface ProjetEstimations {
|
||||
p99: Estimation
|
||||
p90: Estimation
|
||||
p68: Estimation
|
||||
}
|
||||
|
||||
export function useProjectEstimations(p :Project): ProjetEstimations {
|
||||
const [ estimations, setEstimations ] = useState({
|
||||
p99: { e: 0, sd: 0 },
|
||||
p90: { e: 0, sd: 0 },
|
||||
p68: { e: 0, sd: 0 },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const projectWeightedMean = getProjectWeightedMean(p)
|
||||
const projectStandardDeviation = getProjectStandardDeviation(p);
|
||||
|
||||
setEstimations({
|
||||
p99: { e: projectWeightedMean, sd: (projectStandardDeviation * 3) },
|
||||
p90: { e: projectWeightedMean, sd: (projectStandardDeviation * 1.645) },
|
||||
p68: { e: projectWeightedMean, sd: (projectStandardDeviation) },
|
||||
})
|
||||
}, [p.tasks]);
|
||||
|
||||
return estimations;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import "./style/index.css";
|
||||
|
||||
import "bulma/css/bulma.css";
|
||||
|
||||
import App from "./components/app.tsx";
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { FunctionalComponent, h } from "preact";
|
||||
import { Project } from "../../models/project";
|
||||
import TaskTable from "./tasks-table";
|
||||
import TimePreview from "./time-preview";
|
||||
import FinancialPreview from "./financial-preview";
|
||||
import { addTask, updateTaskEstimation, removeTask, updateProjectLabel, updateTaskLabel, ProjectReducerActions } from "../../hooks/use-project-reducer";
|
||||
import { Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||
|
||||
export interface EstimationTabProps {
|
||||
project: Project
|
||||
dispatch: (action: ProjectReducerActions) => void
|
||||
}
|
||||
|
||||
const EstimationTab: FunctionalComponent<EstimationTabProps> = ({ project, dispatch }) => {
|
||||
const onTaskAdd = (task: Task) => {
|
||||
dispatch(addTask(task));
|
||||
};
|
||||
|
||||
const onTaskRemove = (taskId: TaskID) => {
|
||||
dispatch(removeTask(taskId));
|
||||
}
|
||||
|
||||
const onTaskLabelUpdate = (taskId: TaskID, label: string) => {
|
||||
dispatch(updateTaskLabel(taskId, label));
|
||||
}
|
||||
|
||||
const onEstimationChange = (taskId: TaskID, confidence: EstimationConfidence, value: number) => {
|
||||
dispatch(updateTaskEstimation(taskId, confidence, value));
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div class="columns">
|
||||
<div class="column is-9">
|
||||
<TaskTable
|
||||
project={project}
|
||||
onTaskAdd={onTaskAdd}
|
||||
onTaskRemove={onTaskRemove}
|
||||
onTaskLabelUpdate={onTaskLabelUpdate}
|
||||
onEstimationChange={onEstimationChange} />
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<TimePreview project={project} />
|
||||
<FinancialPreview project={project} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EstimationTab;
|
|
@ -1,12 +1,17 @@
|
|||
import { FunctionalComponent, h } from "preact";
|
||||
import * as style from "./style.css";
|
||||
import { Project } from "../../models/project";
|
||||
import { useProjectEstimations } from "../../hooks/use-project-estimations";
|
||||
|
||||
export interface FinancialPreviewProps {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project }) => {
|
||||
const estimations = useProjectEstimations(project);
|
||||
const costPerTimeUnit = 500;
|
||||
const maxCost = Math.ceil((estimations.p99.e + estimations.p99.sd) * costPerTimeUnit);
|
||||
const minCost = Math.ceil((estimations.p99.e - estimations.p99.sd) * costPerTimeUnit);
|
||||
return (
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-striped is-fullwidth">
|
||||
|
@ -15,18 +20,18 @@ const FinancialPreview: FunctionalComponent<FinancialPreviewProps> = ({ project
|
|||
<th colSpan={2}>Prévisionnel financier</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Temps</th>
|
||||
<th class="is-narrow">Temps</th>
|
||||
<th>Coût</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Maximum</td>
|
||||
<td></td>
|
||||
<td class="is-narrow">Maximum</td>
|
||||
<td>~ {maxCost} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Minimum</td>
|
||||
<td></td>
|
||||
<td class="is-narrow">Minimum</td>
|
||||
<td>~ {minCost} €</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -2,14 +2,12 @@ import { FunctionalComponent, h } from "preact";
|
|||
import { useEffect } from "preact/hooks";
|
||||
import * as style from "./style.css";
|
||||
import { newProject } from "../../models/project";
|
||||
import TaskTable from "./tasks-table";
|
||||
import TimePreview from "./time-preview";
|
||||
import FinancialPreview from "./financial-preview";
|
||||
import { useProjectReducer, addTask, updateTaskEstimation, removeTask, updateProjectLabel, updateTaskLabel } from "../../hooks/use-project-reducer";
|
||||
import { Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||
import { useProjectReducer, updateProjectLabel } from "../../hooks/use-project-reducer";
|
||||
import { getProjectStorageKey } from "../../util/storage";
|
||||
import { useLocalStorage } from "../../hooks/use-local-storage";
|
||||
import EditableText from "../../components/editable-text";
|
||||
import Tabs from "../../components/tabs";
|
||||
import EstimationTab from "./estimation-tab";
|
||||
|
||||
export interface ProjectProps {
|
||||
projectId: string
|
||||
|
@ -20,22 +18,6 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
|||
const [ storedProject, storeProject ] = useLocalStorage(projectStorageKey, newProject(projectId));
|
||||
const [ project, dispatch ] = useProjectReducer(storedProject);
|
||||
|
||||
const onTaskAdd = (task: Task) => {
|
||||
dispatch(addTask(task));
|
||||
};
|
||||
|
||||
const onTaskRemove = (taskId: TaskID) => {
|
||||
dispatch(removeTask(taskId));
|
||||
}
|
||||
|
||||
const onTaskLabelUpdate = (taskId: TaskID, label: string) => {
|
||||
dispatch(updateTaskLabel(taskId, label));
|
||||
}
|
||||
|
||||
const onEstimationChange = (taskId: TaskID, confidence: EstimationConfidence, value: number) => {
|
||||
dispatch(updateTaskEstimation(taskId, confidence, value));
|
||||
};
|
||||
|
||||
const onProjectLabelChange = (projectLabel: string) => {
|
||||
dispatch(updateProjectLabel(projectLabel));
|
||||
};
|
||||
|
@ -49,38 +31,28 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
|||
<div class={`container ${style.estimation}`}>
|
||||
<EditableText
|
||||
editIconClass="is-size-4"
|
||||
render={(value) => (<h3 class="is-size-3">{value}</h3>)}
|
||||
render={(value) => (<h2 class="is-size-3">{value}</h2>)}
|
||||
onChange={onProjectLabelChange}
|
||||
value={project.label ? project.label : "Projet sans nom"} />
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li class="is-active">
|
||||
<a>
|
||||
<span class="icon is-small">📋</span>
|
||||
Estimation
|
||||
</a>
|
||||
</li>
|
||||
{/* <li>
|
||||
<a disabled>
|
||||
<span class="icon is-small">⚙️</span>
|
||||
Paramètres
|
||||
</a>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-9">
|
||||
<TaskTable
|
||||
project={project}
|
||||
onTaskAdd={onTaskAdd}
|
||||
onTaskRemove={onTaskRemove}
|
||||
onTaskLabelUpdate={onTaskLabelUpdate}
|
||||
onEstimationChange={onEstimationChange} />
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<TimePreview project={project} />
|
||||
<FinancialPreview project={project} />
|
||||
</div>
|
||||
<div class={style.tabContainer}>
|
||||
<Tabs items={[
|
||||
{
|
||||
label: 'Estimation',
|
||||
icon: '📋',
|
||||
render: () => <EstimationTab project={project} dispatch={dispatch} />
|
||||
},
|
||||
{
|
||||
label: 'Paramètres',
|
||||
icon: '⚙️',
|
||||
render: () => null
|
||||
},
|
||||
{
|
||||
label: 'Exporter',
|
||||
icon: '↗️',
|
||||
render: () => null
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -13,4 +13,12 @@
|
|||
|
||||
.mainColumn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.middleTable td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
padding-top: 1em;
|
||||
}
|
|
@ -3,3 +3,5 @@ export const estimation: string;
|
|||
export const noTasks: string;
|
||||
export const noBorder: string;
|
||||
export const mainColumn: string;
|
||||
export const middleTable: string;
|
||||
export const tabContainer: string;
|
||||
|
|
|
@ -4,8 +4,7 @@ import * as style from "./style.css";
|
|||
import { Project } from "../../models/project";
|
||||
import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||
import EditableText from "../../components/editable-text";
|
||||
|
||||
|
||||
import { usePrintMediaQuery } from "../../hooks/use-media-query";
|
||||
|
||||
export interface TaskTableProps {
|
||||
project: Project
|
||||
|
@ -27,6 +26,8 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
[EstimationConfidence.Pessimistic]: 0,
|
||||
} as EstimationTotals);
|
||||
|
||||
const isPrint = usePrintMediaQuery();
|
||||
|
||||
useEffect(() => {
|
||||
let optimistic = 0;
|
||||
let likely = 0;
|
||||
|
@ -76,10 +77,10 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
|
||||
return (
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||
<table class={`table is-bordered is-striped is-hoverable is-fullwidth ${style.middleTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class={style.noBorder} rowSpan={2}></th>
|
||||
<th class={`${style.noBorder} noPrint`} rowSpan={2}></th>
|
||||
<th class={style.mainColumn} rowSpan={2}>Tâche</th>
|
||||
<th rowSpan={2}>Catégorie</th>
|
||||
<th colSpan={3}>Estimation</th>
|
||||
|
@ -97,7 +98,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
const categoryLabel = category ? category.label : '???';
|
||||
return (
|
||||
<tr key={`taks-${t.id}`}>
|
||||
<td class="is-narrow">
|
||||
<td class={`is-narrow noPrint`}>
|
||||
<button
|
||||
onClick={onTaskRemoveClick.bind(null, t.id)}
|
||||
class="button is-danger is-small is-outlined">
|
||||
|
@ -112,19 +113,31 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
</td>
|
||||
<td>{ categoryLabel }</td>
|
||||
<td>
|
||||
<input class="input" type="number" value={t.estimations.optimistic}
|
||||
min={0}
|
||||
onChange={onOptimisticChange.bind(null, t.id)} />
|
||||
{
|
||||
isPrint ?
|
||||
<span>{t.estimations.optimistic}</span> :
|
||||
<input class="input" type="number" value={t.estimations.optimistic}
|
||||
min={0}
|
||||
onChange={onOptimisticChange.bind(null, t.id)} />
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<input class="input" type="number" value={t.estimations.likely}
|
||||
min={0}
|
||||
onChange={onLikelyChange.bind(null, t.id)} />
|
||||
{
|
||||
isPrint ?
|
||||
<span>{t.estimations.likely}</span> :
|
||||
<input class="input" type="number" value={t.estimations.likely}
|
||||
min={0}
|
||||
onChange={onLikelyChange.bind(null, t.id)} />
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<input class="input" type="number" value={t.estimations.pessimistic}
|
||||
min={0}
|
||||
onChange={onPessimisticChange.bind(null, t.id)} />
|
||||
{
|
||||
isPrint ?
|
||||
<span>{t.estimations.pessimistic}</span> :
|
||||
<input class="input" type="number" value={t.estimations.pessimistic}
|
||||
min={0}
|
||||
onChange={onPessimisticChange.bind(null, t.id)} />
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
@ -133,7 +146,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
{
|
||||
Object.keys(project.tasks).length === 0 ?
|
||||
<tr>
|
||||
<td class={style.noBorder}></td>
|
||||
<td class={`${style.noBorder} noPrint`}></td>
|
||||
<td class={style.noTasks} colSpan={5}>Aucune tâche pour l'instant.</td>
|
||||
</tr> :
|
||||
null
|
||||
|
@ -141,9 +154,9 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class={style.noBorder}></td>
|
||||
<td colSpan={2}>
|
||||
<div class="field has-addons">
|
||||
<td class={`${style.noBorder} noPrint`}></td>
|
||||
<td colSpan={2} class={isPrint ? style.noBorder : ''}>
|
||||
<div class="field has-addons noPrint">
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="text" placeholder="Nouvelle tâche"
|
||||
value={task.label} onChange={onNewTaskLabelChange} />
|
||||
|
@ -171,7 +184,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
<th colSpan={3}>Total</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={3} class={style.noBorder}></td>
|
||||
<td colSpan={isPrint ? 2 : 3} class={style.noBorder}></td>
|
||||
<td>{totals.optimistic}</td>
|
||||
<td>{totals.likely}</td>
|
||||
<td>{totals.pessimistic}</td>
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
import { FunctionalComponent, h } from "preact";
|
||||
import { Project } from "../../models/project";
|
||||
import { Task } from "../../models/task";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import { getProjectWeightedMean, getProjectStandardDeviation } from "../../util/stat";
|
||||
import { useProjectEstimations } from "../../hooks/use-project-estimations";
|
||||
|
||||
export interface TimePreviewProps {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
|
||||
const [ estimations, setEstimations ] = useState({
|
||||
p99: { e: '0', sd: '0' },
|
||||
p90: { e: '0', sd: '0' },
|
||||
p68: { e: '0', sd: '0' },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const projectWeightedMean = getProjectWeightedMean(project).toFixed(2);
|
||||
const projectStandardDeviation = getProjectStandardDeviation(project);
|
||||
setEstimations({
|
||||
p99: { e: projectWeightedMean, sd: (projectStandardDeviation * 3).toFixed(2) },
|
||||
p90: { e: projectWeightedMean, sd: (projectStandardDeviation * 1.645).toFixed(2) },
|
||||
p68: { e: projectWeightedMean, sd: (projectStandardDeviation).toFixed(2) },
|
||||
})
|
||||
}, [project.tasks]);
|
||||
const estimations = useProjectEstimations(project);
|
||||
|
||||
return (
|
||||
<div class="table-container">
|
||||
|
@ -33,25 +17,25 @@ const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
|
|||
<th colSpan={2}>Prévisionnel temps</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Niveau de confiance</th>
|
||||
<th class="is-narrow">Confiance</th>
|
||||
<th>Estimation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>>= 99.7%</td>
|
||||
<td>{`${estimations.p99.e} ± ${estimations.p99.sd} j/h`}</td>
|
||||
<td class="is-narrow">>= 99.7%</td>
|
||||
<td>{`${estimations.p99.e.toPrecision(2)} ± ${estimations.p99.sd.toPrecision(2)} j/h`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>>= 90%</td>
|
||||
<td>{`${estimations.p90.e} ± ${estimations.p90.sd} j/h`}</td>
|
||||
<td class="is-narrow">>= 90%</td>
|
||||
<td>{`${estimations.p90.e.toPrecision(2)} ± ${estimations.p90.sd.toPrecision(2)} j/h`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>>= 68%</td>
|
||||
<td>{`${estimations.p68.e} ± ${estimations.p68.sd} j/h`}</td>
|
||||
<td class="is-narrow">>= 68%</td>
|
||||
<td>{`${estimations.p68.e.toPrecision(2)} ± ${estimations.p68.sd.toPrecision(2)} j/h`}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tfoot class="noPrint">
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<a class="is-small is-pulled-right" href="https://en.wikipedia.org/wiki/Three-point_estimation" target="_blank">❓ Estimation à 3 points</a>
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
#app {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
@media print
|
||||
{
|
||||
.noPrint, .noPrint * {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
// This file is automatically generated from your CSS. Any edits will be overwritten.
|
||||
export const app: string;
|
||||
export const app: string;
|
Loading…
Reference in New Issue