@filipgutica/ui 1.17.0 → 1.18.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.
package/README.md CHANGED
@@ -186,9 +186,9 @@ import read from './captures/read.html?raw'
186
186
 
187
187
  `UiSiteNav` renders a labelled list of route rows in optional `groups`. Links to
188
188
  `#id` sections track the reader's position with `useActiveSection` and get
189
- `aria-current="location"`, an accent bar without a fill. An item with
189
+ `aria-current="location"`, an accent pip without a fill. An item with
190
190
  `current: true` gets
191
- `aria-current="page"`, the selected fill and accent bar; `code: true` shows the
191
+ `aria-current="page"`, a quiet filled row with a slight lift and accent text; `code: true` shows the
192
192
  label in the monospace font. The page grid places the rail, which stays in view
193
193
  on desktop. At 800px and below it is a native disclosure until the page is
194
194
  interactive, then a drawer. The disclosure, the drawer button, and the drawer
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./use-active-section-DMbpGE2f.js";
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./use-active-section-DpcSUhA9.js";
2
2
  import { Fragment as s, computed as c, createBlock as l, createCommentVNode as u, createElementBlock as d, createElementVNode as f, createTextVNode as p, createVNode as m, defineComponent as h, inject as g, mergeModels as _, mergeProps as v, normalizeStyle as y, openBlock as b, provide as x, ref as S, renderList as C, renderSlot as w, resolveDynamicComponent as T, shallowRef as ee, toDisplayString as E, unref as D, useAttrs as te, useId as O, useModel as ne, useSlots as k, watch as re, withCtx as A } from "vue";
3
3
  import { CheckboxIndicator as j, CheckboxRoot as M, Primitive as N, ProgressIndicator as P, ProgressRoot as F, RadioGroupItem as I, RadioGroupRoot as L, SliderRange as R, SliderRoot as z, SliderThumb as B, SliderTrack as V, Toggle as H } from "reka-ui";
4
4
  //#region src/components/UiCard.vue?vue&type=script&setup=true&lang.ts
package/dist/site.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./use-active-section-DMbpGE2f.js";
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./use-active-section-DpcSUhA9.js";
2
2
  import { Fragment as s, computed as c, createBlock as l, createCommentVNode as u, createElementBlock as d, createElementVNode as f, createTextVNode as p, createVNode as m, defineComponent as h, mergeModels as g, nextTick as _, normalizeClass as v, normalizeStyle as y, onBeforeUnmount as b, onMounted as x, onUnmounted as S, openBlock as C, ref as w, renderList as T, renderSlot as E, shallowRef as D, toDisplayString as O, unref as k, useId as A, useModel as j, useSlots as M, useTemplateRef as N, watch as P, withCtx as F } from "vue";
3
3
  import { tryOnMounted as I, usePreferredDark as L, useStorage as R } from "@vueuse/core";
4
4
  //#region src/site/appearance.ts
@@ -185,13 +185,32 @@ var N = /* @__PURE__ */ c({
185
185
  },
186
186
  emits: ["update:modelValue"],
187
187
  setup(t, { emit: n }) {
188
- let a = n;
189
- return (n, o) => (f(), i("div", {
188
+ let a = n, o = b("root"), s = !1;
189
+ u(() => requestAnimationFrame(() => {
190
+ s = !0;
191
+ }));
192
+ let c = () => {
193
+ o.value && (delete o.value.dataset.slide, o.value.style.removeProperty("--fg-segmented-slide-left"), o.value.style.removeProperty("--fg-segmented-slide-right"));
194
+ };
195
+ return x(() => t.modelValue, (e, n) => {
196
+ let r = o.value;
197
+ if (!r) return;
198
+ let i = Array.from(r.children).filter((e) => e instanceof HTMLElement), a = i[t.items.findIndex((e) => e.value === n)], l = i[t.items.findIndex((t) => t.value === e)];
199
+ if (!s || !a || !l || matchMedia("(prefers-reduced-motion: reduce)").matches) {
200
+ c();
201
+ return;
202
+ }
203
+ let u = getComputedStyle(a, "::before"), d = a.getBoundingClientRect(), f = d.left + parseFloat(u.left), p = d.right - parseFloat(u.right), m = l.getBoundingClientRect();
204
+ c(), r.style.setProperty("--fg-segmented-slide-left", `${f - m.left}px`), r.style.setProperty("--fg-segmented-slide-right", `${m.right - p}px`), r.dataset.slide = "";
205
+ }, { flush: "pre" }), (n, s) => (f(), i("div", {
206
+ ref_key: "root",
207
+ ref: o,
190
208
  class: "fg-segmented",
191
209
  role: "group",
192
210
  "aria-label": t.label,
193
211
  "data-size": t.size,
194
- "data-icon-only": t.iconOnly ? "true" : void 0
212
+ "data-icon-only": t.iconOnly ? "true" : void 0,
213
+ onAnimationend: s[0] ||= (e) => e.animationName === "fg-segmented-slide" && c()
195
214
  }, [(f(!0), i(e, null, h(t.items, (e) => (f(), i("button", {
196
215
  key: e.value,
197
216
  type: "button",
@@ -204,7 +223,7 @@ var N = /* @__PURE__ */ c({
204
223
  }, [g(n.$slots, "icon", {
205
224
  item: e,
206
225
  pressed: t.modelValue === e.value
207
- }), t.iconOnly ? r("", !0) : (f(), i("span", W, _(e.label), 1))], 8, U))), 128))], 8, H));
226
+ }), t.iconOnly ? r("", !0) : (f(), i("span", W, _(e.label), 1))], 8, U))), 128))], 40, H));
208
227
  }
