@stnd/ui 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/Accordion.astro +52 -0
- package/AccordionItem.astro +65 -0
- package/Alert.svelte +59 -0
- package/AlertDialog.svelte +146 -0
- package/CHANGELOG.md +25 -0
- package/CfImage.astro +70 -0
- package/Combobox.svelte +458 -0
- package/ContextMenu.svelte +120 -0
- package/ContextMenuItem.svelte +64 -0
- package/ContextMenuItemCheckbox.svelte +105 -0
- package/ContextMenuLabel.svelte +18 -0
- package/ContextMenuSeparator.svelte +1 -0
- package/DialogManager.svelte +32 -0
- package/Dropdown.svelte +107 -0
- package/DropdownItem.svelte +87 -0
- package/DropdownLabel.svelte +11 -0
- package/DropdownSeparator.svelte +1 -0
- package/LauncherHint.astro +35 -0
- package/Pagination.astro +154 -0
- package/Panel.svelte +365 -0
- package/README.md +150 -0
- package/Scroller.astro +11 -0
- package/Scroller.svelte +44 -0
- package/TableOfContents.astro +217 -0
- package/Toast.astro +202 -0
- package/dialog.js +53 -0
- package/package.json +35 -0
- package/transitions.js +78 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte";
|
|
3
|
+
import Icon from "@stnd/icon/Icon.svelte";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} ContextMenuItemCheckboxProps
|
|
7
|
+
* @property {import('svelte').Snippet} [children] - Item label
|
|
8
|
+
* @property {string} [label] - Label as a string
|
|
9
|
+
* @property {boolean} [checked] - Checked state
|
|
10
|
+
* @property {boolean} [disabled] - Disabled state
|
|
11
|
+
* @property {(checked: boolean) => void} [onCheckedChange] - Change handler
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @type {ContextMenuItemCheckboxProps} */
|
|
15
|
+
let {
|
|
16
|
+
children,
|
|
17
|
+
label,
|
|
18
|
+
checked = $bindable(false),
|
|
19
|
+
disabled = false,
|
|
20
|
+
onCheckedChange,
|
|
21
|
+
...props
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
const ctx = getContext("context-menu");
|
|
25
|
+
|
|
26
|
+
function handleClick(e) {
|
|
27
|
+
if (disabled) return;
|
|
28
|
+
checked = !checked;
|
|
29
|
+
onCheckedChange?.(checked);
|
|
30
|
+
// Usually checkboxes don't close the menu immediately in all implementations,
|
|
31
|
+
// but for context menus they often do, or stay open if you use e.stopPropagation().
|
|
32
|
+
// We'll follow the standard and close it unless the user prevents it.
|
|
33
|
+
ctx?.close();
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
class="context-menu-item"
|
|
40
|
+
class:disabled
|
|
41
|
+
onclick={handleClick}
|
|
42
|
+
role="menuitemcheckbox"
|
|
43
|
+
aria-checked={checked}
|
|
44
|
+
aria-disabled={disabled}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
<div class="item-check">
|
|
48
|
+
{#if checked}
|
|
49
|
+
<Icon icon="check" />
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<span class="item-label">
|
|
54
|
+
{#if children}
|
|
55
|
+
{@render children()}
|
|
56
|
+
{:else}
|
|
57
|
+
{label}
|
|
58
|
+
{/if}
|
|
59
|
+
</span>
|
|
60
|
+
</button>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.context-menu-item {
|
|
64
|
+
all: unset;
|
|
65
|
+
display: flex;
|
|
66
|
+
width: 100%;
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 0;
|
|
70
|
+
padding: 0 var(--space-d4);
|
|
71
|
+
border-radius: var(--radius-sm);
|
|
72
|
+
font-size: var(--size-sm);
|
|
73
|
+
font-family: var(--font-interface);
|
|
74
|
+
color: var(--color-foreground);
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
user-select: none;
|
|
77
|
+
transition: all var(--transition-fast);
|
|
78
|
+
min-height: var(--space);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.context-menu-item:hover:not(.disabled) {
|
|
82
|
+
background: var(--color-surface-high);
|
|
83
|
+
box-shadow: var(--shadow-raised);
|
|
84
|
+
}
|
|
85
|
+
.context-menu-item:active:not(.disabled) {
|
|
86
|
+
background: var(--color-surface-low);
|
|
87
|
+
box-shadow: var(--shadow-inset);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.context-menu-item.disabled {
|
|
91
|
+
opacity: 0.5;
|
|
92
|
+
cursor: not-allowed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.item-check {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.item-label {
|
|
102
|
+
flex: 1;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} ContextMenuLabelProps
|
|
4
|
+
* @property {import('svelte').Snippet} [children] - Label content
|
|
5
|
+
* @property {string} [label] - Label as a string
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @type {ContextMenuLabelProps} */
|
|
9
|
+
let { children, label } = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="std-menu-label">
|
|
13
|
+
{#if children}
|
|
14
|
+
{@render children()}
|
|
15
|
+
{:else}
|
|
16
|
+
{label}
|
|
17
|
+
{/if}
|
|
18
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<hr class="std-menu-separator" />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import AlertDialog from "./AlertDialog.svelte";
|
|
3
|
+
import { dialogState } from "./dialog.js";
|
|
4
|
+
|
|
5
|
+
let state = $state({ open: false });
|
|
6
|
+
|
|
7
|
+
// Sync store to local state
|
|
8
|
+
$effect(() => {
|
|
9
|
+
const unsub = dialogState.subscribe((s) => {
|
|
10
|
+
state = s;
|
|
11
|
+
});
|
|
12
|
+
return unsub;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Sync local open state back to store (when dialog closes internally)
|
|
16
|
+
function handleOpenChange(isOpen) {
|
|
17
|
+
if (!isOpen && state.open) {
|
|
18
|
+
dialogState.update(s => ({ ...s, open: false }));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<AlertDialog
|
|
24
|
+
open={state.open}
|
|
25
|
+
title={state.title}
|
|
26
|
+
description={state.description}
|
|
27
|
+
confirmLabel={state.confirmLabel}
|
|
28
|
+
cancelLabel={state.cancelLabel}
|
|
29
|
+
intent={state.intent}
|
|
30
|
+
onconfirm={state.onconfirm}
|
|
31
|
+
oncancel={state.oncancel}
|
|
32
|
+
/>
|
package/Dropdown.svelte
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { setContext } from "svelte";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} DropdownProps
|
|
6
|
+
* @property {import('svelte').Snippet} trigger - The trigger element
|
|
7
|
+
* @property {import('svelte').Snippet} children - The menu items
|
|
8
|
+
* @property {'start' | 'end'} [align] - Alignment of the dropdown
|
|
9
|
+
* @property {string} [class] - Optional class
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @type {DropdownProps} */
|
|
13
|
+
let {
|
|
14
|
+
trigger,
|
|
15
|
+
children,
|
|
16
|
+
align = "start",
|
|
17
|
+
class: className = "",
|
|
18
|
+
...props
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
let open = $state(false);
|
|
22
|
+
let triggerElement = $state();
|
|
23
|
+
let menuElement = $state();
|
|
24
|
+
|
|
25
|
+
function toggle() {
|
|
26
|
+
open = !open;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function close() {
|
|
30
|
+
open = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setContext("dropdown", {
|
|
34
|
+
close,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function handleWindowClick(e) {
|
|
38
|
+
if (
|
|
39
|
+
open &&
|
|
40
|
+
menuElement &&
|
|
41
|
+
!menuElement.contains(e.target) &&
|
|
42
|
+
(!triggerElement || !triggerElement.contains(e.target))
|
|
43
|
+
) {
|
|
44
|
+
close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleKeydown(e) {
|
|
49
|
+
if (!open) return;
|
|
50
|
+
if (e.key === "Escape") {
|
|
51
|
+
close();
|
|
52
|
+
triggerElement?.focus();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<svelte:window onclick={handleWindowClick} onkeydown={handleKeydown} />
|
|
58
|
+
|
|
59
|
+
<div class="std-dropdown {className}" {...props}>
|
|
60
|
+
<div
|
|
61
|
+
bind:this={triggerElement}
|
|
62
|
+
class="std-dropdown-trigger"
|
|
63
|
+
onclick={toggle}
|
|
64
|
+
role="button"
|
|
65
|
+
tabindex="0"
|
|
66
|
+
onkeydown={(e) => (e.key === "Enter" || e.key === " ") && toggle()}
|
|
67
|
+
aria-haspopup="menu"
|
|
68
|
+
aria-expanded={open}
|
|
69
|
+
>
|
|
70
|
+
{@render trigger?.()}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{#if open}
|
|
74
|
+
<div
|
|
75
|
+
bind:this={menuElement}
|
|
76
|
+
class="std-menu-content std-dropdown-content no-rhythm"
|
|
77
|
+
class:align-end={align === "end"}
|
|
78
|
+
role="menu"
|
|
79
|
+
>
|
|
80
|
+
{@render children?.()}
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.std-dropdown {
|
|
87
|
+
position: relative;
|
|
88
|
+
display: inline-block;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.std-dropdown-trigger {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.std-dropdown-content {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 100%;
|
|
99
|
+
margin-top: var(--space-d2);
|
|
100
|
+
min-width: var(--space-6);
|
|
101
|
+
|
|
102
|
+
&.align-end {
|
|
103
|
+
right: 0;
|
|
104
|
+
transform-origin: top right;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte";
|
|
3
|
+
import Icon from "@stnd/icon/Icon.svelte";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} DropdownItemProps
|
|
7
|
+
* @property {import('svelte').Snippet} [children] - Item label
|
|
8
|
+
* @property {string} [label] - Label as a string
|
|
9
|
+
* @property {string} [icon] - Icon name
|
|
10
|
+
* @property {string} [shortcut] - Shortcut text (e.g. ⌘C)
|
|
11
|
+
* @property {boolean} [disabled] - Disabled state
|
|
12
|
+
* @property {boolean} [destructive] - Destructive state
|
|
13
|
+
* @property {string} [href] - Link URL
|
|
14
|
+
* @property {(e: MouseEvent) => void} [onclick] - Click handler
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {DropdownItemProps} */
|
|
18
|
+
let {
|
|
19
|
+
children,
|
|
20
|
+
label,
|
|
21
|
+
icon,
|
|
22
|
+
shortcut,
|
|
23
|
+
disabled = false,
|
|
24
|
+
destructive = false,
|
|
25
|
+
href,
|
|
26
|
+
onclick,
|
|
27
|
+
...props
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
const ctx = getContext("dropdown");
|
|
31
|
+
|
|
32
|
+
function handleClick(e) {
|
|
33
|
+
if (disabled) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
onclick?.(e);
|
|
38
|
+
ctx?.close();
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
{#snippet content()}
|
|
43
|
+
{#if icon}
|
|
44
|
+
<div class="item-icon">
|
|
45
|
+
<Icon {icon} width="1em" height="1em" />
|
|
46
|
+
</div>
|
|
47
|
+
{/if}
|
|
48
|
+
|
|
49
|
+
<span class="item-label">
|
|
50
|
+
{#if children}
|
|
51
|
+
{@render children()}
|
|
52
|
+
{:else}
|
|
53
|
+
{label}
|
|
54
|
+
{/if}
|
|
55
|
+
</span>
|
|
56
|
+
|
|
57
|
+
{#if shortcut}
|
|
58
|
+
<span class="item-shortcut">{shortcut}</span>
|
|
59
|
+
{/if}
|
|
60
|
+
{/snippet}
|
|
61
|
+
|
|
62
|
+
{#if href && !disabled}
|
|
63
|
+
<a
|
|
64
|
+
{href}
|
|
65
|
+
class="std-menu-item"
|
|
66
|
+
class:destructive
|
|
67
|
+
onclick={handleClick}
|
|
68
|
+
role="menuitem"
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{@render content()}
|
|
72
|
+
</a>
|
|
73
|
+
{:else}
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="std-menu-item"
|
|
77
|
+
class:disabled
|
|
78
|
+
class:destructive
|
|
79
|
+
onclick={handleClick}
|
|
80
|
+
role="menuitem"
|
|
81
|
+
aria-disabled={disabled}
|
|
82
|
+
{disabled}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{@render content()}
|
|
86
|
+
</button>
|
|
87
|
+
{/if}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<hr class="std-menu-separator" role="separator" />
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LauncherHint.astro
|
|
4
|
+
*
|
|
5
|
+
* Renders a small launcher hint button (⌘K).
|
|
6
|
+
* Logic for visibility/positioning is now handled by the container (e.g. Header slots).
|
|
7
|
+
*/
|
|
8
|
+
const {
|
|
9
|
+
title = "Open Launcher — Cmd/Ctrl+K",
|
|
10
|
+
kbd = "⌘K",
|
|
11
|
+
class: className = "",
|
|
12
|
+
} = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<button
|
|
16
|
+
class={`smaller ghost text-subtle ${className}`}
|
|
17
|
+
aria-label={title}
|
|
18
|
+
title={title}
|
|
19
|
+
type="button"
|
|
20
|
+
data-launcher-toggle
|
|
21
|
+
>
|
|
22
|
+
<span class="launcher-hint" aria-hidden="true">{kbd}</span>
|
|
23
|
+
</button>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.launcher-hint {
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
user-select: none;
|
|
29
|
+
/* Ensure it behaves as a simple button */
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
text-box-trim: none;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
package/Pagination.astro
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Pagination.astro — Zero-JS pagination nav
|
|
4
|
+
*
|
|
5
|
+
* Renders a previous / page numbers / next navigation bar.
|
|
6
|
+
* Uses standard button classes (ghost, outline) from @stnd/styles.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <Pagination currentPage={2} totalPages={5} baseUrl="/chroniques/page" firstPageUrl="/chroniques" />
|
|
10
|
+
*
|
|
11
|
+
* URL pattern:
|
|
12
|
+
* Page 1 → firstPageUrl (or baseUrl if omitted)
|
|
13
|
+
* Page N → baseUrl/N (e.g. /chroniques/page/2)
|
|
14
|
+
*/
|
|
15
|
+
import Icon from "@stnd/icon/Icon.astro";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
currentPage: number;
|
|
19
|
+
totalPages: number;
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
/** URL for page 1 when it differs from baseUrl (e.g. "/chroniques" vs "/chroniques/page") */
|
|
22
|
+
firstPageUrl?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
currentPage,
|
|
28
|
+
totalPages,
|
|
29
|
+
baseUrl,
|
|
30
|
+
firstPageUrl,
|
|
31
|
+
class: className,
|
|
32
|
+
} = Astro.props;
|
|
33
|
+
|
|
34
|
+
function pageUrl(page: number) {
|
|
35
|
+
return page === 1 ? (firstPageUrl ?? baseUrl) : `${baseUrl}/${page}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build the list of page numbers to show, with ellipsis gaps. */
|
|
39
|
+
function getPageNumbers(
|
|
40
|
+
current: number,
|
|
41
|
+
total: number,
|
|
42
|
+
): (number | "ellipsis")[] {
|
|
43
|
+
if (total <= 7) {
|
|
44
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
45
|
+
}
|
|
46
|
+
const pages: (number | "ellipsis")[] = [1];
|
|
47
|
+
if (current > 3) pages.push("ellipsis");
|
|
48
|
+
for (
|
|
49
|
+
let i = Math.max(2, current - 1);
|
|
50
|
+
i <= Math.min(total - 1, current + 1);
|
|
51
|
+
i++
|
|
52
|
+
) {
|
|
53
|
+
pages.push(i);
|
|
54
|
+
}
|
|
55
|
+
if (current < total - 2) pages.push("ellipsis");
|
|
56
|
+
pages.push(total);
|
|
57
|
+
return pages;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pages = getPageNumbers(currentPage, totalPages);
|
|
61
|
+
const hasPrev = currentPage > 1;
|
|
62
|
+
const hasNext = currentPage < totalPages;
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
totalPages > 1 && (
|
|
67
|
+
<nav
|
|
68
|
+
role="navigation"
|
|
69
|
+
aria-label="pagination"
|
|
70
|
+
class:list={["stnd-pagination no-bullet no-rhythm text-sm", className]}
|
|
71
|
+
>
|
|
72
|
+
<ul>
|
|
73
|
+
<li>
|
|
74
|
+
{hasPrev ? (
|
|
75
|
+
<a href={pageUrl(currentPage - 1)} class="btn ghost">
|
|
76
|
+
<Icon icon="lucide:chevron-left" />
|
|
77
|
+
Précédent
|
|
78
|
+
</a>
|
|
79
|
+
) : (
|
|
80
|
+
<span class="btn ghost" aria-disabled="true">
|
|
81
|
+
<Icon icon="lucide:chevron-left" />
|
|
82
|
+
Précédent
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</li>
|
|
86
|
+
|
|
87
|
+
{pages.map((p) =>
|
|
88
|
+
p === "ellipsis" ? (
|
|
89
|
+
<li aria-hidden="true">
|
|
90
|
+
<span class="btn ghost icon ellipsis">
|
|
91
|
+
<Icon icon="lucide:ellipsis" />
|
|
92
|
+
</span>
|
|
93
|
+
</li>
|
|
94
|
+
) : (
|
|
95
|
+
<li>
|
|
96
|
+
<a
|
|
97
|
+
href={pageUrl(p)}
|
|
98
|
+
class:list={[
|
|
99
|
+
"btn page-number text-center",
|
|
100
|
+
p === currentPage ? "outline" : "ghost",
|
|
101
|
+
]}
|
|
102
|
+
aria-current={p === currentPage ? "page" : undefined}
|
|
103
|
+
>
|
|
104
|
+
{p}
|
|
105
|
+
</a>
|
|
106
|
+
</li>
|
|
107
|
+
),
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<li>
|
|
111
|
+
{hasNext ? (
|
|
112
|
+
<a href={pageUrl(currentPage + 1)} class="btn ghost">
|
|
113
|
+
Suivant
|
|
114
|
+
<Icon icon="lucide:chevron-right" />
|
|
115
|
+
</a>
|
|
116
|
+
) : (
|
|
117
|
+
<span class="btn ghost" aria-disabled="true">
|
|
118
|
+
Suivant
|
|
119
|
+
<Icon icon="lucide:chevron-right" />
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
</li>
|
|
123
|
+
</ul>
|
|
124
|
+
</nav>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
<style>
|
|
129
|
+
.stnd-pagination {
|
|
130
|
+
display: flex;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
width: 100%;
|
|
133
|
+
}
|
|
134
|
+
ul {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-wrap: wrap;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: var(--space-d4);
|
|
139
|
+
list-style: none;
|
|
140
|
+
padding: 0;
|
|
141
|
+
margin: 0;
|
|
142
|
+
}
|
|
143
|
+
.page-number {
|
|
144
|
+
justify-content: center;
|
|
145
|
+
}
|
|
146
|
+
.ellipsis {
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
opacity: 0.5;
|
|
149
|
+
}
|
|
150
|
+
[aria-disabled="true"] {
|
|
151
|
+
opacity: 0.4;
|
|
152
|
+
pointer-events: none;
|
|
153
|
+
}
|
|
154
|
+
</style>
|