@flowdrop/flowdrop 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +10 -0
  2. package/dist/chat/responseParser.js +7 -0
  3. package/dist/commands/parser.js +12 -0
  4. package/dist/components/App.svelte +92 -54
  5. package/dist/components/App.svelte.d.ts +13 -0
  6. package/dist/components/ConfigModal.svelte +2 -1
  7. package/dist/components/ConfigPanel.svelte +3 -2
  8. package/dist/components/FlowDropZone.svelte +2 -1
  9. package/dist/components/LogsSidebar.svelte +3 -2
  10. package/dist/components/Navbar.svelte +10 -6
  11. package/dist/components/NodeSidebar.svelte +4 -3
  12. package/dist/components/NodeStatusOverlay.svelte +14 -7
  13. package/dist/components/NodeSwapPicker.svelte +2 -1
  14. package/dist/components/PipelineStatus.svelte +10 -7
  15. package/dist/components/ReadOnlyDetails.svelte +4 -2
  16. package/dist/components/SchemaForm.svelte +20 -9
  17. package/dist/components/SchemaForm.svelte.d.ts +2 -4
  18. package/dist/components/SettingsModal.svelte +4 -3
  19. package/dist/components/SettingsPanel.svelte +3 -2
  20. package/dist/components/SwapMappingEditor.svelte +2 -1
  21. package/dist/components/WorkflowEditor.svelte +3 -2
  22. package/dist/components/chat/AIChatPanel.svelte +33 -8
  23. package/dist/components/chat/AIChatPanel.svelte.d.ts +3 -0
  24. package/dist/components/chat/CommandPreview.svelte +10 -6
  25. package/dist/components/console/CommandConsole.svelte +4 -3
  26. package/dist/components/form/FormArray.svelte +33 -20
  27. package/dist/components/form/FormArray.svelte.d.ts +3 -1
  28. package/dist/components/form/FormAutocomplete.svelte +18 -7
  29. package/dist/components/form/FormCodeEditor.svelte +2 -1
  30. package/dist/components/form/FormFieldWrapper.svelte +2 -1
  31. package/dist/components/form/FormMarkdownEditor.svelte +152 -108
  32. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +1 -1
  33. package/dist/components/form/FormTemplateEditor.svelte +2 -1
  34. package/dist/components/form/FormToggle.svelte +23 -5
  35. package/dist/components/form/FormToggle.svelte.d.ts +6 -2
  36. package/dist/components/interrupt/ChoicePrompt.svelte +14 -5
  37. package/dist/components/interrupt/ConfirmationPrompt.svelte +8 -5
  38. package/dist/components/interrupt/FormPrompt.svelte +28 -7
  39. package/dist/components/interrupt/InterruptBubble.svelte +27 -18
  40. package/dist/components/interrupt/ReviewPrompt.svelte +32 -22
  41. package/dist/components/interrupt/TextInputPrompt.svelte +12 -5
  42. package/dist/components/layouts/MainLayout.svelte +4 -3
  43. package/dist/components/nodes/GatewayNode.svelte +8 -3
  44. package/dist/components/nodes/IdeaNode.svelte +2 -1
  45. package/dist/components/nodes/NotesNode.svelte +18 -12
  46. package/dist/components/nodes/WorkflowNode.svelte +8 -3
  47. package/dist/components/playground/ChatPanel.svelte +36 -24
  48. package/dist/components/playground/MessageBubble.svelte +15 -7
  49. package/dist/components/playground/Playground.svelte +2 -1
  50. package/dist/components/playground/PlaygroundModal.svelte +2 -1
  51. package/dist/components/playground/SessionManager.svelte +14 -10
  52. package/dist/core/index.d.ts +2 -0
  53. package/dist/core/index.js +9 -0
  54. package/dist/editor/index.d.ts +1 -1
  55. package/dist/editor/index.js +1 -1
  56. package/dist/messages/context.d.ts +29 -0
  57. package/dist/messages/context.js +38 -0
  58. package/dist/messages/defaults.d.ts +396 -0
  59. package/dist/messages/defaults.js +356 -0
  60. package/dist/messages/deprecation.d.ts +20 -0
  61. package/dist/messages/deprecation.js +33 -0
  62. package/dist/messages/index.d.ts +11 -0
  63. package/dist/messages/index.js +10 -0
  64. package/dist/messages/merge.d.ts +28 -0
  65. package/dist/messages/merge.js +53 -0
  66. package/dist/messages/types.d.ts +29 -0
  67. package/dist/messages/types.js +13 -0
  68. package/dist/services/draftStorage.d.ts +13 -0
  69. package/dist/services/draftStorage.js +36 -0
  70. package/dist/styles/base.css +13 -4
  71. package/dist/svelte-app.d.ts +11 -0
  72. package/dist/svelte-app.js +11 -2
  73. package/package.json +1 -1
