@sybilion/uilib 1.2.12 → 1.2.14

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.
Files changed (35) hide show
  1. package/dist/esm/components/ui/Page/PageContent/PageContent.js +2 -2
  2. package/dist/esm/components/ui/Page/PageContent/PageContent.styl.js +2 -2
  3. package/dist/esm/components/ui/Page/PageFooter/PageFooter.js +23 -2
  4. package/dist/esm/components/ui/Page/PageFooter/PageFooter.styl.js +2 -2
  5. package/dist/esm/components/ui/Tabs/Tabs.js +2 -2
  6. package/dist/esm/components/ui/Tabs/Tabs.styl.js +2 -2
  7. package/dist/esm/hooks/index.js +1 -0
  8. package/dist/esm/index.js +1 -0
  9. package/dist/esm/types/src/components/ui/Page/PageContent/PageContent.d.ts +2 -1
  10. package/dist/esm/types/src/components/ui/Page/PageFooter/PageFooter.d.ts +13 -1
  11. package/dist/esm/types/src/components/ui/Page/index.d.ts +1 -1
  12. package/dist/esm/types/src/components/ui/Tabs/Tabs.d.ts +2 -1
  13. package/dist/esm/types/src/docs/pages/PageFooterPage.d.ts +1 -0
  14. package/dist/esm/types/src/docs/pages/PageTabsPage.d.ts +1 -0
  15. package/dist/esm/types/src/hooks/index.d.ts +1 -0
  16. package/dist/esm/types/src/index.d.ts +1 -0
  17. package/package.json +6 -1
  18. package/src/components/ui/Page/PageContent/PageContent.styl +3 -1
  19. package/src/components/ui/Page/PageContent/PageContent.styl.d.ts +1 -0
  20. package/src/components/ui/Page/PageContent/PageContent.tsx +3 -1
  21. package/src/components/ui/Page/PageFooter/PageFooter.styl +21 -1
  22. package/src/components/ui/Page/PageFooter/PageFooter.styl.d.ts +3 -0
  23. package/src/components/ui/Page/PageFooter/PageFooter.tsx +57 -13
  24. package/src/components/ui/Page/index.ts +4 -1
  25. package/src/components/ui/Tabs/Tabs.styl +38 -0
  26. package/src/components/ui/Tabs/Tabs.styl.d.ts +2 -0
  27. package/src/components/ui/Tabs/Tabs.tsx +8 -1
  28. package/src/docs/pages/PageFooterPage.styl +10 -0
  29. package/src/docs/pages/PageFooterPage.styl.d.ts +8 -0
  30. package/src/docs/pages/PageFooterPage.tsx +99 -0
  31. package/src/docs/pages/PageTabsPage.tsx +60 -0
  32. package/src/docs/pages/TabsPage.tsx +106 -10
  33. package/src/docs/registry.ts +12 -0
  34. package/src/hooks/index.ts +1 -0
  35. package/src/index.ts +1 -0
@@ -5,8 +5,8 @@ import S from './PageContent.styl.js';
5
5
  function PageContent({ className, children, variant = 'default', }) {
6
6
  return (jsx("div", { className: cn(S.root, S[`variant-${variant}`], className), children: children }));
7
7
  }
