@rogieking/figui3 4.2.0 → 4.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.
Files changed (3) hide show
  1. package/components.css +35 -2
  2. package/fig.js +368 -14
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -1045,11 +1045,18 @@ fig-tab,
1045
1045
  &:hover {
1046
1046
  background-color: var(--figma-color-bg-secondary);
1047
1047
  }
1048
+
1049
+ &[disabled]:not([disabled="false"]) {
1050
+ opacity: 0.4;
1051
+ pointer-events: none;
1052
+ cursor: default;
1053
+ }
1048
1054
  }
1049
1055
 
1050
1056
  fig-tabs,
1051
1057
  .tabs {
1052
1058
  display: flex;
1059
+ width: 100%;
1053
1060
  gap: var(--spacer-1);
1054
1061
  padding: var(--spacer-1) var(--spacer-2);
1055
1062
  }
@@ -1057,6 +1064,7 @@ fig-tabs,
1057
1064
  fig-tab-content,
1058
1065
  .fig-tab-content {
1059
1066
  display: none;
1067
+ width: 100%;
1060
1068
  }
1061
1069
 
1062
1070
  /* Avatar */
@@ -3415,7 +3423,7 @@ fig-input-palette {
3415
3423
  }
3416
3424
  &[open]:not([open="false"]):not([edit="false"]) {
3417
3425
  .palette-colors-expanded {
3418
- display: flex;
3426
+ display: grid;
3419
3427
  }
3420
3428
  }
