From f6a81b2a5a38829c6a41a48b8ea608e481307c48 Mon Sep 17 00:00:00 2001 From: krulod Date: Mon, 9 Oct 2023 16:27:57 +0300 Subject: [PATCH] feat(logger): devtools draft --- package-lock.json | 30 +++ packages/logger/README.md | 13 +- packages/logger/package.json | 5 + .../logger/src/{devtools.tsx => devtools.ts} | 7 +- packages/logger/src/graphView.ts | 79 ------- packages/logger/src/index.ts | 16 +- packages/logger/src/rld-filter-model.ts | 140 ++++++++++++ packages/logger/src/rld-filter.ts | 148 +++++++++++++ packages/logger/src/rld-inspect-model.ts | 48 +++++ packages/logger/src/rld-inspect.ts | 203 ++++++++++++++++++ packages/logger/src/rld-model.ts | 11 + packages/logger/src/rld.ts | 141 ++++++++++++ packages/logger/src/rld.tsx | 5 - packages/logger/src/sandbox.tsx | 3 + packages/logger/src/vite-env.d.ts | 1 + packages/logger/tsconfig.json | 1 + 16 files changed, 745 insertions(+), 106 deletions(-) rename packages/logger/src/{devtools.tsx => devtools.ts} (72%) delete mode 100644 packages/logger/src/graphView.ts create mode 100644 packages/logger/src/rld-filter-model.ts create mode 100644 packages/logger/src/rld-filter.ts create mode 100644 packages/logger/src/rld-inspect-model.ts create mode 100644 packages/logger/src/rld-inspect.ts create mode 100644 packages/logger/src/rld-model.ts create mode 100644 packages/logger/src/rld.ts delete mode 100644 packages/logger/src/rld.tsx create mode 100644 packages/logger/src/vite-env.d.ts diff --git a/package-lock.json b/package-lock.json index 03de35c2e..902ef39e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3030,6 +3030,14 @@ "node": ">= 8" } }, + "node_modules/@observablehq/inspector": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@observablehq/inspector/-/inspector-5.0.0.tgz", + "integrity": "sha512-Vvg/TQdsZTUaeYbH0IKxYEz37FbRO6kdowoz2PrHLQif54NC1CjEihEjg+ZMSBn587GQxTFABu0CGkFZgtR1UQ==", + "dependencies": { + "isoformat": "^0.2.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6308,6 +6316,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isoformat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz", + "integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==" + }, "node_modules/jackspeak": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz", @@ -10908,8 +10921,13 @@ "version": "3.6.0", "license": "MIT", "dependencies": { + "@observablehq/inspector": "^5.0.0", "@reatom/core": "^3.1.0", "@reatom/jsx": "^3.4.0", + "@reatom/lens": "^3.6.2", + "@reatom/persist": "^3.3.0", + "@reatom/persist-web-storage": "^3.2.3", + "@reatom/primitives": "^3.2.1", "stylerun": "^1.0.0" }, "devDependencies": { @@ -11072,6 +11090,18 @@ "@reatom/utils": "^3.4.0" } }, + "packages/ui": { + "name": "@reatom/ui", + "version": "3.4.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@reatom/core": ">=3.5.0", + "@reatom/lens": "^3.6.2", + "@reatom/utils": "^3.5.0", + "csstype": "^3.1.2" + } + }, "packages/undo": { "name": "@reatom/undo", "version": "3.3.1", diff --git a/packages/logger/README.md b/packages/logger/README.md index 1660b5f5f..c53d27a2f 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -9,14 +9,12 @@ All atoms and actions with names or without underscore logs automatically ```ts import { connectLogger, createLogBatched } from '@reatom/logger' -connectLogger(ctx) - -// OR - -connectLogger( +const disconnectLogger = connectLogger( ctx, // optional configuration { + // whether to connect devtools + devtools: false, // the length of the atom history (patches) to store history: 10, // `false` by default to made your logs short @@ -46,6 +44,11 @@ connectLogger( domain: '', }, ) + +// if using HMR +if (import.meta.hot) { + import.meta.hot.accept(disconnectLogger) +} ``` Every log record includes a number in the start of the name to fix autosorting keys in a console. diff --git a/packages/logger/package.json b/packages/logger/package.json index 8ad40a143..7acc549b6 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -26,8 +26,13 @@ "test:watch": "tsx watch src/index.test.ts" }, "dependencies": { + "@observablehq/inspector": "^5.0.0", "@reatom/core": "^3.1.0", "@reatom/jsx": "^3.4.0", + "@reatom/lens": "^3.6.2", + "@reatom/persist": "^3.3.0", + "@reatom/persist-web-storage": "^3.2.3", + "@reatom/primitives": "^3.2.1", "stylerun": "^1.0.0" }, "author": "artalar", diff --git a/packages/logger/src/devtools.tsx b/packages/logger/src/devtools.ts similarity index 72% rename from packages/logger/src/devtools.tsx rename to packages/logger/src/devtools.ts index 844f4ba6e..df9ac0312 100644 --- a/packages/logger/src/devtools.tsx +++ b/packages/logger/src/devtools.ts @@ -1,6 +1,7 @@ import { Ctx } from '@reatom/core' import { noop } from '@reatom/utils' -import { mount } from '@reatom/jsx' +import { h, hf, mount } from '@reatom/jsx' +import { Rld } from './rld' export function devtoolsCreate(app: Ctx) { if (typeof window === 'undefined') { @@ -11,10 +12,10 @@ export function devtoolsCreate(app: Ctx) { root.id = 'reatom-logger-devtools' document.body.appendChild(root) - mount(root, ) + mount(root, h(Rld, { app })) return () => { - // mount(root, <>) + mount(root, h(hf, {})) root.remove() } } diff --git a/packages/logger/src/graphView.ts b/packages/logger/src/graphView.ts deleted file mode 100644 index 610437a84..000000000 --- a/packages/logger/src/graphView.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { AtomCache, __root } from '@reatom/core' - -export const logGraph = (logsSet: Set) => { - const visited = new Set() - const checkCause = (patch: AtomCache) => { - if ( - patch.cause && - patch.cause.proto !== __root && - (!patch.cause.proto.name!.startsWith('_') || - !patch.cause.proto.name!.includes('._')) && - !logsSet.has(patch.cause) && - !visited.has(patch.cause) - ) { - checkCause(patch.cause) - visited.add(patch.cause) - } - } - for (const patch of logsSet) checkCause(patch) - const logs = [...logsSet] - const r = 10 - const xGap = r * 2 - const yGap = r * 3 - const maxDistance = logs.reduce( - (acc, patch, i) => - Math.max(acc, i - ((patch.cause && logs.indexOf(patch.cause)) ?? i)), - 0, - ) - const shiftRatio = maxDistance * xGap - const maxShift = Math.floor((maxDistance / logs.length) * shiftRatio) - - const x = maxShift + xGap - let y = yGap - let body = '' - let width = x - - for (const patch of logs) { - // if (!patch.cause) continue; - const { isAction, name } = patch.proto - const color = isAction - ? name!.endsWith('.onFulfill') - ? '#E6DC73' - : '#ffff80' - : '#151134' - body += `` - body += `${name}` - y += yGap - width = Math.max(width, x + name!.length * r) - } - - logs.forEach(({ cause }, idx) => { - if (!cause || cause.proto === __root || idx === 0) return - - const causeIdx = logs.indexOf(cause) - if (causeIdx < 0) return - const causeY = causeIdx * yGap + yGap - const shift = (idx - causeIdx) / logs.length - const shiftX = Math.floor(x - shift * shiftRatio - xGap / 2) - const shiftY = Math.floor((causeIdx + (idx - causeIdx) / 2) * yGap) + yGap - const idxY = idx * yGap + yGap - const lineX = Math.floor(x - xGap / 2) - - const start = `${lineX},${causeY}` - const middle = `${shiftX},${shiftY}` - const end = `${lineX},${idxY}` - - body += `` - }) - - const svg = `${body}` - - const dataUrl = `data:image/svg+xml,${encodeURIComponent(svg)}` - const bgUrl = `url(${dataUrl})` - console.log( - '%c ', - `font-size:${y}px; background: ${bgUrl} no-repeat; font-family: monospace;`, - ) -} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 65e199eb5..4dae024a7 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,7 +1,6 @@ import { AtomCache, AtomProto, Ctx, Fn, Rec, __root } from '@reatom/core' import { isShallowEqual, noop } from '@reatom/utils' -import { logGraph } from './graphView' -// import { devtoolsCreate } from './devtools' +import { devtoolsCreate } from './devtools' export interface unstable_ChangeMsg { newState?: any @@ -76,17 +75,6 @@ export const createLogBatched = ({ `Reatom ${domain}${length} transaction${length > 1 ? 's' : ''}`, ) - if (shouldLogGraph) { - logGraph( - new Set( - queue - .flatMap(({ changes }) => Object.values(changes)) - .sort((a, b) => a.time - b.time) - .map(({ patch }) => patch), - ), - ) - } - for (const { changes, time, error } of queue) { console.log( `%c ${time}`, @@ -179,7 +167,7 @@ export const connectLogger = ( let read: Fn<[AtomProto], undefined | AtomCache> ctx.get((r) => (read = r)) - const devtoolsDispose = /* devtools ? devtoolsCreate(ctx) : */ noop + const devtoolsDispose = devtools ? devtoolsCreate(ctx) : noop const ctxUnsubscribe = ctx.subscribe((logs, error) => { let i = -1 diff --git a/packages/logger/src/rld-filter-model.ts b/packages/logger/src/rld-filter-model.ts new file mode 100644 index 000000000..49e193c33 --- /dev/null +++ b/packages/logger/src/rld-filter-model.ts @@ -0,0 +1,140 @@ +import { atom, AtomCache, Ctx, action, AtomMut } from '@reatom/core' +import { parseAtoms } from '@reatom/lens' +import { WithPersistOptions } from '@reatom/persist' +import { + withSessionStorage, + withLocalStorage, +} from '@reatom/persist-web-storage' +import { reatomArray, ArrayAtom } from '@reatom/primitives' + +export type FilterColor = (typeof FilterColors)[number] +export const FilterColors = [ + 'red', + 'green', + 'blue', + 'yellow', + 'gray', + 'black', +] as const + +export type FilterAction = (typeof FilterActions)[number] +export const FilterActions = ['hide', ...FilterColors] as const + +export type Filter = { + code: AtomMut + action: AtomMut +} + +const filterMake = (code: string, action: FilterAction) => ({ + code: atom(code), + action: atom(action), +}) + +// FIXME https://t.me/reatom_ru/49316 +const filterPersistOptions: Partial> = { + toSnapshot: parseAtoms, + fromSnapshot: (ctx, snapshot = []) => + (snapshot as any[]).map((filter) => filterMake(filter.code, filter.action)), +} + +export const filtersSession = reatomArray( + [] as Filter[], + 'filtersSession', +).pipe( + withSessionStorage({ + key: 'rld/filtersSession', + ...filterPersistOptions, + }), +) + +export const filtersLocal = reatomArray([] as Filter[], 'rldFiltersLocal').pipe( + withLocalStorage({ + key: 'rld/filterLocal', + ...filterPersistOptions, + }), +) + +// TODO preserve order +export const filters = atom( + (ctx) => [...ctx.spy(filtersSession), ...ctx.spy(filtersLocal)], + 'filters', +) + +export const filterFunctions = atom((ctx) => { + return ctx.spy(filters).map((filter) => { + // TODO execute in a worker + return new Function( + 'log', + `var proto = log.proto; var name = proto.name; return ${ctx.spy( + filter.code, + )}`, + ) as (node: AtomCache) => unknown + }) +}, 'filterFunctions') + +const filterRun = action((ctx, node: AtomCache) => { + return ctx.get(filterFunctions).map((fn, i) => { + try { + if (fn(node)) return ctx.get(ctx.get(filters).at(i)!.action) + } catch (error) { + console.error(error) + // TODO report error in UI + } + }) +}) + +export const filterHide = action((ctx, node: AtomCache) => + filterRun(ctx, node).some((action) => action === 'hide'), +) + +export const filterColor = action( + (ctx, node: AtomCache) => + filterRun(ctx, node).find((action) => action !== 'hide') as + | FilterColor + | undefined, +) + +export function filterList(ctx: Ctx, filter: Filter) { + let list: ArrayAtom + + if (ctx.get(filtersSession).includes(filter)) list = filtersSession + else if (ctx.get(filtersLocal).includes(filter)) list = filtersLocal + else throw new Error('Unknown filter') + + return list +} + +export const filterSplice = action( + (ctx, filter: Filter, ...replaceWith: Filter[]) => { + const list = filterList(ctx, filter) + const index = ctx.get(list).indexOf(filter) + list(ctx, list.toSpliced(ctx, index, 1, ...replaceWith)) + }, + 'filterSplice', +) + +export const filterPersistSet = action( + (ctx, filter: Filter, persist: boolean) => { + const nextList = persist ? filtersLocal : filtersSession + filterSplice(ctx, filter) + nextList(ctx, (prev) => [...prev, filter]) + }, + 'filterPersistSet', +) + +export const draftCode = atom('', 'draftCode') +export const draftAction = atom('hide' as FilterAction, 'filterDraftAction') +export const draftPersist = atom(false, 'filterDraftPersist') +export const draftCreate = action((ctx) => { + const filter: Filter = { + code: atom(ctx.get(draftCode)), + action: atom(ctx.get(draftAction)), + } + + const list = ctx.get(draftPersist) ? filtersLocal : filtersSession + list(ctx, (prev) => [...prev, filter]) + + draftCode(ctx, '') + draftAction(ctx, 'hide') + draftPersist(ctx, false) +}, 'filterDraftCreate') diff --git a/packages/logger/src/rld-filter.ts b/packages/logger/src/rld-filter.ts new file mode 100644 index 000000000..7ae8792d3 --- /dev/null +++ b/packages/logger/src/rld-filter.ts @@ -0,0 +1,148 @@ +import { Action, Atom, AtomMaybe, Ctx, action, atom } from '@reatom/core' +import { h } from '@reatom/jsx' +import styled from 'stylerun' +import * as model from './rld-filter-model' +import { t } from './t' +import { match } from '@reatom/lens' +import { buttonStyles } from './rld' + +export function RldFilterMenu() { + return atom((ctx) => + t.div({ + ...menuStyles({}), + children: [ + RldFilterMenuDraft(), + match(ctx.get(model.filters).length, t.div(draftHrStyles({}))), + ...ctx.spy(model.filters).map((filter) => + FilterView({ + persist: atom((ctx) => + ctx.spy(model.filtersLocal).includes(filter), + ), + setPersist: action((ctx, next) => + model.filterPersistSet(ctx, filter, next), + ), + action: filter.action, + setAction: filter.action, + code: filter.code, + setCode: filter.code, + buttonLabel: 'remove', + buttonClicked: action((ctx) => model.filterSplice(ctx, filter)), + }), + ), + ], + }), + ) +} + +function RldFilterMenuDraft() { + return FilterView({ + action: model.draftAction, + setAction: model.draftAction, + code: model.draftCode, + setCode: model.draftCode, + persist: model.draftPersist, + setPersist: model.draftPersist, + buttonLabel: 'create', + buttonClicked: model.draftCreate, + }) +} + +function FilterView({ + persist, + setPersist, + action: actionAtom, + setAction, + code, + setCode, + buttonLabel, + buttonClicked, +}: { + persist: AtomMaybe + setPersist: (ctx: Ctx, next: boolean) => void + action: Atom + setAction: (ctx: Ctx, next: model.FilterAction) => void + code: AtomMaybe + setCode: (ctx: Ctx, next: string) => void + buttonLabel: string + buttonClicked: (ctx: Ctx) => void +}) { + return h('div', listItemStyles({}), [ + t.input({ + ...filterPersistStyles({}), + type: 'checkbox', + title: 'Persist filter?', + checked: persist, + oninput: action((ctx, event) => + setPersist(ctx, event.target.checked), + ) as any, + }), + FilterActionView({ + value: actionAtom, + setValue: action((ctx, action) => setAction(ctx, action)), + }), + t.input({ + ...filterCodeStyles({}), + type: 'string', + placeholder: 'filter code', + value: code, + size: 50, + // FIXME types + oninput: action((ctx, event) => setCode(ctx, event.target.value)) as any, + }), + t.button({ + ...buttonStyles({}), + onclick: action((ctx) => buttonClicked(ctx)), + children: [buttonLabel], + }), + ]) +} + +function FilterActionView({ + value, + setValue, +}: { + value: Atom + setValue: (ctx: Ctx, action: model.FilterAction) => void +}) { + return t.select({ + ...buttonStyles({}), + value, + onchange: action((ctx, event) => + setValue(ctx, event.target.value as model.FilterAction), + ) as any, + children: model.FilterActions.map((action) => + t.option({ + value: action, + selected: atom((ctx) => ctx.spy(value) === action), + children: [action], + }), + ), + }) +} + +const menuStyles = styled('')` + display: flex; + flex-direction: column; + gap: 0.25rem; +` + +const draftHrStyles = styled('')` + width: 100%; + border-bottom: 1px solid #c5c6de88; + margin: 0.25rem 0; +` + +const listItemStyles = styled('')` + display: flex; + gap: 0.25rem; +` + +const filterPersistStyles = styled('')`` + +const filterCodeStyles = styled('')` + font: var(--mono_fonts); +` + +const filterActionStyles = styled('')`` + +const filterButtonStyles = styled('')`` diff --git a/packages/logger/src/rld-inspect-model.ts b/packages/logger/src/rld-inspect-model.ts new file mode 100644 index 000000000..ed934be83 --- /dev/null +++ b/packages/logger/src/rld-inspect-model.ts @@ -0,0 +1,48 @@ +import { AtomCache, action, atom } from '@reatom/core' +import { caches } from './rld-model' +import { + FilterAction, + FilterColor, + filterColor, + filterHide, + filters, +} from './rld-filter-model' +import { parseAtoms } from '@reatom/lens' + +export type Log = { cache: AtomCache; color?: FilterColor } +export type LogGroup = Log | { hide: Log[] } + +export const logSelected = atom(null as Log | null, 'logSelected') +export const logSelect = action((ctx, log: Log) => { + if (ctx.get(logSelected) === log) { + logSelected(ctx, null) + return + } + logSelected(ctx, log) +}, 'logSelect') + +export const logGroups = atom((ctx) => { + const logs = ctx + .spy(caches) + .map((cache) => ({ cache, color: filterColor(ctx, cache) })) + const groups: LogGroup[] = [] + + // otherwise we lose reactivity because filters are used in actions + parseAtoms(ctx, filters) + + for (const log of logs) { + if (filterHide(ctx, log.cache)) { + let group = groups.at(-1) + if (!group || !('hide' in group)) { + group = { hide: [] } + groups.push(group) + } + // TODO colors for hidden logs + group.hide.push({ cache: log.cache }) + continue + } + groups.push(log) + } + + return groups +}) diff --git a/packages/logger/src/rld-inspect.ts b/packages/logger/src/rld-inspect.ts new file mode 100644 index 000000000..498120a59 --- /dev/null +++ b/packages/logger/src/rld-inspect.ts @@ -0,0 +1,203 @@ +import { match } from '@reatom/lens' +import * as model from './rld-inspect-model' +import { JSXElement } from '@reatom/jsx' +import { Atom, AtomCache, AtomMut, Ctx, action, atom } from '@reatom/core' +import styled from 'stylerun' +// @ts-expect-error +import { Inspector } from '@observablehq/inspector' +import { t } from './t' + +export function RldInspect() { + const height = atom(400, 'height') + const graphWidth = atom(300, 'graphWidth') + const stateWidth = atom(300, 'stateWidth') + + return t.div({ + ...inspectStyles({}), + children: [ + InspectView({ + height, + width: graphWidth, + children: [InspectGraph()], + }), + match( + model.logSelected, + InspectView({ + height, + width: stateWidth, + children: [InspectState()], + }), + t.div([]), + ), + ], + }) +} + +function InspectView({ + height, + width, + children, +}: { + height: AtomMut + width: Atom + resizeLeft?: (ctx: Ctx, to: number) => void + resizeRight?: (ctx: Ctx, to: number) => void + children: JSXElement[] +}) { + return atom((ctx) => + t.div({ + ...viewStyles({}), + children: [ + t.div({ + ...viewHandleTopStyles({}), + children: [ + t.div({ + ...viewHorStyles({}), + children: [ + t.div({ + ...viewContentStyles({}), + style: `width: ${ctx.spy(width)}px; height: ${ctx.spy( + height, + )}px;`, + children, + }), + ], + }), + ], + }), + ], + }), + ) +} + +function InspectGraph() { + return atom((ctx) => + t.div({ + ...graphNodesStyles({}), + children: ctx + .spy(model.logGroups) + .map((group) => + 'hide' in group + ? InspectGraphHidden({ hide: group.hide }) + : InspectGraphLog({ log: group }), + ), + }), + ) +} + +function InspectGraphLog({ log }: { log: model.Log }) { + return t.div({ + ...graphLogStyles({}), + role: 'button', + onclick: action((ctx) => model.logSelect(ctx, log)), + children: [ + t.span({ + ...graphLogMarkerStyles({} /* { color: log.color! } */), + style: `background-color: ${log.color!}`, + }), + t.span([log.cache.proto.name]), + ], + }) +} + +function InspectGraphHidden({ hide }: { hide: model.Log[] }) { + const opened = atom(false, 'opened') + return t.div({ + ...graphHiddenStyles({}), + children: [ + t.div({ + ...graphHiddenLabelStyles({}), + role: 'button', + onclick: action((ctx) => opened(ctx, (prev) => !prev)), + children: [`${hide.length} log${hide.length != 1 ? 's' : ''} hidden`], + }), + match( + opened, + atom((ctx) => + t.div({ + ...graphHiddenListStyles({}), + children: hide.map((log) => InspectGraphLog({ log })), + }), + ), + ), + ], + }) +} + +function InspectState() { + const target = t.div() + const inspector = new Inspector(target) + inspector.pending() + model.logSelected.onChange((ctx, log) => { + if (!log) return + return inspector.fulfilled(log.cache.state) + }) + return t.div([target]) +} + +const inspectStyles = styled('')` + display: flex; + gap: 0.25rem; +` + +const viewStyles = styled('')` + background: #fefefe; + font: var(--mono_fonts); + color: #111; + padding: 0.25rem; + border-radius: 0.25rem; + flex-grow: 1; +` + +const viewHorStyles = styled('')` + /* */ +` + +const viewHandleTopStyles = styled('')`` + +const viewHandleSideStyles = styled('')`` + +const viewContentStyles = styled('')` + overflow-y: scroll; +` + +const graphNodesStyles = styled('')` + display: flex; + flex-direction: column; + gap: 0.25rem; +` + +const graphLogStyles = styled('')` + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + gap: 0.3rem; +` + +const graphLogMarkerStyles = styled('')` + border: 1px solid black; + display: inline-block; + width: 16px; + height: 16px; + border-radius: 50%; +` + +const graphHiddenStyles = styled('')` + display: flex; + flex-direction: column; + margin: 0.1rem 0; +` + +const graphHiddenLabelStyles = styled('')` + cursor: pointer; + user-select: none; + opacity: 0.6; + font-size: smaller; +` +const graphHiddenListStyles = styled('')` + padding: 0.1rem 1rem; + gap: 0.25rem; + display: flex; + flex-direction: column; +` diff --git a/packages/logger/src/rld-model.ts b/packages/logger/src/rld-model.ts new file mode 100644 index 000000000..1a1ec08e3 --- /dev/null +++ b/packages/logger/src/rld-model.ts @@ -0,0 +1,11 @@ +import { atom, AtomCache, action } from '@reatom/core' + +export const caches = atom([] as AtomCache[], 'caches') +export const cachesHistorySize = atom(30, 'cachesHistorySize') +export const cachesAdd = action((ctx, next: AtomCache[]) => { + const historySize = ctx.get(cachesHistorySize) + + const joined = ctx.get(caches).concat(next) + const trimmed = joined.slice(Math.max(0, joined.length - historySize)) + caches(ctx, trimmed) +}, 'cachesAdd') diff --git a/packages/logger/src/rld.ts b/packages/logger/src/rld.ts new file mode 100644 index 000000000..2ea897499 --- /dev/null +++ b/packages/logger/src/rld.ts @@ -0,0 +1,141 @@ +import { Ctx, action, atom } from '@reatom/core' +import { onConnect } from '@reatom/hooks' +import { match } from '@reatom/lens' +import styled from 'stylerun' +import * as model from './rld-model' +import { RldInspect } from './rld-inspect' +import { RldFilterMenu } from './rld-filter' +import { t } from './t' + +type Tab = (typeof Tabs)[number] +const Tabs = ['inspect', 'filters'] as const + +export function Rld({ app }: { app: Ctx }) { + const opened = atom(true, 'opened') + const currentTab = atom('inspect', 'currentTab') + + const jsx = atom((ctx) => + t.div({ + ...devtoolsStyles({}), + children: [ + match( + opened, + t.div({ + ...devtoolsContentStyles({}), + children: [ + t.div({ + ...tabsStyles({}), + children: Tabs.map((tab) => + t.button({ + ...tabStyles( + {}, + {}, + tab === ctx.spy(currentTab) ? 'active' : '', + ), + children: [tab], + onclick: action((ctx) => currentTab(ctx, tab)), + }), + ), + }), + atom( + (ctx) => + ({ + inspect: RldInspect(), + filters: RldFilterMenu(), + })[ctx.spy(currentTab)], + ), + t.button({ + ...buttonStyles({}), + children: ['Close Devtools'], + onclick: action((ctx) => opened(ctx, false)), + }), + ], + }), + t.button({ + ...buttonStyles({}), + children: ['Reatom Devtools'], + onclick: action((ctx) => opened(ctx, true)), + }), + ), + ], + }), + ) + + onConnect(jsx, (ctx) => { + const unsub = app.subscribe((logs) => + // execute in the microtask to prevent 'cause collision' error + queueMicrotask(() => model.cachesAdd(ctx, logs)), + ) + return unsub + }) + + return jsx +} + +const devtoolsStyles = styled('')` + position: absolute; + left: 1rem; + bottom: 1rem; + background: #171721; + color: #fff; + border-radius: 0.25rem; + --syntax_normal: #1b1e23; + --syntax_comment: #a9b0bc; + --syntax_number: #20a5ba; + --syntax_keyword: #c30771; + --syntax_atom: #10a778; + --syntax_string: #008ec4; + --syntax_error: #ffbedc; + --syntax_unknown_variable: #838383; + --syntax_known_variable: #005f87; + --syntax_matchbracket: #20bbfc; + --syntax_key: #6636b4; + --mono_fonts: 82%/1.5 Menlo, Consolas, monospace; +` + +const devtoolsContentStyles = styled('')` + padding: 0.25rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +` + +export const buttonStyles = styled('')` + border: none; + font: inherit; + font-weight: bolder; + background: #24243a; + color: white; + border-radius: 0.25rem; + border: 1px solid #c5c6de88; + padding: 0.25rem; + width: 100%; +`.styled(':hover')` + background-color: #3B3B4E; +` + +const tabsStyles = styled('')` + margin: -0.25rem; + width: calc(100% + 0.5rem); + display: flex; +` + +const tabStyles = styled('')` + border: none; + font: inherit; + font-weight: bolder; + background: #24243a; + color: white; + padding: 0.25rem; + flex-grow: 1; +`.styled(':hover')` + background-color: #3B3B4E; +`.styled(':first-child')` + border-top-left-radius: 0.25rem; +`.styled(':last-child')` + border-top-right-radius: 0.25rem; +`.styled('.active')` + background-color: #f9364d; +`.styled('.active:hover')` + background-color: #d32e41; +` diff --git a/packages/logger/src/rld.tsx b/packages/logger/src/rld.tsx deleted file mode 100644 index 4b2eea99b..000000000 --- a/packages/logger/src/rld.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Ctx } from '@reatom/core' - -export function Rld({ app }: { app: Ctx }) { - return
Hello world
-} diff --git a/packages/logger/src/sandbox.tsx b/packages/logger/src/sandbox.tsx index b1031c5b1..e5c73daa4 100644 --- a/packages/logger/src/sandbox.tsx +++ b/packages/logger/src/sandbox.tsx @@ -68,6 +68,9 @@ const ctx = createCtx() // https://www.reatom.dev/packages/logger // change things and check the devtools console! const disconnect = connectLogger(ctx, { devtools: true }) +if (import.meta.hot) { + import.meta.hot.accept(disconnect) +} const root = ReactDOM.createRoot(document.getElementById('root')!) root.render( diff --git a/packages/logger/src/vite-env.d.ts b/packages/logger/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/logger/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json index 98266c2a2..55edd9a77 100644 --- a/packages/logger/tsconfig.json +++ b/packages/logger/tsconfig.json @@ -2,5 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "declarationMap": true, + "jsx": "react" } }