@sovereignfs/ui 0.21.0 → 0.21.2
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 +75 -0
- package/dist/Dialog.module.css +4 -0
- package/dist/Drawer.module.css +1 -0
- package/dist/index.js +24 -0
- package/package.json +11 -1
- package/src/components/Dialog/Dialog.module.css +4 -0
- package/src/components/Dialog/Dialog.tsx +10 -0
- package/src/components/Drawer/Drawer.module.css +1 -0
- package/src/components/Drawer/Drawer.tsx +7 -0
- package/src/scroll-lock.ts +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @sovereignfs/ui
|
|
2
|
+
|
|
3
|
+
Sovereign Design System — design tokens and React components for building [Sovereign](https://github.com/sovereignfs/sovereign) plugins.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sovereignfs/ui
|
|
9
|
+
# pnpm add @sovereignfs/ui
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**Peer dependencies:** React 19+
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Components
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { Button, Input, Card, Dialog, Drawer } from '@sovereignfs/ui';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Available components: `Avatar`, `Badge`, `Button`, `Card`, `Dialog`, `Drawer`, `EmptyState`, `FormField`, `Icon`, `Input`, `NavTabs`, `PageHeader`, `Popover`, `SegmentedControl`, `Select`, `Spinner`, `SystemBanner`, `Tabs`, `Toast` / `useToast`, `Toggle`, `Tooltip`.
|
|
23
|
+
|
|
24
|
+
### Tokens
|
|
25
|
+
|
|
26
|
+
The Sovereign runtime injects design tokens globally. Reference them directly in your CSS — no import needed at runtime:
|
|
27
|
+
|
|
28
|
+
```css
|
|
29
|
+
.my-component {
|
|
30
|
+
color: var(--sv-color-text-primary);
|
|
31
|
+
background: var(--sv-color-surface);
|
|
32
|
+
padding: var(--sv-space-4);
|
|
33
|
+
border-radius: var(--sv-radius-md);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If you need tokens outside the Sovereign runtime (e.g. Storybook, standalone use), import the CSS file:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import '@sovereignfs/ui/tokens.css';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Token architecture
|
|
44
|
+
|
|
45
|
+
Tokens follow a two-tier model:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Primitive tokens --sv-grey-50 … --sv-grey-950 / --sv-space-1 … --sv-space-16
|
|
49
|
+
│
|
|
50
|
+
▼ mapped by semantic layer
|
|
51
|
+
Semantic tokens --sv-color-surface / --sv-color-text-primary / --sv-shadow-card
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Plugin developers reference **semantic tokens** — never primitive colours directly. Dark mode and instance theming work by swapping semantic token values; no component changes required.
|
|
55
|
+
|
|
56
|
+
## Icon component
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { Icon } from '@sovereignfs/ui';
|
|
60
|
+
|
|
61
|
+
<Icon name="bell" size={20} />;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Available icon names: `activity`, `alert-triangle`, `bell`, `check`, `chevron-down`, `chevron-left`, `chevron-right`, `chevron-up`, `eye`, `eye-off`, `grid-2x2`, `house`, `info`, `lock`, `log-out`, `mail`, `package`, `pencil`, `plus`, `rotate-ccw`, `search`, `settings`, `shield`, `sliders-horizontal`, `terminal`, `trash-2`, `user`, `x`.
|
|
65
|
+
|
|
66
|
+
## Documentation
|
|
67
|
+
|
|
68
|
+
- [Storybook — live component & token gallery](https://sovereignfs.github.io/storybook)
|
|
69
|
+
- [Design system reference](https://github.com/sovereignfs/sovereign/blob/main/docs/design-system.md)
|
|
70
|
+
- [Plugin development guide](https://github.com/sovereignfs/sovereign/blob/main/docs/plugin-development.md)
|
|
71
|
+
- [SDK stability & semver policy](https://github.com/sovereignfs/sovereign/blob/main/docs/sdk-stability.md)
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
AGPL-3.0-or-later
|
package/dist/Dialog.module.css
CHANGED
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
flex: 1 1 auto;
|
|
50
50
|
overflow-y: auto;
|
|
51
51
|
padding: var(--sv-space-6);
|
|
52
|
+
/* Prevent iOS scroll-chaining: when the dialog content reaches its top/bottom
|
|
53
|
+
boundary, stop the gesture from propagating to the document (which would
|
|
54
|
+
scroll the shell header and footer out of view in standalone PWA mode). */
|
|
55
|
+
overscroll-behavior: contain;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/* Fixed sizes. `max-*: 100%` caps to the scrim's content box (viewport minus
|
package/dist/Drawer.module.css
CHANGED
package/dist/index.js
CHANGED
|
@@ -905,6 +905,20 @@ function Input({ type = "text", className, ...rest }) {
|
|
|
905
905
|
|
|
906
906
|
// src/components/Dialog/Dialog.tsx
|
|
907
907
|
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
908
|
+
|
|
909
|
+
// src/scroll-lock.ts
|
|
910
|
+
function lockBodyScroll() {
|
|
911
|
+
const count = parseInt(document.body.dataset.scrollLocks ?? "0", 10);
|
|
912
|
+
document.body.dataset.scrollLocks = String(count + 1);
|
|
913
|
+
if (count === 0) document.body.style.overflow = "hidden";
|
|
914
|
+
}
|
|
915
|
+
function unlockBodyScroll() {
|
|
916
|
+
const next = Math.max(0, parseInt(document.body.dataset.scrollLocks ?? "0", 10) - 1);
|
|
917
|
+
document.body.dataset.scrollLocks = String(next);
|
|
918
|
+
if (next === 0) document.body.style.overflow = "";
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/components/Dialog/Dialog.tsx
|
|
908
922
|
import styles11 from "./Dialog.module.css";
|
|
909
923
|
import { jsx as jsx39, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
910
924
|
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
@@ -918,6 +932,11 @@ function Dialog({
|
|
|
918
932
|
}) {
|
|
919
933
|
const panelRef = useRef2(null);
|
|
920
934
|
const previouslyFocused = useRef2(null);
|
|
935
|
+
useEffect2(() => {
|
|
936
|
+
if (!open) return;
|
|
937
|
+
lockBodyScroll();
|
|
938
|
+
return unlockBodyScroll;
|
|
939
|
+
}, [open]);
|
|
921
940
|
useEffect2(() => {
|
|
922
941
|
if (!open) return;
|
|
923
942
|
previouslyFocused.current = document.activeElement;
|
|
@@ -1012,6 +1031,11 @@ var FOCUSABLE2 = 'a[href], button:not([disabled]), textarea:not([disabled]), inp
|
|
|
1012
1031
|
function Drawer({ open, onClose, "aria-label": ariaLabel, children }) {
|
|
1013
1032
|
const panelRef = useRef3(null);
|
|
1014
1033
|
const previouslyFocused = useRef3(null);
|
|
1034
|
+
useEffect3(() => {
|
|
1035
|
+
if (!open) return;
|
|
1036
|
+
lockBodyScroll();
|
|
1037
|
+
return unlockBodyScroll;
|
|
1038
|
+
}, [open]);
|
|
1015
1039
|
useEffect3(() => {
|
|
1016
1040
|
if (!open) return;
|
|
1017
1041
|
previouslyFocused.current = document.activeElement;
|
package/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sovereignfs/ui",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sovereign Design System — design tokens and React components.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"sovereign",
|
|
8
|
+
"design-system",
|
|
9
|
+
"react",
|
|
10
|
+
"components",
|
|
11
|
+
"css-modules",
|
|
12
|
+
"tokens",
|
|
13
|
+
"plugin",
|
|
14
|
+
"self-hosted"
|
|
15
|
+
],
|
|
6
16
|
"license": "AGPL-3.0-or-later",
|
|
7
17
|
"publishConfig": {
|
|
8
18
|
"access": "public"
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
flex: 1 1 auto;
|
|
50
50
|
overflow-y: auto;
|
|
51
51
|
padding: var(--sv-space-6);
|
|
52
|
+
/* Prevent iOS scroll-chaining: when the dialog content reaches its top/bottom
|
|
53
|
+
boundary, stop the gesture from propagating to the document (which would
|
|
54
|
+
scroll the shell header and footer out of view in standalone PWA mode). */
|
|
55
|
+
overscroll-behavior: contain;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/* Fixed sizes. `max-*: 100%` caps to the scrim's content box (viewport minus
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { type ReactNode, useEffect, useRef } from 'react';
|
|
4
|
+
import { lockBodyScroll, unlockBodyScroll } from '../../scroll-lock';
|
|
4
5
|
import styles from './Dialog.module.css';
|
|
5
6
|
|
|
6
7
|
export type DialogSize = 'sm' | 'md' | 'lg' | 'full';
|
|
@@ -44,6 +45,15 @@ export function Dialog({
|
|
|
44
45
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
45
46
|
const previouslyFocused = useRef<HTMLElement | null>(null);
|
|
46
47
|
|
|
48
|
+
// Prevent document-level scroll while open. Ref-counted so nested overlays
|
|
49
|
+
// (e.g. a confirmation dialog inside an overlay plugin) don't release the
|
|
50
|
+
// lock while a sibling is still open.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!open) return;
|
|
53
|
+
lockBodyScroll();
|
|
54
|
+
return unlockBodyScroll;
|
|
55
|
+
}, [open]);
|
|
56
|
+
|
|
47
57
|
// Capture focus on open; restore it on close/unmount.
|
|
48
58
|
useEffect(() => {
|
|
49
59
|
if (!open) return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { type ReactNode, useEffect, useRef } from 'react';
|
|
4
|
+
import { lockBodyScroll, unlockBodyScroll } from '../../scroll-lock';
|
|
4
5
|
import styles from './Drawer.module.css';
|
|
5
6
|
|
|
6
7
|
export interface DrawerProps {
|
|
@@ -30,6 +31,12 @@ export function Drawer({ open, onClose, 'aria-label': ariaLabel, children }: Dra
|
|
|
30
31
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
31
32
|
const previouslyFocused = useRef<HTMLElement | null>(null);
|
|
32
33
|
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open) return;
|
|
36
|
+
lockBodyScroll();
|
|
37
|
+
return unlockBodyScroll;
|
|
38
|
+
}, [open]);
|
|
39
|
+
|
|
33
40
|
useEffect(() => {
|
|
34
41
|
if (!open) return;
|
|
35
42
|
previouslyFocused.current = document.activeElement as HTMLElement | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Ref-counted body scroll lock. Multiple overlays (Dialog, Drawer) can be
|
|
2
|
+
// open simultaneously; the body stays locked until all of them close.
|
|
3
|
+
// Uses data-scroll-locks on <body> as a counter so concurrent callers
|
|
4
|
+
// don't clobber each other's lock state.
|
|
5
|
+
|
|
6
|
+
export function lockBodyScroll(): void {
|
|
7
|
+
const count = parseInt(document.body.dataset.scrollLocks ?? '0', 10);
|
|
8
|
+
document.body.dataset.scrollLocks = String(count + 1);
|
|
9
|
+
if (count === 0) document.body.style.overflow = 'hidden';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function unlockBodyScroll(): void {
|
|
13
|
+
const next = Math.max(0, parseInt(document.body.dataset.scrollLocks ?? '0', 10) - 1);
|
|
14
|
+
document.body.dataset.scrollLocks = String(next);
|
|
15
|
+
if (next === 0) document.body.style.overflow = '';
|
|
16
|
+
}
|