@jini-ai/ui 0.4.6 → 0.4.8

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 (151) hide show
  1. package/README.md +33 -0
  2. package/dist/features/admin-widgets/components/InfoTip.d.ts +23 -2
  3. package/dist/features/admin-widgets/components/InfoTip.hooks.d.ts +22 -2
  4. package/dist/features/admin-widgets/components/InfoTip.hooks.js +75 -28
  5. package/dist/features/admin-widgets/components/InfoTip.js +12 -4
  6. package/dist/features/admin-widgets/components/InfoTip.rules.d.ts +72 -0
  7. package/dist/features/admin-widgets/components/InfoTip.rules.js +68 -0
  8. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.d.ts +36 -0
  9. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.d.ts +58 -0
  10. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.js +66 -0
  11. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.js +24 -0
  12. package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.d.ts +23 -0
  13. package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.js +28 -0
  14. package/dist/features/admin-widgets/index.d.ts +9 -0
  15. package/dist/features/admin-widgets/index.js +5 -0
  16. package/dist/features/execution/constants.js +11 -10
  17. package/dist/features/execution/http-adapter.d.ts +3 -1
  18. package/dist/features/execution/http-adapter.js +9 -4
  19. package/dist/features/execution/index.d.ts +1 -1
  20. package/dist/features/execution/index.js +1 -1
  21. package/dist/features/execution/react/components/ByokProviderForm.d.ts +18 -1
  22. package/dist/features/execution/react/components/ByokProviderForm.js +14 -16
  23. package/dist/features/execution/react/components/LocalCliAgentCard.d.ts +5 -1
  24. package/dist/features/execution/react/components/LocalCliAgentCard.js +12 -87
  25. package/dist/features/execution/react/components/LocalCliAgentList.d.ts +4 -1
  26. package/dist/features/execution/react/components/LocalCliAgentList.js +13 -4
  27. package/dist/features/execution/react/components/LocalCliAgentSettings.d.ts +20 -0
  28. package/dist/features/execution/react/components/LocalCliAgentSettings.js +20 -0
  29. package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.d.ts +24 -0
  30. package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.js +27 -0
  31. package/dist/features/execution/react/hooks/useExecutionTab.js +10 -1
  32. package/dist/features/execution/react/hooks/useLocalCliAgentCard.d.ts +48 -0
  33. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +110 -0
  34. package/dist/features/execution/react/hooks/useLocalCliAgentList.d.ts +19 -0
  35. package/dist/features/execution/react/hooks/useLocalCliAgentList.js +13 -0
  36. package/dist/features/execution/rules.d.ts +13 -0
  37. package/dist/features/execution/rules.js +14 -0
  38. package/dist/features/execution/types.d.ts +2 -0
  39. package/dist/features/html-editor/canvas-embed-placeholders.d.ts +13 -1
  40. package/dist/features/html-editor/canvas-embed-placeholders.js +61 -12
  41. package/dist/features/html-editor/dom.d.ts +5 -0
  42. package/dist/features/html-editor/dom.js +10 -0
  43. package/dist/features/html-editor/editor-blocks.d.ts +71 -0
  44. package/dist/features/html-editor/editor-blocks.js +185 -0
  45. package/dist/features/html-editor/editor-serialization.d.ts +24 -0
  46. package/dist/features/html-editor/editor-serialization.js +110 -0
  47. package/dist/features/html-editor/index.d.ts +5 -0
  48. package/dist/features/html-editor/index.js +3 -0
  49. package/dist/features/html-editor/protected-elements.d.ts +64 -0
  50. package/dist/features/html-editor/protected-elements.js +189 -0
  51. package/dist/features/html-editor/react/components/InteractiveHtmlEditor.d.ts +6 -4
  52. package/dist/features/html-editor/react/components/InteractiveHtmlEditor.js +3 -3
  53. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.d.ts +9 -1
  54. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +107 -196
  55. package/dist/features/html-editor/source-splice.d.ts +23 -0
  56. package/dist/features/html-editor/source-splice.js +45 -0
  57. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +4 -0
  58. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +4 -0
  59. package/dist/features/i18n/dictionaries/settings-dialog.de.js +4 -0
  60. package/dist/features/i18n/dictionaries/settings-dialog.en.js +4 -0
  61. package/dist/features/i18n/dictionaries/settings-dialog.es.js +4 -0
  62. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +4 -0
  63. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +4 -0
  64. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +4 -0
  65. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +4 -0
  66. package/dist/features/i18n/dictionaries/settings-dialog.id.js +4 -0
  67. package/dist/features/i18n/dictionaries/settings-dialog.it.js +4 -0
  68. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +4 -0
  69. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +4 -0
  70. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +4 -0
  71. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +4 -0
  72. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +4 -0
  73. package/dist/features/i18n/dictionaries/settings-dialog.th.js +4 -0
  74. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +4 -0
  75. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +4 -0
  76. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +4 -0
  77. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +4 -0
  78. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +4 -0
  79. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  80. package/dist/features/interactive-ui/providers/native/data-table.js +8 -1
  81. package/dist/features/interactive-ui/providers/native/data-table.manifest.d.ts +3 -5
  82. package/dist/features/interactive-ui/providers/native/data-table.manifest.js +5 -7
  83. package/dist/features/interactive-ui/providers/recharts/bar-chart.js +8 -3
  84. package/dist/features/interactive-ui/providers/recharts/chart-config.d.ts +40 -0
  85. package/dist/features/interactive-ui/providers/recharts/chart-config.js +32 -0
  86. package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +18 -12
  87. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +6 -11
  88. package/dist/features/interactive-ui/providers/recharts/chart.d.ts +31 -0
  89. package/dist/features/interactive-ui/providers/recharts/chart.js +34 -0
  90. package/dist/features/interactive-ui/providers/recharts/line-chart.js +10 -3
  91. package/dist/features/interactive-ui/providers/recharts/pie-chart.js +8 -3
  92. package/dist/features/interactive-ui/providers/shadcn/button.d.ts +1 -1
  93. package/dist/features/interactive-ui/providers/shadcn/data-table.d.ts +5 -1
  94. package/dist/features/interactive-ui/providers/shadcn/data-table.js +26 -2
  95. package/dist/features/interactive-ui/providers/shadcn/data-table.manifest.d.ts +8 -1
  96. package/dist/features/interactive-ui/providers/shadcn/data-table.manifest.js +6 -3
  97. package/dist/features/interactive-ui/table-data.d.ts +25 -0
  98. package/dist/features/interactive-ui/table-data.js +36 -0
  99. package/dist/features/mcp-ui/escape.js +2 -6
  100. package/dist/features/mcp-ui/sandbox-proxy-link-forwarder.d.ts +26 -0
  101. package/dist/features/mcp-ui/sandbox-proxy-link-forwarder.js +40 -0
  102. package/dist/features/mcp-ui/sandbox-proxy.d.ts +5 -1
  103. package/dist/features/mcp-ui/sandbox-proxy.js +11 -0
  104. package/dist/features/mcp-ui/surfaces/bridge.js +9 -3
  105. package/dist/features/mcp-ui/surfaces/document.d.ts +11 -4
  106. package/dist/features/mcp-ui/surfaces/document.js +25 -3
  107. package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
  108. package/dist/features/media-providers/constants.d.ts +0 -7
  109. package/dist/features/media-providers/constants.js +7 -15
  110. package/dist/features/media-providers/react/components/MediaProvidersTab.js +3 -2
  111. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +6 -3
  112. package/dist/features/memory/dependencies.d.ts +2 -0
  113. package/dist/features/memory/dependencies.js +9 -25
  114. package/dist/features/memory/index.d.ts +1 -0
  115. package/dist/features/memory/index.js +1 -0
  116. package/dist/features/memory/react/MemoryPorts.d.ts +17 -0
  117. package/dist/features/memory/react/MemoryPorts.js +15 -0
  118. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.d.ts +2 -2
  119. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +7 -7
  120. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.d.ts +2 -3
  121. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +4 -5
  122. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.d.ts +2 -3
  123. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -5
  124. package/dist/features/memory/react/hooks/useMemoryExtractions.hooks.js +2 -2
  125. package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +2 -2
  126. package/dist/features/panel-kit/fetch-query/adapter.react.js +5 -3
  127. package/dist/features/panel-kit/fetch-query/adapter.tanstack.d.ts +11 -0
  128. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +212 -0
  129. package/dist/features/panel-kit/fetch-query/binding.react.d.ts +17 -0
  130. package/dist/features/panel-kit/fetch-query/binding.react.js +24 -0
  131. package/dist/features/panel-kit/fetch-query/environment.browser.d.ts +1 -1
  132. package/dist/features/panel-kit/fetch-query/index.d.ts +6 -3
  133. package/dist/features/panel-kit/fetch-query/index.js +2 -1
  134. package/dist/features/panel-kit/fetch-query/tanstack.d.ts +8 -0
  135. package/dist/features/panel-kit/fetch-query/tanstack.js +4 -0
  136. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +3 -1
  137. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +9 -2
  138. package/dist/features/settings/dialog/styles/settings-dialog.css +169 -21
  139. package/dist/features/tabbed-dialog/styles/tabbed-dialog.css +91 -0
  140. package/dist/react/components/RemixIcon.d.ts +1 -41
  141. package/dist/react/components/RemixIcon.js +3 -176
  142. package/dist/react/components/RemixIcon.stylesheet.d.ts +84 -0
  143. package/dist/react/components/RemixIcon.stylesheet.js +208 -0
  144. package/dist/react/components/remixicon-font-recovery.d.ts +55 -0
  145. package/dist/react/components/remixicon-font-recovery.js +110 -0
  146. package/dist/styles/admin.css +8 -2
  147. package/package.json +25 -8
  148. package/dist/features/panel-kit/fetch-query/cache.d.ts +0 -123
  149. package/dist/features/panel-kit/fetch-query/cache.js +0 -231
  150. package/dist/features/panel-kit/fetch-query/types.d.ts +0 -91
  151. package/dist/features/panel-kit/fetch-query/types.js +0 -2