8
- function PageContentSection({ className, children, ...rest }) {
9
- return (jsx("div", { className: cn(S.section, className), ...rest, children: children }));
8
+ function PageContentSection({ className, children, grow = true, ...rest }) {
9
+ return (jsx("div", { className: cn(S.section, className, grow && S.grow), ...rest, children: children }));
10
10
  }
11
11
 
12
12
  export { PageContent, PageContentSection };
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.PageContent_root__caExB{max-width:var(--page-width);padding-bottom:var(--page-y-padding);width:100%}.PageContent_root__caExB,.PageContent_section__Wve-w{display:flex;flex:1;flex-direction:column;min-height:0}.PageContent_section__Wve-w{padding-left:var(--page-x-padding);padding-right:var(--page-x-padding)}";
4
- var S = {"root":"PageContent_root__caExB","section":"PageContent_section__Wve-w"};
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.PageContent_root__caExB{display:flex;flex:1;flex-direction:column;max-width:var(--page-width);min-height:0;padding-bottom:var(--page-y-padding);width:100%}.PageContent_section__Wve-w{display:flex;flex-direction:column;min-height:0;padding-left:var(--page-x-padding);padding-right:var(--page-x-padding)}.PageContent_grow__Nkfqk{flex-grow:1}";
4
+ var S = {"root":"PageContent_root__caExB","section":"PageContent_section__Wve-w","grow":"PageContent_grow__Nkfqk"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -3,8 +3,29 @@ import { Link } from 'react-router-dom';
3
3
  import { GlobeIcon, MailIcon } from 'lucide-react';
4
4
  import S from './PageFooter.styl.js';
5
5
 
6
- function PageFooter({ children, versionLink, versionLabel, homeTo = '/', brandText = 'Sybilion', logo, websiteHref = 'https://sybilion.com', mailHref = 'mailto:support@sybilion.com', copyrightText = '© 2026 Sybilion. All rights reserved.', }) {
7
- return (jsx("footer", { className: S.root, children: jsxs("div", { className: S.line, children: [jsxs("div", { className: S.logo, children: [jsxs(Link, { to: homeTo, children: [logo, "\u00A0", brandText] }), versionLink !== '' && (jsx("div", { className: S.version, children: jsx(Link, { to: versionLink, className: S.versionLink, children: versionLabel }) }))] }), children, jsxs("div", { className: S.copyright, children: [copyrightText, jsx(Link, { to: websiteHref, target: "_blank", rel: "noreferrer", "aria-label": "Visit Sybilion website", children: jsx(GlobeIcon, {}) }), jsx("a", { href: mailHref, "aria-label": "Contact support via email", children: jsx(MailIcon, {}) })] })] }) }));
6
+ function PageFooterLinkEl({ item }) {
7
+ const { label, url, ariaLabel } = item;
8
+ const openInNewTab = /^https?:\/\//i.test(url);
9
+ return (jsx("a", { href: url, className: S.link, ...(ariaLabel !== undefined ? { 'aria-label': ariaLabel } : {}), ...(openInNewTab ? { target: '_blank', rel: 'noreferrer' } : {}), children: label }));
10
+ }
11
+ function PageFooter({ children, versionLink, versionLabel, homeTo = '/', brandText = 'Sybilion', logo, websiteHref = 'https://sybilion.com', mailHref = 'mailto:support@sybilion.com', copyrightText = '© 2026 Sybilion. All rights reserved.', links: linksProp, }) {
12
+ const resolvedLinks = linksProp !== undefined
13
+ ? linksProp
14
+ : [
15
+ {
16
+ id: 'website',
17
+ label: jsx(GlobeIcon, { "aria-hidden": true }),
18
+ url: websiteHref,
19
+ ariaLabel: 'Visit Sybilion website',
20
+ },
21
+ {
22
+ id: 'email',
23
+ label: jsx(MailIcon, { "aria-hidden": true }),
24
+ url: mailHref,
25
+ ariaLabel: 'Contact support via email',
26
+ },
27
+ ];
28
+ return (jsx("footer", { className: S.root, children: jsxs("div", { className: S.line, children: [jsxs("div", { className: S.logo, children: [jsxs(Link, { to: homeTo, children: [logo, "\u00A0", brandText] }), versionLink !== '' && (jsx("div", { className: S.version, children: jsx(Link, { to: versionLink, className: S.versionLink, children: versionLabel }) }))] }), children, jsxs("div", { className: S.meta, children: [jsx("div", { className: S.copyright, children: copyrightText }), resolvedLinks.length > 0 && (jsx("nav", { className: S.links, "aria-label": "Footer links", children: resolvedLinks.map(item => (jsx(PageFooterLinkEl, { item: item }, item.id))) }))] })] }) }));
8
29
  }
9
30
 
10
31
  export { PageFooter };
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.PageFooter_root__TAfX6{display:flex;flex-direction:column;gap:6px;padding:var(--p-5) var(--p-10)}.PageFooter_line__OyGA-{align-items:center;display:flex;flex-wrap:wrap;justify-content:space-between}.PageFooter_line__OyGA-:last-child{min-height:55px}.PageFooter_copyright__4LFUP{align-items:center;display:flex;font-size:14px;gap:var(--p-4);position:relative}.PageFooter_copyright__4LFUP svg{color:var(--brand-color-400);height:20px;width:20px}.PageFooter_logo__B2SRg{align-items:center;display:flex;font-size:var(--text-2xl);position:relative}.PageFooter_logo__B2SRg a{align-items:center;color:var(--foreground);display:flex;gap:var(--p-2);text-decoration:none}.PageFooter_logo__B2SRg svg{height:24px;width:24px}.PageFooter_version__8pZXn{align-items:center;bottom:5px;color:var(--muted-foreground);display:flex;font-size:14px;left:calc(100% + 10px);opacity:.5;position:absolute;transition:opacity .3s ease-out;white-space:nowrap}.PageFooter_version__8pZXn:before{content:\"v\"}.PageFooter_versionLink__dILP5{color:inherit;text-decoration:none}.PageFooter_logo__B2SRg:hover .PageFooter_version__8pZXn{opacity:1}";
4
- var S = {"root":"PageFooter_root__TAfX6","line":"PageFooter_line__OyGA-","copyright":"PageFooter_copyright__4LFUP","logo":"PageFooter_logo__B2SRg","version":"PageFooter_version__8pZXn","versionLink":"PageFooter_versionLink__dILP5"};
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.PageFooter_root__TAfX6{display:flex;flex-direction:column;gap:6px;padding:var(--p-5) var(--p-10)}.PageFooter_line__OyGA-{align-items:center;display:flex;flex-wrap:wrap;justify-content:space-between}.PageFooter_line__OyGA-:last-child{min-height:55px}.PageFooter_meta__Gvenf{align-items:center;display:flex;flex-wrap:wrap;gap:var(--p-4)}.PageFooter_copyright__4LFUP{font-size:14px}.PageFooter_links__q5BXo{align-items:center;display:flex;flex-wrap:wrap;gap:var(--p-4)}.PageFooter_link__mel3N{align-items:center;color:var(--foreground);display:inline-flex;font-size:14px;gap:var(--p-2);text-decoration:none}.PageFooter_link__mel3N:hover{text-decoration:underline}.PageFooter_link__mel3N svg{color:var(--brand-color-400);flex-shrink:0;height:20px;width:20px}.PageFooter_logo__B2SRg{align-items:center;display:flex;font-size:var(--text-2xl);position:relative}.PageFooter_logo__B2SRg a{align-items:center;color:var(--foreground);display:flex;gap:var(--p-2);text-decoration:none}.PageFooter_logo__B2SRg svg{height:24px;width:24px}.PageFooter_version__8pZXn{align-items:center;bottom:5px;color:var(--muted-foreground);display:flex;font-size:14px;left:calc(100% + 10px);opacity:.5;position:absolute;transition:opacity .3s ease-out;white-space:nowrap}.PageFooter_version__8pZXn:before{content:\"v\"}.PageFooter_versionLink__dILP5{color:inherit;text-decoration:none}.PageFooter_logo__B2SRg:hover .PageFooter_version__8pZXn{opacity:1}";
4
+ var S = {"root":"PageFooter_root__TAfX6","line":"PageFooter_line__OyGA-","meta":"PageFooter_meta__Gvenf","copyright":"PageFooter_copyright__4LFUP","links":"PageFooter_links__q5BXo","link":"PageFooter_link__mel3N","logo":"PageFooter_logo__B2SRg","version":"PageFooter_version__8pZXn","versionLink":"PageFooter_versionLink__dILP5"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -3,8 +3,8 @@ import cn from 'classnames';
3
3
  import * as TabsPrimitive from '@radix-ui/react-tabs';
4
4
  import S from './Tabs.styl.js';
5
5
 
6
- function Tabs({ className, variant = 'button', darker = false, ...props }) {
7
- return (jsx(TabsPrimitive.Root, { "data-slot": "tabs", className: cn(S.root, S[`variant-${variant}`], darker && S.darker, className), ...props }));
6
+ function Tabs({ className, variant = 'button', size = 'md', darker = false, ...props }) {
7
+ return (jsx(TabsPrimitive.Root, { "data-slot": "tabs", className: cn(S.root, S[`variant-${variant}`], size === 'sm' && S.sizeSm, size === 'lg' && S.sizeLg, darker && S.darker, className), ...props }));
8
8
  }
9
9
  function TabsList({ className, fullWidth, darker, ...props }) {
10
10
  return (jsx(TabsPrimitive.List, { "data-slot": "tabs-list", className: cn(S.list, fullWidth && S.fullWidth, darker && S.darker, className), ...props }));
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = ".Tabs_root__cB9Au{display:flex;flex-direction:column;gap:.5rem}.Tabs_list__9Hs22{align-items:center;color:var(--muted-foreground);display:inline-flex;height:48px;justify-content:center;width:-moz-fit-content;width:fit-content}.Tabs_list__9Hs22.Tabs_fullWidth__Lj2Dd{width:100%}.Tabs_variant-link__xOMDg>.Tabs_list__9Hs22{background-color:transparent!important}.Tabs_variant-button__wtc8k .Tabs_list__9Hs22{border-radius:1.5rem;box-shadow:none;gap:.25rem;height:auto}.Tabs_trigger__uzdqH{align-items:center;border:1px solid transparent;border-radius:.375rem;color:var(--muted-foreground);cursor:pointer;display:inline-flex;flex:1;font-size:var(--text-sm);font-weight:500;gap:.375rem;height:calc(100% - 1px);justify-content:center;padding:.25rem .5rem;transition:color .2s,box-shadow .2s;white-space:nowrap}.Tabs_trigger__uzdqH:focus-visible{border-color:var(--ring);box-shadow:0 0 0 3px var(--ring-50);outline:1px solid var(--ring);outline-offset:0}.Tabs_trigger__uzdqH:disabled{opacity:.5;pointer-events:none}.Tabs_trigger__uzdqH[data-state=active]{background-color:var(--background);box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.Tabs_trigger__uzdqH:hover,.Tabs_trigger__uzdqH[data-state=active]{color:var(--foreground)!important}.Tabs_darker__l-R0I .Tabs_trigger__uzdqH{background-color:var(--muted)}.Tabs_variant-button__wtc8k .Tabs_list__9Hs22 .Tabs_trigger__uzdqH{border:none;border-radius:var(--p-4);box-shadow:none;color:var(--muted-foreground);flex-shrink:0;font-weight:600;padding:var(--p-1) var(--p-3);transition:all .15s ease-in-out}.Tabs_variant-button__wtc8k .Tabs_list__9Hs22 .Tabs_trigger__uzdqH[data-state=active]{border-radius:var(--p-6);box-shadow:0 1px 2px 0 rgba(0,0,0,.05);color:var(--foreground)}.Tabs_variant-link__xOMDg>.Tabs_list__9Hs22 .Tabs_trigger__uzdqH{background-color:transparent!important;border:0!important;border-radius:0!important;box-shadow:0!important;font-size:16px;padding:12px 16px}.Tabs_variant-link__xOMDg>.Tabs_list__9Hs22 .Tabs_trigger__uzdqH[data-state=active]{box-shadow:inset 0 -3px 0 -1px var(--sb-cyan-400)!important}.Tabs_trigger__uzdqH svg{flex-shrink:0;pointer-events:none}.Tabs_trigger__uzdqH svg:not([class*=size-]){height:1rem;width:1rem}.dark .Tabs_trigger__uzdqH{color:var(--muted-foreground)}.dark .Tabs_trigger__uzdqH[data-state=active]{background-color:var(--input-30);border-color:var(--input);color:var(--foreground)}.Tabs_content__K27Vl{flex:1;outline:none}";
4
- var S = {"root":"Tabs_root__cB9Au","list":"Tabs_list__9Hs22","fullWidth":"Tabs_fullWidth__Lj2Dd","variant-link":"Tabs_variant-link__xOMDg","variant-button":"Tabs_variant-button__wtc8k","trigger":"Tabs_trigger__uzdqH","darker":"Tabs_darker__l-R0I","content":"Tabs_content__K27Vl"};
3
+ var css_248z = ".Tabs_root__cB9Au{display:flex;flex-direction:column;gap:.5rem}.Tabs_list__9Hs22{align-items:center;color:var(--muted-foreground);display:inline-flex;height:48px;justify-content:center;width:-moz-fit-content;width:fit-content}.Tabs_list__9Hs22.Tabs_fullWidth__Lj2Dd{width:100%}.Tabs_variant-link__xOMDg>.Tabs_list__9Hs22{background-color:transparent!important}.Tabs_variant-button__wtc8k .Tabs_list__9Hs22{border-radius:1.5rem;box-shadow:none;gap:.25rem;height:auto}.Tabs_trigger__uzdqH{align-items:center;border:1px solid transparent;border-radius:.375rem;color:var(--muted-foreground);cursor:pointer;display:inline-flex;flex:1;font-size:var(--text-sm);font-weight:500;gap:.375rem;height:calc(100% - 1px);justify-content:center;padding:.25rem .5rem;transition:color .2s,box-shadow .2s;white-space:nowrap}.Tabs_trigger__uzdqH:focus-visible{border-color:var(--ring);box-shadow:0 0 0 3px var(--ring-50);outline:1px solid var(--ring);outline-offset:0}.Tabs_trigger__uzdqH:disabled{opacity:.5;pointer-events:none}.Tabs_trigger__uzdqH[data-state=active]{background-color:var(--background);box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.Tabs_trigger__uzdqH:hover,.Tabs_trigger__uzdqH[data-state=active]{color:var(--foreground)!important}.Tabs_darker__l-R0I .Tabs_trigger__uzdqH{background-color:var(--muted)}.Tabs_variant-button__wtc8k .Tabs_list__9Hs22 .Tabs_trigger__uzdqH{border:none;border-radius:var(--p-4);box-shadow:none;color:var(--muted-foreground);flex-shrink:0;font-weight:600;padding:var(--p-1) var(--p-3);transition:all .15s ease-in-out}.Tabs_variant-button__wtc8k .Tabs_list__9Hs22 .Tabs_trigger__uzdqH[data-state=active]{border-radius:var(--p-6);box-shadow:0 1px 2px 0 rgba(0,0,0,.05);color:var(--foreground)}.Tabs_variant-link__xOMDg>.Tabs_list__9Hs22 .Tabs_trigger__uzdqH{background-color:transparent!important;border:0!important;border-radius:0!important;box-shadow:0!important;font-size:16px;padding:12px 16px}.Tabs_variant-link__xOMDg>.Tabs_list__9Hs22 .Tabs_trigger__uzdqH[data-state=active]{box-shadow:inset 0 -3px 0 -1px var(--sb-cyan-400)!important}.Tabs_trigger__uzdqH svg{flex-shrink:0;pointer-events:none}.Tabs_trigger__uzdqH svg:not([class*=size-]){height:1rem;width:1rem}.dark .Tabs_trigger__uzdqH{color:var(--muted-foreground)}.dark .Tabs_trigger__uzdqH[data-state=active]{background-color:var(--input-30);border-color:var(--input);color:var(--foreground)}.Tabs_content__K27Vl{flex:1;outline:none}.Tabs_root__cB9Au.Tabs_variant-button__wtc8k.Tabs_sizeSm__gfpc6 .Tabs_list__9Hs22 .Tabs_trigger__uzdqH{font-size:var(--text-xs);padding:var(--p-1) var(--p-2)}.Tabs_root__cB9Au.Tabs_variant-button__wtc8k.Tabs_sizeSm__gfpc6 .Tabs_list__9Hs22 .Tabs_trigger__uzdqH[data-state=active]{border-radius:var(--p-4)}.Tabs_root__cB9Au.Tabs_variant-button__wtc8k.Tabs_sizeLg__CcRFZ .Tabs_list__9Hs22 .Tabs_trigger__uzdqH{font-size:var(--text-base);padding:var(--p-2) var(--p-4)}.Tabs_root__cB9Au.Tabs_variant-button__wtc8k.Tabs_sizeLg__CcRFZ .Tabs_list__9Hs22 .Tabs_trigger__uzdqH[data-state=active]{border-radius:var(--p-8)}.Tabs_root__cB9Au.Tabs_variant-link__xOMDg.Tabs_sizeSm__gfpc6>.Tabs_list__9Hs22{height:auto;min-height:36px}.Tabs_root__cB9Au.Tabs_variant-link__xOMDg.Tabs_sizeSm__gfpc6>.Tabs_list__9Hs22 .Tabs_trigger__uzdqH{font-size:14px;padding:8px 12px}.Tabs_root__cB9Au.Tabs_variant-link__xOMDg.Tabs_sizeLg__CcRFZ>.Tabs_list__9Hs22{height:auto;min-height:56px}.Tabs_root__cB9Au.Tabs_variant-link__xOMDg.Tabs_sizeLg__CcRFZ>.Tabs_list__9Hs22 .Tabs_trigger__uzdqH{font-size:18px;padding:16px 24px}";
4
+ var S = {"root":"Tabs_root__cB9Au","list":"Tabs_list__9Hs22","fullWidth":"Tabs_fullWidth__Lj2Dd","variant-link":"Tabs_variant-link__xOMDg","variant-button":"Tabs_variant-button__wtc8k","trigger":"Tabs_trigger__uzdqH","darker":"Tabs_darker__l-R0I","content":"Tabs_content__K27Vl","sizeSm":"Tabs_sizeSm__gfpc6","sizeLg":"Tabs_sizeLg__CcRFZ"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -0,0 +1 @@
1
+ export { useIsMobile } from './useIsMobile.js';
package/dist/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { useIsMobile } from './hooks/useIsMobile.js';
1
2
  export { ThemeProvider, useTheme } from './contexts/theme-context.js';
2
3
  export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme.js';
3
4
  export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
@@ -3,7 +3,8 @@ export declare function PageContent({ className, children, variant, }: {
3
3
  children: React.ReactNode;
4
4
  variant?: 'default' | 'clean';
5
5
  }): import("react/jsx-runtime").JSX.Element;
6
- export declare function PageContentSection({ className, children, ...rest }: {
6
+ export declare function PageContentSection({ className, children, grow, ...rest }: {
7
7
  className?: string;
8
8
  children: React.ReactNode;
9
+ grow?: boolean;
9
10
  } & Omit<React.ComponentProps<'div'>, 'className' | 'children'>): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,11 @@
1
1
  import type { ReactNode } from 'react';
2
+ export interface PageFooterLinkItem {
3
+ id: string;
4
+ label: ReactNode;
5
+ url: string;
6
+ /** For icon-only or non-text labels (screen readers). */
7
+ ariaLabel?: string;
8
+ }
2
9
  export interface PageFooterProps {
3
10
  logo?: ReactNode;
4
11
  /** Rendered between logo block and copyright (e.g. debug UI from the app). */
@@ -12,5 +19,10 @@ export interface PageFooterProps {
12
19
  websiteHref?: string;
13
20
  mailHref?: string;
14
21
  copyrightText?: string;
22
+ /**
23
+ * Footer links. When set (including `[]`), replaces default website + email icons.
24
+ * Omit to keep icon links from `websiteHref` / `mailHref`.
25
+ */
26
+ links?: PageFooterLinkItem[];
15
27
  }
16
- export declare function PageFooter({ children, versionLink, versionLabel, homeTo, brandText, logo, websiteHref, mailHref, copyrightText, }: PageFooterProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function PageFooter({ children, versionLink, versionLabel, homeTo, brandText, logo, websiteHref, mailHref, copyrightText, links: linksProp, }: PageFooterProps): import("react/jsx-runtime").JSX.Element;
@@ -6,7 +6,7 @@ export { PageScroll } from './PageScroll/PageScroll';
6
6
  export { PageXScroll } from './PageXScroll/PageXScroll';
7
7
  export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs';
8
8
  export { PageFooter } from './PageFooter/PageFooter';
9
- export type { PageFooterProps } from './PageFooter/PageFooter';
9
+ export type { PageFooterLinkItem, PageFooterProps, } from './PageFooter/PageFooter';
10
10
  export { PageTabs } from './PageTabs/PageTabs';
11
11
  export { PageColumns } from './PageColumns/PageColumns';
12
12
  export { SectionHeader } from './SectionHeader';
@@ -1,6 +1,7 @@
1
1
  import { TabsContentProps, TabsListProps, TabsProps, TabsTriggerProps } from './Tabs.types';
2
- declare function Tabs({ className, variant, darker, ...props }: TabsProps & {
2
+ declare function Tabs({ className, variant, size, darker, ...props }: TabsProps & {
3
3
  variant?: 'button' | 'link';
4
+ size?: 'sm' | 'md' | 'lg';
4
5
  darker?: boolean;
5
6
  }): import("react/jsx-runtime").JSX.Element;
6
7
  declare function TabsList({ className, fullWidth, darker, ...props }: TabsListProps & {
@@ -0,0 +1 @@
1
+ export default function PageFooterPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export default function PageTabsPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export { useIsMobile } from './useIsMobile';
@@ -1,3 +1,4 @@
1
+ export { useIsMobile } from './hooks/useIsMobile';
1
2
  export * from './contexts/theme-context';
2
3
  export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
3
4
  export * from './sybilion-auth';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -29,6 +29,11 @@
29
29
  "import": "./src/index.ts",
30
30
  "default": "./src/index.ts"
31
31
  },
32
+ "./hooks": {
33
+ "types": "./dist/esm/types/src/hooks/index.d.ts",
34
+ "import": "./dist/esm/hooks/index.js",
35
+ "default": "./dist/esm/hooks/index.js"
36
+ },
32
37
  "./src/*": "./src/*",
33
38
  "./vite-standalone-dev": {
34
39
  "types": "./dist/standalone/vite-sybilion-standalone-dev.d.ts",
@@ -13,5 +13,7 @@
13
13
  pageXPadding()
14
14
  display flex
15
15
  flex-direction column
16
- flex 1
17
16
  min-height 0
17
+
18
+ .grow
19
+ flex-grow 1
@@ -1,6 +1,7 @@
1
1
  // This file is automatically generated.
2
2
  // Please do not change this file!
3
3
  interface CssExports {
4
+ 'grow': string;
4
5
  'root': string;
5
6
  'section': string;
6
7
  }
@@ -21,13 +21,15 @@ export function PageContent({
21
21
  export function PageContentSection({
22
22
  className,
23
23
  children,
24
+ grow = true,
24
25
  ...rest
25
26
  }: {
26
27
  className?: string;
27
28
  children: React.ReactNode;
29
+ grow?: boolean;
28
30
  } & Omit<React.ComponentProps<'div'>, 'className' | 'children'>) {
29
31
  return (
30
- <div className={cn(S.section, className)} {...rest}>
32
+ <div className={cn(S.section, className, grow && S.grow)} {...rest}>
31
33
  {children}
32
34
  </div>
33
35
  );
@@ -15,14 +15,34 @@
15
15
  &:last-child
16
16
  min-height 55px
17
17
 
18
+ .meta
19
+ display flex
20
+ flex-wrap wrap
21
+ align-items center
22
+ gap var(--p-4)
23
+
18
24
  .copyright
19
- position relative
25
+ font-size 14px
26
+
27
+ .links
20
28
  display flex
29
+ flex-wrap wrap
21
30
  align-items center
22
31
  gap var(--p-4)
32
+
33
+ .link
34
+ display inline-flex
35
+ align-items center
36
+ gap var(--p-2)
23
37
  font-size 14px
38
+ color var(--foreground)
39
+ text-decoration none
40
+
41
+ &:hover
42
+ text-decoration underline
24
43
 
25
44
  svg
45
+ flex-shrink 0
26
46
  color var(--brand-color-400)
27
47
  height 20px
28
48
  width 20px
@@ -3,7 +3,10 @@
3
3
  interface CssExports {
4
4
  'copyright': string;
5
5
  'line': string;
6
+ 'link': string;
7
+ 'links': string;
6
8
  'logo': string;
9
+ 'meta': string;
7
10
  'root': string;
8
11
  'version': string;
9
12
  'versionLink': string;
@@ -5,6 +5,14 @@ import { GlobeIcon, MailIcon } from 'lucide-react';
5
5
 
6
6
  import S from './PageFooter.styl';
7
7
 
8
+ export interface PageFooterLinkItem {
9
+ id: string;
10
+ label: ReactNode;
11
+ url: string;
12
+ /** For icon-only or non-text labels (screen readers). */
13
+ ariaLabel?: string;
14
+ }
15
+
8
16
  export interface PageFooterProps {
9
17
  logo?: ReactNode;
10
18
  /** Rendered between logo block and copyright (e.g. debug UI from the app). */
@@ -18,6 +26,27 @@ export interface PageFooterProps {
18
26
  websiteHref?: string;
19
27
  mailHref?: string;
20
28
  copyrightText?: string;
29
+ /**
30
+ * Footer links. When set (including `[]`), replaces default website + email icons.
31
+ * Omit to keep icon links from `websiteHref` / `mailHref`.
32
+ */
33
+ links?: PageFooterLinkItem[];
34
+ }
35
+
36
+ function PageFooterLinkEl({ item }: { item: PageFooterLinkItem }): ReactNode {
37
+ const { label, url, ariaLabel } = item;
38
+ const openInNewTab = /^https?:\/\//i.test(url);
39
+
40
+ return (
41
+ <a
42
+ href={url}
43
+ className={S.link}
44
+ {...(ariaLabel !== undefined ? { 'aria-label': ariaLabel } : {})}
45
+ {...(openInNewTab ? { target: '_blank', rel: 'noreferrer' } : {})}
46
+ >
47
+ {label}
48
+ </a>
49
+ );
21
50
  }
22
51
 
23
52
  export function PageFooter({
@@ -30,7 +59,26 @@ export function PageFooter({
30
59
  websiteHref = 'https://sybilion.com',
31
60
  mailHref = 'mailto:support@sybilion.com',
32
61
  copyrightText = '© 2026 Sybilion. All rights reserved.',
62
+ links: linksProp,
33
63
  }: PageFooterProps) {
64
+ const resolvedLinks: PageFooterLinkItem[] =
65
+ linksProp !== undefined
66
+ ? linksProp
67
+ : [
68
+ {
69
+ id: 'website',
70
+ label: <GlobeIcon aria-hidden />,
71
+ url: websiteHref,
72
+ ariaLabel: 'Visit Sybilion website',
73
+ },
74
+ {
75
+ id: 'email',
76
+ label: <MailIcon aria-hidden />,
77
+ url: mailHref,
78
+ ariaLabel: 'Contact support via email',
79
+ },
80
+ ];
81
+
34
82
  return (
35
83
  <footer className={S.root}>
36
84
  <div className={S.line}>
@@ -48,19 +96,15 @@ export function PageFooter({
48
96
  )}
49
97
  </div>
50
98
  {children}
51
- <div className={S.copyright}>
52
- {copyrightText}
53
- <Link
54
- to={websiteHref}
55
- target="_blank"
56
- rel="noreferrer"
57
- aria-label="Visit Sybilion website"
58
- >
59
- <GlobeIcon />
60
- </Link>
61
- <a href={mailHref} aria-label="Contact support via email">
62
- <MailIcon />
63
- </a>
99
+ <div className={S.meta}>
100
+ <div className={S.copyright}>{copyrightText}</div>
101
+ {resolvedLinks.length > 0 && (
102
+ <nav className={S.links} aria-label="Footer links">
103
+ {resolvedLinks.map(item => (
104
+ <PageFooterLinkEl key={item.id} item={item} />
105
+ ))}
106
+ </nav>
107
+ )}
64
108
  </div>
65
109
  </div>
66
110
  </footer>
@@ -11,7 +11,10 @@ export { PageScroll } from './PageScroll/PageScroll';
11
11
  export { PageXScroll } from './PageXScroll/PageXScroll';
12
12
  export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs';
13
13
  export { PageFooter } from './PageFooter/PageFooter';
14
- export type { PageFooterProps } from './PageFooter/PageFooter';
14
+ export type {
15
+ PageFooterLinkItem,
16
+ PageFooterProps,
17
+ } from './PageFooter/PageFooter';
15
18
  export { PageTabs } from './PageTabs/PageTabs';
16
19
  export { PageColumns } from './PageColumns/PageColumns';
17
20
  export { SectionHeader } from './SectionHeader';
@@ -115,3 +115,41 @@
115
115
  flex 1
116
116
  outline none
117
117
 
118
+ // Sizes (md = default styles above)
119
+ .root
120
+ &.variant-button
121
+ &.sizeSm
122
+ .list .trigger
123
+ padding var(--p-1) var(--p-2)
124
+ font-size var(--text-xs)
125
+
126
+ &[data-state="active"]
127
+ border-radius var(--p-4)
128
+
129
+ &.sizeLg
130
+ .list .trigger
131
+ padding var(--p-2) var(--p-4)
132
+ font-size var(--text-base)
133
+
134
+ &[data-state="active"]
135
+ border-radius var(--p-8)
136
+
137
+ &.variant-link
138
+ &.sizeSm
139
+ > .list
140
+ height auto
141
+ min-height 36px
142
+
143
+ > .list .trigger
144
+ padding 8px 12px
145
+ font-size 14px
146
+
147
+ &.sizeLg
148
+ > .list
149
+ height auto
150
+ min-height 56px
151
+
152
+ > .list .trigger
153
+ padding 16px 24px
154
+ font-size 18px
155
+
@@ -6,6 +6,8 @@ interface CssExports {
6
6
  'fullWidth': string;
7
7
  'list': string;
8
8
  'root': string;
9
+ 'sizeLg': string;
10
+ 'sizeSm': string;
9
11
  'trigger': string;
10
12
  'variant-button': string;
11
13
  'variant-link': string;
@@ -13,15 +13,22 @@ import {
13
13
  function Tabs({
14
14
  className,
15
15
  variant = 'button',
16
+ size = 'md',
16
17
  darker = false,
17
18
  ...props
18
- }: TabsProps & { variant?: 'button' | 'link'; darker?: boolean }) {
19
+ }: TabsProps & {
20
+ variant?: 'button' | 'link';
21
+ size?: 'sm' | 'md' | 'lg';
22
+ darker?: boolean;
23
+ }) {
19
24
  return (
20
25
  <TabsPrimitive.Root
21
26
  data-slot="tabs"
22
27
  className={cn(
23
28
  S.root,
24
29
  S[`variant-${variant}`],
30
+ size === 'sm' && S.sizeSm,
31
+ size === 'lg' && S.sizeLg,
25
32
  darker && S.darker,
26
33
  className,
27
34
  )}
@@ -0,0 +1,10 @@
1
+ @import 'lib/theme.styl'
2
+
3
+ .demo
4
+ margin-top var(--p-4)
5
+ border 1px solid var(--border)
6
+ border-radius var(--radius-md)
7
+ overflow hidden
8
+
9
+ .isMobile
10
+ max-width 560px
@@ -0,0 +1,8 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'demo': string;
5
+ 'isMobile': string;
6
+ }
7
+ export const cssExports: CssExports;
8
+ export default cssExports;
@@ -0,0 +1,99 @@
1
+ import { useState } from 'react';
2
+
3
+ import { Label } from '#uilib/components/ui/Label';
4
+ import {
5
+ PageContent,
6
+ PageContentSection,
7
+ PageFooter,
8
+ } from '#uilib/components/ui/Page';
9
+ import type { PageFooterLinkItem } from '#uilib/components/ui/Page/PageFooter/PageFooter';
10
+ import { Switch } from '#uilib/components/ui/Switch';
11
+ import { GlobeIcon, Hash, MailIcon, MessageCircle } from 'lucide-react';
12
+
13
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
14
+ import { DocsHeaderActions } from '../docsHeaderActions';
15
+ import S from './PageFooterPage.styl';
16
+
17
+ const DEMO_LINKS: PageFooterLinkItem[] = [
18
+ {
19
+ id: 'site',
20
+ label: (
21
+ <>
22
+ <GlobeIcon aria-hidden />
23
+ Website
24
+ </>
25
+ ),
26
+ url: 'https://sybilion.com',
27
+ },
28
+ {
29
+ id: 'email',
30
+ label: (
31
+ <>
32
+ <MailIcon aria-hidden />
33
+ Email
34
+ </>
35
+ ),
36
+ url: 'mailto:support@sybilion.com',
37
+ },
38
+ {
39
+ id: 'discord',
40
+ label: (
41
+ <>
42
+ <MessageCircle aria-hidden />
43
+ Discord
44
+ </>
45
+ ),
46
+ url: 'https://discord.com',
47
+ },
48
+ {
49
+ id: 'slack',
50
+ label: (
51
+ <>
52
+ <Hash aria-hidden />
53
+ Slack
54
+ </>
55
+ ),
56
+ url: 'https://slack.com',
57
+ },
58
+ ];
59
+
60
+ export default function PageFooterPage() {
61
+ const [isMobile, setIsMobile] = useState(false);
62
+
63
+ return (
64
+ <>
65
+ <AppPageHeader
66
+ breadcrumbs={[{ label: 'PageFooter' }]}
67
+ title="PageFooter"
68
+ subheader="Copyright + optional link row with gap and wrapping."
69
+ actions={<DocsHeaderActions />}
70
+ />
71
+ <PageContent>
72
+ <PageContentSection title="With links prop">
73
+ <div
74
+ style={{
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ gap: 8,
78
+ marginTop: 'var(--p-3)',
79
+ }}
80
+ >
81
+ <Switch
82
+ id="page-footer-mobile-preview"
83
+ checked={isMobile}
84
+ onCheckedChange={setIsMobile}
85
+ />
86
+ <Label htmlFor="page-footer-mobile-preview">Mobile</Label>
87
+ </div>
88
+ <div className={isMobile ? `${S.demo} ${S.isMobile}` : S.demo}>
89
+ <PageFooter
90
+ versionLink=""
91
+ versionLabel="0.0.0"
92
+ links={DEMO_LINKS}
93
+ />
94
+ </div>
95
+ </PageContentSection>
96
+ </PageContent>
97
+ </>
98
+ );
99
+ }
@@ -0,0 +1,60 @@
1
+ import { PageContentSection, PageTabs } from '#uilib/components/ui/Page';
2
+
3
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
4
+ import { DocsHeaderActions } from '../docsHeaderActions';
5
+
6
+ const SCROLL_TEST_ITEMS = Array.from({ length: 10 }, (_, i) => {
7
+ const n = i + 1;
8
+ return {
9
+ value: `scroll-demo-${n}`,
10
+ label: `Tab ${n} — wider label`,
11
+ content: (
12
+ <p style={{ margin: 0, padding: '0 var(--page-x-padding)' }}>
13
+ Content for tab {n}. Narrow container forces horizontal scroll on tab
14
+ row.
15
+ </p>
16
+ ),
17
+ };
18
+ });
19
+
20
+ export default function PageTabsPage() {
21
+ return (
22
+ <>
23
+ <AppPageHeader
24
+ breadcrumbs={[{ label: 'PageTabs' }]}
25
+ title="PageTabs"
26
+ subheader="Page-scoped tabs with horizontally scrollable tab strip."
27
+ actions={<DocsHeaderActions />}
28
+ />
29
+ <PageContentSection
30
+ style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}
31
+ >
32
+ <section>
33
+ <h3 style={{ marginBottom: '0.75rem' }}>Scrollable row (10 tabs)</h3>
34
+ <p
35
+ style={{
36
+ marginBottom: '1rem',
37
+ color: 'var(--muted-foreground)',
38
+ }}
39
+ >
40
+ Container capped at <code>280px</code> so tab triggers overflow and
41
+ use <code>PageXScroll</code>.
42
+ </p>
43
+ <div
44
+ style={{
45
+ maxWidth: 500,
46
+ border: '2px dashed var(--border)',
47
+ borderRadius: 'var(--p-3)',
48
+ backgroundColor: 'var(--background)',
49
+ }}
50
+ >
51
+ <PageTabs
52
+ defaultValue={SCROLL_TEST_ITEMS[0].value}
53
+ items={SCROLL_TEST_ITEMS}
54
+ />
55
+ </div>
56
+ </section>
57
+ </PageContentSection>
58
+ </>
59
+ );
60
+ }
@@ -9,24 +9,120 @@ import {
9
9
  import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
10
10
  import { DocsHeaderActions } from '../docsHeaderActions';
11
11
 
12
+ const VARIANTS = ['button', 'link'] as const;
13
+ const SIZES = ['sm', 'md', 'lg'] as const;
14
+
15
+ function TabsDemoCell({
16
+ variant,
17
+ size,
18
+ }: {
19
+ variant: (typeof VARIANTS)[number];
20
+ size: (typeof SIZES)[number];
21
+ }) {
22
+ const id = `${variant}-${size}`;
23
+ return (
24
+ <Tabs
25
+ variant={variant}
26
+ size={size}
27
+ defaultValue={`${id}-a`}
28
+ style={{ maxWidth: 360 }}
29
+ >
30
+ <TabsList>
31
+ <TabsTrigger value={`${id}-a`}>Alpha</TabsTrigger>
32
+ <TabsTrigger value={`${id}-b`}>Beta</TabsTrigger>
33
+ </TabsList>
34
+ <TabsContent value={`${id}-a`}>
35
+ Panel A ({variant}, {size}).
36
+ </TabsContent>
37
+ <TabsContent value={`${id}-b`}>
38
+ Panel B ({variant}, {size}).
39
+ </TabsContent>
40
+ </Tabs>
41
+ );
42
+ }
43
+
12
44
  export default function TabsPage() {
13
45
  return (
14
46
  <>
15
47
  <AppPageHeader
16
48
  breadcrumbs={[{ label: 'Tabs' }]}
17
49
  title="Tabs"
18
- subheader="Tabbed panels."
50
+ subheader="Variants, sizes, full-width list, darker surface."
19
51
  actions={<DocsHeaderActions />}
20
52
  />
21
- <PageContentSection>
22
- <Tabs defaultValue="a" style={{ maxWidth: 400 }}>
23
- <TabsList>
24
- <TabsTrigger value="a">Tab A</TabsTrigger>
25
- <TabsTrigger value="b">Tab B</TabsTrigger>
26
- </TabsList>
27
- <TabsContent value="a">Content for tab A.</TabsContent>
28
- <TabsContent value="b">Content for tab B.</TabsContent>
29
- </Tabs>
53
+ <PageContentSection
54
+ style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
55
+ >
56
+ <section>
57
+ <h3 style={{ marginBottom: '1rem' }}>Variants × sizes</h3>
58
+ <div style={{ overflowX: 'auto' }}>
59
+ <table style={{ borderCollapse: 'collapse', width: '100%' }}>
60
+ <thead>
61
+ <tr>
62
+ <th style={{ textAlign: 'left', padding: '0.5rem' }} />
63
+ {SIZES.map(s => (
64
+ <th key={s} style={{ padding: '0.5rem' }}>
65
+ {s}
66
+ </th>
67
+ ))}
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ {VARIANTS.map(v => (
72
+ <tr key={v}>
73
+ <td
74
+ style={{
75
+ padding: '0.5rem',
76
+ fontWeight: 600,
77
+ whiteSpace: 'nowrap',
78
+ verticalAlign: 'top',
79
+ }}
80
+ >
81
+ {v}
82
+ </td>
83
+ {SIZES.map(s => (
84
+ <td key={s} style={{ padding: '0.5rem' }}>
85
+ <TabsDemoCell variant={v} size={s} />
86
+ </td>
87
+ ))}
88
+ </tr>
89
+ ))}
90
+ </tbody>
91
+ </table>
92
+ </div>
93
+ </section>
94
+
95
+ <section>
96
+ <h3 style={{ marginBottom: '1rem' }}>Darker</h3>
97
+ <p
98
+ style={{
99
+ marginBottom: '0.75rem',
100
+ color: 'var(--muted-foreground)',
101
+ }}
102
+ >
103
+ Pass <code>darker</code> on root and/or list for muted segment
104
+ background on inactive triggers.
105
+ </p>
106
+ <div
107
+ style={{
108
+ display: 'flex',
109
+ gap: '1rem',
110
+ backgroundColor: 'var(--background)',
111
+ padding: '1rem',
112
+ borderRadius: 'var(--p-3)',
113
+ width: '200px',
114
+ }}
115
+ >
116
+ <Tabs defaultValue="dk-a" variant="button" darker>
117
+ <TabsList darker>
118
+ <TabsTrigger value="dk-a">One</TabsTrigger>
119
+ <TabsTrigger value="dk-b">Two</TabsTrigger>
120
+ </TabsList>
121
+ <TabsContent value="dk-a">Button tabs + darker.</TabsContent>
122
+ <TabsContent value="dk-b">Second tab.</TabsContent>
123
+ </Tabs>
124
+ </div>
125
+ </section>
30
126
  </PageContentSection>
31
127
  </>
32
128
  );
@@ -216,6 +216,18 @@ export const DOC_REGISTRY: DocEntry[] = [
216
216
  section: 'Layout',
217
217
  load: () => import('./pages/PagePage'),
218
218
  },
219
+ {
220
+ slug: 'page-footer',
221
+ title: 'PageFooter',
222
+ section: 'Layout',
223
+ load: () => import('./pages/PageFooterPage'),
224
+ },
225
+ {
226
+ slug: 'page-tabs',
227
+ title: 'PageTabs',
228
+ section: 'Layout',
229
+ load: () => import('./pages/PageTabsPage'),
230
+ },
219
231
  {
220
232
  slug: 'standalone-app-layout',
221
233
  title: 'Standalone app layout',
@@ -0,0 +1 @@
1
+ export { useIsMobile } from './useIsMobile';
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { useIsMobile } from './hooks/useIsMobile';
1
2
  export * from './contexts/theme-context';
2
3
  export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
3
4
  export * from './sybilion-auth';