@filipgutica/ui 1.9.0 → 1.11.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
@@ -50,9 +50,10 @@ cancelable `closeAutoFocus` event and focus that heading.
50
50
 
51
51
  `UiTabs` renders every labelled panel during SSR and initial hydration. After
52
52
  mounting, it adds keyboard-accessible tabs and hides inactive panels without
53
- unmounting them. Use unique item values and keep page anchor headings outside
54
- the panels. Re-align an initial hash after layout settles if enhancement changes
55
- the page height.
53
+ unmounting them. A tab strip narrower than its tabs scrolls, fades each clipped
54
+ edge, and keeps the selected tab in view when it resizes. Use unique item values
55
+ and keep page anchor headings outside the panels. Re-align an initial hash after
56
+ layout settles if enhancement changes the page height.
56
57
 
57
58
  Track page sections from a component's setup function:
58
59
 
@@ -68,7 +69,10 @@ The returned readonly ref contains the last supplied heading above the root's
68
69
  CSS `scroll-padding-top` inset, or the final section at the document bottom.
69
70
  IDs must remain stable and in page order; missing elements are ignored after
70
71
  mounting. Bind matching links to `aria-current="location"`. Scrolling, resizing,
71
- layout changes, completed transitions, and restored pages update the value.
72
+ layout changes, completed transitions, and restored pages update the value. After
73
+ a jump to a section, through a `#id` link, a hash change, or a deep link, that
74
+ section stays current until the reader scrolls away from where the jump lands, so
75
+ a page too short to bring it to the top still marks it.
72
76
  The composable does not change focus, the URL, or browser history, and removes
73
77
  its listeners and observer when the component unmounts. During SSR it returns
74
78
  the first supplied ID without accessing browser globals.
@@ -198,9 +202,10 @@ description and wraps.
198
202
 
199
203
  `site.css` also includes page helpers: `.fg-site-steps` for an ordered list with
200
204
  numbered circles, `.fg-site-facts` for an inline `dl` of short label and value
201
- pairs, `.fg-site-definitions` for a two-column `dl` of `dt` and `dd` that stacks
202
- on phones, `.fg-site-sr-only` for visually hidden text, and `.fg-site-skip` for
203
- a skip link that appears on focus.
205
+ pairs whose links get a `--control-lg` hit area at 640px and below,
206
+ `.fg-site-definitions` for a two-column `dl` of `dt` and `dd` that stacks on
207
+ phones and keeps `code` such as `--all` on one line, `.fg-site-sr-only` for
208
+ visually hidden text, and `.fg-site-skip` for a skip link that appears on focus.
204
209
 
205
210
  ### Docs shell
206
211
 
@@ -211,9 +216,11 @@ main content, and footer. The shell then supplies:
211
216
  the footer on the frame edges. At 800px and below the rail stacks above `main`.
212
217
  - element defaults inside the shell, plus the page background and font on
213
218
  `body`: box sizing, heading sizes and margins, paragraph measure, links,
214
- `kbd`, and a 2px focus outline.
219
+ inline `code` at `--text-sm`, `kbd`, a 2px focus outline, and a `[hidden]`
220
+ rule that outranks the page's own `display` rules.
215
221
  - `.fg-site-footer` for the footer and `.fg-site-legend` for a `dl` that explains
216
- what a capture shows.
222
+ what a capture shows. Legend columns are at least `--site-legend-column` wide,
223
+ `--sidebar` by default; set it on the legend to fit more entries in a row.
217
224
 
