astro-dev-edit 0.11.0

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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/package.json +52 -0
  4. package/src/client/admin-bar.ts +622 -0
  5. package/src/client/api.ts +370 -0
  6. package/src/client/classify-cache.ts +61 -0
  7. package/src/client/css-inspect.ts +345 -0
  8. package/src/client/editors/asset-picker.ts +155 -0
  9. package/src/client/editors/body-editor.ts +419 -0
  10. package/src/client/editors/collections-panel.ts +1532 -0
  11. package/src/client/editors/copy-panel.ts +73 -0
  12. package/src/client/editors/drawer.ts +95 -0
  13. package/src/client/editors/entry.ts +433 -0
  14. package/src/client/editors/expression.ts +77 -0
  15. package/src/client/editors/fields.ts +309 -0
  16. package/src/client/editors/image.ts +268 -0
  17. package/src/client/editors/markup-insert.ts +73 -0
  18. package/src/client/editors/markup.ts +125 -0
  19. package/src/client/editors/media-grid.ts +326 -0
  20. package/src/client/editors/media-modal.ts +588 -0
  21. package/src/client/editors/notice.ts +160 -0
  22. package/src/client/editors/peek.ts +135 -0
  23. package/src/client/editors/settings-panel.ts +457 -0
  24. package/src/client/editors/source-popup.ts +166 -0
  25. package/src/client/editors/text.ts +105 -0
  26. package/src/client/editors/unsplash-pane.ts +317 -0
  27. package/src/client/element-context.ts +308 -0
  28. package/src/client/features.ts +81 -0
  29. package/src/client/focus.ts +166 -0
  30. package/src/client/group.ts +186 -0
  31. package/src/client/highlight.ts +146 -0
  32. package/src/client/hover.ts +485 -0
  33. package/src/client/icons.ts +160 -0
  34. package/src/client/markdown.ts +319 -0
  35. package/src/client/overlay.ts +466 -0
  36. package/src/client/page-source.ts +143 -0
  37. package/src/client/router.ts +198 -0
  38. package/src/client/shadow.ts +111 -0
  39. package/src/client/source-map.ts +150 -0
  40. package/src/client/state.ts +153 -0
  41. package/src/client/styles.ts +3485 -0
  42. package/src/client/tree-model.ts +45 -0
  43. package/src/client/tree.ts +366 -0
  44. package/src/client/ui.ts +987 -0
  45. package/src/client/unsplash-search.ts +250 -0
  46. package/src/index.ts +299 -0
  47. package/src/patcher/astro.ts +792 -0
  48. package/src/patcher/content-config.ts +1035 -0
  49. package/src/patcher/dotenv.ts +121 -0
  50. package/src/patcher/expression-trace.ts +326 -0
  51. package/src/patcher/frontmatter.ts +249 -0
  52. package/src/patcher/registry.ts +11 -0
  53. package/src/patcher/types.ts +32 -0
  54. package/src/server/annotate.ts +173 -0
  55. package/src/server/assets.ts +167 -0
  56. package/src/server/collection-entries.ts +91 -0
  57. package/src/server/content-config.ts +210 -0
  58. package/src/server/editor.ts +15 -0
  59. package/src/server/entry-detect.ts +110 -0
  60. package/src/server/entry-resolve-routes.ts +218 -0
  61. package/src/server/entry-routes.ts +304 -0
  62. package/src/server/inspect-locate.ts +81 -0
  63. package/src/server/inspect-routes.ts +94 -0
  64. package/src/server/middleware.ts +480 -0
  65. package/src/server/options.ts +778 -0
  66. package/src/server/page-source-routes.ts +71 -0
  67. package/src/server/paths.ts +219 -0
  68. package/src/server/private-files.ts +116 -0
  69. package/src/server/route-manifest.ts +200 -0
  70. package/src/server/router.ts +94 -0
  71. package/src/server/schema-introspect.ts +233 -0
  72. package/src/server/schema-routes.ts +808 -0
  73. package/src/server/settings-routes.ts +246 -0
  74. package/src/server/settings.ts +382 -0
  75. package/src/server/text-writes.ts +105 -0
  76. package/src/server/unsplash-routes.ts +515 -0
  77. package/src/server/zod-adapt.ts +239 -0
  78. package/src/shared/asset-path.ts +132 -0
  79. package/src/shared/protocol.ts +935 -0
  80. package/src/shared/slug.ts +17 -0
  81. package/src/shared/unsplash.ts +51 -0
