@flowdrop/flowdrop 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@
9
9
  import MainLayout from "./layouts/MainLayout.svelte";
10
10
  import WorkflowEditor from "./WorkflowEditor.svelte";
11
11
  import NodeSidebar from "./NodeSidebar.svelte";
12
+ import Icon from "@iconify/svelte";
12
13
  import ConfigForm from "./ConfigForm.svelte";
13
14
  import ConfigPanel from "./ConfigPanel.svelte";
14
15
  import Navbar from "./Navbar.svelte";
@@ -46,12 +47,16 @@
46
47
  } from "../services/globalSave.js";
47
48
  import { apiToasts, dismissToast } from "../services/toastService.js";
48
49
  import { initAutoSave } from "../services/autoSaveService.js";
49
- import { getUiSettings } from "../stores/settingsStore.svelte.js";
50
+ import {
51
+ getUiSettings,
52
+ updateSettings,
53
+ } from "../stores/settingsStore.svelte.js";
50
54
  import { initializePortCompatibility } from "../utils/connections.js";
51
55
  import { DEFAULT_PORT_CONFIG } from "../config/defaultPortConfig.js";
52
56
  import { workflowFormatRegistry } from "../registry/workflowFormatRegistry.js";
53
57
  import { logger } from "../utils/logger.js";
54
58
  import { validateWorkflowData } from "../utils/validation.js";
59
+ import type { SettingsCategory } from "../types/settings.js";
55
60
 
56
61
  /**
57
62
  * Configuration props for runtime customization
@@ -104,6 +109,12 @@
104
109
  features?: FlowDropFeatures;
105
110
  /** Visual theme — named built-in ('default' | 'minimal') or custom theme object */
106
111
  theme?: FlowDropTheme | FlowDropThemeName;
112
+ /** Which settings tabs to show in the modal */
113
+ settingsCategories?: SettingsCategory[];
114
+ /** Show the "Sync to Cloud" button in the settings modal */
115
+ showSettingsSyncButton?: boolean;
116
+ /** Show the reset buttons in the settings modal */
117
+ showSettingsResetButton?: boolean;
107
118
  }
108
119
 
109
120
  let {
@@ -126,6 +137,9 @@
126
137
  eventHandlers,
127
138
  features: propFeatures,
128
139
  theme: themeProp,
140
+ settingsCategories,
141
+ showSettingsSyncButton,
142
+ showSettingsResetButton,
129
143
  }: Props = $props();
130
144
 
131
145
  // svelte-ignore state_referenced_locally — feature flags don't change at runtime
@@ -667,12 +681,22 @@
667
681
 
668
682
  /**
669
683
  * Calculate left sidebar width based on collapsed state
670
- * When collapsed, use 48px; otherwise use user-configured width
684
+ * When collapsed, use 0; otherwise use user-configured width
671
685
  */
672
686
  const leftSidebarWidth = $derived(
673
- getUiSettings().sidebarCollapsed ? 48 : getUiSettings().sidebarWidth,
687
+ getUiSettings().sidebarCollapsed ? 0 : getUiSettings().sidebarWidth,
674
688
  );
675
689
 
690
+ /** Whether the sidebar is collapsed */
691
+ const isSidebarCollapsed = $derived(getUiSettings().sidebarCollapsed);
692
+
693
+ /** Toggle sidebar collapsed state */
694
+ function toggleSidebar(): void {
695
+ updateSettings({
696
+ ui: { sidebarCollapsed: !getUiSettings().sidebarCollapsed },
697
+ });
698
+ }
699
+
676
700
  // File input reference for workflow import
677
701
  let fileInputRef = $state<HTMLInputElement | null>(null);
678
702
  </script>
@@ -705,8 +729,8 @@
705
729
  headerHeight={60}
706
730
  {leftSidebarWidth}
707
731
  rightSidebarWidth={400}