@@ -1,10 +1,12 @@
1
- import { useCallback, useEffect, useRef } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import grapesjs, {} from 'grapesjs';
3
3
  import { applyCanvasContentWrapper } from '../../canvas-content-wrapper.js';
4
4
  import { applyCanvasEmbedPlaceholders } from '../../canvas-embed-placeholders.js';
5
5
  import { buildCanvasStyleConfig, CANVAS_STYLES_PENDING_CLASS } from '../../canvas-style.js';
6
- import { prettifyCss } from '../../css.js';
7
- import { indexElementSourceRanges, pathKey, spliceElementInner } from '../../source-splice.js';
6
+ import { serializeEditorSource } from '../../editor-serialization.js';
7
+ import { prepareEditorSource } from '../../editor-blocks.js';
8
+ import { assertPreparedSourceModel, assertProtectedSourceMetadata, EditorSourceError, PROTECTED_ELEMENT_TYPE, protectedCanvasDescriber, protectTextAncestors, registerProtectedElementType } from '../../protected-elements.js';
9
+ import { pathKey } from '../../source-splice.js';
8
10
  /**
9
11
  * @file `InteractiveHtmlEditor`'s GrapesJS lifecycle: construct the editor once per mount against
10
12
  * the container ref, wire content-change notifications out, and tear the editor down on unmount.
@@ -22,87 +24,52 @@ import { indexElementSourceRanges, pathKey, spliceElementInner } from '../../sou
22
24
  * fresh document only mounts this component while it's active and remounts with fresh `html` — see
23
25
  * `@jini-ai/admin/react`'s `InteractiveHtmlEditor` adapter and its caller in the host product's `PageEditor.tsx`.
24
26
  *
25
- * **This module has zero knowledge of what "protected" means.** It only knows how to register a
26
- * locked GrapesJS component type for whatever `isProtectedElement` predicate its caller supplies —
27
- * see this file's `registerProtectedElementType`. The predicate itself (e.g. recognizing a host's own
28
- * embed-placeholder convention) belongs to the caller, not this primitive.
27
+ * **This module has zero knowledge of host embed conventions.** The shared source detector owns
28
+ * native/runtime blocks; the caller owns its embed predicate and descriptions. Both go through
29
+ * `../../protected-elements.ts`'s single raw-source type and the existing canvas card renderer.
29
30
  *
30
31
  * **`canvasStyling.contentWrapper` is applied on `load`, against the live canvas — never folded into
31
32
  * `components` above.** See `../../canvas-content-wrapper.ts`'s own file header for why: GrapesJS's
32
33
  * exported HTML/CSS are built from its component model, which `components` seeds and this wrapper
33
34
  * never touches, so it cannot leak into a save no matter how the canvas is edited afterward.
34
35
  */
