@pienter/ui 0.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/CHANGELOG.md +64 -0
- package/CONVENTIONS.md +1499 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/components/action/button/Button.astro +95 -0
- package/components/action/button/Button.vue +92 -0
- package/components/action/button/IconButton.astro +86 -0
- package/components/action/button/IconButton.vue +85 -0
- package/components/action/button/button.css +194 -0
- package/components/action/toggle/Toggle.vue +38 -0
- package/components/action/toggle/toggle.css +77 -0
- package/components/action/toggle-group/ToggleGroup.vue +67 -0
- package/components/action/toggle-group/toggle-group.css +78 -0
- package/components/display/avatar/Avatar.astro +17 -0
- package/components/display/avatar/Avatar.vue +30 -0
- package/components/display/avatar/AvatarStack.astro +9 -0
- package/components/display/avatar/AvatarStack.vue +11 -0
- package/components/display/avatar/avatar.css +58 -0
- package/components/display/badge/Badge.astro +15 -0
- package/components/display/badge/Badge.vue +23 -0
- package/components/display/badge/badge.css +56 -0
- package/components/display/empty/Empty.astro +9 -0
- package/components/display/empty/Empty.vue +11 -0
- package/components/display/empty/empty.css +39 -0
- package/components/display/icon/Icon.astro +52 -0
- package/components/display/icon/Icon.vue +57 -0
- package/components/display/icon/icon.css +47 -0
- package/components/feedback/alert/Alert.astro +52 -0
- package/components/feedback/alert/Alert.vue +60 -0
- package/components/feedback/alert/alert.css +78 -0
- package/components/feedback/progress/Progress.astro +68 -0
- package/components/feedback/progress/Progress.vue +82 -0
- package/components/feedback/progress/progress.css +68 -0
- package/components/feedback/skeleton/Skeleton.astro +32 -0
- package/components/feedback/skeleton/Skeleton.vue +39 -0
- package/components/feedback/skeleton/skeleton.css +56 -0
- package/components/feedback/spinner/Spinner.astro +25 -0
- package/components/feedback/spinner/Spinner.vue +36 -0
- package/components/feedback/spinner/spinner.css +91 -0
- package/components/feedback/toast/Toast.astro +50 -0
- package/components/feedback/toast/Toast.vue +74 -0
- package/components/feedback/toast/toast.css +128 -0
- package/components/form/checkbox/Checkbox.astro +79 -0
- package/components/form/checkbox/Checkbox.vue +95 -0
- package/components/form/checkbox/checkbox.css +59 -0
- package/components/form/combobox/Combobox.vue +508 -0
- package/components/form/combobox/combobox.css +110 -0
- package/components/form/date-input/DateInput.astro +105 -0
- package/components/form/date-input/DateInput.vue +121 -0
- package/components/form/date-input/date-input.css +19 -0
- package/components/form/form/Form.astro +106 -0
- package/components/form/form/Form.vue +181 -0
- package/components/form/form/form.css +46 -0
- package/components/form/input-otp/InputOTP.astro +147 -0
- package/components/form/input-otp/InputOTP.vue +209 -0
- package/components/form/input-otp/input-otp.css +52 -0
- package/components/form/label/Label.astro +13 -0
- package/components/form/label/Label.vue +20 -0
- package/components/form/label/label.css +11 -0
- package/components/form/number-field/NumberField.astro +142 -0
- package/components/form/number-field/NumberField.vue +155 -0
- package/components/form/number-field/number-field.css +115 -0
- package/components/form/radio-group/RadioGroup.astro +105 -0
- package/components/form/radio-group/RadioGroup.vue +110 -0
- package/components/form/radio-group/radio-group.css +114 -0
- package/components/form/radio-group/types.ts +14 -0
- package/components/form/select/Segmented.vue +36 -0
- package/components/form/select/Select.astro +105 -0
- package/components/form/select/Select.vue +109 -0
- package/components/form/select/select.css +96 -0
- package/components/form/slider/Slider.astro +205 -0
- package/components/form/slider/Slider.vue +321 -0
- package/components/form/slider/slider.css +115 -0
- package/components/form/switch/Switch.astro +75 -0
- package/components/form/switch/Switch.vue +89 -0
- package/components/form/switch/switch.css +64 -0
- package/components/form/tags-input/TagsInput.astro +153 -0
- package/components/form/tags-input/TagsInput.vue +207 -0
- package/components/form/tags-input/tags-input.css +128 -0
- package/components/form/text-input/TextInput.astro +84 -0
- package/components/form/text-input/TextInput.vue +99 -0
- package/components/form/text-input/text-input.css +165 -0
- package/components/form/textarea/Textarea.astro +86 -0
- package/components/form/textarea/Textarea.vue +102 -0
- package/components/form/textarea/textarea.css +25 -0
- package/components/layout/accordion/Accordion.vue +59 -0
- package/components/layout/accordion/accordion.css +87 -0
- package/components/layout/card/Card.astro +13 -0
- package/components/layout/card/Card.vue +20 -0
- package/components/layout/card/card.css +55 -0
- package/components/layout/collapsible/Collapsible.vue +77 -0
- package/components/layout/collapsible/collapsible.css +76 -0
- package/components/layout/separator/Separator.astro +31 -0
- package/components/layout/separator/Separator.vue +33 -0
- package/components/layout/separator/separator.css +27 -0
- package/components/layout/table/DataTable.vue +127 -0
- package/components/layout/table/Table.astro +116 -0
- package/components/layout/table/Table.vue +146 -0
- package/components/layout/table/TableRow.vue +59 -0
- package/components/layout/table/table.css +201 -0
- package/components/layout/table/types.ts +35 -0
- package/components/layout/table/useTable.ts +7 -0
- package/components/navigation/breadcrumb/Breadcrumb.astro +36 -0
- package/components/navigation/breadcrumb/Breadcrumb.vue +36 -0
- package/components/navigation/breadcrumb/breadcrumb.css +37 -0
- package/components/navigation/navbar/Navbar.astro +62 -0
- package/components/navigation/navbar/Navbar.vue +50 -0
- package/components/navigation/navbar/navbar.css +77 -0
- package/components/navigation/pagination/Pagination.vue +107 -0
- package/components/navigation/pagination/pagination.css +53 -0
- package/components/navigation/sidebar/Sidebar.astro +132 -0
- package/components/navigation/sidebar/Sidebar.vue +174 -0
- package/components/navigation/sidebar/SidebarItemRender.astro +83 -0
- package/components/navigation/sidebar/SidebarItemRender.vue +98 -0
- package/components/navigation/sidebar/sidebar.css +303 -0
- package/components/navigation/sidebar/types.ts +72 -0
- package/components/navigation/tabs/Tabs.vue +84 -0
- package/components/navigation/tabs/tabs.css +39 -0
- package/components/overlay/alert-dialog/AlertDialog.astro +112 -0
- package/components/overlay/alert-dialog/AlertDialog.vue +117 -0
- package/components/overlay/alert-dialog/alert-dialog.css +57 -0
- package/components/overlay/command/Command.vue +356 -0
- package/components/overlay/command/command.css +179 -0
- package/components/overlay/dropdown-menu/DropdownMenu.vue +143 -0
- package/components/overlay/dropdown-menu/dropdown-menu.css +120 -0
- package/components/overlay/modal/Modal.astro +66 -0
- package/components/overlay/modal/Modal.vue +85 -0
- package/components/overlay/modal/modal.css +60 -0
- package/components/overlay/popover/Popover.vue +113 -0
- package/components/overlay/popover/popover.css +53 -0
- package/components/overlay/sheet/Sheet.vue +88 -0
- package/components/overlay/sheet/sheet.css +108 -0
- package/components/overlay/tooltip/Tooltip.vue +210 -0
- package/components/overlay/tooltip/tooltip.css +50 -0
- package/composables/useUrlSort.ts +48 -0
- package/icons/alert-triangle.ts +1 -0
- package/icons/arrow-down.ts +1 -0
- package/icons/arrow-up-down.ts +5 -0
- package/icons/arrow-up.ts +1 -0
- package/icons/bell.ts +1 -0
- package/icons/check.ts +1 -0
- package/icons/chevron-down.ts +1 -0
- package/icons/chevron-left.ts +1 -0
- package/icons/chevron-right.ts +1 -0
- package/icons/chevron-up-down.ts +5 -0
- package/icons/chevron-up.ts +1 -0
- package/icons/circle-alert.ts +1 -0
- package/icons/circle-check.ts +1 -0
- package/icons/clipboard.ts +1 -0
- package/icons/download.ts +1 -0
- package/icons/edit.ts +1 -0
- package/icons/external-link.ts +1 -0
- package/icons/eye.ts +1 -0
- package/icons/file.ts +1 -0
- package/icons/filter.ts +1 -0
- package/icons/folder.ts +1 -0
- package/icons/image.ts +1 -0
- package/icons/inbox.ts +1 -0
- package/icons/index.ts +91 -0
- package/icons/info.ts +1 -0
- package/icons/layers.ts +1 -0
- package/icons/link-2.ts +1 -0
- package/icons/list.ts +1 -0
- package/icons/loader.ts +3 -0
- package/icons/menu.ts +1 -0
- package/icons/more-horizontal.ts +1 -0
- package/icons/more-vertical.ts +1 -0
- package/icons/plus-circle.ts +1 -0
- package/icons/plus.ts +1 -0
- package/icons/save.ts +1 -0
- package/icons/search.ts +1 -0
- package/icons/send.ts +1 -0
- package/icons/settings.ts +1 -0
- package/icons/tool.ts +1 -0
- package/icons/trash-2.ts +1 -0
- package/icons/trash.ts +1 -0
- package/icons/upload-cloud.ts +1 -0
- package/icons/upload.ts +1 -0
- package/icons/x.ts +1 -0
- package/package.json +150 -0
- package/styles/0-settings/colors.css +241 -0
- package/styles/0-settings/index.css +5 -0
- package/styles/0-settings/layout.css +52 -0
- package/styles/0-settings/motion.css +11 -0
- package/styles/0-settings/spacing.css +15 -0
- package/styles/0-settings/typography.css +37 -0
- package/styles/0-utils/index.css +1 -0
- package/styles/1-reset/index.css +1 -0
- package/styles/1-reset/reset.css +26 -0
- package/styles/2-base/base.css +42 -0
- package/styles/2-base/forms.css +23 -0
- package/styles/2-base/index.css +2 -0
- package/styles/3-layout/container.css +57 -0
- package/styles/3-layout/index.css +2 -0
- package/styles/3-layout/section.css +17 -0
- package/styles/5-utilities/accessibility.css +13 -0
- package/styles/5-utilities/index.css +2 -0
- package/styles/5-utilities/text.css +5 -0
- package/styles/main.css +8 -0
- package/styles/styles.d.ts +6 -0
- package/utils/a11y/focus.ts +68 -0
- package/utils/a11y/id.ts +10 -0
- package/utils/a11y/index.ts +9 -0
- package/utils/a11y/keyboard.ts +32 -0
- package/utils/a11y/live-region.ts +36 -0
- package/utils/controllers/dialog.ts +205 -0
- package/utils/controllers/disclosure.ts +117 -0
- package/utils/controllers/form.ts +524 -0
- package/utils/controllers/index.ts +39 -0
- package/utils/controllers/menu.ts +255 -0
- package/utils/controllers/number-field.ts +103 -0
- package/utils/controllers/otp.ts +252 -0
- package/utils/controllers/popover.ts +434 -0
- package/utils/controllers/sidebar.ts +610 -0
- package/utils/controllers/slider.ts +336 -0
- package/utils/controllers/tags-input.ts +255 -0
- package/utils/controllers/toast.ts +426 -0
- package/utils/dom/index.ts +1 -0
- package/utils/dom/scroll-lock.ts +48 -0
- package/utils/index.ts +3 -0
- package/utils/sort/index.ts +3 -0
- package/utils/sort/serialize.ts +19 -0
- package/utils/sort/state.ts +11 -0
- package/utils/sort/types.ts +15 -0
- package/utils/validation/form.ts +93 -0
- package/utils/validation/index.ts +13 -0
- package/utils/validation/rules.ts +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pienter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @pienter/ui
|
|
2
|
+
|
|
3
|
+
Shared Pienter UI package with framework-specific components, CSS, icons, and browser utilities.
|
|
4
|
+
|
|
5
|
+
This package publishes raw source files. Consuming projects compile the Vue, Astro, TypeScript, and CSS files with their own toolchain.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @pienter/ui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Install the framework peer you use:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install vue
|
|
17
|
+
npm install astro
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import Button from '@pienter/ui/components/Button.vue';
|
|
24
|
+
import '@pienter/ui/styles';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Astro components are available beside Vue components:
|
|
28
|
+
|
|
29
|
+
```astro
|
|
30
|
+
---
|
|
31
|
+
import Button from '@pienter/ui/components/Button.astro';
|
|
32
|
+
---
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Components use explicit flat public entrypoints. Intentional shared types are
|
|
36
|
+
available through module type entrypoints:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import type { RadioOption } from '@pienter/ui/components/RadioGroup.types';
|
|
40
|
+
import type { SidebarItem } from '@pienter/ui/components/Sidebar.types';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Utilities and icons are available through subpath imports:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { icons } from '@pienter/ui/icons';
|
|
47
|
+
import { generateId } from '@pienter/ui/utils/a11y/id.js';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Themes
|
|
51
|
+
|
|
52
|
+
The default theme follows the operating-system color preference, using
|
|
53
|
+
Catppuccin Latte for light mode and Mocha for dark mode. Set `data-theme` on
|
|
54
|
+
the document element to override it:
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<html data-theme="dark"></html>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Supported explicit values are `light` and `dark`. Remove the attribute to
|
|
61
|
+
return to the system preference.
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
make verify
|
|
67
|
+
make smoke-install CONSUMER=/path/to/consumer-app
|
|
68
|
+
make publish
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
See [CONVENTIONS.md](./CONVENTIONS.md) for component authoring rules.
|
|
@@ -0,0 +1,95 @@
|
|
|
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>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="href ? 'a' : 'button'"
|
|
4
|
+
:id="id"
|
|
5
|
+
class="pui-btn"
|
|
6
|
+
:type="href ? undefined : type"
|
|
7
|
+
:href="href"
|
|
8
|
+
:target="href ? target : undefined"
|
|
9
|
+
:rel="resolvedRel"
|
|
10
|
+
:disabled="href ? undefined : disabled || loading"
|
|
11
|
+
:aria-disabled="href && (disabled || loading) ? 'true' : undefined"
|
|
12
|
+
:tabindex="href && (disabled || loading) ? '-1' : undefined"
|
|
13
|
+
:aria-busy="loading ? 'true' : undefined"
|
|
14
|
+
:data-variant="variant"
|
|
15
|
+
:data-size="size"
|
|
16
|
+
:data-block="block ? 'true' : undefined"
|
|
17
|
+
:data-state="loading ? 'loading' : undefined"
|
|
18
|
+
>
|
|
19
|
+
<span v-if="loading" class="pui-btn__spinner" aria-hidden="true">
|
|
20
|
+
<Spinner />
|
|
21
|
+
</span>
|
|
22
|
+
<span v-if="hasIcon" class="pui-btn__icon" aria-hidden="true">
|
|
23
|
+
<Icon v-if="$slots.icon" :size="iconSize"
|
|
24
|
+
><slot name="icon"
|
|
25
|
+
/></Icon>
|
|
26
|
+
<Icon v-else :name="icon" :size="iconSize" />
|
|
27
|
+
</span>
|
|
28
|
+
<span class="pui-btn__label"><slot /></span>
|
|
29
|
+
</component>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { computed, useSlots } from 'vue';
|
|
34
|
+
import Icon from '../../display/icon/Icon.vue';
|
|
35
|
+
import Spinner from '../../feedback/spinner/Spinner.vue';
|
|
36
|
+
import type { IconName } from '../../../icons/index.js';
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(
|
|
39
|
+
defineProps<{
|
|
40
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
|
|
41
|
+
size?: 'sm' | 'md';
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
loading?: boolean;
|
|
44
|
+
type?: 'button' | 'submit' | 'reset';
|
|
45
|
+
block?: boolean;
|
|
46
|
+
href?: string;
|
|
47
|
+
/** Leading icon from the shipped set. Use the `icon` slot for a custom <svg>. */
|
|
48
|
+
icon?: IconName;
|
|
49
|
+
/** Anchor target, only applied when `href` is set (renders an <a>). */
|
|
50
|
+
target?: string;
|
|
51
|
+
/** Anchor rel; defaults to a safe value for `target="_blank"`. */
|
|
52
|
+
rel?: string;
|
|
53
|
+
id?: string;
|
|
54
|
+
}>(),
|
|
55
|
+
{
|
|
56
|
+
variant: 'secondary',
|
|
57
|
+
size: 'md',
|
|
58
|
+
disabled: false,
|
|
59
|
+
loading: false,
|
|
60
|
+
type: 'button',
|
|
61
|
+
block: false,
|
|
62
|
+
href: undefined,
|
|
63
|
+
icon: undefined,
|
|
64
|
+
target: undefined,
|
|
65
|
+
rel: undefined,
|
|
66
|
+
id: undefined,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const slots = useSlots();
|
|
71
|
+
const hasIcon = computed(() => !!props.icon || !!slots.icon);
|
|
72
|
+
|
|
73
|
+
// the icon slot wins over the `icon` name; flag the redundant combination in dev
|
|
74
|
+
if (props.icon && slots.icon) {
|
|
75
|
+
console.warn(
|
|
76
|
+
'[pienter-ui] Button received both an `icon` name and an `icon` slot; the slot is used and `icon` is ignored.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Button tier -> Icon size for the leading glyph (the Spinner is sized in button.css)
|
|
81
|
+
const iconSize = computed(() => (props.size === 'sm' ? 'sm' : 'md'));
|
|
82
|
+
const resolvedRel = computed(() =>
|
|
83
|
+
props.href
|
|
84
|
+
? (props.rel ??
|
|
85
|
+
(props.target === '_blank' ? 'noopener noreferrer' : undefined))
|
|
86
|
+
: undefined,
|
|
87
|
+
);
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
@import './button.css';
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,86 @@
|
|
|
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>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="href ? 'a' : 'button'"
|
|
4
|
+
:id="id"
|
|
5
|
+
class="pui-icon-btn"
|
|
6
|
+
:type="href ? undefined : type"
|
|
7
|
+
:href="href"
|
|
8
|
+
:target="href ? target : undefined"
|
|
9
|
+
:rel="resolvedRel"
|
|
10
|
+
:disabled="href ? undefined : disabled || loading"
|
|
11
|
+
:aria-disabled="href && (disabled || loading) ? 'true' : undefined"
|
|
12
|
+
:tabindex="href && (disabled || loading) ? '-1' : undefined"
|
|
13
|
+
:aria-busy="loading ? 'true' : undefined"
|
|
14
|
+
:aria-label="label"
|
|
15
|
+
:data-variant="variant"
|
|
16
|
+
:data-size="size"
|
|
17
|
+
:data-state="loading ? 'loading' : undefined"
|
|
18
|
+
>
|
|
19
|
+
<span v-if="loading" class="pui-btn__spinner" aria-hidden="true">
|
|
20
|
+
<Spinner />
|
|
21
|
+
</span>
|
|
22
|
+
<Icon v-else-if="$slots.default" :size="iconSize"><slot /></Icon>
|
|
23
|
+
<Icon v-else :name="name" :size="iconSize" />
|
|
24
|
+
</component>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import { computed, useSlots } from 'vue';
|
|
29
|
+
import Icon from '../../display/icon/Icon.vue';
|
|
30
|
+
import Spinner from '../../feedback/spinner/Spinner.vue';
|
|
31
|
+
import type { IconName } from '../../../icons/index.js';
|
|
32
|
+
|
|
33
|
+
const props = withDefaults(
|
|
34
|
+
defineProps<{
|
|
35
|
+
/** Accessible name for the icon-only control (required — there is no visible label). */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Shipped icon to render. Omit and pass your own SVG via the default slot instead. */
|
|
38
|
+
name?: IconName;
|
|
39
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
40
|
+
size?: 'sm' | 'md';
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
type?: 'button' | 'submit' | 'reset';
|
|
44
|
+
href?: string;
|
|
45
|
+
/** Anchor target, only applied when `href` is set (renders an <a>). */
|
|
46
|
+
target?: string;
|
|
47
|
+
/** Anchor rel; defaults to a safe value for `target="_blank"`. */
|
|
48
|
+
rel?: string;
|
|
49
|
+
id?: string;
|
|
50
|
+
}>(),
|
|
51
|
+
{
|
|
52
|
+
name: undefined,
|
|
53
|
+
variant: 'secondary',
|
|
54
|
+
size: 'md',
|
|
55
|
+
disabled: false,
|
|
56
|
+
loading: false,
|
|
57
|
+
type: 'button',
|
|
58
|
+
href: undefined,
|
|
59
|
+
target: undefined,
|
|
60
|
+
rel: undefined,
|
|
61
|
+
id: undefined,
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const slots = useSlots();
|
|
66
|
+
|
|
67
|
+
// the slot wins over the `name`; flag the redundant combination in dev
|
|
68
|
+
if (props.name && slots.default) {
|
|
69
|
+
console.warn(
|
|
70
|
+
'[pienter-ui] IconButton received both a `name` and slot content; the slot is used and `name` is ignored.',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const iconSize = computed(() => (props.size === 'sm' ? 'sm' : 'md'));
|
|
75
|
+
const resolvedRel = computed(() =>
|
|
76
|
+
props.href
|
|
77
|
+
? (props.rel ??
|
|
78
|
+
(props.target === '_blank' ? 'noopener noreferrer' : undefined))
|
|
79
|
+
: undefined,
|
|
80
|
+
);
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<style>
|
|
84
|
+
@import './button.css';
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pui-btn,
|
|
3
|
+
.pui-icon-btn {
|
|
4
|
+
position: relative;
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
gap: var(--control-gap);
|
|
9
|
+
font-weight: var(--fw-control);
|
|
10
|
+
border: var(--stroke-sm) solid var(--border-clr-input);
|
|
11
|
+
border-radius: var(--radius-sm);
|
|
12
|
+
background: var(--bg-clr-surface);
|
|
13
|
+
color: var(--text-clr-base);
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
text-decoration: none;
|
|
16
|
+
line-height: var(--lh-control);
|
|
17
|
+
transition:
|
|
18
|
+
transform var(--duration-fast) var(--ease-base),
|
|
19
|
+
background-color var(--duration-base) var(--ease-base),
|
|
20
|
+
border-color var(--duration-base) var(--ease-base);
|
|
21
|
+
|
|
22
|
+
&:hover:not(:disabled) {
|
|
23
|
+
background: var(--bg-clr-surface-2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:active:not(:disabled) {
|
|
27
|
+
transform: translateY(1px);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&:focus-visible {
|
|
31
|
+
outline: var(--outline-width) solid
|
|
32
|
+
var(--btn-ring, var(--border-clr-strong));
|
|
33
|
+
outline-offset: var(--outline-offset);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&:disabled,
|
|
37
|
+
&[aria-disabled='true'] {
|
|
38
|
+
opacity: var(--opacity-disabled);
|
|
39
|
+
cursor: not-allowed;
|
|
40
|
+
transform: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Busy, not unavailable: full opacity, or --opacity-disabled compounds with the
|
|
44
|
+
spinner's own ramp. Must stay ordered after the disabled rule — specificity ties. */
|
|
45
|
+
&[data-state='loading'] {
|
|
46
|
+
opacity: 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* aria-disabled isn't enforced natively (e.g. on <a>) */
|
|
50
|
+
&[aria-disabled='true'] {
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&[data-state='loading'] {
|
|
55
|
+
cursor: progress;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ===== variants ===== */
|
|
59
|
+
|
|
60
|
+
&[data-variant='primary'] {
|
|
61
|
+
background: var(--bg-clr-brand);
|
|
62
|
+
color: var(--bg-clr-brand-ink);
|
|
63
|
+
border-color: var(--bg-clr-brand);
|
|
64
|
+
box-shadow: var(--shadow-brand);
|
|
65
|
+
--btn-ring: var(--outline-clr-base);
|
|
66
|
+
|
|
67
|
+
&:hover:not(:disabled) {
|
|
68
|
+
background: var(--bg-clr-brand-hover);
|
|
69
|
+
border-color: var(--bg-clr-brand-hover);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
&[data-variant='secondary'] {
|
|
74
|
+
background: var(--bg-clr-surface);
|
|
75
|
+
color: var(--text-clr-base);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&[data-variant='ghost'] {
|
|
79
|
+
background: transparent;
|
|
80
|
+
border-color: transparent;
|
|
81
|
+
color: var(--text-clr-base);
|
|
82
|
+
|
|
83
|
+
&:hover:not(:disabled) {
|
|
84
|
+
background: var(--bg-clr-surface-2);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&[data-variant='danger'] {
|
|
89
|
+
background: var(--bg-clr-danger);
|
|
90
|
+
color: var(--bg-clr-danger-ink);
|
|
91
|
+
border-color: var(--bg-clr-danger);
|
|
92
|
+
--btn-ring: var(--border-clr-danger);
|
|
93
|
+
|
|
94
|
+
&:hover:not(:disabled) {
|
|
95
|
+
background: var(--bg-clr-danger-hover);
|
|
96
|
+
border-color: var(--bg-clr-danger-hover);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ===== sizes ===== */
|
|
101
|
+
|
|
102
|
+
&[data-size='md'] {
|
|
103
|
+
min-height: var(--control-height-md);
|
|
104
|
+
padding-inline: var(--control-inline-pad-md);
|
|
105
|
+
font-size: var(--step--1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&[data-size='sm'] {
|
|
109
|
+
min-height: var(--control-height-sm);
|
|
110
|
+
padding-inline: var(--control-inline-pad-sm);
|
|
111
|
+
font-size: var(--step--2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ===== spinner ===== */
|
|
115
|
+
|
|
116
|
+
/* just centres the Spinner; the spin and the fade ramp live in spinner.css */
|
|
117
|
+
.pui-btn__spinner {
|
|
118
|
+
position: absolute;
|
|
119
|
+
inset: 0;
|
|
120
|
+
display: grid;
|
|
121
|
+
place-items: center;
|
|
122
|
+
|
|
123
|
+
/* Match the leading icon, not Spinner's standalone md; reaching in through the
|
|
124
|
+
block is what beats Spinner's own [data-size] rule. */
|
|
125
|
+
.pui-spinner {
|
|
126
|
+
--pui-spinner-size: 1.25rem;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&[data-size='sm'] .pui-btn__spinner .pui-spinner {
|
|
131
|
+
--pui-spinner-size: 1rem;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ===== button-only ===== */
|
|
136
|
+
|
|
137
|
+
.pui-btn {
|
|
138
|
+
&[data-variant='link'] {
|
|
139
|
+
background: transparent;
|
|
140
|
+
border: 0;
|
|
141
|
+
color: var(--text-clr-brand);
|
|
142
|
+
text-decoration: underline;
|
|
143
|
+
text-underline-offset: 3px; /* optical; no scale token fits */
|
|
144
|
+
--btn-ring: var(--outline-clr-base);
|
|
145
|
+
|
|
146
|
+
&:hover:not(:disabled) {
|
|
147
|
+
background: transparent;
|
|
148
|
+
color: var(--text-clr-brand-hover);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
&[data-block='true'] {
|
|
153
|
+
width: 100%;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* kept in flow while loading so the size holds; the spinner overlays */
|
|
157
|
+
&[data-state='loading'] {
|
|
158
|
+
.pui-btn__icon,
|
|
159
|
+
.pui-btn__label {
|
|
160
|
+
opacity: 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.pui-btn__icon {
|
|
165
|
+
display: inline-flex;
|
|
166
|
+
flex: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.pui-btn__label {
|
|
170
|
+
display: inline-flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: center;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ===== icon-only (IconButton) ===== */
|
|
177
|
+
|
|
178
|
+
.pui-icon-btn {
|
|
179
|
+
gap: 0;
|
|
180
|
+
|
|
181
|
+
/* [data-size] specificity outweighs the shared size padding */
|
|
182
|
+
&[data-size] {
|
|
183
|
+
padding-inline: 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
&[data-size='md'] {
|
|
187
|
+
width: var(--control-height-md);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
&[data-size='sm'] {
|
|
191
|
+
width: var(--control-height-sm);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
class="pui-toggle"
|
|
4
|
+
type="button"
|
|
5
|
+
:data-tone="tone"
|
|
6
|
+
:data-size="size"
|
|
7
|
+
:disabled="disabled"
|
|
8
|
+
:aria-pressed="pressed ? 'true' : 'false'"
|
|
9
|
+
@click="emit('update:pressed', !pressed)"
|
|
10
|
+
>
|
|
11
|
+
<slot />
|
|
12
|
+
</button>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
withDefaults(
|
|
17
|
+
defineProps<{
|
|
18
|
+
pressed?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
size?: 'sm' | 'md' | 'lg';
|
|
21
|
+
tone?: 'default' | 'brand';
|
|
22
|
+
}>(),
|
|
23
|
+
{
|
|
24
|
+
pressed: false,
|
|
25
|
+
disabled: false,
|
|
26
|
+
size: 'md',
|
|
27
|
+
tone: 'default',
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{
|
|
32
|
+
'update:pressed': [value: boolean];
|
|
33
|
+
}>();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
@import './toggle.css';
|
|
38
|
+
</style>
|