@sendbird/actionbook-core 0.10.1 → 0.10.3

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/dist/ui/index.js CHANGED
@@ -5149,6 +5149,320 @@ function analyzeJinjaBlocks(doc2) {
5149
5149
  });
5150
5150
  }
5151
5151
 
5152
+ // src/jinja/evaluator.ts
5153
+ var KEYWORDS2 = {
5154
+ and: "AND",
5155
+ or: "OR",
5156
+ not: "NOT",
5157
+ in: "IN",
5158
+ is: "IS",
5159
+ True: "BOOL",
5160
+ False: "BOOL",
5161
+ true: "BOOL",
5162
+ false: "BOOL",
5163
+ None: "NONE",
5164
+ null: "NONE"
5165
+ };
5166
+ function tokenize(input) {
5167
+ const tokens = [];
5168
+ let i = 0;
5169
+ while (i < input.length) {
5170
+ if (/\s/.test(input[i])) {
5171
+ i++;
5172
+ continue;
5173
+ }
5174
+ if (input[i] === '"' || input[i] === "'") {
5175
+ const quote = input[i];
5176
+ let str = "";
5177
+ i++;
5178
+ while (i < input.length && input[i] !== quote) {
5179
+ if (input[i] === "\\" && i + 1 < input.length) {
5180
+ str += input[i + 1];
5181
+ i += 2;
5182
+ } else {
5183
+ str += input[i];
5184
+ i++;
5185
+ }
5186
+ }
5187
+ if (i >= input.length) return { tokens: [], error: "Unterminated string literal" };
5188
+ i++;
5189
+ tokens.push({ type: "STRING", value: str });
5190
+ continue;
5191
+ }
5192
+ if (/[0-9]/.test(input[i]) || input[i] === "-" && i + 1 < input.length && /[0-9]/.test(input[i + 1]) && (tokens.length === 0 || ["AND", "OR", "NOT", "EQ", "NEQ", "LT", "GT", "LTE", "GTE", "LPAREN", "IN", "IS"].includes(tokens[tokens.length - 1].type))) {
5193
+ let num = "";
5194
+ if (input[i] === "-") {
5195
+ num = "-";
5196
+ i++;
5197
+ }
5198
+ while (i < input.length && /[0-9]/.test(input[i])) {
5199
+ num += input[i];
5200
+ i++;
5201
+ }
5202
+ if (i < input.length && input[i] === ".") {
5203
+ num += ".";
5204
+ i++;
5205
+ while (i < input.length && /[0-9]/.test(input[i])) {
5206
+ num += input[i];
5207
+ i++;
5208
+ }
5209
+ }
5210
+ tokens.push({ type: "NUMBER", value: num });
5211
+ continue;
5212
+ }
5213
+ if (/[a-zA-Z_]/.test(input[i])) {
5214
+ let ident = "";
5215
+ while (i < input.length && /[a-zA-Z0-9_.]/.test(input[i])) {
5216
+ ident += input[i];
5217
+ i++;
5218
+ }
5219
+ const kwType = KEYWORDS2[ident];
5220
+ if (kwType) {
5221
+ tokens.push({ type: kwType, value: ident });
5222
+ } else {
5223
+ tokens.push({ type: "IDENT", value: ident });
5224
+ }
5225
+ continue;
5226
+ }
5227
+ if (i + 1 < input.length) {
5228
+ const two = input[i] + input[i + 1];
5229
+ if (two === "==") {
5230
+ tokens.push({ type: "EQ", value: "==" });
5231
+ i += 2;
5232
+ continue;
5233
+ }
5234
+ if (two === "!=") {
5235
+ tokens.push({ type: "NEQ", value: "!=" });
5236
+ i += 2;
5237
+ continue;
5238
+ }
5239
+ if (two === "<=") {
5240
+ tokens.push({ type: "LTE", value: "<=" });
5241
+ i += 2;
5242
+ continue;
5243
+ }
5244
+ if (two === ">=") {
5245
+ tokens.push({ type: "GTE", value: ">=" });
5246
+ i += 2;
5247
+ continue;
5248
+ }
5249
+ }
5250
+ if (input[i] === "<") {
5251
+ tokens.push({ type: "LT", value: "<" });
5252
+ i++;
5253
+ continue;
5254
+ }
5255
+ if (input[i] === ">") {
5256
+ tokens.push({ type: "GT", value: ">" });
5257
+ i++;
5258
+ continue;
5259
+ }
5260
+ if (input[i] === "(") {
5261
+ tokens.push({ type: "LPAREN", value: "(" });
5262
+ i++;
5263
+ continue;
5264
+ }
5265
+ if (input[i] === ")") {
5266
+ tokens.push({ type: "RPAREN", value: ")" });
5267
+ i++;
5268
+ continue;
5269
+ }
5270
+ return { tokens: [], error: `Unexpected character: ${input[i]}` };
5271
+ }
5272
+ tokens.push({ type: "EOF", value: "" });
5273
+ return { tokens };
5274
+ }
5275
+ var Parser = class {
5276
+ pos = 0;
5277
+ tokens;
5278
+ variables;
5279
+ constructor(tokens, variables) {
5280
+ this.tokens = tokens;
5281
+ this.variables = variables;
5282
+ }
5283
+ peek() {
5284
+ return this.tokens[this.pos] ?? { type: "EOF", value: "" };
5285
+ }
5286
+ advance() {
5287
+ const t = this.tokens[this.pos];
5288
+ this.pos++;
5289
+ return t;
5290
+ }
5291
+ expect(type) {
5292
+ const t = this.peek();
5293
+ if (t.type !== type) {
5294
+ throw new Error(`Expected ${type}, got ${t.type}`);
5295
+ }
5296
+ return this.advance();
5297
+ }
5298
+ // Grammar (precedence low→high):
5299
+ // expr → or_expr
5300
+ // or_expr → and_expr ('or' and_expr)*
5301
+ // and_expr → not_expr ('and' not_expr)*
5302
+ // not_expr → 'not' not_expr | cmp_expr
5303
+ // cmp_expr → primary (('=='|'!='|'<'|'>'|'<='|'>='|'in'|'is'|'is not') primary)?
5304
+ // primary → STRING | NUMBER | BOOL | NONE | IDENT | '(' expr ')'
5305
+ evaluate() {
5306
+ const result = this.orExpr();
5307
+ if (this.peek().type !== "EOF") {
5308
+ throw new Error(`Unexpected token: ${this.peek().value}`);
5309
+ }
5310
+ return result;
5311
+ }
5312
+ orExpr() {
5313
+ let left = this.andExpr();
5314
+ while (this.peek().type === "OR") {
5315
+ this.advance();
5316
+ const right = this.andExpr();
5317
+ left = isTruthy(left) || isTruthy(right);
5318
+ }
5319
+ return left;
5320
+ }
5321
+ andExpr() {
5322
+ let left = this.notExpr();
5323
+ while (this.peek().type === "AND") {
5324
+ this.advance();
5325
+ const right = this.notExpr();
5326
+ left = isTruthy(left) && isTruthy(right);
5327
+ }
5328
+ return left;
5329
+ }
5330
+ notExpr() {
5331
+ if (this.peek().type === "NOT") {
5332
+ this.advance();
5333
+ const val = this.notExpr();
5334
+ return !isTruthy(val);
5335
+ }
5336
+ return this.cmpExpr();
5337
+ }
5338
+ cmpExpr() {
5339
+ const left = this.primary();
5340
+ const op = this.peek().type;
5341
+ switch (op) {
5342
+ case "EQ":
5343
+ this.advance();
5344
+ return looseEqual(left, this.primary());
5345
+ case "NEQ":
5346
+ this.advance();
5347
+ return !looseEqual(left, this.primary());
5348
+ case "LT":
5349
+ this.advance();
5350
+ return toNumber(left) < toNumber(this.primary());
5351
+ case "GT":
5352
+ this.advance();
5353
+ return toNumber(left) > toNumber(this.primary());
5354
+ case "LTE":
5355
+ this.advance();
5356
+ return toNumber(left) <= toNumber(this.primary());
5357
+ case "GTE":
5358
+ this.advance();
5359
+ return toNumber(left) >= toNumber(this.primary());
5360
+ case "IN": {
5361
+ this.advance();
5362
+ const collection = this.primary();
5363
+ if (Array.isArray(collection)) {
5364
+ return collection.some((item) => looseEqual(item, left));
5365
+ }
5366
+ if (typeof collection === "string" && typeof left === "string") {
5367
+ return collection.includes(left);
5368
+ }
5369
+ return false;
5370
+ }
5371
+ case "IS": {
5372
+ this.advance();
5373
+ if (this.peek().type === "NOT") {
5374
+ this.advance();
5375
+ return !looseEqual(left, this.primary());
5376
+ }
5377
+ return looseEqual(left, this.primary());
5378
+ }
5379
+ default:
5380
+ return left;
5381
+ }
5382
+ }
5383
+ primary() {
5384
+ const t = this.peek();
5385
+ switch (t.type) {
5386
+ case "STRING":
5387
+ this.advance();
5388
+ return t.value;
5389
+ case "NUMBER":
5390
+ this.advance();
5391
+ return parseFloat(t.value);
5392
+ case "BOOL":
5393
+ this.advance();
5394
+ return t.value === "True" || t.value === "true";
5395
+ case "NONE":
5396
+ this.advance();
5397
+ return null;
5398
+ case "IDENT": {
5399
+ this.advance();
5400
+ return this.resolveVariable(t.value);
5401
+ }
5402
+ case "LPAREN": {
5403
+ this.advance();
5404
+ const val = this.orExpr();
5405
+ this.expect("RPAREN");
5406
+ return val;
5407
+ }
5408
+ default:
5409
+ throw new Error(`Unexpected token in primary: ${t.type} "${t.value}"`);
5410
+ }
5411
+ }
5412
+ resolveVariable(name) {
5413
+ const entry = this.variables.get(name);
5414
+ if (entry === void 0) {
5415
+ return null;
5416
+ }
5417
+ switch (entry.type) {
5418
+ case "string":
5419
+ return String(entry.value);
5420
+ case "number":
5421
+ return Number(entry.value);
5422
+ case "boolean":
5423
+ return entry.value === true || entry.value === "true";
5424
+ default:
5425
+ return String(entry.value);
5426
+ }
5427
+ }
5428
+ };
5429
+ function isTruthy(v) {
5430
+ if (v === null || v === void 0) return false;
5431
+ if (typeof v === "boolean") return v;
5432
+ if (typeof v === "number") return v !== 0;
5433
+ if (typeof v === "string") return v.length > 0;
5434
+ if (Array.isArray(v)) return v.length > 0;
5435
+ return true;
5436
+ }
5437
+ function looseEqual(a, b) {
5438
+ if (a === b) return true;
5439
+ if (a === null || b === null) return a === b;
5440
+ if (typeof a === "number" && typeof b === "string") return a === parseFloat(b);
5441
+ if (typeof a === "string" && typeof b === "number") return parseFloat(a) === b;
5442
+ return false;
5443
+ }
5444
+ function toNumber(v) {
5445
+ if (typeof v === "number") return v;
5446
+ if (typeof v === "string") return parseFloat(v) || 0;
5447
+ if (typeof v === "boolean") return v ? 1 : 0;
5448
+ return 0;
5449
+ }
5450
+ function validateCondition(condition) {
5451
+ const trimmed = condition.trim();
5452
+ if (trimmed === "") return { valid: true };
5453
+ const { tokens, error: tokenError } = tokenize(trimmed);
5454
+ if (tokenError) return { valid: false, error: tokenError };
5455
+ if (tokens.length === 0) return { valid: false, error: "Empty expression" };
5456
+ try {
5457
+ const parser = new Parser(tokens, /* @__PURE__ */ new Map());
5458
+ parser.evaluate();
5459
+ return { valid: true };
5460
+ } catch (e) {
5461
+ const msg = e instanceof Error ? e.message : "Invalid expression";
5462
+ return { valid: false, error: msg };
5463
+ }
5464
+ }
5465
+
5152
5466
  // src/tree/documentTree.ts
