@filipgutica/ui 1.1.0 → 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
@@ -1,7 +1,11 @@
1
1
  # @filipgutica/ui
2
2
 
3
- A compact Vue 3.5+ design system built on [Reka UI](https://reka-ui.com), with
4
- Tailwind CSS 4 tokens and safe VS Code/Open VSX color-theme normalization.
3
+ Compact Vue 3.5+ components built on [Reka UI](https://reka-ui.com), with
4
+ Tailwind CSS 4 and VS Code theme support.
5
+
6
+ [Docs and playground](https://filipgutica.github.io/ui/) ·
7
+ [GitHub](https://github.com/filipgutica/ui) ·
8
+ [npm](https://www.npmjs.com/package/@filipgutica/ui)
5
9
 
6
10
  ## Install
7
11
 
@@ -9,7 +13,7 @@ Tailwind CSS 4 tokens and safe VS Code/Open VSX color-theme normalization.
9
13
  pnpm add @filipgutica/ui
10
14
  ```
11
15
 
12
- Import Tailwind and the shared theme files from your application stylesheet:
16
+ Import the styles from your application stylesheet:
13
17
 
14
18
  ```css
15
19
  @import "tailwindcss";
@@ -17,193 +21,76 @@ Import Tailwind and the shared theme files from your application stylesheet:
17
21
  @import "@filipgutica/ui/components.css";
18
22
  ```
19
23
 
20
- The theme exposes Tailwind utilities such as `bg-ui-bg`, `bg-ui-surface`,
21
- `text-ui-text`, `text-ui-muted`, `border-ui-border`, and `text-ui-error`.
22
-
23
- ## Vue components
24
-
25
- The first application-focused set includes:
26
-
27
- - `UiButton` with primary, secondary, ghost, danger, and text variants
28
- - `UiInput`, `UiSelect`, and `UiField` for consistent form controls
29
- - `UiCheckbox`, `UiSlider`, `UiRadioCard`, and `UiRadioCardGroup` for user input
30
- - `UiBadge`, `UiAlert`, and `UiProgress` for state and feedback
31
- - `UiCard` for structured content, actions, and footer metadata
32
- - `UiCodeBlock` for copyable, theme-aware source snippets
33
- - `UiDialog`, built on Reka UI, for accessible modal interaction
34
-
35
- Place `UiInput` or `UiSelect` inside `UiField`. The field supplies the control ID
36
- and automatically connects its description or error through the corresponding
37
- ARIA relationship.
24
+ ## Use
38
25
 
39
26
  ```vue
27
+ <script setup lang="ts">
28
+ import { UiButton } from '@filipgutica/ui'
29
+ </script>
30
+
40
31
  <template>
41
32
  <UiButton variant="secondary" size="sm">
42
- Inspect evidence
33
+ Save changes
43
34
  </UiButton>
44
35
  </template>
45
-
46
- <script setup lang="ts">
47
- import { UiButton } from '@filipgutica/ui'
48
- </script>
49
36
  ```
50
37
 
51
- `UiSelect` retains native form and keyboard behavior. Browsers with
52
- `appearance: base-select` support show a themed menu; other browsers use their
53
- native menu.
38
+ The [component docs](https://filipgutica.github.io/ui/#/components/button)
39
+ cover examples, props, slots, and events. Place inputs and selects inside
40
+ `UiField` to connect labels, descriptions, and errors automatically.
54
41
 
55
- `UiButton`, `UiInput`, `UiSelect`, `UiCheckbox`, and `UiSlider` share
56
- `size="sm | md | lg"`
57
- and default to the tighter `md` density. Existing `UiButton` values remain
58
- compatible: `compact` retains its former compact treatment and `default` maps
59
- to `lg`.
42
+ ## Page navigation
60
43
 
61
- `UiCodeBlock` accepts source text through `code`, shows a copy action by default,
62
- and highlights the selected language using the active theme. It can display a
63
- title, line numbers, or wrapped lines; unknown languages stay readable as plain text:
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.
64
49
 
65
- ```vue
66
- <UiCodeBlock
67
- title="Example.vue"
68
- language="vue"
69
- :code="snippet"
70
- :line-numbers="true"
71
- />
72
- ```
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.
73
55
 
74
- Highlighting supports Vue, JavaScript (`js`), TypeScript (`ts`), HTML, CSS,
75
- JSON, and Bash (`sh`/`shell`). Imported TextMate `tokenColors` are normalized
76
- into a shared syntax palette; language-specific overrides and font styles are
77
- not reproduced exactly. Themes saved before syntax support should be re-imported
78
- to recover their syntax colors. Existing saved themes remain usable with themed fallbacks.
79
-
80
- `UiSurface` remains exported for compatibility. New structured containers
81
- should use `UiCard`.
82
-
83
- ## Design conventions
84
-
85
- Use a compact system-font hierarchy: 14px for UI body text, 13px for
86
- secondary text, and 18px for dialog titles. Treat Open VSX/VS Code themes as
87
- the color authority, and use semantic tokens and package components instead of
88
- hard-coded provider color keys.
89
- Typography is package-owned through `--font-ui` and `--font-mono`; override
90
- these once at the application root rather than restyling each component.
91
-
92
- - Use shared control sizes `sm`, `md`, and `lg` (32px, 36px, and 44px); the
93
- default is `md`. Keep layout on a 4px rhythm, with 12px, 16px, and 24px as
94
- common gaps and insets.
95
- - Choose `UiCard` for structured content and optional actions or footer data.
96
- Choose `UiRadioCardGroup` with `UiRadioCard` for one-of-many rich options;
97
- choose `UiDialog` for modal tasks that need focus management.
98
- - Use one primary action per task, secondary buttons for supporting actions,
99
- and danger styling only for destructive actions. Use `UiBadge` for a short
100
- status, `UiAlert` for feedback, and `UiCodeBlock` for source code.
101
- - Put `UiInput` and `UiSelect` in `UiField` with a visible label. Supply help
102
- text or an error when needed so the field exposes the matching ARIA relationship.
103
- - Select menus use the same surface, type, radius, and spacing tokens as other
104
- controls. Keep option rows compact, show a checkmark for the selected value, and
105
- use quiet hover fills with visible keyboard focus.
106
- - Use raised surfaces and subtle borders for standalone cards. Align title, content,
107
- and footer insets; use a quiet divider for footer metadata and avoid card shadows.
108
- - Keep borders quiet and skip decorative grids. Preserve visible
109
- `:focus-visible` states, use 44px targets for touch-oriented actions, and provide
110
- an accessible name for every interactive control.
111
-
112
- ## VS Code themes
113
-
114
- The browser-safe theme entry point parses VS Code JSON/JSONC and maps supported
115
- workbench colors into the package's finite semantic token contract:
56
+ Track page sections from a component's setup function:
116
57
 
117
58
  ```ts
118
- import { applySemanticTheme, parseVsCodeTheme } from '@filipgutica/ui/theme'
59
+ import { useActiveSection } from '@filipgutica/ui'
119
60
 
120
- const theme = parseVsCodeTheme({ source, fileName })
121
- applySemanticTheme({ root: document.documentElement, theme })
61
+ const activeSection = useActiveSection({
62
+ targetIds: ['overview', 'install', 'commands'],
63
+ })
122
64
  ```
123
65
 
124
- The Node-only Open VSX entry point searches and imports theme extensions without
125
- executing extension code:
126
-
127
- ```ts
128
- import { createOpenVsxThemeService } from '@filipgutica/ui/open-vsx'
129
-
130
- const themes = await createOpenVsxThemeService().search('Catppuccin')
131
- ```
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.
132
74
 
133
- Open VSX packages are untrusted input. The importer restricts origins, validates
134
- identifiers and archive paths, caps downloaded and expanded sizes, checks the
135
- advertised SHA-256 checksum, and retains only normalized colors. Consumers that
136
- redistribute theme assets remain responsible for their licenses.
75
+ ## Themes
137
76
 
138
- ## Boundaries
77
+ Import a VS Code JSON or JSONC theme in the browser:
139
78
 
140
- - Open VSX themes control color and light/dark/high-contrast appearance only.
141
- - Typography, spacing, radius, motion, and component structure remain owned by
142
- this package so applications keep a consistent design language.
143
- - `@filipgutica/ui/open-vsx` is Node-only. Do not import it into browser bundles.
144
-
145
- ## Docs and sandbox
146
-
147
- Start the component docs and sandbox:
148
-
149
- ```sh
150
- pnpm docs:dev
151
- ```
152
-
153
- Build the provider-neutral static assets:
154
-
155
- ```sh
156
- pnpm docs:build
157
- ```
158
-
159
- The docs use hash routes and relative asset paths, so the static assets work
160
- under a repository path without rewrite rules.
161
- The local Vite server also provides the same-origin `/api/open-vsx` endpoint.
162
- The theme picker uses this endpoint to search and import Open VSX themes.
163
-
164
- ### GitHub Pages
165
-
166
- The GitHub Pages site URL is [filipgutica.github.io/ui](https://filipgutica.github.io/ui/).
167
- The `docs.yml` workflow builds and deploys `docs-dist` on every push to `main`.
168
- It can also be started manually from the Actions tab. In the repository's
169
- **Settings → Pages**, select **GitHub Actions** as the build and deployment source.
170
-
171
- GitHub Pages serves static files and cannot run the Open VSX API. The workflow
172
- sets `VITE_STATIC_HOST=true` to disable live Open VSX search and imports;
173
- built-in themes and local theme-file imports remain available. To build the
174
- same static configuration locally:
79
+ ```ts
80
+ import { applySemanticTheme, parseVsCodeTheme } from '@filipgutica/ui/theme'
175
81
 
176
- ```sh
177
- VITE_STATIC_HOST=true pnpm docs:build
82
+ const theme = parseVsCodeTheme({ source, fileName })
83
+ applySemanticTheme({ root: document.documentElement, theme })
178
84
  ```
179
85
 
180
- Other production hosts must provide `/api/open-vsx` for live Open VSX imports.
181
- Netlify and Amplify can use a serverless adapter.
86
+ Themes supply colors; the library keeps typography, spacing, and component
87
+ structure consistent. Open VSX search and imports are available through
88
+ `@filipgutica/ui/open-vsx`, which must run on a Node server.
182
89
 
183
90
  ## Development
184
91
 
185
92
  ```sh
186
93
  pnpm install
94
+ pnpm docs:dev
187
95
  pnpm verify
188
96
  ```
189
-
190
- Commits follow the Conventional Commits format. Releases run from `main`:
191
-
192
- - `fix:` publishes a patch release
193
- - `feat:` publishes a minor release
194
- - `feat!:` publishes a major release
195
- - `BREAKING CHANGE:` or `[BREAKING CHANGE]` anywhere in a commit body publishes
196
- a major release
197
- - the same markers anywhere in an associated merged PR body publish a major
198
- release
199
-
200
- The `release.yml` workflow runs on every push to `main`. When semantic-release
201
- finds qualifying commits, it publishes the next version to npm, creates the
202
- matching `v<version>` tag, and creates a GitHub Release. Configure an `NPM_TOKEN`
203
- Actions secret with permission to publish `@filipgutica/ui`; contributors do not
204
- publish locally or add a token to the repository.
205
-
206
- Before the first token-backed release, remove the package's existing trusted
207
- publisher in npm. The workflow retains `id-token: write` only for the provenance
208
- attestations enabled in `package.json`; npm registry authentication comes from
209
- the `NPM_TOKEN` secret.
@@ -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.0",
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;