209
228
  }), K = { class: "fg-tabs" }, q = ["aria-label", "data-clipped"], J = [
210
229
  "id",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filipgutica/ui",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "A compact Vue design system with Tailwind CSS and VS Code theme support",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -147,13 +147,15 @@
147
147
  }
148
148
 
149
149
  .fg-button {
150
+ --fg-button-shadow: var(--shadow-raise);
150
151
  display: inline-flex;
151
152
  align-items: center;
152
153
  justify-content: center;
153
154
  gap: var(--space-2);
154
155
  color: var(--color-button-text);
155
156
  background: var(--color-button-bg);
156
- box-shadow: none;
157
+ box-shadow: var(--fg-button-shadow);
158
+ transition: background-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
157
159
  font-weight: 500;
158
160
  letter-spacing: -0.01em;
159
161
  line-height: 1.25;
@@ -164,8 +166,8 @@
164
166
  background: var(--color-button-hover);
165
167
  }
166
168
 
167
- .fg-button:active:not(:disabled) {
168
- box-shadow: none;
169
+ .fg-button:where(:not([data-variant]), [data-variant="primary"]):is(:active, [aria-pressed="true"]):not(:disabled) {
170
+ background: color-mix(in srgb, var(--color-button-bg) 80%, var(--color-bg));
169
171
  }
170
172
 
171
173
  .fg-button:disabled,
@@ -241,12 +243,12 @@
241
243
 
242
244
  .fg-button[data-variant="secondary"] {
243
245
  color: var(--color-text);
244
- background: var(--color-surface-muted);
246
+ background: var(--color-control-thumb);
245
247
  }
246
248
 
247
249
  .fg-button[data-variant="secondary"]:hover:not(:disabled) {
248
250
  color: var(--color-text);
249
- background: color-mix(in srgb, var(--color-text) 8%, var(--color-surface-muted));
251
+ background: color-mix(in srgb, var(--color-text) 8%, var(--color-control-thumb));
250
252
  }
251
253
 
252
254
  .fg-button[data-variant="ghost"]:hover:not(:disabled) {
@@ -255,12 +257,14 @@
255
257
  }
256
258
 
257
259
  .fg-button[data-variant="ghost"] {
260
+ --fg-button-shadow: 0 0 0 0 transparent;
258
261
  color: var(--color-muted);
259
262
  background: transparent;
260
263
  box-shadow: none;
261
264
  }
262
265
 
263
266
  .fg-button[data-variant="danger"] {
267
+ --fg-button-shadow: 0 0 0 0 transparent;
264
268
  color: var(--color-status-text, var(--color-error));
265
269
  background: var(--color-error-surface);
266
270
  box-shadow: none;
@@ -274,6 +278,7 @@
274
278
  }
275
279
 
276
280
  .fg-button[data-variant="text"] {
281
+ --fg-button-shadow: 0 0 0 0 transparent;
277
282
  height: auto;
278
283
  min-height: auto;
279
284
  padding: var(--space-1);
@@ -289,6 +294,14 @@
289
294
  background: transparent;
290
295
  }
291
296
 
297
+ /* Keep press specificity level with focus so its halo can wrap the sink shadow. */
298
+ .fg-button:where(:not([data-variant="danger"], [data-variant="text"])):is(:active, [aria-pressed="true"]):not(:disabled) {
299
+ --fg-button-shadow: var(--shadow-sink);
300
+ box-shadow: var(--fg-button-shadow);
301
+ }
302
+ .fg-button[data-variant="secondary"]:is(:active, [aria-pressed="true"]):not(:disabled) { background: var(--color-well); }
303
+ .fg-button:disabled { box-shadow: none; }
304
+
292
305
  .fg-input,
293
306
  .fg-select {
294
307
  width: 100%;
@@ -759,13 +772,14 @@
759
772
  border-radius: var(--radius-control);
760
773
  background: var(--color-well);
761
774
  box-shadow: var(--shadow-sink);
775
+ /* Thumbs stack on the track, under every label, so a sliding thumb never covers its neighbour's. */
776
+ isolation: isolate;
762
777
  }
763
778
  :root.dark .fg-segmented { --fg-segmented-thumb: color-mix(in srgb, var(--color-surface-raised) 88%, white); }
764
779
  .fg-segmented[data-size="sm"] { --fg-segmented-item: var(--control-sm); font-size: var(--text-sm); }
765
780
  .fg-segmented[data-size="lg"] { --fg-segmented-item: var(--control-lg); font-size: var(--text-lg); }
766
781
  .fg-segmented__item {
767
782
  position: relative;
768
- isolation: isolate;
769
783
  display: inline-flex;
770
784
  gap: var(--space-2);
771
785
  min-height: var(--fg-segmented-item);
@@ -781,7 +795,7 @@
781
795
  }
782
796
  .fg-segmented[data-icon-only="true"] .fg-segmented__item { width: var(--fg-segmented-item); padding: 0; }
783
797
  /* The thumb is inset inside each full-size button, so it floats on the track and the hit target keeps its size. */
784
- .fg-segmented__item::before { content: ""; position: absolute; inset: 2px; z-index: -1; border-radius: var(--radius-control); }
798
+ .fg-segmented__item::before { content: ""; position: absolute; inset: 2px; z-index: -2; border-radius: var(--radius-control); pointer-events: none; }
785
799
  .fg-segmented__item:hover:not(:disabled, [aria-pressed="true"]) { color: var(--color-text); }
786
800
  .fg-segmented__item:hover:not(:disabled, [aria-pressed="true"])::before { background: color-mix(in srgb, var(--fg-segmented-thumb) 45%, transparent); }
787
801
  /* The accent label or icon marks the pressed choice at 3:1 against its thumb. */
@@ -789,9 +803,20 @@
789
803
  /* Drawn inside the button, the ring hugs the thumb instead of reaching the next choice. */
790
804
  .fg-segmented .fg-segmented__item:focus-visible { outline-offset: -2px; }
791
805
  .fg-segmented__item[aria-pressed="true"]::before {
806
+ z-index: -1;
792
807
  background: var(--fg-segmented-thumb);
793
808
  box-shadow: var(--shadow-raise);
794
809
  }
810
+ /* Motion answers only the reader: hover, and a change of choice, which the script marks
811
+ * with the thumb's start over the previous option. A theme change recolours at once. */
812
+ @media (prefers-reduced-motion: no-preference) {
813
+ .fg-segmented__item:hover:not(:disabled, [aria-pressed="true"]) { transition: color var(--duration-fast) var(--ease-out); }
814
+ .fg-segmented[data-slide] .fg-segmented__item { transition: color var(--duration) var(--ease-out); }
815
+ .fg-segmented[data-slide] .fg-segmented__item[aria-pressed="true"]::before { animation: fg-segmented-slide var(--duration) var(--ease-out); }
816
+ }
817
+ @keyframes fg-segmented-slide {
818
+ from { left: var(--fg-segmented-slide-left); right: var(--fg-segmented-slide-right); }
819
+ }
795
820
 
796
821
  .fg-switch { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
797
822
  .fg-switch__control {
@@ -1024,6 +1049,10 @@
1024
1049
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-accent) 18%, transparent);
1025
1050
  }