5153
5467
  var MAX_DEPTH6 = 128;
5154
5468
  var END_ACTION_RESOURCE_IDS = /* @__PURE__ */ new Set([
@@ -6116,6 +6430,20 @@ var JINJA_STYLES = `
6116
6430
  letter-spacing: -0.3px;
6117
6431
  }
6118
6432
 
6433
+ .jinja-branch-header-invalid {
6434
+ border: 1px solid #D9352C;
6435
+ border-radius: 4px;
6436
+ }
6437
+
6438
+ .jinja-condition-error {
6439
+ display: flex;
6440
+ align-items: center;
6441
+ padding: 4px 0 4px 52px;
6442
+ color: #D9352C;
6443
+ font-size: 12px;
6444
+ line-height: 16px;
6445
+ }
6446
+
6119
6447
  .jinja-token-variable {
6120
6448
  color: #4141B2;
6121
6449
  }
@@ -6501,7 +6829,8 @@ function ConditionDisplay({
6501
6829
  condition,
6502
6830
  editable = true,
6503
6831
  onConditionChange,
6504
- onBackspaceEmpty
6832
+ onBackspaceEmpty,
6833
+ onConditionBlur
6505
6834
  }) {
6506
6835
  const [isEditing, setIsEditing] = useState3(false);
6507
6836
  const [draftValue, setDraftValue] = useState3(condition);
@@ -6529,6 +6858,7 @@ function ConditionDisplay({
6529
6858
  }
6530
6859
  const commit = () => {
6531
6860
  setIsEditing(false);
6861
+ onConditionBlur?.();
6532
6862
  if (draftValue !== condition) {
6533
6863
  onConditionChange(draftValue);
6534
6864
  }
@@ -6670,8 +7000,17 @@ function JinjaBranchHeader({
6670
7000
  }
6671
7001
  const canOpenFooterMenu = editable && isLastBranch && branchType !== "else";
6672
7002
  const isMenuOpen = (source) => menuSource === source && menuItems.length > 0;
7003
+ const [conditionTouched, setConditionTouched] = useState3(false);
7004
+ const conditionError = (() => {
7005
+ if (branchType === "else") return null;
7006
+ if (condition.length === 0) {
7007
+ return conditionTouched ? "This field is required." : null;
7008
+ }
7009
+ const result = validateCondition(condition);
7010
+ return result.valid ? null : result.error || "Invalid condition";
7011
+ })();
6673
7012
  return /* @__PURE__ */ jsxs5(Fragment2, { children: [
6674
- /* @__PURE__ */ jsxs5("div", { className: "jinja-branch-header", children: [
7013
+ /* @__PURE__ */ jsxs5("div", { className: `jinja-branch-header${conditionError ? " jinja-branch-header-invalid" : ""}`, children: [
6675
7014
  /* @__PURE__ */ jsx6("span", { className: `jinja-branch-badge jinja-branch-badge-${branchType}`, children: branchType === "elif" ? "ELSE IF" : branchType.toUpperCase() }),
6676
7015
  /* @__PURE__ */ jsx6(
6677
7016
  ConditionDisplay,
@@ -6680,7 +7019,8 @@ function JinjaBranchHeader({
6680
7019
  condition,
6681
7020
  editable,
6682
7021
  onConditionChange,
6683
- onBackspaceEmpty: onDelete
7022
+ onBackspaceEmpty: onDelete,
7023
+ onConditionBlur: () => setConditionTouched(true)
6684
7024
  }
6685
7025
  ),
6686
7026
  editable ? /* @__PURE__ */ jsxs5("div", { className: "jinja-branch-actions", ref: kebabRef, children: [
@@ -6718,6 +7058,7 @@ function JinjaBranchHeader({
6718
7058
  ) : null
6719
7059
  ] }) : null
6720
7060
  ] }),
7061
+ conditionError && /* @__PURE__ */ jsx6("div", { className: "jinja-condition-error", children: conditionError }),
6721
7062
  isLastBranch && editable ? /* @__PURE__ */ jsx6("div", { className: "jinja-add-footer", children: /* @__PURE__ */ jsx6("div", { className: "jinja-add-footer-col", children: /* @__PURE__ */ jsxs5("div", { className: "jinja-add-footer-actions", ref: footerRef, children: [
6722
7063
  /* @__PURE__ */ jsx6(
6723
7064
  "button",
@@ -7027,17 +7368,17 @@ function createLinkEditPlugin() {
7027
7368
  if (match) {
7028
7369
  const [, label, href, title] = match;
7029
7370
  const linkMarkType = newState.schema.marks.link;
7030
- if (linkMarkType && href) {
7371
+ if (!label) {
7372
+ reconvertTr.delete(safeFrom, safeTo);
7373
+ } else if (linkMarkType && href) {
7031
7374
  const attrs = title ? { href, title: title.trim() } : { href };
7032
7375
  reconvertTr.delete(safeFrom, safeTo);
7033
- if (label.length > 0) {
7034
- reconvertTr.insertText(label, safeFrom);
7035
- reconvertTr.addMark(
7036
- safeFrom,
7037
- safeFrom + label.length,
7038
- linkMarkType.create(attrs)
7039
- );
7040
- }
7376
+ reconvertTr.insertText(label, safeFrom);
7377
+ reconvertTr.addMark(
7378
+ safeFrom,
7379
+ safeFrom + label.length,
7380
+ linkMarkType.create(attrs)
7381
+ );
7041
7382
  }
7042
7383
  }
7043
7384
  }
@@ -7051,12 +7392,17 @@ function createLinkInputRule() {
7051
7392
  (state, match, start, end) => {
7052
7393
  const [, label, href, title] = match;
7053
7394
  const linkMarkType = state.schema.marks.link;
7054
- if (!linkMarkType || !href) return null;
7055
- const displayText = label || href;
7056
- const attrs = title ? { href, title: title.trim() } : { href };
7395
+ if (!linkMarkType) return null;
7057
7396
  const { tr } = state;
7058
- tr.replaceWith(start, end, state.schema.text(displayText));
7059
- tr.addMark(start, start + displayText.length, linkMarkType.create(attrs));
7397
+ if (!label) {
7398
+ tr.delete(start, end);
7399
+ tr.setMeta(linkEditKey, null);
7400
+ return tr;
7401
+ }
7402
+ if (!href) return null;
7403
+ const attrs = title ? { href, title: title.trim() } : { href };
7404
+ tr.replaceWith(start, end, state.schema.text(label));
7405
+ tr.addMark(start, start + label.length, linkMarkType.create(attrs));
7060
7406
  tr.setMeta(linkEditKey, null);
7061
7407
  return tr;
7062
7408
  }
@@ -8123,14 +8469,12 @@ function injectScrollbarStyle() {
8123
8469
  const style = document.createElement("style");
8124
8470
  style.id = SCROLLBAR_STYLE_ID;
8125
8471
  style.textContent = `
8126
- .ab-slash-menu::-webkit-scrollbar { width: 6px; }
8127
- .ab-slash-menu::-webkit-scrollbar-track { background: transparent; }
8128
- .ab-slash-menu::-webkit-scrollbar-thumb { background: #c4c4c4; border-radius: 3px; }
8129
- .ab-slash-menu::-webkit-scrollbar-thumb:hover { background: #999; }
8472
+ .ab-slash-menu::-webkit-scrollbar { display: none; }
8473
+ .ab-slash-menu { scrollbar-width: none; }
8130
8474
  `;
8131
8475
  document.head.appendChild(style);
8132
8476
  }
8133
- var POPUP_SHADOW = "0 8px 10px rgba(13,13,13,0.12), 0 3px 14px rgba(13,13,13,0.08), 0 3px 5px rgba(13,13,13,0.04)";
8477
+ var POPUP_SHADOW = "0 0 0 1px #CCCCCC, 0 4px 16px rgba(0, 0, 0, 0.12)";
8134
8478
  var BTN_RESET = {
8135
8479
  border: "none",
8136
8480
  padding: 0,
@@ -8212,12 +8556,10 @@ function SlashCommandMenu({ view, editorState, items }) {
8212
8556
  width: MENU_WIDTH,
8213
8557
  maxHeight: MAX_MENU_H,
8214
8558
  overflowY: "auto",
8215
- scrollbarWidth: "thin",
8216
- scrollbarColor: "#c4c4c4 transparent",
8217
8559
  background: "#fff",
8218
- borderRadius: 4,
8560
+ borderRadius: 8,
8219
8561
  boxShadow: POPUP_SHADOW,
8220
- padding: "8px 0",
8562
+ padding: "4px",
8221
8563
  zIndex: 1100,
8222
8564
  animation: "ab-float-in 0.12s ease"
8223
8565
  },
@@ -8283,8 +8625,8 @@ function SlashMenuItem({ item, index, selected, onMouseEnter, onMouseDown }) {
8283
8625
  display: "flex",
8284
8626
  alignItems: "center",
8285
8627
  gap: 12,
8286
- padding: "6px 16px",
8287
- background: selected ? "rgba(13,13,13,0.04)" : "transparent",
8628
+ padding: "6px 8px",
8629
+ background: selected ? "rgba(0, 0, 0, 0.04)" : "transparent",
8288
8630
  transition: "background 0.08s"
8289
8631
  },
8290
8632
  onMouseEnter,
@@ -8705,7 +9047,7 @@ function getSelectionDOMRect() {
8705
9047
  if (rect.width === 0 && rect.height === 0) return null;
8706
9048
  return rect;
8707
9049
  }
8708
- var POPUP_SHADOW2 = "0 0 0 1px rgba(0,0,0,0.07), 0 4px 20px rgba(0,0,0,0.11), 0 1px 4px rgba(0,0,0,0.05)";
9050
+ var POPUP_SHADOW2 = "0 0 0 1px #CCCCCC, 0 4px 16px rgba(0, 0, 0, 0.12)";
8709
9051
  var BTN_RESET2 = {
8710
9052
  border: "none",
8711
9053
  padding: 0,
@@ -8737,8 +9079,8 @@ function TBtn({ children, active, title, onMouseDown, style }) {
8737
9079
  borderRadius: 6,
8738
9080
  fontSize: 13,
8739
9081
  fontWeight: 600,
8740
- color: active ? "#6366f1" : "#3f3f46",
8741
- background: active ? "rgba(99,102,241,0.1)" : hover ? "rgba(0,0,0,0.05)" : "transparent",
9082
+ color: active ? "#6210CC" : "#0D0D0D",
9083
+ background: active ? "rgba(98,16,204,0.08)" : hover ? "rgba(0,0,0,0.05)" : "transparent",
8742
9084
  transition: "background 0.1s, color 0.1s",
8743
9085
  ...style
8744
9086
  },
@@ -8832,8 +9174,8 @@ function BlockTypeDropdown({ view, state, label }) {
8832
9174
  borderRadius: 6,
8833
9175
  fontSize: 11,
8834
9176
  fontWeight: 600,
8835
- color: open ? "#6366f1" : "#3f3f46",
8836
- background: open ? "rgba(99,102,241,0.1)" : hover ? "rgba(0,0,0,0.05)" : "transparent",
9177
+ color: open ? "#6210CC" : "#0D0D0D",
9178
+ background: open ? "rgba(98,16,204,0.08)" : hover ? "rgba(0,0,0,0.04)" : "transparent",
8837
9179
  transition: "background 0.1s, color 0.1s",
8838
9180
  whiteSpace: "nowrap"
8839
9181
  },
@@ -8854,9 +9196,9 @@ function BlockTypeDropdown({ view, state, label }) {
8854
9196
  left: btnRect.left,
8855
9197
  top: btnRect.bottom + 4,
8856
9198
  background: "#fff",
8857
- borderRadius: 10,
9199
+ borderRadius: 8,
8858
9200
  boxShadow: POPUP_SHADOW2,
8859
- padding: 6,
9201
+ padding: 4,
8860
9202
  minWidth: 168,
8861
9203
  animation: "ab-float-in 0.12s ease"
8862
9204
  },
@@ -8887,16 +9229,16 @@ function DropdownItem({ item, onRun }) {
8887
9229
  gap: 10,
8888
9230
  width: "100%",
8889
9231
  padding: "6px 8px",
8890
- borderRadius: 6,
9232
+ borderRadius: 4,
8891
9233
  textAlign: "left",
8892
9234
  fontSize: 13,
8893
- color: item.active ? "#6366f1" : "#3f3f46",
9235
+ color: item.active ? "#6210CC" : "#0D0D0D",
8894
9236
  fontWeight: item.active ? 600 : 400,
8895
9237
  background: hover ? "rgba(0,0,0,0.04)" : "transparent",
8896
9238
  transition: "background 0.1s"
8897
9239
  },
8898
9240
  children: [
8899
- /* @__PURE__ */ jsx10("span", { style: { width: 24, flexShrink: 0, textAlign: "center", fontWeight: 700, fontSize: 12, fontFamily: "monospace", color: "#6366f1" }, children: item.shortLabel }),
9241
+ /* @__PURE__ */ jsx10("span", { style: { width: 24, flexShrink: 0, textAlign: "center", fontWeight: 700, fontSize: 12, fontFamily: "monospace", color: "#6210CC" }, children: item.shortLabel }),
8900
9242
  item.label
8901
9243
  ]
8902
9244
  }
@@ -8973,7 +9315,7 @@ function SelectionToolbar({ view, state, selectionRect }) {
8973
9315
  gap: 2,
8974
9316
  padding: "0 6px",
8975
9317
  background: "#fff",
8976
- borderRadius: 10,
9318
+ borderRadius: 8,
8977
9319
  boxShadow: POPUP_SHADOW2,
8978
9320
  userSelect: "none",
8979
9321
  animation: "ab-float-in 0.12s ease"
@@ -9021,7 +9363,7 @@ function SelectionToolbar({ view, state, selectionRect }) {
9021
9363
  e.preventDefault();
9022
9364
  applyLink();
9023
9365
  },
9024
- style: { color: "#6366f1", width: 24, fontSize: 14 },
9366
+ style: { color: "#6210CC", width: 24, fontSize: 14 },
9025
9367
  children: "\u21B5"
9026
9368
  }
9027
9369
  ),
@@ -9270,8 +9612,8 @@ function LinkEditDialog({ view, link: link2, onClose }) {
9270
9612
  top = Math.min(anchorBottom + 12, window.innerHeight - estimatedDialogH - VPORT_MARGIN2);
9271
9613
  }
9272
9614
  const finalHref = normalizeLinkHref(hrefValue);
9273
- const finalText = textValue.trim() ? textValue : finalHref;
9274
- const canSave = Boolean(finalHref) && (finalHref !== link2.href || finalText !== link2.text);
9615
+ const finalText = textValue.trim() ? textValue : "";
9616
+ const canSave = !textValue.trim() || Boolean(finalHref) && (finalHref !== link2.href || finalText !== link2.text);
9275
9617
  const currentLink = () => {
9276
9618
  const activeLink = getLinkAtCursor(view.state);
9277
9619
  if (activeLink && activeLink.from === link2.from && activeLink.to === link2.to) {
@@ -9282,7 +9624,14 @@ function LinkEditDialog({ view, link: link2, onClose }) {
9282
9624
  const saveLink = () => {
9283
9625
  const activeLink = currentLink();
9284
9626
  const nextHref = normalizeLinkHref(hrefValue);
9285
- const nextText = textValue.trim() ? textValue : nextHref;
9627
+ if (!textValue.trim()) {
9628
+ const tr2 = view.state.tr.delete(activeLink.from, activeLink.to);
9629
+ view.dispatch(tr2);
9630
+ onClose();
9631
+ view.focus();
9632
+ return;
9633
+ }
9634
+ const nextText = textValue;
9286
9635
  if (!nextHref) return;
9287
9636
  if (nextHref === activeLink.href && nextText === activeLink.text) return;
9288
9637
  const cursorOffset = Math.max(0, view.state.selection.from - activeLink.from);
@@ -9342,8 +9691,8 @@ function LinkEditDialog({ view, link: link2, onClose }) {
9342
9691
  gap: 16,
9343
9692
  padding: 16,
9344
9693
  background: "#FFFFFF",
9345
- borderRadius: 4,
9346
- boxShadow: "0px 8px 10px rgba(13,13,13,0.12), 0px 3px 14px rgba(13,13,13,0.08), 0px 3px 5px rgba(13,13,13,0.04)",
9694
+ borderRadius: 8,
9695
+ boxShadow: "0 0 0 1px #CCCCCC, 0 4px 16px rgba(0, 0, 0, 0.12)",
9347
9696
  boxSizing: "border-box",
9348
9697
  animation: "ab-float-in 0.12s ease"
9349
9698
  },
@@ -9511,6 +9860,189 @@ function LinkEditDialog({ view, link: link2, onClose }) {
9511
9860
  document.body
9512
9861
  );
9513
9862
  }
9863
+ var INSERT_LINK_EVENT = "ab-insert-link";
9864
+ function LinkInsertDialog({ view, cursorPos, onClose }) {
9865
+ const [textValue, setTextValue] = useState7("");
9866
+ const [hrefValue, setHrefValue] = useState7("");
9867
+ const textInputRef = useRef4(null);
9868
+ useEffect4(() => {
9869
+ setTimeout(() => textInputRef.current?.focus(), 0);
9870
+ }, []);
9871
+ useEffect4(() => {
9872
+ const onDown = (e) => {
9873
+ if (!e.target.closest(`[${FLOAT_ATTR}]`)) {
9874
+ onClose();
9875
+ }
9876
+ };
9877
+ document.addEventListener("mousedown", onDown, true);
9878
+ return () => document.removeEventListener("mousedown", onDown, true);
9879
+ }, [onClose]);
9880
+ let coords;
9881
+ try {
9882
+ coords = view.coordsAtPos(cursorPos);
9883
+ } catch {
9884
+ onClose();
9885
+ return null;
9886
+ }
9887
+ const dialogW = 280;
9888
+ const left = Math.max(
9889
+ VPORT_MARGIN2,
9890
+ Math.min(coords.left - dialogW / 2, window.innerWidth - dialogW - VPORT_MARGIN2)
9891
+ );
9892
+ const top = Math.min(coords.bottom + 8, window.innerHeight - 220 - VPORT_MARGIN2);
9893
+ const finalHref = normalizeLinkHref(hrefValue);
9894
+ const canSave = Boolean(finalHref);
9895
+ const saveLink = () => {
9896
+ if (!finalHref) return;
9897
+ const displayText = textValue.trim() || finalHref;
9898
+ const tr = view.state.tr;
9899
+ const linkMarkType = view.state.schema.marks.link;
9900
+ if (!linkMarkType) return;
9901
+ tr.insertText(displayText, cursorPos);
9902
+ tr.addMark(cursorPos, cursorPos + displayText.length, linkMarkType.create({ href: finalHref }));
9903
+ view.dispatch(tr);
9904
+ onClose();
9905
+ view.focus();
9906
+ };
9907
+ const handleKeyDown = (e) => {
9908
+ if (e.key === "Escape") {
9909
+ e.preventDefault();
9910
+ onClose();
9911
+ view.focus();
9912
+ }
9913
+ if (e.key === "Enter") {
9914
+ e.preventDefault();
9915
+ saveLink();
9916
+ }
9917
+ };
9918
+ return createPortal2(
9919
+ /* @__PURE__ */ jsxs9(
9920
+ "div",
9921
+ {
9922
+ ...{ [FLOAT_ATTR]: "" },
9923
+ style: {
9924
+ position: "fixed",
9925
+ left,
9926
+ top,
9927
+ zIndex: 1001,
9928
+ width: dialogW,
9929
+ display: "flex",
9930
+ flexDirection: "column",
9931
+ gap: 16,
9932
+ padding: 16,
9933
+ background: "#FFFFFF",
9934
+ borderRadius: 8,
9935
+ boxShadow: POPUP_SHADOW2,
9936
+ boxSizing: "border-box",
9937
+ animation: "ab-float-in 0.12s ease"
9938
+ },
9939
+ children: [
9940
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
9941
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { fontSize: 12, lineHeight: "16px", fontWeight: 600, color: "#0D0D0D" }, children: [
9942
+ "Text ",
9943
+ /* @__PURE__ */ jsx10("span", { style: { color: "#858585" }, children: "(optional)" })
9944
+ ] }),
9945
+ /* @__PURE__ */ jsx10(
9946
+ "input",
9947
+ {
9948
+ ref: textInputRef,
9949
+ ...{ [FLOAT_ATTR]: "" },
9950
+ value: textValue,
9951
+ placeholder: "",
9952
+ onChange: (e) => setTextValue(e.target.value),
9953
+ onKeyDown: handleKeyDown,
9954
+ style: {
9955
+ height: 32,
9956
+ border: "1px solid #CCCCCC",
9957
+ borderRadius: 4,
9958
+ padding: "0 16px",
9959
+ fontSize: 14,
9960
+ color: "#0D0D0D",
9961
+ outline: "none",
9962
+ boxSizing: "border-box"
9963
+ }
9964
+ }
9965
+ )
9966
+ ] }),
9967
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
9968
+ /* @__PURE__ */ jsx10("div", { ...{ [FLOAT_ATTR]: "" }, style: { fontSize: 12, lineHeight: "16px", fontWeight: 600, color: "#0D0D0D" }, children: "Link" }),
9969
+ /* @__PURE__ */ jsx10(
9970
+ "input",
9971
+ {
9972
+ ...{ [FLOAT_ATTR]: "" },
9973
+ value: hrefValue,
9974
+ onChange: (e) => setHrefValue(e.target.value),
9975
+ onKeyDown: handleKeyDown,
9976
+ style: {
9977
+ height: 32,
9978
+ border: "1px solid #CCCCCC",
9979
+ borderRadius: 4,
9980
+ padding: "0 16px",
9981
+ fontSize: 14,
9982
+ color: "#0D0D0D",
9983
+ outline: "none",
9984
+ boxSizing: "border-box"
9985
+ }
9986
+ }
9987
+ )
9988
+ ] }),
9989
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 8 }, children: [
9990
+ /* @__PURE__ */ jsx10(
9991
+ "button",
9992
+ {
9993
+ type: "button",
9994
+ ...{ [FLOAT_ATTR]: "" },
9995
+ onMouseDown: (e) => {
9996
+ e.preventDefault();
9997
+ onClose();
9998
+ view.focus();
9999
+ },
10000
+ style: {
10001
+ ...BTN_RESET2,
10002
+ height: 32,
10003
+ padding: "0 13px",
10004
+ border: "1px solid #CCCCCC",
10005
+ borderRadius: 4,
10006
+ fontSize: 14,
10007
+ lineHeight: "20px",
10008
+ fontWeight: 600,
10009
+ color: "#0D0D0D"
10010
+ },
10011
+ children: "Cancel"
10012
+ }
10013
+ ),
10014
+ /* @__PURE__ */ jsx10(
10015
+ "button",
10016
+ {
10017
+ type: "button",
10018
+ disabled: !canSave,
10019
+ ...{ [FLOAT_ATTR]: "" },
10020
+ onMouseDown: (e) => {
10021
+ e.preventDefault();
10022
+ if (canSave) saveLink();
10023
+ },
10024
+ style: {
10025
+ ...BTN_RESET2,
10026
+ height: 32,
10027
+ padding: "0 13px",
10028
+ borderRadius: 4,
10029
+ fontSize: 14,
10030
+ lineHeight: "20px",
10031
+ fontWeight: 600,
10032
+ background: canSave ? "#6210CC" : "#ECECEC",
10033
+ color: canSave ? "#FFFFFF" : "#A6A6A6",
10034
+ cursor: canSave ? "pointer" : "default"
10035
+ },
10036
+ children: "Save"
10037
+ }
10038
+ )
10039
+ ] })
10040
+ ]
10041
+ }
10042
+ ),
10043
+ document.body
10044
+ );
10045
+ }
9514
10046
  var BLOCK_ITEMS = [
9515
10047
  {
9516
10048
  shortLabel: "\xB6",
@@ -9633,12 +10165,12 @@ function EmptyParaHandle({ view, cursorPos }) {
9633
10165
  height: btnSize,
9634
10166
  border: "1px solid rgba(0,0,0,0.1)",
9635
10167
  borderRadius: 5,
9636
- background: menuOpen ? "rgba(99,102,241,0.08)" : "#fff",
10168
+ background: menuOpen ? "rgba(98,16,204,0.08)" : "#fff",
9637
10169
  boxShadow: "0 1px 3px rgba(0,0,0,0.06)",
9638
10170
  display: "flex",
9639
10171
  alignItems: "center",
9640
10172
  justifyContent: "center",
9641
- color: menuOpen ? "#6366f1" : "#9ca3af",
10173
+ color: menuOpen ? "#6210CC" : "#9ca3af",
9642
10174
  fontSize: 16,
9643
10175
  animation: "ab-float-in 0.15s ease",
9644
10176
  transition: "color 0.1s, background 0.1s, border-color 0.1s"
@@ -9656,9 +10188,9 @@ function EmptyParaHandle({ view, cursorPos }) {
9656
10188
  left: menuLeft,
9657
10189
  top: menuTop,
9658
10190
  background: "#fff",
9659
- borderRadius: 10,
10191
+ borderRadius: 8,
9660
10192
  boxShadow: POPUP_SHADOW2,
9661
- padding: 6,
10193
+ padding: 4,
9662
10194
  minWidth: 168,
9663
10195
  animation: "ab-float-in 0.12s ease"
9664
10196
  },
@@ -9702,7 +10234,7 @@ function BlockMenuItem({
9702
10234
  borderRadius: 6,
9703
10235
  textAlign: "left",
9704
10236
  fontSize: 13,
9705
- color: "#3f3f46",
10237
+ color: "#0D0D0D",
9706
10238
  background: hover ? "rgba(0,0,0,0.04)" : "transparent",
9707
10239
  transition: "background 0.1s"
9708
10240
  },
@@ -9717,7 +10249,7 @@ function BlockMenuItem({
9717
10249
  fontWeight: 700,
9718
10250
  fontSize: 12,
9719
10251
  fontFamily: "monospace",
9720
- color: "#6366f1"
10252
+ color: "#6210CC"
9721
10253
  },
9722
10254
  children: item.shortLabel
9723
10255
  }
@@ -9730,8 +10262,19 @@ function BlockMenuItem({
9730
10262
  function FloatingMenu({ view, editorState }) {
9731
10263
  const [showEmptyHandle, setShowEmptyHandle] = useState7(false);
9732
10264
  const [editingLink, setEditingLink] = useState7(null);
10265
+ const [insertLinkPos, setInsertLinkPos] = useState7(null);
9733
10266
  const dwellTimerRef = useRef4(null);
9734
10267
  const lastEmptyPosRef = useRef4(null);
10268
+ useEffect4(() => {
10269
+ const dom = view?.dom;
10270
+ if (!dom) return;
10271
+ const handler = () => {
10272
+ const pos = view.state.selection.from;
10273
+ setInsertLinkPos(pos);
10274
+ };
10275
+ dom.addEventListener(INSERT_LINK_EVENT, handler);
10276
+ return () => dom.removeEventListener(INSERT_LINK_EVENT, handler);
10277
+ }, [view]);
9735
10278
  const [, setScrollTick] = useState7(0);
9736
10279
  useEffect4(() => {
9737
10280
  const editorDom = view?.dom;
@@ -9804,7 +10347,15 @@ function FloatingMenu({ view, editorState }) {
9804
10347
  onClose: () => setEditingLink(null)
9805
10348
  }
9806
10349
  ),
9807
- !hasSelection && !linkAtCursor && showEmptyHandle && emptyPos !== null && /* @__PURE__ */ jsx10(EmptyParaHandle, { view, cursorPos: emptyPos })
10350
+ insertLinkPos !== null && /* @__PURE__ */ jsx10(
10351
+ LinkInsertDialog,
10352
+ {
10353
+ view,
10354
+ cursorPos: insertLinkPos,
10355
+ onClose: () => setInsertLinkPos(null)
10356
+ }
10357
+ ),
10358
+ !hasSelection && !linkAtCursor && !insertLinkPos && showEmptyHandle && emptyPos !== null && /* @__PURE__ */ jsx10(EmptyParaHandle, { view, cursorPos: emptyPos })
9808
10359
  ] }),
9809
10360
  document.body
9810
10361
  );
@@ -9971,11 +10522,28 @@ function createInlineSuggestPlugin(provider, endpoint, options) {
9971
10522
  ]
9972
10523
  };
9973
10524
  }
10525
+
10526
+ // src/ui/bridge/jinjaValidation.ts
10527
+ function hasInvalidJinjaConditions(state) {
10528
+ let found = false;
10529
+ state.doc.descendants((node) => {
10530
+ if (found) return false;
10531
+ if (node.type.name === "jinjaIfBranch") {
10532
+ const condition = String(node.attrs.condition ?? "");
10533
+ if (condition && !validateCondition(condition).valid) {
10534
+ found = true;
10535
+ }
10536
+ return false;
10537
+ }
10538
+ });
10539
+ return found;
10540
+ }
9974
10541
  export {
9975
10542
  ActionbookRenderer,
9976
10543
  DocumentTreeView,
9977
10544
  EditorShell,
9978
10545
  FloatingMenu,
10546
+ INSERT_LINK_EVENT,
9979
10547
  JUMP_POINT_ADJACENT_SPEC,
9980
10548
  JinjaTreeView,
9981
10549
  SlashCommandMenu,
@@ -10004,6 +10572,7 @@ export {
10004
10572
  createTodoNodeViewPlugin,
10005
10573
  hasBrokenAnchorRefs,
10006
10574
  hasDuplicateJumpPoints,
10575
+ hasInvalidJinjaConditions,
10007
10576
  inlineSuggestKey,
10008
10577
  slashCommandKey,
10009
10578
  toProseMirrorJSON,