@filipgutica/ui 1.14.0 → 1.15.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.
@@ -154,20 +154,72 @@ var N = /* @__PURE__ */ c({
154
154
  _: 3
155
155
  }, 8, ["open"]));
156
156
  }
157
- }), H = { class: "fg-tabs" }, U = ["aria-label", "data-clipped"], W = [
157
+ }), H = [
158
+ "aria-label",
159
+ "data-size",
160
+ "data-icon-only"
161
+ ], U = [
162
+ "aria-pressed",
163
+ "aria-label",
164
+ "title",
165
+ "disabled",
166
+ "onClick"
167
+ ], W = {
168
+ key: 0,
169
+ class: "fg-segmented__label"
170
+ }, G = /* @__PURE__ */ c({
171
+ __name: "UiSegmentedControl",
172
+ props: {
173
+ label: {},
174
+ items: {},
175
+ modelValue: { default: () => void 0 },
176
+ iconOnly: {
177
+ type: Boolean,
178
+ default: !1
179
+ },
180
+ disabled: {
181
+ type: Boolean,
182
+ default: !1
183
+ },
184
+ size: { default: "md" }
185
+ },
186
+ emits: ["update:modelValue"],
187
+ setup(t, { emit: n }) {
188
+ let a = n;
189
+ return (n, o) => (f(), i("div", {
190
+ class: "fg-segmented",
191
+ role: "group",
192
+ "aria-label": t.label,
193
+ "data-size": t.size,
194
+ "data-icon-only": t.iconOnly ? "true" : void 0
195
+ }, [(f(!0), i(e, null, h(t.items, (e) => (f(), i("button", {
196
+ key: e.value,
197
+ type: "button",
198
+ class: "fg-segmented__item",
199
+ "aria-pressed": t.modelValue === e.value,
200
+ "aria-label": t.iconOnly ? e.label : void 0,
201
+ title: t.iconOnly ? e.label : void 0,
202
+ disabled: t.disabled || e.disabled,
203
+ onClick: (t) => a("update:modelValue", e.value)
204
+ }, [g(n.$slots, "icon", {
205
+ item: e,
206
+ pressed: t.modelValue === e.value
207
+ }), t.iconOnly ? r("", !0) : (f(), i("span", W, _(e.label), 1))], 8, U))), 128))], 8, H));
208
+ }
209
+ }), K = { class: "fg-tabs" }, q = ["aria-label", "data-clipped"], J = [
158
210
  "id",
159
211
  "aria-controls",
160
212
  "aria-selected",
161
213
  "tabindex",
162
214
  "onClick",
163
215
  "onFocus"
164
- ], G = [
216
+ ], Y = [
165
217
  "id",
166
218
  "role",
167
219
  "aria-labelledby",
168
220
  "hidden",
169
221
  "tabindex"
170
- ], K = ["id", "hidden"], q = /* @__PURE__ */ c({
222
+ ], X = ["id", "hidden"], Z = /* @__PURE__ */ c({
171
223
  __name: "UiTabs",
172
224
  props: {
173
225
  items: {},
@@ -224,7 +276,7 @@ var N = /* @__PURE__ */ c({
224
276
  inline: "nearest"
225
277
  }));
226
278
  };
227
- return (t, o) => (f(), i("div", H, [l.value ? (f(), i("div", {
279
+ return (t, o) => (f(), i("div", K, [l.value ? (f(), i("div", {
228
280
  key: 0,
229
281
  ref_key: "list",
230
282
  ref: C,
@@ -245,7 +297,7 @@ var N = /* @__PURE__ */ c({
245
297
  tabindex: e.value === d.value ? 0 : -1,
246
298
  onClick: (t) => O(e.value),
247
299
  onFocus: (t) => O(e.value)
248
- }, _(e.label), 41, W))), 128))], 40, U)) : r("", !0), (f(!0), i(e, null, h(n.items, (e) => (f(), i("section", {
300
+ }, _(e.label), 41, J))), 128))], 40, q)) : r("", !0), (f(!0), i(e, null, h(n.items, (e) => (f(), i("section", {
249
301
  id: v(e.value),
250
302
  key: e.value,
251
303
  class: "fg-tabs__panel",
@@ -257,9 +309,9 @@ var N = /* @__PURE__ */ c({
257
309
  id: S(e.value),
258
310
  class: "fg-tabs__heading",
259
311
  hidden: l.value
260
- }, _(e.label), 9, K), g(t.$slots, "panel", { value: e.value })], 8, G))), 128))]));
312
+ }, _(e.label), 9, X), g(t.$slots, "panel", { value: e.value })], 8, Y))), 128))]));
261
313
  }