1026
1051
 
1052
+ .fg-button:focus-visible:not(:disabled) {
1053
+ box-shadow: var(--fg-button-shadow), 0 0 0 6px color-mix(in srgb, var(--color-accent) 18%, transparent);
1054
+ }
1055
+
1027
1056
  .fg-input:focus-visible,
1028
1057
  .fg-select:focus-visible,
1029
1058
  .fg-radio-card[data-state="checked"]:focus-visible,
package/src/site.css CHANGED
@@ -201,30 +201,39 @@
201
201
 
202
202
  /* Route rows. Hover is neutral, and the current page keeps its selected fill on
203
203
  * hover. An in-page section marker (aria-current="location") must look different
204
- * from the current page, so it keeps the bar without the selected fill. */
204
+ * from the current page, so it uses an accent pip without the raised fill. */
205
205
  .fg-site-nav a {
206
206
  display: flex;
207
207
  min-height: var(--row);
208
208
  align-items: center;
209
209
  padding-inline: var(--space-3);
210
- border-radius: var(--radius-inline);
210
+ border-radius: var(--radius-field);
211
211
  color: var(--color-muted);
212
212
  font-family: var(--font-mono);
213
213
  font-size: var(--text-sm);
214
214
  text-decoration: none;
215
- transition: background-color var(--duration-fast) ease, color var(--duration-fast) ease;
215
+ transition: background-color var(--duration-fast) ease, color var(--duration-fast) ease, box-shadow var(--duration-fast) ease;
216
216
  }