3421
3429
  &[add="false"] {
@@ -3426,7 +3434,7 @@ fig-input-palette {
3426
3434
  }
3427
3435
  .palette-colors-expanded {
3428
3436
  display: none;
3429
- flex-direction: column;
3437
+ grid-template-columns: [input] 1fr [button] 1.5rem;
3430
3438
  overflow: visible;
3431
3439
  border-radius: 0;
3432
3440
  gap: var(--spacer-2);
@@ -3434,6 +3442,16 @@ fig-input-palette {
3434
3442
 
3435
3443
  > fig-input-color {
3436
3444
  min-width: 0;
3445
+ grid-column: input / -1;
3446
+
3447
+ &:has(+ fig-button) {
3448
+ grid-column: input;
3449
+ }
3450
+ }
3451
+
3452
+ > fig-button,
3453
+ > button {
3454
+ grid-column: button;
3437
3455
  }
3438
3456
 
3439
3457
  fig-chit {
@@ -3678,6 +3696,21 @@ fig-segmented-control {
3678
3696
  }
3679
3697
  }
3680
3698
 
3699
+ /* Options */
3700
+ fig-options {
3701
+ display: flex;
3702
+ width: 100%;
3703
+
3704
+ & > fig-segmented-control {
3705
+ flex: 1;
3706
+ min-width: 0;
3707
+ }
3708
+
3709
+ & > fig-dropdown {
3710
+ flex: 1;
3711
+ }
3712
+ }
3713
+
3681
3714
  fig-joystick {
3682
3715
  --size: 100%;
3683
3716
  --aspect-ratio: 1 / 1;
package/fig.js CHANGED
@@ -517,6 +517,8 @@ class FigTooltip extends HTMLElement {
517
517
  #boundHandleTouchMove;
518
518
  #boundHandleTouchEnd;
519
519
  #boundHandleTouchCancel;
520
+ #boundHandleDialogClose;
521
+ #parentDialog = null;
520
522
  #touchTimeout;
521
523
  #isTouching = false;
522
524
  #observer = null;
@@ -535,13 +537,23 @@ class FigTooltip extends HTMLElement {
535
537
  this.#boundHandleTouchMove = this.#handleTouchMove.bind(this);
536
538
  this.#boundHandleTouchEnd = this.#handleTouchEnd.bind(this);
537
539
  this.#boundHandleTouchCancel = this.#handleTouchCancel.bind(this);
540
+ this.#boundHandleDialogClose = () => {
541
+ clearTimeout(this.timeout);
542
+ this.destroy();
543
+ this.isOpen = false;
544
+ };
538
545
  }
539
546
  connectedCallback() {
540
547
  this.setup();
541
548
  this.setupEventListeners();
549
+ this.#parentDialog = this.closest("dialog");
550
+ if (this.#parentDialog) {
551
+ this.#parentDialog.addEventListener("close", this.#boundHandleDialogClose);
552
+ }
542
553
  }
543
554
 
544
555
  disconnectedCallback() {
556
+ clearTimeout(this.timeout);
545
557
  this.destroy();
546
558
  document.removeEventListener(
547
559
  "mousedown",
@@ -549,6 +561,10 @@ class FigTooltip extends HTMLElement {
549
561
  true,
550
562
  );
551
563
  this.#stopObserving();
564
+ if (this.#parentDialog) {
565
+ this.#parentDialog.removeEventListener("close", this.#boundHandleDialogClose);
566
+ this.#parentDialog = null;
567
+ }
552
568
 
553
569
  if (this.action === "click") {
554
570
  document.body.removeEventListener(
@@ -697,6 +713,7 @@ class FigTooltip extends HTMLElement {
697
713
  }
698
714
 
699
715
  showPopup() {
716
+ if (this.#parentDialog && !this.#parentDialog.open) return;
700
717
  if (!this.popup) this.render();
701
718
  this.popup.style.display = "block";
702
719
  this.popup.style.visibility = "hidden";
@@ -1112,6 +1129,7 @@ customElements.define("fig-truncate", FigTruncate);
1112
1129
  * @attr {string} position - Position of the dialog (e.g., "bottom right", "top left", "center center")
1113
1130
  * @attr {string} title - Title text for the auto-generated header. If no fig-header[dialog-header] exists, one is prepended with this title and a close button.
1114
1131
  * @attr {boolean} resizable - Whether the dialog can be manually resized by the user (default: false)
1132
+ * @attr {string} closedby - Controls how the dialog can be dismissed: "any" (default, Escape + light dismiss), "closerequest" (Escape only), "none" (programmatic only)
1115
1133
  */
1116
1134
  class FigDialog extends HTMLDialogElement {
1117
1135
  #isDragging = false;
@@ -1397,7 +1415,7 @@ class FigDialog extends HTMLDialogElement {
1397
1415
  }
1398
1416
 
1399
1417
  static get observedAttributes() {
1400
- return ["modal", "drag", "position", "handle", "title", "resizable"];
1418
+ return ["modal", "drag", "position", "handle", "title", "resizable", "closedby"];
1401
1419
  }
1402
1420
 
1403
1421
  attributeChangedCallback(name, oldValue, newValue) {
@@ -1419,6 +1437,20 @@ class FigDialog extends HTMLDialogElement {
1419
1437
  this.#applyPosition();
1420
1438
  }
1421
1439
 
1440
+ if (name === "modal") {
1441
+ const wasModal = this.modal;
1442
+ this.modal = newValue !== null && newValue !== "false";
1443
+ if (this.open && wasModal !== this.modal) {
1444
+ this.close();
1445
+ if (this.modal) this.showModal();
1446
+ else this.show();
1447
+ }
1448
+ }
1449
+
1450
+ if (name === "closedby") {
1451
+ this.closedby = newValue || "any";
1452
+ }
1453
+
1422
1454
  if (name === "title") {
1423
1455
  const autoHeader = this.querySelector("fig-header[data-auto] h3");
1424
1456
  if (autoHeader) {
@@ -2505,6 +2537,7 @@ class FigTab extends HTMLElement {
2505
2537
  this.removeEventListener("click", this.#boundHandleClick);
2506
2538
  }
2507
2539
  handleClick() {
2540
+ if (this.hasAttribute("disabled")) return;
2508
2541
  this.selected = true;
2509
2542
  if (this.content) {
2510
2543
  this.content.style.display = "block";
@@ -2610,7 +2643,9 @@ class FigTabs extends HTMLElement {
2610
2643
  if (newIndex !== currentIndex && tabs[newIndex]) {
2611
2644
  tabs.forEach((tab) => tab.removeAttribute("selected"));
2612
2645
  this.selectedTab = tabs[newIndex];
2613
- this.setAttribute("value", tabs[newIndex].getAttribute("value") || "");
2646
+ tabs[newIndex].setAttribute("selected", "true");
2647
+ const val = tabs[newIndex].getAttribute("value");
2648
+ if (val) this.setAttribute("value", val);
2614
2649
  tabs[newIndex].focus();
2615
2650
  }
2616
2651
  }
@@ -2650,19 +2685,19 @@ class FigTabs extends HTMLElement {
2650
2685
 
2651
2686
  handleClick(event) {
2652
2687
  if (this.hasAttribute("disabled")) return;
2653
- const target = event.target;
2654
- if (target.nodeName.toLowerCase() === "fig-tab") {
2655
- const tabs = this.querySelectorAll("fig-tab");
2656
- for (const tab of tabs) {
2657
- if (tab === target) {
2658
- this.selectedTab = tab;
2659
- tab.setAttribute("selected", "true");
2660
- this.setAttribute("value", tab.getAttribute("value") || "");
2661
- } else {
2662
- tab.removeAttribute("selected");
2663
- }
2688
+ const target = event.target.closest("fig-tab");
2689
+ if (!target || !this.contains(target)) return;
2690
+ const tabs = this.querySelectorAll("fig-tab");
2691
+ for (const tab of tabs) {
2692
+ if (tab === target) {
2693
+ this.selectedTab = tab;
2694
+ tab.setAttribute("selected", "true");
2695
+ } else {
2696
+ tab.removeAttribute("selected");
2664
2697
  }
2665
2698
  }
2699
+ const val = target.getAttribute("value");
2700
+ if (val) this.setAttribute("value", val);
2666
2701
  }
2667
2702
  }
2668
2703
  customElements.define("fig-tabs", FigTabs);
@@ -3105,6 +3140,277 @@ class FigSegmentedControl extends HTMLElement {
3105
3140
  }
3106
3141
  customElements.define("fig-segmented-control", FigSegmentedControl);
3107
3142
 
3143
+ /* Options */
3144
+ /**
3145
+ * A responsive option picker that renders as a segmented control by default,
3146
+ * automatically swapping to a dropdown when any label overflows.
3147
+ * @attr {string} options - Comma-separated list of option labels
3148
+ * @attr {string} value - Currently selected value
3149
+ * @attr {boolean} disabled - Disables the control
3150
+ * @attr {boolean} full - Full-width segmented control
3151
+ * @attr {string} sizing - Segment sizing mode: "equal" (default) or "auto"
3152
+ */
3153
+ class FigOptions extends HTMLElement {
3154
+ static get observedAttributes() {
3155
+ return ["options", "value", "disabled", "full", "sizing"];
3156
+ }
3157
+
3158
+ #currentMode = "segments"; // "segments" | "dropdown"
3159
+ #naturalWidth = 0;
3160
+ #resizeObserver = null;
3161
+ #parsedOptions = [];
3162
+ #childControl = null;
3163
+ #suppressEvents = false;
3164
+
3165
+ connectedCallback() {
3166
+ this.#parseOptions();
3167
+ this.#renderSegments();
3168
+ this.#startResizeObserver();
3169
+ requestAnimationFrame(() => {
3170
+ requestAnimationFrame(() => this.#checkOverflow());
3171
+ });
3172
+ }
3173
+
3174
+ disconnectedCallback() {
3175
+ this.#resizeObserver?.disconnect();
3176
+ this.#resizeObserver = null;
3177
+ }
3178
+
3179
+ get value() {
3180
+ return this.getAttribute("value") || "";
3181
+ }
3182
+
3183
+ set value(val) {
3184
+ if (val === null || val === undefined) {
3185
+ this.removeAttribute("value");
3186
+ } else {
3187
+ this.setAttribute("value", String(val));
3188
+ }
3189
+ }
3190
+
3191
+ get options() {
3192
+ return this.#parsedOptions.slice();
3193
+ }
3194
+
3195
+ set options(val) {
3196
+ if (Array.isArray(val)) {
3197
+ const hasComma = val.some((v) => String(v).includes(","));
3198
+ const str = hasComma ? JSON.stringify(val) : val.join(",");
3199
+ this.setAttribute("options", str);
3200
+ } else {
3201
+ this.setAttribute("options", String(val || ""));
3202
+ }
3203
+ }
3204
+
3205
+ attributeChangedCallback(name, oldValue, newValue) {
3206
+ if (oldValue === newValue) return;
3207
+
3208
+ if (name === "options") {
3209
+ this.#parseOptions();
3210
+ this.#rebuildCurrentControl();
3211
+ return;
3212
+ }
3213
+
3214
+ if (name === "value") {
3215
+ this.#syncValueToChild();
3216
+ return;
3217
+ }
3218
+
3219
+ if (name === "disabled") {
3220
+ this.#syncAttrToChild("disabled");
3221
+ return;
3222
+ }
3223
+
3224
+ if (name === "full") {
3225
+ this.#syncAttrToChild("full");
3226
+ return;
3227
+ }
3228
+
3229
+ if (name === "sizing") {
3230
+ this.#syncAttrToChild("sizing");
3231
+ this.#rebuildCurrentControl();
3232
+ }
3233
+ }
3234
+
3235
+ #parseOptions() {
3236
+ const raw = this.getAttribute("options") || "";
3237
+ if (raw.startsWith("[")) {
3238
+ try { this.#parsedOptions = JSON.parse(raw); return; } catch {}
3239
+ }
3240
+ const delimiter = raw.includes("\n") ? "\n" : ",";
3241
+ this.#parsedOptions = raw.split(delimiter).map((s) => s.trim()).filter(Boolean);
3242
+ }
3243
+
3244
+ #renderSegments() {
3245
+ this.innerHTML = "";
3246
+ if (this.#parsedOptions.length === 0) return;
3247
+
3248
+ const sc = document.createElement("fig-segmented-control");
3249
+ sc.setAttribute("sizing", this.getAttribute("sizing") || "equal");
3250
+
3251
+ if (this.hasAttribute("disabled")) sc.setAttribute("disabled", "");
3252
+ if (this.hasAttribute("full")) sc.setAttribute("full", "");
3253
+
3254
+ const currentValue = this.getAttribute("value");
3255
+ let hasSelection = false;
3256
+
3257
+ for (const opt of this.#parsedOptions) {
3258
+ const seg = document.createElement("fig-segment");
3259
+ seg.setAttribute("value", opt);
3260
+ seg.textContent = opt;
3261
+ if (currentValue === opt) {
3262
+ seg.setAttribute("selected", "true");
3263
+ hasSelection = true;
3264
+ }
3265
+ sc.appendChild(seg);
3266
+ }
3267
+
3268
+ if (currentValue) sc.setAttribute("value", currentValue);
3269
+
3270
+ sc.addEventListener("input", (e) => {
3271
+ if (this.#suppressEvents) return;
3272
+ this.#suppressEvents = true;
3273
+ this.setAttribute("value", e.detail);
3274
+ this.#suppressEvents = false;
3275
+ this.dispatchEvent(
3276
+ new CustomEvent("input", { detail: e.detail, bubbles: true }),
3277
+ );
3278
+ });
3279
+ sc.addEventListener("change", (e) => {
3280
+ if (this.#suppressEvents) return;
3281
+ this.dispatchEvent(
3282
+ new CustomEvent("change", { detail: e.detail, bubbles: true }),
3283
+ );
3284
+ });
3285
+
3286
+ this.appendChild(sc);
3287
+ this.#childControl = sc;
3288
+ this.#currentMode = "segments";
3289
+ }
3290
+
3291
+ #renderDropdown() {
3292
+ this.innerHTML = "";
3293
+ if (this.#parsedOptions.length === 0) return;
3294
+
3295
+ const dd = document.createElement("fig-dropdown");
3296
+ if (this.hasAttribute("disabled")) dd.setAttribute("disabled", "");
3297
+
3298
+ const currentValue = this.getAttribute("value");
3299
+
3300
+ for (const opt of this.#parsedOptions) {
3301
+ const option = document.createElement("option");
3302
+ option.value = opt;
3303
+ option.textContent = opt;
3304
+ if (currentValue === opt) option.selected = true;
3305
+ dd.appendChild(option);
3306
+ }
3307
+
3308
+ if (currentValue) dd.setAttribute("value", currentValue);
3309
+
3310
+ dd.addEventListener("input", (e) => {
3311
+ if (this.#suppressEvents) return;
3312
+ this.#suppressEvents = true;
3313
+ this.setAttribute("value", e.detail);
3314
+ this.#suppressEvents = false;
3315
+ this.dispatchEvent(
3316
+ new CustomEvent("input", { detail: e.detail, bubbles: true }),
3317
+ );
3318
+ });
3319
+ dd.addEventListener("change", (e) => {
3320
+ if (this.#suppressEvents) return;
3321
+ this.dispatchEvent(
3322
+ new CustomEvent("change", { detail: e.detail, bubbles: true }),
3323
+ );
3324
+ });
3325
+
3326
+ this.appendChild(dd);
3327
+ this.#childControl = dd;
3328
+ this.#currentMode = "dropdown";
3329
+ }
3330
+
3331
+ #rebuildCurrentControl() {
3332
+ if (this.#currentMode === "segments") {
3333
+ this.#renderSegments();
3334
+ requestAnimationFrame(() => {
3335
+ requestAnimationFrame(() => this.#checkOverflow());
3336
+ });
3337
+ } else {
3338
+ this.#renderDropdown();
3339
+ }
3340
+ }
3341
+
3342
+ #syncValueToChild() {
3343
+ if (!this.#childControl || this.#suppressEvents) return;
3344
+ const val = this.getAttribute("value") || "";
3345
+ this.#childControl.value = val;
3346
+ }
3347
+
3348
+ #syncAttrToChild(attr) {
3349
+ if (!this.#childControl) return;
3350
+ if (this.hasAttribute(attr)) {
3351
+ this.#childControl.setAttribute(attr, this.getAttribute(attr) || "");
3352
+ } else {
3353
+ this.#childControl.removeAttribute(attr);
3354
+ }
3355
+ }
3356
+
3357
+ #startResizeObserver() {
3358
+ this.#resizeObserver?.disconnect();
3359
+ this.#resizeObserver = new ResizeObserver(() => {
3360
+ this.#checkOverflow();
3361
+ });
3362
+ this.#resizeObserver.observe(this);
3363
+ }
3364
+
3365
+ #isSegmentTruncated(seg) {
3366
+ const range = document.createRange();
3367
+ range.selectNodeContents(seg);
3368
+ const textWidth = range.getBoundingClientRect().width;
3369
+ const segRect = seg.getBoundingClientRect();
3370
+ const segWidth = segRect.width;
3371
+ const cs = getComputedStyle(seg);
3372
+ const padL = parseFloat(cs.paddingLeft) || 0;
3373
+ const padR = parseFloat(cs.paddingRight) || 0;
3374
+ const contentWidth = segWidth - padL - padR;
3375
+ return textWidth > contentWidth + 0.5;
3376
+ }
3377
+
3378
+ #anySegmentTruncated() {
3379
+ const segments = this.querySelectorAll("fig-segment");
3380
+ for (const seg of segments) {
3381
+ if (this.#isSegmentTruncated(seg)) return true;
3382
+ }
3383
+ return false;
3384
+ }
3385
+
3386
+ #checkOverflow() {
3387
+ if (this.#parsedOptions.length <= 1) return;
3388
+
3389
+ if (this.#currentMode === "segments") {
3390
+ const sc = this.#childControl;
3391
+ const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
3392
+ if (containerOverflow || this.#anySegmentTruncated()) {
3393
+ this.#naturalWidth = this.clientWidth;
3394
+ this.#renderDropdown();
3395
+ }
3396
+ } else {
3397
+ if (this.#naturalWidth > 0 && this.clientWidth >= this.#naturalWidth) {
3398
+ this.#renderSegments();
3399
+ requestAnimationFrame(() => {
3400
+ requestAnimationFrame(() => {
3401
+ const sc = this.#childControl;
3402
+ const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
3403
+ if (containerOverflow || this.#anySegmentTruncated()) {
3404
+ this.#renderDropdown();
3405
+ }
3406
+ });
3407
+ });
3408
+ }
3409
+ }
3410
+ }
3411
+ }
3412
+ customElements.define("fig-options", FigOptions);
3413
+
3108
3414
  /* Slider */
3109
3415
  /**
3110
3416
  * A custom slider input element.
@@ -6139,6 +6445,7 @@ class FigInputPalette extends HTMLElement {
6139
6445
  expandedWrap.className = "palette-colors-expanded";
6140
6446
  this.#colors.forEach((entry, i) => {
6141
6447
  expandedWrap.appendChild(this.#createPicker(entry, i, disabled));
6448
+ expandedWrap.appendChild(this.#createRemoveButton(i, disabled));
6142
6449
  });
6143
6450
  this.appendChild(expandedWrap);
6144
6451
  }
@@ -6203,6 +6510,31 @@ class FigInputPalette extends HTMLElement {
6203
6510
  return ic;
6204
6511
  }
6205
6512
 
6513
+ #createRemoveButton(index, disabled) {
6514
+ const btn = document.createElement("fig-button");
6515
+ btn.setAttribute("variant", "ghost");
6516
+ btn.setAttribute("icon", "true");
6517
+ btn.setAttribute("aria-label", "Remove color");
6518
+ btn.className = "palette-remove-btn";
6519
+ if (disabled || this.#colors.length <= this.#min) btn.setAttribute("disabled", "");
6520
+ btn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>`;
6521
+ btn.addEventListener("click", () => {
6522
+ if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
6523
+ this.#removeColor(index);
6524
+ });
6525
+ return btn;
6526
+ }
6527
+
6528
+ #removeColor(index) {
6529
+ if (index < 0 || index >= this.#colors.length) return;
6530
+ if (this.#colors.length <= this.#min) return;
6531
+ this.#colors.splice(index, 1);
6532
+ this.#inlinePickers = [];
6533
+ this.#expandedPickers = [];
6534
+ this.#render();
6535
+ this.#emitChange();
6536
+ }
6537
+
6206
6538
  #createAddButton(disabled, parent = this) {
6207
6539
  const atMax = this.#colors.length >= this.#max;
6208
6540
  const addBtn = document.createElement("fig-button");
@@ -6242,7 +6574,10 @@ class FigInputPalette extends HTMLElement {
6242
6574
 
6243
6575
  const expandedIc = this.#createPicker(entry, index, disabled);
6244
6576
  const expandedWrap = this.querySelector(".palette-colors-expanded");
6245
- if (expandedWrap) expandedWrap.appendChild(expandedIc);
6577
+ if (expandedWrap) {
6578
+ expandedWrap.appendChild(expandedIc);
6579
+ expandedWrap.appendChild(this.#createRemoveButton(index, disabled));
6580
+ }
6246
6581
 
6247
6582
  if (this.#colors.length >= this.#max) {
6248
6583
  const addBtn = this.querySelector(".palette-add-btn");
@@ -10704,6 +11039,25 @@ customElements.define("fig-header", FigHeader);
10704
11039
  class FigFooter extends HTMLElement {}
10705
11040
  customElements.define("fig-footer", FigFooter);
10706
11041
 
11042
+ /* Presentational elements (CSS-only, no behavior) */
11043
+ class FigSpinner extends HTMLElement {}
11044
+ customElements.define("fig-spinner", FigSpinner);
11045
+
11046
+ class FigIcon extends HTMLElement {}
11047
+ customElements.define("fig-icon", FigIcon);
11048
+
11049
+ class FigContent extends HTMLElement {}
11050
+ customElements.define("fig-content", FigContent);
11051
+
11052
+ class FigTabContent extends HTMLElement {}
11053
+ customElements.define("fig-tab-content", FigTabContent);
11054
+
11055
+ class FigButtonCombo extends HTMLElement {}
11056
+ customElements.define("fig-button-combo", FigButtonCombo);
11057
+
11058
+ class FigInputCombo extends HTMLElement {}
11059
+ customElements.define("fig-input-combo", FigInputCombo);
11060
+
10707
11061
  // FigFillPicker
10708
11062
  /**
10709
11063
  * A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "MIT",