262
- }), J = ({ targetIds: e }) => {
314
+ }), Q = ({ targetIds: e }) => {
263
315
  let t = m(e[0]), n, r, i;
264
316
  return u(() => {
265
317
  let a = e.flatMap((e) => {
@@ -302,4 +354,4 @@ var N = /* @__PURE__ */ c({
302
354
  }), p(t);
303
355
  };
304
356
  //#endregion
305
- export { P as a, R as i, q as n, N as o, V as r, J as t };
357
+ export { R as a, V as i, Z as n, P as o, G as r, N as s, Q as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filipgutica/ui",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "A compact Vue design system with Tailwind CSS and VS Code theme support",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -5,7 +5,7 @@
5
5
  /* Package-owned typography keeps controls consistent across consuming apps. */
6
6
  .fg-button, .fg-input, .fg-select, .fg-checkbox, .fg-field,
7
7
  .fg-radio-card, .fg-badge, .fg-alert, .fg-surface, .fg-card,
8
- .fg-dialog__content, .fg-code-block, .fg-toggle-button, .fg-switch {
8
+ .fg-dialog__content, .fg-code-block, .fg-toggle-button, .fg-switch, .fg-segmented {
9
9
  font-family: var(--font-ui);
10
10
  font-size: var(--text-md);
11
11
  font-weight: var(--weight-regular);
@@ -172,7 +172,8 @@
172
172
  .fg-select:disabled,
173
173
  .fg-checkbox[data-disabled="true"],
174
174
  .fg-switch[data-disabled="true"],
175
- .fg-toggle-button:disabled {
175
+ .fg-toggle-button:disabled,
176
+ .fg-segmented__item:disabled {
176
177
  cursor: not-allowed;
177
178
  opacity: 0.58;
178
179
  }
@@ -721,6 +722,49 @@
721
722
  .fg-toggle-button:hover:not(:disabled, [aria-pressed="true"]) { border-color: var(--color-border); }
722
723
  .fg-toggle-button[aria-pressed="true"] { border-color: var(--color-accent); }
723
724
 
725
+ /* Segmented control: one pressed choice on a muted track, so it reads as a raised thumb in both themes. */
726
+ .fg-segmented {
727
+ --fg-segmented-item: var(--control-md);
728
+ /* The thumb is lighter than the track in both themes; the dark raised surface is darker than the track. */
729
+ --fg-segmented-thumb: light-dark(var(--color-row-selected), color-mix(in srgb, var(--color-text) 10%, var(--color-surface-muted)));
730
+ display: inline-flex;
731
+ flex: none;
732
+ border: 1px solid var(--color-border-subtle);
733
+ border-radius: var(--radius-control);
734
+ background: var(--color-surface-muted);
735
+ }
736
+ .fg-segmented[data-size="sm"] { --fg-segmented-item: var(--control-sm); font-size: var(--text-sm); }
737
+ .fg-segmented[data-size="lg"] { --fg-segmented-item: var(--control-lg); font-size: var(--text-lg); }
738
+ .fg-segmented__item {
739
+ position: relative;
740
+ isolation: isolate;
741
+ display: inline-flex;
742
+ gap: var(--space-2);
743
+ min-height: var(--fg-segmented-item);
744
+ align-items: center;
745
+ justify-content: center;
746
+ padding: 0 var(--space-3);
747
+ border: 0;
748
+ border-radius: var(--radius-control);
749
+ color: var(--color-muted);
750
+ background: transparent;
751
+ font: inherit;
752
+ cursor: pointer;
753
+ }
754
+ .fg-segmented[data-icon-only="true"] .fg-segmented__item { width: var(--fg-segmented-item); padding: 0; }
755
+ /* The thumb is inset inside each full-size button, so it floats on the track and the hit target keeps its size. */
756
+ .fg-segmented__item::before { content: ""; position: absolute; inset: 2px; z-index: -1; border-radius: var(--radius-inline); }
757
+ .fg-segmented__item:hover:not(:disabled, [aria-pressed="true"]) { color: var(--color-text); }
758
+ .fg-segmented__item:hover:not(:disabled, [aria-pressed="true"])::before { background: color-mix(in srgb, var(--fg-segmented-thumb) 45%, transparent); }
759
+ /* The ring gives the pressed choice 3:1 against the track in both themes (WCAG 1.4.11). */
760
+ .fg-segmented__item[aria-pressed="true"] { color: var(--color-text); }
761
+ /* Drawn inside the button, the ring hugs the thumb instead of reaching the next choice. */
762
+ .fg-segmented .fg-segmented__item:focus-visible { outline-offset: -2px; }
763
+ .fg-segmented__item[aria-pressed="true"]::before {
764
+ background: var(--fg-segmented-thumb);
765
+ box-shadow: inset 0 0 0 1px var(--color-border);
766
+ }
767
+
724
768
  .fg-switch { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
725
769
  .fg-switch__control {
726
770
  display: inline-flex;
@@ -943,6 +987,7 @@
943
987
  .fg-dialog__close:focus-visible,
944
988
  .fg-dialog__content:focus-visible,
945
989
  .fg-toggle-button:focus-visible,
990
+ .fg-segmented__item:focus-visible,
946
991
  .fg-switch__control:focus-visible .fg-switch__track {
947
992
  outline: 2px solid var(--color-focus);
948
993
  outline-offset: 2px;
@@ -979,6 +1024,9 @@
979
1024
  }
980
1025
 
981
1026
  @media (forced-colors: active) {
1027
+ /* Forced colors drop the fill and the ring, so the pressed choice takes the system highlight. */
1028
+ .fg-segmented__item[aria-pressed="true"] { forced-color-adjust: none; color: HighlightText; }
1029
+ .fg-segmented__item[aria-pressed="true"]::before { forced-color-adjust: none; background: Highlight; }
982
1030
  .fg-switch__track { border-color: ButtonText; background: Canvas; }
983
1031
  .fg-switch__track::after { background: ButtonText; }
984
1032
  .fg-switch__control[aria-checked="true"] .fg-switch__track { border-color: Highlight; background: Highlight; forced-color-adjust: none; }
package/src/site.css CHANGED
@@ -224,48 +224,15 @@
224
224
  .fg-site-nav a[aria-current="location"] { box-shadow: inset 2px 0 0 var(--color-project); color: var(--color-text); font-weight: var(--weight-medium); }
225
225
  .fg-site-nav a code { font-family: var(--font-mono); font-size: var(--text-sm); }
226
226
 
227
- .fg-site-appearance {
228
- display: inline-flex;
229
- flex: none;
230
- border: 1px solid var(--color-border-subtle);
231
- border-radius: var(--radius-control);
232
- /* A muted track, so the pressed choice reads as a raised thumb in both themes. */
233
- background: var(--color-surface-muted);
234
- }
235
- .fg-site-appearance__choice {
236
- display: inline-flex;
237
- width: var(--control-lg);
238
- min-height: var(--control-lg);
239
- align-items: center;
240
- justify-content: center;
241
- padding: 0;
242
- border: 0;
243
- color: var(--color-muted);
244
- background: transparent;
245
- font: inherit;
246
- cursor: pointer;
247
- }
248
- .fg-site-appearance__choice + .fg-site-appearance__choice { border-left: 1px solid var(--color-border-subtle); }
249
- .fg-site-appearance__choice:first-child { border-radius: var(--radius-control) 0 0 var(--radius-control); }
250
- .fg-site-appearance__choice:last-child { border-radius: 0 var(--radius-control) var(--radius-control) 0; }
251
- .fg-site-appearance__choice:hover { color: var(--color-text); background: var(--color-row-hover); }
252
- /* The ring gives the pressed choice 3:1 against the track in both themes (WCAG 1.4.11). */
253
- .fg-site-appearance__choice[aria-pressed="true"] {
254
- color: var(--color-text);
255
- background: var(--color-row-selected);
256
- box-shadow: inset 0 0 0 1px var(--color-border);
257
- }
258
- /* Forced colors drop the fill and the ring, so the pressed choice takes the system highlight. */
227
+ /* Forced colors drop the inset marker, so the current link takes a system border. */
259
228
  @media (forced-colors: active) {
260
229
  .fg-site-nav a[aria-current] { border-inline-start: 2px solid Highlight; }
261
- .fg-site-appearance__choice[aria-pressed="true"] { forced-color-adjust: none; color: HighlightText; background: Highlight; }
262
230
  }
263
231
 
264
232
  /* Focus is a 2px outline outside the control, which forced-colors mode keeps. An
265
233
  * inset ring is used only where a scrolling ancestor would clip an outer one. */
266
234
  .fg-site-header a:focus-visible,
267
- .fg-site-projects__trigger:focus-visible,
268
- .fg-site-appearance__choice:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
235
+ .fg-site-projects__trigger:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
269
236
  /* Inset: the desktop rail and the phone drawer scroll. */
270
237
  .fg-site-nav a:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
271
238
 
@@ -277,8 +244,6 @@
277
244
  top: var(--space-6);
278
245
  align-self: start;
279
246
  min-width: 0;
280
- max-height: calc(100svh - var(--space-12));
281
- overflow-y: auto;
282
247
  padding-block: var(--space-4);
283
248
  color: var(--color-text);
284
249
  font-family: var(--font-ui);
@@ -626,6 +591,8 @@
626
591
  /* No-JavaScript desktop: the rail's disclosure stays expanded. */
627
592
  @media (min-width: 801px) {
628
593
  .fg-site-rail { border-inline-end: 1px solid var(--color-border-subtle); padding-inline-end: var(--space-4); }
594
+ /* The links scroll, not the bordered rail, so an overlay scrollbar sits clear of the divider. */
595
+ .fg-site-rail__content { max-height: calc(100svh - var(--space-12) - var(--space-8)); overflow-y: auto; }
629
596
  .fg-site-rail__menu::details-content { content-visibility: visible; }
630
597
  .fg-site-rail__menu:not([open]) > .fg-site-rail__content { display: block; }
631
598
  }
@@ -655,8 +622,6 @@
655
622
  .fg-site-docs > main { gap: var(--space-8); padding-top: var(--space-6); }
656
623
  .fg-site-rail {
657
624
  position: static;
658
- max-height: none;
659
- overflow: visible;
660
625
  padding-block: var(--space-2);
661
626
  }
662
627
  .fg-site-rail__summary,