@cascivo/editor 0.2.0 → 0.2.2

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.
@@ -0,0 +1,2 @@
1
+ @layer cascivo.component{._root_1m6ns_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_1m6ns_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_1m6ns_28{min-block-size:1lh;display:block}._gutterActive_1m6ns_35{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-gutter-active,var(--cascivo-editor-current-line));pointer-events:none;position:absolute}._pre_1m6ns_45{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_1m6ns_57{font:inherit}._line_1m6ns_62{min-block-size:1lh;display:block}._root_1m6ns_2[data-wrap=true] ._pre_1m6ns_45{white-space:pre-wrap;word-break:break-word}._root_1m6ns_2[data-line-numbers=false] ._gutter_1m6ns_17{display:none}._plain_1m6ns_77{color:var(--cascivo-editor-fg)}._keyword_1m6ns_80{color:var(--cascivo-editor-syntax-keyword)}._string_1m6ns_83{color:var(--cascivo-editor-syntax-string)}._number_1m6ns_86{color:var(--cascivo-editor-syntax-number)}._comment_1m6ns_89{color:var(--cascivo-editor-syntax-comment);font-style:italic}._function_1m6ns_93{color:var(--cascivo-editor-syntax-function)}._type_1m6ns_96{color:var(--cascivo-editor-syntax-type)}._operator_1m6ns_99{color:var(--cascivo-editor-syntax-operator)}._punctuation_1m6ns_102{color:var(--cascivo-editor-syntax-punctuation)}._variable_1m6ns_105{color:var(--cascivo-editor-syntax-variable)}._tag_1m6ns_108{color:var(--cascivo-editor-syntax-tag)}._attr_1m6ns_111{color:var(--cascivo-editor-syntax-attr)}._regexp_1m6ns_114{color:var(--cascivo-editor-syntax-regexp)}._boolean_1m6ns_117{color:var(--cascivo-editor-syntax-boolean)}._property_1m6ns_120{color:var(--cascivo-editor-syntax-property)}._match_1m6ns_125{background:var(--cascivo-editor-match,#7dd3fc47);border-radius:2px}._matchCurrent_1m6ns_129{background:var(--cascivo-editor-match-current,#facc1580);border-radius:2px}._bracketMatch_1m6ns_133{outline:1px solid var(--cascivo-editor-bracket,currentColor);border-radius:2px}._root_18xci_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_18xci_2:has(._textarea_18xci_18:focus-visible){box-shadow:var(--cascivo-focus-ring);border-color:var(--cascivo-color-accent)}._gutter_18xci_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);position:relative;overflow:hidden}._codeArea_18xci_36{flex:auto;min-inline-size:0;position:relative}._pre_18xci_43{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_18xci_58{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_18xci_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_18xci_18::placeholder{color:var(--cascivo-editor-gutter-fg)}._textarea_18xci_18::selection{background:var(--cascivo-editor-selection)}._textarea_18xci_18:disabled{cursor:not-allowed;opacity:var(--cascivo-disabled-opacity)}._root_18xci_2[data-wrap=true] ._pre_18xci_43,._root_18xci_2[data-wrap=true] ._textarea_18xci_18{white-space:pre-wrap;word-break:break-word}._root_18xci_2[data-line-numbers=false] ._gutter_18xci_23{display:none}@media (pointer:coarse){._root_18xci_2{min-block-size:var(--cascivo-target-min-coarse,2.75rem)}}@media (prefers-reduced-motion:reduce){._root_18xci_2{transition:none}}._panel_hawkv_2{z-index:2;gap:var(--cascivo-space-1);padding:var(--cascivo-space-2);background:var(--cascivo-editor-bg);border:1px solid var(--cascivo-editor-border);border-radius:var(--cascivo-radius-field);box-shadow:var(--cascivo-shadow-md,0 4px 12px #0003);font-family:var(--cascivo-font-sans);font-size:var(--cascivo-text-sm);color:var(--cascivo-editor-fg);flex-direction:column;display:flex;position:absolute;inset-block-start:var(--cascivo-space-2);inset-inline-end:var(--cascivo-space-2)}._row_hawkv_20{align-items:center;gap:var(--cascivo-space-1);display:flex}._input_hawkv_26{min-inline-size:8rem;padding-block:var(--cascivo-space-1);padding-inline:var(--cascivo-space-2);background:var(--cascivo-editor-gutter-bg,transparent);color:inherit;border:1px solid var(--cascivo-editor-border);border-radius:var(--cascivo-radius-field);font:inherit;flex:auto}._input_hawkv_26:focus-visible{border-color:var(--cascivo-color-accent);box-shadow:var(--cascivo-focus-ring);outline:none}._count_hawkv_44{padding-inline:var(--cascivo-space-1);color:var(--cascivo-editor-gutter-fg);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none}._button_hawkv_52,._toggle_hawkv_53{min-inline-size:1.75rem;padding-block:var(--cascivo-space-1);padding-inline:var(--cascivo-space-2);color:inherit;border-radius:var(--cascivo-radius-field);font:inherit;cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center;align-items:center;line-height:1;display:inline-flex}._button_hawkv_52:hover,._toggle_hawkv_53:hover{background:var(--cascivo-editor-current-line)}._button_hawkv_52:focus-visible,._toggle_hawkv_53:focus-visible{box-shadow:var(--cascivo-focus-ring);outline:none}._active_hawkv_80{border-color:var(--cascivo-color-accent);color:var(--cascivo-color-accent)}@media (pointer:coarse){._button_hawkv_52,._toggle_hawkv_53,._input_hawkv_26{min-block-size:var(--cascivo-target-min-coarse,2.75rem)}}}@media (forced-colors:active){._root_18xci_2{border:1px solid fieldtext}._pre_18xci_43{display:none}._textarea_18xci_18{color:canvastext;caret-color:canvastext}}
2
+ /*$vite$:1*/
@@ -0,0 +1,293 @@
1
+ import { HTMLAttributes, KeyboardEvent, 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
+ /**
35
+ * A persistent, mutable index of per-line grammar **end-states** — the state
36
+ * carried *after* each line, which is exactly the start-state of the next line.
37
+ * It lets a windowed tokenizer obtain the start-state of any visible line as a
38
+ * read (or a lazy compute) instead of re-walking the whole document from line 0
39
+ * on every render.
40
+ *
41
+ * `endStates[i]` is the state after line `i`; the start-state of line `i` is
42
+ * therefore `endStates[i-1]` (line 0 starts at `grammar.initialState`). The array
43
+ * is always a contiguous prefix `[0, length-1]` — methods never leave gaps.
44
+ *
45
+ * Framework-free: pure data + methods, no React, no signals. The component holds
46
+ * one in a `useRef` as mutable infrastructure (like the undo/redo history handle).
47
+ */
48
+ interface LineStateIndex {
49
+ /** The grammar this index threads state for. */
50
+ readonly grammar: Grammar;
51
+ /** Count of known end-states (always a contiguous prefix from line 0). */
52
+ readonly length: number;
53
+ /** Lazily compute + memoize end-states up to (and including) line `upto`. */
54
+ ensure(lines: readonly string[], upto: number): void;
55
+ /** The threaded start-state for line `i` (ensures the prefix as a side effect). */
56
+ startStateOf(lines: readonly string[], i: number): GrammarState;
57
+ /** Record the end-state of line `i` (append at the prefix end, or overwrite). */
58
+ setEndState(i: number, state: GrammarState): void;
59
+ /** Drop the end-state of `line` and everything after it (called on an edit). */
60
+ invalidateFrom(line: number): void;
61
+ }
62
+ /** Create an empty {@link LineStateIndex} for `grammar`. */
63
+ declare function createLineStateIndex(grammar: Grammar): LineStateIndex;
64
+ /** Result of tokenizing one line: spans plus the continuation state. */
65
+ interface TokenizeResult {
66
+ tokens: Token[];
67
+ endState: GrammarState;
68
+ }
69
+ /**
70
+ * Tokenize a single line, restartable from `startState`, memoized. Pure from the
71
+ * caller's perspective: identical `(grammar, line, startState)` yield identical
72
+ * results (a cache hit returns the previously-computed value).
73
+ */
74
+ declare function tokenize(grammar: Grammar, line: string, startState: GrammarState): TokenizeResult;
75
+ /**
76
+ * Tokenize a whole document, threading each line's `endState` into the next so
77
+ * block comments / template literals / fenced code continue across lines.
78
+ * Returns one token array per line. The per-line memo makes a single-line edit
79
+ * re-tokenize only the changed line(s) until the start-state reconverges.
80
+ */
81
+ declare function tokenizeDocument(grammar: Grammar, text: string): Token[][];
82
+ /**
83
+ * Tokenize only the line range `[from, to)`, using a {@link LineStateIndex} for
84
+ * the window's seed state instead of re-walking the document from line 0. The
85
+ * index supplies the start-state of `from` (computing the prefix lazily, once),
86
+ * and each line's fresh end-state is written back into the index so later windows
87
+ * reuse it — making per-render work O(viewport), not O(document).
88
+ *
89
+ * Returns a plain `Token[][]` of length `to - from`: element `j` is line
90
+ * `from + j`. The consumer knows the window starts at `from`, so alignment with
91
+ * the rendered rows stays trivial. Output is byte-for-byte identical to
92
+ * `tokenizeDocument(grammar, lines.join('\n')).slice(from, to)`.
93
+ */
94
+ declare function tokenizeRange(grammar: Grammar, lines: readonly string[], from: number, to: number, index: LineStateIndex): Token[][];
95
+ /** Register (or replace) a grammar under its `name`. */
96
+ declare function registerGrammar(grammar: Grammar): void;
97
+ /** Resolve a grammar by name, falling back to `plaintext` for unknown languages. */
98
+ declare function getGrammar(name?: string): Grammar;
99
+ /** List the names of all currently-registered grammars. */
100
+ declare function listGrammars(): string[];
101
+ /**
102
+ * One tokenizer rule. `match` is anchored at the current scan position (compiled
103
+ * sticky internally). On a non-empty match the matched text becomes a token of
104
+ * `kind`, the position advances, and the state transitions via `push`/`pop`.
105
+ */
106
+ interface Rule {
107
+ match: RegExp;
108
+ kind: TokenKind;
109
+ /** Switch to this state after matching (for entering multi-line constructs). */
110
+ push?: string;
111
+ /** Return to the `default` state after matching (for leaving them). */
112
+ pop?: boolean;
113
+ }
114
+ interface RuleSpec {
115
+ name: string;
116
+ /** Per-state ordered rule lists. Must include a `default` state. */
117
+ states: Record<string, Rule[]>;
118
+ }
119
+ /**
120
+ * Build a {@link Grammar} from ordered, state-keyed regex rules — a tiny
121
+ * Monarch/Prism-style engine. At each position, the first rule in the current
122
+ * state whose sticky regex matches wins; unmatched characters accumulate into
123
+ * `plain` tokens, so the tokenizer is always lossless. State carries across
124
+ * lines for block comments, template literals, and fenced code.
125
+ */
126
+ declare function createRuleGrammar(spec: RuleSpec): Grammar;
127
+ /** The trivial fallback grammar: one uncolored `plain` token per line. */
128
+ declare const plaintext: Grammar;
129
+ /**
130
+ * JSON: property keys vs string values, numbers, booleans/null, punctuation.
131
+ * A string immediately followed by a colon is a property key; other strings are
132
+ * values.
133
+ */
134
+ declare const json: Grammar;
135
+ /** JavaScript: keywords, strings (incl. multi-line template literals), numbers,
136
+ * line + block comments (state carry), function calls, identifiers, operators. */
137
+ declare const javascript: Grammar;
138
+ declare const typescript: Grammar;
139
+ /**
140
+ * CSS: at-rules, selectors, property names, values, numbers/units, strings, and
141
+ * `/* *\/` block comments (state carried across lines).
142
+ */
143
+ declare const css: Grammar;
144
+ /**
145
+ * HTML: tags, attribute names/values, `<!-- -->` comments (state carried across
146
+ * lines), and text. Inside a tag, attributes and strings are colored until `>`.
147
+ */
148
+ declare const html: Grammar;
149
+ /**
150
+ * Markdown for a notes/wiki surface: ATX headings, horizontal rules, task &
151
+ * ordered/unordered lists, blockquotes, inline + fenced code (state carried across
152
+ * lines), strong/emphasis/strikethrough, and links/images. Line-based, so setext
153
+ * headings and CommonMark tables/footnotes are intentionally out of scope; the
154
+ * fence body is colored uniformly (embedded-language highlight is a future step).
155
+ */
156
+ declare const markdown: Grammar;
157
+ /** Bash: comments, strings, keywords, variables (`$VAR` / `${...}`), operators. */
158
+ declare const bash: Grammar;
159
+ /**
160
+ * A visual decoration: a `[start, end)` column range on a given line tagged with a
161
+ * CSS class. The overlay layer splits token spans at decoration boundaries and adds
162
+ * the class to the covered slice. Used internally for search matches and bracket
163
+ * pairs, and exposed publicly via the editor's `decorations` prop.
164
+ */
165
+ interface Decoration {
166
+ line: number;
167
+ start: number;
168
+ end: number;
169
+ className: string;
170
+ }
171
+ /** What a {@link Command} receives: the live textarea, the originating event, and the commit fn. */
172
+ interface CommandContext {
173
+ textarea: HTMLTextAreaElement;
174
+ event: KeyboardEvent<HTMLTextAreaElement>;
175
+ /** Commit a new document value through the editor's controllable signal + history. */
176
+ setText: (value: string) => void;
177
+ }
178
+ /** A keymap command. Returns `true` when it handled the event (the caller then prevents default). */
179
+ type Command = (ctx: CommandContext) => boolean;
180
+ /** A public keymap: chord string → command. Merged over the built-ins (user wins). */
181
+ type KeyMap = Record<string, Command>;
182
+ /**
183
+ * Per-instance theme overrides — a partial map of `--cascivo-editor-*` custom
184
+ * properties spread onto the editor root. Swapping it re-themes live (Zen mode),
185
+ * while global `data-theme` remains the default.
186
+ */
187
+ type EditorTheme = Partial<Record<`--cascivo-editor-${string}`, string>>;
188
+ /**
189
+ * Imperative handle (via `ref`) for callers that drive their own transactions —
190
+ * a remote pull, a programmatic insert, a reset. Edits route through the same
191
+ * history as keyboard edits, so they stay undoable.
192
+ */
193
+ interface CodeEditorHandle {
194
+ /** Replace `[from, to)` with `text` (an undoable edit), leaving the caret after it. */
195
+ applyEdit(range: {
196
+ from: number;
197
+ to: number;
198
+ }, text: string): void;
199
+ /** Current selection offsets. */
200
+ getSelection(): {
201
+ start: number;
202
+ end: number;
203
+ };
204
+ /** Focus the editing surface. */
205
+ focus(): void;
206
+ /** Undo / redo the owned history. */
207
+ undo(): void;
208
+ redo(): void;
209
+ /** Open the find panel. */
210
+ openFind(): void;
211
+ }
212
+ interface CodeEditorProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'defaultValue' | 'onChange' | 'wrap'> {
213
+ /** Controlled value. */
214
+ value?: string;
215
+ /** Initial value for uncontrolled use. */
216
+ defaultValue?: string;
217
+ /** Called with the new text on every edit. */
218
+ onValueChange?: (value: string) => void;
219
+ /** Grammar name (defaults to `plaintext`; unknown names fall back to it). */
220
+ language?: string;
221
+ /** Show the line-number gutter (default true). */
222
+ lineNumbers?: boolean;
223
+ /** Spaces per tab stop (default 2). */
224
+ tabSize?: number;
225
+ /** Insert spaces (default) vs a literal tab on Tab. */
226
+ insertSpaces?: boolean;
227
+ /** Soft-wrap long lines instead of scrolling horizontally (default false). */
228
+ wrap?: boolean;
229
+ /**
230
+ * Render only the visible lines for large documents. Defaults to auto
231
+ * (on above ~1000 lines); disabled when `wrap` makes row heights variable.
232
+ */
233
+ virtualize?: boolean;
234
+ /** Accessible label for the editor (defaults to the i18n "Code editor"). */
235
+ label?: string;
236
+ /** Called with the current value on `Mod-S` (the browser save dialog is suppressed). */
237
+ onSave?: (value: string) => void;
238
+ /** Extra key bindings, merged over the built-ins (user wins on the same chord). */
239
+ keymap?: KeyMap;
240
+ /** Extra decorations — offset ranges → CSS class — merged with internal ones. */
241
+ decorations?: readonly Decoration[] | ((value: string) => readonly Decoration[]);
242
+ /** Per-instance `--cascivo-editor-*` overrides; swapping it re-themes live. */
243
+ theme?: EditorTheme;
244
+ /** Highlight the bracket matching the one adjacent to the caret (default false). */
245
+ bracketMatching?: boolean;
246
+ }
247
+ /**
248
+ * A lightweight code editor: a native `<textarea>` overlaid on a syntax-highlighted
249
+ * `Highlight` layer. The browser owns editing/caret/IME/undo/a11y; JS handles only
250
+ * tokenizing, scroll-sync, and Tab/Shift-Tab indent. Signal-driven, no banned hooks.
251
+ *
252
+ * Performance: tokenization is per-line memoized, the highlight layer is
253
+ * rAF-debounced (typing never blocks), and large documents window to the visible
254
+ * range while the textarea always holds the full text.
255
+ */
256
+ declare const CodeEditor: import("react").ForwardRefExoticComponent<CodeEditorProps & import("react").RefAttributes<CodeEditorHandle>>;
257
+ interface HighlightProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
258
+ /** Code to render. */
259
+ value: string;
260
+ /** Grammar name (defaults to `plaintext`; unknown names fall back to it). */
261
+ language?: string;
262
+ /** Show the line-number gutter. */
263
+ lineNumbers?: boolean;
264
+ /** Soft-wrap long lines instead of scrolling horizontally. */
265
+ wrap?: boolean;
266
+ /** Spaces per tab stop (default 2). */
267
+ tabSize?: number;
268
+ /** Accessible label for the read-only code block. */
269
+ label?: string;
270
+ /** Ref to the scrollable `<pre>` (used by `CodeEditor` for scroll-sync). */
271
+ preRef?: Ref<HTMLPreElement>;
272
+ /** Ref to the gutter column (used by `CodeEditor` for scroll-sync). */
273
+ gutterRef?: Ref<HTMLDivElement>;
274
+ }
275
+ /**
276
+ * Read-only, syntax-highlighted `<pre><code>` rendered from the owned tokenizer.
277
+ * The same render layer `CodeEditor` overlays its `<textarea>` on. Signal-safe,
278
+ * themeable, and accessible — no banned hooks.
279
+ */
280
+ declare function Highlight({
281
+ value,
282
+ language,
283
+ lineNumbers,
284
+ wrap,
285
+ tabSize,
286
+ label,
287
+ className,
288
+ preRef,
289
+ gutterRef,
290
+ style,
291
+ ...rest
292
+ }: HighlightProps): import("react").JSX.Element;
293
+ export { CodeEditor, type CodeEditorHandle, type CodeEditorProps, type Command, type CommandContext, type Decoration, type EditorTheme, type Grammar, type GrammarState, Highlight, type HighlightProps, type KeyMap, type LineStateIndex, type LineTokens, type Rule, type RuleSpec, type Token, type TokenKind, type TokenizeResult, bash, createLineStateIndex, createRuleGrammar, css, getGrammar, html, javascript, json, listGrammars, markdown, plaintext, registerGrammar, tokenize, tokenizeDocument, tokenizeRange, typescript };