@@ -30,6 +30,7 @@
30
30
  interruptActions,
31
31
  getInterruptByMessageId
32
32
  } from '../../stores/interruptStore.svelte.js';
33
+ import { m } from '../../messages/index.js';
33
34
 
34
35
  /**
35
36
  * Component props
@@ -78,7 +79,7 @@
78
79
  let {
79
80
  showTimestamps = true,
80
81
  autoScroll = true,
81
- placeholder = 'Type your message...',
82
+ placeholder,
82
83
  onSendMessage,
83
84
  onStopExecution,
84
85
  showLogsInline = false,
@@ -86,10 +87,21 @@
86
87
  onInterruptResolved,
87
88
  showChatInput = true,
88
89
  showRunButton = true,
89
- predefinedMessage = 'Run workflow',
90
+ predefinedMessage,
90
91
  compactSystemMessages = true
91
92
  }: Props = $props();
92
93
 
94
+ // Hoist playground branches — states/actions are read 8+ times each in the
95
+ // template. Single getter walk per render instead of per-string.
96
+ const states = $derived(m().playground.states);
97
+ const actions = $derived(m().playground.actions);
98
+ const chat = $derived(m().playground.chat);
99
+
100
+ // Playground placeholders/labels are configurable per-instance (workflow
101
+ // author) but fall back to the localized messages tree when not provided.
102
+ const resolvedPlaceholder = $derived(placeholder ?? chat.placeholder);
103
+ const resolvedPredefinedMessage = $derived(predefinedMessage ?? chat.predefinedRun);
104
+
93
105
  /**
94
106
  * Tracks whether the Run button is enabled.
95
107
  * Starts as true, becomes false after Run is clicked,
@@ -283,7 +295,7 @@
283
295
  }
284
296
  // Disable the Run button after clicking
285
297
  runEnabled = false;
286
- onSendMessage?.(predefinedMessage);
298
+ onSendMessage?.(resolvedPredefinedMessage);
287
299
  }
288
300
 
289
301
  /**
@@ -468,16 +480,16 @@
468
480
  </svg>
469
481
  </div>
470
482
  {#if noInputsAvailable}
471
- <h2 class="chat-panel__welcome-title">View only</h2>
483
+ <h2 class="chat-panel__welcome-title">{states.viewOnlyTitle}</h2>
472
484
  <p class="chat-panel__welcome-text">
473
- This playground is in view-only mode. No inputs are available.
485
+ {states.viewOnlyText}
474
486
  </p>
475
487
  {:else if showChatInput}
476
- <h2 class="chat-panel__welcome-title">New session</h2>
477
- <p class="chat-panel__welcome-text">Test your flow with a prompt</p>
488
+ <h2 class="chat-panel__welcome-title">{states.newSessionTitle}</h2>
489
+ <p class="chat-panel__welcome-text">{states.newSessionText}</p>
478
490
  {:else}
479
- <h2 class="chat-panel__welcome-title">Ready to run</h2>
480
- <p class="chat-panel__welcome-text">Click Run to execute your workflow</p>
491
+ <h2 class="chat-panel__welcome-title">{states.readyTitle}</h2>
492
+ <p class="chat-panel__welcome-text">{states.readyText}</p>
481
493
  {/if}
482
494
  </div>
483
495
  {:else if showEmptyChat}
@@ -509,16 +521,16 @@
509
521
  </svg>
510
522
  </div>
511
523
  {#if noInputsAvailable}
512
- <h2 class="chat-panel__welcome-title">View only</h2>
524
+ <h2 class="chat-panel__welcome-title">{states.viewOnlyTitle}</h2>
513
525
  <p class="chat-panel__welcome-text">
514
- This playground is in view-only mode. No inputs are available.
526
+ {states.viewOnlyText}
515
527
  </p>
516
528
  {:else if showChatInput}
517
- <h2 class="chat-panel__welcome-title">New session</h2>
518
- <p class="chat-panel__welcome-text">Test your flow with a prompt</p>
529
+ <h2 class="chat-panel__welcome-title">{states.newSessionTitle}</h2>
530
+ <p class="chat-panel__welcome-text">{states.newSessionText}</p>
519
531
  {:else}
520
- <h2 class="chat-panel__welcome-title">Ready to run</h2>
521
- <p class="chat-panel__welcome-text">Click Run to execute your workflow</p>
532
+ <h2 class="chat-panel__welcome-title">{states.readyTitle}</h2>
533
+ <p class="chat-panel__welcome-text">{states.readyText}</p>
522
534
  {/if}
523
535
  </div>
524
536
  {:else}
@@ -552,7 +564,7 @@
552
564
  <span></span>
553
565
  <span></span>
554
566
  </div>
555
- <span class="chat-panel__typing-text">Processing...</span>
567
+ <span class="chat-panel__typing-text">{states.processing}</span>
556
568
  </div>
557
569
  {/if}
558
570
  {/if}
@@ -564,7 +576,7 @@
564
576
  <!-- No inputs available - show informational message -->
565
577
  <div class="chat-panel__no-inputs">
566
578
  <Icon icon="mdi:information-outline" />
567
- <span>View-only mode. Workflow execution is controlled externally.</span>
579
+ <span>{states.viewOnlyHelp}</span>
568
580
  </div>
569
581
  {:else}
570
582
  <div
@@ -577,7 +589,7 @@
577
589
  bind:this={inputField}
578
590
  bind:value={inputValue}
579
591
  class="chat-panel__input"
580
- {placeholder}
592
+ placeholder={resolvedPlaceholder}
581
593
  rows="1"
582
594
  disabled={getIsExecuting()}
583
595
  onkeydown={handleKeydown}
@@ -591,10 +603,10 @@
591
603
  type="button"
592
604
  class="chat-panel__stop-btn"
593
605
  onclick={handleStop}
594
- title="Stop execution"
606
+ title={actions.stopTitle}
595
607
  >
596
608
  <Icon icon="mdi:stop" />
597
- Stop
609
+ {actions.stop}
598
610
  </button>
599
611
  {:else if showChatInput}
600
612
  <button
@@ -602,9 +614,9 @@
602
614
  class="chat-panel__send-btn"
603
615
  onclick={handleSend}
604
616
  disabled={!inputValue.trim()}
605
- title="Send message"
617
+ title={actions.sendTitle}
606
618
  >
607
- Send
619
+ {actions.send}
608
620
  </button>
609
621
  {:else if showRunButton}
610
622
  <button
@@ -612,10 +624,10 @@
612
624
  class="chat-panel__run-btn"
613
625
  onclick={handleRun}
614
626
  disabled={!runEnabled}
615
- title={runEnabled ? 'Run workflow' : 'Waiting for workflow to be ready...'}
627
+ title={runEnabled ? actions.runTitle : actions.runWaitingTitle}
616
628
  >
617
629
  <Icon icon="mdi:play" />
618
- Run
630
+ {actions.run}
619
631
  </button>
620
632
  {/if}
621
633
  </div>
@@ -17,6 +17,7 @@
17
17
  PlaygroundMessageMetadata,
18
18
  PlaygroundMessageRole
19
19
  } from '../../types/playground.js';
20
+ import { m } from '../../messages/index.js';
20
21
 
21
22
  /**
22
23
  * Component props
@@ -91,17 +92,18 @@
91
92
  * @returns Display label
92
93
  */
