@pienter/ui 0.2.0 → 0.5.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/CHANGELOG.md +259 -0
- package/CONVENTIONS.md +297 -385
- package/README.md +3 -18
- package/components/feedback/toast/toast.css +1 -1
- package/components/form/combobox/Combobox.vue +21 -38
- package/components/form/combobox/combobox.css +1 -1
- package/components/form/form/Form.vue +1 -2
- package/components/form/label/label.css +1 -1
- package/components/form/number-field/NumberField.vue +1 -1
- package/components/form/number-field/number-field.css +1 -1
- package/components/form/radio-group/RadioGroup.vue +2 -5
- package/components/form/slider/slider.css +2 -3
- package/components/form/tags-input/tags-input.css +1 -2
- package/components/form/textarea/textarea.css +1 -1
- package/components/layout/accordion/Accordion.vue +6 -27
- package/components/layout/collapsible/Collapsible.vue +9 -19
- package/components/layout/table/table.css +3 -3
- package/components/navigation/pagination/Pagination.vue +1 -1
- package/components/navigation/pagination/PaginationFooter.vue +23 -0
- package/components/navigation/pagination/pagination.css +128 -29
- package/components/navigation/sidebar/Sidebar.vue +326 -84
- package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
- package/components/navigation/sidebar/context.ts +21 -0
- package/components/navigation/sidebar/sidebar.css +346 -78
- package/components/navigation/sidebar/types.ts +13 -1
- package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
- package/components/overlay/command/Command.vue +10 -38
- package/components/overlay/command/command.css +1 -1
- package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
- package/components/overlay/modal/Modal.vue +7 -28
- package/components/overlay/popover/Popover.vue +9 -31
- package/components/overlay/sheet/Sheet.vue +7 -28
- package/components/overlay/tooltip/Tooltip.vue +14 -19
- package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
- package/composables/useDisclosure.ts +113 -0
- package/composables/useEventListener.ts +16 -0
- package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
- package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
- package/package.json +6 -38
- package/styles/0-settings/colors.css +8 -1
- package/styles/0-settings/layout.css +18 -0
- package/styles/0-settings/motion.css +2 -2
- package/styles/0-settings/spacing.css +3 -1
- package/utils/navigation/sidebar.ts +97 -0
- package/utils/validation/form.ts +6 -9
- package/components/action/button/Button.astro +0 -95
- package/components/action/button/IconButton.astro +0 -86
- package/components/display/avatar/Avatar.astro +0 -17
- package/components/display/avatar/AvatarStack.astro +0 -9
- package/components/display/badge/Badge.astro +0 -15
- package/components/display/empty/Empty.astro +0 -9
- package/components/display/icon/Icon.astro +0 -52
- package/components/feedback/alert/Alert.astro +0 -52
- package/components/feedback/progress/Progress.astro +0 -68
- package/components/feedback/skeleton/Skeleton.astro +0 -32
- package/components/feedback/spinner/Spinner.astro +0 -25
- package/components/feedback/toast/Toast.astro +0 -50
- package/components/form/checkbox/Checkbox.astro +0 -79
- package/components/form/date-input/DateInput.astro +0 -105
- package/components/form/form/Form.astro +0 -106
- package/components/form/input-otp/InputOTP.astro +0 -147
- package/components/form/label/Label.astro +0 -13
- package/components/form/number-field/NumberField.astro +0 -142
- package/components/form/radio-group/RadioGroup.astro +0 -105
- package/components/form/select/Select.astro +0 -105
- package/components/form/slider/Slider.astro +0 -205
- package/components/form/switch/Switch.astro +0 -75
- package/components/form/tags-input/TagsInput.astro +0 -153
- package/components/form/text-input/TextInput.astro +0 -84
- package/components/form/textarea/Textarea.astro +0 -86
- package/components/layout/card/Card.astro +0 -13
- package/components/layout/separator/Separator.astro +0 -31
- package/components/layout/table/Table.astro +0 -116
- package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
- package/components/navigation/navbar/Navbar.astro +0 -62
- package/components/navigation/sidebar/Sidebar.astro +0 -132
- package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
- package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
- package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
- package/components/overlay/modal/Modal.astro +0 -66
- package/utils/controllers/disclosure.ts +0 -117
- package/utils/controllers/form.ts +0 -524
- package/utils/controllers/index.ts +0 -39
- package/utils/controllers/number-field.ts +0 -103
- package/utils/controllers/otp.ts +0 -252
- package/utils/controllers/sidebar.ts +0 -610
- package/utils/controllers/slider.ts +0 -336
- package/utils/controllers/tags-input.ts +0 -255
- package/utils/controllers/toast.ts +0 -426
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar primitives, applied reactively by Sidebar.vue. See ADR 0007.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** A group's expanded/flyout state, as the templates render it. */
|
|
6
|
+
export type SubmenuState = 'open' | 'closed';
|
|
7
|
+
|
|
8
|
+
/** Grace period before an unpinned flyout closes on pointer-out. */
|
|
9
|
+
export const SUBMENU_CLOSE_DELAY = 140;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hierarchical-href match — locked algorithm. Returns true when
|
|
13
|
+
* `itemHref` is the prefix of `activeHref` honoring path boundaries.
|
|
14
|
+
*
|
|
15
|
+
* - `/` (root) is special-cased to strict equality so it doesn't
|
|
16
|
+
* greedily match every URL.
|
|
17
|
+
* - Substring-only matches are rejected (`/admin/members-old` does
|
|
18
|
+
* NOT match `/admin/members`).
|
|
19
|
+
*
|
|
20
|
+
* See CONVENTIONS.md § Active-href hierarchical-match algorithm.
|
|
21
|
+
*/
|
|
22
|
+
export function isHrefActive(
|
|
23
|
+
activeHref: string | null | undefined,
|
|
24
|
+
itemHref: string,
|
|
25
|
+
): boolean {
|
|
26
|
+
if (!activeHref) return false;
|
|
27
|
+
if (itemHref === '/') return activeHref === '/';
|
|
28
|
+
if (activeHref === itemHref) return true;
|
|
29
|
+
return activeHref.startsWith(itemHref + '/');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Kebab-case slug used as a group's key when none was supplied. */
|
|
33
|
+
export function deriveGroupKey(label: string): string {
|
|
34
|
+
return label
|
|
35
|
+
.normalize('NFKD')
|
|
36
|
+
.replace(/[̀-ͯ]/g, '')
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
39
|
+
.replace(/^-+|-+$/g, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Viewport coordinates for a collapsed-rail flyout. */
|
|
43
|
+
export interface SubmenuPosition {
|
|
44
|
+
left: number;
|
|
45
|
+
top: number;
|
|
46
|
+
height: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Where a collapsed rail's flyout sits: flush with the rail, starting below
|
|
51
|
+
* the brand if there is one, running the column's full height. Physical
|
|
52
|
+
* `left` because RTL is deferred library-wide.
|
|
53
|
+
*/
|
|
54
|
+
export function computeSubmenuPosition(
|
|
55
|
+
railRect: DOMRect,
|
|
56
|
+
brandRect: DOMRect | null,
|
|
57
|
+
): SubmenuPosition {
|
|
58
|
+
const top = brandRect ? brandRect.bottom : railRect.top;
|
|
59
|
+
return {
|
|
60
|
+
left: railRect.right,
|
|
61
|
+
top,
|
|
62
|
+
height: Math.max(0, railRect.bottom - top),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Below the drawer breakpoint there is no rail, so no flyouts. */
|
|
67
|
+
export function isSubmenuActive(options: {
|
|
68
|
+
policy: 'hidden' | 'panel';
|
|
69
|
+
collapsed: boolean;
|
|
70
|
+
viewportWidth: number;
|
|
71
|
+
drawerBreakpoint: number;
|
|
72
|
+
}): boolean {
|
|
73
|
+
return (
|
|
74
|
+
options.policy === 'panel' &&
|
|
75
|
+
options.collapsed &&
|
|
76
|
+
options.viewportWidth >= options.drawerBreakpoint
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether a group's subtree contains the current page. */
|
|
81
|
+
export function groupContainsActive(
|
|
82
|
+
items: readonly SidebarItemLike[],
|
|
83
|
+
activeHref: string | null | undefined,
|
|
84
|
+
): boolean {
|
|
85
|
+
return items.some((item) => {
|
|
86
|
+
if (item.type === 'link') return isHrefActive(activeHref, item.href);
|
|
87
|
+
if (item.type === 'group')
|
|
88
|
+
return groupContainsActive(item.items, activeHref);
|
|
89
|
+
return false;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The part of `SidebarItem` these primitives read; avoids a component import. */
|
|
94
|
+
export type SidebarItemLike =
|
|
95
|
+
| { type: 'link'; href: string }
|
|
96
|
+
| { type: 'group'; items: readonly SidebarItemLike[] }
|
|
97
|
+
| { type: 'section' };
|
package/utils/validation/form.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { type Validator, validate } from './rules.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Placeholder shape for form-submit responses, consumed
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* shape is **subject to change when ts-sdk publishes the canonical
|
|
9
|
-
* response shape**, at which point both implementations are updated
|
|
10
|
-
* together.
|
|
4
|
+
* Placeholder shape for form-submit responses, consumed by `Form.vue`'s
|
|
5
|
+
* orchestration. Locked in design D13 / spec "Unified Form Submit Response
|
|
6
|
+
* Handling"; the exact shape is **subject to change when ts-sdk publishes
|
|
7
|
+
* the canonical response shape**.
|
|
11
8
|
*/
|
|
12
9
|
export interface FormSubmitResponse {
|
|
13
10
|
/** Whether the submit succeeded. */
|
|
@@ -28,8 +25,8 @@ export interface FormSubmitResponse {
|
|
|
28
25
|
|
|
29
26
|
/**
|
|
30
27
|
* Run a per-field validator map against a `FormData` instance and return
|
|
31
|
-
* a `Record<string, string[]>` of field errors. Pure function;
|
|
32
|
-
*
|
|
28
|
+
* a `Record<string, string[]>` of field errors. Pure function; used by
|
|
29
|
+
* Form.vue's orchestration.
|
|
33
30
|
*
|
|
34
31
|
* Field values are read via `formData.get(name)` and coerced to strings.
|
|
35
32
|
* Fields without an entry in `validators` are not checked. Fields whose
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './button.css';
|
|
3
|
-
import Icon from '../../display/icon/Icon.astro';
|
|
4
|
-
import Spinner from '../../feedback/spinner/Spinner.astro';
|
|
5
|
-
import type { IconName } from '../../../icons/index.js';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
|
|
9
|
-
size?: 'sm' | 'md';
|
|
10
|
-
disabled?: boolean;
|
|
11
|
-
loading?: boolean;
|
|
12
|
-
type?: 'button' | 'submit' | 'reset';
|
|
13
|
-
block?: boolean;
|
|
14
|
-
href?: string;
|
|
15
|
-
/** Leading icon from the shipped set. Use the `icon` slot for a custom <svg>. */
|
|
16
|
-
icon?: IconName;
|
|
17
|
-
/** Anchor target, only applied when `href` is set (renders an <a>). */
|
|
18
|
-
target?: string;
|
|
19
|
-
/** Anchor rel; defaults to a safe value for `target="_blank"`. */
|
|
20
|
-
rel?: string;
|
|
21
|
-
id?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const {
|
|
25
|
-
variant = 'secondary',
|
|
26
|
-
size = 'md',
|
|
27
|
-
disabled = false,
|
|
28
|
-
loading = false,
|
|
29
|
-
type = 'button',
|
|
30
|
-
block = false,
|
|
31
|
-
href,
|
|
32
|
-
icon,
|
|
33
|
-
target,
|
|
34
|
-
rel,
|
|
35
|
-
id,
|
|
36
|
-
...rest
|
|
37
|
-
} = Astro.props;
|
|
38
|
-
|
|
39
|
-
const Tag = href ? 'a' : 'button';
|
|
40
|
-
const isInert = disabled || loading;
|
|
41
|
-
// Button tier -> Icon size for the leading glyph (the Spinner is sized in button.css)
|
|
42
|
-
const iconSize = size === 'sm' ? 'sm' : 'md';
|
|
43
|
-
const hasIconSlot = Astro.slots.has('icon');
|
|
44
|
-
const hasIcon = !!icon || hasIconSlot;
|
|
45
|
-
const resolvedRel = href
|
|
46
|
-
? (rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined))
|
|
47
|
-
: undefined;
|
|
48
|
-
|
|
49
|
-
// the icon slot wins over the `icon` name; flag the redundant combination in dev
|
|
50
|
-
if (import.meta.env.DEV && icon && hasIconSlot) {
|
|
51
|
-
console.warn(
|
|
52
|
-
'[pienter-ui] Button received both an `icon` name and an `icon` slot; the slot is used and `icon` is ignored.',
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
<Tag
|
|
58
|
-
id={id}
|
|
59
|
-
class="pui-btn"
|
|
60
|
-
type={href ? undefined : type}
|
|
61
|
-
href={href}
|
|
62
|
-
target={href ? target : undefined}
|
|
63
|
-
rel={resolvedRel}
|
|
64
|
-
disabled={href ? undefined : isInert}
|
|
65
|
-
aria-disabled={href && isInert ? 'true' : undefined}
|
|
66
|
-
tabindex={href && isInert ? '-1' : undefined}
|
|
67
|
-
aria-busy={loading ? 'true' : undefined}
|
|
68
|
-
data-variant={variant}
|
|
69
|
-
data-size={size}
|
|
70
|
-
data-block={block ? 'true' : undefined}
|
|
71
|
-
data-state={loading ? 'loading' : undefined}
|
|
72
|
-
{...rest}
|
|
73
|
-
>
|
|
74
|
-
{
|
|
75
|
-
loading && (
|
|
76
|
-
<span class="pui-btn__spinner" aria-hidden="true">
|
|
77
|
-
<Spinner />
|
|
78
|
-
</span>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
{
|
|
82
|
-
hasIcon && (
|
|
83
|
-
<span class="pui-btn__icon" aria-hidden="true">
|
|
84
|
-
{hasIconSlot ? (
|
|
85
|
-
<Icon size={iconSize}>
|
|
86
|
-
<slot name="icon" />
|
|
87
|
-
</Icon>
|
|
88
|
-
) : (
|
|
89
|
-
<Icon name={icon} size={iconSize} />
|
|
90
|
-
)}
|
|
91
|
-
</span>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
<span class="pui-btn__label"><slot /></span>
|
|
95
|
-
</Tag>
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './button.css';
|
|
3
|
-
import Icon from '../../display/icon/Icon.astro';
|
|
4
|
-
import Spinner from '../../feedback/spinner/Spinner.astro';
|
|
5
|
-
import type { IconName } from '../../../icons/index.js';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
/** Accessible name for the icon-only control (required — there is no visible label). */
|
|
9
|
-
label: string;
|
|
10
|
-
/** Shipped icon to render. Omit and pass your own SVG via the default slot instead. */
|
|
11
|
-
name?: IconName;
|
|
12
|
-
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
13
|
-
size?: 'sm' | 'md';
|
|
14
|
-
disabled?: boolean;
|
|
15
|
-
loading?: boolean;
|
|
16
|
-
type?: 'button' | 'submit' | 'reset';
|
|
17
|
-
href?: string;
|
|
18
|
-
/** Anchor target, only applied when `href` is set (renders an <a>). */
|
|
19
|
-
target?: string;
|
|
20
|
-
/** Anchor rel; defaults to a safe value for `target="_blank"`. */
|
|
21
|
-
rel?: string;
|
|
22
|
-
id?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const {
|
|
26
|
-
label,
|
|
27
|
-
name,
|
|
28
|
-
variant = 'secondary',
|
|
29
|
-
size = 'md',
|
|
30
|
-
disabled = false,
|
|
31
|
-
loading = false,
|
|
32
|
-
type = 'button',
|
|
33
|
-
href,
|
|
34
|
-
target,
|
|
35
|
-
rel,
|
|
36
|
-
id,
|
|
37
|
-
...rest
|
|
38
|
-
} = Astro.props;
|
|
39
|
-
|
|
40
|
-
const Tag = href ? 'a' : 'button';
|
|
41
|
-
const isInert = disabled || loading;
|
|
42
|
-
const iconSize = size === 'sm' ? 'sm' : 'md';
|
|
43
|
-
const hasSlot = Astro.slots.has('default');
|
|
44
|
-
const resolvedRel = href
|
|
45
|
-
? (rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined))
|
|
46
|
-
: undefined;
|
|
47
|
-
|
|
48
|
-
// the slot wins over the `name`; flag the redundant combination in dev
|
|
49
|
-
if (import.meta.env.DEV && name && hasSlot) {
|
|
50
|
-
console.warn(
|
|
51
|
-
'[pienter-ui] IconButton received both a `name` and slot content; the slot is used and `name` is ignored.',
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
<Tag
|
|
57
|
-
id={id}
|
|
58
|
-
class="pui-icon-btn"
|
|
59
|
-
type={href ? undefined : type}
|
|
60
|
-
href={href}
|
|
61
|
-
target={href ? target : undefined}
|
|
62
|
-
rel={resolvedRel}
|
|
63
|
-
disabled={href ? undefined : isInert}
|
|
64
|
-
aria-disabled={href && isInert ? 'true' : undefined}
|
|
65
|
-
tabindex={href && isInert ? '-1' : undefined}
|
|
66
|
-
aria-busy={loading ? 'true' : undefined}
|
|
67
|
-
aria-label={label}
|
|
68
|
-
data-variant={variant}
|
|
69
|
-
data-size={size}
|
|
70
|
-
data-state={loading ? 'loading' : undefined}
|
|
71
|
-
{...rest}
|
|
72
|
-
>
|
|
73
|
-
{
|
|
74
|
-
loading ? (
|
|
75
|
-
<span class="pui-btn__spinner" aria-hidden="true">
|
|
76
|
-
<Spinner />
|
|
77
|
-
</span>
|
|
78
|
-
) : hasSlot ? (
|
|
79
|
-
<Icon size={iconSize}>
|
|
80
|
-
<slot />
|
|
81
|
-
</Icon>
|
|
82
|
-
) : (
|
|
83
|
-
<Icon name={name} size={iconSize} />
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
</Tag>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './avatar.css';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
size?: 'sm' | 'lg';
|
|
6
|
-
tone?: 'brand' | 'solid';
|
|
7
|
-
src?: string;
|
|
8
|
-
alt?: string;
|
|
9
|
-
initials?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const { size, tone, src, alt = '', initials, ...rest } = Astro.props;
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
<span class="pui-avatar" data-size={size} data-tone={tone} {...rest}>
|
|
16
|
-
{src ? <img src={src} alt={alt} /> : initials ? initials : <slot />}
|
|
17
|
-
</span>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './badge.css';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
tone?: 'brand' | 'success' | 'warning' | 'danger' | 'solid';
|
|
6
|
-
dot?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { tone, dot = false, ...rest } = Astro.props;
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
<span class="pui-badge" data-tone={tone} {...rest}>
|
|
13
|
-
{dot && <span class="pui-badge__dot" />}
|
|
14
|
-
<slot />
|
|
15
|
-
</span>
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './icon.css';
|
|
3
|
-
import { icons, type IconName } from '../../../icons/index.js';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
/** Shipped icon to render. Omit and pass a custom <svg> via the default slot. */
|
|
7
|
-
name?: IconName;
|
|
8
|
-
size?: 'sm' | 'md' | 'lg';
|
|
9
|
-
/** Continuous animation applied to the glyph (e.g. a loading spinner). */
|
|
10
|
-
animation?: 'spin';
|
|
11
|
-
label?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { name, size = 'md', animation, label, ...rest } = Astro.props;
|
|
15
|
-
const hasSlot = Astro.slots.has('default');
|
|
16
|
-
|
|
17
|
-
// slot wins over name; flag the redundant combination in development
|
|
18
|
-
if (import.meta.env.DEV && name && hasSlot) {
|
|
19
|
-
console.warn(
|
|
20
|
-
'[pienter-ui] Icon received both a `name` and slot content; the slot is used and `name` is ignored.',
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const svg = name ? icons[name] : '';
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
hasSlot ? (
|
|
29
|
-
<span
|
|
30
|
-
class="pui-icon"
|
|
31
|
-
data-size={size}
|
|
32
|
-
data-animation={animation}
|
|
33
|
-
aria-hidden={!label}
|
|
34
|
-
aria-label={label}
|
|
35
|
-
role={label ? 'img' : undefined}
|
|
36
|
-
{...rest}
|
|
37
|
-
>
|
|
38
|
-
<slot />
|
|
39
|
-
</span>
|
|
40
|
-
) : (
|
|
41
|
-
<span
|
|
42
|
-
class="pui-icon"
|
|
43
|
-
data-size={size}
|
|
44
|
-
data-animation={animation}
|
|
45
|
-
aria-hidden={!label}
|
|
46
|
-
aria-label={label}
|
|
47
|
-
role={label ? 'img' : undefined}
|
|
48
|
-
{...rest}
|
|
49
|
-
set:html={svg}
|
|
50
|
-
/>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './alert.css';
|
|
3
|
-
import Icon from '../../display/icon/Icon.astro';
|
|
4
|
-
import type { IconName } from '../../../icons/index.js';
|
|
5
|
-
|
|
6
|
-
type Tone = 'brand' | 'success' | 'warning' | 'danger';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
tone?: Tone;
|
|
10
|
-
title?: string;
|
|
11
|
-
/** Override the tone's default icon with a shipped one. Use the `icon` slot for a custom <svg>. */
|
|
12
|
-
icon?: IconName;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const toneIconMap: Record<Tone, IconName> = {
|
|
16
|
-
brand: 'info',
|
|
17
|
-
success: 'circle-check',
|
|
18
|
-
warning: 'alert-triangle',
|
|
19
|
-
danger: 'circle-alert',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const { tone, title, icon, ...rest } = Astro.props;
|
|
23
|
-
const resolvedIcon: IconName = icon ?? (tone ? toneIconMap[tone] : 'info');
|
|
24
|
-
const hasIconSlot = Astro.slots.has('icon');
|
|
25
|
-
|
|
26
|
-
// the icon slot wins over the `icon` name; flag the redundant combination in dev
|
|
27
|
-
if (import.meta.env.DEV && icon && hasIconSlot) {
|
|
28
|
-
console.warn(
|
|
29
|
-
'[pienter-ui] Alert received both an `icon` name and an `icon` slot; the slot is used and `icon` is ignored.',
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
<div class="pui-alert" data-tone={tone} role="alert" {...rest}>
|
|
35
|
-
<span class="pui-alert__icon">
|
|
36
|
-
{
|
|
37
|
-
hasIconSlot ? (
|
|
38
|
-
<Icon size="md">
|
|
39
|
-
<slot name="icon" />
|
|
40
|
-
</Icon>
|
|
41
|
-
) : (
|
|
42
|
-
<Icon name={resolvedIcon} size="md" />
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
</span>
|
|
46
|
-
<div class="pui-alert__content">
|
|
47
|
-
{title && <p class="pui-alert__title">{title}</p>}
|
|
48
|
-
<div class="pui-alert__body">
|
|
49
|
-
<slot />
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './progress.css';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
/**
|
|
6
|
-
* Current value of the progress bar. When `null` or `undefined`, the
|
|
7
|
-
* component renders the indeterminate ("loading") state per WAI-ARIA
|
|
8
|
-
* convention — `aria-valuenow` is OMITTED from the wrapper so assistive
|
|
9
|
-
* technology announces an indeterminate progressbar.
|
|
10
|
-
*/
|
|
11
|
-
value?: number | null;
|
|
12
|
-
/** Maximum value of the range. */
|
|
13
|
-
max?: number;
|
|
14
|
-
/** Minimum value of the range. */
|
|
15
|
-
min?: number;
|
|
16
|
-
/**
|
|
17
|
-
* Accessible name for the progressbar — emitted as `aria-label`. Omit
|
|
18
|
-
* when an external `aria-labelledby` is supplied via `...rest`.
|
|
19
|
-
*/
|
|
20
|
-
label?: string;
|
|
21
|
-
/** Tone variant — drives indicator color via `data-tone`. */
|
|
22
|
-
tone?: 'brand' | 'success' | 'warning' | 'danger';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const {
|
|
26
|
-
value = null,
|
|
27
|
-
max = 100,
|
|
28
|
-
min = 0,
|
|
29
|
-
label,
|
|
30
|
-
tone = 'brand',
|
|
31
|
-
...rest
|
|
32
|
-
} = Astro.props;
|
|
33
|
-
|
|
34
|
-
const isIndeterminate = value === null || value === undefined;
|
|
35
|
-
|
|
36
|
-
const clampedValue = isIndeterminate
|
|
37
|
-
? min
|
|
38
|
-
: Math.min(Math.max(value as number, min), max);
|
|
39
|
-
|
|
40
|
-
const span = max - min;
|
|
41
|
-
const percent =
|
|
42
|
-
isIndeterminate || span <= 0 ? 0 : ((clampedValue - min) / span) * 100;
|
|
43
|
-
|
|
44
|
-
const state: 'loading' | 'in-progress' | 'complete' = isIndeterminate
|
|
45
|
-
? 'loading'
|
|
46
|
-
: clampedValue === max
|
|
47
|
-
? 'complete'
|
|
48
|
-
: 'in-progress';
|
|
49
|
-
|
|
50
|
-
const indicatorStyle: Record<string, string> = {};
|
|
51
|
-
if (!isIndeterminate) {
|
|
52
|
-
indicatorStyle.width = `${percent}%`;
|
|
53
|
-
}
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
<div
|
|
57
|
-
class="pui-progress"
|
|
58
|
-
role="progressbar"
|
|
59
|
-
aria-valuenow={isIndeterminate ? undefined : clampedValue}
|
|
60
|
-
aria-valuemin={min}
|
|
61
|
-
aria-valuemax={max}
|
|
62
|
-
aria-label={label}
|
|
63
|
-
data-state={state}
|
|
64
|
-
data-tone={tone}
|
|
65
|
-
{...rest}
|
|
66
|
-
>
|
|
67
|
-
<div class="pui-progress__indicator" style={indicatorStyle}></div>
|
|
68
|
-
</div>
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './skeleton.css';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
variant?: 'text' | 'rect' | 'circle';
|
|
6
|
-
width?: string;
|
|
7
|
-
height?: string;
|
|
8
|
-
rounded?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
variant = 'rect',
|
|
13
|
-
width,
|
|
14
|
-
height,
|
|
15
|
-
rounded = false,
|
|
16
|
-
...rest
|
|
17
|
-
} = Astro.props;
|
|
18
|
-
|
|
19
|
-
const inlineStyle: Record<string, string> = {};
|
|
20
|
-
if (width) inlineStyle.width = width;
|
|
21
|
-
if (height) inlineStyle.height = height;
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
<div
|
|
25
|
-
class="pui-skeleton"
|
|
26
|
-
data-variant={variant}
|
|
27
|
-
data-rounded={rounded ? '' : undefined}
|
|
28
|
-
style={inlineStyle}
|
|
29
|
-
aria-hidden="true"
|
|
30
|
-
{...rest}
|
|
31
|
-
>
|
|
32
|
-
</div>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Spinner (Astro) — same contract as Spinner.vue; composes Icon so the spin stays in icon.css.
|
|
3
|
-
import './spinner.css';
|
|
4
|
-
import Icon from '../../display/icon/Icon.astro';
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
8
|
-
/** Accessible name; supply it when the spinner is the only signal. See Spinner.vue. */
|
|
9
|
-
label?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const { size = 'md', label, ...rest } = Astro.props;
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
<span
|
|
16
|
-
class="pui-spinner"
|
|
17
|
-
data-size={size}
|
|
18
|
-
role={label ? 'status' : undefined}
|
|
19
|
-
aria-hidden={label ? undefined : 'true'}
|
|
20
|
-
{...rest}
|
|
21
|
-
>
|
|
22
|
-
<Icon name="loader" animation="spin" />
|
|
23
|
-
{/* Real text, not an aria-label — see Spinner.vue. */}
|
|
24
|
-
{label && <span class="pui-spinner__label">{label}</span>}
|
|
25
|
-
</span>
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './toast.css';
|
|
3
|
-
import Icon from '../../display/icon/Icon.astro';
|
|
4
|
-
import type { IconName } from '../../../icons/index.js';
|
|
5
|
-
|
|
6
|
-
type Tone = 'brand' | 'success' | 'warning' | 'danger';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
tone?: Tone;
|
|
10
|
-
title: string;
|
|
11
|
-
message?: string;
|
|
12
|
-
dismissible?: boolean;
|
|
13
|
-
icon?: IconName;
|
|
14
|
-
action?: { label: string };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const toneIconMap: Record<Tone, IconName> = {
|
|
18
|
-
brand: 'info',
|
|
19
|
-
success: 'circle-check',
|
|
20
|
-
warning: 'alert-triangle',
|
|
21
|
-
danger: 'circle-alert',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const { tone, title, message, dismissible = true, icon, action } = Astro.props;
|
|
25
|
-
const resolvedIcon: IconName = icon ?? (tone ? toneIconMap[tone] : 'info');
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
<div class="pui-toast" data-tone={tone}>
|
|
29
|
-
<span class="pui-toast__icon" aria-hidden="true">
|
|
30
|
-
<Icon name={resolvedIcon} size="sm" />
|
|
31
|
-
</span>
|
|
32
|
-
<div class="pui-toast__body">
|
|
33
|
-
<p class="pui-toast__title">{title}</p>
|
|
34
|
-
{message && <p class="pui-toast__msg">{message}</p>}
|
|
35
|
-
</div>
|
|
36
|
-
{
|
|
37
|
-
action && (
|
|
38
|
-
<button class="pui-toast__action" type="button">
|
|
39
|
-
{action.label}
|
|
40
|
-
</button>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
{
|
|
44
|
-
dismissible && (
|
|
45
|
-
<button class="pui-toast__close" type="button" aria-label="Close">
|
|
46
|
-
<Icon name="x" size="sm" />
|
|
47
|
-
</button>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
</div>
|