35
- const PROTECTED_ELEMENT_TYPE = 'protected-element';
36
36
  /** Bold/italic/link only. GrapesJS's other built-in RTE actions (underline, strikethrough, wrap) are
37
37
  * omitted, not merely unconfigured, by naming exactly these three rather than leaving the default
38
38
  * action list in place. */
39
39
  const RTE_ACTIONS = ['bold', 'italic', 'link'];
40
- /**
41
- * Registers the `protected-element` component type so any parsed element `isProtectedElement`
42
- * recognizes becomes one atomic block: its content can never be edited (excluded from RTE
43
- * content-editing, including as a nested island inside a `text` component), dropped into, or styled,
44
- * but the element itself is selectable and can be deliberately moved, copied or deleted like any other
45
- * block. Selectable matters for safety: a non-selectable element hands a click to its nearest
46
- * selectable ancestor, so clicking an embed selected the block around it and Delete then removed that
47
- * block together with the embed. Pair it with `applyCanvasEmbedPlaceholders`, which makes the marker
48
- * the clickable surface of its card. Passed as a `plugins` entry so it registers before
49
- * `editor.setComponents` parses the initial `html` (see `initInteractiveHtmlEditor` below).
50
- *
51
- * @complexity O(1) — one type registration; the per-element check GrapesJS then runs during parsing
52
- * is O(1) per element (whatever `isProtectedElement` costs), not owned by this function.
53
- * @overallScore 100
54
- */
55
- function registerProtectedElementType(editor, isProtectedElement) {
56
- editor.Components.addType(PROTECTED_ELEMENT_TYPE, {
57
- isComponent: (el) => (isProtectedElement(el) ? { type: PROTECTED_ELEMENT_TYPE } : undefined),
58
- model: {
59
- defaults: {
60
- editable: false,
61
- droppable: false,
62
- stylable: false,
63
- layerable: false,
64
- badgable: false,
65
- selectable: true,
66
- hoverable: true,
67
- highlightable: true,
68
- removable: true,
69
- draggable: true,
70
- copyable: true,
71
- },
72
- },
73
- });
74
- }
75
- function initInteractiveHtmlEditor(container, html, isProtectedElement, canvasStyling) {
40
+ function initInteractiveHtmlEditor(container, sourcePlan, canvasStyling, options) {
76
41
  return grapesjs.init({
77
42
  container,
78
43
  height: '100%',
79
44
  fromElement: false,
80
- components: html,
45
+ components: sourcePlan.html,
81
46
  storageManager: { type: 'none' },
82
47
  panels: { defaults: [] },
83
48
  blockManager: { blocks: [] },
84
49
  richTextEditor: { actions: RTE_ACTIONS },
85
- // Default `true`. Left `true`, GrapesJS rewrites every inline `style="…"` into a generated
86
- // `#iXXXX` CSS rule the moment ANY edit triggers a re-sync — including on elements the operator
87
- // never touched — which is one of Bug C's root-cause symptoms (see this repo's `pages-redo` plan,
88
- // "Bug C"). `false` here is the editor-wide config half of that fix; `serializeCleaned` below is
89
- // the other half (the splice path does not depend on this at all, since it never re-serializes
90
- // untouched elements from the component model in the first place).
50
+ // Both default to `true`: avoidInlineStyle moves styles into ID rules; forceClass converts
51
+ // them to .cNNNN rules even with avoidInlineStyle disabled, including on untouched elements.
52
+ // Keep authored styles inline for structural saves (Bug C in this repo's `pages-redo` plan).
53
+ // serializeCleaned's JSON CSS export is the other half; the splice path already keeps bytes.
91
54
  avoidInlineStyle: false,
92
- canvas: buildCanvasStyleConfig(canvasStyling),
93
- plugins: isProtectedElement
94
- ? [(editor) => registerProtectedElementType(editor, isProtectedElement)]
95
- : [],
55
+ forceClass: false,
56
+ canvas: buildCanvasStyleConfig({ ...canvasStyling, css: [canvasStyling.css, sourcePlan.canvasCss].filter(Boolean).join('\n') }),
57
+ // Structural export can only preserve whitespace gaps the parser retained in the model.
58
+ parser: { optionsHtml: { allowScripts: false, keepEmptyTextNodes: true } },
59
+ plugins: [(editor) => registerProtectedElementType({ editor, sourcePlan }, {
60
+ onRequestEditHtml: options.onRequestEditHtml, editHtmlLabel: options.translate?.('Edit HTML'),
61
+ beforeRequestEditHtml: () => disableActiveTextEditing(editor), onSerializationError: options.onSerializationError,
62
+ })],
96
63
  });
97
64
  }
98
65
  /** Mirrors `source-splice.ts`'s own `UNINDEXED_TAGS` filter, but for GrapesJS's live component tree
99
66
  * rather than a parsed HTML string: `textnode` and `comment` components never consume a path slot,
100
- * matching how `indexElementSourceRanges` numbers the original string. No equivalent check is needed
101
- * for `style`/`script` here — GrapesJS never represents either as a tree component in the first
102
- * place (moved into `CssComposer`, or dropped outright), so a live sibling list already excludes
103
- * them without this hook's help. */
67
+ * matching how `indexElementSourceRanges` numbers the original string. Raw style/script atoms
68
+ * now stay in the tree, but their original source tag still excludes them from path slots; before
69
+ * source preservation GrapesJS moved styles into CssComposer and dropped scripts outright. */
104
70
  function isPathableComponent(component) {
105
- return !component.is('textnode') && !component.is('comment');
71
+ return !component.is('textnode') && !component.is('comment')
72
+ && !['style', 'script'].includes(component.get?.('sourceTagName'));
106
73
  }
107
74
  /**
108
75
  * `component`'s element-only child path from the editor's wrapper root, in the exact numbering
@@ -164,94 +131,6 @@ function isInsideTextComponent(component) {
164
131
  }
165
132
  return false;
166
133
  }
167
- /** Dev-only, so a fallback is never silent — see `serializeWithSplice` for each trigger this names. */
168
- function warnFallback(reason) {
169
- if (process.env.NODE_ENV !== 'production') {
170
- console.warn(`[useInteractiveHtmlEditor] falling back to full re-serialization: ${reason}`);
171
- }
172
- }
173
- /**
174
- * Fallback whole-document serialization for when the lossless splice (see `serializeWithSplice`)
175
- * cannot be trusted. This is the "cleaned" formula from this repo's `pages-redo` plan ("Bug C"), and
176
- * differs from the whole-document serialization this hook used before the splice existed:
177
- * `avoidProtected: true` on `getCss()` drops GrapesJS's own `* { box-sizing: border-box; } body
178
- * {margin: 0;}` block, which GrapesJS otherwise prepends even to a page with no CSS of its own; and
179
- * `getWrapper().getInnerHTML()` — the wrapper's INNER content — omits the `<body>` tag `editor.
180
- * getHtml()` serializes the wrapper component itself as. Combined with `avoidInlineStyle: false` on
181
- * the editor's own config (see `initInteractiveHtmlEditor`), inline `style="…"` attributes also
182
- * survive here instead of being rewritten into generated `#iXXXX` CSS rules. Still not a byte-
183
- * identical round trip — this is a full re-serialization from GrapesJS's component model, which
184
- * cannot recover the original string's entities or authored formatting — just the least-lossy shape
185
- * available when a true splice is not possible.
186
- *
187
- * `keepUnusedStyles: true` carries over unchanged from the serialization this replaces: real
188
- * documents commonly carry `@media` blocks and `::before`/`::after` rules that don't correspond to
189
- * any single "matched" component in the tree, which GrapesJS's default CSS export can otherwise drop.
190
- */
191
- function serializeCleaned(editor) {
192
- const css = editor.getCss({ avoidProtected: true, keepUnusedStyles: true });
193
- const wrapper = editor.getWrapper();
194
- // `wrapper` is undefined only before/without a successful init, which cannot be true once this
195
- // hook's `editor` has fired `update` — defensive only, mirroring `serializeCleaned`'s own doc.
196
- const html = wrapper ? wrapper.getInnerHTML() : editor.getHtml();
197
- return css ? `<style>${prettifyCss(css)}</style>${html}` : html;
198
- }
199
- /**
200
- * Builds this edit cycle's exported HTML. The default path splices every accumulated dirty text edit
201
- * (see `recordDirtyTextEdit`) back into the ORIGINAL source string via `spliceElementInner` (see
202
- * `source-splice.ts`), so every other byte — entities, attribute quoting, an untouched embed marker,
203
- * unrelated formatting — passes through unchanged instead of being rebuilt from GrapesJS's component
204
- * model. Falls back to `serializeCleaned` (and warns via `warnFallback`, so it is never silent)
205
- * whenever that guarantee cannot be trusted:
206
- * - `structuralChange` — a block was added or removed since mount (a block move, clone, or delete;
207
- * a move fires a remove then an add). Its value names the first such event, for the warning. The
208
- * child churn inside a text component that every RTE close causes does not count (see
209
- * `isInsideTextComponent`); that is a text edit, which the splice covers. Every dirty path's offset was computed against the
210
- * pristine `original` string; once the LIVE tree's shape has diverged from it, those offsets (and
211
- * even the paths themselves) can silently address the wrong element instead of failing loudly, so
212
- * this is checked first and unconditionally, regardless of what is or isn't in `dirty`. There is no
213
- * path back to `false` once this flips — a later purely-textual edit still cannot trust offsets
214
- * computed before the structural change, so the whole rest of the mount stays on the cleaned path.
215
- * - `dirty` is empty — `update` fired (a real content/style/attribute mutation happened) but no
216
- * text-component edit was ever recorded for it. Nothing addressable means nothing spliceable.
217
- * - any individual `spliceElementInner` call returns `null` — that path's element is no longer where
218
- * it was, or its tag no longer matches what the live component believes.
219
- *
220
- * Dirty edits are applied in descending original-offset order (highest first): a splice at a later
221
- * offset never shifts the byte positions of anything before it, so replaying strictly back-to-front
222
- * lets every subsequent `spliceElementInner` call re-locate its own (still-untouched-until-its-turn)
223
- * range correctly without this function tracking running offset deltas itself.
224
- *
225
- * @complexity O(n·k) where n = size of `original` (each of up to k dirty splices re-indexes the
226
- * current string internally) and k = number of dirty edits accumulated this mount — small in
227
- * practice (an operator edits a handful of elements per session), never a hot loop here.
228
- */
229
- function serializeWithSplice(editor, original, dirty, structuralChange) {
230
- if (structuralChange) {
231
- warnFallback(`a block was added, moved, copied or deleted since mount (first: ${structuralChange})`);
232
- return serializeCleaned(editor);
233
- }
234
- if (dirty.size === 0) {
235
- warnFallback('no text edit was recorded for this change');
236
- return serializeCleaned(editor);
237
- }
238
- const originalIndex = indexElementSourceRanges(original);
239
- const ordered = [...dirty.values()].sort((a, b) => {
240
- const aStart = originalIndex.get(pathKey(a.path))?.start ?? -1;
241
- const bStart = originalIndex.get(pathKey(b.path))?.start ?? -1;
242
- return bStart - aStart;
243
- });
244
- let result = original;
245
- for (const edit of ordered) {
246
- const spliced = spliceElementInner(result, edit.path, edit.tagName, edit.component.getInnerHTML());
247
- if (spliced === null) {
248
- warnFallback(`element at path "${pathKey(edit.path)}" could not be spliced (removed, or its tag drifted)`);
249
- return serializeCleaned(editor);
250
- }
251
- result = spliced;
252
- }
253
- return result;
254
- }
255
134
  /**
256
135
  * Ends whatever RTE session is currently open, so its pending edit syncs into the component model
257
136
  * before `flush` reads it — GrapesJS only writes an RTE session's edited text into the model when
@@ -297,7 +176,7 @@ const STYLESHEET_SETTLE_TIMEOUT_MS = 3000;
297
176
  * @complexity O(n) in the number of `<link rel="stylesheet">` elements found — one listener pair per
298
177
  * link, no polling.
299
178
  */
300
- function revealCanvasWhenStylesheetsSettle(editor, body) {
179
+ function revealCanvasWhenStylesheetsSettle(editor, body, onSettled) {
301
180
  body.classList.add(CANVAS_STYLES_PENDING_CLASS);
302
181
  const canvasDocument = editor.Canvas.getDocument();
303
182
  // `getDocument()` returning null is not reachable once `load` has fired on a real editor — defensive
@@ -317,6 +196,8 @@ function revealCanvasWhenStylesheetsSettle(editor, body) {
317
196
  link.removeEventListener('error', handleSettle);
318
197
  }
319
198
  pending.clear();
199
+ if (links.length > 0)
200
+ onSettled();
320
201
  body.classList.remove(CANVAS_STYLES_PENDING_CLASS);
321
202
  };
322
203
  function handleSettle(event) {
@@ -335,44 +216,9 @@ function revealCanvasWhenStylesheetsSettle(editor, body) {
335
216
  timeoutId = setTimeout(reveal, STYLESHEET_SETTLE_TIMEOUT_MS);
336
217
  return reveal;
337
218
  }
338
- /**
339
- * Owns one GrapesJS `Editor` instance for the lifetime of the calling component's mount. `onChange`
340
- * fires with the editor's current serialized content (see `serializeWithSplice`) on every content
341
- * mutation (GrapesJS's `update` event — components, styles, or attributes changing); it is not
342
- * debounced, so a caller replacing an existing uncontrolled `<textarea>` (which already pushes a full
343
- * value on every keystroke) sees matching behavior.
344
- *
345
- * **Mounts into a dedicated child node, not the wrapper div `containerRef` itself, and removes that
346
- * child node (not just its content) on cleanup.** Defensive, for React StrictMode's dev-time
347
- * double-invoke (mount -> cleanup -> mount again, synchronously): if a destroyed instance's canvas
348
- * were still mid-render when cleanup ran, clearing the container's *current* content at that moment
349
- * cannot remove DOM that does not exist yet, and a later async write from the discarded instance
350
- * would land after the fact. Detaching the whole per-instance node sidesteps that class of ordering
351
- * problem entirely — whatever a destroyed instance's async work later does, it mutates a node no
352
- * longer connected to `document`, so it can never become visible or queryable in the live page no
353
- * matter when it runs. (Investigated live via Playwright while chasing an unrelated symptom — two
354
- * canvas iframes coexisting after one mount — which turned out to be a separate, harmless GrapesJS
355
- * quirk unrelated to StrictMode: `Canvas`'s `FramesView` always constructs one `FrameView` that never
356
- * proceeds past construction to `render()`, alongside the real one; it keeps Backbone's default
357
- * `class="frame"` forever, inert and unrendered. `.gjs-frame` — the class `FrameView.render()`
358
- * assigns — is what callers should select on, never a bare `iframe` under the wrapper.)
359
- *
360
- * **`flush()` exists because GrapesJS's RTE only syncs on `disableEditing`, and `update` is
361
- * double-debounced and never fires after `off()`.** A host that calls `onChange` on every keystroke
362
- * (like an uncontrolled `<textarea>`) still misses the operator's last edit if they click Save or
363
- * navigate away while an RTE session is still open: GrapesJS writes that session's text into the
364
- * component model only when it closes, and by the time a host's save handler runs, this hook has
365
- * already stopped listening (or the debounce hasn't fired yet). `flush` closes any open RTE session,
366
- * computes and emits the current content the same way `handleUpdate` does, and hands the result back
367
- * directly — see its own doc below for exactly what it returns and when.
368
- *
369
- * @complexity O(1) setup/teardown. Steady-state cost is GrapesJS's own serialization per edit,
370
- * proportional to document size — not something this hook controls or should hide a note about
371
- * beyond that.
372
- * @overallScore 100
373
- */
374
- export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, canvasStyling = {}, describeEmbedPlaceholder) {
219
+ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, canvasStyling = {}, describeEmbedPlaceholder, options = {}) {
375
220
  const wrapperRef = useRef(null);
221
+ const [sourceError, setSourceError] = useState();
376
222
  const onChangeRef = useRef(onChange);
377
223
  onChangeRef.current = onChange;
378
224
  // Delegated to by the stable `flush` callback returned below. Reset to a no-op on cleanup, before
@@ -382,10 +228,48 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
382
228
  const wrapper = wrapperRef.current;
383
229
  if (!wrapper)
384
230
  return;
231
+ const sourcePlan = prepareEditorSource({ html }, { isProtectedElement, describeHostElement: describeEmbedPlaceholder, translate: options.translate });
232
+ let integrityFailure;
233
+ const reportSourceError = (error) => {
234
+ integrityFailure = error;
235
+ setSourceError(options.translate?.(error.message) ?? error.message);
236
+ options.onSerializationError?.(error);
237
+ };
238
+ if (!sourcePlan.ok) {
239
+ reportSourceError(new EditorSourceError({ message: sourcePlan.problem }, {}));
240
+ return;
241
+ }
385
242
  const mountEl = document.createElement('div');
386
243
  mountEl.style.height = '100%';
387
244
  wrapper.appendChild(mountEl);
388
- const editor = initInteractiveHtmlEditor(mountEl, html, isProtectedElement, canvasStyling);
245
+ let editor;
246
+ try {
247
+ editor = initInteractiveHtmlEditor(mountEl, sourcePlan, canvasStyling, { ...options, onSerializationError: reportSourceError });
248
+ }
249
+ catch {
250
+ reportSourceError(new EditorSourceError());
251
+ mountEl.remove();
252
+ return;
253
+ }
254
+ const decorate = () => {
255
+ protectTextAncestors({ editor }, {});
256
+ const describe = sourcePlan.instances.length ? protectedCanvasDescriber({ editor }, {}) : describeEmbedPlaceholder;
257
+ if (describe)
258
+ applyCanvasEmbedPlaceholders(editor.Canvas.getBody(), describe);
259
+ };
260
+ const serialize = () => {
261
+ try {
262
+ const next = serializeEditorSource({ editor, html, edits: dirty, sourcePlan }, { structuralChange });
263
+ if (next.includes(sourcePlan.markerAttribute))
264
+ throw new EditorSourceError();
265
+ return next;
266
+ }
267
+ catch (error) {
268
+ const failure = error instanceof EditorSourceError ? error : new EditorSourceError();
269
+ reportSourceError(failure);
270
+ throw failure;
271
+ }
272
+ };
389
273
  // Splice bookkeeping — see `serializeWithSplice`'s own doc for how these two are used. Plain
390
274
  // closure state scoped to this one mount-only effect run (like `editor`/`mountEl` above), not
391
275
  // `useRef`: nothing outside this effect ever reads them.
@@ -398,22 +282,26 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
398
282
  // Set inside `handleLoad`, once the canvas body exists — see `revealCanvasWhenStylesheetsSettle`.
399
283
  // `undefined` until then (or if `load` never fires before unmount), so cleanup below guards it.
400
284
  let cancelStylesheetWait;
285
+ let canvasWindow;
401
286
  // Embed placeholder cards live only in the canvas DOM, so a structural edit (delete, move, clone,
402
287
  // undo) can leave one stale. Repaired on the tick after the edit, once GrapesJS has rendered it;
403
288
  // see `applyCanvasEmbedPlaceholders`. Only after `load`: before it, adds are the initial parse.
404
289
  let loaded = false;
405
290
  let cardRepair;
406
291
  const scheduleCardRepair = () => {
407
- if (!loaded || !describeEmbedPlaceholder || cardRepair !== undefined)
292
+ if (!loaded || cardRepair !== undefined)
408
293
  return;
409
294
  cardRepair = setTimeout(() => {
410
295
  cardRepair = undefined;
411
- applyCanvasEmbedPlaceholders(editor.Canvas.getBody(), describeEmbedPlaceholder);
296
+ decorate();
412
297
  }, 0);
413
298
  };
414
299
  const handleUpdate = () => {
415
300
  changed = true;
416
- onChangeRef.current(serializeWithSplice(editor, html, dirty, structuralChange));
301
+ try {
302
+ onChangeRef.current(serialize());
303
+ }
304
+ catch { /* Source error is already visible; retain the last intact working copy. */ }
417
305
  };
418
306
  // `component:update:content` fires with the changed component itself as its argument (GrapesJS's
419
307
  // general `component:update:{propertyName}` pattern); `rte:disable` fires with `(view, rte)` —
@@ -429,7 +317,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
429
317
  // for the rest of this mount; see its own doc for why this never resets.
430
318
  const handleStructuralChange = (event) => (component) => {
431
319
  scheduleCardRepair();
432
- if (structuralChange || (component && isInsideTextComponent(component)))
320
+ if (structuralChange || (component && !component.is(PROTECTED_ELEMENT_TYPE) && isInsideTextComponent(component)))
433
321
  return;
434
322
  structuralChange = `${event} <${component?.tagName?.toLowerCase() || 'unknown'}>`;
435
323
  };
@@ -447,9 +335,19 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
447
335
  // structural change happened either.
448
336
  flushRef.current = async () => {
449
337
  await disableActiveTextEditing(editor);
338
+ if (integrityFailure)
339
+ throw integrityFailure;
340
+ try {
341
+ assertProtectedSourceMetadata({ editor }, {});
342
+ }
343
+ catch {
344
+ const failure = new EditorSourceError();
345
+ reportSourceError(failure);
346
+ throw failure;
347
+ }
450
348
  if (!changed && dirty.size === 0 && !structuralChange)
451
349
  return undefined;
452
- const next = serializeWithSplice(editor, html, dirty, structuralChange);
350
+ const next = serialize();
453
351
  onChangeRef.current(next);
454
352
  return next;
455
353
  };
@@ -461,14 +359,26 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
461
359
  // Applied AFTER the content wrapper so a placeholder can sit correctly however deep the wrapper
462
360
  // chain nests its marker, though nothing about either function actually depends on this order.
463
361
  const handleLoad = () => {
362
+ if (sourcePlan.instances.length) {
363
+ try {
364
+ assertPreparedSourceModel({ editor, sourcePlan }, {});
365
+ }
366
+ catch {
367
+ reportSourceError(new EditorSourceError());
368
+ return;
369
+ }
370
+ }
464
371
  const body = editor.Canvas.getBody();
465
372
  applyCanvasContentWrapper(body, canvasStyling.contentWrapper);
466
- if (describeEmbedPlaceholder)
467
- applyCanvasEmbedPlaceholders(body, describeEmbedPlaceholder);
373
+ decorate();
468
374
  // Runs LAST, after both decorations above — see `revealCanvasWhenStylesheetsSettle`'s own doc
469
375
  // for why that order matters even in the zero-stylesheet case.
470
- cancelStylesheetWait = revealCanvasWhenStylesheetsSettle(editor, body);
376
+ cancelStylesheetWait = revealCanvasWhenStylesheetsSettle(editor, body, decorate);
471
377
  loaded = true;
378
+ // An iframe window's native resize event already covers canvas-width changes. Reuse it
379
+ // instead of maintaining a second ResizeObserver lifecycle for the same view geometry.
380
+ canvasWindow = editor.Canvas.getDocument()?.defaultView;
381
+ canvasWindow?.addEventListener('resize', scheduleCardRepair);
472
382
  };
473
383
  editor.on('load', handleLoad);
474
384
  return () => {
@@ -482,6 +392,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
482
392
  editor.off('component:remove', handleRemove);
483
393
  editor.off('load', handleLoad);
484
394
  cancelStylesheetWait?.();
395
+ canvasWindow?.removeEventListener('resize', scheduleCardRepair);
485
396
  clearTimeout(cardRepair);
486
397
  editor.destroy();
487
398
  mountEl.remove();
@@ -491,6 +402,6 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
491
402
  // Stable across renders (the effect above mutates `flushRef.current`, not this callback's identity)
492
403
  // so a caller can safely pass `flush` into a `useCallback`/`useImperativeHandle` dependency array.
493
404
  const flush = useCallback(() => flushRef.current(), []);
494
- return { containerRef: wrapperRef, flush };
405
+ return { containerRef: wrapperRef, flush, sourceError };
495
406
  }
496
407
  //# sourceMappingURL=useInteractiveHtmlEditor.js.map
@@ -53,4 +53,27 @@ export declare function indexElementSourceRanges(html: string): Map<string, Elem
53
53
  * @overallScore 100
54
54
  */
55
55
  export declare function spliceElementInner(html: string, path: ElementPath, expectedTagName: string, newInner: string): string | null;
56
+ /** Verified offsets are JS string indexes (UTF-16), suitable for slice and textarea selection. */
57
+ export interface ElementOuterRange {
58
+ readonly outerStart: number;
59
+ readonly outerEnd: number;
60
+ readonly openingEnd: number;
61
+ }
62
+ /** Source tree shared by block detection and preparation; raw atoms never consume a splice slot. */
63
+ export interface SourceElement {
64
+ readonly tagName: string;
65
+ readonly attributes: Readonly<Record<string, string>>;
66
+ readonly path: ElementPath | null;
67
+ readonly outerRange: ElementOuterRange | null;
68
+ readonly sourceSupported: boolean;
69
+ readonly children: readonly SourceElement[];
70
+ }
71
+ /**
72
+ * Extends the inner-splice index without changing its published range shape. Missing end tags,
73
+ * implicit nodes and reordered/foster-parented children cannot establish an opaque source owner.
74
+ * Validation is linear in source nodes; ranges are checked against their immediate source parent.
75
+ */
76
+ export declare function indexSourceElements({ html }: {
77
+ html: string;
78
+ }, _options?: object): SourceElement[];
56
79
  //# sourceMappingURL=source-splice.d.ts.map
@@ -86,4 +86,49 @@ export function spliceElementInner(html, path, expectedTagName, newInner) {
86
86
  return null;
87
87
  return html.slice(0, range.start) + newInner + html.slice(range.end);
88
88
  }
89
+ const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
90
+ /**
91
+ * Extends the inner-splice index without changing its published range shape. Missing end tags,
92
+ * implicit nodes and reordered/foster-parented children cannot establish an opaque source owner.
93
+ * Validation is linear in source nodes; ranges are checked against their immediate source parent.
94
+ */
95
+ export function indexSourceElements({ html }, _options = {}) {
96
+ const fragment = parseFragment(html, { sourceCodeLocationInfo: true });
97
+ const walk = (nodes, parentPath, parentStart, parentEnd) => {
98
+ let slot = 0;
99
+ const elements = nodes.filter(isElement).map(node => {
100
+ const atom = UNINDEXED_TAGS.has(node.tagName);
101
+ const path = atom ? null : [...parentPath, slot++];
102
+ const loc = node.sourceCodeLocation;
103
+ const start = loc?.startTag;
104
+ const end = loc?.endTag;
105
+ const voidTag = VOID_TAGS.has(node.tagName) || (node.namespaceURI !== 'http://www.w3.org/1999/xhtml'
106
+ && !!start && html.slice(start.startOffset, start.endOffset).endsWith('/>'));
107
+ const outerEnd = voidTag ? start?.endOffset : end?.endOffset;
108
+ const boundsValid = !!start && outerEnd !== undefined && start.startOffset >= parentStart && outerEnd <= parentEnd;
109
+ const children = walk(node.childNodes, path ?? parentPath, start?.endOffset ?? parentStart, end?.startOffset ?? parentEnd);
110
+ // Template content is a separate fragment; it has no ordinary component path/shell here.
111
+ const valid = boundsValid && node.tagName !== 'template';
112
+ return {
113
+ tagName: node.tagName,
114
+ attributes: Object.fromEntries(node.attrs.map(attr => [attr.name, attr.value])),
115
+ path,
116
+ outerRange: valid ? { outerStart: start.startOffset, outerEnd: outerEnd, openingEnd: start.endOffset } : null,
117
+ sourceSupported: valid && children.every(child => child.sourceSupported),
118
+ children,
119
+ };
120
+ });
121
+ // Foster parenting can move a valid-looking node ahead of a source-earlier table. Neither
122
+ // sibling can be trusted as a model-addressable source owner in that recovered ordering.
123
+ for (let i = 1; i < elements.length; i++) {
124
+ const previous = elements[i - 1];
125
+ const current = elements[i];
126
+ if (previous.outerRange && current.outerRange && previous.outerRange.outerEnd > current.outerRange.outerStart) {
127
+ previous.sourceSupported = current.sourceSupported = false;
128
+ }
129
+ }
130
+ return elements;
131
+ };
132
+ return walk(fragment.childNodes, [], 0, html.length);
133
+ }
89
134
  //# sourceMappingURL=source-splice.js.map
@@ -7,6 +7,7 @@ export const SETTINGS_DIALOG_AR = {
7
7
  'About': 'حول',
8
8
  'Accent color': 'لون التمييز',
9
9
  'Account': 'الحساب',
10
+ 'Advanced': 'متقدّم',
10
11
  'Advanced: proxy & custom paths': 'متقدم: الوكيل والمسارات المخصصة',
11
12
  'Agent check failed': 'فشل التحقق من الوكيل',
12
13
  'Agent is ready.': 'الوكيل جاهز.',
@@ -131,6 +132,9 @@ export const SETTINGS_DIALOG_AR = {
131
132
  'Not available for this daemon.': 'غير متاح لهذه الخدمة الخلفية.',
132
133
  'Not configured': 'غير مهيّأ',
133
134
  'Not installed': 'غير مثبَّت',
135
+ 'Show {count} not installed': 'إظهار {count} غير مثبَّتة',
136
+ 'Hide not installed': 'إخفاء غير المثبَّتة',
137
+ 'Show not-installed CLIs': 'إظهار واجهات CLI غير المثبَّتة',
134
138
  'Not sharing': 'لا تتم المشاركة',
135
139
  'Nothing is shared unless you opt in.': 'لا تتم مشاركة أي شيء ما لم تفعّل ذلك بنفسك.',
136
140
  'Off': 'إيقاف',
@@ -7,6 +7,7 @@ export const SETTINGS_DIALOG_BN = {
7
7
  'About': 'সম্পর্কে',
8
8
  'Accent color': 'হাইলাইট রং',
9
9
  'Account': 'অ্যাকাউন্ট',
10
+ 'Advanced': 'উন্নত',
10
11
  'Advanced: proxy & custom paths': 'উন্নত: প্রক্সি ও কাস্টম পাথ',
11
12
  'Agent check failed': 'এজেন্ট পরীক্ষা ব্যর্থ হয়েছে',
12
13
  'Agent is ready.': 'এজেন্ট প্রস্তুত।',
@@ -131,6 +132,9 @@ export const SETTINGS_DIALOG_BN = {
131
132
  'Not available for this daemon.': 'এই ডিমনের জন্য উপলব্ধ নয়।',
132
133
  'Not configured': 'কনফিগার করা হয়নি',
133
134
  'Not installed': 'ইনস্টল করা নেই',
135
+ 'Show {count} not installed': '{count}টি ইনস্টল না করা দেখান',
136
+ 'Hide not installed': 'ইনস্টল না করা লুকান',
137
+ 'Show not-installed CLIs': 'ইনস্টল না করা CLI দেখান',
134
138
  'Not sharing': 'শেয়ার করা হচ্ছে না',
135
139
  'Nothing is shared unless you opt in.': 'আপনি সম্মতি না দেওয়া পর্যন্ত কিছুই শেয়ার করা হয় না।',
136
140
  'Off': 'বন্ধ',
@@ -7,6 +7,7 @@ export const SETTINGS_DIALOG_DE = {
7
7
  'About': 'Info',
8
8
  'Accent color': 'Akzentfarbe',
9
9
  'Account': 'Konto',
10
+ 'Advanced': 'Erweitert',
10
11
  'Advanced: proxy & custom paths': 'Erweitert: Proxy und benutzerdefinierte Pfade',
11
12
  'Agent check failed': 'Agentenprüfung fehlgeschlagen',
12
13
  'Agent is ready.': 'Agent ist bereit.',
@@ -131,6 +132,9 @@ export const SETTINGS_DIALOG_DE = {
131
132
  'Not available for this daemon.': 'Für diesen Daemon nicht verfügbar.',
132
133
  'Not configured': 'Nicht konfiguriert',
133
134
  'Not installed': 'Nicht installiert',
135
+ 'Show {count} not installed': '{count} nicht installierte anzeigen',
136
+ 'Hide not installed': 'Nicht installierte ausblenden',
137
+ 'Show not-installed CLIs': 'Nicht installierte CLIs anzeigen',
134
138
  'Not sharing': 'Wird nicht geteilt',
135
139
  'Nothing is shared unless you opt in.': 'Es wird nichts geteilt, außer du stimmst ausdrücklich zu.',
136
140
  'Off': 'Aus',
@@ -7,6 +7,7 @@ export const SETTINGS_DIALOG_EN = {
7
7
  'About': 'About',
8
8
  'Accent color': 'Accent color',
9
9
  'Account': 'Account',
10
+ 'Advanced': 'Advanced',
10
11
  'Advanced: proxy & custom paths': 'Advanced: proxy & custom paths',
11
12
  'Agent check failed': 'Agent check failed',
12
13
  'Agent is ready.': 'Agent is ready.',
@@ -131,6 +132,9 @@ export const SETTINGS_DIALOG_EN = {
131
132
  'Not available for this daemon.': 'Not available for this daemon.',
132
133
  'Not configured': 'Not configured',
133
134
  'Not installed': 'Not installed',
135
+ 'Show {count} not installed': 'Show {count} not installed',
136
+ 'Hide not installed': 'Hide not installed',
137
+ 'Show not-installed CLIs': 'Show not-installed CLIs',
134
138
  'Not sharing': 'Not sharing',
135
139
  'Nothing is shared unless you opt in.': 'Nothing is shared unless you opt in.',
136
140
  'Off': 'Off',
@@ -7,6 +7,7 @@ export const SETTINGS_DIALOG_ES = {
7
7
  'About': 'Acerca de',
8
8
  'Accent color': 'Color de acento',
9
9
  'Account': 'Cuenta',
10
+ 'Advanced': 'Avanzado',
10
11
  'Advanced: proxy & custom paths': 'Avanzado: proxy y rutas personalizadas',
11
12
  'Agent check failed': 'Falló la comprobación del agente',
12
13
  'Agent is ready.': 'El agente está listo.',
@@ -131,6 +132,9 @@ export const SETTINGS_DIALOG_ES = {
131
132
  'Not available for this daemon.': 'No disponible para este daemon.',
132
133
  'Not configured': 'No configurado',
133
134
  'Not installed': 'No instalado',
135
+ 'Show {count} not installed': 'Mostrar {count} no instalados',
136
+ 'Hide not installed': 'Ocultar no instalados',
137
+ 'Show not-installed CLIs': 'Mostrar CLIs no instalados',
134
138
  'Not sharing': 'No se comparte',
135
139
  'Nothing is shared unless you opt in.': 'No se comparte nada a menos que lo actives.',
136
140
  'Off': 'Desactivado',
@@ -7,6 +7,7 @@ export const SETTINGS_DIALOG_FA = {
7
7
  'About': 'درباره',
8
8
  'Accent color': 'رنگ تأکیدی',
9
9
  'Account': 'حساب کاربری',
10
+ 'Advanced': 'پیشرفته',
10
11
  'Advanced: proxy & custom paths': 'پیشرفته: پراکسی و مسیرهای سفارشی',
11
12
  'Agent check failed': 'بررسی عامل ناموفق بود',
12
13
  'Agent is ready.': 'عامل آماده است.',
@@ -131,6 +132,9 @@ export const SETTINGS_DIALOG_FA = {
131
132
  'Not available for this daemon.': 'برای این دیمون در دسترس نیست.',
132
133
  'Not configured': 'پیکربندی‌نشده',
133
134
  'Not installed': 'نصب‌نشده',
135
+ 'Show {count} not installed': 'نمایش {count} مورد نصب‌نشده',
136
+ 'Hide not installed': 'پنهان کردن موارد نصب‌نشده',
137
+ 'Show not-installed CLIs': 'نمایش CLIهای نصب‌نشده',
134
138
  'Not sharing': 'اشتراک‌گذاری نمی‌شود',
135
139
  'Nothing is shared unless you opt in.': 'تا زمانی که خودتان فعال نکنید، چیزی به اشتراک گذاشته نمی‌شود.',
136
140
  'Off': 'خاموش',