93
94
  function getRoleLabel(role: PlaygroundMessageRole, metadata?: PlaygroundMessageMetadata): string {
95
+ const roles = m().playground.roles;
94
96
  switch (role) {
95
97
  case 'user':
96
- return metadata?.userName ?? 'You';
98
+ return metadata?.userName ?? roles.you;
97
99
  case 'assistant':
98
- return 'Assistant';
100
+ return roles.assistant;
99
101
  case 'system':
100
- return 'System';
102
+ return roles.system;
101
103
  case 'log':
102
- return metadata?.nodeLabel ?? 'Log';
104
+ return metadata?.nodeLabel ?? roles.log;
103
105
  default:
104
- return 'Message';
106
+ return roles.message;
105
107
  }
106
108
  }
107
109
 
@@ -208,13 +210,19 @@
208
210
  {#if message.metadata?.duration !== undefined || message.nodeId}
209
211
  <div class="message-bubble__footer">
210
212
  {#if message.nodeId}
211
- <span class="message-bubble__node" title="Node ID: {message.nodeId}">
213
+ <span
214
+ class="message-bubble__node"
215
+ title={m().playground.messageTooltips.nodeId({ id: message.nodeId })}
216
+ >
212
217
  <Icon icon="mdi:graph" />
213
218
  {message.metadata?.nodeLabel ?? message.nodeId}
214
219
  </span>
215
220
  {/if}
216
221
  {#if message.metadata?.duration !== undefined}
217
- <span class="message-bubble__duration" title="Execution duration">
222
+ <span
223
+ class="message-bubble__duration"
224
+ title={m().playground.messageTooltips.executionDuration}
225
+ >
218
226
  <Icon icon="mdi:timer-outline" />
219
227
  {formatDuration(message.metadata.duration)}
220
228
  </span>
@@ -29,6 +29,7 @@
29
29
  } from '../../stores/playgroundStore.svelte.js';
30
30
  import { interruptActions } from '../../stores/interruptStore.svelte.js';
31
31
  import { logger } from '../../utils/logger.js';
32
+ import { m } from '../../messages/index.js';
32
33
 
33
34
  /**
34
35
  * Component props
@@ -545,7 +546,7 @@
545
546
  role="button"
546
547
  tabindex="0"
547
548
  title="Click to load this session"
548
- aria-label="Load session: {session.name}"
549
+ aria-label={m().layout.loadSession({ name: session.name })}
549
550
  onclick={() => handleSelectSession(session.id)}
550
551
  onkeydown={(e) => e.key === 'Enter' && handleSelectSession(session.id)}
551
552
  >
@@ -12,6 +12,7 @@
12
12
  import type { Workflow } from '../../types/index.js';
13
13
  import type { EndpointConfig } from '../../config/endpoints.js';
14
14
  import type { PlaygroundConfig } from '../../types/playground.js';
15
+ import { m } from '../../messages/index.js';
15
16
 
16
17
  /**
17
18
  * Component props
@@ -86,7 +87,7 @@
86
87
  type="button"
87
88
  class="playground-modal__close-btn"
88
89
  onclick={onClose}
89
- aria-label="Close playground modal"
90
+ aria-label={m().layout.closePlaygroundModal}
90
91
  >
91
92
  <Icon icon="mdi:close" />
92
93
  </button>
@@ -16,6 +16,7 @@
16
16
  getIsLoading,
17
17
  getSessionCount
18
18
  } from '../../stores/playgroundStore.svelte.js';
19
+ import { m } from '../../messages/index.js';
19
20
 
20
21
  /**
21
22
  * Component props
@@ -44,6 +45,9 @@
44
45
  mode = 'sidebar'
45
46
  }: Props = $props();
46
47
 
48
+ // Hoist the sessions branch — used in formatDate plus several template reads.
49
+ const t = $derived(m().playground.sessions);
50
+
47
51
  /** Session pending deletion (for confirmation) */
48
52
  let pendingDeleteId = $state<string | null>(null);
49
53
 
@@ -105,16 +109,16 @@
105
109
  const diffDays = Math.floor(diffMs / 86400000);
106
110
 
107
111
  if (diffMins < 1) {
108
- return 'Just now';
112
+ return t.justNow;
109
113
  }
110
114
  if (diffMins < 60) {
111
- return `${diffMins}m ago`;
115
+ return t.minutesAgo({ n: diffMins });
112
116
  }
113
117
  if (diffHours < 24) {
114
- return `${diffHours}h ago`;
118
+ return t.hoursAgo({ n: diffHours });
115
119
  }
116
120
  if (diffDays < 7) {
117
- return `${diffDays}d ago`;
121
+ return t.daysAgo({ n: diffDays });
118
122
  }
119
123
  return date.toLocaleDateString('en-US', {
120
124
  month: 'short',
@@ -170,7 +174,7 @@
170
174
  >
171
175
  <div class="session-manager__title">
172
176
  <Icon icon="mdi:history" />
173
- <span>Sessions</span>
177
+ <span>{t.header}</span>
174
178
  {#if getSessionCount() > 0}
175
179
  <span class="session-manager__count">{getSessionCount()}</span>
176
180
  {/if}
@@ -192,7 +196,7 @@
192
196
  disabled={getIsLoading()}
193
197
  >
194
198
  <Icon icon="mdi:plus" />
195
- New Session
199
+ {t.newSession}
196
200
  </button>
197
201
 
198
202
  <!-- Sessions List -->
@@ -200,7 +204,7 @@
200
204
  {#if getSessions().length === 0}
201
205
  <div class="session-manager__empty">
202
206
  <Icon icon="mdi:chat-outline" />
203
- <span>No sessions yet</span>
207
+ <span>{t.empty}</span>
204
208
  </div>
205
209
  {:else}
206
210
  {#each getSessions() as session (session.id)}
@@ -236,7 +240,7 @@
236
240
  type="button"
237
241
  class="session-manager__delete-btn session-manager__delete-btn--confirm"
238
242
  onclick={(e) => handleDeleteClick(e, session.id)}
239
- title="Click again to confirm"
243
+ title={t.clickAgainToConfirm}
240
244
  >
241
245
  <Icon icon="mdi:check" />
242
246
  </button>
@@ -244,7 +248,7 @@
244
248
  type="button"
245
249
  class="session-manager__delete-btn session-manager__delete-btn--cancel"
246
250
  onclick={cancelDelete}
247
- title="Cancel"
251
+ title={t.cancel}
248
252
  >
249
253
  <Icon icon="mdi:close" />
250
254
  </button>
@@ -253,7 +257,7 @@
253
257
  type="button"
254
258
  class="session-manager__delete-btn"
255
259
  onclick={(e) => handleDeleteClick(e, session.id)}
256
- title="Delete session"
260
+ title={t.deleteSession}
257
261
  >
258
262
  <Icon icon="mdi:delete-outline" />
259
263
  </button>
@@ -82,3 +82,5 @@ export { logger, setLogLevel, getLogLevel } from '../utils/logger.js';
82
82
  export type { LogLevel } from '../utils/logger.js';
83
83
  export { computeSwapPreview, computeSwapPreviewWithOptions, executeSwap, mapConfig, getVersionUpgrade, compareSemver, performSwap, validateSwapResult, SwapValidationError } from '../utils/nodeSwap.js';
84
84
  export type { SwapPreview, SwapResult, PortMapping, DroppedEdge, MatchQuality, PortMappingOverride, ConfigMappingOverride, SwapOptions, SwapStrategy, SwapStrategyContext, SwapEventContext } from '../utils/nodeSwap.js';
85
+ export { defaultMessages, mergeMessages, setMessages } from '../messages/index.js';
86
+ export type { Messages, MessagesOverride } from '../messages/index.js';
@@ -101,3 +101,12 @@ export { logger, setLogLevel, getLogLevel } from '../utils/logger.js';
101
101
  // Node Swap
102
102
  // ============================================================================
103
103
  export { computeSwapPreview, computeSwapPreviewWithOptions, executeSwap, mapConfig, getVersionUpgrade, compareSemver, performSwap, validateSwapResult, SwapValidationError } from '../utils/nodeSwap.js';
104
+ // ============================================================================
105
+ // Messages (typed, overridable, translation-ready user-facing strings)
106
+ //
107
+ // Consumers pass `messages={partial}` (or a getter) to <FlowDrop>; type the
108
+ // partial as `MessagesOverride`. `setMessages` + `mergeMessages` are exported
109
+ // for ad-hoc mounts that render flowdrop components outside the root provider
110
+ // (Storybook stories, standalone <SchemaForm>, etc.) — see the i18n guide.
111
+ // ============================================================================
112
+ export { defaultMessages, mergeMessages, setMessages } from '../messages/index.js';
@@ -82,7 +82,7 @@ export { globalSaveWorkflow, globalExportWorkflow } from '../services/globalSave
82
82
  export { fetchPortConfig, validatePortConfig } from '../services/portConfigApi.js';
83
83
  export { fetchCategories, validateCategories } from '../services/categoriesApi.js';
84
84
  export { fetchDynamicSchema, resolveExternalEditUrl, getEffectiveConfigEditOptions, clearSchemaCache, invalidateSchemaCache, hasConfigEditOptions, shouldShowExternalEdit, shouldUseDynamicSchema } from '../services/dynamicSchemaService.js';
85
- export { getDraftStorageKey, saveDraft, loadDraft, deleteDraft, hasDraft, getDraftMetadata, DraftAutoSaveManager } from '../services/draftStorage.js';
85
+ export { getDraftStorageKey, saveDraft, loadDraft, deleteDraft, hasDraft, getDraftMetadata, clearAllDrafts, DraftAutoSaveManager } from '../services/draftStorage.js';
86
86
  export { EnhancedFlowDropApiClient, ApiError } from '../api/enhanced-client.js';
87
87
  export { isLoopbackEdge, isValidLoopbackCycle, PortCompatibilityChecker, initializePortCompatibility, getPortCompatibilityChecker, getPossibleConnections, validateConnection, getConnectionSuggestions, hasCycles, hasInvalidCycles, getExecutionOrder } from '../utils/connections.js';
88
88
  export { fetchRuntimeConfig, getRuntimeConfig, clearRuntimeConfigCache, initRuntimeConfig } from '../config/runtimeConfig.js';
@@ -125,7 +125,7 @@ export { globalSaveWorkflow, globalExportWorkflow } from '../services/globalSave
125
125
  export { fetchPortConfig, validatePortConfig } from '../services/portConfigApi.js';
126
126
  export { fetchCategories, validateCategories } from '../services/categoriesApi.js';
127
127
  export { fetchDynamicSchema, resolveExternalEditUrl, getEffectiveConfigEditOptions, clearSchemaCache, invalidateSchemaCache, hasConfigEditOptions, shouldShowExternalEdit, shouldUseDynamicSchema } from '../services/dynamicSchemaService.js';
128
- export { getDraftStorageKey, saveDraft, loadDraft, deleteDraft, hasDraft, getDraftMetadata, DraftAutoSaveManager } from '../services/draftStorage.js';
128
+ export { getDraftStorageKey, saveDraft, loadDraft, deleteDraft, hasDraft, getDraftMetadata, clearAllDrafts, DraftAutoSaveManager } from '../services/draftStorage.js';
129
129
  // ============================================================================
130
130
  // API Clients
131
131
  // ============================================================================
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Svelte context plumbing for the messages system.
3
+ *
4
+ * The context value is a **getter** `() => Messages`, not a snapshot. Storing
5
+ * a getter lets the consumer's i18n library drive reactivity transparently:
6
+ * they pass `messages={() => somethingReactive()}` to `<FlowDrop>`, the root
7
+ * `$derived`s the merged tree, and `m()` re-reads it on every access. No
8
+ * subscription, no store, no flowdrop-owned reactive primitive.
9
+ *
10
+ * Components rendered outside the root provider (Storybook stories, ad-hoc
11
+ * mounts) silently fall back to `defaultMessages` rather than throwing — see
12
+ * `getMessages()` below. Trade-off: a missing provider produces English
13
+ * regardless of the consumer's locale, by design.
14
+ */
15
+ import type { Messages } from './types.js';
16
+ export declare function setMessages(getter: () => Messages): void;
17
+ /**
18
+ * Returns the current messages getter. Components should call the result on
19
+ * every read so locale switches in the consumer's i18n library propagate
20
+ * without a subscription.
21
+ *
22
+ * Outside a provider, returns a getter that always yields `defaultMessages`.
23
+ */
24
+ export declare function getMessages(): () => Messages;
25
+ /**
26
+ * Shorthand for the common pattern `getMessages()()` — read the current
27
+ * merged messages tree once. Use inside templates: `m().form.array.moveUp`.
28
+ */
29
+ export declare function m(): Messages;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Svelte context plumbing for the messages system.
3
+ *
4
+ * The context value is a **getter** `() => Messages`, not a snapshot. Storing
5
+ * a getter lets the consumer's i18n library drive reactivity transparently:
6
+ * they pass `messages={() => somethingReactive()}` to `<FlowDrop>`, the root
7
+ * `$derived`s the merged tree, and `m()` re-reads it on every access. No
8
+ * subscription, no store, no flowdrop-owned reactive primitive.
9
+ *
10
+ * Components rendered outside the root provider (Storybook stories, ad-hoc
11
+ * mounts) silently fall back to `defaultMessages` rather than throwing — see
12
+ * `getMessages()` below. Trade-off: a missing provider produces English
13
+ * regardless of the consumer's locale, by design.
14
+ */
15
+ import { getContext, setContext } from 'svelte';
16
+ import { defaultMessages } from './defaults.js';
17
+ const KEY = Symbol('flowdrop.messages');
18
+ const defaultGetter = () => defaultMessages;
19
+ export function setMessages(getter) {
20
+ setContext(KEY, getter);
21
+ }
22
+ /**
23
+ * Returns the current messages getter. Components should call the result on
24
+ * every read so locale switches in the consumer's i18n library propagate
25
+ * without a subscription.
26
+ *
27
+ * Outside a provider, returns a getter that always yields `defaultMessages`.
28
+ */
29
+ export function getMessages() {
30
+ return getContext(KEY) ?? defaultGetter;
31
+ }
32
+ /**
33
+ * Shorthand for the common pattern `getMessages()()` — read the current
34
+ * merged messages tree once. Use inside templates: `m().form.array.moveUp`.
35
+ */
36
+ export function m() {
37
+ return getMessages()();
38
+ }