diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 144f2dd88e..aa375f8e88 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -61,8 +61,6 @@ jobs: - run: yarn format - run: yarn lint - run: yarn build - - run: yarn workspace mutxt-element run bundle - - run: yarn workspace mu-txt run electron:build - run: yarn vitest - run: yarn workspace @jsonjoy.com/rpc-server test:e2e:all - run: yarn workspace @jsonjoy.com/json-crdt-server test:e2e diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea882a0d9c..6b47872149 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,12 +57,6 @@ jobs: - name: Run typecheck run: yarn typecheck - - name: Bundle mutxt-element (Webpack) - run: yarn workspace mutxt-element run bundle - - - name: Build mu-txt Electron (renderer + main process) - run: yarn workspace mu-txt run electron:build - - name: Run tests (vitest) run: yarn vitest diff --git a/.storybook/main.ts b/.storybook/main.ts index da5a330364..5844f09753 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -34,8 +34,6 @@ const packages = [ 'collaborative-ui', 'json-crdt-explorer', 'json-joy', - 'mutxt-element', - 'mutxt-react', 'peritext', 'ui', ]; @@ -113,10 +111,6 @@ const config: StorybookConfig = { '@jsonjoy.com/json-equal': resolve(__dirname, '../packages/json-equal/src'), '@jsonjoy.com/keyboard/lib': resolve(__dirname, '../packages/keyboard/src'), '@jsonjoy.com/keyboard': resolve(__dirname, '../packages/keyboard/src'), - 'mutxt-react/lib': resolve(__dirname, '../packages/mutxt-react/src'), - 'mutxt-react': resolve(__dirname, '../packages/mutxt-react/src'), - 'mutxt-element/lib': resolve(__dirname, '../packages/mutxt-element/src'), - 'mutxt-element': resolve(__dirname, '../packages/mutxt-element/src'), '@jsonjoy.com/json-expression/lib': resolve(__dirname, '../packages/json-expression/src'), '@jsonjoy.com/json-pack/lib': resolve(__dirname, '../packages/json-pack/src'), '@jsonjoy.com/json-path/lib': resolve(__dirname, '../packages/json-path/src'), diff --git a/package.json b/package.json index 3416921583..d3b22659f2 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ } }, "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "2.4.16", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-icons": "^1.3.1", "@storybook/addon-a11y": "^10.3.5", diff --git a/packages/click-json/src/ClickableJson/JsonHoverable.tsx b/packages/click-json/src/ClickableJson/JsonHoverable.tsx index 78dffb914d..549f8e52f3 100644 --- a/packages/click-json/src/ClickableJson/JsonHoverable.tsx +++ b/packages/click-json/src/ClickableJson/JsonHoverable.tsx @@ -49,7 +49,7 @@ export const JsonHoverable: React.FC = ({pointer, children}) onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onDelete={ - !!onChange && !input.focused && prefixedPointer === focused + onChange && !input.focused && prefixedPointer === focused ? () => onChange([{op: 'remove', path: pointer}]) : undefined } diff --git a/packages/collaborative-prosemirror/src/ProseMirrorFacade.ts b/packages/collaborative-prosemirror/src/ProseMirrorFacade.ts index 52564f0a59..44957be8dc 100644 --- a/packages/collaborative-prosemirror/src/ProseMirrorFacade.ts +++ b/packages/collaborative-prosemirror/src/ProseMirrorFacade.ts @@ -41,7 +41,7 @@ const tryExtractPeritextOperation = (tr: Transaction, txt: Peritext, doc: PmNode const step = steps[0]; if (!(step instanceof ReplaceStep)) return; const slice = step.slice; - if (!!slice.openStart || !!slice.openEnd) return; + if (slice.openStart || slice.openEnd) return; const content = slice.content; let insertedText = ''; const deleteLen = step.to - step.from; diff --git a/packages/collaborative-slate/src/SlateFacade.ts b/packages/collaborative-slate/src/SlateFacade.ts index 074002cf6a..d3dc863af7 100644 --- a/packages/collaborative-slate/src/SlateFacade.ts +++ b/packages/collaborative-slate/src/SlateFacade.ts @@ -115,7 +115,7 @@ export class SlateFacade implements RichtextEditorFacade { if (installHistory) withHistory(editor); this._origOnChange = (editor as any).onChange; (editor.onChange as SlateEditorOnChange) = this._slateOnChange = (options?: {operation?: SlateOperation}) => { - if (this._disposed || !!this._remoteCnt) { + if (this._disposed || this._remoteCnt) { this._origOnChange?.call(editor); return; } diff --git a/packages/collaborative-ui/src/JsonBlock/JsonBlockClickable/index.tsx b/packages/collaborative-ui/src/JsonBlock/JsonBlockClickable/index.tsx index 111bbc5fe6..3f4d2444af 100644 --- a/packages/collaborative-ui/src/JsonBlock/JsonBlockClickable/index.tsx +++ b/packages/collaborative-ui/src/JsonBlock/JsonBlockClickable/index.tsx @@ -26,7 +26,7 @@ export const JsonBlockClickable: React.FC = ({value, pa }, [value, path]); return ( -
+
= ({ listItems.push( , ); - } else if (item === 'display' && !!renderDisplay) { + } else if (item === 'display' && renderDisplay) { listItems.push( { /** Optional initial room ID. When omitted the entry screen is shown. */ diff --git a/packages/json-crdt-repo/src/local/level/LevelLocalRepo.ts b/packages/json-crdt-repo/src/local/level/LevelLocalRepo.ts index 18c1cb3caf..233005588c 100644 --- a/packages/json-crdt-repo/src/local/level/LevelLocalRepo.ts +++ b/packages/json-crdt-repo/src/local/level/LevelLocalRepo.ts @@ -391,8 +391,7 @@ export class LevelLocalRepo implements LocalRepo { const model = Model.load(blob, this.sid); return model; } catch (error) { - if (!!error && typeof error === 'object' && (error as any).code === 'LEVEL_NOT_FOUND') - throw new Error('NOT_FOUND'); + if (error && typeof error === 'object' && (error as any).code === 'LEVEL_NOT_FOUND') throw new Error('NOT_FOUND'); throw error; } } @@ -487,7 +486,7 @@ export class LevelLocalRepo implements LocalRepo { const meta = await this.readMeta(keyBase); if (meta.seq === -1) { remote.create(remoteId).catch((error) => { - const code = !!error && typeof error === 'object' ? (error as any).code : undefined; + const code = error && typeof error === 'object' ? (error as any).code : undefined; const message = error instanceof Error ? error.message : undefined; if (code === 'CONFLICT' || message === 'CONFLICT') return; this.emitSyncError(error); @@ -747,7 +746,7 @@ export class LevelLocalRepo implements LocalRepo { if (!model) throw RpcError.notFound(); return {model, cursor}; } catch (error) { - const code = !!error && typeof error === 'object' ? (error as any).code : undefined; + const code = error && typeof error === 'object' ? (error as any).code : undefined; const message = error instanceof Error ? error.message : undefined; const notFound = code === 'NOT_FOUND' || message === 'NOT_FOUND' || message === 'Not Found'; if (remote && notFound) return await this.load(id); @@ -967,7 +966,7 @@ export class LevelLocalRepo implements LocalRepo { const {model, meta} = await this.pullExisting(id, keyBase); return {model, cursor: meta.seq}; } catch (error) { - if (!!error && typeof error === 'object' && (error as any).code === 'LEVEL_NOT_FOUND') { + if (error && typeof error === 'object' && (error as any).code === 'LEVEL_NOT_FOUND') { try { const {model, meta} = await this.pullNew(id, keyBase); return {model, cursor: meta.seq}; @@ -1168,7 +1167,7 @@ export class LevelLocalRepo implements LocalRepo { return; } } catch (error) { - if (!!error && typeof error === 'object' && (error as any).code === 'LEVEL_NOT_FOUND') { + if (error && typeof error === 'object' && (error as any).code === 'LEVEL_NOT_FOUND') { if (this._stopped) return; await this.pullNew(id, keyBase); return; diff --git a/packages/json-crdt-repo/src/session/EditSessionFactory.ts b/packages/json-crdt-repo/src/session/EditSessionFactory.ts index ebc8184e71..2bd6a700a5 100644 --- a/packages/json-crdt-repo/src/session/EditSessionFactory.ts +++ b/packages/json-crdt-repo/src/session/EditSessionFactory.ts @@ -64,7 +64,7 @@ export class EditSessionFactory { return session; } catch (error) { const errorCode = - !!error && typeof error === 'object' + error && typeof error === 'object' ? (error as Record).code || (error as Record).message || '' : ''; if (errorCode === 'NOT_FOUND') { @@ -81,7 +81,7 @@ export class EditSessionFactory { return session; } catch (error) { const errorCode = - !!error && typeof error === 'object' + error && typeof error === 'object' ? (error as Record).code || (error as Record).message || '' : ''; switch (errorCode) { diff --git a/packages/json-expression/docs/collections.md b/packages/json-expression/docs/collections.md new file mode 100644 index 0000000000..f3cb8694e1 --- /dev/null +++ b/packages/json-expression/docs/collections.md @@ -0,0 +1,104 @@ +Operators that build and transform arrays and objects, plus a JSON Patch `add` +for non-destructive updates. The higher-order array operators (`filter`, `map`, +`reduce`) run a sub-expression per element, binding a named variable you read +with `$`. + +Remember to box literal arrays: `[[1, 2, 3]]` is the array `[1, 2, 3]`, while a +bare `[1, 2, 3]` is read as a call to the operator named `1`. + +## Array operators + +```ts +['concat', ['$', '/a'], ['$', '/b']]; // a ++ b +['head', ['$', '/items'], 3]; // first 3 items +['in', [['a', 'b', 'c']], ['$', '/x']]; // is x one of a/b/c? +``` + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `concat` | `++` | variadic | Concatenate arrays into one | +| `push` | | variadic | `(array, ...items)` --- array with items appended (non-mutating) | +| `head` | | 2 | `(array, n)` --- first `n` items; negative `n` takes from the end | +| `slice` | | 3 | `(array, start, end)` --- sub-array | +| `sort` | | 1 | Sorted copy | +| `reverse` | | 1 | Reversed copy | +| `in` | | 2 | `(array, value)` --- whether `value` is in `array` (deep equality) | +| `indexOf` | | 2 | `(array, value)` --- index of `value`, or `-1` | +| `zip` | | 2 | Pair up two arrays into `[a, b]` tuples | +| `fromEntries` | | 1 | Build an object from `[key, value]` pairs | +| `filter` | | 3 | Keep elements matching a predicate (see below) | +| `map` | | 3 | Transform each element (see below) | +| `reduce` | | 5 | Fold to a single value (see below) | + +### `filter` and `map` + +Both take `[op, array, varname, expression]`. `varname` is a constant string +naming the per-element variable; `expression` runs once per element and reads +the element via `['$', varname]`. + +```ts +// Keep even numbers. +['filter', [[1, 2, 3, 4, 5]], 'x', ['not', ['%', ['$', 'x'], 2]]]; // => [2, 4] + +// Triple each number. +['map', [[1, 2, 3]], 'x', ['*', ['$', 'x'], 3]]; // => [3, 6, 9] +``` + +The element variable coexists with the input, so `map`/`filter` bodies can still +read the outer data: + +```ts +['map', ['$', '/arr'], 'x', ['*', ['$', 'x'], ['$', '/multiple']]]; +``` + +### `reduce` + +`reduce` takes `[reduce, array, initial, accName, varName, expression]`. The +expression reads the accumulator via `['$', accName]` and the current element +via `['$', varName]`, and returns the next accumulator. + +```ts +['reduce', [[1, 2, 3, 4, 5]], 0, 'acc', 'x', ['+', ['$', 'acc'], ['$', 'x']]]; // => 15 +``` + +## Object operators + +```ts +['keys', ['$', '/user']]; // property names +['o.set', ['$', '/user'], 'active', true]; // user with active: true +['o.del', ['$', '/user'], 'password']; // user without password +``` + +| Operator | Arity | Result | +|----------|-------|--------| +| `keys` | 1 | Array of the object's keys | +| `values` | 1 | Array of the object's values | +| `entries` | 1 | Array of `[key, value]` pairs | +| `o.set` | variadic | `(object, key, value, ...)` --- object with the keys set (non-mutating) | +| `o.del` | variadic | `(object, key, ...)` --- object with the keys removed | + +`o.set` takes alternating key/value operands and refuses `__proto__`: + +```ts +['o.set', {}, 'foo', 'bar', 'baz', ['+', ['$', ''], 3]]; +// with input 2 => {foo: 'bar', baz: 5} +``` + +## JSON Patch: `jp.add` + +`jp.add` applies one or more JSON Patch *add* operations, addressing locations +by JSON Pointer. Operands alternate pointer and value; the original input is not +mutated. + +```ts +['jp.add', {}, '/foo', 'bar', '/baz', ['+', ['$', ''], 3]]; +// with input 2 => {foo: 'bar', baz: 5} +``` + +| Operator | Arity | Result | +|----------|-------|--------| +| `jp.add` | variadic | `(document, pointer, value, ...)` --- document with values added at each pointer | + +Pointers must be constant strings. Adding at an array index inserts at that +position; adding at the array's length appends, and an out-of-range index +throws. diff --git a/packages/json-expression/docs/evaluate-and-compile.md b/packages/json-expression/docs/evaluate-and-compile.md new file mode 100644 index 0000000000..ced2f96102 --- /dev/null +++ b/packages/json-expression/docs/evaluate-and-compile.md @@ -0,0 +1,91 @@ +There are two ways to run an expression: interpret it with `evaluate`, or +JIT-compile it to a JavaScript function with `JsonExpressionCodegen`. Both +share the same operator set and produce identical results. + +## `evaluate` + +`evaluate` walks the expression tree once and returns the result. It is bound to +the full operator set, so you only pass the expression and a context with a +`Vars` instance: + +```ts +import {evaluate, Vars} from '@jsonjoy.com/json-expression'; + +evaluate(['*', 2, ['$', '/x']], {vars: new Vars({x: 21})}); // => 42 +``` + +Reach for `evaluate` when the expression runs once, changes often, or is +supplied at runtime --- there is no compile step to amortize. + +## `JsonExpressionCodegen` + +The codegen compiles an expression into a plain function `(vars) => result`. +Compiling has up-front cost but the returned function runs about an order of +magnitude faster, so it pays off when one expression runs against many inputs. + +Unlike `evaluate`, the codegen needs the operator set passed explicitly: + +```ts +import {JsonExpressionCodegen, Vars} from '@jsonjoy.com/json-expression'; +import {operatorsMap} from '@jsonjoy.com/json-expression/lib/operators'; + +const codegen = new JsonExpressionCodegen({ + expression: ['*', 2, ['$', '/x']], + operators: operatorsMap, +}); +const fn = codegen.run().compile(); + +fn(new Vars({x: 21})); // => 42 +fn(new Vars({x: 50})); // => 100 +``` + +Call `.run()` once to build the code, then `.compile()` to get the function. +`.compile()` wraps execution so thrown non-`Error` values are normalized; +`.compileRaw()` skips that wrapper. `.generate()` returns the generated source +if you want to inspect what was produced. + +## Constant folding + +The compiler evaluates *pure* sub-expressions whose operands are all literals at +compile time, baking the result into the generated code: + +```ts +// ['+', 1, 2] is folded to the constant 3 during compilation. +const fn = new JsonExpressionCodegen({ + expression: ['+', ['+', 1, 2], ['$', '/x']], + operators: operatorsMap, +}).run().compile(); +``` + +Operators that read input or otherwise have side effects --- `$`, `get`, `get?` --- are +marked impure and are never folded, so they re-run on every call. + +## Matching with `createPattern` + +The `matches` string operator needs a pattern compiler, supplied via the +`createPattern` option on *both* `evaluate` and the codegen. It receives the +pattern string and returns a `(value: string) => boolean` tester. This keeps the +choice of regex/glob engine (and its security trade-offs) in your hands: + +```ts +const createPattern = (pattern: string) => { + const re = new RegExp(pattern); + return (value: string) => re.test(value); +}; + +evaluate(['matches', ['$', '/name'], 'A.*'], { + vars: new Vars({name: 'Alice'}), + createPattern, +}); // => true +``` + +Without `createPattern`, `matches` throws. + +## Which to use + +| Situation | Use | +|-----------|-----| +| Expression supplied at runtime, runs once | `evaluate` | +| One expression, many inputs (hot path) | `JsonExpressionCodegen` | +| Need to inspect generated JS | `JsonExpressionCodegen` + `.generate()` | +| Using `matches` | either, with `createPattern` | diff --git a/packages/json-expression/docs/index.ts b/packages/json-expression/docs/index.ts new file mode 100644 index 0000000000..b547e06752 --- /dev/null +++ b/packages/json-expression/docs/index.ts @@ -0,0 +1,49 @@ +import type {LibPage} from '@jsonjoy.com/ui/src/types/libs'; + +export const page: LibPage = { + name: 'JSON Expression', + title: 'JSON Expression', + type: 'lib', + subtitle: 'A JSON-native expression language that evaluates directly or JIT-compiles to fast JavaScript.', + pkg: '@jsonjoy.com/json-expression', + group: 'tooling', + repo: 'streamich/json-joy', + repoPath: 'tree/master/packages/json-expression', + tech: 'TypeScript', + techIcon: {set: 'lineicons', icon: 'typescript'}, + showContentsTable: true, + children: [ + { + name: 'Variables', + subtitle: 'Read input with the $ / get operators and the Vars container.', + // @ts-ignore raw markdown, loaded by the site's webpack raw-loader + src: async () => (await import('./variables.md')).default, + }, + { + name: 'Evaluate and compile', + subtitle: 'Interpret expressions with evaluate() or JIT-compile them with JsonExpressionCodegen.', + // @ts-ignore + src: async () => (await import('./evaluate-and-compile.md')).default, + }, + { + name: 'Math and logic', + subtitle: 'Arithmetic, comparison, logical, bitwise, and branching operators.', + // @ts-ignore + src: async () => (await import('./math-and-logic.md')).default, + }, + { + name: 'Strings and types', + subtitle: 'Type checks and casts, container access, string operators, and binary reads.', + // @ts-ignore + src: async () => (await import('./strings-and-types.md')).default, + }, + { + name: 'Collections', + subtitle: 'Array, object, and JSON Patch operators, including filter, map, and reduce.', + // @ts-ignore + src: async () => (await import('./collections.md')).default, + }, + ], + // @ts-ignore + src: async () => (await import('./text.md')).default, +}; diff --git a/packages/json-expression/docs/math-and-logic.md b/packages/json-expression/docs/math-and-logic.md new file mode 100644 index 0000000000..3a8f20de5b --- /dev/null +++ b/packages/json-expression/docs/math-and-logic.md @@ -0,0 +1,101 @@ +Operators for numbers, comparisons, booleans, bit twiddling, and control flow. +Every operator that has a symbolic alias lists it; the word form and the symbol +are interchangeable. + +## Arithmetic + +```ts +['+', 1, 2, 3]; // => 6 +['-', 10, ['$', '/x']]; // 10 - x +['*', ['$', '/price'], 1.2]; // price * 1.2 +``` + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `add` | `+` | variadic | Sum of all operands | +| `subtract` | `-` | variadic | First operand minus the rest | +| `multiply` | `*` | variadic | Product of all operands | +| `divide` | `/` | variadic | First operand divided by the rest | +| `mod` | `%` | variadic | Remainder, folded left to right | +| `min` | - | variadic | Smallest operand | +| `max` | - | variadic | Largest operand | +| `round` | - | 1 | Round to nearest integer | +| `ceil` | - | 1 | Round up | +| `floor` | - | 1 | Round down | +| `trunc` | - | 1 | Drop the fractional part (toward zero) | +| `abs` | - | 1 | Absolute value | +| `sqrt` | - | 1 | Square root | +| `exp` | - | 1 | e raised to the operand | +| `ln` | - | 1 | Natural logarithm | +| `log` | - | 2 | Logarithm of operand 1 in base operand 2 | +| `log10` | - | 1 | Base-10 logarithm | +| `pow` | `**` | 2 | Operand 1 raised to operand 2 | + +## Comparison + +Equality is **deep** for objects and arrays. The ordering operators compare +numbers and strings. + +```ts +['==', ['$', '/status'], 'active']; +['cmp', 2, 1]; // => 1 (-1, 0, or 1) +['between', ['$', '/age'], 18, 65]; // 18 <= age <= 65 +``` + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `eq` | `==` | 2 | Deep equality | +| `ne` | `!=` | 2 | Deep inequality | +| `gt` | `>` | 2 | Greater than | +| `ge` | `>=` | 2 | Greater than or equal | +| `lt` | `<` | 2 | Less than | +| `le` | `<=` | 2 | Less than or equal | +| `cmp` | - | 2 | Three-way compare: `-1`, `0`, or `1` | +| `between` | `=><=` | 3 | `low <= value <= high` | +| `><` | - | 3 | `low < value < high` | +| `=><` | - | 3 | `low <= value < high` | +| `><=` | - | 3 | `low < value <= high` | + +The between-family takes the value first, then the bounds: +`['between', value, low, high]`. The operator name encodes which ends are +inclusive (`=`) versus exclusive --- `=><=` is inclusive on both ends. + +## Logical + +```ts +['and', ['>', ['$', '/age'], 18], ['$', '/verified']]; +['or', ['$', '/admin'], ['$', '/owner']]; +['not', ['$', '/disabled']]; +``` + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `and` | `&&` | variadic | True when all operands are truthy | +| `or` | `\|\|` | variadic | True when any operand is truthy | +| `not` | `!` | 1 | Logical negation | + +## Bitwise + +Integer bit operations. + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `bitAnd` | `&` | variadic | Bitwise AND | +| `bitOr` | `\|` | variadic | Bitwise OR | +| `bitXor` | `^` | variadic | Bitwise XOR | +| `bitNot` | `~` | 1 | Bitwise NOT | + +## Branching + +```ts +['?', ['>', ['$', '/n'], 0], 'positive', 'non-positive']; +['throw', 'invalid input']; +``` + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `if` | `?` | 3 | `condition ? then : otherwise`; only the taken branch is evaluated | +| `throw` | - | 1 | Throws its operand as the error value | + +`if` short-circuits: the untaken branch is never evaluated, so it is safe to put +a `throw` or a `$` that might not resolve in either arm. diff --git a/packages/json-expression/docs/strings-and-types.md b/packages/json-expression/docs/strings-and-types.md new file mode 100644 index 0000000000..a1691e1bd1 --- /dev/null +++ b/packages/json-expression/docs/strings-and-types.md @@ -0,0 +1,113 @@ +Operators for inspecting and casting values, measuring and indexing containers, +working with strings, and reading numbers out of binary buffers. + +## Type inspection and casts + +`type` returns one of `boolean`, `null`, `number`, `string`, `object`, `array`, +`undefined`, or `binary`. The cast operators coerce a value to a target type. + +```ts +['type', ['$', '/x']]; // e.g. 'number' +['num', ['$', '/count']]; // coerce to number +['str', 123]; // => '123' +``` + +| Operator | Arity | Result | +|----------|-------|--------| +| `type` | 1 | Runtime type name (see list above) | +| `bool` | 1 | Coerce to boolean | +| `num` | 1 | Coerce to number | +| `str` | 1 | Coerce to string | + +## Type guards + +Each guard returns a boolean. Note the trailing `?` is part of the operator +name. + +| Operator | True when the value is | +|----------|------------------------| +| `und?` | `undefined` | +| `nil?` | `null` | +| `bool?` | a boolean | +| `num?` | a number | +| `str?` | a string | +| `bin?` | binary (`Uint8Array`) | +| `arr?` | an array | +| `obj?` | an object | + +```ts +['?', ['str?', ['$', '/id']], ['$', '/id'], ['str', ['$', '/id']]]; +``` + +## Containers: length and member access + +`len` measures strings, arrays, objects (key count), and binary; it returns `0` +for scalars. `member` (alias `[]`) reads an element by index or key. + +```ts +['len', ['$', '/items']]; // number of items +['member', ['$', '/items'], 0]; // first item +['[]', ['$', '/user'], 'name']; // user.name +``` + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `len` | | 1 | Length / size of a string, array, object, or binary | +| `member` | `[]` | 2 | Element of an array/object/string/binary by index or key | + +## Strings + +```ts +['.', 'Hello, ', ['$', '/name']]; // concatenation +['starts', ['$', '/type'], 'com.']; // prefix test +['substr', 'abcde', 1, 3]; // => 'bc' +``` + +| Operator | Aliases | Arity | Result | +|----------|---------|-------|--------| +| `cat` | `.` | variadic | Concatenate operands into a string | +| `contains` | | 2 | Whether operand 1 contains operand 2 | +| `starts` | | 2 | Whether operand 1 starts with operand 2 | +| `ends` | | 2 | Whether operand 1 ends with operand 2 | +| `substr` | | 3 | Substring `(string, start, end)` | +| `matches` | | 2 | Whether the string matches a pattern --- requires `createPattern` | + +`matches` needs a pattern compiler supplied through the `createPattern` option; +see [Evaluate and compile](/libs/json-expression/evaluate-and-compile). + +### String validators + +Boolean operators that test common string formats: + +| Operator | Validates | +|----------|-----------| +| `email?` | Email address | +| `hostname?` | Hostname | +| `ip4?` | IPv4 address | +| `ip6?` | IPv6 address | +| `uuid?` | UUID | +| `uri?` | URI | +| `duration?` | ISO 8601 duration | +| `date?` | Full date | +| `time?` | Time | +| `dateTime?` | Date-time | + +```ts +['?', ['email?', ['$', '/contact']], 'ok', 'bad email']; +``` + +## Binary reads + +When the input is a `Uint8Array`, these operators read a typed number at a byte +offset: `[op, binary, offset]`. + +| Operator | Reads | +|----------|-------| +| `u8` / `i8` | Unsigned / signed 8-bit integer | +| `u16` / `i16` | Unsigned / signed 16-bit integer | +| `u32` / `i32` | Unsigned / signed 32-bit integer | +| `f32` / `f64` | 32- / 64-bit float | + +```ts +['u8', ['$', '/buf'], 0]; // first byte of the buffer as an unsigned int +``` diff --git a/packages/json-expression/docs/text.md b/packages/json-expression/docs/text.md new file mode 100644 index 0000000000..928cba5bd7 --- /dev/null +++ b/packages/json-expression/docs/text.md @@ -0,0 +1,114 @@ +JSON Expression is a small expression language whose syntax *is* JSON. An +expression is a JSON array whose first element names an operator; the remaining +elements are operands. Everything else is a literal. + +```ts +['+', 1, 2]; // => 3 +['and', ['>', ['$', '/age'], 18], ['starts', ['$', '/name'], 'A']]; +``` + +Expressions can be **evaluated** directly, or **JIT-compiled** to a JavaScript +function that runs roughly an order of magnitude faster. The same expression +and the same operators back both paths, so you can prototype with `evaluate` +and switch to the compiler in hot code without changing the expression. + +This package is the reference implementation of the +[JSON Expression specification](/specs/json-expression), which defines the +language and its operators independent of any one runtime. + +## Install + +``` +npm install @jsonjoy.com/json-expression +``` + +## Quick start + +Evaluate an expression against some input data: + +```ts +import {evaluate, Vars} from '@jsonjoy.com/json-expression'; + +const expression = ['+', 1, ['$', '/foo']]; +evaluate(expression, {vars: new Vars({foo: 2})}); // => 3 +``` + +Or compile it once and reuse the function: + +```ts +import {JsonExpressionCodegen, Vars} from '@jsonjoy.com/json-expression'; +import {operatorsMap} from '@jsonjoy.com/json-expression/lib/operators'; + +const codegen = new JsonExpressionCodegen({ + expression: ['+', 1, ['$', '/foo']], + operators: operatorsMap, +}); +const fn = codegen.run().compile(); + +fn(new Vars({foo: 2})); // => 3 +fn(new Vars({foo: 9})); // => 10 +``` + +See [Evaluate and compile](/libs/json-expression/evaluate-and-compile) for when +to use each. + +## Data model and syntax rules + +An expression is plain JSON, parsed by three rules: + +1. An expression is a valid JSON value. +2. **Arrays are operator calls.** The first element is the operator name (a + string), the rest are operand expressions, evaluated recursively. For + example `['$', '/some/path']` reads a value from the input. +3. **Everything else is a literal** --- numbers, strings, booleans, `null`, and + objects. To use a *literal array*, wrap it in an extra array so it reads as a + one-element expression: `[[1, 2, 3]]` evaluates to `[1, 2, 3]`, and `[[]]` + evaluates to `[]`. + +~~~jj.note +Rule 3 is the one that trips people up: a bare `[1, 2, 3]` is read as a call to +the operator named `1`. Box literal arrays as `[[1, 2, 3]]`. +~~~ + +Operators have **aliases** --- a readable word form and often a symbolic form. +`['add', 1, 2]` and `['+', 1, 2]` are the same expression, as are `['get', '/x']` +and `['$', '/x']`. The tables on the operator pages list every alias. For the +formal grammar and the authoritative operator list, see the +[JSON Expression spec](/specs/json-expression). + +## Example: filtering an event stream + +Given CloudEvents-shaped input: + +```ts +{ + specversion: '1.0', + type: 'com.example.someevent', + datacontenttype: 'application/json', + data: {appinfoA: 'abc'}, +} +``` + +a single expression decides whether an event is interesting: + +```ts +[ + 'and', + ['==', ['$', '/specversion'], '1.0'], + ['starts', ['$', '/type'], 'com.example.'], + ['in', [['application/octet-stream', 'application/json']], ['$', '/datacontenttype']], + ['==', ['$', '/data/appinfoA'], 'abc'], +]; +``` + +Compile it once and run it over every event in the stream. + +## Operator reference + +| Page | Covers | +|------|--------| +| [Variables](/libs/json-expression/variables) | Reading input: `$` / `get`, `get?`, default values, named variables, the `Vars` container | +| [Evaluate and compile](/libs/json-expression/evaluate-and-compile) | `evaluate()`, `JsonExpressionCodegen`, constant folding, the `createPattern` option | +| [Math and logic](/libs/json-expression/math-and-logic) | Arithmetic, comparison, logical, bitwise, and branching (`if`, `throw`) | +| [Strings and types](/libs/json-expression/strings-and-types) | `type`/casts, type guards, `len`/`member`, string operators and validators, binary reads | +| [Collections](/libs/json-expression/collections) | Array operators (incl. `filter`/`map`/`reduce`), object operators, and `jp.add` | diff --git a/packages/json-expression/docs/variables.md b/packages/json-expression/docs/variables.md new file mode 100644 index 0000000000..20e285b510 --- /dev/null +++ b/packages/json-expression/docs/variables.md @@ -0,0 +1,92 @@ +Expressions read their input through **variables**. The input you pass at +evaluation time is the *default variable*; named variables are bound by +scoping operators like `filter`, `map`, and `reduce`, or set manually on the +`Vars` container. + +## The `$` / `get` operator + +`['$', varname]` (alias `['get', varname]`) reads a value. The `varname` is a +string combining an optional variable name with a JSON Pointer: + +```ts +['$', ''] // the whole input (default variable) +['$', '/foo'] // input.foo +['$', '/foo/1'] // input.foo[1] +['$', 'item'] // the named variable `item` +['$', 'item/name'] // named variable `item`, then /name inside it +``` + +A leading `/` (or an empty string) targets the default variable; otherwise the +text up to the first `/` is the variable name. + +```ts +import {evaluate, Vars} from '@jsonjoy.com/json-expression'; + +evaluate(['$', '/foo'], {vars: new Vars({foo: 2})}); // => 2 +evaluate(['$', ''], {vars: new Vars({foo: 2})}); // => {foo: 2} +``` + +### Default values + +By default, reading a missing path **throws** `NOT_FOUND`. Pass a third operand +to return a fallback instead: + +```ts +evaluate(['$', '/foo/5'], {vars: new Vars({foo: [1, 2]})}); // throws NOT_FOUND +evaluate(['$', '/foo/5', 'miss'], {vars: new Vars({foo: [1, 2]})}); // => 'miss' +``` + +### Checking existence with `$?` + +`['$?', varname]` (alias `['get?', varname]`) returns a boolean instead of the +value --- `true` when the path resolves, `false` when it does not. Use it to +branch without risking a throw: + +```ts +['?', ['$?', '/email'], ['$', '/email'], 'no email']; +``` + +### Dynamic paths + +The `varname` operand can itself be an expression, so the path can be computed +at runtime: + +```ts +// Read the pointer to follow out of the data itself. +evaluate(['$', ['$', '/foo/0']], {vars: new Vars({foo: ['/foo']})}); // => ['/foo'] +``` + +~~~jj.note +`$` / `get` is an *impure* operator (its result depends on input), so the +compiler never folds it to a constant --- unlike pure operators over literal +operands. See [Evaluate and compile](/libs/json-expression/evaluate-and-compile). +~~~ + +## The `Vars` container + +Both `evaluate` and a compiled function receive a `Vars` instance, not the raw +data. `Vars` wraps the default-variable value (`env`) and holds named +variables: + +```ts +import {Vars} from '@jsonjoy.com/json-expression'; + +const vars = new Vars({foo: 2}); +vars.get(''); // => {foo: 2} (the env / default variable) +vars.set('x', 42); +vars.get('x'); // => 42 +vars.has('x'); // => true +vars.find('', '/foo'); // => 2 (env, then JSON Pointer /foo) +``` + +| Method | Purpose | +|--------|---------| +| `get(name)` | Named variable, or the env when `name` is `''` | +| `set(name, value)` | Bind a named variable | +| `has(name)` | Whether a named variable is bound | +| `del(name)` | Remove a named variable | +| `find(name, pointer)` | Resolve a JSON Pointer inside a variable | + +You rarely call `set` yourself --- the scoping operators bind per-element +variables for you. See `filter`, `map`, and `reduce` in +[Collections](/libs/json-expression/collections). diff --git a/packages/json-joy/docs/index.ts b/packages/json-joy/docs/index.ts index 408f582f9b..b7bfdf5152 100644 --- a/packages/json-joy/docs/index.ts +++ b/packages/json-joy/docs/index.ts @@ -1,12 +1,10 @@ import type {LibPage} from '@jsonjoy.com/ui/src/types/libs'; import {page as gettingStartedPage} from './getting-started'; -import {page as jsonPackPage} from './json-pack'; import {page as jsonCrdtPage} from './json-crdt'; import {page as jsonCrdtPatchPage} from './json-crdt-patch'; import {page as jsonBinaryPage} from './json-binary'; import {page as jsonBrandPage} from './json-brand'; import {page as jsonClonePage} from './json-clone'; -import {page as jsonExpressionPage} from './json-expression'; import {page as jsonEqualPage} from './json-equal'; import {page as jsonPatchPage} from './json-patch'; import {page as jsonPointerPage} from './json-pointer'; @@ -31,10 +29,8 @@ export const page: LibPage = { gettingStartedPage, jsonCrdtPage, jsonCrdtPatchPage, - jsonPackPage, jsonPatchPage, jsonPointerPage, - jsonExpressionPage, jsonBinaryPage, jsonEqualPage, jsonClonePage, diff --git a/packages/json-joy/docs/json-expression/index.ts b/packages/json-joy/docs/json-expression/index.ts deleted file mode 100644 index 9c0783137a..0000000000 --- a/packages/json-joy/docs/json-expression/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; - -export const page: ContentPage = { - name: 'json-expression', - slug: 'json-expression', - children: [], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-expression/text.md b/packages/json-joy/docs/json-expression/text.md deleted file mode 100644 index ca7e28b300..0000000000 --- a/packages/json-joy/docs/json-expression/text.md +++ /dev/null @@ -1,104 +0,0 @@ -JSON Expression is a simple JSON DSL, which allows to write expressions and -evaluate expressions. - -For example, the following expression - -```js -['+', 1, 2]; // 1 + 2 -``` - -evaluates to 3. - -## Example - -Consider you application receives a stream of JSON Cloud Events, like this: - -```js -{ - "specversion" : "1.0", - "type" : "com.example.someevent", - "source" : "/mycontext", - "subject": null, - "id" : "C234-1234-1234", - "time" : "2018-04-05T17:31:00Z", - "comexampleextension1" : "value", - "comexampleothervalue" : 5, - "datacontenttype" : "application/json", - "data" : { - "appinfoA" : "abc", - "appinfoB" : 123, - "appinfoC" : true - } -} -``` - -You could write and compile a JSON Expression to efficiently filter out events -you are interested in, for example your expression could look like this: - -```js -[ - 'and', - ['==', ['get', '/specversion'], '1.0'], - ['starts', ['get', '/type'], 'com.example.'], - ['in', ['get', '/datacontenttype'], [['application/octet-stream', 'application/json']]], - ['==', ['=', '/data/appinfoA'], 'abc'], -]; -``` - -## Documentation - -`json-expression` library supports few dozen operators, see full list in `Expr` -type [here](https://github.com/streamich/json-joy/blob/master/src/json-expression/types.ts). - -Parsing rules: - -1. JSON Expression is a valid JSON value. -2. All expressions are JSON arrays, which start with a string which specifies - the operator and remaining array elements are operands. For example, the - "get" operator fetches some value from supplied data using JSON - Pointer:`["get", "/some/path"]`. -3. All other values are treated as literals. Except for arrays, which need to - be enclosed in square brackets. For example, to specify an empty array, you - box your array in square brackets: `[[]]`. This evaluates to an empty array - JSON value `[]`. - -## Reference - -`json-expression` library can immediately evaluate expressions or it can -compile an efficient expression to a function, which will execute about -an order of magnitude faster. - -Evaluating expression immediately as-is. - -```ts -import {evaluate} from 'json-joy/{lib,es2020}/json-expression'; - -const expression = ['+', 1, ['get', '/foo']]; -const data = {foo: 2}; - -evaluate(expression, {data}); // 3 -``` - -Pre-compiling expression to an optimized function. - -```ts -import {JsonExpressionCodegen} from 'json-joy/{lib,es2020}/json-expression'; - -const expression = ['+', 1, ['get', '/foo']]; -const codegen = new JsonExpressionCodegen({expression}); -const fn = codegen.run().compile(); -const data = {foo: 2}; - -fn({data}); // 3 -``` - -## Benchmark - -``` -node benchmarks/json-expression/main.js -json-joy/json-expression JsonExpressionCodegen x 14,557,786 ops/sec ±0.09% (100 runs sampled), 69 ns/op -json-joy/json-expression JsonExpressionCodegen with codegen x 170,098 ops/sec ±0.13% (101 runs sampled), 5879 ns/op -json-joy/json-expression evaluate x 864,956 ops/sec ±0.10% (101 runs sampled), 1156 ns/op -json-logic-js x 821,799 ops/sec ±0.18% (99 runs sampled), 1217 ns/op -Fastest is json-joy/json-expression JsonExpressionCodegen -``` diff --git a/packages/json-joy/docs/json-expression/text.ts b/packages/json-joy/docs/json-expression/text.ts deleted file mode 100644 index 87ba8e1486..0000000000 --- a/packages/json-joy/docs/json-expression/text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {md} from '@jsonjoy.com/ui/lib/markdown/parser'; - -const markdown = require('./text.md').default; -export const text = md(markdown); diff --git a/packages/json-joy/docs/json-pack/cbor/benchmarks/index.ts b/packages/json-joy/docs/json-pack/cbor/benchmarks/index.ts deleted file mode 100644 index 4fa31ff206..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/benchmarks/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; - -export const page: ContentPage = { - name: 'Benchmarks', - slug: 'benchmarks', - children: [], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/cbor/benchmarks/text.md b/packages/json-joy/docs/json-pack/cbor/benchmarks/text.md deleted file mode 100644 index 8a69cbf151..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/benchmarks/text.md +++ /dev/null @@ -1,426 +0,0 @@ -`json-pack` aims to implement the fastest CBOR code in NPM ecosystem. Below you can -see the benchmark results comparing `json-joy` against other CBOR packages. - -Overall, `json-pack` is much faster than all other packages, except `cbor-x` which -comes close to `json-pack`, but still slower. - - -### Encoding - -Below table compares CBOR encoding performance of `json-joy` against `cbor-x`, `cborg`, and `cbor` packages. - -![cbor-encoding-benchmark-results](https://appsets.jsonjoy.com/libraries/json-joy-js/json-pack/cbor-encoding-results.png) - - -### Decoding - -Below table compares CBOR decoding performance of `json-joy` against `cbor-x`, `cborg`, and `cbor` packages. - -![cbor-decoding-benchmark-results](https://appsets.jsonjoy.com/libraries/json-joy-js/json-pack/cbor-decoding-results.png) - - -### More details - -More benchmark results in raw text format below. - - -#### Encoding - -``` -npx ts-node benchmarks/json-pack/bench.encoding.cbor.ts -=========================================================== Benchmark: Encoding -Warmup: 1000x , Node.js: v20.1.0 , Arch: arm64 , CPU: Apple M1 --------------------------------------------------------- Small object, 44 bytes -🤞 Buffer.from(JSON.stringify()) x 2,339,523 ops/sec ±0.50% (99 runs sampled) -🤞 JSON.stringify() x 3,802,757 ops/sec ±0.17% (100 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 6,132,816 ops/sec ±0.43% (99 runs sampled) -🤞 json-joy/json-pack CborEncoder x 6,248,575 ops/sec ±0.13% (98 runs sampled) -🤞 cbor-x x 4,924,643 ops/sec ±0.31% (99 runs sampled) -🤞 cbor-js x 670,013 ops/sec ±1.51% (80 runs sampled) -🤞 cborg x 777,829 ops/sec ±0.16% (98 runs sampled) -🤞 cbor-sync x 444,785 ops/sec ±3.07% (96 runs sampled) -Fastest is 🤞 json-joy/json-pack CborEncoder ------------------------------------------------------ Typical object, 993 bytes -🤞 Buffer.from(JSON.stringify()) x 208,448 ops/sec ±0.07% (101 runs sampled) -🤞 JSON.stringify() x 335,826 ops/sec ±0.14% (101 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 468,552 ops/sec ±0.31% (96 runs sampled) -🤞 json-joy/json-pack CborEncoder x 446,904 ops/sec ±0.15% (98 runs sampled) -🤞 cbor-x x 400,380 ops/sec ±0.89% (91 runs sampled) -🤞 cbor-js x 109,455 ops/sec ±0.13% (98 runs sampled) -🤞 cborg x 60,584 ops/sec ±0.10% (102 runs sampled) -🤞 cbor-sync x 75,523 ops/sec ±0.21% (96 runs sampled) -Fastest is 🤞 json-joy/json-pack CborEncoderFast ------------------------------------------------------- Large object, 3741 bytes -🤞 Buffer.from(JSON.stringify()) x 64,232 ops/sec ±0.07% (99 runs sampled) -🤞 JSON.stringify() x 108,186 ops/sec ±0.24% (101 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 135,553 ops/sec ±0.11% (101 runs sampled) -🤞 json-joy/json-pack CborEncoder x 130,092 ops/sec ±0.24% (100 runs sampled) -🤞 cbor-x x 110,045 ops/sec ±0.63% (95 runs sampled) -🤞 cbor-js x 33,044 ops/sec ±0.11% (102 runs sampled) -🤞 cborg x 18,516 ops/sec ±0.13% (101 runs sampled) -🤞 cbor-sync x 25,829 ops/sec ±0.43% (98 runs sampled) -Fastest is 🤞 json-joy/json-pack CborEncoderFast ------------------------------------------------- Very large object, 45750 bytes -🤞 Buffer.from(JSON.stringify()) x 7,175 ops/sec ±0.76% (98 runs sampled) -🤞 JSON.stringify() x 7,783 ops/sec ±0.51% (101 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 5,759 ops/sec ±0.53% (100 runs sampled) -🤞 json-joy/json-pack CborEncoder x 5,569 ops/sec ±0.43% (100 runs sampled) -🤞 cbor-x x 5,671 ops/sec ±0.71% (94 runs sampled) -🤞 cbor-js x 2,513 ops/sec ±0.40% (100 runs sampled) -🤞 cborg x 818 ops/sec ±1.04% (92 runs sampled) -🤞 cbor-sync x 1,579 ops/sec ±0.34% (98 runs sampled) -Fastest is 🤞 JSON.stringify() ----------------------------------------------- Object with many keys, 969 bytes -🤞 Buffer.from(JSON.stringify()) x 182,418 ops/sec ±0.69% (99 runs sampled) -🤞 JSON.stringify() x 166,880 ops/sec ±5.89% (82 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 276,754 ops/sec ±1.11% (99 runs sampled) -🤞 json-joy/json-pack CborEncoder x 272,113 ops/sec ±0.77% (94 runs sampled) -🤞 cbor-x x 193,156 ops/sec ±0.49% (96 runs sampled) -🤞 cbor-js x 73,180 ops/sec ±0.38% (100 runs sampled) -🤞 cborg x 35,937 ops/sec ±0.19% (95 runs sampled) -🤞 cbor-sync x 53,410 ops/sec ±0.66% (100 runs sampled) -Fastest is 🤞 json-joy/json-pack CborEncoderFast ------------------------------------------------------ String ladder, 3398 bytes -🤞 Buffer.from(JSON.stringify()) x 148,910 ops/sec ±0.24% (98 runs sampled) -🤞 JSON.stringify() x 172,582 ops/sec ±0.06% (102 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 276,018 ops/sec ±0.64% (92 runs sampled) -🤞 json-joy/json-pack CborEncoder x 278,835 ops/sec ±0.85% (92 runs sampled) -🤞 cbor-x x 209,737 ops/sec ±0.44% (95 runs sampled) -🤞 cbor-js x 29,304 ops/sec ±0.15% (101 runs sampled) -🤞 cborg x 61,577 ops/sec ±0.10% (102 runs sampled) -🤞 cbor-sync x 73,548 ops/sec ±2.14% (93 runs sampled) -Fastest is 🤞 json-joy/json-pack CborEncoder,🤞 json-joy/json-pack CborEncoderFast ------------------------------------------------------- Long strings, 7011 bytes -🤞 Buffer.from(JSON.stringify()) x 28,860 ops/sec ±0.06% (99 runs sampled) -🤞 JSON.stringify() x 59,800 ops/sec ±0.07% (99 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 403,027 ops/sec ±1.97% (93 runs sampled) -🤞 json-joy/json-pack CborEncoder x 415,001 ops/sec ±1.38% (95 runs sampled) -🤞 cbor-x x 364,240 ops/sec ±1.95% (85 runs sampled) -🤞 cbor-js x 13,370 ops/sec ±0.11% (101 runs sampled) -🤞 cborg x 118,723 ops/sec ±0.54% (99 runs sampled) -🤞 cbor-sync x 117,072 ops/sec ±0.17% (94 runs sampled) -Fastest is 🤞 json-joy/json-pack CborEncoder ------------------------------------------------------- Short strings, 170 bytes -🤞 Buffer.from(JSON.stringify()) x 1,016,012 ops/sec ±0.12% (102 runs sampled) -🤞 JSON.stringify() x 1,828,820 ops/sec ±0.15% (102 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 1,848,409 ops/sec ±0.56% (99 runs sampled) -🤞 json-joy/json-pack CborEncoder x 1,860,103 ops/sec ±0.18% (98 runs sampled) -🤞 cbor-x x 1,360,519 ops/sec ±0.22% (98 runs sampled) -🤞 cbor-js x 367,320 ops/sec ±0.25% (97 runs sampled) -🤞 cborg x 278,084 ops/sec ±0.15% (98 runs sampled) -🤞 cbor-sync x 181,966 ops/sec ±0.17% (92 runs sampled) -Fastest is 🤞 json-joy/json-pack CborEncoder ------------------------------------------------------------- Numbers, 136 bytes -🤞 Buffer.from(JSON.stringify()) x 1,231,696 ops/sec ±0.15% (100 runs sampled) -🤞 JSON.stringify() x 1,610,733 ops/sec ±0.16% (100 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 2,775,684 ops/sec ±0.17% (101 runs sampled) -🤞 json-joy/json-pack CborEncoder x 3,112,233 ops/sec ±0.18% (100 runs sampled) -🤞 cbor-x x 3,264,422 ops/sec ±0.14% (101 runs sampled) -🤞 cbor-js x 558,877 ops/sec ±1.31% (89 runs sampled) -🤞 cborg x 296,104 ops/sec ±0.14% (100 runs sampled) -🤞 cbor-sync x 379,437 ops/sec ±0.28% (99 runs sampled) -Fastest is 🤞 cbor-x -------------------------------------------------------------- Tokens, 308 bytes -🤞 Buffer.from(JSON.stringify()) x 1,101,690 ops/sec ±0.17% (98 runs sampled) -🤞 JSON.stringify() x 1,560,523 ops/sec ±0.14% (98 runs sampled) -🤞 json-joy/json-pack CborEncoderFast x 1,352,703 ops/sec ±0.24% (96 runs sampled) -🤞 json-joy/json-pack CborEncoder x 1,371,395 ops/sec ±0.24% (101 runs sampled) -🤞 cbor-x x 1,975,990 ops/sec ±0.19% (98 runs sampled) -🤞 cbor-js x 525,540 ops/sec ±1.25% (91 runs sampled) -🤞 cborg x 227,011 ops/sec ±0.15% (98 runs sampled) -🤞 cbor-sync x 418,451 ops/sec ±0.30% (97 runs sampled) -Fastest is 🤞 cbor-x -``` - -Node 18: - -``` -npx ts-node benchmarks/json-pack/bench.cbor.encoding.ts -=========================================================== Benchmark: Encoding -Warmup: 1000x , Node.js: v18.16.0 , Arch: arm64 , CPU: Apple M1 Max --------------------------------------------------------- Small object, 44 bytes -👍 json-joy/json-pack CborEncoderFast x 6,233,741 ops/sec ±0.48% (97 runs sampled) -👍 json-joy/json-pack CborEncoder x 6,284,071 ops/sec ±0.52% (98 runs sampled) -👍 cborg x 593,217 ops/sec ±0.75% (98 runs sampled) -👍 cbor-x x 4,360,950 ops/sec ±0.61% (92 runs sampled) -Fastest is 👍 json-joy/json-pack CborEncoder ------------------------------------------------------ Typical object, 993 bytes -👍 json-joy/json-pack CborEncoderFast x 450,797 ops/sec ±0.43% (94 runs sampled) -👍 json-joy/json-pack CborEncoder x 465,790 ops/sec ±0.39% (97 runs sampled) -👍 cborg x 48,343 ops/sec ±0.57% (99 runs sampled) -👍 cbor-x x 414,580 ops/sec ±0.38% (98 runs sampled) -Fastest is 👍 json-joy/json-pack CborEncoder ------------------------------------------------------- Large object, 3741 bytes -👍 json-joy/json-pack CborEncoderFast x 132,873 ops/sec ±0.37% (99 runs sampled) -👍 json-joy/json-pack CborEncoder x 134,572 ops/sec ±0.49% (96 runs sampled) -👍 cborg x 14,615 ops/sec ±0.59% (96 runs sampled) -👍 cbor-x x 114,106 ops/sec ±0.46% (100 runs sampled) -Fastest is 👍 json-joy/json-pack CborEncoder ------------------------------------------------- Very large object, 45750 bytes -👍 json-joy/json-pack CborEncoderFast x 5,498 ops/sec ±0.60% (97 runs sampled) -👍 json-joy/json-pack CborEncoder x 5,474 ops/sec ±1.15% (94 runs sampled) -👍 cborg x 659 ops/sec ±0.99% (92 runs sampled) -👍 cbor-x x 5,635 ops/sec ±0.76% (96 runs sampled) -Fastest is 👍 cbor-x ----------------------------------------------- Object with many keys, 969 bytes -👍 json-joy/json-pack CborEncoderFast x 279,077 ops/sec ±0.52% (96 runs sampled) -👍 json-joy/json-pack CborEncoder x 279,231 ops/sec ±0.35% (98 runs sampled) -👍 cborg x 26,533 ops/sec ±0.62% (95 runs sampled) -👍 cbor-x x 194,635 ops/sec ±0.58% (95 runs sampled) -Fastest is 👍 json-joy/json-pack CborEncoder,👍 json-joy/json-pack CborEncoderFast ------------------------------------------------------ String ladder, 3398 bytes -👍 json-joy/json-pack CborEncoderFast x 295,817 ops/sec ±0.61% (98 runs sampled) -👍 json-joy/json-pack CborEncoder x 293,260 ops/sec ±0.37% (97 runs sampled) -👍 cborg x 46,351 ops/sec ±0.46% (99 runs sampled) -👍 cbor-x x 221,037 ops/sec ±0.49% (96 runs sampled) -Fastest is 👍 json-joy/json-pack CborEncoderFast ------------------------------------------------------- Long strings, 7011 bytes -👍 json-joy/json-pack CborEncoderFast x 397,191 ops/sec ±1.10% (93 runs sampled) -👍 json-joy/json-pack CborEncoder x 393,080 ops/sec ±0.86% (91 runs sampled) -👍 cborg x 73,491 ops/sec ±0.51% (98 runs sampled) -👍 cbor-x x 386,859 ops/sec ±0.82% (94 runs sampled) -Fastest is 👍 json-joy/json-pack CborEncoderFast ------------------------------------------------------- Short strings, 170 bytes -👍 json-joy/json-pack CborEncoderFast x 1,746,092 ops/sec ±0.40% (98 runs sampled) -👍 json-joy/json-pack CborEncoder x 1,745,521 ops/sec ±0.40% (99 runs sampled) -👍 cborg x 198,683 ops/sec ±0.57% (96 runs sampled) -👍 cbor-x x 1,276,409 ops/sec ±0.62% (93 runs sampled) -Fastest is 👍 json-joy/json-pack CborEncoderFast,👍 json-joy/json-pack CborEncoder ------------------------------------------------------------- Numbers, 136 bytes -👍 json-joy/json-pack CborEncoderFast x 2,558,939 ops/sec ±0.46% (98 runs sampled) -👍 json-joy/json-pack CborEncoder x 2,575,323 ops/sec ±0.39% (95 runs sampled) -👍 cborg x 230,191 ops/sec ±0.40% (98 runs sampled) -👍 cbor-x x 2,966,610 ops/sec ±0.34% (97 runs sampled) -Fastest is 👍 cbor-x -------------------------------------------------------------- Tokens, 308 bytes -👍 json-joy/json-pack CborEncoderFast x 1,318,484 ops/sec ±0.45% (100 runs sampled) -👍 json-joy/json-pack CborEncoder x 1,332,239 ops/sec ±0.40% (100 runs sampled) -👍 cborg x 168,853 ops/sec ±0.42% (96 runs sampled) -👍 cbor-x x 1,824,744 ops/sec ±0.43% (95 runs sampled) -Fastest is 👍 cbor-x -``` - -#### Decoding - -``` -npx ts-node benchmarks/json-pack/bench.cbor.decoding.ts -====================================================== Benchmark: CBOR Decoding -Warmup: 1000x , Node.js: v20.2.0 , Arch: arm64 , CPU: Apple M1 --------------------------------------------------------- Combined, 634613 bytes -👍 json-joy/json-pack CborDecoder x 3,869 ops/sec ±0.18% (98 runs sampled) -👎 cbor-x x 3,636 ops/sec ±0.13% (100 runs sampled) -👍 cborg x 1,848 ops/sec ±0.27% (99 runs sampled) -👍 cbor x 313 ops/sec ±0.85% (95 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder -------------------------------------------------------- Small object, 274 bytes -👍 json-joy/json-pack CborDecoder x 4,547,927 ops/sec ±0.13% (98 runs sampled) -👍 cbor-x x 4,146,745 ops/sec ±0.15% (94 runs sampled) -👍 cborg x 1,979,229 ops/sec ±0.15% (99 runs sampled) -👍 cbor x 133,271 ops/sec ±2.51% (92 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder ----------------------------------------------------- Typical object, 8253 bytes -👍 json-joy/json-pack CborDecoder x 373,571 ops/sec ±0.33% (97 runs sampled) -👍 cbor-x x 254,533 ops/sec ±0.57% (99 runs sampled) -👍 cborg x 121,327 ops/sec ±0.36% (97 runs sampled) -👍 cbor x 19,516 ops/sec ±0.22% (98 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder ------------------------------------------------------ Large object, 34563 bytes -👍 json-joy/json-pack CborDecoder x 108,250 ops/sec ±0.70% (96 runs sampled) -👍 cbor-x x 86,146 ops/sec ±0.32% (101 runs sampled) -👍 cborg x 33,641 ops/sec ±0.56% (93 runs sampled) -👍 cbor x 6,383 ops/sec ±0.58% (97 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder ------------------------------------------------ Very large object, 437014 bytes -👍 json-joy/json-pack CborDecoder x 4,374 ops/sec ±0.31% (94 runs sampled) -👎 cbor-x x 3,943 ops/sec ±0.30% (98 runs sampled) -👍 cborg x 1,685 ops/sec ±0.29% (79 runs sampled) -👍 cbor x 310 ops/sec ±0.15% (89 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder ---------------------------------------------- Object with many keys, 7575 bytes -👍 json-joy/json-pack CborDecoder x 92,625 ops/sec ±0.51% (95 runs sampled) -👎 cbor-x x 91,511 ops/sec ±0.94% (93 runs sampled) -👍 cborg x 54,355 ops/sec ±0.41% (97 runs sampled) -👍 cbor x 13,289 ops/sec ±1.41% (99 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder,👎 cbor-x ----------------------------------------------------- String ladder, 35622 bytes -👍 json-joy/json-pack CborDecoder x 240,683 ops/sec ±0.34% (100 runs sampled) -👍 cbor-x x 324,927 ops/sec ±0.40% (96 runs sampled) -👍 cborg x 70,820 ops/sec ±0.58% (95 runs sampled) -👍 cbor x 24,792 ops/sec ±0.76% (96 runs sampled) -Fastest is 👍 cbor-x ------------------------------------------------------ Long strings, 85228 bytes -👍 json-joy/json-pack CborDecoder x 96,957 ops/sec ±0.50% (98 runs sampled) -👍 cbor-x x 94,397 ops/sec ±0.51% (94 runs sampled) -👍 cborg x 69,925 ops/sec ±6.38% (91 runs sampled) -👍 cbor x 34,779 ops/sec ±10.73% (79 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder ------------------------------------------------------ Short strings, 1211 bytes -👍 json-joy/json-pack CborDecoder x 1,177,079 ops/sec ±0.61% (94 runs sampled) -👍 cbor-x x 1,070,770 ops/sec ±1.19% (90 runs sampled) -👍 cborg x 385,823 ops/sec ±0.79% (94 runs sampled) -👍 cbor x 53,147 ops/sec ±0.91% (91 runs sampled) -Fastest is 👍 json-joy/json-pack CborDecoder ------------------------------------------------------------ Numbers, 1544 bytes -👍 json-joy/json-pack CborDecoder x 974,821 ops/sec ±0.72% (98 runs sampled) -👍 cbor-x x 1,576,220 ops/sec ±0.68% (95 runs sampled) -👍 cborg x 464,996 ops/sec ±0.44% (94 runs sampled) -👍 cbor x 34,161 ops/sec ±0.76% (92 runs sampled) -Fastest is 👍 cbor-x -------------------------------------------------------------- Tokens, 530 bytes -👍 json-joy/json-pack CborDecoder x 1,198,726 ops/sec ±0.53% (96 runs sampled) -👍 cbor-x x 1,927,307 ops/sec ±0.67% (80 runs sampled) -👍 cborg x 957,531 ops/sec ±0.62% (98 runs sampled) -👍 cbor x 44,276 ops/sec ±10.58% (80 runs sampled) -Fastest is 👍 cbor-x -``` - - -### Benchmarks of `Writer` buffer size - -When encoding bytes `CborEncoder` uses `Writer` class to write bytes to the underlying buffer. -The `Writer` preallocates a buffer of a certain size and then writes bytes to it. - -Below benchmarks test various default buffer size allocation sizes of the underlying -`Writer` class. The default buffer size is 64 KB, but it can be changed to any value. - -``` -npx ts-node benchmarks/json-pack/bench.writer-size.ts -=========================================================== Benchmark: Encoding -Warmup: 1000x , Node.js: v18.16.0 , Arch: arm64 , CPU: Apple M1 Max --------------------------------------------------------- Small object, 44 bytes -👍 1 MB x 6,313,890 ops/sec ±0.12% (101 runs sampled) -👍 256 KB x 6,289,685 ops/sec ±0.11% (97 runs sampled) -👍 64 KB x 6,275,863 ops/sec ±0.12% (100 runs sampled) -👍 16 KB x 6,254,832 ops/sec ±0.24% (98 runs sampled) -👍 4 KB x 6,187,636 ops/sec ±0.13% (99 runs sampled) -👍 1 KB x 5,890,157 ops/sec ±0.14% (99 runs sampled) -Fastest is 👍 1 MB ------------------------------------------------------ Typical object, 993 bytes -👍 1 MB x 497,752 ops/sec ±0.21% (100 runs sampled) -👍 256 KB x 495,574 ops/sec ±0.15% (99 runs sampled) -👍 64 KB x 494,724 ops/sec ±0.15% (98 runs sampled) -👍 16 KB x 489,579 ops/sec ±0.23% (97 runs sampled) -👍 4 KB x 455,526 ops/sec ±0.34% (98 runs sampled) -👍 1 KB x 433,038 ops/sec ±0.48% (97 runs sampled) -Fastest is 👍 1 MB ------------------------------------------------------- Large object, 3741 bytes -👍 1 MB x 140,580 ops/sec ±0.39% (96 runs sampled) -👍 256 KB x 136,933 ops/sec ±0.39% (92 runs sampled) -👍 64 KB x 139,697 ops/sec ±0.27% (98 runs sampled) -👍 16 KB x 137,278 ops/sec ±0.33% (98 runs sampled) -👍 4 KB x 130,838 ops/sec ±0.19% (98 runs sampled) -👍 1 KB x 122,987 ops/sec ±0.45% (94 runs sampled) -Fastest is 👍 1 MB ------------------------------------------------- Very large object, 45750 bytes -👍 1 MB x 5,883 ops/sec ±0.12% (101 runs sampled) -👍 256 KB x 5,845 ops/sec ±0.66% (91 runs sampled) -👍 64 KB x 5,783 ops/sec ±0.26% (100 runs sampled) -👍 16 KB x 5,584 ops/sec ±0.59% (94 runs sampled) -👍 4 KB x 5,648 ops/sec ±0.35% (98 runs sampled) -👍 1 KB x 5,649 ops/sec ±0.35% (95 runs sampled) -Fastest is 👍 1 MB,👍 256 KB ----------------------------------------------- Object with many keys, 969 bytes -👍 1 MB x 282,535 ops/sec ±0.34% (98 runs sampled) -👍 256 KB x 282,055 ops/sec ±0.34% (95 runs sampled) -👍 64 KB x 286,786 ops/sec ±0.22% (97 runs sampled) -👍 16 KB x 283,067 ops/sec ±0.27% (97 runs sampled) -👍 4 KB x 281,647 ops/sec ±0.24% (100 runs sampled) -👍 1 KB x 259,775 ops/sec ±0.33% (96 runs sampled) -Fastest is 👍 64 KB ------------------------------------------------------ String ladder, 3398 bytes -👍 1 MB x 308,326 ops/sec ±0.23% (96 runs sampled) -👍 256 KB x 307,324 ops/sec ±0.34% (100 runs sampled) -👍 64 KB x 305,368 ops/sec ±0.23% (97 runs sampled) -👍 16 KB x 289,570 ops/sec ±0.46% (99 runs sampled) -👍 4 KB x 270,486 ops/sec ±0.52% (96 runs sampled) -👍 1 KB x 211,091 ops/sec ±0.57% (95 runs sampled) -Fastest is 👍 1 MB,👍 256 KB ------------------------------------------------------- Long strings, 7011 bytes -👍 1 MB x 446,622 ops/sec ±0.48% (98 runs sampled) -👍 256 KB x 438,083 ops/sec ±0.58% (94 runs sampled) -👍 64 KB x 421,277 ops/sec ±0.50% (97 runs sampled) -👍 16 KB x 349,768 ops/sec ±1.32% (93 runs sampled) -👍 4 KB x 350,886 ops/sec ±0.76% (92 runs sampled) -👍 1 KB x 348,879 ops/sec ±1.00% (92 runs sampled) -Fastest is 👍 1 MB ------------------------------------------------------- Short strings, 170 bytes -👍 1 MB x 2,003,291 ops/sec ±0.18% (99 runs sampled) -👍 256 KB x 2,002,815 ops/sec ±0.30% (98 runs sampled) -👍 64 KB x 2,003,416 ops/sec ±0.22% (98 runs sampled) -👍 16 KB x 1,973,326 ops/sec ±0.31% (96 runs sampled) -👍 4 KB x 1,938,991 ops/sec ±0.28% (98 runs sampled) -👍 1 KB x 1,815,441 ops/sec ±0.24% (99 runs sampled) -Fastest is 👍 1 MB,👍 64 KB,👍 256 KB ------------------------------------------------------------- Numbers, 136 bytes -👍 1 MB x 3,301,798 ops/sec ±0.25% (99 runs sampled) -👍 256 KB x 3,284,645 ops/sec ±0.30% (98 runs sampled) -👍 64 KB x 3,272,060 ops/sec ±0.94% (96 runs sampled) -👍 16 KB x 3,317,569 ops/sec ±0.25% (98 runs sampled) -👍 4 KB x 3,238,186 ops/sec ±0.34% (96 runs sampled) -👍 1 KB x 3,017,336 ops/sec ±0.68% (98 runs sampled) -Fastest is 👍 16 KB -------------------------------------------------------------- Tokens, 308 bytes -👍 1 MB x 1,698,059 ops/sec ±0.24% (101 runs sampled) -👍 256 KB x 1,644,210 ops/sec ±0.70% (99 runs sampled) -👍 64 KB x 1,680,855 ops/sec ±0.22% (97 runs sampled) -👍 16 KB x 1,651,801 ops/sec ±0.35% (97 runs sampled) -👍 4 KB x 1,634,786 ops/sec ±0.72% (95 runs sampled) -👍 1 KB x 1,633,724 ops/sec ±0.25% (98 runs sampled) -Fastest is 👍 1 MB -``` - - -### Result Slices - -The method `.encode()` returns a `Uint8Array` but you can also get a -`Slice` which is a view into the underlying buffer. Create a `Slice` is much -faster than creating a `Uint8Array`. - -This overhead becomes significant when encoding small objects. To receive a -`Slice` as a result, use the `.encodeToSlice()` method. - -``` -npx ts-node benchmarks/json-pack/bench.slice.ts -=========================================================== Benchmark: Encoding -Warmup: 1000x , Node.js: v18.16.0 , Arch: arm64 , CPU: Apple M1 Max --------------------------------------------------------- Small object, 44 bytes -👍 Uint8Array x 6,375,191 ops/sec ±0.29% (99 runs sampled) -👎 Slice x 7,477,318 ops/sec ±0.24% (99 runs sampled) -Fastest is 👎 Slice ------------------------------------------------------ Typical object, 993 bytes -👍 Uint8Array x 481,245 ops/sec ±0.27% (95 runs sampled) -👎 Slice x 487,881 ops/sec ±0.24% (95 runs sampled) -Fastest is 👎 Slice ------------------------------------------------------- Large object, 3741 bytes -👍 Uint8Array x 139,034 ops/sec ±0.28% (99 runs sampled) -👎 Slice x 139,084 ops/sec ±0.30% (93 runs sampled) -Fastest is 👎 Slice,👍 Uint8Array ------------------------------------------------- Very large object, 45750 bytes -👍 Uint8Array x 5,992 ops/sec ±0.17% (98 runs sampled) -👎 Slice x 5,973 ops/sec ±0.18% (101 runs sampled) -Fastest is 👍 Uint8Array ----------------------------------------------- Object with many keys, 969 bytes -👍 Uint8Array x 283,511 ops/sec ±0.21% (96 runs sampled) -👎 Slice x 284,962 ops/sec ±0.20% (100 runs sampled) -Fastest is 👎 Slice ------------------------------------------------------ String ladder, 3398 bytes -👍 Uint8Array x 321,418 ops/sec ±0.36% (97 runs sampled) -👎 Slice x 324,213 ops/sec ±0.34% (99 runs sampled) -Fastest is 👎 Slice ------------------------------------------------------- Long strings, 7011 bytes -👍 Uint8Array x 417,711 ops/sec ±0.72% (94 runs sampled) -👎 Slice x 421,504 ops/sec ±0.72% (94 runs sampled) -Fastest is 👎 Slice ------------------------------------------------------- Short strings, 170 bytes -👍 Uint8Array x 2,186,736 ops/sec ±0.21% (97 runs sampled) -👎 Slice x 2,283,908 ops/sec ±0.26% (98 runs sampled) -Fastest is 👎 Slice ------------------------------------------------------------- Numbers, 136 bytes -👍 Uint8Array x 3,305,268 ops/sec ±0.21% (100 runs sampled) -👎 Slice x 3,526,413 ops/sec ±0.32% (97 runs sampled) -Fastest is 👎 Slice -------------------------------------------------------------- Tokens, 308 bytes -👍 Uint8Array x 1,681,882 ops/sec ±0.14% (100 runs sampled) -👎 Slice x 1,721,419 ops/sec ±0.35% (97 runs sampled) -Fastest is 👎 Slice -``` diff --git a/packages/json-joy/docs/json-pack/cbor/benchmarks/text.ts b/packages/json-joy/docs/json-pack/cbor/benchmarks/text.ts deleted file mode 100644 index 87ba8e1486..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/benchmarks/text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {md} from '@jsonjoy.com/ui/lib/markdown/parser'; - -const markdown = require('./text.md').default; -export const text = md(markdown); diff --git a/packages/json-joy/docs/json-pack/cbor/index.ts b/packages/json-joy/docs/json-pack/cbor/index.ts deleted file mode 100644 index 29e8f11c96..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; -import {page as partialDecodingPage} from './partial-decoding'; -import {page as benchmarksPage} from './benchmarks'; - -export const page: ContentPage = { - name: 'CBOR', - // title: 'json-pack', - subtitle: 'CBOR serialization and deserialization codec', - slug: 'cbor', - children: [partialDecodingPage, benchmarksPage], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/cbor/partial-decoding/index.ts b/packages/json-joy/docs/json-pack/cbor/partial-decoding/index.ts deleted file mode 100644 index 19bfe64e09..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/partial-decoding/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; - -export const page: ContentPage = { - name: 'Partial decoding', - slug: 'partial-decoding', - children: [], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/cbor/partial-decoding/text.md b/packages/json-joy/docs/json-pack/cbor/partial-decoding/text.md deleted file mode 100644 index 9d04756055..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/partial-decoding/text.md +++ /dev/null @@ -1,85 +0,0 @@ -The `CborDecoder` class allows to parse values from a CBOR blob without fully -decoding it. This is useful when you want to save CPU or memory resources, when -you only need few specific values from a larger document. - -~~~jj.note -[Try this example][link] without installation on RunKit. - -[link]: https://runkit.com/streamich/json-joy-json-pack-cbor-partial-decoding -~~~ - -For these advanced decoding features you will need the to use the `CborDecoder` -class. Instantiate the decoder: - -```ts -const decoder = new CborDecoder(); -``` - -For our examples we will use the following CBOR encoded document: - -```ts -const pojo = { - id: 123, - foo: 'bar', - tags: ['a', 'b', 'c'], - nested: { - a: 1, - b: 2, - level2: { - c: 3, - } - }, -}; - -const encoder = new CborEncoder(); -const encoded = encoder.encode(pojo); -``` - - -### One level decoding - -Then use the `decodeLevel()` method to decode only one level of JSON nesting, -while returning the rest of the document as a `Uint8Array` blobs. - -```ts -const decodedLevel = decoder.decodeLevel(encoded); - -console.log(decodedLevel); -// { -// id: 123, -// foo: 'bar', -// tags: JsonPackValue { -// val: Uint8Array(7) [ -// 131, 97, 97, 97, -// 98, 97, 99 -// ] -// }, -// nested: JsonPackValue { -// val: Uint8Array(19) [ -// 163, 97, 97, 1, 97, 98, 2, -// 120, 6, 108, 101, 118, 101, 108, -// 50, 161, 97, 99, 3 -// ] -// } -// } -``` - - -### Finding by path (skipping) - -You can also retrieve specific values from the document by path without -decoding the whole document. The decoder skips over the values that are not -needed and returns only the value specified by the path. - -```ts -decoder.reader.reset(encoded); -const tag = decoder.find(['tags', 1]).val(); - -console.log(tag); -// b -``` - -Skipping is much faster than full decoding, if you need to retrieve only one -or few values from the document. If you need to retrieve many values from the -document, it is better to use either the one level decoding or full document -decoding. diff --git a/packages/json-joy/docs/json-pack/cbor/partial-decoding/text.ts b/packages/json-joy/docs/json-pack/cbor/partial-decoding/text.ts deleted file mode 100644 index 87ba8e1486..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/partial-decoding/text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {md} from '@jsonjoy.com/ui/lib/markdown/parser'; - -const markdown = require('./text.md').default; -export const text = md(markdown); diff --git a/packages/json-joy/docs/json-pack/cbor/text.md b/packages/json-joy/docs/json-pack/cbor/text.md deleted file mode 100644 index 64022a5c97..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/text.md +++ /dev/null @@ -1,82 +0,0 @@ -`json-pack` implements fast [CBOR][cbor] encoder and decoder. It is written in TypeScript -and has no external dependencies. - - -[cbor]: https://cbor.io/ - - -## Getting started - -~~~jj.note -[Try this example][link] without installation on RunKit. - -[link]: https://runkit.com/streamich/json-joy-json-pack-cbor-codec -~~~ - -To get started you need to import `CborEncoder` and `CborDecoder` classes like -this: - -```ts -import {CborEncoder} from 'json-joy/es2020/json-pack/cbor/CborEncoder'; -import {CborDecoder} from 'json-joy/es2020/json-pack/cbor/CborDecoder'; -``` - -The `CborDecoder` implements full decoding feature set including advanced -features like value skipping and decoding one level at-a-time. Those features -are not necessary for most use cases, to save on bundle size you can import -the "base" decoder instead: - -```ts -import {CborDecoderBase} from 'json-joy/es2020/json-pack/cbor/CborDecoderBase'; -``` - -The base decoder implements all CBOR decoding features except for the advanced -shallow decoding features, like skipping, one level at-a-time decoding. - - -## Usage - -Encode a JavaScript POJO to CBOR: - -```ts -const encoder = new CborEncoder(); - -const pojo = { - id: 123, - foo: 'bar', - tags: ['a', 'b', 'c'], - nested: { - a: 1, - b: 2, - level2: { - c: 3, - } - }, -}; - -const encoded = encoder.encode(pojo); -console.log(encoded); -// Uint8Array(53) [ -// 164, 98, 105, 100, 24, 123, 99, 102, 111, 111, -// 99, 98, 97, 114, 100, 116, 97, 103, 115, 131, -// 97, 97, 97, 98, 97, 99, 120, 6, 110, 101, -// 115, 116, 101, 100, 163, 97, 97, 1, 97, 98, -// 2, 120, 6, 108, 101, 118, 101, 108, 50, 161, -// 97, 99, 3 -// ] -``` - -Decode CBOR back to JavaScript POJO: - -```ts -const decoderBase = new CborDecoderBase(); -const decoded = decoderBase.read(encoded); - -console.log(decoded); -// { -// id: 123, -// foo: 'bar', -// tags: [ 'a', 'b', 'c' ], -// nested: { a: 1, b: 2, level2: { c: 3 } } -// } -``` diff --git a/packages/json-joy/docs/json-pack/cbor/text.ts b/packages/json-joy/docs/json-pack/cbor/text.ts deleted file mode 100644 index 87ba8e1486..0000000000 --- a/packages/json-joy/docs/json-pack/cbor/text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {md} from '@jsonjoy.com/ui/lib/markdown/parser'; - -const markdown = require('./text.md').default; -export const text = md(markdown); diff --git a/packages/json-joy/docs/json-pack/index.ts b/packages/json-joy/docs/json-pack/index.ts deleted file mode 100644 index d98949f491..0000000000 --- a/packages/json-joy/docs/json-pack/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; -import {page as cborPage} from './cbor'; -import {page as msgpackPage} from './msgpack'; -import {page as jsonPage} from './json'; -import {page as ubjsonPage} from './ubjson'; - -export const page: ContentPage = { - name: 'json-pack', - // title: 'json-pack', - subtitle: 'JSON serialization and deserialization codecs', - slug: 'json-pack', - children: [cborPage, msgpackPage, jsonPage, ubjsonPage], - // @ts-ignore - src: async () => (await import('./text.md')).default, - showContentsTable: true, -}; diff --git a/packages/json-joy/docs/json-pack/json/index.ts b/packages/json-joy/docs/json-pack/json/index.ts deleted file mode 100644 index 2782f78da2..0000000000 --- a/packages/json-joy/docs/json-pack/json/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; - -export const page: ContentPage = { - name: 'JSON', - subtitle: 'JSON serialization and deserialization codec', - slug: 'json', - children: [], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/json/text.md b/packages/json-joy/docs/json-pack/json/text.md deleted file mode 100644 index dccc7e0abc..0000000000 --- a/packages/json-joy/docs/json-pack/json/text.md +++ /dev/null @@ -1,100 +0,0 @@ -`json-pack` implements fast [JSON][json] encoder and decoder. It is written in TypeScript -and has no external dependencies. - -`json-pack` JSON codec supports binary data and is faster than the -native way to encode to a `Uint8Array`, however, JavaScript built-in JSON parser -is faster. - - -[json]: https://www.json.org/json-en.html - - -## Getting started - -To get started you need to import `JsonEncoder` and `JsonDecoder` classes like -this: - -```ts -import {JsonEncoder} from 'json-joy/es2020/json-pack/json/JsonEncoder'; -import {JsonDecoder} from 'json-joy/es2020/json-pack/json/JsonDecoder'; -import {Writer} from 'json-joy/es2020/util/buffers/Writer'; -``` - - -## Usage - -Encode a JavaScript POJO to JSON (as `Uint8Array`): - -```ts -const pojo = { - id: 123, - foo: 'bar', - tags: ['a', 'b', 'c'], - binary: new Uint8Array([1, 2, 3]), -}; -const encoder = new JsonEncoder(new Writer()); -const encoded = encoder.encode(pojo); -``` - -Decode JSON `Uint8Array` back to JavaScript POJO: - -```ts -const decoder = new JsonDecoder(); -const decoded = decoder.read(encoded); - -console.log(decoded); -// { -// id: 123, -// foo: 'bar', -// tags: [ 'a', 'b', 'c' ], -// binary: Uint8Array(3) [ 1, 2, 3 ] -// } -``` - - -## Binary data - -If your POJO contains binary data, it will be encoded as a JSON string, when -decoding, it will be decoded back to a `Uint8Array`. `json-pack` encodes binary -blobs as data URLs. - -```ts -const blob = encoder.encode({binary: new Uint8Array([1, 2, 3])}); - -console.log(Buffer.from(blob).toString()); -// {"binary":"data:application/octet-stream;base64,AQID"} -``` - -~~~jj.note -`json-pack` ships with the fastest Base64 encoder and decoder for JavaScript. -It encodes binary data directly to a `Uint8Array` without any intermediate -encoding to a Base64 string. - -Likewise, when parsing, it decodes Base64 strings directly from the `Uint8Array` -without any intermediate decoding to a string. -~~~ - - -## Benchmarks - -`json-pack` JSON encoder generally performs better than the native way using -` Buffer.from(JSON.stringify(obj))`. However, the native Node.js JSON decoder `JSON.parse(buf.toString())` is -faster than `json-pack` JSON decoder. - -~~~jj.aside -Benchmark were performed using Node.js v20.2.0 on Apple M1 chip. The numbers show -the number of POJO objects encoded/decoded per second. -~~~ - -![json-codec-benchmark-results](https://appsets.jsonjoy.com/libraries/json-joy-js/json-pack/json-codec-results.png) - - -### Running the benchmarks - -You can run the benchmarks yourself by cloning the `json-joy` repository and -running the following commands: - -``` -npx ts-node benchmarks/json-pack/bench.json.encoding.ts -npx ts-node benchmarks/json-pack/bench.json.decoding.ts -``` diff --git a/packages/json-joy/docs/json-pack/json/text.ts b/packages/json-joy/docs/json-pack/json/text.ts deleted file mode 100644 index 87ba8e1486..0000000000 --- a/packages/json-joy/docs/json-pack/json/text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {md} from '@jsonjoy.com/ui/lib/markdown/parser'; - -const markdown = require('./text.md').default; -export const text = md(markdown); diff --git a/packages/json-joy/docs/json-pack/msgpack/benchmarks/index.ts b/packages/json-joy/docs/json-pack/msgpack/benchmarks/index.ts deleted file mode 100644 index 4fa31ff206..0000000000 --- a/packages/json-joy/docs/json-pack/msgpack/benchmarks/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; - -export const page: ContentPage = { - name: 'Benchmarks', - slug: 'benchmarks', - children: [], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/msgpack/benchmarks/text.md b/packages/json-joy/docs/json-pack/msgpack/benchmarks/text.md deleted file mode 100644 index 5d2a88cce0..0000000000 --- a/packages/json-joy/docs/json-pack/msgpack/benchmarks/text.md +++ /dev/null @@ -1,30 +0,0 @@ -`json-pack` ships the fastest MessagePack encoder and decoder in the NPM ecosystem. -The benchmarks below show that `json-pack` is 40x faster than `messagepack` package, -about 2x faster than `@msgpack/msgpack`, and even slightly faster than the highly -optimized `msgpackr` package. - -~~~jj.note -Benchmark were performed using Node.js v20.2.0 on Apple M1 chip. The numbers show -the number of POJO objects encoded/decoded per second. -~~~ - - -## Encoding - -![msgpack-encoding-benchmark-results](https://appsets.jsonjoy.com/libraries/json-joy-js/json-pack/msgpack-encoding-results.png) - - -## Decoding - -![msgpack-decoding-benchmark-results](https://appsets.jsonjoy.com/libraries/json-joy-js/json-pack/msgpack-decoding-results.png) - - -## Running the benchmarks - -You can run the benchmarks yourself by cloning the `json-joy` repository and -running the following commands: - -``` -npx ts-node benchmarks/json-pack/bench.msgpack.encoding.ts -npx ts-node benchmarks/json-pack/bench.msgpack.decoding.ts -``` diff --git a/packages/json-joy/docs/json-pack/msgpack/index.ts b/packages/json-joy/docs/json-pack/msgpack/index.ts deleted file mode 100644 index a8b8c98022..0000000000 --- a/packages/json-joy/docs/json-pack/msgpack/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; -import {page as partialDecodingPage} from './partial-decoding'; -import {page as benchmarksPage} from './benchmarks'; - -export const page: ContentPage = { - name: 'MessagePack', - subtitle: 'MessagePack serialization and deserialization codec', - slug: 'msgpack', - children: [partialDecodingPage, benchmarksPage], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/msgpack/partial-decoding/index.ts b/packages/json-joy/docs/json-pack/msgpack/partial-decoding/index.ts deleted file mode 100644 index 19bfe64e09..0000000000 --- a/packages/json-joy/docs/json-pack/msgpack/partial-decoding/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; - -export const page: ContentPage = { - name: 'Partial decoding', - slug: 'partial-decoding', - children: [], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/msgpack/partial-decoding/text.md b/packages/json-joy/docs/json-pack/msgpack/partial-decoding/text.md deleted file mode 100644 index 39f223831b..0000000000 --- a/packages/json-joy/docs/json-pack/msgpack/partial-decoding/text.md +++ /dev/null @@ -1,22 +0,0 @@ -The `MsgPackDecoder` class allows to parse values from a MessagePack blob without fully -decoding it. This is useful when you want to save CPU or memory resources, when -you only need few specific values from a larger document. - - -### Finding by path (skipping) - -You can retrieve specific values from the document by path without -decoding the whole document. The decoder skips over the values that are not -needed and returns only the value specified by the path. - -```ts -decoder.reader.reset(encoded); -const tag = decoder.find(['tags', 1]).val(); - -console.log(tag); -// b -``` - -Skipping is much faster than full decoding, if you need to retrieve only one -or few values from the document. If you need to retrieve many values from the -document, it could be better to parse the whole document. diff --git a/packages/json-joy/docs/json-pack/msgpack/partial-decoding/text.ts b/packages/json-joy/docs/json-pack/msgpack/partial-decoding/text.ts deleted file mode 100644 index 87ba8e1486..0000000000 --- a/packages/json-joy/docs/json-pack/msgpack/partial-decoding/text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {md} from '@jsonjoy.com/ui/lib/markdown/parser'; - -const markdown = require('./text.md').default; -export const text = md(markdown); diff --git a/packages/json-joy/docs/json-pack/msgpack/text.md b/packages/json-joy/docs/json-pack/msgpack/text.md deleted file mode 100644 index 146e317e48..0000000000 --- a/packages/json-joy/docs/json-pack/msgpack/text.md +++ /dev/null @@ -1,63 +0,0 @@ -`json-pack` implements fast [MessagePack][msgpack] encoder and decoder. It is written in TypeScript -and has no external dependencies. - - -[msgpack]: https://msgpack.org/ - - -## Getting started - -To get started you need to import `MsgPackEncoder` and `MsgPackDecoder` classes like -this: - -```ts -import {MsgPackEncoder} from 'json-joy/es2020/json-pack/msgpack/MsgPackEncoder'; -import {MsgPackDecoder} from 'json-joy/es2020/json-pack/msgpack/MsgPackDecoder'; -``` - - -## Usage - -Encode a JavaScript POJO to MessagePack: - -```ts -const pojo = { - id: 123, - foo: 'bar', - tags: ['a', 'b', 'c'], - nested: { - a: 1, - b: 2, - level2: { - c: 3, - } - }, -}; -const encoder = new MsgPackEncoder(); -const encoded = encoder.encode(pojo); - -console.log(encoded); -// Uint8Array(50) [ -// 132, 162, 105, 100, 123, 163, 102, 111, 111, -// 163, 98, 97, 114, 164, 116, 97, 103, 115, -// 147, 161, 97, 161, 98, 161, 99, 166, 110, -// 101, 115, 116, 101, 100, 131, 161, 97, 1, -// 161, 98, 2, 166, 108, 101, 118, 101, 108, -// 50, 129, 161, 99, 3 -// ] -``` - -Decode MessagePack back to JavaScript POJO: - -```ts -const decoder = new MsgPackDecoder(); -const decoded = decoder.read(encoded); - -console.log(decoded); -// { -// id: 123, -// foo: 'bar', -// tags: [ 'a', 'b', 'c' ], -// nested: { a: 1, b: 2, level2: { c: 3 } } -// } -``` diff --git a/packages/json-joy/docs/json-pack/text.md b/packages/json-joy/docs/json-pack/text.md deleted file mode 100644 index 97c6cfa147..0000000000 --- a/packages/json-joy/docs/json-pack/text.md +++ /dev/null @@ -1,9 +0,0 @@ -`json-pack` implements the fastest JSON serialization and deserialization algorithms -in NPM ecosystem. This module implements [CBOR][cbor], [MessagePack][msgpack], -[JSON][json], and [UBJSON][ubjson] codecs. The module does not use any external -dependencies and it works, both, in Node.js and in browsers. - -[cbor]: https://cbor.io/ -[msgpack]: https://msgpack.org/ -[json]: https://www.json.org/json-en.html -[ubjson]: http://ubjson.org/ diff --git a/packages/json-joy/docs/json-pack/ubjson/index.ts b/packages/json-joy/docs/json-pack/ubjson/index.ts deleted file mode 100644 index a585afe662..0000000000 --- a/packages/json-joy/docs/json-pack/ubjson/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {ContentPage} from '@jsonjoy.com/ui/src/types/libs'; - -export const page: ContentPage = { - name: 'UBJSON', - subtitle: 'UBJSON serialization and deserialization codec', - slug: 'ubjson', - children: [], - // @ts-ignore - src: async () => (await import('./text.md')).default, -}; diff --git a/packages/json-joy/docs/json-pack/ubjson/text.md b/packages/json-joy/docs/json-pack/ubjson/text.md deleted file mode 100644 index 26d595171a..0000000000 --- a/packages/json-joy/docs/json-pack/ubjson/text.md +++ /dev/null @@ -1,98 +0,0 @@ -`json-pack` implements fast [UBJSON][ubjson] encoder and decoder. It is written in TypeScript -and has no external dependencies. - -~~~jj.note -`json-pack` UBJSON codec supports all JSON types as well as binary data, using -the ``js Uint8Array`` class. So, you can encode and decode any JSON POJO and -binary data. - -However, `json-pack` UBJSON decoder does not support [*Optimized Format*](https://ubjson.org/type-reference/container-types/). -Hence, if you will try to decode a UBJSON document that uses *Optimized Format*, -the decoder will throw an error. -~~~ - -[ubjson]: https://ubjson.org/ - - -## Getting started - -To get started you need to import `UbjsonEncoder` and `UbjsonDecoder` classes like -this: - -```ts -import {UbjsonEncoder} from 'json-joy/es2020/json-pack/ubjson/UbjsonEncoder'; -import {UbjsonDecoder} from 'json-joy/es2020/json-pack/ubjson/UbjsonDecoder'; -import {Writer} from 'json-joy/es2020/util/buffers/Writer'; -``` - - -## Usage - -Encode a JavaScript POJO to UBJSON: - -```ts -const pojo = { - id: 123, - foo: 'bar', - tags: ['a', 'b', 'c'], - binary: new Uint8Array([1, 2, 3]), -}; -const encoder = new UbjsonEncoder(new Writer()); -const encoded = encoder.encode(pojo); - -console.log(encoded); -// Uint8Array(56) [ -// 123, 85, 2, 105, 100, 85, 123, 85, 3, 102, 111, 111, -// 83, 85, 3, 98, 97, 114, 85, 4, 116, 97, 103, 115, -// 91, 83, 85, 1, 97, 83, 85, 1, 98, 83, 85, 1, -// 99, 93, 85, 6, 98, 105, 110, 97, 114, 121, 91, 36, -// 85, 35, 85, 3, 1, 2, 3, 125 -// ] -``` - -Decode UBJSON back to JavaScript POJO: - -```ts -const decoder = new UbjsonDecoder(); -const decoded = decoder.read(encoded); - -console.log(decoded); -// { -// id: 123, -// foo: 'bar', -// tags: [ 'a', 'b', 'c' ], -// binary: Uint8Array(3) [ 1, 2, 3 ] -// } -``` - - -## Benchmarks - -`json-pack` ships the fastest UBJSON encoder and decoder in NPM ecosystem. The -encoder is at least 20x faster than `@shelacek/ubjson`. - -~~~jj.aside -Benchmark were performed using Node.js v20.2.0 on Apple M1 chip. The numbers show -the number of POJO objects encoded/decoded per second. -~~~ - - -### Encoding - -![ubjson-encoding-benchmark-results](https://appsets.jsonjoy.com/libraries/json-joy-js/json-pack/ubjson-encoding-results.png) - - -### Decoding - -![ubjson-decoding-benchmark-results](https://appsets.jsonjoy.com/libraries/json-joy-js/json-pack/ubjson-decoding-results.png) - - -### Running the benchmarks - -You can run the benchmarks yourself by cloning the `json-joy` repository and -running the following commands: - -``` -npx ts-node benchmarks/json-pack/bench.ubjson.encoding.ts -npx ts-node benchmarks/json-pack/bench.ubjson.decoding.ts -``` diff --git a/packages/json-joy/docs/json-pack/ubjson/text.ts b/packages/json-joy/docs/json-pack/ubjson/text.ts deleted file mode 100644 index 87ba8e1486..0000000000 --- a/packages/json-joy/docs/json-pack/ubjson/text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {md} from '@jsonjoy.com/ui/lib/markdown/parser'; - -const markdown = require('./text.md').default; -export const text = md(markdown); diff --git a/packages/json-joy/package.json b/packages/json-joy/package.json index d17c940fd6..16ca7e8273 100644 --- a/packages/json-joy/package.json +++ b/packages/json-joy/package.json @@ -107,7 +107,7 @@ "fast-diff": "^1.3.0", "fast-json-patch": "^3.1.1", "html-webpack-plugin": "^5.6.0", - "jest": "^29.7.0", + "jest": "^30.2.0", "json-crdt-traces": "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d", "json-logic-js": "^2.0.2", "nano-theme": "workspace:*", diff --git a/packages/json-joy/src/json-crdt-extensions/peritext/editor/Editor.ts b/packages/json-joy/src/json-crdt-extensions/peritext/editor/Editor.ts index 2ab3c3d085..11075dd752 100644 --- a/packages/json-joy/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/packages/json-joy/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -1508,7 +1508,7 @@ export class Editor implements Printable { const x1Dest = x1Capped + pos; const annotationLength = x2Capped - x1Capped; const range = txt.rangeAt(x1Dest, annotationLength); - if (!!x1Dest && anchor1 === Anchor.After) range.start.refAfter(); + if (x1Dest && anchor1 === Anchor.After) range.start.refAfter(); // else range.start.refBefore(); if (anchor2 === Anchor.Before) range.end.refBefore(); // else range.end.refAfter(); diff --git a/packages/json-joy/src/json-crdt-extensions/peritext/rga/Point.ts b/packages/json-joy/src/json-crdt-extensions/peritext/rga/Point.ts index 2a1c6c8b17..63fa683e37 100644 --- a/packages/json-joy/src/json-crdt-extensions/peritext/rga/Point.ts +++ b/packages/json-joy/src/json-crdt-extensions/peritext/rga/Point.ts @@ -195,7 +195,7 @@ export class Point implements Pick, Printable { } else { chunk = this.chunk(); if (!chunk) return undefined; - if (!chunk.del || !!deleted) { + if (!chunk.del || deleted) { const offset = id.time - chunk.id.time; const span = chunk.span; if (offset + remaining < span) return tick(id, remaining); @@ -234,7 +234,7 @@ export class Point implements Pick, Printable { const {id, rga} = this; let chunk = this.chunk(); if (!chunk) return rga.id; - if (!chunk.del || !!deleted) { + if (!chunk.del || deleted) { const offset = id.time - chunk.id.time; if (offset >= remaining) return tick(id, -remaining); remaining -= offset; diff --git a/packages/json-joy/src/json-crdt-extensions/peritext/slice/Slice.ts b/packages/json-joy/src/json-crdt-extensions/peritext/slice/Slice.ts index 50833bd546..5c7c5cff23 100644 --- a/packages/json-joy/src/json-crdt-extensions/peritext/slice/Slice.ts +++ b/packages/json-joy/src/json-crdt-extensions/peritext/slice/Slice.ts @@ -298,7 +298,7 @@ export class Slice extends Range implements Stateful, Printable { const {model} = this; const diff = new JsonCrdtDiff(model); diff.shallowConBin = true; - if (this.dataNode() instanceof ObjApi && !!data && typeof data === 'object' && !Array.isArray(data)) { + if (this.dataNode() instanceof ObjApi && data && typeof data === 'object' && !Array.isArray(data)) { const dataNode = this.dataAsObj(); const patch = diff.diff(dataNode.node, data); model.applyPatch(patch); diff --git a/packages/json-joy/src/json-crdt/model/api/nodes.ts b/packages/json-joy/src/json-crdt/model/api/nodes.ts index 7002d4a44b..3c5104beb7 100644 --- a/packages/json-joy/src/json-crdt/model/api/nodes.ts +++ b/packages/json-joy/src/json-crdt/model/api/nodes.ts @@ -76,7 +76,7 @@ export class NodeApi implements Printable { } throw new Error('CANNOT_IN'); } - if (typeof path === 'string' && !!path && path[0] !== '/') path = '/' + path; + if (typeof path === 'string' && path && path[0] !== '/') path = '/' + path; if (typeof path === 'number') path = [path]; while (node instanceof ValNode) node = node.child()!; return find(node, path); diff --git a/packages/json-joy/src/json-ot/types/ot-json/tree.ts b/packages/json-joy/src/json-ot/types/ot-json/tree.ts index 4ca83beac1..565aa5091e 100644 --- a/packages/json-joy/src/json-ot/types/ot-json/tree.ts +++ b/packages/json-joy/src/json-ot/types/ot-json/tree.ts @@ -354,7 +354,7 @@ export class OpTree { clone.regId = reg.id; } } - if (!!d1.parent && !!d2.parent) { + if (d1.parent && d2.parent) { const child = d1.parent.children.get(d1.index); if (child) this.removeDrop(child); d1.parent.children.set(d2.index, clone); diff --git a/packages/json-joy/src/json-patch/applyPatch/v3.ts b/packages/json-joy/src/json-patch/applyPatch/v3.ts index cf18dfd6a6..de7beb4953 100644 --- a/packages/json-joy/src/json-patch/applyPatch/v3.ts +++ b/packages/json-joy/src/json-patch/applyPatch/v3.ts @@ -88,7 +88,7 @@ export function applyOperation(doc: unknown, operation: Operation): OpResult { break; } obj = obj[key]; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { key = unescapeComponent(key); if (indexOfSlash === -1) { switch (operation.op) { diff --git a/packages/json-pack/docs/cbor.md b/packages/json-pack/docs/cbor.md new file mode 100644 index 0000000000..b164c66304 --- /dev/null +++ b/packages/json-pack/docs/cbor.md @@ -0,0 +1,73 @@ +[CBOR](https://cbor.io/) (RFC 8949) is a standardized, self-describing binary +format. `json-pack`'s CBOR codec is the fastest in this package and a good +default when you control both ends of the wire. + +```ts +import {CborEncoder, CborDecoder} from '@jsonjoy.com/json-pack/lib/cbor'; + +const encoder = new CborEncoder(); +const decoder = new CborDecoder(); + +const buf = encoder.encode({id: 123, tags: ['a', 'b']}); +decoder.decode(buf); // {id: 123, tags: ['a', 'b']} +``` + +## Encoders + +| Class | Use | +|-------|-----| +| `CborEncoderFast` | Plain JSON values only --- the fastest path | +| `CborEncoder` | Full CBOR: adds binary (`Uint8Array`), tags, `undefined`, and `bigint` | +| `CborEncoderStable` | Deterministic output: object keys sorted, shortest-form integers | +| `CborEncoderDag` | [DAG-CBOR](https://ipld.io/specs/codecs/dag-cbor/spec/) for IPLD/content addressing | + +Pick `CborEncoderFast` when you only ever encode JSON-compatible data; reach for +`CborEncoder` the moment you need `Uint8Array` values or tags. Use the `Stable` +variant when bytes must be reproducible (hashing, signatures), and `Dag` for +IPLD. + +## Decoders + +| Class | Use | +|-------|-----| +| `CborDecoderBase` | All standard decoding; smallest footprint | +| `CborDecoder` | Adds shallow / one-level-at-a-time decoding and value skipping | +| `CborDecoderDag` | DAG-CBOR decoding | + +If you do not need the shallow-reading helpers, importing `CborDecoderBase` +keeps the bundle smaller. + +## Tags + +CBOR tags (and MessagePack extensions) are represented by `JsonPackExtension`. +Wrap a value to have it encoded with a tag; the decoder hands you back a +`JsonPackExtension` for tagged values. + +```ts +import {CborEncoder, JsonPackExtension} from '@jsonjoy.com/json-pack/lib/cbor'; + +const encoder = new CborEncoder(); +// Tag 0 (date/time string): +const buf = encoder.encode(new JsonPackExtension(0, '2024-01-01T00:00:00Z')); +``` + +To splice in already-encoded CBOR bytes verbatim, wrap them in a +`JsonPackValue`; the encoder writes the buffer as-is. + +## Shallow decoding + +`CborDecoder` can read a single value deep inside a document without +materializing the whole thing --- useful for plucking one field out of a large +blob. Reset the reader onto your buffer, then read level by level. + +```ts +import {CborDecoder} from '@jsonjoy.com/json-pack/lib/cbor'; + +const decoder = new CborDecoder(); +decoder.reader.reset(buf); +// ...navigate with the reader / map helpers and read just what you need. +``` + +For an ergonomic key/index navigation API of the same kind, see the +`findKey`/`findIndex`/`find` helpers on +[MessagePack's decoder](/libs/json-pack/messagepack). diff --git a/packages/json-pack/docs/index.ts b/packages/json-pack/docs/index.ts new file mode 100644 index 0000000000..6b00d99016 --- /dev/null +++ b/packages/json-pack/docs/index.ts @@ -0,0 +1,43 @@ +import type {LibPage} from '@jsonjoy.com/ui/src/types/libs'; + +export const page: LibPage = { + name: 'json-pack', + title: 'json-pack', + type: 'lib', + subtitle: 'The fastest JSON, CBOR, MessagePack, and binary serialization codecs for JavaScript.', + pkg: '@jsonjoy.com/json-pack', + group: 'tooling', + repo: 'streamich/json-joy', + repoPath: 'tree/master/packages/json-pack', + tech: 'TypeScript', + techIcon: {set: 'lineicons', icon: 'typescript'}, + showContentsTable: true, + children: [ + { + name: 'CBOR', + subtitle: 'Concise Binary Object Representation: fast, full, stable, and DAG-CBOR codecs.', + // @ts-ignore raw markdown, loaded by the site's webpack raw-loader + src: async () => (await import('./cbor.md')).default, + }, + { + name: 'MessagePack', + subtitle: 'MessagePack encoders and decoders, extensions, and shallow reading.', + // @ts-ignore + src: async () => (await import('./messagepack.md')).default, + }, + { + name: 'JSON', + subtitle: 'Binary JSON encoder/decoder and the JSON Binary extension for Uint8Array.', + // @ts-ignore + src: async () => (await import('./json.md')).default, + }, + { + name: 'More formats', + subtitle: 'UBJSON, BSON, RESP, Bencode, and Amazon Ion codecs.', + // @ts-ignore + src: async () => (await import('./more-formats.md')).default, + }, + ], + // @ts-ignore + src: async () => (await import('./text.md')).default, +}; diff --git a/packages/json-pack/docs/json.md b/packages/json-pack/docs/json.md new file mode 100644 index 0000000000..7efde4a6cb --- /dev/null +++ b/packages/json-pack/docs/json.md @@ -0,0 +1,55 @@ +Two related things live here: a **binary JSON codec** that reads and writes +standard JSON as `Uint8Array` (often faster than the native `JSON` on the hot +path, and integer-keyed), and **JSON Binary**, a small extension that lets plain +JSON carry `Uint8Array` values. + +## Binary JSON codec + +`JsonEncoder` encodes a value to UTF-8 JSON bytes; `JsonDecoder` parses JSON +bytes back to a value. The encoder needs a `Writer` to write into. + +```ts +import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; +import {JsonEncoder, JsonDecoder} from '@jsonjoy.com/json-pack/lib/json'; + +const encoder = new JsonEncoder(new Writer()); +const decoder = new JsonDecoder(); + +const buf = encoder.encode({id: 123}); // Uint8Array of `{"id":123}` +decoder.decode(buf); // {id: 123} +``` + +| Class | Use | +|-------|-----| +| `JsonEncoder` | Encode a value to JSON bytes | +| `JsonEncoderStable` | Same, with object keys sorted (deterministic output) | +| `JsonEncoderDag` | [DAG-JSON](https://ipld.io/specs/codecs/dag-json/spec/): binary as `{"/": {"bytes": ""}}` | +| `JsonDecoder` | Parse JSON bytes to a value | +| `JsonDecoderDag` | DAG-JSON decoding | + +This codec emits ordinary JSON, so its output is readable by any JSON parser --- +the win is speed and working directly in bytes without a `string` round-trip. + +## JSON Binary + +Standard JSON has no way to represent a `Uint8Array`. The `json-binary` module +solves this with drop-in `stringify` / `parse` that encode binary as a base64 +data-URI string, leaving everything else as normal JSON. + +```ts +import {stringify, parse} from '@jsonjoy.com/json-pack/lib/json-binary'; + +const doc = {name: 'file', body: new Uint8Array([1, 2, 3])}; + +const json = stringify(doc); +// {"name":"file","body":"data:application/octet-stream;base64,AQID"} + +const back = parse(json); +// {name: 'file', body: Uint8Array [1, 2, 3]} -- restored as a Uint8Array +``` + +`stringify(value, replacer?, space?)` mirrors `JSON.stringify`'s signature, and +`parse(json)` mirrors `JSON.parse`. Values that are not `Uint8Array` pass +through unchanged, so the output stays valid JSON for any consumer --- only this +`parse` revives the binary. `stringifyBinary(uint8)` returns the data-URI string +for a single buffer. diff --git a/packages/json-pack/docs/messagepack.md b/packages/json-pack/docs/messagepack.md new file mode 100644 index 0000000000..e6466df7ef --- /dev/null +++ b/packages/json-pack/docs/messagepack.md @@ -0,0 +1,72 @@ +[MessagePack](https://msgpack.org/) is a compact binary format with a wide +ecosystem. Use this codec when you need to interoperate with other MessagePack +implementations. + +```ts +import {MessagePackEncoder, MessagePackDecoder} from '@jsonjoy.com/json-pack/lib/msgpack'; + +const encoder = new MessagePackEncoder(); +const decoder = new MessagePackDecoder(); + +const buf = encoder.encode({id: 123, tags: ['a', 'b']}); +decoder.decode(buf); // {id: 123, tags: ['a', 'b']} +``` + +`MessagePackEncoder` / `MessagePackDecoder` are aliases for `MsgPackEncoder` / +`MsgPackDecoder`. + +## Encoders + +| Class | Use | +|-------|-----| +| `MsgPackEncoderFast` | Plain JSON values only --- the fastest path | +| `MsgPackEncoder` | Full MessagePack: adds binary (`Uint8Array`) and extensions | +| `MsgPackEncoderStable` | Deterministic output with object keys sorted | + +## Decoders + +| Class | Use | +|-------|-----| +| `MsgPackDecoderFast` | Standard decoding | +| `MsgPackDecoder` | Adds shallow reading (`findKey`, `findIndex`, `find`) and value skipping | + +## Extensions + +MessagePack extensions are represented by `JsonPackExtension` (the same wrapper +CBOR uses for tags). The first argument is the extension type, the second the +payload. + +```ts +import {MsgPackEncoder, JsonPackExtension} from '@jsonjoy.com/json-pack/lib/msgpack'; + +const encoder = new MsgPackEncoder(); +const buf = encoder.encode(new JsonPackExtension(1, new Uint8Array([1, 2, 3]))); +``` + +Use `JsonPackValue` to embed a pre-encoded MessagePack value verbatim. + +## Shallow reading + +`MsgPackDecoder` can extract a single value from deep inside a document without +decoding everything around it --- much faster than a full `decode` when you only +want one field. Reset the reader onto the buffer, navigate by key or index, then +read. + +```ts +import {MsgPackDecoder} from '@jsonjoy.com/json-pack/lib/msgpack'; + +const decoder = new MsgPackDecoder(); +decoder.reader.reset(buf); + +decoder.findKey('foo').readAny(); // value at .foo +``` + +`findKey`, `findIndex`, and `find` return the decoder, so they chain. `find` +takes a path of mixed keys and indices: + +```ts +decoder.reader.reset(buf); +decoder.find(['a', 'b', 1]).readAny(); // value at .a.b[1] +``` + +Reset the reader before each independent lookup. diff --git a/packages/json-pack/docs/more-formats.md b/packages/json-pack/docs/more-formats.md new file mode 100644 index 0000000000..f0f7f03762 --- /dev/null +++ b/packages/json-pack/docs/more-formats.md @@ -0,0 +1,80 @@ +Beyond CBOR, MessagePack, and JSON, `json-pack` ships codecs for several other +binary and wire formats. They follow the same `encode` / `decode` shape; reach +for one when you need to talk to a system that speaks that protocol. Some +encoders require a `Writer` instance (from `@jsonjoy.com/buffers`) in their +constructor --- the examples below show which. + +## UBJSON + +[Universal Binary JSON](https://ubjson.org/). Encoder needs a `Writer`. + +```ts +import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; +import {UbjsonEncoder, UbjsonDecoder} from '@jsonjoy.com/json-pack/lib/ubjson'; + +const encoder = new UbjsonEncoder(new Writer()); +const decoder = new UbjsonDecoder(); +decoder.decode(encoder.encode({id: 123})); +``` + +## BSON + +[BSON](https://bsonspec.org/), the binary format used by MongoDB. Encode a +document with `BsonEncoder` (needs a `Writer`), decode with `BsonDecoder`. The +`bson` module also exports value wrappers for BSON-specific types. + +```ts +import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; +import {BsonEncoder, BsonDecoder} from '@jsonjoy.com/json-pack/lib/bson'; + +const encoder = new BsonEncoder(new Writer()); +const decoder = new BsonDecoder(); +``` + +## RESP + +[Redis Serialization Protocol](https://redis.io/docs/latest/develop/reference/protocol-spec/), +versions 2 and 3 --- handy for building Redis clients/servers. + +| Class | Use | +|-------|-----| +| `RespEncoder` | Encode values to RESP (RESP3) | +| `RespEncoderLegacy` | RESP2 encoder | +| `RespDecoder` | Decode a complete RESP message | +| `RespStreamingDecoder` | Incremental decoder for streamed data --- feed bytes, pull values as they complete | + +`RespEncoder` defaults its `Writer`, so it constructs with no arguments. + +```ts +import {RespEncoder, RespDecoder} from '@jsonjoy.com/json-pack/lib/resp'; + +const encoder = new RespEncoder(); +const decoder = new RespDecoder(); +decoder.decode(encoder.encode(['PING'])); +``` + +## Bencode + +[Bencode](https://en.wikipedia.org/wiki/Bencode), BitTorrent's encoding. Encoder +needs a `Writer`. + +```ts +import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; +import {BencodeEncoder, BencodeDecoder} from '@jsonjoy.com/json-pack/lib/bencode'; + +const encoder = new BencodeEncoder(new Writer()); +const decoder = new BencodeDecoder(); +``` + +## Amazon Ion + +[Amazon Ion](https://amazon-ion.github.io/ion-docs/). `IonEncoderFast` defaults +its `Writer`; decode with `IonDecoder`. + +```ts +import {IonEncoderFast, IonDecoder} from '@jsonjoy.com/json-pack/lib/ion'; + +const encoder = new IonEncoderFast(); +const decoder = new IonDecoder(); +decoder.decode(encoder.encode({id: 123})); +``` diff --git a/packages/json-pack/docs/text.md b/packages/json-pack/docs/text.md new file mode 100644 index 0000000000..415ca473ba --- /dev/null +++ b/packages/json-pack/docs/text.md @@ -0,0 +1,75 @@ +`json-pack` is a collection of high-performance serialization codecs --- +the fastest in the JavaScript ecosystem for both encoding and decoding. It has +no runtime dependencies on external codec libraries and runs in Node.js, Deno, +Bun, and browsers. + +```ts +import {MessagePackEncoder, MessagePackDecoder} from '@jsonjoy.com/json-pack/lib/msgpack'; + +const encoder = new MessagePackEncoder(); +const decoder = new MessagePackDecoder(); + +const data = {hello: 'world', numbers: [1, 2, 3]}; +const blob = encoder.encode(data); // Uint8Array +decoder.decode(blob); // {hello: 'world', numbers: [1, 2, 3]} +``` + +## Install + +``` +npm install @jsonjoy.com/json-pack +``` + +Each codec lives under its own subpath, so you only pull in the format you use: +`@jsonjoy.com/json-pack/lib/cbor`, `.../lib/msgpack`, `.../lib/json`, and so on. + +## The encoder / decoder model + +Every format follows the same shape: an **encoder** turns a JavaScript value +into a `Uint8Array`, and a **decoder** turns a `Uint8Array` back into a value. + +```ts +const buf = encoder.encode(value); // value -> Uint8Array +const value = decoder.decode(buf); // Uint8Array -> value +``` + +Encoders and decoders are **stateful, reusable objects** --- construct one and +call it many times. An encoder writes into an internal growable buffer (a +`Writer` from [`@jsonjoy.com/buffers`](https://github.com/streamich/json-joy)), +which it reuses across calls, so a long-lived encoder avoids per-call +allocations. You can pass your own `Writer` to the constructor to share one +buffer across several encoders: + +```ts +import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; +import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor'; + +const writer = new Writer(); +const encoder = new CborEncoder(writer); +``` + +~~~jj.note +Because the encoder reuses its buffer, the `Uint8Array` returned by `encode()` +is only valid until the next `encode()` call on that encoder. Copy it if you +need to keep it around. +~~~ + +## Choosing a format + +| Page | Format | Reach for it when | +|------|--------|-------------------| +| [CBOR](/libs/json-pack/cbor) | RFC 8949 | A standardized, self-describing binary format; the fastest codec here | +| [MessagePack](/libs/json-pack/messagepack) | msgpack | Interop with the MessagePack ecosystem; compact binary | +| [JSON](/libs/json-pack/json) | JSON + binary | Drop-in for `JSON.stringify`/`parse` on the hot path, or JSON that must carry `Uint8Array` | +| [More formats](/libs/json-pack/more-formats) | UBJSON, BSON, RESP, Bencode, Ion | Talking to a system that speaks one of these protocols | + +## Fast vs. full encoders + +Several formats ship a `*Fast` encoder alongside the full one. The fast variant +handles only plain JSON values (`null`, booleans, numbers, strings, arrays, +objects) and is the quickest path when that is all you have. The full encoder +adds binary data, extensions/tags, and other format-specific types. See each +codec's page for the exact set of classes. + +This package is part of [json-joy](/libs/json-joy-js); the serialization +formats are also described in the [specs section](/specs). diff --git a/packages/json-pack/src/json/JsonDecoder.ts b/packages/json-pack/src/json/JsonDecoder.ts index a8ff14c81b..5a01f89e0d 100644 --- a/packages/json-pack/src/json/JsonDecoder.ts +++ b/packages/json-pack/src/json/JsonDecoder.ts @@ -202,17 +202,18 @@ export class JsonDecoder implements BinaryJsonDecoder { const uint8 = reader.uint8; const char = uint8[x]; switch (char) { - case 34 /* " */: { - if (uint8[x + 1] === 0x64 /* d */) { - const bin = this.tryReadBin(); - if (bin) return bin; - if (isUndefined(uint8, x + 2)) { - reader.x = x + 35; - return undefined; + case 34 /* " */: + { + if (uint8[x + 1] === 0x64 /* d */) { + const bin = this.tryReadBin(); + if (bin) return bin; + if (isUndefined(uint8, x + 2)) { + reader.x = x + 35; + return undefined; + } } + return this.readStr(); } - return this.readStr(); - } case 91 /* [ */: return this.readArr(); case 102 /* f */: diff --git a/packages/json-pointer/src/find.ts b/packages/json-pointer/src/find.ts index 4d022453c9..29b61e36ca 100644 --- a/packages/json-pointer/src/find.ts +++ b/packages/json-pointer/src/find.ts @@ -58,7 +58,7 @@ export const find = (val: unknown, path: Path): Reference => { } } val = obj[key]; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { val = has(obj, key as string) ? (obj as any)[key] : undefined; } else throw new Error('NOT_FOUND'); } diff --git a/packages/json-pointer/src/findByPointer/v1.ts b/packages/json-pointer/src/findByPointer/v1.ts index cfc73cb14d..c31bd60884 100644 --- a/packages/json-pointer/src/findByPointer/v1.ts +++ b/packages/json-pointer/src/findByPointer/v1.ts @@ -25,7 +25,7 @@ export const findByPointer = (pointer: string, val: unknown): Reference => { if (key < 0) throw new Error('INVALID_INDEX'); } val = has(obj, String(key)) ? obj[key] : undefined; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { val = has(obj, String(key)) ? (obj as any)[key] : undefined; } else throw new Error('NOT_FOUND'); } diff --git a/packages/json-pointer/src/findByPointer/v2.ts b/packages/json-pointer/src/findByPointer/v2.ts index bf75af1e9e..d02aaa5a8b 100644 --- a/packages/json-pointer/src/findByPointer/v2.ts +++ b/packages/json-pointer/src/findByPointer/v2.ts @@ -30,7 +30,7 @@ export const findByPointer = (pointer: string, val: unknown): Reference => { if (key < 0) throw new Error('INVALID_INDEX'); } val = has(obj, String(key)) ? obj[key] : undefined; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { val = has(obj, String(key)) ? (obj as any)[key] : undefined; } else throw new Error('NOT_FOUND'); } diff --git a/packages/json-pointer/src/findByPointer/v3.ts b/packages/json-pointer/src/findByPointer/v3.ts index 14fe86f96d..d60f3e5be8 100644 --- a/packages/json-pointer/src/findByPointer/v3.ts +++ b/packages/json-pointer/src/findByPointer/v3.ts @@ -30,7 +30,7 @@ export const findByPointer = (pointer: string, val: unknown): Reference => { if (key < 0) throw new Error('INVALID_INDEX'); } val = has(obj, String(key)) ? obj[key] : undefined; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { val = has(obj, String(key)) ? (obj as any)[key] : undefined; } else throw new Error('NOT_FOUND'); } diff --git a/packages/json-pointer/src/findByPointer/v4.ts b/packages/json-pointer/src/findByPointer/v4.ts index a5f8668ea3..39d7edda1d 100644 --- a/packages/json-pointer/src/findByPointer/v4.ts +++ b/packages/json-pointer/src/findByPointer/v4.ts @@ -25,7 +25,7 @@ export const findByPointer = (pointer: string, val: unknown): Reference => { if (key < 0) throw new Error('INVALID_INDEX'); } val = has(obj, key as any) ? obj[~~key] : undefined; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { val = has(obj, key) ? (obj as any)[key] : undefined; } else throw new Error('NOT_FOUND'); } diff --git a/packages/json-pointer/src/findByPointer/v5.ts b/packages/json-pointer/src/findByPointer/v5.ts index ef05623467..9b16ad1e97 100644 --- a/packages/json-pointer/src/findByPointer/v5.ts +++ b/packages/json-pointer/src/findByPointer/v5.ts @@ -27,7 +27,7 @@ export const findByPointer = (pointer: string, val: unknown): Reference => { if (key < 0) throw 'INVALID_INDEX'; } val = obj[key]; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { key = unescapeComponent(key); val = has(obj, key) ? (obj as any)[key] : undefined; } else throw 'NOT_FOUND'; diff --git a/packages/json-pointer/src/findByPointer/v6.ts b/packages/json-pointer/src/findByPointer/v6.ts index 3bb8136618..c2232e0ad8 100644 --- a/packages/json-pointer/src/findByPointer/v6.ts +++ b/packages/json-pointer/src/findByPointer/v6.ts @@ -24,7 +24,7 @@ export const findByPointer = (pointer: string, val: unknown): [Reference['obj'], if (key < 0) throw 'INVALID_INDEX'; } val = obj[key]; - } else if (typeof obj === 'object' && !!obj) { + } else if (typeof obj === 'object' && obj) { key = unescapeComponent(key); val = has(obj, key) ? (obj as any)[key] : undefined; } else throw 'NOT_FOUND'; diff --git a/packages/mu-txt/LICENSE b/packages/mu-txt/LICENSE deleted file mode 100644 index 0ad25db4bd..0000000000 --- a/packages/mu-txt/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/packages/mu-txt/README.md b/packages/mu-txt/README.md deleted file mode 100644 index 418c140689..0000000000 --- a/packages/mu-txt/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# `mu-txt` - -This package contains the `mutxt.com` web app. - -## Development - -From the monorepo root: - -```sh -yarn workspace mu-txt dev -``` - -Other useful commands: - -```sh -yarn workspace mu-txt build -yarn workspace mu-txt typecheck -yarn workspace mu-txt clean -``` - -## Deployment - -### Wrangler / Cloudflare Pages - -```sh -yarn workspace mu-txt deploy -yarn workspace mu-txt deploy:preview -``` - -Wrangler publishes the generated `dist/` folder. - -Common commands: - -| Command | Purpose | -|---|---| -| `npx wrangler login` | Authenticate once | -| `npx wrangler pages project create mutxt --production-branch=master` | Create the Pages project once | -| `npx wrangler pages deploy dist` | Deploy to production | -| `npx wrangler pages deploy dist --branch=preview` | Deploy a preview build | -| `npx wrangler pages deployment list` | Inspect recent deployments | diff --git a/packages/mu-txt/SECURITY.md b/packages/mu-txt/SECURITY.md deleted file mode 100644 index a5497b62af..0000000000 --- a/packages/mu-txt/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security Policy - -## Supported Versions - -We release patches for security vulnerabilities. The latest major version -will support security patches. - -## Reporting a Vulnerability - -Please report (suspected) security vulnerabilities to -**[streamich@gmail.com](mailto:streamich@gmail.com)**. We will try to respond -within 48 hours. If the issue is confirmed, we will release a patch as soon -as possible depending on complexity. diff --git a/packages/mu-txt/bin/mu-txt.js b/packages/mu-txt/bin/mu-txt.js deleted file mode 100755 index dc95a8839c..0000000000 --- a/packages/mu-txt/bin/mu-txt.js +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -const {spawn} = require('node:child_process'); -const path = require('node:path'); - -const electron = require('electron'); -const main = path.resolve(__dirname, '..', 'out-electron', 'main.js'); - -const child = spawn(electron, [main, ...process.argv.slice(2)], { - stdio: 'inherit', - env: process.env, - windowsHide: false, -}); - -child.on('close', (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - } else { - process.exit(code == null ? 0 : code); - } -}); diff --git a/packages/mu-txt/electron/main.ts b/packages/mu-txt/electron/main.ts deleted file mode 100644 index aad32809f1..0000000000 --- a/packages/mu-txt/electron/main.ts +++ /dev/null @@ -1,268 +0,0 @@ -import {app, BrowserWindow, dialog, ipcMain, shell, protocol, net, Menu, screen, type MenuItemConstructorOptions} from 'electron'; -import * as path from 'node:path'; -import * as fs from 'node:fs/promises'; -import {pathToFileURL} from 'node:url'; - -app.setName('mu-txt'); - -const isDev = process.env.MUTXT_DEV === '1'; -const distRoot = path.resolve(__dirname, '..', 'dist'); -const iconsRoot = path.resolve(__dirname, '..', 'public', 'icons'); - -const TRAFFIC_LIGHT_DEFAULT = {x: 16, y: 21}; -const TRAFFIC_LIGHT_COMPACT = {x: 16, y: 18}; -type TitlebarMode = 'default' | 'compact'; - -type PendingInput = - | {kind: 'file'; name: string; path: string; bytes?: Uint8Array} - | {kind: 'url'; value: string}; - -const pending: PendingInput[] = []; -let mainWindow: BrowserWindow | null = null; -let rendererReady = false; - -const channelFor = (kind: PendingInput['kind']) => (kind === 'file' ? 'mutxt:open-file' : 'mutxt:open-url'); - -const payloadFor = (item: PendingInput) => - item.kind === 'file' ? {name: item.name, path: item.path, bytes: item.bytes} : item.value; - -const send = (item: PendingInput) => { - if (!mainWindow || mainWindow.isDestroyed()) return; - mainWindow.webContents.send(channelFor(item.kind), payloadFor(item)); -}; - -const dispatch = (item: PendingInput) => { - if (rendererReady && mainWindow && !mainWindow.isDestroyed()) { - send(item); - } else { - pending.push(item); - } -}; - -const flushPending = () => { - if (!mainWindow || mainWindow.isDestroyed()) return; - while (pending.length) send(pending.shift()!); -}; - -const userArgv = (argv: readonly string[]): readonly string[] => argv.slice(process.defaultApp ? 2 : 1); - -type ParsedArg = {kind: 'path'; value: string} | {kind: 'url'; value: string}; - -const parseArg = (argv: readonly string[], cwd: string): ParsedArg | null => { - for (const arg of userArgv(argv)) { - if (!arg || arg.startsWith('-')) continue; - if (arg.startsWith('mutxt://')) return {kind: 'url', value: arg}; - return {kind: 'path', value: path.isAbsolute(arg) ? arg : path.resolve(cwd, arg)}; - } - return null; -}; - -const dispatchPath = async (filePath: string) => { - const name = path.basename(filePath); - try { - const buf = await fs.readFile(filePath); - const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); - dispatch({kind: 'file', name, path: filePath, bytes}); - } catch (err) { - if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') { - dispatch({kind: 'file', name, path: filePath}); - } else { - console.error('[mutxt] failed to read file', filePath, err); - } - } -}; - -const dispatchArg = (argv: readonly string[], cwd: string) => { - const parsed = parseArg(argv, cwd); - if (!parsed) return; - if (parsed.kind === 'url') dispatch({kind: 'url', value: parsed.value}); - else void dispatchPath(parsed.value); -}; - -const openDialog = async (): Promise => { - const opts: Electron.OpenDialogOptions = { - title: 'Open File', - properties: ['openFile', 'multiSelections'], - }; - const result = - mainWindow && !mainWindow.isDestroyed() - ? await dialog.showOpenDialog(mainWindow, opts) - : await dialog.showOpenDialog(opts); - if (result.canceled) return; - for (const filePath of result.filePaths) void dispatchPath(filePath); -}; - -protocol.registerSchemesAsPrivileged([ - { - scheme: 'app', - privileges: {standard: true, secure: true, supportFetchAPI: true, stream: true, codeCache: true}, - }, -]); - -const gotLock = app.requestSingleInstanceLock(); -if (!gotLock) { - app.quit(); -} else { - app.on('second-instance', (_event, argv, workingDirectory) => { - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); - } - dispatchArg(argv, workingDirectory || process.cwd()); - }); - - app.on('open-url', (event, url) => { - event.preventDefault(); - dispatch({kind: 'url', value: url}); - }); - app.on('open-file', (event, filePath) => { - event.preventDefault(); - void dispatchPath(filePath); - }); - - ipcMain.handle('mutxt:open-dialog', () => openDialog()); - ipcMain.handle('mutxt:write-file', async (_event, filePath: string, bytes: Uint8Array) => { - await fs.writeFile(filePath, bytes); - }); - ipcMain.handle('mutxt:set-titlebar-mode', (_event, mode: TitlebarMode) => { - if (process.platform !== 'darwin') return; - if (!mainWindow || mainWindow.isDestroyed()) return; - mainWindow.setWindowButtonPosition(mode === 'compact' ? TRAFFIC_LIGHT_COMPACT : TRAFFIC_LIGHT_DEFAULT); - }); - - if (!app.isDefaultProtocolClient('mutxt')) { - app.setAsDefaultProtocolClient('mutxt'); - } - - dispatchArg(process.argv, process.cwd()); - - app.whenReady().then(() => { - if (!isDev) registerAppProtocol(); - buildAppMenu(); - createWindow(); - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); - }); - }); - - app.on('window-all-closed', () => { - if (process.platform !== 'darwin') app.quit(); - }); -} - -function buildAppMenu(): void { - const isMac = process.platform === 'darwin'; - const sendCloseFile = () => { - if (!mainWindow || mainWindow.isDestroyed()) return; - if (!rendererReady) return; - const wc = mainWindow.webContents; - if (wc.isDestroyed() || wc.isCrashed()) return; - wc.send('mutxt:close-file'); - }; - const template: MenuItemConstructorOptions[] = [ - ...(isMac - ? ([ - { - label: app.name, - submenu: [ - {role: 'about'}, - {type: 'separator'}, - {role: 'services'}, - {type: 'separator'}, - {role: 'hide'}, - {role: 'hideOthers'}, - {role: 'unhide'}, - {type: 'separator'}, - {role: 'quit'}, - ], - }, - ] as MenuItemConstructorOptions[]) - : []), - { - label: 'File', - submenu: [ - {label: 'Open…', accelerator: 'CmdOrCtrl+O', click: () => void openDialog()}, - {type: 'separator'}, - {label: 'Close File', accelerator: 'CmdOrCtrl+W', click: sendCloseFile}, - {label: 'Close Window', accelerator: 'CmdOrCtrl+Shift+W', role: 'close'}, - ...(isMac ? [] : ([{type: 'separator'}, {role: 'quit'}] as MenuItemConstructorOptions[])), - ], - }, - {role: 'editMenu'}, - {role: 'viewMenu'}, - {role: 'windowMenu'}, - ]; - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); -} - -const CSP = - "default-src 'self' app: https: http: data: blob: 'unsafe-inline' 'unsafe-eval'; " + - "img-src 'self' app: https: http: data: blob:; " + - "media-src 'self' app: https: http: data: blob:; " + - "style-src 'self' app: https: http: 'unsafe-inline'; " + - "script-src 'self' app: https: http: 'unsafe-inline' 'unsafe-eval'; " + - "font-src 'self' app: https: http: data:; " + - "connect-src 'self' app: https: http: ws: wss: data: blob:; " + - "frame-src 'self' app: https: http: data: blob:; " + - "worker-src 'self' app: blob: data:;"; - -function registerAppProtocol(): void { - protocol.handle('app', async (request) => { - const url = new URL(request.url); - let pathname = decodeURIComponent(url.pathname); - if (!pathname || pathname === '/') pathname = '/index.html'; - - const candidate = path.normalize(path.join(distRoot, pathname)); - if (!candidate.startsWith(distRoot)) { - return new Response('Forbidden', {status: 403}); - } - - const target = path.extname(candidate) ? candidate : path.join(distRoot, 'index.html'); - const upstream = await net.fetch(pathToFileURL(target).toString()); - const headers = new Headers(upstream.headers); - headers.set('Content-Security-Policy', CSP); - return new Response(upstream.body, {status: upstream.status, statusText: upstream.statusText, headers}); - }); -} - -function createWindow(): void { - const {width: screenWidth, height: screenHeight} = screen.getPrimaryDisplay().workAreaSize; - mainWindow = new BrowserWindow({ - width: Math.round(screenWidth * 0.85), - height: Math.round(screenHeight * 0.85), - minWidth: 600, - minHeight: 400, - titleBarStyle: 'hiddenInset', - trafficLightPosition: TRAFFIC_LIGHT_DEFAULT, - backgroundColor: '#0f172a', - icon: path.join(iconsRoot, 'icon-512.png'), - webPreferences: { - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - preload: path.join(__dirname, 'preload.js'), - }, - }); - - mainWindow.webContents.setWindowOpenHandler(({url}) => { - shell.openExternal(url); - return {action: 'deny'}; - }); - - mainWindow.webContents.on('did-finish-load', () => { - rendererReady = true; - flushPending(); - }); - - mainWindow.on('closed', () => { - mainWindow = null; - rendererReady = false; - }); - - if (isDev) { - mainWindow.loadURL('http://localhost:3000'); - mainWindow.webContents.openDevTools({mode: 'detach'}); - } else { - mainWindow.loadURL('app://-/index.html'); - } -} diff --git a/packages/mu-txt/electron/preload.ts b/packages/mu-txt/electron/preload.ts deleted file mode 100644 index ff4dd54803..0000000000 --- a/packages/mu-txt/electron/preload.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {contextBridge, ipcRenderer, type IpcRendererEvent} from 'electron'; - -type Listener = (value: T) => void; -type Unsubscribe = () => void; - -const subscribe = (channel: string, cb: Listener): Unsubscribe => { - const listener = (_event: IpcRendererEvent, value: T) => cb(value); - ipcRenderer.on(channel, listener); - return () => { - ipcRenderer.off(channel, listener); - }; -}; - -const subscribeVoid = (channel: string, cb: () => void): Unsubscribe => { - const listener = () => cb(); - ipcRenderer.on(channel, listener); - return () => { - ipcRenderer.off(channel, listener); - }; -}; - -type OpenFilePayload = {name: string; path: string; bytes?: Uint8Array}; - -const api = { - platform: process.platform, - versions: {...process.versions}, - openDialog: (): Promise => ipcRenderer.invoke('mutxt:open-dialog'), - writeFile: (path: string, bytes: Uint8Array): Promise => - ipcRenderer.invoke('mutxt:write-file', path, bytes), - setTitlebarMode: (mode: 'default' | 'compact'): Promise => - ipcRenderer.invoke('mutxt:set-titlebar-mode', mode), - onOpenFile: (cb: Listener): Unsubscribe => subscribe('mutxt:open-file', cb), - onOpenUrl: (cb: Listener): Unsubscribe => subscribe('mutxt:open-url', cb), - onCloseFile: (cb: () => void): Unsubscribe => subscribeVoid('mutxt:close-file', cb), -} as const; - -export type MutxtBridge = typeof api; - -contextBridge.exposeInMainWorld('mutxt', api); diff --git a/packages/mu-txt/forge.config.ts b/packages/mu-txt/forge.config.ts deleted file mode 100644 index 508051c1c8..0000000000 --- a/packages/mu-txt/forge.config.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as path from 'node:path'; -import type {ForgeConfig} from '@electron-forge/shared-types'; - -const iconBase = path.join(__dirname, 'public', 'icons', 'icon-512'); - -const hasAppleIdentity = Boolean(process.env.APPLE_IDENTITY); -const hasAppleNotarize = Boolean( - process.env.APPLE_ID && process.env.APPLE_PASSWORD && process.env.APPLE_TEAM_ID, -); - -const config: ForgeConfig = { - packagerConfig: { - name: 'mu-txt', - executableName: 'mutxt', - appBundleId: 'com.mutxt', - icon: iconBase, - asar: true, - // Registers `mutxt://` as a default scheme on macOS via Info.plist. On - // Windows/Linux registration happens at runtime in main.ts. - protocols: [{name: 'mu-txt', schemes: ['mutxt']}], - osxSign: hasAppleIdentity - ? { - identity: process.env.APPLE_IDENTITY, - optionsForFile: () => ({ - hardenedRuntime: true, - entitlements: path.join(__dirname, 'electron', 'entitlements.plist'), - 'entitlements-inherit': path.join(__dirname, 'electron', 'entitlements.plist'), - }), - } - : undefined, - osxNotarize: hasAppleNotarize - ? { - appleId: process.env.APPLE_ID!, - appleIdPassword: process.env.APPLE_PASSWORD!, - teamId: process.env.APPLE_TEAM_ID!, - } - : undefined, - win32metadata: { - CompanyName: 'streamich', - FileDescription: 'mu-txt', - ProductName: 'mu-txt', - }, - // Trim the packaged app to just the bits Electron needs at runtime: - // dist/ (renderer), out-electron/ (main+preload), public/icons/ (window - // icon), package.json. Everything else is build-time only. - ignore: [ - /^\/src($|\/)/, - /^\/electron($|\/)/, - /^\/bin($|\/)/, - /^\/docs($|\/)/, - /^\/lib($|\/)/, - /^\/out($|\/)/, - /^\/public\/(?!icons($|\/))/, - /^\/tsconfig.*\.json$/, - /^\/webpack\.config\.ts$/, - /^\/forge\.config\.ts$/, - /^\/wrangler\.toml$/, - /^\/SIGNING\.md$/, - /^\/SECURITY\.md$/, - /^\/README\.md$/, - /\.map$/, - ], - }, - rebuildConfig: {}, - makers: [ - { - name: '@electron-forge/maker-squirrel', - config: { - name: 'mutxt', - setupIcon: `${iconBase}.ico`, - }, - }, - {name: '@electron-forge/maker-zip', config: {}, platforms: ['darwin', 'linux']}, - { - name: '@electron-forge/maker-deb', - config: { - options: { - mimeType: ['x-scheme-handler/mutxt'], - }, - }, - }, - { - name: '@electron-forge/maker-rpm', - config: { - options: { - mimeType: ['x-scheme-handler/mutxt'], - }, - }, - }, - ], - publishers: [ - { - name: '@electron-forge/publisher-github', - config: { - repository: {owner: 'streamich', name: 'json-joy'}, - prerelease: false, - draft: true, - }, - }, - ], -}; - -export default config; diff --git a/packages/mu-txt/package.json b/packages/mu-txt/package.json deleted file mode 100644 index 077c4b4cd1..0000000000 --- a/packages/mu-txt/package.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "name": "mu-txt", - "publishConfig": { - "access": "public" - }, - "version": "18.26.0", - "description": "Mutxt web app for creating and managing multiple rich-text files", - "author": { - "name": "streamich", - "url": "https://github.com/streamich" - }, - "homepage": "https://mutxt.com", - "repository": { - "type": "git", - "url": "https://github.com/streamich/json-joy.git", - "directory": "packages/mu-txt" - }, - "license": "AGPL-3.0-only", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "main": "out-electron/main.js", - "bin": "bin/mu-txt.js", - "files": [ - "LICENSE", - "lib/", - "dist/", - "out-electron/", - "bin/", - "public/icons/" - ], - "keywords": [ - "mutxt", - "rich-text", - "editor", - "files", - "workspace", - "site", - "json-joy", - "peritext" - ], - "peerDependencies": { - "rxjs": "7", - "tslib": "*" - }, - "dependencies": { - "electron": "^42.0.0" - }, - "engines": { - "node": ">=18.0" - }, - "installConfig": { - "hoistingLimits": "workspaces" - }, - "scripts": { - "dev": "webpack serve --mode development", - "dev:prod": "webpack serve --mode production", - "build": "webpack --mode production", - "deploy": "yarn build && wrangler pages deploy --branch=master dist", - "deploy:preview": "yarn build && wrangler pages deploy --branch=preview dist", - "typecheck": "npx tsc --noEmit -p tsconfig.app.json && npx tsc --noEmit -p tsconfig.electron.json", - "clean": "npx rimraf dist out out-electron out-stage", - "electron:build:main": "npx tsc -p tsconfig.electron.json", - "electron:build": "yarn build && yarn electron:build:main", - "electron:stage": "node scripts/stage-electron.js", - "electron:dev": "yarn electron:build:main && MUTXT_DEV=1 electron out-electron/main.js", - "electron:package": "yarn electron:build && yarn electron:stage && electron-forge package out-stage", - "electron:make": "yarn electron:build && yarn electron:stage && electron-forge make out-stage", - "electron:publish": "yarn electron:build && yarn electron:stage && electron-forge publish out-stage", - "prepack": "yarn electron:build" - }, - "devDependencies": { - "@electron-forge/cli": "^7.11.1", - "@electron-forge/maker-deb": "^7.11.1", - "@electron-forge/maker-rpm": "^7.11.1", - "@electron-forge/maker-squirrel": "^7.11.1", - "@electron-forge/maker-zip": "^7.11.1", - "@electron-forge/publisher-github": "^7.11.1", - "@electron-forge/shared-types": "^7.11.1", - "@jsonjoy.com/base64": "workspace:*", - "@jsonjoy.com/collaborative-peritext": "workspace:*", - "@jsonjoy.com/collaborative-slate": "workspace:*", - "@jsonjoy.com/collaborative-ui": "workspace:*", - "@jsonjoy.com/json-pack": "workspace:*", - "@jsonjoy.com/ui": "workspace:*", - "@jsonjoy.com/util": "workspace:*", - "@types/node": "^22.14.1", - "css-loader": "^7.1.2", - "electron": "^42.0.0", - "flexible-input": "^1.0.3", - "html-webpack-plugin": "^5.6.3", - "json-joy": "workspace:*", - "mini-css-extract-plugin": "^2.9.2", - "mutxt-react": "workspace:*", - "style-loader": "^4.0.0", - "ts-loader": "^9.5.2", - "webpack": "^5.99.5", - "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.1", - "wrangler": "^4.81.1" - } -} diff --git a/packages/mu-txt/public/favicon.png b/packages/mu-txt/public/favicon.png deleted file mode 100644 index 17200b4029..0000000000 Binary files a/packages/mu-txt/public/favicon.png and /dev/null differ diff --git a/packages/mu-txt/public/icon.svg b/packages/mu-txt/public/icon.svg deleted file mode 100644 index 4998186de6..0000000000 --- a/packages/mu-txt/public/icon.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/128x128-fitted.png b/packages/mu-txt/public/icons/128x128-fitted.png deleted file mode 100644 index ec3b1148d3..0000000000 Binary files a/packages/mu-txt/public/icons/128x128-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/128x128-fitted.svg b/packages/mu-txt/public/icons/128x128-fitted.svg deleted file mode 100644 index a8ddca9ac9..0000000000 --- a/packages/mu-txt/public/icons/128x128-fitted.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/128x128-fitted@2x.png b/packages/mu-txt/public/icons/128x128-fitted@2x.png deleted file mode 100644 index f05e58f4da..0000000000 Binary files a/packages/mu-txt/public/icons/128x128-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/128x128-spaced-1.png b/packages/mu-txt/public/icons/128x128-spaced-1.png deleted file mode 100644 index 5c21e9a6f9..0000000000 Binary files a/packages/mu-txt/public/icons/128x128-spaced-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/128x128-spaced-1.svg b/packages/mu-txt/public/icons/128x128-spaced-1.svg deleted file mode 100644 index be0f7b053b..0000000000 --- a/packages/mu-txt/public/icons/128x128-spaced-1.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/128x128-spaced.png b/packages/mu-txt/public/icons/128x128-spaced.png deleted file mode 100644 index d75d08a88d..0000000000 Binary files a/packages/mu-txt/public/icons/128x128-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/128x128-spaced.svg b/packages/mu-txt/public/icons/128x128-spaced.svg deleted file mode 100644 index bce637f28f..0000000000 --- a/packages/mu-txt/public/icons/128x128-spaced.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/128x128-spaced@2x-1.png b/packages/mu-txt/public/icons/128x128-spaced@2x-1.png deleted file mode 100644 index 42e3abe1b0..0000000000 Binary files a/packages/mu-txt/public/icons/128x128-spaced@2x-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/128x128-spaced@2x.png b/packages/mu-txt/public/icons/128x128-spaced@2x.png deleted file mode 100644 index 7cf43b0c1d..0000000000 Binary files a/packages/mu-txt/public/icons/128x128-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/16x16-fitted.png b/packages/mu-txt/public/icons/16x16-fitted.png deleted file mode 100644 index f5f68280b6..0000000000 Binary files a/packages/mu-txt/public/icons/16x16-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/16x16-fitted.svg b/packages/mu-txt/public/icons/16x16-fitted.svg deleted file mode 100644 index c670390bdd..0000000000 --- a/packages/mu-txt/public/icons/16x16-fitted.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/16x16-fitted@2x.png b/packages/mu-txt/public/icons/16x16-fitted@2x.png deleted file mode 100644 index 2fa7322ff6..0000000000 Binary files a/packages/mu-txt/public/icons/16x16-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/16x16-spaced-1.png b/packages/mu-txt/public/icons/16x16-spaced-1.png deleted file mode 100644 index 1f3a4dd387..0000000000 Binary files a/packages/mu-txt/public/icons/16x16-spaced-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/16x16-spaced-1.svg b/packages/mu-txt/public/icons/16x16-spaced-1.svg deleted file mode 100644 index 3059cbef71..0000000000 --- a/packages/mu-txt/public/icons/16x16-spaced-1.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/16x16-spaced.png b/packages/mu-txt/public/icons/16x16-spaced.png deleted file mode 100644 index 17200b4029..0000000000 Binary files a/packages/mu-txt/public/icons/16x16-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/16x16-spaced.svg b/packages/mu-txt/public/icons/16x16-spaced.svg deleted file mode 100644 index dc4b60d07f..0000000000 --- a/packages/mu-txt/public/icons/16x16-spaced.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/16x16-spaced@2x-1.png b/packages/mu-txt/public/icons/16x16-spaced@2x-1.png deleted file mode 100644 index d082917605..0000000000 Binary files a/packages/mu-txt/public/icons/16x16-spaced@2x-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/16x16-spaced@2x.png b/packages/mu-txt/public/icons/16x16-spaced@2x.png deleted file mode 100644 index 52fd7eb39c..0000000000 Binary files a/packages/mu-txt/public/icons/16x16-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/192x192-spaced.png b/packages/mu-txt/public/icons/192x192-spaced.png deleted file mode 100644 index d99aac9211..0000000000 Binary files a/packages/mu-txt/public/icons/192x192-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/192x192-spaced.svg b/packages/mu-txt/public/icons/192x192-spaced.svg deleted file mode 100644 index bbd37b6081..0000000000 --- a/packages/mu-txt/public/icons/192x192-spaced.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/192x192-spaced@2x.png b/packages/mu-txt/public/icons/192x192-spaced@2x.png deleted file mode 100644 index 8773cd6e9c..0000000000 Binary files a/packages/mu-txt/public/icons/192x192-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/24x24-fitted.png b/packages/mu-txt/public/icons/24x24-fitted.png deleted file mode 100644 index dd0e3e12ea..0000000000 Binary files a/packages/mu-txt/public/icons/24x24-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/24x24-fitted.svg b/packages/mu-txt/public/icons/24x24-fitted.svg deleted file mode 100644 index bd85c6d781..0000000000 --- a/packages/mu-txt/public/icons/24x24-fitted.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/24x24-fitted@2x.png b/packages/mu-txt/public/icons/24x24-fitted@2x.png deleted file mode 100644 index 60380388b9..0000000000 Binary files a/packages/mu-txt/public/icons/24x24-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/24x24-spaced-1.png b/packages/mu-txt/public/icons/24x24-spaced-1.png deleted file mode 100644 index 95807938a3..0000000000 Binary files a/packages/mu-txt/public/icons/24x24-spaced-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/24x24-spaced-1.svg b/packages/mu-txt/public/icons/24x24-spaced-1.svg deleted file mode 100644 index a1f41f187b..0000000000 --- a/packages/mu-txt/public/icons/24x24-spaced-1.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/24x24-spaced.png b/packages/mu-txt/public/icons/24x24-spaced.png deleted file mode 100644 index 3336d153b6..0000000000 Binary files a/packages/mu-txt/public/icons/24x24-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/24x24-spaced.svg b/packages/mu-txt/public/icons/24x24-spaced.svg deleted file mode 100644 index c31c2333cd..0000000000 --- a/packages/mu-txt/public/icons/24x24-spaced.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/24x24-spaced@2x-1.png b/packages/mu-txt/public/icons/24x24-spaced@2x-1.png deleted file mode 100644 index dece56d4c3..0000000000 Binary files a/packages/mu-txt/public/icons/24x24-spaced@2x-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/24x24-spaced@2x.png b/packages/mu-txt/public/icons/24x24-spaced@2x.png deleted file mode 100644 index f056b3be6b..0000000000 Binary files a/packages/mu-txt/public/icons/24x24-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/256x256-fitted.png b/packages/mu-txt/public/icons/256x256-fitted.png deleted file mode 100644 index c19ee7d4f0..0000000000 Binary files a/packages/mu-txt/public/icons/256x256-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/256x256-fitted.svg b/packages/mu-txt/public/icons/256x256-fitted.svg deleted file mode 100644 index 4159568689..0000000000 --- a/packages/mu-txt/public/icons/256x256-fitted.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/256x256-fitted@2x.png b/packages/mu-txt/public/icons/256x256-fitted@2x.png deleted file mode 100644 index b7d23a0ddc..0000000000 Binary files a/packages/mu-txt/public/icons/256x256-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/256x256-spaced-1.png b/packages/mu-txt/public/icons/256x256-spaced-1.png deleted file mode 100644 index 9afece3b2f..0000000000 Binary files a/packages/mu-txt/public/icons/256x256-spaced-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/256x256-spaced-1.svg b/packages/mu-txt/public/icons/256x256-spaced-1.svg deleted file mode 100644 index 2cdc011726..0000000000 --- a/packages/mu-txt/public/icons/256x256-spaced-1.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/256x256-spaced.png b/packages/mu-txt/public/icons/256x256-spaced.png deleted file mode 100644 index e67fb653a0..0000000000 Binary files a/packages/mu-txt/public/icons/256x256-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/256x256-spaced.svg b/packages/mu-txt/public/icons/256x256-spaced.svg deleted file mode 100644 index 64775d3fb7..0000000000 --- a/packages/mu-txt/public/icons/256x256-spaced.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/256x256-spaced@2x-1.png b/packages/mu-txt/public/icons/256x256-spaced@2x-1.png deleted file mode 100644 index bc922d9546..0000000000 Binary files a/packages/mu-txt/public/icons/256x256-spaced@2x-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/256x256-spaced@2x.png b/packages/mu-txt/public/icons/256x256-spaced@2x.png deleted file mode 100644 index 28c8f1f32a..0000000000 Binary files a/packages/mu-txt/public/icons/256x256-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/32x32-fitted.png b/packages/mu-txt/public/icons/32x32-fitted.png deleted file mode 100644 index b63070484b..0000000000 Binary files a/packages/mu-txt/public/icons/32x32-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/32x32-fitted.svg b/packages/mu-txt/public/icons/32x32-fitted.svg deleted file mode 100644 index cb787e87ad..0000000000 --- a/packages/mu-txt/public/icons/32x32-fitted.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/32x32-fitted@2x.png b/packages/mu-txt/public/icons/32x32-fitted@2x.png deleted file mode 100644 index 985fb7c25a..0000000000 Binary files a/packages/mu-txt/public/icons/32x32-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/32x32-spaced-1.png b/packages/mu-txt/public/icons/32x32-spaced-1.png deleted file mode 100644 index 508e7057ac..0000000000 Binary files a/packages/mu-txt/public/icons/32x32-spaced-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/32x32-spaced-1.svg b/packages/mu-txt/public/icons/32x32-spaced-1.svg deleted file mode 100644 index c3bf4869fe..0000000000 --- a/packages/mu-txt/public/icons/32x32-spaced-1.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/32x32-spaced.png b/packages/mu-txt/public/icons/32x32-spaced.png deleted file mode 100644 index 0dd1639ebf..0000000000 Binary files a/packages/mu-txt/public/icons/32x32-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/32x32-spaced.svg b/packages/mu-txt/public/icons/32x32-spaced.svg deleted file mode 100644 index 8d6d8b4cc3..0000000000 --- a/packages/mu-txt/public/icons/32x32-spaced.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/32x32-spaced@2x-1.png b/packages/mu-txt/public/icons/32x32-spaced@2x-1.png deleted file mode 100644 index a8128515b4..0000000000 Binary files a/packages/mu-txt/public/icons/32x32-spaced@2x-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/32x32-spaced@2x.png b/packages/mu-txt/public/icons/32x32-spaced@2x.png deleted file mode 100644 index 482661e81a..0000000000 Binary files a/packages/mu-txt/public/icons/32x32-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/512x512-spaced.png b/packages/mu-txt/public/icons/512x512-spaced.png deleted file mode 100644 index 0ae032894b..0000000000 Binary files a/packages/mu-txt/public/icons/512x512-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/512x512-spaced.svg b/packages/mu-txt/public/icons/512x512-spaced.svg deleted file mode 100644 index 4998186de6..0000000000 --- a/packages/mu-txt/public/icons/512x512-spaced.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/512x512-spaced@2x.png b/packages/mu-txt/public/icons/512x512-spaced@2x.png deleted file mode 100644 index 2624db5378..0000000000 Binary files a/packages/mu-txt/public/icons/512x512-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/64x64-fitted.png b/packages/mu-txt/public/icons/64x64-fitted.png deleted file mode 100644 index d8db56e03e..0000000000 Binary files a/packages/mu-txt/public/icons/64x64-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/64x64-fitted.svg b/packages/mu-txt/public/icons/64x64-fitted.svg deleted file mode 100644 index 19ca1b8983..0000000000 --- a/packages/mu-txt/public/icons/64x64-fitted.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/64x64-fitted@2x.png b/packages/mu-txt/public/icons/64x64-fitted@2x.png deleted file mode 100644 index 8c1409955e..0000000000 Binary files a/packages/mu-txt/public/icons/64x64-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/64x64-spaced-1.png b/packages/mu-txt/public/icons/64x64-spaced-1.png deleted file mode 100644 index 08250f13da..0000000000 Binary files a/packages/mu-txt/public/icons/64x64-spaced-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/64x64-spaced-1.svg b/packages/mu-txt/public/icons/64x64-spaced-1.svg deleted file mode 100644 index 5a55f32646..0000000000 --- a/packages/mu-txt/public/icons/64x64-spaced-1.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/64x64-spaced.png b/packages/mu-txt/public/icons/64x64-spaced.png deleted file mode 100644 index 9b6ab3a13e..0000000000 Binary files a/packages/mu-txt/public/icons/64x64-spaced.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/64x64-spaced.svg b/packages/mu-txt/public/icons/64x64-spaced.svg deleted file mode 100644 index 58a1604dc1..0000000000 --- a/packages/mu-txt/public/icons/64x64-spaced.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/64x64-spaced@2x-1.png b/packages/mu-txt/public/icons/64x64-spaced@2x-1.png deleted file mode 100644 index 336c7d5582..0000000000 Binary files a/packages/mu-txt/public/icons/64x64-spaced@2x-1.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/64x64-spaced@2x.png b/packages/mu-txt/public/icons/64x64-spaced@2x.png deleted file mode 100644 index 92dafa6882..0000000000 Binary files a/packages/mu-txt/public/icons/64x64-spaced@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-128x128-fitted.png b/packages/mu-txt/public/icons/avatar-128x128-fitted.png deleted file mode 100644 index 19e1b4bb91..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-128x128-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-128x128-fitted.svg b/packages/mu-txt/public/icons/avatar-128x128-fitted.svg deleted file mode 100644 index d8fa6a9dc3..0000000000 --- a/packages/mu-txt/public/icons/avatar-128x128-fitted.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/avatar-128x128-fitted@2x.png b/packages/mu-txt/public/icons/avatar-128x128-fitted@2x.png deleted file mode 100644 index 499658ec51..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-128x128-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-16x16-fitted.png b/packages/mu-txt/public/icons/avatar-16x16-fitted.png deleted file mode 100644 index 103e76fe89..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-16x16-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-16x16-fitted.svg b/packages/mu-txt/public/icons/avatar-16x16-fitted.svg deleted file mode 100644 index b1dad8f96d..0000000000 --- a/packages/mu-txt/public/icons/avatar-16x16-fitted.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/avatar-16x16-fitted@2x.png b/packages/mu-txt/public/icons/avatar-16x16-fitted@2x.png deleted file mode 100644 index 2ff7f57dec..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-16x16-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-24x24-fitted.png b/packages/mu-txt/public/icons/avatar-24x24-fitted.png deleted file mode 100644 index 5ca4e51dda..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-24x24-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-24x24-fitted.svg b/packages/mu-txt/public/icons/avatar-24x24-fitted.svg deleted file mode 100644 index e2ad32e8f7..0000000000 --- a/packages/mu-txt/public/icons/avatar-24x24-fitted.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/avatar-24x24-fitted@2x.png b/packages/mu-txt/public/icons/avatar-24x24-fitted@2x.png deleted file mode 100644 index 087686b309..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-24x24-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-256x256-fitted.png b/packages/mu-txt/public/icons/avatar-256x256-fitted.png deleted file mode 100644 index 80f7b885a2..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-256x256-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-256x256-fitted.svg b/packages/mu-txt/public/icons/avatar-256x256-fitted.svg deleted file mode 100644 index c37e4ac0f9..0000000000 --- a/packages/mu-txt/public/icons/avatar-256x256-fitted.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/avatar-256x256-fitted@2x.png b/packages/mu-txt/public/icons/avatar-256x256-fitted@2x.png deleted file mode 100644 index e8f5da8d71..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-256x256-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-32x32-fitted.png b/packages/mu-txt/public/icons/avatar-32x32-fitted.png deleted file mode 100644 index 1d4882707b..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-32x32-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-32x32-fitted.svg b/packages/mu-txt/public/icons/avatar-32x32-fitted.svg deleted file mode 100644 index cf8d7c2439..0000000000 --- a/packages/mu-txt/public/icons/avatar-32x32-fitted.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/avatar-32x32-fitted@2x.png b/packages/mu-txt/public/icons/avatar-32x32-fitted@2x.png deleted file mode 100644 index 8afcd18408..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-32x32-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-64x64-fitted.png b/packages/mu-txt/public/icons/avatar-64x64-fitted.png deleted file mode 100644 index b6b6506922..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-64x64-fitted.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/avatar-64x64-fitted.svg b/packages/mu-txt/public/icons/avatar-64x64-fitted.svg deleted file mode 100644 index 9ca411055e..0000000000 --- a/packages/mu-txt/public/icons/avatar-64x64-fitted.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/mu-txt/public/icons/avatar-64x64-fitted@2x.png b/packages/mu-txt/public/icons/avatar-64x64-fitted@2x.png deleted file mode 100644 index 34ac884611..0000000000 Binary files a/packages/mu-txt/public/icons/avatar-64x64-fitted@2x.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/icon-16.png b/packages/mu-txt/public/icons/icon-16.png deleted file mode 100644 index afe3d6808f..0000000000 Binary files a/packages/mu-txt/public/icons/icon-16.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/icon-192.png b/packages/mu-txt/public/icons/icon-192.png deleted file mode 100644 index d99aac9211..0000000000 Binary files a/packages/mu-txt/public/icons/icon-192.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/icon-32.png b/packages/mu-txt/public/icons/icon-32.png deleted file mode 100644 index 0dd1639ebf..0000000000 Binary files a/packages/mu-txt/public/icons/icon-32.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/icon-512.icns b/packages/mu-txt/public/icons/icon-512.icns deleted file mode 100644 index 121b190154..0000000000 Binary files a/packages/mu-txt/public/icons/icon-512.icns and /dev/null differ diff --git a/packages/mu-txt/public/icons/icon-512.ico b/packages/mu-txt/public/icons/icon-512.ico deleted file mode 100644 index 2ae438c8e8..0000000000 Binary files a/packages/mu-txt/public/icons/icon-512.ico and /dev/null differ diff --git a/packages/mu-txt/public/icons/icon-512.png b/packages/mu-txt/public/icons/icon-512.png deleted file mode 100644 index 0ae032894b..0000000000 Binary files a/packages/mu-txt/public/icons/icon-512.png and /dev/null differ diff --git a/packages/mu-txt/public/icons/icon-64.png b/packages/mu-txt/public/icons/icon-64.png deleted file mode 100644 index 9b6ab3a13e..0000000000 Binary files a/packages/mu-txt/public/icons/icon-64.png and /dev/null differ diff --git a/packages/mu-txt/public/index.html b/packages/mu-txt/public/index.html deleted file mode 100644 index 7e711a2ad1..0000000000 --- a/packages/mu-txt/public/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - μ txt (Micro Text) editor — free, clean, functional rich-text document editor - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - diff --git a/packages/mu-txt/public/manifest.webmanifest b/packages/mu-txt/public/manifest.webmanifest deleted file mode 100644 index f2c357c1bd..0000000000 --- a/packages/mu-txt/public/manifest.webmanifest +++ /dev/null @@ -1,47 +0,0 @@ -{ - "id": "/", - "name": "mutxt • μtxt • Micro Rich-Text Workspace", - "short_name": "mutxt", - "description": "Rich-text editor and workspace for creating and organizing multiple documents with a fast editor powered by mutxt and json-joy.", - "start_url": "/", - "scope": "/", - "display": "standalone", - "display_override": ["window-controls-overlay", "standalone"], - "background_color": "#ffffff", - "theme_color": "#ffffff", - "launch_handler": { - "client_mode": "navigate-new" - }, - "icons": [ - { - "src": "/icons/icon-16.png", - "sizes": "16x16", - "type": "image/png" - }, - { - "src": "/icons/icon-32.png", - "sizes": "32x32", - "type": "image/png" - }, - { - "src": "/icons/icon-64.png", - "sizes": "64x64", - "type": "image/png" - }, - { - "src": "/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "/icon.svg", - "sizes": "any", - "type": "image/svg+xml" - } - ] -} \ No newline at end of file diff --git a/packages/mu-txt/public/mutxt-logo-mark.png b/packages/mu-txt/public/mutxt-logo-mark.png deleted file mode 100644 index 3b7e7b7dfc..0000000000 Binary files a/packages/mu-txt/public/mutxt-logo-mark.png and /dev/null differ diff --git a/packages/mu-txt/public/mutxt-logo-mark.svg b/packages/mu-txt/public/mutxt-logo-mark.svg deleted file mode 100644 index d3000ee48d..0000000000 --- a/packages/mu-txt/public/mutxt-logo-mark.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/mu-txt/public/mutxt-logo-mark@2x.png b/packages/mu-txt/public/mutxt-logo-mark@2x.png deleted file mode 100644 index 47744d40d4..0000000000 Binary files a/packages/mu-txt/public/mutxt-logo-mark@2x.png and /dev/null differ diff --git a/packages/mu-txt/scripts/stage-electron.js b/packages/mu-txt/scripts/stage-electron.js deleted file mode 100644 index 0aef50b86c..0000000000 --- a/packages/mu-txt/scripts/stage-electron.js +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Build a self-contained staging directory that Electron Forge can package. -// -// Forge's dep walker (flora-colossus) doesn't compose with yarn workspaces: -// it walks transitive `workspace:*` deps and dies. We side-step this by -// assembling an isolated copy that looks like a vanilla npm project to Forge -// — only `electron` in the dep tree, with the already-bundled renderer -// (`dist/`) and main process (`out-electron/`) dropped in as data. - -const fs = require('node:fs'); -const path = require('node:path'); - -const root = path.resolve(__dirname, '..'); -const repoRoot = path.resolve(root, '..', '..'); -const stage = path.join(root, 'out-stage'); - -const die = (msg) => { - process.stderr.write(`stage-electron: ${msg}\n`); - process.exit(1); -}; - -const must = (relPath) => { - const abs = path.join(root, relPath); - if (!fs.existsSync(abs)) die(`missing ${relPath} — run \`yarn electron:build\` first`); - return abs; -}; - -const distDir = must('dist'); -const mainDir = must('out-electron'); -const binDir = must('bin'); -const iconsDir = must('public/icons'); -const forgeConfig = must('forge.config.ts'); - -const electronCandidates = [ - path.join(root, 'node_modules', 'electron'), - path.join(repoRoot, 'node_modules', 'electron'), -]; -const electronModule = electronCandidates.find((p) => fs.existsSync(p)); -if (!electronModule) die('cannot find electron in node_modules — run `yarn install`'); - -const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); -const electronVersion = pkg.devDependencies?.electron ?? pkg.dependencies?.electron; -if (!electronVersion) die('cannot resolve electron version from package.json'); - -fs.rmSync(stage, {recursive: true, force: true}); -fs.mkdirSync(stage, {recursive: true}); - -// Forge sees only `electron` as a dep — flora-colossus has nothing to choke on. -// Pull every @electron-forge/* version from the source package so the staged -// devDependencies satisfy Forge's project-resolver (which walks up looking -// for `@electron-forge/cli`). -const forgeDeps = Object.fromEntries( - Object.entries(pkg.devDependencies ?? {}).filter(([name]) => name.startsWith('@electron-forge/')), -); - -const stagePkg = { - name: pkg.name, - productName: 'mu-txt', - version: pkg.version, - description: pkg.description, - author: pkg.author, - license: pkg.license, - homepage: pkg.homepage, - repository: pkg.repository, - main: 'out-electron/main.js', - bin: pkg.bin, - dependencies: {electron: electronVersion}, - devDependencies: { - ...forgeDeps, - electron: electronVersion, - }, -}; -fs.writeFileSync(path.join(stage, 'package.json'), JSON.stringify(stagePkg, null, 2) + '\n'); - -const copy = (src, dest) => fs.cpSync(src, dest, {recursive: true, dereference: true}); - -copy(distDir, path.join(stage, 'dist')); -copy(mainDir, path.join(stage, 'out-electron')); -copy(binDir, path.join(stage, 'bin')); -copy(iconsDir, path.join(stage, 'public', 'icons')); - -fs.copyFileSync(forgeConfig, path.join(stage, 'forge.config.ts')); - -// Forge loads .ts configs via cosmiconfig + ts-node; a minimal tsconfig keeps -// the loader happy without inheriting the strict renderer config. -fs.writeFileSync( - path.join(stage, 'tsconfig.json'), - JSON.stringify( - { - compilerOptions: { - target: 'ES2022', - module: 'commonjs', - moduleResolution: 'node', - esModuleInterop: true, - skipLibCheck: true, - strict: false, - types: ['node'], - }, - }, - null, - 2, - ) + '\n', -); - -// Surface every dependency Forge needs from the repo's existing node_modules -// trees. We symlink each entry one-by-one so the stage looks like a flat npm -// install, which is what flora-colossus expects. -const stageNm = path.join(stage, 'node_modules'); -fs.mkdirSync(stageNm); -fs.symlinkSync(electronModule, path.join(stageNm, 'electron'), 'dir'); - -const linkAllExceptElectron = (sourceNm) => { - if (!fs.existsSync(sourceNm)) return; - for (const entry of fs.readdirSync(sourceNm)) { - if (entry === 'electron') continue; - const linkPath = path.join(stageNm, entry); - if (fs.existsSync(linkPath)) continue; - fs.symlinkSync(path.join(sourceNm, entry), linkPath, 'dir'); - } -}; - -linkAllExceptElectron(path.join(root, 'node_modules')); -linkAllExceptElectron(path.join(repoRoot, 'node_modules')); - -process.stdout.write(`stage-electron: staged → ${path.relative(process.cwd(), stage)}\n`); diff --git a/packages/mu-txt/src/App.stories.tsx b/packages/mu-txt/src/App.stories.tsx deleted file mode 100644 index 366edf4dcb..0000000000 --- a/packages/mu-txt/src/App.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import {App} from './App'; -import type {Meta, StoryObj} from '@storybook/react-webpack5'; - -const meta: Meta = { - title: 'App', - component: App, - tags: ['autodocs'], - argTypes: {}, - parameters: { - layout: 'fullscreen', - }, -}; - -export default meta; - -export const Primary: StoryObj = { - args: {}, -}; diff --git a/packages/mu-txt/src/App.tsx b/packages/mu-txt/src/App.tsx deleted file mode 100644 index d49ddd4299..0000000000 --- a/packages/mu-txt/src/App.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {UiProvider} from '@jsonjoy.com/ui'; -import {AppGrid} from '@jsonjoy.com/ui/lib/7-fullscreen/AppGrid'; -import {ErrorBoundary} from '@jsonjoy.com/ui/lib/misc/ErrorBoundary'; -import {MainContent} from './components/MainContent'; -import {MuTxtAppState} from './state'; -import {ctx} from './context'; -import {LeftSidebar} from './components/LeftSidebar'; -import {useT} from 'use-t'; -import {TabsHeader} from './components/TabsHeader'; -import {FileOptionsContextPane} from './components/LeftSidebar/SavedFileList/FileOptionsContextPane'; -import type {FileMetadataDto, OpenFile} from './state/file'; -import {host} from './util/host'; -import type {AnchorPoint} from '@jsonjoy.com/ui/lib/utils/popup'; - -const columnClass = rule({ - d: 'flex', - fld: 'column', - h: '100%', - minH: 0, -}); - -export const App: React.FC = () => { - const [_t] = useT(); - const state = React.useMemo(() => { - return new MuTxtAppState(); - }, []); - const [optionsPane, setOptionsPane] = React.useState<{file: FileMetadataDto; point: AnchorPoint} | null>(null); - const optionsPaneRef = React.useRef(null); - optionsPaneRef.current = optionsPane; - const wasOpenAtPressDownRef = React.useRef(false); - React.useEffect(() => { - const onPress = () => { - wasOpenAtPressDownRef.current = optionsPaneRef.current !== null; - }; - document.addEventListener('mousedown', onPress, true); - document.addEventListener('touchstart', onPress, true); - return () => { - document.removeEventListener('mousedown', onPress, true); - document.removeEventListener('touchstart', onPress, true); - }; - }, []); - React.useEffect(() => { - state.start().catch(() => {}); - return () => { - state.stop().catch(() => {}); - }; - }, [state]); - React.useEffect(() => { - const open = (file: OpenFile, point: AnchorPoint) => { - if (wasOpenAtPressDownRef.current) return; - setOptionsPane({file: file.toMeta(), point}); - }; - state.ondoubleclick = open; - state.onclick = open; - return () => { - state.ondoubleclick = void 0; - state.onclick = void 0; - }; - }, [state]); - - // Cmd+W in Electron - React.useEffect(() => host?.onCloseFile(() => state.closeSelected()), [state]); - - // Open a file passed by the host (CLI arg, dock open-file, etc.) - React.useEffect(() => { - if (!host) return; - return host.onOpenFile(({name, path, bytes}) => { - const link = {source: 'native-fs', path}; - if (bytes) { - const file = new File([bytes as BlobPart], name); - state.addFile(file, link).catch(() => {}); - } else { - // File doesn't exist yet — open a fresh document linked to this path. - state.createNewMuTxt(name, link); - } - }); - }, [state]); - - // Cmd/Ctrl+N: create a new document - React.useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - const mod = e.metaKey || e.ctrlKey; - if (!mod || e.shiftKey || e.altKey) return; - if (e.key !== 'n' && e.key !== 'N') return; - e.preventDefault(); - e.stopPropagation(); - state.createNewMuTxt(); - }; - window.addEventListener('keydown', onKeyDown, true); - return () => window.removeEventListener('keydown', onKeyDown, true); - }, [state]); - - const theme = state.theme.resolved.use(); - - return ( - - - ( - - - - )} - // footer={
} - column={(toggle) => ( -
- - - - - - -
- )} - /> - {optionsPane && ( - setOptionsPane(null)} - /> - )} -
-
- ); -}; diff --git a/packages/mu-txt/src/components/DropArea/index.tsx b/packages/mu-txt/src/components/DropArea/index.tsx deleted file mode 100644 index 9b41fe0136..0000000000 --- a/packages/mu-txt/src/components/DropArea/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import {DropArea as UiDropArea} from '@jsonjoy.com/ui/lib/5-block/DropArea'; -import {Text} from '@jsonjoy.com/ui/lib/1-inline/Text'; -import {useBehaviorSubject} from '@jsonjoy.com/ui/lib/hooks/useBehaviorSubject'; -import {useT} from 'use-t'; -import {useExplorer} from '../../context'; - -export interface DropAreaProps { - compact?: boolean; -} - -export const DropArea: React.FC = ({compact}) => { - const state = useExplorer(); - const files = useBehaviorSubject(state.files$); - const [t] = useT(); - - const onFiles = React.useCallback((dropped: File[]) => state.addFiles(dropped), [state]); - const onUri = React.useCallback((uri: string) => console.log('uri', uri), []); - const onText = React.useCallback((text: string) => console.log('text', text), []); - - return ( - 0} onFiles={onFiles} onUri={onUri} onText={onText}> - - {t('Click or drop files here')} - - - ); -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/BrandLogo.tsx b/packages/mu-txt/src/components/LeftSidebar/BrandLogo.tsx deleted file mode 100644 index 4500e402b2..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/BrandLogo.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import * as React from 'react'; -import {rule, theme} from 'nano-theme'; -import {BasicTooltip} from '@jsonjoy.com/ui/lib/4-card/BasicTooltip'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; - -const rootClass = rule({ - d: 'inline-flex', - h: '42px', - w: '48px', - mr: '0 0 4px', - ai: 'center', - jc: 'center', - us: 'none', -}); - -const fontSize = 22.5; - -const muClass = rule({ - ...theme.font.serif.mid, - letterSpacing: '.05em', - d: 'inline-flex', - tr: 'translateY(3px)', - sub: { - fz: `${(fontSize * 15.3) / 22.5}px`, - }, - trs: 'letter-spacing 0.1s ease, transform 0.1s ease', - [`.${rootClass.trim()}:hover &`]: { - letterSpacing: '-0.065em', - tr: 'translateY(3px)', - }, -}); - -const txtClass = rule({ - ...theme.font.slab.bold, - fz: `${fontSize}px`, -}); - -const txt1Class = rule({ - letterSpacing: '.03em', - d: 'inline-flex', - trs: 'letter-spacing 0.1s ease, transform 0.1s ease', - [`.${rootClass.trim()}:hover &`]: { - letterSpacing: '-0.03em', - tr: 'translateY(-5px)', - }, -}); - -const txt2Class = rule({ - letterSpacing: '.02em', - d: 'inline-flex', - trs: 'letter-spacing 0.1s ease, transform 0.1s ease', - [`.${rootClass.trim()}:hover &`]: { - letterSpacing: '0em', - // tr: 'translateY(1px)', - }, -}); - -const txt3Class = rule({ - d: 'inline-flex', - trs: 'letter-spacing 0.1s ease, transform 0.1s ease', - [`.${rootClass.trim()}:hover &`]: { - letterSpacing: '.01em', - tr: 'translateY(5px)', - }, -}); - -export type BrandLogoProps = { - color?: string; -}; - -export const BrandLogo: React.FC = ({color}) => { - const styles = useStyles(); - const label = '\\mu txt — Micro rich-text editor'; - - return ( - label} delay={555}> - - - μ - - - t - x - t - - - - ); -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/Header.tsx b/packages/mu-txt/src/components/LeftSidebar/Header.tsx deleted file mode 100644 index 1ff83e25ed..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/Header.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -import {Split} from '@jsonjoy.com/ui/lib/3-list-item/Split'; -import {Flex} from '@jsonjoy.com/ui/lib/3-list-item/Flex'; -import {useExplorer} from '../../context'; -import {HeaderMenu} from './HeaderMenu'; -import {rule} from 'nano-theme'; -import {isMacElectron} from '../../util/host'; - -// Width reserved for the macOS traffic-light controls (close/minimize/zoom). -const TRAFFIC_LIGHT_WIDTH = 78; - -const blockClass = rule({ - '-webkit-app-region': 'drag', // Drag for Electron app. - '& button, & a, & input, & textarea, & select, & [role="button"], & [role="tablist"], & [role="img"]': { - '-webkit-app-region': 'no-drag', - }, -}); - -export interface HeaderProps { - toggle: React.ReactNode; -} - -export const Header: React.FC = ({toggle}) => { - const state = useExplorer(); - const inputRef = React.useRef(null); - - return ( - - -
-
{toggle}
- {/* */} - {/* {!isMacElectron && } */} - { - const input = inputRef.current; - if (!input) return; - const files = input.files; - if (files) state.addFiles(Array.from(files)); - }} - /> - -
- - {/* {isMacElectron && } */} - {/* {toggle} */} -
- - ); -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/HeaderMenu.tsx b/packages/mu-txt/src/components/LeftSidebar/HeaderMenu.tsx deleted file mode 100644 index 1fd27f96c7..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/HeaderMenu.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import * as React from 'react'; -import {useExplorer} from '../../context'; -import {Popup} from '@jsonjoy.com/ui/lib/4-card/Popup'; -import {ContextMenu} from '@jsonjoy.com/ui/lib/4-card/ContextMenu'; -import {BasicButtonMore} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonMore'; -import {Sidetip} from '@jsonjoy.com/ui/lib/1-inline/Sidetip'; -import {Tabs} from '@jsonjoy.com/ui/lib/3-list-item/Tabs'; -import {useT} from 'use-t'; -import {Iconista} from '@jsonjoy.com/ui/lib/icons/Iconista'; -import {formatKeys} from 'mutxt-react'; -import {host} from '../../util/host'; -import type {ThemePreference} from '../../state/theme'; - -export type HeaderMenuProps = {}; - -const ThemeTabs: React.FC = () => { - const [t] = useT(); - const state = useExplorer(); - const preference = state.theme.preference.use(); - return ( - - state.theme.set(key as ThemePreference)} - active={preference} - items={[ - { - key: 'auto', - label: ( - - - {t('Auto')} - - ), - }, - { - key: 'light', - label: ( - - - {t('Light')} - - ), - }, - { - key: 'dark', - label: ( - - - {t('Dark')} - - ), - }, - ]} - /> - - ); -}; - -export const HeaderMenu: React.FC = () => { - const state = useExplorer(); - const inputRef = React.useRef(null); - const onOpenClick = React.useCallback(() => { - if (host) void host.openDialog(); - else inputRef.current?.click(); - }, []); - const onInputChange = React.useCallback( - (e: React.ChangeEvent) => { - const files = e.currentTarget.files; - if (files && files.length) void state.addFiles(Array.from(files)); - e.currentTarget.value = ''; - }, - [state], - ); - - const templateMenu = state.menus.newFileMenu(); - - return ( - <> - ( - , - right: () => {formatKeys(['Primary', 'N'])}, - onSelect: () => state.createNewMuTxt(), - }, - { - ...templateMenu, - name: 'From template', - expand: 0, - icon: () => , - }, - { - name: 'Open file', - icon: () => , - onSelect: onOpenClick, - }, - { - name: 'Close all files', - sepBefore: true, - icon: () => , - onSelect: state.closeAll, - }, - { - name: 'Theme', - sepBefore: true, - raw: () => , - }, - ], - }} - /> - )} - > - - - - - ); -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/FileOptionsContextPane.tsx b/packages/mu-txt/src/components/LeftSidebar/SavedFileList/FileOptionsContextPane.tsx deleted file mode 100644 index 7844440183..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/FileOptionsContextPane.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react'; -import {context as popupContext} from '@jsonjoy.com/ui/lib/4-card/Popup/context'; -import {ClickAway} from '@jsonjoy.com/ui/lib/utils/ClickAway'; -import {PositionAtPoint} from '@jsonjoy.com/ui/lib/utils/popup/PositionAtPoint'; -import {MoveToViewport} from '@jsonjoy.com/ui/lib/utils/popup/MoveToViewport'; -import type {AnchorPoint} from '@jsonjoy.com/ui/lib/utils/popup'; -import {FileOptionsForm2} from './FileOptionsForm2'; -import type {FileMetadataDto} from '../../../state/file'; - -export interface FileOptionsContextPaneProps { - file: FileMetadataDto; - point: AnchorPoint; - onClose: () => void; -} - -export const FileOptionsContextPane: React.FC = ({file, point, onClose}) => { - const popupCtx = React.useMemo(() => ({close: onClose}), [onClose]); - return ( - - - - - - - - - - ); -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/FileOptionsForm2.tsx b/packages/mu-txt/src/components/LeftSidebar/SavedFileList/FileOptionsForm2.tsx deleted file mode 100644 index ce316f5ecc..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/FileOptionsForm2.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import * as React from 'react'; -import {ArgsPane} from '@jsonjoy.com/ui/lib/4-card/ContextMenu/ArgsPane'; -import {usePopup} from '@jsonjoy.com/ui/lib/4-card/Popup/context'; -import {syncStore} from '@jsonjoy.com/ui/lib/hooks/useSyncStore'; -import {useBehaviorSubject} from '@jsonjoy.com/ui/lib/hooks/useBehaviorSubject'; -import {useT} from 'use-t'; -import {useExplorer} from '../../../context'; -import type {MenuItem, Param} from '@jsonjoy.com/ui/lib/4-card/StructuralMenu/types'; -import type {FileMetadataDto, OpenFile} from '../../../state/file'; -import NameIcon__svg from 'iconista/lib/react/bootstrap/file-earmark-binary'; -import HashIcon__svg from 'iconista/lib/react/bootstrap/hash'; -import ClockIcon__svg from 'iconista/lib/react/bootstrap/clock'; -import FileIcon__svg from 'iconista/lib/react/bootstrap/file-earmark'; -import LinkIcon__svg from 'iconista/lib/react/bootstrap/link-45deg'; -import FolderIcon__svg from 'iconista/lib/react/bootstrap/folder2-open'; -import DownloadIcon__svg from 'iconista/lib/react/auth0/download'; -import TrashIcon__svg from 'iconista/lib/react/bootstrap/trash2'; - -const NameIcon: React.FC> = (props) => ( - -); -const HashIcon: React.FC> = (props) => ( - -); -const ClockIcon: React.FC> = (props) => ( - -); -const FileIcon: React.FC> = (props) => ( - -); -const LinkIcon: React.FC> = (props) => ( - -); -const FolderIcon: React.FC> = (props) => ( - -); -const DownloadIcon: React.FC> = (props) => ( - -); -const TrashIcon: React.FC> = (props) => ( - -); - -const renderNameIcon = () => ; -const renderHashIcon = () => ; -const renderClockIcon = () => ; -const renderFileIcon = () => ; -const renderLinkIcon = () => ; -const renderFolderIcon = () => ; -const renderDownloadIcon = () => ; -const renderTrashIcon = () => ; - -export interface FileOptionsForm2Props { - file: FileMetadataDto; -} - -export const FileOptionsForm2: React.FC = ({file}) => { - const [t] = useT(); - const state = useExplorer(); - const popup = usePopup(); - const openFiles = useBehaviorSubject(state.files$); - const openFile: OpenFile | undefined = openFiles.find((f) => f.id === file.id); - const currentName = (openFile ? openFile.name.value : undefined) ?? file.name; - const currentNameRef = React.useRef(currentName); - currentNameRef.current = currentName; - - const onCancel = React.useCallback(() => { - popup?.close(); - }, [popup]); - - const handleDownload = React.useCallback(() => { - state.download(file.id).catch(() => {}); - }, [state, file.id]); - - const handleDelete = React.useCallback(async () => { - popup?.close(); - await state.deleteSaved(file.id); - }, [state, file.id, popup]); - - const item: MenuItem = React.useMemo(() => ({name: t('File options'), compact: true}), [t]); - - const handleSubmit = React.useCallback(() => { - popup?.close(); - }, [popup]); - - const hasLink = !!file.link; - const hasSize = !!openFile?.size; - - const params: (Param | MenuItem)[] = React.useMemo(() => { - const list: (Param | MenuItem)[] = []; - - // list.push({name: t('File'), heading: true, collapsible: true}); - list.push({ - kind: 'str', - id: 'name', - name: t('Name'), - icon: renderNameIcon, - default: currentName, - placeholder: t('Untitled'), - onSubmit: handleSubmit, - }); - list.push({ - kind: 'code', - id: 'id', - name: 'ID', - icon: renderHashIcon, - value: file.id, - }); - - list.push({name: t('Metadata'), heading: true, collapsible: true}); - list.push({ - kind: 'info', - id: 'createdAt', - name: t('Created'), - icon: renderClockIcon, - variant: 'date', - value: file.createdAt, - }); - list.push({ - kind: 'info', - id: 'updatedAt', - name: t('Updated'), - icon: renderClockIcon, - variant: 'date', - value: file.updatedAt, - }); - list.push({ - kind: 'info', - id: 'size', - name: t('Size'), - icon: renderFileIcon, - variant: 'bytes', - value: openFile?.size ?? 0, - visible: syncStore(hasSize), - }); - - if (hasLink && file.link) { - list.push({name: t('Link'), heading: true, collapsible: true}); - list.push({ - kind: 'code', - id: 'linkSource', - name: t('Source'), - icon: renderLinkIcon, - value: file.link.source, - }); - list.push({ - kind: 'code', - id: 'linkPath', - name: t('Path'), - icon: renderFolderIcon, - value: file.link.path, - }); - } - - list.push({name: t('Actions'), heading: true, collapsible: true, initialCollapsed: true}); - list.push({ - kind: 'btn', - id: 'download', - name: t('Create a copy'), - icon: renderDownloadIcon, - buttonLabel: t('Download'), - buttonIcon: renderDownloadIcon, - onClick: handleDownload, - }); - - list.push({ - name: t('Danger zone'), - heading: true, - collapsible: true, - initialCollapsed: true, - danger: true, - }); - list.push({ - kind: 'btn', - id: 'delete', - name: '', - buttonLabel: t('Delete permanently'), - buttonIcon: renderTrashIcon, - danger: true, - confirm: true, - confirmLabel: t('Confirm delete'), - confirmActionLabel: t('Delete'), - onClick: handleDelete, - }); - - return list; - }, [ - t, - currentName, - file.id, - file.createdAt, - file.updatedAt, - file.link, - hasLink, - hasSize, - openFile?.size, - handleDownload, - handleDelete, - handleSubmit, - ]); - - const onChange = React.useCallback( - (_list: [string, unknown][], map: Record) => { - const nextName = (map.name as string | undefined) ?? ''; - if (nextName === currentNameRef.current) return; - if (openFile) { - state.renameFile(openFile, nextName); - } else { - void state.rename(file.id, nextName); - } - }, - [openFile, state, file.id], - ); - - return ; -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/SavedFile.tsx b/packages/mu-txt/src/components/LeftSidebar/SavedFileList/SavedFile.tsx deleted file mode 100644 index a0f3601911..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/SavedFile.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from 'react'; -import {useExplorer} from '../../../context'; -import {Bytes} from '@jsonjoy.com/ui/lib/1-inline/Bytes'; -import {TimeAgo} from '@jsonjoy.com/ui/lib/1-inline/TimeAgo'; -import {FileListItem} from '@jsonjoy.com/ui/lib/3-list-item/FileListItem'; -import {BasicButtonMore} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonMore'; -import {FileIcon} from '@jsonjoy.com/ui/lib/1-inline/FileIcon'; -import {Popup} from '@jsonjoy.com/ui/lib/4-card/Popup'; -import {FileOptionsForm2 as FileOptionsForm} from './FileOptionsForm2'; -import type {FileMetadataDto} from '../../../state/file'; -import GhostFileIcon__svg from 'iconista/lib/react/bootstrap/file-earmark-font'; - -const GhostFileIcon: React.FC> = (props) => ( - -); -const icon = ; - -export interface SavedFileProps { - file: FileMetadataDto; -} - -export const SavedFile: React.FC = ({file}) => { - const state = useExplorer(); - const selected = state.tabs.selected.use(); - const activeIcon = ; - const openFile = state.fileIfOpen(file.id); - const isOpen = !!openFile; - - return ( - - - {!!openFile && ( - <> - {' · '} - - - )} - - } - icon={isOpen ? activeIcon : icon} - iconHover={activeIcon} - actions={ -
- }> - - -
- } - onClick={() => { - state.openSaved(file.id).catch(() => {}); - state.appGrid.closeLeftIfOverlay(); - }} - /> - ); -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/index.tsx b/packages/mu-txt/src/components/LeftSidebar/SavedFileList/index.tsx deleted file mode 100644 index 45f1d93d40..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/SavedFileList/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import {useT} from 'use-t'; -import {useExplorer} from '../../../context'; -import {SavedFile} from './SavedFile'; -import {MiniTitle} from '@jsonjoy.com/ui/lib/3-list-item/MiniTitle'; - -export type SavedFileListProps = Record; - -export const SavedFileList: React.FC = () => { - const [t] = useT(); - const state = useExplorer(); - const files = state.saved.use(); - - if (!files.length) return null; - - return ( - <> -
- {t('My Files')} -
- {files.map((file) => ( - - ))} - - ); -}; diff --git a/packages/mu-txt/src/components/LeftSidebar/index.tsx b/packages/mu-txt/src/components/LeftSidebar/index.tsx deleted file mode 100644 index 1d9feb8a18..0000000000 --- a/packages/mu-txt/src/components/LeftSidebar/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import {SavedFileList} from './SavedFileList'; -import {useBehaviorSubject} from '@jsonjoy.com/ui/lib/hooks/useBehaviorSubject'; -import {useExplorer} from '../../context'; -import {useT} from 'use-t'; -import {NewFileForm} from '../NewFileForm'; -import {AppGridColumn} from '@jsonjoy.com/ui/lib/7-fullscreen/AppGrid'; -import {Header} from './Header'; - -export interface LeftSidebarProps { - toggle: React.ReactNode; -} - -export const LeftSidebar: React.FC = ({toggle}) => { - const [_t] = useT(); - const state = useExplorer(); - const files = useBehaviorSubject(state.files$); - - return ( - } - footer={ - !!files.length && ( -
- -
- ) - } - scrollRailWidth={4} - > -
-
- -
-
-
- ); -}; diff --git a/packages/mu-txt/src/components/MainContent/Document.tsx b/packages/mu-txt/src/components/MainContent/Document.tsx deleted file mode 100644 index 321bdb4af0..0000000000 --- a/packages/mu-txt/src/components/MainContent/Document.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react'; -import {isMobile, isTouch} from '@jsonjoy.com/ui'; -import {Log} from './Log'; -import {DocumentMuTxt} from './DocumentMuTxt'; -import {useBehaviorSubject} from '@jsonjoy.com/ui/lib/hooks/useBehaviorSubject'; -import {useExplorer} from '../../context'; -import {DocumentJson} from './DocumentJson'; -import {SetNamedTrace} from '@jsonjoy.com/ui'; -import type {ObjApi} from 'json-joy/lib/json-crdt'; -import type {OpenFile} from '../../state/file'; - -export interface DocumentProps { - file: OpenFile; -} - -export const Document: React.FC = ({file}) => { - const activeModel = file.activeModel.use(); - const obj: ObjApi | undefined = React.useMemo(() => { - if (activeModel.api.read('/@type') !== 'mutxt') return; - return activeModel.api.obj([]); - }, [activeModel]); - const state = useExplorer(); - const selected = useBehaviorSubject(state.file$); - - const readonly = activeModel !== file.log.end; - const visible = selected === file; - - if (obj) { - return ( - - - {!isTouch && !isMobile && file.activeModel.next(model)} />} - - ); - } - - return ( - - - - ); -}; diff --git a/packages/mu-txt/src/components/MainContent/DocumentJson.tsx b/packages/mu-txt/src/components/MainContent/DocumentJson.tsx deleted file mode 100644 index 91e9ab88e3..0000000000 --- a/packages/mu-txt/src/components/MainContent/DocumentJson.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import type {OpenFile} from '../../state/file'; -import {JsonCrdtLog} from '@jsonjoy.com/collaborative-ui/lib/JsonCrdtLog'; - -const editorShellClass = rule({ - d: 'flex', - fld: 'column', - flex: '1 1 0%', - bxz: 'border-box', - pd: '0', - minH: 0, -}); - -export interface DocumentJsonProps { - file: OpenFile; - readOnly?: boolean; - visible?: boolean; -} - -export const DocumentJson: React.FC = ({file, visible}) => { - React.useEffect(() => { - file.logState.view$.next('model'); - // file.logState.modelState. - }, [file]); - - return ( -
- file.activeModel.next(model)} - /> -
- ); -}; diff --git a/packages/mu-txt/src/components/MainContent/DocumentMuTxt.tsx b/packages/mu-txt/src/components/MainContent/DocumentMuTxt.tsx deleted file mode 100644 index 1845be58fa..0000000000 --- a/packages/mu-txt/src/components/MainContent/DocumentMuTxt.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {MuTxt, type MuTxtApi} from 'mutxt-react'; -import type {ObjApi} from 'json-joy/lib/json-crdt'; -import type {OpenFile} from '../../state/file'; - -const editorShellClass = rule({ - d: 'flex', - fld: 'column', - flex: '1 1 0%', - bxz: 'border-box', - pd: '0', - minH: 0, -}); - -const minHeight = 400; - -export interface DocumentMuTxtProps { - file: OpenFile; - obj: ObjApi; - readOnly?: boolean; - visible?: boolean; -} - -export const DocumentMuTxt: React.FC = ({file, obj, readOnly, visible}) => { - const [api, setApi] = React.useState(null); - React.useEffect(() => { - if (!api || !visible) return; - return file.bindMuTxt(api); - }, [api, visible, file]); - - return ( -
- { - if (!title) return; - const now = Date.now(); - const fileLifeTime = now - file.meta.createdAt; - const isFileOlderThan3Minutes = fileLifeTime > 3 * 60 * 1000; - if (isFileOlderThan3Minutes) return; - file.name.next(title); - }} - /> -
- ); -}; diff --git a/packages/mu-txt/src/components/MainContent/Log/index.tsx b/packages/mu-txt/src/components/MainContent/Log/index.tsx deleted file mode 100644 index 915ad8fd01..0000000000 --- a/packages/mu-txt/src/components/MainContent/Log/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {useT} from 'use-t'; -import {useExplorer} from '../../../context'; -import {useBehaviorSubject, useBehaviorSubjectOpt} from '@jsonjoy.com/ui/lib/hooks/useBehaviorSubject'; -import {JsonCrdtLog} from '@jsonjoy.com/collaborative-ui/lib/JsonCrdtLog'; -import * as ScrollArea from '@jsonjoy.com/ui/src/4-card/ScrollArea'; -import {BasicButtonClose} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonClose'; -import {BasicTooltip} from '@jsonjoy.com/ui/lib/4-card/BasicTooltip'; -import type {Model} from 'json-joy/lib/json-crdt'; - -const openButtonClass = rule({ - w: '33%', - h: '20px', - pad: 0, - mar: 0, - bd: 'none', - bgc: 'transparent', - cursor: 'pointer', - d: 'flex', - ai: 'center', - jc: 'center', -}); - -const openBarClass = rule({ - d: 'block', - w: '100%', - h: '4px', - bdrad: '2px', - bgc: 'currentColor', - op: 0.1, - trs: 'opacity 120ms ease', - [`.${openButtonClass.trim()}:hover &`]: { - op: 0.5, - }, -}); - -export interface LogProps { - visible?: boolean; - onModel?: (model: Model, readonly: boolean) => void; -} - -export const Log: React.FC = ({visible, onModel}) => { - const [t] = useT(); - const state = useExplorer(); - const file = useBehaviorSubject(state.file$); - const logView = useBehaviorSubjectOpt(file?.logState.view$); - const [open, setOpen] = React.useState(false); - - if (!file || !visible) { - return null; - } - - if (!open) { - const showLog = t('Show log'); - return ( -
- -
- ); - } - - return ( -
-
- t('Hide log')}> - setOpen(false)} /> - -
-
- - - - - - -
-
- ); -}; diff --git a/packages/mu-txt/src/components/MainContent/index.tsx b/packages/mu-txt/src/components/MainContent/index.tsx deleted file mode 100644 index 4beb6dbb16..0000000000 --- a/packages/mu-txt/src/components/MainContent/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {useBehaviorSubject} from '@jsonjoy.com/ui/lib/hooks/useBehaviorSubject'; -import {ErrorBoundary} from '@jsonjoy.com/ui/lib/misc/ErrorBoundary'; -import {useExplorer} from '../../context'; -import {Welcome} from '../Welcome'; -import {Document} from './Document'; -import {DotBackground} from '@jsonjoy.com/ui/src/5-block/DotBackground'; -import {AppGridColumn} from '@jsonjoy.com/ui/src/7-fullscreen/AppGrid'; - -const blockClass = rule({ - w: '100%', - d: 'flex', - fld: 'column', - flex: '1 1 0%', - minH: 0, -}); - -const contentClass = rule({ - w: '100%', - h: '100%', - minH: 0, - d: 'flex', - fld: 'column', - gap: '12px', - flex: '1 1 auto', - bxz: 'border-box', - pd: '16px 0 0', -}); - -export const MainContent: React.FC = () => { - const state = useExplorer(); - const started = state.started.use(); - const files = useBehaviorSubject(state.files$); - const selected = useBehaviorSubject(state.file$); - - if (!started) return; - - if (!files.length) { - return ( - -
- - - -
-
- ); - } - - return ( -
- - {files.map((f) => ( - - - - ))} - -
- ); -}; diff --git a/packages/mu-txt/src/components/NewFileForm/index.tsx b/packages/mu-txt/src/components/NewFileForm/index.tsx deleted file mode 100644 index c03c80a81e..0000000000 --- a/packages/mu-txt/src/components/NewFileForm/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import {Space} from '@jsonjoy.com/ui/lib/3-list-item/Space'; -import {DropArea} from '../DropArea'; -import {useExplorer} from '../../context'; -import {CreateButton} from '@jsonjoy.com/collaborative-ui/lib/molecules/CreateButton'; -import {MiniTitle} from '@jsonjoy.com/ui/lib/3-list-item/MiniTitle'; -import {useT} from 'use-t'; -import {Flex} from '@jsonjoy.com/ui/lib/3-list-item/Flex'; -import {BasicButtonMore} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonMore'; -import {ContextMenu} from '@jsonjoy.com/ui/lib/4-card/ContextMenu'; -import {Popup} from '@jsonjoy.com/ui/lib/4-card/Popup'; - -export interface NewFileFormProps { - expanded?: boolean; -} - -export const NewFileForm: React.FC = ({expanded}) => { - const [t] = useT(); - const state = useExplorer(); - - return ( -
e.stopPropagation()} onKeyDown={() => {}} style={{maxWidth: 360, margin: '0 auto'}}> -
- {!!expanded && {t('New')}} - - - state.createNewMuTxt()} - > - {t('New document')} - - - } - tooltip={{renderTooltip: () => t('From template'), nowrap: true}} - > - - - -
- {!!expanded && ( - <> - -
- Open - - -
- - )} -
- ); -}; diff --git a/packages/mu-txt/src/components/TabsHeader/PwaInstallButton.tsx b/packages/mu-txt/src/components/TabsHeader/PwaInstallButton.tsx deleted file mode 100644 index 87a7197775..0000000000 --- a/packages/mu-txt/src/components/TabsHeader/PwaInstallButton.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from 'react'; -import {BasicButton} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton'; -import {Label} from '@jsonjoy.com/ui/lib/1-inline/Label'; - -interface BeforeInstallPromptEvent extends Event { - readonly platforms: string[]; - readonly userChoice: Promise<{outcome: 'accepted' | 'dismissed'; platform: string}>; - prompt(): Promise; -} - -const isStandaloneDisplay = (): boolean => { - if (typeof window === 'undefined') return false; - return ( - window.matchMedia('(display-mode: standalone)').matches || - !!(navigator as Navigator & {standalone?: boolean}).standalone - ); -}; - -export type PwaInstallButtonProps = {}; - -export const PwaInstallButton: React.FC = () => { - const [deferredPrompt, setDeferredPrompt] = React.useState(null); - const [installed, setInstalled] = React.useState(() => isStandaloneDisplay()); - - React.useEffect(() => { - const handleBeforeInstallPrompt = (event: Event) => { - const promptEvent = event as BeforeInstallPromptEvent; - promptEvent.preventDefault(); - setDeferredPrompt(promptEvent); - }; - - const handleAppInstalled = () => { - setInstalled(true); - setDeferredPrompt(null); - }; - - const mediaQuery = window.matchMedia('(display-mode: standalone)'); - const handleDisplayModeChange = () => { - if (isStandaloneDisplay()) { - setInstalled(true); - setDeferredPrompt(null); - } - }; - - window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt as EventListener); - window.addEventListener('appinstalled', handleAppInstalled); - mediaQuery.addEventListener('change', handleDisplayModeChange); - - return () => { - window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt as EventListener); - window.removeEventListener('appinstalled', handleAppInstalled); - mediaQuery.removeEventListener('change', handleDisplayModeChange); - }; - }, []); - - if (installed || !deferredPrompt) return null; - - const handleInstallClick = async () => { - await deferredPrompt.prompt(); - const choice = await deferredPrompt.userChoice.catch(() => null); - setDeferredPrompt(null); - if (choice?.outcome === 'accepted') setInstalled(true); - }; - - return ( - void handleInstallClick()} - > - Install as  - - ); -}; diff --git a/packages/mu-txt/src/components/TabsHeader/index.tsx b/packages/mu-txt/src/components/TabsHeader/index.tsx deleted file mode 100644 index ce6f243c9a..0000000000 --- a/packages/mu-txt/src/components/TabsHeader/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import {rule, useTheme} from 'nano-theme'; -import {useExplorer} from '../../context'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {FileTabs} from '@jsonjoy.com/ui/lib/3-list-item/FileTabs'; -import {PwaInstallButton} from './PwaInstallButton'; - -const enum Sizes { - TabsHeight = 48, - TabsFadeHeight = 16, -} - -const blockClass = rule({ - h: Sizes.TabsHeight + 'px', -}); - -export interface TabsHeaderProps { - toggle: React.ReactNode; -} - -export const TabsHeader: React.FC = ({toggle}) => { - const state = useExplorer(); - const styles = useStyles(); - const theme = useTheme(); - - return ( -
- {toggle}
} - right={} - render={() =>
} - barStyle={{ - borderRadius: 12, // Needed for electron on Mac to match the titlebar's rounded corners - }} - /> -
- ); -}; diff --git a/packages/mu-txt/src/components/Typesetting/index.tsx b/packages/mu-txt/src/components/Typesetting/index.tsx deleted file mode 100644 index bd9abe15d8..0000000000 --- a/packages/mu-txt/src/components/Typesetting/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {useT} from 'use-t'; -import {fonts} from '@jsonjoy.com/ui/lib/styles'; - -const blockClass = rule({ - ...fonts.get('sans', 'mid', 0), - fz: '13.4px', - lh: '1.6em', - p: { - mr: 0, - pd: '4px 0', - }, - a: { - textDecoration: 'underline', - textDecorationThickness: '1px', - textUnderlineOffset: '4px', - textDecorationColor: 'rgb(from currentColor r g b / 0.2)', - '&:hover': { - textDecorationColor: 'currentColor', - }, - }, - code: { - lh: '1.3em', - }, - 'ol, ul': { - mr: 0, - pd: '4px 0 4px 20px', - }, - li: { - pd: '4px 0', - }, - h6: { - fz: '1.05em', - fw: 600, - mr: '16px 0 4px', - }, - h5: { - fz: '1.1em', - fw: 600, - mr: '16px 0 4px', - }, - h4: { - fz: '1.25em', - fw: 600, - mr: '16px 0 4px', - }, - h3: { - fz: '1.3em', - fw: 700, - mr: '16px 0 4px', - o: 0.88, - lh: '1.4em', - }, - h2: { - fz: '1.5em', - fw: 700, - mr: '16px 0 4px', - }, - h1: { - fz: '2em', - fw: 700, - mr: '16px 0 4px', - }, -}); - -export interface TypesettingProps extends React.HTMLAttributes { - children?: React.ReactNode; -} - -export const Typesetting: React.FC = ({children, ...props}) => { - const [_t] = useT(); - - return ( -
- {children} -
- ); -}; diff --git a/packages/mu-txt/src/components/Welcome/ActionCard.tsx b/packages/mu-txt/src/components/Welcome/ActionCard.tsx deleted file mode 100644 index 72afc917b7..0000000000 --- a/packages/mu-txt/src/components/Welcome/ActionCard.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import * as React from 'react'; -import {drule, rule, useTheme} from 'nano-theme'; -import {Tilt} from '@jsonjoy.com/ui/lib/4-card/Tilt'; - -const blockClass = drule({ - pos: 'relative', - d: 'flex', - fld: 'column', - ai: 'flex-start', - gap: '6px', - pd: '20px 20px 18px', - cur: 'pointer', - ta: 'left', - us: 'none', - transition: 'background .15s ease, border-color .15s ease', - overflow: 'hidden', - w: '100%', - h: '100%', - minH: '128px', - boxSizing: 'border-box', - '&:active': { - transform: 'scale(.99)', - }, -}); - -const iconWrapClass = rule({ - d: 'inline-flex', - ai: 'center', - jc: 'center', - w: '34px', - h: '34px', - bdrad: '10px', - mrb: '6px', -}); - -const titleClass = rule({ - fz: '14px', - fw: 600, - lh: '1.3em', - mr: 0, -}); - -const descClass = rule({ - fz: '12px', - lh: '1.4em', - mr: 0, -}); - -export interface ActionCardProps { - icon: React.ReactNode; - title: React.ReactNode; - description?: React.ReactNode; - onClick?: React.MouseEventHandler; -} - -export const ActionCard: React.FC = ({icon, title, description, onClick}) => { - const theme = useTheme(); - - const iconBg = theme.g(0.77, 0.2); - const iconCol = theme.g(0.18, 0.92); - - const baseBg = theme.g(0.93, 0.11); - const hoverBg = theme.g(0.88, 0.4); - - const baseBd = theme.g(0.74, 0.32); - const hoverBd = theme.g(0.66, 0.4); - - const cls = blockClass({ - bg: baseBg, - bd: `1px solid ${baseBd}`, - '&:hover': { - bg: hoverBg, - bd: `1px solid ${hoverBd}`, - }, - }); - - const paperStyle: React.CSSProperties = { - borderRadius: 14, - boxShadow: `0 1px 0 ${theme.g(0, 0.02)}`, - }; - - return ( - -
) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - (e.currentTarget as HTMLDivElement).click(); - } - }} - role="button" - tabIndex={0} - className={cls} - style={paperStyle} - > -
- {icon} -
-

- {title} -

- {description && ( -

- {description} -

- )} -
-
- ); -}; diff --git a/packages/mu-txt/src/components/Welcome/RecentFiles.tsx b/packages/mu-txt/src/components/Welcome/RecentFiles.tsx deleted file mode 100644 index d740b0dd62..0000000000 --- a/packages/mu-txt/src/components/Welcome/RecentFiles.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import * as React from 'react'; -import {drule, rule, useTheme} from 'nano-theme'; -import {useT} from 'use-t'; -import {TimeAgo} from '@jsonjoy.com/ui/lib/1-inline/TimeAgo'; -import {FileIcon} from '@jsonjoy.com/ui/lib/1-inline/FileIcon'; -import {useExplorer} from '../../context'; -import type {FileMetadataDto} from '../../state/file'; - -const listClass = rule({ - display: 'flex', - flexDirection: 'column', - gap: '2px', - width: '100%', -}); - -const itemClass = drule({ - display: 'flex', - alignItems: 'center', - gap: '12px', - padding: '8px 12px', - borderRadius: '8px', - cursor: 'pointer', - transition: 'background .12s ease, color .12s ease', - textAlign: 'left', - border: '1px solid transparent', - background: 'transparent', - width: '100%', - font: 'inherit', - '&:active': { - transform: 'scale(.99)', - }, -}); - -const iconWrapClass = rule({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flex: '0 0 auto', -}); - -const nameClass = rule({ - flex: '1 1 auto', - minWidth: 0, - fontSize: '13px', - fontWeight: 500, - lineHeight: '1.3em', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', -}); - -const metaClass = rule({ - flex: '0 0 auto', - fontSize: '11px', - opacity: 0.5, -}); - -export interface RecentFilesProps { - files: FileMetadataDto[]; -} - -export const RecentFiles: React.FC = ({files}) => { - const [t] = useT(); - const state = useExplorer(); - const theme = useTheme(); - - if (!files.length) return null; - - return ( -
- {files.map((file) => { - const cls = itemClass({ - color: theme.g(0.18, 0.9), - '&:hover': { - background: theme.g(0.94, 0.2), - borderColor: theme.g(0.86, 0.26), - }, - }); - return ( - - ); - })} -
- ); -}; diff --git a/packages/mu-txt/src/components/Welcome/index.tsx b/packages/mu-txt/src/components/Welcome/index.tsx deleted file mode 100644 index b3980414e4..0000000000 --- a/packages/mu-txt/src/components/Welcome/index.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import * as React from 'react'; -import {rule, useTheme} from 'nano-theme'; -import {useT} from 'use-t'; -import {MuTxtLogoV3} from '@jsonjoy.com/ui/lib/icons/svg/MuTxtLogoV3'; -import {Popup} from '@jsonjoy.com/ui/lib/4-card/Popup'; -import {ContextMenu} from '@jsonjoy.com/ui/lib/4-card/ContextMenu'; -import {useExplorer} from '../../context'; -import {host} from '../../util/host'; -import {BrandLogo} from '../LeftSidebar/BrandLogo'; -import {ActionCard} from './ActionCard'; -import {RecentFiles} from './RecentFiles'; -import NewDocIcon__svg from 'iconista/lib/react/bootstrap/file-earmark-plus'; -import TemplateIcon__svg from 'iconista/lib/react/bootstrap/collection'; -import FolderIcon__svg from 'iconista/lib/react/bootstrap/folder2-open'; - -const NewDocIcon: React.FC> = (props) => ( - -); -const TemplateIcon: React.FC> = (props) => ( - -); -const FolderIcon: React.FC> = (props) => ( - -); - -const blockClass = rule({ - d: 'flex', - fld: 'column', - ai: 'center', - jc: 'flex-start', - w: '100%', - h: '100%', - pd: '32px 24px', - bxz: 'border-box', - overflowY: 'auto', -}); - -const innerClass = rule({ - w: '100%', - maxW: '680px', - d: 'flex', - fld: 'column', - ai: 'stretch', - margin: '0 auto', -}); - -const heroClass = rule({ - ta: 'center', - mr: '0 0 24px', -}); - -const logoTileClass = rule({ - d: 'inline-flex', - ai: 'center', - jc: 'center', - mr: '0 auto 32px', -}); - -const titleClass = rule({ - d: 'flex', - ai: 'center', - jc: 'center', - gap: '6px', - pd: '0 12px 0 0', - fontSize: '28px', - lineHeight: '1.2em', - fontWeight: 700, - letterSpacing: '-0.02em', - mr: '-20px 0 6px', -}); - -const subtitleClass = rule({ - fontSize: '14px', - lineHeight: '1.55em', - opacity: 0.62, - margin: '0 auto', - fontWeight: 400, - maxWidth: '440px', -}); - -const actionsClass = rule({ - display: 'grid', - gap: '12px', - width: '100%', - gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', - alignItems: 'stretch', - '> span, > span span': { - display: 'flex', - height: '100%', - width: '100%', - minWidth: 0, - }, - '@media (max-width: 720px)': { - gridTemplateColumns: '1fr', - }, -}); - -const sectionHeadClass = rule({ - display: 'flex', - alignItems: 'baseline', - justifyContent: 'space-between', - margin: '32px 0 10px', - padding: '0 4px', -}); - -const sectionTitleClass = rule({ - fontSize: '11px', - fontWeight: 700, - letterSpacing: '0.1em', - textTransform: 'uppercase', - opacity: 0.5, - margin: 0, -}); - -const sectionHintClass = rule({ - fontSize: '11px', - opacity: 0.45, -}); - -const footerClass = rule({ - margin: '36px 0 0', - padding: '20px 0 0', - fontSize: '11.5px', - textAlign: 'center', - display: 'flex', - gap: '10px', - alignItems: 'center', - justifyContent: 'center', - flexWrap: 'wrap', - opacity: 0.55, -}); - -const dotClass = rule({ - width: '3px', - height: '3px', - borderRadius: '50%', - background: 'currentColor', - opacity: 0.5, -}); - -const inputClass = rule({display: 'none'}); - -const brandWrapClass = rule({ - display: 'inline-flex', - alignItems: 'center', - transform: 'translate(4px, 2px) scale(1.3)', -}); - -export type WelcomeProps = Record; - -export const Welcome: React.FC = () => { - const [t] = useT(); - const theme = useTheme(); - const state = useExplorer(); - const saved = state.saved.use(); - - const inputRef = React.useRef(null); - const onOpenClick = React.useCallback(() => { - if (host) void host.openDialog(); - else inputRef.current?.click(); - }, []); - const onInputChange = React.useCallback( - (e: React.ChangeEvent) => { - const files = e.currentTarget.files; - if (files && files.length) void state.addFiles(Array.from(files)); - e.currentTarget.value = ''; - }, - [state], - ); - - const recent = saved.slice(0, 5); - - return ( -
-
-
-
- -
-

- {t('Welcome to')} - - - -

-

- {t('A local-first rich-text editor. Saves to your disk, works offline, no account needed.')} -

-
- -
- } - title={t('New document')} - description={t('Start with a blank page')} - onClick={() => state.createNewMuTxt()} - /> - } - tooltip={{renderTooltip: () => t('Browse templates'), nowrap: true}} - > - } - title={t('From template')} - description={t('Notes, journal, and more')} - /> - - } - title={t('Open file')} - description={t('Or drop one anywhere')} - onClick={onOpenClick} - /> - -
- - {recent.length > 0 && ( -
-
-

- {t('Recent')} -

- {saved.length > recent.length && ( - {`+${saved.length - recent.length} ${t('more in sidebar')}`} - )} -
- -
- )} - - -
-
- ); -}; diff --git a/packages/mu-txt/src/context.ts b/packages/mu-txt/src/context.ts deleted file mode 100644 index 5bea0d2c57..0000000000 --- a/packages/mu-txt/src/context.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as React from 'react'; -import type {MuTxtAppState} from './state'; - -export const ctx = React.createContext(null!); -export const useExplorer = () => React.useContext(ctx)!; diff --git a/packages/mu-txt/src/main.tsx b/packages/mu-txt/src/main.tsx deleted file mode 100644 index 0005e65b68..0000000000 --- a/packages/mu-txt/src/main.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; -import {ErrorBoundary} from '@jsonjoy.com/ui/lib/misc/ErrorBoundary'; -import {createRoot} from 'react-dom/client'; -import {App} from './App'; - -import 'nano-theme/lib/global-reset'; - -window.addEventListener('error', (event) => { - console.error('[mutxt] window error:', event.error || event.message); -}); -window.addEventListener('unhandledrejection', (event) => { - console.error('[mutxt] unhandled rejection:', event.reason); -}); - -const root = document.getElementById('root')!; -createRoot(root).render( - - - , -); diff --git a/packages/mu-txt/src/state/file-storage.ts b/packages/mu-txt/src/state/file-storage.ts deleted file mode 100644 index 95fefcdbdf..0000000000 --- a/packages/mu-txt/src/state/file-storage.ts +++ /dev/null @@ -1,98 +0,0 @@ -import {gzip, ungzip} from '@jsonjoy.com/util/lib/compression/gzip'; -import type {FileMetadataDto, FileDto} from './file'; - -export interface IFileStorage { - save(file: FileDto): Promise; - load(id: string): Promise; - list(): Promise; - delete(id: string): Promise; - renameMeta(id: string, name: string): Promise; -} - -const ROOT = 'explorer.jsoncrdt.org'; -const FILES_DIR = 'files'; -const META_FILENAME = 'meta.json'; -const DATA_FILENAME = 'data.seq.cbor.gz'; - -async function getFilesDir(): Promise { - const opfs = await navigator.storage.getDirectory(); - const root = await opfs.getDirectoryHandle(ROOT, {create: true}); - return root.getDirectoryHandle(FILES_DIR, {create: true}); -} - -export class FileStorage implements IFileStorage { - async save(file: FileDto): Promise { - const {data, ...meta} = file; - const compressed = await gzip(data); - const filesDir = await getFilesDir(); - const fileDir = await filesDir.getDirectoryHandle(meta.id, {create: true}); - - const metaHandle = await fileDir.getFileHandle(META_FILENAME, {create: true}); - const metaWriter = await metaHandle.createWritable(); - await metaWriter.write(JSON.stringify(meta)); - await metaWriter.close(); - - const dataHandle = await fileDir.getFileHandle(DATA_FILENAME, {create: true}); - const dataWriter = await dataHandle.createWritable(); - await dataWriter.write(compressed as any); - await dataWriter.close(); - } - - async load(id: string): Promise { - const filesDir = await getFilesDir(); - const fileDir = await filesDir.getDirectoryHandle(id); - - const metaHandle = await fileDir.getFileHandle(META_FILENAME); - const metaFile = await metaHandle.getFile(); - const meta: FileDto = JSON.parse(await metaFile.text()); - - const dataHandle = await fileDir.getFileHandle(DATA_FILENAME); - const dataFile = await dataHandle.getFile(); - const compressed = new Uint8Array(await dataFile.arrayBuffer()); - meta.data = await ungzip(compressed); - - return meta; - } - - // async open(id: string): Promise { - // const dto = await this.load(id); - // return OpenFile.fromDto(dto); - // } - - async list(): Promise { - const filesDir = await getFilesDir(); - const results: FileMetadataDto[] = []; - for await (const [, entry] of filesDir as any) { - if (entry.kind !== 'directory') continue; - try { - const metaHandle = await entry.getFileHandle(META_FILENAME); - const metaFile = await metaHandle.getFile(); - const meta: FileMetadataDto = JSON.parse(await metaFile.text()); - results.push(meta); - } catch { - // skip folders without a valid meta.json - } - } - return results; - } - - async delete(id: string): Promise { - const filesDir = await getFilesDir(); - await filesDir.removeEntry(id, {recursive: true}); - } - - async renameMeta(id: string, name: string): Promise { - const filesDir = await getFilesDir(); - const fileDir = await filesDir.getDirectoryHandle(id); - const metaHandle = await fileDir.getFileHandle(META_FILENAME); - const metaFile = await metaHandle.getFile(); - const meta: FileMetadataDto = JSON.parse(await metaFile.text()); - if (meta.name === name) return meta; - meta.name = name; - meta.updatedAt = Date.now(); - const metaWriter = await metaHandle.createWritable(); - await metaWriter.write(JSON.stringify(meta)); - await metaWriter.close(); - return meta; - } -} diff --git a/packages/mu-txt/src/state/file.tsx b/packages/mu-txt/src/state/file.tsx deleted file mode 100644 index 0dcbea9aad..0000000000 --- a/packages/mu-txt/src/state/file.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import * as React from 'react'; -import {ext} from 'json-joy/lib/json-crdt-extensions'; -import {LogDecoder} from 'json-joy/lib/json-crdt/log/codec/LogDecoder'; -import {JsonCrdtLogState} from '@jsonjoy.com/collaborative-ui/lib/JsonCrdtLog/JsonCrdtLogState'; -import {rsync} from '@jsonjoy.com/ui'; -import {CborDecoder} from '@jsonjoy.com/json-pack/lib/cbor/CborDecoder'; -import {LogEncoder} from 'json-joy/lib/json-crdt/log/codec/LogEncoder'; -import {compact} from 'json-joy/lib/json-crdt-patch/compaction'; -import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder'; -import {ungzip} from '@jsonjoy.com/util/lib/compression/gzip'; -import {FileIcon} from '@jsonjoy.com/ui/lib/1-inline/FileIcon'; -import {DebounceQueue} from '../util/DebounceQueue'; -import {host} from '../util/host'; -import type {Log} from 'json-joy/lib/json-crdt/log/Log'; -import type {TabItem} from '@jsonjoy.com/ui/lib/3-list-item/FileTabs'; -import type {DisplayMode, MuTxtApi} from 'mutxt-react'; -import type {IFileStorage} from './file-storage'; -import type {Model} from 'json-joy/lib/json-crdt'; - -const RenderName: React.FC<{file: OpenFile}> = ({file}) => { - const name = file.name.use(); - return <>{name}; -}; - -export interface FileMetadataDto { - /** Unique constant identifier for the file. */ - id: string; - /** A custom human-readable name for the file that user can edit in UI. */ - name: string; - /** Timestamp when file was created. */ - createdAt: number; - /** Timestamp when file was last updated or saved. */ - updatedAt: number; - /** - * Optional link to an external location where this file was opened from - * and where its contents can be saved back to. - */ - link?: FileLink; -} - -/** - * Link from an internally stored file to an external location. - */ -export interface FileLink { - /** The access mechanism / provider used to reach the external file. */ - source: string; - /** Locator within the source — filesystem path, provider id, etc. */ - path: string; -} - -export interface FileDto extends FileMetadataDto { - data: Uint8Array; -} - -export interface OpenFileOptions { - storage?: IFileStorage; - flushDebounceMs?: number; - onPersisted?: ((meta: FileMetadataDto) => void | Promise) | undefined; -} - -export const DEFAULT_FLUSH_DEBOUNCE_MS = 250; - -export class OpenFile { - public readonly id: string; - public readonly openTime: number = Date.now(); - public readonly name: rsync.ReactValue; - public readonly logState: JsonCrdtLogState; - public readonly activeModel: rsync.ReactValue>; - private readonly storage?: IFileStorage; - private readonly flushDebounceMs: number; - private readonly onPersisted?: (meta: FileMetadataDto) => void | Promise; - private flushTimer: ReturnType | null = null; - private pendingFlush = false; - private saving = false; - private active = true; - private disposeAfterSave = false; - private readonly unsubscribers: Array<() => void> = []; - public mutxt?: MuTxtApi; - - constructor( - public readonly meta: FileMetadataDto, - public readonly log: Log, - options: OpenFileOptions = {}, - public size: number = 0, - ) { - this.id = meta.id; - this.name = rsync.val(meta.name); - this.logState = new JsonCrdtLogState(log, {view: 'tiny'}); - this.logState.patchState.toggleShow(false); - this.activeModel = rsync.val(log.end); - this.storage = options.storage; - this.flushDebounceMs = options.flushDebounceMs ?? DEFAULT_FLUSH_DEBOUNCE_MS; - this.onPersisted = options.onPersisted; - this.attachPersistenceListeners(); - - // Schedule CRDT operation flushing. - const queue = new DebounceQueue(100, 500); - queue.onflush = () => { - const patch = api.builder.patch; - if (patch.ops.length) { - compact(patch); - api.flush(); - } - }; - const api = log.end.api; - const enqueue = () => queue.push(null); - api.onLocalChanges.listen(enqueue); - api.onBeforeTransaction.listen(enqueue); - api.onTransaction.listen(enqueue); - } - - public static async decodeLog(uint8: Uint8Array, sid: number): Promise> { - try { - uint8 = await ungzip(uint8); - } catch {} - const cborDecoder = new CborDecoder(); - const decoder = new LogDecoder({cborDecoder}); - const {history: log} = decoder.decode(uint8, {history: true, frontier: true, format: 'seq.cbor'}); - if (!log) throw new Error('Incompatible JSON CRDT log file.'); - const start = log.start; - log.start = () => { - const model = start(); - model.ext.register(ext.peritext); - model.ext.register(ext.quill); - return model; - }; - log.end.ext.register(ext.peritext); - log.end.ext.register(ext.quill); - log.end.setSid(sid); - return log; - } - - private readonly attachPersistenceListeners = () => { - if (!this.storage || this.unsubscribers.length) return; - this.unsubscribers.push( - this.name.subscribe(() => this.scheduleFlush()), - this.log.end.api.onPatch.listen(() => this.scheduleFlush()), - this.log.end.api.onFlush.listen(() => this.scheduleFlush()), - ); - }; - - private readonly clearFlushTimer = () => { - if (this.flushTimer) { - clearTimeout(this.flushTimer); - this.flushTimer = null; - } - }; - - private readonly finalizeDisposal = () => { - this.clearFlushTimer(); - this.disposeAfterSave = false; - }; - - public readonly bindMuTxt = (api: MuTxtApi): (() => void) => { - this.mutxt = api; - const setMode = host?.platform === 'darwin' ? host.setTitlebarMode.bind(host) : null; - const apply = () => { - if (!setMode) return; - const mode: DisplayMode = api.state.displayMode.value; - void setMode(mode === 'fullwindow' ? 'compact' : 'default').catch(() => {}); - }; - apply(); - const unsubscribe = setMode ? api.state.displayMode.subscribe(apply) : null; - return () => { - unsubscribe?.(); - if (setMode) void setMode('default').catch(() => {}); - if (this.mutxt === api) this.mutxt = undefined; - }; - }; - - public toMeta(): FileMetadataDto { - const meta: FileMetadataDto = { - id: this.meta.id, - name: this.name.value, - createdAt: this.meta.createdAt, - updatedAt: Date.now(), - }; - if (this.meta.link) meta.link = this.meta.link; - return meta; - } - - public toDto(): FileDto { - const cborEncoder = new CborEncoder(); - const encoder = new LogEncoder({cborEncoder}); - const encoded = encoder.encode(this.log, { - format: 'seq.cbor', - history: 'binary', - model: 'none', - noView: true, - }); - const dto: FileDto = this.toMeta() as FileDto; - dto.data = encoded; - return dto; - } - - // Write a `.toBinary()` snapshot of the current model to the linked external - // location (e.g. local fs in Electron). OPFS keeps the full CRDT log; the - // linked file holds only the latest snapshot. - private readonly writeLinkedSnapshot = async (): Promise => { - const link = this.meta.link; - if (!link) return; - if (link.source !== 'native-fs' || !host) return; - try { - const snapshot = this.log.end.toBinary(); - await host.writeFile(link.path, snapshot); - } catch (err) { - // eslint-disable-next-line no-console - console.error('[mutxt] failed to write linked file', link.path, err); - } - }; - - public readonly scheduleFlush = () => { - if (!this.storage || !this.active) return; - this.pendingFlush = true; - this.clearFlushTimer(); - this.flushTimer = setTimeout(() => { - this.flushTimer = null; - void this.flush(); - }, this.flushDebounceMs); - }; - - public readonly flush = async () => { - const storage = this.storage; - if (!storage || this.saving || !this.pendingFlush) { - if (this.disposeAfterSave && !this.saving && !this.pendingFlush) this.finalizeDisposal(); - return; - } - this.pendingFlush = false; - this.saving = true; - const dto = this.toDto(); - try { - await storage.save(dto); - this.meta.name = dto.name; - this.meta.updatedAt = dto.updatedAt; - const onPersisted = this.onPersisted; - if (onPersisted) await onPersisted(dto); - await this.writeLinkedSnapshot(); - } catch { - } finally { - this.saving = false; - } - if (this.disposeAfterSave && !this.pendingFlush) { - this.finalizeDisposal(); - return; - } - if (this.pendingFlush) this.scheduleFlush(); - }; - - public readonly destroy = async (flushPending = false) => { - if (!this.active && !this.saving) return; - this.active = false; - this.clearFlushTimer(); - while (this.unsubscribers.length) { - const unsubscribe = this.unsubscribers.pop(); - unsubscribe?.(); - } - if (flushPending && this.pendingFlush) { - this.disposeAfterSave = true; - await this.flush(); - return; - } - if (this.saving) { - this.disposeAfterSave = true; - return; - } - this.finalizeDisposal(); - }; - - public toTab(): TabItem { - return { - id: this.id, - name: this.name.value, - display: () => , - icon: () => , - }; - } -} diff --git a/packages/mu-txt/src/state/index.ts b/packages/mu-txt/src/state/index.ts deleted file mode 100644 index 9c9fc4056c..0000000000 --- a/packages/mu-txt/src/state/index.ts +++ /dev/null @@ -1,625 +0,0 @@ -import {Patch} from 'json-joy/lib/json-crdt'; -import {Model} from 'json-joy/lib/json-crdt'; -import {ModelWithExt} from 'json-joy/lib/json-crdt-extensions'; -import {Log} from 'json-joy/lib/json-crdt/log/Log'; -import {CborDecoder} from '@jsonjoy.com/json-pack/lib/cbor/CborDecoder'; -import {rsync} from '@jsonjoy.com/ui'; -import {BehaviorSubject, map, switchMap, distinctUntilChanged} from 'rxjs'; -import {ungzip} from '@jsonjoy.com/util/lib/compression/gzip'; -import {decodeShareBlob, decodeEncryptedShareBlob} from 'mutxt-react'; -import {downloadFile, stripExtensions} from './util'; -import {type FileLink, type FileMetadataDto, OpenFile} from './file'; -import {FileTabsState} from '@jsonjoy.com/ui/lib/3-list-item/FileTabs/state'; -import {AppGridState, type SidebarState} from '@jsonjoy.com/ui/lib/7-fullscreen/AppGrid/state'; -import {FileStorage, type IFileStorage} from './file-storage'; -import {Menus} from './menus'; -import {s} from 'json-joy/lib/json-crdt'; -import {ext} from 'json-joy/lib/json-crdt-extensions'; -import {FromSlate, type SlateDocument} from '@jsonjoy.com/collaborative-slate'; -import {getSyncStore, type ISyncStore} from './sync-store'; -import {Theme} from './theme'; -import type {TabItem} from '@jsonjoy.com/ui/lib/3-list-item/FileTabs'; -import type {AnchorPoint} from '@jsonjoy.com/ui/lib/utils/popup'; - -const toObservable = (val: rsync.ReactValue): BehaviorSubject => { - const observable = new BehaviorSubject(val.value); - val.subscribe(() => observable.next(val.value)); - return observable; -}; - -const SAVED_REFRESH_INTERVAL_MS = 5_000; -const LEFT_SIZE_STORAGE_KEY = 'mutxt_left_sidebar_size'; -const LEFT_SIZE_PERSIST_DEBOUNCE_MS = 300; -const LEFT_STATE_STORAGE_KEY = 'mutxt_left_sidebar_state'; - -const isSidebarState = (value: string): value is SidebarState => - value === 'open' || value === 'close' || value === 'mini' || value === 'none'; - -const CLOSE_SELECTED_MIN_GAP_MS = 300; -const CLOSE_SELECTED_DESTROY_TIMEOUT_MS = 1500; - -const sortSavedFiles = (saved: FileMetadataDto[]): FileMetadataDto[] => - [...saved].sort((a, b) => b.updatedAt - a.updatedAt || b.createdAt - a.createdAt); - -interface SharedFromUrl { - encoded: string; - encrypted: boolean; - title: string; - message: string; -} - -/** Plain text passed via `#t=...` to seed the first paragraph of a new document. */ -interface SeedTextFromUrl { - text: string; -} - -type ConsumedFromUrl = SharedFromUrl | SeedTextFromUrl; - -const isSeedText = (consumed: ConsumedFromUrl): consumed is SeedTextFromUrl => - (consumed as SeedTextFromUrl).text !== undefined; - -const PLAIN_KEYS: readonly string[] = ['n', 'new']; -const ENCRYPTED_KEYS: readonly string[] = ['e', 'enc']; -const PLAINTEXT_KEYS: readonly string[] = ['t', 'text']; -const METADATA_KEYS: readonly string[] = ['t', 'm']; - -const takeFirst = (params: URLSearchParams, keys: readonly string[]): string | null => { - for (const key of keys) { - const value = params.get(key); - if (value) return value; - } - return null; -}; - -const consumeSharedParams = (params: URLSearchParams): ConsumedFromUrl | null => { - const plain = takeFirst(params, PLAIN_KEYS); - const encrypted = plain ? null : takeFirst(params, ENCRYPTED_KEYS); - const encoded = plain ?? encrypted; - if (encoded) { - const title = params.get('t') ?? ''; - const message = params.get('m') ?? ''; - for (const key of [...PLAIN_KEYS, ...ENCRYPTED_KEYS, ...METADATA_KEYS]) params.delete(key); - return {encoded, encrypted: !plain, title, message}; - } - // No encoded document: use `#t=...` as seed text for a new document. - const text = takeFirst(params, PLAINTEXT_KEYS); - if (text !== null) { - for (const key of PLAINTEXT_KEYS) params.delete(key); - return {text}; - } - return null; -}; - -const consumeFromHash = (url: URL): ConsumedFromUrl | null => { - if (!url.hash) return null; - const hashParams = new URLSearchParams(url.hash.replace(/^#/, '')); - const result = consumeSharedParams(hashParams); - if (!result) return null; - const remaining = hashParams.toString(); - url.hash = remaining ? '#' + remaining : ''; - return result; -}; - -export class MuTxtAppState { - public readonly tabs: FileTabsState; - public readonly appGrid = new AppGridState(); - public readonly files$ = new BehaviorSubject([]); - public readonly selected$: BehaviorSubject<[id: TabItem, index: number] | null>; - public readonly file$ = new BehaviorSubject(null); - public readonly sync: ISyncStore; - public readonly sid: number; - public readonly saved: rsync.ReactValue = rsync.val([]); - public readonly menus: Menus; - public readonly theme: Theme; - public ondoubleclick: ((file: OpenFile, point: AnchorPoint) => void) | undefined = void 0; - public onclick: ((file: OpenFile, point: AnchorPoint) => void) | undefined = void 0; - protected readonly storage: IFileStorage; - private savedRefreshTimer: ReturnType | null = null; - private leftSizePersistTimer: ReturnType | null = null; - private leftSizeUnsubscribe: (() => void) | null = null; - private leftStateUnsubscribe: (() => void) | null = null; - - constructor(storage: IFileStorage = new FileStorage()) { - const sync = (this.sync = getSyncStore()); - const sid = sync.getItem('json_joy_sid'); - if (sid) { - this.sid = Number(sid); - } else { - this.sid = Model.sid(); - sync.setItem('json_joy_sid', this.sid.toString()); - } - this.menus = new Menus(this); - this.theme = new Theme(sync); - this.restoreLeftSidebarSize(); - this.restoreLeftSidebarState(); - this.leftSizeUnsubscribe = this.appGrid.leftSize.subscribe(this.scheduleLeftSidebarSizePersist); - this.leftStateUnsubscribe = this.appGrid.leftState.subscribe(this.persistLeftSidebarState); - this.storage = storage; - this.tabs = new FileTabsState(rsync.val([] as any)); - this.tabs.onNewTab = () => { - this.createNewMuTxt(); - return void 0; - }; - this.tabs.onDeleteTab = (tab) => { - this.disposeFile(tab.id!); - }; - this.tabs.onTabDoubleClick = (tab, _index, event) => { - const file = this.fileIfOpen(tab.id!); - if (!file) return; - const point: AnchorPoint = {x: event.clientX, y: event.clientY, dx: 1, dy: 1}; - this.ondoubleclick?.(file, point); - }; - this.tabs.onTabClick = (tab, _index, event) => { - const file = this.fileIfOpen(tab.id!); - if (!file) return; - const point: AnchorPoint = {x: event.clientX, y: event.clientY + 24, dx: 0, dy: 1}; - this.onclick?.(file, point); - }; - this.selected$ = toObservable(this.tabs.selected); - this.files$ - .pipe( - switchMap(() => this.selected$), - map((selected) => { - if (!selected) return null; - const files = this.files$.getValue(); - return files.find((file) => file.id === selected[0].id) ?? null; - }), - distinctUntilChanged(), - ) - .subscribe(this.file$); - } - - private stopped = false; - private detachDragDrop: (() => void) | null = null; - - private readonly attachDragDrop = (): (() => void) => { - const hasFiles = (event: DragEvent): boolean => - !!event.dataTransfer && Array.from(event.dataTransfer.types).includes('Files'); - const onDragOver = (event: DragEvent): void => { - if (hasFiles(event)) event.preventDefault(); - }; - const onDrop = (event: DragEvent): void => { - if (event.defaultPrevented) return; - const files = event.dataTransfer?.files; - if (!files || !files.length) return; - event.preventDefault(); - this.addFiles(Array.from(files)); - }; - window.addEventListener('dragover', onDragOver); - window.addEventListener('drop', onDrop); - return () => { - window.removeEventListener('dragover', onDragOver); - window.removeEventListener('drop', onDrop); - }; - }; - - private readonly restoreLeftSidebarSize = (): void => { - const stored = this.sync.getItem(LEFT_SIZE_STORAGE_KEY); - if (stored === null) return; - const value = Number(stored); - if (!Number.isFinite(value) || value <= 0) return; - this.appGrid.leftSize.set(value); - }; - - private readonly scheduleLeftSidebarSizePersist = (): void => { - if (this.leftSizePersistTimer) clearTimeout(this.leftSizePersistTimer); - this.leftSizePersistTimer = setTimeout(() => { - this.leftSizePersistTimer = null; - this.sync.setItem(LEFT_SIZE_STORAGE_KEY, String(this.appGrid.leftSize.value)); - }, LEFT_SIZE_PERSIST_DEBOUNCE_MS); - }; - - private readonly restoreLeftSidebarState = (): void => { - const stored = this.sync.getItem(LEFT_STATE_STORAGE_KEY); - if (stored === null) { - this.appGrid.leftState.set('close'); - return; - } - if (!isSidebarState(stored)) return; - this.appGrid.leftState.set(stored); - }; - - private readonly persistLeftSidebarState = (): void => { - this.sync.setItem(LEFT_STATE_STORAGE_KEY, this.appGrid.leftState.value); - }; - - private readonly stopSavedRefresh = () => { - if (this.savedRefreshTimer) { - clearInterval(this.savedRefreshTimer); - this.savedRefreshTimer = null; - } - }; - - private readonly refreshSaved = async () => { - try { - const saved = await this.storage.list(); - if (this.stopped) return; - this.saved.next(sortSavedFiles(saved)); - } catch {} - }; - - private readonly handleFilePersisted = async () => { - await this.refreshSaved(); - }; - - public started = rsync.val(false); - - async start() { - this.stopped = false; - this.stopSavedRefresh(); - await this.refreshSaved(); - if (this.stopped) return; // TODO: Is this still needed? - this.savedRefreshTimer = setInterval(() => { - void this.refreshSaved(); - }, SAVED_REFRESH_INTERVAL_MS); - this.detachDragDrop?.(); - this.detachDragDrop = this.attachDragDrop(); - - const consumed = this.consumeSharedFromUrl(); - let opened = false; - if (consumed) { - if (isSeedText(consumed)) { - this.createNewMuTxtFromText(consumed.text); - opened = true; - } else { - opened = await this.importSharedDocument(consumed); - } - } - if (this.stopped) return; - - if (!opened) { - // Try to reopen the most recently saved file. If there is none, or if it - // fails to load (e.g. corrupt), fall back to creating a fresh empty - // document so the user always lands on something usable. - const saved = this.saved.value; - opened = saved.length ? await this.openSaved(saved[0].id) : false; - } - if (!opened && !this.files$.getValue().length) this.createNewMuTxt(); - this.started.set(true); - } - - private readonly consumeSharedFromUrl = (): ConsumedFromUrl | null => { - if (typeof window === 'undefined' || !window.location) return null; - try { - const url = new URL(window.location.href); - const consumed = consumeSharedParams(url.searchParams) ?? consumeFromHash(url); - if (!consumed) return null; - const newUrl = url.pathname + url.search + url.hash; - window.history.replaceState({}, '', newUrl); - return consumed; - } catch { - return null; - } - }; - - public readonly importSharedDocument = async (shared: SharedFromUrl): Promise => { - try { - let bytes: Uint8Array; - if (shared.encrypted) { - const messagePrefix = 'Enter the password to open the shared document.'; - const password = window.prompt(shared.message ? `${messagePrefix}\n\n${shared.message}` : messagePrefix, ''); - if (password === null) return false; // user cancelled - bytes = (await decodeEncryptedShareBlob(shared.encoded, password)) as Uint8Array; - } else { - bytes = (await decodeShareBlob(shared.encoded)) as Uint8Array; - } - if (this.stopped) return false; - const model = ModelWithExt.load(bytes, this.sid); - const log = new Log(() => model); - log.end.setSid(this.sid); - this.openFile(log, shared.title || 'Shared'); - return true; - } catch (error) { - // eslint-disable-next-line no-console - console.error('[mutxt] failed to load shared document from URL', error); - if (shared.encrypted) { - window.alert('Failed to open the shared document. The password may be wrong, or the link may be corrupted.'); - } - return false; - } - }; - - async stop() { - this.stopped = true; - this.started.set(false); - this.stopSavedRefresh(); - this.detachDragDrop?.(); - this.detachDragDrop = null; - this.leftSizeUnsubscribe?.(); - this.leftSizeUnsubscribe = null; - this.leftStateUnsubscribe?.(); - this.leftStateUnsubscribe = null; - if (this.leftSizePersistTimer) { - clearTimeout(this.leftSizePersistTimer); - this.leftSizePersistTimer = null; - this.sync.setItem(LEFT_SIZE_STORAGE_KEY, String(this.appGrid.leftSize.value)); - } - this.theme.dispose(); - await Promise.all(this.files$.getValue().map((file) => file.destroy(true))); - } - - public isOpen(id: string) { - return !!this.fileIfOpen(id); - } - - public fileIfOpen(id: string): OpenFile | undefined { - return this.files$.value.find((file) => file.id === id); - } - - public async openSaved(id: string): Promise { - try { - if (this.isOpen(id)) { - this.tabs.selectById(id); - return true; - } - const dto = await this.storage.load(id); - if (this.stopped) return false; - const size = dto.data.length; - await this.addLog(dto.data, dto.name, dto, size); - return true; - } catch (error) { - // eslint-disable-next-line no-console - console.error('[mutxt] failed to open file', id, error); - return false; - } - } - - public readonly openFile = ( - log: Log, - name: string = 'Untitled' + (this.newCnt > 1 ? ` (${this.newCnt})` : ''), - dto?: FileMetadataDto, - size: number = 0, - link?: FileLink, - ) => { - const now = Date.now(); - const meta: FileMetadataDto = dto ?? { - id: Math.random().toString(36).slice(2) + '.' + now.toString(36), - name, - createdAt: now, - updatedAt: now, - ...(link ? {link} : {}), - }; - const file = new OpenFile( - meta, - log, - { - storage: this.storage, - onPersisted: this.handleFilePersisted, - }, - size, - ); - this.files$.next([...this.files$.getValue(), file]); - this.tabs.add(file.toTab()); - this.tabs.selectById(file.meta.id); - if (!dto) file.scheduleFlush(); - return file; - }; - - public select(id: string) { - this.tabs.selectById(id); - } - - private readonly destroying = new Map>(); - - private readonly disposeFile = (id: string) => { - const list = this.files$.getValue(); - const file = list.find((openFile) => openFile.id === id); - if (!file) return; - this.files$.next(list.filter((m) => m.id !== id)); - const promise = file.destroy(true).finally(() => { - if (this.destroying.get(id) === promise) this.destroying.delete(id); - }); - this.destroying.set(id, promise); - }; - - public readonly awaitDestroy = async (id: string): Promise => { - const promise = this.destroying.get(id); - if (promise) await promise; - }; - - private closeSelectedBusy = false; - private closeSelectedLastAt = 0; - - public readonly closeSelected = async (): Promise => { - const now = Date.now(); - if (now - this.closeSelectedLastAt < CLOSE_SELECTED_MIN_GAP_MS) return; - if (this.closeSelectedBusy) return; - this.closeSelectedLastAt = now; - this.closeSelectedBusy = true; - try { - const id = this.tabs.selected.value?.[0]?.id ?? this.file$.getValue()?.id; - const ok = this.tabs.deleteSelected(); - if (!ok) return; - if (id) { - const timeout = new Promise((resolve) => setTimeout(resolve, CLOSE_SELECTED_DESTROY_TIMEOUT_MS)); - await Promise.race([this.awaitDestroy(id), timeout]); - } - if (typeof requestAnimationFrame === 'function') { - await new Promise((resolve) => requestAnimationFrame(() => resolve())); - } - } catch (err) { - // eslint-disable-next-line no-console - console.error('mutxt:close-selected failed', err); - } finally { - this.closeSelectedBusy = false; - } - }; - - public readonly close = (id: string) => { - this.tabs.deleteById(id); - // tabs.deleteById fires onDeleteTab → disposeFile, which handles file - // teardown. Falling back here covers callers where the tab had already - // been removed externally. - this.disposeFile(id); - }; - - public readonly closeAll = () => { - const files = this.files$.getValue(); - for (const file of files) this.close(file.id); - }; - - public readonly rename = async (id: string, name: string): Promise => { - const file = this.files$.getValue().find((m) => m.id === id); - if (file) { - this.renameFile(file, name); - return; - } - try { - await this.storage.renameMeta(id, name); - await this.refreshSaved(); - } catch (error) { - // eslint-disable-next-line no-console - console.error('[mutxt] failed to rename saved file', id, error); - } - }; - - public renameFile(file: OpenFile, name: string): void { - if (file.name.value === name) return; - file.name.set(name); - } - - public async deleteSaved(id: string) { - this.tabs.deleteById(id); - await this.awaitDestroy(id); - await this.storage.delete(id); - await this.refreshSaved(); - } - - public readonly addFile = async (file: File, link?: FileLink) => { - if (!file) return; - const uint8 = new Uint8Array(await file.arrayBuffer()); - const name = file.name ? stripExtensions(file.name) : 'model'; - try { - if (file.name.endsWith('patches.bin')) { - const cborDecoder = new CborDecoder(); - const array = cborDecoder.decode(uint8) as Uint8Array[]; - if (!Array.isArray(array)) throw new Error('Incompatible JSON CRDT log file.'); - const patches: Patch[] = array.map((patch) => Patch.fromBinary(patch)); - const lastPatch = patches[patches.length - 1]; - if (!lastPatch) throw new Error('Incompatible JSON CRDT log file.'); - const id = lastPatch.getId(); - if (!id) throw new Error('Incompatible JSON CRDT log file.'); - const model = ModelWithExt.create(undefined, id.sid); - const log = new Log(() => model.clone()); - log.end.applyBatch(patches); - log.end.setSid(this.sid); - this.openFile(log, name, undefined, 0, link); - return; - } - if (file.name.endsWith('.crdt')) { - let bytes = uint8; - try { - bytes = (await ungzip(bytes)) as Uint8Array; - } catch {} - const model = ModelWithExt.load(bytes, this.sid); - const log = new Log(() => model); - log.end.setSid(this.sid); - this.openFile(log, name, undefined, 0, link); - return; - } - await this.addLog(uint8, name, undefined, 0, link); - return; - } catch {} - this.createNewMuTxtWithFile(file, uint8, name, link); - }; - - public readonly addLog = async ( - uint8: Uint8Array, - name?: string, - dto?: FileMetadataDto, - size: number = 0, - link?: FileLink, - ) => { - let log: Log; - try { - log = await OpenFile.decodeLog(uint8, this.sid); - } catch { - let bytes = uint8; - try { - bytes = (await ungzip(bytes)) as Uint8Array; - } catch {} - const model = ModelWithExt.load(bytes, this.sid); - log = new Log(() => model); - log.end.setSid(this.sid); - } - if (this.stopped) return; - this.openFile(log, name, dto, size, link); - }; - - public readonly addFiles = async (files: File[]) => { - files.map((file) => this.addFile(file).catch(() => {})); - }; - - private newCnt = 0; - - public readonly createNew = (data: unknown = void 0, name?: string, link?: FileLink) => { - // const schema = s.obj(data); - const model = ModelWithExt.create(data, this.sid); - this.createFromModel(model, name, link); - }; - - public readonly createNewMuTxt = (name?: string, link?: FileLink) => { - this.createNew(s.obj({'@type': s.con('mutxt'), text: ext.peritext.new('')}), name, link); - }; - - public readonly createNewMuTxtFromText = (text: string, name?: string, link?: FileLink) => { - this.createNew(s.obj({'@type': s.con('mutxt'), text: ext.peritext.new(text)}), name, link); - }; - - public readonly createNewMuTxtFromSlate = (slate: SlateDocument, name?: string) => { - const model = ModelWithExt.create(s.obj({'@type': s.con('mutxt'), text: ext.peritext.new('')}), this.sid); - const peritextApi = model.api.in('text').asExt(ext.peritext); - const txt = peritextApi.peritext(); - const viewRange = FromSlate.convert(slate); - txt.editor.import(0, viewRange); - txt.refresh(); - this.newCnt++; - const log = Log.from(model); - this.openFile(log, name); - }; - - public readonly createNewMuTxtWithFile = (file: File, bytes: Uint8Array, name?: string, link?: FileLink) => { - const thingId = `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; - const model = ModelWithExt.create( - s.obj({ - '@type': s.con('mutxt'), - text: ext.peritext.new(''), - things: s.obj({}), - }), - this.sid, - ); - model.api.obj(['things']).set({ - [thingId]: { - '@type': 'file', - '@id': thingId, - name: file.name || 'file', - mimeType: file.type || 'application/octet-stream', - size: file.size, - data: s.con(bytes), - }, - }); - const peritextApi = model.api.in('text').asExt(ext.peritext); - const txt = peritextApi.peritext(); - const slate: SlateDocument = [ - {type: 'file', '@thing': thingId, children: [{text: ''}]} as any, - {type: 'p', children: [{text: ''}]} as any, - ]; - const viewRange = FromSlate.convert(slate); - txt.editor.import(0, viewRange); - txt.refresh(); - this.newCnt++; - const log = Log.from(model); - this.openFile(log, name ?? file.name, undefined, 0, link); - }; - - public readonly createFromModel = (model: Model, name?: string, link?: FileLink) => { - this.newCnt++; - const log = Log.fromNewModel(model); - this.openFile(log, name, undefined, 0, link); - }; - - public async download(id: string) { - const {name, data} = await this.storage.load(id); - downloadFile(data, `${name}.seq.cbor.gz`); - } -} diff --git a/packages/mu-txt/src/state/menus.tsx b/packages/mu-txt/src/state/menus.tsx deleted file mode 100644 index 1396e7429e..0000000000 --- a/packages/mu-txt/src/state/menus.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import * as React from 'react'; -import type {MenuItem} from '@jsonjoy.com/ui/lib/4-card/StructuralMenu/types'; -import {Sidetip} from '@jsonjoy.com/ui/lib/1-inline/Sidetip'; -import {Code} from '@jsonjoy.com/ui/lib/1-inline/Code'; -import {templates} from './templates'; -import type {MuTxtAppState} from '.'; -import FileMuTxtIcon__svg from 'iconista/lib/react/bootstrap/file-earmark-text'; -import FileJsonIcon__svg from 'iconista/lib/react/bootstrap/filetype-json'; - -const FileMuTxtIcon: React.FC> = (props) => ( - -); -const FileJsonIcon: React.FC> = (props) => ( - -); - -export class Menus { - constructor(public readonly state: MuTxtAppState) {} - - public newFileMenu(): MenuItem { - const {state} = this; - return { - name: 'New', - minWidth: 300, - expand: 8, - children: [ - // { - // name: 'Rich-text', - // expand: 8, - // children: [ - // { - // name: 'Empty document', - // icon: () => , - // onSelect: () => state.createNewMuTxt(), - // }, - // ], - // }, - { - name: 'Work', - expand: 8, - // sepBefore: true, - children: templates - .filter((tpl) => tpl.category === 'work') - .map((tpl) => ({ - id: tpl.id, - name: tpl.name, - text: tpl.name + ' - ' + tpl.description, - icon: () => , - right: () => {tpl.description}, - onSelect: () => state.createNewMuTxtFromSlate(tpl.doc, tpl.fileName), - })), - }, - { - name: 'Everyday', - expand: 8, - sepBefore: true, - children: templates - .filter((tpl) => tpl.category === 'personal') - .map((tpl) => ({ - id: tpl.id, - name: tpl.name, - text: tpl.name + ' - ' + tpl.description, - icon: () => , - right: () => {tpl.description}, - onSelect: () => state.createNewMuTxtFromSlate(tpl.doc, tpl.fileName), - })), - }, - { - name: 'JSON', - expand: 0, - sepBefore: true, - minWidth: 288, - children: [ - { - name: 'Empty document', - right: () => ( - - - {'()'} - - - ), - icon: () => , - onSelect: () => state.createNew(), - }, - { - name: 'Empty object', - right: () => ( - - - {'{}'} - - - ), - icon: () => , - onSelect: () => state.createNew({}), - }, - { - name: 'Empty array', - right: () => ( - - - {'[]'} - - - ), - icon: () => , - onSelect: () => state.createNew([]), - }, - { - name: 'Sample document', - right: () => ( - - - {'{"foo": ...'} - - - ), - icon: () => , - onSelect: () => - state.createNew({ - name: 'Alice', - age: 30, - hobbies: ['reading', 'hiking'], - address: { - city: 'Wonderland', - zip: '12345', - }, - }), - }, - ], - }, - ], - }; - } -} diff --git a/packages/mu-txt/src/state/sync-store.ts b/packages/mu-txt/src/state/sync-store.ts deleted file mode 100644 index 10ad1e4bc6..0000000000 --- a/packages/mu-txt/src/state/sync-store.ts +++ /dev/null @@ -1,70 +0,0 @@ -const getCookie = (key: string): string | null => { - const name = key + '='; - const decodedCookie = decodeURIComponent(document.cookie); - const ca = decodedCookie.split(';'); - for (let i = 0; i < ca.length; i++) { - const c = ca[i].trim(); - if (c.indexOf(name) === 0) return c.substring(name.length, c.length); - } - return null; -}; - -export interface ISyncStore { - setItem(key: string, value: string): void; - getItem(key: string): string | null; - removeItem(key: string): void; -} - -class CookieStore implements ISyncStore { - constructor(private cache: Map | undefined = new Map()) {} - - public setItem(key: string, value: string): void { - this.cache?.set(key, value); - // biome-ignore lint/suspicious/noDocumentCookie: simple sync cookie store; Cookie Store API is async - document.cookie = `${key}=${encodeURIComponent(value)}; path=/; SameSite=Lax`; - } - - public getItem(key: string): string | null { - if (this.cache?.has(key)) return this.cache.get(key)!; - const cookieVal = getCookie(key); - return cookieVal ?? null; - } - - public removeItem(key: string): void { - this.cache?.delete(key); - // biome-ignore lint/suspicious/noDocumentCookie: simple sync cookie store; Cookie Store API is async - document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; - } -} - -class MemoryStore implements ISyncStore { - private cache: Map = new Map(); - - public setItem(key: string, value: string): void { - this.cache.set(key, value); - } - - public getItem(key: string): string | null { - return this.cache.get(key) ?? null; - } - - public removeItem(key: string): void { - this.cache.delete(key); - } -} - -export const getSyncStore = (): ISyncStore => { - try { - const testKey = '__sync_store_test__'; - localStorage.setItem(testKey, ' '); - localStorage.removeItem(testKey); - return localStorage; - } catch { - try { - if (typeof document.cookie === 'string') return new CookieStore(); - throw 1; - } catch { - return new MemoryStore(); - } - } -}; diff --git a/packages/mu-txt/src/state/templates.ts b/packages/mu-txt/src/state/templates.ts deleted file mode 100644 index de0fe4c063..0000000000 --- a/packages/mu-txt/src/state/templates.ts +++ /dev/null @@ -1,1270 +0,0 @@ -import type {SlateDocument} from '@jsonjoy.com/collaborative-slate'; - -export type MuTxtTemplateCategory = 'work' | 'personal'; - -export interface MuTxtTemplate { - id: string; - name: string; - description: string; - fileName: string; - category: MuTxtTemplateCategory; - doc: SlateDocument; -} - -const designDoc: SlateDocument = [ - {type: 'h1', children: [{text: 'RFC: '}]}, - { - type: 'p', - children: [ - {text: 'Author: '}, - {text: '<your name>', italic: true}, - {text: ' · Status: '}, - {text: 'Draft', bold: true}, - {text: ' · Last updated: '}, - {text: '<date>', italic: true}, - ], - }, - { - type: 'callout', - icon: '📌', - title: 'TL;DR', - children: [{text: 'One-paragraph summary of the proposal and the change it produces.'}], - }, - {type: 'h2', children: [{text: 'Background'}]}, - { - type: 'p', - children: [ - { - text: 'Why are we doing this? What problem are we solving, and for whom? Link to prior art, incidents, and related docs.', - }, - ], - }, - {type: 'h2', children: [{text: 'Goals'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Goal 1 — concrete, measurable.'}]}, - {type: 'li', children: [{text: 'Goal 2 — concrete, measurable.'}]}, - ], - }, - {type: 'h2', children: [{text: 'Non-goals'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: 'Things we are explicitly not solving in this round.'}]}], - }, - {type: 'h2', children: [{text: 'Proposal'}]}, - { - type: 'p', - children: [ - { - text: 'High-level approach, then drill into the specifics. Describe the data model, API surface, and any user-facing changes.', - }, - ], - }, - {type: 'h3', children: [{text: 'API'}]}, - { - type: 'code-block', - language: 'ts', - children: [{text: '// Sketch the public API here\nexport interface Example {\n id: string;\n}'}], - }, - {type: 'h3', children: [{text: 'Data model'}]}, - {type: 'p', children: [{text: 'Schema, migration plan, and storage characteristics.'}]}, - {type: 'h2', children: [{text: 'Alternatives considered'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: 'Alternative A — '}, {text: 'why rejected', italic: true}, {text: '.'}]}, - {type: 'li', children: [{text: 'Alternative B — '}, {text: 'why rejected', italic: true}, {text: '.'}]}, - ], - }, - {type: 'h2', children: [{text: 'Risks & open questions'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Risk: … Mitigation: …'}]}, - {type: 'li', children: [{text: 'Open question: …'}]}, - ], - }, - {type: 'h2', children: [{text: 'Rollout plan'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: 'Land behind a feature flag'}]}, - {type: 'li', checked: false, children: [{text: 'Internal dogfood'}]}, - {type: 'li', checked: false, children: [{text: 'Staged rollout'}]}, - {type: 'li', checked: false, children: [{text: 'GA + cleanup'}]}, - ], - }, -]; - -const meetingNotes: SlateDocument = [ - {type: 'h1', children: [{text: 'Meeting notes — <topic>'}]}, - { - type: 'p', - children: [ - {text: 'Date: '}, - {text: '<YYYY-MM-DD>', italic: true}, - {text: ' · Facilitator: '}, - {text: '<name>', italic: true}, - {text: ' · Notetaker: '}, - {text: '<name>', italic: true}, - ], - }, - {type: 'h2', children: [{text: 'Attendees'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<name>'}]}, - {type: 'li', children: [{text: '<name>'}]}, - ], - }, - {type: 'h2', children: [{text: 'Agenda'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: 'Item 1'}]}, - {type: 'li', children: [{text: 'Item 2'}]}, - {type: 'li', children: [{text: 'Item 3'}]}, - ], - }, - {type: 'h2', children: [{text: 'Discussion'}]}, - {type: 'p', children: [{text: 'Capture key points, decisions, and dissenting views as the conversation unfolds.'}]}, - { - type: 'blockquote', - children: [{text: 'Decision: ', bold: true}, {text: 'short statement of what was decided and by whom.'}], - }, - {type: 'h2', children: [{text: 'Action items'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: '<owner> — <action> — due <date>'}]}, - {type: 'li', checked: false, children: [{text: '<owner> — <action> — due <date>'}]}, - ], - }, - {type: 'h2', children: [{text: 'Follow-ups'}]}, - {type: 'p', children: [{text: 'Topics parked for later discussion.'}]}, -]; - -const bugReport: SlateDocument = [ - {type: 'h1', children: [{text: 'Bug: <one-line summary>'}]}, - { - type: 'callout', - icon: '🐞', - title: 'Severity', - children: [{text: 'P0 / P1 / P2 / P3 — describe user impact in one sentence.'}], - }, - {type: 'h2', children: [{text: 'Environment'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Build / version: '}, {text: '<sha or tag>', code: true}]}, - {type: 'li', children: [{text: 'Platform / OS / browser: '}, {text: '<…>', code: true}]}, - {type: 'li', children: [{text: 'Account / tenant: '}, {text: '<…>', code: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Steps to reproduce'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: 'Step 1'}]}, - {type: 'li', children: [{text: 'Step 2'}]}, - {type: 'li', children: [{text: 'Step 3'}]}, - ], - }, - {type: 'h2', children: [{text: 'Expected behavior'}]}, - {type: 'p', children: [{text: 'What should happen.'}]}, - {type: 'h2', children: [{text: 'Actual behavior'}]}, - {type: 'p', children: [{text: 'What actually happens. Attach screenshots or screen recordings as embeds.'}]}, - {type: 'h2', children: [{text: 'Logs / stack trace'}]}, - { - type: 'code-block', - children: [{text: 'Paste relevant log output here'}], - }, - {type: 'h2', children: [{text: 'Workaround'}]}, - { - type: 'p', - children: [{text: 'Any temporary mitigation users can apply, or '}, {text: 'none', italic: true}, {text: '.'}], - }, - {type: 'h2', children: [{text: 'Investigation notes'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Hypothesis 1 — …'}]}, - {type: 'li', children: [{text: 'Hypothesis 2 — …'}]}, - ], - }, -]; - -const readme: SlateDocument = [ - {type: 'h1', children: [{text: '<project-name>'}]}, - { - type: 'p', - children: [ - {text: 'A '}, - {text: 'one-line', italic: true}, - {text: ' description of what this project does and who it is for.'}, - ], - }, - {type: 'h2', children: [{text: 'Features'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Feature one'}]}, - {type: 'li', children: [{text: 'Feature two'}]}, - {type: 'li', children: [{text: 'Feature three'}]}, - ], - }, - {type: 'h2', children: [{text: 'Installation'}]}, - { - type: 'code-block', - language: 'bash', - children: [{text: 'npm install <project-name>'}], - }, - {type: 'h2', children: [{text: 'Usage'}]}, - { - type: 'code-block', - language: 'ts', - children: [{text: "import {example} from '<project-name>';\n\nexample();"}], - }, - {type: 'h2', children: [{text: 'API'}]}, - { - type: 'p', - children: [{text: 'Document each public export — its signature, parameters, return value, and side effects.'}], - }, - {type: 'h2', children: [{text: 'Development'}]}, - { - type: 'code-block', - language: 'bash', - children: [{text: 'git clone <repo>\nnpm install\nnpm test'}], - }, - {type: 'h2', children: [{text: 'Contributing'}]}, - { - type: 'p', - children: [ - {text: 'Issues and pull requests are welcome. See '}, - {text: 'CONTRIBUTING.md', code: true}, - {text: ' for guidelines.'}, - ], - }, - {type: 'h2', children: [{text: 'License'}]}, - {type: 'p', children: [{text: 'MIT'}]}, -]; - -const postmortem: SlateDocument = [ - {type: 'h1', children: [{text: 'Postmortem: <incident name>'}]}, - { - type: 'p', - children: [ - {text: 'Date: '}, - {text: '<YYYY-MM-DD>', italic: true}, - {text: ' · Authors: '}, - {text: '<names>', italic: true}, - {text: ' · Status: '}, - {text: 'Draft', bold: true}, - ], - }, - { - type: 'callout', - icon: '🔥', - title: 'Summary', - children: [{text: 'One paragraph: what broke, who was affected, how long, and how it was fixed.'}], - }, - {type: 'h2', children: [{text: 'Impact'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Users affected: '}, {text: '<count / %>', code: true}]}, - {type: 'li', children: [{text: 'Duration: '}, {text: '<HH:MM UTC – HH:MM UTC>', code: true}]}, - {type: 'li', children: [{text: 'Systems affected: '}, {text: '<services>', code: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Timeline (UTC)'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<HH:MM> — first signal / page'}]}, - {type: 'li', children: [{text: '<HH:MM> — incident declared, IC assigned'}]}, - {type: 'li', children: [{text: '<HH:MM> — root cause identified'}]}, - {type: 'li', children: [{text: '<HH:MM> — mitigation applied'}]}, - {type: 'li', children: [{text: '<HH:MM> — incident resolved'}]}, - ], - }, - {type: 'h2', children: [{text: 'Root cause'}]}, - { - type: 'p', - children: [ - { - text: 'Walk the chain of events down to the underlying cause. Resist stopping at the first plausible explanation.', - }, - ], - }, - {type: 'h2', children: [{text: 'Detection'}]}, - { - type: 'p', - children: [ - {text: 'How was the issue detected? How long did detection take, and could we have detected it sooner?'}, - ], - }, - {type: 'h2', children: [{text: 'What went well'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '…'}]}], - }, - {type: 'h2', children: [{text: 'What went poorly'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '…'}]}], - }, - {type: 'h2', children: [{text: 'Action items'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: '<owner> — prevention — <action>'}]}, - {type: 'li', checked: false, children: [{text: '<owner> — detection — <action>'}]}, - {type: 'li', checked: false, children: [{text: '<owner> — response — <action>'}]}, - ], - }, - { - type: 'callout', - icon: '🙅', - title: 'Blameless', - children: [{text: 'Focus on systems and processes, not individuals. The goal is learning, not assigning fault.'}], - }, -]; - -const retro: SlateDocument = [ - {type: 'h1', children: [{text: 'Sprint retrospective — <sprint / dates>'}]}, - { - type: 'p', - children: [ - {text: 'Team: '}, - {text: '<team>', italic: true}, - {text: ' · Facilitator: '}, - {text: '<name>', italic: true}, - ], - }, - {type: 'h2', children: [{text: 'What went well 🎉'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - ], - }, - {type: 'h2', children: [{text: 'What didn’t go well 😬'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - ], - }, - {type: 'h2', children: [{text: 'What we learned 💡'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '…'}]}], - }, - {type: 'h2', children: [{text: 'Action items'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: '<owner> — <action> — due <date>'}]}, - {type: 'li', checked: false, children: [{text: '<owner> — <action> — due <date>'}]}, - ], - }, - {type: 'h2', children: [{text: 'Carry-over from last retro'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: 'Status of previous action items.'}]}], - }, -]; - -const streamCheck: SlateDocument = [ - {type: 'h1', children: [{text: 'Going live: <stream title>'}]}, - { - type: 'callout', - icon: '🔴', - title: 'Schedule', - children: [{text: '<day>, <HH:MM> <timezone> · est. <duration>'}], - }, - {type: 'h2', children: [{text: 'Pre-stream check'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: 'Mic levels + pop filter'}]}, - {type: 'li', checked: false, children: [{text: 'Camera framing & lighting'}]}, - {type: 'li', checked: false, children: [{text: 'OBS scenes & alerts'}]}, - {type: 'li', checked: false, children: [{text: 'Game/app updated, captures running'}]}, - { - type: 'li', - checked: false, - children: [{text: 'Internet speed test (up '}, {text: '> 8 Mbps', code: true}, {text: ')'}], - }, - {type: 'li', checked: false, children: [{text: 'Snacks + water within reach'}]}, - {type: 'li', checked: false, children: [{text: 'Phone on do-not-disturb'}]}, - {type: 'li', checked: false, children: [{text: 'Stream title + tags + category set'}]}, - ], - }, - {type: 'h2', children: [{text: 'Today’s plan'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: 'Welcome / catch-up (~5 min)'}]}, - {type: 'li', children: [{text: 'Main segment — '}, {text: '<topic>', italic: true}]}, - {type: 'li', children: [{text: 'Break / Q&A'}]}, - {type: 'li', children: [{text: 'Wrap-up + raid'}]}, - ], - }, - {type: 'h2', children: [{text: 'Talking points'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - ], - }, - {type: 'h2', children: [{text: 'Shoutouts & sponsors'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '@<creator> — '}, {text: 'why', italic: true}]}, - {type: 'li', children: [{text: '<sponsor> — code '}, {text: '<CODE>', code: true}, {text: ' · plug at <time>'}]}, - ], - }, - {type: 'h2', children: [{text: 'Stream goals'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: 'Hit <N> concurrent viewers'}]}, - {type: 'li', checked: false, children: [{text: '<N> new follows'}]}, - {type: 'li', checked: false, children: [{text: 'Reach charity/donation goal'}]}, - ], - }, - {type: 'h2', children: [{text: 'Post-stream notes'}]}, - {type: 'p', children: [{text: 'What worked, what to fix next time, clip ideas, VOD timestamps.'}]}, -]; - -const videoScript: SlateDocument = [ - {type: 'h1', children: [{text: '<video title>'}]}, - { - type: 'p', - children: [ - {text: 'Platform: '}, - {text: '<YouTube / TikTok / Reels / Shorts>', italic: true}, - {text: ' · Length: '}, - {text: '<m:ss>', italic: true}, - {text: ' · Aspect: '}, - {text: '<16:9 / 9:16>', italic: true}, - ], - }, - { - type: 'callout', - icon: '🪝', - title: 'Hook (first 3 seconds)', - children: [{text: 'One sentence that stops the scroll. Promise the payoff.'}], - }, - {type: 'h2', children: [{text: 'Intro'}]}, - {type: 'p', children: [{text: 'Who you are, what this video gives them, why they should stay.'}]}, - {type: 'h2', children: [{text: 'Beat sheet'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: 'Beat 1 — set up the problem'}]}, - {type: 'li', children: [{text: 'Beat 2 — first reveal / demo'}]}, - {type: 'li', children: [{text: 'Beat 3 — twist or deeper insight'}]}, - {type: 'li', children: [{text: 'Beat 4 — payoff'}]}, - ], - }, - {type: 'h2', children: [{text: 'Script'}]}, - { - type: 'p', - children: [{text: 'Write spoken lines exactly as you’ll deliver them. Short sentences. Read them aloud.'}], - }, - {type: 'h3', children: [{text: 'B-roll / on-screen'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<timestamp> — '}, {text: '<shot description>', italic: true}]}, - {type: 'li', children: [{text: '<timestamp> — '}, {text: '<text overlay>', italic: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Call to action'}]}, - { - type: 'p', - children: [{text: 'Like, subscribe, follow, link in bio — be specific about what you want them to do next.'}], - }, - {type: 'h2', children: [{text: 'Outro'}]}, - {type: 'p', children: [{text: 'Tease the next video and end strong.'}]}, - {type: 'h2', children: [{text: 'Description / SEO'}]}, - { - type: 'code-block', - children: [{text: 'Title: <title>\n\n<2–3 sentence description with target keyword>\n\n#<tag1> #<tag2> #<tag3>'}], - }, - {type: 'h2', children: [{text: 'Thumbnail ideas'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Concept A — '}, {text: '<text + visual>', italic: true}]}, - {type: 'li', children: [{text: 'Concept B — '}, {text: '<text + visual>', italic: true}]}, - ], - }, -]; - -const journal: SlateDocument = [ - {type: 'title', children: [{text: '<YYYY-MM-DD>'}]}, - { - type: 'p', - children: [ - {text: 'Mood: '}, - {text: '<emoji or word>', italic: true}, - {text: ' · Weather: '}, - {text: '<…>', italic: true}, - ], - }, - {type: 'h2', children: [{text: 'Today’s highlights'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - ], - }, - {type: 'h2', children: [{text: 'Grateful for'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - ], - }, - {type: 'h2', children: [{text: 'Today I learned'}]}, - {type: 'p', children: [{text: 'One thing — could be tiny.'}]}, - {type: 'h2', children: [{text: 'Mind dump'}]}, - {type: 'p', children: [{text: 'Anything that’s rattling around — write without editing.'}]}, - { - type: 'blockquote', - children: [{text: 'Quote / lyric / line that stuck with me today.'}], - }, - {type: 'h2', children: [{text: 'Tomorrow'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: 'One thing I will do'}]}, - {type: 'li', checked: false, children: [{text: 'One thing I will not do'}]}, - ], - }, -]; - -const recipe: SlateDocument = [ - {type: 'h1', children: [{text: '<recipe name>'}]}, - { - type: 'p', - children: [ - {text: 'Serves: '}, - {text: '<N>', bold: true}, - {text: ' · Prep: '}, - {text: '<N min>', bold: true}, - {text: ' · Cook: '}, - {text: '<N min>', bold: true}, - {text: ' · '}, - {text: '<vegetarian / vegan / gluten-free>', italic: true}, - ], - }, - { - type: 'callout', - icon: '👩‍🍳', - title: 'Notes', - children: [{text: 'Where the recipe came from, why you love it, what to swap if you’re missing something.'}], - }, - {type: 'h2', children: [{text: 'Ingredients'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<amount> '}, {text: '<unit>', italic: true}, {text: ' <ingredient>'}]}, - {type: 'li', children: [{text: '<amount> '}, {text: '<unit>', italic: true}, {text: ' <ingredient>'}]}, - {type: 'li', children: [{text: '<amount> '}, {text: '<unit>', italic: true}, {text: ' <ingredient>'}]}, - ], - }, - {type: 'h2', children: [{text: 'Equipment'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '<pan / pot / bowl / etc>'}]}], - }, - {type: 'h2', children: [{text: 'Instructions'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: 'Prep — wash, chop, measure.'}]}, - {type: 'li', children: [{text: 'Heat the pan…'}]}, - {type: 'li', children: [{text: 'Combine…'}]}, - {type: 'li', children: [{text: 'Season to taste and serve.'}]}, - ], - }, - {type: 'h2', children: [{text: 'Variations'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Spicier: add '}, {text: '<…>', code: true}]}, - {type: 'li', children: [{text: 'Vegetarian swap: '}, {text: '<…>', code: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Pairings'}]}, - {type: 'p', children: [{text: 'Goes well with…'}]}, -]; - -const travel: SlateDocument = [ - {type: 'h1', children: [{text: '<destination> — <month YYYY>'}]}, - { - type: 'p', - children: [ - {text: 'Dates: '}, - {text: '<YYYY-MM-DD> → <YYYY-MM-DD>', bold: true}, - {text: ' · Travelers: '}, - {text: '<names>', italic: true}, - {text: ' · Budget: '}, - {text: '<$>', italic: true}, - ], - }, - {type: 'h2', children: [{text: 'Reservations'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '✈️ '}, {text: '<flight / confirmation #>', code: true}]}, - {type: 'li', children: [{text: '🏨 '}, {text: '<hotel / Airbnb / confirmation #>', code: true}]}, - {type: 'li', children: [{text: '🚗 '}, {text: '<car rental / transit pass>', code: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Packing list'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: 'Passport / ID'}]}, - {type: 'li', checked: false, children: [{text: 'Chargers + adapter'}]}, - {type: 'li', checked: false, children: [{text: 'Meds'}]}, - {type: 'li', checked: false, children: [{text: 'Weather-appropriate clothes'}]}, - {type: 'li', checked: false, children: [{text: 'Snacks for the trip'}]}, - ], - }, - {type: 'h2', children: [{text: 'Day-by-day'}]}, - {type: 'h3', children: [{text: 'Day 1 — arrival'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Morning — …'}]}, - {type: 'li', children: [{text: 'Afternoon — …'}]}, - {type: 'li', children: [{text: 'Evening — …'}]}, - ], - }, - {type: 'h3', children: [{text: 'Day 2'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '…'}]}], - }, - {type: 'h2', children: [{text: 'Want-to-eat / want-to-see'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '🍜 <restaurant> — '}, {text: '<dish>', italic: true}]}, - {type: 'li', children: [{text: '🏛️ <landmark>'}]}, - {type: 'li', children: [{text: '🌳 <park / view>'}]}, - ], - }, - {type: 'h2', children: [{text: 'Important contacts'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Embassy: '}, {text: '<phone>', code: true}]}, - {type: 'li', children: [{text: 'Hotel front desk: '}, {text: '<phone>', code: true}]}, - {type: 'li', children: [{text: 'Travel insurance: '}, {text: '<policy / phone>', code: true}]}, - ], - }, -]; - -const competitorAnalysis: SlateDocument = [ - {type: 'h1', children: [{text: 'Competitor analysis: <market / category>'}]}, - { - type: 'p', - children: [ - {text: 'Owner: '}, - {text: '<your name>', italic: true}, - {text: ' · Last updated: '}, - {text: '<YYYY-MM-DD>', italic: true}, - {text: ' · Status: '}, - {text: 'Living document', bold: true}, - ], - }, - { - type: 'callout', - icon: '🎯', - title: 'TL;DR', - children: [{text: 'One paragraph: who the players are, where we stand, and the one move we should make next.'}], - }, - {type: 'h2', children: [{text: 'Market & context'}]}, - { - type: 'p', - children: [{text: 'What is this market? Size, growth, target buyer, dominant trends. Why it matters now.'}], - }, - {type: 'h2', children: [{text: 'Our positioning'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Who we serve: '}, {text: '<segment>', italic: true}]}, - {type: 'li', children: [{text: 'Core promise: '}, {text: '<value prop>', italic: true}]}, - {type: 'li', children: [{text: 'Wedge / unfair advantage: '}, {text: '<…>', italic: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Competitors'}]}, - {type: 'h3', children: [{text: '<Competitor A>'}]}, - { - type: 'p', - children: [ - {text: 'Site: '}, - {text: '<url>', a: {href: 'https://example.com'}}, - {text: ' · HQ: '}, - {text: '<city>', italic: true}, - {text: ' · Founded: '}, - {text: '<year>', italic: true}, - {text: ' · Funding: '}, - {text: '<stage / $>', italic: true}, - {text: ' · Headcount: '}, - {text: '<N>', italic: true}, - ], - }, - { - type: 'p', - children: [{text: 'One-line description of what they do and who they serve.'}], - }, - { - type: 'callout', - icon: '✅', - title: 'Strengths', - color: '#3aaf5f', - children: [{text: 'Where they’re genuinely better than us — features, distribution, brand, capital, talent.'}], - }, - { - type: 'callout', - icon: '⚠️', - title: 'Weaknesses', - color: '#e07a5f', - children: [{text: 'Where they fall short — gaps, complaints, missing segments, organizational drag.'}], - }, - { - type: 'p', - children: [ - {text: 'Pricing: '}, - {text: '<starting tier / model>', code: true}, - {text: ' · Notable customers: '}, - {text: '<…>', italic: true}, - ], - }, - {type: 'h3', children: [{text: '<Competitor B>'}]}, - { - type: 'p', - children: [ - {text: 'Site: '}, - {text: '<url>', a: {href: 'https://example.com'}}, - {text: ' · HQ: '}, - {text: '<city>', italic: true}, - {text: ' · Founded: '}, - {text: '<year>', italic: true}, - {text: ' · Funding: '}, - {text: '<stage / $>', italic: true}, - ], - }, - {type: 'p', children: [{text: 'One-line description.'}]}, - { - type: 'callout', - icon: '✅', - title: 'Strengths', - color: '#3aaf5f', - children: [{text: '…'}], - }, - { - type: 'callout', - icon: '⚠️', - title: 'Weaknesses', - color: '#e07a5f', - children: [{text: '…'}], - }, - {type: 'h3', children: [{text: '<Competitor C>'}]}, - {type: 'p', children: [{text: 'Repeat the same template for each player worth tracking.'}]}, - {type: 'h2', children: [{text: 'Feature comparison'}]}, - { - type: 'p', - children: [ - { - text: 'Score each player on the dimensions that actually drive purchase decisions. Use ✅ / ⚠️ / ❌ for quick scanning.', - }, - ], - }, - {type: 'h3', children: [{text: 'Core capability'}]}, - { - type: 'ul', - children: [ - { - type: 'li', - children: [ - {text: 'Us — '}, - {text: '✅ ', bold: true}, - {text: 'best-in-class for '}, - {text: '<scenario>', italic: true}, - ], - }, - {type: 'li', children: [{text: '<Competitor A> — '}, {text: '✅ ', bold: true}, {text: '…'}]}, - {type: 'li', children: [{text: '<Competitor B> — '}, {text: '⚠️ ', bold: true}, {text: '…'}]}, - ], - }, - {type: 'h3', children: [{text: 'Pricing & packaging'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Us — '}, {text: '<model>', italic: true}]}, - {type: 'li', children: [{text: '<Competitor A> — '}, {text: '<model>', italic: true}]}, - {type: 'li', children: [{text: '<Competitor B> — '}, {text: '<model>', italic: true}]}, - ], - }, - {type: 'h3', children: [{text: 'Distribution & GTM'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Us — '}, {text: '<channels>', italic: true}]}, - {type: 'li', children: [{text: '<Competitor A> — '}, {text: '<channels>', italic: true}]}, - ], - }, - {type: 'h3', children: [{text: 'Ecosystem & integrations'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Us — '}, {text: '<…>', italic: true}]}, - {type: 'li', children: [{text: '<Competitor A> — '}, {text: '<…>', italic: true}]}, - ], - }, - {type: 'h2', children: [{text: 'SWOT — us vs. the field'}]}, - { - type: 'callout', - icon: '💪', - title: 'Strengths', - color: '#3aaf5f', - children: [{text: 'What we do better than anyone — protect and amplify these.'}], - }, - { - type: 'callout', - icon: '🩹', - title: 'Weaknesses', - color: '#e07a5f', - children: [{text: 'Honest gaps — where we lose deals or look weak.'}], - }, - { - type: 'callout', - icon: '🚀', - title: 'Opportunities', - color: '#4d96ff', - children: [{text: 'Open lanes — underserved segments, missing features, regulatory shifts.'}], - }, - { - type: 'callout', - icon: '☁️', - title: 'Threats', - color: '#8b8b8b', - children: [{text: 'What could blindside us — funded entrants, platform changes, pricing pressure.'}], - }, - {type: 'h2', children: [{text: 'Recommendations'}]}, - { - type: 'checklist', - children: [ - {type: 'li', checked: false, children: [{text: '<owner> — defend: <action>'}]}, - {type: 'li', checked: false, children: [{text: '<owner> — close gap: <action>'}]}, - {type: 'li', checked: false, children: [{text: '<owner> — exploit opportunity: <action>'}]}, - ], - }, - {type: 'h2', children: [{text: 'Sources & evidence'}]}, - { - type: 'ol', - children: [ - {type: 'li', children: [{text: '<source — link or note>'}]}, - {type: 'li', children: [{text: '<customer interview / win-loss / analyst report>'}]}, - ], - }, -]; - -const toolComparison: SlateDocument = [ - {type: 'h1', children: [{text: 'Tool comparison: <decision>'}]}, - { - type: 'p', - children: [ - {text: 'Owner: '}, - {text: '<name>', italic: true}, - {text: ' · Decision needed by: '}, - {text: '<YYYY-MM-DD>', italic: true}, - ], - }, - { - type: 'callout', - icon: '🎯', - title: 'Decision criteria', - children: [ - { - text: 'What we’re optimizing for — performance, ergonomics, cost, ecosystem, lock-in, etc. Rank in order of priority.', - }, - ], - }, - {type: 'h2', children: [{text: 'Requirements'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Must-have — '}, {text: '<requirement>', italic: true}]}, - {type: 'li', children: [{text: 'Must-have — '}, {text: '<requirement>', italic: true}]}, - {type: 'li', children: [{text: 'Nice-to-have — '}, {text: '<requirement>', italic: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Options'}]}, - {type: 'h3', children: [{text: '<Option A>'}]}, - { - type: 'p', - children: [ - {text: 'Docs: '}, - {text: '<url>', a: {href: 'https://example.com'}}, - {text: ' · License: '}, - {text: '<…>', code: true}, - {text: ' · Maintainer: '}, - {text: '<…>', italic: true}, - {text: ' · Last release: '}, - {text: '<date / version>', code: true}, - ], - }, - { - type: 'code-block', - language: 'bash', - children: [{text: 'npm install <package-a>'}], - }, - {type: 'h4', children: [{text: 'Pros'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - ], - }, - {type: 'h4', children: [{text: 'Cons'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '…'}]}, - {type: 'li', children: [{text: '…'}]}, - ], - }, - {type: 'h4', children: [{text: 'Cost / footprint'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Bundle size: '}, {text: '<kB>', code: true}]}, - {type: 'li', children: [{text: 'Runtime cost: '}, {text: '<…>', code: true}]}, - {type: 'li', children: [{text: 'Pricing: '}, {text: '<$ / free>', code: true}]}, - ], - }, - {type: 'h3', children: [{text: '<Option B>'}]}, - { - type: 'p', - children: [ - {text: 'Docs: '}, - {text: '<url>', a: {href: 'https://example.com'}}, - {text: ' · License: '}, - {text: '<…>', code: true}, - {text: ' · Maintainer: '}, - {text: '<…>', italic: true}, - ], - }, - { - type: 'code-block', - language: 'bash', - children: [{text: 'npm install <package-b>'}], - }, - {type: 'h4', children: [{text: 'Pros'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '…'}]}], - }, - {type: 'h4', children: [{text: 'Cons'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '…'}]}], - }, - {type: 'h3', children: [{text: 'Build it ourselves'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'Estimated effort: '}, {text: '<weeks>', code: true}]}, - {type: 'li', children: [{text: 'Why we might: '}, {text: '<…>', italic: true}]}, - {type: 'li', children: [{text: 'Why we wouldn’t: '}, {text: '<…>', italic: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Scorecard'}]}, - {type: 'p', children: [{text: 'Rate each option against the criteria — keep it lightweight (✅ / ⚠️ / ❌).'}]}, - {type: 'h3', children: [{text: 'Performance'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'A — ✅ '}, {text: '<note>', italic: true}]}, - {type: 'li', children: [{text: 'B — ⚠️ '}, {text: '<note>', italic: true}]}, - ], - }, - {type: 'h3', children: [{text: 'Developer experience'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'A — '}, {text: '<note>', italic: true}]}, - {type: 'li', children: [{text: 'B — '}, {text: '<note>', italic: true}]}, - ], - }, - {type: 'h3', children: [{text: 'Ecosystem & community'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'A — '}, {text: '<github stars / discord / SO>', italic: true}]}, - {type: 'li', children: [{text: 'B — '}, {text: '<…>', italic: true}]}, - ], - }, - {type: 'h3', children: [{text: 'Risk & lock-in'}]}, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: 'A — '}, {text: '<note>', italic: true}]}, - {type: 'li', children: [{text: 'B — '}, {text: '<note>', italic: true}]}, - ], - }, - {type: 'h2', children: [{text: 'Decision'}]}, - { - type: 'callout', - icon: '✅', - title: 'We’ll go with: <option>', - color: '#3aaf5f', - children: [ - { - text: 'One-paragraph justification — which criteria pushed us over the line, and what we are accepting as trade-offs.', - }, - ], - }, - {type: 'h2', children: [{text: 'Open questions'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '<question to follow up on>'}]}], - }, -]; - -const tierList: SlateDocument = [ - {type: 'h1', children: [{text: 'Tier list: <what you’re ranking>'}]}, - { - type: 'p', - children: [ - {text: 'Ranked by '}, - {text: '<your criteria>', italic: true}, - {text: ' — '}, - {text: '<who / when>', italic: true}, - {text: '.'}, - ], - }, - { - type: 'blockquote', - children: [{text: 'How I’m ranking these — be specific so future-you remembers what S actually meant.'}], - }, - { - type: 'callout', - icon: 'S', - title: 'God tier', - color: '#ff5e5e', - children: [{text: 'Defining, must-have, perfect for what it does.'}], - }, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<item>'}]}, - {type: 'li', children: [{text: '<item>'}]}, - ], - }, - { - type: 'callout', - icon: 'A', - title: 'Great', - color: '#ff9f43', - children: [{text: 'Excellent — small flaws keep it from S.'}], - }, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<item>'}]}, - {type: 'li', children: [{text: '<item>'}]}, - ], - }, - { - type: 'callout', - icon: 'B', - title: 'Good', - color: '#ffd93d', - children: [{text: 'Solid — happy to recommend.'}], - }, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<item>'}]}, - {type: 'li', children: [{text: '<item>'}]}, - ], - }, - { - type: 'callout', - icon: 'C', - title: 'Average', - color: '#6bcb77', - children: [{text: 'Fine. Take it or leave it.'}], - }, - { - type: 'ul', - children: [ - {type: 'li', children: [{text: '<item>'}]}, - {type: 'li', children: [{text: '<item>'}]}, - ], - }, - { - type: 'callout', - icon: 'D', - title: 'Below average', - color: '#4d96ff', - children: [{text: 'Real problems — only if nothing else is around.'}], - }, - { - type: 'ul', - children: [{type: 'li', children: [{text: '<item>'}]}], - }, - { - type: 'callout', - icon: 'F', - title: 'Avoid', - color: '#8b8b8b', - children: [{text: 'Don’t.'}], - }, - { - type: 'ul', - children: [{type: 'li', children: [{text: '<item>'}]}], - }, - {type: 'h2', children: [{text: 'Honorable mentions'}]}, - { - type: 'ul', - children: [{type: 'li', children: [{text: '<item>'}, {text: ' — couldn’t place yet.', italic: true}]}], - }, -]; - -export const templates: MuTxtTemplate[] = [ - { - id: 'design-doc', - name: 'Engineering design doc (RFC)', - description: 'Goals, proposal, alternatives.', - fileName: 'Design doc', - category: 'work', - doc: designDoc, - }, - { - id: 'meeting-notes', - name: 'Meeting notes', - description: 'Agenda, discussion, decisions, action items.', - fileName: 'Meeting notes', - category: 'work', - doc: meetingNotes, - }, - { - id: 'bug-report', - name: 'Bug report', - description: 'Repro steps, environment, logs.', - fileName: 'Bug report', - category: 'work', - doc: bugReport, - }, - { - id: 'readme', - name: 'Project README', - description: 'Description, features, install, usage, API.', - fileName: 'README', - category: 'work', - doc: readme, - }, - { - id: 'postmortem', - name: 'Incident postmortem', - description: 'Impact, timeline, root cause, action items.', - fileName: 'Postmortem', - category: 'work', - doc: postmortem, - }, - { - id: 'sprint-retro', - name: 'Sprint retrospective', - description: 'What went well, what did not.', - fileName: 'Retrospective', - category: 'work', - doc: retro, - }, - { - id: 'competitor-analysis', - name: 'Competitor analysis', - description: 'Market, positioning, SWOT, recommendations.', - fileName: 'Competitor analysis', - category: 'work', - doc: competitorAnalysis, - }, - { - id: 'tool-comparison', - name: 'Tool / library comparison', - description: 'Requirements, options, pros/cons, scorecard.', - fileName: 'Tool comparison', - category: 'work', - doc: toolComparison, - }, - { - id: 'stream-check', - name: 'Stream go-live checklist', - description: 'Pre-stream check, plan, shoutouts, notes.', - fileName: 'Stream notes', - category: 'personal', - doc: streamCheck, - }, - { - id: 'video-script', - name: 'Video script', - description: 'Hook, beats, script, B-roll, CTA, thumbnails.', - fileName: 'Video script', - category: 'personal', - doc: videoScript, - }, - { - id: 'journal', - name: 'Daily journal', - description: 'Highlights, gratitude, mind dump, plan.', - fileName: 'Journal', - category: 'personal', - doc: journal, - }, - { - id: 'recipe', - name: 'Recipe', - description: 'Servings, ingredients, equipment, steps.', - fileName: 'Recipe', - category: 'personal', - doc: recipe, - }, - { - id: 'travel', - name: 'Travel itinerary', - description: 'Reservations, packing, plan, contacts.', - fileName: 'Trip', - category: 'personal', - doc: travel, - }, - { - id: 'tier-list', - name: 'Tier list (S–F)', - description: 'S/A/B/C/D/F rows for ranking anything.', - fileName: 'Tier list', - category: 'personal', - doc: tierList, - }, -]; diff --git a/packages/mu-txt/src/state/theme.ts b/packages/mu-txt/src/state/theme.ts deleted file mode 100644 index 65f1911b39..0000000000 --- a/packages/mu-txt/src/state/theme.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {rsync} from '@jsonjoy.com/ui'; -import type {ISyncStore} from './sync-store'; - -export type ThemePreference = 'auto' | 'light' | 'dark'; -export type ResolvedTheme = 'light' | 'dark'; - -const STORAGE_KEY = 'mutxt_theme'; - -const isPreference = (val: unknown): val is ThemePreference => val === 'auto' || val === 'light' || val === 'dark'; - -const detectSystemTheme = (): ResolvedTheme => { - if (typeof window === 'undefined' || !window.matchMedia) return 'light'; - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; -}; - -export class Theme { - public readonly preference: rsync.ReactValue<ThemePreference>; - public readonly resolved: rsync.ReactValue<ResolvedTheme>; - private mediaQuery: MediaQueryList | null = null; - - constructor(private readonly sync: ISyncStore) { - const stored = sync.getItem(STORAGE_KEY); - const preference: ThemePreference = isPreference(stored) ? stored : 'auto'; - this.preference = rsync.val<ThemePreference>(preference); - this.resolved = rsync.val<ResolvedTheme>(this.resolve(preference)); - this.attachMedia(); - } - - private resolve(preference: ThemePreference): ResolvedTheme { - return preference === 'auto' ? detectSystemTheme() : preference; - } - - private readonly onSystemChange = () => { - if (this.preference.value === 'auto') { - this.resolved.set(detectSystemTheme()); - } - }; - - private attachMedia(): void { - if (typeof window === 'undefined' || !window.matchMedia) return; - this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - this.mediaQuery.addEventListener('change', this.onSystemChange); - } - - public readonly set = (preference: ThemePreference): void => { - if (this.preference.value === preference) return; - this.preference.set(preference); - this.sync.setItem(STORAGE_KEY, preference); - this.resolved.set(this.resolve(preference)); - }; - - public dispose(): void { - this.mediaQuery?.removeEventListener('change', this.onSystemChange); - this.mediaQuery = null; - } -} diff --git a/packages/mu-txt/src/state/util.ts b/packages/mu-txt/src/state/util.ts deleted file mode 100644 index af1442661c..0000000000 --- a/packages/mu-txt/src/state/util.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const stripExtensions = (filename: string): string => { - if (filename.endsWith('.gz')) filename = filename.slice(0, -3); - if (filename.endsWith('.json')) filename = filename.slice(0, -5); - if (filename.endsWith('.njson')) filename = filename.slice(0, -6); - if (filename.endsWith('.cbor')) filename = filename.slice(0, -5); - if (filename.endsWith('.crdt')) filename = filename.slice(0, -5); - if (filename.endsWith('.seq')) filename = filename.slice(0, -4); - return filename; -}; - -export const downloadFile = (uint8: Uint8Array, filename: string = 'file.bin') => { - const blob = new Blob([uint8 as BlobPart], {type: 'application/octet-stream'}); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - window.URL.revokeObjectURL(url); -}; diff --git a/packages/mu-txt/src/util/DebounceQueue.ts b/packages/mu-txt/src/util/DebounceQueue.ts deleted file mode 100644 index 632e1e0992..0000000000 --- a/packages/mu-txt/src/util/DebounceQueue.ts +++ /dev/null @@ -1,50 +0,0 @@ -export class DebounceQueue<T> { - constructor( - /** - * Queue will be flushed when it reaches this number of items. - */ - public itemLimit: number, - - /** - * Queue will be flushed after this many milliseconds since the last item - * was pushed, if it hasn't been flushed by item limit already. - */ - public timeLimit: number, - ) {} - - /** - * Method that will be called when queue is flushed. - */ - onflush = (_list: T[]) => {}; - - private list: T[] = []; - private timer: any = null; - - push(item: T) { - this.list.push(item); - if (this.list.length >= this.itemLimit) { - this.flush(); - return; - } - clearTimeout(this.timer); - this.timer = setTimeout(() => { - this.flush(); - }, this.timeLimit); - } - - flush(): T[] { - const list = this.list; - this.list = []; - clearTimeout(this.timer); - this.timer = null; - if (list.length) { - try { - this.onflush(list); - } catch (error) { - // tslint:disable-next-line - console.error('DebounceQueue', error); - } - } - return list; - } -} diff --git a/packages/mu-txt/src/util/host.ts b/packages/mu-txt/src/util/host.ts deleted file mode 100644 index a02909db68..0000000000 --- a/packages/mu-txt/src/util/host.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface MutxtHost { - readonly platform: 'darwin' | 'win32' | 'linux' | (string & {}); - readonly versions: Readonly<Record<string, string | undefined>>; - openDialog(): Promise<void>; - writeFile(path: string, bytes: Uint8Array): Promise<void>; - setTitlebarMode(mode: 'default' | 'compact'): Promise<void>; - onOpenFile(cb: (file: {name: string; path: string; bytes?: Uint8Array}) => void): () => void; - onOpenUrl(cb: (url: string) => void): () => void; - /** Fires when the user presses Cmd/Ctrl+W. */ - onCloseFile(cb: () => void): () => void; -} - -declare global { - interface Window { - mutxt?: MutxtHost; - } -} - -export const host: MutxtHost | null = typeof window !== 'undefined' && window.mutxt ? window.mutxt : null; - -export const isElectron = host !== null; -export const isMacElectron = isElectron && host!.platform === 'darwin'; - -export const TITLEBAR_INSET_PX = 78; - -if (typeof document !== 'undefined') { - const value = isMacElectron ? `${TITLEBAR_INSET_PX}px` : 'env(titlebar-area-x, 0px)'; - document.documentElement.style.setProperty('--titlebar-inset-left', value); -} diff --git a/packages/mu-txt/tsconfig.app.json b/packages/mu-txt/tsconfig.app.json deleted file mode 100644 index f8867cf97a..0000000000 --- a/packages/mu-txt/tsconfig.app.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "node", - "isolatedModules": true, - "noEmit": true, - "jsx": "react", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true - }, - "include": ["src", "webpack.config.ts"], - "exclude": [ - "src/**/*.stories.ts", - "src/**/*.stories.tsx" - ] -} diff --git a/packages/mu-txt/tsconfig.build.json b/packages/mu-txt/tsconfig.build.json deleted file mode 100644 index 111e0106ac..0000000000 --- a/packages/mu-txt/tsconfig.build.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "sourceMap": false, - "rootDir": "./src", - "tsBuildInfoFile": "./lib/.tsbuildinfo" - }, - "exclude": [ - "src/demo", - "src/__tests__", - "src/**/__demos__/**/*.*", - "src/**/__tests__/**/*.*", - "src/**/__bench__/**/*.*", - "src/**/__mocks__/**/*.*", - "src/**/__jest__/**/*.*", - "src/**/__mocha__/**/*.*", - "src/**/__tap__/**/*.*", - "src/**/__tape__/**/*.*", - "*.test.ts", - "*.spec.ts", - "src/**/__stories__/**/*.*", - "src/**/*.stories.ts", - "src/**/*.stories.tsx" - ], - "references": [ - {"path": "../mutxt-react/tsconfig.build.json"}, - {"path": "../json-joy/tsconfig.build.json"}, - {"path": "../json-pack/tsconfig.build.json"}, - {"path": "../ui/tsconfig.build.json"}, - {"path": "../util/tsconfig.build.json"}, - {"path": "../collaborative-ui/tsconfig.build.json"}, - {"path": "../collaborative-slate/tsconfig.build.json"}, - {"path": "../collaborative-peritext/tsconfig.build.json"} - ] -} diff --git a/packages/mu-txt/tsconfig.electron.json b/packages/mu-txt/tsconfig.electron.json deleted file mode 100644 index e56b1dab54..0000000000 --- a/packages/mu-txt/tsconfig.electron.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022"], - "module": "commonjs", - "moduleResolution": "node", - "outDir": "out-electron", - "rootDir": "electron", - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "declaration": false, - "noEmit": false, - "isolatedModules": false, - "types": ["node"] - }, - "include": ["electron/**/*"] -} diff --git a/packages/mu-txt/tsconfig.json b/packages/mu-txt/tsconfig.json deleted file mode 100644 index 5ffe5261ba..0000000000 --- a/packages/mu-txt/tsconfig.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib" - }, - "include": [ - "src" - ], - "exclude": [ - "src/demo", - "src/__tests__", - "src/**/__demos__/**/*.*", - "src/**/__tests__/**/*.*", - "src/**/__bench__/**/*.*", - "src/**/__mocks__/**/*.*", - "src/**/__jest__/**/*.*", - "src/**/__mocha__/**/*.*", - "src/**/__tap__/**/*.*", - "src/**/__tape__/**/*.*", - "*.test.ts", - "*.spec.ts", - "src/**/*.stories.ts", - "src/**/*.stories.tsx" - ], - "references": [ - {"path": "../mutxt-react"}, - {"path": "../json-joy"}, - {"path": "../json-pack"}, - {"path": "../ui"}, - {"path": "../util"}, - {"path": "../collaborative-ui"}, - {"path": "../collaborative-slate"}, - {"path": "../collaborative-peritext"} - ] -} diff --git a/packages/mu-txt/webpack.config.ts b/packages/mu-txt/webpack.config.ts deleted file mode 100644 index 7db867b923..0000000000 --- a/packages/mu-txt/webpack.config.ts +++ /dev/null @@ -1,158 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import {Compilation, sources, type Compiler, type Configuration} from 'webpack'; -import type {Configuration as DevServerConfiguration} from 'webpack-dev-server'; - -const root = path.resolve(__dirname, '..'); -const isDev = process.env.NODE_ENV !== 'production'; - -class CopyPublicAssetsPlugin { - constructor(private readonly assets: Array<{from: string; to?: string}>) {} - - public apply(compiler: Compiler): void { - compiler.hooks.thisCompilation.tap('CopyPublicAssetsPlugin', (compilation) => { - compilation.hooks.processAssets.tapPromise( - { - name: 'CopyPublicAssetsPlugin', - stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, - }, - async () => { - await Promise.all( - this.assets.map(async ({from, to = from}) => { - const assetPath = path.resolve(__dirname, 'public', from); - const source = await fs.readFile(assetPath); - compilation.emitAsset(to, new sources.RawSource(source)); - }), - ); - }, - ); - }); - } -} - -const config: Configuration & {devServer?: DevServerConfiguration} = { - entry: './src/main.tsx', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'assets/[name].[contenthash:8].js', - chunkFilename: 'assets/[name].[contenthash:8].chunk.js', - publicPath: '/', - clean: true, - }, - resolve: { - extensions: ['.tsx', '.ts', '.js', '.jsx'], - // Redirect workspace package lib/ imports to TypeScript source. - alias: { - // @jsonjoy.com/pkg/lib/path -> packages/pkg/src/path - // Handled by the rules below using webpack alias with functions isn't ideal; - // instead we use resolve.alias with regex patterns via the nsm approach. - }, - plugins: [ - { - apply(resolver) { - // Rewrite @jsonjoy.com/pkg/lib/... -> packages/pkg/src/... - const target = resolver.ensureHook('resolve'); - resolver.getHook('resolve').tapAsync('WorkspaceSourceAlias', (request, resolveContext, callback) => { - const req = request.request ?? ''; - let rewritten: string | null = null; - - const jsonjoyMatch = req.match(/^@jsonjoy\.com\/([^/]+)\/lib\/(.+)$/); - if (jsonjoyMatch) { - rewritten = path.join(root, jsonjoyMatch[1], 'src', jsonjoyMatch[2]); - } else if (/^@jsonjoy\.com\/([^/]+)$/.test(req)) { - const m = req.match(/^@jsonjoy\.com\/([^/]+)$/)!; - rewritten = path.join(root, m[1], 'src'); - } else { - const mutxtReactLibMatch = req.match(/^mutxt-react\/lib(?:\/(.+))?$/); - if (mutxtReactLibMatch) { - rewritten = path.join(root, 'mutxt-react', 'src', mutxtReactLibMatch[1] ?? ''); - } else if (req === 'mutxt-react') { - rewritten = path.join(root, 'mutxt-react', 'src'); - } - } - - if (!rewritten) { - const joyMatch = req.match(/^json-joy\/lib\/(.+)$/); - if (joyMatch) { - rewritten = path.join(root, 'json-joy', 'src', joyMatch[1]); - } else if (req === 'json-joy') { - rewritten = path.join(root, 'json-joy', 'src'); - } - } - - if (!rewritten) { - const nanoThemeLibMatch = req.match(/^nano-theme\/lib(?:\/(.+))?$/); - if (nanoThemeLibMatch) { - rewritten = path.join(root, 'nano-theme', 'src', nanoThemeLibMatch[1] ?? ''); - } else if (req === 'nano-theme') { - rewritten = path.join(root, 'nano-theme', 'src'); - } - } - - if (!rewritten) return callback(); - resolver.doResolve( - target, - {...request, request: rewritten}, - `WorkspaceSourceAlias: ${req} -> ${rewritten}`, - resolveContext, - callback, - ); - }); - }, - }, - ], - }, - module: { - rules: [ - { - test: /\.tsx?$/, - use: [ - { - loader: 'ts-loader', - options: { - configFile: path.resolve(__dirname, 'tsconfig.app.json'), - transpileOnly: true, - }, - }, - ], - exclude: /node_modules/, - }, - { - test: /\.css$/, - use: isDev ? ['style-loader', 'css-loader'] : [MiniCssExtractPlugin.loader, 'css-loader'], - }, - { - test: /\.(woff2?|ttf|eot|svg|png|jpg|gif)$/, - type: 'asset/resource', - generator: {filename: 'assets/[name].[hash:8][ext]'}, - }, - ], - }, - plugins: [ - new HtmlWebpackPlugin({template: './public/index.html'}), - new CopyPublicAssetsPlugin([ - {from: 'manifest.webmanifest'}, - {from: 'favicon.png'}, - {from: 'icon.svg'}, - {from: 'icons/icon-16.png'}, - {from: 'icons/icon-32.png'}, - {from: 'icons/icon-64.png'}, - {from: 'icons/icon-192.png'}, - {from: 'icons/icon-512.png'}, - ]), - ...(isDev ? [] : [new MiniCssExtractPlugin({filename: 'assets/[name].[contenthash:8].css'})]), - ], - devServer: { - port: 3000, - hot: true, - historyApiFallback: true, - open: false, - }, - optimization: { - splitChunks: {chunks: 'all'}, - }, -}; - -export default config; diff --git a/packages/mu-txt/wrangler.toml b/packages/mu-txt/wrangler.toml deleted file mode 100644 index ca859815de..0000000000 --- a/packages/mu-txt/wrangler.toml +++ /dev/null @@ -1,2 +0,0 @@ -name = "mutxt" -pages_build_output_dir = "dist" diff --git a/packages/mutxt-element/.gitignore b/packages/mutxt-element/.gitignore deleted file mode 100644 index 5cb47a02a9..0000000000 --- a/packages/mutxt-element/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/index.js -/index.js.map diff --git a/packages/mutxt-element/LICENSE b/packages/mutxt-element/LICENSE deleted file mode 100644 index 0ad25db4bd..0000000000 --- a/packages/mutxt-element/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -<https://www.gnu.org/licenses/>. diff --git a/packages/mutxt-element/README.md b/packages/mutxt-element/README.md deleted file mode 100644 index 9e6d93d221..0000000000 --- a/packages/mutxt-element/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# mutxt-element - -`<mu-txt>` — a custom element wrapping the [MuTxt](../mutxt-react) rich-text editor. - -## Use from CDN - -```html -<mu-txt style="height:calc(100vh-32px);padding:16px"></mu-txt> -<script src="https://cdn.jsdelivr.net/npm/mutxt-element"></script> -``` - -## Use from NPM - -```bash -npm install mutxt-element -``` - -```js -import 'mutxt-element'; -``` - -Then use `<mu-txt></mu-txt>` anywhere in your HTML. - -## Seeding the document - -`<mu-txt>` accepts an initial document via either inline children or an external source. The seed is consumed once at mount; later changes are ignored. - -```html -<!-- Plain text from children (default when children are present) --> -<mu-txt> - Just some plain text. Newlines become paragraphs. -</mu-txt> - -<!-- Markdown from children --> -<mu-txt format="markdown"> - # Hello - - A paragraph with **bold** and a [link](https://jsonjoy.com). - - - bullet one - - bullet two -</mu-txt> - -<!-- Slate-like JSON from children --> -<mu-txt format="slate"> - [{"type":"h1","children":[{"text":"Hi"}]}] -</mu-txt> - -<!-- Native binary .mutxt document via a data URL --> -<mu-txt src="data:application/vnd.mutxt;base64,..."></mu-txt> - -<!-- Native binary .mutxt document fetched from a URL --> -<mu-txt src="/docs/intro.mutxt"></mu-txt> -``` - -### `format` attribute - -| value | meaning | -|---|---| -| `text` | Each line of source becomes a paragraph (default when children are present). | -| `markdown` | Markdown source — rendered to rich text. | -| `slate` | A JSON-serialised Slate document. | -| `native` | mu-txt's own binary format (history-preserving). Requires `src`; cannot live in children. Default when `src` is present. | - -### `src` attribute - -Any URL the browser can `fetch()` — including `https://`, relative paths, `data:` URLs, and `blob:` URLs. The data URL form replaces ad-hoc base64 attributes: - -```html -<mu-txt src="data:application/vnd.mutxt;base64,o2F0eXBlYW11dHh0..."></mu-txt> -``` - -When `src` is set, children are ignored. - -### Indentation - -For text/markdown/slate-from-children, the smallest leading whitespace common to every non-empty line is stripped before parsing. So you can indent the inline content along with the surrounding HTML and the parser sees clean, left-aligned source. - -There are two known caveats: content placed on the same line as the opening tag clamps the dedent baseline to 0 (put content on the next line), and tabs and spaces are compared as raw characters (don't mix them). - -## API - -The element exposes the editor controller (`MuTxtApi`) as `.api` once mounted: - -```js -const el = document.querySelector('mu-txt'); -const api = await el.ready(); // wait for the editor to mount -api.focus(); -``` - -Or listen for the `ready` event: - -```js -el.addEventListener('ready', (e) => { - const api = e.detail; // MuTxtApi -}); -``` - -## Custom tag name - -Subclass and register under a different name: - -```js -import {MuTxtElement} from 'mutxt-element'; -class MyEditor extends MuTxtElement {} -customElements.define('my-editor', MyEditor); -``` diff --git a/packages/mutxt-element/SECURITY.md b/packages/mutxt-element/SECURITY.md deleted file mode 100644 index a5497b62af..0000000000 --- a/packages/mutxt-element/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security Policy - -## Supported Versions - -We release patches for security vulnerabilities. The latest major version -will support security patches. - -## Reporting a Vulnerability - -Please report (suspected) security vulnerabilities to -**[streamich@gmail.com](mailto:streamich@gmail.com)**. We will try to respond -within 48 hours. If the issue is confirmed, we will release a patch as soon -as possible depending on complexity. diff --git a/packages/mutxt-element/demo.html b/packages/mutxt-element/demo.html deleted file mode 100644 index eca5697017..0000000000 --- a/packages/mutxt-element/demo.html +++ /dev/null @@ -1,17 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="utf-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title><mu-txt> editor demo - - -

<mu-txt> editor

- - - [{"type":"p","children":[{"text":"Hello, internet!"}]},{"type":"embed","url":"https://soundcloud.com/nichole-negron-830665936/welcome-to-the-internet","children":[{"text":""}]},{"type":"p","children":[{"text":""}]}] - - - - - diff --git a/packages/mutxt-element/minimal.html b/packages/mutxt-element/minimal.html deleted file mode 100644 index 50b020d72f..0000000000 --- a/packages/mutxt-element/minimal.html +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/packages/mutxt-element/package.json b/packages/mutxt-element/package.json deleted file mode 100644 index 3ecc4bb5b4..0000000000 --- a/packages/mutxt-element/package.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "mutxt-element", - "publishConfig": { - "access": "public" - }, - "version": "18.26.0", - "description": " custom element to render mutxt.com rich-text editor", - "author": { - "name": "streamich", - "url": "https://github.com/streamich" - }, - "homepage": "https://mutxt.com", - "repository": { - "type": "git", - "url": "https://github.com/streamich/json-joy.git", - "directory": "packages/mutxt-element" - }, - "license": "AGPL-3.0-only", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "keywords": [ - "mutxt", - "web-component", - "custom-element", - "rich-text", - "editor", - "collaborative", - "real-time", - "json", - "crdt", - "json-joy", - "peritext" - ], - "engines": { - "node": ">=18.0" - }, - "main": "lib/index.js", - "module": "lib/index.js", - "types": "lib/index.d.ts", - "typings": "lib/index.d.ts", - "unpkg": "index.js", - "jsdelivr": "index.js", - "sideEffects": [ - "./lib/index.js", - "./src/index.ts" - ], - "files": [ - "LICENSE", - "lib/", - "index.js", - "index.js.map" - ], - "scripts": { - "clean": "npx rimraf lib index.js index.js.map typedocs coverage gh-pages yarn-error.log", - "build:tsc": "tsc -b tsconfig.build.json", - "bundle": "webpack --mode production", - "build": "yarn build:tsc && yarn bundle", - "typecheck": "tsc -b --noEmit" - }, - "dependencies": { - "mutxt-react": "workspace:*", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "slate": "^0.124.1", - "slate-dom": "^0.124.1", - "slate-history": "^0.113.1", - "slate-react": "^0.124.2" - }, - "devDependencies": { - "@types/react": "^19", - "@types/react-dom": "^19", - "css-loader": "^7.1.2", - "style-loader": "^4.0.0", - "terser-webpack-plugin": "^5.3.16", - "ts-loader": "^9.5.4", - "tslib": "^2.8.1", - "webpack": "^5.95.0", - "webpack-cli": "^5.1.4" - } -} diff --git a/packages/mutxt-element/src/MuTxtElement.stories.tsx b/packages/mutxt-element/src/MuTxtElement.stories.tsx deleted file mode 100644 index 9424fbb8f3..0000000000 --- a/packages/mutxt-element/src/MuTxtElement.stories.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import * as React from 'react'; -import preview from '../../../.storybook/preview'; -import './index'; -import type {MuTxtElement} from './index'; -import type {MuTxtApi, SlateEditorDocument} from 'mutxt-react'; -import {ModelWithExt, ext} from 'json-joy/lib/json-crdt-extensions'; -import {s} from 'json-joy/lib/json-crdt'; - -const Wrap: React.FC<{children: React.ReactNode; height?: number | string}> = ({children, height = 600}) => ( -
-
{children}
-
-); - -const meta = preview.meta({ - title: 'mutxt-element', -}); - -export const Default = meta.story({ - render: () => ( - - - - ), -}); - -export const ApiAccess = meta.story({ - render: () => { - const ref = React.useRef(null); - const [info, setInfo] = React.useState('Editor is mounting...'); - - React.useEffect(() => { - const el = ref.current; - if (!el) return; - let cancelled = false; - el.ready().then((api: MuTxtApi) => { - if (cancelled) return; - setInfo('Editor is ready. The MuTxtApi is on element.api.'); - // Demonstrate one API call. - api.focus(); - }); - return () => { - cancelled = true; - }; - }, []); - - return ( - -
{info}
- -
- ); - }, -}); - -export const ReadyEvent = meta.story({ - render: () => { - const ref = React.useRef(null); - const [log, setLog] = React.useState([]); - - React.useEffect(() => { - const el = ref.current; - if (!el) return; - const onReady = (e: Event) => { - const api = (e as CustomEvent).detail; - setLog((prev) => [...prev, `ready event fired (api focused=${api.focused()})`]); - }; - el.addEventListener('ready', onReady); - return () => el.removeEventListener('ready', onReady); - }, []); - - return ( - -
- {log.length === 0 ? 'Waiting for ready event...' : log.join('\n')} -
- -
- ); - }, -}); - -export const TextFromChildren = meta.story({ - render: () => ( - - {` - Plain text seeded from element children. - - Each newline becomes a paragraph. Indentation here is stripped by the dedent logic so the editor sees clean, left-aligned text regardless of where in the HTML you nest the element. - - Edit freely - the seed is applied only once, at mount. - `} - - ), -}); - -const slateSeed: SlateEditorDocument = [ - {type: 'h1', children: [{text: 'Seeded with '}, {text: 'format="slate"', code: true}]}, - { - type: 'p', - children: [{text: 'This editor was initialized from a JSON Slate document placed inside:'}], - }, - { - type: 'code-block', - language: 'html', - children: [{text: '...'}], - }, - { - type: 'p', - children: [ - {text: 'Inline marks work: '}, - {text: 'bold', bold: true}, - {text: ', '}, - {text: 'italic', italic: true}, - {text: ', '}, - {text: 'underline', underline: true}, - {text: ', '}, - {text: 'code', code: true}, - {text: ', and '}, - {text: 'links', a: {href: 'https://jsonjoy.com'}}, - {text: '.'}, - ], - }, -]; - -export const SlateFromChildren = meta.story({ - render: () => ( - - - {JSON.stringify(slateSeed)} - - - ), -}); - -export const MarkdownFromChildren = meta.story({ - render: () => ( - - {` - # Markdown seed - - This editor was seeded with markdown source. Right now the markdown - parser is a stub and the source is loaded as plain text. Once the - markdown to Slate transform is wired in, **bold**, *italics*, lists, - code fences, and so on will all render. - - - bullet one - - bullet two - - nested - `} - - ), -}); - -const buildNativeDataUrl = (): string => { - const model = ModelWithExt.create( - s.obj({ - '@type': s.con('mutxt'), - text: ext.peritext.new('Hello from a binary mu-txt document!'), - }), - ); - const bytes = model.toBinary(); - let bin = ''; - for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); - return `data:application/vnd.mutxt;base64,${btoa(bin)}`; -}; - -export const NativeFromDataUrl = meta.story({ - render: () => { - const url = React.useMemo(buildNativeDataUrl, []); - return ( - - - - ); - }, -}); - -export const NativeFromBlobUrl = meta.story({ - render: () => { - const url = React.useMemo(() => { - const model = ModelWithExt.create( - s.obj({ - '@type': s.con('mutxt'), - text: ext.peritext.new('Loaded via a blob: URL - same fetch pipeline.'), - }), - ); - return URL.createObjectURL(new Blob([model.toBinary() as BlobPart])); - }, []); - React.useEffect(() => () => URL.revokeObjectURL(url), [url]); - return ( - - - - ); - }, -}); - -export const TwoInstances = meta.story({ - render: () => ( -
-
- -
-
- -
-
- ), -}); diff --git a/packages/mutxt-element/src/PlainHtml.stories.tsx b/packages/mutxt-element/src/PlainHtml.stories.tsx deleted file mode 100644 index 60db0b0db8..0000000000 --- a/packages/mutxt-element/src/PlainHtml.stories.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import * as React from 'react'; -import preview from '../../../.storybook/preview'; -import './index'; -import {ModelWithExt, ext} from 'json-joy/lib/json-crdt-extensions'; -import {s} from 'json-joy/lib/json-crdt'; - -const Frame: React.FC<{html: string}> = ({html}) => ( -
element after this — the same path a CDN consumer takes. - // biome-ignore lint/security/noDangerouslySetInnerHtml: storybook-only fixture rendering trusted, hardcoded HTML to mirror the CDN consumer path - dangerouslySetInnerHTML={{__html: html}} - /> -); - -const meta = preview.meta({ - title: 'mutxt-element/Plain HTML', -}); - -export const Empty = meta.story({ - render: () => ( - - `} - /> - ), -}); - -export const Text = meta.story({ - render: () => ( - - Plain text seeded via static HTML. - - No React, no JSX - just an HTML custom element. - - Each newline becomes a paragraph. Indentation in the surrounding - HTML is stripped before parsing. - - `} - /> - ), -}); - -export const Markdown = meta.story({ - render: () => ( - - # Markdown seed - - Markdown source loaded from element children. The transform is a - stub for now, so the source is rendered as plain text. - - - bullet one - - bullet two - - nested - - `} - /> - ), -}); - -export const Slate = meta.story({ - render: () => { - const doc = JSON.stringify([ - {type: 'h1', children: [{text: 'Seeded via plain HTML'}]}, - { - type: 'p', - children: [{text: 'A JSON Slate document placed inside the element.'}], - }, - { - type: 'p', - children: [ - {text: 'Inline marks: '}, - {text: 'bold', bold: true}, - {text: ', '}, - {text: 'italic', italic: true}, - {text: ', '}, - {text: 'code', code: true}, - {text: '.'}, - ], - }, - ]); - return ( - ${doc} - `} - /> - ); - }, -}); - -const buildNativeDataUrl = (sample: string): string => { - const model = ModelWithExt.create( - s.obj({ - '@type': s.con('mutxt'), - text: ext.peritext.new(sample), - }), - ); - const bytes = model.toBinary(); - let bin = ''; - for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); - return `data:application/vnd.mutxt;base64,${btoa(bin)}`; -}; - -export const NativeFromDataUrl = meta.story({ - render: () => { - const url = React.useMemo(() => buildNativeDataUrl('Loaded from a data URL via plain HTML.'), []); - return ( - - `} - /> - ); - }, -}); - -export const TwoInstances = meta.story({ - render: () => ( - - First editor. - Second editor. -
- `} - /> - ), -}); diff --git a/packages/mutxt-element/src/constants.ts b/packages/mutxt-element/src/constants.ts deleted file mode 100644 index 79ce9e84f5..0000000000 --- a/packages/mutxt-element/src/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const FORMATS = ['native', 'text', 'markdown', 'slate'] as const; - -export const HOST_STYLE_ID = 'mu-txt-host-style'; diff --git a/packages/mutxt-element/src/index.ts b/packages/mutxt-element/src/index.ts deleted file mode 100644 index dee718df8e..0000000000 --- a/packages/mutxt-element/src/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -import * as React from 'react'; -import {createRoot, type Root} from 'react-dom/client'; -import {MuTxt, type MuTxtApi, type SlateEditorDocument} from 'mutxt-react'; -import {ensureHostStyle, loadFromSrc, resolveFormat, seedFromChildren} from './util'; -import type {MuTxtFormat, SeedProps} from './types'; - -export type {MuTxtFormat} from './types'; - -export class MuTxtElement extends HTMLElement { - private _root: Root | null = null; - private _mount: HTMLDivElement | null = null; - private _api: MuTxtApi | undefined; - private _readyResolvers: Array<(api: MuTxtApi) => void> = []; - private _fromSlate: SlateEditorDocument | undefined; - private _abortController: AbortController | null = null; - - /** Public editor API. Available after the element has mounted. */ - public get api(): MuTxtApi | undefined { - return this._api; - } - - /** - * Programmatic Slate seed. Equivalent to `{JSON}` - * but takes a JS array directly. Set BEFORE the element is connected to the - * DOM. Ignored when `src` is present. - */ - public get fromSlate(): SlateEditorDocument | undefined { - return this._fromSlate; - } - public set fromSlate(value: SlateEditorDocument | undefined) { - this._fromSlate = value; - } - - /** Resolves with the editor API once the editor has mounted. */ - public ready(): Promise { - if (this._api) return Promise.resolve(this._api); - return new Promise((resolve) => this._readyResolvers.push(resolve)); - } - - private mountReact(seed: SeedProps): void { - if (!this._mount) return; - const root = createRoot(this._mount); - this._root = root; - root.render( - React.createElement(MuTxt, { - heightFit: true, - fromSlate: seed.fromSlate, - obj: seed.obj, - onApi: (api: MuTxtApi) => { - this._api = api; - const resolvers = this._readyResolvers; - this._readyResolvers = []; - for (const resolve of resolvers) resolve(api); - this.dispatchEvent(new CustomEvent('ready', {detail: api})); - }, - }), - ); - } - - public connectedCallback(): void { - if (this._root) return; - // Defer: React 19 inserts custom elements into the DOM BEFORE appending - // their child text nodes, so reading `textContent` synchronously here - // would miss the seed. The microtask runs after React's commit phase - // completes - by then children are present. - queueMicrotask(() => this.init()); - } - - private init(): void { - if (this._root) return; - if (!this.isConnected) return; // disconnected before microtask ran - ensureHostStyle(); - // Capture seed inputs before mutating the DOM. - const childrenText = this.textContent ?? ''; - const hasChildren = !!childrenText.trim(); - const formatAttr = this.getAttribute('format'); - const srcAttr = this.getAttribute('src'); - const format = resolveFormat(formatAttr, srcAttr, hasChildren, !!this._fromSlate); - // Clear seed text/JSON children so they don't flash before React mounts. - while (this.firstChild) this.removeChild(this.firstChild); - const mount = document.createElement('div'); - mount.style.cssText = 'display:block;width:100%;height:100%;min-height:0;'; - this.appendChild(mount); - this._mount = mount; - if (srcAttr && format) { - const controller = new AbortController(); - this._abortController = controller; - loadFromSrc(srcAttr, format, controller.signal) - .then((seed) => this.mountReact(seed)) - .catch((err) => { - if (err && err.name === 'AbortError') return; - // eslint-disable-next-line no-console - console.error('[mu-txt] failed to load src:', err); - this.mountReact({}); - }); - } else { - this.mountReact(seedFromChildren(format, childrenText, this._fromSlate)); - } - } - - public disconnectedCallback(): void { - this._abortController?.abort(); - this._abortController = null; - const root = this._root; - const mount = this._mount; - this._root = null; - this._mount = null; - this._api = undefined; - // Defer unmount: when the host element is removed from the DOM mid-render - // (e.g. storybook story switch), calling root.unmount() synchronously - // collides with the in-flight render. Microtask-defer to let the current - // render commit first. - if (root) { - queueMicrotask(() => { - try { - root.unmount(); - } catch {} - }); - } - if (mount && mount.parentNode === this) this.removeChild(mount); - } -} - -if (typeof customElements !== 'undefined' && !customElements.get('mu-txt')) { - customElements.define('mu-txt', MuTxtElement); -} - -export default MuTxtElement; - -declare module 'react' { - namespace JSX { - interface IntrinsicElements { - 'mu-txt': React.DetailedHTMLProps, MuTxtElement> & { - format?: MuTxtFormat; - src?: string; - }; - } - } -} diff --git a/packages/mutxt-element/src/types.ts b/packages/mutxt-element/src/types.ts deleted file mode 100644 index 275bbf3970..0000000000 --- a/packages/mutxt-element/src/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {SlateEditorDocument} from 'mutxt-react'; -import type {ObjApi, ObjNode} from 'json-joy/lib/json-crdt'; -import type {FORMATS} from './constants'; - -export type MuTxtFormat = (typeof FORMATS)[number]; - -/** Resolved seed payload passed to `` at mount time. */ -export interface SeedProps { - fromSlate?: SlateEditorDocument; - obj?: ObjApi; -} diff --git a/packages/mutxt-element/src/util.ts b/packages/mutxt-element/src/util.ts deleted file mode 100644 index b78876d32d..0000000000 --- a/packages/mutxt-element/src/util.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {ModelWithExt} from 'json-joy/lib/json-crdt-extensions'; -import {FORMATS, HOST_STYLE_ID} from './constants'; -import type {SlateEditorDocument} from 'mutxt-react'; -import type {ObjApi, ObjNode} from 'json-joy/lib/json-crdt'; -import type {MuTxtFormat, SeedProps} from './types'; - -/** Inject the host stylesheet exactly once per document. */ -export const ensureHostStyle = (): void => { - if (typeof document === 'undefined') return; - if (document.getElementById(HOST_STYLE_ID)) return; - const style = document.createElement('style'); - style.id = HOST_STYLE_ID; - style.textContent = 'mu-txt{display:block;box-sizing:border-box;width:100%;height:100%;min-height:320px;}'; - document.head.appendChild(style); -}; - -/** - * Strip the common leading whitespace shared by every non-empty line. Lets - * authors indent inline content along with surrounding HTML without the - * indent leaking into the parsed source. - */ -const dedent = (raw: string): string => { - const trimmed = raw.replace(/^[ \t]*\r?\n|\r?\n[ \t]*$/g, ''); - const lines = trimmed.split(/\r?\n/); - let min = Infinity; - for (const line of lines) { - if (!line.trim()) continue; - const m = line.match(/^[ \t]*/); - if (m) min = Math.min(min, m[0].length); - } - if (!Number.isFinite(min) || min === 0) return lines.join('\n'); - return lines.map((l) => l.slice(min)).join('\n'); -}; - -const textToSlate = (text: string): SlateEditorDocument => - text.split(/\r?\n/).map((line) => ({type: 'p', children: [{text: line}]})) as SlateEditorDocument; - -const parseSlateJson = (text: string): SlateEditorDocument | undefined => { - try { - const parsed = JSON.parse(text); - if (Array.isArray(parsed)) return parsed as SlateEditorDocument; - } catch {} - return undefined; -}; - -// TODO: replace with a real markdown -const markdownToSlate = (text: string): SlateEditorDocument => textToSlate(text); - -const isFormat = (v: string | null): v is MuTxtFormat => v !== null && (FORMATS as readonly string[]).includes(v); - -/** Decide which seed format to use based on attributes and presence of inputs. */ -export const resolveFormat = ( - formatAttr: string | null, - srcAttr: string | null, - hasChildren: boolean, - hasFromSlateProp: boolean, -): MuTxtFormat | null => { - if (isFormat(formatAttr)) return formatAttr; - if (srcAttr) return 'native'; - if (hasFromSlateProp) return 'slate'; - if (hasChildren) return 'text'; - return null; -}; - -const seedFromText = (format: MuTxtFormat, text: string): SeedProps => { - switch (format) { - case 'slate': { - const doc = parseSlateJson(text); - return doc ? {fromSlate: doc} : {}; - } - case 'markdown': - return {fromSlate: markdownToSlate(text)}; - case 'text': - return {fromSlate: textToSlate(text)}; - case 'native': - // `native` is binary; no meaningful interpretation of text source. - return {}; - } -}; - -/** Synchronous seed resolution from the element's children + JS property. */ -export const seedFromChildren = ( - format: MuTxtFormat | null, - raw: string, - fromSlateProp?: SlateEditorDocument, -): SeedProps => { - if (!format) return fromSlateProp ? {fromSlate: fromSlateProp} : {}; - if (format === 'native') return {}; // native requires `src` - if (format === 'slate' && fromSlateProp) return {fromSlate: fromSlateProp}; - return seedFromText(format, dedent(raw)); -}; - -/** Async seed resolution from a URL or data URL via `fetch()`. */ -export const loadFromSrc = async (src: string, format: MuTxtFormat, signal?: AbortSignal): Promise => { - const response = await fetch(src, {signal}); - if (format === 'native') { - const buffer = await response.arrayBuffer(); - const model = ModelWithExt.load(new Uint8Array(buffer)); - return {obj: model.api.obj([]) as ObjApi}; - } - const text = await response.text(); - return seedFromText(format, text); -}; diff --git a/packages/mutxt-element/tsconfig.build.json b/packages/mutxt-element/tsconfig.build.json deleted file mode 100644 index f3994e3cf9..0000000000 --- a/packages/mutxt-element/tsconfig.build.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "sourceMap": false, - "rootDir": "./src", - "tsBuildInfoFile": "./lib/.tsbuildinfo" - }, - "exclude": [ - "src/demo", - "src/__tests__", - "src/**/__demos__/**/*.*", - "src/**/__tests__/**/*.*", - "src/**/__bench__/**/*.*", - "src/**/__mocks__/**/*.*", - "src/**/__jest__/**/*.*", - "src/**/__mocha__/**/*.*", - "src/**/__tap__/**/*.*", - "src/**/__tape__/**/*.*", - "*.test.ts", - "*.spec.ts", - "src/**/__stories__/**/*.*", - "src/**/*.stories.ts", - "src/**/*.stories.tsx" - ], - "references": [ - {"path": "../mutxt-react/tsconfig.build.json"} - ] -} diff --git a/packages/mutxt-element/tsconfig.json b/packages/mutxt-element/tsconfig.json deleted file mode 100644 index 6cfdb0834d..0000000000 --- a/packages/mutxt-element/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib" - }, - "include": [ - "src" - ], - "exclude": [ - "src/demo", - "src/__tests__", - "src/**/__demos__/**/*.*", - "src/**/__tests__/**/*.*", - "src/**/__bench__/**/*.*", - "src/**/__mocks__/**/*.*", - "src/**/__jest__/**/*.*", - "src/**/__mocha__/**/*.*", - "src/**/__tap__/**/*.*", - "src/**/__tape__/**/*.*", - "*.test.ts", - "*.spec.ts", - "src/**/*.stories.ts", - "src/**/*.stories.tsx" - ], - "references": [ - {"path": "../mutxt-react"} - ] -} diff --git a/packages/mutxt-element/webpack.config.ts b/packages/mutxt-element/webpack.config.ts deleted file mode 100644 index 05a2c0163c..0000000000 --- a/packages/mutxt-element/webpack.config.ts +++ /dev/null @@ -1,71 +0,0 @@ -import path from 'path'; -import webpack, {type Configuration} from 'webpack'; -import TerserPlugin from 'terser-webpack-plugin'; - -const config: Configuration = { - mode: 'production', - entry: './src/index.ts', - target: ['web', 'es2020'], - devtool: 'source-map', - output: { - path: path.resolve(__dirname), - filename: 'index.js', - globalObject: 'self', - library: { - type: 'window', - name: 'muTxtElement', - }, - }, - resolve: { - extensions: ['.tsx', '.ts', '.mjs', '.js', '.jsx'], - }, - optimization: { - splitChunks: false, - runtimeChunk: false, - minimizer: [ - new TerserPlugin({ - extractComments: false, - terserOptions: { - format: {comments: false}, - }, - }), - ], - }, - performance: { - hints: false, - }, - plugins: [ - // Collapse all dynamic `import()` chunks (e.g. React.lazy) into the main bundle - // so consumers can load the editor with a single `; - -const REACT_SNIPPET = `import {MuTxt} from 'mutxt-react'; - -export const App = () => - ;`; - -interface SectionProps { - icon: React.ReactNode; - title: string; - intro: React.ReactNode; - snippet: string; - lang: string; - links: {label: string; href: string; icon?: React.ReactNode}[]; -} - -const Section: React.FC = ({icon, title, intro, snippet, lang, links}) => { - const styles = useStyles(); - return ( -
-
- {icon} - - {title} - -
- - -
- {intro} -
- -
- {links.map((link) => ( - - {link.icon} - {link.label} - - ))} -
-
- ); -}; - -const ExternalIcon: React.FC = () => ( - -); - -export const EmbedDocsModal: React.FC = () => { - const mutxt = useMuTxt(); - const open = mutxt.embedDocsOpen.use(); - const close = React.useCallback(() => mutxt.embedDocsOpen.set(false), [mutxt]); - - return ( - -
-
-
} - title="HTML — custom element" - intro={ - <> - A drop-in web component. Works in any HTML page — no build step required. Children seed the document; - children content can be plain text, Markdown, or Slate JSON via the format attribute. - - } - snippet={ELEMENT_SNIPPET} - lang="html" - links={[ - { - label: 'mutxt-element on npm', - href: 'https://www.npmjs.com/package/mutxt-element', - icon: , - }, - { - label: 'README & docs', - href: 'https://github.com/streamich/json-joy/tree/master/packages/mutxt-element', - icon: , - }, - ]} - /> -
} - title="React — component" - intro={ - <> - Batteries-included React component for tighter integration — seed from Slate JSON, hand in a - Peritext-backed json-joy model for collaboration, and wire up your own presence manager. - - } - snippet={REACT_SNIPPET} - lang="jsx" - links={[ - { - label: 'mutxt-react on npm', - href: 'https://www.npmjs.com/package/mutxt-react', - icon: , - }, - { - label: 'README & docs', - href: 'https://github.com/streamich/json-joy/tree/master/packages/mutxt-react', - icon: , - }, - ]} - /> -
-
-
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/chrome/KeyboardShortcuts/data.ts b/packages/mutxt-react/src/MuTxt/chrome/KeyboardShortcuts/data.ts deleted file mode 100644 index 028597abb3..0000000000 --- a/packages/mutxt-react/src/MuTxt/chrome/KeyboardShortcuts/data.ts +++ /dev/null @@ -1,102 +0,0 @@ -import {formatKeys} from '../../util/keys'; - -export interface ShortcutSpec { - /** Keys passed to `formatKeys` for platform-aware rendering. */ - keys?: string[]; - /** Override the rendered key string (used for chords like "Shift Shift"). */ - display?: string; - /** Short, user-facing description. */ - label: string; -} - -export interface ShortcutGroup { - title: string; - shortcuts: ShortcutSpec[]; -} - -export const renderShortcut = (shortcut: ShortcutSpec): string => - shortcut.display ?? (shortcut.keys ? formatKeys(shortcut.keys) : ''); - -export const SHORTCUT_GROUPS: ShortcutGroup[] = [ - { - title: 'General', - shortcuts: [ - {keys: ['Primary', '/'], label: 'Show keyboard shortcuts'}, - {display: 'Shift Shift', label: 'Quick actions at caret'}, - {keys: ['Primary', 'j'], label: 'Command palette'}, - {keys: ['Space'], label: 'Open menu in empty block or with selection'}, - {keys: ['Primary', 'Shift', 'm'], label: 'Toggle maximized view'}, - {keys: ['Primary', 'Shift', 'f'], label: 'Toggle fullscreen'}, - ], - }, - { - title: 'Inline formatting', - shortcuts: [ - {keys: ['Primary', 'b'], label: 'Bold'}, - {keys: ['Primary', 'i'], label: 'Italic'}, - {keys: ['Primary', 'u'], label: 'Underline'}, - {keys: ['Primary', 'e'], label: 'Inline code'}, - {keys: ['Primary', 'Shift', 'x'], label: 'Strikethrough'}, - {keys: ['Primary', 'k'], label: 'Insert / edit link'}, - ], - }, - { - title: 'Turn into', - shortcuts: [ - {keys: ['Primary', 'Alt', '0'], label: 'Paragraph'}, - {keys: ['Primary', 'Alt', '1'], label: 'Heading 1'}, - {keys: ['Primary', 'Alt', '2'], label: 'Heading 2'}, - {keys: ['Primary', 'Alt', '3'], label: 'Heading 3'}, - {keys: ['Primary', 'Alt', '4'], label: 'Heading 4'}, - {keys: ['Primary', 'Alt', '5'], label: 'Heading 5'}, - {keys: ['Primary', 'Alt', '6'], label: 'Heading 6'}, - {keys: ['Primary', 'Alt', '7'], label: 'Numbered list'}, - {keys: ['Primary', 'Alt', '8'], label: 'Bulleted list'}, - {keys: ['Primary', 'Shift', 'q'], label: 'Blockquote'}, - {keys: ['Primary', 'Shift', 'c'], label: 'Code block'}, - ], - }, - { - title: 'Block operations', - shortcuts: [ - {keys: ['Alt', 'Shift', '↑'], label: 'Move block up'}, - {keys: ['Alt', 'Shift', '↓'], label: 'Move block down'}, - {keys: ['Primary', 'Shift', 'd'], label: 'Duplicate block'}, - {keys: ['Primary', 'Shift', 'k'], label: 'Delete block'}, - ], - }, - { - title: 'Alignment & indent', - shortcuts: [ - {keys: ['Primary', 'Shift', 'l'], label: 'Align left'}, - {keys: ['Primary', 'Shift', 'e'], label: 'Align center'}, - {keys: ['Primary', 'Shift', 'r'], label: 'Align right'}, - {keys: ['Primary', 'Shift', 'j'], label: 'Justify'}, - {keys: ['Primary', ']'], label: 'Increase indent'}, - {keys: ['Primary', '['], label: 'Decrease indent'}, - ], - }, - { - title: 'Code blocks', - shortcuts: [ - {keys: ['Primary', 'Enter'], label: 'Insert hard break'}, - {keys: ['Shift', 'Enter'], label: 'Exit code block'}, - {display: 'Enter x3', label: 'Exit on a blank line at the end'}, - ], - }, - { - title: 'Editing', - shortcuts: [ - {keys: ['Backspace'], label: 'Convert empty / start-of-line block to paragraph'}, - {keys: ['Primary', 'z'], label: 'Undo'}, - {keys: ['Primary', 'Shift', 'z'], label: 'Redo'}, - ], - }, - { - title: 'Translit', - shortcuts: [ - {keys: ['Primary', 'Alt', 't'], label: 'Toggle translit mode'}, - {keys: ['Primary', 'Alt', 'Shift', 't'], label: 'Show translit phonetic map'}, - ], - }, -]; diff --git a/packages/mutxt-react/src/MuTxt/chrome/KeyboardShortcuts/index.tsx b/packages/mutxt-react/src/MuTxt/chrome/KeyboardShortcuts/index.tsx deleted file mode 100644 index aa9362a20f..0000000000 --- a/packages/mutxt-react/src/MuTxt/chrome/KeyboardShortcuts/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {Key} from '@jsonjoy.com/ui/lib/1-inline/Key'; -import {MiniTitle} from '@jsonjoy.com/ui/lib/3-list-item/MiniTitle'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {ModalOverlay} from '../ModalOverlay'; -import {useMuTxt} from '../../context'; -import {remap} from '../../util/keys'; -import {SHORTCUT_GROUPS, type ShortcutSpec} from './data'; -import {Separator} from '@jsonjoy.com/ui/lib/3-list-item/Separator'; -import {Space} from '@jsonjoy.com/ui/lib/3-list-item/Space'; - -const wrapperClass = rule({ - bxz: 'border-box', - w: '100%', - maxW: '1444px', - mr: '0 auto', - pd: '40px 48px 56px', -}); - -const gridClass = rule({ - columnWidth: '280px', - columnGap: '56px', -}); - -const groupClass = rule({ - d: 'flex', - fld: 'column', - gap: '0', - miw: 0, - mar: '0 0 48px', - breakInside: 'avoid', - WebkitColumnBreakInside: 'avoid', - pageBreakInside: 'avoid', -}); - -const groupTitleClass = rule({ - mar: '0 0 10px', -}); - -const rowClass = rule({ - d: 'flex', - ai: 'center', - jc: 'space-between', - gap: '12px', - pd: '4px 0', - fz: '13.5px', - lh: '1.25', -}); - -const labelClass = rule({ - fls: '1 1 auto', - miw: 0, - ov: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', -}); - -const keysClass = rule({ - fls: '0 0 auto', - d: 'inline-flex', - ai: 'center', - gap: '2px', -}); - -const renderKey = (raw: string): string => { - const mapped = remap[raw] ?? raw; - return mapped.length === 1 ? mapped.toUpperCase() : mapped; -}; - -const ShortcutKeys: React.FC<{spec: ShortcutSpec}> = ({spec}) => { - const styles = useStyles(); - if (spec.display) - return ( - - {spec.display} - - ); - const keys = spec.keys ?? []; - const plusStyle: React.CSSProperties = {color: styles.g(0.5, 0.45)}; - return ( - - {keys.map((k, i) => ( - - {i > 0 && +} - {renderKey(k)} - - ))} - - ); -}; - -const ShortcutRow: React.FC<{spec: ShortcutSpec}> = ({spec}) => { - const styles = useStyles(); - return ( -
- - {spec.label} - - -
- ); -}; - -export const KeyboardShortcutsModal: React.FC = () => { - const mutxt = useMuTxt(); - const open = mutxt.shortcutsOpen.use(); - const close = React.useCallback(() => mutxt.shortcutsOpen.set(false), [mutxt]); - - return ( - -
-
- {SHORTCUT_GROUPS.map((group) => ( -
-
- - {group.title} - -
- - - {group.shortcuts.map((spec) => ( - - ))} -
- ))} -
-
-
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/chrome/ModalOverlay/index.tsx b/packages/mutxt-react/src/MuTxt/chrome/ModalOverlay/index.tsx deleted file mode 100644 index 3baa55f23b..0000000000 --- a/packages/mutxt-react/src/MuTxt/chrome/ModalOverlay/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import * as React from 'react'; -import {rule, useTheme} from 'nano-theme'; -import {BasicButtonClose} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonClose'; -import * as ScrollArea from '@jsonjoy.com/ui/lib/4-card/ScrollArea'; -import {ScrollState} from '@jsonjoy.com/ui/lib/4-card/ScrollArea'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; - -const overlayClass = rule({ - pos: 'absolute', - top: '0', - l: '0', - r: '0', - b: '0', - z: 50, - d: 'flex', - fld: 'column', - animation: 'fadeIn .12s', -}); - -const HEADER_HEIGHT = 48; - -const headerClass = rule({ - pos: 'relative', - d: 'flex', - ai: 'center', - jc: 'flex-end', - h: HEADER_HEIGHT + 'px', - paddingBlock: 0, - paddingInlineStart: '12px', - paddingInlineEnd: '12px', - bxz: 'border-box', - '-webkit-app-region': 'drag', // Drag for Electron app. - '& button, & a, & input, & textarea, & select, & [role="button"], & [role="tablist"], & [role="img"]': { - '-webkit-app-region': 'no-drag', - }, -}); - -const titleClass = rule({ - pos: 'absolute', - l: '50%', - tr: 'translateX(-50%)', - fz: '14px', - fw: '600', - letterSpacing: '0.005em', - ws: 'nowrap', - pointerEvents: 'none', -}); - -export interface ModalOverlayProps { - open: boolean; - title?: React.ReactNode; - onClose: () => void; - children?: React.ReactNode; -} - -/** - * A modal that overlays the editor area. - */ -export const ModalOverlay: React.FC = ({open, title, onClose, children}) => { - const styles = useStyles(); - const theme = useTheme(); - const onCloseRef = React.useRef(onClose); - React.useEffect(() => { - onCloseRef.current = onClose; - }, [onClose]); - const scrollState = React.useMemo(() => new ScrollState(), []); - - React.useEffect(() => { - if (!open) return; - const onKey = (event: KeyboardEvent) => { - if (event.key !== 'Escape') return; - event.preventDefault(); - event.stopPropagation(); - onCloseRef.current(); - }; - window.addEventListener('keydown', onKey, true); - return () => window.removeEventListener('keydown', onKey, true); - }, [open]); - - // When the modal opens, programmatically focus the scrollable viewport so - // the user can scroll with arrow keys / Space / PgUp / PgDn immediately. - React.useEffect(() => { - if (!open) return; - const previouslyFocused = document.activeElement as HTMLElement | null; - const id = requestAnimationFrame(() => { - scrollState.viewportEl?.focus({preventScroll: true}); - }); - return () => { - cancelAnimationFrame(id); - if (previouslyFocused && typeof previouslyFocused.focus === 'function') { - try { - previouslyFocused.focus({preventScroll: true}); - } catch {} - } - }; - }, [open, scrollState]); - - if (!open) return null; - - const overlayBg = theme.bg; - const borderColor = styles.g(0, 0.08); - - return ( -
- - -
- - {title} - - -
-
- - {children} - - - - -
-
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/chrome/ShareDocumentPane.tsx b/packages/mutxt-react/src/MuTxt/chrome/ShareDocumentPane.tsx deleted file mode 100644 index 40662bc443..0000000000 --- a/packages/mutxt-react/src/MuTxt/chrome/ShareDocumentPane.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {BasicButton} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton'; -import {Checkbox} from '@jsonjoy.com/ui/lib/2-inline-block/Checkbox'; -import {Input} from '@jsonjoy.com/ui/lib/2-inline-block/Input'; -import {TextBlock} from '@jsonjoy.com/ui/lib/5-block/TextBlock'; -import {FormRow} from '@jsonjoy.com/ui/lib/3-list-item/FormRow'; -import {Separator} from '@jsonjoy.com/ui/lib/3-list-item/Separator'; -import {Iconista} from '@jsonjoy.com/ui/lib/icons/Iconista'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {useMuTxt} from '../context'; -import {encodeShareBlob, encodeEncryptedShareBlob} from '../util/shareEncoding'; -import {buildShareUrl, writeClipboard} from '../util/share'; -import type {MuTxtState} from '../state/MuTxtState'; -import {useT} from 'use-t'; -import {Split} from '@jsonjoy.com/ui/lib/3-list-item/Split'; -import {Space} from '@jsonjoy.com/ui/lib/3-list-item/Space'; -import LockIcon__svg from 'iconista/lib/react/tabler/lock'; - -const LockIcon: React.FC> = (props) => ( - -); - -const stopKeyDown = (e: React.KeyboardEvent): void => { - e.stopPropagation(); -}; - -// Container that hard-caps the popup width so a long URL inside the link -// preview can never push the popup wider mid-session. -const formClass = rule({ - d: 'flex', - fld: 'column', - // gap: '12px', - w: '100%', - pd: '4px 16px 8px', - bxz: 'border-box', - // Critical: in a flex parent the auto min-width is min-content; without - // this, a long single-line URL would force the column to grow. - minW: 0, -}); - -const passwordRowClass = rule({ - d: 'flex', - fld: 'column', - gap: '6px', -}); - -const socialRowClass = rule({ - d: 'flex', - fld: 'row', - gap: '8px', - flexWrap: 'wrap', -}); - -const sectionDescriptionClass = rule({ - fz: '12px', - lh: 1.45, - mr: '0 0 10px', -}); - -const errorClass = rule({ - fz: '12px', - lh: 1.4, - pdt: '4px', -}); - -const messageInputClass = rule({ - '& textarea': { - resize: 'none', - }, -}); - -interface SocialTarget { - id: 'x' | 'facebook' | 'linkedin' | 'reddit'; - label: string; - icon: 'brand-x' | 'brand-facebook' | 'brand-linkedin' | 'brand-reddit'; - href: (url: string, text: string) => string; -} - -const SOCIAL_TARGETS: readonly SocialTarget[] = [ - { - id: 'x', - label: 'X', - icon: 'brand-x', - href: (url, text) => - `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`, - }, - { - id: 'facebook', - label: 'Facebook', - icon: 'brand-facebook', - href: (url) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, - }, - { - id: 'linkedin', - label: 'LinkedIn', - icon: 'brand-linkedin', - href: (url) => `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`, - }, - { - id: 'reddit', - label: 'Reddit', - icon: 'brand-reddit', - href: (url, text) => - `https://www.reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(text)}`, - }, -]; - -const formatBytes = (n: number): string => { - if (n < 1024) return `${n} B`; - if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; - return `${(n / 1024 / 1024).toFixed(1)} MB`; -}; - -const HTML_ESCAPES: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', -}; -const escapeHtml = (s: string): string => s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]); - -const DEFAULT_TITLE = 'Shared MuTxt document'; - -const getDefaultTitle = (mutxt: MuTxtState): string => { - try { - const outline = mutxt.outline(); - const first = outline.find((o) => o.title.trim()); - if (first) return first.title.trim(); - } catch {} - return DEFAULT_TITLE; -}; - -export const ShareDocumentForm: React.FC = () => { - const [t] = useT(); - const mutxt = useMuTxt(); - const styles = useStyles(); - const [title, setTitle] = React.useState(() => getDefaultTitle(mutxt)); - const [message, setMessage] = React.useState(''); - const [encrypt, setEncrypt] = React.useState(false); - const [password, setPassword] = React.useState(''); - const [shareUrl, setShareUrl] = React.useState(''); - const [encoding, setEncoding] = React.useState(false); - const [error, setError] = React.useState(null); - const [htmlCopied, setHtmlCopied] = React.useState(false); - - const bytesRef = React.useRef(null); - if (!bytesRef.current) bytesRef.current = mutxt.api.toBinary(); - const bytes = bytesRef.current; - const effectiveTitle = title.trim() || DEFAULT_TITLE; - const recomputeRef = React.useRef(0); - - const recompute = React.useCallback(async () => { - const token = ++recomputeRef.current; - setEncoding(true); - setError(null); - try { - let encoded: string; - if (encrypt) { - if (!password) { - if (recomputeRef.current === token) { - setShareUrl(''); - setEncoding(false); - } - return; - } - encoded = await encodeEncryptedShareBlob(bytes, password); - } else { - encoded = await encodeShareBlob(bytes); - } - if (recomputeRef.current !== token) return; - setShareUrl(buildShareUrl(encoded, encrypt, effectiveTitle, message)); - } catch (err) { - if (recomputeRef.current !== token) return; - setError(err instanceof Error ? err.message : 'Failed to encode the document.'); - setShareUrl(''); - } finally { - if (recomputeRef.current === token) setEncoding(false); - } - }, [bytes, encrypt, password, effectiveTitle, message]); - - React.useEffect(() => { - void recompute(); - }, [recompute]); - - const openSocial = (target: SocialTarget) => { - if (!shareUrl) return; - const text = message.trim() || effectiveTitle; - const href = target.href(shareUrl, text); - window.open(href, '_blank', 'noopener,noreferrer'); - }; - - const onCopyHtmlLink = async (): Promise => { - if (!shareUrl) return; - const html = `${escapeHtml(effectiveTitle)}`; - try { - await writeClipboard(html); - setHtmlCopied(true); - setTimeout(() => setHtmlCopied(false), 2000); - mutxt.toasts?.add({title: 'HTML link copied to clipboard.', duration: 3000}); - } catch (err) { - // eslint-disable-next-line no-console - console.error('[mutxt] copy html link failed', err); - mutxt.toasts?.add({type: 'error', title: 'Failed to copy HTML link.', duration: 5000}); - } - }; - - const errorColor = styles.col.get('error', 'solid-1'); - - return ( -
- - - -
- -
-
- - - - {t('Encrypt')} - - } - descriptionAbove - description={t('Encrypts with AES-GCM 256 (PBKDF2).')} - right - > -
- setEncrypt(!encrypt)} /> -
-
- {encrypt && ( -
- -
- )} - - {!!shareUrl && ( - <> - - - - - )} - - {!!shareUrl && ( - -
{t('Share link')}
-
- {encoding - ? t('Encoding…') - : shareUrl - ? `${formatBytes(shareUrl.length)} · ${encrypt ? t('Encrypted') : t('Plaintext')}` - : null} -
- - } - > - {error ? ( -
- {error} -
- ) : ( -
- -
- )} -
- )} - - {shareUrl && ( - -
- {SOCIAL_TARGETS.map((t) => ( - openSocial(t)} - > - - - {/* {t.label} */} - - - ))} - - - - - {htmlCopied ? t('Copied!') : t('Copy HTML')} - - -
-
- )} -
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/chrome/footer/MuTxtFooter.tsx b/packages/mutxt-react/src/MuTxt/chrome/footer/MuTxtFooter.tsx deleted file mode 100644 index 0f00969f24..0000000000 --- a/packages/mutxt-react/src/MuTxt/chrome/footer/MuTxtFooter.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {Label} from '@jsonjoy.com/ui/lib/1-inline/Label'; -import {Favicon} from '@jsonjoy.com/ui/lib/1-inline/Favicon'; -import {CopyCode} from '@jsonjoy.com/ui/lib/1-inline/CopyCode'; -import {CopyButton} from '@jsonjoy.com/ui/lib/2-inline-block/CopyButton'; -import {Breadcrumb, Breadcrumbs} from '@jsonjoy.com/ui/lib/3-list-item/Breadcrumbs'; -import {useMuTxt} from '../../context'; -import {getWordCount, pluralize} from '../../util'; -import {typeToLabel} from '../../util/typeToLabel'; -import {TranslitFooterPill} from '../../translit/TranslitFooterPill'; -import type {MathThing} from '../../types'; - -const footerClass = rule({ - d: 'flex', - jc: 'space-between', - ai: 'center', - fw: 'wrap', - gap: '12px', - pd: '0 18px', - h: '48px', - fz: '12px', -}); - -const footerGroupClass = rule({ - d: 'inline-flex', - ai: 'center', - gap: '12px', - fw: 'wrap', -}); - -const statusPathClass = rule({ - d: 'inline-flex', - ai: 'center', - gap: '12px', - fw: 'wrap', -}); - -const pathLinkClass = rule({ - d: 'inline-block', - maxW: '220px', - ws: 'nowrap', - ov: 'hidden', - textOverflow: 'ellipsis', - verticalAlign: 'bottom', - textDecoration: 'underline', - textDecorationThickness: '1px', - textUnderlineOffset: '4px', - textDecorationColor: 'rgb(from currentColor r g b / 0.2)', - '&:hover': { - textDecorationColor: 'currentColor', - }, -}); - -export type MuTxtFooterProps = {}; - -export const MuTxtFooter: React.FC = () => { - const styles = useStyles(); - const state = useMuTxt(); - const footerRef = React.useRef(null); - const availableWidth = state.sizer.width.use(); - const desiredWidth = state.sizer.content.use(); - const focused = state.focused.use(); - const linkOpen = state.inline.link.open.use(); - const readOnly = state.readOnly.use(); - const wordCount = state.wordCount.use(); - const characterCount = state.characterCount.use(); - const selectionText = state.selectionText.use(); - const caretPath = state.caretPath.use(); - const caretLinkHref = state.caretLinkHref.use(); - const caretEmbedUrl = state.caretEmbedUrl.use(); - const caretCodeText = state.caretCodeText.use(); - const caretMathThingId = state.caretMathThingId.use(); - state.things.version.use(); - const caretMathTex = caretMathThingId - ? ((state.things.get(caretMathThingId) as MathThing | undefined)?.val ?? '') - : ''; - - const width = Math.min(availableWidth, desiredWidth); - - const infoColor = styles.g(0.34); - const selectionSummary = selectionText - ? `${pluralize(getWordCount(selectionText), 'word')} (${pluralize(selectionText.length, 'char')}) selected` - : ''; - const statusText = readOnly ? 'Read-only' : focused ? 'Editing' : ''; - const footerUrl = caretEmbedUrl || caretLinkHref; - - const showCaretPath = width > 700; - const showCaretCode = width > 900; - const showCaretUrl = width > 1100; - const showCharacterCount = width > 500; - const showSelectionSummary = width > 600; - - const preventBreadcrumbMouseDown = (event: React.MouseEvent): void => { - event.preventDefault(); - }; - - const handleLinkBreadcrumbClick = (event: React.MouseEvent): void => { - event.preventDefault(); - const shell = state.shellEl; - const footer = footerRef.current; - if (!shell || !footer) return; - const shellRect = shell.getBoundingClientRect(); - const footerRect = footer.getBoundingClientRect(); - const popupWidth = Math.max(Math.min(560, window.innerWidth * 0.4), 320); - // Offset by LinkFloater's internal GAP (8px) so the popup sits flush against the footer. - const rect = new DOMRect(shellRect.left, footerRect.top + 8, popupWidth, 0); - state.inline.link.setAnchorRect(rect); - state.inline.dismissed.next(true); - state.inline.link.toggle(); - }; - - return ( -
-
- {!!statusText && } - {!readOnly && (focused || linkOpen) && !!caretPath && showCaretPath && ( - - ( - - {typeToLabel(segment) || segment} - - ))} - /> - {!!footerUrl && showCaretUrl && ( - <> - {'→'} - - { - event.preventDefault(); - }} - > - {footerUrl} - - event.preventDefault()} - onCopy={() => footerUrl} - tooltip={{nowrap: true, renderTooltip: () => 'Copy link'}} - /> - - )} - {!!caretCodeText && showCaretCode && ( - <> - {'→'} - - - )} - {!!caretMathTex && showCaretCode && ( - <> - {'→'} - - - )} - - )} -
- -
- - {!!selectionSummary && showSelectionSummary && ( - <> - {selectionSummary} - {'•'} - - )} - {pluralize(wordCount, 'word')} - {showCharacterCount && ( - <> - {'•'} - {pluralize(characterCount, 'character')} - - )} -
-
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/chrome/header/MuTxtHeader.tsx b/packages/mutxt-react/src/MuTxt/chrome/header/MuTxtHeader.tsx deleted file mode 100644 index ec566ac45b..0000000000 --- a/packages/mutxt-react/src/MuTxt/chrome/header/MuTxtHeader.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {ToolbarMenu} from '@jsonjoy.com/ui/lib/4-card/Toolbar/ToolbarMenu'; -import {AutoExpandableToolbar} from '@jsonjoy.com/ui/lib/4-card/Toolbar/ToolbarMenu/AutoExpandableToolbar'; -import {ToolbarSep} from '@jsonjoy.com/ui/lib/4-card/Toolbar/ToolbarSep'; -import {Split} from '@jsonjoy.com/ui/lib/3-list-item/Split'; -import {DocumentOutlineButton} from '../../chrome/DocumentOutlineButton'; -import {useMuTxt} from '../../context'; -import {SetNamedTrace} from '@jsonjoy.com/ui'; -import type {Editor} from 'slate'; - -const HEIGHT = 48; - -const blockClass = rule({ - pos: 'relative', - pd: '0 32px', - ai: 'center', - h: HEIGHT + 'px', - bxz: 'border-box', - - // Electron window dragging. - '-webkit-app-region': 'drag', - '& button, & a, & input, & textarea, & select, & [role="button"], & [role="tablist"], & [role="img"]': { - '-webkit-app-region': 'no-drag', - }, -}); - -const toolbarContainerClass = rule({ - d: 'flex', - fld: 'row', - ai: 'center', - h: HEIGHT + 'px', -}); - -export interface MuTxtHeaderProps { - editor: Editor; -} - -const readTitlebarInsetPx = (): number => { - if (typeof window === 'undefined' || !document.documentElement) return 0; - const raw = getComputedStyle(document.documentElement).getPropertyValue('--titlebar-inset-left').trim(); - return parseFloat(raw) || 0; -}; - -export const MuTxtHeader: React.FC = ({editor}) => { - const mutxt = useMuTxt(); - const availableWidth = mutxt.sizer.width.use(); - const desiredWidth = mutxt.sizer.content.use(); - const shellBox = mutxt.shellBox.use(); - const displayMode = mutxt.displayMode.use(); - const styles = useStyles(); - mutxt.version.use(); - - // Needed to account correctly for Electron window traffic lights on Mac. - const shellWidth = shellBox[2]; - const width = - displayMode === 'fullscreen' - ? shellWidth - : displayMode === 'fullwindow' - ? Math.max(0, shellWidth - readTitlebarInsetPx()) - : Math.min(availableWidth, desiredWidth); - const basePad = width < 1200 ? 8 : 32; - const paddingLeft = displayMode === 'fullwindow' ? `max(${basePad}px, var(--titlebar-inset-left, 0px))` : basePad; - - const inlineMenu = mutxt.inline.menu.buildToolbarMenu(); - const voidsMenu = mutxt.voids.menu.buildToolbarMenu(); - const blockMenu = mutxt.block.menu.buildToolbarMenu(width > 1300 ? 2 : width > 1200 ? 1 : 0); - - return ( - - -
- {width > 900 ? ( - <> - - - - - - - - - - ) : ( - - )} -
-
- - - 1500 ? 5 : width > 1400 ? 4 : width > 1300 ? 3 : width > 1200 ? 2 : width > 1100 ? 1 : 0, - )} - /> -
-
-
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/chrome/scroll/ScrollMap.tsx b/packages/mutxt-react/src/MuTxt/chrome/scroll/ScrollMap.tsx deleted file mode 100644 index 81bffe856d..0000000000 --- a/packages/mutxt-react/src/MuTxt/chrome/scroll/ScrollMap.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import * as React from 'react'; -import {keyframes, rule} from 'nano-theme'; -import {Marker, useScrollArea} from '@jsonjoy.com/ui/lib/4-card/ScrollArea'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {useSyncStore} from '@jsonjoy.com/ui/lib/hooks/useSyncStore'; -import {measureScrollMapMarkers} from '../../behavior/scroll-map'; -import {useMuTxt} from '../../context'; -import type {Editor} from 'slate'; - -const selectionMarkerBlink = keyframes({ - '0%': {opacity: 1}, - '50%': {opacity: 0.22}, - '100%': {opacity: 1}, -}); - -const selectionMarkerClass = rule({ - // bd: '1px dashed #000', - bd: '1px dashed #07f', - bg: 'rgba(7,122,255,.22)', - bxz: 'border-box', - minH: '2px', - trs: 'height .1s ease-out, top .1s ease-out, bottom .1s ease-out', - animation: `${selectionMarkerBlink} .5s step-start infinite`, - zIndex: 1, -}); - -export interface ScrollMapProps { - editor: Editor; -} - -export const ScrollMap: React.FC = ({editor}) => { - const state = useMuTxt(); - const styles = useStyles(); - const scrollArea = useScrollArea(); - const scrollHeight = useSyncStore(scrollArea.scrollHeight$); - const clientHeight = useSyncStore(scrollArea.clientHeight$); - const focused = state.focused.use(); - state.wnd.use(); - const version = state.version.use(); - const cursor = state.cursor.use(); - const scrollVersion = state.scrollVersion.use(); - const thingsVersion = state.things.version.use(); - const [markers, setMarkers] = React.useState>([]); - - // biome-ignore lint/correctness/useExhaustiveDependencies: focused/version/scrollVersion/cursor are change triggers for re-measuring markers - React.useLayoutEffect(() => { - if (!scrollArea.viewportEl || scrollHeight <= clientHeight || scrollHeight <= 0) { - setMarkers([]); - return; - } - const viewportEl = scrollArea.viewportEl; - const frame = requestAnimationFrame(() => { - const railHeight = scrollArea.railEl?.clientHeight ?? clientHeight; - setMarkers( - measureScrollMapMarkers(editor, viewportEl, scrollHeight, railHeight, styles.light ?? true, (id) => - state.things.get(id), - ), - ); - }); - return () => cancelAnimationFrame(frame); - }, [ - clientHeight, - editor, - focused, - scrollArea, - scrollHeight, - styles.light, - version, - scrollVersion, - cursor, - thingsVersion, - state, - ]); - - if (!markers.length) return null; - - return ( - <> - {markers.map((marker) => { - const {variant = 'left'} = marker; - if (variant === 'selection') { - return ( - - {(markerStyle) => ( -
- )} - - ); - } - - return ( - - ); - })} - - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/BlockElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/BlockElement.tsx deleted file mode 100644 index dcbe0ae984..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/BlockElement.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from 'react'; -import type {RenderElementProps} from 'slate-react'; -import type {CustomElement} from '../../types'; -import {ParagraphElement} from './ParagraphElement'; -import {HeadingElement} from './HeadingElement'; -import {BlockquoteElement} from './BlockquoteElement'; -import {CalloutElement} from '../../block/callout/CalloutElement'; -import {CodeBlock} from './code-block/CodeBlock'; -import {PreformattedElement} from './PreformattedElement'; -import {EmbedElement} from './EmbedElement'; -import {HrElement} from './hr/HrElement'; -import {ListContainerElement, ListItemElement} from './ListElement'; -import {TwoColumnsElement} from './TwoColumnsElement'; -import {FileElement} from './file/FileElement'; -import {TocElement} from './toc/TocElement'; -import {MathElement} from './math/MathElement'; -import {MathInlineElement} from './math/MathInlineElement'; - -export interface BlockElementProps extends RenderElementProps { - element: CustomElement; -} - -export const BlockElement: React.FC = (props) => { - switch (props.element.type) { - case 'columns': - return ; - case 'h1': - case 'h2': - case 'h3': - case 'h4': - case 'h5': - case 'h6': - case 'title': - case 'subtitle': - return ; - case 'blockquote': - return ; - case 'callout': - return ; - case 'code-block': - return ; - case 'pre': - return ; - case 'embed': - return ; - case 'hr': - return ; - case 'file': - return ; - case 'toc': - return ; - case 'math': - return ; - case 'math-inline': - return ; - case 'ul': - case 'ol': - case 'checklist': - case 'stepper': - return ; - case 'li': - return ; - default: - return ; - } -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/BlockPlaceholder.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/BlockPlaceholder.tsx deleted file mode 100644 index f94216cd43..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/BlockPlaceholder.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {isTouch} from '@jsonjoy.com/ui'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {useSelected} from 'slate-react'; -import {typeToLabel} from '../../util/typeToLabel'; -import {useMuTxt} from '../../context'; -import {Key} from '@jsonjoy.com/ui/lib/1-inline/Key'; -import type {CustomElement} from '../../types'; - -const blockPlaceholderClass = rule({ - pos: 'absolute', - t: '0', - l: '0', - pe: 'none', - us: 'none', - mr: '0', - pd: 'inherit', - lh: 'inherit', -}); - -export interface BlockPlaceholderProps { - element: CustomElement; -} - -const getPlaceholderText = (element: CustomElement): React.ReactNode => { - // if (element.type === 'p') return 'Type here or press "/" for options...'; - if (element.type === 'p') - return ( - <> - Type here - {!isTouch && ( - <> -  or double tap Shift, Shift for menu - - )} - ... - - ); - return typeToLabel(element.type); -}; - -export const BlockPlaceholder: React.FC = ({element}) => { - const mutxt = useMuTxt(); - const focused = mutxt.focused.use(); - const styles = useStyles(); - const isSelected = useSelected(); - const selection = mutxt.selection.use(); - - if (selection || !isSelected || !focused) return; - - const isEmpty = mutxt.api.isEmpty(); - if (isEmpty) return; - - const placeholder = getPlaceholderText(element); - if (!placeholder) return; - - return ( - - {placeholder} - - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/BlockquoteElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/BlockquoteElement.tsx deleted file mode 100644 index c6516f61bf..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/BlockquoteElement.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react'; -import {rule, useRule} from 'nano-theme'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {BlockPlaceholder} from './BlockPlaceholder'; -import {indentPadding} from '../../behavior/indentation'; -import {fontFamilyOf} from '../../behavior/font'; -import {fgVar} from '../../custom-style/css'; -import {isEmptyBlock} from '../../util'; -import type {RenderElementProps} from 'slate-react'; -import type {BlockquoteElement as BlockquoteElementType} from '../../types'; - -const blockquoteClass = rule({ - pos: 'relative', - mr: '18px 0', - pd: '24px 18px', - paddingInlineStart: '42px', - fz: '1.05em', - // lh: 1.9, - borderStartStartRadius: 0, - borderStartEndRadius: '16px', - borderEndEndRadius: '16px', - borderEndStartRadius: 0, -}); - -export interface BlockquoteElementProps extends RenderElementProps { - element: BlockquoteElementType; -} - -export const BlockquoteElement: React.FC = ({attributes, children, element}) => { - const styles = useStyles(); - const dynamicClass = useRule(({g}) => ({ - bg: g(0, 0.015), - borderInlineStart: `3px solid ${g(0.24)}`, - trs: 'background 140ms ease, border-color 140ms ease', - '&:hover': { - bg: g(0, 0.035), - borderInlineStartColor: g(0), - }, - })); - - return ( -
- {children} - {isEmptyBlock(element) && } -
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/EmbedElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/EmbedElement.tsx deleted file mode 100644 index 93507e7e91..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/EmbedElement.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import Embed from 'react-embed'; -import {Paper} from '@jsonjoy.com/ui/lib/4-card/Paper'; -import {Placeholder} from '@jsonjoy.com/ui/lib/3-list-item/Placeholder'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {useFocused, useSelected, type RenderElementProps} from 'slate-react'; -import {Favicon} from '@jsonjoy.com/ui/lib/1-inline/Favicon'; -import {VoidSelectionOverlay} from './VoidSelectionOverlay'; -import type {EmbedElement as EmbedElementType} from '../../types'; - -const blockClass = rule({ - pos: 'relative', - pb: '18px', - bxz: 'border-box', -}); - -const previewClass = rule({ - d: 'flex', - fld: 'column', - gap: '10px', - us: 'none', -}); - -const getEmbedHost = (url: string): string => { - try { - return new URL(url).hostname.replace(/^www\./, ''); - } catch { - return 'external content'; - } -}; - -const EmbedFallback: React.FC<{url: string}> = ({url}) => { - const styles = useStyles(); - return ( - -
- - {getEmbedHost(url)} -
-
- {url} -
-
- ); -}; - -export interface EmbedPreviewProps { - url: string; - caption?: string; - compact?: boolean; - selected?: boolean; - width?: number; -} - -export const EmbedPreview: React.FC = ({url, caption, compact, selected, width = 720}) => { - const styles = useStyles(); - - return ( -
- -
- } - renderVoid={() => } - /> -
- -
- {!!caption && ( -
- {caption} -
- )} -
- ); -}; - -export interface EmbedElementProps extends RenderElementProps { - element: EmbedElementType; -} - -export const EmbedElement: React.FC = ({attributes, children, element}) => { - const isSelected = useSelected(); - const isFocused = useFocused(); - const selected = isSelected && isFocused; - - return ( -
-
- -
- {children} -
- ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/HeadingElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/HeadingElement.tsx deleted file mode 100644 index 4b2abe21d7..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/HeadingElement.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {BlockPlaceholder} from './BlockPlaceholder'; -import {indentPadding} from '../../behavior/indentation'; -import {fontFamilyOf} from '../../behavior/font'; -import {fgVar} from '../../custom-style/css'; -import {isEmptyBlock} from '../../util'; -import type {RenderElementProps} from 'slate-react'; -import type {HeadingElement as HeadingElementType} from '../../types'; - -const headingClass = rule({ - pos: 'relative', - ls: '-0.03em', -}); - -export interface HeadingElementProps extends RenderElementProps { - element: HeadingElementType; -} - -export const HeadingElement: React.FC = ({attributes, children, element}) => { - const styles = useStyles(); - const shared: React.CSSProperties = { - textAlign: element.align, - paddingInlineStart: indentPadding(element.indent), - fontFamily: fontFamilyOf(element.font), - color: fgVar(8, styles.g(0.08)), - }; - - switch (element.type) { - case 'title': - return ( -

- {children} - {isEmptyBlock(element) && } -

- ); - case 'subtitle': - return ( -

- {children} - {isEmptyBlock(element) && } -

- ); - case 'h1': - return ( -

- {children} - {isEmptyBlock(element) && } -

- ); - case 'h2': - return ( -

- {children} - {isEmptyBlock(element) && } -

- ); - case 'h3': - return ( -

- {children} - {isEmptyBlock(element) && } -

- ); - case 'h4': - return ( -

- {children} - {isEmptyBlock(element) && } -

- ); - case 'h5': - return ( -
- {children} - {isEmptyBlock(element) && } -
- ); - case 'h6': - return ( -
- {children} - {isEmptyBlock(element) && } -
- ); - default: - return
{children}
; - } -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/LineStyleTab.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/LineStyleTab.tsx deleted file mode 100644 index 4a234aa14e..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/LineStyleTab.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {HrLine} from './hr/HrLine'; -import type {HrLineStyle} from '../../types'; - -const PREVIEW_WIDTH = 24; - -const previewWrapClass = rule({ - d: 'inline-flex', - ai: 'center', - jc: 'center', - w: `${PREVIEW_WIDTH}px`, - minH: '16px', -}); - -export interface LineStylePreviewProps { - style: HrLineStyle; - strokeWidth?: number; -} - -/** - * Tiny horizontal stroke preview used inside option pickers to show what - * each line-style choice looks like (solid / dashed / dotted / squiggly). - */ -export const LineStylePreview: React.FC = ({style, strokeWidth = 2}) => ( - -); - -export interface LineStyleTabLabelProps { - style: HrLineStyle; - label: string; - strokeWidth?: number; -} - -export const LineStyleTabLabel: React.FC = ({style, label, strokeWidth = 2}) => ( - - - -); diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/ListElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/ListElement.tsx deleted file mode 100644 index 27bcfbbae2..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/ListElement.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {Editor, Element as SlateElement} from 'slate'; -import {ReactEditor, type RenderElementProps, useReadOnly, useSlateStatic} from 'slate-react'; -import {setChecklistItemChecked} from '../../behavior'; -import {BlockPlaceholder} from './BlockPlaceholder'; -import {fontFamilyOf} from '../../behavior/font'; -import {lhVar} from '../../custom-style/css'; -import {isEmptyBlock} from '../../util'; -import {StepperItem} from '../../block/stepper/StepperItem'; -import {ProgressCap} from '../../block/stepper/ProgressCap'; -import type { - BulletedListElement, - ChecklistListElement, - ListItemElement as ListItemNode, - NumberedListElement, - StepperListElement, -} from '../../types'; - -const listClass = rule({ - mr: '0 0 16px', - paddingInlineStart: '24px', -}); - -const checklistClass = rule({ - mr: '0 0 16px', - paddingInlineStart: '24px', - listStyle: 'none', -}); - -const itemClass = rule({ - pos: 'relative', - mr: '1px 0', - lh: lhVar('1.7'), -}); - -const checklistItemClass = rule({ - pos: 'relative', - mr: '1px 0', - lh: lhVar('1.7'), - listStyle: 'none', -}); - -const checkboxWrapClass = rule({ - pos: 'absolute', - insetInlineStart: '-24px', - t: '6px', - d: 'inline-flex', - ai: 'center', - jc: 'center', - flexShrink: 0, -}); - -const checkboxClass = rule({ - w: '16px', - h: '16px', - mar: 0, - bd: '1px solid rgba(127,127,127,0.4)', - bdrad: '4px', - appearance: 'none', - d: 'grid', - placeContent: 'center', - cur: 'pointer', - '&::before': { - content: '""', - w: '12px', - h: '12px', - trs: '120ms transform ease-in-out', - bg: '#fff', - clipPath: 'polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%)', - transform: 'scale(0)', - }, - '&:checked': { - bg: '#2f8f4e', - borderColor: '#2f8f4e', - }, - '&:checked::before': { - transform: 'scale(.7)', - }, -}); - -const checklistContentClass = rule({ - d: 'block', - minW: '0', -}); - -const stepperClass = rule({ - mr: '0 0 16px', - pad: '4px 0 0 0', - listStyle: 'none', -}); - -export interface ListContainerElementProps extends RenderElementProps { - element: BulletedListElement | NumberedListElement | ChecklistListElement | StepperListElement; -} - -export interface ListItemElementProps extends RenderElementProps { - element: ListItemNode; -} - -export const ListContainerElement: React.FC = ({attributes, children, element}) => { - if (element.type === 'checklist') { - return ( -
    - {children} -
- ); - } - if (element.type === 'stepper') { - return ( -
    - {element.progress ? : null} - {children} -
- ); - } - if (element.type === 'ol') { - return ( -
    - {children} -
- ); - } - return ( -
    - {children} -
- ); -}; - -export const ListItemElement: React.FC = ({attributes, children, element}) => { - const editor = useSlateStatic(); - const readOnly = useReadOnly(); - - const parentType = React.useMemo(() => { - try { - const path = ReactEditor.findPath(editor, element); - const [parent] = Editor.parent(editor, path); - return SlateElement.isElement(parent) ? parent.type : null; - } catch { - return null; - } - }, [editor, element]); - - const isChecklistItem = parentType === 'checklist'; - const isStepperItem = parentType === 'stepper'; - - const onToggle = React.useCallback( - (event: React.MouseEvent) => { - event.preventDefault(); - if (readOnly || !isChecklistItem) return; - const path = ReactEditor.findPath(editor, element); - setChecklistItemChecked(editor, path, !element.checked); - }, - [editor, element, isChecklistItem, readOnly], - ); - - if (isStepperItem) { - return ( - - {children} - - ); - } - - if (!isChecklistItem) { - return ( -
  • - {children} - {isEmptyBlock(element) && } -
  • - ); - } - - return ( -
  • - - - - - {children} - {isEmptyBlock(element) && } - -
  • - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/ParagraphElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/ParagraphElement.tsx deleted file mode 100644 index cb9a4c1743..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/ParagraphElement.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {BlockPlaceholder} from './BlockPlaceholder'; -import {indentPadding} from '../../behavior/indentation'; -import {fontFamilyOf} from '../../behavior/font'; -import {lhVar} from '../../custom-style/css'; -import {isEmptyBlock} from '../../util'; -import type {RenderElementProps} from 'slate-react'; -import type {ParagraphElement as ParagraphElementType} from '../../types'; - -const paragraphClass = rule({ - pos: 'relative', - mr: '0 0 1.2em', - lh: lhVar('1.8'), -}); - -export interface ParagraphElementProps extends RenderElementProps { - element: ParagraphElementType; -} - -export const ParagraphElement: React.FC = ({attributes, children, element}) => ( -

    - {children} - {isEmptyBlock(element) && } -

    -); diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/PreformattedElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/PreformattedElement.tsx deleted file mode 100644 index 5cd2e2ad6d..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/PreformattedElement.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react'; -import {rule, font} from 'nano-theme'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import {BlockPlaceholder} from './BlockPlaceholder'; -import {indentPadding} from '../../behavior/indentation'; -import {fontFamilyOf} from '../../behavior/font'; -import {fgVar} from '../../custom-style/css'; -import {isEmptyBlock} from '../../util'; -import type {RenderElementProps} from 'slate-react'; -import type {PreformattedElement as PreformattedElementType} from '../../types'; - -const preClass = rule({ - pos: 'relative', - mr: '1em 0', - pd: '12px 14px', - bdrad: '8px', - ff: '"JetBrains Mono", "Fira Code", Menlo, monospace,' + font.mono.mid.ff, - fz: '0.9rem', - lh: 1.55, - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', - ovx: 'auto', - tabSize: 4, -}); - -export interface PreformattedElementProps extends RenderElementProps { - element: PreformattedElementType; -} - -export const PreformattedElement: React.FC = ({attributes, children, element}) => { - const styles = useStyles(); - - return ( -
    -      {children}
    -      {isEmptyBlock(element) && }
    -    
    - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/StripBarHandle.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/StripBarHandle.tsx deleted file mode 100644 index 00366810ce..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/StripBarHandle.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {BasicTooltip} from '@jsonjoy.com/ui/lib/4-card/BasicTooltip'; - -export const stripBarHandleTriggerClass = 'mutxt-strip-handle'; -export const stripBarHandleFillClass = 'mutxt-strip-handle-fill'; - -const triggerClass = rule({ - d: 'flex', - ai: 'center', - jc: 'flex-start', - w: '48px', - h: '14px', - pad: 0, - bd: 'none', - bg: 'transparent', - cur: 'default', - opacity: 0, - pointerEvents: 'none', - color: 'var(--mutxt-handle-color, currentColor)', - trs: 'opacity .12s ease, color .12s ease, transform .12s ease', - '&:active': { - transform: 'scale(.97)', - }, - '&:hover': { - color: 'var(--mutxt-handle-color-hover, var(--mutxt-handle-color, currentColor))', - }, - [`&:hover .${stripBarHandleFillClass}`]: { - h: '5px', - }, -}); - -const fillClass = rule({ - d: 'block', - w: '40%', - h: '3px', - bdrad: '2px', - bg: 'currentColor', - trs: 'background-color .12s ease, width .25s ease, height .12s ease', -}); - -export interface StripBarHandleProps { - /** Tooltip text shown when the user hovers the handle. */ - tooltip?: React.ReactNode; - /** Accessibility label for the underlying button. */ - ariaLabel?: string; - /** Fired on click. */ - onActivate: () => void; - visible?: boolean; - color?: string; - colorHover?: string; - width?: number | string; -} - -export const StripBarHandle: React.FC = ({ - tooltip, - ariaLabel, - onActivate, - visible, - color, - colorHover, - width, -}) => { - const onMouseDown = React.useCallback((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - }, []); - const onClick = React.useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onActivate(); - }, - [onActivate], - ); - - const buttonStyle: React.CSSProperties = {}; - if (visible !== undefined) { - buttonStyle.opacity = visible ? 1 : 0; - buttonStyle.pointerEvents = visible ? 'auto' : 'none'; - } - if (color !== undefined) (buttonStyle as Record)['--mutxt-handle-color'] = color; - if (colorHover !== undefined) (buttonStyle as Record)['--mutxt-handle-color-hover'] = colorHover; - if (width !== undefined) buttonStyle.width = width; - const fillStyle: React.CSSProperties | undefined = visible ? {width: '100%'} : undefined; - const titleAttr = !tooltip && ariaLabel ? ariaLabel : undefined; - - const button = ( - - ); - - return tooltip ? ( - tooltip} nowrap delay={400}> - {button} - - ) : ( - button - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/TwoColumnsElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/TwoColumnsElement.tsx deleted file mode 100644 index 3e149d5e07..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/TwoColumnsElement.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {BlockPlaceholder} from './BlockPlaceholder'; -import {indentPadding} from '../../behavior/indentation'; -import {fontFamilyOf} from '../../behavior/font'; -import {isEmptyBlock} from '../../util'; -import {lhVar} from '../../custom-style/css'; -import type {RenderElementProps} from 'slate-react'; -import type {TwoColumnsElement as TwoColumnsElementType} from '../../types'; - -const twoColumnsClass = rule({ - pos: 'relative', - mr: '0 0 1.2em', - lh: lhVar('1.8'), - columnCount: 2, - columnGap: '36px', - columnFill: 'balance', - columnWidth: '18rem', -}); - -export interface TwoColumnsElementProps extends RenderElementProps { - element: TwoColumnsElementType; -} - -export const TwoColumnsElement: React.FC = ({attributes, children, element}) => ( -
    - {children} - {isEmptyBlock(element) && } -
    -); diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/VoidSelectionOverlay.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/VoidSelectionOverlay.tsx deleted file mode 100644 index 258e9f0447..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/VoidSelectionOverlay.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react'; - -export interface VoidSelectionOverlayProps { - selected: boolean; -} - -export const VoidSelectionOverlay: React.FC = ({selected}) => { - if (!selected) return null; - return ( -
    - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlock.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlock.tsx deleted file mode 100644 index efc397eb87..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlock.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import * as React from 'react'; -import {drule, rule, useTheme} from 'nano-theme'; -import {type RenderElementProps, useReadOnly} from 'slate-react'; -import {BasicButtonMore} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonMore'; -import {CopyButton} from '@jsonjoy.com/ui/lib/2-inline-block/CopyButton'; -import {Popup} from '@jsonjoy.com/ui/lib/4-card/Popup'; -import {useStyles} from '@jsonjoy.com/ui/lib/styles/context'; -import Paper from '@jsonjoy.com/ui/lib/4-card/Paper'; -import {ColorTokens} from 'code-colors-react'; -import {Label} from '@jsonjoy.com/ui/lib/1-inline/Label'; -import {Iconista} from '@jsonjoy.com/ui/lib/icons/Iconista'; -import {SetTrace} from '@jsonjoy.com/ui'; -import {CodeBlockOptionsPopup} from './CodeBlockOptionsPopup'; -import * as settings from './settings'; -import type {CodeBlockElement as CodeBlockElementType} from '../../../types'; - -const CODE_LINE_HEIGHT = 1.7; - -const SHARED_TEXT_CSS = { - ff: '"JetBrains Mono", "Fira Code", Menlo, monospace', - fz: '0.9rem', - lh: `${CODE_LINE_HEIGHT}em`, - pd: '16px 18px 18px 14px', - mr: 0, - letterSpacing: '0', - tabSize: 4, - fontKerning: 'none', - fontVariantLigatures: 'none', - fontFeatureSettings: 'normal', -} as const; - -const codeWrapClass = rule({ - pos: 'relative', - mr: '12px 0', - ff: '"JetBrains Mono", "Fira Code", Menlo, monospace', - containerType: 'scroll-state', -}); - -const stickyHeaderClass = rule({ - pos: 'sticky', - t: 0, - z: 1, - containerType: 'scroll-state', -}); - -const metaBarClass = rule({ - d: 'flex', - ai: 'center', - jc: 'space-between', - gap: '12px', - pd: '8px 14px', - fw: 'wrap', - us: 'none', - bdrad: '16px 16px 0 0', - trs: 'border-bottom-left-radius .3s, border-bottom-right-radius .3s', - '@container scroll-state(stuck: top)': { - '&': { - bdrad: '0 0 16px 16px', - }, - }, -}); - -const metaInputsClass = rule({ - d: 'flex', - ai: 'center', - gap: '10px', - fw: 'wrap', - w: '100%', -}); - -const metaPreviewClass = rule({ - d: 'inline-flex', - ai: 'center', - gap: '10px', - fw: 'wrap', - minW: '0', -}); - -const metaActionsClass = rule({ - d: 'inline-flex', - ai: 'center', - gap: '8px', - marginInlineStart: 'auto', -}); - -const metaLabelClass = rule({ - d: 'inline-flex', - ai: 'center', - gap: '8px', - fz: '12px', - minW: '0', -}); - -const metaChipClass = rule({ - fz: '12px', -}); - -const codePreClass = rule({ - mr: '0', - w: '100%', - ovx: 'auto', - lh: `${CODE_LINE_HEIGHT}em`, - d: 'flex', - ai: 'stretch', - trs: 'background .3s', - [`.${codeWrapClass.trim()}:hover &`]: { - bg: 'var(--code-hover-bg)', - }, -}); - -const gutterClass = rule({ - flexShrink: 0, - pd: '16px 12px 18px 18px', - us: 'none', - pe: 'none', - ff: '"JetBrains Mono", "Fira Code", Menlo, monospace', - fz: '0.9rem', - lh: '1.7', - op: 0.3, - whiteSpace: 'pre', - ta: 'right', - bdr: '1px solid rgba(127,127,127,0.22)', -}); - -const codeClass = drule({ - ...SHARED_TEXT_CSS, - d: 'block', - pos: 'relative', - flex: '1', - minW: '0', - '&::selection': { - bg: 'rgba(0,127,255,0.36)', - col: 'inherit', - }, - '&::before': { - content: '""', - pos: 'absolute', - t: '0', - h: '100%', - w: '1px', - bg: 'rgba(127,127,127,0.06)', - pointerEvents: 'none', - }, -}); - -const overlayClass = rule({ - ...SHARED_TEXT_CSS, - pos: 'absolute', - t: 0, - l: 0, - r: 0, - b: 0, - pe: 'none', - us: 'none', - ov: 'hidden', - '& *': { - fontWeight: 'inherit', - fontStyle: 'inherit', - padding: 0, - margin: 0, - border: 0, - cursor: 'inherit', - }, -}); - -export interface CodeBlockProps extends RenderElementProps { - element: CodeBlockElementType; -} - -export const CodeBlock: React.FC = ({attributes, children, element}) => { - const styles = useStyles(); - const theme = useTheme(); - const readOnly = useReadOnly(); - - const stopPointerPropagation = React.useCallback((event: React.MouseEvent) => { - event.stopPropagation(); - }, []); - - const preventMouseDown = React.useCallback((event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - }, []); - - const codeText = React.useMemo(() => element.children.map(({text}) => text).join(''), [element.children]); - - const getCodeText = React.useCallback(() => codeText, [codeText]); - - const metaBarStyle: React.CSSProperties = { - borderBottom: `1px solid ${styles.g(0, styles.light ? 0.06 : 0.1)}`, - background: theme.bg, - }; - - const lineCount = React.useMemo(() => { - return (codeText.match(/\n/g)?.length ?? 0) + 1; - }, [codeText]); - - const lineNumbers = React.useMemo( - () => Array.from({length: lineCount}, (_, i) => String(i + 1)).join('\n'), - [lineCount], - ); - - const languageValue = element.language?.trim() || ''; - const fileNameValue = element.fileName?.trim() || ''; - const showReadOnlyMeta = !!fileNameValue || !!languageValue; - const wrapColumn = settings.getCodeBlockWrapColumn(element.wrap); - const showLineNumbers = settings.getCodeBlockShowLineNumbers(element.showLineNumbers); - const metaLabelMarginLeft = showLineNumbers ? (lineCount > 10 ? 34 : 26) : 0; - const codeClassName = codeClass({ - textDecorationColor: styles.g(0), - '&::before': { - l: `${wrapColumn}ch`, - }, - }); - - const codeStyle: React.CSSProperties = { - paddingLeft: showLineNumbers ? '14px' : '18px', - caretColor: styles.g(0), - color: languageValue ? 'rgba(127,127,127,.1)' : void 0, - }; - - const codeOverlayStyle: React.CSSProperties = { - paddingLeft: showLineNumbers ? '14px' : '18px', - }; - - const codeContent = ( -
    -      {showLineNumbers ? (
    -        
    -      ) : null}
    -      
    -        {!!languageValue && (
    -          
    -        )}
    -        
    -          {children}
    -        
    -      
    -    
    - ); - - const header = (!readOnly || showReadOnlyMeta) && ( -
    -
    - {readOnly ? ( -
    - {!!fileNameValue && ( - - {fileNameValue || 'Code block'} - - )} - -
    - ) : ( - // biome-ignore lint/a11y/useKeyWithClickEvents: click handler only stops propagation; keyboard interaction lives on inner inputs -
    -
    - - {fileNameValue || } - -
    - -
    - {!!languageValue && } - - }> - - -
    -
    - )} -
    -
    - ); - - return ( -
    - - {header} - {codeContent} - -
    - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlockOptions.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlockOptions.tsx deleted file mode 100644 index 634f94f3fd..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlockOptions.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {BasicButtonMore} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonMore'; -import {Checkbox} from '@jsonjoy.com/ui/lib/2-inline-block/Checkbox'; -import {Input} from '@jsonjoy.com/ui/lib/2-inline-block/Input'; -import {Slider} from '@jsonjoy.com/ui/lib/2-inline-block/Slider'; -import {ContextMenu} from '@jsonjoy.com/ui/lib/4-card/ContextMenu'; -import {Separator} from '@jsonjoy.com/ui/lib/3-list-item/Separator'; -import {Popup} from '@jsonjoy.com/ui/lib/4-card/Popup'; -import {FormRow} from '@jsonjoy.com/ui/lib/3-list-item/FormRow'; -import {MoveToViewport} from '@jsonjoy.com/ui/lib/utils/popup/MoveToViewport'; -import {useCodeBlockOptionsState} from './state'; -import {Space} from '@jsonjoy.com/ui/lib/3-list-item/Space'; -import type {MenuItem} from '@jsonjoy.com/ui/lib/4-card/StructuralMenu/types'; - -const LANGUAGE_OPTIONS = [ - 'bash', - 'c', - 'cpp', - 'css', - 'go', - 'html', - 'java', - 'js', - 'json', - 'jsx', - 'py', - 'rust', - 'sh', - 'sql', - 'text', - 'toml', - 'ts', - 'tsx', - 'yaml', - 'zsh', -]; - -const blockClass = rule({ - bxz: 'border-box', - d: 'flex', - fld: 'column', - gap: '12px', - maxW: '400px', -}); - -const popupFieldRowClass = rule({ - d: 'grid', - gridTemplateColumns: '1fr auto', - gap: '8px', - ai: 'center', -}); - -const stopInputKeyDown = (event: React.KeyboardEvent): void => { - event.stopPropagation(); -}; - -const preventPopupMouseDown = (event: React.MouseEvent): void => { - event.preventDefault(); - event.stopPropagation(); -}; - -export const CodeBlockOptions: React.FC = () => { - const state = useCodeBlockOptionsState(); - const fileName = state.fileName.use(); - const language = state.language.use(); - const wrapColumn = state.wrapColumn.use(); - const showLineNumbers = state.showLineNumbers.use(); - - const languageMenu: MenuItem = { - name: 'Language', - minWidth: 220, - children: [ - { - id: 'plain-text', - name: 'Plain text', - onSelect: () => state.setLanguage(''), - right: !language.trim() ? () => Current : undefined, - }, - {id: 'language-sep', name: 'language-sep', sep: true}, - ...LANGUAGE_OPTIONS.map((option) => ({ - id: option, - name: option, - display: () => .{option}, - onSelect: () => state.setLanguage(option), - right: - language.trim().toLowerCase() === option - ? () => Current - : undefined, - })), - ], - }; - - return ( -
    - { - event.preventDefault(); - event.stopPropagation(); - state.apply(); - }} - onEsc={(event) => { - event.preventDefault(); - event.stopPropagation(); - state.cancel(); - }} - onBlur={state.inferLanguageFromFileName} - /> - -
    - { - event.preventDefault(); - event.stopPropagation(); - state.apply(); - }} - onEsc={(event) => { - event.preventDefault(); - event.stopPropagation(); - state.cancel(); - }} - /> - ( - - - - )} - > - - -
    - - - - - - - - - - - -
    - -
    -
    -
    - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlockOptionsPopup.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlockOptionsPopup.tsx deleted file mode 100644 index 892b846051..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/CodeBlockOptionsPopup.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react'; -import {useSlateStatic} from 'slate-react'; -import {usePopup} from '@jsonjoy.com/ui/lib/4-card/Popup/context'; -import {EditorContextPopup} from '../../../chrome/EditorContextPopup'; -import type {CodeBlockElement as CodeBlockElementType} from '../../../types'; -import {CodeBlockOptions} from './CodeBlockOptions'; -import {CodeBlockOptionsStateProvider, useCodeBlockOptionsState} from './state'; - -export interface CodeBlockOptionsPopupProps { - element: CodeBlockElementType; -} - -const CodeBlockOptionsPopupBody: React.FC = () => { - const state = useCodeBlockOptionsState(); - - return ( - - - - ); -}; - -export const CodeBlockOptionsPopup: React.FC = ({element}) => { - const editor = useSlateStatic(); - const popup = usePopup(); - - return ( - popup?.close()}> - - - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/settings.ts b/packages/mutxt-react/src/MuTxt/components/blocks/code-block/settings.ts deleted file mode 100644 index 74a57bab35..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/settings.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const DEFAULT_CODE_BLOCK_WRAP_COLUMN = 80; -export const DEFAULT_CODE_BLOCK_SHOW_LINE_NUMBERS = true; - -const roundPositive = (value: number | undefined, fallback: number): number => { - if (typeof value !== 'number' || !Number.isFinite(value)) return fallback; - return Math.max(0, Math.round(value)); -}; - -export const getCodeBlockWrapColumn = (value?: number): number => { - const nextValue = roundPositive(value, DEFAULT_CODE_BLOCK_WRAP_COLUMN); - return nextValue > 0 ? nextValue : DEFAULT_CODE_BLOCK_WRAP_COLUMN; -}; - -export const getCodeBlockShowLineNumbers = (value?: boolean): boolean => - value === undefined ? DEFAULT_CODE_BLOCK_SHOW_LINE_NUMBERS : value; - -export const getStoredCodeBlockWrapColumn = (value: number): number | undefined => { - const nextValue = getCodeBlockWrapColumn(value); - return nextValue === DEFAULT_CODE_BLOCK_WRAP_COLUMN ? undefined : nextValue; -}; - -export const getStoredCodeBlockShowLineNumbers = (value: boolean): boolean | undefined => - value === DEFAULT_CODE_BLOCK_SHOW_LINE_NUMBERS ? undefined : value; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/state.ts b/packages/mutxt-react/src/MuTxt/components/blocks/code-block/state.ts deleted file mode 100644 index b06dd5d0e0..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/code-block/state.ts +++ /dev/null @@ -1,190 +0,0 @@ -import * as React from 'react'; -import {rsync} from '@jsonjoy.com/ui'; -import {Transforms, type Editor} from 'slate'; -import {ReactEditor} from 'slate-react'; -import type {CodeBlockElement as CodeBlockElementType} from '../../../types'; -import * as settings from './settings'; - -const getElementSignature = (element: CodeBlockElementType): string => - JSON.stringify([ - element.language ?? '', - element.fileName ?? '', - element.wrap ?? null, - element.showLineNumbers ?? null, - ]); - -const CodeBlockOptionsStateContext = React.createContext(null); - -export class CodeBlockOptionsState { - public readonly fileName = rsync.val(''); - public readonly language = rsync.val(''); - public readonly wrapColumn = rsync.val(0); - public readonly showLineNumbers = rsync.val(true); - - private element: CodeBlockElementType; - private signature = ''; - private closePopup?: () => void; - - constructor( - private readonly editor: Editor, - element: CodeBlockElementType, - closePopup?: () => void, - ) { - this.element = element; - this.closePopup = closePopup; - this.syncFromElement(element); - } - - public readonly setElement = (element: CodeBlockElementType): void => { - const signature = getElementSignature(element); - this.element = element; - if (signature === this.signature) return; - this.syncFromElement(element, signature); - }; - - public readonly setClosePopup = (closePopup?: () => void): void => { - this.closePopup = closePopup; - }; - - public readonly setFileName = (value: string): void => { - this.fileName.set(value); - }; - - public readonly setLanguage = (value: string): void => { - this.language.set(value); - }; - - public readonly setWrapColumn = (value: number): void => { - this.wrapColumn.set(settings.getCodeBlockWrapColumn(value)); - }; - - public readonly toggleShowLineNumbers = (): void => { - this.showLineNumbers.set(!this.showLineNumbers.value); - }; - - public readonly inferLanguageFromFileName = (): void => { - if (this.language.value.trim() !== '') return; - const extension = this.fileName.value.split('.').pop()?.trim() || ''; - if (extension && extension !== this.language.value.trim()) this.language.set(extension); - }; - - public readonly cancel = (): void => { - this.syncFromElement(this.element); - this.closePopup?.(); - }; - - public readonly apply = (): void => { - const nextLanguage = this.language.value.trim(); - const nextFileName = this.fileName.value.trim(); - const nextWrapColumn = settings.getStoredCodeBlockWrapColumn(this.wrapColumn.value); - const nextShowLineNumbers = settings.getStoredCodeBlockShowLineNumbers(this.showLineNumbers.value); - - this.applyMetaValue('language', nextLanguage, this.element.language); - this.applyMetaValue('fileName', nextFileName, this.element.fileName); - this.applyNumberMetaValue('wrap', nextWrapColumn, this.element.wrap); - this.applyBooleanMetaValue('showLineNumbers', nextShowLineNumbers, this.element.showLineNumbers); - this.removeDeprecatedMetaValues(); - - this.fileName.set(nextFileName); - this.language.set(nextLanguage); - this.wrapColumn.set(settings.getCodeBlockWrapColumn(nextWrapColumn)); - this.showLineNumbers.set(settings.getCodeBlockShowLineNumbers(nextShowLineNumbers)); - - this.closePopup?.(); - }; - - public readonly dispose = (): void => {}; - - private readonly syncFromElement = ( - element: CodeBlockElementType, - signature = getElementSignature(element), - ): void => { - this.signature = signature; - this.fileName.set(element.fileName ?? ''); - this.language.set(element.language ?? ''); - this.wrapColumn.set(settings.getCodeBlockWrapColumn(element.wrap)); - this.showLineNumbers.set(settings.getCodeBlockShowLineNumbers(element.showLineNumbers)); - }; - - private readonly applyMetaValue = (field: 'language' | 'fileName', value: string, currentValue?: string): void => { - const nextValue = value.trim(); - const prevValue = currentValue?.trim() ?? ''; - if (nextValue === prevValue) return; - - const path = ReactEditor.findPath(this.editor, this.element); - if (nextValue) { - Transforms.setNodes(this.editor, {[field]: nextValue} as Partial, {at: path}); - } else { - Transforms.unsetNodes(this.editor, field, {at: path}); - } - }; - - private readonly applyNumberMetaValue = (field: 'wrap', value: number | undefined, currentValue?: number): void => { - if (value === currentValue) return; - const path = ReactEditor.findPath(this.editor, this.element); - if (value !== undefined) { - Transforms.setNodes(this.editor, {[field]: value} as Partial, {at: path}); - } else { - Transforms.unsetNodes(this.editor, field, {at: path}); - } - }; - - private readonly applyBooleanMetaValue = ( - field: 'showLineNumbers', - value: boolean | undefined, - currentValue?: boolean, - ): void => { - if (value === currentValue) return; - const path = ReactEditor.findPath(this.editor, this.element); - if (value !== undefined) { - Transforms.setNodes(this.editor, {[field]: value} as Partial, {at: path}); - } else { - Transforms.unsetNodes(this.editor, field, {at: path}); - } - }; - - private readonly removeDeprecatedMetaValues = (): void => { - const legacyElement = this.element as CodeBlockElementType & {scrollAfterLines?: number; previewLines?: number}; - if (legacyElement.scrollAfterLines === undefined && legacyElement.previewLines === undefined) return; - const path = ReactEditor.findPath(this.editor, this.element); - Transforms.unsetNodes(this.editor, ['scrollAfterLines', 'previewLines'], {at: path}); - }; -} - -export interface CodeBlockOptionsStateProviderProps { - editor: Editor; - element: CodeBlockElementType; - closePopup?: () => void; - children: React.ReactNode; -} - -export const CodeBlockOptionsStateProvider: React.FC = ({ - editor, - element, - closePopup, - children, -}) => { - const stateRef = React.useRef(null); - if (!stateRef.current) stateRef.current = new CodeBlockOptionsState(editor, element, closePopup); - const state = stateRef.current; - - React.useEffect(() => { - state.setClosePopup(closePopup); - }, [closePopup, state]); - - React.useEffect(() => { - state.setElement(element); - }, [element, state]); - - React.useEffect(() => { - return () => state.dispose(); - }, [state]); - - return React.createElement(CodeBlockOptionsStateContext.Provider, {value: state}, children); -}; - -export const useCodeBlockOptionsState = (): CodeBlockOptionsState => { - const state = React.useContext(CodeBlockOptionsStateContext); - if (!state) throw new Error('CodeBlockOptionsStateContext is not available.'); - return state; -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/file/BrokenFileCard.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/file/BrokenFileCard.tsx deleted file mode 100644 index 1e4c593646..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/file/BrokenFileCard.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import {Paper} from '@jsonjoy.com/ui/lib/4-card/Paper'; -import {FileIcon} from '@jsonjoy.com/ui/lib/1-inline/FileIcon'; -import FileListItem from '@jsonjoy.com/ui/lib/3-list-item/FileListItem'; -import {VoidSelectionOverlay} from '../VoidSelectionOverlay'; - -export interface BrokenFileCardProps { - thingId: string; - selected: boolean; -} - -export const BrokenFileCard: React.FC = ({thingId, selected}) => { - return ( - - } - title={'Missing file'} - metadata={`Reference ${thingId} could not be resolved`} - /> - - - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/file/FileElement.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/file/FileElement.tsx deleted file mode 100644 index f04cd71d5e..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/file/FileElement.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import {useFocused, useSelected, type RenderElementProps} from 'slate-react'; -import {useMuTxt} from '../../../context'; -import {BrokenFileCard} from './BrokenFileCard'; -import {ResolvedFileCard} from './ResolvedFileCard'; -import type {FileElement as FileElementType, FileThing} from '../../../types'; - -export interface FileElementProps extends RenderElementProps { - element: FileElementType; -} - -export const FileElement: React.FC = ({attributes, children, element}) => { - const mutxt = useMuTxt(); - mutxt.things.version.use(); - const isSelected = useSelected(); - const isFocused = useFocused(); - const selected = isSelected && isFocused; - - const thingId = element['@thing']; - const thing = thingId ? (mutxt.things.get(thingId) as FileThing | undefined) : undefined; - - return ( -
    -
    - {thing ? ( - - ) : ( - - )} -
    - {children} -
    - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/file/FileOptions.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/file/FileOptions.tsx deleted file mode 100644 index 630525ddb8..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/file/FileOptions.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {Input} from '@jsonjoy.com/ui/lib/2-inline-block/Input'; -import {useFileOptions} from './context'; -import {useT} from 'use-t'; - -const blockClass = rule({ - bxz: 'border-box', - d: 'flex', - fld: 'column', - gap: '12px', - maxW: '500px', -}); - -const stopInputKeyDown = (event: React.KeyboardEvent): void => { - event.stopPropagation(); -}; - -export const FileOptions: React.FC = () => { - const [t] = useT(); - const state = useFileOptions(); - const caption = state.caption.use(); - const displayName = state.displayName.use(); - - return ( -
    - { - event.preventDefault(); - event.stopPropagation(); - state.apply(); - }} - onEsc={(event) => { - event.preventDefault(); - event.stopPropagation(); - state.cancel(); - }} - /> - { - event.preventDefault(); - event.stopPropagation(); - state.apply(); - }} - onEsc={(event) => { - event.preventDefault(); - event.stopPropagation(); - state.cancel(); - }} - /> -
    - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/file/FileOptionsPopup.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/file/FileOptionsPopup.tsx deleted file mode 100644 index b065468192..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/file/FileOptionsPopup.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import {useT} from 'use-t'; -import {usePopup} from '@jsonjoy.com/ui/lib/4-card/Popup/context'; -import {BasicButton} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton'; -import {BasicButtonDelete} from '@jsonjoy.com/ui/lib/2-inline-block/BasicButton/BasicButtonDelete'; -import {BasicTooltip} from '@jsonjoy.com/ui/lib/4-card/BasicTooltip'; -import {Iconista} from '@jsonjoy.com/ui/lib/icons/Iconista'; -import {EditorContextPopup} from '../../../chrome/EditorContextPopup'; -import {FileOptions} from './FileOptions'; -import {FileOptionsState} from './state'; -import {ctx} from './context'; -import {useMuTxt} from '../../../context'; -import type {FileElement as FileElementType} from '../../../types'; - -const preventMouseDown = (event: React.MouseEvent): void => { - event.preventDefault(); -}; - -const stopBubble = (event: React.SyntheticEvent): void => { - event.stopPropagation(); -}; - -export interface FileOptionsPopupProps { - element: FileElementType; -} - -export const FileOptionsPopup: React.FC = ({element}) => { - const [t] = useT(); - const popup = usePopup(); - const mutxt = useMuTxt(); - const state = React.useMemo(() => new FileOptionsState(mutxt, element, popup?.close), [mutxt, element, popup]); - - const headerRight = ( - // biome-ignore lint/a11y/useKeyWithClickEvents: click handler only stops bubbling; keyboard interaction is on inner buttons -
    - t('Download')}> - - - - - t('Remove file')}> - - -
    - ); - - return ( - - - - - - ); -}; diff --git a/packages/mutxt-react/src/MuTxt/components/blocks/file/MediaPreview.tsx b/packages/mutxt-react/src/MuTxt/components/blocks/file/MediaPreview.tsx deleted file mode 100644 index f1c348d071..0000000000 --- a/packages/mutxt-react/src/MuTxt/components/blocks/file/MediaPreview.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react'; -import {rule} from 'nano-theme'; -import {useObjectUrl} from './useObjectUrl'; -import type {FileThing} from '../../../types'; - -const imgClass = rule({ - display: 'block', - maxW: '100%', - maxH: '480px', - bdrad: '6px', - bxz: 'border-box', -}); - -const mediaClass = rule({ - display: 'block', - w: '100%', - bdrad: '6px', - bxz: 'border-box', -}); - -export type MediaKind = 'image' | 'video' | 'audio'; - -export interface MediaPreviewProps { - thing: FileThing; - kind: MediaKind; -} - -export const MediaPreview: React.FC = ({thing, kind}) => { - const url = useObjectUrl(thing.data, thing.mimeType); - if (!url) return null; - if (kind === 'image') { - return ( -
    - {thing.name -
    - ); - } - // biome-ignore lint/a11y/useMediaCaption: caption track is unavailable for user-uploaded media - if (kind === 'video') return