218
225
  ```vue
219
226
  <template>
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, i as t, n, o as r, r as i, t as a } from "./use-active-section-qEJQS1S9.js";
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./use-active-section-DCS_wyYq.js";
2
2
  import { Fragment as o, computed as s, createBlock as c, createCommentVNode as l, createElementBlock as u, createElementVNode as d, createTextVNode as f, createVNode as p, defineComponent as m, inject as h, mergeModels as g, mergeProps as _, normalizeStyle as v, openBlock as y, provide as b, ref as ee, renderList as x, renderSlot as S, resolveDynamicComponent as C, shallowRef as te, toDisplayString as w, unref as T, useId as E, useModel as ne, useSlots as D, watch as re, withCtx as O } from "vue";
3
3
  import { CheckboxIndicator as k, CheckboxRoot as A, Primitive as j, ProgressIndicator as M, ProgressRoot as N, RadioGroupItem as P, RadioGroupRoot as F, SliderRange as I, SliderRoot as L, SliderThumb as R, SliderTrack as z, Toggle as B } 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, t as a } from "./use-active-section-qEJQS1S9.js";
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./use-active-section-DCS_wyYq.js";
2
2
  import { Fragment as o, computed as s, createBlock as c, createCommentVNode as l, createElementBlock as u, createElementVNode as d, createTextVNode as f, createVNode as p, defineComponent as m, mergeModels as h, nextTick as g, normalizeClass as _, normalizeStyle as v, onBeforeUnmount as y, onMounted as b, onUnmounted as x, openBlock as S, ref as C, renderList as w, renderSlot as T, shallowRef as E, toDisplayString as D, unref as O, useId as k, useModel as ee, useSlots as A, useTemplateRef as te, watch as j, withCtx as M } from "vue";
3
3
  import { tryOnMounted as N, usePreferredDark as P, useStorage as F } from "@vueuse/core";
4
4
  //#region src/site/appearance.ts
@@ -154,7 +154,7 @@ var N = /* @__PURE__ */ c({
154
154
  _: 3
155
155
  }, 8, ["open"]));
156
156
  }
157
- }), H = { class: "fg-tabs" }, U = ["aria-label"], W = [
157
+ }), H = { class: "fg-tabs" }, U = ["aria-label", "data-clipped"], W = [
158
158
  "id",
159
159
  "aria-controls",
160
160
  "aria-selected",
@@ -180,16 +180,34 @@ var N = /* @__PURE__ */ c({
180
180
  u(() => {
181
181
  l.value = !0;
182
182
  });
183
- let C = b("list");
184
- x([l, d], () => {
183
+ let C = b("list"), w = () => {
185
184
  let e = C.value, t = e?.querySelector("[aria-selected=\"true\"]");
186
185
  if (!e || !t) return;
187
- let n = e.getBoundingClientRect(), { left: r, right: i } = t.getBoundingClientRect();
188
- r < n.left ? e.scrollLeft -= n.left - r : i > n.right && (e.scrollLeft += i - n.right);
189
- }, { flush: "post" });
190
- let w = (e) => {
186
+ let n = e.getBoundingClientRect(), r = getComputedStyle(e), i = n.left + (Number.parseFloat(r.scrollPaddingLeft) || 0), a = n.right - (Number.parseFloat(r.scrollPaddingRight) || 0), { left: o, right: s } = t.getBoundingClientRect();
187
+ o < i ? e.scrollLeft -= i - o : s > a && (e.scrollLeft += s - a);
188
+ }, T = m(), E = () => {
189
+ let e = C.value;
190
+ if (!e) return;
191
+ let t = e.scrollWidth - e.clientWidth, n = getComputedStyle(e).direction === "rtl" ? e.scrollLeft + t : e.scrollLeft, r = n > 1, i = n < t - 1;
192
+ T.value = r && i ? "both" : r ? "left" : i ? "right" : void 0;
193
+ }, D = () => {
194
+ w(), E();
195
+ };
196
+ x([
197
+ l,
198
+ d,
199
+ () => n.items
200
+ ], D, {
201
+ flush: "post",
202
+ deep: !0
203
+ }), x(C, (e, t, n) => {
204
+ if (!e || typeof ResizeObserver > "u") return;
205
+ let r = new ResizeObserver(D);
206
+ r.observe(e), n(() => r.disconnect());
207
+ });
208
+ let O = (e) => {
191
209
  e !== d.value && s("update:modelValue", e);
192
- }, T = (e) => {
210
+ }, k = (e) => {
193
211
  if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || ![
194
212
  "ArrowLeft",
195
213
  "ArrowRight",
@@ -213,7 +231,9 @@ var N = /* @__PURE__ */ c({
213
231
  class: "fg-tabs__list",
214
232
  role: "tablist",
215
233
  "aria-label": n.label,
216
- onKeydown: T
234
+ "data-clipped": T.value,
235
+ onKeydown: k,
236
+ onScrollPassive: E
217
237
  }, [(f(!0), i(e, null, h(n.items, (e) => (f(), i("button", {
218
238
  id: p(e.value),
219
239
  key: e.value,
@@ -223,8 +243,8 @@ var N = /* @__PURE__ */ c({
223
243
  "aria-controls": v(e.value),
224
244
  "aria-selected": e.value === d.value,
225
245
  tabindex: e.value === d.value ? 0 : -1,
226
- onClick: (t) => w(e.value),
227
- onFocus: (t) => w(e.value)
246
+ onClick: (t) => O(e.value),
247
+ onFocus: (t) => O(e.value)
228
248
  }, _(e.label), 41, W))), 128))], 40, U)) : r("", !0), (f(!0), i(e, null, h(n.items, (e) => (f(), i("section", {
229
249
  id: v(e.value),
230
250
  key: e.value,
@@ -248,17 +268,35 @@ var N = /* @__PURE__ */ c({
248
268
  id: e,
249
269
  element: t
250
270
  }] : [];
251
- }), o = () => {
252
- n = void 0;
253
- let e = document.documentElement, r = Number.parseFloat(getComputedStyle(e).scrollPaddingTop) || 0, i = a[0]?.id;
254
- for (let e of a) e.element.getBoundingClientRect().top <= r + 1 && (i = e.id);
255
- window.scrollY > 0 && window.scrollY + window.innerHeight >= e.scrollHeight - 2 && (i = a.at(-1)?.id), t.value = i;
256
- }, s = () => {
257
- n === void 0 && (n = requestAnimationFrame(o));
271
+ }), o = document.documentElement, s = () => Number.parseFloat(getComputedStyle(o).scrollPaddingTop) || 0, c, l = () => {
272
+ if (n = void 0, c) {
273
+ let e = Math.abs(window.scrollY - c.landing);
274
+ e > c.closest + 2 ? c = void 0 : c.closest = Math.min(c.closest, e);
275
+ }
276
+ let e = s(), r = a[0]?.id;
277
+ for (let t of a) t.element.getBoundingClientRect().top <= e + 1 && (r = t.id);
278
+ window.scrollY > 0 && window.scrollY + window.innerHeight >= o.scrollHeight - 2 && (r = a.at(-1)?.id), t.value = c?.id ?? r;
279
+ }, u = () => {
280
+ n === void 0 && (n = requestAnimationFrame(l));
281
+ }, d = (e) => {
282
+ let t = a.find((t) => t.id === e);
283
+ if (c = void 0, t) {
284
+ let n = Math.max(0, o.scrollHeight - window.innerHeight), r = window.scrollY + t.element.getBoundingClientRect().top - s(), i = Math.min(Math.max(r, 0), n);
285
+ c = {
286
+ id: e,
287
+ landing: i,
288
+ closest: Math.abs(window.scrollY - i)
289
+ };
290
+ }
291
+ u();
292
+ }, f = () => d(location.hash.slice(1)), p = (e) => {
293
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
294
+ let t = e.target instanceof Element ? e.target.closest("a[href^=\"#\"]")?.getAttribute("href") : void 0;
295
+ t && d(t.slice(1));
258
296
  };
259
- window.addEventListener("scroll", s, { passive: !0 }), window.addEventListener("resize", s), window.addEventListener("pageshow", s), document.body.addEventListener("transitionend", s), typeof ResizeObserver < "u" && (r = new ResizeObserver(s), r.observe(document.body)), i = () => {
260
- window.removeEventListener("scroll", s), window.removeEventListener("resize", s), window.removeEventListener("pageshow", s), document.body.removeEventListener("transitionend", s);
261
- }, o();
297
+ window.addEventListener("scroll", u, { passive: !0 }), window.addEventListener("resize", u), window.addEventListener("pageshow", u), window.addEventListener("hashchange", f), document.addEventListener("click", p), document.body.addEventListener("transitionend", u), typeof ResizeObserver < "u" && (r = new ResizeObserver(u), r.observe(document.body)), i = () => {
298
+ window.removeEventListener("scroll", u), window.removeEventListener("resize", u), window.removeEventListener("pageshow", u), window.removeEventListener("hashchange", f), document.removeEventListener("click", p), document.body.removeEventListener("transitionend", u);
299
+ }, location.hash && f(), l();
262
300
  }), d(() => {
263
301
  i?.(), r?.disconnect(), n !== void 0 && cancelAnimationFrame(n);
264
302
  }), p(t);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filipgutica/ui",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "A compact Vue design system with Tailwind CSS and VS Code theme support",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -12,10 +12,19 @@
12
12
  width: max-content;
13
13
  max-width: 100%;
14
14
  overflow-x: auto;
15
+ scroll-padding-inline: var(--space-6);
15
16
  margin-block-end: var(--space-4);
16
17
  border-bottom: 1px solid var(--color-border-subtle);
17
18
  }
18
19
 
20
+ /* A strip that scrolls fades each clipped edge; the scroll padding keeps a revealed tab clear
21
+ * of the fade. Mask stops are alpha only: #000 keeps a pixel and transparent hides it. */
22
+ .fg-tabs__list[data-clipped="right"] { mask-image: linear-gradient(to right, #000 calc(100% - var(--space-6)), transparent); }
23
+ .fg-tabs__list[data-clipped="left"] { mask-image: linear-gradient(to left, #000 calc(100% - var(--space-6)), transparent); }
24
+ .fg-tabs__list[data-clipped="both"] {
25
+ mask-image: linear-gradient(to right, transparent, #000 var(--space-6), #000 calc(100% - var(--space-6)), transparent);
26
+ }
27
+
19
28
  .fg-tabs__tab {
20
29
  flex: none;
21
30
  min-height: var(--control-md);
package/src/site.css CHANGED
@@ -27,7 +27,13 @@
27
27
  .fg-site-docs :where(h1) { max-width: 22ch; font-size: var(--text-3xl); letter-spacing: var(--tracking-tight); }
28
28
  .fg-site-docs :where(h2) { font-size: var(--text-xl); }
29
29
  .fg-site-docs :where(p) { max-width: var(--measure); }
30
+ /* Tab panels and other enhanced markup use the hidden attribute, so it outranks the page's own
31
+ * display rules. until-found keeps the browser's find-in-page behavior. */
32
+ .fg-site-docs :where([hidden]:not([hidden="until-found"])) { display: none !important; }
30
33
  .fg-site-docs :where(code, kbd, pre) { font-family: var(--font-mono); }
34
+ /* Monospace looks larger than the UI face, so inline code steps down: --text-sm in --text-md text.
35
+ * Code inside pre keeps the block's own size. */
36
+ .fg-site-docs :where(:not(pre) > code) { font-size: var(--text-sm); }
31
37
  .fg-site-docs :where(a) { color: var(--color-link); text-underline-offset: 0.18em; }
32
38
  .fg-site-docs :where(kbd) {
33
39
  display: inline-block;
@@ -90,10 +96,11 @@
90
96
  }
91
97
  .fg-site-footer a:hover { color: var(--color-text); text-decoration: underline; }
92
98
 
93
- /* Legend: a dl of what a capture shows, with one div per dt and dd pair. */
99
+ /* Legend: a dl of what a capture shows, with one div per dt and dd pair. Columns are at
100
+ * least --site-legend-column wide (default --sidebar); a page can lower it to fit more. */
94
101
  .fg-site-legend {
95
102
  display: grid;
96
- grid-template-columns: repeat(auto-fit, minmax(min(var(--sidebar), 100%), 1fr));
103
+ grid-template-columns: repeat(auto-fit, minmax(min(var(--site-legend-column, var(--sidebar)), 100%), 1fr));
97
104
  gap: 0 var(--space-6);
98
105
  margin: 0;
99
106
  font-size: var(--text-sm);
@@ -513,6 +520,8 @@
513
520
  }
514
521
  .fg-site-definitions dt { font-weight: var(--weight-medium); }
515
522
  .fg-site-definitions dd { margin: 0; color: var(--color-muted); }
523
+ /* Keep flags such as --all whole instead of breaking after a hyphen. */
524
+ .fg-site-definitions code { white-space: nowrap; }
516
525
 
517
526
  .fg-site-sr-only {
518
527
  position: absolute;
@@ -606,6 +615,16 @@
606
615
  .fg-site-docs { padding-inline: var(--space-4); }
607
616
  /* Docs-shell pages keep one header row on phones and repeat these links in their footer. */
608
617
  .fg-site-docs .fg-site-header__links { display: none; }
618
+ /* Fact links, such as a Source link that stands in for the hidden header link, get a
619
+ * touch-sized hit area. The negative margin keeps each fact row one line tall, and the
620
+ * row gap of a list with links makes room for the part of the hit area past the line. */
621
+ .fg-site-facts:has(a) { row-gap: calc(var(--control-lg) - 1lh); }
622
+ .fg-site-facts a {
623
+ display: inline-flex;
624
+ min-height: var(--control-lg);
625
+ align-items: center;
626
+ margin-block: calc((1lh - var(--control-lg)) / 2);
627
+ }
609
628
  .fg-dialog__content.fg-capture-viewer { width: 100%; max-width: none; }
610
629
  /* Phones keep the icon; the accessible name still says Full size. */
611
630
  .fg-capture[data-enhanced="true"] .fg-tabs__list { margin-inline-end: calc(var(--space-2) + var(--control-lg)); }
@@ -5,7 +5,7 @@
5
5
 
6
6
  :root {
7
7
  color-scheme: light;
8
- --color-bg: #f4f5f8;
8
+ --color-bg: #f8f9fb;
9
9
  --color-surface: #e7e8ec;
10
10
  --color-surface-raised: #ffffff;
11
11
  --color-surface-muted: #e4e6eb;
@@ -106,6 +106,6 @@
106
106
  --color-warning-surface: color-mix(in srgb, var(--color-warning) 12%, var(--color-bg));
107
107
  --color-success-surface: color-mix(in srgb, var(--color-success) 11%, var(--color-bg));
108
108
  --color-info-surface: color-mix(in srgb, var(--color-info) 10%, var(--color-bg));
109
- --shadow-dialog: 0 8px 24px rgb(0 0 0 / 45%), 0 0 0 1px var(--color-border);
109
+ --shadow-dialog: 0 8px 24px rgb(0 0 0 / 45%), 0 0 0 1px var(--color-border-subtle);
110
110
  --color-status-text: var(--color-text);
111
111
  }