@@ -0,0 +1,186 @@
1
+ import { styled } from './ui.ts';
2
+
3
+ /**
4
+ * Layout primitives for grouping: the structural half of the overlay's look,
5
+ * as opposed to the token half in `ui.ts`.
6
+ *
7
+ * A drawer that is a flat run of controls reads as a form dump however well
8
+ * each control is styled — nothing tells the eye which fields belong together,
9
+ * which action is the one to take, or where one concern ends. These are the
10
+ * pieces that say so, and they are deliberately the same small vocabulary
11
+ * shadcn settled on, because it is the one the look is *made* of:
12
+ *
13
+ * - a **card** is a bounded concern: a header that names it, a body, and an
14
+ * optional footer band holding the action that completes it;
15
+ * - a **field group** is a run of controls that answer one question, separated
16
+ * from the next run by a rule rather than by guessing from whitespace;
17
+ * - an **item** is one row in a list — media, title, description, actions — so
18
+ * every list in the overlay is built from the same row rather than each
19
+ * panel inventing its own.
20
+ *
21
+ * All three are pure DOM: no state, no listeners, no imports beyond `styled`.
22
+ * Callers own behaviour. Styling is `.atx-card*`, `.atx-field-group`,
23
+ * `.atx-item*` and `.atx-sep` in `styles.ts` — internal classes, as ever.
24
+ */
25
+
26
+ export interface CardHeadOptions {
27
+ /** The concern's name. 16px/500 — the only thing at that size in a drawer. */
28
+ title: string;
29
+ /** One line under it, in muted ink. Where a file path or a caveat goes. */
30
+ description?: string;
31
+ /**
32
+ * The action *this* card offers, pinned to the header's top-right corner —
33
+ * "+ New", "View all". It belongs here rather than in the footer precisely
34
+ * because it does not complete the card: it leaves it.
35
+ */
36
+ action?: HTMLElement;
37
+ }
38
+
39
+ export interface Card {
40
+ root: HTMLElement;
41
+ /** Content slot. */
42
+ body: HTMLElement;
43
+ /**
44
+ * Footer band, created on first read. Lazy because the band is a visible
45
+ * thing — a top rule and a tinted ground — and an empty one reads as a
46
+ * missing action rather than as no action.
47
+ */
48
+ foot(): HTMLElement;
49
+ }
50
+
51
+ /** A bounded concern: header, body, and a footer band if one is asked for. */
52
+ export function card(head?: CardHeadOptions): Card {
53
+ const root = styled('div', 'atx-card');
54
+ if (head) root.append(cardHead(head));
55
+
56
+ const body = styled('div', 'atx-card-body');
57
+ root.append(body);
58
+
59
+ let footEl: HTMLElement | null = null;
60
+ return {
61
+ root,
62
+ body,
63
+ foot: () => {
64
+ if (!footEl) {
65
+ footEl = styled('div', 'atx-card-foot');
66
+ root.append(footEl);
67
+ }
68
+ return footEl;
69
+ },
70
+ };
71
+ }
72
+
73
+ /**
74
+ * The header on its own, for a surface that is card-shaped without being a
75
+ * card — the drawer's own title bar is the case that matters.
76
+ *
77
+ * Two columns when there is an action and one when there is not, which is why
78
+ * it is a grid: the title and the description both have to stop short of the
79
+ * button rather than run under it, and `[data-action]` on the host is what the
80
+ * stylesheet keys the second column off.
81
+ */
82
+ export function cardHead(o: CardHeadOptions): HTMLElement {
83
+ const root = styled('div', 'atx-card-head');
84
+
85
+ const title = styled('div', 'atx-card-title');
86
+ title.textContent = o.title;
87
+ root.append(title);
88
+
89
+ if (o.description) {
90
+ const desc = styled('div', 'atx-card-desc');
91
+ desc.textContent = o.description;
92
+ root.append(desc);
93
+ }
94
+ if (o.action) {
95
+ const slot = styled('div', 'atx-card-action');
96
+ slot.append(o.action);
97
+ root.append(slot);
98
+ root.dataset.action = '';
99
+ }
100
+ return root;
101
+ }
102
+
103
+ /** A run of fields that answer one question. Owns the spacing between them. */
104
+ export function fieldGroup(): HTMLElement {
105
+ return styled('div', 'atx-field-group');
106
+ }
107
+
108
+ /** A 1px rule. Separates two groups inside one surface, where a second card
109
+ * would overstate how far apart they are. */
110
+ export function separator(): HTMLElement {
111
+ const el = styled('div', 'atx-sep');
112
+ el.role = 'separator';
113
+ return el;
114
+ }
115
+
116
+ export interface ItemOptions {
117
+ /** Title line, 14px/500. */
118
+ title: string;
119
+ /** Second line, 12px muted. */
120
+ description?: string;
121
+ /** A 16px icon slot ahead of the text. */
122
+ media?: HTMLElement;
123
+ /** Buttons pinned to the row's right edge. */
124
+ actions?: readonly HTMLElement[];
125
+ /**
126
+ * `muted` draws the row as a filled tile — right for a standalone card of
127
+ * one. `plain` leaves it transparent, which is what a row in a list wants:
128
+ * the list is the object, not each row.
129
+ */
130
+ variant?: 'plain' | 'muted';
131
+ }
132
+
133
+ export interface Item {
134
+ root: HTMLElement;
135
+ /** The text column, for a caller that needs to append to it. */
136
+ content: HTMLElement;
137
+ title: HTMLElement;
138
+ }
139
+
140
+ /** One row of a list: media, title over description, actions. */
141
+ export function item(o: ItemOptions): Item {
142
+ const root = styled('div', 'atx-item');
143
+ root.dataset.variant = o.variant ?? 'plain';
144
+
145
+ if (o.media) {
146
+ const media = styled('div', 'atx-item-media');
147
+ media.append(o.media);
148
+ root.append(media);
149
+ }
150
+
151
+ const content = styled('div', 'atx-item-content');
152
+ const title = styled('div', 'atx-item-title');
153
+ title.textContent = o.title;
154
+ content.append(title);
155
+ if (o.description) {
156
+ const desc = styled('div', 'atx-item-desc');
157
+ desc.textContent = o.description;
158
+ content.append(desc);
159
+ }
160
+ root.append(content);
161
+
162
+ if (o.actions?.length) {
163
+ const actions = styled('div', 'atx-item-actions');
164
+ actions.append(...o.actions);
165
+ root.append(actions);
166
+ }
167
+ return { root, content, title };
168
+ }
169
+
170
+ export interface ItemGroupOptions {
171
+ /**
172
+ * Run the rows edge to edge in the surface holding them: cancels a card
173
+ * body's horizontal padding so the rules between rows reach the card's own
174
+ * edges and a hover fill covers the whole row. A list sitting directly in a
175
+ * card body wants this; one indented inside other content does not.
176
+ */
177
+ bleed?: boolean;
178
+ }
179
+
180
+ /** The list an `item` belongs to: rows flush, separated by a rule. */
181
+ export function itemGroup(o: ItemGroupOptions = {}): HTMLElement {
182
+ const el = styled('div', 'atx-item-group');
183
+ el.role = 'list';
184
+ if (o.bleed) el.dataset.bleed = '';
185
+ return el;
186
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Minimal syntax tokenizer for the source-peek panel. Pure string→token logic,
3
+ * no DOM (peek.ts renders tokens to styled spans) — keep it that way so it
4
+ * stays unit-testable like markdown.ts.
5
+ *
6
+ * This is a readability aid, not a grammar: one lexer covers the mix that
7
+ * appears in editable files (.astro markup + frontmatter JS, .md/.mdx, YAML)
8
+ * well enough to tell comments, strings, tags, and keywords apart. Anything it
9
+ * doesn't recognize stays plain text — a wrong guess renders as uncolored
10
+ * code, never as wrong code.
11
+ */
12
+
13
+ export type TokenKind =
14
+ | 'plain'
15
+ | 'comment'
16
+ | 'string'
17
+ | 'tag'
18
+ | 'attr'
19
+ | 'keyword'
20
+ | 'number'
21
+ | 'fence'; // the `---` frontmatter delimiter line
22
+
23
+ export interface Token {
24
+ text: string;
25
+ kind: TokenKind;
26
+ }
27
+
28
+ const KEYWORDS = new Set([
29
+ 'import', 'export', 'from', 'const', 'let', 'var', 'function', 'return',
30
+ 'if', 'else', 'for', 'of', 'in', 'await', 'async', 'new', 'class',
31
+ 'extends', 'interface', 'type', 'typeof', 'true', 'false', 'null',
32
+ 'undefined',
33
+ ]);
34
+
35
+ const STRING_RE = /^("(?:[^"\\]|\\.)*(?:"|$)|'(?:[^'\\]|\\.)*(?:'|$)|`(?:[^`\\]|\\.)*(?:`|$))/;
36
+ const TAG_RE = /^<\/?[a-zA-Z][\w.:-]*/;
37
+ const IDENT_RE = /^[A-Za-z_$][\w$-]*/;
38
+ const NUMBER_RE = /^\d+(?:\.\d+)?/;
39
+ const BLOCK_COMMENT_RE = /^\/\*.*?(?:\*\/|$)/;
40
+
41
+ /**
42
+ * Tokenize a window of source lines. Lines are processed in order because one
43
+ * bit of state crosses them: an unclosed `<!--` comment colors the following
44
+ * lines until its `-->`. (JS block comments are not carried across lines —
45
+ * they are rare in the files we peek at, and a miss degrades to plain text.)
46
+ */
47
+ export function tokenizeLines(lines: string[]): Token[][] {
48
+ let inHtmlComment = false;
49
+ return lines.map((line) => {
50
+ const tokens: Token[] = [];
51
+ let plain = '';
52
+ const flushPlain = (): void => {
53
+ if (plain) {
54
+ tokens.push({ text: plain, kind: 'plain' });
55
+ plain = '';
56
+ }
57
+ };
58
+ const push = (text: string, kind: TokenKind): void => {
59
+ flushPlain();
60
+ tokens.push({ text, kind });
61
+ };
62
+
63
+ // The frontmatter fence is a whole-line affair.
64
+ if (!inHtmlComment && line.trim() === '---') {
65
+ return [{ text: line, kind: 'fence' }];
66
+ }
67
+
68
+ let i = 0;
69
+ while (i < line.length) {
70
+ const rest = line.slice(i);
71
+
72
+ if (inHtmlComment) {
73
+ const end = rest.indexOf('-->');
74
+ const text = end === -1 ? rest : rest.slice(0, end + 3);
75
+ if (end !== -1) inHtmlComment = false;
76
+ push(text, 'comment');
77
+ i += text.length;
78
+ continue;
79
+ }
80
+
81
+ if (rest.startsWith('<!--')) {
82
+ const end = rest.indexOf('-->', 4);
83
+ const text = end === -1 ? rest : rest.slice(0, end + 3);
84
+ if (end === -1) inHtmlComment = true;
85
+ push(text, 'comment');
86
+ i += text.length;
87
+ continue;
88
+ }
89
+
90
+ // `//` comments only at line start or after whitespace, so URLs
91
+ // (`https://…`) in prose or attributes stay plain.
92
+ if (rest.startsWith('//') && (i === 0 || /\s/.test(line[i - 1] ?? ''))) {
93
+ push(rest, 'comment');
94
+ break;
95
+ }
96
+
97
+ let m = BLOCK_COMMENT_RE.exec(rest);
98
+ if (m) {
99
+ push(m[0], 'comment');
100
+ i += m[0].length;
101
+ continue;
102
+ }
103
+
104
+ // An apostrophe right after a word char is prose ("it's"), not a string
105
+ // opener — without this, half the line in a .md peek turns green.
106
+ const proseApostrophe = rest[0] === "'" && i > 0 && /\w/.test(line[i - 1] ?? '');
107
+ m = proseApostrophe ? null : STRING_RE.exec(rest);
108
+ if (m) {
109
+ push(m[0], 'string');
110
+ i += m[0].length;
111
+ continue;
112
+ }
113
+
114
+ m = TAG_RE.exec(rest);
115
+ if (m) {
116
+ push(m[0], 'tag');
117
+ i += m[0].length;
118
+ continue;
119
+ }
120
+
121
+ // Consume identifiers whole, so keywords/numbers can't match mid-word
122
+ // (`h1` is a tag name fragment, not `h` + number `1`).
123
+ m = IDENT_RE.exec(rest);
124
+ if (m) {
125
+ const word = m[0];
126
+ if (line[i + word.length] === '=') push(word, 'attr');
127
+ else if (KEYWORDS.has(word)) push(word, 'keyword');
128
+ else plain += word;
129
+ i += word.length;
130
+ continue;
131
+ }
132
+
133
+ m = NUMBER_RE.exec(rest);
134
+ if (m) {
135
+ push(m[0], 'number');
136
+ i += m[0].length;
137
+ continue;
138
+ }
139
+
140
+ plain += line[i];
141
+ i += 1;
142
+ }
143
+ flushPlain();
144
+ return tokens;
145
+ });
146
+ }