Initial config with a few files that build on client and server side.

This commit is contained in:
Dmitry S 2020-05-20 00:50:46 -04:00
commit ec182792be
20 changed files with 4762 additions and 0 deletions

68
.gitignore vendored Normal file
View 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
View 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
View 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); }
`);

View 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);
}
}
}

View 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;
}
`);

View 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);
}
}

View 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"];

View 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);
}

View 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
View File

@ -0,0 +1 @@
export type ProductFlavor = 'grist';

6
app/common/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "../../buildtools/tsconfig-base.json",
"compilerOptions": {
"outDir": "../../build/app/common",
}
}

24
app/server/server.ts Normal file
View 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
View File

@ -0,0 +1,9 @@
{
"extends": "../../buildtools/tsconfig-base.json",
"compilerOptions": {
"outDir": "../../build/app/server",
},
"references": [
{ "path": "../common" }
]
}

9
app/tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"files": [],
"include": [],
"references": [
{ "path": "./client" },
{ "path": "./server" },
{ "path": "./common" },
]
}

1
bin Symbolic link
View File

@ -0,0 +1 @@
node_modules/.bin

View 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"
}
}

View 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
View 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
View File

@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
</head>
<body>
<script src='/main.bundle.js'></script>
</body>
</html>

3767
yarn.lock Normal file

File diff suppressed because it is too large Load Diff