217
217
  .fg-site-nav a:hover { background: var(--color-row-hover); color: var(--color-text); }
218
218
  .fg-site-nav a[aria-current="page"] {
219
- background: var(--color-surface-raised);
220
- box-shadow: inset 2px 0 0 var(--color-project);
221
- color: var(--color-text);
219
+ background: var(--color-nav-current);
220
+ box-shadow: var(--shadow-nav-current);
221
+ color: var(--color-accent);
222
222
  font-weight: var(--weight-medium);
223
223
  }
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); }
224
+ .fg-site-nav a[aria-current="location"] { position: relative; color: var(--color-text); font-weight: var(--weight-medium); }
225
+ .fg-site-nav a[aria-current="location"]::before {
226
+ content: "";
227
+ position: absolute;
228
+ inset-inline-start: var(--space-1);
229
+ width: 3px;
230
+ height: 14px;
231
+ border-radius: var(--radius-pill);
232
+ background: var(--color-accent);
233
+ }
225
234
  .fg-site-nav a code { font-family: var(--font-mono); font-size: var(--text-sm); }
226
235
 
227
- /* Forced colors drop the inset marker, so the current link takes a system border. */
236
+ /* Forced colors drop shadows, so the current link takes a system border. */
228
237
  @media (forced-colors: active) {
229
238
  .fg-site-nav a[aria-current] { border-inline-start: 2px solid Highlight; }
230
239
  }
package/src/theme.css CHANGED
@@ -38,6 +38,9 @@
38
38
  --color-surface-raised: #fbfbfc;
39
39
  --color-surface-muted: #e2e5e8;
40
40
  --color-well: var(--color-surface-muted);
41
+ --color-control-thumb: var(--color-surface-raised);
42
+ --color-nav-current: var(--color-surface-raised);
43
+ --shadow-nav-current: 0 1px 3px rgb(130 142 158 / 40%);
41
44
  --color-text: #101315;
42
45
  --color-muted: #50575e;
43
46
  --color-link: #0b5f7c;
@@ -82,6 +85,7 @@
82
85
  --radius-inline: 6px;
83
86
  --radius-control: 999px;
84
87
  --radius-panel: 16px;
88
+ --radius-field: 10px;
85
89
  --radius-pill: 999px;
86
90
  --duration-fast: 120ms;
87
91
  --duration: 180ms;
@@ -110,6 +114,9 @@
110
114
  --color-surface-raised: #181d21;
111
115
  --color-surface-muted: #20262b;
112
116
  --color-well: color-mix(in srgb, var(--color-bg) 87.5%, black);
117
+ --color-control-thumb: color-mix(in srgb, var(--color-surface-raised) 88%, white);
118
+ --color-nav-current: color-mix(in srgb, var(--color-text) 7%, transparent);
119
+ --shadow-nav-current: 0 1px 2px rgb(0 0 0 / 25%);
113
120
  --color-text: #e6e9eb;
114
121
  --color-muted: #9aa3ab;
115
122
  --color-link: #67c1df;