708
- leftSidebarMinWidth={getUiSettings().sidebarCollapsed ? 48 : 280}
709
- leftSidebarMaxWidth={getUiSettings().sidebarCollapsed ? 48 : 450}
732
+ leftSidebarMinWidth={getUiSettings().sidebarCollapsed ? 0 : 280}
733
+ leftSidebarMaxWidth={getUiSettings().sidebarCollapsed ? 0 : 450}
710
734
  rightSidebarMinWidth={320}
711
735
  rightSidebarMaxWidth={550}
712
736
  enableLeftSplitPane={false}
@@ -763,6 +787,9 @@
763
787
  ]}
764
788
  showStatus={true}
765
789
  {showSettings}
790
+ {settingsCategories}
791
+ {showSettingsSyncButton}
792
+ {showSettingsResetButton}
766
793
  />
767
794
  {/snippet}
768
795
 
@@ -974,6 +1001,20 @@
974
1001
  role="region"
975
1002
  aria-label="Workflow canvas"
976
1003
  >
1004
+ <!-- Floating sidebar toggle — always visible on the canvas top-left -->
1005
+ {#if !disableSidebar}
1006
+ <button
1007
+ class="flowdrop-sidebar-fab"
1008
+ onclick={toggleSidebar}
1009
+ aria-label={isSidebarCollapsed
1010
+ ? "Expand sidebar"
1011
+ : "Collapse sidebar"}
1012
+ title={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
1013
+ >
1014
+ <Icon icon={isSidebarCollapsed ? "mdi:menu" : "mdi:menu-open"} />
1015
+ </button>
1016
+ {/if}
1017
+
977
1018
  <WorkflowEditor
978
1019
  bind:this={workflowEditorRef}
979
1020
  {nodes}
@@ -1098,6 +1139,40 @@
1098
1139
  font-weight: 500;
1099
1140
  }
1100
1141
 
1142
+ /* Floating sidebar toggle button */
1143
+ .flowdrop-sidebar-fab {
1144
+ position: absolute;
1145
+ top: 12px;
1146
+ left: 12px;
1147
+ z-index: 50;
1148
+ display: flex;
1149
+ align-items: center;
1150
+ justify-content: center;
1151
+ width: 2.25rem;
1152
+ height: 2.25rem;
1153
+ border: 1px solid var(--fd-border);
1154
+ border-radius: var(--fd-radius-md);
1155
+ background-color: var(--fd-background);
1156
+ color: var(--fd-muted-foreground);
1157
+ cursor: pointer;
1158
+ box-shadow: var(--fd-shadow-md);
1159
+ transition:
1160
+ color var(--fd-transition-fast),
1161
+ background-color var(--fd-transition-fast),
1162
+ box-shadow var(--fd-transition-fast);
1163
+ }
1164
+
1165
+ .flowdrop-sidebar-fab:hover {
1166
+ color: var(--fd-foreground);
1167
+ background-color: var(--fd-subtle);
1168
+ box-shadow: var(--fd-shadow-lg);
1169
+ }
1170
+
1171
+ .flowdrop-sidebar-fab:focus {
1172
+ outline: none;
1173
+ box-shadow: 0 0 0 2px var(--fd-ring);
1174
+ }
1175
+
1101
1176
  /* Main editor area */
1102
1177
  .flowdrop-editor-main {
1103
1178
  flex: 1;
@@ -3,6 +3,7 @@ import type { EndpointConfig } from "../config/endpoints.js";
3
3
  import type { AuthProvider } from "../types/auth.js";
4
4
  import type { FlowDropEventHandlers, FlowDropFeatures } from "../types/events.js";
5
5
  import type { FlowDropTheme, FlowDropThemeName } from "../types/theme.js";
6
+ import type { SettingsCategory } from "../types/settings.js";
6
7
  /**
7
8
  * Configuration props for runtime customization
8
9
  */
@@ -51,6 +52,12 @@ interface Props {
51
52
  features?: FlowDropFeatures;
52
53
  /** Visual theme — named built-in ('default' | 'minimal') or custom theme object */
53
54
  theme?: FlowDropTheme | FlowDropThemeName;
55
+ /** Which settings tabs to show in the modal */
56
+ settingsCategories?: SettingsCategory[];
57
+ /** Show the "Sync to Cloud" button in the settings modal */
58
+ showSettingsSyncButton?: boolean;
59
+ /** Show the reset buttons in the settings modal */
60
+ showSettingsResetButton?: boolean;
54
61
  }
55
62
  declare const App: import("svelte").Component<Props, {}, "">;
56
63
  type App = ReturnType<typeof App>;
@@ -2,6 +2,7 @@
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import CanvasBanner from "./CanvasBanner.svelte";
4
4
  import CanvasDecorator from "../stories/CanvasDecorator.svelte";
5
+ import Icon from "@iconify/svelte";
5
6
 
6
7
  const { Story } = defineMeta({
7
8
  title: "Display/CanvasBanner",
@@ -17,8 +18,11 @@
17
18
  <CanvasBanner
18
19
  title="Empty Canvas"
19
20
  description="Drag nodes from the sidebar to get started"
20
- iconName="heroicons:squares-plus"
21
- />
21
+ >
22
+ {#snippet icon()}
23
+ <Icon icon="heroicons:squares-plus" />
24
+ {/snippet}
25
+ </CanvasBanner>
22
26
  </CanvasDecorator>
23
27
  </Story>
24
28
 
@@ -25,9 +25,12 @@
25
25
  WorkflowNode,
26
26
  WorkflowEdge,
27
27
  NodeUIExtensions,
28
+ NodePort,
29
+ DynamicPort,
28
30
  ConfigEditOptions,
29
31
  AuthProvider,
30
32
  } from "../types/index.js";
33
+ import { dynamicPortToNodePort } from "../types/index.js";
31
34
  import type { UISchemaElement } from "../types/uischema.js";
32
35
  import {
33
36
  FormField,
@@ -46,6 +49,11 @@
46
49
  import { globalSaveWorkflow } from "../services/globalSave.js";
47
50
  import { getAvailableVariables } from "../services/variableService.js";
48
51
  import { logger } from "../utils/logger.js";
52
+ import {
53
+ getDataTypeColorToken,
54
+ getPortBackgroundColor,
55
+ } from "../utils/colors.js";
56
+ import { applyPortOrder } from "../utils/portUtils.js";
49
57
 
50
58
  interface Props {
51
59
  /** Optional workflow node (if provided, schema and values are derived from it) */
@@ -338,9 +346,118 @@
338
346
  uiExtensionValues = {
339
347
  hideUnconnectedHandles:
340
348
  initialUIExtensions.hideUnconnectedHandles ?? false,
349
+ portOrder: initialUIExtensions.portOrder
350
+ ? {
351
+ inputs: initialUIExtensions.portOrder.inputs
352
+ ? [...initialUIExtensions.portOrder.inputs]
353
+ : undefined,
354
+ outputs: initialUIExtensions.portOrder.outputs
355
+ ? [...initialUIExtensions.portOrder.outputs]
356
+ : undefined,
357
+ }
358
+ : undefined,
359
+ hiddenPorts: initialUIExtensions.hiddenPorts
360
+ ? {
361
+ inputs: initialUIExtensions.hiddenPorts.inputs
362
+ ? [...initialUIExtensions.hiddenPorts.inputs]
363
+ : undefined,
364
+ outputs: initialUIExtensions.hiddenPorts.outputs
365
+ ? [...initialUIExtensions.hiddenPorts.outputs]
366
+ : undefined,
367
+ }
368
+ : undefined,
341
369
  };
342
370
  });
343
371
 
372
+ /**
373
+ * All input ports in current display order for the port management UI.
374
+ * Combines static metadata inputs + dynamic config inputs, sorted by portOrder.
375
+ */
376
+ const allInputPortsForUI = $derived.by<NodePort[]>(() => {
377
+ if (!node) return [];
378
+ const staticInputs = node.data.metadata.inputs ?? [];
379
+ const dynInputs = (
380
+ (node.data.config?.dynamicInputs as DynamicPort[]) || []
381
+ ).map((p) => dynamicPortToNodePort(p, "input"));
382
+ return applyPortOrder(
383
+ [...staticInputs, ...dynInputs],
384
+ uiExtensionValues.portOrder?.inputs,
385
+ );
386
+ });
387
+
388
+ /**
389
+ * All output ports in current display order for the port management UI.
390
+ * Combines static metadata outputs + dynamic config outputs, sorted by portOrder.
391
+ */
392
+ const allOutputPortsForUI = $derived.by<NodePort[]>(() => {
393
+ if (!node) return [];
394
+ const staticOutputs = node.data.metadata.outputs ?? [];
395
+ const dynOutputs = (
396
+ (node.data.config?.dynamicOutputs as DynamicPort[]) || []
397
+ ).map((p) => dynamicPortToNodePort(p, "output"));
398
+ return applyPortOrder(
399
+ [...staticOutputs, ...dynOutputs],
400
+ uiExtensionValues.portOrder?.outputs,
401
+ );
402
+ });
403
+
404
+ /**
405
+ * Move a port one position up or down in the display order.
406
+ */
407
+ function movePort(
408
+ direction: "inputs" | "outputs",
409
+ portId: string,
410
+ delta: -1 | 1,
411
+ ): void {
412
+ const list =
413
+ direction === "inputs" ? allInputPortsForUI : allOutputPortsForUI;
414
+ const idx = list.findIndex((p) => p.id === portId);
415
+ if (idx === -1) return;
416
+ const newIdx = idx + delta;
417
+ if (newIdx < 0 || newIdx >= list.length) return;
418
+ const newOrder = list.map((p) => p.id);
419
+ [newOrder[idx], newOrder[newIdx]] = [newOrder[newIdx], newOrder[idx]];
420
+ uiExtensionValues.portOrder = {
421
+ ...uiExtensionValues.portOrder,
422
+ [direction]: newOrder,
423
+ };
424
+ handleFormBlur();
425
+ }
426
+
427
+ /**
428
+ * Toggle manual visibility of a port. Required ports cannot be hidden.
429
+ */
430
+ function togglePortHidden(
431
+ direction: "inputs" | "outputs",
432
+ portId: string,
433
+ ): void {
434
+ const current = uiExtensionValues.hiddenPorts?.[direction] ?? [];
435
+ const isHidden = current.includes(portId);
436
+ const next = isHidden
437
+ ? current.filter((id) => id !== portId)
438
+ : [...current, portId];
439
+ uiExtensionValues.hiddenPorts = {
440
+ ...uiExtensionValues.hiddenPorts,
441
+ [direction]: next.length > 0 ? next : undefined,
442
+ };
443
+ handleFormBlur();
444
+ }
445
+
446
+ /**
447
+ * Reset all port customizations (order + hidden) for a direction back to defaults.
448
+ */
449
+ function resetPortCustomizations(direction: "inputs" | "outputs"): void {
450
+ const order = { ...uiExtensionValues.portOrder };
451
+ const hidden = { ...uiExtensionValues.hiddenPorts };
452
+ delete order[direction];
453
+ delete hidden[direction];
454
+ uiExtensionValues.portOrder =
455
+ Object.keys(order).length > 0 ? order : undefined;
456
+ uiExtensionValues.hiddenPorts =
457
+ Object.keys(hidden).length > 0 ? hidden : undefined;
458
+ handleFormBlur();
459
+ }
460
+
344
461
  /**
345
462
  * Check if a field is required based on schema
346
463
  */
@@ -708,6 +825,176 @@
708
825
  }}
709
826
  />
710
827
  </FormFieldWrapper>
828
+
829
+ <!-- Input Port Order & Visibility -->
830
+ {#if allInputPortsForUI.length > 0}
831
+ <div class="config-form__port-order">
832
+ <div class="config-form__port-order-header">
833
+ <span class="config-form__port-order-label">Input Ports</span>
834
+ {#if uiExtensionValues.portOrder?.inputs?.length || uiExtensionValues.hiddenPorts?.inputs?.length}
835
+ <button
836
+ type="button"
837
+ class="config-form__port-order-reset"
838
+ onclick={() => resetPortCustomizations("inputs")}
839
+ title="Reset to default order and visibility"
840
+ >
841
+ <Icon icon="heroicons:arrow-uturn-left" />
842
+ Reset
843
+ </button>
844
+ {/if}
845
+ </div>
846
+ <ul class="config-form__port-order-list">
847
+ {#each allInputPortsForUI as port, i (port.id)}
848
+ {@const isHidden =
849
+ uiExtensionValues.hiddenPorts?.inputs?.includes(port.id) ??
850
+ false}
851
+ {@const isRequired = port.required ?? false}
852
+ <li
853
+ class="config-form__port-order-item"
854
+ class:config-form__port-order-item--hidden={isHidden}
855
+ >
856
+ <span class="config-form__port-order-name">{port.name}</span
857
+ >
858
+ <span
859
+ class="config-form__port-order-badge"
860
+ style="background-color:{getPortBackgroundColor(
861
+ port.dataType,
862
+ 15,
863
+ )};color:{getDataTypeColorToken(
864
+ port.dataType,
865
+ )};border:1px solid {getPortBackgroundColor(
866
+ port.dataType,
867
+ 30,
868
+ )}"
869
+ >
870
+ {port.dataType}
871
+ </span>
872
+ <div class="config-form__port-order-actions">
873
+ <button
874
+ type="button"
875
+ disabled={isRequired}
876
+ title={isRequired
877
+ ? "Required ports cannot be hidden"
878
+ : isHidden
879
+ ? "Show port"
880
+ : "Hide port"}
881
+ class:active={isHidden}
882
+ onclick={() => togglePortHidden("inputs", port.id)}
883
+ >
884
+ <Icon
885
+ icon={isHidden
886
+ ? "heroicons:eye-slash"
887
+ : "heroicons:eye"}
888
+ />
889
+ </button>
890
+ <button
891
+ type="button"
892
+ disabled={i === 0 || allInputPortsForUI.length === 1}
893
+ onclick={() => movePort("inputs", port.id, -1)}
894
+ title="Move up"
895
+ >
896
+ <Icon icon="heroicons:chevron-up" />
897
+ </button>
898
+ <button
899
+ type="button"
900
+ disabled={i === allInputPortsForUI.length - 1 ||
901
+ allInputPortsForUI.length === 1}
902
+ onclick={() => movePort("inputs", port.id, 1)}
903
+ title="Move down"
904
+ >
905
+ <Icon icon="heroicons:chevron-down" />
906
+ </button>
907
+ </div>
908
+ </li>
909
+ {/each}
910
+ </ul>
911
+ </div>
912
+ {/if}
913
+
914
+ <!-- Output Port Order & Visibility -->
915
+ {#if allOutputPortsForUI.length > 0}
916
+ <div class="config-form__port-order">
917
+ <div class="config-form__port-order-header">
918
+ <span class="config-form__port-order-label">Output Ports</span>
919
+ {#if uiExtensionValues.portOrder?.outputs?.length || uiExtensionValues.hiddenPorts?.outputs?.length}
920
+ <button
921
+ type="button"
922
+ class="config-form__port-order-reset"
923
+ onclick={() => resetPortCustomizations("outputs")}
924
+ title="Reset to default order and visibility"
925
+ >
926
+ <Icon icon="heroicons:arrow-uturn-left" />
927
+ Reset
928
+ </button>
929
+ {/if}
930
+ </div>
931
+ <ul class="config-form__port-order-list">
932
+ {#each allOutputPortsForUI as port, i (port.id)}
933
+ {@const isHidden =
934
+ uiExtensionValues.hiddenPorts?.outputs?.includes(port.id) ??
935
+ false}
936
+ {@const isRequired = port.required ?? false}
937
+ <li
938
+ class="config-form__port-order-item"
939
+ class:config-form__port-order-item--hidden={isHidden}
940
+ >
941
+ <span class="config-form__port-order-name">{port.name}</span
942
+ >
943
+ <span
944
+ class="config-form__port-order-badge"
945
+ style="background-color:{getPortBackgroundColor(
946
+ port.dataType,
947
+ 15,
948
+ )};color:{getDataTypeColorToken(
949
+ port.dataType,
950
+ )};border:1px solid {getPortBackgroundColor(
951
+ port.dataType,
952
+ 30,
953
+ )}"
954
+ >
955
+ {port.dataType}
956
+ </span>
957
+ <div class="config-form__port-order-actions">
958
+ <button
959
+ type="button"
960
+ disabled={isRequired}
961
+ title={isRequired
962
+ ? "Required ports cannot be hidden"
963
+ : isHidden
964
+ ? "Show port"
965
+ : "Hide port"}
966
+ class:active={isHidden}
967
+ onclick={() => togglePortHidden("outputs", port.id)}
968
+ >
969
+ <Icon
970
+ icon={isHidden
971
+ ? "heroicons:eye-slash"
972
+ : "heroicons:eye"}
973
+ />
974
+ </button>
975
+ <button
976
+ type="button"
977
+ disabled={i === 0 || allOutputPortsForUI.length === 1}
978
+ onclick={() => movePort("outputs", port.id, -1)}
979
+ title="Move up"
980
+ >
981
+ <Icon icon="heroicons:chevron-up" />
982
+ </button>
983
+ <button
984
+ type="button"
985
+ disabled={i === allOutputPortsForUI.length - 1 ||
986
+ allOutputPortsForUI.length === 1}
987
+ onclick={() => movePort("outputs", port.id, 1)}
988
+ title="Move down"
989
+ >
990
+ <Icon icon="heroicons:chevron-down" />
991
+ </button>
992
+ </div>
993
+ </li>
994
+ {/each}
995
+ </ul>
996
+ </div>
997
+ {/if}
711
998
  </div>
712
999
  </div>
713
1000
  {/if}
@@ -928,6 +1215,140 @@
928
1215
  gap: var(--fd-space-xl);
929
1216
  }
930
1217
 
1218
+ /* ============================================
1219
+ PORT ORDER & VISIBILITY
1220
+ ============================================ */
1221
+
1222
+ .config-form__port-order {
1223
+ border-top: 1px solid var(--fd-border-muted);
1224
+ padding-top: var(--fd-space-md);
1225
+ margin-top: calc(var(--fd-space-xl) * -0.25);
1226
+ }
1227
+
1228
+ .config-form__port-order-header {
1229
+ display: flex;
1230
+ align-items: center;
1231
+ justify-content: space-between;
1232
+ margin-bottom: var(--fd-space-xs);
1233
+ }
1234
+
1235
+ .config-form__port-order-label {
1236
+ font-size: var(--fd-text-xs);
1237
+ font-weight: 600;
1238
+ color: var(--fd-muted-foreground);
1239
+ text-transform: uppercase;
1240
+ letter-spacing: 0.05em;
1241
+ }
1242
+
1243
+ .config-form__port-order-reset {
1244
+ background: none;
1245
+ border: none;
1246
+ font-size: var(--fd-text-xs);
1247
+ color: var(--fd-muted-foreground);
1248
+ cursor: pointer;
1249
+ display: inline-flex;
1250
+ align-items: center;
1251
+ gap: var(--fd-space-3xs);
1252
+ padding: 0;
1253
+ transition: color var(--fd-transition-fast);
1254
+ }
1255
+
1256
+ .config-form__port-order-reset:hover {
1257
+ color: var(--fd-foreground);
1258
+ }
1259
+
1260
+ .config-form__port-order-reset :global(svg) {
1261
+ width: 0.75rem;
1262
+ height: 0.75rem;
1263
+ }
1264
+
1265
+ .config-form__port-order-list {
1266
+ list-style: none;
1267
+ margin: 0;
1268
+ padding: 0;
1269
+ display: flex;
1270
+ flex-direction: column;
1271
+ gap: var(--fd-space-3xs);
1272
+ }
1273
+
1274
+ .config-form__port-order-item {
1275
+ display: flex;
1276
+ align-items: center;
1277
+ gap: var(--fd-space-xs);
1278
+ padding: var(--fd-space-3xs) var(--fd-space-xs);
1279
+ background: var(--fd-muted);
1280
+ border-radius: var(--fd-radius-sm);
1281
+ border: 1px solid var(--fd-border-muted);
1282
+ transition: opacity var(--fd-transition-fast);
1283
+ }
1284
+
1285
+ .config-form__port-order-item--hidden {
1286
+ opacity: 0.4;
1287
+ }
1288
+
1289
+ .config-form__port-order-name {
1290
+ flex: 1;
1291
+ font-size: var(--fd-text-xs);
1292
+ font-weight: 500;
1293
+ color: var(--fd-foreground);
1294
+ min-width: 0;
1295
+ overflow: hidden;
1296
+ text-overflow: ellipsis;
1297
+ white-space: nowrap;
1298
+ }
1299
+
1300
+ .config-form__port-order-badge {
1301
+ padding: 0.125rem var(--fd-space-3xs);
1302
+ border-radius: var(--fd-radius-sm);
1303
+ font-size: 0.625rem;
1304
+ font-weight: 500;
1305
+ text-transform: uppercase;
1306
+ letter-spacing: 0.04em;
1307
+ flex-shrink: 0;
1308
+ }
1309
+
1310
+ .config-form__port-order-actions {
1311
+ display: flex;
1312
+ gap: var(--fd-space-3xs);
1313
+ flex-shrink: 0;
1314
+ }
1315
+
1316
+ .config-form__port-order-actions button {
1317
+ width: 1.25rem;
1318
+ height: 1.25rem;
1319
+ display: flex;
1320
+ align-items: center;
1321
+ justify-content: center;
1322
+ background: var(--fd-card);
1323
+ border: 1px solid var(--fd-border);
1324
+ border-radius: var(--fd-radius-sm);
1325
+ color: var(--fd-muted-foreground);
1326
+ cursor: pointer;
1327
+ padding: 0;
1328
+ transition: all var(--fd-transition-fast);
1329
+ }
1330
+
1331
+ .config-form__port-order-actions button:hover:not(:disabled) {
1332
+ background: var(--fd-backdrop);
1333
+ color: var(--fd-foreground);
1334
+ border-color: var(--fd-border-strong);
1335
+ }
1336
+
1337
+ .config-form__port-order-actions button:disabled {
1338
+ opacity: 0.3;
1339
+ cursor: not-allowed;
1340
+ }
1341
+
1342
+ .config-form__port-order-actions button.active {
1343
+ color: var(--fd-foreground);
1344
+ border-color: var(--fd-border-strong);
1345
+ }
1346
+
1347
+ .config-form__port-order-actions button :global(svg) {
1348
+ width: 0.75rem;
1349
+ height: 0.75rem;
1350
+ }
1351
+
931
1352
  /* ============================================
932
1353
  DEBUG SECTION
933
1354
  ============================================ */