mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Initial config with a few files that build on client and server side.
This commit is contained in:
commit
ec182792be
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/node_modules/
|
||||||
|
/build/
|
||||||
|
/static/*.bundle.js
|
||||||
|
/static/*.bundle.js.map
|
||||||
|
|
||||||
|
# Build helper files.
|
||||||
|
/.build*
|
||||||
|
|
||||||
|
*.swp
|
||||||
|
*.pyc
|
||||||
|
*.bak
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
/node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
9
app/client/tsconfig.json
Normal file
9
app/client/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../buildtools/tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../build/app/client"
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{ "path": "../common" }
|
||||||
|
]
|
||||||
|
}
|
193
app/client/ui/PagePanels.ts
Normal file
193
app/client/ui/PagePanels.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* Note that it assumes the presence of cssVars.cssRootVars on <body>.
|
||||||
|
*/
|
||||||
|
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
|
||||||
|
import {transition} from 'app/client/ui/transitions';
|
||||||
|
import {colors} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {dom, DomArg, noTestId, Observable, styled, TestId} from "grainjs";
|
||||||
|
|
||||||
|
export interface PageSidePanel {
|
||||||
|
// Note that widths need to start out with a correct default in JS (having them in CSS is not
|
||||||
|
// enough), needed for open/close transitions.
|
||||||
|
panelWidth: Observable<number>;
|
||||||
|
panelOpen: Observable<boolean>;
|
||||||
|
hideOpener?: boolean; // If true, don't show the opener handle.
|
||||||
|
header: DomArg;
|
||||||
|
content: DomArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageContents {
|
||||||
|
leftPanel: PageSidePanel;
|
||||||
|
rightPanel?: PageSidePanel; // If omitted, the right panel isn't shown at all.
|
||||||
|
|
||||||
|
headerMain: DomArg;
|
||||||
|
contentMain: DomArg;
|
||||||
|
|
||||||
|
onResize?: () => void; // Callback for when either pane is opened, closed, or resized.
|
||||||
|
testId?: TestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pagePanels(page: PageContents) {
|
||||||
|
const testId = page.testId || noTestId;
|
||||||
|
const left = page.leftPanel;
|
||||||
|
const right = page.rightPanel;
|
||||||
|
const onResize = page.onResize || (() => null);
|
||||||
|
|
||||||
|
return cssPageContainer(
|
||||||
|
cssLeftPane(
|
||||||
|
testId('left-panel'),
|
||||||
|
cssTopHeader(left.header),
|
||||||
|
left.content,
|
||||||
|
|
||||||
|
dom.style('width', (use) => use(left.panelOpen) ? use(left.panelWidth) + 'px' : ''),
|
||||||
|
|
||||||
|
// Opening/closing the left pane, with transitions.
|
||||||
|
cssLeftPane.cls('-open', left.panelOpen),
|
||||||
|
transition(left.panelOpen, {
|
||||||
|
prepare(elem, open) { elem.style.marginRight = (open ? -1 : 1) * (left.panelWidth.get() - 48) + 'px'; },
|
||||||
|
run(elem, open) { elem.style.marginRight = ''; },
|
||||||
|
finish: onResize,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Resizer for the left pane.
|
||||||
|
// TODO: resizing to small size should collapse. possibly should allow expanding too
|
||||||
|
cssResizeFlexVHandle(
|
||||||
|
{target: 'left', onSave: (val) => { left.panelWidth.set(val); onResize(); }},
|
||||||
|
testId('left-resizer'),
|
||||||
|
dom.show(left.panelOpen)),
|
||||||
|
|
||||||
|
// Show plain border when the resize handle is hidden.
|
||||||
|
cssResizeDisabledBorder(dom.hide(left.panelOpen)),
|
||||||
|
|
||||||
|
cssMainPane(
|
||||||
|
cssTopHeader(
|
||||||
|
(left.hideOpener ? null :
|
||||||
|
cssPanelOpener('PanelRight', cssPanelOpener.cls('-open', left.panelOpen),
|
||||||
|
testId('left-opener'),
|
||||||
|
dom.on('click', () => toggleObs(left.panelOpen)))
|
||||||
|
),
|
||||||
|
|
||||||
|
page.headerMain,
|
||||||
|
|
||||||
|
(!right || right.hideOpener ? null :
|
||||||
|
cssPanelOpener('PanelLeft', cssPanelOpener.cls('-open', right.panelOpen),
|
||||||
|
testId('right-opener'),
|
||||||
|
dom.on('click', () => toggleObs(right.panelOpen)))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
page.contentMain,
|
||||||
|
),
|
||||||
|
(right ? [
|
||||||
|
// Resizer for the right pane.
|
||||||
|
cssResizeFlexVHandle(
|
||||||
|
{target: 'right', onSave: (val) => { right.panelWidth.set(val); onResize(); }},
|
||||||
|
testId('right-resizer'),
|
||||||
|
dom.show(right.panelOpen)),
|
||||||
|
|
||||||
|
cssRightPane(
|
||||||
|
testId('right-panel'),
|
||||||
|
cssTopHeader(right.header),
|
||||||
|
right.content,
|
||||||
|
|
||||||
|
dom.style('width', (use) => use(right.panelOpen) ? use(right.panelWidth) + 'px' : ''),
|
||||||
|
|
||||||
|
// Opening/closing the right pane, with transitions.
|
||||||
|
cssRightPane.cls('-open', right.panelOpen),
|
||||||
|
transition(right.panelOpen, {
|
||||||
|
prepare(elem, open) { elem.style.marginLeft = (open ? -1 : 1) * right.panelWidth.get() + 'px'; },
|
||||||
|
run(elem, open) { elem.style.marginLeft = ''; },
|
||||||
|
finish: onResize,
|
||||||
|
}),
|
||||||
|
)] : null
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleObs(boolObs: Observable<boolean>) {
|
||||||
|
boolObs.set(!boolObs.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssVBox = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`);
|
||||||
|
const cssHBox = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
`);
|
||||||
|
const cssPageContainer = styled(cssHBox, `
|
||||||
|
position: absolute;
|
||||||
|
isolation: isolate; /* Create a new stacking context */
|
||||||
|
z-index: 0; /* As of March 2019, isolation does not have Edge support, so force one with z-index */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 600px;
|
||||||
|
background-color: ${colors.lightGrey};
|
||||||
|
`);
|
||||||
|
export const cssLeftPane = styled(cssVBox, `
|
||||||
|
position: relative;
|
||||||
|
background-color: ${colors.lightGrey};
|
||||||
|
width: 48px;
|
||||||
|
margin-right: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: margin-right 0.4s;
|
||||||
|
&-open {
|
||||||
|
width: 240px;
|
||||||
|
min-width: 160px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const cssMainPane = styled(cssVBox, `
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 0px;
|
||||||
|
min-width: 0px;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 1;
|
||||||
|
`);
|
||||||
|
const cssRightPane = styled(cssVBox, `
|
||||||
|
position: relative;
|
||||||
|
background-color: ${colors.lightGrey};
|
||||||
|
width: 0px;
|
||||||
|
margin-left: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: margin-left 0.4s;
|
||||||
|
z-index: 0;
|
||||||
|
&-open {
|
||||||
|
width: 240px;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const cssTopHeader = styled('div', `
|
||||||
|
height: 48px;
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid ${colors.mediumGrey};
|
||||||
|
`);
|
||||||
|
const cssResizeFlexVHandle = styled(resizeFlexVHandle, `
|
||||||
|
--resize-handle-color: ${colors.mediumGrey};
|
||||||
|
--resize-handle-highlight: ${colors.lightGreen};
|
||||||
|
`);
|
||||||
|
const cssResizeDisabledBorder = styled('div', `
|
||||||
|
flex: none;
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: ${colors.mediumGrey};
|
||||||
|
`);
|
||||||
|
const cssPanelOpener = styled(icon, `
|
||||||
|
flex: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 8px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-mask-size: 16px 16px;
|
||||||
|
background-color: ${colors.lightGreen};
|
||||||
|
transition: transform 0.4s;
|
||||||
|
&:hover { background-color: ${colors.darkGreen}; }
|
||||||
|
&-open { transform: rotateY(180deg); }
|
||||||
|
`);
|
47
app/client/ui/mouseDrag.ts
Normal file
47
app/client/ui/mouseDrag.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Small utility to help with processing mouse-drag events. Usage is:
|
||||||
|
* dom('div', mouseDrag((startEvent, elem) => ({
|
||||||
|
* onMove(moveEvent) { ... },
|
||||||
|
* onStop(stopEvent) { ... },
|
||||||
|
* })));
|
||||||
|
*
|
||||||
|
* The passed-in callback is called on 'mousedown' events. It may return null to ignore the event.
|
||||||
|
* Otherwise, it should return a handler for mousemove/mouseup: we will then subscribe to these
|
||||||
|
* events, and clean up on mouseup.
|
||||||
|
*/
|
||||||
|
import {dom, DomElementMethod, IDisposable} from "grainjs";
|
||||||
|
|
||||||
|
|
||||||
|
export interface MouseDragHandler {
|
||||||
|
onMove(moveEv: MouseEvent): void;
|
||||||
|
onStop(endEv: MouseEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MouseDragStart = (startEv: MouseEvent, elem: Element) => MouseDragHandler|null;
|
||||||
|
|
||||||
|
export function mouseDragElem(elem: HTMLElement, onStart: MouseDragStart): IDisposable {
|
||||||
|
|
||||||
|
// This prevents the default text-drag behavior when elem is part of a text selection.
|
||||||
|
elem.style.userSelect = 'none';
|
||||||
|
|
||||||
|
return dom.onElem(elem, 'mousedown', (ev, el) => _startDragging(ev, el, onStart));
|
||||||
|
}
|
||||||
|
export function mouseDrag(onStart: MouseDragStart): DomElementMethod {
|
||||||
|
return (elem) => { mouseDragElem(elem, onStart); };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _startDragging(startEv: MouseEvent, elem: Element, onStart: MouseDragStart) {
|
||||||
|
const dragHandler = onStart(startEv, elem);
|
||||||
|
if (dragHandler) {
|
||||||
|
|
||||||
|
const {onMove, onStop} = dragHandler;
|
||||||
|
const upLis = dom.onElem(document, 'mouseup', stop, {useCapture: true});
|
||||||
|
const moveLis = dom.onElem(document, 'mousemove', onMove, {useCapture: true});
|
||||||
|
|
||||||
|
function stop(stopEv: MouseEvent) {
|
||||||
|
moveLis.dispose();
|
||||||
|
upLis.dispose();
|
||||||
|
onStop(stopEv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
app/client/ui/resizeHandle.ts
Normal file
155
app/client/ui/resizeHandle.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Exports resizeFlexVHandle() for resizing flex items. The returned handle should be attached to
|
||||||
|
* a flexbox container next to the item that needs resizing. Usage:
|
||||||
|
*
|
||||||
|
* dom('div.flex',
|
||||||
|
* dom('div.child-to-be-resized', ...),
|
||||||
|
* resizeFlexVHandle({target: 'left', onSave: (width) => { ... })
|
||||||
|
* dom('div.other-children', ...),
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* The .target parameter determines whether to resize the left or right sibling of the handle. The
|
||||||
|
* handle shows up as a 1px line of color --resize-handle-color. On hover and while resizing, it
|
||||||
|
* changes to --resize-handle-highlight.
|
||||||
|
*
|
||||||
|
* The handle may be dragged to change the width of the target. It sets style.width while
|
||||||
|
* dragging, and calls .onSave(width) at the end, and optionally .onDrag(width) while dragging.
|
||||||
|
*
|
||||||
|
* You may limit the width of the target with min-width, max-width properties as usual.
|
||||||
|
*
|
||||||
|
* At the moment, flexbox width resizing is the only need, but the same approach is intended to be
|
||||||
|
* easily extended to non-flexbox situation, and to height-resizing.
|
||||||
|
*/
|
||||||
|
import {mouseDrag} from 'app/client/ui/mouseDrag';
|
||||||
|
import {DomElementArg, styled} from "grainjs";
|
||||||
|
|
||||||
|
export type ChangeFunc = (value: number) => void;
|
||||||
|
export type Edge = 'left' | 'right';
|
||||||
|
|
||||||
|
export interface IResizeFlexOptions {
|
||||||
|
// Whether to change the width of the flex item to the left or to the right of this handle.
|
||||||
|
target: 'left'|'right';
|
||||||
|
onDrag?(value: number): void;
|
||||||
|
onSave(value: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResizeOptions {
|
||||||
|
prop: 'width' | 'height';
|
||||||
|
sign: 1 | -1;
|
||||||
|
getTarget(handle: Element): Element|null;
|
||||||
|
onDrag?(value: number): void;
|
||||||
|
onSave(value: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See module documentation for usage.
|
||||||
|
export function resizeFlexVHandle(options: IResizeFlexOptions, ...args: DomElementArg[]): Element {
|
||||||
|
const resizeOptions: IResizeOptions = {
|
||||||
|
prop: 'width',
|
||||||
|
sign: options.target === 'left' ? 1 : -1,
|
||||||
|
getTarget(handle: Element) {
|
||||||
|
return options.target === 'left' ? handle.previousElementSibling : handle.nextElementSibling;
|
||||||
|
},
|
||||||
|
onDrag: options.onDrag,
|
||||||
|
onSave: options.onSave,
|
||||||
|
};
|
||||||
|
return cssResizeFlexVHandle(
|
||||||
|
mouseDrag((ev, handle) => onResizeStart(ev, handle, resizeOptions)),
|
||||||
|
...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The core of the implementation. This is intended to be very general, so as to be adaptable to
|
||||||
|
// other kinds of resizing (edge of parent, resizing height) by only attaching this to the
|
||||||
|
// mouseDrag() event with suitable options. See resizeFlexVHandle().
|
||||||
|
function onResizeStart(startEv: MouseEvent, handle: Element, options: IResizeOptions) {
|
||||||
|
const target = options.getTarget(handle) as HTMLElement|null;
|
||||||
|
if (!target) { return null; }
|
||||||
|
|
||||||
|
const {sign, prop, onDrag, onSave} = options;
|
||||||
|
const startSize = getComputedSize(target, prop);
|
||||||
|
|
||||||
|
// Set the body cursor to that on the handle, so that it doesn't jump to different shapes as
|
||||||
|
// the mouse moves temporarily outside of the handle. (Approach from jqueryui-resizable.)
|
||||||
|
const startBodyCursor = document.body.style.cursor;
|
||||||
|
document.body.style.cursor = window.getComputedStyle(handle).cursor;
|
||||||
|
|
||||||
|
handle.classList.add(cssResizeDragging.className);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// While moving, just adjust the size of the target, relying on min-width/max-width for
|
||||||
|
// constraints.
|
||||||
|
onMove(ev: MouseEvent) {
|
||||||
|
target.style[prop] = (startSize + sign * (ev.pageX - startEv.pageX)) + 'px';
|
||||||
|
if (onDrag) { onDrag(getComputedSize(target, prop)); }
|
||||||
|
},
|
||||||
|
|
||||||
|
// At the end, undo temporary changes, and call onSave() with the actual size.
|
||||||
|
onStop(ev: MouseEvent) {
|
||||||
|
handle.classList.remove(cssResizeDragging.className);
|
||||||
|
|
||||||
|
// Restore the body cursor to what it was.
|
||||||
|
document.body.style.cursor = startBodyCursor;
|
||||||
|
|
||||||
|
target.style[prop] = (startSize + sign * (ev.pageX - startEv.pageX)) + 'px';
|
||||||
|
onSave(getComputedSize(target, prop));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Compute the CSS width or height of the element. If element.style[prop] is set to it, it should
|
||||||
|
// be unchanged. (Note that when an element has borders or padding, the size from
|
||||||
|
// getBoundingClientRect() would be different, and isn't suitable for style[prop].)
|
||||||
|
function getComputedSize(elem: Element, prop: 'width'|'height'): number {
|
||||||
|
const sizePx = window.getComputedStyle(elem)[prop];
|
||||||
|
const sizeNum = sizePx && parseFloat(sizePx);
|
||||||
|
// If we can't get the size, fall back to getBoundingClientRect().
|
||||||
|
return Number.isFinite(sizeNum as number) ? sizeNum as number : elem.getBoundingClientRect()[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssResizeFlexVHandle = styled('div', `
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
cursor: ew-resize;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
/* width with negative margins leaves exactly 1px line in the middle */
|
||||||
|
width: 7px;
|
||||||
|
margin: auto -3px;
|
||||||
|
|
||||||
|
/* two highlighted 1px lines are placed in the middle, normal and highlighted one */
|
||||||
|
&::before, &::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
left: 3px;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
background-color: var(--resize-handle-color, lightgrey);
|
||||||
|
}
|
||||||
|
/* the highlighted line is shown on hover with opacity transition */
|
||||||
|
&::after {
|
||||||
|
background-color: var(--resize-handle-highlight, black);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity linear 0.2s;
|
||||||
|
}
|
||||||
|
&:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity linear 0.2s 0.2s;
|
||||||
|
}
|
||||||
|
/* the highlighted line is also always shown while dragging */
|
||||||
|
&-dragging::after {
|
||||||
|
opacity: 1;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// May be applied to Handle class to show the highlighted line while dragging.
|
||||||
|
const cssResizeDragging = styled('div', `
|
||||||
|
&::after {
|
||||||
|
opacity: 1;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
`);
|
116
app/client/ui/transitions.ts
Normal file
116
app/client/ui/transitions.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* A helper for CSS transitions. Usage:
|
||||||
|
*
|
||||||
|
* dom(...,
|
||||||
|
* transition(obs, {
|
||||||
|
* prepare(elem, val) { SET STYLE WITH TRANSITIONS OFF },
|
||||||
|
* run(elem, val) { SET STYLE WITH TRANSITIONS ON },
|
||||||
|
* // finish(elem, val) { console.log("transition finished"); }
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* Allows modifiying styles in response to changes in an observable. Any time the observable
|
||||||
|
* changes, the prepare() callback allows preparing the styles, with transitions off. Then
|
||||||
|
* the run() callback can set the styles that will be subject to transitions.
|
||||||
|
*
|
||||||
|
* The actual transition styles (e.g. 'transition: width 0.2s') should be set on elem elsewhere.
|
||||||
|
*
|
||||||
|
* The optional finish() callback is called when the transition ends. If CSS transitions are set
|
||||||
|
* on multiple properties, only the first one is used to determine when the transition ends.
|
||||||
|
*
|
||||||
|
* All callbacks are called with the element this is attached to, and the value of the observable.
|
||||||
|
*
|
||||||
|
* The recommendation is to avoid setting styles at transition end, since it's not entirely
|
||||||
|
* reliable; it's better to arrange CSS so that the desired final styles can be set in run(). The
|
||||||
|
* finish() callback is intended to tell other code that the element is in its transitioned state.
|
||||||
|
*
|
||||||
|
* When the observable changes during a transition, the prepare() callback is skipped, the run()
|
||||||
|
* callback is called, and the finish() callback delayed until the new transition ends.
|
||||||
|
*
|
||||||
|
* If other styles are changed (or css classes applied) when the observable changes, subscriptions
|
||||||
|
* triggered BEFORE the transition() subscription are applied with transitions OFF (like
|
||||||
|
* prepare()); those triggered AFTER are subject to transitions (like run()).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {BindableValue, Disposable, dom, DomElementMethod, subscribeElem} from 'grainjs';
|
||||||
|
|
||||||
|
export interface ITransitionLogic<T = void> {
|
||||||
|
prepare(elem: HTMLElement, value: T): void;
|
||||||
|
run(elem: HTMLElement, value: T): void;
|
||||||
|
finish?(elem: HTMLElement, value: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transition<T>(obs: BindableValue<T>, trans: ITransitionLogic<T>): DomElementMethod {
|
||||||
|
const {prepare, run, finish} = trans;
|
||||||
|
let watcher: TransitionWatcher|null = null;
|
||||||
|
let firstCall = true;
|
||||||
|
return (elem) => subscribeElem<T>(elem, obs, (val) => {
|
||||||
|
// First call is initialization, don't treat it as a transition
|
||||||
|
if (firstCall) { firstCall = false; return; }
|
||||||
|
if (watcher) {
|
||||||
|
watcher.reschedule();
|
||||||
|
} else {
|
||||||
|
watcher = new TransitionWatcher(elem);
|
||||||
|
watcher.onDispose(() => {
|
||||||
|
watcher = null;
|
||||||
|
if (finish) { finish(elem, val); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call prepare() with transitions turned off.
|
||||||
|
const prior = elem.style.transitionProperty;
|
||||||
|
elem.style.transitionProperty = 'none';
|
||||||
|
prepare(elem, val);
|
||||||
|
|
||||||
|
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565
|
||||||
|
// for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
|
||||||
|
// here to trigger a style computation without a reflow.
|
||||||
|
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
|
||||||
|
|
||||||
|
// Restore transitions before run().
|
||||||
|
elem.style.transitionProperty = prior;
|
||||||
|
}
|
||||||
|
run(elem, val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for waiting for an active transition to end. Beyond listening to 'transitionend', it
|
||||||
|
* does a few things:
|
||||||
|
*
|
||||||
|
* (1) if the transition lists multiple properties, only the first property and duration are used
|
||||||
|
* ('transitionend' on additional properties is inconsistent across browsers).
|
||||||
|
* (2) if 'transitionend' fails to fire, the transition is considered ended when duration elapses,
|
||||||
|
* plus 10ms grace period (to let 'transitionend' fire first normally).
|
||||||
|
* (3) reschedule() allows resetting the timer if a new transition is known to have started.
|
||||||
|
*
|
||||||
|
* When the transition ends, TransitionWatcher disposes itself. Its onDispose() method allows
|
||||||
|
* registering callbacks.
|
||||||
|
*/
|
||||||
|
class TransitionWatcher extends Disposable {
|
||||||
|
private _propertyName: string;
|
||||||
|
private _durationMs: number;
|
||||||
|
private _timer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
constructor(elem: Element) {
|
||||||
|
super();
|
||||||
|
const style = window.getComputedStyle(elem);
|
||||||
|
this._propertyName = style.transitionProperty.split(",")[0].trim();
|
||||||
|
|
||||||
|
// Gets the duration of the transition from the styles of the given element, in ms.
|
||||||
|
// FF and Chrome both return transitionDuration in seconds (e.g. "0.150s") In case of multiple
|
||||||
|
// values, e.g. "0.150s, 2s"; parseFloat will just parse the first one.
|
||||||
|
const duration = style.transitionDuration;
|
||||||
|
this._durationMs = ((duration && parseFloat(duration)) || 0) * 1000;
|
||||||
|
|
||||||
|
this.autoDispose(dom.onElem(elem, 'transitionend', (e) =>
|
||||||
|
(e.propertyName === this._propertyName) && this.dispose()));
|
||||||
|
|
||||||
|
this._timer = setTimeout(() => this.dispose(), this._durationMs + 10);
|
||||||
|
this.onDispose(() => clearTimeout(this._timer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public reschedule() {
|
||||||
|
clearTimeout(this._timer);
|
||||||
|
this._timer = setTimeout(() => this.dispose(), this._durationMs + 10);
|
||||||
|
}
|
||||||
|
}
|
3
app/client/ui2018/IconList.ts
Normal file
3
app/client/ui2018/IconList.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// tslint:disable:max-line-length
|
||||||
|
export type IconName = "ChartArea" | "ChartBar" | "ChartKaplan" | "ChartLine" | "ChartPie" | "TypeCard" | "TypeCardList" | "TypeCell" | "TypeChart" | "TypeCustom" | "TypeDetails" | "TypeTable" | "FieldAny" | "FieldAttachment" | "FieldCheckbox" | "FieldChoice" | "FieldColumn" | "FieldDate" | "FieldDateTime" | "FieldFunction" | "FieldFunctionEqual" | "FieldInteger" | "FieldLink" | "FieldNumeric" | "FieldReference" | "FieldSpinner" | "FieldSwitcher" | "FieldTable" | "FieldText" | "FieldTextbox" | "FieldToggle" | "GristLogo" | "ThumbPreview" | "CenterAlign" | "Code" | "Collapse" | "Convert" | "CrossBig" | "CrossSmall" | "Dots" | "Download" | "DragDrop" | "Dropdown" | "DropdownUp" | "Expand" | "EyeHide" | "EyeShow" | "Feedback" | "Filter" | "FilterSimple" | "Folder" | "FunctionResult" | "Help" | "Home" | "Idea" | "Import" | "LeftAlign" | "Log" | "Mail" | "Minus" | "NewNotification" | "Notification" | "Offline" | "Page" | "PanelLeft" | "PanelRight" | "PinBig" | "PinSmall" | "Pivot" | "Plus" | "Redo" | "Remove" | "Repl" | "ResizePanel" | "RightAlign" | "Search" | "Settings" | "Share" | "Sort" | "Tick" | "Undo" | "Validation" | "Widget" | "Wrap" | "Zoom";
|
||||||
|
export const IconList: IconName[] = ["ChartArea", "ChartBar", "ChartKaplan", "ChartLine", "ChartPie", "TypeCard", "TypeCardList", "TypeCell", "TypeChart", "TypeCustom", "TypeDetails", "TypeTable", "FieldAny", "FieldAttachment", "FieldCheckbox", "FieldChoice", "FieldColumn", "FieldDate", "FieldDateTime", "FieldFunction", "FieldFunctionEqual", "FieldInteger", "FieldLink", "FieldNumeric", "FieldReference", "FieldSpinner", "FieldSwitcher", "FieldTable", "FieldText", "FieldTextbox", "FieldToggle", "GristLogo", "ThumbPreview", "CenterAlign", "Code", "Collapse", "Convert", "CrossBig", "CrossSmall", "Dots", "Download", "DragDrop", "Dropdown", "DropdownUp", "Expand", "EyeHide", "EyeShow", "Feedback", "Filter", "FilterSimple", "Folder", "FunctionResult", "Help", "Home", "Idea", "Import", "LeftAlign", "Log", "Mail", "Minus", "NewNotification", "Notification", "Offline", "Page", "PanelLeft", "PanelRight", "PinBig", "PinSmall", "Pivot", "Plus", "Redo", "Remove", "Repl", "ResizePanel", "RightAlign", "Search", "Settings", "Share", "Sort", "Tick", "Undo", "Validation", "Widget", "Wrap", "Zoom"];
|
163
app/client/ui2018/cssVars.ts
Normal file
163
app/client/ui2018/cssVars.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* CSS Variables. To use in your web appication, add `cssRootVars` to the class list for your app's
|
||||||
|
* root node, typically `<body>`.
|
||||||
|
*
|
||||||
|
* The fonts used attempt to default to system fonts as described here:
|
||||||
|
* https://css-tricks.com/snippets/css/system-font-stack/
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import {ProductFlavor} from 'app/common/gristUrls';
|
||||||
|
import {dom, makeTestId, styled, TestId} from 'grainjs';
|
||||||
|
import values = require('lodash/values');
|
||||||
|
|
||||||
|
const VAR_PREFIX = 'grist';
|
||||||
|
|
||||||
|
class CustomProp {
|
||||||
|
constructor(public name: string, public value: string) { }
|
||||||
|
|
||||||
|
public decl() {
|
||||||
|
return `--${VAR_PREFIX}-${this.name}: ${this.value};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString() {
|
||||||
|
return `var(--${VAR_PREFIX}-${this.name})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const colors = {
|
||||||
|
lightGrey: new CustomProp('color-light-grey', '#F7F7F7'),
|
||||||
|
mediumGrey: new CustomProp('color-medium-grey', 'rgba(217,217,217,0.6)'),
|
||||||
|
mediumGreyOpaque: new CustomProp('color-medium-grey-opaque', '#E8E8E8'),
|
||||||
|
darkGrey: new CustomProp('color-dark-grey', '#D9D9D9'),
|
||||||
|
|
||||||
|
light: new CustomProp('color-light', '#FFFFFF'),
|
||||||
|
dark: new CustomProp('color-dark', '#262633'),
|
||||||
|
darkBg: new CustomProp('color-dark-bg', '#262633'),
|
||||||
|
slate: new CustomProp('color-slate', '#929299'),
|
||||||
|
|
||||||
|
lightGreen: new CustomProp('color-light-green', '#16B378'),
|
||||||
|
darkGreen: new CustomProp('color-dark-green', '#009058'),
|
||||||
|
darkerGreen: new CustomProp('color-darker-green', '#007548'),
|
||||||
|
lighterGreen: new CustomProp('color-lighter-green', '#b1ffe2'),
|
||||||
|
|
||||||
|
cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen
|
||||||
|
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
|
||||||
|
inactiveCursor: new CustomProp('color-inactive-cursor', '#A2E1C9'),
|
||||||
|
|
||||||
|
hover: new CustomProp('color-hover', '#bfbfbf'),
|
||||||
|
error: new CustomProp('color-error', '#D0021B'),
|
||||||
|
backdrop: new CustomProp('color-backdrop', 'rgba(38,38,51,0.9)')
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export const vars = {
|
||||||
|
/* Fonts */
|
||||||
|
fontFamily: new CustomProp('font-family', `-apple-system,BlinkMacSystemFont,Segoe UI,
|
||||||
|
Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol`),
|
||||||
|
|
||||||
|
// This is more monospace and looks better for data that should often align (e.g. to have 00000
|
||||||
|
// take similar space to 11111). This is the main font for user data.
|
||||||
|
fontFamilyData: new CustomProp('font-family-data',
|
||||||
|
`Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol`),
|
||||||
|
|
||||||
|
/* Font sizes */
|
||||||
|
xxsmallFontSize: new CustomProp('xx-font-size', '8px'),
|
||||||
|
xsmallFontSize: new CustomProp('x-small-font-size', '10px'),
|
||||||
|
smallFontSize: new CustomProp('small-font-size', '11px'),
|
||||||
|
mediumFontSize: new CustomProp('medium-font-size', '13px'),
|
||||||
|
largeFontSize: new CustomProp('large-font-size', '16px'),
|
||||||
|
xlargeFontSize: new CustomProp('x-large-font-size', '18px'),
|
||||||
|
xxlargeFontSize: new CustomProp('xx-large-font-size', '20px'),
|
||||||
|
xxxlargeFontSize: new CustomProp('xxx-large-font-size', '22px'),
|
||||||
|
|
||||||
|
/* Controls size and space */
|
||||||
|
controlFontSize: new CustomProp('control-font-size', '12px'),
|
||||||
|
smallControlFontSize: new CustomProp('small-control-font-size', '10px'),
|
||||||
|
bigControlFontSize: new CustomProp('big-control-font-size', '13px'),
|
||||||
|
headerControlFontSize: new CustomProp('header-control-font-size', '22px'),
|
||||||
|
bigControlTextWeight: new CustomProp('big-text-weight', '500'),
|
||||||
|
headerControlTextWeight: new CustomProp('header-text-weight', '600'),
|
||||||
|
|
||||||
|
/* Labels */
|
||||||
|
labelTextSize: new CustomProp('label-text-size', 'medium'),
|
||||||
|
labelTextBg: new CustomProp('label-text-bg', '#FFFFFF'),
|
||||||
|
labelActiveBg: new CustomProp('label-active-bg', '#F0F0F0'),
|
||||||
|
|
||||||
|
controlMargin: new CustomProp('normal-margin', '2px'),
|
||||||
|
controlPadding: new CustomProp('normal-padding', '3px 5px'),
|
||||||
|
tightPadding: new CustomProp('tight-padding', '1px 2px'),
|
||||||
|
loosePadding: new CustomProp('loose-padding', '5px 15px'),
|
||||||
|
|
||||||
|
/* Control colors and borders */
|
||||||
|
primaryBg: new CustomProp('primary-fg', '#16B378'),
|
||||||
|
primaryBgHover: new CustomProp('primary-fg-hover', '#009058'),
|
||||||
|
primaryFg: new CustomProp('primary-bg', '#ffffff'),
|
||||||
|
|
||||||
|
controlBg: new CustomProp('control-bg', '#ffffff'),
|
||||||
|
controlFg: new CustomProp('control-fg', '#16B378'),
|
||||||
|
controlFgHover: new CustomProp('primary-fg-hover', '#009058'),
|
||||||
|
|
||||||
|
controlBorder: new CustomProp('control-border', '1px solid #11B683'),
|
||||||
|
controlBorderRadius: new CustomProp('border-radius', '4px'),
|
||||||
|
|
||||||
|
logoBg: new CustomProp('logo-bg', '#040404'),
|
||||||
|
toastBg: new CustomProp('toast-bg', '#040404'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssColors = values(colors).map(v => v.decl()).join('\n');
|
||||||
|
const cssVars = values(vars).map(v => v.decl()).join('\n');
|
||||||
|
const cssFontParams = `
|
||||||
|
font-family: ${vars.fontFamily};
|
||||||
|
font-size: ${vars.mediumFontSize};
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// We set box-sizing globally to match bootstrap's setting of border-box, since we are integrating
|
||||||
|
// into an app which already has it set, and it's impossible to make things look consistently with
|
||||||
|
// AND without it. This duplicates bootstrap's setting.
|
||||||
|
const cssBorderBox = `
|
||||||
|
*, *:before, *:after {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// These styles duplicate bootstrap's global settings, which we rely on even on pages that don't
|
||||||
|
// have bootstrap.
|
||||||
|
const cssInputFonts = `
|
||||||
|
button, input, select, textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const cssVarsOnly = styled('div', cssColors + cssVars);
|
||||||
|
const cssBodyVars = styled('div', cssFontParams + cssColors + cssVars + cssBorderBox + cssInputFonts);
|
||||||
|
|
||||||
|
const cssBody = styled('body', `
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssRoot = styled('html', `
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const cssRootVars = cssBodyVars.className;
|
||||||
|
|
||||||
|
// Also make a globally available testId, with a simple "test-" prefix (i.e. in tests, query css
|
||||||
|
// class ".test-{name}". Ideally, we'd use noTestId() instead in production.
|
||||||
|
export const testId: TestId = makeTestId('test-');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the global css properties to the document's root to them available in the page.
|
||||||
|
*/
|
||||||
|
export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolean = false) {
|
||||||
|
dom.update(document.documentElement!, varsOnly ? dom.cls(cssVarsOnly.className) : dom.cls(cssRootVars));
|
||||||
|
document.documentElement!.classList.add(cssRoot.className);
|
||||||
|
document.body.classList.add(cssBody.className);
|
||||||
|
}
|
75
app/client/ui2018/icons.ts
Normal file
75
app/client/ui2018/icons.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Exports a single `icon` function which returns a DOM Element for the given icon name.
|
||||||
|
* Names are of type `IconName` imported from `IconList.ts`, which is auto-generated during the
|
||||||
|
* build process.
|
||||||
|
*
|
||||||
|
* In order to use the icons, you must first include the generated CSS file:
|
||||||
|
*
|
||||||
|
* <link rel="stylesheet" href="icons.css">
|
||||||
|
*
|
||||||
|
* The CSS file encodes each icon as a `url()` with base64 DataURI of the icon SVG and saves it
|
||||||
|
* as a CSS :root var of the form --icon-${name}. It also includes a class for each icon that
|
||||||
|
* uses the variable to set the mask-image (vs background-image, allowing you to change colors
|
||||||
|
* using background-color):
|
||||||
|
*
|
||||||
|
* .icon.Search_icon { -webkit-mask-image: var(--icon-Search); }
|
||||||
|
*
|
||||||
|
* This approach is more performant than inlining SVGs or using <symbol> with <use>.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* // Display icon with default color and size
|
||||||
|
* dom('div',
|
||||||
|
* icon('Search')
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // Display bigger icon in blue
|
||||||
|
* const bigBlueIcon = styled(icon, `
|
||||||
|
* background-color: blue;
|
||||||
|
* width: 32px;
|
||||||
|
* height: 32px;
|
||||||
|
* `);
|
||||||
|
* dom('div',
|
||||||
|
* bigBlueIcon('Search')
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // Use icon image directly in css to style a checkbox
|
||||||
|
* // Refer to https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Advanced_styling_for_HTML_forms
|
||||||
|
* const checkbox = styled('input#checkbox', `
|
||||||
|
* -webkit-appearance: none;
|
||||||
|
* -moz-appearance: none;
|
||||||
|
* width: 1rem;
|
||||||
|
* height: 1rem;
|
||||||
|
* border: 1px solid blue;
|
||||||
|
*
|
||||||
|
* &:checked::before {
|
||||||
|
* position: absolute;
|
||||||
|
* content: var(--icon-Select);
|
||||||
|
* }
|
||||||
|
* `);
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dom, DomElementArg, styled } from 'grainjs';
|
||||||
|
import { IconName } from './IconList';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults for all icons.
|
||||||
|
*/
|
||||||
|
const iconDiv = styled('div', `
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-position: center;
|
||||||
|
-webkit-mask-size: contain;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: var(--icon-color, black);
|
||||||
|
`);
|
||||||
|
|
||||||
|
export function icon(name: IconName, ...domArgs: DomElementArg[]): HTMLElement {
|
||||||
|
return iconDiv(
|
||||||
|
dom.style('-webkit-mask-image', `var(--icon-${name})`),
|
||||||
|
...domArgs
|
||||||
|
);
|
||||||
|
}
|
1
app/common/gristUrls.ts
Normal file
1
app/common/gristUrls.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type ProductFlavor = 'grist';
|
6
app/common/tsconfig.json
Normal file
6
app/common/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../buildtools/tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../build/app/common",
|
||||||
|
}
|
||||||
|
}
|
24
app/server/server.ts
Normal file
24
app/server/server.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as express from 'express';
|
||||||
|
import * as http from 'http';
|
||||||
|
import {AddressInfo} from 'net';
|
||||||
|
|
||||||
|
const G = {
|
||||||
|
port: parseInt(process.env.PORT!, 10) || 8484,
|
||||||
|
host: process.env.HOST || 'localhost',
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
app.use(express.static('static'));
|
||||||
|
|
||||||
|
// Start listening.
|
||||||
|
await new Promise((resolve, reject) => server.listen(G.port, G.host, resolve).on('error', reject));
|
||||||
|
const address = server.address() as AddressInfo;
|
||||||
|
console.warn(`Server listening at http://${address.address}:${address.port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main().catch((err) => console.error(err));
|
||||||
|
}
|
9
app/server/tsconfig.json
Normal file
9
app/server/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../buildtools/tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../build/app/server",
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{ "path": "../common" }
|
||||||
|
]
|
||||||
|
}
|
9
app/tsconfig.json
Normal file
9
app/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./client" },
|
||||||
|
{ "path": "./server" },
|
||||||
|
{ "path": "./common" },
|
||||||
|
]
|
||||||
|
}
|
20
buildtools/tsconfig-base.json
Normal file
20
buildtools/tsconfig-base.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"baseUrl": "..",
|
||||||
|
"composite": true,
|
||||||
|
"plugins": [{
|
||||||
|
"name": "typescript-tslint-plugin"
|
||||||
|
}],
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"jsx": "react"
|
||||||
|
}
|
||||||
|
}
|
56
buildtools/webpack.config.js
Normal file
56
buildtools/webpack.config.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const StatsPlugin = require('stats-webpack-plugin');
|
||||||
|
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
target: 'web',
|
||||||
|
entry: {
|
||||||
|
main: "./build/app/client/ui/PagePanels.js",
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: "[name].bundle.js",
|
||||||
|
sourceMapFilename: "[file].map",
|
||||||
|
path: path.resolve("./static"),
|
||||||
|
// Workaround for a known issue with webpack + onerror under chrome, see:
|
||||||
|
// https://github.com/webpack/webpack/issues/5681
|
||||||
|
// "We use a source map plugin here with this special configuration
|
||||||
|
// because if we do not - the window.onerror function does not work properly in chrome
|
||||||
|
// and it swallows the errors because normally source maps have begin with webpack:///
|
||||||
|
// here we are changing how the module file names are created
|
||||||
|
// See this bug
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=765909
|
||||||
|
// See this for syntax
|
||||||
|
// https://webpack.js.org/configuration/output/#output-devtoolmodulefilenametemplate
|
||||||
|
// "
|
||||||
|
devtoolModuleFilenameTemplate: "[resourcePath]?[loaders]",
|
||||||
|
crossOriginLoading: "anonymous",
|
||||||
|
},
|
||||||
|
// This creates .map files, and takes webpack a couple of seconds to rebuild while developing,
|
||||||
|
// but provides correct mapping back to typescript, and allows breakpoints to be set in
|
||||||
|
// typescript ("cheap-module-eval-source-map" is faster, but breakpoints are largely broken).
|
||||||
|
devtool: "source-map",
|
||||||
|
resolve: {
|
||||||
|
modules: [
|
||||||
|
path.resolve('./build'),
|
||||||
|
path.resolve('./node_modules')
|
||||||
|
],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{ test: /\.js$/,
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
enforce: "pre"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new StatsPlugin(
|
||||||
|
'../.build_stats_js_bundle', // relative to output folder
|
||||||
|
{source: false}, // Omit sources, which unnecessarily make the stats file huge.
|
||||||
|
),
|
||||||
|
// To strip all locales except “en”
|
||||||
|
new MomentLocalesPlugin()
|
||||||
|
],
|
||||||
|
};
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "grist-core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsc --build app -w --preserveWatchOutput & webpack --config buildtools/webpack.config.js --mode development --watch --hide-modules & nodemon -w build/app/server -w build/app/common build/app/server/server & wait",
|
||||||
|
"build:prod": "tsc --build app && webpack --config buildtools/webpack.config.js --mode production",
|
||||||
|
"start:prod": "node build/app/server/server"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.6",
|
||||||
|
"@types/lodash": "^4.14.151",
|
||||||
|
"@types/node": "10",
|
||||||
|
"moment-locales-webpack-plugin": "^1.2.0",
|
||||||
|
"nodemon": "^2.0.4",
|
||||||
|
"source-map-loader": "^0.2.4",
|
||||||
|
"stats-webpack-plugin": "^0.7.0",
|
||||||
|
"typescript": "^3.9.2",
|
||||||
|
"webpack": "^4.43.0",
|
||||||
|
"webpack-cli": "^3.3.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"grainjs": "^1.0.1",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"moment": "^2.25.3"
|
||||||
|
}
|
||||||
|
}
|
9
static/index.html
Normal file
9
static/index.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src='/main.bundle.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user