@filipgutica/ui 1.1.1 → 1.2.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
@@ -39,6 +39,39 @@ The [component docs](https://filipgutica.github.io/ui/#/components/button)
39
39
  cover examples, props, slots, and events. Place inputs and selects inside
40
40
  `UiField` to connect labels, descriptions, and errors automatically.
41
41
 
42
+ ## Page navigation
43
+
44
+ `UiDrawer` provides modal focus containment, Escape dismissal, and trigger focus
45
+ restoration. Keep native navigation in the server-rendered page until hydration;
46
+ the application decides when to replace it and when to close the drawer at a
47
+ desktop breakpoint. To focus a link destination after closing, prevent the
48
+ cancelable `closeAutoFocus` event and focus that heading.
49
+
50
+ `UiTabs` renders every labelled panel during SSR and initial hydration. After
51
+ mounting, it adds keyboard-accessible tabs and hides inactive panels without
52
+ unmounting them. Use unique item values and keep page anchor headings outside
53
+ the panels. Re-align an initial hash after layout settles if enhancement changes
54
+ the page height.
55
+
56
+ Track page sections from a component's setup function:
57
+
58
+ ```ts
59
+ import { useActiveSection } from '@filipgutica/ui'
60
+
61
+ const activeSection = useActiveSection({
62
+ targetIds: ['overview', 'install', 'commands'],
63
+ })
64
+ ```
65
+
66
+ The returned readonly ref contains the last supplied heading above the root's
67
+ CSS `scroll-padding-top` inset, or the final section at the document bottom.
68
+ IDs must remain stable and in page order; missing elements are ignored after
69
+ mounting. Bind matching links to `aria-current="location"`. Scrolling, resizing,
70
+ layout changes, completed transitions, and restored pages update the value.
71
+ The composable does not change focus, the URL, or browser history, and removes
72
+ its listeners and observer when the component unmounts. During SSR it returns
73
+ the first supplied ID without accessing browser globals.
74
+
42
75
  ## Themes
43
76
 
44
77
  Import a VS Code JSON or JSONC theme in the browser:
@@ -0,0 +1,23 @@
1
+ export interface UiDrawerProps {
2
+ open: boolean;
3
+ title: string;
4
+ }
5
+ type __VLS_Slots = {
6
+ default(): unknown;
7
+ trigger?(): unknown;
8
+ };
9
+ declare const __VLS_base: import("vue").DefineComponent<UiDrawerProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
10
+ "update:open": (value: boolean) => any;
11
+ closeAutoFocus: (value: Event) => any;
12
+ }, string, import("vue").PublicProps, Readonly<UiDrawerProps> & Readonly<{
13
+ "onUpdate:open"?: (value: boolean) => any;
14
+ onCloseAutoFocus?: (value: Event) => any;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
17
+ declare const _default: typeof __VLS_export;
18
+ export default _default;
19
+ type __VLS_WithSlots<T, S> = T & {
20
+ new (): {
21
+ $slots: S;
22
+ };
23
+ };
@@ -0,0 +1,27 @@
1
+ export interface UiTabItem {
2
+ value: string;
3
+ label: string;
4
+ }
5
+ export interface UiTabsProps {
6
+ items: readonly UiTabItem[];
7
+ label: string;
8
+ modelValue: string;
9
+ }
10
+ type __VLS_Slots = {
11
+ panel(props: {
12
+ value: string;
13
+ }): unknown;
14
+ };
15
+ declare const __VLS_base: import("vue").DefineComponent<UiTabsProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
16
+ "update:modelValue": (value: string) => any;
17
+ }, string, import("vue").PublicProps, Readonly<UiTabsProps> & Readonly<{
18
+ "onUpdate:modelValue"?: (value: string) => any;
19
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
21
+ declare const _default: typeof __VLS_export;
22
+ export default _default;
23
+ type __VLS_WithSlots<T, S> = T & {
24
+ new (): {
25
+ $slots: S;
26
+ };
27
+ };
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { default as UiCheckbox } from './components/UiCheckbox.vue';
12
12
  export type { UiCheckboxProps } from './components/UiCheckbox.vue';
13
13
  export { default as UiDialog } from './components/UiDialog.vue';
14
14
  export type { UiDialogProps } from './components/UiDialog.vue';
15
+ export { default as UiDrawer } from './components/UiDrawer.vue';
16
+ export type { UiDrawerProps } from './components/UiDrawer.vue';
15
17
  export { default as UiField } from './components/UiField.vue';
16
18
  export type { UiFieldProps } from './components/UiField.vue';
17
19
  export { default as UiInput } from './components/UiInput.vue';
@@ -26,7 +28,11 @@ export { default as UiSelect } from './components/UiSelect.vue';
26
28
  export type { UiSelectProps } from './components/UiSelect.vue';
27
29
  export { default as UiSlider } from './components/UiSlider.vue';
28
30
  export type { UiSliderProps } from './components/UiSlider.vue';
31
+ export { default as UiTabs } from './components/UiTabs.vue';
32
+ export type { UiTabItem, UiTabsProps } from './components/UiTabs.vue';
29
33
  /** @deprecated Use UiCard for structured content. */
30
34
  export { default as UiSurface } from './components/UiSurface.vue';
31
35
  export type { UiSurfaceProps } from './components/UiSurface.vue';
32
36
  export type { UiControlSize } from './control.js';
37
+ export { useActiveSection } from './use-active-section.js';
38
+ export type { ActiveSectionOptions } from './use-active-section.js';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { Fragment as e, computed as t, createBlock as n, createCommentVNode as r, createElementBlock as i, createElementVNode as a, createTextVNode as o, createVNode as s, defineComponent as c, inject as l, mergeModels as u, mergeProps as d, normalizeStyle as f, openBlock as p, provide as m, ref as h, renderList as g, renderSlot as _, resolveDynamicComponent as v, shallowRef as ee, toDisplayString as y, unref as b, useModel as x, useSlots as S, watch as te, withCtx as C } from "vue";
2
- import { CheckboxIndicator as w, CheckboxRoot as T, DialogClose as E, DialogContent as D, DialogDescription as O, DialogOverlay as k, DialogPortal as A, DialogRoot as j, DialogTitle 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 } from "reka-ui";
3
- import { useTimeoutFn as H } from "@vueuse/core";
1
+ import { Fragment as e, computed as t, createBlock as n, createCommentVNode as r, createElementBlock as i, createElementVNode as a, createTextVNode as o, createVNode as s, defineComponent as c, inject as l, mergeModels as u, mergeProps as d, normalizeStyle as f, onMounted as p, onUnmounted as m, openBlock as h, provide as g, readonly as _, ref as v, renderList as y, renderSlot as b, resolveDynamicComponent as x, shallowRef as ee, toDisplayString as S, unref as C, useId as te, useModel as w, useSlots as T, watch as E, withCtx as D } from "vue";
2
+ import { CheckboxIndicator as O, CheckboxRoot as ne, DialogClose as k, DialogContent as A, DialogDescription as j, DialogOverlay as M, DialogPortal as N, DialogRoot as P, DialogTitle as F, DialogTrigger as I, Primitive as L, ProgressIndicator as R, ProgressRoot as z, RadioGroupItem as B, RadioGroupRoot as V, SliderRange as H, SliderRoot as U, SliderThumb as W, SliderTrack as G } from "reka-ui";
3
+ import { useTimeoutFn as K } from "@vueuse/core";
4
4
  //#endregion
5
5
  //#region src/components/UiButton.vue
6
- var U = /* @__PURE__ */ c({
6
+ var q = /* @__PURE__ */ c({
7
7
  __name: "UiButton",
8
8
  props: {
9
9
  disabled: {
@@ -19,7 +19,7 @@ var U = /* @__PURE__ */ c({
19
19
  variant: { default: "primary" }
20
20
  },
21
21
  setup(e) {
22
- return (t, r) => (p(), n(b(N), {
22
+ return (t, r) => (h(), n(C(L), {
23
23
  as: "button",
24
24
  class: "fg-button",
25
25
  "data-size": e.size,
@@ -28,7 +28,7 @@ var U = /* @__PURE__ */ c({
28
28
  disabled: e.disabled || e.loading,
29
29
  type: e.type
30
30
  }, {
31
- default: C(() => [_(t.$slots, "default")]),
31
+ default: D(() => [b(t.$slots, "default")]),
32
32
  _: 3
33
33
  }, 8, [
34
34
  "data-size",
@@ -38,49 +38,49 @@ var U = /* @__PURE__ */ c({
38
38
  "type"
39
39
  ]));
40
40
  }
41
- }), W = { class: "fg-card" }, G = {
41
+ }), J = { class: "fg-card" }, Y = {
42
42
  key: 0,
43
43
  class: "fg-card__header"
44
- }, K = {
44
+ }, X = {
45
45
  key: 1,
46
46
  class: "fg-card__actions"
47
- }, q = {
47
+ }, Z = {
48
48
  key: 1,
49
49
  class: "fg-card__body"
50
- }, J = {
50
+ }, re = {
51
51
  key: 2,
52
52
  class: "fg-card__footer"
53
- }, Y = /* @__PURE__ */ c({
53
+ }, ie = /* @__PURE__ */ c({
54
54
  __name: "UiCard",
55
55
  props: {
56
56
  title: { default: () => void 0 },
57
57
  titleTag: { default: "h3" }
58
58
  },
59
59
  setup(e) {
60
- let t = S();
61
- return (a, s) => (p(), i("div", W, [
62
- e.title || t.title || t.actions ? (p(), i("header", G, [e.title || t.title ? (p(), n(v(e.titleTag), {
60
+ let t = T();
61
+ return (a, s) => (h(), i("div", J, [
62
+ e.title || t.title || t.actions ? (h(), i("header", Y, [e.title || t.title ? (h(), n(x(e.titleTag), {
63
63
  key: 0,
64
64
  class: "fg-card__title"
65
65
  }, {
66
- default: C(() => [_(a.$slots, "title", {}, () => [o(y(e.title), 1)])]),
66
+ default: D(() => [b(a.$slots, "title", {}, () => [o(S(e.title), 1)])]),
67
67
  _: 3
68
- })) : r("", !0), t.actions ? (p(), i("div", K, [_(a.$slots, "actions")])) : r("", !0)])) : r("", !0),
69
- t.default ? (p(), i("div", q, [_(a.$slots, "default")])) : r("", !0),
70
- t.footer ? (p(), i("footer", J, [_(a.$slots, "footer")])) : r("", !0)
68
+ })) : r("", !0), t.actions ? (h(), i("div", X, [b(a.$slots, "actions")])) : r("", !0)])) : r("", !0),
69
+ t.default ? (h(), i("div", Z, [b(a.$slots, "default")])) : r("", !0),
70
+ t.footer ? (h(), i("footer", re, [b(a.$slots, "footer")])) : r("", !0)
71
71
  ]));
72
72
  }
73
- }), X = ["data-wrap", "data-line-numbers"], Z = { class: "fg-code-block__header" }, ne = { class: "fg-code-block__title" }, re = {
73
+ }), ae = ["data-wrap", "data-line-numbers"], oe = { class: "fg-code-block__header" }, se = { class: "fg-code-block__title" }, ce = {
74
74
  key: 0,
75
75
  class: "fg-code-block__language"
76
- }, ie = { class: "fg-code-block__actions" }, ae = ["disabled"], oe = ["aria-label"], se = {
76
+ }, le = { class: "fg-code-block__actions" }, ue = ["disabled"], de = ["aria-label"], fe = {
77
77
  key: 0,
78
78
  class: "fg-code-block__number",
79
79
  "aria-hidden": "true"
80
- }, ce = { class: "fg-code-block__text" }, le = {
80
+ }, pe = { class: "fg-code-block__text" }, me = {
81
81
  class: "fg-code-block__status",
82
82
  role: "status"
83
- }, ue = /* @__PURE__ */ c({
83
+ }, he = /* @__PURE__ */ c({
84
84
  __name: "UiCodeBlock",
85
85
  props: {
86
86
  code: {},
@@ -100,7 +100,7 @@ var U = /* @__PURE__ */ c({
100
100
  }
101
101
  },
102
102
  setup(n) {
103
- let s = ee(), c = h(""), l = t(() => s.value ?? n.code.split("\n").map((e) => [{ content: e }])), u = {
103
+ let s = ee(), c = v(""), l = t(() => s.value ?? n.code.split("\n").map((e) => [{ content: e }])), u = {
104
104
  vue: "vue",
105
105
  ts: "typescript",
106
106
  typescript: "typescript",
@@ -113,7 +113,7 @@ var U = /* @__PURE__ */ c({
113
113
  sh: "bash",
114
114
  shell: "bash"
115
115
  };
116
- te(() => [n.code, n.language], async ([e, t], n, r) => {
116
+ E(() => [n.code, n.language], async ([e, t], n, r) => {
117
117
  let i = !1;
118
118
  r(() => {
119
119
  i = !0;
@@ -136,37 +136,37 @@ var U = /* @__PURE__ */ c({
136
136
  i || (c.value = "Syntax highlighting unavailable. Showing plain text.");
137
137
  }
138
138
  }, { immediate: !0 });
139
- let d = h(!1), { start: m } = H(() => {
139
+ let d = v(!1), { start: p } = K(() => {
140
140
  d.value = !1;
141
- }, 1500, { immediate: !1 }), v = h(!1), b = h(""), x = async () => {
142
- if (b.value = "", d.value = !1, typeof navigator > "u" || !navigator.clipboard?.writeText) {
143
- b.value = "Clipboard unavailable. Select the code to copy it.";
141
+ }, 1500, { immediate: !1 }), m = v(!1), g = v(""), _ = async () => {
142
+ if (g.value = "", d.value = !1, typeof navigator > "u" || !navigator.clipboard?.writeText) {
143
+ g.value = "Clipboard unavailable. Select the code to copy it.";
144
144
  return;
145
145
  }
146
- v.value = !0;
146
+ m.value = !0;
147
147
  try {
148
- await navigator.clipboard.writeText(n.code), d.value = !0, m();
148
+ await navigator.clipboard.writeText(n.code), d.value = !0, p();
149
149
  } catch {
150
- b.value = "Could not copy. Select the code to copy it.";
150
+ g.value = "Could not copy. Select the code to copy it.";
151
151
  } finally {
152
- v.value = !1;
152
+ m.value = !1;
153
153
  }
154
154
  };
155
- return (t, s) => (p(), i("div", {
155
+ return (t, s) => (h(), i("div", {
156
156
  class: "fg-code-block",
157
157
  "data-wrap": n.wrap,
158
158
  "data-line-numbers": n.lineNumbers
159
159
  }, [
160
- a("div", Z, [
161
- a("span", ne, y(n.title || n.language), 1),
162
- n.title ? (p(), i("span", re, y(n.language), 1)) : r("", !0),
163
- a("div", ie, [_(t.$slots, "actions"), n.copyable ? (p(), i("button", {
160
+ a("div", oe, [
161
+ a("span", se, S(n.title || n.language), 1),
162
+ n.title ? (h(), i("span", ce, S(n.language), 1)) : r("", !0),
163
+ a("div", le, [b(t.$slots, "actions"), n.copyable ? (h(), i("button", {
164
164
  key: 0,
165
165
  type: "button",
166
166
  class: "fg-code-block__copy",
167
- disabled: v.value,
167
+ disabled: m.value,
168
168
  "aria-label": "Copy code",
169
- onClick: x
169
+ onClick: _
170
170
  }, [s[0] ||= a("svg", {
171
171
  viewBox: "0 0 24 24",
172
172
  fill: "none",
@@ -179,50 +179,50 @@ var U = /* @__PURE__ */ c({
179
179
  width: "12",
180
180
  height: "13",
181
181
  rx: "2"
182
- }), a("path", { d: "M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3" })], -1), o(" " + y(d.value && !b.value ? "Copied" : "Copy"), 1)], 8, ae)) : r("", !0)])
182
+ }), a("path", { d: "M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3" })], -1), o(" " + S(d.value && !g.value ? "Copied" : "Copy"), 1)], 8, ue)) : r("", !0)])
183
183
  ]),
184
184
  a("pre", {
185
185
  class: "fg-code-block__pre",
186
186
  tabindex: "0",
187
187
  "aria-label": n.title || `${n.language} code`
188
- }, [a("code", null, [(p(!0), i(e, null, g(l.value, (t, s) => (p(), i("span", {
188
+ }, [a("code", null, [(h(!0), i(e, null, y(l.value, (t, s) => (h(), i("span", {
189
189
  key: s,
190
190
  class: "fg-code-block__line"
191
- }, [n.lineNumbers ? (p(), i("span", se, y(s + 1), 1)) : r("", !0), a("span", ce, [(p(!0), i(e, null, g(t, (e, t) => (p(), i("span", {
191
+ }, [n.lineNumbers ? (h(), i("span", fe, S(s + 1), 1)) : r("", !0), a("span", pe, [(h(!0), i(e, null, y(t, (e, t) => (h(), i("span", {
192
192
  key: t,
193
193
  style: f({ color: e.color })
194
- }, y(e.content), 5))), 128)), o(y(s < l.value.length - 1 ? "\n" : ""), 1)])]))), 128))])], 8, oe),
195
- a("span", le, y(b.value || (d.value ? "Code copied to clipboard." : c.value)), 1)
196
- ], 8, X));
194
+ }, S(e.content), 5))), 128)), o(S(s < l.value.length - 1 ? "\n" : ""), 1)])]))), 128))])], 8, de),
195
+ a("span", me, S(g.value || (d.value ? "Code copied to clipboard." : c.value)), 1)
196
+ ], 8, ae));
197
197
  }
198
- }), de = /* @__PURE__ */ c({
198
+ }), ge = /* @__PURE__ */ c({
199
199
  __name: "UiAlert",
200
200
  props: { tone: { default: "neutral" } },
201
201
  setup(e) {
202
- return (t, r) => (p(), n(b(N), {
202
+ return (t, r) => (h(), n(C(L), {
203
203
  as: "div",
204
204
  class: "fg-alert",
205
205
  "data-tone": e.tone,
206
206
  role: e.tone === "danger" || e.tone === "error" ? "alert" : "status"
207
207
  }, {
208
- default: C(() => [_(t.$slots, "default")]),
208
+ default: D(() => [b(t.$slots, "default")]),
209
209
  _: 3
210
210
  }, 8, ["data-tone", "role"]));
211
211
  }
212
- }), fe = /* @__PURE__ */ c({
212
+ }), _e = /* @__PURE__ */ c({
213
213
  __name: "UiBadge",
214
214
  props: { tone: { default: "neutral" } },
215
215
  setup(e) {
216
- return (t, r) => (p(), n(b(N), {
216
+ return (t, r) => (h(), n(C(L), {
217
217
  as: "span",
218
218
  class: "fg-badge",
219
219
  "data-tone": e.tone
220
220
  }, {
221
- default: C(() => [_(t.$slots, "default")]),
221
+ default: D(() => [b(t.$slots, "default")]),
222
222
  _: 3
223
223
  }, 8, ["data-tone"]));
224
224
  }
225
- }), pe = ["data-size", "data-disabled"], me = { class: "fg-checkbox__label" }, he = /* @__PURE__ */ c({
225
+ }), ve = ["data-size", "data-disabled"], ye = { class: "fg-checkbox__label" }, be = /* @__PURE__ */ c({
226
226
  __name: "UiCheckbox",
227
227
  props: {
228
228
  disabled: {
@@ -240,27 +240,28 @@ var U = /* @__PURE__ */ c({
240
240
  let n = t, r = (e) => {
241
241
  n("update:modelValue", e === !0);
242
242
  };
243
- return (t, n) => (p(), i("label", {
243
+ return (t, n) => (h(), i("label", {
244
244
  class: "fg-checkbox",
245
245
  "data-size": e.size,
246
246
  "data-disabled": e.disabled ? "true" : void 0
247
- }, [s(b(T), {
247
+ }, [s(C(ne), {
248
248
  class: "fg-checkbox__control",
249
249
  disabled: e.disabled,
250
250
  "model-value": e.modelValue,
251
251
  "onUpdate:modelValue": r
252
252
  }, {
253
- default: C(() => [s(b(w), { class: "fg-checkbox__indicator" }, {
254
- default: C(() => [...n[0] ||= [o("✓", -1)]]),
253
+ default: D(() => [s(C(O), { class: "fg-checkbox__indicator" }, {
254
+ default: D(() => [...n[0] ||= [o("✓", -1)]]),
255
255
  _: 1
256
256
  })]),
257
257
  _: 1
258
- }, 8, ["disabled", "model-value"]), a("span", me, [_(t.$slots, "default")])], 8, pe));
258
+ }, 8, ["disabled", "model-value"]), a("span", ye, [b(t.$slots, "default")])], 8, ve));
259
259
  }
260
- }), ge = { class: "fg-dialog__header" }, _e = { class: "fg-dialog__body" }, ve = {
260
+ }), xe = { class: "fg-dialog__header" }, Se = { class: "fg-dialog__body" }, Ce = {
261
261
  key: 0,
262
262
  class: "fg-dialog__footer"
263
- }, ye = /* @__PURE__ */ c({
263
+ }, we = /* @__PURE__ */ c({
264
+ inheritAttrs: !1,
264
265
  __name: "UiDialog",
265
266
  props: {
266
267
  description: { default: () => void 0 },
@@ -270,43 +271,86 @@ var U = /* @__PURE__ */ c({
270
271
  emits: ["update:open"],
271
272
  setup(e, { emit: t }) {
272
273
  let c = t;
273
- return (t, l) => (p(), n(b(j), {
274
+ return (t, l) => (h(), n(C(P), {
274
275
  open: e.open,
275
276
  "onUpdate:open": l[0] ||= (e) => c("update:open", e)
276
277
  }, {
277
- default: C(() => [s(b(A), null, {
278
- default: C(() => [s(b(k), { class: "fg-dialog__overlay" }), s(b(D), {
278
+ default: D(() => [s(C(N), null, {
279
+ default: D(() => [s(C(M), { class: "fg-dialog__overlay" }), s(C(A), d({
279
280
  class: "fg-dialog__content",
280
281
  "aria-modal": "true"
281
- }, {
282
- default: C(() => [
283
- a("div", ge, [a("div", null, [s(b(M), { class: "fg-dialog__title" }, {
284
- default: C(() => [o(y(e.title), 1)]),
282
+ }, t.$attrs), {
283
+ default: D(() => [
284
+ a("div", xe, [a("div", null, [s(C(F), { class: "fg-dialog__title" }, {
285
+ default: D(() => [o(S(e.title), 1)]),
285
286
  _: 1
286
- }), e.description ? (p(), n(b(O), {
287
+ }), e.description ? (h(), n(C(j), {
287
288
  key: 0,
288
289
  class: "fg-dialog__description"
289
290
  }, {
290
- default: C(() => [o(y(e.description), 1)]),
291
+ default: D(() => [o(S(e.description), 1)]),
291
292
  _: 1
292
- })) : r("", !0)]), s(b(E), {
293
+ })) : r("", !0)]), s(C(k), {
293
294
  class: "fg-dialog__close",
294
295
  "aria-label": "Close dialog"
295
296
  }, {
296
- default: C(() => [...l[1] ||= [o(" × ", -1)]]),
297
+ default: D(() => [...l[1] ||= [o(" × ", -1)]]),
297
298
  _: 1
298
299
  })]),
299
- a("div", _e, [_(t.$slots, "default")]),
300
- t.$slots.footer ? (p(), i("div", ve, [_(t.$slots, "footer")])) : r("", !0)
300
+ a("div", Se, [b(t.$slots, "default")]),
301
+ t.$slots.footer ? (h(), i("div", Ce, [b(t.$slots, "footer")])) : r("", !0)
301
302
  ]),
302
303
  _: 3
303
- })]),
304
+ }, 16)]),
304
305
  _: 3
305
306
  })]),
306
307
  _: 3
307
308
  }, 8, ["open"]));
308
309
  }
309
- }), Q = Symbol("UiFieldContext"), be = ["data-invalid"], $ = ["for"], xe = ["id"], Se = ["id"], Ce = /* @__PURE__ */ c({
310
+ }), Te = { class: "fg-drawer__header" }, Ee = { class: "fg-drawer__body" }, De = /* @__PURE__ */ c({
311
+ inheritAttrs: !1,
312
+ __name: "UiDrawer",
313
+ props: {
314
+ open: { type: Boolean },
315
+ title: {}
316
+ },
317
+ emits: ["update:open", "closeAutoFocus"],
318
+ setup(e, { emit: t }) {
319
+ let i = t;
320
+ return (t, c) => (h(), n(C(P), {
321
+ open: e.open,
322
+ "onUpdate:open": c[1] ||= (e) => i("update:open", e)
323
+ }, {
324
+ default: D(() => [t.$slots.trigger ? (h(), n(C(I), {
325
+ key: 0,
326
+ "as-child": ""
327
+ }, {
328
+ default: D(() => [b(t.$slots, "trigger")]),
329
+ _: 3
330
+ })) : r("", !0), s(C(N), null, {
331
+ default: D(() => [s(C(M), { class: "fg-drawer__overlay" }), s(C(A), d({
332
+ class: "fg-drawer__content",
333
+ "aria-modal": "true",
334
+ "aria-describedby": void 0
335
+ }, t.$attrs, { onCloseAutoFocus: c[0] ||= (e) => i("closeAutoFocus", e) }), {
336
+ default: D(() => [a("div", Te, [s(C(F), { class: "fg-drawer__title" }, {
337
+ default: D(() => [o(S(e.title), 1)]),
338
+ _: 1
339
+ }), s(C(k), {
340
+ class: "fg-drawer__close",
341
+ "aria-label": "Close drawer"
342
+ }, {
343
+ default: D(() => [...c[2] ||= [o(" × ", -1)]]),
344
+ _: 1
345
+ })]), a("div", Ee, [b(t.$slots, "default")])]),
346
+ _: 3
347
+ }, 16)]),
348
+ _: 3
349
+ })]),
350
+ _: 3
351
+ }, 8, ["open"]));
352
+ }
353
+ }), Q = Symbol("UiFieldContext"), Oe = ["data-invalid"], ke = ["for"], Ae = ["id"], je = ["id"], Me = /* @__PURE__ */ c({
310
354
  __name: "UiField",
311
355
  props: {
312
356
  controlId: {},
@@ -316,38 +360,38 @@ var U = /* @__PURE__ */ c({
316
360
  },
317
361
  setup(e) {
318
362
  let n = t(() => e.description && !e.error ? `${e.controlId}-description` : void 0), o = t(() => e.error ? `${e.controlId}-error` : void 0), s = t(() => !!e.error);
319
- return m(Q, {
363
+ return g(Q, {
320
364
  controlId: t(() => e.controlId),
321
365
  descriptionId: n,
322
366
  errorId: o,
323
367
  invalid: s
324
- }), (t, s) => (p(), i("div", {
368
+ }), (t, s) => (h(), i("div", {
325
369
  class: "fg-field",
326
370
  "data-invalid": e.error ? "true" : void 0
327
371
  }, [
328
372
  a("label", {
329
373
  class: "fg-field__label",
330
374
  for: e.controlId
331
- }, y(e.label), 9, $),
332
- _(t.$slots, "default", {
375
+ }, S(e.label), 9, ke),
376
+ b(t.$slots, "default", {
333
377
  descriptionId: n.value,
334
378
  errorId: o.value
335
379
  }),
336
- e.description && !e.error ? (p(), i("p", {
380
+ e.description && !e.error ? (h(), i("p", {
337
381
  key: 0,
338
382
  id: n.value,
339
383
  class: "fg-field__description",
340
384
  "data-field-description": ""
341
- }, y(e.description), 9, xe)) : r("", !0),
342
- e.error ? (p(), i("p", {
385
+ }, S(e.description), 9, Ae)) : r("", !0),
386
+ e.error ? (h(), i("p", {
343
387
  key: 1,
344
388
  id: o.value,
345
389
  class: "fg-field__error",
346
390
  "data-field-error": ""
347
- }, y(e.error), 9, Se)) : r("", !0)
348
- ], 8, be));
391
+ }, S(e.error), 9, je)) : r("", !0)
392
+ ], 8, Oe));
349
393
  }
350
- }), we = /* @__PURE__ */ c({
394
+ }), Ne = /* @__PURE__ */ c({
351
395
  __name: "UiInput",
352
396
  props: {
353
397
  disabled: {
@@ -368,13 +412,13 @@ var U = /* @__PURE__ */ c({
368
412
  let r = l(Q, void 0), i = t, a = (e) => {
369
413
  e.currentTarget instanceof HTMLInputElement && i("update:modelValue", e.currentTarget.value);
370
414
  };
371
- return (t, i) => (p(), n(b(N), {
372
- id: b(r)?.controlId.value ?? e.id,
415
+ return (t, i) => (h(), n(C(L), {
416
+ id: C(r)?.controlId.value ?? e.id,
373
417
  as: "input",
374
418
  class: "fg-input",
375
- "aria-describedby": b(r)?.descriptionId.value,
376
- "aria-errormessage": b(r)?.errorId.value,
377
- "aria-invalid": e.invalid || b(r)?.invalid.value ? "true" : void 0,
419
+ "aria-describedby": C(r)?.descriptionId.value,
420
+ "aria-errormessage": C(r)?.errorId.value,
421
+ "aria-invalid": e.invalid || C(r)?.invalid.value ? "true" : void 0,
378
422
  "data-size": e.size,
379
423
  disabled: e.disabled,
380
424
  type: e.type,
@@ -391,7 +435,7 @@ var U = /* @__PURE__ */ c({
391
435
  "value"
392
436
  ]));
393
437
  }
394
- }), Te = 100, Ee = /* @__PURE__ */ c({
438
+ }), Pe = 100, $ = /* @__PURE__ */ c({
395
439
  __name: "UiProgress",
396
440
  props: {
397
441
  label: {},
@@ -400,19 +444,19 @@ var U = /* @__PURE__ */ c({
400
444
  },
401
445
  setup(e) {
402
446
  let r = t(() => {
403
- let t = Number.isFinite(e.max) && e.max > 0 ? e.max : Te;
447
+ let t = Number.isFinite(e.max) && e.max > 0 ? e.max : Pe;
404
448
  return {
405
449
  max: t,
406
450
  value: e.value === null || !Number.isFinite(e.value) ? null : Math.min(t, Math.max(0, e.value))
407
451
  };
408
452
  }), i = t(() => r.value.value === null ? 0 : r.value.value / r.value.max * 100);
409
- return (t, a) => (p(), n(b(F), {
453
+ return (t, a) => (h(), n(C(z), {
410
454
  class: "fg-progress",
411
455
  "aria-label": e.label,
412
456
  max: r.value.max,
413
457
  "model-value": r.value.value
414
458
  }, {
415
- default: C(() => [s(b(P), {
459
+ default: D(() => [s(C(R), {
416
460
  class: "fg-progress__indicator",
417
461
  style: f({ transform: `translateX(-${100 - i.value}%)` })
418
462
  }, null, 8, ["style"])]),
@@ -423,7 +467,7 @@ var U = /* @__PURE__ */ c({
423
467
  "model-value"
424
468
  ]));
425
469
  }
426
- }), De = { class: "fg-radio-card__content" }, Oe = /* @__PURE__ */ c({
470
+ }), Fe = { class: "fg-radio-card__content" }, Ie = /* @__PURE__ */ c({
427
471
  __name: "UiRadioCard",
428
472
  props: {
429
473
  disabled: {
@@ -433,19 +477,19 @@ var U = /* @__PURE__ */ c({
433
477
  value: {}
434
478
  },
435
479
  setup(e) {
436
- return (t, r) => (p(), n(b(I), {
480
+ return (t, r) => (h(), n(C(B), {
437
481
  class: "fg-radio-card",
438
482
  disabled: e.disabled,
439
483
  value: e.value
440
484
  }, {
441
- default: C(() => [r[0] ||= a("span", {
485
+ default: D(() => [r[0] ||= a("span", {
442
486
  class: "fg-radio-card__indicator",
443
487
  "aria-hidden": "true"
444
- }, null, -1), a("span", De, [_(t.$slots, "default")])]),
488
+ }, null, -1), a("span", Fe, [b(t.$slots, "default")])]),
445
489
  _: 3
446
490
  }, 8, ["disabled", "value"]));
447
491
  }
448
- }), ke = /* @__PURE__ */ c({
492
+ }), Le = /* @__PURE__ */ c({
449
493
  __name: "UiRadioCardGroup",
450
494
  props: /*@__PURE__*/ u({
451
495
  disabled: {
@@ -463,7 +507,7 @@ var U = /* @__PURE__ */ c({
463
507
  }),
464
508
  emits: ["update:modelValue"],
465
509
  setup(e) {
466
- let t = x(e, "modelValue"), r = (t) => {
510
+ let t = w(e, "modelValue"), r = (t) => {
467
511
  if (t.altKey || t.ctrlKey || t.metaKey || t.shiftKey) return;
468
512
  let n = t.currentTarget, r = t.target;
469
513
  if (!(n instanceof HTMLElement) || !(r instanceof HTMLElement)) return;
@@ -479,14 +523,14 @@ var U = /* @__PURE__ */ c({
479
523
  let d = o[u];
480
524
  !d || d === r || (t.preventDefault(), d.focus(), d.click());
481
525
  };
482
- return (i, a) => (p(), n(b(L), d({
526
+ return (i, a) => (h(), n(C(V), d({
483
527
  modelValue: t.value,
484
528
  "onUpdate:modelValue": a[0] ||= (e) => t.value = e,
485
529
  class: "fg-radio-card-group",
486
530
  disabled: e.disabled,
487
531
  loop: e.loop
488
532
  }, e.orientation === void 0 ? {} : { orientation: e.orientation }, { onKeydown: r }), {
489
- default: C(() => [_(i.$slots, "default")]),
533
+ default: D(() => [b(i.$slots, "default")]),
490
534
  _: 3
491
535
  }, 16, [
492
536
  "modelValue",
@@ -494,7 +538,7 @@ var U = /* @__PURE__ */ c({
494
538
  "loop"
495
539
  ]));
496
540
  }
497
- }), Ae = /* @__PURE__ */ c({
541
+ }), Re = /* @__PURE__ */ c({
498
542
  __name: "UiSelect",
499
543
  props: {
500
544
  disabled: {
@@ -514,19 +558,19 @@ var U = /* @__PURE__ */ c({
514
558
  let r = l(Q, void 0), i = t, a = (e) => {
515
559
  e.currentTarget instanceof HTMLSelectElement && i("update:modelValue", e.currentTarget.value);
516
560
  };
517
- return (t, i) => (p(), n(b(N), {
518
- id: b(r)?.controlId.value ?? e.id,
561
+ return (t, i) => (h(), n(C(L), {
562
+ id: C(r)?.controlId.value ?? e.id,
519
563
  as: "select",
520
564
  class: "fg-select",
521
- "aria-describedby": b(r)?.descriptionId.value,
522
- "aria-errormessage": b(r)?.errorId.value,
523
- "aria-invalid": e.invalid || b(r)?.invalid.value ? "true" : void 0,
565
+ "aria-describedby": C(r)?.descriptionId.value,
566
+ "aria-errormessage": C(r)?.errorId.value,
567
+ "aria-invalid": e.invalid || C(r)?.invalid.value ? "true" : void 0,
524
568
  "data-size": e.size,
525
569
  disabled: e.disabled,
526
570
  value: e.modelValue,
527
571
  onChange: a
528
572
  }, {
529
- default: C(() => [_(t.$slots, "default")]),
573
+ default: D(() => [b(t.$slots, "default")]),
530
574
  _: 3
531
575
  }, 8, [
532
576
  "id",
@@ -538,7 +582,7 @@ var U = /* @__PURE__ */ c({
538
582
  "value"
539
583
  ]));
540
584
  }
541
- }), je = /* @__PURE__ */ c({
585
+ }), ze = /* @__PURE__ */ c({
542
586
  __name: "UiSlider",
543
587
  props: {
544
588
  disabled: {
@@ -571,7 +615,7 @@ var U = /* @__PURE__ */ c({
571
615
  e !== void 0 && o("update:modelValue", e);
572
616
  }
573
617
  });
574
- return (t, r) => (p(), n(b(z), {
618
+ return (t, r) => (h(), n(C(U), {
575
619
  modelValue: c.value,
576
620
  "onUpdate:modelValue": r[0] ||= (e) => c.value = e,
577
621
  class: "fg-slider",
@@ -581,15 +625,15 @@ var U = /* @__PURE__ */ c({
581
625
  min: a.value.min,
582
626
  step: a.value.step
583
627
  }, {
584
- default: C(() => [s(b(V), { class: "fg-slider__track" }, {
585
- default: C(() => [s(b(R), { class: "fg-slider__range" })]),
628
+ default: D(() => [s(C(G), { class: "fg-slider__track" }, {
629
+ default: D(() => [s(C(H), { class: "fg-slider__range" })]),
586
630
  _: 1
587
- }), s(b(B), {
588
- id: b(i)?.controlId.value ?? e.id,
631
+ }), s(C(W), {
632
+ id: C(i)?.controlId.value ?? e.id,
589
633
  class: "fg-slider__thumb",
590
- "aria-describedby": b(i)?.descriptionId.value,
591
- "aria-errormessage": b(i)?.errorId.value,
592
- "aria-invalid": b(i)?.invalid.value ? "true" : void 0,
634
+ "aria-describedby": C(i)?.descriptionId.value,
635
+ "aria-errormessage": C(i)?.errorId.value,
636
+ "aria-invalid": C(i)?.invalid.value ? "true" : void 0,
593
637
  "aria-label": e.label
594
638
  }, null, 8, [
595
639
  "id",
@@ -608,22 +652,118 @@ var U = /* @__PURE__ */ c({
608
652
  "step"
609
653
  ]));
610
654
  }
611
- }), Me = /* @__PURE__ */ c({
655
+ }), Be = { class: "fg-tabs" }, Ve = ["aria-label"], He = [
656
+ "id",
657
+ "aria-controls",
658
+ "aria-selected",
659
+ "tabindex",
660
+ "onClick",
661
+ "onFocus"
662
+ ], Ue = [
663
+ "id",
664
+ "role",
665
+ "aria-labelledby",
666
+ "hidden",
667
+ "tabindex"
668
+ ], We = ["id", "hidden"], Ge = /* @__PURE__ */ c({
669
+ __name: "UiTabs",
670
+ props: {
671
+ items: {},
672
+ label: {},
673
+ modelValue: {}
674
+ },
675
+ emits: ["update:modelValue"],
676
+ setup(n, { emit: o }) {
677
+ let s = o, c = te(), l = v(!1), u = t(() => n.items.some((e) => e.value === n.modelValue) ? n.modelValue : n.items[0]?.value), d = (e) => `${c}-tab-${encodeURIComponent(e)}`, f = (e) => `${c}-panel-${encodeURIComponent(e)}`, m = (e) => `${c}-heading-${encodeURIComponent(e)}`;
678
+ p(() => {
679
+ l.value = !0;
680
+ });
681
+ let g = (e) => {
682
+ e !== u.value && s("update:modelValue", e);
683
+ }, _ = (e) => {
684
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || ![
685
+ "ArrowLeft",
686
+ "ArrowRight",
687
+ "Home",
688
+ "End"
689
+ ].includes(e.key)) return;
690
+ let t = e.currentTarget, n = e.target;
691
+ if (!(t instanceof HTMLElement) || !(n instanceof HTMLButtonElement)) return;
692
+ let r = [...t.querySelectorAll("[role=\"tab\"]")], i = r.indexOf(n);
693
+ if (i < 0 || r.length === 0) return;
694
+ let a = getComputedStyle(t).direction === "rtl", o = e.key === (a ? "ArrowRight" : "ArrowLeft"), s = r[e.key === "Home" ? 0 : e.key === "End" ? r.length - 1 : (i + (o ? -1 : 1) + r.length) % r.length];
695
+ s && (e.preventDefault(), s.focus());
696
+ };
697
+ return (t, o) => (h(), i("div", Be, [l.value ? (h(), i("div", {
698
+ key: 0,
699
+ class: "fg-tabs__list",
700
+ role: "tablist",
701
+ "aria-label": n.label,
702
+ onKeydown: _
703
+ }, [(h(!0), i(e, null, y(n.items, (e) => (h(), i("button", {
704
+ id: d(e.value),
705
+ key: e.value,
706
+ type: "button",
707
+ class: "fg-tabs__tab",
708
+ role: "tab",
709
+ "aria-controls": f(e.value),
710
+ "aria-selected": e.value === u.value,
711
+ tabindex: e.value === u.value ? 0 : -1,
712
+ onClick: (t) => g(e.value),
713
+ onFocus: (t) => g(e.value)
714
+ }, S(e.label), 41, He))), 128))], 40, Ve)) : r("", !0), (h(!0), i(e, null, y(n.items, (e) => (h(), i("section", {
715
+ id: f(e.value),
716
+ key: e.value,
717
+ class: "fg-tabs__panel",
718
+ role: l.value ? "tabpanel" : void 0,
719
+ "aria-labelledby": l.value ? d(e.value) : m(e.value),
720
+ hidden: l.value && e.value !== u.value,
721
+ tabindex: l.value ? 0 : void 0
722
+ }, [a("h3", {
723
+ id: m(e.value),
724
+ class: "fg-tabs__heading",
725
+ hidden: l.value
726
+ }, S(e.label), 9, We), b(t.$slots, "panel", { value: e.value })], 8, Ue))), 128))]));
727
+ }
728
+ }), Ke = /* @__PURE__ */ c({
612
729
  __name: "UiSurface",
613
730
  props: {
614
731
  as: { default: "section" },
615
732
  padding: { default: "default" }
616
733
  },
617
734
  setup(e) {
618
- return (t, r) => (p(), n(b(N), {
735
+ return (t, r) => (h(), n(C(L), {
619
736
  as: e.as,
620
737
  class: "fg-surface",
621
738
  "data-padding": e.padding
622
739
  }, {
623
- default: C(() => [_(t.$slots, "default")]),
740
+ default: D(() => [b(t.$slots, "default")]),
624
741
  _: 3
625
742
  }, 8, ["as", "data-padding"]));
626
743
  }
627
- });
744
+ }), qe = ({ targetIds: e }) => {
745
+ let t = v(e[0]), n, r, i;
746
+ return p(() => {
747
+ let a = e.flatMap((e) => {
748
+ let t = document.getElementById(e);
749
+ return t ? [{
750
+ id: e,
751
+ element: t
752
+ }] : [];
753
+ }), o = () => {
754
+ n = void 0;
755
+ let e = document.documentElement, r = Number.parseFloat(getComputedStyle(e).scrollPaddingTop) || 0, i = a[0]?.id;
756
+ for (let e of a) e.element.getBoundingClientRect().top <= r + 1 && (i = e.id);
757
+ window.scrollY > 0 && window.scrollY + window.innerHeight >= e.scrollHeight - 2 && (i = a.at(-1)?.id), t.value = i;
758
+ }, s = () => {
759
+ n === void 0 && (n = requestAnimationFrame(o));
760
+ };
761
+ 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 = () => {
762
+ window.removeEventListener("scroll", s), window.removeEventListener("resize", s), window.removeEventListener("pageshow", s), document.body.removeEventListener("transitionend", s);
763
+ }, o();
764
+ }), m(() => {
765
+ i?.(), r?.disconnect(), n !== void 0 && cancelAnimationFrame(n);
766
+ }), _(t);
767
+ };
628
768
  //#endregion
629
- export { de as UiAlert, fe as UiBadge, U as UiButton, Y as UiCard, he as UiCheckbox, ue as UiCodeBlock, ye as UiDialog, Ce as UiField, we as UiInput, Ee as UiProgress, Oe as UiRadioCard, ke as UiRadioCardGroup, Ae as UiSelect, je as UiSlider, Me as UiSurface };
769
+ export { ge as UiAlert, _e as UiBadge, q as UiButton, ie as UiCard, be as UiCheckbox, he as UiCodeBlock, we as UiDialog, De as UiDrawer, Me as UiField, Ne as UiInput, $ as UiProgress, Ie as UiRadioCard, Le as UiRadioCardGroup, Re as UiSelect, ze as UiSlider, Ke as UiSurface, Ge as UiTabs, qe as useActiveSection };
@@ -0,0 +1,7 @@
1
+ import type { Ref } from 'vue';
2
+ export interface ActiveSectionOptions {
3
+ /** Section IDs in page order. Missing elements are ignored after mounting. */
4
+ targetIds: readonly string[];
5
+ }
6
+ /** Tracks the last section heading above the document's scroll-padding inset. */
7
+ export declare const useActiveSection: ({ targetIds }: ActiveSectionOptions) => Readonly<Ref<string | undefined>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filipgutica/ui",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A compact Vue design system with Tailwind CSS and VS Code theme support",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -10,6 +10,7 @@
10
10
  "dist",
11
11
  "src/theme.css",
12
12
  "src/components.css",
13
+ "src/components/*.css",
13
14
  "README.md",
14
15
  "LICENSE"
15
16
  ],
@@ -0,0 +1,100 @@
1
+ @layer components {
2
+ .fg-drawer__overlay {
3
+ position: fixed;
4
+ inset: 0;
5
+ z-index: 50;
6
+ background: var(--color-overlay);
7
+ }
8
+
9
+ .fg-drawer__content {
10
+ position: fixed;
11
+ inset-block: 0;
12
+ left: 0;
13
+ z-index: 51;
14
+ display: grid;
15
+ grid-template-rows: auto minmax(0, 1fr);
16
+ width: min(calc(var(--sidebar) + var(--space-12)), calc(100vw - var(--space-12)));
17
+ height: 100vh;
18
+ height: 100dvh;
19
+ overflow: hidden;
20
+ border-right: 1px solid var(--color-border-subtle);
21
+ color: var(--color-text);
22
+ background: var(--color-surface-raised);
23
+ box-shadow: var(--shadow-dialog);
24
+ font-family: var(--font-ui);
25
+ font-size: var(--text-md);
26
+ font-weight: var(--weight-regular);
27
+ line-height: var(--leading);
28
+ }
29
+
30
+ .fg-drawer__header {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ gap: var(--space-3);
35
+ padding: var(--space-3) var(--space-4);
36
+ border-bottom: 1px solid var(--color-border-subtle);
37
+ }
38
+
39
+ .fg-drawer__title {
40
+ min-width: 0;
41
+ margin: 0;
42
+ font-size: var(--text-md);
43
+ font-weight: var(--weight-medium);
44
+ overflow-wrap: anywhere;
45
+ }
46
+
47
+ .fg-drawer__close {
48
+ display: inline-grid;
49
+ flex: none;
50
+ min-width: var(--control-lg);
51
+ min-height: var(--control-lg);
52
+ padding: 0;
53
+ place-items: center;
54
+ border: 0;
55
+ border-radius: var(--radius-control);
56
+ color: var(--color-muted);
57
+ background: transparent;
58
+ font: inherit;
59
+ font-size: var(--text-xl);
60
+ cursor: pointer;
61
+ }
62
+
63
+ .fg-drawer__close:hover {
64
+ color: var(--color-text);
65
+ background: var(--color-row-hover);
66
+ }
67
+
68
+ .fg-drawer__body {
69
+ min-height: 0;
70
+ overflow: auto;
71
+ overscroll-behavior: contain;
72
+ padding: var(--space-4);
73
+ }
74
+
75
+ .fg-drawer__close:focus-visible,
76
+ .fg-drawer__content:focus-visible {
77
+ outline: 2px solid var(--color-focus);
78
+ outline-offset: 2px;
79
+ }
80
+
81
+ .fg-drawer__overlay[data-state="open"] { animation: fg-drawer-fade-in var(--duration) var(--ease-out); }
82
+ .fg-drawer__overlay[data-state="closed"] { animation: fg-drawer-fade-out var(--duration-fast) var(--ease-out); }
83
+ .fg-drawer__content[data-state="open"] { animation: fg-drawer-slide-in var(--duration) var(--ease-out); }
84
+ .fg-drawer__content[data-state="closed"] { animation: fg-drawer-slide-out var(--duration-fast) var(--ease-out); }
85
+
86
+ @keyframes fg-drawer-fade-in { from { opacity: 0; } to { opacity: 1; } }
87
+ @keyframes fg-drawer-fade-out { from { opacity: 1; } to { opacity: 0; } }
88
+ @keyframes fg-drawer-slide-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }
89
+ @keyframes fg-drawer-slide-out { from { transform: translateX(0); } to { transform: translateX(-100%); } }
90
+
91
+ :root.high-contrast .fg-drawer__content {
92
+ border-width: 2px;
93
+ border-color: var(--color-border);
94
+ }
95
+
96
+ @media (prefers-reduced-motion: reduce) {
97
+ .fg-drawer__overlay,
98
+ .fg-drawer__content { animation: none; }
99
+ }
100
+ }
@@ -0,0 +1,52 @@
1
+ @layer components {
2
+ .fg-tabs {
3
+ min-width: 0;
4
+ font-family: var(--font-ui);
5
+ font-size: var(--text-md);
6
+ line-height: var(--leading);
7
+ }
8
+
9
+ .fg-tabs__list {
10
+ display: flex;
11
+ max-width: 100%;
12
+ overflow-x: auto;
13
+ border-bottom: 1px solid var(--color-border-subtle);
14
+ }
15
+
16
+ .fg-tabs__tab {
17
+ flex: none;
18
+ min-height: var(--control-md);
19
+ padding: var(--space-2) var(--space-3);
20
+ border: 0;
21
+ border-bottom: 2px solid transparent;
22
+ color: var(--color-muted);
23
+ background: transparent;
24
+ font: inherit;
25
+ white-space: nowrap;
26
+ cursor: pointer;
27
+ }
28
+
29
+ .fg-tabs__tab:hover { color: var(--color-text); }
30
+ .fg-tabs__tab[aria-selected="true"] {
31
+ border-bottom-color: var(--color-link);
32
+ color: var(--color-text);
33
+ font-weight: var(--weight-medium);
34
+ }
35
+
36
+ .fg-tabs__panel { min-width: 0; }
37
+ .fg-tabs__panel[hidden], .fg-tabs__heading[hidden] { display: none; }
38
+ .fg-tabs__heading {
39
+ margin-block: var(--space-4) var(--space-2);
40
+ font-size: var(--text-md);
41
+ font-weight: var(--weight-medium);
42
+ }
43
+
44
+ .fg-tabs__tab:focus-visible, .fg-tabs__panel:focus-visible {
45
+ outline: 2px solid var(--color-focus);
46
+ outline-offset: -2px;
47
+ }
48
+
49
+ @media (max-width: 640px) {
50
+ .fg-tabs__tab { min-height: var(--control-lg); }
51
+ }
52
+ }
@@ -1,3 +1,6 @@
1
+ @import './components/drawer.css';
2
+ @import './components/tabs.css';
3
+
1
4
  @layer components {
2
5
  /* Package-owned typography keeps controls consistent across consuming apps. */
3
6
  .fg-button, .fg-input, .fg-select, .fg-checkbox, .fg-field,
@@ -34,7 +37,10 @@
34
37
  .fg-code-block__copy {
35
38
  display: inline-flex;
36
39
  align-items: center;
40
+ justify-content: center;
37
41
  gap: 0.4rem;
42
+ min-width: var(--control-lg);
43
+ min-height: var(--control-lg);
38
44
  padding: 0.25rem 0.4rem;
39
45
  border: 0;
40
46
  border-radius: 0.25rem;