@cascivo/editor 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ <!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
2
+
3
+ <div align="center">
4
+ <a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="72" height="72" alt="cascivo logo"></a>
5
+ <h1>@cascivo/editor</h1>
6
+ <p><strong>Lightweight CSS-native code editor — native textarea overlay + owned zero-dependency tokenizer</strong></p>
7
+
8
+ [![npm](https://img.shields.io/npm/v/%40cascivo%2Feditor?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/editor)
9
+ [![downloads](https://img.shields.io/npm/dm/%40cascivo%2Feditor?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/editor)
10
+ [![license](https://img.shields.io/npm/l/%40cascivo%2Feditor?style=flat-square&color=0079bf)](https://github.com/cascivo/cascivo/blob/main/LICENSE)
11
+ ![types](https://img.shields.io/badge/types-included-0079bf?style=flat-square&logo=typescript&logoColor=white)
12
+
13
+ [npm](https://www.npmjs.com/package/@cascivo/editor) · [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ A lightweight, CSS-native, signal-driven code editor for cascivo. `CodeEditor` overlays a native `<textarea>` on a syntax-highlighted `<pre>`, so the browser owns the caret, selection, IME, undo, and accessibility — JS is limited to a tiny owned tokenizer and scroll-sync. `Highlight` is the read-only renderer for snippets and docs. Zero runtime dependencies, themeable through the cascivo token system.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ pnpm add @cascivo/editor @preact/signals-react
25
+ ```
26
+
27
+ `react`, `react-dom`, and `@preact/signals-react` are peer dependencies — install them in your app.
28
+
29
+ ## Usage
30
+
31
+ ```tsx
32
+ import { CodeEditor } from '@cascivo/editor'
33
+ import '@cascivo/editor/styles.css' // required — maps to the dist `editor.css`
34
+ import '@cascivo/themes/dark.css' // any cascivo theme drives the editor colors
35
+
36
+ export function Example() {
37
+ return <CodeEditor language="typescript" lineNumbers defaultValue={`const x = 1\n`} />
38
+ }
39
+ ```
40
+
41
+ The editor is CSS-token-themed: drop it inside any element carrying a `data-theme` (or import a
42
+ theme stylesheet) and it picks up the same palette, radii, and typography as the rest of cascivo —
43
+ including the syntax colors, which map onto the `--cascivo-editor-syntax-*` palette.
44
+
45
+ ### Read-only highlighting
46
+
47
+ ```tsx
48
+ import { Highlight } from '@cascivo/editor'
49
+ ;<Highlight language="json" value={`{ "ok": true }`} />
50
+ ```
51
+
52
+ ### Languages
53
+
54
+ Ships small, tree-shakeable grammars: `plaintext`, `json`, `javascript`, `typescript`, `css`,
55
+ `html`, `markdown`, `bash`. Register your own with `registerGrammar(grammar)`.
56
+
57
+ ### React apps must subscribe to signals
58
+
59
+ `CodeEditor` and `Highlight` are signal-driven. In a plain React app (no Babel signals transform),
60
+ they already call `useSignals()` internally — no extra wiring is required.
61
+
62
+ ## Install
63
+
64
+ ```sh
65
+ pnpm add @cascivo/editor
66
+ ```
67
+
68
+ ---
69
+
70
+ [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo) · AI agents: use [`@cascivo/mcp`](https://github.com/cascivo/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/cascivo/cascivo/blob/main/registry.json) · MIT
71
+
72
+ <div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="28" height="28" alt="cascivo"></a></div>
@@ -0,0 +1,2 @@
1
+ @layer cascivo.component{._root_4k2k5_2{background:var(--cascivo-editor-bg);max-block-size:100%;color:var(--cascivo-editor-fg);border:1px solid var(--cascivo-editor-border);border-radius:var(--cascivo-radius-field);font-family:var(--cascivo-font-mono);font-size:var(--cascivo-text-sm);line-height:var(--cascivo-leading-normal);tab-size:var(--cascivo-editor-tab-size,2);display:flex;overflow:auto}._gutter_4k2k5_17{padding-block:var(--cascivo-space-3);background:var(--cascivo-editor-gutter-bg);color:var(--cascivo-editor-gutter-fg);text-align:end;-webkit-user-select:none;user-select:none;flex:none;padding-inline-start:var(--cascivo-space-3);padding-inline-end:var(--cascivo-space-2)}._gutterLine_4k2k5_28{min-block-size:1lh;display:block}._pre_4k2k5_33{min-inline-size:0;padding-block:var(--cascivo-space-3);padding-inline:var(--cascivo-space-3);white-space:pre;font:inherit;tab-size:inherit;flex:auto;margin-block:0;margin-inline:0}._code_4k2k5_45{font:inherit}._line_4k2k5_50{min-block-size:1lh;display:block}._root_4k2k5_2[data-wrap=true] ._pre_4k2k5_33{white-space:pre-wrap;word-break:break-word}._root_4k2k5_2[data-line-numbers=false] ._gutter_4k2k5_17{display:none}._plain_4k2k5_65{color:var(--cascivo-editor-fg)}._keyword_4k2k5_68{color:var(--cascivo-editor-syntax-keyword)}._string_4k2k5_71{color:var(--cascivo-editor-syntax-string)}._number_4k2k5_74{color:var(--cascivo-editor-syntax-number)}._comment_4k2k5_77{color:var(--cascivo-editor-syntax-comment);font-style:italic}._function_4k2k5_81{color:var(--cascivo-editor-syntax-function)}._type_4k2k5_84{color:var(--cascivo-editor-syntax-type)}._operator_4k2k5_87{color:var(--cascivo-editor-syntax-operator)}._punctuation_4k2k5_90{color:var(--cascivo-editor-syntax-punctuation)}._variable_4k2k5_93{color:var(--cascivo-editor-syntax-variable)}._tag_4k2k5_96{color:var(--cascivo-editor-syntax-tag)}._attr_4k2k5_99{color:var(--cascivo-editor-syntax-attr)}._regexp_4k2k5_102{color:var(--cascivo-editor-syntax-regexp)}._boolean_4k2k5_105{color:var(--cascivo-editor-syntax-boolean)}._property_4k2k5_108{color:var(--cascivo-editor-syntax-property)}._root_q71hw_2{background:var(--cascivo-editor-bg);color:var(--cascivo-editor-fg);border:1px solid var(--cascivo-editor-border);border-radius:var(--cascivo-radius-field);font-family:var(--cascivo-font-mono);font-size:var(--cascivo-text-sm);line-height:var(--cascivo-leading-normal);tab-size:var(--cascivo-editor-tab-size,2);transition:box-shadow var(--cascivo-duration-150) var(--cascivo-ease-out);display:flex;position:relative;overflow:hidden}._root_q71hw_2:has(._textarea_q71hw_18:focus-visible){box-shadow:var(--cascivo-focus-ring);border-color:var(--cascivo-color-accent)}._gutter_q71hw_23{padding-block:var(--cascivo-space-3);background:var(--cascivo-editor-gutter-bg);color:var(--cascivo-editor-gutter-fg);text-align:end;-webkit-user-select:none;user-select:none;flex:none;padding-inline-start:var(--cascivo-space-3);padding-inline-end:var(--cascivo-space-2);overflow:hidden}._codeArea_q71hw_35{flex:auto;min-inline-size:0;position:relative}._pre_q71hw_42{padding-block:var(--cascivo-space-3);padding-inline:var(--cascivo-space-3);white-space:pre;pointer-events:none;font:inherit;tab-size:inherit;margin-block:0;margin-inline:0;position:absolute;inset:0;overflow:hidden}._currentLine_q71hw_57{inset-inline:0;top:var(--cascivo-space-3);block-size:1lh;transform:translateY(calc(var(--cascivo-editor-caret-line,0) * 1lh));background:var(--cascivo-editor-current-line);pointer-events:none;position:absolute}._textarea_q71hw_18{block-size:100%;inline-size:100%;padding-block:var(--cascivo-space-3);padding-inline:var(--cascivo-space-3);resize:none;color:#0000;caret-color:var(--cascivo-editor-fg);white-space:pre;font:inherit;tab-size:inherit;background:0 0;border:0;outline:none;margin-block:0;margin-inline:0;position:absolute;inset:0;overflow:auto}._textarea_q71hw_18::placeholder{color:var(--cascivo-editor-gutter-fg)}._textarea_q71hw_18::selection{background:var(--cascivo-editor-selection)}._textarea_q71hw_18:disabled{cursor:not-allowed;opacity:var(--cascivo-disabled-opacity)}._root_q71hw_2[data-wrap=true] ._pre_q71hw_42,._root_q71hw_2[data-wrap=true] ._textarea_q71hw_18{white-space:pre-wrap;word-break:break-word}._root_q71hw_2[data-line-numbers=false] ._gutter_q71hw_23{display:none}@media (pointer:coarse){._root_q71hw_2{min-block-size:var(--cascivo-target-min-coarse,2.75rem)}}@media (prefers-reduced-motion:reduce){._root_q71hw_2{transition:none}}}@media (forced-colors:active){._root_q71hw_2{border:1px solid fieldtext}._pre_q71hw_42{display:none}._textarea_q71hw_18{color:canvastext;caret-color:canvastext}}
2
+ /*$vite$:1*/
@@ -0,0 +1,202 @@
1
+ import { HTMLAttributes, Ref, TextareaHTMLAttributes } from "react";
2
+
3
+ /**
4
+ * Token classes the syntax palette colors. Every kind maps to a
5
+ * `--cascivo-editor-syntax-<kind>` custom property (see @cascivo/tokens). `plain`
6
+ * is uncolored text (whitespace, identifiers a grammar chose not to classify).
7
+ */
8
+ type TokenKind = 'keyword' | 'string' | 'number' | 'comment' | 'function' | 'type' | 'operator' | 'punctuation' | 'variable' | 'tag' | 'attr' | 'regexp' | 'boolean' | 'property' | 'plain';
9
+ /** A single highlighted span. The concatenation of a line's token values is the line. */
10
+ interface Token {
11
+ kind: TokenKind;
12
+ value: string;
13
+ }
14
+ /**
15
+ * Opaque per-grammar continuation carried from one line to the next so multi-line
16
+ * constructs (block comments, template literals, fenced code) continue correctly.
17
+ * Each grammar owns the meaning of its own state strings; `'default'` is the start.
18
+ */
19
+ type GrammarState = string;
20
+ /** Result of tokenizing one line: its spans plus the state to feed the next line. */
21
+ interface LineTokens {
22
+ tokens: Token[];
23
+ state: GrammarState;
24
+ }
25
+ /**
26
+ * A language grammar. Pure and deterministic: `tokenizeLine` must depend only on
27
+ * `(line, state)` and must be lossless (the token values concatenate back to `line`).
28
+ */
29
+ interface Grammar {
30
+ name: string;
31
+ initialState: GrammarState;
32
+ tokenizeLine(line: string, state: GrammarState): LineTokens;
33
+ }
34
+ /** Result of tokenizing one line: spans plus the continuation state. */
35
+ interface TokenizeResult {
36
+ tokens: Token[];
37
+ endState: GrammarState;
38
+ }
39
+ /**
40
+ * Tokenize a single line, restartable from `startState`, memoized. Pure from the
41
+ * caller's perspective: identical `(grammar, line, startState)` yield identical
42
+ * results (a cache hit returns the previously-computed value).
43
+ */
44
+ declare function tokenize(grammar: Grammar, line: string, startState: GrammarState): TokenizeResult;
45
+ /**
46
+ * Tokenize a whole document, threading each line's `endState` into the next so
47
+ * block comments / template literals / fenced code continue across lines.
48
+ * Returns one token array per line. The per-line memo makes a single-line edit
49
+ * re-tokenize only the changed line(s) until the start-state reconverges.
50
+ */
51
+ declare function tokenizeDocument(grammar: Grammar, text: string): Token[][];
52
+ /** Register (or replace) a grammar under its `name`. */
53
+ declare function registerGrammar(grammar: Grammar): void;
54
+ /** Resolve a grammar by name, falling back to `plaintext` for unknown languages. */
55
+ declare function getGrammar(name?: string): Grammar;
56
+ /** List the names of all currently-registered grammars. */
57
+ declare function listGrammars(): string[];
58
+ /**
59
+ * One tokenizer rule. `match` is anchored at the current scan position (compiled
60
+ * sticky internally). On a non-empty match the matched text becomes a token of
61
+ * `kind`, the position advances, and the state transitions via `push`/`pop`.
62
+ */
63
+ interface Rule {
64
+ match: RegExp;
65
+ kind: TokenKind;
66
+ /** Switch to this state after matching (for entering multi-line constructs). */
67
+ push?: string;
68
+ /** Return to the `default` state after matching (for leaving them). */
69
+ pop?: boolean;
70
+ }
71
+ interface RuleSpec {
72
+ name: string;
73
+ /** Per-state ordered rule lists. Must include a `default` state. */
74
+ states: Record<string, Rule[]>;
75
+ }
76
+ /**
77
+ * Build a {@link Grammar} from ordered, state-keyed regex rules — a tiny
78
+ * Monarch/Prism-style engine. At each position, the first rule in the current
79
+ * state whose sticky regex matches wins; unmatched characters accumulate into
80
+ * `plain` tokens, so the tokenizer is always lossless. State carries across
81
+ * lines for block comments, template literals, and fenced code.
82
+ */
83
+ declare function createRuleGrammar(spec: RuleSpec): Grammar;
84
+ /** The trivial fallback grammar: one uncolored `plain` token per line. */
85
+ declare const plaintext: Grammar;
86
+ /**
87
+ * JSON: property keys vs string values, numbers, booleans/null, punctuation.
88
+ * A string immediately followed by a colon is a property key; other strings are
89
+ * values.
90
+ */
91
+ declare const json: Grammar;
92
+ /** JavaScript: keywords, strings (incl. multi-line template literals), numbers,
93
+ * line + block comments (state carry), function calls, identifiers, operators. */
94
+ declare const javascript: Grammar;
95
+ declare const typescript: Grammar;
96
+ /**
97
+ * CSS: at-rules, selectors, property names, values, numbers/units, strings, and
98
+ * `/* *\/` block comments (state carried across lines).
99
+ */
100
+ declare const css: Grammar;
101
+ /**
102
+ * HTML: tags, attribute names/values, `<!-- -->` comments (state carried across
103
+ * lines), and text. Inside a tag, attributes and strings are colored until `>`.
104
+ */
105
+ declare const html: Grammar;
106
+ /**
107
+ * Markdown: headings, strong/emphasis, inline code, fenced code blocks (state
108
+ * carried across lines), links, and list markers.
109
+ */
110
+ declare const markdown: Grammar;
111
+ /** Bash: comments, strings, keywords, variables (`$VAR` / `${...}`), operators. */
112
+ declare const bash: Grammar;
113
+ interface CodeEditorProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'defaultValue' | 'onChange' | 'wrap'> {
114
+ /** Controlled value. */
115
+ value?: string;
116
+ /** Initial value for uncontrolled use. */
117
+ defaultValue?: string;
118
+ /** Called with the new text on every edit. */
119
+ onValueChange?: (value: string) => void;
120
+ /** Grammar name (defaults to `plaintext`; unknown names fall back to it). */
121
+ language?: string;
122
+ /** Show the line-number gutter (default true). */
123
+ lineNumbers?: boolean;
124
+ /** Spaces per tab stop (default 2). */
125
+ tabSize?: number;
126
+ /** Insert spaces (default) vs a literal tab on Tab. */
127
+ insertSpaces?: boolean;
128
+ /** Soft-wrap long lines instead of scrolling horizontally (default false). */
129
+ wrap?: boolean;
130
+ /**
131
+ * Render only the visible lines for large documents. Defaults to auto
132
+ * (on above ~1000 lines); disabled when `wrap` makes row heights variable.
133
+ */
134
+ virtualize?: boolean;
135
+ /** Accessible label for the editor (defaults to the i18n "Code editor"). */
136
+ label?: string;
137
+ }
138
+ /**
139
+ * A lightweight code editor: a native `<textarea>` overlaid on a syntax-highlighted
140
+ * `Highlight` layer. The browser owns editing/caret/IME/undo/a11y; JS handles only
141
+ * tokenizing, scroll-sync, and Tab/Shift-Tab indent. Signal-driven, no banned hooks.
142
+ *
143
+ * Performance: tokenization is per-line memoized, the highlight layer is
144
+ * rAF-debounced (typing never blocks), and large documents window to the visible
145
+ * range while the textarea always holds the full text.
146
+ */
147
+ declare function CodeEditor({
148
+ value,
149
+ defaultValue,
150
+ onValueChange,
151
+ language,
152
+ lineNumbers,
153
+ tabSize,
154
+ insertSpaces,
155
+ wrap,
156
+ virtualize,
157
+ readOnly,
158
+ disabled,
159
+ spellCheck,
160
+ label,
161
+ className,
162
+ style,
163
+ onKeyDown,
164
+ ...rest
165
+ }: CodeEditorProps): import("react").JSX.Element;
166
+ interface HighlightProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
167
+ /** Code to render. */
168
+ value: string;
169
+ /** Grammar name (defaults to `plaintext`; unknown names fall back to it). */
170
+ language?: string;
171
+ /** Show the line-number gutter. */
172
+ lineNumbers?: boolean;
173
+ /** Soft-wrap long lines instead of scrolling horizontally. */
174
+ wrap?: boolean;
175
+ /** Spaces per tab stop (default 2). */
176
+ tabSize?: number;
177
+ /** Accessible label for the read-only code block. */
178
+ label?: string;
179
+ /** Ref to the scrollable `<pre>` (used by `CodeEditor` for scroll-sync). */
180
+ preRef?: Ref<HTMLPreElement>;
181
+ /** Ref to the gutter column (used by `CodeEditor` for scroll-sync). */
182
+ gutterRef?: Ref<HTMLDivElement>;
183
+ }
184
+ /**
185
+ * Read-only, syntax-highlighted `<pre><code>` rendered from the owned tokenizer.
186
+ * The same render layer `CodeEditor` overlays its `<textarea>` on. Signal-safe,
187
+ * themeable, and accessible — no banned hooks.
188
+ */
189
+ declare function Highlight({
190
+ value,
191
+ language,
192
+ lineNumbers,
193
+ wrap,
194
+ tabSize,
195
+ label,
196
+ className,
197
+ preRef,
198
+ gutterRef,
199
+ style,
200
+ ...rest
201
+ }: HighlightProps): import("react").JSX.Element;
202
+ export { CodeEditor, type CodeEditorProps, type Grammar, type GrammarState, Highlight, type HighlightProps, type LineTokens, type Rule, type RuleSpec, type Token, type TokenKind, type TokenizeResult, bash, createRuleGrammar, css, getGrammar, html, javascript, json, listGrammars, markdown, plaintext, registerGrammar, tokenize, tokenizeDocument, typescript };
package/dist/index.js ADDED
@@ -0,0 +1,701 @@
1
+ "use client";
2
+ import { cn as e, useControllableSignal as t, useSignal as n, useSignalEffect as r, useSignals as i } from "@cascivo/core";
3
+ import { builtin as a, t as o } from "@cascivo/i18n";
4
+ import { useRef as s } from "react";
5
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
+ //#region src/engine/tokenize.ts
7
+ var u = 5e3, d = /* @__PURE__ */ new Map(), f = "\0";
8
+ function p(e, t, n) {
9
+ let r = `${e.name}${f}${n}${f}${t}`, i = d.get(r);
10
+ if (i) return d.delete(r), d.set(r, i), i;
11
+ let { tokens: a, state: o } = e.tokenizeLine(t, n), s = {
12
+ tokens: a,
13
+ endState: o
14
+ };
15
+ if (d.set(r, s), d.size > u) {
16
+ let e = d.keys().next().value;
17
+ e !== void 0 && d.delete(e);
18
+ }
19
+ return s;
20
+ }
21
+ function m(e, t) {
22
+ let n = t.split("\n"), r = [], i = e.initialState;
23
+ for (let t of n) {
24
+ let n = p(e, t, i);
25
+ r.push(n.tokens), i = n.endState;
26
+ }
27
+ return r;
28
+ }
29
+ //#endregion
30
+ //#region src/engine/registry.ts
31
+ var h = /* @__PURE__ */ new Map(), g = {
32
+ name: "plaintext",
33
+ initialState: "default",
34
+ tokenizeLine: (e) => ({
35
+ tokens: e.length > 0 ? [{
36
+ kind: "plain",
37
+ value: e
38
+ }] : [],
39
+ state: "default"
40
+ })
41
+ };
42
+ h.set(g.name, g);
43
+ function _(e) {
44
+ h.set(e.name, e);
45
+ }
46
+ function v(e) {
47
+ if (e !== void 0) {
48
+ let t = h.get(e);
49
+ if (t) return t;
50
+ }
51
+ return h.get("plaintext");
52
+ }
53
+ function y() {
54
+ return [...h.keys()];
55
+ }
56
+ //#endregion
57
+ //#region src/grammars/rules.ts
58
+ function b(e) {
59
+ let t = {};
60
+ for (let [n, r] of Object.entries(e.states)) t[n] = r.map((e) => {
61
+ let t = `${e.match.flags.replace(/[gy]/g, "")}y`, n = {
62
+ re: new RegExp(e.match.source, t),
63
+ kind: e.kind
64
+ };
65
+ return e.push !== void 0 && (n.push = e.push), e.pop !== void 0 && (n.pop = e.pop), n;
66
+ });
67
+ return {
68
+ name: e.name,
69
+ initialState: "default",
70
+ tokenizeLine(e, n) {
71
+ let r = t[n] ? n : "default", i = [], a = -1, o = 0, s = (t) => {
72
+ a >= 0 && (i.push({
73
+ kind: "plain",
74
+ value: e.slice(a, t)
75
+ }), a = -1);
76
+ };
77
+ for (; o < e.length;) {
78
+ let n = t[r], c = !1;
79
+ for (let a of n) {
80
+ a.re.lastIndex = o;
81
+ let n = a.re.exec(e);
82
+ if (n !== null && n.index === o && n[0].length > 0) {
83
+ s(o), i.push({
84
+ kind: a.kind,
85
+ value: n[0]
86
+ }), o += n[0].length, a.pop ? r = "default" : a.push && t[a.push] && (r = a.push), c = !0;
87
+ break;
88
+ }
89
+ }
90
+ c || (a < 0 && (a = o), o++);
91
+ }
92
+ return s(e.length), {
93
+ tokens: i,
94
+ state: r
95
+ };
96
+ }
97
+ };
98
+ }
99
+ //#endregion
100
+ //#region src/grammars/plaintext.ts
101
+ var x = {
102
+ name: "plaintext",
103
+ initialState: "default",
104
+ tokenizeLine: (e) => ({
105
+ tokens: e.length > 0 ? [{
106
+ kind: "plain",
107
+ value: e
108
+ }] : [],
109
+ state: "default"
110
+ })
111
+ };
112
+ _(x);
113
+ //#endregion
114
+ //#region src/grammars/json.ts
115
+ var S = b({
116
+ name: "json",
117
+ states: { default: [
118
+ {
119
+ match: /"(?:[^"\\]|\\.)*"(?=\s*:)/,
120
+ kind: "property"
121
+ },
122
+ {
123
+ match: /"(?:[^"\\]|\\.)*"/,
124
+ kind: "string"
125
+ },
126
+ {
127
+ match: /\b(?:true|false|null)\b/,
128
+ kind: "boolean"
129
+ },
130
+ {
131
+ match: /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/,
132
+ kind: "number"
133
+ },
134
+ {
135
+ match: /[{}[\],]/,
136
+ kind: "punctuation"
137
+ },
138
+ {
139
+ match: /:/,
140
+ kind: "operator"
141
+ }
142
+ ] }
143
+ });
144
+ _(S);
145
+ //#endregion
146
+ //#region src/grammars/clike.ts
147
+ var C = /* @__PURE__ */ "async.await.break.case.catch.class.const.continue.debugger.default.delete.do.else.export.extends.finally.for.function.if.import.in.instanceof.let.new.of.return.static.super.switch.this.throw.try.typeof.var.void.while.with.yield.as.from.get.set".split(".");
148
+ function w(e) {
149
+ return RegExp(`(?:${e.join("|")})\\b`);
150
+ }
151
+ function T(e, t, n = []) {
152
+ let r = [
153
+ {
154
+ match: /\/\/.*/,
155
+ kind: "comment"
156
+ },
157
+ {
158
+ match: /\/\*/,
159
+ kind: "comment",
160
+ push: "block"
161
+ },
162
+ {
163
+ match: /`/,
164
+ kind: "string",
165
+ push: "template"
166
+ },
167
+ {
168
+ match: /'(?:[^'\\]|\\.)*'/,
169
+ kind: "string"
170
+ },
171
+ {
172
+ match: /"(?:[^"\\]|\\.)*"/,
173
+ kind: "string"
174
+ },
175
+ {
176
+ match: /0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|(?:\d[\d_]*\.?\d*|\.\d+)(?:[eE][+-]?\d+)?n?/,
177
+ kind: "number"
178
+ },
179
+ {
180
+ match: /\b(?:true|false|null|undefined|NaN|Infinity)\b/,
181
+ kind: "boolean"
182
+ }
183
+ ];
184
+ return n.length > 0 && r.push({
185
+ match: w(n),
186
+ kind: "type"
187
+ }), r.push({
188
+ match: w(t),
189
+ kind: "keyword"
190
+ }, {
191
+ match: /[A-Za-z_$][\w$]*(?=\s*\()/,
192
+ kind: "function"
193
+ }, {
194
+ match: /[A-Z][\w$]*/,
195
+ kind: "type"
196
+ }, {
197
+ match: /[A-Za-z_$][\w$]*/,
198
+ kind: "variable"
199
+ }, {
200
+ match: /[+\-*/%=<>!&|^~?]+/,
201
+ kind: "operator"
202
+ }, {
203
+ match: /[{}()[\];,.:]/,
204
+ kind: "punctuation"
205
+ }), b({
206
+ name: e,
207
+ states: {
208
+ default: r,
209
+ block: [
210
+ {
211
+ match: /\*\//,
212
+ kind: "comment",
213
+ pop: !0
214
+ },
215
+ {
216
+ match: /[^*]+/,
217
+ kind: "comment"
218
+ },
219
+ {
220
+ match: /\*/,
221
+ kind: "comment"
222
+ }
223
+ ],
224
+ template: [
225
+ {
226
+ match: /\\./,
227
+ kind: "string"
228
+ },
229
+ {
230
+ match: /`/,
231
+ kind: "string",
232
+ pop: !0
233
+ },
234
+ {
235
+ match: /\$\{/,
236
+ kind: "punctuation"
237
+ },
238
+ {
239
+ match: /[^`\\$]+/,
240
+ kind: "string"
241
+ },
242
+ {
243
+ match: /\$/,
244
+ kind: "string"
245
+ }
246
+ ]
247
+ }
248
+ });
249
+ }
250
+ //#endregion
251
+ //#region src/grammars/javascript.ts
252
+ var E = T("javascript", C);
253
+ _(E);
254
+ var D = T("typescript", C, [
255
+ "interface",
256
+ "type",
257
+ "enum",
258
+ "implements",
259
+ "namespace",
260
+ "declare",
261
+ "abstract",
262
+ "readonly",
263
+ "public",
264
+ "private",
265
+ "protected",
266
+ "keyof",
267
+ "infer",
268
+ "satisfies",
269
+ "is",
270
+ "asserts",
271
+ "override",
272
+ "string",
273
+ "number",
274
+ "boolean",
275
+ "any",
276
+ "unknown",
277
+ "never",
278
+ "object"
279
+ ]);
280
+ _(D);
281
+ //#endregion
282
+ //#region src/grammars/css.ts
283
+ var O = b({
284
+ name: "css",
285
+ states: {
286
+ default: [
287
+ {
288
+ match: /\/\*/,
289
+ kind: "comment",
290
+ push: "block"
291
+ },
292
+ {
293
+ match: /@[\w-]+/,
294
+ kind: "keyword"
295
+ },
296
+ {
297
+ match: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/,
298
+ kind: "string"
299
+ },
300
+ {
301
+ match: /#[0-9a-fA-F]{3,8}\b/,
302
+ kind: "number"
303
+ },
304
+ {
305
+ match: /-?\d+(?:\.\d+)?(?:px|rem|em|%|vh|vw|s|ms|deg|fr|ch|ex)?/,
306
+ kind: "number"
307
+ },
308
+ {
309
+ match: /--[\w-]+/,
310
+ kind: "variable"
311
+ },
312
+ {
313
+ match: /[.#][\w-]+/,
314
+ kind: "tag"
315
+ },
316
+ {
317
+ match: /&|::?[\w-]+/,
318
+ kind: "tag"
319
+ },
320
+ {
321
+ match: /[\w-]+(?=\s*:)/,
322
+ kind: "property"
323
+ },
324
+ {
325
+ match: /[\w-]+(?=\s*\()/,
326
+ kind: "function"
327
+ },
328
+ {
329
+ match: /[{}();,:]/,
330
+ kind: "punctuation"
331
+ }
332
+ ],
333
+ block: [
334
+ {
335
+ match: /\*\//,
336
+ kind: "comment",
337
+ pop: !0
338
+ },
339
+ {
340
+ match: /[^*]+/,
341
+ kind: "comment"
342
+ },
343
+ {
344
+ match: /\*/,
345
+ kind: "comment"
346
+ }
347
+ ]
348
+ }
349
+ });
350
+ _(O);
351
+ //#endregion
352
+ //#region src/grammars/html.ts
353
+ var k = b({
354
+ name: "html",
355
+ states: {
356
+ default: [
357
+ {
358
+ match: /<!--/,
359
+ kind: "comment",
360
+ push: "comment"
361
+ },
362
+ {
363
+ match: /<!?\/?[A-Za-z][\w-]*/,
364
+ kind: "tag",
365
+ push: "tag"
366
+ },
367
+ {
368
+ match: /&[a-zA-Z#0-9]+;/,
369
+ kind: "boolean"
370
+ }
371
+ ],
372
+ comment: [
373
+ {
374
+ match: /-->/,
375
+ kind: "comment",
376
+ pop: !0
377
+ },
378
+ {
379
+ match: /[^-]+/,
380
+ kind: "comment"
381
+ },
382
+ {
383
+ match: /-/,
384
+ kind: "comment"
385
+ }
386
+ ],
387
+ tag: [
388
+ {
389
+ match: /\/?>/,
390
+ kind: "tag",
391
+ pop: !0
392
+ },
393
+ {
394
+ match: /"(?:[^"\\]|\\.)*"|'[^']*'/,
395
+ kind: "string"
396
+ },
397
+ {
398
+ match: /[\w-]+(?=\s*=)/,
399
+ kind: "attr"
400
+ },
401
+ {
402
+ match: /=/,
403
+ kind: "operator"
404
+ },
405
+ {
406
+ match: /[\w-]+/,
407
+ kind: "attr"
408
+ }
409
+ ]
410
+ }
411
+ });
412
+ _(k);
413
+ //#endregion
414
+ //#region src/grammars/markdown.ts
415
+ var A = b({
416
+ name: "markdown",
417
+ states: {
418
+ default: [
419
+ {
420
+ match: /^```.*/,
421
+ kind: "keyword",
422
+ push: "fence"
423
+ },
424
+ {
425
+ match: /^#{1,6}\s.*/,
426
+ kind: "keyword"
427
+ },
428
+ {
429
+ match: /^\s*(?:[-*+]|\d+\.)\s/,
430
+ kind: "operator"
431
+ },
432
+ {
433
+ match: /^\s*>\s.*/,
434
+ kind: "comment"
435
+ },
436
+ {
437
+ match: /`[^`]+`/,
438
+ kind: "string"
439
+ },
440
+ {
441
+ match: /\*\*[^*]+\*\*|__[^_]+__/,
442
+ kind: "keyword"
443
+ },
444
+ {
445
+ match: /\*[^*\s][^*]*\*|_[^_\s][^_]*_/,
446
+ kind: "type"
447
+ },
448
+ {
449
+ match: /!?\[[^\]]*\]\([^)]*\)/,
450
+ kind: "function"
451
+ }
452
+ ],
453
+ fence: [{
454
+ match: /^```.*/,
455
+ kind: "keyword",
456
+ pop: !0
457
+ }, {
458
+ match: /.+/,
459
+ kind: "string"
460
+ }]
461
+ }
462
+ });
463
+ _(A);
464
+ //#endregion
465
+ //#region src/grammars/bash.ts
466
+ var j = b({
467
+ name: "bash",
468
+ states: { default: [
469
+ {
470
+ match: /#.*/,
471
+ kind: "comment"
472
+ },
473
+ {
474
+ match: /"(?:[^"\\]|\\.)*"/,
475
+ kind: "string"
476
+ },
477
+ {
478
+ match: /'[^']*'/,
479
+ kind: "string"
480
+ },
481
+ {
482
+ match: /\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|return|exit|break|continue|local|export|readonly|declare|echo|cd|source|set|unset)\b/,
483
+ kind: "keyword"
484
+ },
485
+ {
486
+ match: /\$\{[^}]*\}|\$[A-Za-z_]\w*|\$[0-9@*#?$!]/,
487
+ kind: "variable"
488
+ },
489
+ {
490
+ match: /-?\b\d+\b/,
491
+ kind: "number"
492
+ },
493
+ {
494
+ match: /[A-Za-z_][\w-]*(?=\s*\(\s*\))/,
495
+ kind: "function"
496
+ },
497
+ {
498
+ match: /[|&;<>()]+|&&|\|\||=/,
499
+ kind: "operator"
500
+ }
501
+ ] }
502
+ });
503
+ _(j);
504
+ var M = {
505
+ root: "_root_4k2k5_2",
506
+ gutter: "_gutter_4k2k5_17",
507
+ gutterLine: "_gutterLine_4k2k5_28",
508
+ pre: "_pre_4k2k5_33",
509
+ code: "_code_4k2k5_45",
510
+ line: "_line_4k2k5_50",
511
+ plain: "_plain_4k2k5_65",
512
+ keyword: "_keyword_4k2k5_68",
513
+ string: "_string_4k2k5_71",
514
+ number: "_number_4k2k5_74",
515
+ comment: "_comment_4k2k5_77",
516
+ function: "_function_4k2k5_81",
517
+ type: "_type_4k2k5_84",
518
+ operator: "_operator_4k2k5_87",
519
+ punctuation: "_punctuation_4k2k5_90",
520
+ variable: "_variable_4k2k5_93",
521
+ tag: "_tag_4k2k5_96",
522
+ attr: "_attr_4k2k5_99",
523
+ regexp: "_regexp_4k2k5_102",
524
+ boolean: "_boolean_4k2k5_105",
525
+ property: "_property_4k2k5_108"
526
+ };
527
+ //#endregion
528
+ //#region src/editor/view.tsx
529
+ function N(e, t = 0, n = e.length) {
530
+ let r = [];
531
+ for (let i = t; i < n; i++) {
532
+ let t = e[i];
533
+ r.push(/* @__PURE__ */ c("span", {
534
+ className: M.line,
535
+ children: t.length === 0 ? "​" : t.map((e, t) => /* @__PURE__ */ c("span", {
536
+ className: M[e.kind],
537
+ children: e.value
538
+ }, t))
539
+ }, i));
540
+ }
541
+ return r;
542
+ }
543
+ function P({ count: e, className: t, gutterRef: n, start: r = 0, end: i = e, topPad: a = 0, bottomPad: o = 0 }) {
544
+ let s = [];
545
+ for (let e = r + 1; e <= i; e++) s.push(/* @__PURE__ */ c("span", {
546
+ className: M.gutterLine,
547
+ children: e
548
+ }, e));
549
+ return /* @__PURE__ */ l("div", {
550
+ ref: n,
551
+ className: t,
552
+ "aria-hidden": "true",
553
+ children: [
554
+ a > 0 && /* @__PURE__ */ c("div", { style: { blockSize: a } }),
555
+ s,
556
+ o > 0 && /* @__PURE__ */ c("div", { style: { blockSize: o } })
557
+ ]
558
+ });
559
+ }
560
+ var F = {
561
+ root: "_root_q71hw_2",
562
+ textarea: "_textarea_q71hw_18",
563
+ gutter: "_gutter_q71hw_23",
564
+ codeArea: "_codeArea_q71hw_35",
565
+ pre: "_pre_q71hw_42",
566
+ currentLine: "_currentLine_q71hw_57"
567
+ }, I = 1e3, L = 12;
568
+ function R({ value: u, defaultValue: d, onValueChange: f, language: p = "plaintext", lineNumbers: h = !0, tabSize: g = 2, insertSpaces: _ = !0, wrap: y = !1, virtualize: b, readOnly: x = !1, disabled: S = !1, spellCheck: C = !1, label: w, className: T, style: E, onKeyDown: D, ...O }) {
569
+ i();
570
+ let [k, A] = t({
571
+ value: u,
572
+ defaultValue: d ?? "",
573
+ onChange: f
574
+ }), j = s(null), M = s(null), R = s(null), z = s(null), B = n(k.value), V = n(0), H = n(0), U = n(0);
575
+ r(() => {
576
+ let e = k.value;
577
+ if (typeof requestAnimationFrame != "function") {
578
+ B.value = e;
579
+ return;
580
+ }
581
+ let t = requestAnimationFrame(() => {
582
+ B.value = e;
583
+ });
584
+ return () => cancelAnimationFrame(t);
585
+ }), r(() => {
586
+ let e = z.current;
587
+ if (!e) return;
588
+ let t = () => {
589
+ let t = Number.parseFloat(getComputedStyle(e).lineHeight);
590
+ U.value = Number.isFinite(t) && t > 0 ? t : 0, H.value = e.clientHeight;
591
+ }, n = () => {
592
+ V.value = e.scrollTop, H.value = e.clientHeight, M.current && (M.current.scrollTop = e.scrollTop, M.current.scrollLeft = e.scrollLeft), R.current && (R.current.scrollTop = e.scrollTop);
593
+ }, r = () => {
594
+ let t = e.value.slice(0, e.selectionStart).split("\n").length - 1;
595
+ j.current?.style.setProperty("--cascivo-editor-caret-line", String(t));
596
+ };
597
+ return t(), r(), e.addEventListener("scroll", n), e.addEventListener("keyup", r), e.addEventListener("click", r), e.addEventListener("input", r), () => {
598
+ e.removeEventListener("scroll", n), e.removeEventListener("keyup", r), e.removeEventListener("click", r), e.removeEventListener("input", r);
599
+ };
600
+ });
601
+ let W = m(v(p), B.value), G = W.length, K = U.value, q = (b ?? G > I) && !y && K > 0, J = 0, Y = G;
602
+ if (q) {
603
+ J = Math.max(0, Math.floor(V.value / K) - L);
604
+ let e = Math.ceil(H.value / K);
605
+ Y = Math.min(G, J + e + L * 2);
606
+ }
607
+ let X = J * K, Z = (G - Y) * K, Q = (e) => {
608
+ if (e.key === "Tab" && !x && !S) {
609
+ e.preventDefault();
610
+ let t = e.currentTarget, { selectionStart: n, selectionEnd: r, value: i } = t, a = _ ? " ".repeat(g) : " ", o = i.lastIndexOf("\n", n - 1) + 1;
611
+ if (e.shiftKey) {
612
+ let e = i.slice(o, r).replace(RegExp(`^(?:\\t| {1,${g}})`, "gm"), "");
613
+ t.setRangeText(e, o, r, "select");
614
+ } else if (n === r) t.setRangeText(a, n, r, "end");
615
+ else {
616
+ let e = i.slice(o, r).replace(/^/gm, a);
617
+ t.setRangeText(e, o, r, "select");
618
+ }
619
+ A(t.value);
620
+ }
621
+ D?.(e);
622
+ }, $ = {
623
+ "--cascivo-editor-tab-size": g,
624
+ ...E
625
+ };
626
+ return /* @__PURE__ */ l("div", {
627
+ ref: j,
628
+ className: e(F.root, T),
629
+ "data-wrap": y,
630
+ "data-line-numbers": h,
631
+ style: $,
632
+ children: [h && /* @__PURE__ */ c(P, {
633
+ count: G,
634
+ className: F.gutter,
635
+ gutterRef: R,
636
+ start: J,
637
+ end: Y,
638
+ topPad: X,
639
+ bottomPad: Z
640
+ }), /* @__PURE__ */ l("div", {
641
+ className: F.codeArea,
642
+ children: [/* @__PURE__ */ l("pre", {
643
+ ref: M,
644
+ className: F.pre,
645
+ "aria-hidden": "true",
646
+ children: [
647
+ /* @__PURE__ */ c("div", { className: F.currentLine }),
648
+ X > 0 && /* @__PURE__ */ c("div", { style: { blockSize: X } }),
649
+ /* @__PURE__ */ c("code", { children: N(W, J, Y) }),
650
+ Z > 0 && /* @__PURE__ */ c("div", { style: { blockSize: Z } })
651
+ ]
652
+ }), /* @__PURE__ */ c("textarea", {
653
+ ref: z,
654
+ className: F.textarea,
655
+ value: k.value,
656
+ onChange: (e) => A(e.currentTarget.value),
657
+ onKeyDown: Q,
658
+ readOnly: x,
659
+ disabled: S,
660
+ spellCheck: C,
661
+ "aria-label": w ?? o(a.editor.label),
662
+ autoCapitalize: "off",
663
+ autoCorrect: "off",
664
+ autoComplete: "off",
665
+ wrap: y ? "soft" : "off",
666
+ ...O
667
+ })]
668
+ })]
669
+ });
670
+ }
671
+ //#endregion
672
+ //#region src/editor/highlight/highlight.tsx
673
+ function z({ value: t, language: n = "plaintext", lineNumbers: r = !1, wrap: a = !1, tabSize: o = 2, label: s, className: u, preRef: d, gutterRef: f, style: p, ...h }) {
674
+ i();
675
+ let g = m(v(n), t), _ = {
676
+ "--cascivo-editor-tab-size": o,
677
+ ...p
678
+ };
679
+ return /* @__PURE__ */ l("div", {
680
+ className: e(M.root, u),
681
+ "data-wrap": a,
682
+ "data-line-numbers": r,
683
+ style: _,
684
+ "aria-label": s,
685
+ ...h,
686
+ children: [r && /* @__PURE__ */ c(P, {
687
+ count: g.length,
688
+ className: M.gutter,
689
+ gutterRef: f
690
+ }), /* @__PURE__ */ c("pre", {
691
+ ref: d,
692
+ className: M.pre,
693
+ children: /* @__PURE__ */ c("code", {
694
+ className: M.code,
695
+ children: N(g)
696
+ })
697
+ })]
698
+ });
699
+ }
700
+ //#endregion
701
+ export { R as CodeEditor, z as Highlight, j as bash, b as createRuleGrammar, O as css, v as getGrammar, k as html, E as javascript, S as json, y as listGrammars, A as markdown, x as plaintext, _ as registerGrammar, p as tokenize, m as tokenizeDocument, D as typescript };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@cascivo/editor",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "description": "Lightweight CSS-native code editor — native textarea overlay + owned zero-dependency tokenizer",
6
+ "keywords": [
7
+ "cascivo",
8
+ "code-editor",
9
+ "css",
10
+ "design-system",
11
+ "react",
12
+ "signals",
13
+ "syntax-highlighting",
14
+ "textarea"
15
+ ],
16
+ "homepage": "https://github.com/cascivo/cascivo/tree/main/packages/editor#readme",
17
+ "bugs": "https://github.com/cascivo/cascivo/issues",
18
+ "license": "MIT",
19
+ "author": "urbanisierung",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/cascivo/cascivo.git",
23
+ "directory": "packages/editor"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": [
30
+ "**/*.css"
31
+ ],
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./styles.css": "./dist/editor.css"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "provenance": true
44
+ },
45
+ "scripts": {
46
+ "build": "vp build && node scripts/flatten-types.mjs && node scripts/check-types-flat.mjs",
47
+ "check": "tsc --noEmit",
48
+ "test": "vp test"
49
+ },
50
+ "dependencies": {
51
+ "@cascivo/core": "workspace:^",
52
+ "@cascivo/i18n": "workspace:^"
53
+ },
54
+ "devDependencies": {
55
+ "@testing-library/jest-dom": "catalog:",
56
+ "@testing-library/react": "catalog:",
57
+ "@testing-library/user-event": "catalog:",
58
+ "@types/react": "catalog:",
59
+ "@types/react-dom": "catalog:",
60
+ "fast-check": "catalog:",
61
+ "jsdom": "catalog:",
62
+ "react": "catalog:",
63
+ "react-dom": "catalog:",
64
+ "typescript": "catalog:",
65
+ "vite-plus": "catalog:"
66
+ },
67
+ "peerDependencies": {
68
+ "@preact/signals-react": ">=2.0.0",
69
+ "react": ">=18.0.0",
70
+ "react-dom": ">=18.0.0"
71
+ }
72
+ }
package/readme.body.md ADDED
@@ -0,0 +1,42 @@
1
+ A lightweight, CSS-native, signal-driven code editor for cascivo. `CodeEditor` overlays a native `<textarea>` on a syntax-highlighted `<pre>`, so the browser owns the caret, selection, IME, undo, and accessibility — JS is limited to a tiny owned tokenizer and scroll-sync. `Highlight` is the read-only renderer for snippets and docs. Zero runtime dependencies, themeable through the cascivo token system.
2
+
3
+ ## Install
4
+
5
+ ```sh
6
+ pnpm add @cascivo/editor @preact/signals-react
7
+ ```
8
+
9
+ `react`, `react-dom`, and `@preact/signals-react` are peer dependencies — install them in your app.
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { CodeEditor } from '@cascivo/editor'
15
+ import '@cascivo/editor/styles.css' // required — maps to the dist `editor.css`
16
+ import '@cascivo/themes/dark.css' // any cascivo theme drives the editor colors
17
+
18
+ export function Example() {
19
+ return <CodeEditor language="typescript" lineNumbers defaultValue={`const x = 1\n`} />
20
+ }
21
+ ```
22
+
23
+ The editor is CSS-token-themed: drop it inside any element carrying a `data-theme` (or import a
24
+ theme stylesheet) and it picks up the same palette, radii, and typography as the rest of cascivo —
25
+ including the syntax colors, which map onto the `--cascivo-editor-syntax-*` palette.
26
+
27
+ ### Read-only highlighting
28
+
29
+ ```tsx
30
+ import { Highlight } from '@cascivo/editor'
31
+ ;<Highlight language="json" value={`{ "ok": true }`} />
32
+ ```
33
+
34
+ ### Languages
35
+
36
+ Ships small, tree-shakeable grammars: `plaintext`, `json`, `javascript`, `typescript`, `css`,
37
+ `html`, `markdown`, `bash`. Register your own with `registerGrammar(grammar)`.
38
+
39
+ ### React apps must subscribe to signals
40
+
41
+ `CodeEditor` and `Highlight` are signal-driven. In a plain React app (no Babel signals transform),
42
+ they already call `useSignals()` internally — no extra wiring is required.