From 19b8103392156e8b546094c67c140894fb951526 Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Tue, 12 Nov 2024 22:08:15 +0100 Subject: [PATCH] add hightable-demo (#39) * move the app to apps/cli/ * fix ci * try to fix the CI * add hightable-demo * clearer titles * add missing favicon and update README also: use eslint configuration recommended by vite --- .../workflows/{ci_cli.yml => ci_apps_cli.yml} | 4 +- .github/workflows/ci_apps_hightable_demo.yml | 32 +++ ...ponents.yml => ci_packages_components.yml} | 4 +- apps/hightable-demo/.gitignore | 24 ++ apps/hightable-demo/README.md | 18 ++ apps/hightable-demo/eslint.config.js | 40 ++++ apps/hightable-demo/index.html | 19 ++ apps/hightable-demo/package.json | 31 +++ apps/hightable-demo/public/favicon.png | Bin 0 -> 1061 bytes apps/hightable-demo/src/HighTable.css | 215 ++++++++++++++++++ apps/hightable-demo/src/data.tsx | 41 ++++ apps/hightable-demo/src/hightable.svg | 5 + apps/hightable-demo/src/index.css | 56 +++++ apps/hightable-demo/src/logo.svg | 8 + apps/hightable-demo/src/main.tsx | 13 ++ apps/hightable-demo/src/vite-env.d.ts | 1 + apps/hightable-demo/tsconfig.json | 26 +++ apps/hightable-demo/vite.config.ts | 7 + package.json | 1 + 19 files changed, 541 insertions(+), 4 deletions(-) rename .github/workflows/{ci_cli.yml => ci_apps_cli.yml} (93%) create mode 100644 .github/workflows/ci_apps_hightable_demo.yml rename .github/workflows/{ci_components.yml => ci_packages_components.yml} (88%) create mode 100644 apps/hightable-demo/.gitignore create mode 100644 apps/hightable-demo/README.md create mode 100644 apps/hightable-demo/eslint.config.js create mode 100644 apps/hightable-demo/index.html create mode 100644 apps/hightable-demo/package.json create mode 100644 apps/hightable-demo/public/favicon.png create mode 100644 apps/hightable-demo/src/HighTable.css create mode 100644 apps/hightable-demo/src/data.tsx create mode 100644 apps/hightable-demo/src/hightable.svg create mode 100644 apps/hightable-demo/src/index.css create mode 100644 apps/hightable-demo/src/logo.svg create mode 100644 apps/hightable-demo/src/main.tsx create mode 100644 apps/hightable-demo/src/vite-env.d.ts create mode 100644 apps/hightable-demo/tsconfig.json create mode 100644 apps/hightable-demo/vite.config.ts diff --git a/.github/workflows/ci_cli.yml b/.github/workflows/ci_apps_cli.yml similarity index 93% rename from .github/workflows/ci_cli.yml rename to .github/workflows/ci_apps_cli.yml index 7de8eb4..94f737b 100644 --- a/.github/workflows/ci_cli.yml +++ b/.github/workflows/ci_apps_cli.yml @@ -1,10 +1,10 @@ -name: CI in the cli app +name: apps/cli on: push: paths: - 'packages/components/**' - 'apps/cli/**' - - '.github/workflows/ci_cli.yml' + - '.github/workflows/ci_apps_cli.yml' defaults: run: diff --git a/.github/workflows/ci_apps_hightable_demo.yml b/.github/workflows/ci_apps_hightable_demo.yml new file mode 100644 index 0000000..39f52cc --- /dev/null +++ b/.github/workflows/ci_apps_hightable_demo.yml @@ -0,0 +1,32 @@ +name: apps/hightable-demo +on: + push: + paths: + - 'apps/hightable-demo/**' + - '.github/workflows/ci_apps_hightable_demo.yml' + +defaults: + run: + working-directory: ./apps/hightable-demo + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm i + - run: npm run lint + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm i + - run: tsc + + buildcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm i + - run: npm run build \ No newline at end of file diff --git a/.github/workflows/ci_components.yml b/.github/workflows/ci_packages_components.yml similarity index 88% rename from .github/workflows/ci_components.yml rename to .github/workflows/ci_packages_components.yml index 279bc65..64289ed 100644 --- a/.github/workflows/ci_components.yml +++ b/.github/workflows/ci_packages_components.yml @@ -1,9 +1,9 @@ -name: CI in the components package +name: packages/components on: push: paths: - 'packages/components/**' - - '.github/workflows/ci_components.yml' + - '.github/workflows/ci_packages_components.yml' defaults: run: diff --git a/apps/hightable-demo/.gitignore b/apps/hightable-demo/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/hightable-demo/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/hightable-demo/README.md b/apps/hightable-demo/README.md new file mode 100644 index 0000000..02a4747 --- /dev/null +++ b/apps/hightable-demo/README.md @@ -0,0 +1,18 @@ +# HighTable demo + +This is an example project showing how to use [hightable](https://github.com/hyparam/hightable). + +## Build + +```bash +cd apps/hightable-demo +npm i +npm run build +``` + +The build artifacts will be stored in the `dist/` directory and can be served using any static server, eg. `http-server`: + +```bash +npm i -g http-server +http-server dist/ +``` diff --git a/apps/hightable-demo/eslint.config.js b/apps/hightable-demo/eslint.config.js new file mode 100644 index 0000000..f39d22c --- /dev/null +++ b/apps/hightable-demo/eslint.config.js @@ -0,0 +1,40 @@ +import js from '@eslint/js' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked, ...tseslint.configs.stylisticTypeChecked], + // Set the react version + settings: { react: { version: '18.3' } }, + files: ['src/**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + + '@typescript-eslint/restrict-template-expressions': 'off', + }, + }, +) diff --git a/apps/hightable-demo/index.html b/apps/hightable-demo/index.html new file mode 100644 index 0000000..d2d5102 --- /dev/null +++ b/apps/hightable-demo/index.html @@ -0,0 +1,19 @@ + + + + + HighTable Viewer Demo + + + + + + +
+ + + diff --git a/apps/hightable-demo/package.json b/apps/hightable-demo/package.json new file mode 100644 index 0000000..3ba0fad --- /dev/null +++ b/apps/hightable-demo/package.json @@ -0,0 +1,31 @@ +{ + "name": "hightable-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "hightable": "0.7.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^9.13.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.11.0", + "vite": "^5.4.10" + } +} diff --git a/apps/hightable-demo/public/favicon.png b/apps/hightable-demo/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd162fdd55d56df5257e4223716cec44488f8036 GIT binary patch literal 1061 zcmV+=1ls$FP)99=+5==ikro#WncqZT9)xCUUK z^b-n-&2rer*BhQ~4$q7Dqe}d?q#M zI^8>>K-H|XAL@ldL}P@#FAFT+HhpzZ0XxNIIH!shGU;yDq!#_`oB~=xM{a44O^f1s zvZK)+H{A>Q1850dx*)r%0ro6yqkQP-JC%GWT_2(`f>Fz`hPw8w)9oBYngXg)RE4ay z>l2p(5fvQGFPn(Qs;nYq`~2VemDO>)oPX?lkyR#xSC;}>LPsHgz^)G~tK)x1ah%}o zbXTb`N+_UkprTAse5F7uS^%c(P}TRLw&z=8GO~HwHp+*tWG!$2$jYXQ#niw7U|VJD zv@Ms@vlZW0n0y6*qa=P?paAHGg#d&AgaCv9gaCv9gaCv9gaB~n_xHtQ`q~VD^Jb22 z7B~Gi#wzli*t)Vk-fEG=?LLCi?QzQ?<`i8QSNL}U;V0a$C- zqD_l#03ow!V!e4ox(HfFO5#qf+d%knX24!WW}Xj<(;vHWtpY@JW@UpTEegds7_|(B z;t|i%oq0kmWd-hdaFQ*9Tv`CY_U^n=TCA*&bJKmz3Y>j(R(c?0(IUd7MJKW;vuR?b zyY%5{S0h0py0Nmsu|8yT(=cioj85_|hDBrKNxFMxEpCA|wRRUIo;~>}%x^z^y+xw> z!h~PTy2>t0D4h&(Y0-h%=^mUzifyu?H!n>!0ix7l@Sa|q>-me5erH4AuFeKZHhXS4 f>Q?N--Ri?{OcU{xVzL%x00000NkvXXu0mjf^?mGD literal 0 HcmV?d00001 diff --git a/apps/hightable-demo/src/HighTable.css b/apps/hightable-demo/src/HighTable.css new file mode 100644 index 0000000..aa54220 --- /dev/null +++ b/apps/hightable-demo/src/HighTable.css @@ -0,0 +1,215 @@ +.table-container { + display: flex; + flex: 1; + min-height: 0; + position: relative; +} + +.table-container * { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +.table-scroll { + flex: 1; + overflow: auto; +} +.table-scroll > div { + position: relative; +} +.table-scroll .table { + position: absolute; +} + +.table { + border-collapse: separate; + border-spacing: 0; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + max-width: 100%; + overflow-x: auto; +} +.table:focus-visible { + outline: none; +} + +/* header */ +.table thead th { + background-color: #eaeaeb; + border: none; + border-bottom: 2px solid #c9c9c9; + box-sizing: content-box; + color: #444; + height: 20px; + padding-top: 8px; + position: sticky; + top: -1px; /* fix 1px gap above thead */ + user-select: none; + z-index: 10; +} +.table thead th:first-child { + border: none; +} +.table thead th:first-child span { + cursor: default; + width: 0; +} +.table tbody tr:first-child td { + border-top: 1px solid transparent; +} + +/* sortable */ +.table.sortable thead th { + cursor: pointer; +} +.table thead th.orderby ::after { + position: absolute; + right: 8px; + top: 8px; + padding-left: 2px; + background-color: #eaeaeb; + content: "▾"; +} + +/* column resize */ +.table thead span { + position: absolute; + border-right: 1px solid #ddd; + top: 0; + right: 0; + bottom: 0; + width: 8px; + cursor: col-resize; + transition: background-color 0.2s ease; +} +.table thead span:hover { + background-color: #aab; +} + +/* row numbers */ +.table td:first-child { + background-color: #eaeaeb; + border-right: 1px solid #ddd; + color: #888; + font-size: 10px; + padding: 0 2px; + position: sticky; + left: 0; + text-align: center; + user-select: none; + min-width: 32px; + max-width: none; + width: 32px; +} + +/* cells */ +.table th, +.table td { + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + height: 32px; + max-width: 2000px; /* prevent columns expanding */ + padding: 4px 12px; + text-align: left; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +/* pending cell state */ +.table td.pending { + position: relative; +} +.table td.pending::after { + content: ''; + position: absolute; + top: 8px; + left: 8px; + right: 8px; + bottom: 8px; + border-radius: 4px; + background: linear-gradient( + 60deg, + rgba(0, 0, 0, 0.05) 25%, + rgba(0, 0, 0, 0.08) 50%, + rgba(0, 0, 0, 0.05) 75% + ); + background-size: 120px 100%; + animation: textshimmer 3s infinite linear; +} +/* stagger row shimmering */ +.table tr:nth-child(2n) td.pending::after { animation-delay: -1s; } +.table tr:nth-child(2n+1) td.pending::after { animation-delay: -3s; } +.table tr:nth-child(3n) td.pending::after { animation-delay: -2s; } +.table tr:nth-child(5n) td.pending::after { animation-delay: -4s; } +.table tr:nth-child(7n) td.pending::after { animation-delay: -1.5s; } +@keyframes textshimmer { + 0% { + background-position: -120px 0; + } + 100% { + background-position: 120px 0; + } +} + +/* pending table state */ +.table th::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 4px; + background-color: #706fb1; + z-index: 100; +} +.pending .table th::before { + animation: shimmer 2s infinite linear; +} +@keyframes shimmer { + 0%, 100% { background-color: #6fb176; } + 50% { background-color: #adc6b0; } +} + +/* don't hover on mobile */ +@media (hover: hover) { + .table tbody tr:hover { + background-color: #dbdbe5; + } + .table tbody tr:hover td { + border-right-color: #bbb; + } + .table tbody tr:hover td:first-child { + background-color: #ccd; + } +} + +/* row error */ +.table tr[title] { + color: #a11; +} + +/* table corner */ +.table-corner { + background-color: #e4e4e6; + border-right: 1px solid #ccc; + position: absolute; + height: 33px; + width: 32px; + top: 0; + left: 0; + z-index: 15; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2); +} +/* mock row numbers */ +.mock-row-label { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + background: #eaeaeb; + z-index: -10; +} diff --git a/apps/hightable-demo/src/data.tsx b/apps/hightable-demo/src/data.tsx new file mode 100644 index 0000000..496f6fa --- /dev/null +++ b/apps/hightable-demo/src/data.tsx @@ -0,0 +1,41 @@ +import { rowCache, sortableDataFrame, wrapPromise } from 'hightable' + +function lorem(rand: number, length: number): string { + const words = 'lorem ipsum dolor sit amet consectetur adipiscing elit'.split(' ') + const str = Array.from({ length }, (_, i) => words[Math.floor(i + rand * 8) % 8]).join(' ') + return str[0].toUpperCase() + str.slice(1) +} + +function delay(value: T, ms: number): Promise { + return new Promise(resolve => setTimeout(() => { resolve(value);}, ms)) +} + +const header = ['ID', 'Name', 'Age', 'UUID', 'Text', 'JSON'] +const mockData = { + header, + numRows: 10000, + rows(start: number, end: number) { + const arr: Record>>[] = [] + for (let i = start; i < end; i++) { + const rand = Math.abs(Math.sin(i + 1)) + const uuid = rand.toString(16).substring(2) + const partial = { + ID: i + 1, + Name: `Name${i}`, + Age: 20 + i % 80, + UUID: uuid, + Text: lorem(rand, 100), + } + const row = { ...partial, JSON: JSON.stringify(partial) } + // Map to randomly delayed promises + const promised = Object.fromEntries(Object.entries(row).map(([key, value]) => + // discrete time delay for each cell to simulate async data loading + [key, wrapPromise(delay(value, 100 * Math.floor(10 * Math.random())))], + )) + arr.push(promised) + } + return arr + }, +} + +export const data = rowCache(sortableDataFrame(mockData)) diff --git a/apps/hightable-demo/src/hightable.svg b/apps/hightable-demo/src/hightable.svg new file mode 100644 index 0000000..927446a --- /dev/null +++ b/apps/hightable-demo/src/hightable.svg @@ -0,0 +1,5 @@ + + + diff --git a/apps/hightable-demo/src/index.css b/apps/hightable-demo/src/index.css new file mode 100644 index 0000000..1e9c39a --- /dev/null +++ b/apps/hightable-demo/src/index.css @@ -0,0 +1,56 @@ +body { + display: flex; + font-family: 'Mulish', 'Helvetica Neue', Helvetica, Arial, sans-serif; + margin: 0; + padding: 0; +} + +/* sidebar */ +nav { + height: 100vh; + min-width: 48px; + background-image: linear-gradient(to bottom, #667, #585669); + box-shadow: 0 0 4px rgba(10, 10, 10, 0.5); + height: 100vh; +} + +/* brand logo */ +.brand { + color: #fff; + display: flex; + align-items: center; + filter: drop-shadow(0 0 2px #444); + font-family: 'Century Gothic', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1.1em; + font-weight: bold; + text-orientation: mixed; + opacity: 0.85; + padding: 10px 12px; + user-select: none; + writing-mode: vertical-rl; + text-decoration: none; +} +.brand:hover { + color: #fff; + filter: drop-shadow(0 0 2px #333); + opacity: 0.9; + text-decoration: none; +} +.brand::before { + content: ''; + background: url(logo.svg) no-repeat 0 center; + background-size: 26px; + height: 26px; + width: 26px; + margin-bottom: 10px; +} + +#app { + height: 100vh; + display: flex; + flex: 1; +} + +.table-corner { + background: url('hightable.svg') #e4e4e6 no-repeat center 6px; +} diff --git a/apps/hightable-demo/src/logo.svg b/apps/hightable-demo/src/logo.svg new file mode 100644 index 0000000..90903e2 --- /dev/null +++ b/apps/hightable-demo/src/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/hightable-demo/src/main.tsx b/apps/hightable-demo/src/main.tsx new file mode 100644 index 0000000..e90e4d7 --- /dev/null +++ b/apps/hightable-demo/src/main.tsx @@ -0,0 +1,13 @@ +import { HighTable } from 'hightable' +import { StrictMode } from 'react' +import ReactDOM from 'react-dom/client' +import { data } from './data' +import './HighTable.css' +import './index.css' + +const app = document.getElementById('app') +if (!app) throw new Error('missing app element') + +ReactDOM.createRoot(app).render( + +) diff --git a/apps/hightable-demo/src/vite-env.d.ts b/apps/hightable-demo/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/hightable-demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/hightable-demo/tsconfig.json b/apps/hightable-demo/tsconfig.json new file mode 100644 index 0000000..243bf41 --- /dev/null +++ b/apps/hightable-demo/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/hightable-demo/vite.config.ts b/apps/hightable-demo/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/apps/hightable-demo/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/package.json b/package.json index 03a38a9..ec931f1 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "license": "MIT", "workspaces": [ "apps/cli", + "apps/hightable-demo", "packages/components" ] }